opencode-swarm 4.3.2 → 4.4.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/dist/cli/index.js CHANGED
@@ -96,6 +96,66 @@ Next steps:`);
96
96
  console.log(" what expertise is needed and requests it dynamically.");
97
97
  return 0;
98
98
  }
99
+ async function uninstall() {
100
+ try {
101
+ console.log(`\uD83D\uDC1D Uninstalling OpenCode Swarm...
102
+ `);
103
+ const opencodeConfig = loadJson(OPENCODE_CONFIG_PATH);
104
+ if (!opencodeConfig) {
105
+ if (fs.existsSync(OPENCODE_CONFIG_PATH)) {
106
+ console.log(`\u2717 Could not parse opencode config at: ${OPENCODE_CONFIG_PATH}`);
107
+ return 1;
108
+ } else {
109
+ console.log(`\u26A0 No opencode config found at: ${OPENCODE_CONFIG_PATH}`);
110
+ console.log("Nothing to uninstall.");
111
+ return 0;
112
+ }
113
+ }
114
+ if (!opencodeConfig.plugin || opencodeConfig.plugin.length === 0) {
115
+ console.log("\u26A0 opencode-swarm is not installed (no plugins configured).");
116
+ return 0;
117
+ }
118
+ const pluginName = "opencode-swarm";
119
+ const filteredPlugins = opencodeConfig.plugin.filter((p) => p !== pluginName && !p.startsWith(`${pluginName}@`));
120
+ if (filteredPlugins.length === opencodeConfig.plugin.length) {
121
+ console.log("\u26A0 opencode-swarm is not installed.");
122
+ return 0;
123
+ }
124
+ opencodeConfig.plugin = filteredPlugins;
125
+ if (opencodeConfig.agent) {
126
+ delete opencodeConfig.agent.explore;
127
+ delete opencodeConfig.agent.general;
128
+ if (Object.keys(opencodeConfig.agent).length === 0) {
129
+ delete opencodeConfig.agent;
130
+ }
131
+ }
132
+ saveJson(OPENCODE_CONFIG_PATH, opencodeConfig);
133
+ console.log("\u2713 Removed opencode-swarm from OpenCode plugins");
134
+ console.log("\u2713 Re-enabled default OpenCode agents (explore, general)");
135
+ if (process.argv.includes("--clean")) {
136
+ let cleaned = false;
137
+ if (fs.existsSync(PLUGIN_CONFIG_PATH)) {
138
+ fs.unlinkSync(PLUGIN_CONFIG_PATH);
139
+ console.log(`\u2713 Removed plugin config: ${PLUGIN_CONFIG_PATH}`);
140
+ cleaned = true;
141
+ }
142
+ if (fs.existsSync(PROMPTS_DIR)) {
143
+ fs.rmSync(PROMPTS_DIR, { recursive: true });
144
+ console.log(`\u2713 Removed custom prompts: ${PROMPTS_DIR}`);
145
+ cleaned = true;
146
+ }
147
+ if (!cleaned) {
148
+ console.log("\u2713 No config files to clean up");
149
+ }
150
+ }
151
+ console.log(`
152
+ \u2705 Uninstall complete!`);
153
+ return 0;
154
+ } catch (error) {
155
+ console.log("\u2717 Uninstall failed: " + (error instanceof Error ? error.message : String(error)));
156
+ return 1;
157
+ }
158
+ }
99
159
  function printHelp() {
100
160
  console.log(`
101
161
  opencode-swarm - Architect-centric agentic swarm plugin for OpenCode
@@ -104,8 +164,10 @@ Usage: bunx opencode-swarm [command] [OPTIONS]
104
164
 
105
165
  Commands:
106
166
  install Install and configure the plugin (default)
167
+ uninstall Remove the plugin from OpenCode config
107
168
 
108
169
  Options:
170
+ --clean Also remove config files and custom prompts (with uninstall)
109
171
  -h, --help Show this help message
110
172
 
111
173
  Configuration:
@@ -122,6 +184,8 @@ Custom Prompts:
122
184
 
123
185
  Examples:
124
186
  bunx opencode-swarm install
