@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/README.md +63 -10
- package/dist/swarm-hook.js +281 -1
- package/dist/swarm-mcp.js +18 -2
- package/dist/swarm.js +430 -48
- package/dist/swarmd.js +956 -68
- package/package.json +1 -1
- package/web/app.js +388 -127
- package/web/index.html +63 -3
- package/web/menus.js +1 -1
- package/web/release-notes.js +1 -1
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 ??
|
|
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/
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
var
|
|
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 (!
|
|
479
|
+
if (!existsSync4(p))
|
|
162
480
|
return {};
|
|
163
481
|
try {
|
|
164
|
-
return JSON.parse(
|
|
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 ??
|
|
198
|
-
var geminiSettingsPath = () => process.env.GEMINI_SETTINGS ??
|
|
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 (!
|
|
527
|
+
if (!existsSync4(join3(p, "..")))
|
|
210
528
|
return false;
|
|
211
|
-
const cur =
|
|
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 (!
|
|
539
|
+
if (!existsSync4(p))
|
|
222
540
|
return false;
|
|
223
|
-
const cur =
|
|
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 (!
|
|
552
|
+
if (!existsSync4(join3(p, "..")))
|
|
235
553
|
return false;
|
|
236
554
|
let c = {};
|
|
237
555
|
try {
|
|
238
|
-
c =
|
|
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 (!
|
|
569
|
+
if (!existsSync4(p))
|
|
252
570
|
return false;
|
|
253
571
|
try {
|
|
254
|
-
const c = JSON.parse(
|
|
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
|
|
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 (
|
|
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 (
|
|
693
|
+
if (existsSync4(gp) && JSON.parse(readFileSync3(gp, "utf8")).mcpServers?.swarm)
|
|
376
694
|
otherAgents.push("gemini");
|
|
377
695
|
} catch {}
|
|
378
|
-
return {
|
|
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
|
|
708
|
+
import { join as join4, resolve as resolve2 } from "path";
|
|
384
709
|
async function call(path, init) {
|
|
385
|
-
const r = await
|
|
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 =
|
|
738
|
+
const logDir = join4(swarmHome(), "logs", slug(pid));
|
|
414
739
|
mkdirSync2(logDir, { recursive: true });
|
|
415
|
-
const 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
|
|
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
|
|
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
|
|
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) =>
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
2009
|
+
const tok = readToken();
|
|
2010
|
+
Bun.spawn(["open", tok ? `${base}/?token=${tok}` : base]).unref?.();
|
|
1629
2011
|
console.log(base);
|
|
1630
2012
|
break;
|
|
1631
2013
|
}
|