agent.go
143 lines1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
package internal
import (
"encoding/json"
"fmt"
"os/exec"
"strings"
"time"
"congo.gg/dev/models"
)
const claudeBin = "/home/coder/.local/bin/claude"
var agentSystemPrompt = `You are the infrastructure assistant for this Congo Dev workbench. You help manage Docker services, git repositories, and server infrastructure.
Available context:
- Docker containers are on the "internal" network
- User services are named "svc-<slug>"
- Code-server runs as "congo-dev-coder"
- Repos are at /home/coder/repos/
- The congo CLI is available for scaffolding projects
Be concise and actionable. When asked to inspect something, do it directly.`
// IsAgentAvailable checks if Claude Code CLI is authenticated in the code-server.
func IsAgentAvailable() bool {
if !IsCoderRunning() {
return false
}
out, err := CoderExec("test -f /home/coder/.claude/.credentials.json && echo yes")
if err != nil {
return false
}
return strings.TrimSpace(out) == "yes"
}
// RunAgentChat sends a message to Claude Code CLI and returns the response.
// It builds context about the current infrastructure state and passes it along.
func RunAgentChat(message string) (string, error) {
if !IsCoderRunning() {
return "", fmt.Errorf("code-server is not running")
}
// Build infrastructure context
context := buildInfraContext()
// Construct the prompt with context
prompt := fmt.Sprintf("%s\n\nCurrent infrastructure state:\n%s\n\nUser request: %s", agentSystemPrompt, context, message)
// Run claude CLI in print mode with --max-turns 1 for quick responses
// Use --no-input to prevent interactive prompts
cmd := exec.Command("docker", "exec", coderContainer,
claudeBin, "-p", prompt, "--max-turns", "3")
cmd.Env = append(cmd.Environ(), "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1")
// Set a timeout
done := make(chan struct{})
var out []byte
var err error
go func() {
out, err = cmd.CombinedOutput()
close(done)
}()
select {
case <-done:
if err != nil {
output := strings.TrimSpace(string(out))
if output != "" {
return "", fmt.Errorf("claude: %s", output)
}
return "", fmt.Errorf("claude: %w", err)
}
return strings.TrimSpace(string(out)), nil
case <-time.After(2 * time.Minute):
if cmd.Process != nil {
cmd.Process.Kill()
}
return "", fmt.Errorf("claude: timed out after 2 minutes")
}
}
// buildInfraContext gathers current state to give the agent context.
func buildInfraContext() string {
var parts []string
// Containers
containers, err := ListContainers()
if err == nil && len(containers) > 0 {
var names []string
for _, c := range containers {
names = append(names, fmt.Sprintf("%s (%s)", c.Name, c.State))
}
parts = append(parts, "Containers: "+strings.Join(names, ", "))
}
// Repos
repos, _ := models.Repositories.All()
if len(repos) > 0 {
var names []string
for _, r := range repos {
names = append(names, r.Name)
}
parts = append(parts, "Repos: "+strings.Join(names, ", "))
}
// Services
services, _ := models.Services.All()
if len(services) > 0 {
var svcInfo []string
for _, s := range services {
status := s.Status
if IsContainerRunning("svc-" + s.Slug) {
status = "running"
}
svcInfo = append(svcInfo, fmt.Sprintf("%s (%s, port %d)", s.Name, status, s.Port))
}
parts = append(parts, "Services: "+strings.Join(svcInfo, ", "))
}
// System stats
stats := GetSystemStats()
if stats != nil {
parts = append(parts, fmt.Sprintf("System: %s, %d CPU, mem %s/%s (%d%%), disk %s/%s (%d%%)",
stats.Hostname, stats.NumCPU,
stats.MemUsed, stats.MemTotal, stats.MemPercent,
stats.DiskUsed, stats.DiskTotal, stats.DiskPercent))
}
if len(parts) == 0 {
return "No infrastructure data available"
}
return strings.Join(parts, "\n")
}
func toJSON(v any) string {
data, err := json.MarshalIndent(v, "", " ")
if err != nil {
return fmt.Sprintf("%v", v)
}
return string(data)
}