opencode-swarm 6.2.0 → 6.3.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/README.md +306 -543
- package/dist/config/schema.d.ts +42 -0
- package/dist/index.js +1243 -10
- package/dist/tools/imports.d.ts +5 -0
- package/dist/tools/index.d.ts +3 -0
- package/dist/tools/lint.d.ts +34 -0
- package/dist/tools/secretscan.d.ts +31 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -13842,6 +13842,62 @@ var CompactionAdvisoryConfigSchema = exports_external.object({
|
|
|
13842
13842
|
thresholds: exports_external.array(exports_external.number().int().min(10).max(500)).default([50, 75, 100, 125, 150]),
|
|
13843
13843
|
message: exports_external.string().default("[SWARM HINT] Session has ${totalToolCalls} tool calls. Consider compacting at next phase boundary to maintain context quality.")
|
|
13844
13844
|
});
|
|
13845
|
+
var LintConfigSchema = exports_external.object({
|
|
13846
|
+
enabled: exports_external.boolean().default(true),
|
|
13847
|
+
mode: exports_external.enum(["check", "fix"]).default("check"),
|
|
13848
|
+
linter: exports_external.enum(["biome", "eslint", "auto"]).default("auto"),
|
|
13849
|
+
patterns: exports_external.array(exports_external.string()).default([
|
|
13850
|
+
"**/*.{ts,tsx,js,jsx,mjs,cjs}",
|
|
13851
|
+
"**/biome.json",
|
|
13852
|
+
"**/biome.jsonc"
|
|
13853
|
+
]),
|
|
13854
|
+
exclude: exports_external.array(exports_external.string()).default([
|
|
13855
|
+
"**/node_modules/**",
|
|
13856
|
+
"**/dist/**",
|
|
13857
|
+
"**/.git/**",
|
|
13858
|
+
"**/coverage/**",
|
|
13859
|
+
"**/*.min.js"
|
|
13860
|
+
])
|
|
13861
|
+
});
|
|
13862
|
+
var SecretscanConfigSchema = exports_external.object({
|
|
13863
|
+
enabled: exports_external.boolean().default(true),
|
|
13864
|
+
patterns: exports_external.array(exports_external.string()).default([
|
|
13865
|
+
"**/*.{env,properties,yml,yaml,json,js,ts}",
|
|
13866
|
+
"**/.env*",
|
|
13867
|
+
"**/secrets/**",
|
|
13868
|
+
"**/credentials/**",
|
|
13869
|
+
"**/config/**/*.ts",
|
|
13870
|
+
"**/config/**/*.js"
|
|
13871
|
+
]),
|
|
13872
|
+
exclude: exports_external.array(exports_external.string()).default([
|
|
13873
|
+
"**/node_modules/**",
|
|
13874
|
+
"**/dist/**",
|
|
13875
|
+
"**/.git/**",
|
|
13876
|
+
"**/coverage/**",
|
|
13877
|
+
"**/test/**",
|
|
13878
|
+
"**/tests/**",
|
|
13879
|
+
"**/__tests__/**",
|
|
13880
|
+
"**/*.test.ts",
|
|
13881
|
+
"**/*.test.js",
|
|
13882
|
+
"**/*.spec.ts",
|
|
13883
|
+
"**/*.spec.js"
|
|
13884
|
+
]),
|
|
13885
|
+
extensions: exports_external.array(exports_external.string()).default([
|
|
13886
|
+
".env",
|
|
13887
|
+
".properties",
|
|
13888
|
+
".yml",
|
|
13889
|
+
".yaml",
|
|
13890
|
+
".json",
|
|
13891
|
+
".js",
|
|
13892
|
+
".ts",
|
|
13893
|
+
".py",
|
|
13894
|
+
".rb",
|
|
13895
|
+
".go",
|
|
13896
|
+
".java",
|
|
13897
|
+
".cs",
|
|
13898
|
+
".php"
|
|
13899
|
+
])
|
|
13900
|
+
});
|
|
13845
13901
|
var GuardrailsProfileSchema = exports_external.object({
|
|
13846
13902
|
max_tool_calls: exports_external.number().min(0).max(1000).optional(),
|
|
13847
13903
|
max_duration_minutes: exports_external.number().min(0).max(480).optional(),
|
|
@@ -13956,7 +14012,9 @@ var PluginConfigSchema = exports_external.object({
|
|
|
13956
14012
|
integration_analysis: IntegrationAnalysisConfigSchema.optional(),
|
|
13957
14013
|
docs: DocsConfigSchema.optional(),
|
|
13958
14014
|
ui_review: UIReviewConfigSchema.optional(),
|
|
13959
|
-
compaction_advisory: CompactionAdvisoryConfigSchema.optional()
|
|
14015
|
+
compaction_advisory: CompactionAdvisoryConfigSchema.optional(),
|
|
14016
|
+
lint: LintConfigSchema.optional(),
|
|
14017
|
+
secretscan: SecretscanConfigSchema.optional()
|
|
13960
14018
|
});
|
|
13961
14019
|
|
|
13962
14020
|
// src/config/loader.ts
|
|
@@ -14127,7 +14185,7 @@ You THINK. Subagents DO. You have the largest context window and strongest reaso
|
|
|
14127
14185
|
- If NEEDS_REVISION: Revise plan and re-submit to critic (max 2 cycles)
|
|
14128
14186
|
- If REJECTED after 2 cycles: Escalate to user with explanation
|
|
14129
14187
|
- ONLY AFTER critic approval: Proceed to implementation (Phase 3+)
|
|
14130
|
-
7. **MANDATORY QA GATE (Execute AFTER every coder task)** \u2014 sequence: coder \u2192 diff \u2192
|
|
14188
|
+
7. **MANDATORY QA GATE (Execute AFTER every coder task)** \u2014 sequence: coder \u2192 diff \u2192 imports \u2192 lint fix \u2192 lint check \u2192 secretscan \u2192 (NO FINDINGS \u2192 proceed to reviewer) \u2192 reviewer \u2192 security review \u2192 verification tests \u2192 adversarial tests \u2192 next task.
|
|
14131
14189
|
- After coder completes: run \`diff\` tool. If \`hasContractChanges\` is true \u2192 delegate {{AGENT_PREFIX}}explorer for integration impact analysis. BREAKING \u2192 return to coder. COMPATIBLE \u2192 proceed.
|
|
14132
14190
|
- Delegate {{AGENT_PREFIX}}reviewer with CHECK dimensions. REJECTED \u2192 return to coder (max {{QA_RETRY_LIMIT}} attempts). APPROVED \u2192 continue.
|
|
14133
14191
|
- If file matches security globs (auth, api, crypto, security, middleware, session, token) OR coder output contains security keywords \u2192 delegate {{AGENT_PREFIX}}reviewer AGAIN with security-only CHECK. REJECTED \u2192 return to coder.
|
|
@@ -14154,7 +14212,7 @@ You THINK. Subagents DO. You have the largest context window and strongest reaso
|
|
|
14154
14212
|
|
|
14155
14213
|
SMEs advise only. Reviewer and critic review only. None of them write code.
|
|
14156
14214
|
|
|
14157
|
-
Available Tools: diff (structured git diff with contract change detection)
|
|
14215
|
+
Available Tools: diff (structured git diff with contract change detection), imports (dependency audit), lint (code quality), secretscan (secret detection)
|
|
14158
14216
|
|
|
14159
14217
|
## DELEGATION FORMAT
|
|
14160
14218
|
|
|
@@ -14300,12 +14358,15 @@ For each task (respecting dependencies):
|
|
|
14300
14358
|
5a. **UI DESIGN GATE** (conditional \u2014 Rule 9): If task matches UI trigger \u2192 {{AGENT_PREFIX}}designer produces scaffold \u2192 pass scaffold to coder as INPUT. If no match \u2192 skip.
|
|
14301
14359
|
5b. {{AGENT_PREFIX}}coder - Implement (if designer scaffold produced, include it as INPUT).
|
|
14302
14360
|
5c. Run \`diff\` tool. If \`hasContractChanges\` \u2192 {{AGENT_PREFIX}}explorer integration analysis. BREAKING \u2192 coder retry.
|
|
14303
|
-
5d.
|
|
14304
|
-
5e.
|
|
14305
|
-
5f.
|
|
14306
|
-
5g. {{AGENT_PREFIX}}
|
|
14307
|
-
5h.
|
|
14308
|
-
5i.
|
|
14361
|
+
5d. Run \`imports\` tool for dependency audit. ISSUES \u2192 return to coder.
|
|
14362
|
+
5e. Run \`lint\` tool with fix mode for auto-fixes. If issues remain \u2192 run \`lint\` tool with check mode. FAIL \u2192 return to coder.
|
|
14363
|
+
5f. Run \`secretscan\` tool. FINDINGS \u2192 return to coder. NO FINDINGS \u2192 proceed to reviewer.
|
|
14364
|
+
5g. {{AGENT_PREFIX}}reviewer - General review. REJECTED (< {{QA_RETRY_LIMIT}}) \u2192 coder retry. REJECTED ({{QA_RETRY_LIMIT}}) \u2192 escalate.
|
|
14365
|
+
5h. Security gate: if file matches security globs OR content has security keywords OR secretscan has ANY findings \u2192 {{AGENT_PREFIX}}reviewer security-only. REJECTED \u2192 coder retry.
|
|
14366
|
+
5i. {{AGENT_PREFIX}}test_engineer - Verification tests. FAIL \u2192 coder retry from 5g.
|
|
14367
|
+
5j. {{AGENT_PREFIX}}test_engineer - Adversarial tests. FAIL \u2192 coder retry from 5g.
|
|
14368
|
+
5k. COVERAGE CHECK: If test_engineer reports coverage < 70% \u2192 delegate {{AGENT_PREFIX}}test_engineer for an additional test pass targeting uncovered paths. This is a soft guideline; use judgment for trivial tasks.
|
|
14369
|
+
5l. Update plan.md [x], proceed to next task.
|
|
14309
14370
|
|
|
14310
14371
|
### Phase 6: Phase Complete
|
|
14311
14372
|
1. {{AGENT_PREFIX}}explorer - Rescan
|
|
@@ -17653,6 +17714,12 @@ function createSystemEnhancerHook(config2, directory) {
|
|
|
17653
17714
|
if (config2.docs?.enabled === false) {
|
|
17654
17715
|
tryInject("[SWARM CONFIG] Docs agent is DISABLED. Skip docs delegation in Phase 6.");
|
|
17655
17716
|
}
|
|
17717
|
+
if (config2.lint?.enabled === false) {
|
|
17718
|
+
tryInject("[SWARM CONFIG] Lint gate is DISABLED. Skip lint check/fix in QA sequence.");
|
|
17719
|
+
}
|
|
17720
|
+
if (config2.secretscan?.enabled === false) {
|
|
17721
|
+
tryInject("[SWARM CONFIG] Secretscan gate is DISABLED. Skip secretscan in QA sequence.");
|
|
17722
|
+
}
|
|
17656
17723
|
const sessionId_retro = _input.sessionID;
|
|
17657
17724
|
const activeAgent_retro = swarmState.activeAgent.get(sessionId_retro ?? "");
|
|
17658
17725
|
const isArchitect = !activeAgent_retro || stripKnownSwarmPrefix(activeAgent_retro) === "architect";
|
|
@@ -17836,6 +17903,28 @@ function createSystemEnhancerHook(config2, directory) {
|
|
|
17836
17903
|
metadata: { contentType: "prose" }
|
|
17837
17904
|
});
|
|
17838
17905
|
}
|
|
17906
|
+
if (config2.lint?.enabled === false) {
|
|
17907
|
+
const text = "[SWARM CONFIG] Lint gate is DISABLED. Skip lint check/fix in QA sequence.";
|
|
17908
|
+
candidates.push({
|
|
17909
|
+
id: `candidate-${idCounter++}`,
|
|
17910
|
+
kind: "phase",
|
|
17911
|
+
text,
|
|
17912
|
+
tokens: estimateTokens(text),
|
|
17913
|
+
priority: 1,
|
|
17914
|
+
metadata: { contentType: "prose" }
|
|
17915
|
+
});
|
|
17916
|
+
}
|
|
17917
|
+
if (config2.secretscan?.enabled === false) {
|
|
17918
|
+
const text = "[SWARM CONFIG] Secretscan gate is DISABLED. Skip secretscan in QA sequence.";
|
|
17919
|
+
candidates.push({
|
|
17920
|
+
id: `candidate-${idCounter++}`,
|
|
17921
|
+
kind: "phase",
|
|
17922
|
+
text,
|
|
17923
|
+
tokens: estimateTokens(text),
|
|
17924
|
+
priority: 1,
|
|
17925
|
+
metadata: { contentType: "prose" }
|
|
17926
|
+
});
|
|
17927
|
+
}
|
|
17839
17928
|
const sessionId_retro_b = _input.sessionID;
|
|
17840
17929
|
const activeAgent_retro_b = swarmState.activeAgent.get(sessionId_retro_b ?? "");
|
|
17841
17930
|
const isArchitect_b = !activeAgent_retro_b || stripKnownSwarmPrefix(activeAgent_retro_b) === "architect";
|
|
@@ -30940,6 +31029,510 @@ var gitingest = tool({
|
|
|
30940
31029
|
return fetchGitingest(args);
|
|
30941
31030
|
}
|
|
30942
31031
|
});
|
|
31032
|
+
// src/tools/imports.ts
|
|
31033
|
+
import * as fs5 from "fs";
|
|
31034
|
+
import * as path9 from "path";
|
|
31035
|
+
var MAX_FILE_PATH_LENGTH = 500;
|
|
31036
|
+
var MAX_SYMBOL_LENGTH = 256;
|
|
31037
|
+
var MAX_FILE_SIZE_BYTES = 1024 * 1024;
|
|
31038
|
+
var MAX_CONSUMERS = 100;
|
|
31039
|
+
var SUPPORTED_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
|
|
31040
|
+
var BINARY_SIGNATURES = [
|
|
31041
|
+
0,
|
|
31042
|
+
2303741511,
|
|
31043
|
+
4292411360,
|
|
31044
|
+
1195984440,
|
|
31045
|
+
626017350,
|
|
31046
|
+
1347093252
|
|
31047
|
+
];
|
|
31048
|
+
var BINARY_PREFIX_BYTES = 4;
|
|
31049
|
+
var BINARY_NULL_CHECK_BYTES = 8192;
|
|
31050
|
+
var BINARY_NULL_THRESHOLD = 0.1;
|
|
31051
|
+
function containsPathTraversal(str) {
|
|
31052
|
+
return /\.\.[/\\]/.test(str);
|
|
31053
|
+
}
|
|
31054
|
+
function containsControlChars(str) {
|
|
31055
|
+
return /[\0\t\r\n]/.test(str);
|
|
31056
|
+
}
|
|
31057
|
+
function validateFileInput(file3) {
|
|
31058
|
+
if (!file3 || file3.length === 0) {
|
|
31059
|
+
return "file is required";
|
|
31060
|
+
}
|
|
31061
|
+
if (file3.length > MAX_FILE_PATH_LENGTH) {
|
|
31062
|
+
return `file exceeds maximum length of ${MAX_FILE_PATH_LENGTH}`;
|
|
31063
|
+
}
|
|
31064
|
+
if (containsControlChars(file3)) {
|
|
31065
|
+
return "file contains control characters";
|
|
31066
|
+
}
|
|
31067
|
+
if (containsPathTraversal(file3)) {
|
|
31068
|
+
return "file contains path traversal";
|
|
31069
|
+
}
|
|
31070
|
+
return null;
|
|
31071
|
+
}
|
|
31072
|
+
function validateSymbolInput(symbol3) {
|
|
31073
|
+
if (symbol3 === undefined || symbol3 === "") {
|
|
31074
|
+
return null;
|
|
31075
|
+
}
|
|
31076
|
+
if (symbol3.length > MAX_SYMBOL_LENGTH) {
|
|
31077
|
+
return `symbol exceeds maximum length of ${MAX_SYMBOL_LENGTH}`;
|
|
31078
|
+
}
|
|
31079
|
+
if (containsControlChars(symbol3)) {
|
|
31080
|
+
return "symbol contains control characters";
|
|
31081
|
+
}
|
|
31082
|
+
if (containsPathTraversal(symbol3)) {
|
|
31083
|
+
return "symbol contains path traversal";
|
|
31084
|
+
}
|
|
31085
|
+
return null;
|
|
31086
|
+
}
|
|
31087
|
+
function isBinaryFile(filePath, buffer) {
|
|
31088
|
+
const ext = path9.extname(filePath).toLowerCase();
|
|
31089
|
+
if (ext === ".json" || ext === ".md" || ext === ".txt") {
|
|
31090
|
+
return false;
|
|
31091
|
+
}
|
|
31092
|
+
if (buffer.length >= BINARY_PREFIX_BYTES) {
|
|
31093
|
+
const prefix = buffer.subarray(0, BINARY_PREFIX_BYTES);
|
|
31094
|
+
const uint323 = prefix.readUInt32BE(0);
|
|
31095
|
+
for (const sig of BINARY_SIGNATURES) {
|
|
31096
|
+
if (uint323 === sig)
|
|
31097
|
+
return true;
|
|
31098
|
+
}
|
|
31099
|
+
}
|
|
31100
|
+
let nullCount = 0;
|
|
31101
|
+
const checkLen = Math.min(buffer.length, BINARY_NULL_CHECK_BYTES);
|
|
31102
|
+
for (let i = 0;i < checkLen; i++) {
|
|
31103
|
+
if (buffer[i] === 0)
|
|
31104
|
+
nullCount++;
|
|
31105
|
+
}
|
|
31106
|
+
return nullCount > checkLen * BINARY_NULL_THRESHOLD;
|
|
31107
|
+
}
|
|
31108
|
+
function parseImports(content, targetFile, targetSymbol) {
|
|
31109
|
+
const imports = [];
|
|
31110
|
+
let resolvedTarget;
|
|
31111
|
+
try {
|
|
31112
|
+
resolvedTarget = path9.resolve(targetFile);
|
|
31113
|
+
} catch {
|
|
31114
|
+
resolvedTarget = targetFile;
|
|
31115
|
+
}
|
|
31116
|
+
const targetBasename = path9.basename(targetFile, path9.extname(targetFile));
|
|
31117
|
+
const targetWithExt = targetFile;
|
|
31118
|
+
const targetWithoutExt = targetFile.replace(/\.(ts|tsx|js|jsx|mjs|cjs)$/i, "");
|
|
31119
|
+
const normalizedTargetWithExt = path9.normalize(targetWithExt).replace(/\\/g, "/");
|
|
31120
|
+
const normalizedTargetWithoutExt = path9.normalize(targetWithoutExt).replace(/\\/g, "/");
|
|
31121
|
+
const importRegex = /import\s+(?:\{[\s\S]*?\}|(?:\*\s+as\s+\w+)|\w+)\s+from\s+['"`]([^'"`]+)['"`]|import\s+['"`]([^'"`]+)['"`]|require\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g;
|
|
31122
|
+
let match;
|
|
31123
|
+
while ((match = importRegex.exec(content)) !== null) {
|
|
31124
|
+
const modulePath = match[1] || match[2] || match[3];
|
|
31125
|
+
if (!modulePath)
|
|
31126
|
+
continue;
|
|
31127
|
+
const beforeMatch = content.substring(0, match.index);
|
|
31128
|
+
const lineNum = (beforeMatch.match(/\n/g) || []).length + 1;
|
|
31129
|
+
const matchedString = match[0];
|
|
31130
|
+
let importType = "named";
|
|
31131
|
+
if (matchedString.includes("* as")) {
|
|
31132
|
+
importType = "namespace";
|
|
31133
|
+
} else if (/^import\s+\{/.test(matchedString)) {
|
|
31134
|
+
importType = "named";
|
|
31135
|
+
} else if (/^import\s+\w+\s+from\s+['"`]/.test(matchedString)) {
|
|
31136
|
+
importType = "default";
|
|
31137
|
+
} else if (/^import\s+['"`]/m.test(matchedString)) {
|
|
31138
|
+
importType = "sideeffect";
|
|
31139
|
+
} else if (matchedString.includes("require(")) {
|
|
31140
|
+
importType = "require";
|
|
31141
|
+
}
|
|
31142
|
+
const normalizedModule = modulePath.replace(/^\.\//, "").replace(/^\.\.\\/, "../");
|
|
31143
|
+
let isMatch = false;
|
|
31144
|
+
const targetDir = path9.dirname(targetFile);
|
|
31145
|
+
const targetExt = path9.extname(targetFile);
|
|
31146
|
+
const targetBasenameNoExt = path9.basename(targetFile, targetExt);
|
|
31147
|
+
const moduleNormalized = modulePath.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
31148
|
+
const moduleName = modulePath.split(/[/\\]/).pop() || "";
|
|
31149
|
+
const moduleNameNoExt = moduleName.replace(/\.(ts|tsx|js|jsx|mjs|cjs)$/i, "");
|
|
31150
|
+
if (modulePath === targetBasename || modulePath === targetBasenameNoExt || modulePath === "./" + targetBasename || modulePath === "./" + targetBasenameNoExt || modulePath === "../" + targetBasename || modulePath === "../" + targetBasenameNoExt || moduleNormalized === normalizedTargetWithExt || moduleNormalized === normalizedTargetWithoutExt || modulePath.endsWith("/" + targetBasename) || modulePath.endsWith("\\" + targetBasename) || modulePath.endsWith("/" + targetBasenameNoExt) || modulePath.endsWith("\\" + targetBasenameNoExt) || moduleNameNoExt === targetBasenameNoExt || "./" + moduleNameNoExt === targetBasenameNoExt || moduleName === targetBasename || moduleName === targetBasenameNoExt) {
|
|
31151
|
+
isMatch = true;
|
|
31152
|
+
}
|
|
31153
|
+
if (isMatch && targetSymbol) {
|
|
31154
|
+
if (importType === "namespace" || importType === "sideeffect") {
|
|
31155
|
+
isMatch = false;
|
|
31156
|
+
} else {
|
|
31157
|
+
const namedMatch = matchedString.match(/import\s+\{([\s\S]*?)\}\s+from/);
|
|
31158
|
+
if (namedMatch) {
|
|
31159
|
+
const importedNames = namedMatch[1].split(",").map((s) => {
|
|
31160
|
+
const parts = s.trim().split(/\s+as\s+/i);
|
|
31161
|
+
const originalName = parts[0].trim();
|
|
31162
|
+
const aliasName = parts[1]?.trim();
|
|
31163
|
+
return { original: originalName, alias: aliasName };
|
|
31164
|
+
});
|
|
31165
|
+
isMatch = importedNames.some(({ original, alias }) => original === targetSymbol || alias === targetSymbol);
|
|
31166
|
+
} else if (importType === "default") {
|
|
31167
|
+
const defaultMatch = matchedString.match(/^import\s+(\w+)\s+from/m);
|
|
31168
|
+
if (defaultMatch) {
|
|
31169
|
+
const defaultName = defaultMatch[1];
|
|
31170
|
+
isMatch = defaultName === targetSymbol;
|
|
31171
|
+
}
|
|
31172
|
+
}
|
|
31173
|
+
}
|
|
31174
|
+
}
|
|
31175
|
+
if (isMatch) {
|
|
31176
|
+
imports.push({
|
|
31177
|
+
line: lineNum,
|
|
31178
|
+
imports: modulePath,
|
|
31179
|
+
importType,
|
|
31180
|
+
raw: matchedString.trim()
|
|
31181
|
+
});
|
|
31182
|
+
}
|
|
31183
|
+
}
|
|
31184
|
+
return imports;
|
|
31185
|
+
}
|
|
31186
|
+
var SKIP_DIRECTORIES = new Set([
|
|
31187
|
+
"node_modules",
|
|
31188
|
+
".git",
|
|
31189
|
+
"dist",
|
|
31190
|
+
"build",
|
|
31191
|
+
"out",
|
|
31192
|
+
"coverage",
|
|
31193
|
+
".next",
|
|
31194
|
+
".nuxt",
|
|
31195
|
+
".cache",
|
|
31196
|
+
"vendor",
|
|
31197
|
+
".svn",
|
|
31198
|
+
".hg"
|
|
31199
|
+
]);
|
|
31200
|
+
function findSourceFiles(dir, files = [], stats = { skippedDirs: [], skippedFiles: 0, fileErrors: [] }) {
|
|
31201
|
+
let entries;
|
|
31202
|
+
try {
|
|
31203
|
+
entries = fs5.readdirSync(dir);
|
|
31204
|
+
} catch (e) {
|
|
31205
|
+
stats.fileErrors.push({
|
|
31206
|
+
path: dir,
|
|
31207
|
+
reason: e instanceof Error ? e.message : "readdir failed"
|
|
31208
|
+
});
|
|
31209
|
+
return files;
|
|
31210
|
+
}
|
|
31211
|
+
entries.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
|
|
31212
|
+
for (const entry of entries) {
|
|
31213
|
+
if (SKIP_DIRECTORIES.has(entry)) {
|
|
31214
|
+
stats.skippedDirs.push(path9.join(dir, entry));
|
|
31215
|
+
continue;
|
|
31216
|
+
}
|
|
31217
|
+
const fullPath = path9.join(dir, entry);
|
|
31218
|
+
let stat;
|
|
31219
|
+
try {
|
|
31220
|
+
stat = fs5.statSync(fullPath);
|
|
31221
|
+
} catch (e) {
|
|
31222
|
+
stats.fileErrors.push({
|
|
31223
|
+
path: fullPath,
|
|
31224
|
+
reason: e instanceof Error ? e.message : "stat failed"
|
|
31225
|
+
});
|
|
31226
|
+
continue;
|
|
31227
|
+
}
|
|
31228
|
+
if (stat.isDirectory()) {
|
|
31229
|
+
findSourceFiles(fullPath, files, stats);
|
|
31230
|
+
} else if (stat.isFile()) {
|
|
31231
|
+
const ext = path9.extname(fullPath).toLowerCase();
|
|
31232
|
+
if (SUPPORTED_EXTENSIONS.includes(ext)) {
|
|
31233
|
+
files.push(fullPath);
|
|
31234
|
+
}
|
|
31235
|
+
}
|
|
31236
|
+
}
|
|
31237
|
+
return files;
|
|
31238
|
+
}
|
|
31239
|
+
var imports = tool({
|
|
31240
|
+
description: "Find all consumers that import from a given file. Returns JSON with file path, line numbers, and import metadata for each consumer. Useful for understanding dependency relationships.",
|
|
31241
|
+
args: {
|
|
31242
|
+
file: tool.schema.string().describe('Source file path to find importers for (e.g., "./src/utils.ts")'),
|
|
31243
|
+
symbol: tool.schema.string().optional().describe('Optional specific symbol to filter imports (e.g., "MyClass")')
|
|
31244
|
+
},
|
|
31245
|
+
async execute(args, _context) {
|
|
31246
|
+
let file3;
|
|
31247
|
+
let symbol3;
|
|
31248
|
+
try {
|
|
31249
|
+
if (args && typeof args === "object") {
|
|
31250
|
+
file3 = args.file;
|
|
31251
|
+
symbol3 = args.symbol;
|
|
31252
|
+
}
|
|
31253
|
+
} catch {}
|
|
31254
|
+
if (file3 === undefined) {
|
|
31255
|
+
const errorResult = {
|
|
31256
|
+
error: "invalid arguments: file is required",
|
|
31257
|
+
target: "",
|
|
31258
|
+
symbol: undefined,
|
|
31259
|
+
consumers: [],
|
|
31260
|
+
count: 0
|
|
31261
|
+
};
|
|
31262
|
+
return JSON.stringify(errorResult, null, 2);
|
|
31263
|
+
}
|
|
31264
|
+
const fileValidationError = validateFileInput(file3);
|
|
31265
|
+
if (fileValidationError) {
|
|
31266
|
+
const errorResult = {
|
|
31267
|
+
error: `invalid file: ${fileValidationError}`,
|
|
31268
|
+
target: file3,
|
|
31269
|
+
symbol: symbol3,
|
|
31270
|
+
consumers: [],
|
|
31271
|
+
count: 0
|
|
31272
|
+
};
|
|
31273
|
+
return JSON.stringify(errorResult, null, 2);
|
|
31274
|
+
}
|
|
31275
|
+
const symbolValidationError = validateSymbolInput(symbol3);
|
|
31276
|
+
if (symbolValidationError) {
|
|
31277
|
+
const errorResult = {
|
|
31278
|
+
error: `invalid symbol: ${symbolValidationError}`,
|
|
31279
|
+
target: file3,
|
|
31280
|
+
symbol: symbol3,
|
|
31281
|
+
consumers: [],
|
|
31282
|
+
count: 0
|
|
31283
|
+
};
|
|
31284
|
+
return JSON.stringify(errorResult, null, 2);
|
|
31285
|
+
}
|
|
31286
|
+
try {
|
|
31287
|
+
const targetFile = path9.resolve(file3);
|
|
31288
|
+
if (!fs5.existsSync(targetFile)) {
|
|
31289
|
+
const errorResult = {
|
|
31290
|
+
error: `target file not found: ${file3}`,
|
|
31291
|
+
target: file3,
|
|
31292
|
+
symbol: symbol3,
|
|
31293
|
+
consumers: [],
|
|
31294
|
+
count: 0
|
|
31295
|
+
};
|
|
31296
|
+
return JSON.stringify(errorResult, null, 2);
|
|
31297
|
+
}
|
|
31298
|
+
const targetStat = fs5.statSync(targetFile);
|
|
31299
|
+
if (!targetStat.isFile()) {
|
|
31300
|
+
const errorResult = {
|
|
31301
|
+
error: "target must be a file, not a directory",
|
|
31302
|
+
target: file3,
|
|
31303
|
+
symbol: symbol3,
|
|
31304
|
+
consumers: [],
|
|
31305
|
+
count: 0
|
|
31306
|
+
};
|
|
31307
|
+
return JSON.stringify(errorResult, null, 2);
|
|
31308
|
+
}
|
|
31309
|
+
const baseDir = path9.dirname(targetFile);
|
|
31310
|
+
const scanStats = {
|
|
31311
|
+
skippedDirs: [],
|
|
31312
|
+
skippedFiles: 0,
|
|
31313
|
+
fileErrors: []
|
|
31314
|
+
};
|
|
31315
|
+
const sourceFiles = findSourceFiles(baseDir, [], scanStats);
|
|
31316
|
+
const filesToScan = sourceFiles.filter((f) => f !== targetFile).sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase())).slice(0, MAX_CONSUMERS * 10);
|
|
31317
|
+
const consumers = [];
|
|
31318
|
+
let skippedFileCount = 0;
|
|
31319
|
+
let totalMatchesFound = 0;
|
|
31320
|
+
for (const filePath of filesToScan) {
|
|
31321
|
+
if (consumers.length >= MAX_CONSUMERS)
|
|
31322
|
+
break;
|
|
31323
|
+
try {
|
|
31324
|
+
const stat = fs5.statSync(filePath);
|
|
31325
|
+
if (stat.size > MAX_FILE_SIZE_BYTES) {
|
|
31326
|
+
skippedFileCount++;
|
|
31327
|
+
continue;
|
|
31328
|
+
}
|
|
31329
|
+
const buffer = fs5.readFileSync(filePath);
|
|
31330
|
+
if (isBinaryFile(filePath, buffer)) {
|
|
31331
|
+
skippedFileCount++;
|
|
31332
|
+
continue;
|
|
31333
|
+
}
|
|
31334
|
+
const content = buffer.toString("utf-8");
|
|
31335
|
+
const fileImports = parseImports(content, targetFile, symbol3);
|
|
31336
|
+
for (const imp of fileImports) {
|
|
31337
|
+
totalMatchesFound++;
|
|
31338
|
+
if (consumers.length >= MAX_CONSUMERS)
|
|
31339
|
+
continue;
|
|
31340
|
+
const exists = consumers.some((c) => c.file === filePath && c.line === imp.line);
|
|
31341
|
+
if (exists)
|
|
31342
|
+
continue;
|
|
31343
|
+
consumers.push({
|
|
31344
|
+
file: filePath,
|
|
31345
|
+
line: imp.line,
|
|
31346
|
+
imports: imp.imports,
|
|
31347
|
+
importType: imp.importType,
|
|
31348
|
+
raw: imp.raw
|
|
31349
|
+
});
|
|
31350
|
+
}
|
|
31351
|
+
} catch (e) {
|
|
31352
|
+
skippedFileCount++;
|
|
31353
|
+
}
|
|
31354
|
+
}
|
|
31355
|
+
const result = {
|
|
31356
|
+
target: file3,
|
|
31357
|
+
symbol: symbol3,
|
|
31358
|
+
consumers,
|
|
31359
|
+
count: consumers.length
|
|
31360
|
+
};
|
|
31361
|
+
const parts = [];
|
|
31362
|
+
if (filesToScan.length >= MAX_CONSUMERS * 10) {
|
|
31363
|
+
parts.push(`Scanned ${filesToScan.length} files`);
|
|
31364
|
+
}
|
|
31365
|
+
if (skippedFileCount > 0) {
|
|
31366
|
+
parts.push(`${skippedFileCount} skipped (size/binary/errors)`);
|
|
31367
|
+
}
|
|
31368
|
+
if (consumers.length >= MAX_CONSUMERS) {
|
|
31369
|
+
const hidden = totalMatchesFound - consumers.length;
|
|
31370
|
+
if (hidden > 0) {
|
|
31371
|
+
parts.push(`Results limited to ${MAX_CONSUMERS} consumers (${hidden} hidden)`);
|
|
31372
|
+
} else {
|
|
31373
|
+
parts.push(`Results limited to ${MAX_CONSUMERS} consumers`);
|
|
31374
|
+
}
|
|
31375
|
+
}
|
|
31376
|
+
if (parts.length > 0) {
|
|
31377
|
+
result.message = parts.join("; ") + ".";
|
|
31378
|
+
}
|
|
31379
|
+
return JSON.stringify(result, null, 2);
|
|
31380
|
+
} catch (e) {
|
|
31381
|
+
const errorResult = {
|
|
31382
|
+
error: e instanceof Error ? `scan failed: ${e.message || "internal error"}` : "scan failed: unknown error",
|
|
31383
|
+
target: file3,
|
|
31384
|
+
symbol: symbol3,
|
|
31385
|
+
consumers: [],
|
|
31386
|
+
count: 0
|
|
31387
|
+
};
|
|
31388
|
+
return JSON.stringify(errorResult, null, 2);
|
|
31389
|
+
}
|
|
31390
|
+
}
|
|
31391
|
+
});
|
|
31392
|
+
// src/tools/lint.ts
|
|
31393
|
+
var MAX_OUTPUT_BYTES = 512000;
|
|
31394
|
+
var MAX_COMMAND_LENGTH = 500;
|
|
31395
|
+
function validateArgs(args) {
|
|
31396
|
+
if (typeof args !== "object" || args === null)
|
|
31397
|
+
return false;
|
|
31398
|
+
const obj = args;
|
|
31399
|
+
if (obj.mode !== "fix" && obj.mode !== "check")
|
|
31400
|
+
return false;
|
|
31401
|
+
return true;
|
|
31402
|
+
}
|
|
31403
|
+
function getLinterCommand(linter, mode) {
|
|
31404
|
+
const isWindows = process.platform === "win32";
|
|
31405
|
+
switch (linter) {
|
|
31406
|
+
case "biome":
|
|
31407
|
+
if (mode === "fix") {
|
|
31408
|
+
return isWindows ? ["npx", "biome", "check", "--write", "."] : ["npx", "biome", "check", "--write", "."];
|
|
31409
|
+
}
|
|
31410
|
+
return isWindows ? ["npx", "biome", "check", "."] : ["npx", "biome", "check", "."];
|
|
31411
|
+
case "eslint":
|
|
31412
|
+
if (mode === "fix") {
|
|
31413
|
+
return isWindows ? ["npx", "eslint", ".", "--fix"] : ["npx", "eslint", ".", "--fix"];
|
|
31414
|
+
}
|
|
31415
|
+
return isWindows ? ["npx", "eslint", "."] : ["npx", "eslint", "."];
|
|
31416
|
+
}
|
|
31417
|
+
}
|
|
31418
|
+
async function detectAvailableLinter() {
|
|
31419
|
+
const DETECT_TIMEOUT = 2000;
|
|
31420
|
+
try {
|
|
31421
|
+
const biomeProc = Bun.spawn(["npx", "biome", "--version"], {
|
|
31422
|
+
stdout: "pipe",
|
|
31423
|
+
stderr: "pipe"
|
|
31424
|
+
});
|
|
31425
|
+
const biomeExit = biomeProc.exited;
|
|
31426
|
+
const timeout = new Promise((resolve4) => setTimeout(() => resolve4("timeout"), DETECT_TIMEOUT));
|
|
31427
|
+
const result = await Promise.race([biomeExit, timeout]);
|
|
31428
|
+
if (result === "timeout") {
|
|
31429
|
+
biomeProc.kill();
|
|
31430
|
+
} else if (biomeProc.exitCode === 0) {
|
|
31431
|
+
return "biome";
|
|
31432
|
+
}
|
|
31433
|
+
} catch {}
|
|
31434
|
+
try {
|
|
31435
|
+
const eslintProc = Bun.spawn(["npx", "eslint", "--version"], {
|
|
31436
|
+
stdout: "pipe",
|
|
31437
|
+
stderr: "pipe"
|
|
31438
|
+
});
|
|
31439
|
+
const eslintExit = eslintProc.exited;
|
|
31440
|
+
const timeout = new Promise((resolve4) => setTimeout(() => resolve4("timeout"), DETECT_TIMEOUT));
|
|
31441
|
+
const result = await Promise.race([eslintExit, timeout]);
|
|
31442
|
+
if (result === "timeout") {
|
|
31443
|
+
eslintProc.kill();
|
|
31444
|
+
} else if (eslintProc.exitCode === 0) {
|
|
31445
|
+
return "eslint";
|
|
31446
|
+
}
|
|
31447
|
+
} catch {}
|
|
31448
|
+
return null;
|
|
31449
|
+
}
|
|
31450
|
+
async function runLint(linter, mode) {
|
|
31451
|
+
const command = getLinterCommand(linter, mode);
|
|
31452
|
+
const commandStr = command.join(" ");
|
|
31453
|
+
if (commandStr.length > MAX_COMMAND_LENGTH) {
|
|
31454
|
+
return {
|
|
31455
|
+
success: false,
|
|
31456
|
+
mode,
|
|
31457
|
+
linter,
|
|
31458
|
+
command,
|
|
31459
|
+
error: "Command exceeds maximum allowed length"
|
|
31460
|
+
};
|
|
31461
|
+
}
|
|
31462
|
+
try {
|
|
31463
|
+
const proc = Bun.spawn(command, {
|
|
31464
|
+
stdout: "pipe",
|
|
31465
|
+
stderr: "pipe"
|
|
31466
|
+
});
|
|
31467
|
+
const [stdout, stderr] = await Promise.all([
|
|
31468
|
+
new Response(proc.stdout).text(),
|
|
31469
|
+
new Response(proc.stderr).text()
|
|
31470
|
+
]);
|
|
31471
|
+
const exitCode = await proc.exited;
|
|
31472
|
+
let output = stdout;
|
|
31473
|
+
if (stderr) {
|
|
31474
|
+
output += (output ? `
|
|
31475
|
+
` : "") + stderr;
|
|
31476
|
+
}
|
|
31477
|
+
if (output.length > MAX_OUTPUT_BYTES) {
|
|
31478
|
+
output = output.slice(0, MAX_OUTPUT_BYTES) + `
|
|
31479
|
+
... (output truncated)`;
|
|
31480
|
+
}
|
|
31481
|
+
const result = {
|
|
31482
|
+
success: true,
|
|
31483
|
+
mode,
|
|
31484
|
+
linter,
|
|
31485
|
+
command,
|
|
31486
|
+
exitCode,
|
|
31487
|
+
output
|
|
31488
|
+
};
|
|
31489
|
+
if (exitCode === 0) {
|
|
31490
|
+
result.message = `${linter} ${mode} completed successfully with no issues`;
|
|
31491
|
+
} else if (mode === "fix") {
|
|
31492
|
+
result.message = `${linter} fix completed with exit code ${exitCode}. Run check mode to see remaining issues.`;
|
|
31493
|
+
} else {
|
|
31494
|
+
result.message = `${linter} check found issues (exit code ${exitCode}).`;
|
|
31495
|
+
}
|
|
31496
|
+
return result;
|
|
31497
|
+
} catch (error93) {
|
|
31498
|
+
return {
|
|
31499
|
+
success: false,
|
|
31500
|
+
mode,
|
|
31501
|
+
linter,
|
|
31502
|
+
command,
|
|
31503
|
+
error: error93 instanceof Error ? `Execution failed: ${error93.message}` : "Execution failed: unknown error"
|
|
31504
|
+
};
|
|
31505
|
+
}
|
|
31506
|
+
}
|
|
31507
|
+
var lint = tool({
|
|
31508
|
+
description: "Run project linter in check or fix mode. Supports biome and eslint. Returns JSON with success status, exit code, and output for architect pre-reviewer gate. Use check mode for CI/linting and fix mode to automatically apply fixes.",
|
|
31509
|
+
args: {
|
|
31510
|
+
mode: tool.schema.enum(["fix", "check"]).describe('Linting mode: "check" for read-only lint check, "fix" to automatically apply fixes')
|
|
31511
|
+
},
|
|
31512
|
+
async execute(args, _context) {
|
|
31513
|
+
if (!validateArgs(args)) {
|
|
31514
|
+
const errorResult = {
|
|
31515
|
+
success: false,
|
|
31516
|
+
mode: "check",
|
|
31517
|
+
error: 'Invalid arguments: mode must be "fix" or "check"'
|
|
31518
|
+
};
|
|
31519
|
+
return JSON.stringify(errorResult, null, 2);
|
|
31520
|
+
}
|
|
31521
|
+
const { mode } = args;
|
|
31522
|
+
const linter = await detectAvailableLinter();
|
|
31523
|
+
if (!linter) {
|
|
31524
|
+
const errorResult = {
|
|
31525
|
+
success: false,
|
|
31526
|
+
mode,
|
|
31527
|
+
error: "No linter found. Install biome or eslint to use this tool.",
|
|
31528
|
+
message: "Run: npm install -D @biomejs/biome eslint"
|
|
31529
|
+
};
|
|
31530
|
+
return JSON.stringify(errorResult, null, 2);
|
|
31531
|
+
}
|
|
31532
|
+
const result = await runLint(linter, mode);
|
|
31533
|
+
return JSON.stringify(result, null, 2);
|
|
31534
|
+
}
|
|
31535
|
+
});
|
|
30943
31536
|
// src/tools/retrieve-summary.ts
|
|
30944
31537
|
var RETRIEVE_MAX_BYTES = 10 * 1024 * 1024;
|
|
30945
31538
|
var retrieve_summary = tool({
|
|
@@ -30970,6 +31563,643 @@ var retrieve_summary = tool({
|
|
|
30970
31563
|
return fullOutput;
|
|
30971
31564
|
}
|
|
30972
31565
|
});
|
|
31566
|
+
// src/tools/secretscan.ts
|
|
31567
|
+
import * as fs6 from "fs";
|
|
31568
|
+
import * as path10 from "path";
|
|
31569
|
+
var MAX_FILE_PATH_LENGTH2 = 500;
|
|
31570
|
+
var MAX_FILE_SIZE_BYTES2 = 512 * 1024;
|
|
31571
|
+
var MAX_FILES_SCANNED = 1000;
|
|
31572
|
+
var MAX_FINDINGS = 100;
|
|
31573
|
+
var MAX_OUTPUT_BYTES2 = 512000;
|
|
31574
|
+
var MAX_LINE_LENGTH = 1e4;
|
|
31575
|
+
var MAX_CONTENT_BYTES = 50 * 1024;
|
|
31576
|
+
var BINARY_SIGNATURES2 = [
|
|
31577
|
+
0,
|
|
31578
|
+
2303741511,
|
|
31579
|
+
4292411360,
|
|
31580
|
+
1195984440,
|
|
31581
|
+
626017350,
|
|
31582
|
+
1347093252
|
|
31583
|
+
];
|
|
31584
|
+
var BINARY_PREFIX_BYTES2 = 4;
|
|
31585
|
+
var BINARY_NULL_CHECK_BYTES2 = 8192;
|
|
31586
|
+
var BINARY_NULL_THRESHOLD2 = 0.1;
|
|
31587
|
+
var DEFAULT_EXCLUDE_DIRS = new Set([
|
|
31588
|
+
"node_modules",
|
|
31589
|
+
".git",
|
|
31590
|
+
"dist",
|
|
31591
|
+
"build",
|
|
31592
|
+
"out",
|
|
31593
|
+
"coverage",
|
|
31594
|
+
".next",
|
|
31595
|
+
".nuxt",
|
|
31596
|
+
".cache",
|
|
31597
|
+
"vendor",
|
|
31598
|
+
".svn",
|
|
31599
|
+
".hg",
|
|
31600
|
+
".gradle",
|
|
31601
|
+
"target",
|
|
31602
|
+
"__pycache__",
|
|
31603
|
+
".pytest_cache",
|
|
31604
|
+
".venv",
|
|
31605
|
+
"venv",
|
|
31606
|
+
".env",
|
|
31607
|
+
".idea",
|
|
31608
|
+
".vscode"
|
|
31609
|
+
]);
|
|
31610
|
+
var DEFAULT_EXCLUDE_EXTENSIONS = new Set([
|
|
31611
|
+
".png",
|
|
31612
|
+
".jpg",
|
|
31613
|
+
".jpeg",
|
|
31614
|
+
".gif",
|
|
31615
|
+
".ico",
|
|
31616
|
+
".svg",
|
|
31617
|
+
".pdf",
|
|
31618
|
+
".zip",
|
|
31619
|
+
".tar",
|
|
31620
|
+
".gz",
|
|
31621
|
+
".rar",
|
|
31622
|
+
".7z",
|
|
31623
|
+
".exe",
|
|
31624
|
+
".dll",
|
|
31625
|
+
".so",
|
|
31626
|
+
".dylib",
|
|
31627
|
+
".bin",
|
|
31628
|
+
".dat",
|
|
31629
|
+
".db",
|
|
31630
|
+
".sqlite",
|
|
31631
|
+
".lock",
|
|
31632
|
+
".log",
|
|
31633
|
+
".md"
|
|
31634
|
+
]);
|
|
31635
|
+
var SECRET_PATTERNS = [
|
|
31636
|
+
{
|
|
31637
|
+
type: "aws_access_key",
|
|
31638
|
+
regex: /(?:AWS_ACCESS_KEY_ID|AWS_SECRET_ACCESS_KEY|aws_access_key_id|aws_secret_access_key)\s*[=:]\s*['"]?([A-Z0-9]{20})['"]?/gi,
|
|
31639
|
+
confidence: "high",
|
|
31640
|
+
severity: "critical",
|
|
31641
|
+
redactTemplate: () => "AKIA[REDACTED]"
|
|
31642
|
+
},
|
|
31643
|
+
{
|
|
31644
|
+
type: "aws_secret_key",
|
|
31645
|
+
regex: /(?:AWS_SECRET_ACCESS_KEY|aws_secret_access_key)\s*[=:]\s*['"]?([A-Za-z0-9+/=]{40})['"]?/gi,
|
|
31646
|
+
confidence: "high",
|
|
31647
|
+
severity: "critical",
|
|
31648
|
+
redactTemplate: () => "[REDACTED_AWS_SECRET]"
|
|
31649
|
+
},
|
|
31650
|
+
{
|
|
31651
|
+
type: "api_key",
|
|
31652
|
+
regex: /(?:api[_-]?key|apikey|API[_-]?KEY)\s*[=:]\s*['"]?([a-zA-Z0-9_-]{16,64})['"]?/gi,
|
|
31653
|
+
confidence: "medium",
|
|
31654
|
+
severity: "high",
|
|
31655
|
+
redactTemplate: (m) => {
|
|
31656
|
+
const key = m.match(/[a-zA-Z0-9_-]{16,64}/)?.[0] || "";
|
|
31657
|
+
return `api_key=${key.slice(0, 4)}...${key.slice(-4)}`;
|
|
31658
|
+
}
|
|
31659
|
+
},
|
|
31660
|
+
{
|
|
31661
|
+
type: "bearer_token",
|
|
31662
|
+
regex: /(?:bearer\s+|Bearer\s+)([a-zA-Z0-9_\-.]{1,200})[\s"'<]/gi,
|
|
31663
|
+
confidence: "medium",
|
|
31664
|
+
severity: "high",
|
|
31665
|
+
redactTemplate: () => "bearer [REDACTED]"
|
|
31666
|
+
},
|
|
31667
|
+
{
|
|
31668
|
+
type: "basic_auth",
|
|
31669
|
+
regex: /(?:basic\s+|Basic\s+)([a-zA-Z0-9+/=]{1,200})[\s"'<]/gi,
|
|
31670
|
+
confidence: "medium",
|
|
31671
|
+
severity: "high",
|
|
31672
|
+
redactTemplate: () => "basic [REDACTED]"
|
|
31673
|
+
},
|
|
31674
|
+
{
|
|
31675
|
+
type: "database_url",
|
|
31676
|
+
regex: /(?:mysql|postgres|postgresql|mongodb|redis):\/\/[^\s"'/:]+:[^\s"'/:]+@[^\s"']+/gi,
|
|
31677
|
+
confidence: "high",
|
|
31678
|
+
severity: "critical",
|
|
31679
|
+
redactTemplate: () => "mysql://[user]:[password]@[host]"
|
|
31680
|
+
},
|
|
31681
|
+
{
|
|
31682
|
+
type: "github_token",
|
|
31683
|
+
regex: /(?:ghp|gho|ghu|ghs|ghr)_[a-zA-Z0-9]{36,}/gi,
|
|
31684
|
+
confidence: "high",
|
|
31685
|
+
severity: "critical",
|
|
31686
|
+
redactTemplate: () => "ghp_[REDACTED]"
|
|
31687
|
+
},
|
|
31688
|
+
{
|
|
31689
|
+
type: "generic_token",
|
|
31690
|
+
regex: /(?:token|TOKEN)\s*[=:]\s*['"]?([a-zA-Z0-9_\-.]{20,80})['"]?/gi,
|
|
31691
|
+
confidence: "low",
|
|
31692
|
+
severity: "medium",
|
|
31693
|
+
redactTemplate: (m) => {
|
|
31694
|
+
const token = m.match(/[a-zA-Z0-9_\-.]{20,80}/)?.[0] || "";
|
|
31695
|
+
return `token=${token.slice(0, 4)}...`;
|
|
31696
|
+
}
|
|
31697
|
+
},
|
|
31698
|
+
{
|
|
31699
|
+
type: "password",
|
|
31700
|
+
regex: /(?:password|passwd|pwd|PASSWORD|PASSWD)\s*[=:]\s*['"]?([^\s'"]{4,100})['"]?/gi,
|
|
31701
|
+
confidence: "medium",
|
|
31702
|
+
severity: "high",
|
|
31703
|
+
redactTemplate: () => "password=[REDACTED]"
|
|
31704
|
+
},
|
|
31705
|
+
{
|
|
31706
|
+
type: "private_key",
|
|
31707
|
+
regex: /-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/gi,
|
|
31708
|
+
confidence: "high",
|
|
31709
|
+
severity: "critical",
|
|
31710
|
+
redactTemplate: () => "-----BEGIN PRIVATE KEY-----"
|
|
31711
|
+
},
|
|
31712
|
+
{
|
|
31713
|
+
type: "jwt",
|
|
31714
|
+
regex: /eyJ[a-zA-Z0-9_-]*\.eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]*/g,
|
|
31715
|
+
confidence: "high",
|
|
31716
|
+
severity: "high",
|
|
31717
|
+
redactTemplate: (m) => `eyJ...${m.slice(-10)}`
|
|
31718
|
+
},
|
|
31719
|
+
{
|
|
31720
|
+
type: "stripe_key",
|
|
31721
|
+
regex: /(?:sk|pk)_(?:live|test)_[a-zA-Z0-9]{24,}/gi,
|
|
31722
|
+
confidence: "high",
|
|
31723
|
+
severity: "critical",
|
|
31724
|
+
redactTemplate: () => "sk_live_[REDACTED]"
|
|
31725
|
+
},
|
|
31726
|
+
{
|
|
31727
|
+
type: "slack_token",
|
|
31728
|
+
regex: /xox[baprs]-[0-9]{10,13}-[0-9]{10,13}[a-zA-Z0-9-]*/g,
|
|
31729
|
+
confidence: "high",
|
|
31730
|
+
severity: "critical",
|
|
31731
|
+
redactTemplate: () => "xoxb-[REDACTED]"
|
|
31732
|
+
},
|
|
31733
|
+
{
|
|
31734
|
+
type: "sendgrid_key",
|
|
31735
|
+
regex: /SG\.[a-zA-Z0-9_-]{22}\.[a-zA-Z0-9_-]{43}/g,
|
|
31736
|
+
confidence: "high",
|
|
31737
|
+
severity: "critical",
|
|
31738
|
+
redactTemplate: () => "SG.[REDACTED]"
|
|
31739
|
+
},
|
|
31740
|
+
{
|
|
31741
|
+
type: "twilio_key",
|
|
31742
|
+
regex: /SK[a-f0-9]{32}/gi,
|
|
31743
|
+
confidence: "high",
|
|
31744
|
+
severity: "critical",
|
|
31745
|
+
redactTemplate: () => "SK[REDACTED]"
|
|
31746
|
+
}
|
|
31747
|
+
];
|
|
31748
|
+
function calculateShannonEntropy(str) {
|
|
31749
|
+
if (str.length === 0)
|
|
31750
|
+
return 0;
|
|
31751
|
+
const freq = new Map;
|
|
31752
|
+
for (const char of str) {
|
|
31753
|
+
freq.set(char, (freq.get(char) || 0) + 1);
|
|
31754
|
+
}
|
|
31755
|
+
let entropy = 0;
|
|
31756
|
+
for (const count of freq.values()) {
|
|
31757
|
+
const p = count / str.length;
|
|
31758
|
+
entropy -= p * Math.log2(p);
|
|
31759
|
+
}
|
|
31760
|
+
return entropy;
|
|
31761
|
+
}
|
|
31762
|
+
function isHighEntropyString(str) {
|
|
31763
|
+
if (str.length < 20)
|
|
31764
|
+
return false;
|
|
31765
|
+
const alphanumeric = str.replace(/[^a-zA-Z0-9]/g, "").length;
|
|
31766
|
+
if (alphanumeric / str.length < 0.25)
|
|
31767
|
+
return false;
|
|
31768
|
+
const entropy = calculateShannonEntropy(str);
|
|
31769
|
+
return entropy > 4;
|
|
31770
|
+
}
|
|
31771
|
+
function containsPathTraversal2(str) {
|
|
31772
|
+
if (/\.\.[/\\]/.test(str))
|
|
31773
|
+
return true;
|
|
31774
|
+
const normalized = path10.normalize(str);
|
|
31775
|
+
if (/\.\.[/\\]/.test(normalized))
|
|
31776
|
+
return true;
|
|
31777
|
+
if (str.includes("%2e%2e") || str.includes("%2E%2E"))
|
|
31778
|
+
return true;
|
|
31779
|
+
if (str.includes("..") && /%2e/i.test(str))
|
|
31780
|
+
return true;
|
|
31781
|
+
return false;
|
|
31782
|
+
}
|
|
31783
|
+
function containsControlChars2(str) {
|
|
31784
|
+
return /[\0\r]/.test(str);
|
|
31785
|
+
}
|
|
31786
|
+
function validateDirectoryInput(dir) {
|
|
31787
|
+
if (!dir || dir.length === 0) {
|
|
31788
|
+
return "directory is required";
|
|
31789
|
+
}
|
|
31790
|
+
if (dir.length > MAX_FILE_PATH_LENGTH2) {
|
|
31791
|
+
return `directory exceeds maximum length of ${MAX_FILE_PATH_LENGTH2}`;
|
|
31792
|
+
}
|
|
31793
|
+
if (containsControlChars2(dir)) {
|
|
31794
|
+
return "directory contains control characters";
|
|
31795
|
+
}
|
|
31796
|
+
if (containsPathTraversal2(dir)) {
|
|
31797
|
+
return "directory contains path traversal";
|
|
31798
|
+
}
|
|
31799
|
+
return null;
|
|
31800
|
+
}
|
|
31801
|
+
function isBinaryFile2(filePath, buffer) {
|
|
31802
|
+
const ext = path10.extname(filePath).toLowerCase();
|
|
31803
|
+
if (DEFAULT_EXCLUDE_EXTENSIONS.has(ext)) {
|
|
31804
|
+
return true;
|
|
31805
|
+
}
|
|
31806
|
+
if (buffer.length >= BINARY_PREFIX_BYTES2) {
|
|
31807
|
+
const prefix = buffer.subarray(0, BINARY_PREFIX_BYTES2);
|
|
31808
|
+
const uint323 = prefix.readUInt32BE(0);
|
|
31809
|
+
for (const sig of BINARY_SIGNATURES2) {
|
|
31810
|
+
if (uint323 === sig)
|
|
31811
|
+
return true;
|
|
31812
|
+
}
|
|
31813
|
+
}
|
|
31814
|
+
let nullCount = 0;
|
|
31815
|
+
const checkLen = Math.min(buffer.length, BINARY_NULL_CHECK_BYTES2);
|
|
31816
|
+
for (let i = 0;i < checkLen; i++) {
|
|
31817
|
+
if (buffer[i] === 0)
|
|
31818
|
+
nullCount++;
|
|
31819
|
+
}
|
|
31820
|
+
return nullCount > checkLen * BINARY_NULL_THRESHOLD2;
|
|
31821
|
+
}
|
|
31822
|
+
function scanLineForSecrets(line, lineNum) {
|
|
31823
|
+
const results = [];
|
|
31824
|
+
if (line.length > MAX_LINE_LENGTH) {
|
|
31825
|
+
return results;
|
|
31826
|
+
}
|
|
31827
|
+
for (const pattern of SECRET_PATTERNS) {
|
|
31828
|
+
pattern.regex.lastIndex = 0;
|
|
31829
|
+
let match;
|
|
31830
|
+
while ((match = pattern.regex.exec(line)) !== null) {
|
|
31831
|
+
const fullMatch = match[0];
|
|
31832
|
+
const redacted = pattern.redactTemplate(fullMatch);
|
|
31833
|
+
results.push({
|
|
31834
|
+
type: pattern.type,
|
|
31835
|
+
confidence: pattern.confidence,
|
|
31836
|
+
severity: pattern.severity,
|
|
31837
|
+
redacted,
|
|
31838
|
+
matchStart: match.index,
|
|
31839
|
+
matchEnd: match.index + fullMatch.length
|
|
31840
|
+
});
|
|
31841
|
+
if (match.index === pattern.regex.lastIndex) {
|
|
31842
|
+
pattern.regex.lastIndex++;
|
|
31843
|
+
}
|
|
31844
|
+
}
|
|
31845
|
+
}
|
|
31846
|
+
const valueMatch = line.match(/(?:secret|key|token|password|cred|credential)\s*[=:]\s*["']?([a-zA-Z0-9+/=_-]{20,100})["']?/i);
|
|
31847
|
+
if (valueMatch && isHighEntropyString(valueMatch[1])) {
|
|
31848
|
+
const matchStart = valueMatch.index || 0;
|
|
31849
|
+
const matchEnd = matchStart + valueMatch[0].length;
|
|
31850
|
+
const hasOverlap = results.some((r) => !(r.matchEnd <= matchStart || r.matchStart >= matchEnd));
|
|
31851
|
+
if (!hasOverlap) {
|
|
31852
|
+
results.push({
|
|
31853
|
+
type: "high_entropy",
|
|
31854
|
+
confidence: "low",
|
|
31855
|
+
severity: "medium",
|
|
31856
|
+
redacted: `${valueMatch[0].split("=")[0].trim()}=[HIGH_ENTROPY]`,
|
|
31857
|
+
matchStart,
|
|
31858
|
+
matchEnd
|
|
31859
|
+
});
|
|
31860
|
+
}
|
|
31861
|
+
}
|
|
31862
|
+
return results;
|
|
31863
|
+
}
|
|
31864
|
+
function createRedactedContext(line, findings) {
|
|
31865
|
+
if (findings.length === 0)
|
|
31866
|
+
return line;
|
|
31867
|
+
const sorted = [...findings].sort((a, b) => a.matchStart - b.matchStart);
|
|
31868
|
+
let result = "";
|
|
31869
|
+
let lastEnd = 0;
|
|
31870
|
+
for (const finding of sorted) {
|
|
31871
|
+
result += line.slice(lastEnd, finding.matchStart);
|
|
31872
|
+
result += finding.redacted;
|
|
31873
|
+
lastEnd = finding.matchEnd;
|
|
31874
|
+
}
|
|
31875
|
+
result += line.slice(lastEnd);
|
|
31876
|
+
return result;
|
|
31877
|
+
}
|
|
31878
|
+
var O_NOFOLLOW = process.platform !== "win32" ? fs6.constants.O_NOFOLLOW : undefined;
|
|
31879
|
+
function scanFileForSecrets(filePath) {
|
|
31880
|
+
const findings = [];
|
|
31881
|
+
try {
|
|
31882
|
+
const lstat = fs6.lstatSync(filePath);
|
|
31883
|
+
if (lstat.isSymbolicLink()) {
|
|
31884
|
+
return findings;
|
|
31885
|
+
}
|
|
31886
|
+
if (lstat.size > MAX_FILE_SIZE_BYTES2) {
|
|
31887
|
+
return findings;
|
|
31888
|
+
}
|
|
31889
|
+
let buffer;
|
|
31890
|
+
if (O_NOFOLLOW !== undefined) {
|
|
31891
|
+
const fd = fs6.openSync(filePath, "r", O_NOFOLLOW);
|
|
31892
|
+
try {
|
|
31893
|
+
buffer = fs6.readFileSync(fd);
|
|
31894
|
+
} finally {
|
|
31895
|
+
fs6.closeSync(fd);
|
|
31896
|
+
}
|
|
31897
|
+
} else {
|
|
31898
|
+
buffer = fs6.readFileSync(filePath);
|
|
31899
|
+
}
|
|
31900
|
+
if (isBinaryFile2(filePath, buffer)) {
|
|
31901
|
+
return findings;
|
|
31902
|
+
}
|
|
31903
|
+
let content;
|
|
31904
|
+
if (buffer.length >= 3 && buffer[0] === 239 && buffer[1] === 187 && buffer[2] === 191) {
|
|
31905
|
+
content = buffer.slice(3).toString("utf-8");
|
|
31906
|
+
} else {
|
|
31907
|
+
content = buffer.toString("utf-8");
|
|
31908
|
+
}
|
|
31909
|
+
if (content.includes("\x00")) {
|
|
31910
|
+
return findings;
|
|
31911
|
+
}
|
|
31912
|
+
const scanContent = content.slice(0, MAX_CONTENT_BYTES);
|
|
31913
|
+
const lines = scanContent.split(`
|
|
31914
|
+
`);
|
|
31915
|
+
for (let i = 0;i < lines.length; i++) {
|
|
31916
|
+
const lineResults = scanLineForSecrets(lines[i], i + 1);
|
|
31917
|
+
for (const result of lineResults) {
|
|
31918
|
+
findings.push({
|
|
31919
|
+
path: filePath,
|
|
31920
|
+
line: i + 1,
|
|
31921
|
+
type: result.type,
|
|
31922
|
+
confidence: result.confidence,
|
|
31923
|
+
severity: result.severity,
|
|
31924
|
+
redacted: result.redacted,
|
|
31925
|
+
context: createRedactedContext(lines[i], [result])
|
|
31926
|
+
});
|
|
31927
|
+
}
|
|
31928
|
+
}
|
|
31929
|
+
} catch {}
|
|
31930
|
+
return findings;
|
|
31931
|
+
}
|
|
31932
|
+
function isSymlinkLoop(realPath, visited) {
|
|
31933
|
+
if (visited.has(realPath)) {
|
|
31934
|
+
return true;
|
|
31935
|
+
}
|
|
31936
|
+
visited.add(realPath);
|
|
31937
|
+
return false;
|
|
31938
|
+
}
|
|
31939
|
+
function isPathWithinScope(realPath, scanDir) {
|
|
31940
|
+
const resolvedScanDir = path10.resolve(scanDir);
|
|
31941
|
+
const resolvedRealPath = path10.resolve(realPath);
|
|
31942
|
+
return resolvedRealPath === resolvedScanDir || resolvedRealPath.startsWith(resolvedScanDir + path10.sep) || resolvedRealPath.startsWith(resolvedScanDir + "/") || resolvedRealPath.startsWith(resolvedScanDir + "\\");
|
|
31943
|
+
}
|
|
31944
|
+
function findScannableFiles(dir, excludeDirs, scanDir, visited, stats = {
|
|
31945
|
+
skippedDirs: 0,
|
|
31946
|
+
skippedFiles: 0,
|
|
31947
|
+
fileErrors: 0,
|
|
31948
|
+
symlinkSkipped: 0
|
|
31949
|
+
}) {
|
|
31950
|
+
const files = [];
|
|
31951
|
+
let entries;
|
|
31952
|
+
try {
|
|
31953
|
+
entries = fs6.readdirSync(dir);
|
|
31954
|
+
} catch {
|
|
31955
|
+
stats.fileErrors++;
|
|
31956
|
+
return files;
|
|
31957
|
+
}
|
|
31958
|
+
entries.sort((a, b) => {
|
|
31959
|
+
const aLower = a.toLowerCase();
|
|
31960
|
+
const bLower = b.toLowerCase();
|
|
31961
|
+
if (aLower < bLower)
|
|
31962
|
+
return -1;
|
|
31963
|
+
if (aLower > bLower)
|
|
31964
|
+
return 1;
|
|
31965
|
+
return a.localeCompare(b);
|
|
31966
|
+
});
|
|
31967
|
+
for (const entry of entries) {
|
|
31968
|
+
if (excludeDirs.has(entry)) {
|
|
31969
|
+
stats.skippedDirs++;
|
|
31970
|
+
continue;
|
|
31971
|
+
}
|
|
31972
|
+
const fullPath = path10.join(dir, entry);
|
|
31973
|
+
let lstat;
|
|
31974
|
+
try {
|
|
31975
|
+
lstat = fs6.lstatSync(fullPath);
|
|
31976
|
+
} catch {
|
|
31977
|
+
stats.fileErrors++;
|
|
31978
|
+
continue;
|
|
31979
|
+
}
|
|
31980
|
+
if (lstat.isSymbolicLink()) {
|
|
31981
|
+
stats.symlinkSkipped++;
|
|
31982
|
+
continue;
|
|
31983
|
+
}
|
|
31984
|
+
if (lstat.isDirectory()) {
|
|
31985
|
+
let realPath;
|
|
31986
|
+
try {
|
|
31987
|
+
realPath = fs6.realpathSync(fullPath);
|
|
31988
|
+
} catch {
|
|
31989
|
+
stats.fileErrors++;
|
|
31990
|
+
continue;
|
|
31991
|
+
}
|
|
31992
|
+
if (isSymlinkLoop(realPath, visited)) {
|
|
31993
|
+
stats.symlinkSkipped++;
|
|
31994
|
+
continue;
|
|
31995
|
+
}
|
|
31996
|
+
if (!isPathWithinScope(realPath, scanDir)) {
|
|
31997
|
+
stats.symlinkSkipped++;
|
|
31998
|
+
continue;
|
|
31999
|
+
}
|
|
32000
|
+
const subFiles = findScannableFiles(fullPath, excludeDirs, scanDir, visited, stats);
|
|
32001
|
+
files.push(...subFiles);
|
|
32002
|
+
} else if (lstat.isFile()) {
|
|
32003
|
+
const ext = path10.extname(fullPath).toLowerCase();
|
|
32004
|
+
if (!DEFAULT_EXCLUDE_EXTENSIONS.has(ext)) {
|
|
32005
|
+
files.push(fullPath);
|
|
32006
|
+
} else {
|
|
32007
|
+
stats.skippedFiles++;
|
|
32008
|
+
}
|
|
32009
|
+
}
|
|
32010
|
+
}
|
|
32011
|
+
return files;
|
|
32012
|
+
}
|
|
32013
|
+
var secretscan = tool({
|
|
32014
|
+
description: "Scan directory for potential secrets (API keys, tokens, passwords) using regex patterns and entropy heuristics. Returns metadata-only findings with redacted previews - NEVER returns raw secrets. Excludes common directories (node_modules, .git, dist, etc.) by default.",
|
|
32015
|
+
args: {
|
|
32016
|
+
directory: tool.schema.string().describe('Directory to scan for secrets (e.g., "." or "./src")'),
|
|
32017
|
+
exclude: tool.schema.array(tool.schema.string()).optional().describe("Additional directories to exclude (added to default exclusions like node_modules, .git, dist)")
|
|
32018
|
+
},
|
|
32019
|
+
async execute(args, _context) {
|
|
32020
|
+
let directory;
|
|
32021
|
+
let exclude;
|
|
32022
|
+
try {
|
|
32023
|
+
if (args && typeof args === "object") {
|
|
32024
|
+
directory = args.directory;
|
|
32025
|
+
exclude = args.exclude;
|
|
32026
|
+
}
|
|
32027
|
+
} catch {}
|
|
32028
|
+
if (directory === undefined) {
|
|
32029
|
+
const errorResult = {
|
|
32030
|
+
error: "invalid arguments: directory is required",
|
|
32031
|
+
scan_dir: "",
|
|
32032
|
+
findings: [],
|
|
32033
|
+
count: 0,
|
|
32034
|
+
files_scanned: 0,
|
|
32035
|
+
skipped_files: 0
|
|
32036
|
+
};
|
|
32037
|
+
return JSON.stringify(errorResult, null, 2);
|
|
32038
|
+
}
|
|
32039
|
+
const dirValidationError = validateDirectoryInput(directory);
|
|
32040
|
+
if (dirValidationError) {
|
|
32041
|
+
const errorResult = {
|
|
32042
|
+
error: `invalid directory: ${dirValidationError}`,
|
|
32043
|
+
scan_dir: directory,
|
|
32044
|
+
findings: [],
|
|
32045
|
+
count: 0,
|
|
32046
|
+
files_scanned: 0,
|
|
32047
|
+
skipped_files: 0
|
|
32048
|
+
};
|
|
32049
|
+
return JSON.stringify(errorResult, null, 2);
|
|
32050
|
+
}
|
|
32051
|
+
if (exclude) {
|
|
32052
|
+
for (const exc of exclude) {
|
|
32053
|
+
if (exc.length > MAX_FILE_PATH_LENGTH2) {
|
|
32054
|
+
const errorResult = {
|
|
32055
|
+
error: `invalid exclude path: exceeds maximum length of ${MAX_FILE_PATH_LENGTH2}`,
|
|
32056
|
+
scan_dir: directory,
|
|
32057
|
+
findings: [],
|
|
32058
|
+
count: 0,
|
|
32059
|
+
files_scanned: 0,
|
|
32060
|
+
skipped_files: 0
|
|
32061
|
+
};
|
|
32062
|
+
return JSON.stringify(errorResult, null, 2);
|
|
32063
|
+
}
|
|
32064
|
+
if (containsPathTraversal2(exc) || containsControlChars2(exc)) {
|
|
32065
|
+
const errorResult = {
|
|
32066
|
+
error: `invalid exclude path: contains path traversal or control characters`,
|
|
32067
|
+
scan_dir: directory,
|
|
32068
|
+
findings: [],
|
|
32069
|
+
count: 0,
|
|
32070
|
+
files_scanned: 0,
|
|
32071
|
+
skipped_files: 0
|
|
32072
|
+
};
|
|
32073
|
+
return JSON.stringify(errorResult, null, 2);
|
|
32074
|
+
}
|
|
32075
|
+
}
|
|
32076
|
+
}
|
|
32077
|
+
try {
|
|
32078
|
+
const scanDir = path10.resolve(directory);
|
|
32079
|
+
if (!fs6.existsSync(scanDir)) {
|
|
32080
|
+
const errorResult = {
|
|
32081
|
+
error: "directory not found",
|
|
32082
|
+
scan_dir: directory,
|
|
32083
|
+
findings: [],
|
|
32084
|
+
count: 0,
|
|
32085
|
+
files_scanned: 0,
|
|
32086
|
+
skipped_files: 0
|
|
32087
|
+
};
|
|
32088
|
+
return JSON.stringify(errorResult, null, 2);
|
|
32089
|
+
}
|
|
32090
|
+
const dirStat = fs6.statSync(scanDir);
|
|
32091
|
+
if (!dirStat.isDirectory()) {
|
|
32092
|
+
const errorResult = {
|
|
32093
|
+
error: "target must be a directory, not a file",
|
|
32094
|
+
scan_dir: directory,
|
|
32095
|
+
findings: [],
|
|
32096
|
+
count: 0,
|
|
32097
|
+
files_scanned: 0,
|
|
32098
|
+
skipped_files: 0
|
|
32099
|
+
};
|
|
32100
|
+
return JSON.stringify(errorResult, null, 2);
|
|
32101
|
+
}
|
|
32102
|
+
const excludeDirs = new Set(DEFAULT_EXCLUDE_DIRS);
|
|
32103
|
+
if (exclude) {
|
|
32104
|
+
for (const exc of exclude) {
|
|
32105
|
+
excludeDirs.add(exc);
|
|
32106
|
+
}
|
|
32107
|
+
}
|
|
32108
|
+
const stats = {
|
|
32109
|
+
skippedDirs: 0,
|
|
32110
|
+
skippedFiles: 0,
|
|
32111
|
+
fileErrors: 0,
|
|
32112
|
+
symlinkSkipped: 0
|
|
32113
|
+
};
|
|
32114
|
+
const visited = new Set;
|
|
32115
|
+
const files = findScannableFiles(scanDir, excludeDirs, scanDir, visited, stats);
|
|
32116
|
+
files.sort((a, b) => {
|
|
32117
|
+
const aLower = a.toLowerCase();
|
|
32118
|
+
const bLower = b.toLowerCase();
|
|
32119
|
+
if (aLower < bLower)
|
|
32120
|
+
return -1;
|
|
32121
|
+
if (aLower > bLower)
|
|
32122
|
+
return 1;
|
|
32123
|
+
return a.localeCompare(b);
|
|
32124
|
+
});
|
|
32125
|
+
const filesToScan = files.slice(0, MAX_FILES_SCANNED);
|
|
32126
|
+
const allFindings = [];
|
|
32127
|
+
let filesScanned = 0;
|
|
32128
|
+
let skippedFiles = stats.skippedFiles;
|
|
32129
|
+
for (const filePath of filesToScan) {
|
|
32130
|
+
if (allFindings.length >= MAX_FINDINGS)
|
|
32131
|
+
break;
|
|
32132
|
+
const fileFindings = scanFileForSecrets(filePath);
|
|
32133
|
+
try {
|
|
32134
|
+
const stat = fs6.statSync(filePath);
|
|
32135
|
+
if (stat.size > MAX_FILE_SIZE_BYTES2) {
|
|
32136
|
+
skippedFiles++;
|
|
32137
|
+
continue;
|
|
32138
|
+
}
|
|
32139
|
+
} catch {}
|
|
32140
|
+
filesScanned++;
|
|
32141
|
+
for (const finding of fileFindings) {
|
|
32142
|
+
if (allFindings.length >= MAX_FINDINGS)
|
|
32143
|
+
break;
|
|
32144
|
+
allFindings.push(finding);
|
|
32145
|
+
}
|
|
32146
|
+
}
|
|
32147
|
+
allFindings.sort((a, b) => {
|
|
32148
|
+
const aPathLower = a.path.toLowerCase();
|
|
32149
|
+
const bPathLower = b.path.toLowerCase();
|
|
32150
|
+
if (aPathLower < bPathLower)
|
|
32151
|
+
return -1;
|
|
32152
|
+
if (aPathLower > bPathLower)
|
|
32153
|
+
return 1;
|
|
32154
|
+
if (a.path < b.path)
|
|
32155
|
+
return -1;
|
|
32156
|
+
if (a.path > b.path)
|
|
32157
|
+
return 1;
|
|
32158
|
+
return a.line - b.line;
|
|
32159
|
+
});
|
|
32160
|
+
const result = {
|
|
32161
|
+
scan_dir: directory,
|
|
32162
|
+
findings: allFindings,
|
|
32163
|
+
count: allFindings.length,
|
|
32164
|
+
files_scanned: filesScanned,
|
|
32165
|
+
skipped_files: skippedFiles + stats.fileErrors + stats.symlinkSkipped
|
|
32166
|
+
};
|
|
32167
|
+
const parts = [];
|
|
32168
|
+
if (files.length > MAX_FILES_SCANNED) {
|
|
32169
|
+
parts.push(`Found ${files.length} files, scanned ${MAX_FILES_SCANNED}`);
|
|
32170
|
+
}
|
|
32171
|
+
if (allFindings.length >= MAX_FINDINGS) {
|
|
32172
|
+
parts.push(`Results limited to ${MAX_FINDINGS} findings`);
|
|
32173
|
+
}
|
|
32174
|
+
if (skippedFiles > 0 || stats.fileErrors > 0 || stats.symlinkSkipped > 0) {
|
|
32175
|
+
parts.push(`${skippedFiles + stats.fileErrors + stats.symlinkSkipped} files skipped (binary/oversized/symlinks/errors)`);
|
|
32176
|
+
}
|
|
32177
|
+
if (parts.length > 0) {
|
|
32178
|
+
result.message = parts.join("; ") + ".";
|
|
32179
|
+
}
|
|
32180
|
+
let jsonOutput = JSON.stringify(result, null, 2);
|
|
32181
|
+
if (jsonOutput.length > MAX_OUTPUT_BYTES2) {
|
|
32182
|
+
const truncatedResult = {
|
|
32183
|
+
...result,
|
|
32184
|
+
findings: result.findings.slice(0, Math.floor(MAX_OUTPUT_BYTES2 * 0.8 / 200)),
|
|
32185
|
+
message: "Output truncated due to size limits."
|
|
32186
|
+
};
|
|
32187
|
+
jsonOutput = JSON.stringify(truncatedResult, null, 2);
|
|
32188
|
+
}
|
|
32189
|
+
return jsonOutput;
|
|
32190
|
+
} catch (e) {
|
|
32191
|
+
const errorResult = {
|
|
32192
|
+
error: e instanceof Error ? `scan failed: ${e.message || "internal error"}` : "scan failed: unknown error",
|
|
32193
|
+
scan_dir: directory,
|
|
32194
|
+
findings: [],
|
|
32195
|
+
count: 0,
|
|
32196
|
+
files_scanned: 0,
|
|
32197
|
+
skipped_files: 0
|
|
32198
|
+
};
|
|
32199
|
+
return JSON.stringify(errorResult, null, 2);
|
|
32200
|
+
}
|
|
32201
|
+
}
|
|
32202
|
+
});
|
|
30973
32203
|
// src/index.ts
|
|
30974
32204
|
var OpenCodeSwarm = async (ctx) => {
|
|
30975
32205
|
const { config: config3, loadedFromFile } = loadPluginConfigWithMeta(ctx.directory);
|
|
@@ -31012,8 +32242,11 @@ var OpenCodeSwarm = async (ctx) => {
|
|
|
31012
32242
|
detect_domains,
|
|
31013
32243
|
extract_code_blocks,
|
|
31014
32244
|
gitingest,
|
|
32245
|
+
imports,
|
|
32246
|
+
lint,
|
|
31015
32247
|
diff,
|
|
31016
|
-
retrieve_summary
|
|
32248
|
+
retrieve_summary,
|
|
32249
|
+
secretscan
|
|
31017
32250
|
},
|
|
31018
32251
|
config: async (opencodeConfig) => {
|
|
31019
32252
|
if (!opencodeConfig.agent) {
|