@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 ADDED
@@ -0,0 +1,227 @@
1
+ # AgentSpan CLI
2
+
3
+ Command-line interface for building, running, and managing AI agents powered by the AgentSpan runtime.
4
+
5
+ ## Install
6
+
7
+ ### npm (recommended)
8
+
9
+ ```bash
10
+ npm install -g @agentspan/agentspan
11
+ ```
12
+
13
+ ### Homebrew
14
+
15
+ ```bash
16
+ brew tap agentspan/agentspan
17
+ brew install agentspan
18
+ ```
19
+
20
+ ### Shell script
21
+
22
+ ```bash
23
+ curl -fsSL https://raw.githubusercontent.com/agentspan/agentspan/main/cli/install.sh | sh
24
+ ```
25
+
26
+ ### From source
27
+
28
+ ```bash
29
+ cd cli
30
+ go build -o agentspan .
31
+ ```
32
+
33
+ ## Quickstart
34
+
35
+ ```bash
36
+ # Start the runtime server (downloads automatically)
37
+ agentspan server start
38
+
39
+ # Create an agent config
40
+ agentspan agent init mybot
41
+
42
+ # Run an agent from a config file
43
+ agentspan agent run --config mybot.yaml "What is the weather in NYC?"
44
+
45
+ # Run a registered agent by name
46
+ agentspan agent run --name mybot "What is the weather in NYC?"
47
+ ```
48
+
49
+ ## Commands
50
+
51
+ ### Server Management
52
+
53
+ ```bash
54
+ # Start the server (downloads latest JAR if needed)
55
+ agentspan server start
56
+
57
+ # Start a specific version
58
+ agentspan server start --version 0.1.0
59
+
60
+ # Start on a custom port with a default model
61
+ agentspan server start --port 9090 --model openai/gpt-4o
62
+
63
+ # Stop the server
64
+ agentspan server stop
65
+
66
+ # View server logs
67
+ agentspan server logs
68
+
69
+ # Follow server logs in real-time
70
+ agentspan server logs -f
71
+ ```
72
+
73
+ The server JAR is downloaded from GitHub releases and cached in `~/.agentspan/server/`. On each `server start`, the CLI checks GitHub for updates and re-downloads if a newer version is available.
74
+
75
+ ### Agent Operations
76
+
77
+ ```bash
78
+ # Create a new agent config file
79
+ agentspan agent init mybot
80
+ agentspan agent init mybot --model anthropic/claude-sonnet-4-20250514 --format json
81
+
82
+ # Run an agent
83
+ agentspan agent run --name mybot "Hello, what can you do?"
84
+ agentspan agent run --config mybot.yaml "Hello, what can you do?"
85
+ agentspan agent run --name mybot --no-stream "Fire and forget"
86
+
87
+ # List all registered agents
88
+ agentspan agent list
89
+
90
+ # Get agent definition as JSON
91
+ agentspan agent get mybot
92
+ agentspan agent get mybot --version 2
93
+
94
+ # Delete an agent
95
+ agentspan agent delete mybot
96
+ agentspan agent delete mybot --version 1
97
+
98
+ # Check execution status
99
+ agentspan agent status <execution-id>
100
+
101
+ # Search execution history
102
+ agentspan agent execution
103
+ agentspan agent execution --name mybot
104
+ agentspan agent execution --status COMPLETED --since 1h
105
+ agentspan agent execution --since 7d
106
+ agentspan agent execution --window now-30m
107
+
108
+ # Stream events from a running agent
109
+ agentspan agent stream <execution-id>
110
+
111
+ # Respond to human-in-the-loop tasks
112
+ agentspan agent respond <execution-id> --approve
113
+ agentspan agent respond <execution-id> --deny --reason "Amount too high"
114
+
115
+ # Compile agent config to workflow definition (inspect only)
116
+ agentspan agent compile mybot.yaml
117
+ ```
118
+
119
+ ### Time Filters
120
+
121
+ The `--since` and `--window` flags accept human-readable time specs:
122
+
123
+ | Format | Meaning |
124
+ |--------|---------|
125
+ | `30s` | 30 seconds |
126
+ | `5m` | 5 minutes |
127
+ | `1h` | 1 hour |
128
+ | `1d` | 1 day |
129
+ | `7d` | 7 days |
130
+ | `1mo` | 1 month (30 days) |
131
+ | `1y` | 1 year (365 days) |
132
+
133
+ ### CLI Self-Update
134
+
135
+ ```bash
136
+ agentspan update
137
+ ```
138
+
139
+ ### Configuration
140
+
141
+ ```bash
142
+ # Set server URL and auth credentials
143
+ agentspan configure --url http://myserver:8080
144
+ agentspan configure --auth-key KEY --auth-secret SECRET
145
+
146
+ # Override server URL for a single command
147
+ agentspan --server http://other:8080 agent list
148
+ ```
149
+
150
+ Configuration is stored in `~/.agentspan/config.json`. Environment variables take precedence:
151
+
152
+ | Variable | Description |
153
+ |----------|-------------|
154
+ | `AGENT_SERVER_URL` | Server URL (default: `http://localhost:8080`) |
155
+ | `CONDUCTOR_AUTH_KEY` | Auth key |
156
+ | `CONDUCTOR_AUTH_SECRET` | Auth secret |
157
+
158
+ ### Version
159
+
160
+ ```bash
161
+ agentspan version
162
+ ```
163
+
164
+ ## Agent Config Format
165
+
166
+ YAML or JSON. See [examples/](examples/) for samples.
167
+
168
+ ```yaml
169
+ name: my-agent
170
+ description: A helpful assistant
171
+ model: openai/gpt-4o
172
+ instructions: You are a helpful assistant.
173
+ maxTurns: 25
174
+ tools:
175
+ - name: web_search
176
+ type: worker
177
+ ```
178
+
179
+ ## Distribution
180
+
181
+ The CLI is distributed through three channels:
182
+
183
+ 1. **npm** (`@agentspan/agentspan`) -- Node.js wrapper downloads the Go binary on install
184
+ 2. **Homebrew** (`agentspan/agentspan` tap) -- Pre-built binaries for macOS and Linux
185
+ 3. **Shell installer** -- Direct binary download to `/usr/local/bin`
186
+ 4. **GitHub Releases** -- Pre-built binaries for all platforms
187
+
188
+ ### Supported Platforms
189
+
190
+ | OS | Architecture |
191
+ |----|-------------|
192
+ | macOS | x86_64, ARM64 (Apple Silicon) |
193
+ | Linux | x86_64, ARM64 |
194
+ | Windows | x86_64, ARM64 |
195
+
196
+ ## Development
197
+
198
+ ### Building
199
+
200
+ ```bash
201
+ cd cli
202
+ go build -o agentspan .
203
+ ```
204
+
205
+ ### Cross-platform build
206
+
207
+ ```bash
208
+ cd cli
209
+ VERSION=0.1.0 ./build.sh
210
+ ```
211
+
212
+ Produces binaries in `cli/dist/` for all 6 platform/arch combinations.
213
+
214
+ ### Release
215
+
216
+ Push a tag matching `cli-v*` to trigger the release workflow:
217
+
218
+ ```bash
219
+ git tag cli-v0.1.0
220
+ git push origin cli-v0.1.0
221
+ ```
222
+
223
+ This builds all binaries, creates a GitHub release, publishes to npm, and updates the Homebrew tap.
224
+
225
+ ## License
226
+
227
+ Apache 2.0
package/build.sh ADDED
@@ -0,0 +1,35 @@
1
+ #!/bin/bash
2
+ set -e
3
+
4
+ VERSION="${VERSION:-dev}"
5
+ COMMIT="${COMMIT:-$(git rev-parse --short HEAD 2>/dev/null || echo 'none')}"
6
+ DATE="${DATE:-$(date -u +%Y-%m-%dT%H:%M:%SZ)}"
7
+ LDFLAGS="-X github.com/agentspan/agentspan/cli/cmd.Version=${VERSION} -X github.com/agentspan/agentspan/cli/cmd.Commit=${COMMIT} -X github.com/agentspan/agentspan/cli/cmd.Date=${DATE}"
8
+
9
+ mkdir -p dist
10
+
11
+ echo "Building agentspan CLI v${VERSION}..."
12
+
13
+ GOOS=darwin GOARCH=amd64 go build -ldflags "$LDFLAGS" -o dist/agentspan_darwin_amd64 .
14
+ echo " Built: darwin/amd64"
15
+
16
+ GOOS=darwin GOARCH=arm64 go build -ldflags "$LDFLAGS" -o dist/agentspan_darwin_arm64 .
17
+ echo " Built: darwin/arm64"
18
+
19
+ GOOS=linux GOARCH=amd64 go build -ldflags "$LDFLAGS" -o dist/agentspan_linux_amd64 .
20
+ echo " Built: linux/amd64"
21
+
22
+ GOOS=linux GOARCH=arm64 go build -ldflags "$LDFLAGS" -o dist/agentspan_linux_arm64 .
23
+ echo " Built: linux/arm64"
24
+
25
+ GOOS=windows GOARCH=amd64 go build -ldflags "$LDFLAGS" -o dist/agentspan_windows_amd64.exe .
26
+ echo " Built: windows/amd64"
27
+
28
+ GOOS=windows GOARCH=arm64 go build -ldflags "$LDFLAGS" -o dist/agentspan_windows_arm64.exe .
29
+ echo " Built: windows/arm64"
30
+
31
+ chmod +x dist/agentspan_*
32
+
33
+ echo ""
34
+ echo "Build complete! Binaries in dist/"
35
+ ls -lh dist/
package/cli.js ADDED
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Copyright (c) 2025 AgentSpan
4
+ // Licensed under the MIT License. See LICENSE file in the project root for details.
5
+
6
+
7
+ const { spawnSync } = require('child_process');
8
+ const path = require('path');
9
+ const fs = require('fs');
10
+
11
+ const platform = process.platform;
12
+ const binaryName = platform === 'win32' ? 'agentspan.exe' : 'agentspan';
13
+ const binaryPath = path.join(__dirname, 'bin', binaryName);
14
+
15
+ // Check if binary exists
16
+ if (!fs.existsSync(binaryPath)) {
17
+ console.error('Error: Binary not found. Please reinstall the package.');
18
+ console.error('Run: npm uninstall -g @agentspan/agentspan && npm install -g @agentspan/agentspan');
19
+ process.exit(1);
20
+ }
21
+
22
+ // Execute the binary with all arguments
23
+ const result = spawnSync(binaryPath, process.argv.slice(2), {
24
+ stdio: 'inherit',
25
+ shell: false
26
+ });
27
+
28
+ process.exit(result.status);
@@ -0,0 +1,365 @@
1
+ // Copyright (c) 2025 AgentSpan
2
+ // Licensed under the MIT License. See LICENSE file in the project root for details.
3
+
4
+ package client
5
+
6
+ import (
7
+ "bufio"
8
+ "bytes"
9
+ "encoding/json"
10
+ "fmt"
11
+ "io"
12
+ "net/http"
13
+ "net/url"
14
+ "strings"
15
+ "time"
16
+
17
+ "github.com/agentspan/agentspan/cli/config"
18
+ )
19
+
20
+ type Client struct {
21
+ baseURL string
22
+ httpClient *http.Client
23
+ authKey string
24
+ authSecret string
25
+ }
26
+
27
+ func New(cfg *config.Config) *Client {
28
+ return &Client{
29
+ baseURL: strings.TrimRight(cfg.ServerURL, "/"),
30
+ httpClient: &http.Client{Timeout: 30 * time.Second},
31
+ authKey: cfg.AuthKey,
32
+ authSecret: cfg.AuthSecret,
33
+ }
34
+ }
35
+
36
+ func (c *Client) doRequest(method, path string, body interface{}) (*http.Response, error) {
37
+ var bodyReader io.Reader
38
+ if body != nil {
39
+ data, err := json.Marshal(body)
40
+ if err != nil {
41
+ return nil, fmt.Errorf("marshal request: %w", err)
42
+ }
43
+ bodyReader = bytes.NewReader(data)
44
+ }
45
+
46
+ req, err := http.NewRequest(method, c.baseURL+path, bodyReader)
47
+ if err != nil {
48
+ return nil, err
49
+ }
50
+ if body != nil {
51
+ req.Header.Set("Content-Type", "application/json")
52
+ }
53
+ if c.authKey != "" {
54
+ req.Header.Set("X-Auth-Key", c.authKey)
55
+ }
56
+ if c.authSecret != "" {
57
+ req.Header.Set("X-Auth-Secret", c.authSecret)
58
+ }
59
+
60
+ resp, err := c.httpClient.Do(req)
61
+ if err != nil {
62
+ return nil, fmt.Errorf("request failed: %w", err)
63
+ }
64
+ if resp.StatusCode >= 400 {
65
+ defer resp.Body.Close()
66
+ bodyBytes, _ := io.ReadAll(resp.Body)
67
+ return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(bodyBytes))
68
+ }
69
+ return resp, nil
70
+ }
71
+
72
+ // HealthCheck pings the server
73
+ func (c *Client) HealthCheck() error {
74
+ resp, err := c.doRequest("GET", "/api/agent", nil)
75
+ if err != nil {
76
+ return err
77
+ }
78
+ resp.Body.Close()
79
+ return nil
80
+ }
81
+
82
+ // StartRequest is the payload for starting an agent
83
+ type StartRequest struct {
84
+ AgentConfig map[string]interface{} `json:"agentConfig,omitempty"`
85
+ Prompt string `json:"prompt"`
86
+ SessionID string `json:"sessionId,omitempty"`
87
+ }
88
+
89
+ // StartResponse from the runtime
90
+ type StartResponse struct {
91
+ WorkflowID string `json:"workflowId"`
92
+ WorkflowName string `json:"workflowName"`
93
+ }
94
+
95
+ // Start compiles, registers, and starts an agent workflow
96
+ func (c *Client) Start(req *StartRequest) (*StartResponse, error) {
97
+ resp, err := c.doRequest("POST", "/api/agent/start", req)
98
+ if err != nil {
99
+ return nil, err
100
+ }
101
+ defer resp.Body.Close()
102
+ var result StartResponse
103
+ if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
104
+ return nil, fmt.Errorf("decode response: %w", err)
105
+ }
106
+ return &result, nil
107
+ }
108
+
109
+ // Compile compiles an agent config to a workflow definition
110
+ func (c *Client) Compile(agentConfig map[string]interface{}) (map[string]interface{}, error) {
111
+ body := map[string]interface{}{"agentConfig": agentConfig}
112
+ resp, err := c.doRequest("POST", "/api/agent/compile", body)
113
+ if err != nil {
114
+ return nil, err
115
+ }
116
+ defer resp.Body.Close()
117
+ var result map[string]interface{}
118
+ if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
119
+ return nil, fmt.Errorf("decode response: %w", err)
120
+ }
121
+ return result, nil
122
+ }
123
+
124
+ // AgentSummary represents a registered agent
125
+ type AgentSummary struct {
126
+ Name string `json:"name"`
127
+ Version int `json:"version"`
128
+ Type string `json:"type"`
129
+ Tags []string `json:"tags"`
130
+ CreateTime *int64 `json:"createTime"`
131
+ UpdateTime *int64 `json:"updateTime"`
132
+ Description string `json:"description"`
133
+ Checksum string `json:"checksum"`
134
+ }
135
+
136
+ // ListAgents returns all registered agents
137
+ func (c *Client) ListAgents() ([]AgentSummary, error) {
138
+ resp, err := c.doRequest("GET", "/api/agent/list", nil)
139
+ if err != nil {
140
+ return nil, err
141
+ }
142
+ defer resp.Body.Close()
143
+ var result []AgentSummary
144
+ if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
145
+ return nil, fmt.Errorf("decode response: %w", err)
146
+ }
147
+ return result, nil
148
+ }
149
+
150
+ // GetAgent returns the workflow definition for a named agent
151
+ func (c *Client) GetAgent(name string, version *int) (map[string]interface{}, error) {
152
+ path := "/api/agent/get/" + url.PathEscape(name)
153
+ if version != nil {
154
+ path += fmt.Sprintf("?version=%d", *version)
155
+ }
156
+ resp, err := c.doRequest("GET", path, nil)
157
+ if err != nil {
158
+ return nil, err
159
+ }
160
+ defer resp.Body.Close()
161
+ var result map[string]interface{}
162
+ if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
163
+ return nil, fmt.Errorf("decode response: %w", err)
164
+ }
165
+ return result, nil
166
+ }
167
+
168
+ // DeleteAgent removes an agent workflow definition
169
+ func (c *Client) DeleteAgent(name string, version *int) error {
170
+ path := "/api/agent/delete/" + url.PathEscape(name)
171
+ if version != nil {
172
+ path += fmt.Sprintf("?version=%d", *version)
173
+ }
174
+ resp, err := c.doRequest("DELETE", path, nil)
175
+ if err != nil {
176
+ return err
177
+ }
178
+ resp.Body.Close()
179
+ return nil
180
+ }
181
+
182
+ // ExecutionSearchResult from the search endpoint
183
+ type ExecutionSearchResult struct {
184
+ TotalHits int64 `json:"totalHits"`
185
+ Results []AgentExecutionSummary `json:"results"`
186
+ }
187
+
188
+ // AgentExecutionSummary represents one execution in search results
189
+ type AgentExecutionSummary struct {
190
+ WorkflowID string `json:"workflowId"`
191
+ AgentName string `json:"agentName"`
192
+ Version int `json:"version"`
193
+ Status string `json:"status"`
194
+ StartTime string `json:"startTime"`
195
+ EndTime string `json:"endTime"`
196
+ UpdateTime string `json:"updateTime"`
197
+ ExecutionTime int64 `json:"executionTime"`
198
+ Input string `json:"input"`
199
+ Output string `json:"output"`
200
+ CreatedBy string `json:"createdBy"`
201
+ }
202
+
203
+ // SearchExecutions searches agent executions with optional filters
204
+ func (c *Client) SearchExecutions(start, size int, agentName, status, freeText string) (*ExecutionSearchResult, error) {
205
+ params := url.Values{}
206
+ params.Set("start", fmt.Sprintf("%d", start))
207
+ params.Set("size", fmt.Sprintf("%d", size))
208
+ params.Set("sort", "startTime:DESC")
209
+ if agentName != "" {
210
+ params.Set("agentName", agentName)
211
+ }
212
+ if status != "" {
213
+ params.Set("status", status)
214
+ }
215
+ if freeText != "" {
216
+ params.Set("freeText", freeText)
217
+ }
218
+ resp, err := c.doRequest("GET", "/api/agent/executions?"+params.Encode(), nil)
219
+ if err != nil {
220
+ return nil, err
221
+ }
222
+ defer resp.Body.Close()
223
+ var result ExecutionSearchResult
224
+ if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
225
+ return nil, fmt.Errorf("decode response: %w", err)
226
+ }
227
+ return &result, nil
228
+ }
229
+
230
+ // ExecutionDetail represents detailed execution status
231
+ type ExecutionDetail struct {
232
+ WorkflowID string `json:"workflowId"`
233
+ AgentName string `json:"agentName"`
234
+ Version int `json:"version"`
235
+ Status string `json:"status"`
236
+ Input map[string]interface{} `json:"input"`
237
+ Output map[string]interface{} `json:"output"`
238
+ CurrentTask *CurrentTask `json:"currentTask"`
239
+ }
240
+
241
+ type CurrentTask struct {
242
+ TaskRefName string `json:"taskRefName"`
243
+ TaskType string `json:"taskType"`
244
+ Status string `json:"status"`
245
+ InputData map[string]interface{} `json:"inputData"`
246
+ OutputData map[string]interface{} `json:"outputData"`
247
+ }
248
+
249
+ // GetExecutionDetail returns detailed status for an execution
250
+ func (c *Client) GetExecutionDetail(executionId string) (*ExecutionDetail, error) {
251
+ resp, err := c.doRequest("GET", "/api/agent/executions/"+url.PathEscape(executionId), nil)
252
+ if err != nil {
253
+ return nil, err
254
+ }
255
+ defer resp.Body.Close()
256
+ var result ExecutionDetail
257
+ if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
258
+ return nil, fmt.Errorf("decode response: %w", err)
259
+ }
260
+ return &result, nil
261
+ }
262
+
263
+ // Status gets the workflow execution status (legacy endpoint)
264
+ func (c *Client) Status(workflowID string) (map[string]interface{}, error) {
265
+ resp, err := c.doRequest("GET", "/api/agent/"+workflowID+"/status", nil)
266
+ if err != nil {
267
+ return nil, err
268
+ }
269
+ defer resp.Body.Close()
270
+ var result map[string]interface{}
271
+ if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
272
+ return nil, fmt.Errorf("decode response: %w", err)
273
+ }
274
+ return result, nil
275
+ }
276
+
277
+ // Respond sends a HITL response
278
+ func (c *Client) Respond(workflowID string, approved bool, reason, message string) error {
279
+ body := map[string]interface{}{
280
+ "approved": approved,
281
+ "reason": reason,
282
+ "message": message,
283
+ }
284
+ resp, err := c.doRequest("POST", "/api/agent/"+workflowID+"/respond", body)
285
+ if err != nil {
286
+ return err
287
+ }
288
+ resp.Body.Close()
289
+ return nil
290
+ }
291
+
292
+ // SSEEvent represents a server-sent event
293
+ type SSEEvent struct {
294
+ ID string
295
+ Event string
296
+ Data string
297
+ }
298
+
299
+ // Stream opens an SSE connection and sends events to the channel
300
+ func (c *Client) Stream(workflowID string, lastEventID string, events chan<- SSEEvent, done chan<- error) {
301
+ go func() {
302
+ defer close(events)
303
+ defer close(done)
304
+
305
+ streamClient := &http.Client{Timeout: 0} // no timeout for SSE
306
+
307
+ req, err := http.NewRequest("GET", c.baseURL+"/api/agent/stream/"+workflowID, nil)
308
+ if err != nil {
309
+ done <- err
310
+ return
311
+ }
312
+ req.Header.Set("Accept", "text/event-stream")
313
+ if lastEventID != "" {
314
+ req.Header.Set("Last-Event-ID", lastEventID)
315
+ }
316
+ if c.authKey != "" {
317
+ req.Header.Set("X-Auth-Key", c.authKey)
318
+ }
319
+
320
+ resp, err := streamClient.Do(req)
321
+ if err != nil {
322
+ done <- err
323
+ return
324
+ }
325
+ defer resp.Body.Close()
326
+
327
+ if resp.StatusCode >= 400 {
328
+ body, _ := io.ReadAll(resp.Body)
329
+ done <- fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body))
330
+ return
331
+ }
332
+
333
+ scanner := bufio.NewScanner(resp.Body)
334
+ scanner.Buffer(make([]byte, 0, 1024*1024), 1024*1024)
335
+
336
+ var current SSEEvent
337
+ for scanner.Scan() {
338
+ line := scanner.Text()
339
+
340
+ if line == "" {
341
+ // Empty line = end of event
342
+ if current.Data != "" || current.Event != "" {
343
+ events <- current
344
+ current = SSEEvent{}
345
+ }
346
+ continue
347
+ }
348
+
349
+ if strings.HasPrefix(line, ":") {
350
+ // Comment (heartbeat), skip
351
+ continue
352
+ }
353
+
354
+ if strings.HasPrefix(line, "id:") {
355
+ current.ID = strings.TrimSpace(strings.TrimPrefix(line, "id:"))
356
+ } else if strings.HasPrefix(line, "event:") {
357
+ current.Event = strings.TrimSpace(strings.TrimPrefix(line, "event:"))
358
+ } else if strings.HasPrefix(line, "data:") {
359
+ current.Data = strings.TrimSpace(strings.TrimPrefix(line, "data:"))
360
+ }
361
+ }
362
+
363
+ done <- scanner.Err()
364
+ }()
365
+ }