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)
This commit is contained in:
127
internal/provider/claudecode/adapter.go
Normal file
127
internal/provider/claudecode/adapter.go
Normal file
@@ -0,0 +1,127 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user