opencode-swarm 6.1.2 → 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 +310 -510
- package/dist/config/evidence-schema.d.ts +94 -0
- package/dist/config/schema.d.ts +53 -0
- package/dist/index.js +1443 -55
- package/dist/state.d.ts +2 -0
- 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
|
@@ -1,20 +1,5 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
var __create = Object.create;
|
|
3
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
4
2
|
var __defProp = Object.defineProperty;
|
|
5
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
-
var __toESM = (mod, isNodeMode, target) => {
|
|
8
|
-
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
9
|
-
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
10
|
-
for (let key of __getOwnPropNames(mod))
|
|
11
|
-
if (!__hasOwnProp.call(to, key))
|
|
12
|
-
__defProp(to, key, {
|
|
13
|
-
get: () => mod[key],
|
|
14
|
-
enumerable: true
|
|
15
|
-
});
|
|
16
|
-
return to;
|
|
17
|
-
};
|
|
18
3
|
var __export = (target, all) => {
|
|
19
4
|
for (var name in all)
|
|
20
5
|
__defProp(target, name, {
|
|
@@ -13644,7 +13629,8 @@ var EvidenceTypeSchema = exports_external.enum([
|
|
|
13644
13629
|
"test",
|
|
13645
13630
|
"diff",
|
|
13646
13631
|
"approval",
|
|
13647
|
-
"note"
|
|
13632
|
+
"note",
|
|
13633
|
+
"retrospective"
|
|
13648
13634
|
]);
|
|
13649
13635
|
var EvidenceVerdictSchema = exports_external.enum([
|
|
13650
13636
|
"pass",
|
|
@@ -13695,12 +13681,27 @@ var ApprovalEvidenceSchema = BaseEvidenceSchema.extend({
|
|
|
13695
13681
|
var NoteEvidenceSchema = BaseEvidenceSchema.extend({
|
|
13696
13682
|
type: exports_external.literal("note")
|
|
13697
13683
|
});
|
|
13684
|
+
var RetrospectiveEvidenceSchema = BaseEvidenceSchema.extend({
|
|
13685
|
+
type: exports_external.literal("retrospective"),
|
|
13686
|
+
phase_number: exports_external.number().int().min(0),
|
|
13687
|
+
total_tool_calls: exports_external.number().int().min(0),
|
|
13688
|
+
coder_revisions: exports_external.number().int().min(0),
|
|
13689
|
+
reviewer_rejections: exports_external.number().int().min(0),
|
|
13690
|
+
test_failures: exports_external.number().int().min(0),
|
|
13691
|
+
security_findings: exports_external.number().int().min(0),
|
|
13692
|
+
integration_issues: exports_external.number().int().min(0),
|
|
13693
|
+
task_count: exports_external.number().int().min(1),
|
|
13694
|
+
task_complexity: exports_external.enum(["trivial", "simple", "moderate", "complex"]),
|
|
13695
|
+
top_rejection_reasons: exports_external.array(exports_external.string()).default([]),
|
|
13696
|
+
lessons_learned: exports_external.array(exports_external.string()).max(5).default([])
|
|
13697
|
+
});
|
|
13698
13698
|
var EvidenceSchema = exports_external.discriminatedUnion("type", [
|
|
13699
13699
|
ReviewEvidenceSchema,
|
|
13700
13700
|
TestEvidenceSchema,
|
|
13701
13701
|
DiffEvidenceSchema,
|
|
13702
13702
|
ApprovalEvidenceSchema,
|
|
13703
|
-
NoteEvidenceSchema
|
|
13703
|
+
NoteEvidenceSchema,
|
|
13704
|
+
RetrospectiveEvidenceSchema
|
|
13704
13705
|
]);
|
|
13705
13706
|
var EvidenceBundleSchema = exports_external.object({
|
|
13706
13707
|
schema_version: exports_external.literal("1.0.0"),
|
|
@@ -13836,6 +13837,67 @@ var UIReviewConfigSchema = exports_external.object({
|
|
|
13836
13837
|
"profile page"
|
|
13837
13838
|
])
|
|
13838
13839
|
});
|
|
13840
|
+
var CompactionAdvisoryConfigSchema = exports_external.object({
|
|
13841
|
+
enabled: exports_external.boolean().default(true),
|
|
13842
|
+
thresholds: exports_external.array(exports_external.number().int().min(10).max(500)).default([50, 75, 100, 125, 150]),
|
|
13843
|
+
message: exports_external.string().default("[SWARM HINT] Session has ${totalToolCalls} tool calls. Consider compacting at next phase boundary to maintain context quality.")
|
|
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
|
+
});
|
|
13839
13901
|
var GuardrailsProfileSchema = exports_external.object({
|
|
13840
13902
|
max_tool_calls: exports_external.number().min(0).max(1000).optional(),
|
|
13841
13903
|
max_duration_minutes: exports_external.number().min(0).max(480).optional(),
|
|
@@ -13949,7 +14011,10 @@ var PluginConfigSchema = exports_external.object({
|
|
|
13949
14011
|
review_passes: ReviewPassesConfigSchema.optional(),
|
|
13950
14012
|
integration_analysis: IntegrationAnalysisConfigSchema.optional(),
|
|
13951
14013
|
docs: DocsConfigSchema.optional(),
|
|
13952
|
-
ui_review: UIReviewConfigSchema.optional()
|
|
14014
|
+
ui_review: UIReviewConfigSchema.optional(),
|
|
14015
|
+
compaction_advisory: CompactionAdvisoryConfigSchema.optional(),
|
|
14016
|
+
lint: LintConfigSchema.optional(),
|
|
14017
|
+
secretscan: SecretscanConfigSchema.optional()
|
|
13953
14018
|
});
|
|
13954
14019
|
|
|
13955
14020
|
// src/config/loader.ts
|
|
@@ -14120,7 +14185,7 @@ You THINK. Subagents DO. You have the largest context window and strongest reaso
|
|
|
14120
14185
|
- If NEEDS_REVISION: Revise plan and re-submit to critic (max 2 cycles)
|
|
14121
14186
|
- If REJECTED after 2 cycles: Escalate to user with explanation
|
|
14122
14187
|
- ONLY AFTER critic approval: Proceed to implementation (Phase 3+)
|
|
14123
|
-
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.
|
|
14124
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.
|
|
14125
14190
|
- Delegate {{AGENT_PREFIX}}reviewer with CHECK dimensions. REJECTED \u2192 return to coder (max {{QA_RETRY_LIMIT}} attempts). APPROVED \u2192 continue.
|
|
14126
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.
|
|
@@ -14132,6 +14197,7 @@ You THINK. Subagents DO. You have the largest context window and strongest reaso
|
|
|
14132
14197
|
- Target file is in: pages/, components/, views/, screens/, ui/, layouts/
|
|
14133
14198
|
If triggered: delegate to {{AGENT_PREFIX}}designer FIRST to produce a code scaffold. Then pass the scaffold to {{AGENT_PREFIX}}coder as INPUT alongside the task. The coder implements the TODOs in the scaffold without changing component structure or accessibility attributes.
|
|
14134
14199
|
If not triggered: delegate directly to {{AGENT_PREFIX}}coder as normal.
|
|
14200
|
+
10. **RETROSPECTIVE TRACKING**: At the end of every phase, record phase metrics in .swarm/context.md under "## Phase Metrics" and write a retrospective evidence entry via the evidence manager. Track: phase_number, total_tool_calls, coder_revisions, reviewer_rejections, test_failures, security_findings, integration_issues, task_count, task_complexity, top_rejection_reasons, lessons_learned (max 5). Reset Phase Metrics to 0 after writing.
|
|
14135
14201
|
|
|
14136
14202
|
## AGENTS
|
|
14137
14203
|
|
|
@@ -14146,7 +14212,7 @@ You THINK. Subagents DO. You have the largest context window and strongest reaso
|
|
|
14146
14212
|
|
|
14147
14213
|
SMEs advise only. Reviewer and critic review only. None of them write code.
|
|
14148
14214
|
|
|
14149
|
-
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)
|
|
14150
14216
|
|
|
14151
14217
|
## DELEGATION FORMAT
|
|
14152
14218
|
|
|
@@ -14292,11 +14358,15 @@ For each task (respecting dependencies):
|
|
|
14292
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.
|
|
14293
14359
|
5b. {{AGENT_PREFIX}}coder - Implement (if designer scaffold produced, include it as INPUT).
|
|
14294
14360
|
5c. Run \`diff\` tool. If \`hasContractChanges\` \u2192 {{AGENT_PREFIX}}explorer integration analysis. BREAKING \u2192 coder retry.
|
|
14295
|
-
5d.
|
|
14296
|
-
5e.
|
|
14297
|
-
5f.
|
|
14298
|
-
5g. {{AGENT_PREFIX}}
|
|
14299
|
-
5h.
|
|
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.
|
|
14300
14370
|
|
|
14301
14371
|
### Phase 6: Phase Complete
|
|
14302
14372
|
1. {{AGENT_PREFIX}}explorer - Rescan
|
|
@@ -14305,8 +14375,9 @@ For each task (respecting dependencies):
|
|
|
14305
14375
|
- Summary of what was added/modified/removed
|
|
14306
14376
|
- List of doc files that may need updating (README.md, CONTRIBUTING.md, docs/)
|
|
14307
14377
|
3. Update context.md
|
|
14308
|
-
4.
|
|
14309
|
-
5.
|
|
14378
|
+
4. Write retrospective evidence: record phase_number, total_tool_calls, coder_revisions, reviewer_rejections, test_failures, security_findings, integration_issues, task_count, task_complexity, top_rejection_reasons, lessons_learned to .swarm/evidence/ via the evidence manager. Reset Phase Metrics in context.md to 0.
|
|
14379
|
+
5. Summarize to user
|
|
14380
|
+
6. Ask: "Ready for Phase [N+1]?"
|
|
14310
14381
|
|
|
14311
14382
|
### Blockers
|
|
14312
14383
|
Mark [BLOCKED] in plan.md, skip to next unblocked task, inform user.
|
|
@@ -14906,7 +14977,13 @@ OUTPUT FORMAT:
|
|
|
14906
14977
|
VERDICT: PASS | FAIL
|
|
14907
14978
|
TESTS: [total count] tests, [pass count] passed, [fail count] failed
|
|
14908
14979
|
FAILURES: [list of failed test names + error messages, if any]
|
|
14909
|
-
COVERAGE: [areas covered]
|
|
14980
|
+
COVERAGE: [areas covered]
|
|
14981
|
+
|
|
14982
|
+
COVERAGE REPORTING:
|
|
14983
|
+
- After running tests, report the line/branch coverage percentage if the test runner provides it.
|
|
14984
|
+
- Format: COVERAGE_PCT: [N]% (or "N/A" if not available)
|
|
14985
|
+
- If COVERAGE_PCT < 70%, add a note: "COVERAGE_WARNING: Below 70% threshold \u2014 consider additional test cases for uncovered paths."
|
|
14986
|
+
- The architect uses this to decide whether to request an additional test pass (Rule 10 / Phase 5 step 5h).`;
|
|
14910
14987
|
function createTestEngineerAgent(model, customPrompt, customAppendPrompt) {
|
|
14911
14988
|
let prompt = TEST_ENGINEER_PROMPT;
|
|
14912
14989
|
if (customPrompt) {
|
|
@@ -15449,7 +15526,8 @@ function startAgentSession(sessionId, agentName, staleDurationMs = 7200000) {
|
|
|
15449
15526
|
delegationActive: false,
|
|
15450
15527
|
activeInvocationId: 0,
|
|
15451
15528
|
lastInvocationIdByAgent: {},
|
|
15452
|
-
windows: {}
|
|
15529
|
+
windows: {},
|
|
15530
|
+
lastCompactionHint: 0
|
|
15453
15531
|
};
|
|
15454
15532
|
swarmState.agentSessions.set(sessionId, sessionState);
|
|
15455
15533
|
swarmState.activeAgent.set(sessionId, agentName);
|
|
@@ -15473,6 +15551,9 @@ function ensureAgentSession(sessionId, agentName) {
|
|
|
15473
15551
|
session.lastInvocationIdByAgent = {};
|
|
15474
15552
|
session.windows = {};
|
|
15475
15553
|
}
|
|
15554
|
+
if (session.lastCompactionHint === undefined) {
|
|
15555
|
+
session.lastCompactionHint = 0;
|
|
15556
|
+
}
|
|
15476
15557
|
session.lastToolCallTime = now;
|
|
15477
15558
|
return session;
|
|
15478
15559
|
}
|
|
@@ -17494,6 +17575,10 @@ ${originalText}`;
|
|
|
17494
17575
|
})
|
|
17495
17576
|
};
|
|
17496
17577
|
}
|
|
17578
|
+
// src/hooks/system-enhancer.ts
|
|
17579
|
+
import * as fs3 from "fs";
|
|
17580
|
+
import * as path7 from "path";
|
|
17581
|
+
|
|
17497
17582
|
// src/hooks/context-scoring.ts
|
|
17498
17583
|
function calculateAgeFactor(ageHours, config2) {
|
|
17499
17584
|
if (ageHours <= 0) {
|
|
@@ -17629,6 +17714,72 @@ function createSystemEnhancerHook(config2, directory) {
|
|
|
17629
17714
|
if (config2.docs?.enabled === false) {
|
|
17630
17715
|
tryInject("[SWARM CONFIG] Docs agent is DISABLED. Skip docs delegation in Phase 6.");
|
|
17631
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
|
+
}
|
|
17723
|
+
const sessionId_retro = _input.sessionID;
|
|
17724
|
+
const activeAgent_retro = swarmState.activeAgent.get(sessionId_retro ?? "");
|
|
17725
|
+
const isArchitect = !activeAgent_retro || stripKnownSwarmPrefix(activeAgent_retro) === "architect";
|
|
17726
|
+
if (isArchitect) {
|
|
17727
|
+
try {
|
|
17728
|
+
const evidenceDir = path7.join(directory, ".swarm", "evidence");
|
|
17729
|
+
if (fs3.existsSync(evidenceDir)) {
|
|
17730
|
+
const files = fs3.readdirSync(evidenceDir).filter((f) => f.endsWith(".json")).sort().reverse();
|
|
17731
|
+
for (const file2 of files.slice(0, 5)) {
|
|
17732
|
+
const content = JSON.parse(fs3.readFileSync(path7.join(evidenceDir, file2), "utf-8"));
|
|
17733
|
+
if (content.type === "retrospective") {
|
|
17734
|
+
const retro = content;
|
|
17735
|
+
const hints = [];
|
|
17736
|
+
if (retro.reviewer_rejections > 2) {
|
|
17737
|
+
hints.push(`Phase ${retro.phase_number} had ${retro.reviewer_rejections} reviewer rejections.`);
|
|
17738
|
+
}
|
|
17739
|
+
if (retro.top_rejection_reasons.length > 0) {
|
|
17740
|
+
hints.push(`Common rejection reasons: ${retro.top_rejection_reasons.join(", ")}.`);
|
|
17741
|
+
}
|
|
17742
|
+
if (retro.lessons_learned.length > 0) {
|
|
17743
|
+
hints.push(`Lessons: ${retro.lessons_learned.join("; ")}.`);
|
|
17744
|
+
}
|
|
17745
|
+
if (hints.length > 0) {
|
|
17746
|
+
const retroHint = `[SWARM RETROSPECTIVE] From Phase ${retro.phase_number}: ${hints.join(" ")}`;
|
|
17747
|
+
if (retroHint.length <= 800) {
|
|
17748
|
+
tryInject(retroHint);
|
|
17749
|
+
} else {
|
|
17750
|
+
tryInject(retroHint.substring(0, 800) + "...");
|
|
17751
|
+
}
|
|
17752
|
+
}
|
|
17753
|
+
break;
|
|
17754
|
+
}
|
|
17755
|
+
}
|
|
17756
|
+
}
|
|
17757
|
+
} catch {}
|
|
17758
|
+
const compactionConfig = config2.compaction_advisory;
|
|
17759
|
+
if (compactionConfig?.enabled !== false && sessionId_retro) {
|
|
17760
|
+
const session = swarmState.agentSessions.get(sessionId_retro);
|
|
17761
|
+
if (session) {
|
|
17762
|
+
const totalToolCalls = Array.from(swarmState.toolAggregates.values()).reduce((sum, agg) => sum + agg.count, 0);
|
|
17763
|
+
const thresholds = compactionConfig?.thresholds ?? [
|
|
17764
|
+
50,
|
|
17765
|
+
75,
|
|
17766
|
+
100,
|
|
17767
|
+
125,
|
|
17768
|
+
150
|
|
17769
|
+
];
|
|
17770
|
+
const lastHint = session.lastCompactionHint || 0;
|
|
17771
|
+
for (const threshold of thresholds) {
|
|
17772
|
+
if (totalToolCalls >= threshold && lastHint < threshold) {
|
|
17773
|
+
const messageTemplate = compactionConfig?.message ?? "[SWARM HINT] Session has ${totalToolCalls} tool calls. Consider compacting at next phase boundary to maintain context quality.";
|
|
17774
|
+
const message = messageTemplate.replace("${totalToolCalls}", String(totalToolCalls));
|
|
17775
|
+
tryInject(message);
|
|
17776
|
+
session.lastCompactionHint = threshold;
|
|
17777
|
+
break;
|
|
17778
|
+
}
|
|
17779
|
+
}
|
|
17780
|
+
}
|
|
17781
|
+
}
|
|
17782
|
+
}
|
|
17632
17783
|
return;
|
|
17633
17784
|
}
|
|
17634
17785
|
const userScoringConfig = config2.context_budget?.scoring;
|
|
@@ -17752,6 +17903,99 @@ function createSystemEnhancerHook(config2, directory) {
|
|
|
17752
17903
|
metadata: { contentType: "prose" }
|
|
17753
17904
|
});
|
|
17754
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
|
+
}
|
|
17928
|
+
const sessionId_retro_b = _input.sessionID;
|
|
17929
|
+
const activeAgent_retro_b = swarmState.activeAgent.get(sessionId_retro_b ?? "");
|
|
17930
|
+
const isArchitect_b = !activeAgent_retro_b || stripKnownSwarmPrefix(activeAgent_retro_b) === "architect";
|
|
17931
|
+
if (isArchitect_b) {
|
|
17932
|
+
try {
|
|
17933
|
+
const evidenceDir_b = path7.join(directory, ".swarm", "evidence");
|
|
17934
|
+
if (fs3.existsSync(evidenceDir_b)) {
|
|
17935
|
+
const files_b = fs3.readdirSync(evidenceDir_b).filter((f) => f.endsWith(".json")).sort().reverse();
|
|
17936
|
+
for (const file2 of files_b.slice(0, 5)) {
|
|
17937
|
+
const content_b = JSON.parse(fs3.readFileSync(path7.join(evidenceDir_b, file2), "utf-8"));
|
|
17938
|
+
if (content_b.type === "retrospective") {
|
|
17939
|
+
const retro_b = content_b;
|
|
17940
|
+
const hints_b = [];
|
|
17941
|
+
if (retro_b.reviewer_rejections > 2) {
|
|
17942
|
+
hints_b.push(`Phase ${retro_b.phase_number} had ${retro_b.reviewer_rejections} reviewer rejections.`);
|
|
17943
|
+
}
|
|
17944
|
+
if (retro_b.top_rejection_reasons.length > 0) {
|
|
17945
|
+
hints_b.push(`Common rejection reasons: ${retro_b.top_rejection_reasons.join(", ")}.`);
|
|
17946
|
+
}
|
|
17947
|
+
if (retro_b.lessons_learned.length > 0) {
|
|
17948
|
+
hints_b.push(`Lessons: ${retro_b.lessons_learned.join("; ")}.`);
|
|
17949
|
+
}
|
|
17950
|
+
if (hints_b.length > 0) {
|
|
17951
|
+
const retroHint_b = `[SWARM RETROSPECTIVE] From Phase ${retro_b.phase_number}: ${hints_b.join(" ")}`;
|
|
17952
|
+
const retroText = retroHint_b.length <= 800 ? retroHint_b : retroHint_b.substring(0, 800) + "...";
|
|
17953
|
+
candidates.push({
|
|
17954
|
+
id: `candidate-${idCounter++}`,
|
|
17955
|
+
kind: "phase",
|
|
17956
|
+
text: retroText,
|
|
17957
|
+
tokens: estimateTokens(retroText),
|
|
17958
|
+
priority: 2,
|
|
17959
|
+
metadata: { contentType: "prose" }
|
|
17960
|
+
});
|
|
17961
|
+
}
|
|
17962
|
+
break;
|
|
17963
|
+
}
|
|
17964
|
+
}
|
|
17965
|
+
}
|
|
17966
|
+
} catch {}
|
|
17967
|
+
const compactionConfig_b = config2.compaction_advisory;
|
|
17968
|
+
if (compactionConfig_b?.enabled !== false && sessionId_retro_b) {
|
|
17969
|
+
const session_b = swarmState.agentSessions.get(sessionId_retro_b);
|
|
17970
|
+
if (session_b) {
|
|
17971
|
+
const totalToolCalls_b = Array.from(swarmState.toolAggregates.values()).reduce((sum, agg) => sum + agg.count, 0);
|
|
17972
|
+
const thresholds_b = compactionConfig_b?.thresholds ?? [
|
|
17973
|
+
50,
|
|
17974
|
+
75,
|
|
17975
|
+
100,
|
|
17976
|
+
125,
|
|
17977
|
+
150
|
|
17978
|
+
];
|
|
17979
|
+
const lastHint_b = session_b.lastCompactionHint || 0;
|
|
17980
|
+
for (const threshold of thresholds_b) {
|
|
17981
|
+
if (totalToolCalls_b >= threshold && lastHint_b < threshold) {
|
|
17982
|
+
const messageTemplate_b = compactionConfig_b?.message ?? "[SWARM HINT] Session has ${totalToolCalls} tool calls. Consider compacting at next phase boundary to maintain context quality.";
|
|
17983
|
+
const compactionText = messageTemplate_b.replace("${totalToolCalls}", String(totalToolCalls_b));
|
|
17984
|
+
candidates.push({
|
|
17985
|
+
id: `candidate-${idCounter++}`,
|
|
17986
|
+
kind: "phase",
|
|
17987
|
+
text: compactionText,
|
|
17988
|
+
tokens: estimateTokens(compactionText),
|
|
17989
|
+
priority: 1,
|
|
17990
|
+
metadata: { contentType: "prose" }
|
|
17991
|
+
});
|
|
17992
|
+
session_b.lastCompactionHint = threshold;
|
|
17993
|
+
break;
|
|
17994
|
+
}
|
|
17995
|
+
}
|
|
17996
|
+
}
|
|
17997
|
+
}
|
|
17998
|
+
}
|
|
17755
17999
|
const ranked = rankCandidates(candidates, effectiveConfig);
|
|
17756
18000
|
for (const candidate of ranked) {
|
|
17757
18001
|
if (injectedTokens + candidate.tokens > maxInjectionTokens) {
|
|
@@ -18678,10 +18922,10 @@ function mergeDefs2(...defs) {
|
|
|
18678
18922
|
function cloneDef2(schema) {
|
|
18679
18923
|
return mergeDefs2(schema._zod.def);
|
|
18680
18924
|
}
|
|
18681
|
-
function getElementAtPath2(obj,
|
|
18682
|
-
if (!
|
|
18925
|
+
function getElementAtPath2(obj, path8) {
|
|
18926
|
+
if (!path8)
|
|
18683
18927
|
return obj;
|
|
18684
|
-
return
|
|
18928
|
+
return path8.reduce((acc, key) => acc?.[key], obj);
|
|
18685
18929
|
}
|
|
18686
18930
|
function promiseAllObject2(promisesObj) {
|
|
18687
18931
|
const keys = Object.keys(promisesObj);
|
|
@@ -19040,11 +19284,11 @@ function aborted2(x, startIndex = 0) {
|
|
|
19040
19284
|
}
|
|
19041
19285
|
return false;
|
|
19042
19286
|
}
|
|
19043
|
-
function prefixIssues2(
|
|
19287
|
+
function prefixIssues2(path8, issues) {
|
|
19044
19288
|
return issues.map((iss) => {
|
|
19045
19289
|
var _a2;
|
|
19046
19290
|
(_a2 = iss).path ?? (_a2.path = []);
|
|
19047
|
-
iss.path.unshift(
|
|
19291
|
+
iss.path.unshift(path8);
|
|
19048
19292
|
return iss;
|
|
19049
19293
|
});
|
|
19050
19294
|
}
|
|
@@ -19212,7 +19456,7 @@ function treeifyError2(error49, _mapper) {
|
|
|
19212
19456
|
return issue3.message;
|
|
19213
19457
|
};
|
|
19214
19458
|
const result = { errors: [] };
|
|
19215
|
-
const processError = (error50,
|
|
19459
|
+
const processError = (error50, path8 = []) => {
|
|
19216
19460
|
var _a2, _b;
|
|
19217
19461
|
for (const issue3 of error50.issues) {
|
|
19218
19462
|
if (issue3.code === "invalid_union" && issue3.errors.length) {
|
|
@@ -19222,7 +19466,7 @@ function treeifyError2(error49, _mapper) {
|
|
|
19222
19466
|
} else if (issue3.code === "invalid_element") {
|
|
19223
19467
|
processError({ issues: issue3.issues }, issue3.path);
|
|
19224
19468
|
} else {
|
|
19225
|
-
const fullpath = [...
|
|
19469
|
+
const fullpath = [...path8, ...issue3.path];
|
|
19226
19470
|
if (fullpath.length === 0) {
|
|
19227
19471
|
result.errors.push(mapper(issue3));
|
|
19228
19472
|
continue;
|
|
@@ -19254,8 +19498,8 @@ function treeifyError2(error49, _mapper) {
|
|
|
19254
19498
|
}
|
|
19255
19499
|
function toDotPath2(_path) {
|
|
19256
19500
|
const segs = [];
|
|
19257
|
-
const
|
|
19258
|
-
for (const seg of
|
|
19501
|
+
const path8 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
|
|
19502
|
+
for (const seg of path8) {
|
|
19259
19503
|
if (typeof seg === "number")
|
|
19260
19504
|
segs.push(`[${seg}]`);
|
|
19261
19505
|
else if (typeof seg === "symbol")
|
|
@@ -30295,14 +30539,14 @@ function validateBase(base) {
|
|
|
30295
30539
|
function validatePaths(paths) {
|
|
30296
30540
|
if (!paths)
|
|
30297
30541
|
return null;
|
|
30298
|
-
for (const
|
|
30299
|
-
if (!
|
|
30542
|
+
for (const path8 of paths) {
|
|
30543
|
+
if (!path8 || path8.length === 0) {
|
|
30300
30544
|
return "empty path not allowed";
|
|
30301
30545
|
}
|
|
30302
|
-
if (
|
|
30546
|
+
if (path8.length > MAX_PATH_LENGTH) {
|
|
30303
30547
|
return `path exceeds maximum length of ${MAX_PATH_LENGTH}`;
|
|
30304
30548
|
}
|
|
30305
|
-
if (SHELL_METACHARACTERS.test(
|
|
30549
|
+
if (SHELL_METACHARACTERS.test(path8)) {
|
|
30306
30550
|
return "path contains shell metacharacters";
|
|
30307
30551
|
}
|
|
30308
30552
|
}
|
|
@@ -30365,8 +30609,8 @@ var diff = tool({
|
|
|
30365
30609
|
if (parts.length >= 3) {
|
|
30366
30610
|
const additions = parseInt(parts[0]) || 0;
|
|
30367
30611
|
const deletions = parseInt(parts[1]) || 0;
|
|
30368
|
-
const
|
|
30369
|
-
files.push({ path:
|
|
30612
|
+
const path8 = parts[2];
|
|
30613
|
+
files.push({ path: path8, additions, deletions });
|
|
30370
30614
|
}
|
|
30371
30615
|
}
|
|
30372
30616
|
const contractChanges = [];
|
|
@@ -30592,8 +30836,8 @@ Use these as DOMAIN values when delegating to @sme.`;
|
|
|
30592
30836
|
}
|
|
30593
30837
|
});
|
|
30594
30838
|
// src/tools/file-extractor.ts
|
|
30595
|
-
import * as
|
|
30596
|
-
import * as
|
|
30839
|
+
import * as fs4 from "fs";
|
|
30840
|
+
import * as path8 from "path";
|
|
30597
30841
|
var EXT_MAP = {
|
|
30598
30842
|
python: ".py",
|
|
30599
30843
|
py: ".py",
|
|
@@ -30655,8 +30899,8 @@ var extract_code_blocks = tool({
|
|
|
30655
30899
|
execute: async (args) => {
|
|
30656
30900
|
const { content, output_dir, prefix } = args;
|
|
30657
30901
|
const targetDir = output_dir || process.cwd();
|
|
30658
|
-
if (!
|
|
30659
|
-
|
|
30902
|
+
if (!fs4.existsSync(targetDir)) {
|
|
30903
|
+
fs4.mkdirSync(targetDir, { recursive: true });
|
|
30660
30904
|
}
|
|
30661
30905
|
const pattern = /```(\w*)\n([\s\S]*?)```/g;
|
|
30662
30906
|
const matches = [...content.matchAll(pattern)];
|
|
@@ -30671,16 +30915,16 @@ var extract_code_blocks = tool({
|
|
|
30671
30915
|
if (prefix) {
|
|
30672
30916
|
filename = `${prefix}_${filename}`;
|
|
30673
30917
|
}
|
|
30674
|
-
let filepath =
|
|
30675
|
-
const base =
|
|
30676
|
-
const ext =
|
|
30918
|
+
let filepath = path8.join(targetDir, filename);
|
|
30919
|
+
const base = path8.basename(filepath, path8.extname(filepath));
|
|
30920
|
+
const ext = path8.extname(filepath);
|
|
30677
30921
|
let counter = 1;
|
|
30678
|
-
while (
|
|
30679
|
-
filepath =
|
|
30922
|
+
while (fs4.existsSync(filepath)) {
|
|
30923
|
+
filepath = path8.join(targetDir, `${base}_${counter}${ext}`);
|
|
30680
30924
|
counter++;
|
|
30681
30925
|
}
|
|
30682
30926
|
try {
|
|
30683
|
-
|
|
30927
|
+
fs4.writeFileSync(filepath, code.trim(), "utf-8");
|
|
30684
30928
|
savedFiles.push(filepath);
|
|
30685
30929
|
} catch (error93) {
|
|
30686
30930
|
errors5.push(`Failed to save ${filename}: ${error93 instanceof Error ? error93.message : String(error93)}`);
|
|
@@ -30785,6 +31029,510 @@ var gitingest = tool({
|
|
|
30785
31029
|
return fetchGitingest(args);
|
|
30786
31030
|
}
|
|
30787
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
|
+
});
|
|
30788
31536
|
// src/tools/retrieve-summary.ts
|
|
30789
31537
|
var RETRIEVE_MAX_BYTES = 10 * 1024 * 1024;
|
|
30790
31538
|
var retrieve_summary = tool({
|
|
@@ -30815,6 +31563,643 @@ var retrieve_summary = tool({
|
|
|
30815
31563
|
return fullOutput;
|
|
30816
31564
|
}
|
|
30817
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
|
+
});
|
|
30818
32203
|
// src/index.ts
|
|
30819
32204
|
var OpenCodeSwarm = async (ctx) => {
|
|
30820
32205
|
const { config: config3, loadedFromFile } = loadPluginConfigWithMeta(ctx.directory);
|
|
@@ -30857,8 +32242,11 @@ var OpenCodeSwarm = async (ctx) => {
|
|
|
30857
32242
|
detect_domains,
|
|
30858
32243
|
extract_code_blocks,
|
|
30859
32244
|
gitingest,
|
|
32245
|
+
imports,
|
|
32246
|
+
lint,
|
|
30860
32247
|
diff,
|
|
30861
|
-
retrieve_summary
|
|
32248
|
+
retrieve_summary,
|
|
32249
|
+
secretscan
|
|
30862
32250
|
},
|
|
30863
32251
|
config: async (opencodeConfig) => {
|
|
30864
32252
|
if (!opencodeConfig.agent) {
|