@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/README.md +63 -10
- package/dist/swarm-hook.js +283 -1
- package/dist/swarm-mcp.js +53 -8
- package/dist/swarm.js +606 -49
- package/dist/swarmd.js +1899 -132
- package/package.json +1 -1
- package/web/app.js +533 -133
- package/web/index.html +106 -3
- package/web/menus.js +1 -1
- package/web/release-notes.js +1 -1
- package/web/viz.js +20 -3
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 ??
|
|
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,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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
var
|
|
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 (!
|
|
534
|
+
if (!existsSync4(p))
|
|
162
535
|
return {};
|
|
163
536
|
try {
|
|
164
|
-
return JSON.parse(
|
|
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 ??
|
|
198
|
-
var geminiSettingsPath = () => process.env.GEMINI_SETTINGS ??
|
|
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 (!
|
|
582
|
+
if (!existsSync4(join3(p, "..")))
|
|
210
583
|
return false;
|
|
211
|
-
const cur =
|
|
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 (!
|
|
594
|
+
if (!existsSync4(p))
|
|
222
595
|
return false;
|
|
223
|
-
const cur =
|
|
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 (!
|
|
607
|
+
if (!existsSync4(join3(p, "..")))
|
|
235
608
|
return false;
|
|
236
609
|
let c = {};
|
|
237
610
|
try {
|
|
238
|
-
c =
|
|
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 (!
|
|
624
|
+
if (!existsSync4(p))
|
|
252
625
|
return false;
|
|
253
626
|
try {
|
|
254
|
-
const c = JSON.parse(
|
|
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
|
|
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 (
|
|
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 (
|
|
748
|
+
if (existsSync4(gp) && JSON.parse(readFileSync3(gp, "utf8")).mcpServers?.swarm)
|
|
376
749
|
otherAgents.push("gemini");
|
|
377
750
|
} catch {}
|
|
378
|
-
return {
|
|
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
|
|
763
|
+
import { join as join4, resolve as resolve2 } from "path";
|
|
384
764
|
async function call(path, init) {
|
|
385
|
-
const r = await
|
|
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 =
|
|
793
|
+
const logDir = join4(swarmHome(), "logs", slug(pid));
|
|
414
794
|
mkdirSync2(logDir, { recursive: true });
|
|
415
|
-
const 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
|
|
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
|
|
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
|
|
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) =>
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
2184
|
+
const tok = readToken();
|
|
2185
|
+
Bun.spawn(["open", tok ? `${base}/?token=${tok}` : base]).unref?.();
|
|
1629
2186
|
console.log(base);
|
|
1630
2187
|
break;
|
|
1631
2188
|
}
|