@desplega.ai/agent-swarm 1.0.1 → 1.0.2

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/Dockerfile ADDED
@@ -0,0 +1,16 @@
1
+ FROM oven/bun:1-alpine
2
+
3
+ WORKDIR /app
4
+
5
+ COPY package.json bun.lock ./
6
+ RUN bun install --frozen-lockfile --production
7
+
8
+ COPY . .
9
+
10
+ ENV PORT=3013
11
+ EXPOSE 3013
12
+
13
+ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
14
+ CMD wget -qO- http://localhost:3013/health || exit 1
15
+
16
+ CMD ["bun", "run", "start:http"]
@@ -0,0 +1,57 @@
1
+ # Deployment
2
+
3
+ ## Environment Variables
4
+
5
+ | Variable | Default | Description |
6
+ |----------|---------|-------------|
7
+ | `PORT` | `3013` | Server port |
8
+ | `API_KEY` | _(empty)_ | Bearer token for auth (optional) |
9
+
10
+ ## Docker
11
+
12
+ ```bash
13
+ # Build
14
+ docker build -t agent-swarm .
15
+
16
+ # Run (persists database to ./agent-swarm-db.sqlite on host)
17
+ docker run -d --name agent-swarm -p 3013:3013 \
18
+ -e API_KEY=your-secret-key \
19
+ -v $(pwd)/agent-swarm-db.sqlite:/app/agent-swarm-db.sqlite \
20
+ agent-swarm
21
+ ```
22
+
23
+ ## systemd
24
+
25
+ ```bash
26
+ # Install service
27
+ sudo bun deploy/install.ts
28
+
29
+ # Control
30
+ sudo systemctl start agent-swarm
31
+ sudo systemctl stop agent-swarm
32
+ sudo systemctl status agent-swarm
33
+ journalctl -u agent-swarm -f
34
+
35
+ # Uninstall
36
+ sudo bun deploy/uninstall.ts
37
+ ```
38
+
39
+ ## Caddy (reverse proxy)
40
+
41
+ Add to your Caddyfile:
42
+
43
+ ```
44
+ agent-swarm.example.com {
45
+ reverse_proxy localhost:3013
46
+ }
47
+ ```
48
+
49
+ Or with API key header injection:
50
+
51
+ ```
52
+ agent-swarm.example.com {
53
+ reverse_proxy localhost:3013 {
54
+ header_up Authorization "Bearer {env.AGENT_SWARM_API_KEY}"
55
+ }
56
+ }
57
+ ```
@@ -0,0 +1,16 @@
1
+ [Unit]
2
+ Description=Agent Swarm MCP Server
3
+ After=network.target
4
+
5
+ [Service]
6
+ Type=simple
7
+ User=www-data
8
+ Group=www-data
9
+ WorkingDirectory=/opt/agent-swarm
10
+ ExecStart=/usr/local/bin/bun run start:http
11
+ Restart=always
12
+ RestartSec=5
13
+ EnvironmentFile=/opt/agent-swarm/.env
14
+
15
+ [Install]
16
+ WantedBy=multi-user.target
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { $ } from "bun";
4
+
5
+ const APP_DIR = "/opt/agent-swarm";
6
+ const SERVICE_FILE = "/etc/systemd/system/agent-swarm.service";
7
+ const SCRIPT_DIR = import.meta.dir;
8
+ const PROJECT_DIR = `${SCRIPT_DIR}/..`;
9
+
10
+ // Copy project files
11
+ await $`mkdir -p ${APP_DIR}`;
12
+ await $`cp -r ${PROJECT_DIR}/src ${APP_DIR}/`;
13
+ await $`cp ${PROJECT_DIR}/package.json ${PROJECT_DIR}/bun.lock ${APP_DIR}/`;
14
+
15
+ // Install dependencies
16
+ await $`cd ${APP_DIR} && bun install --frozen-lockfile --production`;
17
+
18
+ // Create .env if not exists
19
+ const envFile = Bun.file(`${APP_DIR}/.env`);
20
+ if (!(await envFile.exists())) {
21
+ await Bun.write(envFile, `PORT=3013
22
+ API_KEY=
23
+ `);
24
+ console.log("Created .env - set API_KEY for authentication");
25
+ }
26
+
27
+ // Set ownership
28
+ await $`chown -R www-data:www-data ${APP_DIR}`;
29
+
30
+ // Install systemd service
31
+ await $`cp ${SCRIPT_DIR}/agent-swarm.service ${SERVICE_FILE}`;
32
+ await $`systemctl daemon-reload`;
33
+ await $`systemctl enable agent-swarm`;
34
+
35
+ console.log("Installed. Edit /opt/agent-swarm/.env then run: systemctl start agent-swarm");
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { $ } from "bun";
4
+
5
+ await $`systemctl stop agent-swarm`.nothrow();
6
+ await $`systemctl disable agent-swarm`.nothrow();
7
+ await $`rm -f /etc/systemd/system/agent-swarm.service`;
8
+ await $`systemctl daemon-reload`;
9
+
10
+ console.log("Service removed. Data remains at /opt/agent-swarm");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@desplega.ai/agent-swarm",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
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",
package/src/be/db.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import { Database } from "bun:sqlite";
2
- import type { Agent, AgentStatus, AgentTask, AgentTaskStatus, AgentWithTasks } from "../types";
2
+ import type { Agent, AgentLog, AgentLogEventType, AgentStatus, AgentTask, AgentTaskStatus, AgentWithTasks } from "../types";
3
3
 
