@ra3orblade/swarm 0.4.0 → 0.5.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 +5 -5
- package/dist/swarm-mcp.js +347 -240
- package/dist/swarm.js +279 -8
- package/dist/swarmd.js +871 -113
- package/package.json +1 -1
- package/web/app.js +151 -13
- package/web/icons.js +2 -2
- package/web/index.html +21 -0
- package/web/menus.js +11 -11
package/dist/swarm.js
CHANGED
|
@@ -151,6 +151,45 @@ var WRITE_TOOLS = new Set(["Write", "Edit", "MultiEdit", "NotebookEdit"]);
|
|
|
151
151
|
var MARK = "swarm-hook";
|
|
152
152
|
var isOurs = (h) => h.command.includes(MARK) || h.command.includes("/packages/hook/src/bin.ts");
|
|
153
153
|
var settingsPath = () => process.env.CLAUDE_SETTINGS ?? join2(homedir2(), ".claude", "settings.json");
|
|
154
|
+
var claudeJsonPath = () => process.env.CLAUDE_JSON ?? join2(homedir2(), ".claude.json");
|
|
155
|
+
function loadClaudeJson() {
|
|
156
|
+
const p = claudeJsonPath();
|
|
157
|
+
if (!existsSync3(p))
|
|
158
|
+
return {};
|
|
159
|
+
try {
|
|
160
|
+
return JSON.parse(readFileSync2(p, "utf8"));
|
|
161
|
+
} catch {
|
|
162
|
+
return {};
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function saveClaudeJson(c) {
|
|
166
|
+
writeFileSync2(claudeJsonPath(), `${JSON.stringify(c, null, 2)}
|
|
167
|
+
`);
|
|
168
|
+
}
|
|
169
|
+
function registerMcp() {
|
|
170
|
+
const c = loadClaudeJson();
|
|
171
|
+
const mcp = c.mcpServers ?? {};
|
|
172
|
+
mcp.swarm = { type: "stdio", ...mcpServerConfig() };
|
|
173
|
+
c.mcpServers = mcp;
|
|
174
|
+
saveClaudeJson(c);
|
|
175
|
+
}
|
|
176
|
+
function unregisterMcp() {
|
|
177
|
+
const c = loadClaudeJson();
|
|
178
|
+
const mcp = c.mcpServers ?? {};
|
|
179
|
+
if (!mcp.swarm)
|
|
180
|
+
return false;
|
|
181
|
+
delete mcp.swarm;
|
|
182
|
+
if (Object.keys(mcp).length)
|
|
183
|
+
c.mcpServers = mcp;
|
|
184
|
+
else
|
|
185
|
+
delete c.mcpServers;
|
|
186
|
+
saveClaudeJson(c);
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
function mcpRegistered() {
|
|
190
|
+
const c = loadClaudeJson();
|
|
191
|
+
return Boolean(c.mcpServers?.swarm);
|
|
192
|
+
}
|
|
154
193
|
var hookCommand = (event) => `${binCommand("swarm-hook")} ${event}`;
|
|
155
194
|
var shimPath = () => resolveBin("swarm-hook").at(-1);
|
|
156
195
|
function mcpServerConfig() {
|
|
@@ -179,10 +218,16 @@ function install() {
|
|
|
179
218
|
added.push(ev);
|
|
180
219
|
}
|
|
181
220
|
s.hooks = hooks2;
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
221
|
+
const stale = s.mcpServers ?? {};
|
|
222
|
+
if (stale.swarm) {
|
|
223
|
+
delete stale.swarm;
|
|
224
|
+
if (Object.keys(stale).length)
|
|
225
|
+
s.mcpServers = stale;
|
|
226
|
+
else
|
|
227
|
+
delete s.mcpServers;
|
|
228
|
+
}
|
|
185
229
|
save(s);
|
|
230
|
+
registerMcp();
|
|
186
231
|
return added;
|
|
187
232
|
}
|
|
188
233
|
function uninstall() {
|
|
@@ -211,23 +256,22 @@ function uninstall() {
|
|
|
211
256
|
else
|
|
212
257
|
delete s.hooks;
|
|
213
258
|
const mcp = s.mcpServers ?? {};
|
|
214
|
-
if (mcp.swarm)
|
|
259
|
+
if (mcp.swarm)
|
|
215
260
|
delete mcp.swarm;
|
|
216
|
-
removed++;
|
|
217
|
-
}
|
|
218
261
|
if (Object.keys(mcp).length)
|
|
219
262
|
s.mcpServers = mcp;
|
|
220
263
|
else
|
|
221
264
|
delete s.mcpServers;
|
|
222
265
|
save(s);
|
|
266
|
+
if (unregisterMcp())
|
|
267
|
+
removed++;
|
|
223
268
|
return removed;
|
|
224
269
|
}
|
|
225
270
|
function status() {
|
|
226
271
|
const s = load();
|
|
227
272
|
const hooks2 = s.hooks ?? {};
|
|
228
273
|
const installed = Object.values(hooks2).some((l) => l.some((g) => g.hooks.some(isOurs)));
|
|
229
|
-
|
|
230
|
-
return { installed, mcp, path: settingsPath(), shim: shimPath() };
|
|
274
|
+
return { installed, mcp: mcpRegistered(), path: settingsPath(), shim: shimPath() };
|
|
231
275
|
}
|
|
232
276
|
|
|
233
277
|
// packages/cli/src/procs.ts
|
|
@@ -348,6 +392,13 @@ var help = `swarm \u2014 control plane for AI-agent development
|
|
|
348
392
|
renew <task> extend the lease; release <task> [--force] release + remove worktree
|
|
349
393
|
claims list claims; reap release abandoned claims (keeps ones holding work)
|
|
350
394
|
tasks [--ready] [--json] the repo's task source (.swarm.toml [tasks] source); --ready = claimable now
|
|
395
|
+
gate record <task> <gate> pass|fail --rubric "\u2026" [--evidence "\u2026"] record a verification run (rubric required)
|
|
396
|
+
gate ls [task] latest verdict per gate (and the run history for one task)
|
|
397
|
+
run --task <id> (--prompt "\u2026" | --prompt-file f) [--model m] [--permission-mode m] [--allowed-tools a,b] [--max-turns n]
|
|
398
|
+
claim the task and spawn claude -p in its worktree; the session shows in Fleet
|
|
399
|
+
run ls | send <task|id> "text" | stop <task|id> steer (stdin) or stop a spawned run, by pid never pattern
|
|
400
|
+
handoff <task> --done "\u2026" --remaining "\u2026" [--files a,b] [--verify "\u2026"] leave notes for the next holder
|
|
401
|
+
resume <task> print the latest handoff (the next session gets it automatically on start)
|
|
351
402
|
res ls | acquire <name> [--owner n] [--pid n] [--port n] | release <name> [--force]
|
|
352
403
|
named singletons (ports, processes); fail-closed
|
|
353
404
|
serve start [--name web] [--from-port 3400 | --port n] -- <cmd>
|
|
@@ -546,6 +597,226 @@ url: ${resolveBaseUrl()}`);
|
|
|
546
597
|
console.log(`${c.state.padEnd(9)} ${c.task.padEnd(16)} ${(c.owner || "").padEnd(12)} ${c.worktree}`);
|
|
547
598
|
break;
|
|
548
599
|
}
|
|
600
|
+
case "run": {
|
|
601
|
+
await ensureDaemon({ quiet: true });
|
|
602
|
+
const proj = await api("/v1/projects", {
|
|
603
|
+
method: "POST",
|
|
604
|
+
headers: { "content-type": "application/json" },
|
|
605
|
+
body: JSON.stringify({ path: resolve3(".") })
|
|
606
|
+
});
|
|
607
|
+
const base = new SwarmClient().baseUrl;
|
|
608
|
+
const valueFlags = new Set([
|
|
609
|
+
"--task",
|
|
610
|
+
"--prompt",
|
|
611
|
+
"--prompt-file",
|
|
612
|
+
"--model",
|
|
613
|
+
"--permission-mode",
|
|
614
|
+
"--allowed-tools",
|
|
615
|
+
"--max-turns",
|
|
616
|
+
"--owner"
|
|
617
|
+
]);
|
|
618
|
+
const positionals = [];
|
|
619
|
+
for (let i = 0;i < rest.length; i++) {
|
|
620
|
+
const a = rest[i];
|
|
621
|
+
if (valueFlags.has(a))
|
|
622
|
+
i++;
|
|
623
|
+
else if (!a.startsWith("--"))
|
|
624
|
+
positionals.push(a);
|
|
625
|
+
}
|
|
626
|
+
const flag = (n) => {
|
|
627
|
+
const i = rest.indexOf(n);
|
|
628
|
+
return i >= 0 ? rest[i + 1] : undefined;
|
|
629
|
+
};
|
|
630
|
+
const sub = positionals[0];
|
|
631
|
+
if (sub === "ls") {
|
|
632
|
+
const runs = await api(`/v1/runs?project=${proj.id}`);
|
|
633
|
+
if (json)
|
|
634
|
+
console.log(JSON.stringify(runs));
|
|
635
|
+
else if (!runs.length)
|
|
636
|
+
console.log("no live runs here");
|
|
637
|
+
else
|
|
638
|
+
for (const r2 of runs)
|
|
639
|
+
console.log(`${r2.id} ${r2.task.padEnd(12)} pid ${String(r2.pid).padEnd(7)} ${r2.owner.padEnd(10)} ${r2.result ? `$${r2.result.costUsd.toFixed(2)} \xB7 ${r2.result.turns} turns${r2.result.isError ? " \xB7 error" : ""}` : "starting\u2026"}`);
|
|
640
|
+
break;
|
|
641
|
+
}
|
|
642
|
+
if (sub === "send" || sub === "stop") {
|
|
643
|
+
const target = positionals[1];
|
|
644
|
+
if (!target)
|
|
645
|
+
throw new Error(`usage: swarm run ${sub} <task|id>${sub === "send" ? ' "text"' : ""}`);
|
|
646
|
+
const r2 = sub === "send" ? await fetch(`${base}/v1/runs/${encodeURIComponent(target)}/send`, {
|
|
647
|
+
method: "POST",
|
|
648
|
+
headers: { "content-type": "application/json" },
|
|
649
|
+
body: JSON.stringify({ text: positionals.slice(2).join(" ") })
|
|
650
|
+
}) : await fetch(`${base}/v1/runs/${encodeURIComponent(target)}`, { method: "DELETE" });
|
|
651
|
+
const j = await r2.json();
|
|
652
|
+
if (json)
|
|
653
|
+
console.log(JSON.stringify(j));
|
|
654
|
+
else if (j.ok)
|
|
655
|
+
console.log(sub === "send" ? `sent to ${target}` : `stopped ${target}`);
|
|
656
|
+
else {
|
|
657
|
+
console.error(`REFUSED: ${j.error}`);
|
|
658
|
+
process.exit(1);
|
|
659
|
+
}
|
|
660
|
+
break;
|
|
661
|
+
}
|
|
662
|
+
const task = flag("--task") ?? sub;
|
|
663
|
+
let prompt = flag("--prompt");
|
|
664
|
+
const pf = flag("--prompt-file");
|
|
665
|
+
if (!prompt && pf)
|
|
666
|
+
prompt = await Bun.file(resolve3(pf)).text();
|
|
667
|
+
if (!task || !prompt)
|
|
668
|
+
throw new Error('usage: swarm run --task <id> --prompt "\u2026" | --prompt-file f [--model] [--permission-mode] [--allowed-tools a,b] [--max-turns n]');
|
|
669
|
+
const r = await fetch(`${base}/v1/runs`, {
|
|
670
|
+
method: "POST",
|
|
671
|
+
headers: { "content-type": "application/json" },
|
|
672
|
+
body: JSON.stringify({
|
|
673
|
+
projectId: proj.id,
|
|
674
|
+
task,
|
|
675
|
+
prompt,
|
|
676
|
+
owner: flag("--owner") ?? process.env.USER ?? "me",
|
|
677
|
+
model: flag("--model"),
|
|
678
|
+
permissionMode: flag("--permission-mode"),
|
|
679
|
+
allowedTools: flag("--allowed-tools")?.split(",").map((t) => t.trim()).filter(Boolean),
|
|
680
|
+
maxTurns: flag("--max-turns") ? Number(flag("--max-turns")) : undefined
|
|
681
|
+
})
|
|
682
|
+
}).then((x) => x.json());
|
|
683
|
+
if (json)
|
|
684
|
+
console.log(JSON.stringify(r));
|
|
685
|
+
else if (r.ok && r.run)
|
|
686
|
+
console.log(`run ${r.run.id} on ${task} (pid ${r.run.pid})
|
|
687
|
+
worktree: ${r.run.worktree}
|
|
688
|
+
session: ${r.run.sessionId}
|
|
689
|
+
log: ${r.run.log}
|
|
690
|
+
steer: swarm run send ${task} "\u2026" stop: swarm run stop ${task} watch: swarm tail --session ${r.run.sessionId}`);
|
|
691
|
+
else {
|
|
692
|
+
console.error(`REFUSED: ${r.error}`);
|
|
693
|
+
process.exit(1);
|
|
694
|
+
}
|
|
695
|
+
break;
|
|
696
|
+
}
|
|
697
|
+
case "handoff":
|
|
698
|
+
case "resume": {
|
|
699
|
+
await ensureDaemon({ quiet: true });
|
|
700
|
+
const task = arg();
|
|
701
|
+
if (!task)
|
|
702
|
+
throw new Error(`usage: swarm ${cmd} <task> \u2026`);
|
|
703
|
+
const proj = await api("/v1/projects", {
|
|
704
|
+
method: "POST",
|
|
705
|
+
headers: { "content-type": "application/json" },
|
|
706
|
+
body: JSON.stringify({ path: resolve3(".") })
|
|
707
|
+
});
|
|
708
|
+
if (cmd === "resume") {
|
|
709
|
+
const r2 = await fetch(`${new SwarmClient().baseUrl}/v1/handoffs?project=${proj.id}&task=${encodeURIComponent(task)}`);
|
|
710
|
+
const j = await r2.json();
|
|
711
|
+
if (json)
|
|
712
|
+
console.log(JSON.stringify(j.handoff));
|
|
713
|
+
else
|
|
714
|
+
console.log(j.text ?? `no handoff on ${task}`);
|
|
715
|
+
break;
|
|
716
|
+
}
|
|
717
|
+
const flag = (n) => {
|
|
718
|
+
const i = rest.indexOf(n);
|
|
719
|
+
return i >= 0 ? rest[i + 1] : undefined;
|
|
720
|
+
};
|
|
721
|
+
const r = await fetch(`${new SwarmClient().baseUrl}/v1/handoffs`, {
|
|
722
|
+
method: "POST",
|
|
723
|
+
headers: { "content-type": "application/json" },
|
|
724
|
+
body: JSON.stringify({
|
|
725
|
+
projectId: proj.id,
|
|
726
|
+
task,
|
|
727
|
+
done: flag("--done"),
|
|
728
|
+
remaining: flag("--remaining"),
|
|
729
|
+
files: (flag("--files") ?? "").split(",").map((f) => f.trim()).filter(Boolean),
|
|
730
|
+
verify: flag("--verify") ?? null,
|
|
731
|
+
by: flag("--by") ?? process.env.USER ?? null,
|
|
732
|
+
sessionId: process.env.CLAUDE_SESSION_ID ?? null
|
|
733
|
+
})
|
|
734
|
+
}).then((x) => x.json());
|
|
735
|
+
if (json)
|
|
736
|
+
console.log(JSON.stringify(r));
|
|
737
|
+
else if (r.ok)
|
|
738
|
+
console.log(`handoff recorded on ${task} \u2014 the next session in its worktree sees it on start`);
|
|
739
|
+
else {
|
|
740
|
+
console.error(`REFUSED: ${r.error}`);
|
|
741
|
+
process.exit(1);
|
|
742
|
+
}
|
|
743
|
+
break;
|
|
744
|
+
}
|
|
745
|
+
case "gate": {
|
|
746
|
+
await ensureDaemon({ quiet: true });
|
|
747
|
+
const proj = await api("/v1/projects", {
|
|
748
|
+
method: "POST",
|
|
749
|
+
headers: { "content-type": "application/json" },
|
|
750
|
+
body: JSON.stringify({ path: resolve3(".") })
|
|
751
|
+
});
|
|
752
|
+
const valueFlags = new Set(["--rubric", "--evidence"]);
|
|
753
|
+
const positionals = [];
|
|
754
|
+
for (let i = 0;i < rest.length; i++) {
|
|
755
|
+
const a = rest[i];
|
|
756
|
+
if (valueFlags.has(a))
|
|
757
|
+
i++;
|
|
758
|
+
else if (!a.startsWith("--"))
|
|
759
|
+
positionals.push(a);
|
|
760
|
+
}
|
|
761
|
+
const flag = (n) => {
|
|
762
|
+
const i = rest.indexOf(n);
|
|
763
|
+
return i >= 0 ? rest[i + 1] : undefined;
|
|
764
|
+
};
|
|
765
|
+
const sub = positionals[0] ?? "ls";
|
|
766
|
+
if (sub === "record") {
|
|
767
|
+
const [, task, gate, verdict] = positionals;
|
|
768
|
+
if (!task || !gate || !verdict)
|
|
769
|
+
throw new Error('usage: swarm gate record <task> <gate> pass|fail --rubric "what was checked" [--evidence "\u2026"]');
|
|
770
|
+
const r = await fetch(`${new SwarmClient().baseUrl}/v1/gates`, {
|
|
771
|
+
method: "POST",
|
|
772
|
+
headers: { "content-type": "application/json" },
|
|
773
|
+
body: JSON.stringify({
|
|
774
|
+
projectId: proj.id,
|
|
775
|
+
task,
|
|
776
|
+
gate,
|
|
777
|
+
verdict,
|
|
778
|
+
rubric: flag("--rubric"),
|
|
779
|
+
evidence: flag("--evidence"),
|
|
780
|
+
sessionId: process.env.CLAUDE_SESSION_ID ?? null
|
|
781
|
+
})
|
|
782
|
+
}).then((x) => x.json());
|
|
783
|
+
if (json)
|
|
784
|
+
console.log(JSON.stringify(r));
|
|
785
|
+
else if (r.ok)
|
|
786
|
+
console.log(`recorded ${gate} ${verdict} on ${task}`);
|
|
787
|
+
else {
|
|
788
|
+
console.error(`REFUSED: ${r.error}`);
|
|
789
|
+
process.exit(1);
|
|
790
|
+
}
|
|
791
|
+
break;
|
|
792
|
+
}
|
|
793
|
+
if (sub === "ls") {
|
|
794
|
+
const task = positionals[1];
|
|
795
|
+
const q = new URLSearchParams({ project: proj.id });
|
|
796
|
+
if (task)
|
|
797
|
+
q.set("task", task);
|
|
798
|
+
const g = await api(`/v1/gates?${q}`);
|
|
799
|
+
if (json)
|
|
800
|
+
console.log(JSON.stringify(g));
|
|
801
|
+
else if (task) {
|
|
802
|
+
if (!g.status?.length)
|
|
803
|
+
console.log(`no gates on ${task}${g.required.length ? ` (required: ${g.required.join(", ")})` : ""}`);
|
|
804
|
+
for (const st of g.status ?? [])
|
|
805
|
+
console.log(`${(st.verdict ?? "\u2014").padEnd(5)} ${st.gate.padEnd(12)} ${st.runs} run${st.runs === 1 ? "" : "s"}, ${st.fails} fail${st.fails === 1 ? "" : "s"}`);
|
|
806
|
+
for (const r of g.runs)
|
|
807
|
+
console.log(` ${r.createdAt.slice(0, 16)} ${r.gate.padEnd(12)} ${r.verdict.padEnd(5)} ${r.rubric.slice(0, 70)}`);
|
|
808
|
+
} else {
|
|
809
|
+
if (g.required.length)
|
|
810
|
+
console.log(`required: ${g.required.join(", ")}`);
|
|
811
|
+
if (!g.runs.length)
|
|
812
|
+
console.log("no gate runs yet");
|
|
813
|
+
for (const r of g.runs.slice(0, 50))
|
|
814
|
+
console.log(`${r.createdAt.slice(0, 16)} ${r.task.padEnd(10)} ${r.gate.padEnd(12)} ${r.verdict.padEnd(5)} ${r.rubric.slice(0, 60)}`);
|
|
815
|
+
}
|
|
816
|
+
break;
|
|
817
|
+
}
|
|
818
|
+
throw new Error("usage: swarm gate record|ls");
|
|
819
|
+
}
|
|
549
820
|
case "tasks": {
|
|
550
821
|
await ensureDaemon({ quiet: true });
|
|
551
822
|
const proj = await api("/v1/projects", {
|