@ra3orblade/swarm 0.7.0 → 0.9.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
@@ -2,7 +2,7 @@
2
2
  // @bun
3
3
 
4
4
  // packages/cli/src/bin.ts
5
- import { resolve as resolve3 } from "path";
5
+ import { join as join5, resolve as resolve3 } from "path";
6
6
 
7
7
  // packages/client/src/daemon.ts
8
8
  import { existsSync as existsSync2, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
@@ -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,382 @@ 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
+ "workflow.started",
194
+ "workflow.finished"
195
+ ]);
196
+ var AUDIT_TYPES_SQL = [...AUDIT_TYPES].map((t) => `'${t}'`).join(", ");
197
+ var DEFAULT_PRIVACY = {
198
+ store_prompts: true,
199
+ store_reasoning: true,
200
+ redact: []
201
+ };
147
202
  // packages/core/src/budget.ts
148
203
  var BUDGET_ASK_TOOLS = new Set(["Bash", "Edit", "Write", "MultiEdit", "NotebookEdit"]);
204
+ // packages/core/src/config.ts
205
+ import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
206
+ import { join as join2 } from "path";
207
+
208
+ // packages/core/src/workflows.ts
209
+ var NAME_RE = /^[a-z0-9][a-z0-9_.-]{0,39}$/i;
210
+ function isRecord(v) {
211
+ return typeof v === "object" && v !== null && !Array.isArray(v);
212
+ }
213
+ function parseWorkflows(raw) {
214
+ const out = {};
215
+ if (!Array.isArray(raw))
216
+ return out;
217
+ for (const w of raw) {
218
+ if (!isRecord(w) || typeof w.name !== "string" || !NAME_RE.test(w.name))
219
+ continue;
220
+ if (!Array.isArray(w.steps) || !w.steps.length)
221
+ continue;
222
+ const prompts = isRecord(w.prompts) ? w.prompts : {};
223
+ const steps = [];
224
+ for (const s of w.steps) {
225
+ if (typeof s !== "string" || !s.trim()) {
226
+ steps.length = 0;
227
+ break;
228
+ }
229
+ const t = s.trim();
230
+ if (t === "pr")
231
+ steps.push({ kind: "pr" });
232
+ else if (t.startsWith("gate:")) {
233
+ const gate = t.slice(5);
234
+ if (!NAME_RE.test(gate)) {
235
+ steps.length = 0;
236
+ break;
237
+ }
238
+ steps.push({ kind: "gate", gate });
239
+ } else if (NAME_RE.test(t)) {
240
+ const p = prompts[t];
241
+ steps.push({
242
+ kind: "run",
243
+ name: t,
244
+ prompt: typeof p === "string" && p.trim() ? p.trim() : null
245
+ });
246
+ } else {
247
+ steps.length = 0;
248
+ break;
249
+ }
250
+ }
251
+ if (steps.length)
252
+ out[w.name] = { name: w.name, steps };
253
+ }
254
+ return out;
255
+ }
256
+
257
+ // packages/core/src/config.ts
258
+ var DEFAULT_GATE_TIMEOUT_S = 900;
259
+ var AUTO_MODES = ["session-end", "stop", "off"];
260
+ function parseGateDefs(gates) {
261
+ const out = {};
262
+ if (!isRecord2(gates))
263
+ return out;
264
+ for (const [name, v] of Object.entries(gates)) {
265
+ if (!isRecord2(v))
266
+ continue;
267
+ const builtin = v.builtin === "review" ? "review" : null;
268
+ const cmd = typeof v.cmd === "string" ? v.cmd.trim() : "";
269
+ if (!cmd && !builtin)
270
+ continue;
271
+ if (!/^[a-z0-9][a-z0-9_.-]{0,39}$/i.test(name))
272
+ continue;
273
+ const t = Number(v.timeout);
274
+ out[name] = {
275
+ cmd: builtin ? "" : cmd,
276
+ timeout: Number.isFinite(t) && t > 0 ? Math.min(t, 86400) : builtin ? 600 : DEFAULT_GATE_TIMEOUT_S,
277
+ cwd: isRepoRelative(v.cwd) ? v.cwd.trim() : null,
278
+ builtin,
279
+ model: typeof v.model === "string" && v.model.trim() ? v.model.trim() : null
280
+ };
281
+ }
282
+ return out;
283
+ }
284
+ var DEFAULT_CONFIG = {
285
+ daemon: { port: 7777, auth: "loopback-optional" },
286
+ tasks: { source: null, labels: [], team: null },
287
+ gates: { required: [], auto: "session-end", defs: {} },
288
+ workflows: {},
289
+ budget: { daily: null, weekly: null, warn_at: 0.8, on_exceed: "warn" },
290
+ events: { retain_days: 30 },
291
+ audit: { retain_days: 0 },
292
+ privacy: DEFAULT_PRIVACY,
293
+ dispatch: {
294
+ max_parallel: 2,
295
+ permission_mode: null,
296
+ model: null,
297
+ max_turns: null,
298
+ require_pr: true,
299
+ profile: null
300
+ },
301
+ worktree: { setup: null, copy: [], open: null },
302
+ rules: {
303
+ shared_tree: "ask",
304
+ destructive_git: "ask",
305
+ pattern_kill: "ask",
306
+ protected_ports: "ask",
307
+ no_foreign_worktree: "ask",
308
+ claim_required_to_write: "off",
309
+ protected: { ports: [] }
310
+ }
311
+ };
312
+ var MODES = ["ask", "deny", "off"];
313
+ function isRecord2(v) {
314
+ return typeof v === "object" && v !== null && !Array.isArray(v);
315
+ }
316
+ function merge(a, b) {
317
+ if (!isRecord2(a) || !isRecord2(b))
318
+ return b === undefined ? a : b;
319
+ const out = { ...a };
320
+ for (const [k, v] of Object.entries(b))
321
+ out[k] = merge(a[k], v);
322
+ return out;
323
+ }
324
+ function parseToml(text, source) {
325
+ try {
326
+ return Bun.TOML.parse(text) ?? {};
327
+ } catch (e) {
328
+ console.error(`swarm: ignoring invalid TOML in ${source}: ${e.message}`);
329
+ return {};
330
+ }
331
+ }
332
+ function isRepoRelative(f) {
333
+ if (typeof f !== "string")
334
+ return false;
335
+ const t = f.trim();
336
+ if (!t || t.startsWith("/") || t.startsWith("\\") || /^[a-zA-Z]:/.test(t))
337
+ return false;
338
+ return !t.split(/[/\\]/).some((seg) => seg === "..");
339
+ }
340
+ var days = (v, fallback) => {
341
+ const n = Number(v);
342
+ return Number.isInteger(n) && n >= 0 ? Math.min(n, 3650) : fallback;
343
+ };
344
+ function validate(c) {
345
+ const mode = (v, fallback) => MODES.includes(v) ? v : fallback;
346
+ const port = Number(c.daemon?.port);
347
+ const source = c.tasks?.source;
348
+ const setup = c.worktree?.setup;
349
+ const opener = c.worktree?.open;
350
+ const rawGates = c.gates;
351
+ const d = c.dispatch ?? {};
352
+ const mp = Number(d.max_parallel);
353
+ const mt = Number(d.max_turns);
354
+ const str = (v) => typeof v === "string" && v.trim() ? v.trim() : null;
355
+ const b = c.budget ?? {};
356
+ const usd = (v) => {
357
+ const n = Number(v);
358
+ return Number.isFinite(n) && n > 0 ? n : null;
359
+ };
360
+ const warnAt = Number(b.warn_at);
361
+ const auto = rawGates?.auto;
362
+ return {
363
+ ...c,
364
+ daemon: {
365
+ port: Number.isInteger(port) && port > 0 && port < 65536 ? port : 7777,
366
+ auth: c.daemon?.auth === "required" ? "required" : "loopback-optional"
367
+ },
368
+ tasks: {
369
+ source: typeof source === "string" && source.trim() && !source.startsWith("/") ? source.trim() : null,
370
+ labels: Array.isArray(c.tasks?.labels) ? c.tasks.labels.filter((l) => typeof l === "string" && l.trim() !== "") : [],
371
+ team: typeof c.tasks?.team === "string" && c.tasks.team.trim() ? c.tasks.team.trim() : null
372
+ },
373
+ gates: {
374
+ required: Array.isArray(rawGates?.required) ? rawGates.required.filter((g) => typeof g === "string" && g.trim() !== "") : [],
375
+ auto: AUTO_MODES.includes(auto) ? auto : "session-end",
376
+ defs: parseGateDefs(rawGates)
377
+ },
378
+ budget: {
379
+ daily: usd(b.daily),
380
+ weekly: usd(b.weekly),
381
+ warn_at: Number.isFinite(warnAt) && warnAt > 0 && warnAt < 1 ? warnAt : 0.8,
382
+ on_exceed: b.on_exceed === "ask" || b.on_exceed === "stop" ? b.on_exceed : "warn"
383
+ },
384
+ workflows: parseWorkflows(c.workflows),
385
+ events: {
386
+ retain_days: days(c.events?.retain_days, 30)
387
+ },
388
+ audit: {
389
+ retain_days: days(c.audit?.retain_days, 0)
390
+ },
391
+ privacy: {
392
+ store_prompts: c.privacy?.store_prompts !== false,
393
+ store_reasoning: c.privacy?.store_reasoning !== false,
394
+ redact: Array.isArray(c.privacy?.redact) ? c.privacy.redact.filter((r) => typeof r === "string" && r.length > 0) : []
395
+ },
396
+ dispatch: {
397
+ max_parallel: Number.isInteger(mp) && mp > 0 ? Math.min(mp, 16) : 2,
398
+ permission_mode: str(d.permission_mode),
399
+ model: str(d.model),
400
+ max_turns: Number.isInteger(mt) && mt > 0 ? mt : null,
401
+ require_pr: d.require_pr === undefined ? true : d.require_pr === true,
402
+ profile: ["full", "no-edits", "read-only"].includes(String(d.profile)) ? String(d.profile) : null
403
+ },
404
+ worktree: {
405
+ setup: typeof setup === "string" && setup.trim() ? setup.trim() : null,
406
+ copy: Array.isArray(c.worktree?.copy) ? c.worktree.copy.filter((f) => isRepoRelative(f)) : [],
407
+ open: typeof opener === "string" && opener.trim() ? opener.trim() : null
408
+ },
409
+ rules: {
410
+ ...c.rules,
411
+ shared_tree: mode(c.rules?.shared_tree, "ask"),
412
+ destructive_git: mode(c.rules?.destructive_git, "ask"),
413
+ pattern_kill: mode(c.rules?.pattern_kill, "ask"),
414
+ protected_ports: mode(c.rules?.protected_ports, "ask"),
415
+ no_foreign_worktree: mode(c.rules?.no_foreign_worktree, "ask"),
416
+ claim_required_to_write: mode(c.rules?.claim_required_to_write, "off"),
417
+ protected: {
418
+ ports: Array.isArray(c.rules?.protected?.ports) ? c.rules.protected.ports.filter((p) => Number.isInteger(p) && p > 0 && p < 65536) : []
419
+ }
420
+ }
421
+ };
422
+ }
423
+ function leafPaths(v, prefix = "") {
424
+ if (!isRecord2(v))
425
+ return prefix ? [prefix] : [];
426
+ const keys = Object.keys(v);
427
+ if (keys.length === 0)
428
+ return prefix ? [prefix] : [];
429
+ return keys.flatMap((k) => leafPaths(v[k], prefix ? `${prefix}.${k}` : k));
430
+ }
431
+ function getPath(v, path) {
432
+ let cur = v;
433
+ for (const seg of path.split(".")) {
434
+ if (!isRecord2(cur))
435
+ return;
436
+ cur = cur[seg];
437
+ }
438
+ return cur;
439
+ }
440
+ function setPath(obj, path, value) {
441
+ const segs = path.split(".");
442
+ let cur = obj;
443
+ for (const seg of segs.slice(0, -1)) {
444
+ if (!isRecord2(cur[seg]))
445
+ cur[seg] = {};
446
+ cur = cur[seg];
447
+ }
448
+ cur[segs[segs.length - 1]] = value;
449
+ }
450
+ var isLockedBy = (path, lock) => path === lock || path.startsWith(`${lock}.`);
451
+ function readLayer(path) {
452
+ return existsSync3(path) ? parseToml(readFileSync2(path, "utf8"), path) : null;
453
+ }
454
+ function loadConfigDetailed(opts = {}) {
455
+ const home = opts.home ?? process.env.SWARM_HOME ?? join2(process.env.HOME ?? "", ".swarm");
456
+ const policyPath = opts.policy ?? process.env.SWARM_POLICY ?? join2(home, "policy.toml");
457
+ const policyRaw = readLayer(policyPath);
458
+ const locked = Array.isArray(policyRaw?.locked) ? policyRaw.locked.filter((k) => typeof k === "string" && /^[a-z0-9_.-]+$/i.test(k)) : [];
459
+ const policy = { ...policyRaw ?? {} };
460
+ delete policy.locked;
461
+ const layers = [
462
+ ["policy", policyRaw ? policy : null],
463
+ ["global", readLayer(join2(home, "config.toml"))],
464
+ ["repo", opts.repoRoot ? readLayer(join2(opts.repoRoot, ".swarm.toml")) : null]
465
+ ];
466
+ const provenance = {};
467
+ for (const p of leafPaths(DEFAULT_CONFIG))
468
+ provenance[p] = "default";
469
+ const overridden = [];
470
+ let cfg = DEFAULT_CONFIG;
471
+ for (const [layer, raw] of layers) {
472
+ if (!raw)
473
+ continue;
474
+ for (const p of leafPaths(raw)) {
475
+ const lock = layer !== "policy" && locked.find((l) => isLockedBy(p, l));
476
+ if (lock)
477
+ overridden.push({ key: p, layer, attempted: getPath(raw, p) });
478
+ else
479
+ provenance[p] = layer;
480
+ }
481
+ cfg = merge(cfg, raw);
482
+ }
483
+ if (overridden.length) {
484
+ const out = structuredClone(cfg);
485
+ for (const { key } of overridden) {
486
+ const fromPolicy = getPath(policy, key);
487
+ setPath(out, key, fromPolicy === undefined ? getPath(DEFAULT_CONFIG, key) : fromPolicy);
488
+ provenance[key] = fromPolicy === undefined ? "default" : "policy";
489
+ }
490
+ cfg = out;
491
+ }
492
+ return {
493
+ config: validate(cfg),
494
+ provenance,
495
+ overridden,
496
+ policy: { path: policyRaw ? policyPath : null, locked }
497
+ };
498
+ }
149
499
  // packages/core/src/rules.ts
