package claudecode import ( "bytes" "context" "encoding/json" "fmt" "io" "github.com/agentsphere/agent-mgr/internal/provider" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" "k8s.io/client-go/tools/remotecommand" ) type Config struct { Model string `json:"model,omitempty"` MaxTurns int `json:"max_turns,omitempty"` } type Provider struct { client kubernetes.Interface restConfig *rest.Config } func New(client kubernetes.Interface, restConfig *rest.Config) *Provider { return &Provider{client: client, restConfig: restConfig} } func (p *Provider) Info() provider.Info { return provider.Info{ Name: "claude-code", DisplayName: "Claude Code", Description: "Anthropic Claude Code CLI agent — builds full applications from natural language", Capabilities: []provider.Capability{ {Name: "create-app", Description: "Create new applications from scratch"}, {Name: "edit-code", Description: "Modify existing codebases"}, {Name: "interactive", Description: "Supports follow-up messages during a session"}, }, ConfigSchema: json.RawMessage(`{ "type": "object", "properties": { "model": {"type": "string", "description": "Claude model to use", "default": ""}, "max_turns": {"type": "integer", "description": "Maximum agentic turns", "default": 0} } }`), } } func (p *Provider) CreateSession(ctx context.Context, cfg provider.SessionConfig) (*provider.SessionHandle, error) { var opts *Config if len(cfg.Provider) > 0 { opts = &Config{} if err := json.Unmarshal(cfg.Provider, opts); err != nil { return nil, fmt.Errorf("parse claude-code config: %w", err) } } pod := buildPod(cfg, opts) created, err := p.client.CoreV1().Pods(Namespace).Create(ctx, pod, metav1.CreateOptions{}) if err != nil { return nil, fmt.Errorf("create pod: %w", err) } return &provider.SessionHandle{ SessionID: cfg.SessionID, PodName: created.Name, }, nil } func (p *Provider) StopSession(ctx context.Context, handle *provider.SessionHandle) error { return p.client.CoreV1().Pods(Namespace).Delete(ctx, handle.PodName, metav1.DeleteOptions{}) } func (p *Provider) SendMessage(ctx context.Context, handle *provider.SessionHandle, msg string) error { req := p.client.CoreV1().RESTClient().Post(). Resource("pods"). Name(handle.PodName). Namespace(Namespace). SubResource("attach"). VersionedParams(&corev1.PodAttachOptions{ Container: "claude", Stdin: true, Stdout: false, Stderr: false, }, scheme.ParameterCodec) exec, err := remotecommand.NewSPDYExecutor(p.restConfig, "POST", req.URL()) if err != nil { return fmt.Errorf("create attach executor: %w", err) } return exec.StreamWithContext(ctx, remotecommand.StreamOptions{ Stdin: bytes.NewReader([]byte(msg + "\n")), }) } func (p *Provider) StreamOutput(ctx context.Context, handle *provider.SessionHandle) (io.ReadCloser, error) { req := p.client.CoreV1().Pods(Namespace).GetLogs(handle.PodName, &corev1.PodLogOptions{ Container: "claude", Follow: true, }) return req.Stream(ctx) } func (p *Provider) GetStatus(ctx context.Context, handle *provider.SessionHandle) (provider.Status, error) { pod, err := p.client.CoreV1().Pods(Namespace).Get(ctx, handle.PodName, metav1.GetOptions{}) if err != nil { return provider.StatusFailed, fmt.Errorf("get pod: %w", err) } switch pod.Status.Phase { case corev1.PodPending: return provider.StatusPending, nil case corev1.PodRunning: return provider.StatusRunning, nil case corev1.PodSucceeded: return provider.StatusCompleted, nil case corev1.PodFailed: return provider.StatusFailed, nil default: return provider.StatusFailed, nil } }