4
4
  let db: Database | null = null;
5
5
 
6
- export function initDb(dbPath = "./cc-orch.sqlite"): Database {
6
+ export function initDb(dbPath = "./agent-swarm-db.sqlite"): Database {
7
7
  if (db) {
8
8
  return db;
9
9
  }
@@ -38,6 +38,22 @@ export function initDb(dbPath = "./cc-orch.sqlite"): Database {
38
38
 
39
39
  CREATE INDEX IF NOT EXISTS idx_agent_tasks_agentId ON agent_tasks(agentId);
40
40
  CREATE INDEX IF NOT EXISTS idx_agent_tasks_status ON agent_tasks(status);
41
+
42
+ CREATE TABLE IF NOT EXISTS agent_log (
43
+ id TEXT PRIMARY KEY,
44
+ eventType TEXT NOT NULL,
45
+ agentId TEXT,
46
+ taskId TEXT,
47
+ oldValue TEXT,
48
+ newValue TEXT,
49
+ metadata TEXT,
50
+ createdAt TEXT NOT NULL
51
+ );
52
+
53
+ CREATE INDEX IF NOT EXISTS idx_agent_log_agentId ON agent_log(agentId);
54
+ CREATE INDEX IF NOT EXISTS idx_agent_log_taskId ON agent_log(taskId);
55
+ CREATE INDEX IF NOT EXISTS idx_agent_log_eventType ON agent_log(eventType);
56
+ CREATE INDEX IF NOT EXISTS idx_agent_log_createdAt ON agent_log(createdAt);
41
57
  `);
42
58
 
43
59
  return db;
@@ -105,6 +121,9 @@ export function createAgent(
105
121
  const id = agent.id ?? crypto.randomUUID();
106
122
  const row = agentQueries.insert().get(id, agent.name, agent.isLead ? 1 : 0, agent.status);
107
123
  if (!row) throw new Error("Failed to create agent");
124
+ try {
125
+ createLogEntry({ eventType: "agent_joined", agentId: id, newValue: agent.status });
126
+ } catch {}
108
127
  return rowToAgent(row);
109
128
  }
110
129
 
@@ -118,11 +137,23 @@ export function getAllAgents(): Agent[] {
118
137
  }
119
138
 
120
139
  export function updateAgentStatus(id: string, status: AgentStatus): Agent | null {
140
+ const oldAgent = getAgentById(id);
121
141
  const row = agentQueries.updateStatus().get(status, id);
142
+ if (row && oldAgent) {
143
+ try {
144
+ createLogEntry({ eventType: "agent_status_change", agentId: id, oldValue: oldAgent.status, newValue: status });
145
+ } catch {}
146
+ }
122
147
  return row ? rowToAgent(row) : null;
123
148
  }
124
149
 
125
150
  export function deleteAgent(id: string): boolean {
151
+ const agent = getAgentById(id);
152
+ if (agent) {
153
+ try {
154
+ createLogEntry({ eventType: "agent_left", agentId: id, oldValue: agent.status });
155
+ } catch {}
156
+ }
126
157
  const result = getDb().run("DELETE FROM agents WHERE id = ?", [id]);
127
158
  return result.changes > 0;
128
159
  }
@@ -206,6 +237,9 @@ export function createTask(agentId: string, task: string): AgentTask {
206
237
  const id = crypto.randomUUID();
207
238
  const row = taskQueries.insert().get(id, agentId, task, "pending");
208
239
  if (!row) throw new Error("Failed to create task");
240
+ try {
241
+ createLogEntry({ eventType: "task_created", agentId, taskId: id, newValue: "pending" });
242
+ } catch {}
209
243
  return rowToAgentTask(row);
210
244
  }
211
245
 
@@ -219,12 +253,18 @@ export function getPendingTaskForAgent(agentId: string): AgentTask | null {
219
253
  }
220
254
 
221
255
  export function startTask(taskId: string): AgentTask | null {
256
+ const oldTask = getTaskById(taskId);
222
257
  const row = getDb()
223
258
  .prepare<AgentTaskRow, [string]>(
224
259
  `UPDATE agent_tasks SET status = 'in_progress', lastUpdatedAt = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
225
260
  WHERE id = ? RETURNING *`,
226
261
  )
227
262
  .get(taskId);
263
+ if (row && oldTask) {
264
+ try {
265
+ createLogEntry({ eventType: "task_status_change", taskId, agentId: row.agentId, oldValue: oldTask.status, newValue: "in_progress" });
266
+ } catch {}
267
+ }
228
268
  return row ? rowToAgentTask(row) : null;
229
269
  }
230
270
 
@@ -257,6 +297,7 @@ export function getAllTasks(status?: AgentTaskStatus): AgentTask[] {
257
297
  }
258
298
 
259
299
  export function completeTask(id: string, output?: string): AgentTask | null {
300
+ const oldTask = getTaskById(id);
260
301
  const finishedAt = new Date().toISOString();
261
302
  let row = taskQueries.updateStatus().get("completed", finishedAt, id);
262
303
  if (!row) return null;
@@ -265,12 +306,24 @@ export function completeTask(id: string, output?: string): AgentTask | null {
265
306
  row = taskQueries.setOutput().get(output, id);
266
307
  }
267
308
 
309
+ if (row && oldTask) {
310
+ try {
311
+ createLogEntry({ eventType: "task_status_change", taskId: id, agentId: row.agentId, oldValue: oldTask.status, newValue: "completed" });
312
+ } catch {}
313
+ }
314
+
268
315
  return row ? rowToAgentTask(row) : null;
269
316
  }
270
317
 
271
318
  export function failTask(id: string, reason: string): AgentTask | null {
319
+ const oldTask = getTaskById(id);
272
320
  const finishedAt = new Date().toISOString();
273
321
  const row = taskQueries.setFailure().get(reason, finishedAt, id);
322
+ if (row && oldTask) {
323
+ try {
324
+ createLogEntry({ eventType: "task_status_change", taskId: id, agentId: row.agentId, oldValue: oldTask.status, newValue: "failed", metadata: { reason } });
325
+ } catch {}
326
+ }
274
327
  return row ? rowToAgentTask(row) : null;
275
328
  }
276
329
 
@@ -281,6 +334,11 @@ export function deleteTask(id: string): boolean {
281
334
 
282
335
  export function updateTaskProgress(id: string, progress: string): AgentTask | null {
283
336
  const row = taskQueries.setProgress().get(progress, id);
337
+ if (row) {
338
+ try {
339
+ createLogEntry({ eventType: "task_progress", taskId: id, agentId: row.agentId, newValue: progress });
340
+ } catch {}
341
+ }
284
342
  return row ? rowToAgentTask(row) : null;
285
343
  }
286
344
 
@@ -311,3 +369,99 @@ export function getAllAgentsWithTasks(): AgentWithTasks[] {
311
369
 
312
370
  return txn();
313
371
  }
372
+
373
+ // ============================================================================
374
+ // Agent Log Queries
375
+ // ============================================================================
376
+
377
+ type AgentLogRow = {
378
+ id: string;
379
+ eventType: AgentLogEventType;
380
+ agentId: string | null;
381
+ taskId: string | null;
382
+ oldValue: string | null;
383
+ newValue: string | null;
384
+ metadata: string | null;
385
+ createdAt: string;
386
+ };
387
+
388
+ function rowToAgentLog(row: AgentLogRow): AgentLog {
389
+ return {
390
+ id: row.id,
391
+ eventType: row.eventType,
392
+ agentId: row.agentId ?? undefined,
393
+ taskId: row.taskId ?? undefined,
394
+ oldValue: row.oldValue ?? undefined,
395
+ newValue: row.newValue ?? undefined,
396
+ metadata: row.metadata ?? undefined,
397
+ createdAt: row.createdAt,
398
+ };
399
+ }
400
+
401
+ export const logQueries = {
402
+ insert: () =>
403
+ getDb().prepare<AgentLogRow, [string, string, string | null, string | null, string | null, string | null, string | null]>(
404
+ `INSERT INTO agent_log (id, eventType, agentId, taskId, oldValue, newValue, metadata, createdAt)
405
+ VALUES (?, ?, ?, ?, ?, ?, ?, strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) RETURNING *`,
406
+ ),
407
+
408
+ getByAgentId: () =>
409
+ getDb().prepare<AgentLogRow, [string]>(
410
+ "SELECT * FROM agent_log WHERE agentId = ? ORDER BY createdAt DESC",
411
+ ),
412
+
413
+ getByTaskId: () =>
414
+ getDb().prepare<AgentLogRow, [string]>(
415
+ "SELECT * FROM agent_log WHERE taskId = ? ORDER BY createdAt DESC",
416
+ ),
417
+
418
+ getByEventType: () =>
419
+ getDb().prepare<AgentLogRow, [string]>(
420
+ "SELECT * FROM agent_log WHERE eventType = ? ORDER BY createdAt DESC",
421
+ ),
422
+
423
+ getAll: () =>
424
+ getDb().prepare<AgentLogRow, []>(
425
+ "SELECT * FROM agent_log ORDER BY createdAt DESC",
426
+ ),
427
+ };
428
+
429
+ export function createLogEntry(entry: {
430
+ eventType: AgentLogEventType;
431
+ agentId?: string;
432
+ taskId?: string;
433
+ oldValue?: string;
434
+ newValue?: string;
435
+ metadata?: Record<string, unknown>;
436
+ }): AgentLog {
437
+ const id = crypto.randomUUID();
438
+ const row = logQueries.insert().get(
439
+ id,
440
+ entry.eventType,
441
+ entry.agentId ?? null,
442
+ entry.taskId ?? null,
443
+ entry.oldValue ?? null,
444
+ entry.newValue ?? null,
445
+ entry.metadata ? JSON.stringify(entry.metadata) : null,
446
+ );
447
+ if (!row) throw new Error("Failed to create log entry");
448
+ return rowToAgentLog(row);
449
+ }
450
+
451
+ export function getLogsByAgentId(agentId: string): AgentLog[] {
452
+ return logQueries.getByAgentId().all(agentId).map(rowToAgentLog);
453
+ }
454
+
455
+ export function getLogsByTaskId(taskId: string): AgentLog[] {
456
+ return logQueries.getByTaskId().all(taskId).map(rowToAgentLog);
457
+ }
458
+
459
+ export function getAllLogs(limit?: number): AgentLog[] {
460
+ if (limit) {
461
+ return getDb()
462
+ .prepare<AgentLogRow, [number]>("SELECT * FROM agent_log ORDER BY createdAt DESC LIMIT ?")
463
+ .all(limit)
464
+ .map(rowToAgentLog);
465
+ }
466
+ return logQueries.getAll().all().map(rowToAgentLog);
467
+ }
@@ -198,20 +198,22 @@ export function Setup({ dryRun = false, restore = false }: SetupProps) {
198
198
  addLog(".mcp.json exists");
199
199
  }
200
200
 
201
- // Check if it's a git repo
201
+ // Check if it's a git repo by finding the git root
202
202
  let isGitRepo = false;
203
+ let gitRoot = "";
203
204
  try {
204
- const gitDir = Bun.file(`${cwd}/.git`);
205
- isGitRepo = await gitDir.exists();
205
+ const result = await Bun.$`git -C ${cwd} rev-parse --show-toplevel`.quiet();
206
+ gitRoot = result.text().trim();
207
+ isGitRepo = result.exitCode === 0 && gitRoot.length > 0;
206
208
  } catch {
207
209
  isGitRepo = false;
208
210
  }
209
211
 
210
212
  if (isGitRepo) {
211
- addLog("Git repository detected");
213
+ addLog(`Git repository detected (root: ${gitRoot})`);
212
214
 
213
- // Check .gitignore
214
- const gitignoreFile = Bun.file(`${cwd}/.gitignore`);
215
+ // Check .gitignore at git root
216
+ const gitignoreFile = Bun.file(`${gitRoot}/.gitignore`);
215
217
  let gitignoreContent = "";
216
218
 
217
219
  if (await gitignoreFile.exists()) {
@@ -228,7 +230,7 @@ export function Setup({ dryRun = false, restore = false }: SetupProps) {
228
230
 
229
231
  if (entriesToAdd.length > 0) {
230
232
  // Backup .gitignore before modifying
231
- await createBackup(`${cwd}/.gitignore`);
233
+ await createBackup(`${gitRoot}/.gitignore`);
232
234
  if (!dryRun) {
233
235
  const newEntries = `# Added by ${SERVER_NAME} setup\n${entriesToAdd.join("\n")}\n\n`;
234
236
  await Bun.write(gitignoreFile, newEntries + gitignoreContent);
package/src/http.ts CHANGED
@@ -113,7 +113,7 @@ const httpServer = createHttpServer(async (req, res) => {
113
113
  if (!agent) {
114
114
  res.writeHead(404, { "Content-Type": "application/json" });
115
115
  res.end(JSON.stringify({ error: "Agent not found" }));
116
- return;
116
+ return false;
117
117
  }
118
118
 
119
119
  let status: AgentStatus = "idle";
@@ -123,9 +123,13 @@ const httpServer = createHttpServer(async (req, res) => {
123
123
  }
124
124
 
125
125
  updateAgentStatus(agent.id, status);
126
+
127
+ return true;
126
128
  });
127
129
 
128
- tx();
130
+ if (!tx()) {
131
+ return;
132
+ }
129
133
 
130
134
  res.writeHead(204);
131
135
  res.end();
@@ -145,13 +149,17 @@ const httpServer = createHttpServer(async (req, res) => {
145
149
  if (!agent) {
146
150
  res.writeHead(404, { "Content-Type": "application/json" });
147
151
  res.end(JSON.stringify({ error: "Agent not found" }));
148
- return;
152
+ return false;
149
153
  }
150
154
 
151
155
  updateAgentStatus(agent.id, "offline");
156
+
157
+ return true;
152
158
  });
153
159
 
154
- tx();
160
+ if (!tx()) {
161
+ return;
162
+ }
155
163
 
156
164
  res.writeHead(204);
157
165
  res.end();
package/src/types.ts CHANGED
@@ -40,3 +40,27 @@ export type AgentTask = z.infer<typeof AgentTaskSchema>;
40
40
  export type AgentStatus = z.infer<typeof AgentStatusSchema>;
41
41
  export type Agent = z.infer<typeof AgentSchema>;
42
42
  export type AgentWithTasks = z.infer<typeof AgentWithTasksSchema>;
43
+
44
+ // Agent Log Types
45
+ export const AgentLogEventTypeSchema = z.enum([
46
+ "agent_joined",
47
+ "agent_status_change",
48
+ "agent_left",
49
+ "task_created",
50
+ "task_status_change",
51
+ "task_progress",
52
+ ]);
53
+
54
+ export const AgentLogSchema = z.object({
55
+ id: z.uuid(),
56
+ eventType: AgentLogEventTypeSchema,
57
+ agentId: z.string().optional(),
58
+ taskId: z.string().optional(),
59
+ oldValue: z.string().optional(),
60
+ newValue: z.string().optional(),
61
+ metadata: z.string().optional(),
62
+ createdAt: z.iso.datetime(),
63
+ });
64
+
65
+ export type AgentLogEventType = z.infer<typeof AgentLogEventTypeSchema>;
66
+ export type AgentLog = z.infer<typeof AgentLogSchema>;