150
500
  var LIVE_WINDOW_MS = 10 * 60000;
151
501
  var WRITE_TOOLS = new Set(["Write", "Edit", "MultiEdit", "NotebookEdit"]);
152
502
  // packages/core/src/ledger.ts
153
503
  var EDIT_TOOLS = new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
504
+ // packages/core/src/policy.ts
505
+ var HOOK_MARK = "swarm-hook";
506
+ var hookIsOurs = (h) => typeof h.command === "string" && (h.command.includes(HOOK_MARK) || h.command.includes("/packages/hook/src/bin.ts"));
507
+ var MIN_HOOK_TIMEOUT_S = 5;
508
+ function hookCoverage(settings) {
509
+ const hooks = settings && typeof settings === "object" && !Array.isArray(settings) ? settings.hooks : undefined;
510
+ const missing = [];
511
+ const short = [];
512
+ for (const ev of HOOK_EVENTS) {
513
+ const groups = Array.isArray(hooks?.[ev]) ? hooks?.[ev] : [];
514
+ const ours = groups.flatMap((g) => {
515
+ const list = g?.hooks;
516
+ return Array.isArray(list) ? list.filter((h) => hookIsOurs(h)) : [];
517
+ });
518
+ if (!ours.length)
519
+ missing.push(ev);
520
+ else if (ours.every((h) => typeof h.timeout === "number" && h.timeout < MIN_HOOK_TIMEOUT_S))
521
+ short.push(ev);
522
+ }
523
+ return { missing, short, complete: !missing.length && !short.length };
524
+ }
154
525
  // 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");
