opencode-swarm-plugin 0.6.3 → 0.10.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/bin/swarm.ts ADDED
@@ -0,0 +1,693 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * OpenCode Swarm Plugin CLI
4
+ *
5
+ * A beautiful interactive CLI for setting up and managing swarm coordination.
6
+ *
7
+ * Commands:
8
+ * swarm setup - Interactive installer for all dependencies
9
+ * swarm doctor - Check dependency health with detailed status
10
+ * swarm init - Initialize swarm in current project
11
+ * swarm version - Show version info
12
+ * swarm - Interactive mode (same as setup)
13
+ */
14
+
15
+ import * as p from "@clack/prompts";
16
+ import { existsSync, mkdirSync, writeFileSync } from "fs";
17
+ import { homedir } from "os";
18
+ import { join } from "path";
19
+
20
+ const VERSION = "0.10.0";
21
+
22
+ // ============================================================================
23
+ // ASCII Art & Branding
24
+ // ============================================================================
25
+
26
+ const BEE = `
27
+ \\ \` - ' /
28
+ - .(o o). -
29
+ ( >.< )
30
+ /| |\\
31
+ (_| |_) bzzzz...
32
+ `;
33
+
34
+ const BANNER = `
35
+ ███████╗██╗ ██╗ █████╗ ██████╗ ███╗ ███╗
36
+ ██╔════╝██║ ██║██╔══██╗██╔══██╗████╗ ████║
37
+ ███████╗██║ █╗ ██║███████║██████╔╝██╔████╔██║
38
+ ╚════██║██║███╗██║██╔══██║██╔══██╗██║╚██╔╝██║
39
+ ███████║╚███╔███╔╝██║ ██║██║ ██║██║ ╚═╝ ██║
40
+ ╚══════╝ ╚══╝╚══╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝
41
+ `;
42
+
43
+ const TAGLINE = "Multi-agent coordination for OpenCode";
44
+
45
+ const dim = (s: string) => `\x1b[2m${s}\x1b[0m`;
46
+ const yellow = (s: string) => `\x1b[33m${s}\x1b[0m`;
47
+ const cyan = (s: string) => `\x1b[36m${s}\x1b[0m`;
48
+
49
+ // ============================================================================
50
+ // Types
51
+ // ============================================================================
52
+
53
+ interface Dependency {
54
+ name: string;
55
+ command: string;
56
+ checkArgs: string[];
57
+ required: boolean;
58
+ install: string;
59
+ installType: "brew" | "curl" | "go" | "npm" | "manual";
60
+ description: string;
61
+ }
62
+
63
+ interface CheckResult {
64
+ dep: Dependency;
65
+ available: boolean;
66
+ version?: string;
67
+ }
68
+
69
+ // ============================================================================
70
+ // Dependencies
71
+ // ============================================================================
72
+
73
+ const DEPENDENCIES: Dependency[] = [
74
+ {
75
+ name: "OpenCode",
76
+ command: "opencode",
77
+ checkArgs: ["--version"],
78
+ required: true,
79
+ install: "brew install sst/tap/opencode",
80
+ installType: "brew",
81
+ description: "AI coding assistant (plugin host)",
82
+ },
83
+ {
84
+ name: "Beads",
85
+ command: "bd",
86
+ checkArgs: ["--version"],
87
+ required: true,
88
+ install:
89
+ "curl -fsSL https://raw.githubusercontent.com/steveyegge/beads/main/scripts/install.sh | bash",
90
+ installType: "curl",
91
+ description: "Git-backed issue tracking",
92
+ },
93
+ {
94
+ name: "Go",
95
+ command: "go",
96
+ checkArgs: ["version"],
97
+ required: false,
98
+ install: "brew install go",
99
+ installType: "brew",
100
+ description: "Required for Agent Mail",
101
+ },
102
+ {
103
+ name: "Agent Mail",
104
+ command: "agent-mail",
105
+ checkArgs: ["--help"],
106
+ required: false,
107
+ install: "go install github.com/joelhooks/agent-mail/cmd/agent-mail@latest",
108
+ installType: "go",
109
+ description: "Multi-agent coordination & file reservations",
110
+ },
111
+ {
112
+ name: "CASS",
113
+ command: "cass",
114
+ checkArgs: ["--help"],
115
+ required: false,
116
+ install: "https://github.com/Dicklesworthstone/cass",
117
+ installType: "manual",
118
+ description: "Cross-agent session search",
119
+ },
120
+ {
121
+ name: "UBS",
122
+ command: "ubs",
123
+ checkArgs: ["--help"],
124
+ required: false,
125
+ install: "https://github.com/joelhooks/ubs",
126
+ installType: "manual",
127
+ description: "Pre-commit bug scanning",
128
+ },
129
+ {
130
+ name: "semantic-memory",
131
+ command: "semantic-memory",
132
+ checkArgs: ["--help"],
133
+ required: false,
134
+ install: "npm install -g semantic-memory",
135
+ installType: "npm",
136
+ description: "Learning persistence with vector search",
137
+ },
138
+ {
139
+ name: "Redis",
140
+ command: "redis-cli",
141
+ checkArgs: ["ping"],
142
+ required: false,
143
+ install: "brew install redis && brew services start redis",
144
+ installType: "brew",
145
+ description: "Rate limiting (SQLite fallback available)",
146
+ },
147
+ ];
148
+
149
+ // ============================================================================
150
+ // Utilities
151
+ // ============================================================================
152
+
153
+ async function checkCommand(
154
+ cmd: string,
155
+ args: string[],
156
+ ): Promise<{ available: boolean; version?: string }> {
157
+ try {
158
+ const proc = Bun.spawn([cmd, ...args], {
159
+ stdout: "pipe",
160
+ stderr: "pipe",
161
+ });
162
+ const exitCode = await proc.exited;
163
+ if (exitCode === 0) {
164
+ const output = await new Response(proc.stdout).text();
165
+ const versionMatch = output.match(/v?(\d+\.\d+\.\d+)/);
166
+ return { available: true, version: versionMatch?.[1] };
167
+ }
168
+ return { available: false };
169
+ } catch {
170
+ return { available: false };
171
+ }
172
+ }
173
+
174
+ async function runInstall(command: string): Promise<boolean> {
175
+ try {
176
+ const proc = Bun.spawn(["bash", "-c", command], {
177
+ stdout: "inherit",
178
+ stderr: "inherit",
179
+ });
180
+ const exitCode = await proc.exited;
181
+ return exitCode === 0;
182
+ } catch {
183
+ return false;
184
+ }
185
+ }
186
+
187
+ async function checkAllDependencies(): Promise<CheckResult[]> {
188
+ const results: CheckResult[] = [];
189
+ for (const dep of DEPENDENCIES) {
190
+ const { available, version } = await checkCommand(
191
+ dep.command,
192
+ dep.checkArgs,
193
+ );
194
+ results.push({ dep, available, version });
195
+ }
196
+ return results;
197
+ }
198
+
199
+ // ============================================================================
200
+ // File Templates
201
+ // ============================================================================
202
+
203
+ const PLUGIN_WRAPPER = `import { SwarmPlugin } from "opencode-swarm-plugin"
204
+ export default SwarmPlugin
205
+ `;
206
+
207
+ const SWARM_COMMAND = `---
208
+ description: Decompose task into parallel subtasks and coordinate agents
209
+ ---
210
+
211
+ You are a swarm coordinator. Take a complex task, break it into beads, and unleash parallel agents.
212
+
213
+ ## Usage
214
+
215
+ /swarm <task description or bead-id>
216
+
217
+ ## Workflow
218
+
219
+ 1. **Initialize**: \`agentmail_init\` with project_path and task_description
220
+ 2. **Decompose**: Use \`swarm_select_strategy\` then \`swarm_plan_prompt\` to break down the task
221
+ 3. **Create beads**: \`beads_create_epic\` with subtasks and file assignments
222
+ 4. **Reserve files**: \`agentmail_reserve\` for each subtask's files
223
+ 5. **Spawn agents**: Use Task tool with \`swarm_spawn_subtask\` prompts - spawn ALL in parallel
224
+ 6. **Monitor**: Check \`agentmail_inbox\` for progress, use \`agentmail_summarize_thread\` for overview
225
+ 7. **Complete**: \`swarm_complete\` when done, then \`beads_sync\` to push
226
+
227
+ ## Strategy Selection
228
+
229
+ The plugin auto-selects decomposition strategy based on task keywords:
230
+
231
+ | Strategy | Best For | Keywords |
232
+ | ------------- | ----------------------- | -------------------------------------- |
233
+ | file-based | Refactoring, migrations | refactor, migrate, rename, update all |
234
+ | feature-based | New features | add, implement, build, create, feature |
235
+ | risk-based | Bug fixes, security | fix, bug, security, critical, urgent |
236
+
237
+ Begin decomposition now.
238
+ `;
239
+
240
+ const PLANNER_AGENT = `---
241
+ name: swarm-planner
242
+ description: Strategic task decomposition for swarm coordination
243
+ model: claude-sonnet-4-5
244
+ ---
245
+
246
+ You are a swarm planner. Decompose tasks into optimal parallel subtasks.
247
+
248
+ ## Workflow
249
+
250
+ 1. Call \`swarm_select_strategy\` to analyze the task
251
+ 2. Call \`swarm_plan_prompt\` to get strategy-specific guidance
252
+ 3. Create a BeadTree following the guidelines
253
+ 4. Return ONLY valid JSON - no markdown, no explanation
254
+
255
+ ## Output Format
256
+
257
+ \`\`\`json
258
+ {
259
+ "epic": { "title": "...", "description": "..." },
260
+ "subtasks": [
261
+ {
262
+ "title": "...",
263
+ "description": "...",
264
+ "files": ["src/..."],
265
+ "dependencies": [],
266
+ "estimated_complexity": 2
267
+ }
268
+ ]
269
+ }
270
+ \`\`\`
271
+
272
+ ## Rules
273
+
274
+ - 2-7 subtasks (too few = not parallel, too many = overhead)
275
+ - No file overlap between subtasks
276
+ - Include tests with the code they test
277
+ - Order by dependency (if B needs A, A comes first)
278
+ `;
279
+
280
+ // ============================================================================
281
+ // Commands
282
+ // ============================================================================
283
+
284
+ async function doctor() {
285
+ p.intro("swarm doctor v" + VERSION);
286
+
287
+ const s = p.spinner();
288
+ s.start("Checking dependencies...");
289
+
290
+ const results = await checkAllDependencies();
291
+
292
+ s.stop("Dependencies checked");
293
+
294
+ const required = results.filter((r) => r.dep.required);
295
+ const optional = results.filter((r) => !r.dep.required);
296
+
297
+ p.log.step("Required dependencies:");
298
+ for (const { dep, available, version } of required) {
299
+ if (available) {
300
+ p.log.success(dep.name + (version ? " v" + version : ""));
301
+ } else {
302
+ p.log.error(dep.name + " - not found");
303
+ p.log.message(" Install: " + dep.install);
304
+ }
305
+ }
306
+
307
+ p.log.step("Optional dependencies:");
308
+ for (const { dep, available, version } of optional) {
309
+ if (available) {
310
+ p.log.success(
311
+ dep.name + (version ? " v" + version : "") + " - " + dep.description,
312
+ );
313
+ } else {
314
+ p.log.warn(dep.name + " - not found (" + dep.description + ")");
315
+ if (dep.installType !== "manual") {
316
+ p.log.message(" Install: " + dep.install);
317
+ } else {
318
+ p.log.message(" See: " + dep.install);
319
+ }
320
+ }
321
+ }
322
+
323
+ const requiredMissing = required.filter((r) => !r.available);
324
+ const optionalMissing = optional.filter((r) => !r.available);
325
+
326
+ if (requiredMissing.length > 0) {
327
+ p.outro(
328
+ "Missing " +
329
+ requiredMissing.length +
330
+ " required dependencies. Run 'swarm setup' to install.",
331
+ );
332
+ process.exit(1);
333
+ } else if (optionalMissing.length > 0) {
334
+ p.outro(
335
+ "All required dependencies installed. " +
336
+ optionalMissing.length +
337
+ " optional missing.",
338
+ );
339
+ } else {
340
+ p.outro("All dependencies installed!");
341
+ }
342
+ }
343
+
344
+ async function setup() {
345
+ console.clear();
346
+ p.intro("opencode-swarm-plugin v" + VERSION);
347
+
348
+ const s = p.spinner();
349
+ s.start("Checking dependencies...");
350
+
351
+ const results = await checkAllDependencies();
352
+
353
+ s.stop("Dependencies checked");
354
+
355
+ const required = results.filter((r) => r.dep.required);
356
+ const optional = results.filter((r) => !r.dep.required);
357
+ const requiredMissing = required.filter((r) => !r.available);
358
+ const optionalMissing = optional.filter((r) => !r.available);
359
+
360
+ for (const { dep, available } of results) {
361
+ if (available) {
362
+ p.log.success(dep.name);
363
+ } else if (dep.required) {
364
+ p.log.error(dep.name + " (required)");
365
+ } else {
366
+ p.log.warn(dep.name + " (optional)");
367
+ }
368
+ }
369
+
370
+ if (requiredMissing.length > 0) {
371
+ p.log.step("Missing " + requiredMissing.length + " required dependencies");
372
+
373
+ for (const { dep } of requiredMissing) {
374
+ const shouldInstall = await p.confirm({
375
+ message: "Install " + dep.name + "? (" + dep.description + ")",
376
+ initialValue: true,
377
+ });
378
+
379
+ if (p.isCancel(shouldInstall)) {
380
+ p.cancel("Setup cancelled");
381
+ process.exit(0);
382
+ }
383
+
384
+ if (shouldInstall) {
385
+ const installSpinner = p.spinner();
386
+ installSpinner.start("Installing " + dep.name + "...");
387
+
388
+ const success = await runInstall(dep.install);
389
+
390
+ if (success) {
391
+ installSpinner.stop(dep.name + " installed");
392
+ } else {
393
+ installSpinner.stop("Failed to install " + dep.name);
394
+ p.log.error("Manual install: " + dep.install);
395
+ }
396
+ } else {
397
+ p.log.warn("Skipping " + dep.name + " - swarm may not work correctly");
398
+ }
399
+ }
400
+ }
401
+
402
+ if (optionalMissing.length > 0) {
403
+ const installable = optionalMissing.filter(
404
+ (r) => r.dep.installType !== "manual",
405
+ );
406
+
407
+ if (installable.length > 0) {
408
+ const toInstall = await p.multiselect({
409
+ message: "Install optional dependencies?",
410
+ options: installable.map(({ dep }) => ({
411
+ value: dep.name,
412
+ label: dep.name,
413
+ hint: dep.description,
414
+ })),
415
+ required: false,
416
+ });
417
+
418
+ if (p.isCancel(toInstall)) {
419
+ p.cancel("Setup cancelled");
420
+ process.exit(0);
421
+ }
422
+
423
+ if (Array.isArray(toInstall) && toInstall.length > 0) {
424
+ for (const name of toInstall) {
425
+ const { dep } = installable.find((r) => r.dep.name === name)!;
426
+
427
+ if (dep.name === "Agent Mail") {
428
+ const goResult = results.find((r) => r.dep.name === "Go");
429
+ if (!goResult?.available) {
430
+ p.log.warn("Agent Mail requires Go. Installing Go first...");
431
+ const goDep = DEPENDENCIES.find((d) => d.name === "Go")!;
432
+ const goSpinner = p.spinner();
433
+ goSpinner.start("Installing Go...");
434
+ const goSuccess = await runInstall(goDep.install);
435
+ if (goSuccess) {
436
+ goSpinner.stop("Go installed");
437
+ } else {
438
+ goSpinner.stop("Failed to install Go");
439
+ p.log.error("Cannot install Agent Mail without Go");
440
+ continue;
441
+ }
442
+ }
443
+ }
444
+
445
+ const installSpinner = p.spinner();
446
+ installSpinner.start("Installing " + dep.name + "...");
447
+
448
+ const success = await runInstall(dep.install);
449
+
450
+ if (success) {
451
+ installSpinner.stop(dep.name + " installed");
452
+ } else {
453
+ installSpinner.stop("Failed to install " + dep.name);
454
+ p.log.message(" Manual: " + dep.install);
455
+ }
456
+ }
457
+ }
458
+ }
459
+
460
+ const manual = optionalMissing.filter(
461
+ (r) => r.dep.installType === "manual",
462
+ );
463
+ if (manual.length > 0) {
464
+ p.log.step("Manual installation required:");
465
+ for (const { dep } of manual) {
466
+ p.log.message(" " + dep.name + ": " + dep.install);
467
+ }
468
+ }
469
+ }
470
+
471
+ p.log.step("Setting up OpenCode integration...");
472
+
473
+ const configDir = join(homedir(), ".config", "opencode");
474
+ const pluginsDir = join(configDir, "plugins");
475
+ const commandsDir = join(configDir, "commands");
476
+ const agentsDir = join(configDir, "agents");
477
+
478
+ for (const dir of [pluginsDir, commandsDir, agentsDir]) {
479
+ if (!existsSync(dir)) {
480
+ mkdirSync(dir, { recursive: true });
481
+ }
482
+ }
483
+
484
+ const pluginPath = join(pluginsDir, "swarm.ts");
485
+ const commandPath = join(commandsDir, "swarm.md");
486
+ const agentPath = join(agentsDir, "swarm-planner.md");
487
+
488
+ writeFileSync(pluginPath, PLUGIN_WRAPPER);
489
+ p.log.success("Plugin: " + pluginPath);
490
+
491
+ writeFileSync(commandPath, SWARM_COMMAND);
492
+ p.log.success("Command: " + commandPath);
493
+
494
+ writeFileSync(agentPath, PLANNER_AGENT);
495
+ p.log.success("Agent: " + agentPath);
496
+
497
+ p.note(
498
+ 'cd your-project\nbd init\nopencode\n/swarm "your task"',
499
+ "Next steps",
500
+ );
501
+
502
+ p.outro("Setup complete! Run 'swarm doctor' to verify.");
503
+ }
504
+
505
+ async function init() {
506
+ p.intro("swarm init v" + VERSION);
507
+
508
+ const gitDir = existsSync(".git");
509
+ if (!gitDir) {
510
+ p.log.error("Not in a git repository");
511
+ p.log.message("Run 'git init' first, or cd to a git repo");
512
+ p.outro("Aborted");
513
+ process.exit(1);
514
+ }
515
+
516
+ const beadsDir = existsSync(".beads");
517
+ if (beadsDir) {
518
+ p.log.warn("Beads already initialized in this project");
519
+
520
+ const reinit = await p.confirm({
521
+ message: "Re-initialize beads?",
522
+ initialValue: false,
523
+ });
524
+
525
+ if (p.isCancel(reinit) || !reinit) {
526
+ p.outro("Aborted");
527
+ process.exit(0);
528
+ }
529
+ }
530
+
531
+ const s = p.spinner();
532
+ s.start("Initializing beads...");
533
+
534
+ const success = await runInstall("bd init");
535
+
536
+ if (success) {
537
+ s.stop("Beads initialized");
538
+ p.log.success("Created .beads/ directory");
539
+
540
+ const createBead = await p.confirm({
541
+ message: "Create your first bead?",
542
+ initialValue: true,
543
+ });
544
+
545
+ if (!p.isCancel(createBead) && createBead) {
546
+ const title = await p.text({
547
+ message: "Bead title:",
548
+ placeholder: "Implement user authentication",
549
+ validate: (v) => (v.length === 0 ? "Title required" : undefined),
550
+ });
551
+
552
+ if (!p.isCancel(title)) {
553
+ const typeResult = await p.select({
554
+ message: "Type:",
555
+ options: [
556
+ { value: "feature", label: "Feature", hint: "New functionality" },
557
+ { value: "bug", label: "Bug", hint: "Something broken" },
558
+ { value: "task", label: "Task", hint: "General work item" },
559
+ { value: "chore", label: "Chore", hint: "Maintenance" },
560
+ ],
561
+ });
562
+
563
+ if (!p.isCancel(typeResult)) {
564
+ const beadSpinner = p.spinner();
565
+ beadSpinner.start("Creating bead...");
566
+
567
+ const createSuccess = await runInstall(
568
+ 'bd create --title "' + title + '" --type ' + typeResult,
569
+ );
570
+
571
+ if (createSuccess) {
572
+ beadSpinner.stop("Bead created");
573
+ } else {
574
+ beadSpinner.stop("Failed to create bead");
575
+ }
576
+ }
577
+ }
578
+ }
579
+
580
+ p.outro("Project initialized! Use '/swarm' in OpenCode to get started.");
581
+ } else {
582
+ s.stop("Failed to initialize beads");
583
+ p.log.error("Make sure 'bd' is installed: swarm doctor");
584
+ p.outro("Aborted");
585
+ process.exit(1);
586
+ }
587
+ }
588
+
589
+ function version() {
590
+ console.log(yellow(BANNER));
591
+ console.log(dim(" " + TAGLINE));
592
+ console.log();
593
+ console.log(" Version: " + VERSION);
594
+ console.log(" Docs: https://github.com/joelhooks/opencode-swarm-plugin");
595
+ console.log();
596
+ }
597
+
598
+ function config() {
599
+ const configDir = join(homedir(), ".config", "opencode");
600
+ const pluginPath = join(configDir, "plugins", "swarm.ts");
601
+ const commandPath = join(configDir, "commands", "swarm.md");
602
+ const agentPath = join(configDir, "agents", "swarm-planner.md");
603
+
604
+ console.log(yellow(BANNER));
605
+ console.log(dim(" " + TAGLINE + " v" + VERSION));
606
+ console.log();
607
+ console.log(cyan("Config Files:"));
608
+ console.log();
609
+
610
+ const files = [
611
+ { path: pluginPath, desc: "Plugin loader", emoji: "🔌" },
612
+ { path: commandPath, desc: "/swarm command prompt", emoji: "📜" },
613
+ { path: agentPath, desc: "@swarm-planner agent", emoji: "🤖" },
614
+ ];
615
+
616
+ for (const { path, desc, emoji } of files) {
617
+ const exists = existsSync(path);
618
+ const status = exists ? "✓" : "✗";
619
+ const color = exists ? "\x1b[32m" : "\x1b[31m";
620
+ console.log(` ${emoji} ${desc}`);
621
+ console.log(` ${color}${status}\x1b[0m ${dim(path)}`);
622
+ console.log();
623
+ }
624
+
625
+ console.log(dim("Edit these files to customize swarm behavior."));
626
+ console.log(dim("Run 'swarm setup' to regenerate defaults."));
627
+ console.log();
628
+ }
629
+
630
+ function help() {
631
+ console.log(yellow(BANNER));
632
+ console.log(dim(" " + TAGLINE + " v" + VERSION));
633
+ console.log(cyan(BEE));
634
+ console.log(`
635
+ ${cyan("Commands:")}
636
+ swarm setup Interactive installer - checks and installs dependencies
637
+ swarm doctor Health check - shows status of all dependencies
638
+ swarm init Initialize beads in current project
639
+ swarm config Show paths to generated config files
640
+ swarm version Show version and banner
641
+ swarm help Show this help
642
+
643
+ ${cyan("Usage in OpenCode:")}
644
+ /swarm "Add user authentication with OAuth"
645
+ @swarm-planner "Refactor all components to use hooks"
646
+
647
+ ${cyan("Customization:")}
648
+ Edit the generated files to customize behavior:
649
+ ${dim("~/.config/opencode/commands/swarm.md")} - /swarm command prompt
650
+ ${dim("~/.config/opencode/agents/swarm-planner.md")} - @swarm-planner agent
651
+ ${dim("~/.config/opencode/plugins/swarm.ts")} - Plugin loader
652
+
653
+ ${dim("Docs: https://github.com/joelhooks/opencode-swarm-plugin")}
654
+ `);
655
+ }
656
+
657
+ // ============================================================================
658
+ // Main
659
+ // ============================================================================
660
+
661
+ const command = process.argv[2];
662
+
663
+ switch (command) {
664
+ case "setup":
665
+ await setup();
666
+ break;
667
+ case "doctor":
668
+ await doctor();
669
+ break;
670
+ case "init":
671
+ await init();
672
+ break;
673
+ case "config":
674
+ config();
675
+ break;
676
+ case "version":
677
+ case "--version":
678
+ case "-v":
679
+ version();
680
+ break;
681
+ case "help":
682
+ case "--help":
683
+ case "-h":
684
+ help();
685
+ break;
686
+ case undefined:
687
+ await setup();
688
+ break;
689
+ default:
690
+ console.error("Unknown command: " + command);
691
+ help();
692
+ process.exit(1);
693
+ }