Files
agent-mgr/internal/provider/claudecode/adapter.go
Steven Hooker e5b07cc1d8 initial agent-mgr: app builder platform MVP
Go API server + Preact UI + Claude Code adapter.
- App-centric model (ideas, not repos)
- AgentProvider interface for multi-agent support
- K8s pod lifecycle for sandboxed agent sessions
- Gitea integration (create repos, push branches)
- WebSocket streaming for live session output
- Woodpecker CI/CD pipelines (kaniko build + kubectl deploy)
2026-02-18 15:56:32 +01:00

128 lines
3.7 KiB
Go

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
}
}