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/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 review \u2192 security review \u2192 verification tests \u2192 adversarial tests \u2192 next task.
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. {{AGENT_PREFIX}}reviewer - General review. REJECTED (< {{QA_RETRY_LIMIT}}) \u2192 coder retry. REJECTED ({{QA_RETRY_LIMIT}}) \u2192 escalate.
14296
- 5e. Security gate: if file matches security globs or content has security keywords \u2192 {{AGENT_PREFIX}}reviewer security-only. REJECTED \u2192 coder retry.
14297
- 5f. {{AGENT_PREFIX}}test_engineer - Verification tests. FAIL \u2192 coder retry from 5d.
14298
- 5g. {{AGENT_PREFIX}}test_engineer - Adversarial tests. FAIL \u2192 coder retry from 5d.
14299
- 5h. Update plan.md [x], proceed to next task.
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. Summarize to user
14309
- 5. Ask: "Ready for Phase [N+1]?"
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, path7) {
18682
- if (!path7)
18925
+ function getElementAtPath2(obj, path8) {
18926
+ if (!path8)
18683
18927
  return obj;
18684
- return path7.reduce((acc, key) => acc?.[key], obj);
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(path7, issues) {
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(path7);
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, path7 = []) => {
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 = [...path7, ...issue3.path];
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 path7 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
19258
- for (const seg of path7) {
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 path7 of paths) {
30299
- if (!path7 || path7.length === 0) {
30542
+ for (const path8 of paths) {
30543
+ if (!path8 || path8.length === 0) {
30300
30544
  return "empty path not allowed";
30301
30545
  }
30302
- if (path7.length > MAX_PATH_LENGTH) {
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(path7)) {
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 path7 = parts[2];
30369
- files.push({ path: path7, additions, deletions });
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 fs3 from "fs";
30596
- import * as path7 from "path";
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 (!fs3.existsSync(targetDir)) {
30659
- fs3.mkdirSync(targetDir, { recursive: true });
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 = path7.join(targetDir, filename);
30675
- const base = path7.basename(filepath, path7.extname(filepath));
30676
- const ext = path7.extname(filepath);
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 (fs3.existsSync(filepath)) {
30679
- filepath = path7.join(targetDir, `${base}_${counter}${ext}`);
30922
+ while (fs4.existsSync(filepath)) {
30923
+ filepath = path8.join(targetDir, `${base}_${counter}${ext}`);
30680
30924
  counter++;
30681
30925
  }
30682
30926
  try {
30683
- fs3.writeFileSync(filepath, code.trim(), "utf-8");
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) {