@ra3orblade/swarm 0.7.0 → 0.8.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/dist/swarm.js CHANGED
@@ -50,6 +50,22 @@ function readDaemonInfo() {
50
50
  return null;
51
51
  }
52
52
  }
53
+ var tokenFile = (home = swarmHome()) => join(home, "token");
54
+ function readToken(home = swarmHome()) {
55
+ try {
56
+ const t = readFileSync(tokenFile(home), "utf8").trim();
57
+ return /^[a-f0-9]{64}$/.test(t) ? t : null;
58
+ } catch {
59
+ return null;
60
+ }
61
+ }
62
+ var authedFetch = (input, init) => {
63
+ const h = new Headers(init?.headers);
64
+ const t = readToken();
65
+ if (t && !h.has("authorization"))
66
+ h.set("authorization", `Bearer ${t}`);
67
+ return fetch(input, { ...init, headers: h });
68
+ };
53
69
  function alive(pid) {
54
70
  try {
55
71
  process.kill(pid, 0);
@@ -108,7 +124,7 @@ class SwarmClient {
108
124
  f;
109
125
  constructor(opts = {}) {
110
126
  this.baseUrl = resolveBaseUrl(opts.baseUrl);
111
- this.f = opts.fetch ?? fetch;
127
+ this.f = opts.fetch ?? authedFetch;
112
128
  }
113
129
  async health() {
114
130
  const r = await this.f(`${this.baseUrl}/v1/health`);
@@ -127,10 +143,9 @@ class SwarmClient {
127
143
  }
128
144
  }
129
145
 
130
- // packages/cli/src/install.ts
131
- import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
132
- import { homedir as homedir2 } from "os";
133
- import { join as join2 } from "path";
146
+ // packages/core/src/actor.ts
147
+ var HUMAN_ALIASES = new Set(["cli", "dashboard", "me", "desktop", "human"]);
148
+ var DAEMON_ALIASES = new Set(["daemon", "system", "swarm"]);
134
149
  // packages/core/src/adapters/claude-code/hooks.ts
135
150
  var HOOK_EVENTS = [
136
151
  "SessionStart",
@@ -144,24 +159,327 @@ var HOOK_EVENTS = [
144
159
  "Notification",
145
160
  "PreCompact"
146
161
  ];
162
+ // packages/core/src/audit.ts
163
+ var AUDIT_TYPES = new Set([
164
+ "session.started",
165
+ "session.ended",
166
+ "tool.denied",
167
+ "claim.acquired",
168
+ "claim.renewed",
169
+ "claim.released",
170
+ "claim.expired",
171
+ "claim.orphaned",
172
+ "worktree.created",
173
+ "worktree.removed",
174
+ "worktree.bootstrapped",
175
+ "pr.opened",
176
+ "question.asked",
177
+ "question.answered",
178
+ "dispatch.queued",
179
+ "dispatch.started",
180
+ "dispatch.finished",
181
+ "resource.acquired",
182
+ "resource.released",
183
+ "resource.reaped",
184
+ "process.started",
185
+ "process.exited",
186
+ "gate.recorded",
187
+ "handoff.recorded",
188
+ "permission.requested",
189
+ "permission.resolved",
190
+ "incident.opened",
191
+ "incident.acked",
192
+ "run.result"
193
+ ]);
194
+ var AUDIT_TYPES_SQL = [...AUDIT_TYPES].map((t) => `'${t}'`).join(", ");
195
+ var DEFAULT_PRIVACY = {
196
+ store_prompts: true,
197
+ store_reasoning: true,
198
+ redact: []
199
+ };
147
200
  // packages/core/src/budget.ts
148
201
  var BUDGET_ASK_TOOLS = new Set(["Bash", "Edit", "Write", "MultiEdit", "NotebookEdit"]);
202
+ // packages/core/src/config.ts
203
+ import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
204
+ import { join as join2 } from "path";
205
+ var DEFAULT_GATE_TIMEOUT_S = 900;
206
+ var AUTO_MODES = ["session-end", "stop", "off"];
207
+ function parseGateDefs(gates) {
208
+ const out = {};
209
+ if (!isRecord(gates))
210
+ return out;
211
+ for (const [name, v] of Object.entries(gates)) {
212
+ if (!isRecord(v))
213
+ continue;
214
+ const builtin = v.builtin === "review" ? "review" : null;
215
+ const cmd = typeof v.cmd === "string" ? v.cmd.trim() : "";
216
+ if (!cmd && !builtin)
217
+ continue;
218
+ if (!/^[a-z0-9][a-z0-9_.-]{0,39}$/i.test(name))
219
+ continue;
220
+ const t = Number(v.timeout);
221
+ out[name] = {
222
+ cmd: builtin ? "" : cmd,
223
+ timeout: Number.isFinite(t) && t > 0 ? Math.min(t, 86400) : builtin ? 600 : DEFAULT_GATE_TIMEOUT_S,
224
+ cwd: isRepoRelative(v.cwd) ? v.cwd.trim() : null,
225
+ builtin,
226
+ model: typeof v.model === "string" && v.model.trim() ? v.model.trim() : null
227
+ };
228
+ }
229
+ return out;
230
+ }
231
+ var DEFAULT_CONFIG = {
232
+ daemon: { port: 7777, auth: "loopback-optional" },
233
+ tasks: { source: null, labels: [], team: null },
234
+ gates: { required: [], auto: "session-end", defs: {} },
235
+ budget: { daily: null, weekly: null, warn_at: 0.8, on_exceed: "warn" },
236
+ events: { retain_days: 30 },
237
+ audit: { retain_days: 0 },
238
+ privacy: DEFAULT_PRIVACY,
239
+ dispatch: {
240
+ max_parallel: 2,
241
+ permission_mode: null,
242
+ model: null,
243
+ max_turns: null,
244
+ require_pr: true,
245
+ profile: null
246
+ },
247
+ worktree: { setup: null, copy: [], open: null },
248
+ rules: {
249
+ shared_tree: "ask",
250
+ destructive_git: "ask",
251
+ pattern_kill: "ask",
252
+ protected_ports: "ask",
253
+ no_foreign_worktree: "ask",
254
+ claim_required_to_write: "off",
255
+ protected: { ports: [] }
256
+ }
257
+ };
258
+ var MODES = ["ask", "deny", "off"];
259
+ function isRecord(v) {
260
+ return typeof v === "object" && v !== null && !Array.isArray(v);
261
+ }
262
+ function merge(a, b) {
263
+ if (!isRecord(a) || !isRecord(b))
264
+ return b === undefined ? a : b;
265
+ const out = { ...a };
266
+ for (const [k, v] of Object.entries(b))
267
+ out[k] = merge(a[k], v);
268
+ return out;
269
+ }
270
+ function parseToml(text, source) {
271
+ try {
272
+ return Bun.TOML.parse(text) ?? {};
273
+ } catch (e) {
274
+ console.error(`swarm: ignoring invalid TOML in ${source}: ${e.message}`);
275
+ return {};
276
+ }
277
+ }
278
+ function isRepoRelative(f) {
279
+ if (typeof f !== "string")
280
+ return false;
281
+ const t = f.trim();
282
+ if (!t || t.startsWith("/") || t.startsWith("\\") || /^[a-zA-Z]:/.test(t))
283
+ return false;
284
+ return !t.split(/[/\\]/).some((seg) => seg === "..");
285
+ }
286
+ var days = (v, fallback) => {
287
+ const n = Number(v);
288
+ return Number.isInteger(n) && n >= 0 ? Math.min(n, 3650) : fallback;
289
+ };
290
+ function validate(c) {
291
+ const mode = (v, fallback) => MODES.includes(v) ? v : fallback;
292
+ const port = Number(c.daemon?.port);
293
+ const source = c.tasks?.source;
294
+ const setup = c.worktree?.setup;
295
+ const opener = c.worktree?.open;
296
+ const rawGates = c.gates;
297
+ const d = c.dispatch ?? {};
298
+ const mp = Number(d.max_parallel);
299
+ const mt = Number(d.max_turns);
300
+ const str = (v) => typeof v === "string" && v.trim() ? v.trim() : null;
301
+ const b = c.budget ?? {};
302
+ const usd = (v) => {
303
+ const n = Number(v);
304
+ return Number.isFinite(n) && n > 0 ? n : null;
305
+ };
306
+ const warnAt = Number(b.warn_at);
307
+ const auto = rawGates?.auto;
308
+ return {
309
+ ...c,
310
+ daemon: {
311
+ port: Number.isInteger(port) && port > 0 && port < 65536 ? port : 7777,
312
+ auth: c.daemon?.auth === "required" ? "required" : "loopback-optional"
313
+ },
314
+ tasks: {
315
+ source: typeof source === "string" && source.trim() && !source.startsWith("/") ? source.trim() : null,
316
+ labels: Array.isArray(c.tasks?.labels) ? c.tasks.labels.filter((l) => typeof l === "string" && l.trim() !== "") : [],
317
+ team: typeof c.tasks?.team === "string" && c.tasks.team.trim() ? c.tasks.team.trim() : null
318
+ },
319
+ gates: {
320
+ required: Array.isArray(rawGates?.required) ? rawGates.required.filter((g) => typeof g === "string" && g.trim() !== "") : [],
321
+ auto: AUTO_MODES.includes(auto) ? auto : "session-end",
322
+ defs: parseGateDefs(rawGates)
323
+ },
324
+ budget: {
325
+ daily: usd(b.daily),
326
+ weekly: usd(b.weekly),
327
+ warn_at: Number.isFinite(warnAt) && warnAt > 0 && warnAt < 1 ? warnAt : 0.8,
328
+ on_exceed: b.on_exceed === "ask" || b.on_exceed === "stop" ? b.on_exceed : "warn"
329
+ },
330
+ events: {
331
+ retain_days: days(c.events?.retain_days, 30)
332
+ },
333
+ audit: {
334
+ retain_days: days(c.audit?.retain_days, 0)
335
+ },
336
+ privacy: {
337
+ store_prompts: c.privacy?.store_prompts !== false,
338
+ store_reasoning: c.privacy?.store_reasoning !== false,
339
+ redact: Array.isArray(c.privacy?.redact) ? c.privacy.redact.filter((r) => typeof r === "string" && r.length > 0) : []
340
+ },
341
+ dispatch: {
342
+ max_parallel: Number.isInteger(mp) && mp > 0 ? Math.min(mp, 16) : 2,
343
+ permission_mode: str(d.permission_mode),
344
+ model: str(d.model),
345
+ max_turns: Number.isInteger(mt) && mt > 0 ? mt : null,
346
+ require_pr: d.require_pr === undefined ? true : d.require_pr === true,
347
+ profile: ["full", "no-edits", "read-only"].includes(String(d.profile)) ? String(d.profile) : null
348
+ },
349
+ worktree: {
350
+ setup: typeof setup === "string" && setup.trim() ? setup.trim() : null,
351
+ copy: Array.isArray(c.worktree?.copy) ? c.worktree.copy.filter((f) => isRepoRelative(f)) : [],
352
+ open: typeof opener === "string" && opener.trim() ? opener.trim() : null
353
+ },
354
+ rules: {
355
+ ...c.rules,
356
+ shared_tree: mode(c.rules?.shared_tree, "ask"),
357
+ destructive_git: mode(c.rules?.destructive_git, "ask"),
358
+ pattern_kill: mode(c.rules?.pattern_kill, "ask"),
359
+ protected_ports: mode(c.rules?.protected_ports, "ask"),
360
+ no_foreign_worktree: mode(c.rules?.no_foreign_worktree, "ask"),
361
+ claim_required_to_write: mode(c.rules?.claim_required_to_write, "off"),
362
+ protected: {
363
+ ports: Array.isArray(c.rules?.protected?.ports) ? c.rules.protected.ports.filter((p) => Number.isInteger(p) && p > 0 && p < 65536) : []
364
+ }
365
+ }
366
+ };
367
+ }
368
+ function leafPaths(v, prefix = "") {
369
+ if (!isRecord(v))
370
+ return prefix ? [prefix] : [];
371
+ const keys = Object.keys(v);
372
+ if (keys.length === 0)
373
+ return prefix ? [prefix] : [];
374
+ return keys.flatMap((k) => leafPaths(v[k], prefix ? `${prefix}.${k}` : k));
375
+ }
376
+ function getPath(v, path) {
377
+ let cur = v;
378
+ for (const seg of path.split(".")) {
379
+ if (!isRecord(cur))
380
+ return;
381
+ cur = cur[seg];
382
+ }
383
+ return cur;
384
+ }
385
+ function setPath(obj, path, value) {
386
+ const segs = path.split(".");
387
+ let cur = obj;
388
+ for (const seg of segs.slice(0, -1)) {
389
+ if (!isRecord(cur[seg]))
390
+ cur[seg] = {};
391
+ cur = cur[seg];
392
+ }
393
+ cur[segs[segs.length - 1]] = value;
394
+ }
395
+ var isLockedBy = (path, lock) => path === lock || path.startsWith(`${lock}.`);
396
+ function readLayer(path) {
397
+ return existsSync3(path) ? parseToml(readFileSync2(path, "utf8"), path) : null;
398
+ }
399
+ function loadConfigDetailed(opts = {}) {
400
+ const home = opts.home ?? process.env.SWARM_HOME ?? join2(process.env.HOME ?? "", ".swarm");
401
+ const policyPath = opts.policy ?? process.env.SWARM_POLICY ?? join2(home, "policy.toml");
402
+ const policyRaw = readLayer(policyPath);
403
+ const locked = Array.isArray(policyRaw?.locked) ? policyRaw.locked.filter((k) => typeof k === "string" && /^[a-z0-9_.-]+$/i.test(k)) : [];
404
+ const policy = { ...policyRaw ?? {} };
405
+ delete policy.locked;
406
+ const layers = [
407
+ ["policy", policyRaw ? policy : null],
408
+ ["global", readLayer(join2(home, "config.toml"))],
409
+ ["repo", opts.repoRoot ? readLayer(join2(opts.repoRoot, ".swarm.toml")) : null]
410
+ ];
411
+ const provenance = {};
412
+ for (const p of leafPaths(DEFAULT_CONFIG))
413
+ provenance[p] = "default";
414
+ const overridden = [];
415
+ let cfg = DEFAULT_CONFIG;
416
+ for (const [layer, raw] of layers) {
417
+ if (!raw)
418
+ continue;
419
+ for (const p of leafPaths(raw)) {
420
+ const lock = layer !== "policy" && locked.find((l) => isLockedBy(p, l));
421
+ if (lock)
422
+ overridden.push({ key: p, layer, attempted: getPath(raw, p) });
423
+ else
424
+ provenance[p] = layer;
425
+ }
426
+ cfg = merge(cfg, raw);
427
+ }
428
+ if (overridden.length) {
429
+ const out = structuredClone(cfg);
430
+ for (const { key } of overridden) {
431
+ const fromPolicy = getPath(policy, key);
432
+ setPath(out, key, fromPolicy === undefined ? getPath(DEFAULT_CONFIG, key) : fromPolicy);
433
+ provenance[key] = fromPolicy === undefined ? "default" : "policy";
434
+ }
435
+ cfg = out;
436
+ }
437
+ return {
438
+ config: validate(cfg),
439
+ provenance,
440
+ overridden,
441
+ policy: { path: policyRaw ? policyPath : null, locked }
442
+ };
443
+ }
149
444
  // packages/core/src/rules.ts
150
445
  var LIVE_WINDOW_MS = 10 * 60000;
151
446
  var WRITE_TOOLS = new Set(["Write", "Edit", "MultiEdit", "NotebookEdit"]);
152
447
  // packages/core/src/ledger.ts
153
448
  var EDIT_TOOLS = new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
449
+ // packages/core/src/policy.ts
450
+ var HOOK_MARK = "swarm-hook";
451
+ var hookIsOurs = (h) => typeof h.command === "string" && (h.command.includes(HOOK_MARK) || h.command.includes("/packages/hook/src/bin.ts"));
452
+ var MIN_HOOK_TIMEOUT_S = 5;
453
+ function hookCoverage(settings) {
454
+ const hooks = settings && typeof settings === "object" && !Array.isArray(settings) ? settings.hooks : undefined;
455
+ const missing = [];
456
+ const short = [];
457
+ for (const ev of HOOK_EVENTS) {
458
+ const groups = Array.isArray(hooks?.[ev]) ? hooks?.[ev] : [];
459
+ const ours = groups.flatMap((g) => {
460
+ const list = g?.hooks;
461
+ return Array.isArray(list) ? list.filter((h) => hookIsOurs(h)) : [];
462
+ });
463
+ if (!ours.length)
464
+ missing.push(ev);
465
+ else if (ours.every((h) => typeof h.timeout === "number" && h.timeout < MIN_HOOK_TIMEOUT_S))
466
+ short.push(ev);
467
+ }
468
+ return { missing, short, complete: !missing.length && !short.length };
469
+ }
154
470
  // packages/cli/src/install.ts
155
- var MARK = "swarm-hook";
156
- var isOurs = (h) => h.command.includes(MARK) || h.command.includes("/packages/hook/src/bin.ts");
157
- var settingsPath = () => process.env.CLAUDE_SETTINGS ?? join2(homedir2(), ".claude", "settings.json");
158
- var claudeJsonPath = () => process.env.CLAUDE_JSON ?? join2(homedir2(), ".claude.json");
471
+ import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
472
+ import { homedir as homedir2 } from "os";
473
+ import { join as join3 } from "path";
474
+ var isOurs = hookIsOurs;
475
+ var settingsPath = () => process.env.CLAUDE_SETTINGS ?? join3(homedir2(), ".claude", "settings.json");
476
+ var claudeJsonPath = () => process.env.CLAUDE_JSON ?? join3(homedir2(), ".claude.json");
159
477
  function loadClaudeJson() {
160
478
  const p = claudeJsonPath();
161
- if (!existsSync3(p))
479
+ if (!existsSync4(p))
162
480
  return {};
163
481
  try {
164
- return JSON.parse(readFileSync2(p, "utf8"));
482
+ return JSON.parse(readFileSync3(p, "utf8"));
165
483
  } catch {
166
484
  return {};
167
485
  }
@@ -194,8 +512,8 @@ function mcpRegistered() {
194
512
  const c = loadClaudeJson();
195
513
  return Boolean(c.mcpServers?.swarm);
196
514
  }
197
- var codexConfigPath = () => process.env.CODEX_CONFIG ?? join2(homedir2(), ".codex", "config.toml");
198
- var geminiSettingsPath = () => process.env.GEMINI_SETTINGS ?? join2(homedir2(), ".gemini", "settings.json");
515
+ var codexConfigPath = () => process.env.CODEX_CONFIG ?? join3(homedir2(), ".codex", "config.toml");
516
+ var geminiSettingsPath = () => process.env.GEMINI_SETTINGS ?? join3(homedir2(), ".gemini", "settings.json");
199
517
  function codexBlock() {
200
518
  const { command, args } = mcpServerConfig();
201
519
  return `[mcp_servers.swarm]
@@ -206,9 +524,9 @@ args = ${JSON.stringify(args)}
206
524
  var CODEX_BLOCK_RE = /\[mcp_servers\.swarm\]\n(?:(?!\[)[^\n]*\n?)*/;
207
525
  function registerCodex() {
208
526
  const p = codexConfigPath();
209
- if (!existsSync3(join2(p, "..")))
527
+ if (!existsSync4(join3(p, "..")))
210
528
  return false;
211
- const cur = existsSync3(p) ? readFileSync2(p, "utf8") : "";
529
+ const cur = existsSync4(p) ? readFileSync3(p, "utf8") : "";
212
530
  const next = CODEX_BLOCK_RE.test(cur) ? cur.replace(CODEX_BLOCK_RE, codexBlock()) : `${cur.trimEnd()}${cur.trim() ? `
213
531
 
214
532
  ` : ""}${codexBlock()}`;
@@ -218,9 +536,9 @@ function registerCodex() {
218
536
  }
219
537
  function unregisterCodex() {
220
538
  const p = codexConfigPath();
221
- if (!existsSync3(p))
539
+ if (!existsSync4(p))
222
540
  return false;
223
- const cur = readFileSync2(p, "utf8");
541
+ const cur = readFileSync3(p, "utf8");
224
542
  if (!CODEX_BLOCK_RE.test(cur))
225
543
  return false;
226
544
  writeFileSync2(p, cur.replace(CODEX_BLOCK_RE, "").replace(/\n{3,}/g, `
@@ -231,11 +549,11 @@ function unregisterCodex() {
231
549
  }
232
550
  function registerGemini() {
233
551
  const p = geminiSettingsPath();
234
- if (!existsSync3(join2(p, "..")))
552
+ if (!existsSync4(join3(p, "..")))
235
553
  return false;
236
554
  let c = {};
237
555
  try {
238
- c = existsSync3(p) ? JSON.parse(readFileSync2(p, "utf8")) : {};
556
+ c = existsSync4(p) ? JSON.parse(readFileSync3(p, "utf8")) : {};
239
557
  } catch {
240
558
  return false;
241
559
  }
@@ -248,10 +566,10 @@ function registerGemini() {
248
566
  }
249
567
  function unregisterGemini() {
250
568
  const p = geminiSettingsPath();
251
- if (!existsSync3(p))
569
+ if (!existsSync4(p))
252
570
  return false;
253
571
  try {
254
- const c = JSON.parse(readFileSync2(p, "utf8"));
572
+ const c = JSON.parse(readFileSync3(p, "utf8"));
255
573
  const mcp = c.mcpServers ?? {};
256
574
  if (!mcp.swarm)
257
575
  return false;
@@ -291,7 +609,7 @@ function mcpServerConfig() {
291
609
  }
292
610
  function load() {
293
611
  const p = settingsPath();
294
- return existsSync3(p) ? JSON.parse(readFileSync2(p, "utf8")) : {};
612
+ return existsSync4(p) ? JSON.parse(readFileSync3(p, "utf8")) : {};
295
613
  }
296
614
  function save(s) {
297
615
  writeFileSync2(settingsPath(), `${JSON.stringify(s, null, 2)}
@@ -368,21 +686,28 @@ function status() {
368
686
  const installed = Object.values(hooks2).some((l) => l.some((g) => g.hooks.some(isOurs)));
369
687
  const otherAgents = [];
370
688
  const cp = codexConfigPath();
371
- if (existsSync3(cp) && CODEX_BLOCK_RE.test(readFileSync2(cp, "utf8")))
689
+ if (existsSync4(cp) && CODEX_BLOCK_RE.test(readFileSync3(cp, "utf8")))
372
690
  otherAgents.push("codex");
373
691
  const gp = geminiSettingsPath();
374
692
  try {
375
- if (existsSync3(gp) && JSON.parse(readFileSync2(gp, "utf8")).mcpServers?.swarm)
693
+ if (existsSync4(gp) && JSON.parse(readFileSync3(gp, "utf8")).mcpServers?.swarm)
376
694
  otherAgents.push("gemini");
377
695
  } catch {}
378
- return { installed, mcp: mcpRegistered(), path: settingsPath(), shim: shimPath(), otherAgents };
696
+ return {
697
+ installed,
698
+ coverage: hookCoverage(s),
699
+ mcp: mcpRegistered(),
700
+ path: settingsPath(),
701
+ shim: shimPath(),
702
+ otherAgents
703
+ };
379
704
  }
380
705
 
381
706
  // packages/cli/src/procs.ts
382
707
  import { mkdirSync as mkdirSync2, openSync } from "fs";
383
- import { join as join3, resolve as resolve2 } from "path";
708
+ import { join as join4, resolve as resolve2 } from "path";
384
709
  async function call(path, init) {
385
- const r = await fetch(`${new SwarmClient().baseUrl}${path}`, init);
710
+ const r = await authedFetch(`${new SwarmClient().baseUrl}${path}`, init);
386
711
  return await r.json();
387
712
  }
388
713
  var post = (path, body) => call(path, {
@@ -410,9 +735,9 @@ async function start(opts) {
410
735
  port = a.port;
411
736
  }
412
737
  }
413
- const logDir = join3(swarmHome(), "logs", slug(pid));
738
+ const logDir = join4(swarmHome(), "logs", slug(pid));
414
739
  mkdirSync2(logDir, { recursive: true });
415
- const log = join3(logDir, `${slug(opts.name)}.log`);
740
+ const log = join4(logDir, `${slug(opts.name)}.log`);
416
741
  const fd = openSync(log, "a");
417
742
  const cmdline = opts.cmd.join(" ");
418
743
  const child = Bun.spawn(["sh", "-c", cmdline], {
@@ -477,6 +802,14 @@ function fmt(r) {
477
802
  }
478
803
 
479
804
  // packages/cli/src/bin.ts
805
+ function gitToplevel() {
806
+ const r = Bun.spawnSync(["git", "rev-parse", "--show-toplevel"], {
807
+ stdout: "pipe",
808
+ stderr: "ignore"
809
+ });
810
+ const out = r.exitCode === 0 ? r.stdout.toString().trim() : "";
811
+ return out || null;
812
+ }
480
813
  var [cmd = "help", ...rest] = process.argv.slice(2);
481
814
  var json = rest.includes("--json");
482
815
  var arg = () => rest.find((a) => !a.startsWith("--"));
@@ -518,13 +851,14 @@ var help = `swarm \u2014 control plane for AI-agent development
518
851
  stats [-p] [--json] all-time totals, streak, records (the dashboard's Stats view)
519
852
  search <query\u2026> [-p] [--kind handoff|incident|gate|session] [--json] memory over Swarm's own data (handoffs, incidents, gates, what sessions said)
520
853
  rules dryrun [--set rule=mode,\u2026] [--limit n] [--json] replay this repo's history under rule modes; shows what would fire + flaky signals
854
+ audit export [--since 30d|ISO] [-p] [--type claim.acquired] [--format jsonl|csv|json] [--limit n] the audit log (ledger changes + decisions, with actor) to stdout
521
855
 
522
856
  install | uninstall add/remove Swarm hooks in ~/.claude/settings.json
523
857
 
524
858
  Env: SWARM_URL, SWARM_PORT (default 7777), SWARM_HOME (~/.swarm)`;
525
859
  async function api(path, init) {
526
860
  const base = new SwarmClient().baseUrl;
527
- const r = await fetch(`${base}${path}`, init);
861
+ const r = await authedFetch(`${base}${path}`, init);
528
862
  if (!r.ok)
529
863
  throw new Error(`${path}: ${r.status} ${await r.text()}`);
530
864
  return r.status === 204 ? null : r.json();
@@ -597,7 +931,24 @@ Open the dashboard: ${base}`);
597
931
  line(Boolean(bun), `bun ${bun ? `(${bun})` : ""}`, "install bun: https://bun.sh");
598
932
  line(Boolean(claude), "claude CLI on PATH", "install Claude Code: https://claude.com/claude-code");
599
933
  line(running, `daemon ${info ? `(pid ${info.pid}, ${info.url})` : ""}`, "run: swarm start");
934
+ if (running) {
935
+ const h = await authedFetch(`${resolveBaseUrl()}/v1/health`).then((r) => r.json()).catch(() => null);
936
+ if (h)
937
+ console.log(`\xB7 daemon v${h.version ?? "?"} \xB7 schema v${h.schema ?? 0}`);
938
+ }
600
939
  line(st.installed, "hooks installed", "run: swarm install");
940
+ if (st.installed && !st.coverage.complete) {
941
+ if (st.coverage.missing.length)
942
+ line(false, `hooks missing: ${st.coverage.missing.join(", ")}`, "run: swarm install");
943
+ if (st.coverage.short.length)
944
+ line(false, `hook timeout too short: ${st.coverage.short.join(", ")}`, "run: swarm install");
945
+ }
946
+ const pol = loadConfigDetailed({ repoRoot: gitToplevel() });
947
+ if (pol.policy.path) {
948
+ line(true, `policy ${pol.policy.path} (locked: ${pol.policy.locked.join(", ") || "nothing"})`, "");
949
+ for (const o of pol.overridden)
950
+ line(false, `${o.layer} config overrides locked ${o.key}`, `remove it \u2014 policy value stays in effect`);
951
+ }
601
952
  line(st.mcp, "MCP server registered", "run: swarm install");
602
953
  if (st.otherAgents.length)
603
954
  console.log(`\u2713 MCP server also registered for ${st.otherAgents.join(", ")} (swarm_* tools in those CLIs too)`);
@@ -645,7 +996,7 @@ url: ${resolveBaseUrl()}`);
645
996
  headers: { "content-type": "application/json" },
646
997
  body: JSON.stringify({ path: resolve3(".") })
647
998
  });
648
- const r = await fetch(`${new SwarmClient().baseUrl}/v1/claims`, {
999
+ const r = await authedFetch(`${new SwarmClient().baseUrl}/v1/claims`, {
649
1000
  method: "POST",
650
1001
  headers: { "content-type": "application/json" },
651
1002
  body: JSON.stringify({ projectId: proj.id, task, owner })
@@ -673,7 +1024,7 @@ url: ${resolveBaseUrl()}`);
673
1024
  headers: { "content-type": "application/json" },
674
1025
  body: JSON.stringify({ path: resolve3(".") })
675
1026
  });
676
- const r = await fetch(`${new SwarmClient().baseUrl}/v1/claims/${cmd}`, {
1027
+ const r = await authedFetch(`${new SwarmClient().baseUrl}/v1/claims/${cmd}`, {
677
1028
  method: "POST",
678
1029
  headers: { "content-type": "application/json" },
679
1030
  body: JSON.stringify({ projectId: proj.id, task, force: rest.includes("--force") })
@@ -696,7 +1047,7 @@ url: ${resolveBaseUrl()}`);
696
1047
  headers: { "content-type": "application/json" },
697
1048
  body: JSON.stringify({ path: resolve3(".") })
698
1049
  });
699
- const post2 = (path, body) => fetch(`${new SwarmClient().baseUrl}${path}`, {
1050
+ const post2 = (path, body) => authedFetch(`${new SwarmClient().baseUrl}${path}`, {
700
1051
  method: "POST",
701
1052
  headers: { "content-type": "application/json" },
702
1053
  body: JSON.stringify({ projectId: proj.id, ...body })
@@ -852,7 +1203,7 @@ url: ${resolveBaseUrl()}`);
852
1203
  const text = words.join(" ");
853
1204
  if (!id || !text)
854
1205
  throw new Error("usage: swarm answer <id> <text\u2026>");
855
- const r = await fetch(`${new SwarmClient().baseUrl}/v1/questions/${id}/answer`, {
1206
+ const r = await authedFetch(`${new SwarmClient().baseUrl}/v1/questions/${id}/answer`, {
856
1207
  method: "POST",
857
1208
  headers: { "content-type": "application/json" },
858
1209
  body: JSON.stringify({ text, by: process.env.USER ?? "cli" })
@@ -904,7 +1255,7 @@ url: ${resolveBaseUrl()}`);
904
1255
  break;
905
1256
  }
906
1257
  if (sub === "clear") {
907
- const r2 = await fetch(`${new SwarmClient().baseUrl}/v1/dispatch`, {
1258
+ const r2 = await authedFetch(`${new SwarmClient().baseUrl}/v1/dispatch`, {
908
1259
  method: "DELETE",
909
1260
  headers: { "content-type": "application/json" },
910
1261
  body: JSON.stringify({ projectId: proj.id, task: positional[1] })
@@ -912,7 +1263,7 @@ url: ${resolveBaseUrl()}`);
912
1263
  console.log(json ? JSON.stringify(r2) : `cleared ${r2.cleared}`);
913
1264
  break;
914
1265
  }
915
- const r = await fetch(`${new SwarmClient().baseUrl}/v1/dispatch`, {
1266
+ const r = await authedFetch(`${new SwarmClient().baseUrl}/v1/dispatch`, {
916
1267
  method: "POST",
917
1268
  headers: { "content-type": "application/json" },
918
1269
  body: JSON.stringify({
@@ -978,7 +1329,7 @@ watch: swarm dispatch status \xB7 swarm run ls \xB7 the Board`);
978
1329
  ${flag("--body") ?? d.body}`);
979
1330
  break;
980
1331
  }
981
- const r = await fetch(`${new SwarmClient().baseUrl}/v1/prs/open`, {
1332
+ const r = await authedFetch(`${new SwarmClient().baseUrl}/v1/prs/open`, {
982
1333
  method: "POST",
983
1334
  headers: { "content-type": "application/json" },
984
1335
  body: JSON.stringify({
@@ -1070,11 +1421,11 @@ ${flag("--body") ?? d.body}`);
1070
1421
  const target = positionals[1];
1071
1422
  if (!target)
1072
1423
  throw new Error(`usage: swarm run ${sub} <task|id>${sub === "send" ? ' "text"' : ""}`);
1073
- const r2 = sub === "send" ? await fetch(`${base}/v1/runs/${encodeURIComponent(target)}/send`, {
1424
+ const r2 = sub === "send" ? await authedFetch(`${base}/v1/runs/${encodeURIComponent(target)}/send`, {
1074
1425
  method: "POST",
1075
1426
  headers: { "content-type": "application/json" },
1076
1427
  body: JSON.stringify({ text: positionals.slice(2).join(" ") })
1077
- }) : await fetch(`${base}/v1/runs/${encodeURIComponent(target)}`, { method: "DELETE" });
1428
+ }) : await authedFetch(`${base}/v1/runs/${encodeURIComponent(target)}`, { method: "DELETE" });
1078
1429
  const j = await r2.json();
1079
1430
  if (json)
1080
1431
  console.log(JSON.stringify(j));
@@ -1096,7 +1447,7 @@ ${flag("--body") ?? d.body}`);
1096
1447
  prompt = await Bun.file(resolve3(pf)).text();
1097
1448
  if (!task || !prompt)
1098
1449
  throw new Error('usage: swarm run --task <id> --prompt "\u2026" | --prompt-file f [--model] [--permission-mode] [--profile p] [--allowed-tools a,b] [--max-turns n]');
1099
- const r = await fetch(resumeFrom ? `${base}/v1/sessions/${encodeURIComponent(resumeFrom)}/resume` : `${base}/v1/runs`, {
1450
+ const r = await authedFetch(resumeFrom ? `${base}/v1/sessions/${encodeURIComponent(resumeFrom)}/resume` : `${base}/v1/runs`, {
1100
1451
  method: "POST",
1101
1452
  headers: { "content-type": "application/json" },
1102
1453
  body: JSON.stringify({
@@ -1137,7 +1488,7 @@ ${flag("--body") ?? d.body}`);
1137
1488
  body: JSON.stringify({ path: resolve3(".") })
1138
1489
  });
1139
1490
  if (cmd === "resume") {
1140
- const r2 = await fetch(`${new SwarmClient().baseUrl}/v1/handoffs?project=${proj.id}&task=${encodeURIComponent(task)}`);
1491
+ const r2 = await authedFetch(`${new SwarmClient().baseUrl}/v1/handoffs?project=${proj.id}&task=${encodeURIComponent(task)}`);
1141
1492
  const j = await r2.json();
1142
1493
  if (json)
1143
1494
  console.log(JSON.stringify(j.handoff));
@@ -1149,7 +1500,7 @@ ${flag("--body") ?? d.body}`);
1149
1500
  const i = rest.indexOf(n);
1150
1501
  return i >= 0 ? rest[i + 1] : undefined;
1151
1502
  };
1152
- const r = await fetch(`${new SwarmClient().baseUrl}/v1/handoffs`, {
1503
+ const r = await authedFetch(`${new SwarmClient().baseUrl}/v1/handoffs`, {
1153
1504
  method: "POST",
1154
1505
  headers: { "content-type": "application/json" },
1155
1506
  body: JSON.stringify({
@@ -1198,7 +1549,7 @@ ${flag("--body") ?? d.body}`);
1198
1549
  const [, task, gate, verdict] = positionals;
1199
1550
  if (!task || !gate || !verdict)
1200
1551
  throw new Error('usage: swarm gate record <task> <gate> pass|fail --rubric "what was checked" [--evidence "\u2026"]');
1201
- const r = await fetch(`${new SwarmClient().baseUrl}/v1/gates`, {
1552
+ const r = await authedFetch(`${new SwarmClient().baseUrl}/v1/gates`, {
1202
1553
  method: "POST",
1203
1554
  headers: { "content-type": "application/json" },
1204
1555
  body: JSON.stringify({
@@ -1225,7 +1576,7 @@ ${flag("--body") ?? d.body}`);
1225
1576
  const [, task, ...gates2] = positionals;
1226
1577
  if (!task)
1227
1578
  throw new Error("usage: swarm gate run <task> [gate\u2026] (default: the required gates that have a cmd)");
1228
- const r = await fetch(`${new SwarmClient().baseUrl}/v1/gates/run`, {
1579
+ const r = await authedFetch(`${new SwarmClient().baseUrl}/v1/gates/run`, {
1229
1580
  method: "POST",
1230
1581
  headers: { "content-type": "application/json" },
1231
1582
  body: JSON.stringify({
@@ -1309,6 +1660,36 @@ ${flag("--body") ?? d.body}`);
1309
1660
  session ${h.sessionId}` : ""}`);
1310
1661
  break;
1311
1662
  }
1663
+ case "audit": {
1664
+ if (rest[0] !== "export")
1665
+ throw new Error("usage: swarm audit export [--since 30d] [-p] [--type t] [--format jsonl|csv|json] [--limit n]");
1666
+ await ensureDaemon({ quiet: true });
1667
+ const q = new URLSearchParams;
1668
+ const val = (n) => {
1669
+ const i = rest.indexOf(n);
1670
+ return i >= 0 ? rest[i + 1] : undefined;
1671
+ };
1672
+ if (val("--since"))
1673
+ q.set("since", val("--since"));
1674
+ if (val("--type"))
1675
+ q.set("type", val("--type"));
1676
+ if (val("--limit"))
1677
+ q.set("limit", val("--limit"));
1678
+ q.set("format", val("--format") ?? "jsonl");
1679
+ if (rest.includes("-p")) {
1680
+ const proj = await api("/v1/projects", {
1681
+ method: "POST",
1682
+ headers: { "content-type": "application/json" },
1683
+ body: JSON.stringify({ path: resolve3(".") })
1684
+ });
1685
+ q.set("project", proj.id);
1686
+ }
1687
+ const r = await authedFetch(`${new SwarmClient().baseUrl}/v1/audit?${q}`);
1688
+ if (!r.ok)
1689
+ throw new Error((await r.json()).error ?? `audit: ${r.status}`);
1690
+ process.stdout.write(await r.text());
1691
+ break;
1692
+ }
1312
1693
  case "rules": {
1313
1694
  if (rest[0] !== "dryrun")
1314
1695
  throw new Error("usage: swarm rules dryrun [--set rule=mode,\u2026] [--limit n]");
@@ -1539,7 +1920,7 @@ would have fired (newest last):`);
1539
1920
  headers: { "content-type": "application/json" },
1540
1921
  body: JSON.stringify({ path: resolve3(".") })
1541
1922
  });
1542
- const r = await fetch(`${new SwarmClient().baseUrl}/v1/resources`, {
1923
+ const r = await authedFetch(`${new SwarmClient().baseUrl}/v1/resources`, {
1543
1924
  method: "POST",
1544
1925
  headers: { "content-type": "application/json" },
1545
1926
  body: JSON.stringify({
@@ -1575,7 +1956,7 @@ would have fired (newest last):`);
1575
1956
  });
1576
1957
  if (rest.includes("--force"))
1577
1958
  q.set("force", "1");
1578
- const r = await fetch(`${new SwarmClient().baseUrl}/v1/resources/${encodeURIComponent(name)}?${q}`, { method: "DELETE" }).then((x) => x.json());
1959
+ const r = await authedFetch(`${new SwarmClient().baseUrl}/v1/resources/${encodeURIComponent(name)}?${q}`, { method: "DELETE" }).then((x) => x.json());
1579
1960
  if (json)
1580
1961
  console.log(JSON.stringify(r));
1581
1962
  else if (r.ok)
@@ -1593,7 +1974,7 @@ would have fired (newest last):`);
1593
1974
  const base = new SwarmClient().baseUrl;
1594
1975
  const pIdx = rest.indexOf("--session");
1595
1976
  const wantSession = pIdx >= 0 ? rest[pIdx + 1] : undefined;
1596
- const res = await fetch(`${base}/v1/events?since=0`);
1977
+ const res = await authedFetch(`${base}/v1/events?since=0`);
1597
1978
  const reader = res.body?.getReader();
1598
1979
  if (!reader)
1599
1980
  throw new Error("no stream");
@@ -1625,7 +2006,8 @@ would have fired (newest last):`);
1625
2006
  }
1626
2007
  case "ui": {
1627
2008
  const base = await ensureDaemon();
1628
- Bun.spawn(["open", base]).unref?.();
2009
+ const tok = readToken();
2010
+ Bun.spawn(["open", tok ? `${base}/?token=${tok}` : base]).unref?.();
1629
2011
  console.log(base);
1630
2012
  break;
1631
2013
  }