opencode-swarm 4.3.1 → 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
  /**
@@ -1,4 +1,6 @@
1
1
  import { type PluginConfig } from './schema';
2
+ export declare const MAX_CONFIG_FILE_BYTES = 102400;
3
+ export declare const MAX_MERGE_DEPTH = 10;
2
4
  /**
3
5
  * Deep merge two objects, with override values taking precedence.
4
6
  */
@@ -5,4 +5,4 @@ export { createDelegationTrackerHook } from './delegation-tracker';
5
5
  export { extractCurrentPhase, extractCurrentTask, extractDecisions, extractIncompleteTasks, extractPatterns, } from './extractors';
6
6
  export { createPipelineTrackerHook } from './pipeline-tracker';
7
7
  export { createSystemEnhancerHook } from './system-enhancer';
8
- export { composeHandlers, estimateTokens, readSwarmFileAsync, safeHook, } from './utils';
8
+ export { composeHandlers, estimateTokens, readSwarmFileAsync, safeHook, validateSwarmPath, } from './utils';
@@ -7,5 +7,14 @@
7
7
  */
8
8
  export declare function safeHook<I, O>(fn: (input: I, output: O) => Promise<void>): (input: I, output: O) => Promise<void>;
9
9
  export declare function composeHandlers<I, O>(...fns: Array<(input: I, output: O) => Promise<void>>): (input: I, output: O) => Promise<void>;
10
+ /**
11
+ * Validates that a filename is safe to use within the .swarm directory
12
+ *
13
+ * @param directory - The base directory containing the .swarm folder
14
+ * @param filename - The filename to validate
15
+ * @returns The resolved absolute path if validation passes
16
+ * @throws Error if the filename is invalid or attempts path traversal
17
+ */
18
+ export declare function validateSwarmPath(directory: string, filename: string): string;
10
19
  export declare function readSwarmFileAsync(directory: string, filename: string): Promise<string | null>;
11
20
  export declare function estimateTokens(text: string): number;
package/dist/index.js CHANGED
@@ -13603,11 +13603,17 @@ import * as os from "os";
13603
13603
  import * as path from "path";
13604
13604
  var CONFIG_FILENAME = "opencode-swarm.json";
13605
13605
  var PROMPTS_DIR_NAME = "opencode-swarm";
13606
+ var MAX_CONFIG_FILE_BYTES = 102400;
13606
13607
  function getUserConfigDir() {
13607
13608
  return process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
13608
13609
  }
