@sooneocean/agw 1.4.0
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 +116 -0
- package/bin/agw.ts +5 -0
- package/package.json +59 -0
- package/src/agents/base-adapter.ts +113 -0
- package/src/agents/claude-adapter.ts +29 -0
- package/src/agents/codex-adapter.ts +29 -0
- package/src/agents/gemini-adapter.ts +29 -0
- package/src/cli/commands/agents.ts +55 -0
- package/src/cli/commands/combo.ts +130 -0
- package/src/cli/commands/costs.ts +33 -0
- package/src/cli/commands/daemon.ts +110 -0
- package/src/cli/commands/history.ts +29 -0
- package/src/cli/commands/run.ts +59 -0
- package/src/cli/commands/status.ts +29 -0
- package/src/cli/commands/workflow.ts +73 -0
- package/src/cli/error-handler.ts +8 -0
- package/src/cli/http-client.ts +68 -0
- package/src/cli/index.ts +28 -0
- package/src/config.ts +68 -0
- package/src/daemon/middleware/auth.ts +35 -0
- package/src/daemon/middleware/rate-limiter.ts +63 -0
- package/src/daemon/middleware/tenant.ts +64 -0
- package/src/daemon/middleware/workspace.ts +40 -0
- package/src/daemon/routes/agents.ts +13 -0
- package/src/daemon/routes/combos.ts +103 -0
- package/src/daemon/routes/costs.ts +9 -0
- package/src/daemon/routes/health.ts +62 -0
- package/src/daemon/routes/memory.ts +32 -0
- package/src/daemon/routes/tasks.ts +157 -0
- package/src/daemon/routes/ui.ts +18 -0
- package/src/daemon/routes/workflows.ts +73 -0
- package/src/daemon/server.ts +91 -0
- package/src/daemon/services/agent-learning.ts +77 -0
- package/src/daemon/services/agent-manager.ts +71 -0
- package/src/daemon/services/auto-scaler.ts +77 -0
- package/src/daemon/services/circuit-breaker.ts +95 -0
- package/src/daemon/services/combo-executor.ts +300 -0
- package/src/daemon/services/dag-executor.ts +136 -0
- package/src/daemon/services/metrics.ts +35 -0
- package/src/daemon/services/stream-aggregator.ts +64 -0
- package/src/daemon/services/task-executor.ts +184 -0
- package/src/daemon/services/task-queue.ts +75 -0
- package/src/daemon/services/workflow-executor.ts +150 -0
- package/src/daemon/services/ws-manager.ts +90 -0
- package/src/dsl/parser.ts +124 -0
- package/src/plugins/plugin-loader.ts +72 -0
- package/src/router/keyword-router.ts +63 -0
- package/src/router/llm-router.ts +93 -0
- package/src/store/agent-repo.ts +57 -0
- package/src/store/audit-repo.ts +25 -0
- package/src/store/combo-repo.ts +99 -0
- package/src/store/cost-repo.ts +55 -0
- package/src/store/db.ts +137 -0
- package/src/store/memory-repo.ts +46 -0
- package/src/store/task-repo.ts +127 -0
- package/src/store/workflow-repo.ts +69 -0
- package/src/types.ts +208 -0
- package/tsconfig.json +17 -0
- package/ui/index.html +272 -0
package/README.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# AGW — Agent Gateway
|
|
2
|
+
|
|
3
|
+
Multi-agent task router and executor for **Claude Code**, **Codex CLI**, and **Gemini CLI**.
|
|
4
|
+
|
|
5
|
+
Submit a task → AGW picks the best agent → agent executes → you get results + logs + cost data.
|
|
6
|
+
|
|
7
|
+
## Quick Start
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# Install dependencies
|
|
11
|
+
npm install
|
|
12
|
+
|
|
13
|
+
# Start the daemon
|
|
14
|
+
npx agw daemon start
|
|
15
|
+
|
|
16
|
+
# Run a task
|
|
17
|
+
npx agw run "refactor auth.ts"
|
|
18
|
+
|
|
19
|
+
# Run with specific agent
|
|
20
|
+
npx agw run "quick rename" --agent codex --priority 5
|
|
21
|
+
|
|
22
|
+
# Check costs
|
|
23
|
+
npx agw costs
|
|
24
|
+
|
|
25
|
+
# Multi-step workflow
|
|
26
|
+
npx agw workflow run '{"name":"deploy","steps":[{"prompt":"run tests"},{"prompt":"build"}]}'
|
|
27
|
+
|
|
28
|
+
# Open Web UI
|
|
29
|
+
open http://127.0.0.1:4927/ui
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Features
|
|
33
|
+
|
|
34
|
+
| Feature | Description |
|
|
35
|
+
|---------|-------------|
|
|
36
|
+
| **Smart Routing** | LLM classifier (Haiku) + keyword fallback |
|
|
37
|
+
| **3 Agents** | Claude (complex reasoning), Codex (terminal ops), Gemini (research) |
|
|
38
|
+
| **Auth** | Bearer token via `AGW_AUTH_TOKEN`, loopback-only without token |
|
|
39
|
+
| **Priority Queue** | 1-5 priority with per-agent concurrency limits |
|
|
40
|
+
| **Cost Tracking** | Per-task cost recording, daily/monthly quotas |
|
|
41
|
+
| **Workflows** | Sequential or parallel multi-step task chains |
|
|
42
|
+
| **Web UI** | Real-time dashboard at `/ui` |
|
|
43
|
+
| **SSE Streaming** | Live stdout/stderr via `/tasks/:id/stream` |
|
|
44
|
+
| **Workspace Sandbox** | `allowedWorkspaces` whitelist + realpath validation |
|
|
45
|
+
| **Stdin Prompts** | Prompt delivery via stdin (no argv leakage) |
|
|
46
|
+
|
|
47
|
+
## Configuration
|
|
48
|
+
|
|
49
|
+
Config file: `~/.agw/config.json`
|
|
50
|
+
|
|
51
|
+
```json
|
|
52
|
+
{
|
|
53
|
+
"port": 4927,
|
|
54
|
+
"authToken": "your-secret-token",
|
|
55
|
+
"allowedWorkspaces": ["/home/user/projects"],
|
|
56
|
+
"maxConcurrencyPerAgent": 3,
|
|
57
|
+
"dailyCostLimit": 5.00,
|
|
58
|
+
"monthlyCostLimit": 50.00,
|
|
59
|
+
"maxPromptLength": 100000,
|
|
60
|
+
"maxWorkflowSteps": 20,
|
|
61
|
+
"agents": {
|
|
62
|
+
"claude": { "enabled": true, "command": "claude", "args": [] },
|
|
63
|
+
"codex": { "enabled": true, "command": "codex", "args": [] },
|
|
64
|
+
"gemini": { "enabled": false, "command": "gemini", "args": [] }
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Environment variables (override config file):
|
|
70
|
+
- `AGW_PORT` — server port
|
|
71
|
+
- `AGW_AUTH_TOKEN` — Bearer token
|
|
72
|
+
- `ANTHROPIC_API_KEY` — for LLM routing
|
|
73
|
+
|
|
74
|
+
## API
|
|
75
|
+
|
|
76
|
+
| Method | Path | Description |
|
|
77
|
+
|--------|------|-------------|
|
|
78
|
+
| POST | `/tasks` | Create and execute a task |
|
|
79
|
+
| GET | `/tasks/:id` | Get task details |
|
|
80
|
+
| GET | `/tasks/:id/stream` | SSE stream (stdout/stderr/done) |
|
|
81
|
+
| GET | `/tasks` | List tasks |
|
|
82
|
+
| POST | `/workflows` | Create workflow (returns 202) |
|
|
83
|
+
| GET | `/workflows/:id` | Get workflow status |
|
|
84
|
+
| GET | `/workflows` | List workflows |
|
|
85
|
+
| GET | `/agents` | List agents |
|
|
86
|
+
| POST | `/agents/:id/health` | Trigger health check |
|
|
87
|
+
| GET | `/costs` | Cost summary |
|
|
88
|
+
| GET | `/ui` | Web dashboard |
|
|
89
|
+
|
|
90
|
+
## CLI Commands
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
agw run <prompt> Submit a task (--agent, --priority, --background, --cwd)
|
|
94
|
+
agw status <taskId> Check task status
|
|
95
|
+
agw history List recent tasks (--limit N)
|
|
96
|
+
agw agents List agents / agw agents check
|
|
97
|
+
agw daemon start|stop|status
|
|
98
|
+
agw costs Show cost summary
|
|
99
|
+
agw workflow run|status|list
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Development
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
npm test # Run 76 tests
|
|
106
|
+
npm run build # TypeScript compile
|
|
107
|
+
npm run dev # Start dev server
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Tech Stack
|
|
111
|
+
|
|
112
|
+
TypeScript, Fastify, SQLite (better-sqlite3), Commander.js, Node.js 22+
|
|
113
|
+
|
|
114
|
+
## License
|
|
115
|
+
|
|
116
|
+
MIT
|
package/bin/agw.ts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sooneocean/agw",
|
|
3
|
+
"version": "1.4.0",
|
|
4
|
+
"description": "Agent Gateway — multi-agent task router for Claude, Codex, Gemini with combos, DAG, DSL",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"agw": "./bin/agw.ts"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"dev": "tsx src/daemon/server.ts",
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"test": "vitest run",
|
|
13
|
+
"test:watch": "vitest"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"agent",
|
|
17
|
+
"gateway",
|
|
18
|
+
"multi-agent",
|
|
19
|
+
"claude",
|
|
20
|
+
"codex",
|
|
21
|
+
"gemini",
|
|
22
|
+
"ai",
|
|
23
|
+
"orchestration",
|
|
24
|
+
"combo",
|
|
25
|
+
"pipeline",
|
|
26
|
+
"dsl"
|
|
27
|
+
],
|
|
28
|
+
"author": "sooneocean",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "https://github.com/sooneocean/agw.git"
|
|
33
|
+
},
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=22.0.0"
|
|
36
|
+
},
|
|
37
|
+
"files": [
|
|
38
|
+
"bin/",
|
|
39
|
+
"src/",
|
|
40
|
+
"ui/",
|
|
41
|
+
"package.json",
|
|
42
|
+
"tsconfig.json",
|
|
43
|
+
"README.md"
|
|
44
|
+
],
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@anthropic-ai/sdk": "^0.52.0",
|
|
47
|
+
"better-sqlite3": "^11.8.0",
|
|
48
|
+
"commander": "^13.1.0",
|
|
49
|
+
"fastify": "^5.2.0",
|
|
50
|
+
"nanoid": "^5.1.0",
|
|
51
|
+
"tsx": "^4.19.0"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
55
|
+
"@types/node": "^22.13.0",
|
|
56
|
+
"typescript": "^5.7.0",
|
|
57
|
+
"vitest": "^3.0.0"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { spawn, exec } from 'node:child_process';
|
|
2
|
+
import { EventEmitter } from 'node:events';
|
|
3
|
+
import { promisify } from 'node:util';
|
|
4
|
+
import type { TaskDescriptor, TaskResult, AgentDescriptor, UnifiedAgent } from '../types.js';
|
|
5
|
+
|
|
6
|
+
const execAsync = promisify(exec);
|
|
7
|
+
|
|
8
|
+
export abstract class BaseAdapter extends EventEmitter implements UnifiedAgent {
|
|
9
|
+
constructor(
|
|
10
|
+
protected timeout: number,
|
|
11
|
+
protected maxBufferSize: number,
|
|
12
|
+
protected commandOverride?: string,
|
|
13
|
+
) {
|
|
14
|
+
super();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
abstract describe(): AgentDescriptor;
|
|
18
|
+
protected abstract buildArgs(task: TaskDescriptor): string[];
|
|
19
|
+
|
|
20
|
+
/** Whether this adapter sends the prompt via stdin instead of argv. Override to return true. */
|
|
21
|
+
protected useStdin(): boolean {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async execute(task: TaskDescriptor): Promise<TaskResult> {
|
|
26
|
+
const descriptor = this.describe();
|
|
27
|
+
const args = this.buildArgs(task);
|
|
28
|
+
const start = Date.now();
|
|
29
|
+
const useStdin = this.useStdin();
|
|
30
|
+
|
|
31
|
+
return new Promise<TaskResult>((resolve) => {
|
|
32
|
+
let stdout = '';
|
|
33
|
+
let stderr = '';
|
|
34
|
+
let stdoutTruncated = false;
|
|
35
|
+
let stderrTruncated = false;
|
|
36
|
+
let killed = false;
|
|
37
|
+
|
|
38
|
+
const command = this.commandOverride ?? descriptor.command;
|
|
39
|
+
const proc = spawn(command, args, {
|
|
40
|
+
cwd: task.workingDirectory,
|
|
41
|
+
stdio: [useStdin ? 'pipe' : 'ignore', 'pipe', 'pipe'],
|
|
42
|
+
timeout: this.timeout,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Send prompt via stdin if supported (avoids leaking prompt in ps/argv)
|
|
46
|
+
if (useStdin && proc.stdin) {
|
|
47
|
+
proc.stdin.write(task.prompt);
|
|
48
|
+
proc.stdin.end();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
proc.stdout!.on('data', (chunk: Buffer) => {
|
|
52
|
+
const text = chunk.toString();
|
|
53
|
+
this.emit('stdout', text);
|
|
54
|
+
if (stdout.length < this.maxBufferSize) {
|
|
55
|
+
stdout += text;
|
|
56
|
+
if (stdout.length > this.maxBufferSize) {
|
|
57
|
+
stdout = stdout.slice(stdout.length - this.maxBufferSize);
|
|
58
|
+
stdoutTruncated = true;
|
|
59
|
+
}
|
|
60
|
+
} else {
|
|
61
|
+
stdoutTruncated = true;
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
proc.stderr!.on('data', (chunk: Buffer) => {
|
|
66
|
+
const text = chunk.toString();
|
|
67
|
+
this.emit('stderr', text);
|
|
68
|
+
if (stderr.length < this.maxBufferSize) {
|
|
69
|
+
stderr += text;
|
|
70
|
+
if (stderr.length > this.maxBufferSize) {
|
|
71
|
+
stderr = stderr.slice(stderr.length - this.maxBufferSize);
|
|
72
|
+
stderrTruncated = true;
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
stderrTruncated = true;
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
proc.on('error', (err) => {
|
|
80
|
+
resolve({
|
|
81
|
+
exitCode: 1,
|
|
82
|
+
stdout,
|
|
83
|
+
stderr: stderr + '\n' + err.message,
|
|
84
|
+
stdoutTruncated,
|
|
85
|
+
stderrTruncated,
|
|
86
|
+
durationMs: Date.now() - start,
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
proc.on('close', (code, signal) => {
|
|
91
|
+
if (signal === 'SIGTERM') killed = true;
|
|
92
|
+
resolve({
|
|
93
|
+
exitCode: code ?? (killed ? 137 : 1),
|
|
94
|
+
stdout,
|
|
95
|
+
stderr,
|
|
96
|
+
stdoutTruncated,
|
|
97
|
+
stderrTruncated,
|
|
98
|
+
durationMs: Date.now() - start,
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async healthCheck(): Promise<boolean> {
|
|
105
|
+
const descriptor = this.describe();
|
|
106
|
+
try {
|
|
107
|
+
await execAsync(descriptor.healthCheckCommand, { timeout: 10_000 });
|
|
108
|
+
return true;
|
|
109
|
+
} catch {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { BaseAdapter } from './base-adapter.js';
|
|
2
|
+
import type { TaskDescriptor, AgentDescriptor } from '../types.js';
|
|
3
|
+
|
|
4
|
+
export class ClaudeAdapter extends BaseAdapter {
|
|
5
|
+
constructor(timeout: number, maxBufferSize: number, private extraArgs: string[] = [], commandOverride?: string) {
|
|
6
|
+
super(timeout, maxBufferSize, commandOverride);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe(): AgentDescriptor {
|
|
10
|
+
return {
|
|
11
|
+
id: 'claude',
|
|
12
|
+
name: 'Claude Code',
|
|
13
|
+
command: 'claude',
|
|
14
|
+
args: ['--print', ...this.extraArgs],
|
|
15
|
+
enabled: true,
|
|
16
|
+
available: true,
|
|
17
|
+
healthCheckCommand: 'claude --version',
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
protected useStdin(): boolean {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
protected buildArgs(_task: TaskDescriptor): string[] {
|
|
26
|
+
// Prompt is sent via stdin, not argv (prevents ps/argv leakage)
|
|
27
|
+
return ['--print', '--output-format', 'json', ...this.extraArgs, '-'];
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { BaseAdapter } from './base-adapter.js';
|
|
2
|
+
import type { TaskDescriptor, AgentDescriptor } from '../types.js';
|
|
3
|
+
|
|
4
|
+
export class CodexAdapter extends BaseAdapter {
|
|
5
|
+
constructor(timeout: number, maxBufferSize: number, private extraArgs: string[] = [], commandOverride?: string) {
|
|
6
|
+
super(timeout, maxBufferSize, commandOverride);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe(): AgentDescriptor {
|
|
10
|
+
return {
|
|
11
|
+
id: 'codex',
|
|
12
|
+
name: 'Codex CLI',
|
|
13
|
+
command: 'codex',
|
|
14
|
+
args: ['exec', ...this.extraArgs],
|
|
15
|
+
enabled: true,
|
|
16
|
+
available: true,
|
|
17
|
+
healthCheckCommand: 'codex --version',
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
protected useStdin(): boolean {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
protected buildArgs(_task: TaskDescriptor): string[] {
|
|
26
|
+
// Prompt is sent via stdin (reads from stdin when prompt arg is "-")
|
|
27
|
+
return ['exec', ...this.extraArgs, '-'];
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { BaseAdapter } from './base-adapter.js';
|
|
2
|
+
import type { TaskDescriptor, AgentDescriptor } from '../types.js';
|
|
3
|
+
|
|
4
|
+
export class GeminiAdapter extends BaseAdapter {
|
|
5
|
+
constructor(timeout: number, maxBufferSize: number, private extraArgs: string[] = [], commandOverride?: string) {
|
|
6
|
+
super(timeout, maxBufferSize, commandOverride);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe(): AgentDescriptor {
|
|
10
|
+
return {
|
|
11
|
+
id: 'gemini',
|
|
12
|
+
name: 'Gemini CLI',
|
|
13
|
+
command: 'gemini',
|
|
14
|
+
args: [...this.extraArgs],
|
|
15
|
+
enabled: true,
|
|
16
|
+
available: true,
|
|
17
|
+
healthCheckCommand: 'gemini --version',
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
protected useStdin(): boolean {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
protected buildArgs(_task: TaskDescriptor): string[] {
|
|
26
|
+
// Prompt via stdin
|
|
27
|
+
return [...this.extraArgs, '-'];
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
import { HttpClient } from '../http-client.js';
|
|
3
|
+
import type { AgentDescriptor } from '../../types.js';
|
|
4
|
+
|
|
5
|
+
export function registerAgentsCommand(program: Command): void {
|
|
6
|
+
const cmd = program
|
|
7
|
+
.command('agents')
|
|
8
|
+
.description('List agents and health status');
|
|
9
|
+
|
|
10
|
+
cmd.action(async () => {
|
|
11
|
+
const client = new HttpClient();
|
|
12
|
+
try {
|
|
13
|
+
const agents = await client.get<AgentDescriptor[]>('/agents');
|
|
14
|
+
console.log('Agent Status Last Check');
|
|
15
|
+
console.log('─'.repeat(40));
|
|
16
|
+
for (const a of agents) {
|
|
17
|
+
const status = !a.enabled ? '- Disabled' : a.available ? '✓ Ready' : '✗ N/A';
|
|
18
|
+
const lastCheck = a.lastHealthCheck
|
|
19
|
+
? timeSince(new Date(a.lastHealthCheck))
|
|
20
|
+
: 'never';
|
|
21
|
+
console.log(`${a.id.padEnd(9)} ${status.padEnd(11)} ${lastCheck}`);
|
|
22
|
+
}
|
|
23
|
+
} catch (err) {
|
|
24
|
+
console.error(`Error: ${(err as Error).message}`);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
cmd
|
|
30
|
+
.command('check')
|
|
31
|
+
.description('Trigger health checks for all agents')
|
|
32
|
+
.action(async () => {
|
|
33
|
+
const client = new HttpClient();
|
|
34
|
+
try {
|
|
35
|
+
const agents = await client.get<AgentDescriptor[]>('/agents');
|
|
36
|
+
for (const a of agents) {
|
|
37
|
+
if (!a.enabled) continue;
|
|
38
|
+
const result = await client.post<{ id: string; available: boolean }>(`/agents/${a.id}/health`, {});
|
|
39
|
+
console.log(`${a.id}: ${result.available ? '✓ available' : '✗ unavailable'}`);
|
|
40
|
+
}
|
|
41
|
+
} catch (err) {
|
|
42
|
+
console.error(`Error: ${(err as Error).message}`);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function timeSince(date: Date): string {
|
|
49
|
+
const seconds = Math.floor((Date.now() - date.getTime()) / 1000);
|
|
50
|
+
if (seconds < 60) return `${seconds}s ago`;
|
|
51
|
+
const minutes = Math.floor(seconds / 60);
|
|
52
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
53
|
+
const hours = Math.floor(minutes / 60);
|
|
54
|
+
return `${hours}h ago`;
|
|
55
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
import { HttpClient } from '../http-client.js';
|
|
3
|
+
import type { ComboDescriptor, ComboPreset } from '../../types.js';
|
|
4
|
+
|
|
5
|
+
export function registerComboCommand(program: Command): void {
|
|
6
|
+
const combo = program
|
|
7
|
+
.command('combo')
|
|
8
|
+
.description('Multi-agent combo moves — agents collaborate and pass context');
|
|
9
|
+
|
|
10
|
+
// List presets
|
|
11
|
+
combo.command('presets')
|
|
12
|
+
.description('List built-in combo presets')
|
|
13
|
+
.action(async () => {
|
|
14
|
+
const client = new HttpClient();
|
|
15
|
+
try {
|
|
16
|
+
const presets = await client.get<ComboPreset[]>('/combos/presets');
|
|
17
|
+
console.log('Available Combo Presets:');
|
|
18
|
+
console.log('─'.repeat(60));
|
|
19
|
+
for (const p of presets) {
|
|
20
|
+
console.log(` ${p.id.padEnd(25)} ${p.pattern.padEnd(14)} ${p.description}`);
|
|
21
|
+
}
|
|
22
|
+
} catch (err) {
|
|
23
|
+
console.error(`Error: ${(err as Error).message}`);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Run a preset
|
|
29
|
+
combo.command('preset <presetId> <input...>')
|
|
30
|
+
.description('Run a built-in combo preset')
|
|
31
|
+
.option('--cwd <path>', 'Working directory')
|
|
32
|
+
.option('--priority <n>', 'Priority 1-5', '3')
|
|
33
|
+
.action(async (presetId: string, inputParts: string[], options: { cwd?: string; priority?: string }) => {
|
|
34
|
+
const client = new HttpClient();
|
|
35
|
+
try {
|
|
36
|
+
const combo = await client.post<ComboDescriptor>(`/combos/preset/${presetId}`, {
|
|
37
|
+
input: inputParts.join(' '),
|
|
38
|
+
workingDirectory: options.cwd,
|
|
39
|
+
priority: parseInt(options.priority ?? '3', 10),
|
|
40
|
+
});
|
|
41
|
+
console.log(`Combo: ${combo.comboId} ${combo.name}`);
|
|
42
|
+
console.log(`Pattern: ${combo.pattern}`);
|
|
43
|
+
console.log(`Status: ${combo.status}`);
|
|
44
|
+
console.log(`Steps: ${combo.steps.length}`);
|
|
45
|
+
console.log(`\nCheck progress: agw combo status ${combo.comboId}`);
|
|
46
|
+
} catch (err) {
|
|
47
|
+
console.error(`Error: ${(err as Error).message}`);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Run custom combo
|
|
53
|
+
combo.command('run')
|
|
54
|
+
.description('Run a custom combo from JSON')
|
|
55
|
+
.argument('<json>', 'Combo JSON')
|
|
56
|
+
.action(async (json: string) => {
|
|
57
|
+
const client = new HttpClient();
|
|
58
|
+
try {
|
|
59
|
+
const body = JSON.parse(json);
|
|
60
|
+
const combo = await client.post<ComboDescriptor>('/combos', body);
|
|
61
|
+
console.log(`Combo: ${combo.comboId} ${combo.name}`);
|
|
62
|
+
console.log(`Pattern: ${combo.pattern}`);
|
|
63
|
+
console.log(`Status: ${combo.status}`);
|
|
64
|
+
console.log(`\nCheck progress: agw combo status ${combo.comboId}`);
|
|
65
|
+
} catch (err) {
|
|
66
|
+
console.error(`Error: ${(err as Error).message}`);
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Check combo status
|
|
72
|
+
combo.command('status <id>')
|
|
73
|
+
.description('Get combo status and results')
|
|
74
|
+
.action(async (id: string) => {
|
|
75
|
+
const client = new HttpClient();
|
|
76
|
+
try {
|
|
77
|
+
const c = await client.get<ComboDescriptor>(`/combos/${id}`);
|
|
78
|
+
console.log(`Combo: ${c.comboId}`);
|
|
79
|
+
console.log(`Name: ${c.name}`);
|
|
80
|
+
console.log(`Pattern: ${c.pattern}`);
|
|
81
|
+
console.log(`Status: ${c.status}`);
|
|
82
|
+
if (c.iterations) console.log(`Iterations: ${c.iterations}/${c.maxIterations}`);
|
|
83
|
+
console.log(`Tasks: ${c.taskIds.join(', ') || 'none'}`);
|
|
84
|
+
|
|
85
|
+
// Show step results
|
|
86
|
+
const stepEntries = Object.entries(c.stepResults);
|
|
87
|
+
if (stepEntries.length > 0) {
|
|
88
|
+
console.log('\n─── Step Results ───');
|
|
89
|
+
for (const [idx, output] of stepEntries) {
|
|
90
|
+
const step = c.steps[parseInt(idx, 10)];
|
|
91
|
+
const label = step?.role ?? step?.agent ?? `step ${idx}`;
|
|
92
|
+
console.log(`\n[${label}] (${step?.agent}):`);
|
|
93
|
+
const trimmed = output.length > 500 ? output.slice(0, 500) + '...' : output;
|
|
94
|
+
console.log(trimmed);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (c.finalOutput) {
|
|
99
|
+
console.log('\n─── Final Output ───');
|
|
100
|
+
console.log(c.finalOutput);
|
|
101
|
+
}
|
|
102
|
+
} catch (err) {
|
|
103
|
+
console.error(`Error: ${(err as Error).message}`);
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// List combos
|
|
109
|
+
combo.command('list')
|
|
110
|
+
.description('List recent combos')
|
|
111
|
+
.option('--limit <n>', 'Number of combos', '20')
|
|
112
|
+
.action(async (options: { limit?: string }) => {
|
|
113
|
+
const client = new HttpClient();
|
|
114
|
+
try {
|
|
115
|
+
const combos = await client.get<ComboDescriptor[]>(`/combos?limit=${options.limit ?? '20'}`);
|
|
116
|
+
if (combos.length === 0) {
|
|
117
|
+
console.log('No combos found.');
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
console.log('ID Pattern Status Name');
|
|
121
|
+
console.log('─'.repeat(60));
|
|
122
|
+
for (const c of combos) {
|
|
123
|
+
console.log(`${c.comboId} ${c.pattern.padEnd(14)} ${c.status.padEnd(10)} ${c.name}`);
|
|
124
|
+
}
|
|
125
|
+
} catch (err) {
|
|
126
|
+
console.error(`Error: ${(err as Error).message}`);
|
|
127
|
+
process.exit(1);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
import { HttpClient } from '../http-client.js';
|
|
3
|
+
import type { CostSummary } from '../../types.js';
|
|
4
|
+
|
|
5
|
+
export function registerCostsCommand(program: Command): void {
|
|
6
|
+
program
|
|
7
|
+
.command('costs')
|
|
8
|
+
.description('Show cost summary')
|
|
9
|
+
.action(async () => {
|
|
10
|
+
const client = new HttpClient();
|
|
11
|
+
try {
|
|
12
|
+
const costs = await client.get<CostSummary>('/costs');
|
|
13
|
+
console.log('Cost Summary');
|
|
14
|
+
console.log('─'.repeat(40));
|
|
15
|
+
console.log(` Daily: $${costs.daily.toFixed(2)}${costs.dailyLimit ? ` / $${costs.dailyLimit.toFixed(2)}` : ''}`);
|
|
16
|
+
console.log(` Monthly: $${costs.monthly.toFixed(2)}${costs.monthlyLimit ? ` / $${costs.monthlyLimit.toFixed(2)}` : ''}`);
|
|
17
|
+
console.log(` All Time: $${costs.allTime.toFixed(2)}`);
|
|
18
|
+
console.log('');
|
|
19
|
+
console.log('By Agent:');
|
|
20
|
+
const agents = Object.entries(costs.byAgent);
|
|
21
|
+
if (agents.length === 0) {
|
|
22
|
+
console.log(' (no cost data yet)');
|
|
23
|
+
} else {
|
|
24
|
+
for (const [id, cost] of agents) {
|
|
25
|
+
console.log(` ${id.padEnd(10)} $${cost.toFixed(2)}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
} catch (err) {
|
|
29
|
+
console.error(`Error: ${(err as Error).message}`);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
}
|