opencode-swarm 4.3.1 → 4.3.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.
- package/dist/config/loader.d.ts +2 -0
- package/dist/hooks/index.d.ts +1 -1
- package/dist/hooks/utils.d.ts +9 -0
- package/dist/index.js +122 -39
- package/dist/tools/gitingest.d.ts +4 -1
- package/package.json +1 -1
package/dist/config/loader.d.ts
CHANGED
package/dist/hooks/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/hooks/utils.d.ts
CHANGED
|
@@ -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
|
-
|
|
13628
|
-
|
|
13629
|
-
|
|
13630
|
-
|
|
13631
|
-
|
|
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] =
|
|
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,9 @@ function handleAgentsCommand(agents) {
|
|
|
14397
14410
|
`);
|
|
14398
14411
|
}
|
|
14399
14412
|
|
|
14413
|
+
// src/hooks/utils.ts
|
|
14414
|
+
import * as path2 from "path";
|
|
14415
|
+
|
|
14400
14416
|
// src/utils/logger.ts
|
|
14401
14417
|
var DEBUG = process.env.OPENCODE_SWARM_DEBUG === "1";
|
|
14402
14418
|
function log(message, data) {
|
|
@@ -14439,10 +14455,30 @@ function composeHandlers(...fns) {
|
|
|
14439
14455
|
}
|
|
14440
14456
|
};
|
|
14441
14457
|
}
|
|
14458
|
+
function validateSwarmPath(directory, filename) {
|
|
14459
|
+
if (/[\0]/.test(filename)) {
|
|
14460
|
+
throw new Error("Invalid filename: contains null bytes");
|
|
14461
|
+
}
|
|
14462
|
+
if (/\.\.[/\\]/.test(filename)) {
|
|
14463
|
+
throw new Error("Invalid filename: path traversal detected");
|
|
14464
|
+
}
|
|
14465
|
+
const baseDir = path2.normalize(path2.resolve(directory, ".swarm"));
|
|
14466
|
+
const resolved = path2.normalize(path2.resolve(baseDir, filename));
|
|
14467
|
+
if (process.platform === "win32") {
|
|
14468
|
+
if (!resolved.toLowerCase().startsWith((baseDir + path2.sep).toLowerCase())) {
|
|
14469
|
+
throw new Error("Invalid filename: path escapes .swarm directory");
|
|
14470
|
+
}
|
|
14471
|
+
} else {
|
|
14472
|
+
if (!resolved.startsWith(baseDir + path2.sep)) {
|
|
14473
|
+
throw new Error("Invalid filename: path escapes .swarm directory");
|
|
14474
|
+
}
|
|
14475
|
+
}
|
|
14476
|
+
return resolved;
|
|
14477
|
+
}
|
|
14442
14478
|
async function readSwarmFileAsync(directory, filename) {
|
|
14443
|
-
const path2 = `${directory}/.swarm/${filename}`;
|
|
14444
14479
|
try {
|
|
14445
|
-
const
|
|
14480
|
+
const resolvedPath = validateSwarmPath(directory, filename);
|
|
14481
|
+
const file2 = Bun.file(resolvedPath);
|
|
14446
14482
|
const content = await file2.text();
|
|
14447
14483
|
return content;
|
|
14448
14484
|
} catch {
|
|
@@ -14781,8 +14817,8 @@ async function doFlush(directory) {
|
|
|
14781
14817
|
const activitySection = renderActivitySection();
|
|
14782
14818
|
const updated = replaceOrAppendSection(existing, "## Agent Activity", activitySection);
|
|
14783
14819
|
const flushedCount = swarmState.pendingEvents;
|
|
14784
|
-
const
|
|
14785
|
-
await Bun.write(
|
|
14820
|
+
const path3 = `${directory}/.swarm/context.md`;
|
|
14821
|
+
await Bun.write(path3, updated);
|
|
14786
14822
|
swarmState.pendingEvents = Math.max(0, swarmState.pendingEvents - flushedCount);
|
|
14787
14823
|
} catch (error49) {
|
|
14788
14824
|
warn("Agent activity flush failed:", error49);
|
|
@@ -15801,10 +15837,10 @@ function mergeDefs2(...defs) {
|
|
|
15801
15837
|
function cloneDef2(schema) {
|
|
15802
15838
|
return mergeDefs2(schema._zod.def);
|
|
15803
15839
|
}
|
|
15804
|
-
function getElementAtPath2(obj,
|
|
15805
|
-
if (!
|
|
15840
|
+
function getElementAtPath2(obj, path3) {
|
|
15841
|
+
if (!path3)
|
|
15806
15842
|
return obj;
|
|
15807
|
-
return
|
|
15843
|
+
return path3.reduce((acc, key) => acc?.[key], obj);
|
|
15808
15844
|
}
|
|
15809
15845
|
function promiseAllObject2(promisesObj) {
|
|
15810
15846
|
const keys = Object.keys(promisesObj);
|
|
@@ -16163,11 +16199,11 @@ function aborted2(x, startIndex = 0) {
|
|
|
16163
16199
|
}
|
|
16164
16200
|
return false;
|
|
16165
16201
|
}
|
|
16166
|
-
function prefixIssues2(
|
|
16202
|
+
function prefixIssues2(path3, issues) {
|
|
16167
16203
|
return issues.map((iss) => {
|
|
16168
16204
|
var _a2;
|
|
16169
16205
|
(_a2 = iss).path ?? (_a2.path = []);
|
|
16170
|
-
iss.path.unshift(
|
|
16206
|
+
iss.path.unshift(path3);
|
|
16171
16207
|
return iss;
|
|
16172
16208
|
});
|
|
16173
16209
|
}
|
|
@@ -16335,7 +16371,7 @@ function treeifyError2(error49, _mapper) {
|
|
|
16335
16371
|
return issue3.message;
|
|
16336
16372
|
};
|
|
16337
16373
|
const result = { errors: [] };
|
|
16338
|
-
const processError = (error50,
|
|
16374
|
+
const processError = (error50, path3 = []) => {
|
|
16339
16375
|
var _a2, _b;
|
|
16340
16376
|
for (const issue3 of error50.issues) {
|
|
16341
16377
|
if (issue3.code === "invalid_union" && issue3.errors.length) {
|
|
@@ -16345,7 +16381,7 @@ function treeifyError2(error49, _mapper) {
|
|
|
16345
16381
|
} else if (issue3.code === "invalid_element") {
|
|
16346
16382
|
processError({ issues: issue3.issues }, issue3.path);
|
|
16347
16383
|
} else {
|
|
16348
|
-
const fullpath = [...
|
|
16384
|
+
const fullpath = [...path3, ...issue3.path];
|
|
16349
16385
|
if (fullpath.length === 0) {
|
|
16350
16386
|
result.errors.push(mapper(issue3));
|
|
16351
16387
|
continue;
|
|
@@ -16377,8 +16413,8 @@ function treeifyError2(error49, _mapper) {
|
|
|
16377
16413
|
}
|
|
16378
16414
|
function toDotPath2(_path) {
|
|
16379
16415
|
const segs = [];
|
|
16380
|
-
const
|
|
16381
|
-
for (const seg of
|
|
16416
|
+
const path3 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
|
|
16417
|
+
for (const seg of path3) {
|
|
16382
16418
|
if (typeof seg === "number")
|
|
16383
16419
|
segs.push(`[${seg}]`);
|
|
16384
16420
|
else if (typeof seg === "symbol")
|
|
@@ -27574,7 +27610,7 @@ Use these as DOMAIN values when delegating to @sme.`;
|
|
|
27574
27610
|
});
|
|
27575
27611
|
// src/tools/file-extractor.ts
|
|
27576
27612
|
import * as fs2 from "fs";
|
|
27577
|
-
import * as
|
|
27613
|
+
import * as path3 from "path";
|
|
27578
27614
|
var EXT_MAP = {
|
|
27579
27615
|
python: ".py",
|
|
27580
27616
|
py: ".py",
|
|
@@ -27652,12 +27688,12 @@ var extract_code_blocks = tool({
|
|
|
27652
27688
|
if (prefix) {
|
|
27653
27689
|
filename = `${prefix}_${filename}`;
|
|
27654
27690
|
}
|
|
27655
|
-
let filepath =
|
|
27656
|
-
const base =
|
|
27657
|
-
const ext =
|
|
27691
|
+
let filepath = path3.join(targetDir, filename);
|
|
27692
|
+
const base = path3.basename(filepath, path3.extname(filepath));
|
|
27693
|
+
const ext = path3.extname(filepath);
|
|
27658
27694
|
let counter = 1;
|
|
27659
27695
|
while (fs2.existsSync(filepath)) {
|
|
27660
|
-
filepath =
|
|
27696
|
+
filepath = path3.join(targetDir, `${base}_${counter}${ext}`);
|
|
27661
27697
|
counter++;
|
|
27662
27698
|
}
|
|
27663
27699
|
try {
|
|
@@ -27686,26 +27722,73 @@ Errors:
|
|
|
27686
27722
|
}
|
|
27687
27723
|
});
|
|
27688
27724
|
// src/tools/gitingest.ts
|
|
27725
|
+
var GITINGEST_TIMEOUT_MS = 1e4;
|
|
27726
|
+
var GITINGEST_MAX_RESPONSE_BYTES = 5242880;
|
|
27727
|
+
var GITINGEST_MAX_RETRIES = 2;
|
|
27728
|
+
var delay = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
27689
27729
|
async function fetchGitingest(args) {
|
|
27690
|
-
|
|
27691
|
-
|
|
27692
|
-
|
|
27693
|
-
|
|
27694
|
-
|
|
27695
|
-
|
|
27696
|
-
|
|
27697
|
-
|
|
27698
|
-
|
|
27699
|
-
|
|
27700
|
-
|
|
27701
|
-
|
|
27702
|
-
|
|
27703
|
-
|
|
27704
|
-
|
|
27730
|
+
for (let attempt = 0;attempt <= GITINGEST_MAX_RETRIES; attempt++) {
|
|
27731
|
+
try {
|
|
27732
|
+
const controller = new AbortController;
|
|
27733
|
+
const timeoutId = setTimeout(() => controller.abort(), GITINGEST_TIMEOUT_MS);
|
|
27734
|
+
const response = await fetch("https://gitingest.com/api/ingest", {
|
|
27735
|
+
method: "POST",
|
|
27736
|
+
headers: { "Content-Type": "application/json" },
|
|
27737
|
+
body: JSON.stringify({
|
|
27738
|
+
input_text: args.url,
|
|
27739
|
+
max_file_size: args.maxFileSize ?? 50000,
|
|
27740
|
+
pattern: args.pattern ?? "",
|
|
27741
|
+
pattern_type: args.patternType ?? "exclude"
|
|
27742
|
+
}),
|
|
27743
|
+
signal: controller.signal
|
|
27744
|
+
});
|
|
27745
|
+
clearTimeout(timeoutId);
|
|
27746
|
+
if (response.status >= 500 && attempt < GITINGEST_MAX_RETRIES) {
|
|
27747
|
+
const backoff = 200 * 2 ** attempt;
|
|
27748
|
+
await delay(backoff);
|
|
27749
|
+
continue;
|
|
27750
|
+
}
|
|
27751
|
+
if (response.status >= 400 && response.status < 500) {
|
|
27752
|
+
throw new Error(`gitingest API error: ${response.status} ${response.statusText}`);
|
|
27753
|
+
}
|
|
27754
|
+
if (!response.ok) {
|
|
27755
|
+
throw new Error(`gitingest API error: ${response.status} ${response.statusText}`);
|
|
27756
|
+
}
|
|
27757
|
+
const contentLength = Number(response.headers.get("content-length"));
|
|
27758
|
+
if (Number.isFinite(contentLength) && contentLength > GITINGEST_MAX_RESPONSE_BYTES) {
|
|
27759
|
+
throw new Error("gitingest response too large");
|
|
27760
|
+
}
|
|
27761
|
+
const text = await response.text();
|
|
27762
|
+
if (Buffer.byteLength(text) > GITINGEST_MAX_RESPONSE_BYTES) {
|
|
27763
|
+
throw new Error("gitingest response too large");
|
|
27764
|
+
}
|
|
27765
|
+
const data = JSON.parse(text);
|
|
27766
|
+
return `${data.summary}
|
|
27705
27767
|
|
|
27706
27768
|
${data.tree}
|
|
27707
27769
|
|
|
27708
27770
|
${data.content}`;
|
|
27771
|
+
} catch (error93) {
|
|
27772
|
+
if (error93 instanceof DOMException && (error93.name === "TimeoutError" || error93.name === "AbortError")) {
|
|
27773
|
+
if (attempt >= GITINGEST_MAX_RETRIES) {
|
|
27774
|
+
throw new Error("gitingest request timed out");
|
|
27775
|
+
}
|
|
27776
|
+
const backoff = 200 * 2 ** attempt;
|
|
27777
|
+
await delay(backoff);
|
|
27778
|
+
continue;
|
|
27779
|
+
}
|
|
27780
|
+
if (error93 instanceof Error && error93.message.startsWith("gitingest ")) {
|
|
27781
|
+
throw error93;
|
|
27782
|
+
}
|
|
27783
|
+
if (attempt < GITINGEST_MAX_RETRIES) {
|
|
27784
|
+
const backoff = 200 * 2 ** attempt;
|
|
27785
|
+
await delay(backoff);
|
|
27786
|
+
continue;
|
|
27787
|
+
}
|
|
27788
|
+
throw error93;
|
|
27789
|
+
}
|
|
27790
|
+
}
|
|
27791
|
+
throw new Error("gitingest request failed after retries");
|
|
27709
27792
|
}
|
|
27710
27793
|
var gitingest = tool({
|
|
27711
27794
|
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
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-swarm",
|
|
3
|
-
"version": "4.3.
|
|
3
|
+
"version": "4.3.2",
|
|
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",
|