opencode-swarm-plugin 0.11.3 → 0.12.2

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.
@@ -678,6 +678,12 @@
678
678
  {"id":"opencode-swarm-plugin-fmkz8.1","title":"Step 1","description":"","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-08T11:12:43.192563-08:00","updated_at":"2025-12-08T11:12:43.285206-08:00","closed_at":"2025-12-08T11:12:43.285206-08:00","dependencies":[{"issue_id":"opencode-swarm-plugin-fmkz8.1","depends_on_id":"opencode-swarm-plugin-fmkz8","type":"parent-child","created_at":"2025-12-08T11:12:43.192901-08:00","created_by":"daemon"}]}
679
679
  {"id":"opencode-swarm-plugin-fmkz8.2","title":"Step 2","description":"","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-08T11:12:43.229469-08:00","updated_at":"2025-12-08T11:12:43.340511-08:00","closed_at":"2025-12-08T11:12:43.340511-08:00","dependencies":[{"issue_id":"opencode-swarm-plugin-fmkz8.2","depends_on_id":"opencode-swarm-plugin-fmkz8","type":"parent-child","created_at":"2025-12-08T11:12:43.229808-08:00","created_by":"daemon"}]}
680
680
  {"id":"opencode-swarm-plugin-fmte","title":"Thread link test bead","description":"[thread:test-thread-456]","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-08T08:14:30.118389-08:00","updated_at":"2025-12-08T08:14:31.757071-08:00","closed_at":"2025-12-08T08:14:31.757071-08:00"}
681
+ {"id":"opencode-swarm-plugin-fn2a3","title":"CLI-based plugin wrapper for OpenCode","description":"Replace broken npm import plugin with thin wrapper that shells out to `swarm tool` CLI. Fixes bun: protocol crash.","status":"closed","priority":1,"issue_type":"epic","created_at":"2025-12-09T12:44:59.286448-08:00","updated_at":"2025-12-09T12:53:22.927803-08:00","closed_at":"2025-12-09T12:53:22.927803-08:00"}
682
+ {"id":"opencode-swarm-plugin-fn2a3.1","title":"Add CLI tool subcommand with JSON interface","description":"","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-09T12:44:59.380582-08:00","updated_at":"2025-12-09T12:48:28.655484-08:00","closed_at":"2025-12-09T12:48:28.655484-08:00","dependencies":[{"issue_id":"opencode-swarm-plugin-fn2a3.1","depends_on_id":"opencode-swarm-plugin-fn2a3","type":"parent-child","created_at":"2025-12-09T12:44:59.381424-08:00","created_by":"daemon"}]}
683
+ {"id":"opencode-swarm-plugin-fn2a3.2","title":"Add file-based session state for Agent Mail","description":"","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-09T12:44:59.452126-08:00","updated_at":"2025-12-09T12:50:05.569927-08:00","closed_at":"2025-12-09T12:50:05.569927-08:00","dependencies":[{"issue_id":"opencode-swarm-plugin-fn2a3.2","depends_on_id":"opencode-swarm-plugin-fn2a3","type":"parent-child","created_at":"2025-12-09T12:44:59.452571-08:00","created_by":"daemon"}]}
684
+ {"id":"opencode-swarm-plugin-fn2a3.3","title":"Create plugin wrapper template generator","description":"","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-09T12:44:59.511026-08:00","updated_at":"2025-12-09T12:52:13.47915-08:00","closed_at":"2025-12-09T12:52:13.47915-08:00","dependencies":[{"issue_id":"opencode-swarm-plugin-fn2a3.3","depends_on_id":"opencode-swarm-plugin-fn2a3","type":"parent-child","created_at":"2025-12-09T12:44:59.511712-08:00","created_by":"daemon"}]}
685
+ {"id":"opencode-swarm-plugin-fn2a3.4","title":"Update setup command to generate new plugin wrapper","description":"","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-09T12:44:59.559682-08:00","updated_at":"2025-12-09T12:53:13.282356-08:00","closed_at":"2025-12-09T12:53:13.282356-08:00","dependencies":[{"issue_id":"opencode-swarm-plugin-fn2a3.4","depends_on_id":"opencode-swarm-plugin-fn2a3","type":"parent-child","created_at":"2025-12-09T12:44:59.560036-08:00","created_by":"daemon"}]}
686
+ {"id":"opencode-swarm-plugin-fn2a3.5","title":"Export tool registry from source modules","description":"","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-09T12:44:59.609125-08:00","updated_at":"2025-12-09T12:46:18.092813-08:00","closed_at":"2025-12-09T12:46:18.092813-08:00","dependencies":[{"issue_id":"opencode-swarm-plugin-fn2a3.5","depends_on_id":"opencode-swarm-plugin-fn2a3","type":"parent-child","created_at":"2025-12-09T12:44:59.609492-08:00","created_by":"daemon"}]}
681
687
  {"id":"opencode-swarm-plugin-fo9z","title":"Query test bead","description":"","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-08T07:53:43.635138-08:00","updated_at":"2025-12-08T07:53:46.190181-08:00","closed_at":"2025-12-08T07:53:46.190181-08:00"}
682
688
  {"id":"opencode-swarm-plugin-foht","title":"New feature request","description":"","status":"closed","priority":1,"issue_type":"feature","created_at":"2025-12-08T11:11:07.341455-08:00","updated_at":"2025-12-08T11:11:10.086863-08:00","closed_at":"2025-12-08T11:11:10.086863-08:00"}
683
689
  {"id":"opencode-swarm-plugin-fomf","title":"Limit test bead 1","description":"","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-08T11:12:25.714274-08:00","updated_at":"2025-12-08T11:12:28.097683-08:00","closed_at":"2025-12-08T11:12:28.097683-08:00"}
package/bin/swarm.ts CHANGED
@@ -457,9 +457,32 @@ async function checkAllDependencies(): Promise<CheckResult[]> {
457
457
  // File Templates
458
458
  // ============================================================================
459
459
 
460
- const PLUGIN_WRAPPER = `import { SwarmPlugin } from "opencode-swarm-plugin"
460
+ /**
461
+ * Get the plugin wrapper template
462
+ *
463
+ * Reads from examples/plugin-wrapper-template.ts which contains a self-contained
464
+ * plugin that shells out to the `swarm` CLI for all tool execution.
465
+ */
466
+ function getPluginWrapper(): string {
467
+ const templatePath = join(
468
+ __dirname,
469
+ "..",
470
+ "examples",
471
+ "plugin-wrapper-template.ts",
472
+ );
473
+ try {
474
+ return readFileSync(templatePath, "utf-8");
475
+ } catch (error) {
476
+ // Fallback to minimal wrapper if template not found (shouldn't happen in normal install)
477
+ console.warn(
478
+ `[swarm] Could not read plugin template from ${templatePath}, using minimal wrapper`,
479
+ );
480
+ return `// Minimal fallback - install opencode-swarm-plugin globally for full functionality
481
+ import { SwarmPlugin } from "opencode-swarm-plugin"
461
482
  export default SwarmPlugin
462
483
  `;
484
+ }
485
+ }
463
486
 
464
487
  const SWARM_COMMAND = `---
465
488
  description: Decompose task into parallel subtasks and coordinate agents
@@ -961,7 +984,7 @@ async function setup() {
961
984
  }
962
985
  }
