@agentspan/agentspan 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +227 -0
- package/build.sh +35 -0
- package/cli.js +28 -0
- package/client/client.go +365 -0
- package/cmd/agent.go +120 -0
- package/cmd/compile.go +37 -0
- package/cmd/configure.go +48 -0
- package/cmd/delete.go +44 -0
- package/cmd/doctor.go +486 -0
- package/cmd/execution.go +155 -0
- package/cmd/get.go +40 -0
- package/cmd/helpers.go +21 -0
- package/cmd/init.go +83 -0
- package/cmd/list.go +55 -0
- package/cmd/respond.go +56 -0
- package/cmd/root.go +44 -0
- package/cmd/run.go +116 -0
- package/cmd/server.go +446 -0
- package/cmd/server_unix.go +36 -0
- package/cmd/server_windows.go +37 -0
- package/cmd/status.go +72 -0
- package/cmd/stream.go +32 -0
- package/cmd/update.go +91 -0
- package/config/config.go +78 -0
- package/dist/agentspan_darwin_amd64 +0 -0
- package/dist/agentspan_darwin_arm64 +0 -0
- package/dist/agentspan_linux_amd64 +0 -0
- package/dist/agentspan_linux_arm64 +0 -0
- package/dist/agentspan_windows_amd64.exe +0 -0
- package/dist/agentspan_windows_arm64.exe +0 -0
- package/examples/multi-agent.yaml +22 -0
- package/examples/simple-agent.yaml +7 -0
- package/go.mod +17 -0
- package/go.sum +24 -0
- package/install.js +122 -0
- package/install.sh +104 -0
- package/internal/progress/bar.go +121 -0
- package/main.go +10 -0
- package/package.json +43 -0
package/cmd/doctor.go
ADDED
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
// Copyright (c) 2025 AgentSpan
|
|
2
|
+
// Licensed under the MIT License. See LICENSE file in the project root for details.
|
|
3
|
+
|
|
4
|
+
package cmd
|
|
5
|
+
|
|
6
|
+
import (
|
|
7
|
+
"fmt"
|
|
8
|
+
"net"
|
|
9
|
+
"net/http"
|
|
10
|
+
"os"
|
|
11
|
+
"os/exec"
|
|
12
|
+
"path/filepath"
|
|
13
|
+
"regexp"
|
|
14
|
+
"strconv"
|
|
15
|
+
"strings"
|
|
16
|
+
"time"
|
|
17
|
+
|
|
18
|
+
"github.com/fatih/color"
|
|
19
|
+
"github.com/spf13/cobra"
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
type aiProvider struct {
|
|
23
|
+
name string
|
|
24
|
+
envVars []string // all must be set for the provider to be "configured"
|
|
25
|
+
warns []providerWarning // conditional warnings (checked even if not fully configured)
|
|
26
|
+
models []string // example models available with this provider
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type providerWarning struct {
|
|
30
|
+
condition func() bool
|
|
31
|
+
message string
|
|
32
|
+
fix string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const ollamaDefaultURL = "http://localhost:11434"
|
|
36
|
+
|
|
37
|
+
var aiProviders = []aiProvider{
|
|
38
|
+
{
|
|
39
|
+
name: "OpenAI",
|
|
40
|
+
envVars: []string{"OPENAI_API_KEY"},
|
|
41
|
+
models: []string{
|
|
42
|
+
"openai/gpt-4o",
|
|
43
|
+
"openai/gpt-4o-mini",
|
|
44
|
+
"openai/o1",
|
|
45
|
+
"openai/o3-mini",
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: "Anthropic",
|
|
50
|
+
envVars: []string{"ANTHROPIC_API_KEY"},
|
|
51
|
+
models: []string{
|
|
52
|
+
"anthropic/claude-opus-4-20250514",
|
|
53
|
+
"anthropic/claude-sonnet-4-20250514",
|
|
54
|
+
"anthropic/claude-3-5-sonnet-20241022",
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: "Google Gemini",
|
|
59
|
+
envVars: []string{"GEMINI_API_KEY", "GOOGLE_CLOUD_PROJECT"},
|
|
60
|
+
warns: []providerWarning{
|
|
61
|
+
{
|
|
62
|
+
condition: func() bool {
|
|
63
|
+
return os.Getenv("GEMINI_API_KEY") != "" && os.Getenv("GOOGLE_CLOUD_PROJECT") == ""
|
|
64
|
+
},
|
|
65
|
+
message: "GEMINI_API_KEY is set but GOOGLE_CLOUD_PROJECT is missing",
|
|
66
|
+
fix: "export GOOGLE_CLOUD_PROJECT=your-gcp-project-id",
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
models: []string{
|
|
70
|
+
"google_gemini/gemini-2.0-flash",
|
|
71
|
+
"google_gemini/gemini-1.5-pro",
|
|
72
|
+
"google_gemini/gemini-1.5-flash",
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: "Azure OpenAI",
|
|
77
|
+
envVars: []string{"AZURE_OPENAI_API_KEY", "AZURE_OPENAI_ENDPOINT"},
|
|
78
|
+
warns: []providerWarning{
|
|
79
|
+
{
|
|
80
|
+
condition: func() bool {
|
|
81
|
+
return os.Getenv("AZURE_OPENAI_API_KEY") != "" && os.Getenv("AZURE_OPENAI_ENDPOINT") == ""
|
|
82
|
+
},
|
|
83
|
+
message: "AZURE_OPENAI_API_KEY is set but AZURE_OPENAI_ENDPOINT is missing",
|
|
84
|
+
fix: "export AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com",
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
condition: func() bool {
|
|
88
|
+
return os.Getenv("AZURE_OPENAI_API_KEY") != "" && os.Getenv("AZURE_OPENAI_DEPLOYMENT") == ""
|
|
89
|
+
},
|
|
90
|
+
message: "AZURE_OPENAI_DEPLOYMENT is not set (required to route requests)",
|
|
91
|
+
fix: "export AZURE_OPENAI_DEPLOYMENT=your-deployment-name",
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
models: []string{
|
|
95
|
+
"azure_openai/gpt-4o",
|
|
96
|
+
"azure_openai/gpt-4",
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: "AWS Bedrock",
|
|
101
|
+
envVars: []string{"AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"},
|
|
102
|
+
warns: []providerWarning{
|
|
103
|
+
{
|
|
104
|
+
condition: func() bool {
|
|
105
|
+
return os.Getenv("AWS_ACCESS_KEY_ID") != "" && os.Getenv("AWS_DEFAULT_REGION") == "" && os.Getenv("AWS_REGION") == ""
|
|
106
|
+
},
|
|
107
|
+
message: "No AWS region set — defaults to us-east-1",
|
|
108
|
+
fix: "export AWS_DEFAULT_REGION=us-east-1 # or your preferred region",
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
models: []string{
|
|
112
|
+
"aws_bedrock/anthropic.claude-3-5-sonnet-20241022-v2:0",
|
|
113
|
+
"aws_bedrock/meta.llama3-70b-instruct-v1:0",
|
|
114
|
+
"aws_bedrock/amazon.titan-text-express-v1",
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
name: "Mistral",
|
|
119
|
+
envVars: []string{"MISTRAL_API_KEY"},
|
|
120
|
+
models: []string{
|
|
121
|
+
"mistral/mistral-large-latest",
|
|
122
|
+
"mistral/mistral-small-latest",
|
|
123
|
+
"mistral/open-mixtral-8x7b",
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
name: "Cohere",
|
|
128
|
+
envVars: []string{"COHERE_API_KEY"},
|
|
129
|
+
models: []string{
|
|
130
|
+
"cohere/command-r-plus",
|
|
131
|
+
"cohere/command-r",
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
name: "Grok",
|
|
136
|
+
envVars: []string{"XAI_API_KEY"},
|
|
137
|
+
models: []string{
|
|
138
|
+
"grok/grok-3",
|
|
139
|
+
"grok/grok-3-mini",
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
name: "Perplexity",
|
|
144
|
+
envVars: []string{"PERPLEXITY_API_KEY"},
|
|
145
|
+
models: []string{
|
|
146
|
+
"perplexity/sonar-pro",
|
|
147
|
+
"perplexity/sonar",
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
name: "Hugging Face",
|
|
152
|
+
envVars: []string{"HUGGINGFACE_API_KEY"},
|
|
153
|
+
models: []string{
|
|
154
|
+
"hugging_face/meta-llama/Llama-3-70b-chat-hf",
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
name: "Stability AI",
|
|
159
|
+
envVars: []string{"STABILITY_API_KEY"},
|
|
160
|
+
models: []string{
|
|
161
|
+
"stabilityai/sd3.5-large",
|
|
162
|
+
"stabilityai/stable-image-core",
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
var doctorCmd = &cobra.Command{
|
|
168
|
+
Use: "doctor",
|
|
169
|
+
Short: "Check system dependencies and AI provider configuration",
|
|
170
|
+
RunE: runDoctor,
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
func init() {
|
|
174
|
+
rootCmd.AddCommand(doctorCmd)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
func runDoctor(cmd *cobra.Command, args []string) error {
|
|
178
|
+
bold := color.New(color.Bold)
|
|
179
|
+
green := color.New(color.FgGreen)
|
|
180
|
+
yellow := color.New(color.FgYellow)
|
|
181
|
+
red := color.New(color.FgRed)
|
|
182
|
+
dim := color.New(color.Faint)
|
|
183
|
+
|
|
184
|
+
issues := 0
|
|
185
|
+
|
|
186
|
+
// ── System Dependencies ──────────────────────────────────────
|
|
187
|
+
bold.Println("System Dependencies")
|
|
188
|
+
fmt.Println()
|
|
189
|
+
|
|
190
|
+
// Java
|
|
191
|
+
javaOk, javaVersion := checkJava()
|
|
192
|
+
if javaOk {
|
|
193
|
+
green.Printf(" ✓ Java %s\n", javaVersion)
|
|
194
|
+
} else if javaVersion != "" {
|
|
195
|
+
red.Printf(" ✗ Java %s (21+ required)\n", javaVersion)
|
|
196
|
+
fmt.Println(" The server runtime requires Java 21 or later.")
|
|
197
|
+
fmt.Println(" Install: https://adoptium.net/")
|
|
198
|
+
issues++
|
|
199
|
+
} else {
|
|
200
|
+
red.Println(" ✗ Java not found")
|
|
201
|
+
fmt.Println(" The server runtime requires Java 21 or later.")
|
|
202
|
+
fmt.Println(" Install: https://adoptium.net/")
|
|
203
|
+
issues++
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// JAVA_HOME check
|
|
207
|
+
javaHome := os.Getenv("JAVA_HOME")
|
|
208
|
+
if javaHome != "" {
|
|
209
|
+
if _, err := os.Stat(filepath.Join(javaHome, "bin", "java")); err != nil {
|
|
210
|
+
yellow.Println(" ⚠ JAVA_HOME is set but java binary not found there")
|
|
211
|
+
fmt.Printf(" JAVA_HOME=%s\n", javaHome)
|
|
212
|
+
issues++
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Python (optional, for SDK)
|
|
217
|
+
pythonOk, pythonVersion := checkPython()
|
|
218
|
+
if pythonOk {
|
|
219
|
+
green.Printf(" ✓ Python %s\n", pythonVersion)
|
|
220
|
+
} else if pythonVersion != "" {
|
|
221
|
+
yellow.Printf(" ~ Python %s (3.9+ recommended for the Python SDK)\n", pythonVersion)
|
|
222
|
+
} else {
|
|
223
|
+
dim.Println(" - Python not found (optional, needed for the Python SDK)")
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Disk space
|
|
227
|
+
dir := serverDir()
|
|
228
|
+
os.MkdirAll(dir, 0o755)
|
|
229
|
+
freeMB := getFreeDiskMB(dir)
|
|
230
|
+
if freeMB >= 0 {
|
|
231
|
+
if freeMB < 500 {
|
|
232
|
+
yellow.Printf(" ⚠ Low disk space: %d MB free in %s\n", freeMB, dir)
|
|
233
|
+
fmt.Println(" The server JAR is ~200 MB. Free up space if downloads fail.")
|
|
234
|
+
issues++
|
|
235
|
+
} else {
|
|
236
|
+
green.Printf(" ✓ Disk space: %d MB free\n", freeMB)
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Port availability
|
|
241
|
+
port := "8080"
|
|
242
|
+
if serverPort != "" {
|
|
243
|
+
port = serverPort
|
|
244
|
+
}
|
|
245
|
+
if isPortAvailable(port) {
|
|
246
|
+
green.Printf(" ✓ Port %s is available\n", port)
|
|
247
|
+
} else {
|
|
248
|
+
yellow.Printf(" ~ Port %s is in use (server may already be running)\n", port)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Server JAR
|
|
252
|
+
jarPath := filepath.Join(dir, jarName)
|
|
253
|
+
if info, err := os.Stat(jarPath); err == nil {
|
|
254
|
+
sizeMB := float64(info.Size()) / 1024 / 1024
|
|
255
|
+
green.Printf(" ✓ Server JAR cached (%.0f MB)\n", sizeMB)
|
|
256
|
+
} else {
|
|
257
|
+
dim.Println(" - Server JAR not downloaded yet (will download on first start)")
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
fmt.Println()
|
|
261
|
+
|
|
262
|
+
// ── AI Providers ─────────────────────────────────────────────
|
|
263
|
+
bold.Println("AI Providers")
|
|
264
|
+
fmt.Println()
|
|
265
|
+
|
|
266
|
+
configured := 0
|
|
267
|
+
for _, p := range aiProviders {
|
|
268
|
+
allSet := isProviderConfigured(p)
|
|
269
|
+
|
|
270
|
+
if allSet {
|
|
271
|
+
configured++
|
|
272
|
+
green.Printf(" ✓ %s", p.name)
|
|
273
|
+
dim.Printf(" (%s)\n", strings.Join(p.envVars, ", "))
|
|
274
|
+
|
|
275
|
+
// Check for warnings on this provider
|
|
276
|
+
for _, w := range p.warns {
|
|
277
|
+
if w.condition() {
|
|
278
|
+
yellow.Printf(" ⚠ %s\n", w.message)
|
|
279
|
+
fmt.Printf(" %s\n", w.fix)
|
|
280
|
+
issues++
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Print available models
|
|
285
|
+
for _, m := range p.models {
|
|
286
|
+
dim.Printf(" %s\n", m)
|
|
287
|
+
}
|
|
288
|
+
} else {
|
|
289
|
+
dim.Printf(" - %s", p.name)
|
|
290
|
+
dim.Printf(" (%s)\n", strings.Join(p.envVars, ", "))
|
|
291
|
+
|
|
292
|
+
// Still show warnings for partially configured providers
|
|
293
|
+
for _, w := range p.warns {
|
|
294
|
+
if w.condition() {
|
|
295
|
+
yellow.Printf(" ⚠ %s\n", w.message)
|
|
296
|
+
fmt.Printf(" %s\n", w.fix)
|
|
297
|
+
issues++
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Ollama — special case, no API key, check connectivity
|
|
304
|
+
ollamaURL := os.Getenv("OLLAMA_BASE_URL")
|
|
305
|
+
if ollamaURL == "" {
|
|
306
|
+
ollamaURL = ollamaDefaultURL
|
|
307
|
+
}
|
|
308
|
+
ollamaOk := checkOllama(ollamaURL)
|
|
309
|
+
if ollamaOk {
|
|
310
|
+
configured++
|
|
311
|
+
green.Printf(" ✓ Ollama")
|
|
312
|
+
dim.Printf(" (%s)\n", ollamaURL)
|
|
313
|
+
dim.Println(" ollama/llama3, ollama/mistral, ollama/phi3, ollama/codellama")
|
|
314
|
+
} else if os.Getenv("OLLAMA_BASE_URL") != "" {
|
|
315
|
+
yellow.Printf(" ⚠ Ollama (not reachable at %s)\n", ollamaURL)
|
|
316
|
+
fmt.Println(" Check that Ollama is running at the configured URL.")
|
|
317
|
+
fmt.Printf(" OLLAMA_BASE_URL=%s\n", ollamaURL)
|
|
318
|
+
issues++
|
|
319
|
+
} else {
|
|
320
|
+
dim.Println(" - Ollama (not running)")
|
|
321
|
+
dim.Println(" To use a local Ollama instance:")
|
|
322
|
+
dim.Println(" Install: https://ollama.com/download")
|
|
323
|
+
dim.Println(" Default: http://localhost:11434")
|
|
324
|
+
dim.Println(" Custom: export OLLAMA_BASE_URL=http://your-host:11434")
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
fmt.Println()
|
|
328
|
+
|
|
329
|
+
// ── Server Connectivity ──────────────────────────────────────
|
|
330
|
+
bold.Println("Server")
|
|
331
|
+
fmt.Println()
|
|
332
|
+
|
|
333
|
+
serverAddr := fmt.Sprintf("http://localhost:%s", port)
|
|
334
|
+
if serverURL != "" {
|
|
335
|
+
serverAddr = serverURL
|
|
336
|
+
}
|
|
337
|
+
serverOk := checkServer(serverAddr)
|
|
338
|
+
if serverOk {
|
|
339
|
+
green.Printf(" ✓ Server reachable at %s\n", serverAddr)
|
|
340
|
+
} else {
|
|
341
|
+
dim.Printf(" - Server not running at %s\n", serverAddr)
|
|
342
|
+
dim.Println(" Start with: agentspan server start")
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
fmt.Println()
|
|
346
|
+
|
|
347
|
+
// ── Summary ──────────────────────────────────────────────────
|
|
348
|
+
bold.Println("Summary")
|
|
349
|
+
fmt.Println()
|
|
350
|
+
|
|
351
|
+
if configured == 0 {
|
|
352
|
+
red.Println(" ✗ No AI providers configured")
|
|
353
|
+
fmt.Println()
|
|
354
|
+
fmt.Println(" Set at least one provider's API key to get started:")
|
|
355
|
+
fmt.Println()
|
|
356
|
+
fmt.Println(" export OPENAI_API_KEY=sk-...")
|
|
357
|
+
fmt.Println(" export ANTHROPIC_API_KEY=sk-ant-...")
|
|
358
|
+
fmt.Println(" export GEMINI_API_KEY=AI...")
|
|
359
|
+
fmt.Println(" export GOOGLE_CLOUD_PROJECT=your-gcp-project-id")
|
|
360
|
+
fmt.Println()
|
|
361
|
+
issues++
|
|
362
|
+
} else {
|
|
363
|
+
green.Printf(" %d AI provider(s) configured\n", configured)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if issues == 0 {
|
|
367
|
+
fmt.Println()
|
|
368
|
+
green.Println(" Everything looks good!")
|
|
369
|
+
} else {
|
|
370
|
+
fmt.Printf("\n %d issue(s) found — see above for details.\n", issues)
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
fmt.Printf("\n Docs: %s\n\n", aiModelsDocURL)
|
|
374
|
+
|
|
375
|
+
return nil
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
func isProviderConfigured(p aiProvider) bool {
|
|
379
|
+
for _, env := range p.envVars {
|
|
380
|
+
if os.Getenv(env) == "" {
|
|
381
|
+
return false
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return true
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// checkJava returns (meets_minimum, version_string)
|
|
388
|
+
func checkJava() (bool, string) {
|
|
389
|
+
out, err := exec.Command("java", "-version").CombinedOutput()
|
|
390
|
+
if err != nil {
|
|
391
|
+
return false, ""
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Java version output goes to stderr, but CombinedOutput captures both.
|
|
395
|
+
// Matches patterns like: "21.0.1", "17.0.2", "1.8.0_292"
|
|
396
|
+
re := regexp.MustCompile(`"(\d+[\d._]*)"|version "(\d+[\d._]*)"`)
|
|
397
|
+
matches := re.FindStringSubmatch(string(out))
|
|
398
|
+
|
|
399
|
+
version := ""
|
|
400
|
+
if len(matches) > 1 {
|
|
401
|
+
for _, m := range matches[1:] {
|
|
402
|
+
if m != "" {
|
|
403
|
+
version = m
|
|
404
|
+
break
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if version == "" {
|
|
410
|
+
return false, ""
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Extract major version number
|
|
414
|
+
major := version
|
|
415
|
+
if idx := strings.IndexAny(version, "._"); idx > 0 {
|
|
416
|
+
major = version[:idx]
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
majorNum, err := strconv.Atoi(major)
|
|
420
|
+
if err != nil {
|
|
421
|
+
return false, version
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return majorNum >= 21, version
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// checkPython returns (meets_minimum, version_string)
|
|
428
|
+
func checkPython() (bool, string) {
|
|
429
|
+
for _, bin := range []string{"python3", "python"} {
|
|
430
|
+
out, err := exec.Command(bin, "--version").Output()
|
|
431
|
+
if err != nil {
|
|
432
|
+
continue
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// "Python 3.12.1"
|
|
436
|
+
parts := strings.Fields(strings.TrimSpace(string(out)))
|
|
437
|
+
if len(parts) < 2 {
|
|
438
|
+
continue
|
|
439
|
+
}
|
|
440
|
+
version := parts[1]
|
|
441
|
+
|
|
442
|
+
// Extract major.minor
|
|
443
|
+
segments := strings.SplitN(version, ".", 3)
|
|
444
|
+
if len(segments) < 2 {
|
|
445
|
+
return false, version
|
|
446
|
+
}
|
|
447
|
+
major, err1 := strconv.Atoi(segments[0])
|
|
448
|
+
minor, err2 := strconv.Atoi(segments[1])
|
|
449
|
+
if err1 != nil || err2 != nil {
|
|
450
|
+
return false, version
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
return major > 3 || (major == 3 && minor >= 9), version
|
|
454
|
+
}
|
|
455
|
+
return false, ""
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
func isPortAvailable(port string) bool {
|
|
459
|
+
ln, err := net.Listen("tcp", ":"+port)
|
|
460
|
+
if err != nil {
|
|
461
|
+
return false
|
|
462
|
+
}
|
|
463
|
+
ln.Close()
|
|
464
|
+
return true
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
func checkOllama(baseURL string) bool {
|
|
468
|
+
client := &http.Client{Timeout: 3 * time.Second}
|
|
469
|
+
resp, err := client.Get(baseURL)
|
|
470
|
+
if err != nil {
|
|
471
|
+
return false
|
|
472
|
+
}
|
|
473
|
+
resp.Body.Close()
|
|
474
|
+
return resp.StatusCode == http.StatusOK
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
func checkServer(baseURL string) bool {
|
|
478
|
+
client := &http.Client{Timeout: 3 * time.Second}
|
|
479
|
+
resp, err := client.Get(baseURL + "/health")
|
|
480
|
+
if err != nil {
|
|
481
|
+
return false
|
|
482
|
+
}
|
|
483
|
+
resp.Body.Close()
|
|
484
|
+
return resp.StatusCode == http.StatusOK
|
|
485
|
+
}
|
|
486
|
+
|
package/cmd/execution.go
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// Copyright (c) 2025 AgentSpan
|
|
2
|
+
// Licensed under the MIT License. See LICENSE file in the project root for details.
|
|
3
|
+
|
|
4
|
+
package cmd
|
|
5
|
+
|
|
6
|
+
import (
|
|
7
|
+
"fmt"
|
|
8
|
+
"os"
|
|
9
|
+
"regexp"
|
|
10
|
+
"strconv"
|
|
11
|
+
"text/tabwriter"
|
|
12
|
+
"time"
|
|
13
|
+
|
|
14
|
+
"github.com/fatih/color"
|
|
15
|
+
"github.com/spf13/cobra"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
var (
|
|
19
|
+
execName string
|
|
20
|
+
execSince string
|
|
21
|
+
execWindow string
|
|
22
|
+
execStatus string
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
var executionCmd = &cobra.Command{
|
|
26
|
+
Use: "execution",
|
|
27
|
+
Short: "Search agent execution history",
|
|
28
|
+
Long: `Search agent execution history with optional filters.
|
|
29
|
+
|
|
30
|
+
Time formats for --since and --window:
|
|
31
|
+
30s, 5m, 1h, 6h, 1d, 7d, 1mo, 1y
|
|
32
|
+
|
|
33
|
+
Examples:
|
|
34
|
+
agentspan agent execution --since 1h
|
|
35
|
+
agentspan agent execution --name mybot --since 1d
|
|
36
|
+
agentspan agent execution --status COMPLETED --since 7d`,
|
|
37
|
+
RunE: func(cmd *cobra.Command, args []string) error {
|
|
38
|
+
cfg := getConfig()
|
|
39
|
+
c := newClient(cfg)
|
|
40
|
+
|
|
41
|
+
// Build freeText query for time filtering
|
|
42
|
+
freeText := ""
|
|
43
|
+
if execSince != "" {
|
|
44
|
+
dur, err := parseTimeSpec(execSince)
|
|
45
|
+
if err != nil {
|
|
46
|
+
return fmt.Errorf("invalid --since value: %w", err)
|
|
47
|
+
}
|
|
48
|
+
startTime := time.Now().Add(-dur).UnixMilli()
|
|
49
|
+
freeText = fmt.Sprintf("startTime:[%d TO *]", startTime)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if execWindow != "" {
|
|
53
|
+
// Parse window format: "now-1h" or just "1h" (relative to now)
|
|
54
|
+
windowStr := execWindow
|
|
55
|
+
if len(windowStr) > 4 && windowStr[:4] == "now-" {
|
|
56
|
+
windowStr = windowStr[4:]
|
|
57
|
+
}
|
|
58
|
+
dur, err := parseTimeSpec(windowStr)
|
|
59
|
+
if err != nil {
|
|
60
|
+
return fmt.Errorf("invalid --window value: %w", err)
|
|
61
|
+
}
|
|
62
|
+
endTime := time.Now().UnixMilli()
|
|
63
|
+
startTime := time.Now().Add(-dur).UnixMilli()
|
|
64
|
+
windowQuery := fmt.Sprintf("startTime:[%d TO %d]", startTime, endTime)
|
|
65
|
+
if freeText != "" {
|
|
66
|
+
freeText += " AND " + windowQuery
|
|
67
|
+
} else {
|
|
68
|
+
freeText = windowQuery
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
result, err := c.SearchExecutions(0, 50, execName, execStatus, freeText)
|
|
73
|
+
if err != nil {
|
|
74
|
+
return fmt.Errorf("failed to search executions: %w", err)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if len(result.Results) == 0 {
|
|
78
|
+
color.Yellow("No executions found.")
|
|
79
|
+
return nil
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
|
83
|
+
fmt.Fprintln(w, "ID\tAGENT\tSTATUS\tSTART TIME\tDURATION")
|
|
84
|
+
fmt.Fprintln(w, "--\t-----\t------\t----------\t--------")
|
|
85
|
+
|
|
86
|
+
for _, ex := range result.Results {
|
|
87
|
+
duration := ""
|
|
88
|
+
if ex.ExecutionTime > 0 {
|
|
89
|
+
duration = formatDuration(time.Duration(ex.ExecutionTime) * time.Millisecond)
|
|
90
|
+
}
|
|
91
|
+
startTime := ex.StartTime
|
|
92
|
+
if len(startTime) > 19 {
|
|
93
|
+
startTime = startTime[:19]
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
statusStr := ex.Status
|
|
97
|
+
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n",
|
|
98
|
+
truncate(ex.WorkflowID, 12), ex.AgentName, statusStr, startTime, duration)
|
|
99
|
+
}
|
|
100
|
+
w.Flush()
|
|
101
|
+
|
|
102
|
+
fmt.Printf("\n%d of %d execution(s).\n", len(result.Results), result.TotalHits)
|
|
103
|
+
return nil
|
|
104
|
+
},
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
func init() {
|
|
108
|
+
executionCmd.Flags().StringVar(&execName, "name", "", "Filter by agent name")
|
|
109
|
+
executionCmd.Flags().StringVar(&execSince, "since", "", "Show executions since (e.g. 30m, 1h, 1d, 1mo)")
|
|
110
|
+
executionCmd.Flags().StringVar(&execWindow, "window", "", "Time window (e.g. now-1h, now-7d)")
|
|
111
|
+
executionCmd.Flags().StringVar(&execStatus, "status", "", "Filter by status (RUNNING, COMPLETED, FAILED, etc.)")
|
|
112
|
+
agentCmd.AddCommand(executionCmd)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// parseTimeSpec parses time strings like "30s", "5m", "1h", "1d", "1mo", "1y"
|
|
116
|
+
func parseTimeSpec(s string) (time.Duration, error) {
|
|
117
|
+
re := regexp.MustCompile(`^(\d+)(s|m|h|d|mo|y)$`)
|
|
118
|
+
matches := re.FindStringSubmatch(s)
|
|
119
|
+
if matches == nil {
|
|
120
|
+
return 0, fmt.Errorf("expected format like 30s, 5m, 1h, 1d, 1mo, 1y; got %q", s)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
n, _ := strconv.Atoi(matches[1])
|
|
124
|
+
unit := matches[2]
|
|
125
|
+
|
|
126
|
+
switch unit {
|
|
127
|
+
case "s":
|
|
128
|
+
return time.Duration(n) * time.Second, nil
|
|
129
|
+
case "m":
|
|
130
|
+
return time.Duration(n) * time.Minute, nil
|
|
131
|
+
case "h":
|
|
132
|
+
return time.Duration(n) * time.Hour, nil
|
|
133
|
+
case "d":
|
|
134
|
+
return time.Duration(n) * 24 * time.Hour, nil
|
|
135
|
+
case "mo":
|
|
136
|
+
return time.Duration(n) * 30 * 24 * time.Hour, nil
|
|
137
|
+
case "y":
|
|
138
|
+
return time.Duration(n) * 365 * 24 * time.Hour, nil
|
|
139
|
+
default:
|
|
140
|
+
return 0, fmt.Errorf("unknown unit: %s", unit)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
func formatDuration(d time.Duration) string {
|
|
145
|
+
if d < time.Second {
|
|
146
|
+
return fmt.Sprintf("%dms", d.Milliseconds())
|
|
147
|
+
}
|
|
148
|
+
if d < time.Minute {
|
|
149
|
+
return fmt.Sprintf("%.1fs", d.Seconds())
|
|
150
|
+
}
|
|
151
|
+
if d < time.Hour {
|
|
152
|
+
return fmt.Sprintf("%.1fm", d.Minutes())
|
|
153
|
+
}
|
|
154
|
+
return fmt.Sprintf("%.1fh", d.Hours())
|
|
155
|
+
}
|
package/cmd/get.go
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Copyright (c) 2025 AgentSpan
|
|
2
|
+
// Licensed under the MIT License. See LICENSE file in the project root for details.
|
|
3
|
+
|
|
4
|
+
package cmd
|
|
5
|
+
|
|
6
|
+
import (
|
|
7
|
+
"fmt"
|
|
8
|
+
|
|
9
|
+
"github.com/spf13/cobra"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
var getVersion int
|
|
13
|
+
|
|
14
|
+
var getCmd = &cobra.Command{
|
|
15
|
+
Use: "get <name>",
|
|
16
|
+
Short: "Get agent configuration in JSON format",
|
|
17
|
+
Args: cobra.ExactArgs(1),
|
|
18
|
+
RunE: func(cmd *cobra.Command, args []string) error {
|
|
19
|
+
cfg := getConfig()
|
|
20
|
+
c := newClient(cfg)
|
|
21
|
+
|
|
22
|
+
var versionPtr *int
|
|
23
|
+
if cmd.Flags().Changed("version") {
|
|
24
|
+
versionPtr = &getVersion
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
result, err := c.GetAgent(args[0], versionPtr)
|
|
28
|
+
if err != nil {
|
|
29
|
+
return fmt.Errorf("failed to get agent: %w", err)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
printJSON(result)
|
|
33
|
+
return nil
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
func init() {
|
|
38
|
+
getCmd.Flags().IntVar(&getVersion, "version", 0, "Agent version (default: latest)")
|
|
39
|
+
agentCmd.AddCommand(getCmd)
|
|
40
|
+
}
|
package/cmd/helpers.go
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Copyright (c) 2025 AgentSpan
|
|
2
|
+
// Licensed under the MIT License. See LICENSE file in the project root for details.
|
|
3
|
+
|
|
4
|
+
package cmd
|
|
5
|
+
|
|
6
|
+
import (
|
|
7
|
+
"github.com/agentspan/agentspan/cli/client"
|
|
8
|
+
"github.com/agentspan/agentspan/cli/config"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
func getConfig() *config.Config {
|
|
12
|
+
cfg := config.Load()
|
|
13
|
+
if serverURL != "" {
|
|
14
|
+
cfg.ServerURL = serverURL
|
|
15
|
+
}
|
|
16
|
+
return cfg
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
func newClient(cfg *config.Config) *client.Client {
|
|
20
|
+
return client.New(cfg)
|
|
21
|
+
}
|