187
+ bunx opencode-swarm uninstall
188
+ bunx opencode-swarm uninstall --clean
125
189
  bunx opencode-swarm --help
126
190
  `);
127
191
  }
@@ -135,6 +199,9 @@ async function main() {
135
199
  if (command === "install") {
136
200
  const exitCode = await install();
137
201
  process.exit(exitCode);
202
+ } else if (command === "uninstall") {
203
+ const exitCode = await uninstall();
204
+ process.exit(exitCode);
138
205
  } else {
139
206
  console.error(`Unknown command: ${command}`);
140
207
  console.error("Run with --help for usage information");
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Handles the /swarm config command.
3
+ * Loads and displays the current resolved plugin configuration.
4
+ */
5
+ export declare function handleConfigCommand(directory: string, _args: string[]): Promise<string>;
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Handles the /swarm history command.
3
+ * Reads plan.md and displays a summary of all phases and their status.
4
+ */
5
+ export declare function handleHistoryCommand(directory: string, _args: string[]): Promise<string>;
@@ -1,5 +1,7 @@
1
1
  import type { AgentDefinition } from '../agents';
2
2
  export { handleAgentsCommand } from './agents';
3
+ export { handleConfigCommand } from './config';
4
+ export { handleHistoryCommand } from './history';
3
5
  export { handlePlanCommand } from './plan';
4
6
  export { handleStatusCommand } from './status';
5
7
  /**
package/dist/index.js CHANGED
@@ -14410,9 +14410,46 @@ function handleAgentsCommand(agents) {
14410
14410
  `);
14411
14411
  }
14412
14412
 
14413
- // src/hooks/utils.ts
14413
+ // src/commands/config.ts
14414
+ import * as os2 from "os";
14414
14415
  import * as path2 from "path";
14416
+ function getUserConfigDir2() {
14417
+ return process.env.XDG_CONFIG_HOME || path2.join(os2.homedir(), ".config");
14418
+ }
14419
+ async function handleConfigCommand(directory, _args) {
14420
+ const config2 = loadPluginConfig(directory);
14421
+ const userConfigPath = path2.join(getUserConfigDir2(), "opencode", "opencode-swarm.json");
14422
+ const projectConfigPath = path2.join(directory, ".opencode", "opencode-swarm.json");
14423
+ const lines = [
14424
+ "## Swarm Configuration",
14425
+ "",
14426
+ "### Config Files",
14427
+ `- User: \`${userConfigPath}\``,
14428
+ `- Project: \`${projectConfigPath}\``,
14429
+ "",
14430
+ "### Resolved Config",
14431
+ "```json",
14432
+ JSON.stringify(config2, null, 2),
14433
+ "```"
14434
+ ];
14435
+ return lines.join(`
14436
+ `);
14437
+ }
14415
14438
 
14439
+ // src/hooks/utils.ts
14440
+ import * as path3 from "path";
14441
+
14442
+ // src/utils/errors.ts
14443
+ class SwarmError extends Error {
14444
+ code;
14445
+ guidance;
14446
+ constructor(message, code, guidance) {
14447
+ super(message);
14448
+ this.name = "SwarmError";
14449
+ this.code = code;
14450
+ this.guidance = guidance;
14451
+ }
14452
+ }
14416
14453
  // src/utils/logger.ts
14417
14454
  var DEBUG = process.env.OPENCODE_SWARM_DEBUG === "1";
14418
14455
  function log(message, data) {
@@ -14440,7 +14477,12 @@ function safeHook(fn) {
14440
14477
  await fn(input, output);
14441
14478
  } catch (_error) {
14442
14479
  const functionName = fn.name || "unknown";
14443
- warn(`Hook function '${functionName}' failed:`, _error);
14480
+ if (_error instanceof SwarmError) {
14481
+ warn(`Hook '${functionName}' failed: ${_error.message}
14482
+ \u2192 ${_error.guidance}`);
14483
+ } else {
14484
+ warn(`Hook function '${functionName}' failed:`, _error);
14485
+ }
14444
14486
  }
14445
14487
  };
14446
14488
  }