963
986
 
964
- writeFileSync(pluginPath, PLUGIN_WRAPPER);
987
+ writeFileSync(pluginPath, getPluginWrapper());
965
988
  p.log.success("Plugin: " + pluginPath);
966
989
 
967
990
  writeFileSync(commandPath, SWARM_COMMAND);
@@ -1180,8 +1203,14 @@ ${cyan("Commands:")}
1180
1203
  swarm config Show paths to generated config files
1181
1204
  swarm update Update to latest version
1182
1205
  swarm version Show version and banner
1206
+ swarm tool Execute a tool (for plugin wrapper)
1183
1207
  swarm help Show this help
1184
1208
 
1209
+ ${cyan("Tool Execution:")}
1210
+ swarm tool --list List all available tools
1211
+ swarm tool <name> Execute tool with no args
1212
+ swarm tool <name> --json '<args>' Execute tool with JSON args
1213
+
1185
1214
  ${cyan("Usage in OpenCode:")}
1186
1215
  /swarm "Add user authentication with OAuth"
1187
1216
  @swarm-planner "Decompose this into parallel tasks"
@@ -1202,6 +1231,150 @@ ${dim("Docs: https://github.com/joelhooks/opencode-swarm-plugin")}
1202
1231
  if (updateInfo) showUpdateNotification(updateInfo);
1203
1232
  }
1204
1233
 
1234
+ // ============================================================================
1235
+ // Tool Execution (for plugin wrapper)
1236
+ // ============================================================================
1237
+
1238
+ /**
1239
+ * Execute a tool by name with JSON args
1240
+ *
1241
+ * This is the bridge between the plugin wrapper and the actual tool implementations.
1242
+ * The plugin wrapper shells out to `swarm tool <name> --json '<args>'` and this
1243
+ * function executes the tool and returns JSON.
1244
+ *
1245
+ * Exit codes:
1246
+ * - 0: Success
1247
+ * - 1: Tool execution error (error details in JSON output)
1248
+ * - 2: Unknown tool name
1249
+ * - 3: Invalid JSON args
1250
+ */
1251
+ async function executeTool(toolName: string, argsJson?: string) {
1252
+ // Lazy import to avoid loading all tools on every CLI invocation
1253
+ const { allTools } = await import("../src/index");
1254
+
1255
+ // Validate tool name
1256
+ if (!(toolName in allTools)) {
1257
+ const availableTools = Object.keys(allTools).sort();
1258
+ console.log(
1259
+ JSON.stringify({
1260
+ success: false,
1261
+ error: {
1262
+ code: "UNKNOWN_TOOL",
1263
+ message: `Unknown tool: ${toolName}`,
1264
+ available_tools: availableTools,
1265
+ },
1266
+ }),
1267
+ );
1268
+ process.exit(2);
1269
+ }
1270
+
1271
+ // Parse args
1272
+ let args: Record<string, unknown> = {};
1273
+ if (argsJson) {
1274
+ try {
1275
+ args = JSON.parse(argsJson);
1276
+ } catch (e) {
1277
+ console.log(
1278
+ JSON.stringify({
1279
+ success: false,
1280
+ error: {
1281
+ code: "INVALID_JSON",
1282
+ message: `Invalid JSON args: ${e instanceof Error ? e.message : String(e)}`,
1283
+ raw_input: argsJson.slice(0, 200),
1284
+ },
1285
+ }),
1286
+ );
1287
+ process.exit(3);
1288
+ }
1289
+ }
1290
+
1291
+ // Create mock context for tools that need sessionID
1292
+ // This mimics what OpenCode provides to plugins
1293
+ const mockContext = {
1294
+ sessionID: process.env.OPENCODE_SESSION_ID || `cli-${Date.now()}`,
1295
+ messageID: process.env.OPENCODE_MESSAGE_ID || `msg-${Date.now()}`,
1296
+ agent: process.env.OPENCODE_AGENT || "cli",
1297
+ abort: new AbortController().signal,
1298
+ };
1299
+
1300
+ // Get the tool
1301
+ const toolDef = allTools[toolName as keyof typeof allTools];
1302
+
1303
+ // Execute tool
1304
+ // Note: We cast args to any because the CLI accepts arbitrary JSON
1305
+ // The tool's internal Zod validation will catch type errors
1306
+ try {
1307
+ const result = await toolDef.execute(args as any, mockContext);
1308
+
1309
+ // If result is already valid JSON, try to parse and re-wrap it
1310
+ // Otherwise wrap the string result
1311
+ try {
1312
+ const parsed = JSON.parse(result);
1313
+ // If it's already a success/error response, pass through
1314
+ if (typeof parsed === "object" && "success" in parsed) {
1315
+ console.log(JSON.stringify(parsed));
1316
+ } else {
1317
+ console.log(JSON.stringify({ success: true, data: parsed }));
1318
+ }
1319
+ } catch {
1320
+ // Result is a plain string, wrap it
1321
+ console.log(JSON.stringify({ success: true, data: result }));
1322
+ }
1323
+ process.exit(0);
1324
+ } catch (error) {
1325
+ console.log(
1326
+ JSON.stringify({
1327
+ success: false,
1328
+ error: {
1329
+ code: error instanceof Error ? error.name : "TOOL_ERROR",
1330
+ message: error instanceof Error ? error.message : String(error),
1331
+ details:
1332
+ error instanceof Error && "zodError" in error
1333
+ ? (error as { zodError?: unknown }).zodError
1334
+ : undefined,
1335
+ },
1336
+ }),
1337
+ );
1338
+ process.exit(1);
1339
+ }
1340
+ }
1341
+
1342
+ /**
1343
+ * List all available tools
1344
+ */
1345
+ async function listTools() {
1346
+ const { allTools } = await import("../src/index");
1347
+ const tools = Object.keys(allTools).sort();
1348
+
1349
+ console.log(yellow(BANNER));
1350
+ console.log(dim(" " + TAGLINE + " v" + VERSION));
1351
+ console.log();
1352
+ console.log(cyan("Available tools:") + ` (${tools.length} total)`);
1353
+ console.log();
1354
+
1355
+ // Group by prefix
1356
+ const groups: Record<string, string[]> = {};
1357
+ for (const tool of tools) {
1358
+ const prefix = tool.split("_")[0];
1359
+ if (!groups[prefix]) groups[prefix] = [];
1360
+ groups[prefix].push(tool);
1361
+ }
1362
+
1363
+ for (const [prefix, toolList] of Object.entries(groups)) {
1364
+ console.log(green(` ${prefix}:`));
1365
+ for (const t of toolList) {
1366
+ console.log(` ${t}`);
1367
+ }
1368
+ console.log();
1369
+ }
1370
+
1371
+ console.log(dim("Usage: swarm tool <name> [--json '<args>']"));
1372
+ console.log(dim("Example: swarm tool beads_ready"));
1373
+ console.log(
1374
+ dim('Example: swarm tool beads_create --json \'{"title": "Fix bug"}\''),
1375
+ );
1376
+ }
1377
+
1205
1378
  // ============================================================================
1206
1379
  // Main
1207
1380
  // ============================================================================
