opencode-swarm-plugin 0.55.0 → 0.56.1

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 CHANGED
@@ -25,6 +25,29 @@ npm install -g opencode-swarm-plugin@latest
25
25
  swarm setup
26
26
  ```
27
27
 
28
+ ### Claude Code Plugin (Marketplace)
29
+
30
+ ```text
31
+ /plugin
32
+ ```
33
+
34
+ Choose **Marketplace → opencode-swarm-plugin → Install**.
35
+
36
+ **Global install (npm):**
37
+
38
+ ```bash
39
+ # After `npm install -g opencode-swarm-plugin`
40
+ swarm claude install
41
+ ```
42
+
43
+ **Project-local config (standalone):**
44
+
45
+ ```bash
46
+ swarm claude init
47
+ ```
48
+
49
+ **MCP auto-launch:** Claude Code starts MCP servers declared in the plugin `mcpServers` config automatically. You only need `swarm mcp-serve` when debugging outside Claude Code.
50
+
28
51
  ### 2. Initialize in Your Project
29
52
 
30
53
  ```bash
package/bin/swarm.ts CHANGED
@@ -8,6 +8,8 @@
8
8
  * swarm setup - Interactive installer for all dependencies
9
9
  * swarm doctor - Check dependency health with detailed status
10
10
  * swarm init - Initialize swarm in current project
11
+ * swarm claude - Claude Code integration commands
12
+ * swarm mcp-serve - Debug-only MCP server (Claude auto-launches)
11
13
  * swarm version - Show version info
12
14
  * swarm - Interactive mode (same as setup)
13
15
  */
@@ -17,17 +19,20 @@ import {
17
19
  chmodSync,
18
20
  copyFileSync,
19
21
  existsSync,
22
+ lstatSync,
20
23
  mkdirSync,
21
24
  readFileSync,
25
+ readlinkSync,
22
26
  readdirSync,
23
27
  renameSync,
24
28
  rmdirSync,
25
29
  rmSync,
26
30
  statSync,
31
+ symlinkSync,
27
32
  writeFileSync,
28
33
  } from "fs";
29
34
  import { homedir } from "os";
30
- import { basename, dirname, join } from "path";
35
+ import { basename, dirname, join, resolve } from "path";
31
36
  import { fileURLToPath } from "url";
