@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/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
+
@@ -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
+ }