13609
13610
  function loadConfigFromPath(configPath) {
13610
13611
  try {
13612
+ const stats = fs.statSync(configPath);
13613
+ if (stats.size > MAX_CONFIG_FILE_BYTES) {
13614
+ console.warn(`[opencode-swarm] Config file too large (max 100 KB): ${configPath}`);
13615
+ return null;
13616
+ }
13611
13617
  const content = fs.readFileSync(configPath, "utf-8");
13612
13618
  const rawConfig = JSON.parse(content);
13613
13619
  const result = PluginConfigSchema.safeParse(rawConfig);
@@ -13624,23 +13630,30 @@ function loadConfigFromPath(configPath) {
13624
13630
  return null;
13625
13631
  }
13626
13632
  }
13627
- function deepMerge(base, override) {
13628
- if (!base)
13629
- return override;
13630
- if (!override)
13631
- return base;
13633
+ var MAX_MERGE_DEPTH = 10;
13634
+ function deepMergeInternal(base, override, depth) {
13635
+ if (depth >= MAX_MERGE_DEPTH) {
13636
+ throw new Error(`deepMerge exceeded maximum depth of ${MAX_MERGE_DEPTH}`);
13637
+ }
13632
13638
  const result = { ...base };
13633
13639
  for (const key of Object.keys(override)) {
13634
13640
  const baseVal = base[key];
13635
13641
  const overrideVal = override[key];
13636
13642
  if (typeof baseVal === "object" && baseVal !== null && typeof overrideVal === "object" && overrideVal !== null && !Array.isArray(baseVal) && !Array.isArray(overrideVal)) {
13637
- result[key] = deepMerge(baseVal, overrideVal);
13643
+ result[key] = deepMergeInternal(baseVal, overrideVal, depth + 1);
13638
13644
  } else {
13639
13645
  result[key] = overrideVal;
13640
13646
  }
13641
13647
  }
13642
13648
  return result;
13643
13649
  }
13650
+ function deepMerge(base, override) {
13651
+ if (!base)
13652
+ return override;
13653
+ if (!override)
13654
+ return base;
13655
+ return deepMergeInternal(base, override, 0);
13656
+ }
13644
13657
  function loadPluginConfig(directory) {
13645
13658
  const userConfigPath = path.join(getUserConfigDir(), "opencode", CONFIG_FILENAME);
13646
13659
  const projectConfigPath = path.join(directory, ".opencode", CONFIG_FILENAME);
@@ -14397,6 +14410,46 @@ function handleAgentsCommand(agents) {
14397
14410
  `);
14398
14411
  }
14399
14412
 
14413
+ // src/commands/config.ts
14414
+ import * as os2 from "os";
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
+ }
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
+ }
14400
14453
  // src/utils/logger.ts
14401
14454
  var DEBUG = process.env.OPENCODE_SWARM_DEBUG === "1";
14402
14455
  function log(message, data) {
@@ -14424,7 +14477,12 @@ function safeHook(fn) {
14424
14477
  await fn(input, output);
14425
14478
  } catch (_error) {
14426
14479
  const functionName = fn.name || "unknown";
14427
- 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
+ }
14428
14486
  }
14429
14487
  };
14430
14488
  }
@@ -14439,10 +14497,30 @@ function composeHandlers(...fns) {
14439
14497
  }
14440
14498
  };
14441
14499
  }
14500
+ function validateSwarmPath(directory, filename) {
14501
+ if (/[\0]/.test(filename)) {
14502
+ throw new Error("Invalid filename: contains null bytes");
14503
+ }
14504
+ if (/\.\.[/\\]/.test(filename)) {
14505
+ throw new Error("Invalid filename: path traversal detected");
14506
+ }
14507
+ const baseDir = path3.normalize(path3.resolve(directory, ".swarm"));
14508
+ const resolved = path3.normalize(path3.resolve(baseDir, filename));
14509
+ if (process.platform === "win32") {
14510
+ if (!resolved.toLowerCase().startsWith((baseDir + path3.sep).toLowerCase())) {
14511
+ throw new Error("Invalid filename: path escapes .swarm directory");
14512
+ }
14513
+ } else {
14514
+ if (!resolved.startsWith(baseDir + path3.sep)) {
14515
+ throw new Error("Invalid filename: path escapes .swarm directory");
14516
+ }
14517
+ }
14518
+ return resolved;
14519
+ }
14442
14520
  async function readSwarmFileAsync(directory, filename) {
14443
- const path2 = `${directory}/.swarm/${filename}`;
14444
14521
  try {
14445
- const file2 = Bun.file(path2);
14522
+ const resolvedPath = validateSwarmPath(directory, filename);
14523
+ const file2 = Bun.file(resolvedPath);
14446
14524
  const content = await file2.text();
14447
14525
  return content;
14448
14526
  } catch {
@@ -14456,6 +14534,57 @@ function estimateTokens(text) {
14456
14534
  return Math.ceil(text.length * 0.33);
14457
14535
  }
14458
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
+
14459
14588
  // src/commands/plan.ts
14460
14589
  async function handlePlanCommand(directory, args) {
14461
14590
  const planContent = await readSwarmFileAsync(directory, "plan.md");
@@ -14673,7 +14802,9 @@ var HELP_TEXT = [
14673
14802
  "",
14674
14803
  "- `/swarm status` \u2014 Show current swarm state",
14675
14804
  "- `/swarm plan [phase]` \u2014 Show plan (optionally filter by phase number)",
14676
- "- `/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"
14677
14808
  ].join(`
14678
14809
  `);
14679
14810
  function createSwarmCommandHandler(directory, agents) {
@@ -14694,6 +14825,12 @@ function createSwarmCommandHandler(directory, agents) {
14694
14825
  case "agents":
14695
14826
  text = handleAgentsCommand(agents);
14696
14827
  break;
14828
+ case "history":
14829
+ text = await handleHistoryCommand(directory, args);
14830
+ break;
14831
+ case "config":
14832
+ text = await handleConfigCommand(directory, args);
14833
+ break;
14697
14834
  default:
14698
14835
  text = HELP_TEXT;
14699
14836
  break;
@@ -14781,8 +14918,8 @@ async function doFlush(directory) {
14781
14918
  const activitySection = renderActivitySection();
14782
14919
  const updated = replaceOrAppendSection(existing, "## Agent Activity", activitySection);
14783
14920
  const flushedCount = swarmState.pendingEvents;
14784
- const path2 = `${directory}/.swarm/context.md`;
14785
- await Bun.write(path2, updated);
14921
+ const path4 = `${directory}/.swarm/context.md`;
14922
+ await Bun.write(path4, updated);
14786
14923
  swarmState.pendingEvents = Math.max(0, swarmState.pendingEvents - flushedCount);
14787
14924
  } catch (error49) {
14788
14925
  warn("Agent activity flush failed:", error49);
@@ -15801,10 +15938,10 @@ function mergeDefs2(...defs) {
15801
15938
  function cloneDef2(schema) {
15802
15939
  return mergeDefs2(schema._zod.def);
15803
15940
  }
15804
- function getElementAtPath2(obj, path2) {
15805
- if (!path2)
15941
+ function getElementAtPath2(obj, path4) {
15942
+ if (!path4)
15806
15943
  return obj;
15807
- return path2.reduce((acc, key) => acc?.[key], obj);
15944
+ return path4.reduce((acc, key) => acc?.[key], obj);
15808
15945
  }
15809
15946
  function promiseAllObject2(promisesObj) {
15810
15947
  const keys = Object.keys(promisesObj);
@@ -16163,11 +16300,11 @@ function aborted2(x, startIndex = 0) {
16163
16300
  }
16164
16301
  return false;
16165
16302
  }
16166
- function prefixIssues2(path2, issues) {
16303
+ function prefixIssues2(path4, issues) {
16167
16304
  return issues.map((iss) => {
16168
16305
  var _a2;
16169
16306
  (_a2 = iss).path ?? (_a2.path = []);
16170
- iss.path.unshift(path2);
16307
+ iss.path.unshift(path4);
16171
16308
  return iss;
16172
16309
  });
16173
16310
  }
@@ -16335,7 +16472,7 @@ function treeifyError2(error49, _mapper) {
16335
16472
  return issue3.message;
16336
16473
  };
16337
16474
  const result = { errors: [] };
16338
- const processError = (error50, path2 = []) => {
16475
+ const processError = (error50, path4 = []) => {
16339
16476
  var _a2, _b;
16340
16477
  for (const issue3 of error50.issues) {
16341
16478
  if (issue3.code === "invalid_union" && issue3.errors.length) {
@@ -16345,7 +16482,7 @@ function treeifyError2(error49, _mapper) {
16345
16482
  } else if (issue3.code === "invalid_element") {
16346
16483
  processError({ issues: issue3.issues }, issue3.path);
16347
16484
  } else {
16348
- const fullpath = [...path2, ...issue3.path];
16485
+ const fullpath = [...path4, ...issue3.path];
16349
16486
  if (fullpath.length === 0) {
16350
16487
  result.errors.push(mapper(issue3));
16351
16488
  continue;
@@ -16377,8 +16514,8 @@ function treeifyError2(error49, _mapper) {
16377
16514
  }
16378
16515
  function toDotPath2(_path) {
16379
16516
  const segs = [];
16380
- const path2 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
16381
- for (const seg of path2) {
16517
+ const path4 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
16518
+ for (const seg of path4) {
16382
16519
  if (typeof seg === "number")
16383
16520
  segs.push(`[${seg}]`);
16384
16521
  else if (typeof seg === "symbol")
@@ -27574,7 +27711,7 @@ Use these as DOMAIN values when delegating to @sme.`;
27574
27711
  });
27575
27712
  // src/tools/file-extractor.ts
27576
27713
  import * as fs2 from "fs";
27577
- import * as path2 from "path";
27714
+ import * as path4 from "path";
27578
27715
  var EXT_MAP = {
27579
27716
  python: ".py",
27580
27717
  py: ".py",
@@ -27652,12 +27789,12 @@ var extract_code_blocks = tool({
27652
27789
  if (prefix) {
27653
27790
  filename = `${prefix}_${filename}`;
27654
27791
  }
27655
- let filepath = path2.join(targetDir, filename);
27656
- const base = path2.basename(filepath, path2.extname(filepath));
27657
- const ext = path2.extname(filepath);
27792
+ let filepath = path4.join(targetDir, filename);
27793
+ const base = path4.basename(filepath, path4.extname(filepath));
27794
+ const ext = path4.extname(filepath);
27658
27795
  let counter = 1;
27659
27796
  while (fs2.existsSync(filepath)) {
27660
- filepath = path2.join(targetDir, `${base}_${counter}${ext}`);
27797
+ filepath = path4.join(targetDir, `${base}_${counter}${ext}`);
27661
27798
  counter++;
27662
27799
  }
27663
27800
  try {
@@ -27686,26 +27823,73 @@ Errors:
27686
27823
  }
27687
27824
  });
27688
27825
  // src/tools/gitingest.ts
27826
+ var GITINGEST_TIMEOUT_MS = 1e4;
27827
+ var GITINGEST_MAX_RESPONSE_BYTES = 5242880;
27828
+ var GITINGEST_MAX_RETRIES = 2;
27829
+ var delay = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
27689
27830
  async function fetchGitingest(args) {
27690
- const response = await fetch("https://gitingest.com/api/ingest", {
27691
- method: "POST",
27692
- headers: { "Content-Type": "application/json" },
27693
- body: JSON.stringify({
27694
- input_text: args.url,
27695
- max_file_size: args.maxFileSize ?? 50000,
27696
- pattern: args.pattern ?? "",
27697
- pattern_type: args.patternType ?? "exclude"
27698
- })
27699
- });
27700
- if (!response.ok) {
27701
- throw new Error(`gitingest API error: ${response.status} ${response.statusText}`);
27702
- }
27703
- const data = await response.json();
27704
- return `${data.summary}
27831
+ for (let attempt = 0;attempt <= GITINGEST_MAX_RETRIES; attempt++) {
27832
+ try {
27833
+ const controller = new AbortController;
27834
+ const timeoutId = setTimeout(() => controller.abort(), GITINGEST_TIMEOUT_MS);
27835
+ const response = await fetch("https://gitingest.com/api/ingest", {
27836
+ method: "POST",
27837
+ headers: { "Content-Type": "application/json" },
27838
+ body: JSON.stringify({
27839
+ input_text: args.url,
27840
+ max_file_size: args.maxFileSize ?? 50000,
27841
+ pattern: args.pattern ?? "",
27842
+ pattern_type: args.patternType ?? "exclude"
27843
+ }),
27844
+ signal: controller.signal
27845
+ });
27846
+ clearTimeout(timeoutId);
27847
+ if (response.status >= 500 && attempt < GITINGEST_MAX_RETRIES) {
27848
+ const backoff = 200 * 2 ** attempt;
27849
+ await delay(backoff);
27850
+ continue;
27851
+ }
27852
+ if (response.status >= 400 && response.status < 500) {
27853
+ throw new Error(`gitingest API error: ${response.status} ${response.statusText}`);
27854
+ }
27855
+ if (!response.ok) {
27856
+ throw new Error(`gitingest API error: ${response.status} ${response.statusText}`);
27857
+ }
27858
+ const contentLength = Number(response.headers.get("content-length"));
27859
+ if (Number.isFinite(contentLength) && contentLength > GITINGEST_MAX_RESPONSE_BYTES) {
27860
+ throw new Error("gitingest response too large");
27861
+ }
27862
+ const text = await response.text();
27863
+ if (Buffer.byteLength(text) > GITINGEST_MAX_RESPONSE_BYTES) {
27864
+ throw new Error("gitingest response too large");
27865
+ }
27866
+ const data = JSON.parse(text);
27867
+ return `${data.summary}
27705
27868
 
27706
27869
  ${data.tree}
27707
27870
 
27708
27871
  ${data.content}`;
27872
+ } catch (error93) {
27873
+ if (error93 instanceof DOMException && (error93.name === "TimeoutError" || error93.name === "AbortError")) {
27874
+ if (attempt >= GITINGEST_MAX_RETRIES) {
27875
+ throw new Error("gitingest request timed out");
27876
+ }
27877
+ const backoff = 200 * 2 ** attempt;
27878
+ await delay(backoff);
27879
+ continue;
27880
+ }
27881
+ if (error93 instanceof Error && error93.message.startsWith("gitingest ")) {
27882
+ throw error93;
27883
+ }
27884
+ if (attempt < GITINGEST_MAX_RETRIES) {
27885
+ const backoff = 200 * 2 ** attempt;
27886
+ await delay(backoff);
27887
+ continue;
27888
+ }
27889
+ throw error93;
27890
+ }
27891
+ }
27892
+ throw new Error("gitingest request failed after retries");
27709
27893
  }
27710
27894
  var gitingest = tool({
27711
27895
  description: "Fetch a GitHub repository's full content via gitingest.com. Returns summary, directory tree, and file contents optimized for LLM analysis. Use when you need to understand an external repository's structure or code.",
@@ -5,8 +5,11 @@ export interface GitingestArgs {
5
5
  pattern?: string;
6
6
  patternType?: 'include' | 'exclude';
7
7
  }
8
+ export declare const GITINGEST_TIMEOUT_MS = 10000;
9
+ export declare const GITINGEST_MAX_RESPONSE_BYTES = 5242880;
10
+ export declare const GITINGEST_MAX_RETRIES = 2;
8
11
  /**
9
- * Fetch repository content via gitingest.com API
12
+ * Fetch repository content via gitingest.com API with timeout, size guard, and retry logic
10
13
  */
11
14
  export declare function fetchGitingest(args: GitingestArgs): Promise<string>;
12
15
  /**
@@ -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.1",
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
  }