32
37
  import {
33
38
  checkBeadsMigrationNeeded,
@@ -117,6 +122,8 @@ const pkgPath = existsSync(join(__dirname, "..", "package.json"))
117
122
  : join(__dirname, "..", "..", "package.json");
118
123
  const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
119
124
  const VERSION: string = pkg.version;
125
+ const PACKAGE_ROOT = dirname(pkgPath);
126
+ const CLAUDE_PLUGIN_NAME = "swarm";
120
127
 
121
128
  // ============================================================================
122
129
  // ASCII Art & Branding
@@ -493,6 +500,15 @@ const DEPENDENCIES: Dependency[] = [
493
500
  installType: "brew",
494
501
  description: "AI coding assistant (plugin host)",
495
502
  },
503
+ {
504
+ name: "Claude Code",
505
+ command: "claude",
506
+ checkArgs: ["--version"],
507
+ required: false,
508
+ install: "https://docs.anthropic.com/claude-code",
509
+ installType: "manual",
510
+ description: "Claude Code CLI (optional host)",
511
+ },
496
512
  // Note: Beads CLI (bd) is NO LONGER required - we use HiveAdapter from swarm-mail
497
513
  // which provides the same functionality programmatically without external dependencies
498
514
  {
@@ -584,6 +600,134 @@ async function checkAllDependencies(): Promise<CheckResult[]> {
584
600
  return results;
585
601
  }
586
602
 
603
+ // ============================================================================
604
+ // Claude Code Helpers
605
+ // ============================================================================
606
+
607
+ interface ClaudeHookInput {
608
+ project?: { path?: string };
609
+ cwd?: string;
610
+ session?: { id?: string };
611
+ metadata?: { cwd?: string };
612
+ }
613
+
614
+ /**
615
+ * Resolve the Claude Code plugin root bundled with the package.
616
+ */
617
+ function getClaudePluginRoot(): string {
618
+ return join(PACKAGE_ROOT, "claude-plugin");
619
+ }
620
+
621
+ /**
622
+ * Resolve the Claude Code config directory.
623
+ */
624
+ function getClaudeConfigDir(): string {
625
+ return join(homedir(), ".claude");
626
+ }
627
+
628
+ /**
629
+ * Read JSON input from stdin for Claude Code hooks.
630
+ */
631
+ async function readHookInput<T>(): Promise<T | null> {
632
+ if (process.stdin.isTTY) return null;
633
+ const chunks: string[] = [];
634
+ for await (const chunk of process.stdin) {
635
+ chunks.push(chunk.toString());
636
+ }
637
+ const raw = chunks.join("").trim();
638
+ if (!raw) return null;
639
+ try {
640
+ return JSON.parse(raw) as T;
641
+ } catch {
642
+ return null;
643
+ }
644
+ }
645
+
646
+ /**
647
+ * Resolve the project path for Claude Code hook executions.
648
+ */
649
+ function resolveClaudeProjectPath(input: ClaudeHookInput | null): string {
650
+ return (
651
+ input?.project?.path ||
652
+ input?.cwd ||
653
+ input?.metadata?.cwd ||
654
+ process.env.CLAUDE_PROJECT_DIR ||
655
+ process.env.PWD ||
656
+ process.cwd()
657
+ );
658
+ }
659
+
660
+ /**
661
+ * Format hook output for Claude Code context injection.
662
+ */
663
+ function writeClaudeHookContext(additionalContext: string): void {
664
+ if (!additionalContext.trim()) return;
665
+ process.stdout.write(
666
+ `${JSON.stringify({
667
+ additionalContext,
668
+ })}\n`,
669
+ );
670
+ }
671
+
672
+ interface ClaudeInstallStatus {
673
+ pluginRoot: string;
674
+ globalPluginPath: string;
675
+ globalPluginTarget?: string;
676
+ globalPluginExists: boolean;
677
+ globalPluginLinked: boolean;
678
+ projectClaudeDir: string;
679
+ projectConfigExists: boolean;
680
+ projectConfigPaths: string[];
681
+ }
682
+
683
+ /**
684
+ * Inspect Claude Code install state for global and project scopes.
685
+ */
686
+ function getClaudeInstallStatus(projectPath: string): ClaudeInstallStatus {
687
+ const pluginRoot = getClaudePluginRoot();
688
+ const claudeConfigDir = getClaudeConfigDir();
689
+ const globalPluginPath = join(claudeConfigDir, "plugins", CLAUDE_PLUGIN_NAME);
690
+
691
+ let globalPluginExists = false;
692
+ let globalPluginLinked = false;
693
+ let globalPluginTarget: string | undefined;
694
+
695
+ if (existsSync(globalPluginPath)) {
696
+ globalPluginExists = true;
697
+ try {
698
+ const stat = lstatSync(globalPluginPath);
699
+ if (stat.isSymbolicLink()) {
700
+ globalPluginLinked = true;
701
+ const target = readlinkSync(globalPluginPath);
702
+ globalPluginTarget = resolve(dirname(globalPluginPath), target);
703
+ }
704
+ } catch {
705
+ // Ignore errors
706
+ }
707
+ }
708
+
709
+ const projectClaudeDir = join(projectPath, ".claude");
710
+ const projectConfigPaths = [
711
+ join(projectClaudeDir, "commands"),
712
+ join(projectClaudeDir, "agents"),
713
+ join(projectClaudeDir, "skills"),
714
+ join(projectClaudeDir, "hooks"),
715
+ join(projectClaudeDir, ".mcp.json"),
716
+ ];
717
+ const projectConfigExists = projectConfigPaths.some((path) => existsSync(path));
718
+
719
+ return {
720
+ pluginRoot,
721
+ globalPluginPath,
722
+ globalPluginTarget,
723
+ globalPluginExists,
724
+ globalPluginLinked,
725
+ projectClaudeDir,
726
+ projectConfigExists,
727
+ projectConfigPaths,
728
+ };
729
+ }
730
+
587
731
  // ============================================================================
588
732
  // Skills Sync Utilities
589
733
  // ============================================================================
@@ -1594,6 +1738,8 @@ function getFixCommand(dep: Dependency): string | null {
1594
1738
  switch (dep.name) {
1595
1739
  case "OpenCode":
1596
1740
  return "brew install sst/tap/opencode";
1741
+ case "Claude Code":
1742
+ return "See: https://docs.anthropic.com/claude-code";
1597
1743
  case "Ollama":
1598
1744
  return "brew install ollama && ollama pull mxbai-embed-large";
1599
1745
  case "Redis":
@@ -1729,6 +1875,42 @@ async function doctor(debug = false) {
1729
1875
  }
1730
1876
  }
1731
1877
 
1878
+ // Claude Code checks
1879
+ p.log.step("Claude Code:");
1880
+ const claudeResult = results.find((result) => result.dep.name === "Claude Code");
1881
+ const claudeStatus = getClaudeInstallStatus(process.cwd());
1882
+
1883
+ if (!claudeResult?.available) {
1884
+ p.log.warn("Claude Code CLI not detected (optional)");
1885
+ p.log.message(dim(" Install: https://docs.anthropic.com/claude-code"));
1886
+ } else {
1887
+ p.log.success(`Claude Code CLI detected${claudeResult.version ? ` v${claudeResult.version}` : ""}`);
1888
+
1889
+ if (existsSync(claudeStatus.pluginRoot)) {
1890
+ p.log.message(dim(` Plugin bundle: ${claudeStatus.pluginRoot}`));
1891
+ } else {
1892
+ p.log.warn(`Claude plugin bundle missing: ${claudeStatus.pluginRoot}`);
1893
+ }
1894
+
1895
+ if (claudeStatus.globalPluginExists) {
1896
+ if (claudeStatus.globalPluginLinked) {
1897
+ p.log.success(`Global plugin symlink: ${claudeStatus.globalPluginPath}`);
1898
+ } else {
1899
+ p.log.warn(`Global plugin exists but is not a symlink: ${claudeStatus.globalPluginPath}`);
1900
+ }
1901
+ } else {
1902
+ p.log.warn("Global Claude plugin not installed");
1903
+ p.log.message(dim(" Run: swarm claude install"));
1904
+ }
1905
+
1906
+ if (claudeStatus.projectConfigExists) {
1907
+ p.log.success(`Project Claude config: ${claudeStatus.projectClaudeDir}`);
1908
+ } else {
1909
+ p.log.warn("Project Claude config not found");
1910
+ p.log.message(dim(" Run: swarm claude init"));
1911
+ }
1912
+ }
1913
+
1732
1914
  if (requiredMissing.length > 0) {
1733
1915
  p.outro(
1734
1916
  "Missing " +
@@ -2545,6 +2727,36 @@ async function setup(forceReinstall = false, nonInteractive = false) {
2545
2727
  }
2546
2728
  }
2547
2729
 
2730
+ // Claude Code checks
2731
+ p.log.step("Claude Code integration (optional)...");
2732
+ const claudeResult = results.find((result) => result.dep.name === "Claude Code");
2733
+ const claudeStatus = getClaudeInstallStatus(cwd);
2734
+
2735
+ if (!claudeResult?.available) {
2736
+ p.log.warn("Claude Code not detected (optional)");
2737
+ p.log.message(dim(" Install: https://docs.anthropic.com/claude-code"));
2738
+ } else {
2739
+ const versionInfo = claudeResult.version ? ` v${claudeResult.version}` : "";
2740
+ p.log.success(`Claude Code detected${versionInfo}`);
2741
+ p.log.message(dim(` Plugin bundle: ${claudeStatus.pluginRoot}`));
2742
+
2743
+ if (claudeStatus.globalPluginExists) {
2744
+ if (claudeStatus.globalPluginLinked) {
2745
+ p.log.success(`Claude plugin linked: ${claudeStatus.globalPluginPath}`);
2746
+ } else {
2747
+ p.log.warn(`Claude plugin exists but is not a symlink: ${claudeStatus.globalPluginPath}`);
2748
+ }
2749
+ } else {
2750
+ p.log.message(dim(" Run 'swarm claude install' for a dev symlink"));
2751
+ }
2752
+
2753
+ if (claudeStatus.projectConfigExists) {
2754
+ p.log.success(`Project Claude config: ${claudeStatus.projectClaudeDir}`);
2755
+ } else {
2756
+ p.log.message(dim(" Run 'swarm claude init' to create .claude/ config"));
2757
+ }
2758
+ }
2759
+
2548
2760
  // Show setup summary
2549
2761
  const totalFiles = stats.created + stats.updated + stats.unchanged;
2550
2762
  const summaryParts: string[] = [];
@@ -2563,6 +2775,299 @@ async function setup(forceReinstall = false, nonInteractive = false) {
2563
2775
  p.outro("Run 'swarm doctor' to verify installation.");
2564
2776
  }
2565
2777
 
2778
+ // ============================================================================
2779
+ // Claude Code Commands
2780
+ // ============================================================================
2781
+
2782
+ /**
2783
+ * Handle Claude Code subcommands.
2784
+ */
2785
+ async function claudeCommand() {
2786
+ const args = process.argv.slice(3);
2787
+ const subcommand = args[0];
2788
+
2789
+ if (!subcommand || ["help", "--help", "-h"].includes(subcommand)) {
2790
+ showClaudeHelp();
2791
+ return;
2792
+ }
2793
+
2794
+ switch (subcommand) {
2795
+ case "path":
2796
+ claudePath();
2797
+ break;
2798
+ case "install":
2799
+ await claudeInstall();
2800
+ break;
2801
+ case "uninstall":
2802
+ await claudeUninstall();
2803
+ break;
2804
+ case "init":
2805
+ await claudeInit();
2806
+ break;
2807
+ case "session-start":
2808
+ await claudeSessionStart();
2809
+ break;
2810
+ case "user-prompt":
2811
+ await claudeUserPrompt();
2812
+ break;
2813
+ case "pre-compact":
2814
+ await claudePreCompact();
2815
+ break;
2816
+ case "session-end":
2817
+ await claudeSessionEnd();
2818
+ break;
2819
+ default:
2820
+ console.error(`Unknown subcommand: ${subcommand}`);
2821
+ showClaudeHelp();
2822
+ process.exit(1);
2823
+ }
2824
+ }
2825
+
2826
+ function showClaudeHelp() {
2827
+ console.log(`
2828
+ Usage: swarm claude <command>
2829
+
2830
+ Commands:
2831
+ path Print Claude plugin path (for --plugin-dir)
2832
+ install Symlink plugin into ~/.claude/plugins/${CLAUDE_PLUGIN_NAME}
2833
+ uninstall Remove Claude plugin symlink
2834
+ init Create project-local .claude/ config
2835
+ session-start Hook: session start context (JSON output)
2836
+ user-prompt Hook: prompt submit context (JSON output)
2837
+ pre-compact Hook: pre-compaction handler
2838
+ session-end Hook: session cleanup
2839
+ `);
2840
+ }
2841
+
2842
+ /**
2843
+ * Print the bundled Claude plugin path.
2844
+ */
2845
+ function claudePath() {
2846
+ const pluginRoot = getClaudePluginRoot();
2847
+ if (!existsSync(pluginRoot)) {
2848
+ console.error(`Claude plugin not found at ${pluginRoot}`);
2849
+ process.exit(1);
2850
+ }
2851
+ console.log(pluginRoot);
2852
+ }
2853
+
2854
+ /**
2855
+ * Install the Claude plugin symlink for local development.
2856
+ */
2857
+ async function claudeInstall() {
2858
+ p.intro("swarm claude install");
2859
+ const pluginRoot = getClaudePluginRoot();
2860
+ if (!existsSync(pluginRoot)) {
2861
+ p.log.error(`Claude plugin not found at ${pluginRoot}`);
2862
+ p.outro("Aborted");
2863
+ process.exit(1);
2864
+ }
2865
+
2866
+ const claudeConfigDir = getClaudeConfigDir();
2867
+ const pluginsDir = join(claudeConfigDir, "plugins");
2868
+ const pluginPath = join(pluginsDir, CLAUDE_PLUGIN_NAME);
2869
+
2870
+ mkdirWithStatus(pluginsDir);
2871
+
2872
+ if (existsSync(pluginPath)) {
2873
+ const stat = lstatSync(pluginPath);
2874
+ if (!stat.isSymbolicLink()) {
2875
+ p.log.error(`Existing path is not a symlink: ${pluginPath}`);
2876
+ p.outro("Aborted");
2877
+ process.exit(1);
2878
+ }
2879
+
2880
+ const target = readlinkSync(pluginPath);
2881
+ const resolved = resolve(dirname(pluginPath), target);
2882
+ if (resolved === pluginRoot) {
2883
+ p.log.success("Claude plugin already linked");
2884
+ p.outro("Done");
2885
+ return;
2886
+ }
2887
+
2888
+ rmSync(pluginPath, { force: true });
2889
+ }
2890
+
2891
+ symlinkSync(pluginRoot, pluginPath);
2892
+ p.log.success(`Linked ${CLAUDE_PLUGIN_NAME} → ${pluginRoot}`);
2893
+ p.log.message(dim(" Claude Code will auto-launch MCP from .mcp.json"));
2894
+ p.outro("Claude plugin installed");
2895
+ }
2896
+
2897
+ /**
2898
+ * Remove the Claude plugin symlink.
2899
+ */
2900
+ async function claudeUninstall() {
2901
+ p.intro("swarm claude uninstall");
2902
+ const pluginPath = join(getClaudeConfigDir(), "plugins", CLAUDE_PLUGIN_NAME);
2903
+
2904
+ if (!existsSync(pluginPath)) {
2905
+ p.log.warn("Claude plugin symlink not found");
2906
+ p.outro("Nothing to remove");
2907
+ return;
2908
+ }
2909
+
2910
+ rmSync(pluginPath, { recursive: true, force: true });
2911
+ p.log.success(`Removed ${pluginPath}`);
2912
+ p.outro("Claude plugin uninstalled");
2913
+ }
2914
+
2915
+ /**
2916
+ * Create project-local Claude Code config from bundled plugin assets.
2917
+ */
2918
+ async function claudeInit() {
2919
+ p.intro("swarm claude init");
2920
+ const projectPath = process.cwd();
2921
+ const pluginRoot = getClaudePluginRoot();
2922
+
2923
+ if (!existsSync(pluginRoot)) {
2924
+ p.log.error(`Claude plugin not found at ${pluginRoot}`);
2925
+ p.outro("Aborted");
2926
+ process.exit(1);
2927
+ }
2928
+
2929
+ const projectClaudeDir = join(projectPath, ".claude");
2930
+ const commandDir = join(projectClaudeDir, "commands");
2931
+ const agentDir = join(projectClaudeDir, "agents");
2932
+ const skillsDir = join(projectClaudeDir, "skills");
2933
+ const hooksDir = join(projectClaudeDir, "hooks");
2934
+
2935
+ for (const dir of [projectClaudeDir, commandDir, agentDir, skillsDir, hooksDir]) {
2936
+ mkdirWithStatus(dir);
2937
+ }
2938
+
2939
+ const copyMap = [
2940
+ { src: join(pluginRoot, "commands"), dest: commandDir, label: "Commands" },
2941
+ { src: join(pluginRoot, "agents"), dest: agentDir, label: "Agents" },
2942
+ { src: join(pluginRoot, "skills"), dest: skillsDir, label: "Skills" },
2943
+ { src: join(pluginRoot, "hooks"), dest: hooksDir, label: "Hooks" },
2944
+ ];
2945
+
2946
+ for (const { src, dest, label } of copyMap) {
2947
+ if (existsSync(src)) {
2948
+ copyDirRecursiveSync(src, dest);
2949
+ p.log.success(`${label}: ${dest}`);
2950
+ }
2951
+ }
2952
+
2953
+ const mcpSourcePath = join(pluginRoot, ".mcp.json");
2954
+ if (existsSync(mcpSourcePath)) {
2955
+ const mcpDestPath = join(projectClaudeDir, ".mcp.json");
2956
+ const content = readFileSync(mcpSourcePath, "utf-8");
2957
+ writeFileWithStatus(mcpDestPath, content, "MCP config");
2958
+ }
2959
+
2960
+ const lspSourcePath = join(pluginRoot, ".lsp.json");
2961
+ if (existsSync(lspSourcePath)) {
2962
+ const lspDestPath = join(projectClaudeDir, ".lsp.json");
2963
+ const content = readFileSync(lspSourcePath, "utf-8");
2964
+ writeFileWithStatus(lspDestPath, content, "LSP config");
2965
+ }
2966
+
2967
+ p.log.message(dim(" Uses ${CLAUDE_PLUGIN_ROOT} in MCP config"));
2968
+ p.outro("Claude project config ready");
2969
+ }
2970
+
2971
+ /**
2972
+ * Claude hook: start a session and emit context for Claude Code.
2973
+ */
2974
+ async function claudeSessionStart() {
2975
+ try {
2976
+ const input = await readHookInput<ClaudeHookInput>();
2977
+ const projectPath = resolveClaudeProjectPath(input);
2978
+ const swarmMail = await getSwarmMailLibSQL(projectPath);
2979
+ const db = await swarmMail.getDatabase(projectPath);
2980
+ const adapter = createHiveAdapter(db, projectPath);
2981
+
2982
+ await adapter.runMigrations();
2983
+
2984
+ const session = await adapter.startSession(projectPath, {});
2985
+ const contextLines = [
2986
+ `Swarm session started: ${session.id}`,
2987
+ ];
2988
+
2989
+ if (session.active_cell_id) {
2990
+ contextLines.push(`Active cell: ${session.active_cell_id}`);
2991
+ }
2992
+
2993
+ if (session.previous_handoff_notes) {
2994
+ contextLines.push("Previous handoff notes:");
2995
+ contextLines.push(session.previous_handoff_notes);
2996
+ }
2997
+
2998
+ writeClaudeHookContext(contextLines.join("\n"));
2999
+ } catch (error) {
3000
+ console.error(error instanceof Error ? error.message : String(error));
3001
+ }
3002
+ }
3003
+
3004
+ /**
3005
+ * Claude hook: provide active session context on prompt submission.
3006
+ */
3007
+ async function claudeUserPrompt() {
3008
+ try {
3009
+ const input = await readHookInput<ClaudeHookInput>();
3010
+ const projectPath = resolveClaudeProjectPath(input);
3011
+ const swarmMail = await getSwarmMailLibSQL(projectPath);
3012
+ const db = await swarmMail.getDatabase(projectPath);
3013
+ const adapter = createHiveAdapter(db, projectPath);
3014
+
3015
+ await adapter.runMigrations();
3016
+
3017
+ const session = await adapter.getCurrentSession(projectPath);
3018
+ if (!session) return;
3019
+
3020
+ const contextLines = [`Active swarm session: ${session.id}`];
3021
+ if (session.active_cell_id) {
3022
+ contextLines.push(`Active cell: ${session.active_cell_id}`);
3023
+ }
3024
+
3025
+ writeClaudeHookContext(contextLines.join("\n"));
3026
+ } catch (error) {
3027
+ console.error(error instanceof Error ? error.message : String(error));
3028
+ }
3029
+ }
3030
+
3031
+ /**
3032
+ * Claude hook: prepare for compaction (best-effort).
3033
+ */
3034
+ async function claudePreCompact() {
3035
+ try {
3036
+ const input = await readHookInput<ClaudeHookInput>();
3037
+ const projectPath = resolveClaudeProjectPath(input);
3038
+ const swarmMail = await getSwarmMailLibSQL(projectPath);
3039
+ const db = await swarmMail.getDatabase(projectPath);
3040
+ const adapter = createHiveAdapter(db, projectPath);
3041
+
3042
+ await adapter.runMigrations();
3043
+ await adapter.getCurrentSession(projectPath);
3044
+ } catch (error) {
3045
+ console.error(error instanceof Error ? error.message : String(error));
3046
+ }
3047
+ }
3048
+
3049
+ /**
3050
+ * Claude hook: end the active session.
3051
+ */
3052
+ async function claudeSessionEnd() {
3053
+ try {
3054
+ const input = await readHookInput<ClaudeHookInput>();
3055
+ const projectPath = resolveClaudeProjectPath(input);
3056
+ const swarmMail = await getSwarmMailLibSQL(projectPath);
3057
+ const db = await swarmMail.getDatabase(projectPath);
3058
+ const adapter = createHiveAdapter(db, projectPath);
3059
+
3060
+ await adapter.runMigrations();
3061
+
3062
+ const currentSession = await adapter.getCurrentSession(projectPath);
3063
+ if (!currentSession) return;
3064
+
3065
+ await adapter.endSession(projectPath, currentSession.id, {});
3066
+ } catch (error) {
3067
+ console.error(error instanceof Error ? error.message : String(error));
3068
+ }
3069
+ }
3070
+
2566
3071
  async function init() {
2567
3072
  p.intro("swarm init v" + VERSION);
2568
3073
 
@@ -3292,10 +3797,12 @@ ${cyan("Commands:")}
3292
3797
  swarm doctor Health check - shows status of all dependencies
3293
3798
  swarm init Initialize beads in current project
3294
3799
  swarm config Show paths to generated config files
3800
+ swarm claude Claude Code integration (path/install/uninstall/init/hooks)
3295
3801
  swarm agents Update AGENTS.md with skill awareness
3296
3802
  swarm migrate Migrate PGlite database to libSQL
3297
3803
  swarm serve Start SSE server for real-time event streaming (port 4483 - HIVE)
3298
3804
  --port <n> Port to listen on (default: 4483)
3805
+ swarm mcp-serve Debug-only MCP server (Claude auto-launches via .mcp.json)
3299
3806
  swarm viz Alias for 'swarm serve' (deprecated, use serve)
3300
3807
  --port <n> Port to listen on (default: 4483)
3301
3808
  swarm cells List or get cells from database (replaces 'swarm tool hive_query')
@@ -3410,6 +3917,16 @@ ${cyan("Usage in OpenCode:")}
3410
3917
  @swarm-worker "Execute this specific subtask"
3411
3918
  @swarm-researcher "Research Next.js caching APIs"
3412
3919
 
3920
+ ${cyan("Claude Code:")}
3921
+ swarm claude path Show bundled Claude plugin path
3922
+ swarm claude install Symlink plugin into ~/.claude/plugins
3923
+ swarm claude uninstall Remove Claude plugin symlink
3924
+ swarm claude init Create project-local .claude config
3925
+ swarm claude session-start Hook: session start context
3926
+ swarm claude user-prompt Hook: prompt submit context
3927
+ swarm claude pre-compact Hook: pre-compaction handler
3928
+ swarm claude session-end Hook: session cleanup
3929
+
3413
3930
  ${cyan("Customization:")}
3414
3931
  Edit the generated files to customize behavior:
3415
3932
  ${dim("~/.config/opencode/command/swarm.md")} - /swarm command prompt
@@ -5663,6 +6180,40 @@ async function evalRun() {
5663
6180
  }
5664
6181
  }
5665
6182
 
6183
+ // ============================================================================
6184
+ // MCP Server (Debug Only)
6185
+ // ============================================================================
6186
+
6187
+ /**
6188
+ * Start the MCP server over stdio (debug only).
6189
+ */
6190
+ async function mcpServe() {
6191
+ p.intro("swarm mcp-serve");
6192
+
6193
+ const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT || getClaudePluginRoot();
6194
+ const candidates = [
6195
+ join(pluginRoot, "bin", "swarm-mcp-server.ts"),
6196
+ join(PACKAGE_ROOT, "bin", "swarm-mcp-server.ts"),
6197
+ ];
6198
+ const serverPath = candidates.find((path) => existsSync(path));
6199
+
6200
+ if (!serverPath) {
6201
+ p.log.error("MCP server entrypoint not found");
6202
+ p.log.message(dim(` Looked for: ${candidates.join(", ")}`));
6203
+ p.log.message(dim(" Claude Code auto-launches MCP via .mcp.json; use for debugging only"));
6204
+ p.outro("Aborted");
6205
+ process.exit(1);
6206
+ }
6207
+
6208
+ p.log.step("Starting MCP server...");
6209
+ p.log.message(dim(` Using: ${serverPath}`));
6210
+
6211
+ const proc = spawn("bun", ["run", serverPath], { stdio: "inherit" });
6212
+ proc.on("close", (exitCode) => {
6213
+ process.exit(exitCode ?? 0);
6214
+ });
6215
+ }
6216
+
5666
6217
  // ============================================================================
5667
6218
  // Serve Command - Start SSE Server
5668
6219
  // ============================================================================
@@ -6208,6 +6759,12 @@ switch (command) {
6208
6759
  case "config":
6209
6760
  config();
6210
6761
  break;
6762
+ case "claude":
6763
+ await claudeCommand();
6764
+ break;
6765
+ case "mcp-serve":
6766
+ await mcpServe();
6767
+ break;
6211
6768
  case "serve":
6212
6769
  await serve();
6213
6770
  break;
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "swarm",
3
+ "description": "Multi-agent task decomposition and coordination for Claude Code",
4
+ "version": "0.1.0",
5
+ "author": {
6
+ "name": "Joel Hooks"
7
+ },
8
+ "repository": "https://github.com/joelhooks/opencode-swarm-plugin",
9
+ "keywords": ["multi-agent", "coordination", "tasks", "parallel"],
10
+ "license": "MIT"
11
+ }
@@ -0,0 +1 @@
1
+ {}