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)
128 lines
3.7 KiB
Go
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
|
|
}
|
|
}
|