526
+ import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
527
+ import { homedir as homedir2 } from "os";
528
+ import { join as join3 } from "path";
529
+ var isOurs = hookIsOurs;
530
+ var settingsPath = () => process.env.CLAUDE_SETTINGS ?? join3(homedir2(), ".claude", "settings.json");
531
+ var claudeJsonPath = () => process.env.CLAUDE_JSON ?? join3(homedir2(), ".claude.json");
159
532
  function loadClaudeJson() {
160
533
  const p = claudeJsonPath();
161
- if (!existsSync3(p))
534
+ if (!existsSync4(p))
162
535
  return {};
163
536
  try {
164
- return JSON.parse(readFileSync2(p, "utf8"));
537
+ return JSON.parse(readFileSync3(p, "utf8"));
165
538
  } catch {
166
539
  return {};
167
540
  }
@@ -194,8 +567,8 @@ function mcpRegistered() {
194
567
  const c = loadClaudeJson();
195
568
  return Boolean(c.mcpServers?.swarm);
196
569
  }
197
- var codexConfigPath = () => process.env.CODEX_CONFIG ?? join2(homedir2(), ".codex", "config.toml");
198
- var geminiSettingsPath = () => process.env.GEMINI_SETTINGS ?? join2(homedir2(), ".gemini", "settings.json");
570
+ var codexConfigPath = () => process.env.CODEX_CONFIG ?? join3(homedir2(), ".codex", "config.toml");
571
+ var geminiSettingsPath = () => process.env.GEMINI_SETTINGS ?? join3(homedir2(), ".gemini", "settings.json");
199
572
  function codexBlock() {
200
573
  const { command, args } = mcpServerConfig();
201
574
  return `[mcp_servers.swarm]
@@ -206,9 +579,9 @@ args = ${JSON.stringify(args)}
206
579
  var CODEX_BLOCK_RE = /\[mcp_servers\.swarm\]\n(?:(?!\[)[^\n]*\n?)*/;
207
580
  function registerCodex() {
208
581
  const p = codexConfigPath();
209
- if (!existsSync3(join2(p, "..")))
582
+ if (!existsSync4(join3(p, "..")))
210
583
  return false;
211
- const cur = existsSync3(p) ? readFileSync2(p, "utf8") : "";
584
+ const cur = existsSync4(p) ? readFileSync3(p, "utf8") : "";
212
585
  const next = CODEX_BLOCK_RE.test(cur) ? cur.replace(CODEX_BLOCK_RE, codexBlock()) : `${cur.trimEnd()}${cur.trim() ? `
213
586
 
214
587
  ` : ""}${codexBlock()}`;
@@ -218,9 +591,9 @@ function registerCodex() {
218
591
  }
219
592
  function unregisterCodex() {
220
593
  const p = codexConfigPath();
221
- if (!existsSync3(p))
594
+ if (!existsSync4(p))
222
595
  return false;
223
- const cur = readFileSync2(p, "utf8");
596
+ const cur = readFileSync3(p, "utf8");
224
597
  if (!CODEX_BLOCK_RE.test(cur))
225
598
  return false;
226
599
  writeFileSync2(p, cur.replace(CODEX_BLOCK_RE, "").replace(/\n{3,}/g, `
@@ -231,11 +604,11 @@ function unregisterCodex() {
231
604
  }
232
605
  function registerGemini() {
233
606
  const p = geminiSettingsPath();
234
- if (!existsSync3(join2(p, "..")))
607
+ if (!existsSync4(join3(p, "..")))
235
608
  return false;
236
609
  let c = {};
237
610
  try {
238
- c = existsSync3(p) ? JSON.parse(readFileSync2(p, "utf8")) : {};
611
+ c = existsSync4(p) ? JSON.parse(readFileSync3(p, "utf8")) : {};
239
612
  } catch {
240
613
  return false;
241
614
  }
@@ -248,10 +621,10 @@ function registerGemini() {
248
621
  }
249
622
  function unregisterGemini() {
250
623
  const p = geminiSettingsPath();
251
- if (!existsSync3(p))
624
+ if (!existsSync4(p))
252
625
  return false;
253
626
  try {
254
- const c = JSON.parse(readFileSync2(p, "utf8"));
627
+ const c = JSON.parse(readFileSync3(p, "utf8"));
255
628
  const mcp = c.mcpServers ?? {};
256
629
  if (!mcp.swarm)
257
630
  return false;
@@ -291,7 +664,7 @@ function mcpServerConfig() {
291
664
  }
292
665
  function load() {
293
666
  const p = settingsPath();
294
- return existsSync3(p) ? JSON.parse(readFileSync2(p, "utf8")) : {};
667
+ return existsSync4(p) ? JSON.parse(readFileSync3(p, "utf8")) : {};
295
668
  }
296
669
  function save(s) {
297
670
  writeFileSync2(settingsPath(), `${JSON.stringify(s, null, 2)}
@@ -368,21 +741,28 @@ function status() {
368
741
  const installed = Object.values(hooks2).some((l) => l.some((g) => g.hooks.some(isOurs)));
369
742
  const otherAgents = [];
370
743
  const cp = codexConfigPath();
371
- if (existsSync3(cp) && CODEX_BLOCK_RE.test(readFileSync2(cp, "utf8")))
744
+ if (existsSync4(cp) && CODEX_BLOCK_RE.test(readFileSync3(cp, "utf8")))
372
745
  otherAgents.push("codex");
373
746
  const gp = geminiSettingsPath();
374
747
  try {
375
- if (existsSync3(gp) && JSON.parse(readFileSync2(gp, "utf8")).mcpServers?.swarm)
748
+ if (existsSync4(gp) && JSON.parse(readFileSync3(gp, "utf8")).mcpServers?.swarm)
376
749
  otherAgents.push("gemini");
377
750
  } catch {}
378
- return { installed, mcp: mcpRegistered(), path: settingsPath(), shim: shimPath(), otherAgents };
751
+ return {
752
+ installed,
753
+ coverage: hookCoverage(s),
754
+ mcp: mcpRegistered(),
755
+ path: settingsPath(),
756
+ shim: shimPath(),
757
+ otherAgents
758
+ };
379
759
  }
380
760
 
381
761
  // packages/cli/src/procs.ts
382
762
  import { mkdirSync as mkdirSync2, openSync } from "fs";
383
- import { join as join3, resolve as resolve2 } from "path";
763
+ import { join as join4, resolve as resolve2 } from "path";
384
764
  async function call(path, init) {
385
- const r = await fetch(`${new SwarmClient().baseUrl}${path}`, init);
765
+ const r = await authedFetch(`${new SwarmClient().baseUrl}${path}`, init);
386
766
  return await r.json();
387
767
  }
388
768
  var post = (path, body) => call(path, {
@@ -410,9 +790,9 @@ async function start(opts) {
410
790
  port = a.port;
411
791
  }
412
792
  }
413
- const logDir = join3(swarmHome(), "logs", slug(pid));
793
+ const logDir = join4(swarmHome(), "logs", slug(pid));
414
794
  mkdirSync2(logDir, { recursive: true });
415
- const log = join3(logDir, `${slug(opts.name)}.log`);
795
+ const log = join4(logDir, `${slug(opts.name)}.log`);
416
796
  const fd = openSync(log, "a");
417
797
  const cmdline = opts.cmd.join(" ");
418
798
  const child = Bun.spawn(["sh", "-c", cmdline], {
@@ -477,6 +857,14 @@ function fmt(r) {
477
857
  }
478
858
 
479
859
  // packages/cli/src/bin.ts
860
+ function gitToplevel() {
861
+ const r = Bun.spawnSync(["git", "rev-parse", "--show-toplevel"], {
862
+ stdout: "pipe",
863
+ stderr: "ignore"
864
+ });
865
+ const out = r.exitCode === 0 ? r.stdout.toString().trim() : "";
866
+ return out || null;
867
+ }
480
868
  var [cmd = "help", ...rest] = process.argv.slice(2);
481
869
  var json = rest.includes("--json");
482
870
  var arg = () => rest.find((a) => !a.startsWith("--"));
@@ -518,13 +906,18 @@ var help = `swarm \u2014 control plane for AI-agent development
518
906
  stats [-p] [--json] all-time totals, streak, records (the dashboard's Stats view)
519
907
  search <query\u2026> [-p] [--kind handoff|incident|gate|session] [--json] memory over Swarm's own data (handoffs, incidents, gates, what sessions said)
520
908
  rules dryrun [--set rule=mode,\u2026] [--limit n] [--json] replay this repo's history under rule modes; shows what would fire + flaky signals
909
+ workflow <name> <task> | workflow ls | workflow stop <task> run a [[workflows]] sequence on a task (M7.8)
910
+ msg send <to> <text\u2026> [-p] message a session id, a task's holder, or "lead" (M7.6)
911
+ msg ls [-p] [--json] recent messages
912
+ demo open a seeded demo dashboard (own home + port; your real data is untouched)
913
+ 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
914
 
522
915
  install | uninstall add/remove Swarm hooks in ~/.claude/settings.json
523
916
 
524
917
  Env: SWARM_URL, SWARM_PORT (default 7777), SWARM_HOME (~/.swarm)`;
525
918
  async function api(path, init) {
526
919
  const base = new SwarmClient().baseUrl;
527
- const r = await fetch(`${base}${path}`, init);
920
+ const r = await authedFetch(`${base}${path}`, init);
528
921
  if (!r.ok)
529
922
  throw new Error(`${path}: ${r.status} ${await r.text()}`);
530
923
  return r.status === 204 ? null : r.json();
@@ -597,7 +990,24 @@ Open the dashboard: ${base}`);
597
990
  line(Boolean(bun), `bun ${bun ? `(${bun})` : ""}`, "install bun: https://bun.sh");
598
991
  line(Boolean(claude), "claude CLI on PATH", "install Claude Code: https://claude.com/claude-code");
599
992
  line(running, `daemon ${info ? `(pid ${info.pid}, ${info.url})` : ""}`, "run: swarm start");
993
+ if (running) {
994
+ const h = await authedFetch(`${resolveBaseUrl()}/v1/health`).then((r) => r.json()).catch(() => null);
995
+ if (h)
996
+ console.log(`\xB7 daemon v${h.version ?? "?"} \xB7 schema v${h.schema ?? 0}`);
997
+ }
600
998
  line(st.installed, "hooks installed", "run: swarm install");
999
+ if (st.installed && !st.coverage.complete) {
1000
+ if (st.coverage.missing.length)
1001
+ line(false, `hooks missing: ${st.coverage.missing.join(", ")}`, "run: swarm install");
1002
+ if (st.coverage.short.length)
1003
+ line(false, `hook timeout too short: ${st.coverage.short.join(", ")}`, "run: swarm install");
1004
+ }
1005
+ const pol = loadConfigDetailed({ repoRoot: gitToplevel() });
1006
+ if (pol.policy.path) {
1007
+ line(true, `policy ${pol.policy.path} (locked: ${pol.policy.locked.join(", ") || "nothing"})`, "");
1008
+ for (const o of pol.overridden)
1009
+ line(false, `${o.layer} config overrides locked ${o.key}`, `remove it \u2014 policy value stays in effect`);
1010
+ }
601
1011
  line(st.mcp, "MCP server registered", "run: swarm install");
602
1012
  if (st.otherAgents.length)
603
1013
  console.log(`\u2713 MCP server also registered for ${st.otherAgents.join(", ")} (swarm_* tools in those CLIs too)`);
@@ -645,7 +1055,7 @@ url: ${resolveBaseUrl()}`);
645
1055
  headers: { "content-type": "application/json" },
646
1056
  body: JSON.stringify({ path: resolve3(".") })
647
1057
  });
648
- const r = await fetch(`${new SwarmClient().baseUrl}/v1/claims`, {
1058
+ const r = await authedFetch(`${new SwarmClient().baseUrl}/v1/claims`, {
649
1059
  method: "POST",
650
1060
  headers: { "content-type": "application/json" },
651
1061
  body: JSON.stringify({ projectId: proj.id, task, owner })
@@ -673,7 +1083,7 @@ url: ${resolveBaseUrl()}`);
673
1083
  headers: { "content-type": "application/json" },
674
1084
  body: JSON.stringify({ path: resolve3(".") })
675
1085
  });
676
- const r = await fetch(`${new SwarmClient().baseUrl}/v1/claims/${cmd}`, {
1086
+ const r = await authedFetch(`${new SwarmClient().baseUrl}/v1/claims/${cmd}`, {
677
1087
  method: "POST",
678
1088
  headers: { "content-type": "application/json" },
679
1089
  body: JSON.stringify({ projectId: proj.id, task, force: rest.includes("--force") })
@@ -696,7 +1106,7 @@ url: ${resolveBaseUrl()}`);
696
1106
  headers: { "content-type": "application/json" },
697
1107
  body: JSON.stringify({ path: resolve3(".") })
698
1108
  });
699
- const post2 = (path, body) => fetch(`${new SwarmClient().baseUrl}${path}`, {
1109
+ const post2 = (path, body) => authedFetch(`${new SwarmClient().baseUrl}${path}`, {
700
1110
  method: "POST",
701
1111
  headers: { "content-type": "application/json" },
702
1112
  body: JSON.stringify({ projectId: proj.id, ...body })
@@ -852,7 +1262,7 @@ url: ${resolveBaseUrl()}`);
852
1262
  const text = words.join(" ");
853
1263
  if (!id || !text)
854
1264
  throw new Error("usage: swarm answer <id> <text\u2026>");
855
- const r = await fetch(`${new SwarmClient().baseUrl}/v1/questions/${id}/answer`, {
1265
+ const r = await authedFetch(`${new SwarmClient().baseUrl}/v1/questions/${id}/answer`, {
856
1266
  method: "POST",
857
1267
  headers: { "content-type": "application/json" },
858
1268
  body: JSON.stringify({ text, by: process.env.USER ?? "cli" })
@@ -904,7 +1314,7 @@ url: ${resolveBaseUrl()}`);
904
1314
  break;
905
1315
  }
906
1316
  if (sub === "clear") {
907
- const r2 = await fetch(`${new SwarmClient().baseUrl}/v1/dispatch`, {
1317
+ const r2 = await authedFetch(`${new SwarmClient().baseUrl}/v1/dispatch`, {
908
1318
  method: "DELETE",
909
1319
  headers: { "content-type": "application/json" },
910
1320
  body: JSON.stringify({ projectId: proj.id, task: positional[1] })
@@ -912,7 +1322,7 @@ url: ${resolveBaseUrl()}`);
912
1322
  console.log(json ? JSON.stringify(r2) : `cleared ${r2.cleared}`);
913
1323
  break;
914
1324
  }
915
- const r = await fetch(`${new SwarmClient().baseUrl}/v1/dispatch`, {
1325
+ const r = await authedFetch(`${new SwarmClient().baseUrl}/v1/dispatch`, {
916
1326
  method: "POST",
917
1327
  headers: { "content-type": "application/json" },
918
1328
  body: JSON.stringify({
@@ -978,7 +1388,7 @@ watch: swarm dispatch status \xB7 swarm run ls \xB7 the Board`);
978
1388
  ${flag("--body") ?? d.body}`);
979
1389
  break;
980
1390
  }
981
- const r = await fetch(`${new SwarmClient().baseUrl}/v1/prs/open`, {
1391
+ const r = await authedFetch(`${new SwarmClient().baseUrl}/v1/prs/open`, {
982
1392
  method: "POST",
983
1393
  headers: { "content-type": "application/json" },
984
1394
  body: JSON.stringify({
@@ -1070,11 +1480,11 @@ ${flag("--body") ?? d.body}`);
1070
1480
  const target = positionals[1];
1071
1481
  if (!target)
1072
1482
  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`, {
1483
+ const r2 = sub === "send" ? await authedFetch(`${base}/v1/runs/${encodeURIComponent(target)}/send`, {
1074
1484
  method: "POST",
1075
1485
  headers: { "content-type": "application/json" },
1076
1486
  body: JSON.stringify({ text: positionals.slice(2).join(" ") })
1077
- }) : await fetch(`${base}/v1/runs/${encodeURIComponent(target)}`, { method: "DELETE" });
1487
+ }) : await authedFetch(`${base}/v1/runs/${encodeURIComponent(target)}`, { method: "DELETE" });
1078
1488
  const j = await r2.json();
1079
1489
  if (json)
1080
1490
  console.log(JSON.stringify(j));
@@ -1096,7 +1506,7 @@ ${flag("--body") ?? d.body}`);
1096
1506
  prompt = await Bun.file(resolve3(pf)).text();
1097
1507
  if (!task || !prompt)
1098
1508
  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`, {
1509
+ const r = await authedFetch(resumeFrom ? `${base}/v1/sessions/${encodeURIComponent(resumeFrom)}/resume` : `${base}/v1/runs`, {
1100
1510
  method: "POST",
1101
1511
  headers: { "content-type": "application/json" },
1102
1512
  body: JSON.stringify({
@@ -1137,7 +1547,7 @@ ${flag("--body") ?? d.body}`);
1137
1547
  body: JSON.stringify({ path: resolve3(".") })
1138
1548
  });
1139
1549
  if (cmd === "resume") {
1140
- const r2 = await fetch(`${new SwarmClient().baseUrl}/v1/handoffs?project=${proj.id}&task=${encodeURIComponent(task)}`);
1550
+ const r2 = await authedFetch(`${new SwarmClient().baseUrl}/v1/handoffs?project=${proj.id}&task=${encodeURIComponent(task)}`);
1141
1551
  const j = await r2.json();
1142
1552
  if (json)
1143
1553
  console.log(JSON.stringify(j.handoff));
@@ -1149,7 +1559,7 @@ ${flag("--body") ?? d.body}`);
1149
1559
  const i = rest.indexOf(n);
1150
1560
  return i >= 0 ? rest[i + 1] : undefined;
1151
1561
  };
1152
- const r = await fetch(`${new SwarmClient().baseUrl}/v1/handoffs`, {
1562
+ const r = await authedFetch(`${new SwarmClient().baseUrl}/v1/handoffs`, {
1153
1563
  method: "POST",
1154
1564
  headers: { "content-type": "application/json" },
1155
1565
  body: JSON.stringify({
@@ -1198,7 +1608,7 @@ ${flag("--body") ?? d.body}`);
1198
1608
  const [, task, gate, verdict] = positionals;
1199
1609
  if (!task || !gate || !verdict)
1200
1610
  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`, {
1611
+ const r = await authedFetch(`${new SwarmClient().baseUrl}/v1/gates`, {
1202
1612
  method: "POST",
1203
1613
  headers: { "content-type": "application/json" },
1204
1614
  body: JSON.stringify({
@@ -1225,7 +1635,7 @@ ${flag("--body") ?? d.body}`);
1225
1635
  const [, task, ...gates2] = positionals;
1226
1636
  if (!task)
1227
1637
  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`, {
1638
+ const r = await authedFetch(`${new SwarmClient().baseUrl}/v1/gates/run`, {
1229
1639
  method: "POST",
1230
1640
  headers: { "content-type": "application/json" },
1231
1641
  body: JSON.stringify({
@@ -1309,6 +1719,125 @@ ${flag("--body") ?? d.body}`);
1309
1719
  session ${h.sessionId}` : ""}`);
1310
1720
  break;
1311
1721
  }
1722
+ case "workflow": {
1723
+ await ensureDaemon({ quiet: true });
1724
+ const proj = await api("/v1/projects", {
1725
+ method: "POST",
1726
+ headers: { "content-type": "application/json" },
1727
+ body: JSON.stringify({ path: resolve3(".") })
1728
+ });
1729
+ if (rest[0] === "ls" || !rest[0]) {
1730
+ const w = await api(`/v1/workflows?project=${proj.id}`);
1731
+ const names = Object.keys(w.defs);
1732
+ console.log(names.length ? `declared: ${names.join(", ")}` : "no [[workflows]] declared in .swarm.toml");
1733
+ for (const r2 of w.runs.slice(0, 20))
1734
+ console.log(`#${r2.id} ${r2.task} \xB7 ${r2.workflow} \xB7 ${r2.state === "running" ? `${r2.stepLabel} (${r2.step + 1}/${r2.steps.length})` : r2.state}${r2.detail ? ` \u2014 ${r2.detail.slice(0, 80)}` : ""}`);
1735
+ break;
1736
+ }
1737
+ if (rest[0] === "stop") {
1738
+ const task2 = rest[1];
1739
+ if (!task2)
1740
+ throw new Error("usage: swarm workflow stop <task>");
1741
+ const r2 = await api("/v1/workflows/stop", {
1742
+ method: "POST",
1743
+ headers: { "content-type": "application/json" },
1744
+ body: JSON.stringify({ projectId: proj.id, task: task2 })
1745
+ });
1746
+ if (!r2.ok)
1747
+ throw new Error(r2.error ?? "stop failed");
1748
+ console.log(`stopped the workflow on ${task2}`);
1749
+ break;
1750
+ }
1751
+ const [name, task] = rest;
1752
+ if (!name || !task)
1753
+ throw new Error("usage: swarm workflow <name> <task>");
1754
+ const r = await api("/v1/workflows", {
1755
+ method: "POST",
1756
+ headers: { "content-type": "application/json" },
1757
+ body: JSON.stringify({
1758
+ projectId: proj.id,
1759
+ task,
1760
+ workflow: name,
1761
+ owner: process.env.USER ?? "cli"
1762
+ })
1763
+ });
1764
+ if (!r.ok)
1765
+ throw new Error(r.error ?? "workflow failed to start");
1766
+ console.log(`workflow ${name} started on ${task} (#${r.id}) \u2014 watch the Board`);
1767
+ break;
1768
+ }
1769
+ case "msg": {
1770
+ await ensureDaemon({ quiet: true });
1771
+ const proj = await api("/v1/projects", {
1772
+ method: "POST",
1773
+ headers: { "content-type": "application/json" },
1774
+ body: JSON.stringify({ path: resolve3(".") })
1775
+ });
1776
+ if (rest[0] === "send") {
1777
+ const [to, ...words] = rest.slice(1).filter((a) => a !== "-p");
1778
+ if (!to || !words.length)
1779
+ throw new Error('usage: swarm msg send <session|task|"lead"> <text\u2026>');
1780
+ const r = await api("/v1/messages", {
1781
+ method: "POST",
1782
+ headers: { "content-type": "application/json" },
1783
+ body: JSON.stringify({
1784
+ projectId: proj.id,
1785
+ to,
1786
+ text: words.join(" "),
1787
+ from: process.env.USER ?? "me"
1788
+ })
1789
+ });
1790
+ if (!r.ok)
1791
+ throw new Error(r.error ?? "send failed");
1792
+ console.log(`sent #${r.message?.id}${r.message?.sessionId ? "" : " (queued until the target appears)"}`);
1793
+ break;
1794
+ }
1795
+ if (rest[0] === "ls" || !rest[0]) {
1796
+ const ms = await api(`/v1/messages?project=${proj.id}&limit=50`);
1797
+ if (rest.includes("--json")) {
1798
+ console.log(JSON.stringify(ms, null, 2));
1799
+ break;
1800
+ }
1801
+ if (!ms.length) {
1802
+ console.log("no messages");
1803
+ break;
1804
+ }
1805
+ for (const m of ms)
1806
+ console.log(`#${m.id} ${m.deliveredAt ? "\u2713" : "\xB7"} ${m.from ?? "?"} \u2192 ${m.task ?? m.toKind}: ${m.text.slice(0, 100)}`);
1807
+ break;
1808
+ }
1809
+ throw new Error("usage: swarm msg send <to> <text\u2026> | swarm msg ls");
1810
+ }
1811
+ case "audit": {
1812
+ if (rest[0] !== "export")
1813
+ throw new Error("usage: swarm audit export [--since 30d] [-p] [--type t] [--format jsonl|csv|json] [--limit n]");
1814
+ await ensureDaemon({ quiet: true });
1815
+ const q = new URLSearchParams;
1816
+ const val = (n) => {
1817
+ const i = rest.indexOf(n);
1818
+ return i >= 0 ? rest[i + 1] : undefined;
1819
+ };
1820
+ if (val("--since"))
1821
+ q.set("since", val("--since"));
1822
+ if (val("--type"))
1823
+ q.set("type", val("--type"));
1824
+ if (val("--limit"))
1825
+ q.set("limit", val("--limit"));
1826
+ q.set("format", val("--format") ?? "jsonl");
1827
+ if (rest.includes("-p")) {
1828
+ const proj = await api("/v1/projects", {
1829
+ method: "POST",
1830
+ headers: { "content-type": "application/json" },
1831
+ body: JSON.stringify({ path: resolve3(".") })
1832
+ });
1833
+ q.set("project", proj.id);
1834
+ }
1835
+ const r = await authedFetch(`${new SwarmClient().baseUrl}/v1/audit?${q}`);
1836
+ if (!r.ok)
1837
+ throw new Error((await r.json()).error ?? `audit: ${r.status}`);
1838
+ process.stdout.write(await r.text());
1839
+ break;
1840
+ }
1312
1841
  case "rules": {
1313
1842
  if (rest[0] !== "dryrun")
1314
1843
  throw new Error("usage: swarm rules dryrun [--set rule=mode,\u2026] [--limit n]");
@@ -1539,7 +2068,7 @@ would have fired (newest last):`);
1539
2068
  headers: { "content-type": "application/json" },
1540
2069
  body: JSON.stringify({ path: resolve3(".") })
1541
2070
  });
1542
- const r = await fetch(`${new SwarmClient().baseUrl}/v1/resources`, {
2071
+ const r = await authedFetch(`${new SwarmClient().baseUrl}/v1/resources`, {
1543
2072
  method: "POST",
1544
2073
  headers: { "content-type": "application/json" },
1545
2074
  body: JSON.stringify({
@@ -1575,7 +2104,7 @@ would have fired (newest last):`);
1575
2104
  });
1576
2105
  if (rest.includes("--force"))
1577
2106
  q.set("force", "1");
1578
- const r = await fetch(`${new SwarmClient().baseUrl}/v1/resources/${encodeURIComponent(name)}?${q}`, { method: "DELETE" }).then((x) => x.json());
2107
+ const r = await authedFetch(`${new SwarmClient().baseUrl}/v1/resources/${encodeURIComponent(name)}?${q}`, { method: "DELETE" }).then((x) => x.json());
1579
2108
  if (json)
1580
2109
  console.log(JSON.stringify(r));
1581
2110
  else if (r.ok)
@@ -1593,7 +2122,7 @@ would have fired (newest last):`);
1593
2122
  const base = new SwarmClient().baseUrl;
1594
2123
  const pIdx = rest.indexOf("--session");
1595
2124
  const wantSession = pIdx >= 0 ? rest[pIdx + 1] : undefined;
1596
- const res = await fetch(`${base}/v1/events?since=0`);
2125
+ const res = await authedFetch(`${base}/v1/events?since=0`);
1597
2126
  const reader = res.body?.getReader();
1598
2127
  if (!reader)
1599
2128
  throw new Error("no stream");
@@ -1623,9 +2152,37 @@ would have fired (newest last):`);
1623
2152
  }
1624
2153
  break;
1625
2154
  }
2155
+ case "demo": {
2156
+ const home = join5(swarmHome(), "demo");
2157
+ const port = "7799";
2158
+ const [cmd2, ...args] = daemonCommand();
2159
+ if (!cmd2)
2160
+ throw new Error("could not resolve the daemon command");
2161
+ const env = { ...process.env, SWARM_HOME: home, SWARM_PORT: port, SWARM_DEMO: "1" };
2162
+ const up = await authedFetch(`http://127.0.0.1:${port}/v1/health`).then((r) => r.ok).catch(() => false);
2163
+ if (!up) {
2164
+ Bun.spawn([cmd2, ...args], {
2165
+ stdin: "ignore",
2166
+ stdout: "ignore",
2167
+ stderr: "ignore",
2168
+ env
2169
+ }).unref();
2170
+ for (let i = 0;i < 40; i++) {
2171
+ await new Promise((r) => setTimeout(r, 250));
2172
+ if (await authedFetch(`http://127.0.0.1:${port}/v1/health`).then((r) => r.ok).catch(() => false))
2173
+ break;
2174
+ }
2175
+ }
2176
+ const tok = readToken(home);
2177
+ const url = `http://127.0.0.1:${port}/${tok ? `?token=${tok}` : ""}`;
2178
+ Bun.spawn(["open", url]).unref?.();
2179
+ console.log(`demo dashboard: http://127.0.0.1:${port} (home ${home} \u2014 delete it to reset)`);
2180
+ break;
2181
+ }
1626
2182
  case "ui": {
1627
2183
  const base = await ensureDaemon();
1628
- Bun.spawn(["open", base]).unref?.();
2184
+ const tok = readToken();
2185
+ Bun.spawn(["open", tok ? `${base}/?token=${tok}` : base]).unref?.();
1629
2186
  console.log(base);
1630
2187
  break;
1631
2188
  }