@@ -1224,6 +1397,19 @@ switch (command) {
1224
1397
  case "update":
1225
1398
  await update();
1226
1399
  break;
1400
+ case "tool": {
1401
+ const toolName = process.argv[3];
1402
+ if (!toolName || toolName === "--list" || toolName === "-l") {
1403
+ await listTools();
1404
+ } else {
1405
+ // Look for --json flag
1406
+ const jsonFlagIndex = process.argv.indexOf("--json");
1407
+ const argsJson =
1408
+ jsonFlagIndex !== -1 ? process.argv[jsonFlagIndex + 1] : undefined;
1409
+ await executeTool(toolName, argsJson);
1410
+ }
1411
+ break;
1412
+ }
1227
1413
  case "version":
1228
1414
  case "--version":
1229
1415
  case "-v":
package/dist/index.js CHANGED
@@ -22590,6 +22590,15 @@ async function getRateLimiter() {
22590
22590
  }
22591
22591
 
22592
22592
  // src/agent-mail.ts
22593
+ import {
22594
+ existsSync as existsSync2,
22595
+ mkdirSync as mkdirSync2,
22596
+ readFileSync,
22597
+ writeFileSync,
22598
+ unlinkSync
22599
+ } from "fs";
22600
+ import { join as join2 } from "path";
22601
+ import { tmpdir } from "os";
22593
22602
  var AGENT_MAIL_URL = "http://127.0.0.1:8765";
22594
22603
  var DEFAULT_TTL_SECONDS = 3600;
22595
22604
  var MAX_INBOX_LIMIT = 5;
@@ -22605,6 +22614,34 @@ var RECOVERY_CONFIG = {
22605
22614
  restartCooldownMs: 30000,
22606
22615
  enabled: process.env.OPENCODE_AGENT_MAIL_AUTO_RESTART !== "false"
22607
22616
  };
22617
+ var SESSION_STATE_DIR = process.env.SWARM_STATE_DIR || join2(tmpdir(), "swarm-sessions");
22618
+ function getSessionStatePath(sessionID) {
22619
+ const safeID = sessionID.replace(/[^a-zA-Z0-9_-]/g, "_");
22620
+ return join2(SESSION_STATE_DIR, `${safeID}.json`);
22621
+ }
22622
+ function loadSessionState(sessionID) {
22623
+ const path = getSessionStatePath(sessionID);
22624
+ try {
22625
+ if (existsSync2(path)) {
22626
+ const data = readFileSync(path, "utf-8");
22627
+ return JSON.parse(data);
22628
+ }
22629
+ } catch (error45) {
22630
+ console.warn(`[agent-mail] Could not load session state: ${error45}`);
22631
+ }
22632
+ return null;
22633
+ }
22634
+ function saveSessionState(sessionID, state) {
22635
+ try {
22636
+ if (!existsSync2(SESSION_STATE_DIR)) {
22637
+ mkdirSync2(SESSION_STATE_DIR, { recursive: true });
22638
+ }
22639
+ const path = getSessionStatePath(sessionID);
22640
+ writeFileSync(path, JSON.stringify(state, null, 2));
22641
+ } catch (error45) {
22642
+ console.warn(`[agent-mail] Could not save session state: ${error45}`);
22643
+ }
22644
+ }
22608
22645
  var sessionStates = new Map;
22609
22646
 
22610
22647
  class AgentMailError extends Error {
@@ -22908,7 +22945,13 @@ async function mcpCall(toolName, args) {
22908
22945
  throw lastError || new Error("Unknown error in mcpCall");
22909
22946
  }
22910
22947
  function requireState(sessionID) {
22911
- const state = sessionStates.get(sessionID);
22948
+ let state = sessionStates.get(sessionID);
22949
+ if (!state) {
22950
+ state = loadSessionState(sessionID) ?? undefined;
22951
+ if (state) {
22952
+ sessionStates.set(sessionID, state);
22953
+ }
22954
+ }
22912
22955
  if (!state) {
22913
22956
  throw new AgentMailNotInitializedError;
22914
22957
  }
@@ -22916,6 +22959,7 @@ function requireState(sessionID) {
22916
22959
  }
22917
22960
  function setState(sessionID, state) {
22918
22961
  sessionStates.set(sessionID, state);
22962
+ saveSessionState(sessionID, state);
22919
22963
  }
22920
22964
  var agentmail_init = tool({
22921
22965
  description: "Initialize Agent Mail session (ensure project + register agent)",
@@ -25554,6 +25598,12 @@ var SwarmPlugin = async (input) => {
25554
25598
  };
25555
25599
  };
25556
25600
  var src_default = SwarmPlugin;
25601
+ var allTools = {
25602
+ ...beadsTools,
25603
+ ...agentMailTools,
25604
+ ...structuredTools,
25605
+ ...swarmTools
25606
+ };
25557
25607
  export {
25558
25608
  withToolFallback,
25559
25609
  warnMissingTool,
@@ -25592,6 +25642,7 @@ export {
25592
25642
  beads_create,
25593
25643
  beads_close,
25594
25644
  beadsTools,
25645
+ allTools,
25595
25646
  agentMailTools,
25596
25647
  WeightedEvaluationSchema,
25597
25648
  WeightedCriterionEvaluationSchema,
package/dist/plugin.js CHANGED
@@ -22564,6 +22564,15 @@ async function getRateLimiter() {
22564
22564
  }
22565
22565
 
22566
22566
  // src/agent-mail.ts
22567
+ import {
22568
+ existsSync as existsSync2,
22569
+ mkdirSync as mkdirSync2,
22570
+ readFileSync,
22571
+ writeFileSync,
22572
+ unlinkSync
22573
+ } from "fs";
22574
+ import { join as join2 } from "path";
22575
+ import { tmpdir } from "os";
22567
22576
  var AGENT_MAIL_URL = "http://127.0.0.1:8765";
22568
22577
  var DEFAULT_TTL_SECONDS = 3600;
22569
22578
  var MAX_INBOX_LIMIT = 5;
@@ -22579,6 +22588,34 @@ var RECOVERY_CONFIG = {
22579
22588
  restartCooldownMs: 30000,
22580
22589
  enabled: process.env.OPENCODE_AGENT_MAIL_AUTO_RESTART !== "false"
22581
22590
  };
22591
+ var SESSION_STATE_DIR = process.env.SWARM_STATE_DIR || join2(tmpdir(), "swarm-sessions");
22592
+ function getSessionStatePath(sessionID) {
22593
+ const safeID = sessionID.replace(/[^a-zA-Z0-9_-]/g, "_");
22594
+ return join2(SESSION_STATE_DIR, `${safeID}.json`);
22595
+ }
22596
+ function loadSessionState(sessionID) {
22597
+ const path = getSessionStatePath(sessionID);
22598
+ try {
22599
+ if (existsSync2(path)) {
22600
+ const data = readFileSync(path, "utf-8");
22601
+ return JSON.parse(data);
22602
+ }
22603
+ } catch (error45) {
22604
+ console.warn(`[agent-mail] Could not load session state: ${error45}`);
22605
+ }
22606
+ return null;
22607
+ }
22608
+ function saveSessionState(sessionID, state) {
22609
+ try {
22610
+ if (!existsSync2(SESSION_STATE_DIR)) {
22611
+ mkdirSync2(SESSION_STATE_DIR, { recursive: true });
22612
+ }
22613
+ const path = getSessionStatePath(sessionID);
22614
+ writeFileSync(path, JSON.stringify(state, null, 2));
22615
+ } catch (error45) {
22616
+ console.warn(`[agent-mail] Could not save session state: ${error45}`);
22617
+ }
22618
+ }
22582
22619
  var sessionStates = new Map;
22583
22620
 
22584
22621
  class AgentMailError extends Error {
@@ -22882,7 +22919,13 @@ async function mcpCall(toolName, args) {
22882
22919
  throw lastError || new Error("Unknown error in mcpCall");
22883
22920
  }
22884
22921
  function requireState(sessionID) {
22885
- const state = sessionStates.get(sessionID);
22922
+ let state = sessionStates.get(sessionID);
22923
+ if (!state) {
22924
+ state = loadSessionState(sessionID) ?? undefined;
22925
+ if (state) {
22926
+ sessionStates.set(sessionID, state);
22927
+ }
22928
+ }
22886
22929
  if (!state) {
22887
22930
  throw new AgentMailNotInitializedError;
22888
22931
  }
@@ -22890,6 +22933,7 @@ function requireState(sessionID) {
22890
22933
  }
22891
22934
  function setState(sessionID, state) {
22892
22935
  sessionStates.set(sessionID, state);
22936
+ saveSessionState(sessionID, state);
22893
22937
  }
22894
22938
  var agentmail_init = tool({
22895
22939
  description: "Initialize Agent Mail session (ensure project + register agent)",
@@ -25203,6 +25247,12 @@ var SwarmPlugin = async (input) => {
25203
25247
  }
25204
25248
  };
25205
25249
  };
25250
+ var allTools = {
25251
+ ...beadsTools,
25252
+ ...agentMailTools,
25253
+ ...structuredTools,
25254
+ ...swarmTools
25255
+ };
25206
25256
  export {
25207
25257
  SwarmPlugin
25208
25258
  };
@@ -0,0 +1,723 @@
1
+ /**
2
+ * OpenCode Swarm Plugin Wrapper
3
+ *
4
+ * This is a thin wrapper that shells out to the `swarm` CLI for all tool execution.
5
+ * Generated by: swarm setup
6
+ *
7
+ * The plugin only depends on @opencode-ai/plugin (provided by OpenCode).
8
+ * All tool logic lives in the npm package - this just bridges to it.
9
+ *
10
+ * Environment variables:
11
+ * - OPENCODE_SESSION_ID: Passed to CLI for session state persistence
12
+ * - OPENCODE_MESSAGE_ID: Passed to CLI for context
13
+ * - OPENCODE_AGENT: Passed to CLI for context
14
+ */
15
+ import type { Plugin, PluginInput, Hooks } from "@opencode-ai/plugin";
16
+ import { tool } from "@opencode-ai/plugin";
17
+ import { spawn } from "child_process";
18
+
19
+ const SWARM_CLI = "swarm";
20
+
21
+ // =============================================================================
22
+ // CLI Execution Helper
23
+ // =============================================================================
24
+
25
+ /**
26
+ * Execute a swarm tool via CLI
27
+ *
28
+ * Spawns `swarm tool <name> --json '<args>'` and returns the result.
29
+ * Passes session context via environment variables.
30
+ */
31
+ async function execTool(
32
+ name: string,
33
+ args: Record<string, unknown>,
34
+ ctx: { sessionID: string; messageID: string; agent: string },
35
+ ): Promise<string> {
36
+ return new Promise((resolve, reject) => {
37
+ const hasArgs = Object.keys(args).length > 0;
38
+ const cliArgs = hasArgs
39
+ ? ["tool", name, "--json", JSON.stringify(args)]
40
+ : ["tool", name];
41
+
42
+ const proc = spawn(SWARM_CLI, cliArgs, {
43
+ stdio: ["ignore", "pipe", "pipe"],
44
+ env: {
45
+ ...process.env,
46
+ OPENCODE_SESSION_ID: ctx.sessionID,
47
+ OPENCODE_MESSAGE_ID: ctx.messageID,
48
+ OPENCODE_AGENT: ctx.agent,
49
+ },
50
+ });
51
+
52
+ let stdout = "";
53
+ let stderr = "";
54
+
55
+ proc.stdout.on("data", (data) => {
56
+ stdout += data;
57
+ });
58
+ proc.stderr.on("data", (data) => {
59
+ stderr += data;
60
+ });
61
+
62
+ proc.on("close", (code) => {
63
+ if (code === 0) {
64
+ // Success - return the JSON output
65
+ try {
66
+ const result = JSON.parse(stdout);
67
+ if (result.success && result.data !== undefined) {
68
+ // Unwrap the data for cleaner tool output
69
+ resolve(
70
+ typeof result.data === "string"
71
+ ? result.data
72
+ : JSON.stringify(result.data, null, 2),
73
+ );
74
+ } else if (!result.success && result.error) {
75
+ // Tool returned an error in JSON format
76
+ reject(new Error(result.error.message || "Tool execution failed"));
77
+ } else {
78
+ resolve(stdout);
79
+ }
80
+ } catch {
81
+ resolve(stdout);
82
+ }
83
+ } else if (code === 2) {
84
+ reject(new Error(`Unknown tool: ${name}`));
85
+ } else if (code === 3) {
86
+ reject(new Error(`Invalid JSON args: ${stderr}`));
87
+ } else {
88
+ // Tool returned error
89
+ try {
90
+ const result = JSON.parse(stdout);
91
+ if (!result.success && result.error) {
92
+ reject(
93
+ new Error(
94
+ result.error.message || `Tool failed with code ${code}`,
95
+ ),
96
+ );
97
+ } else {
98
+ reject(
99
+ new Error(stderr || stdout || `Tool failed with code ${code}`),
100
+ );
101
+ }
102
+ } catch {
103
+ reject(
104
+ new Error(stderr || stdout || `Tool failed with code ${code}`),
105
+ );
106
+ }
107
+ }
108
+ });
109
+
110
+ proc.on("error", (err) => {
111
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") {
112
+ reject(
113
+ new Error(
114
+ `swarm CLI not found. Install with: npm install -g opencode-swarm-plugin`,
115
+ ),
116
+ );
117
+ } else {
118
+ reject(err);
119
+ }
120
+ });
121
+ });
122
+ }
123
+
124
+ // =============================================================================
125
+ // Beads Tools
126
+ // =============================================================================
127
+
128
+ const beads_create = tool({
129
+ description: "Create a new bead with type-safe validation",
130
+ args: {
131
+ title: tool.schema.string().describe("Bead title"),
132
+ type: tool.schema
133
+ .enum(["bug", "feature", "task", "epic", "chore"])
134
+ .optional()
135
+ .describe("Issue type (default: task)"),
136
+ priority: tool.schema
137
+ .number()
138
+ .min(0)
139
+ .max(3)
140
+ .optional()
141
+ .describe("Priority 0-3 (default: 2)"),
142
+ description: tool.schema.string().optional().describe("Bead description"),
143
+ parent_id: tool.schema
144
+ .string()
145
+ .optional()
146
+ .describe("Parent bead ID for epic children"),
147
+ },
148
+ execute: (args, ctx) => execTool("beads_create", args, ctx),
149
+ });
150
+
151
+ const beads_create_epic = tool({
152
+ description: "Create epic with subtasks in one atomic operation",
153
+ args: {
154
+ epic_title: tool.schema.string().describe("Epic title"),
155
+ epic_description: tool.schema
156
+ .string()
157
+ .optional()
158
+ .describe("Epic description"),
159
+ subtasks: tool.schema
160
+ .array(
161
+ tool.schema.object({
162
+ title: tool.schema.string(),
163
+ priority: tool.schema.number().min(0).max(3).optional(),
164
+ files: tool.schema.array(tool.schema.string()).optional(),
165
+ }),
166
+ )
167
+ .describe("Subtasks to create under the epic"),
168
+ },
169
+ execute: (args, ctx) => execTool("beads_create_epic", args, ctx),
170
+ });
171
+
172
+ const beads_query = tool({
173
+ description: "Query beads with filters (replaces bd list, bd ready, bd wip)",
174
+ args: {
175
+ status: tool.schema
176
+ .enum(["open", "in_progress", "blocked", "closed"])
177
+ .optional()
178
+ .describe("Filter by status"),
179
+ type: tool.schema
180
+ .enum(["bug", "feature", "task", "epic", "chore"])
181
+ .optional()
182
+ .describe("Filter by type"),
183
+ ready: tool.schema
184
+ .boolean()
185
+ .optional()
186
+ .describe("Only show unblocked beads"),
187
+ limit: tool.schema
188
+ .number()
189
+ .optional()
190
+ .describe("Max results (default: 20)"),
191
+ },
192
+ execute: (args, ctx) => execTool("beads_query", args, ctx),
193
+ });
194
+
195
+ const beads_update = tool({
196
+ description: "Update bead status/description",
197
+ args: {
198
+ id: tool.schema.string().describe("Bead ID"),
199
+ status: tool.schema
200
+ .enum(["open", "in_progress", "blocked", "closed"])
201
+ .optional()
202
+ .describe("New status"),
203
+ description: tool.schema.string().optional().describe("New description"),
204
+ priority: tool.schema
205
+ .number()
206
+ .min(0)
207
+ .max(3)
208
+ .optional()
209
+ .describe("New priority"),
210
+ },
211
+ execute: (args, ctx) => execTool("beads_update", args, ctx),
212
+ });
213
+
214
+ const beads_close = tool({
215
+ description: "Close a bead with reason",
216
+ args: {
217
+ id: tool.schema.string().describe("Bead ID"),
218
+ reason: tool.schema.string().describe("Completion reason"),
219
+ },
220
+ execute: (args, ctx) => execTool("beads_close", args, ctx),
221
+ });
222
+
223
+ const beads_start = tool({
224
+ description: "Mark a bead as in-progress",
225
+ args: {
226
+ id: tool.schema.string().describe("Bead ID"),
227
+ },
228
+ execute: (args, ctx) => execTool("beads_start", args, ctx),
229
+ });
230
+
231
+ const beads_ready = tool({
232
+ description: "Get the next ready bead (unblocked, highest priority)",
233
+ args: {},
234
+ execute: (args, ctx) => execTool("beads_ready", args, ctx),
235
+ });
236
+
237
+ const beads_sync = tool({
238
+ description: "Sync beads to git and push (MANDATORY at session end)",
239
+ args: {
240
+ auto_pull: tool.schema.boolean().optional().describe("Pull before sync"),
241
+ },
242
+ execute: (args, ctx) => execTool("beads_sync", args, ctx),
243
+ });
244
+
245
+ const beads_link_thread = tool({
246
+ description: "Add metadata linking bead to Agent Mail thread",
247
+ args: {
248
+ bead_id: tool.schema.string().describe("Bead ID"),
249
+ thread_id: tool.schema.string().describe("Agent Mail thread ID"),
250
+ },
251
+ execute: (args, ctx) => execTool("beads_link_thread", args, ctx),
252
+ });
253
+
254
+ // =============================================================================
255
+ // Agent Mail Tools
256
+ // =============================================================================
257
+
258
+ const agentmail_init = tool({
259
+ description: "Initialize Agent Mail session",
260
+ args: {
261
+ project_path: tool.schema.string().describe("Absolute path to the project"),
262
+ agent_name: tool.schema.string().optional().describe("Custom agent name"),
263
+ task_description: tool.schema
264
+ .string()
265
+ .optional()
266
+ .describe("Task description"),
267
+ },
268
+ execute: (args, ctx) => execTool("agentmail_init", args, ctx),
269
+ });
270
+
271
+ const agentmail_send = tool({
272
+ description: "Send message to other agents",
273
+ args: {
274
+ to: tool.schema
275
+ .array(tool.schema.string())
276
+ .describe("Recipient agent names"),
277
+ subject: tool.schema.string().describe("Message subject"),
278
+ body: tool.schema.string().describe("Message body"),
279
+ thread_id: tool.schema
280
+ .string()
281
+ .optional()
282
+ .describe("Thread ID for grouping"),
283
+ importance: tool.schema
284
+ .enum(["low", "normal", "high", "urgent"])
285
+ .optional()
286
+ .describe("Message importance"),
287
+ ack_required: tool.schema
288
+ .boolean()
289
+ .optional()
290
+ .describe("Require acknowledgment"),
291
+ },
292
+ execute: (args, ctx) => execTool("agentmail_send", args, ctx),
293
+ });
294
+
295
+ const agentmail_inbox = tool({
296
+ description: "Fetch inbox (CONTEXT-SAFE: bodies excluded, limit 5)",
297
+ args: {
298
+ limit: tool.schema
299
+ .number()
300
+ .max(5)
301
+ .optional()
302
+ .describe("Max messages (max 5)"),
303
+ urgent_only: tool.schema
304
+ .boolean()
305
+ .optional()
306
+ .describe("Only urgent messages"),
307
+ since_ts: tool.schema
308
+ .string()
309
+ .optional()
310
+ .describe("Messages since timestamp"),
311
+ },
312
+ execute: (args, ctx) => execTool("agentmail_inbox", args, ctx),
313
+ });
314
+
315
+ const agentmail_read_message = tool({
316
+ description: "Fetch ONE message body by ID",
317
+ args: {
318
+ message_id: tool.schema.number().describe("Message ID"),
319
+ },
320
+ execute: (args, ctx) => execTool("agentmail_read_message", args, ctx),
321
+ });
322
+
323
+ const agentmail_summarize_thread = tool({
324
+ description: "Summarize thread (PREFERRED over fetching all messages)",
325
+ args: {
326
+ thread_id: tool.schema.string().describe("Thread ID"),
327
+ include_examples: tool.schema
328
+ .boolean()
329
+ .optional()
330
+ .describe("Include example messages"),
331
+ },
332
+ execute: (args, ctx) => execTool("agentmail_summarize_thread", args, ctx),
333
+ });
334
+
335
+ const agentmail_reserve = tool({
336
+ description: "Reserve file paths for exclusive editing",
337
+ args: {
338
+ paths: tool.schema
339
+ .array(tool.schema.string())
340
+ .describe("File paths/patterns"),
341
+ ttl_seconds: tool.schema.number().optional().describe("Reservation TTL"),
342
+ exclusive: tool.schema.boolean().optional().describe("Exclusive lock"),
343
+ reason: tool.schema.string().optional().describe("Reservation reason"),
344
+ },
345
+ execute: (args, ctx) => execTool("agentmail_reserve", args, ctx),
346
+ });
347
+
348
+ const agentmail_release = tool({
349
+ description: "Release file reservations",
350
+ args: {
351
+ paths: tool.schema
352
+ .array(tool.schema.string())
353
+ .optional()
354
+ .describe("Paths to release"),
355
+ reservation_ids: tool.schema
356
+ .array(tool.schema.number())
357
+ .optional()
358
+ .describe("Reservation IDs"),
359
+ },
360
+ execute: (args, ctx) => execTool("agentmail_release", args, ctx),
361
+ });
362
+
363
+ const agentmail_ack = tool({
364
+ description: "Acknowledge a message",
365
+ args: {
366
+ message_id: tool.schema.number().describe("Message ID"),
367
+ },
368
+ execute: (args, ctx) => execTool("agentmail_ack", args, ctx),
369
+ });
370
+
371
+ const agentmail_search = tool({
372
+ description: "Search messages by keyword",
373
+ args: {
374
+ query: tool.schema.string().describe("Search query"),
375
+ limit: tool.schema.number().optional().describe("Max results"),
376
+ },
377
+ execute: (args, ctx) => execTool("agentmail_search", args, ctx),
378
+ });
379
+
380
+ const agentmail_health = tool({
381
+ description: "Check if Agent Mail server is running",
382
+ args: {},
383
+ execute: (args, ctx) => execTool("agentmail_health", args, ctx),
384
+ });
385
+
386
+ // =============================================================================
387
+ // Structured Tools
388
+ // =============================================================================
389
+
390
+ const structured_extract_json = tool({
391
+ description: "Extract JSON from markdown/text response",
392
+ args: {
393
+ text: tool.schema.string().describe("Text containing JSON"),
394
+ },
395
+ execute: (args, ctx) => execTool("structured_extract_json", args, ctx),
396
+ });
397
+
398
+ const structured_validate = tool({
399
+ description: "Validate agent response against a schema",
400
+ args: {
401
+ response: tool.schema.string().describe("Agent response to validate"),
402
+ schema_name: tool.schema
403
+ .enum(["evaluation", "task_decomposition", "bead_tree"])
404
+ .describe("Schema to validate against"),
405
+ max_retries: tool.schema
406
+ .number()
407
+ .min(1)
408
+ .max(5)
409
+ .optional()
410
+ .describe("Max retries"),
411
+ },
412
+ execute: (args, ctx) => execTool("structured_validate", args, ctx),
413
+ });
414
+
415
+ const structured_parse_evaluation = tool({
416
+ description: "Parse and validate evaluation response",
417
+ args: {
418
+ response: tool.schema.string().describe("Agent response"),
419
+ },
420
+ execute: (args, ctx) => execTool("structured_parse_evaluation", args, ctx),
421
+ });
422
+
423
+ const structured_parse_decomposition = tool({
424
+ description: "Parse and validate task decomposition response",
425
+ args: {
426
+ response: tool.schema.string().describe("Agent response"),
427
+ },
428
+ execute: (args, ctx) => execTool("structured_parse_decomposition", args, ctx),
429
+ });
430
+
431
+ const structured_parse_bead_tree = tool({
432
+ description: "Parse and validate bead tree response",
433
+ args: {
434
+ response: tool.schema.string().describe("Agent response"),
435
+ },
436
+ execute: (args, ctx) => execTool("structured_parse_bead_tree", args, ctx),
437
+ });
438
+
439
+ // =============================================================================
440
+ // Swarm Tools
441
+ // =============================================================================
442
+
443
+ const swarm_init = tool({
444
+ description: "Initialize swarm session and check tool availability",
445
+ args: {
446
+ project_path: tool.schema.string().optional().describe("Project path"),
447
+ },
448
+ execute: (args, ctx) => execTool("swarm_init", args, ctx),
449
+ });
450
+
451
+ const swarm_select_strategy = tool({
452
+ description: "Analyze task and recommend decomposition strategy",
453
+ args: {
454
+ task: tool.schema.string().min(1).describe("Task to analyze"),
455
+ codebase_context: tool.schema
456
+ .string()
457
+ .optional()
458
+ .describe("Codebase context"),
459
+ },
460
+ execute: (args, ctx) => execTool("swarm_select_strategy", args, ctx),
461
+ });
462
+
463
+ const swarm_plan_prompt = tool({
464
+ description: "Generate strategy-specific decomposition prompt",
465
+ args: {
466
+ task: tool.schema.string().min(1).describe("Task to decompose"),
467
+ strategy: tool.schema
468
+ .enum(["file-based", "feature-based", "risk-based", "auto"])
469
+ .optional()
470
+ .describe("Decomposition strategy"),
471
+ max_subtasks: tool.schema
472
+ .number()
473
+ .int()
474
+ .min(2)
475
+ .max(10)
476
+ .optional()
477
+ .describe("Max subtasks"),
478
+ context: tool.schema.string().optional().describe("Additional context"),
479
+ query_cass: tool.schema
480
+ .boolean()
481
+ .optional()
482
+ .describe("Query CASS for similar tasks"),
483
+ cass_limit: tool.schema
484
+ .number()
485
+ .int()
486
+ .min(1)
487
+ .max(10)
488
+ .optional()
489
+ .describe("CASS limit"),
490
+ },
491
+ execute: (args, ctx) => execTool("swarm_plan_prompt", args, ctx),
492
+ });
493
+
494
+ const swarm_decompose = tool({
495
+ description: "Generate decomposition prompt for breaking task into subtasks",
496
+ args: {
497
+ task: tool.schema.string().min(1).describe("Task to decompose"),
498
+ max_subtasks: tool.schema
499
+ .number()
500
+ .int()
501
+ .min(2)
502
+ .max(10)
503
+ .optional()
504
+ .describe("Max subtasks"),
505
+ context: tool.schema.string().optional().describe("Additional context"),
506
+ query_cass: tool.schema.boolean().optional().describe("Query CASS"),
507
+ cass_limit: tool.schema
508
+ .number()
509
+ .int()
510
+ .min(1)
511
+ .max(10)
512
+ .optional()
513
+ .describe("CASS limit"),
514
+ },
515
+ execute: (args, ctx) => execTool("swarm_decompose", args, ctx),
516
+ });
517
+
518
+ const swarm_validate_decomposition = tool({
519
+ description: "Validate a decomposition response against BeadTreeSchema",
520
+ args: {
521
+ response: tool.schema.string().describe("Decomposition response"),
522
+ },
523
+ execute: (args, ctx) => execTool("swarm_validate_decomposition", args, ctx),
524
+ });
525
+
526
+ const swarm_status = tool({
527
+ description: "Get status of a swarm by epic ID",
528
+ args: {
529
+ epic_id: tool.schema.string().describe("Epic bead ID"),
530
+ project_key: tool.schema.string().describe("Project key"),
531
+ },
532
+ execute: (args, ctx) => execTool("swarm_status", args, ctx),
533
+ });
534
+
535
+ const swarm_progress = tool({
536
+ description: "Report progress on a subtask to coordinator",
537
+ args: {
538
+ project_key: tool.schema.string().describe("Project key"),
539
+ agent_name: tool.schema.string().describe("Agent name"),
540
+ bead_id: tool.schema.string().describe("Bead ID"),
541
+ status: tool.schema
542
+ .enum(["in_progress", "blocked", "completed", "failed"])
543
+ .describe("Status"),
544
+ message: tool.schema.string().optional().describe("Progress message"),
545
+ progress_percent: tool.schema
546
+ .number()
547
+ .min(0)
548
+ .max(100)
549
+ .optional()
550
+ .describe("Progress %"),
551
+ files_touched: tool.schema
552
+ .array(tool.schema.string())
553
+ .optional()
554
+ .describe("Files modified"),
555
+ },
556
+ execute: (args, ctx) => execTool("swarm_progress", args, ctx),
557
+ });
558
+
559
+ const swarm_complete = tool({
560
+ description:
561
+ "Mark subtask complete, release reservations, notify coordinator",
562
+ args: {
563
+ project_key: tool.schema.string().describe("Project key"),
564
+ agent_name: tool.schema.string().describe("Agent name"),
565
+ bead_id: tool.schema.string().describe("Bead ID"),
566
+ summary: tool.schema.string().describe("Completion summary"),
567
+ evaluation: tool.schema.string().optional().describe("Self-evaluation"),
568
+ files_touched: tool.schema
569
+ .array(tool.schema.string())
570
+ .optional()
571
+ .describe("Files modified"),
572
+ skip_ubs_scan: tool.schema.boolean().optional().describe("Skip UBS scan"),
573
+ },
574
+ execute: (args, ctx) => execTool("swarm_complete", args, ctx),
575
+ });
576
+
577
+ const swarm_record_outcome = tool({
578
+ description: "Record subtask outcome for implicit feedback scoring",
579
+ args: {
580
+ bead_id: tool.schema.string().describe("Bead ID"),
581
+ duration_ms: tool.schema.number().int().min(0).describe("Duration in ms"),
582
+ error_count: tool.schema
583
+ .number()
584
+ .int()
585
+ .min(0)
586
+ .optional()
587
+ .describe("Error count"),
588
+ retry_count: tool.schema
589
+ .number()
590
+ .int()
591
+ .min(0)
592
+ .optional()
593
+ .describe("Retry count"),
594
+ success: tool.schema.boolean().describe("Whether task succeeded"),
595
+ files_touched: tool.schema
596
+ .array(tool.schema.string())
597
+ .optional()
598
+ .describe("Files modified"),
599
+ criteria: tool.schema
600
+ .array(tool.schema.string())
601
+ .optional()
602
+ .describe("Evaluation criteria"),
603
+ strategy: tool.schema
604
+ .enum(["file-based", "feature-based", "risk-based"])
605
+ .optional()
606
+ .describe("Strategy used"),
607
+ },
608
+ execute: (args, ctx) => execTool("swarm_record_outcome", args, ctx),
609
+ });
610
+
611
+ const swarm_subtask_prompt = tool({
612
+ description: "Generate the prompt for a spawned subtask agent",
613
+ args: {
614
+ agent_name: tool.schema.string().describe("Agent name"),
615
+ bead_id: tool.schema.string().describe("Bead ID"),
616
+ epic_id: tool.schema.string().describe("Epic ID"),
617
+ subtask_title: tool.schema.string().describe("Subtask title"),
618
+ subtask_description: tool.schema
619
+ .string()
620
+ .optional()
621
+ .describe("Description"),
622
+ files: tool.schema.array(tool.schema.string()).describe("Files to work on"),
623
+ shared_context: tool.schema.string().optional().describe("Shared context"),
624
+ },
625
+ execute: (args, ctx) => execTool("swarm_subtask_prompt", args, ctx),
626
+ });
627
+
628
+ const swarm_spawn_subtask = tool({
629
+ description: "Prepare a subtask for spawning with Task tool",
630
+ args: {
631
+ bead_id: tool.schema.string().describe("Bead ID"),
632
+ epic_id: tool.schema.string().describe("Epic ID"),
633
+ subtask_title: tool.schema.string().describe("Subtask title"),
634
+ subtask_description: tool.schema
635
+ .string()
636
+ .optional()
637
+ .describe("Description"),
638
+ files: tool.schema.array(tool.schema.string()).describe("Files to work on"),
639
+ shared_context: tool.schema.string().optional().describe("Shared context"),
640
+ },
641
+ execute: (args, ctx) => execTool("swarm_spawn_subtask", args, ctx),
642
+ });
643
+
644
+ const swarm_complete_subtask = tool({
645
+ description: "Handle subtask completion after Task agent returns",
646
+ args: {
647
+ bead_id: tool.schema.string().describe("Bead ID"),
648
+ task_result: tool.schema.string().describe("Task result JSON"),
649
+ files_touched: tool.schema
650
+ .array(tool.schema.string())
651
+ .optional()
652
+ .describe("Files modified"),
653
+ },
654
+ execute: (args, ctx) => execTool("swarm_complete_subtask", args, ctx),
655
+ });
656
+
657
+ const swarm_evaluation_prompt = tool({
658
+ description: "Generate self-evaluation prompt for a completed subtask",
659
+ args: {
660
+ bead_id: tool.schema.string().describe("Bead ID"),
661
+ subtask_title: tool.schema.string().describe("Subtask title"),
662
+ files_touched: tool.schema
663
+ .array(tool.schema.string())
664
+ .describe("Files modified"),
665
+ },
666
+ execute: (args, ctx) => execTool("swarm_evaluation_prompt", args, ctx),
667
+ });
668
+
669
+ // =============================================================================
670
+ // Plugin Export
671
+ // =============================================================================
672
+
673
+ export const SwarmPlugin: Plugin = async (
674
+ _input: PluginInput,
675
+ ): Promise<Hooks> => {
676
+ return {
677
+ tool: {
678
+ // Beads
679
+ beads_create,
680
+ beads_create_epic,
681
+ beads_query,
682
+ beads_update,
683
+ beads_close,
684
+ beads_start,
685
+ beads_ready,
686
+ beads_sync,
687
+ beads_link_thread,
688
+ // Agent Mail
689
+ agentmail_init,
690
+ agentmail_send,
691
+ agentmail_inbox,
692
+ agentmail_read_message,
693
+ agentmail_summarize_thread,
694
+ agentmail_reserve,
695
+ agentmail_release,
696
+ agentmail_ack,
697
+ agentmail_search,
698
+ agentmail_health,
699
+ // Structured
700
+ structured_extract_json,
701
+ structured_validate,
702
+ structured_parse_evaluation,
703
+ structured_parse_decomposition,
704
+ structured_parse_bead_tree,
705
+ // Swarm
706
+ swarm_init,
707
+ swarm_select_strategy,
708
+ swarm_plan_prompt,
709
+ swarm_decompose,
710
+ swarm_validate_decomposition,
711
+ swarm_status,
712
+ swarm_progress,
713
+ swarm_complete,
714
+ swarm_record_outcome,
715
+ swarm_subtask_prompt,
716
+ swarm_spawn_subtask,
717
+ swarm_complete_subtask,
718
+ swarm_evaluation_prompt,
719
+ },
720
+ };
721
+ };
722
+
723
+ export default SwarmPlugin;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-swarm-plugin",
3
- "version": "0.11.3",
3
+ "version": "0.12.2",
4
4
  "description": "Multi-agent swarm coordination for OpenCode with learning capabilities, beads integration, and Agent Mail",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -22,7 +22,11 @@
22
22
  "test:integration": "bun test src/*.integration.test.ts",
23
23
  "test:all": "bun test",
24
24
  "typecheck": "tsc --noEmit",
25
- "clean": "rm -rf dist"
25
+ "clean": "rm -rf dist",
26
+ "release": "npm run build && npm version patch && git push && npm run publish:otp",
27
+ "release:minor": "npm run build && npm version minor && git push && npm run publish:otp",
28
+ "release:major": "npm run build && npm version major && git push && npm run publish:otp",
29
+ "publish:otp": "bash -c 'source .env && npm publish --otp=$(op item get $NPM_1P_ITEM --otp)'"
26
30
  },
27
31
  "dependencies": {
28
32
  "@clack/prompts": "^0.11.0",
package/src/agent-mail.ts CHANGED
@@ -62,9 +62,83 @@ export interface AgentMailState {
62
62
  // Module-level state (keyed by sessionID)
63
63
  // ============================================================================
64
64
 
65
+ import {
66
+ existsSync,
67
+ mkdirSync,
68
+ readFileSync,
69
+ writeFileSync,
70
+ unlinkSync,
71
+ } from "fs";
72
+ import { join } from "path";
73
+ import { tmpdir } from "os";
74
+
75
+ /**
76
+ * Directory for persisting session state across CLI invocations
77
+ * This allows `swarm tool` commands to share state
78
+ */
79
+ const SESSION_STATE_DIR =
80
+ process.env.SWARM_STATE_DIR || join(tmpdir(), "swarm-sessions");
81
+
82
+ /**
83
+ * Get the file path for a session's state
84
+ */
85
+ function getSessionStatePath(sessionID: string): string {
86
+ // Sanitize sessionID to be filesystem-safe
87
+ const safeID = sessionID.replace(/[^a-zA-Z0-9_-]/g, "_");
88
+ return join(SESSION_STATE_DIR, `${safeID}.json`);
89
+ }
90
+
91
+ /**
92
+ * Load session state from disk
93
+ */
94
+ function loadSessionState(sessionID: string): AgentMailState | null {
95
+ const path = getSessionStatePath(sessionID);
96
+ try {
97
+ if (existsSync(path)) {
98
+ const data = readFileSync(path, "utf-8");
99
+ return JSON.parse(data) as AgentMailState;
100
+ }
101
+ } catch (error) {
102
+ // File might be corrupted or inaccessible - ignore and return null
103
+ console.warn(`[agent-mail] Could not load session state: ${error}`);
104
+ }
105
+ return null;
106
+ }
107
+
108
+ /**
109
+ * Save session state to disk
110
+ */
111
+ function saveSessionState(sessionID: string, state: AgentMailState): void {
112
+ try {
113
+ // Ensure directory exists
114
+ if (!existsSync(SESSION_STATE_DIR)) {
115
+ mkdirSync(SESSION_STATE_DIR, { recursive: true });
116
+ }
117
+ const path = getSessionStatePath(sessionID);
118
+ writeFileSync(path, JSON.stringify(state, null, 2));
119
+ } catch (error) {
120
+ // Non-fatal - state just won't persist
121
+ console.warn(`[agent-mail] Could not save session state: ${error}`);
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Delete session state from disk
127
+ */
128
+ function deleteSessionState(sessionID: string): void {
129
+ const path = getSessionStatePath(sessionID);
130
+ try {
131
+ if (existsSync(path)) {
132
+ unlinkSync(path);
133
+ }
134
+ } catch {
135
+ // Ignore errors on cleanup
136
+ }
137
+ }
138
+
65
139
  /**
66
140
  * State storage keyed by sessionID.
67
- * Since ToolContext doesn't have persistent state, we use a module-level map.
141
+ * In-memory cache that also persists to disk for CLI usage.
68
142
  */
69
143
  const sessionStates = new Map<string, AgentMailState>();
70
144
 
@@ -691,9 +765,23 @@ export async function mcpCall<T>(
691
765
 
692
766
  /**
693
767
  * Get Agent Mail state for a session, or throw if not initialized
768
+ *
769
+ * Checks in-memory cache first, then falls back to disk storage.
770
+ * This allows CLI invocations to share state across calls.
694
771
  */
695
772
  function requireState(sessionID: string): AgentMailState {
696
- const state = sessionStates.get(sessionID);
773
+ // Check in-memory cache first
774
+ let state = sessionStates.get(sessionID);
775
+
776
+ // If not in memory, try loading from disk
777
+ if (!state) {
778
+ state = loadSessionState(sessionID) ?? undefined;
779
+ if (state) {
780
+ // Cache in memory for subsequent calls in same process
781
+ sessionStates.set(sessionID, state);
782
+ }
783
+ }
784
+
697
785
  if (!state) {
698
786
  throw new AgentMailNotInitializedError();
699
787
  }
@@ -702,23 +790,38 @@ function requireState(sessionID: string): AgentMailState {
702
790
 
703
791
  /**
704
792
  * Store Agent Mail state for a session
793
+ *
794
+ * Saves to both in-memory cache and disk for CLI persistence.
705
795
  */
706
796
  function setState(sessionID: string, state: AgentMailState): void {
707
797
  sessionStates.set(sessionID, state);
798
+ saveSessionState(sessionID, state);
708
799
  }
709
800
 
710
801
  /**
711
802
  * Get state if exists (for cleanup hooks)
803
+ *
804
+ * Checks in-memory cache first, then falls back to disk storage.
712
805
  */
713
806
  function getState(sessionID: string): AgentMailState | undefined {
714
- return sessionStates.get(sessionID);
807
+ let state = sessionStates.get(sessionID);
808
+ if (!state) {
809
+ state = loadSessionState(sessionID) ?? undefined;
810
+ if (state) {
811
+ sessionStates.set(sessionID, state);
812
+ }
813
+ }
814
+ return state;
715
815
  }
716
816
 
717
817
  /**
718
818
  * Clear state for a session
819
+ *
820
+ * Removes from both in-memory cache and disk.
719
821
  */
720
822
  function clearState(sessionID: string): void {
721
823
  sessionStates.delete(sessionID);
824
+ deleteSessionState(sessionID);
722
825
  }
723
826
 
724
827
  // ============================================================================
package/src/index.ts CHANGED
@@ -280,6 +280,28 @@ export {
280
280
  type StrategyDefinition,
281
281
  } from "./swarm";
282
282
 
283
+ // =============================================================================
284
+ // Unified Tool Registry for CLI
285
+ // =============================================================================
286
+
287
+ /**
288
+ * All tools in a single registry for CLI tool execution
289
+ *
290
+ * This is used by `swarm tool <name>` command to dynamically execute tools.
291
+ * Each tool has an `execute` function that takes (args, ctx) and returns a string.
292
+ */
293
+ export const allTools = {
294
+ ...beadsTools,
295
+ ...agentMailTools,
296
+ ...structuredTools,
297
+ ...swarmTools,
298
+ } as const;
299
+
300
+ /**
301
+ * Type for CLI tool names (all available tools)
302
+ */
303
+ export type CLIToolName = keyof typeof allTools;
304
+
283
305
  /**
284
306
  * Re-export storage module
285
307
  *