@@ -14462,14 +14504,14 @@ function validateSwarmPath(directory, filename) {
14462
14504
  if (/\.\.[/\\]/.test(filename)) {
14463
14505
  throw new Error("Invalid filename: path traversal detected");
14464
14506
  }
14465
- const baseDir = path2.normalize(path2.resolve(directory, ".swarm"));
14466
- const resolved = path2.normalize(path2.resolve(baseDir, filename));
14507
+ const baseDir = path3.normalize(path3.resolve(directory, ".swarm"));
14508
+ const resolved = path3.normalize(path3.resolve(baseDir, filename));
14467
14509
  if (process.platform === "win32") {
14468
- if (!resolved.toLowerCase().startsWith((baseDir + path2.sep).toLowerCase())) {
14510
+ if (!resolved.toLowerCase().startsWith((baseDir + path3.sep).toLowerCase())) {
14469
14511
  throw new Error("Invalid filename: path escapes .swarm directory");
14470
14512
  }
14471
14513
  } else {
14472
- if (!resolved.startsWith(baseDir + path2.sep)) {
14514
+ if (!resolved.startsWith(baseDir + path3.sep)) {
14473
14515
  throw new Error("Invalid filename: path escapes .swarm directory");
14474
14516
  }
14475
14517
  }
@@ -14492,6 +14534,57 @@ function estimateTokens(text) {
14492
14534
  return Math.ceil(text.length * 0.33);
14493
14535
  }
14494
14536
 
14537
+ // src/commands/history.ts
14538
+ async function handleHistoryCommand(directory, _args) {
14539
+ const planContent = await readSwarmFileAsync(directory, "plan.md");
14540
+ if (!planContent) {
14541
+ return "No history available.";
14542
+ }
14543
+ const phaseRegex = /^## Phase (\d+):?\s*(.+?)(?:\s*\[(COMPLETE|IN PROGRESS|PENDING)\])?\s*$/gm;
14544
+ const phases = [];
14545
+ const lines = planContent.split(`
14546
+ `);
14547
+ for (let match = phaseRegex.exec(planContent);match !== null; match = phaseRegex.exec(planContent)) {
14548
+ const num = parseInt(match[1], 10);
14549
+ const name = match[2].trim();
14550
+ const status = match[3] || "PENDING";
14551
+ const headerLineIndex = lines.indexOf(match[0]);
14552
+ let completed = 0;
14553
+ let total = 0;
14554
+ if (headerLineIndex !== -1) {
14555
+ for (let i = headerLineIndex + 1;i < lines.length; i++) {
14556
+ const line = lines[i];
14557
+ if (/^## Phase \d+/.test(line) || line.trim() === "---" && total > 0) {
14558
+ break;
14559
+ }
14560
+ if (/^- \[x\]/.test(line)) {
14561
+ completed++;
14562
+ total++;
14563
+ } else if (/^- \[ \]/.test(line)) {
14564
+ total++;
14565
+ }
14566
+ }
14567
+ }
14568
+ phases.push({ num, name, status, completed, total });
14569
+ }
14570
+ if (phases.length === 0) {
14571
+ return "No history available.";
14572
+ }
14573
+ const tableLines = [
14574
+ "## Swarm History",
14575
+ "",
14576
+ "| Phase | Name | Status | Tasks |",
14577
+ "|-------|------|--------|-------|"
14578
+ ];
14579
+ for (const phase of phases) {
14580
+ const statusIcon = phase.status === "COMPLETE" ? "\u2705" : phase.status === "IN PROGRESS" ? "\uD83D\uDD04" : "\u23F3";
14581
+ const tasks = phase.total > 0 ? `${phase.completed}/${phase.total}` : "-";
14582
+ tableLines.push(`| ${phase.num} | ${phase.name} | ${statusIcon} ${phase.status} | ${tasks} |`);
14583
+ }
14584
+ return tableLines.join(`
14585
+ `);
14586
+ }
14587
+
14495
14588
  // src/commands/plan.ts
14496
14589
  async function handlePlanCommand(directory, args) {
14497
14590
  const planContent = await readSwarmFileAsync(directory, "plan.md");
@@ -14709,7 +14802,9 @@ var HELP_TEXT = [
14709
14802
  "",
14710
14803
  "- `/swarm status` \u2014 Show current swarm state",
14711
14804
  "- `/swarm plan [phase]` \u2014 Show plan (optionally filter by phase number)",
14712
- "- `/swarm agents` \u2014 List registered agents"
14805
+ "- `/swarm agents` \u2014 List registered agents",
14806
+ "- `/swarm history` \u2014 Show completed phases summary",
14807
+ "- `/swarm config` \u2014 Show current resolved configuration"
14713
14808
  ].join(`
14714
14809
  `);
14715
14810
  function createSwarmCommandHandler(directory, agents) {
@@ -14730,6 +14825,12 @@ function createSwarmCommandHandler(directory, agents) {
14730
14825
  case "agents":
14731
14826
  text = handleAgentsCommand(agents);
14732
14827
  break;
14828
+ case "history":
14829
+ text = await handleHistoryCommand(directory, args);
14830
+ break;
14831
+ case "config":
14832
+ text = await handleConfigCommand(directory, args);
14833
+ break;
14733
14834
  default:
14734
14835
  text = HELP_TEXT;
14735
14836
  break;
@@ -14817,8 +14918,8 @@ async function doFlush(directory) {
14817
14918
  const activitySection = renderActivitySection();
14818
14919
  const updated = replaceOrAppendSection(existing, "## Agent Activity", activitySection);
14819
14920
  const flushedCount = swarmState.pendingEvents;
14820
- const path3 = `${directory}/.swarm/context.md`;
14821
- await Bun.write(path3, updated);
14921
+ const path4 = `${directory}/.swarm/context.md`;
14922
+ await Bun.write(path4, updated);
14822
14923
  swarmState.pendingEvents = Math.max(0, swarmState.pendingEvents - flushedCount);
14823
14924
  } catch (error49) {
14824
14925
  warn("Agent activity flush failed:", error49);
@@ -15837,10 +15938,10 @@ function mergeDefs2(...defs) {
15837
15938
  function cloneDef2(schema) {
15838
15939
  return mergeDefs2(schema._zod.def);
15839
15940
  }
15840
- function getElementAtPath2(obj, path3) {
15841
- if (!path3)
15941
+ function getElementAtPath2(obj, path4) {
15942
+ if (!path4)
15842
15943
  return obj;
15843
- return path3.reduce((acc, key) => acc?.[key], obj);
15944
+ return path4.reduce((acc, key) => acc?.[key], obj);
15844
15945
  }
15845
15946
  function promiseAllObject2(promisesObj) {
15846
15947
  const keys = Object.keys(promisesObj);
@@ -16199,11 +16300,11 @@ function aborted2(x, startIndex = 0) {
16199
16300
  }
16200
16301
  return false;
16201
16302
  }
16202
- function prefixIssues2(path3, issues) {
16303
+ function prefixIssues2(path4, issues) {
16203
16304
  return issues.map((iss) => {
16204
16305
  var _a2;
16205
16306
  (_a2 = iss).path ?? (_a2.path = []);
16206
- iss.path.unshift(path3);
16307
+ iss.path.unshift(path4);
16207
16308
  return iss;
16208
16309
  });
16209
16310
  }
@@ -16371,7 +16472,7 @@ function treeifyError2(error49, _mapper) {
16371
16472
  return issue3.message;
16372
16473
  };
16373
16474
  const result = { errors: [] };
16374
- const processError = (error50, path3 = []) => {
16475
+ const processError = (error50, path4 = []) => {
16375
16476
  var _a2, _b;
16376
16477
  for (const issue3 of error50.issues) {
16377
16478
  if (issue3.code === "invalid_union" && issue3.errors.length) {
@@ -16381,7 +16482,7 @@ function treeifyError2(error49, _mapper) {
16381
16482
  } else if (issue3.code === "invalid_element") {
16382
16483
  processError({ issues: issue3.issues }, issue3.path);
16383
16484
  } else {
16384
- const fullpath = [...path3, ...issue3.path];
16485
+ const fullpath = [...path4, ...issue3.path];
16385
16486
  if (fullpath.length === 0) {
16386
16487
  result.errors.push(mapper(issue3));
16387
16488
  continue;
@@ -16413,8 +16514,8 @@ function treeifyError2(error49, _mapper) {
16413
16514
  }
16414
16515
  function toDotPath2(_path) {
16415
16516
  const segs = [];
16416
- const path3 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
16417
- for (const seg of path3) {
16517
+ const path4 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
16518
+ for (const seg of path4) {
16418
16519
  if (typeof seg === "number")
16419
16520
  segs.push(`[${seg}]`);
16420
16521
  else if (typeof seg === "symbol")
@@ -27610,7 +27711,7 @@ Use these as DOMAIN values when delegating to @sme.`;
27610
27711
  });
27611
27712
  // src/tools/file-extractor.ts
27612
27713
  import * as fs2 from "fs";
27613
- import * as path3 from "path";
27714
+ import * as path4 from "path";
27614
27715
  var EXT_MAP = {
27615
27716
  python: ".py",
27616
27717
  py: ".py",
@@ -27688,12 +27789,12 @@ var extract_code_blocks = tool({
27688
27789
  if (prefix) {
27689
27790
  filename = `${prefix}_${filename}`;
27690
27791
  }
27691
- let filepath = path3.join(targetDir, filename);
27692
- const base = path3.basename(filepath, path3.extname(filepath));
27693
- const ext = path3.extname(filepath);
27792
+ let filepath = path4.join(targetDir, filename);
27793
+ const base = path4.basename(filepath, path4.extname(filepath));
27794
+ const ext = path4.extname(filepath);
27694
27795
  let counter = 1;
27695
27796
  while (fs2.existsSync(filepath)) {
27696
- filepath = path3.join(targetDir, `${base}_${counter}${ext}`);
27797
+ filepath = path4.join(targetDir, `${base}_${counter}${ext}`);
27697
27798
  counter++;
27698
27799
  }
27699
27800
  try {
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Base error class for all swarm errors.
3
+ * Includes a machine-readable `code` and a user-facing `guidance` string.
4
+ */
5
+ export declare class SwarmError extends Error {
6
+ readonly code: string;
7
+ readonly guidance: string;
8
+ constructor(message: string, code: string, guidance: string);
9
+ }
10
+ /**
11
+ * Error thrown when configuration loading or validation fails.
12
+ */
13
+ export declare class ConfigError extends SwarmError {
14
+ constructor(message: string, guidance: string);
15
+ }
16
+ /**
17
+ * Error thrown when a hook execution fails.
18
+ */
19
+ export declare class HookError extends SwarmError {
20
+ constructor(message: string, guidance: string);
21
+ }
22
+ /**
23
+ * Error thrown when a tool execution fails.
24
+ */
25
+ export declare class ToolError extends SwarmError {
26
+ constructor(message: string, guidance: string);
27
+ }
28
+ /**
29
+ * Error thrown when CLI operations fail.
30
+ */
31
+ export declare class CLIError extends SwarmError {
32
+ constructor(message: string, guidance: string);
33
+ }
@@ -1 +1,2 @@
1
- export { log, warn, error } from './logger';
1
+ export { CLIError, ConfigError, HookError, SwarmError, ToolError, } from './errors';
2
+ export { error, log, warn } from './logger';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-swarm",
3
- "version": "4.3.2",
3
+ "version": "4.4.0",
4
4
  "description": "Architect-centric agentic swarm plugin for OpenCode - hub-and-spoke orchestration with SME consultation, code generation, and QA review",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -35,12 +35,12 @@
35
35
  "prepublishOnly": "bun run build"
36
36
  },
37
37
  "dependencies": {
38
- "@opencode-ai/plugin": "^1.1.19",
39
- "@opencode-ai/sdk": "^1.1.19",
38
+ "@opencode-ai/plugin": "^1.1.53",
39
+ "@opencode-ai/sdk": "^1.1.53",
40
40
  "zod": "^4.1.8"
41
41
  },
42
42
  "devDependencies": {
43
- "@biomejs/biome": "2.3.11",
43
+ "@biomejs/biome": "2.3.14",
44
44
  "bun-types": "latest",
45
45
  "typescript": "^5.7.3"
46
46
  }