@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/server.go
ADDED
|
@@ -0,0 +1,446 @@
|
|
|
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
|
+
"io"
|
|
9
|
+
"net/http"
|
|
10
|
+
"os"
|
|
11
|
+
"os/exec"
|
|
12
|
+
"path/filepath"
|
|
13
|
+
"strconv"
|
|
14
|
+
"strings"
|
|
15
|
+
"time"
|
|
16
|
+
|
|
17
|
+
"github.com/agentspan/agentspan/cli/config"
|
|
18
|
+
"github.com/agentspan/agentspan/cli/internal/progress"
|
|
19
|
+
"github.com/fatih/color"
|
|
20
|
+
"github.com/spf13/cobra"
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
const (
|
|
24
|
+
s3Bucket = "https://agentspan.s3.us-east-2.amazonaws.com"
|
|
25
|
+
jarName = "agentspan-runtime.jar"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
var (
|
|
29
|
+
serverPort string
|
|
30
|
+
serverModel string
|
|
31
|
+
serverVersion string
|
|
32
|
+
serverJar string
|
|
33
|
+
serverLocal bool
|
|
34
|
+
followLogs bool
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
var serverCmd = &cobra.Command{
|
|
38
|
+
Use: "server",
|
|
39
|
+
Short: "Manage the agent runtime server",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
var serverStartCmd = &cobra.Command{
|
|
43
|
+
Use: "start",
|
|
44
|
+
Short: "Download (if needed) and start the agent runtime server",
|
|
45
|
+
RunE: runServerStart,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
var serverStopCmd = &cobra.Command{
|
|
49
|
+
Use: "stop",
|
|
50
|
+
Short: "Stop the running agent runtime server",
|
|
51
|
+
RunE: runServerStop,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
var serverLogsCmd = &cobra.Command{
|
|
55
|
+
Use: "logs",
|
|
56
|
+
Short: "Show server logs",
|
|
57
|
+
RunE: runServerLogs,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
func init() {
|
|
61
|
+
serverStartCmd.Flags().StringVarP(&serverPort, "port", "p", "8080", "Server port")
|
|
62
|
+
serverStartCmd.Flags().StringVarP(&serverModel, "model", "m", "", "Default LLM model (e.g. openai/gpt-4o)")
|
|
63
|
+
serverStartCmd.Flags().StringVar(&serverVersion, "version", "", "Specific server version to download (e.g. 0.1.0)")
|
|
64
|
+
serverStartCmd.Flags().StringVar(&serverJar, "jar", "", "Path to a local JAR file to use directly")
|
|
65
|
+
serverStartCmd.Flags().BoolVar(&serverLocal, "local", false, "Use locally built JAR from server/build/libs/")
|
|
66
|
+
|
|
67
|
+
serverLogsCmd.Flags().BoolVarP(&followLogs, "follow", "f", false, "Follow log output")
|
|
68
|
+
|
|
69
|
+
serverCmd.AddCommand(serverStartCmd, serverStopCmd, serverLogsCmd)
|
|
70
|
+
rootCmd.AddCommand(serverCmd)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
func serverDir() string {
|
|
74
|
+
return filepath.Join(config.ConfigDir(), "server")
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
func pidFile() string {
|
|
78
|
+
return filepath.Join(serverDir(), "server.pid")
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
func logFile() string {
|
|
82
|
+
return filepath.Join(serverDir(), "server.log")
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
func runServerStart(cmd *cobra.Command, args []string) error {
|
|
86
|
+
dir := serverDir()
|
|
87
|
+
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
88
|
+
return fmt.Errorf("create server dir: %w", err)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
var jarPath string
|
|
92
|
+
switch {
|
|
93
|
+
case serverJar != "":
|
|
94
|
+
// Use explicit JAR path
|
|
95
|
+
abs, err := filepath.Abs(serverJar)
|
|
96
|
+
if err != nil {
|
|
97
|
+
return fmt.Errorf("resolve JAR path: %w", err)
|
|
98
|
+
}
|
|
99
|
+
if _, err := os.Stat(abs); err != nil {
|
|
100
|
+
return fmt.Errorf("JAR not found: %s", abs)
|
|
101
|
+
}
|
|
102
|
+
jarPath = abs
|
|
103
|
+
color.Green("Using JAR: %s", jarPath)
|
|
104
|
+
|
|
105
|
+
case serverLocal:
|
|
106
|
+
// Find locally built JAR by walking up from executable or CWD
|
|
107
|
+
localJar, err := findLocalJAR()
|
|
108
|
+
if err != nil {
|
|
109
|
+
return err
|
|
110
|
+
}
|
|
111
|
+
jarPath = localJar
|
|
112
|
+
color.Green("Using local JAR: %s", jarPath)
|
|
113
|
+
|
|
114
|
+
case serverVersion != "":
|
|
115
|
+
jarPath = filepath.Join(dir, fmt.Sprintf("agentspan-runtime-%s.jar", serverVersion))
|
|
116
|
+
if err := ensureVersionedJAR(jarPath, serverVersion); err != nil {
|
|
117
|
+
return err
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
default:
|
|
121
|
+
jarPath = filepath.Join(dir, jarName)
|
|
122
|
+
if err := ensureLatestJAR(jarPath); err != nil {
|
|
123
|
+
return err
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Check if already running
|
|
128
|
+
if pid, err := readPID(); err == nil {
|
|
129
|
+
if processRunning(pid) {
|
|
130
|
+
color.Yellow("Server already running (PID %d). Stop it first with: agentspan server stop", pid)
|
|
131
|
+
return nil
|
|
132
|
+
}
|
|
133
|
+
// Stale PID file
|
|
134
|
+
os.Remove(pidFile())
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
checkAIProviderKeys()
|
|
138
|
+
|
|
139
|
+
bold := color.New(color.Bold)
|
|
140
|
+
bold.Printf("Starting agent runtime on port %s...\n", serverPort)
|
|
141
|
+
|
|
142
|
+
// Build java args
|
|
143
|
+
javaArgs := []string{"-jar", jarPath}
|
|
144
|
+
|
|
145
|
+
env := os.Environ()
|
|
146
|
+
if serverPort != "8080" {
|
|
147
|
+
env = append(env, "SERVER_PORT="+serverPort)
|
|
148
|
+
}
|
|
149
|
+
if serverModel != "" {
|
|
150
|
+
env = append(env, "AGENT_DEFAULT_MODEL="+serverModel)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Open log file
|
|
154
|
+
logF, err := os.OpenFile(logFile(), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644)
|
|
155
|
+
if err != nil {
|
|
156
|
+
return fmt.Errorf("open log file: %w", err)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
proc := exec.Command("java", javaArgs...)
|
|
160
|
+
proc.Env = env
|
|
161
|
+
proc.Stdout = logF
|
|
162
|
+
proc.Stderr = logF
|
|
163
|
+
proc.SysProcAttr = sysProcAttr()
|
|
164
|
+
|
|
165
|
+
if err := proc.Start(); err != nil {
|
|
166
|
+
logF.Close()
|
|
167
|
+
return fmt.Errorf("failed to start server: %w", err)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Write PID
|
|
171
|
+
pid := proc.Process.Pid
|
|
172
|
+
if err := os.WriteFile(pidFile(), []byte(strconv.Itoa(pid)), 0o644); err != nil {
|
|
173
|
+
logF.Close()
|
|
174
|
+
return fmt.Errorf("write PID file: %w", err)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Detach - release the process so CLI can exit
|
|
178
|
+
proc.Process.Release()
|
|
179
|
+
logF.Close()
|
|
180
|
+
|
|
181
|
+
color.Green("Server started (PID %d)", pid)
|
|
182
|
+
fmt.Printf(" Logs: %s\n", logFile())
|
|
183
|
+
fmt.Printf(" URL: http://localhost:%s\n", serverPort)
|
|
184
|
+
fmt.Println("\nUse 'agentspan server logs -f' to follow output.")
|
|
185
|
+
return nil
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
func runServerStop(cmd *cobra.Command, args []string) error {
|
|
189
|
+
pid, err := readPID()
|
|
190
|
+
if err != nil {
|
|
191
|
+
color.Yellow("No server PID file found. Server may not be running.")
|
|
192
|
+
return nil
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if !processRunning(pid) {
|
|
196
|
+
os.Remove(pidFile())
|
|
197
|
+
color.Yellow("Server process (PID %d) is not running. Cleaned up stale PID file.", pid)
|
|
198
|
+
return nil
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
process, err := os.FindProcess(pid)
|
|
202
|
+
if err != nil {
|
|
203
|
+
return fmt.Errorf("find process %d: %w", pid, err)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if err := killProcess(process); err != nil {
|
|
207
|
+
return fmt.Errorf("stop process %d: %w", pid, err)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
os.Remove(pidFile())
|
|
211
|
+
color.Green("Server stopped (PID %d)", pid)
|
|
212
|
+
return nil
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
func runServerLogs(cmd *cobra.Command, args []string) error {
|
|
216
|
+
path := logFile()
|
|
217
|
+
if _, err := os.Stat(path); os.IsNotExist(err) {
|
|
218
|
+
return fmt.Errorf("no log file found at %s", path)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if !followLogs {
|
|
222
|
+
data, err := os.ReadFile(path)
|
|
223
|
+
if err != nil {
|
|
224
|
+
return err
|
|
225
|
+
}
|
|
226
|
+
fmt.Print(string(data))
|
|
227
|
+
return nil
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Follow mode: tail -f style
|
|
231
|
+
f, err := os.Open(path)
|
|
232
|
+
if err != nil {
|
|
233
|
+
return err
|
|
234
|
+
}
|
|
235
|
+
defer f.Close()
|
|
236
|
+
|
|
237
|
+
// Seek to end
|
|
238
|
+
f.Seek(0, io.SeekEnd)
|
|
239
|
+
|
|
240
|
+
buf := make([]byte, 4096)
|
|
241
|
+
for {
|
|
242
|
+
n, err := f.Read(buf)
|
|
243
|
+
if n > 0 {
|
|
244
|
+
fmt.Print(string(buf[:n]))
|
|
245
|
+
}
|
|
246
|
+
if err == io.EOF {
|
|
247
|
+
time.Sleep(200 * time.Millisecond)
|
|
248
|
+
continue
|
|
249
|
+
}
|
|
250
|
+
if err != nil {
|
|
251
|
+
return err
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// --- Local JAR helpers ---
|
|
257
|
+
|
|
258
|
+
func findLocalJAR() (string, error) {
|
|
259
|
+
// Try CWD first, then walk up to find 'server/build/libs/agentspan-runtime.jar'
|
|
260
|
+
cwd, err := os.Getwd()
|
|
261
|
+
if err != nil {
|
|
262
|
+
return "", fmt.Errorf("get working directory: %w", err)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Check common relative paths from likely CWD locations
|
|
266
|
+
candidates := []string{
|
|
267
|
+
filepath.Join(cwd, "server", "build", "libs", jarName),
|
|
268
|
+
filepath.Join(cwd, "build", "libs", jarName),
|
|
269
|
+
filepath.Join(cwd, "..", "server", "build", "libs", jarName),
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Also walk up from CWD looking for server/build/libs/
|
|
273
|
+
dir := cwd
|
|
274
|
+
for i := 0; i < 5; i++ {
|
|
275
|
+
candidate := filepath.Join(dir, "server", "build", "libs", jarName)
|
|
276
|
+
candidates = append(candidates, candidate)
|
|
277
|
+
parent := filepath.Dir(dir)
|
|
278
|
+
if parent == dir {
|
|
279
|
+
break
|
|
280
|
+
}
|
|
281
|
+
dir = parent
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
for _, c := range candidates {
|
|
285
|
+
if _, err := os.Stat(c); err == nil {
|
|
286
|
+
return filepath.Abs(c)
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return "", fmt.Errorf("local JAR not found. Build it first with: cd server && ./gradlew build")
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// --- JAR download helpers ---
|
|
294
|
+
|
|
295
|
+
func ensureVersionedJAR(jarPath, version string) error {
|
|
296
|
+
if _, err := os.Stat(jarPath); err == nil {
|
|
297
|
+
color.Green("Using cached JAR for version %s", version)
|
|
298
|
+
return nil
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
downloadURL := fmt.Sprintf("%s/agentspan-server-%s.jar", s3Bucket, version)
|
|
302
|
+
return downloadJAR(downloadURL, jarPath)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
func ensureLatestJAR(jarPath string) error {
|
|
306
|
+
downloadURL := fmt.Sprintf("%s/agentspan-server-latest.jar", s3Bucket)
|
|
307
|
+
|
|
308
|
+
// If we already have a cached JAR, do a HEAD request to check if remote has changed
|
|
309
|
+
if info, err := os.Stat(jarPath); err == nil {
|
|
310
|
+
httpClient := &http.Client{Timeout: 15 * time.Second}
|
|
311
|
+
resp, err := httpClient.Head(downloadURL)
|
|
312
|
+
if err != nil {
|
|
313
|
+
color.Yellow("Could not check for updates (%v), using cached JAR", err)
|
|
314
|
+
return nil
|
|
315
|
+
}
|
|
316
|
+
resp.Body.Close()
|
|
317
|
+
|
|
318
|
+
if resp.StatusCode == http.StatusOK {
|
|
319
|
+
// Compare content-length as a simple freshness check
|
|
320
|
+
remoteSize := resp.ContentLength
|
|
321
|
+
if remoteSize > 0 && remoteSize == info.Size() {
|
|
322
|
+
color.Green("Server JAR is up to date")
|
|
323
|
+
return nil
|
|
324
|
+
}
|
|
325
|
+
} else if resp.StatusCode == http.StatusNotFound {
|
|
326
|
+
color.Yellow("No remote release found, using cached JAR")
|
|
327
|
+
return nil
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return downloadJAR(downloadURL, jarPath)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
func downloadJAR(downloadURL, destPath string) error {
|
|
335
|
+
color.Yellow("Downloading server JAR...")
|
|
336
|
+
fmt.Printf(" URL: %s\n", downloadURL)
|
|
337
|
+
|
|
338
|
+
httpClient := &http.Client{
|
|
339
|
+
Timeout: 10 * time.Minute,
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
resp, err := httpClient.Get(downloadURL)
|
|
343
|
+
if err != nil {
|
|
344
|
+
return fmt.Errorf("download: %w", err)
|
|
345
|
+
}
|
|
346
|
+
defer resp.Body.Close()
|
|
347
|
+
|
|
348
|
+
if resp.StatusCode != http.StatusOK {
|
|
349
|
+
return fmt.Errorf("download failed: HTTP %d", resp.StatusCode)
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Write to temp file first, then rename
|
|
353
|
+
tmpPath := destPath + ".tmp"
|
|
354
|
+
f, err := os.Create(tmpPath)
|
|
355
|
+
if err != nil {
|
|
356
|
+
return fmt.Errorf("create temp file: %w", err)
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
pr, bar := progress.NewReader(resp.Body, resp.ContentLength, "Downloading")
|
|
360
|
+
_, err = io.Copy(f, pr)
|
|
361
|
+
f.Close()
|
|
362
|
+
bar.Finish()
|
|
363
|
+
if err != nil {
|
|
364
|
+
os.Remove(tmpPath)
|
|
365
|
+
return fmt.Errorf("write JAR: %w", err)
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if err := os.Rename(tmpPath, destPath); err != nil {
|
|
369
|
+
os.Remove(tmpPath)
|
|
370
|
+
return fmt.Errorf("rename JAR: %w", err)
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
color.Green("Download complete!")
|
|
374
|
+
return nil
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// --- AI provider check ---
|
|
378
|
+
|
|
379
|
+
const aiModelsDocURL = "https://github.com/agentspan/agentspan/blob/main/docs/ai-models.md"
|
|
380
|
+
|
|
381
|
+
func checkAIProviderKeys() {
|
|
382
|
+
hasAny := false
|
|
383
|
+
for _, p := range aiProviders {
|
|
384
|
+
allSet := true
|
|
385
|
+
for _, env := range p.envVars {
|
|
386
|
+
if os.Getenv(env) == "" {
|
|
387
|
+
allSet = false
|
|
388
|
+
break
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
if allSet {
|
|
392
|
+
hasAny = true
|
|
393
|
+
break
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Check provider-specific warnings
|
|
398
|
+
for _, p := range aiProviders {
|
|
399
|
+
for _, w := range p.warns {
|
|
400
|
+
if w.condition() {
|
|
401
|
+
warn := color.New(color.FgYellow, color.Bold)
|
|
402
|
+
warn.Printf("WARNING: %s — %s\n", p.name, w.message)
|
|
403
|
+
fmt.Println()
|
|
404
|
+
fmt.Printf(" %s\n", w.fix)
|
|
405
|
+
fmt.Println()
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if hasAny {
|
|
411
|
+
return
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
warn := color.New(color.FgYellow, color.Bold)
|
|
415
|
+
warn.Println("WARNING: No AI provider API keys detected!")
|
|
416
|
+
fmt.Println()
|
|
417
|
+
fmt.Println(" The server will start, but agents won't be able to call any LLM")
|
|
418
|
+
fmt.Println(" until you set at least one provider's API key.")
|
|
419
|
+
fmt.Println()
|
|
420
|
+
fmt.Println(" Set one or more of these before starting the server:")
|
|
421
|
+
fmt.Println()
|
|
422
|
+
fmt.Println(" # OpenAI")
|
|
423
|
+
fmt.Println(" export OPENAI_API_KEY=sk-...")
|
|
424
|
+
fmt.Println()
|
|
425
|
+
fmt.Println(" # Anthropic (Claude)")
|
|
426
|
+
fmt.Println(" export ANTHROPIC_API_KEY=sk-ant-...")
|
|
427
|
+
fmt.Println()
|
|
428
|
+
fmt.Println(" # Google Gemini")
|
|
429
|
+
fmt.Println(" export GEMINI_API_KEY=AI...")
|
|
430
|
+
fmt.Println(" export GOOGLE_CLOUD_PROJECT=your-gcp-project-id")
|
|
431
|
+
fmt.Println()
|
|
432
|
+
fmt.Println(" Run 'agentspan doctor' for a full diagnostic.")
|
|
433
|
+
fmt.Printf(" Docs: %s\n", aiModelsDocURL)
|
|
434
|
+
fmt.Println()
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// --- PID helpers ---
|
|
438
|
+
|
|
439
|
+
func readPID() (int, error) {
|
|
440
|
+
data, err := os.ReadFile(pidFile())
|
|
441
|
+
if err != nil {
|
|
442
|
+
return 0, err
|
|
443
|
+
}
|
|
444
|
+
return strconv.Atoi(strings.TrimSpace(string(data)))
|
|
445
|
+
}
|
|
446
|
+
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Copyright (c) 2025 AgentSpan
|
|
2
|
+
// Licensed under the MIT License. See LICENSE file in the project root for details.
|
|
3
|
+
|
|
4
|
+
//go:build !windows
|
|
5
|
+
|
|
6
|
+
package cmd
|
|
7
|
+
|
|
8
|
+
import (
|
|
9
|
+
"os"
|
|
10
|
+
"syscall"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
func sysProcAttr() *syscall.SysProcAttr {
|
|
14
|
+
return &syscall.SysProcAttr{Setpgid: true}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
func killProcess(process *os.Process) error {
|
|
18
|
+
return process.Signal(syscall.SIGTERM)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
func processRunning(pid int) bool {
|
|
22
|
+
process, err := os.FindProcess(pid)
|
|
23
|
+
if err != nil {
|
|
24
|
+
return false
|
|
25
|
+
}
|
|
26
|
+
err = process.Signal(syscall.Signal(0))
|
|
27
|
+
return err == nil
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
func getFreeDiskMB(path string) int64 {
|
|
31
|
+
var stat syscall.Statfs_t
|
|
32
|
+
if err := syscall.Statfs(path, &stat); err != nil {
|
|
33
|
+
return -1
|
|
34
|
+
}
|
|
35
|
+
return int64(stat.Bavail) * int64(stat.Bsize) / (1024 * 1024)
|
|
36
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Copyright (c) 2025 AgentSpan
|
|
2
|
+
// Licensed under the MIT License. See LICENSE file in the project root for details.
|
|
3
|
+
|
|
4
|
+
//go:build windows
|
|
5
|
+
|
|
6
|
+
package cmd
|
|
7
|
+
|
|
8
|
+
import (
|
|
9
|
+
"os"
|
|
10
|
+
"syscall"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
func sysProcAttr() *syscall.SysProcAttr {
|
|
14
|
+
return &syscall.SysProcAttr{}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
func killProcess(process *os.Process) error {
|
|
18
|
+
return process.Kill()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
func processRunning(pid int) bool {
|
|
22
|
+
process, err := os.FindProcess(pid)
|
|
23
|
+
if err != nil {
|
|
24
|
+
return false
|
|
25
|
+
}
|
|
26
|
+
// On Windows, FindProcess always succeeds; try to open the process
|
|
27
|
+
err = process.Signal(os.Kill)
|
|
28
|
+
if err != nil {
|
|
29
|
+
return false
|
|
30
|
+
}
|
|
31
|
+
return true
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
func getFreeDiskMB(path string) int64 {
|
|
35
|
+
// Not easily available on Windows without unsafe; skip check
|
|
36
|
+
return -1
|
|
37
|
+
}
|
package/cmd/status.go
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
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/fatih/color"
|
|
10
|
+
"github.com/spf13/cobra"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
var statusCmd = &cobra.Command{
|
|
14
|
+
Use: "status <execution-id>",
|
|
15
|
+
Short: "Get the detailed status of an agent execution",
|
|
16
|
+
Args: cobra.ExactArgs(1),
|
|
17
|
+
RunE: func(cmd *cobra.Command, args []string) error {
|
|
18
|
+
cfg := getConfig()
|
|
19
|
+
c := newClient(cfg)
|
|
20
|
+
|
|
21
|
+
detail, err := c.GetExecutionDetail(args[0])
|
|
22
|
+
if err != nil {
|
|
23
|
+
return fmt.Errorf("failed to get status: %w", err)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
bold := color.New(color.Bold)
|
|
27
|
+
bold.Printf("Execution: %s\n", detail.WorkflowID)
|
|
28
|
+
fmt.Printf(" Agent: %s (v%d)\n", detail.AgentName, detail.Version)
|
|
29
|
+
|
|
30
|
+
statusColor := color.FgWhite
|
|
31
|
+
switch detail.Status {
|
|
32
|
+
case "RUNNING":
|
|
33
|
+
statusColor = color.FgYellow
|
|
34
|
+
case "COMPLETED":
|
|
35
|
+
statusColor = color.FgGreen
|
|
36
|
+
case "FAILED", "TERMINATED", "TIMED_OUT":
|
|
37
|
+
statusColor = color.FgRed
|
|
38
|
+
}
|
|
39
|
+
color.New(statusColor, color.Bold).Printf(" Status: %s\n", detail.Status)
|
|
40
|
+
|
|
41
|
+
if detail.Input != nil {
|
|
42
|
+
fmt.Printf("\nInput:\n")
|
|
43
|
+
printJSON(detail.Input)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if detail.Output != nil {
|
|
47
|
+
fmt.Printf("\nOutput:\n")
|
|
48
|
+
printJSON(detail.Output)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if detail.CurrentTask != nil {
|
|
52
|
+
fmt.Printf("\nCurrent Task:\n")
|
|
53
|
+
fmt.Printf(" Name: %s\n", detail.CurrentTask.TaskRefName)
|
|
54
|
+
fmt.Printf(" Type: %s\n", detail.CurrentTask.TaskType)
|
|
55
|
+
fmt.Printf(" Status: %s\n", detail.CurrentTask.Status)
|
|
56
|
+
if detail.CurrentTask.InputData != nil {
|
|
57
|
+
fmt.Printf(" Input:\n")
|
|
58
|
+
printJSON(detail.CurrentTask.InputData)
|
|
59
|
+
}
|
|
60
|
+
if detail.CurrentTask.OutputData != nil {
|
|
61
|
+
fmt.Printf(" Output:\n")
|
|
62
|
+
printJSON(detail.CurrentTask.OutputData)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return nil
|
|
67
|
+
},
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
func init() {
|
|
71
|
+
agentCmd.AddCommand(statusCmd)
|
|
72
|
+
}
|
package/cmd/stream.go
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
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 streamLastEventID string
|
|
13
|
+
|
|
14
|
+
var streamCmd = &cobra.Command{
|
|
15
|
+
Use: "stream <workflow-id>",
|
|
16
|
+
Short: "Stream events from a running agent",
|
|
17
|
+
Args: cobra.ExactArgs(1),
|
|
18
|
+
RunE: func(cmd *cobra.Command, args []string) error {
|
|
19
|
+
cfg := getConfig()
|
|
20
|
+
c := newClient(cfg)
|
|
21
|
+
|
|
22
|
+
workflowID := args[0]
|
|
23
|
+
fmt.Printf("Streaming events for %s...\n\n", workflowID)
|
|
24
|
+
|
|
25
|
+
return streamWorkflow(c, workflowID)
|
|
26
|
+
},
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
func init() {
|
|
30
|
+
streamCmd.Flags().StringVar(&streamLastEventID, "last-event-id", "", "Resume from a specific event ID")
|
|
31
|
+
agentCmd.AddCommand(streamCmd)
|
|
32
|
+
}
|
package/cmd/update.go
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
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
|
+
"io"
|
|
9
|
+
"net/http"
|
|
10
|
+
"os"
|
|
11
|
+
"runtime"
|
|
12
|
+
"time"
|
|
13
|
+
|
|
14
|
+
"github.com/agentspan/agentspan/cli/internal/progress"
|
|
15
|
+
"github.com/fatih/color"
|
|
16
|
+
"github.com/spf13/cobra"
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
const cliS3Bucket = "https://agentspan.s3.us-east-2.amazonaws.com"
|
|
20
|
+
|
|
21
|
+
var updateCmd = &cobra.Command{
|
|
22
|
+
Use: "update",
|
|
23
|
+
Short: "Update the CLI to the latest version",
|
|
24
|
+
RunE: func(cmd *cobra.Command, args []string) error {
|
|
25
|
+
goos := runtime.GOOS
|
|
26
|
+
goarch := runtime.GOARCH
|
|
27
|
+
|
|
28
|
+
binaryName := fmt.Sprintf("agentspan_%s_%s", goos, goarch)
|
|
29
|
+
if goos == "windows" {
|
|
30
|
+
binaryName += ".exe"
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
downloadURL := fmt.Sprintf("%s/cli/latest/%s", cliS3Bucket, binaryName)
|
|
34
|
+
|
|
35
|
+
color.Yellow("Downloading latest CLI...")
|
|
36
|
+
fmt.Printf(" URL: %s\n", downloadURL)
|
|
37
|
+
|
|
38
|
+
httpClient := &http.Client{Timeout: 5 * time.Minute}
|
|
39
|
+
resp, err := httpClient.Get(downloadURL)
|
|
40
|
+
if err != nil {
|
|
41
|
+
return fmt.Errorf("download failed: %w", err)
|
|
42
|
+
}
|
|
43
|
+
defer resp.Body.Close()
|
|
44
|
+
|
|
45
|
+
if resp.StatusCode != http.StatusOK {
|
|
46
|
+
return fmt.Errorf("download failed: HTTP %d", resp.StatusCode)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Find current executable path
|
|
50
|
+
execPath, err := os.Executable()
|
|
51
|
+
if err != nil {
|
|
52
|
+
return fmt.Errorf("find executable path: %w", err)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Write to temp file with progress bar
|
|
56
|
+
tmpPath := execPath + ".new"
|
|
57
|
+
f, err := os.Create(tmpPath)
|
|
58
|
+
if err != nil {
|
|
59
|
+
return fmt.Errorf("create temp file: %w", err)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
pr, bar := progress.NewReader(resp.Body, resp.ContentLength, "Downloading")
|
|
63
|
+
_, err = io.Copy(f, pr)
|
|
64
|
+
f.Close()
|
|
65
|
+
bar.Finish()
|
|
66
|
+
if err != nil {
|
|
67
|
+
os.Remove(tmpPath)
|
|
68
|
+
return fmt.Errorf("write binary: %w", err)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Make executable
|
|
72
|
+
if err := os.Chmod(tmpPath, 0o755); err != nil {
|
|
73
|
+
os.Remove(tmpPath)
|
|
74
|
+
return fmt.Errorf("chmod: %w", err)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Replace current executable
|
|
78
|
+
if err := os.Rename(tmpPath, execPath); err != nil {
|
|
79
|
+
os.Remove(tmpPath)
|
|
80
|
+
return fmt.Errorf("replace binary: %w", err)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
color.Green("Updated successfully!")
|
|
84
|
+
fmt.Println("Run 'agentspan version' to see the new version.")
|
|
85
|
+
return nil
|
|
86
|
+
},
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
func init() {
|
|
90
|
+
rootCmd.AddCommand(updateCmd)
|
|
91
|
+
}
|