@desplega.ai/agent-swarm 1.0.2 → 1.2.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/.claude/settings.local.json +2 -1
- package/.dockerignore +58 -0
- package/.env.docker.example +12 -0
- package/Dockerfile.worker +112 -0
- package/README.md +117 -0
- package/cc-plugin/.claude-plugin/plugin.json +13 -0
- package/cc-plugin/README.md +49 -0
- package/cc-plugin/commands/setup-leader.md +73 -0
- package/cc-plugin/commands/start-worker.md +64 -0
- package/cc-plugin/hooks/hooks.json +71 -0
- package/deploy/DEPLOY.md +3 -0
- package/deploy/agent-swarm.service +3 -2
- package/deploy/install.ts +56 -6
- package/deploy/prod-db.ts +42 -0
- package/deploy/uninstall.ts +4 -2
- package/deploy/update.ts +21 -0
- package/docker-compose.worker.yml +35 -0
- package/docker-entrypoint.sh +62 -0
- package/package.json +9 -2
- package/src/be/db.ts +68 -20
- package/src/cli.tsx +96 -8
- package/src/commands/hook.ts +2 -2
- package/src/commands/setup.tsx +579 -550
- package/src/commands/worker.ts +225 -0
- package/src/hooks/hook.ts +180 -175
- package/src/server.ts +1 -2
- package/src/tools/get-task-details.ts +7 -3
- package/src/tools/join-swarm.ts +23 -11
- package/src/tools/poll-task.ts +34 -2
- package/src/tools/send-task.ts +40 -2
- package/src/tools/store-progress.ts +29 -0
package/deploy/install.ts
CHANGED
|
@@ -7,10 +7,14 @@ const SERVICE_FILE = "/etc/systemd/system/agent-swarm.service";
|
|
|
7
7
|
const SCRIPT_DIR = import.meta.dir;
|
|
8
8
|
const PROJECT_DIR = `${SCRIPT_DIR}/..`;
|
|
9
9
|
|
|
10
|
+
// Detect bun path
|
|
11
|
+
const bunPath = (await $`which bun`.text()).trim();
|
|
12
|
+
console.log(`Using bun at: ${bunPath}`);
|
|
13
|
+
|
|
10
14
|
// Copy project files
|
|
11
15
|
await $`mkdir -p ${APP_DIR}`;
|
|
12
16
|
await $`cp -r ${PROJECT_DIR}/src ${APP_DIR}/`;
|
|
13
|
-
await $`cp ${PROJECT_DIR}/package.json ${PROJECT_DIR}/bun.lock ${APP_DIR}/`;
|
|
17
|
+
await $`cp ${PROJECT_DIR}/package.json ${PROJECT_DIR}/bun.lock ${PROJECT_DIR}/tsconfig.json ${APP_DIR}/`;
|
|
14
18
|
|
|
15
19
|
// Install dependencies
|
|
16
20
|
await $`cd ${APP_DIR} && bun install --frozen-lockfile --production`;
|
|
@@ -25,11 +29,57 @@ API_KEY=
|
|
|
25
29
|
}
|
|
26
30
|
|
|
27
31
|
// Set ownership
|
|
28
|
-
await $`chown -R
|
|
32
|
+
await $`chown -R root:root ${APP_DIR}`;
|
|
33
|
+
|
|
34
|
+
// Install systemd service with detected bun path
|
|
35
|
+
const serviceContent = `[Unit]
|
|
36
|
+
Description=Agent Swarm MCP Server
|
|
37
|
+
After=network.target
|
|
38
|
+
|
|
39
|
+
[Service]
|
|
40
|
+
Type=simple
|
|
41
|
+
User=root
|
|
42
|
+
Group=root
|
|
43
|
+
WorkingDirectory=${APP_DIR}
|
|
44
|
+
ExecStart=${bunPath} run start:http
|
|
45
|
+
ExecStartPost=/bin/sh -c 'sleep 2 && curl -sf http://localhost:3013/health || exit 1'
|
|
46
|
+
Restart=always
|
|
47
|
+
RestartSec=5
|
|
48
|
+
EnvironmentFile=${APP_DIR}/.env
|
|
49
|
+
|
|
50
|
+
[Install]
|
|
51
|
+
WantedBy=multi-user.target
|
|
52
|
+
`;
|
|
53
|
+
|
|
54
|
+
await Bun.write(SERVICE_FILE, serviceContent);
|
|
55
|
+
|
|
56
|
+
// Healthcheck service (runs curl, restarts main service on failure)
|
|
57
|
+
const healthcheckService = `[Unit]
|
|
58
|
+
Description=Agent Swarm Health Check
|
|
59
|
+
|
|
60
|
+
[Service]
|
|
61
|
+
Type=oneshot
|
|
62
|
+
ExecStart=/bin/sh -c 'curl -sf http://localhost:3013/health || systemctl restart agent-swarm'
|
|
63
|
+
`;
|
|
64
|
+
|
|
65
|
+
// Timer to run healthcheck every 30 seconds
|
|
66
|
+
const healthcheckTimer = `[Unit]
|
|
67
|
+
Description=Agent Swarm Health Check Timer
|
|
68
|
+
|
|
69
|
+
[Timer]
|
|
70
|
+
OnBootSec=30s
|
|
71
|
+
OnUnitActiveSec=30s
|
|
72
|
+
|
|
73
|
+
[Install]
|
|
74
|
+
WantedBy=timers.target
|
|
75
|
+
`;
|
|
76
|
+
|
|
77
|
+
await Bun.write("/etc/systemd/system/agent-swarm-healthcheck.service", healthcheckService);
|
|
78
|
+
await Bun.write("/etc/systemd/system/agent-swarm-healthcheck.timer", healthcheckTimer);
|
|
29
79
|
|
|
30
|
-
// Install systemd service
|
|
31
|
-
await $`cp ${SCRIPT_DIR}/agent-swarm.service ${SERVICE_FILE}`;
|
|
32
80
|
await $`systemctl daemon-reload`;
|
|
33
|
-
await $`systemctl enable agent-swarm`;
|
|
81
|
+
await $`systemctl enable agent-swarm agent-swarm-healthcheck.timer`;
|
|
82
|
+
await $`systemctl restart agent-swarm`;
|
|
83
|
+
await $`systemctl start agent-swarm-healthcheck.timer`;
|
|
34
84
|
|
|
35
|
-
console.log("Installed
|
|
85
|
+
console.log("Installed and running with health checks every 30s.");
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { $ } from "bun";
|
|
4
|
+
import * as readline from "node:readline";
|
|
5
|
+
|
|
6
|
+
const DB_PATH = "/opt/agent-swarm/agent-swarm-db.sqlite";
|
|
7
|
+
const SSH_HOST = process.argv[2] || "hetzner";
|
|
8
|
+
|
|
9
|
+
console.log(`Connected to ${SSH_HOST}:${DB_PATH}`);
|
|
10
|
+
console.log("Type SQL queries or .tables, .schema, etc. Ctrl+C to exit.\n");
|
|
11
|
+
|
|
12
|
+
const rl = readline.createInterface({
|
|
13
|
+
input: process.stdin,
|
|
14
|
+
output: process.stdout,
|
|
15
|
+
prompt: "sqlite> ",
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
rl.prompt();
|
|
19
|
+
|
|
20
|
+
rl.on("line", async (line) => {
|
|
21
|
+
const query = line.trim();
|
|
22
|
+
if (!query) {
|
|
23
|
+
rl.prompt();
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
// Escape single quotes in query and wrap in single quotes for remote shell
|
|
29
|
+
const escaped = query.replace(/'/g, "'\\''");
|
|
30
|
+
const result = await $`ssh ${SSH_HOST} sqlite3 -header -column ${DB_PATH} ${"'" + escaped + "'"}`.text();
|
|
31
|
+
if (result) console.log(result);
|
|
32
|
+
} catch (e: any) {
|
|
33
|
+
console.error(e.stderr?.toString() || e.message);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
rl.prompt();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
rl.on("close", () => {
|
|
40
|
+
console.log("\nBye!");
|
|
41
|
+
process.exit(0);
|
|
42
|
+
});
|
package/deploy/uninstall.ts
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
import { $ } from "bun";
|
|
4
4
|
|
|
5
|
-
await $`systemctl stop agent-swarm`.nothrow();
|
|
6
|
-
await $`systemctl disable agent-swarm`.nothrow();
|
|
5
|
+
await $`systemctl stop agent-swarm agent-swarm-healthcheck.timer`.nothrow();
|
|
6
|
+
await $`systemctl disable agent-swarm agent-swarm-healthcheck.timer`.nothrow();
|
|
7
7
|
await $`rm -f /etc/systemd/system/agent-swarm.service`;
|
|
8
|
+
await $`rm -f /etc/systemd/system/agent-swarm-healthcheck.service`;
|
|
9
|
+
await $`rm -f /etc/systemd/system/agent-swarm-healthcheck.timer`;
|
|
8
10
|
await $`systemctl daemon-reload`;
|
|
9
11
|
|
|
10
12
|
console.log("Service removed. Data remains at /opt/agent-swarm");
|
package/deploy/update.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { $ } from "bun";
|
|
4
|
+
|
|
5
|
+
const APP_DIR = "/opt/agent-swarm";
|
|
6
|
+
const SCRIPT_DIR = import.meta.dir;
|
|
7
|
+
const PROJECT_DIR = `${SCRIPT_DIR}/..`;
|
|
8
|
+
|
|
9
|
+
console.log("Updating agent-swarm...");
|
|
10
|
+
|
|
11
|
+
// Copy project files
|
|
12
|
+
await $`cp -r ${PROJECT_DIR}/src ${APP_DIR}/`;
|
|
13
|
+
await $`cp ${PROJECT_DIR}/package.json ${PROJECT_DIR}/bun.lock ${PROJECT_DIR}/tsconfig.json ${APP_DIR}/`;
|
|
14
|
+
|
|
15
|
+
// Install dependencies
|
|
16
|
+
await $`cd ${APP_DIR} && bun install --frozen-lockfile --production`;
|
|
17
|
+
|
|
18
|
+
// Restart service
|
|
19
|
+
await $`systemctl restart agent-swarm`;
|
|
20
|
+
|
|
21
|
+
console.log("Updated and restarted.");
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Docker Compose for Agent Swarm Worker
|
|
2
|
+
#
|
|
3
|
+
# Usage:
|
|
4
|
+
# docker-compose -f docker-compose.worker.yml up --build
|
|
5
|
+
#
|
|
6
|
+
# Required environment variables (in .env.docker or passed directly):
|
|
7
|
+
# - CLAUDE_CODE_OAUTH_TOKEN: OAuth token for Claude CLI
|
|
8
|
+
# - API_KEY: API key for MCP server authentication
|
|
9
|
+
#
|
|
10
|
+
# Optional environment variables:
|
|
11
|
+
# - AGENT_ID: UUID for agent identification (assigned on join if not set)
|
|
12
|
+
# - MCP_BASE_URL: MCP server URL (default: http://host.docker.internal:3013)
|
|
13
|
+
# - SESSION_ID: Folder name for logs (auto-generated if not provided)
|
|
14
|
+
# - WORKER_YOLO: If "true", continue on errors (default: false)
|
|
15
|
+
|
|
16
|
+
services:
|
|
17
|
+
worker:
|
|
18
|
+
build:
|
|
19
|
+
context: .
|
|
20
|
+
dockerfile: Dockerfile.worker
|
|
21
|
+
environment:
|
|
22
|
+
- CLAUDE_CODE_OAUTH_TOKEN=${CLAUDE_CODE_OAUTH_TOKEN}
|
|
23
|
+
- API_KEY=${API_KEY}
|
|
24
|
+
- AGENT_ID=${AGENT_ID:-}
|
|
25
|
+
# Use host.docker.internal to reach host's localhost on Mac/Windows
|
|
26
|
+
# On Linux, use --add-host=host.docker.internal:host-gateway or --network host
|
|
27
|
+
- MCP_BASE_URL=${MCP_BASE_URL:-http://host.docker.internal:3013}
|
|
28
|
+
- SESSION_ID=${SESSION_ID:-}
|
|
29
|
+
- WORKER_YOLO=${WORKER_YOLO:-false}
|
|
30
|
+
volumes:
|
|
31
|
+
- ./logs:/logs
|
|
32
|
+
# Optional: mount a workspace directory for persistent work
|
|
33
|
+
# - ./workspace:/workspace
|
|
34
|
+
restart: unless-stopped
|
|
35
|
+
# No port mappings needed - worker only makes outbound connections
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -e
|
|
3
|
+
|
|
4
|
+
# Validate required environment variables
|
|
5
|
+
if [ -z "$CLAUDE_CODE_OAUTH_TOKEN" ]; then
|
|
6
|
+
echo "Error: CLAUDE_CODE_OAUTH_TOKEN environment variable is required"
|
|
7
|
+
exit 1
|
|
8
|
+
fi
|
|
9
|
+
|
|
10
|
+
if [ -z "$API_KEY" ]; then
|
|
11
|
+
echo "Error: API_KEY environment variable is required"
|
|
12
|
+
exit 1
|
|
13
|
+
fi
|
|
14
|
+
|
|
15
|
+
MCP_URL="${MCP_BASE_URL:-http://host.docker.internal:3013}"
|
|
16
|
+
|
|
17
|
+
echo "=== Agent Swarm Worker ==="
|
|
18
|
+
echo "Agent ID: ${AGENT_ID:-<not set>}"
|
|
19
|
+
echo "MCP Base URL: $MCP_URL"
|
|
20
|
+
echo "YOLO Mode: ${WORKER_YOLO:-false}"
|
|
21
|
+
echo "Session ID: ${SESSION_ID:-<auto-generated>}"
|
|
22
|
+
echo "Working Directory: /workspace"
|
|
23
|
+
echo "=========================="
|
|
24
|
+
|
|
25
|
+
# Create .mcp.json in /workspace (project-level config)
|
|
26
|
+
echo "Creating MCP config in /workspace..."
|
|
27
|
+
if [ -n "$AGENT_ID" ]; then
|
|
28
|
+
# With AGENT_ID header
|
|
29
|
+
cat > /workspace/.mcp.json << EOF
|
|
30
|
+
{
|
|
31
|
+
"mcpServers": {
|
|
32
|
+
"agent-swarm": {
|
|
33
|
+
"type": "http",
|
|
34
|
+
"url": "${MCP_URL}/mcp",
|
|
35
|
+
"headers": {
|
|
36
|
+
"Authorization": "Bearer ${API_KEY}",
|
|
37
|
+
"X-Agent-ID": "${AGENT_ID}"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
EOF
|
|
43
|
+
else
|
|
44
|
+
# Without AGENT_ID header
|
|
45
|
+
cat > /workspace/.mcp.json << EOF
|
|
46
|
+
{
|
|
47
|
+
"mcpServers": {
|
|
48
|
+
"agent-swarm": {
|
|
49
|
+
"type": "http",
|
|
50
|
+
"url": "${MCP_URL}/mcp",
|
|
51
|
+
"headers": {
|
|
52
|
+
"Authorization": "Bearer ${API_KEY}"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
EOF
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
# Run the worker using compiled binary
|
|
61
|
+
echo "Starting worker..."
|
|
62
|
+
exec /usr/local/bin/agent-swarm worker "$@"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@desplega.ai/agent-swarm",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Agent orchestration layer MCP for Claude Code, Codex, Gemini CLI, and more!",
|
|
5
5
|
"module": "src/stdio.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
"hook": "bun src/hooks/hook.ts",
|
|
18
18
|
"claude": "bun src/cli.tsx claude",
|
|
19
19
|
"claude:headless": "bun src/cli.tsx claude --headless",
|
|
20
|
+
"worker": "bun src/cli.tsx worker",
|
|
21
|
+
"worker:yolo": "bun src/cli.tsx worker --yolo",
|
|
20
22
|
"dev": "bun --hot src/stdio.ts",
|
|
21
23
|
"dev:http": "bun --hot src/http.ts",
|
|
22
24
|
"start": "bun src/stdio.ts",
|
|
@@ -25,7 +27,11 @@
|
|
|
25
27
|
"inspector:http": "bunx @modelcontextprotocol/inspector --transport http http://localhost:3013/mcp",
|
|
26
28
|
"lint": "biome check src",
|
|
27
29
|
"lint:fix": "biome check --write src",
|
|
28
|
-
"format": "biome format --write src"
|
|
30
|
+
"format": "biome format --write src",
|
|
31
|
+
"build:binary": "bun build ./src/cli.tsx --compile --target=bun-linux-x64 --outfile ./dist/agent-swarm",
|
|
32
|
+
"build:binary:arm64": "bun build ./src/cli.tsx --compile --target=bun-linux-arm64 --outfile ./dist/agent-swarm",
|
|
33
|
+
"docker:build:worker": "docker build -f Dockerfile.worker -t agent-swarm-worker .",
|
|
34
|
+
"docker:run:worker": "docker run --rm -it --env-file .env.docker -v ./logs:/logs agent-swarm-worker"
|
|
29
35
|
},
|
|
30
36
|
"devDependencies": {
|
|
31
37
|
"@biomejs/biome": "^2.3.9",
|
|
@@ -41,6 +47,7 @@
|
|
|
41
47
|
"date-fns": "^4.1.0",
|
|
42
48
|
"ink": "^6.5.1",
|
|
43
49
|
"react": "^19.2.3",
|
|
50
|
+
"react-devtools-core": "^7.0.1",
|
|
44
51
|
"zod": "^4.2.1"
|
|
45
52
|
}
|
|
46
53
|
}
|
package/src/be/db.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { Database } from "bun:sqlite";
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
Agent,
|
|
4
|
+
AgentLog,
|
|
5
|
+
AgentLogEventType,
|
|
6
|
+
AgentStatus,
|
|
7
|
+
AgentTask,
|
|
8
|
+
AgentTaskStatus,
|
|
9
|
+
AgentWithTasks,
|
|
10
|
+
} from "../types";
|
|
3
11
|
|
|
4
12
|
let db: Database | null = null;
|
|
5
13
|
|
|
@@ -141,7 +149,12 @@ export function updateAgentStatus(id: string, status: AgentStatus): Agent | null
|
|
|
141
149
|
const row = agentQueries.updateStatus().get(status, id);
|
|
142
150
|
if (row && oldAgent) {
|
|
143
151
|
try {
|
|
144
|
-
createLogEntry({
|
|
152
|
+
createLogEntry({
|
|
153
|
+
eventType: "agent_status_change",
|
|
154
|
+
agentId: id,
|
|
155
|
+
oldValue: oldAgent.status,
|
|
156
|
+
newValue: status,
|
|
157
|
+
});
|
|
145
158
|
} catch {}
|
|
146
159
|
}
|
|
147
160
|
return row ? rowToAgent(row) : null;
|
|
@@ -262,7 +275,13 @@ export function startTask(taskId: string): AgentTask | null {
|
|
|
262
275
|
.get(taskId);
|
|
263
276
|
if (row && oldTask) {
|
|
264
277
|
try {
|
|
265
|
-
createLogEntry({
|
|
278
|
+
createLogEntry({
|
|
279
|
+
eventType: "task_status_change",
|
|
280
|
+
taskId,
|
|
281
|
+
agentId: row.agentId,
|
|
282
|
+
oldValue: oldTask.status,
|
|
283
|
+
newValue: "in_progress",
|
|
284
|
+
});
|
|
266
285
|
} catch {}
|
|
267
286
|
}
|
|
268
287
|
return row ? rowToAgentTask(row) : null;
|
|
@@ -308,7 +327,13 @@ export function completeTask(id: string, output?: string): AgentTask | null {
|
|
|
308
327
|
|
|
309
328
|
if (row && oldTask) {
|
|
310
329
|
try {
|
|
311
|
-
createLogEntry({
|
|
330
|
+
createLogEntry({
|
|
331
|
+
eventType: "task_status_change",
|
|
332
|
+
taskId: id,
|
|
333
|
+
agentId: row.agentId,
|
|
334
|
+
oldValue: oldTask.status,
|
|
335
|
+
newValue: "completed",
|
|
336
|
+
});
|
|
312
337
|
} catch {}
|
|
313
338
|
}
|
|
314
339
|
|
|
@@ -321,7 +346,14 @@ export function failTask(id: string, reason: string): AgentTask | null {
|
|
|
321
346
|
const row = taskQueries.setFailure().get(reason, finishedAt, id);
|
|
322
347
|
if (row && oldTask) {
|
|
323
348
|
try {
|
|
324
|
-
createLogEntry({
|
|
349
|
+
createLogEntry({
|
|
350
|
+
eventType: "task_status_change",
|
|
351
|
+
taskId: id,
|
|
352
|
+
agentId: row.agentId,
|
|
353
|
+
oldValue: oldTask.status,
|
|
354
|
+
newValue: "failed",
|
|
355
|
+
metadata: { reason },
|
|
356
|
+
});
|
|
325
357
|
} catch {}
|
|
326
358
|
}
|
|
327
359
|
return row ? rowToAgentTask(row) : null;
|
|
@@ -336,7 +368,12 @@ export function updateTaskProgress(id: string, progress: string): AgentTask | nu
|
|
|
336
368
|
const row = taskQueries.setProgress().get(progress, id);
|
|
337
369
|
if (row) {
|
|
338
370
|
try {
|
|
339
|
-
createLogEntry({
|
|
371
|
+
createLogEntry({
|
|
372
|
+
eventType: "task_progress",
|
|
373
|
+
taskId: id,
|
|
374
|
+
agentId: row.agentId,
|
|
375
|
+
newValue: progress,
|
|
376
|
+
});
|
|
340
377
|
} catch {}
|
|
341
378
|
}
|
|
342
379
|
return row ? rowToAgentTask(row) : null;
|
|
@@ -400,7 +437,10 @@ function rowToAgentLog(row: AgentLogRow): AgentLog {
|
|
|
400
437
|
|
|
401
438
|
export const logQueries = {
|
|
402
439
|
insert: () =>
|
|
403
|
-
getDb().prepare<
|
|
440
|
+
getDb().prepare<
|
|
441
|
+
AgentLogRow,
|
|
442
|
+
[string, string, string | null, string | null, string | null, string | null, string | null]
|
|
443
|
+
>(
|
|
404
444
|
`INSERT INTO agent_log (id, eventType, agentId, taskId, oldValue, newValue, metadata, createdAt)
|
|
405
445
|
VALUES (?, ?, ?, ?, ?, ?, ?, strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) RETURNING *`,
|
|
406
446
|
),
|
|
@@ -420,10 +460,7 @@ export const logQueries = {
|
|
|
420
460
|
"SELECT * FROM agent_log WHERE eventType = ? ORDER BY createdAt DESC",
|
|
421
461
|
),
|
|
422
462
|
|
|
423
|
-
getAll: () =>
|
|
424
|
-
getDb().prepare<AgentLogRow, []>(
|
|
425
|
-
"SELECT * FROM agent_log ORDER BY createdAt DESC",
|
|
426
|
-
),
|
|
463
|
+
getAll: () => getDb().prepare<AgentLogRow, []>("SELECT * FROM agent_log ORDER BY createdAt DESC"),
|
|
427
464
|
};
|
|
428
465
|
|
|
429
466
|
export function createLogEntry(entry: {
|
|
@@ -435,15 +472,17 @@ export function createLogEntry(entry: {
|
|
|
435
472
|
metadata?: Record<string, unknown>;
|
|
436
473
|
}): AgentLog {
|
|
437
474
|
const id = crypto.randomUUID();
|
|
438
|
-
const row = logQueries
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
475
|
+
const row = logQueries
|
|
476
|
+
.insert()
|
|
477
|
+
.get(
|
|
478
|
+
id,
|
|
479
|
+
entry.eventType,
|
|
480
|
+
entry.agentId ?? null,
|
|
481
|
+
entry.taskId ?? null,
|
|
482
|
+
entry.oldValue ?? null,
|
|
483
|
+
entry.newValue ?? null,
|
|
484
|
+
entry.metadata ? JSON.stringify(entry.metadata) : null,
|
|
485
|
+
);
|
|
447
486
|
if (!row) throw new Error("Failed to create log entry");
|
|
448
487
|
return rowToAgentLog(row);
|
|
449
488
|
}
|
|
@@ -456,6 +495,15 @@ export function getLogsByTaskId(taskId: string): AgentLog[] {
|
|
|
456
495
|
return logQueries.getByTaskId().all(taskId).map(rowToAgentLog);
|
|
457
496
|
}
|
|
458
497
|
|
|
498
|
+
export function getLogsByTaskIdChronological(taskId: string): AgentLog[] {
|
|
499
|
+
return getDb()
|
|
500
|
+
.prepare<AgentLogRow, [string]>(
|
|
501
|
+
"SELECT * FROM agent_log WHERE taskId = ? ORDER BY createdAt ASC",
|
|
502
|
+
)
|
|
503
|
+
.all(taskId)
|
|
504
|
+
.map(rowToAgentLog);
|
|
505
|
+
}
|
|
506
|
+
|
|
459
507
|
export function getAllLogs(limit?: number): AgentLog[] {
|
|
460
508
|
if (limit) {
|
|
461
509
|
return getDb()
|
package/src/cli.tsx
CHANGED
|
@@ -6,6 +6,7 @@ import pkg from "../package.json";
|
|
|
6
6
|
import { runClaude } from "./claude.ts";
|
|
7
7
|
import { runHook } from "./commands/hook.ts";
|
|
8
8
|
import { Setup } from "./commands/setup.tsx";
|
|
9
|
+
import { runWorker } from "./commands/worker.ts";
|
|
9
10
|
|
|
10
11
|
// Get CLI name from bin field (assumes single key)
|
|
11
12
|
const binName = Object.keys(pkg.bin)[0];
|
|
@@ -26,6 +27,8 @@ interface ParsedArgs {
|
|
|
26
27
|
headless: boolean;
|
|
27
28
|
dryRun: boolean;
|
|
28
29
|
restore: boolean;
|
|
30
|
+
yes: boolean;
|
|
31
|
+
yolo: boolean;
|
|
29
32
|
additionalArgs: string[];
|
|
30
33
|
}
|
|
31
34
|
|
|
@@ -37,6 +40,8 @@ function parseArgs(args: string[]): ParsedArgs {
|
|
|
37
40
|
let headless = false;
|
|
38
41
|
let dryRun = false;
|
|
39
42
|
let restore = false;
|
|
43
|
+
let yes = false;
|
|
44
|
+
let yolo = false;
|
|
40
45
|
let additionalArgs: string[] = [];
|
|
41
46
|
|
|
42
47
|
// Find if there's a "--" separator for additional args
|
|
@@ -61,10 +66,14 @@ function parseArgs(args: string[]): ParsedArgs {
|
|
|
61
66
|
dryRun = true;
|
|
62
67
|
} else if (arg === "--restore") {
|
|
63
68
|
restore = true;
|
|
69
|
+
} else if (arg === "-y" || arg === "--yes") {
|
|
70
|
+
yes = true;
|
|
71
|
+
} else if (arg === "--yolo") {
|
|
72
|
+
yolo = true;
|
|
64
73
|
}
|
|
65
74
|
}
|
|
66
75
|
|
|
67
|
-
return { command, port, key, msg, headless, dryRun, restore, additionalArgs };
|
|
76
|
+
return { command, port, key, msg, headless, dryRun, restore, yes, yolo, additionalArgs };
|
|
68
77
|
}
|
|
69
78
|
|
|
70
79
|
function Help() {
|
|
@@ -121,6 +130,12 @@ function Help() {
|
|
|
121
130
|
</Box>
|
|
122
131
|
<Text>Run Claude CLI</Text>
|
|
123
132
|
</Box>
|
|
133
|
+
<Box>
|
|
134
|
+
<Box width={12}>
|
|
135
|
+
<Text color="green">worker</Text>
|
|
136
|
+
</Box>
|
|
137
|
+
<Text>Run Claude in headless loop mode</Text>
|
|
138
|
+
</Box>
|
|
124
139
|
<Box>
|
|
125
140
|
<Box width={12}>
|
|
126
141
|
<Text color="green">version</Text>
|
|
@@ -149,6 +164,12 @@ function Help() {
|
|
|
149
164
|
</Box>
|
|
150
165
|
<Text>Restore files from .bak backups</Text>
|
|
151
166
|
</Box>
|
|
167
|
+
<Box>
|
|
168
|
+
<Box width={24}>
|
|
169
|
+
<Text color="yellow">-y, --yes</Text>
|
|
170
|
+
</Box>
|
|
171
|
+
<Text>Non-interactive mode (use env vars)</Text>
|
|
172
|
+
</Box>
|
|
152
173
|
</Box>
|
|
153
174
|
|
|
154
175
|
<Box marginTop={1} flexDirection="column">
|
|
@@ -189,38 +210,82 @@ function Help() {
|
|
|
189
210
|
</Box>
|
|
190
211
|
</Box>
|
|
191
212
|
|
|
213
|
+
<Box marginTop={1} flexDirection="column">
|
|
214
|
+
<Text bold>Options for 'worker':</Text>
|
|
215
|
+
<Box>
|
|
216
|
+
<Box width={24}>
|
|
217
|
+
<Text color="yellow">-m, --msg {"<prompt>"}</Text>
|
|
218
|
+
</Box>
|
|
219
|
+
<Text>Custom prompt (default: /agent-swarm:start-worker)</Text>
|
|
220
|
+
</Box>
|
|
221
|
+
<Box>
|
|
222
|
+
<Box width={24}>
|
|
223
|
+
<Text color="yellow">--yolo</Text>
|
|
224
|
+
</Box>
|
|
225
|
+
<Text>Continue on errors instead of stopping</Text>
|
|
226
|
+
</Box>
|
|
227
|
+
<Box>
|
|
228
|
+
<Box width={24}>
|
|
229
|
+
<Text color="yellow">-- {"<args...>"}</Text>
|
|
230
|
+
</Box>
|
|
231
|
+
<Text>Additional arguments to pass to Claude CLI</Text>
|
|
232
|
+
</Box>
|
|
233
|
+
</Box>
|
|
234
|
+
|
|
192
235
|
<Box marginTop={1} flexDirection="column">
|
|
193
236
|
<Text bold>Examples:</Text>
|
|
194
237
|
<Text dimColor> {binName} setup</Text>
|
|
195
238
|
<Text dimColor> {binName} setup --dry-run</Text>
|
|
239
|
+
<Text dimColor> {binName} setup -y</Text>
|
|
196
240
|
<Text dimColor> {binName} mcp</Text>
|
|
197
241
|
<Text dimColor> {binName} mcp --port 8080</Text>
|
|
198
242
|
<Text dimColor> {binName} mcp -p 8080 -k my-secret-key</Text>
|
|
199
243
|
<Text dimColor> {binName} claude</Text>
|
|
200
244
|
<Text dimColor> {binName} claude --headless -m "Hello"</Text>
|
|
201
245
|
<Text dimColor> {binName} claude -- --resume</Text>
|
|
246
|
+
<Text dimColor> {binName} worker</Text>
|
|
247
|
+
<Text dimColor> {binName} worker --yolo</Text>
|
|
248
|
+
<Text dimColor> {binName} worker -m "Custom prompt"</Text>
|
|
202
249
|
</Box>
|
|
203
250
|
|
|
204
251
|
<Box marginTop={1} flexDirection="column">
|
|
205
252
|
<Text bold>Environment variables:</Text>
|
|
206
253
|
<Box>
|
|
207
|
-
<Box width={
|
|
254
|
+
<Box width={24}>
|
|
208
255
|
<Text color="magenta">PORT</Text>
|
|
209
256
|
</Box>
|
|
210
257
|
<Text>Default port for the MCP server</Text>
|
|
211
258
|
</Box>
|
|
212
259
|
<Box>
|
|
213
|
-
<Box width={
|
|
260
|
+
<Box width={24}>
|
|
214
261
|
<Text color="magenta">API_KEY</Text>
|
|
215
262
|
</Box>
|
|
216
263
|
<Text>API key for authentication (Bearer token)</Text>
|
|
217
264
|
</Box>
|
|
218
265
|
<Box>
|
|
219
|
-
<Box width={
|
|
266
|
+
<Box width={24}>
|
|
220
267
|
<Text color="magenta">MCP_BASE_URL</Text>
|
|
221
268
|
</Box>
|
|
222
269
|
<Text>Base URL for the MCP server (used by setup)</Text>
|
|
223
270
|
</Box>
|
|
271
|
+
<Box>
|
|
272
|
+
<Box width={24}>
|
|
273
|
+
<Text color="magenta">AGENT_ID</Text>
|
|
274
|
+
</Box>
|
|
275
|
+
<Text>UUID for agent identification</Text>
|
|
276
|
+
</Box>
|
|
277
|
+
<Box>
|
|
278
|
+
<Box width={24}>
|
|
279
|
+
<Text color="magenta">SESSION_ID</Text>
|
|
280
|
+
</Box>
|
|
281
|
+
<Text>Folder name for worker logs (auto-generated)</Text>
|
|
282
|
+
</Box>
|
|
283
|
+
<Box>
|
|
284
|
+
<Box width={24}>
|
|
285
|
+
<Text color="magenta">WORKER_YOLO</Text>
|
|
286
|
+
</Box>
|
|
287
|
+
<Text>If "true", worker continues on errors</Text>
|
|
288
|
+
</Box>
|
|
224
289
|
</Box>
|
|
225
290
|
</Box>
|
|
226
291
|
);
|
|
@@ -298,6 +363,27 @@ function ClaudeRunner({ msg, headless, additionalArgs }: ClaudeRunnerProps) {
|
|
|
298
363
|
return null;
|
|
299
364
|
}
|
|
300
365
|
|
|
366
|
+
interface WorkerRunnerProps {
|
|
367
|
+
prompt: string;
|
|
368
|
+
yolo: boolean;
|
|
369
|
+
additionalArgs: string[];
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function WorkerRunner({ prompt, yolo, additionalArgs }: WorkerRunnerProps) {
|
|
373
|
+
const { exit } = useApp();
|
|
374
|
+
|
|
375
|
+
useEffect(() => {
|
|
376
|
+
runWorker({
|
|
377
|
+
prompt: prompt || undefined,
|
|
378
|
+
yolo,
|
|
379
|
+
additionalArgs,
|
|
380
|
+
}).catch((err) => exit(err));
|
|
381
|
+
// Note: runWorker runs indefinitely, so we don't call exit() on success
|
|
382
|
+
}, [prompt, yolo, additionalArgs, exit]);
|
|
383
|
+
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
|
|
301
387
|
function UnknownCommand({ command }: { command: string }) {
|
|
302
388
|
const { exit } = useApp();
|
|
303
389
|
useEffect(() => {
|
|
@@ -328,15 +414,17 @@ function Version() {
|
|
|
328
414
|
}
|
|
329
415
|
|
|
330
416
|
function App({ args }: { args: ParsedArgs }) {
|
|
331
|
-
const { command, port, key, msg, headless, dryRun, restore, additionalArgs } = args;
|
|
417
|
+
const { command, port, key, msg, headless, dryRun, restore, yes, yolo, additionalArgs } = args;
|
|
332
418
|
|
|
333
419
|
switch (command) {
|
|
334
420
|
case "setup":
|
|
335
|
-
return <Setup dryRun={dryRun} restore={restore} />;
|
|
421
|
+
return <Setup dryRun={dryRun} restore={restore} yes={yes} />;
|
|
336
422
|
case "mcp":
|
|
337
423
|
return <McpServer port={port} apiKey={key} />;
|
|
338
424
|
case "claude":
|
|
339
425
|
return <ClaudeRunner msg={msg} headless={headless} additionalArgs={additionalArgs} />;
|
|
426
|
+
case "worker":
|
|
427
|
+
return <WorkerRunner prompt={msg} yolo={yolo} additionalArgs={additionalArgs} />;
|
|
340
428
|
case "version":
|
|
341
429
|
return <Version />;
|
|
342
430
|
case "help":
|
|
@@ -351,7 +439,7 @@ const args = parseArgs(process.argv.slice(2));
|
|
|
351
439
|
|
|
352
440
|
// Handle hook command separately (no UI needed)
|
|
353
441
|
if (args.command === "hook") {
|
|
354
|
-
|
|
442
|
+
runHook();
|
|
355
443
|
} else {
|
|
356
|
-
|
|
444
|
+
render(<App args={args} />);
|
|
357
445
|
}
|
package/src/commands/hook.ts
CHANGED