@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.
Files changed (59) hide show
  1. package/README.md +116 -0
  2. package/bin/agw.ts +5 -0
  3. package/package.json +59 -0
  4. package/src/agents/base-adapter.ts +113 -0
  5. package/src/agents/claude-adapter.ts +29 -0
  6. package/src/agents/codex-adapter.ts +29 -0
  7. package/src/agents/gemini-adapter.ts +29 -0
  8. package/src/cli/commands/agents.ts +55 -0
  9. package/src/cli/commands/combo.ts +130 -0
  10. package/src/cli/commands/costs.ts +33 -0
  11. package/src/cli/commands/daemon.ts +110 -0
  12. package/src/cli/commands/history.ts +29 -0
  13. package/src/cli/commands/run.ts +59 -0
  14. package/src/cli/commands/status.ts +29 -0
  15. package/src/cli/commands/workflow.ts +73 -0
  16. package/src/cli/error-handler.ts +8 -0
  17. package/src/cli/http-client.ts +68 -0
  18. package/src/cli/index.ts +28 -0
  19. package/src/config.ts +68 -0
  20. package/src/daemon/middleware/auth.ts +35 -0
  21. package/src/daemon/middleware/rate-limiter.ts +63 -0
  22. package/src/daemon/middleware/tenant.ts +64 -0
  23. package/src/daemon/middleware/workspace.ts +40 -0
  24. package/src/daemon/routes/agents.ts +13 -0
  25. package/src/daemon/routes/combos.ts +103 -0
  26. package/src/daemon/routes/costs.ts +9 -0
  27. package/src/daemon/routes/health.ts +62 -0
  28. package/src/daemon/routes/memory.ts +32 -0
  29. package/src/daemon/routes/tasks.ts +157 -0
  30. package/src/daemon/routes/ui.ts +18 -0
  31. package/src/daemon/routes/workflows.ts +73 -0
  32. package/src/daemon/server.ts +91 -0
  33. package/src/daemon/services/agent-learning.ts +77 -0
  34. package/src/daemon/services/agent-manager.ts +71 -0
  35. package/src/daemon/services/auto-scaler.ts +77 -0
  36. package/src/daemon/services/circuit-breaker.ts +95 -0
  37. package/src/daemon/services/combo-executor.ts +300 -0
  38. package/src/daemon/services/dag-executor.ts +136 -0
  39. package/src/daemon/services/metrics.ts +35 -0
  40. package/src/daemon/services/stream-aggregator.ts +64 -0
  41. package/src/daemon/services/task-executor.ts +184 -0
  42. package/src/daemon/services/task-queue.ts +75 -0
  43. package/src/daemon/services/workflow-executor.ts +150 -0
  44. package/src/daemon/services/ws-manager.ts +90 -0
  45. package/src/dsl/parser.ts +124 -0
  46. package/src/plugins/plugin-loader.ts +72 -0
  47. package/src/router/keyword-router.ts +63 -0
  48. package/src/router/llm-router.ts +93 -0
  49. package/src/store/agent-repo.ts +57 -0
  50. package/src/store/audit-repo.ts +25 -0
  51. package/src/store/combo-repo.ts +99 -0
  52. package/src/store/cost-repo.ts +55 -0
  53. package/src/store/db.ts +137 -0
  54. package/src/store/memory-repo.ts +46 -0
  55. package/src/store/task-repo.ts +127 -0
  56. package/src/store/workflow-repo.ts +69 -0
  57. package/src/types.ts +208 -0
  58. package/tsconfig.json +17 -0
  59. 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
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env tsx
2
+ import { createCli } from '../src/cli/index.js';
3
+
4
+ const program = createCli();
5
+ program.parse();
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
+ }