@wrongstack/tools 0.155.0 → 0.250.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.
Files changed (71) hide show
  1. package/dist/audit.js +22 -1
  2. package/dist/audit.js.map +1 -1
  3. package/dist/background-indexer-DwJsyAB0.d.ts +373 -0
  4. package/dist/bash.js +121 -24
  5. package/dist/bash.js.map +1 -1
  6. package/dist/builtin.js +1553 -544
  7. package/dist/builtin.js.map +1 -1
  8. package/dist/circuit-breaker.d.ts +9 -2
  9. package/dist/circuit-breaker.js +11 -2
  10. package/dist/circuit-breaker.js.map +1 -1
  11. package/dist/codebase-index/index.d.ts +53 -2
  12. package/dist/codebase-index/index.js +866 -367
  13. package/dist/codebase-index/index.js.map +1 -1
  14. package/dist/codebase-index/worker.d.ts +2 -0
  15. package/dist/codebase-index/worker.js +2321 -0
  16. package/dist/codebase-index/worker.js.map +1 -0
  17. package/dist/diff.js +3 -2
  18. package/dist/diff.js.map +1 -1
  19. package/dist/document.js +1 -1
  20. package/dist/document.js.map +1 -1
  21. package/dist/edit.js +1 -1
  22. package/dist/edit.js.map +1 -1
  23. package/dist/exec.js +61 -11
  24. package/dist/exec.js.map +1 -1
  25. package/dist/fetch.js.map +1 -1
  26. package/dist/format.js +22 -1
  27. package/dist/format.js.map +1 -1
  28. package/dist/git.js +2 -1
  29. package/dist/git.js.map +1 -1
  30. package/dist/glob.js +1 -1
  31. package/dist/glob.js.map +1 -1
  32. package/dist/grep.js +3 -3
  33. package/dist/grep.js.map +1 -1
  34. package/dist/index.d.ts +5 -4
  35. package/dist/index.js +1593 -622
  36. package/dist/index.js.map +1 -1
  37. package/dist/install.js +66 -14
  38. package/dist/install.js.map +1 -1
  39. package/dist/lint.js +22 -1
  40. package/dist/lint.js.map +1 -1
  41. package/dist/logs.js +2 -2
  42. package/dist/logs.js.map +1 -1
  43. package/dist/outdated.js +2 -2
  44. package/dist/outdated.js.map +1 -1
  45. package/dist/pack.js +1553 -544
  46. package/dist/pack.js.map +1 -1
  47. package/dist/patch.js +2 -2
  48. package/dist/patch.js.map +1 -1
  49. package/dist/process-registry.d.ts +21 -16
  50. package/dist/process-registry.js +48 -10
  51. package/dist/process-registry.js.map +1 -1
  52. package/dist/read.js +1 -1
  53. package/dist/read.js.map +1 -1
  54. package/dist/replace.js +4 -3
  55. package/dist/replace.js.map +1 -1
  56. package/dist/scaffold.js +1 -1
  57. package/dist/scaffold.js.map +1 -1
  58. package/dist/search.js +19 -16
  59. package/dist/search.js.map +1 -1
  60. package/dist/test.js +22 -1
  61. package/dist/test.js.map +1 -1
  62. package/dist/todo.js +44 -0
  63. package/dist/todo.js.map +1 -1
  64. package/dist/tree.js +1 -1
  65. package/dist/tree.js.map +1 -1
  66. package/dist/typecheck.js +22 -1
  67. package/dist/typecheck.js.map +1 -1
  68. package/dist/write.js +1 -1
  69. package/dist/write.js.map +1 -1
  70. package/package.json +5 -5
  71. package/dist/background-indexer-CtbgPExj.d.ts +0 -228
package/dist/audit.js CHANGED
@@ -29,6 +29,7 @@ function resolveWin32Command(cmd) {
29
29
  async function* spawnStream(opts) {
30
30
  const max = opts.maxBytes;
31
31
  const flushAt = opts.flushBytes ?? 4 * 1024;
32
+ const maxQueue = opts.maxQueueSize ?? 500;
32
33
  let stdout = "";
33
34
  let stderr = "";
34
35
  let pending = "";
@@ -40,10 +41,12 @@ async function* spawnStream(opts) {
40
41
  signal: opts.signal,
41
42
  env: buildChildEnv(),
42
43
  stdio: ["ignore", "pipe", "pipe"],
44
+ windowsHide: true,
43
45
  ...needsShell ? { shell: true, windowsVerbatimArguments: true } : {}
44
46
  });
45
47
  const queue = [];
46
48
  let waiter;
49
+ let paused = false;
47
50
  const wake = () => {
48
51
  if (waiter) {
49
52
  const w = waiter;
@@ -51,17 +54,34 @@ async function* spawnStream(opts) {
51
54
  w();
52
55
  }
53
56
  };
57
+ const resume = () => {
58
+ if (paused && queue.length < maxQueue) {
59
+ paused = false;
60
+ child.stdout?.resume();
61
+ child.stderr?.resume();
62
+ }
63
+ };
54
64
  child.stdout?.on("data", (c) => {
55
65
  const s = c.toString();
56
66
  if (stdout.length < max) stdout += s;
57
67
  queue.push({ kind: "out", data: s });
58
68
  wake();
69
+ if (!paused && queue.length >= maxQueue) {
70
+ paused = true;
71
+ child.stdout?.pause();
72
+ child.stderr?.pause();
73
+ }
59
74
  });
60
75
  child.stderr?.on("data", (c) => {
61
76
  const s = c.toString();
62
77
  if (stderr.length < max) stderr += s;
63
78
  queue.push({ kind: "err", data: s });
64
79
  wake();
80
+ if (!paused && queue.length >= maxQueue) {
81
+ paused = true;
82
+ child.stdout?.pause();
83
+ child.stderr?.pause();
84
+ }
65
85
  });
66
86
  child.on("error", (e) => {
67
87
  error = e.message;
@@ -81,6 +101,7 @@ async function* spawnStream(opts) {
81
101
  });
82
102
  }
83
103
  const chunk = queue.shift();
104
+ resume();
84
105
  if (chunk.kind === "close") {
85
106
  if (!spawnFailed) exitCode = chunk.code ?? 0;
86
107
  break;
@@ -122,7 +143,7 @@ async function detectPackageManager(cwd) {
122
143
  return "npm";
123
144
  }
124
145
  function resolvePath(input, ctx) {
125
- return path2.isAbsolute(input) ? path2.normalize(input) : path2.resolve(ctx.cwd, input);
146
+ return path2.isAbsolute(input) ? path2.normalize(input) : path2.resolve(ctx.workingDir ?? ctx.cwd, input);
126
147
  }
127
148
  function ensureInsideRoot(absPath, ctx) {
128
149
  const root = path2.resolve(ctx.projectRoot);
package/dist/audit.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/_win32-resolve.ts","../src/_spawn-stream.ts","../src/_util.ts","../src/audit.ts"],"names":["path","resolve"],"mappings":"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","file":"audit.js","sourcesContent":["import * as fs from 'node:fs';\nimport * as path from 'node:path';\n\n/**\n * On Windows, Node.js `spawn()` without a shell does NOT resolve .cmd/.bat\n * extensions through PATHEXT — it only auto-resolves .exe. Most Node.js CLI\n * tools (npx, pnpm, biome, tsc, vitest, etc.) ship as .cmd wrappers on\n * Windows. This function resolves the command name to its full path so spawn\n * can find it without relying on shell-mode argument concatenation.\n *\n * On non-Windows, returns the command unchanged.\n */\nexport function resolveWin32Command(cmd: string): string {\n if (process.platform !== 'win32') return cmd;\n\n // Already has a path or extension — use as-is\n if (cmd.includes('/') || cmd.includes('\\\\') || path.extname(cmd)) {\n return cmd;\n }\n\n const pathext = (process.env['PATHEXT'] ?? '.COM;.EXE;.BAT;.CMD;.VBS;.JS;.WS;.MSC')\n .toLowerCase()\n .split(';');\n\n const pathDirs = (process.env['PATH'] ?? '').split(path.delimiter);\n\n for (const dir of pathDirs) {\n const base = path.join(dir, cmd);\n // Check extensions in PATHEXT order. .EXE should win first because\n // it's typically listed first, and .exe doesn't need shell: true.\n for (const ext of pathext) {\n const full = `${base}${ext}`;\n try {\n fs.accessSync(full, fs.constants.X_OK);\n return full;\n } catch {\n // Not found with this extension — try next\n }\n }\n }\n\n // Not found — return original; let spawn report ENOENT with the\n // expected error message so tools can surface it properly.\n return cmd;\n}\n","import { spawn } from 'node:child_process';\nimport { buildChildEnv } from '@wrongstack/core';\nimport type { ToolProgressEvent } from '@wrongstack/core';\nimport { resolveWin32Command } from './_win32-resolve.js';\nexport interface SpawnStreamResult {\n stdout: string;\n stderr: string;\n exitCode: number;\n truncated: boolean;\n error?: string | undefined;\n}\n\nexport interface SpawnStreamOptions {\n cmd: string;\n args: string[];\n cwd: string;\n signal: AbortSignal;\n maxBytes?: number | undefined;\n /** Bytes of new stdout/stderr to accumulate before yielding a `partial_output` event. */\n flushBytes?: number | undefined;\n}\n\n/**\n * Spawn a child process and yield `partial_output` progress events as\n * stdout/stderr arrive (batched by byte threshold), then return the full\n * buffered result. Shared between install/lint/format/typecheck/test/audit\n * so the TUI live tail sees consistent progress regardless of which tool\n * is running.\n */\nexport async function* spawnStream(\n opts: SpawnStreamOptions,\n): AsyncGenerator<ToolProgressEvent, SpawnStreamResult> {\n const max = opts.maxBytes ?? 200_000;\n const flushAt = opts.flushBytes ?? 4 * 1024;\n let stdout = '';\n let stderr = '';\n let pending = '';\n let error: string | undefined;\n\n const cmd = resolveWin32Command(opts.cmd);\n const needsShell = process.platform === 'win32' && (cmd.endsWith('.cmd') || cmd.endsWith('.bat'));\n\n const child = spawn(cmd, opts.args, {\n cwd: opts.cwd,\n signal: opts.signal,\n env: buildChildEnv(),\n stdio: ['ignore', 'pipe', 'pipe'],\n ...(needsShell ? { shell: true, windowsVerbatimArguments: true } : {}),\n });\n\n type Chunk = { kind: 'out' | 'err' | 'close' | 'error'; data: string; code?: number | undefined };\n const queue: Chunk[] = [];\n let waiter: (() => void) | undefined;\n const wake = () => {\n if (waiter) {\n const w = waiter;\n waiter = undefined;\n w();\n }\n };\n\n child.stdout?.on('data', (c) => {\n const s = c.toString();\n if (stdout.length < max) stdout += s;\n queue.push({ kind: 'out', data: s });\n wake();\n });\n child.stderr?.on('data', (c) => {\n const s = c.toString();\n if (stderr.length < max) stderr += s;\n queue.push({ kind: 'err', data: s });\n wake();\n });\n child.on('error', (e) => {\n error = e.message;\n queue.push({ kind: 'error', data: e.message });\n wake();\n });\n child.on('close', (code) => {\n queue.push({ kind: 'close', data: '', code: code ?? 0 });\n wake();\n });\n\n let exitCode = 0;\n let spawnFailed = false;\n for (;;) {\n while (queue.length === 0) {\n await new Promise<void>((resolve) => {\n waiter = resolve;\n });\n }\n const chunk = queue.shift()!;\n if (chunk.kind === 'close') {\n // If we already saw a spawn error (ENOENT etc.), keep exitCode=1\n // rather than the negative platform code Node fabricates.\n if (!spawnFailed) exitCode = chunk.code ?? 0;\n break;\n }\n if (chunk.kind === 'error') {\n spawnFailed = true;\n exitCode = 1;\n // close usually follows\n continue;\n }\n pending += chunk.data;\n if (pending.length >= flushAt) {\n yield { type: 'partial_output', text: pending };\n pending = '';\n }\n }\n if (pending.length > 0) {\n yield { type: 'partial_output', text: pending };\n }\n\n return {\n stdout,\n stderr,\n exitCode,\n truncated: stdout.length >= max || stderr.length >= max,\n error,\n };\n}\n","import * as fsp from 'node:fs/promises';\nimport * as path from 'node:path';\nimport * as Core from '@wrongstack/core';\nimport type { Context } from '@wrongstack/core';\n/** Detected package manager for a project directory. */\nexport type PackageManager = 'pnpm' | 'yarn' | 'npm';\n\n/**\n * Detect the project's package manager by inspecting lockfiles in `cwd`.\n * Order: pnpm → yarn → npm (default). Missing or unreadable directories fall\n * back to `npm` rather than throwing, so a `safeResolve`-checked cwd that\n * happens to be empty never aborts the tool.\n */\nexport async function detectPackageManager(cwd: string): Promise<PackageManager> {\n const { stat } = await import('node:fs/promises');\n try {\n await stat(`${cwd}/pnpm-lock.yaml`);\n return 'pnpm';\n } catch {\n /* not pnpm */\n }\n try {\n await stat(`${cwd}/yarn.lock`);\n return 'yarn';\n } catch {\n /* not yarn */\n }\n return 'npm';\n}\n\nexport function resolvePath(input: string, ctx: Context): string {\n return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.cwd, input);\n}\n\nexport function ensureInsideRoot(absPath: string, ctx: Context): string {\n const root = path.resolve(ctx.projectRoot);\n const target = path.resolve(absPath);\n const rel = path.relative(root, target);\n if (rel.startsWith('..') || path.isAbsolute(rel)) {\n throw new Error(`Path \"${absPath}\" is outside project root \"${root}\"`);\n }\n return target;\n}\n\nexport function safeResolve(input: string, ctx: Context): string {\n return ensureInsideRoot(resolvePath(input, ctx), ctx);\n}\n\n/**\n * Defense against in-root→out-of-root symlink escape (CWE-59). `safeResolve`\n * only does a syntactic `../` check, so a symlink that lives *inside* the\n * project root but points outside still passes it. This resolves the path\n * through `fs.realpath` and re-verifies containment against the realpath of\n * the project root (comparing like-for-like, since the root itself may be a\n * symlink — macOS `/var`→`/private/var`, Windows 8.3 short names). For a path\n * that does not exist yet (e.g. a `write` to a new file) the nearest existing\n * ancestor directory is checked instead. Throws if the real target escapes.\n *\n * Mirrors the per-file guard already used in `replace.ts`/`grep.ts`; applied\n * to single-file `read`/`edit`/`write` it throws (rather than skips) because\n * the caller named exactly one file.\n */\nexport async function assertRealInsideRoot(absPath: string, ctx: Context): Promise<void> {\n const realRoot = await fsp.realpath(ctx.projectRoot).catch(() => path.resolve(ctx.projectRoot));\n let probe = absPath;\n for (;;) {\n let real: string;\n try {\n real = await fsp.realpath(probe);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') {\n const parent = path.dirname(probe);\n if (parent === probe) return; // reached fs root without escaping\n probe = parent;\n continue;\n }\n throw err;\n }\n const rel = path.relative(realRoot, real);\n if (rel.startsWith('..') || path.isAbsolute(rel)) {\n throw new Error(\n `Path \"${absPath}\" resolves through a symlink outside project root \"${realRoot}\"`,\n );\n }\n return;\n }\n}\n\n/** `safeResolve` + symlink realpath containment check. Async. */\nexport async function safeResolveReal(input: string, ctx: Context): Promise<string> {\n const abs = safeResolve(input, ctx);\n await assertRealInsideRoot(abs, ctx);\n return abs;\n}\n\nexport function truncateMiddle(s: string, max: number): string {\n if (Buffer.byteLength(s, 'utf8') <= max) return s;\n const half = Math.floor(max / 2);\n return (\n s.slice(0, half) +\n `\\n…[truncated ${Buffer.byteLength(s, 'utf8') - max} bytes from middle]…\\n` +\n s.slice(-half)\n );\n}\n\nexport function isBinaryBuffer(buf: Buffer): boolean {\n const len = Math.min(buf.length, 8192);\n for (let i = 0; i < len; i++) {\n if (buf[i] === 0) return true;\n }\n return false;\n}\n\n// ─── Command-output normalization (token-saving) ────────────────────────────\n//\n// Raw process output is full of tokens the model gains nothing from: ANSI\n// escapes, carriage-return progress spam, runs of identical warning lines, and\n// huge tails of build noise. These helpers strip that noise before the output\n// reaches the LLM. They are scoped to COMMAND tools (bash/git/exec and the\n// _spawn-stream consumers) — never applied to structured/code outputs.\n\n/** Unified byte cap for all command tool output fed to the model. */\nexport const COMMAND_OUTPUT_MAX_BYTES = 32_768;\n\n/** Runs of >= this many identical consecutive lines are collapsed. */\nconst REPEAT_RUN_THRESHOLD = 3;\n\n/**\n * Collapse carriage-return overwrites the way a terminal would: `\\r\\n` becomes\n * `\\n`, and a bare `\\r` (progress redraw) keeps only the text after the LAST\n * `\\r` on its physical line. Without this, a single progress bar that redraws\n * 200 times explodes into 200 lines.\n */\nexport function collapseCarriageReturns(text: string): string {\n const lf = text.replace(/\\r\\n/g, '\\n');\n if (!lf.includes('\\r')) return lf;\n return lf\n .split('\\n')\n .map((line) => (line.includes('\\r') ? line.slice(line.lastIndexOf('\\r') + 1) : line))\n .join('\\n');\n}\n\n/**\n * Collapse a run of `minRun`+ identical consecutive lines into the line once\n * plus a marker. Consecutive-only — it never reorders or dedups non-adjacent\n * lines, so diffs/source stay intact.\n */\nexport function collapseConsecutiveDuplicates(text: string, minRun = REPEAT_RUN_THRESHOLD): string {\n const lines = text.split('\\n');\n const out: string[] = [];\n let i = 0;\n while (i < lines.length) {\n let j = i + 1;\n while (j < lines.length && lines[j] === lines[i]) j++;\n const run = j - i;\n if (run >= minRun) {\n out.push(lines[i]!, `… ⟨repeated ${run}×⟩`);\n } else {\n for (let k = i; k < j; k++) out.push(lines[k]!);\n }\n i = j;\n }\n return out.join('\\n');\n}\n\n/** Largest prefix of `s` whose UTF-8 byte length is <= `maxBytes`. */\nfunction takeHeadBytes(s: string, maxBytes: number): string {\n if (maxBytes <= 0) return '';\n if (Buffer.byteLength(s, 'utf8') <= maxBytes) return s;\n let lo = 0;\n let hi = s.length;\n while (lo < hi) {\n const mid = Math.ceil((lo + hi) / 2);\n if (Buffer.byteLength(s.slice(0, mid), 'utf8') <= maxBytes) lo = mid;\n else hi = mid - 1;\n }\n return s.slice(0, lo);\n}\n\n/** Largest suffix of `s` whose UTF-8 byte length is <= `maxBytes`. */\nfunction takeTailBytes(s: string, maxBytes: number): string {\n if (maxBytes <= 0) return '';\n if (Buffer.byteLength(s, 'utf8') <= maxBytes) return s;\n let lo = 0;\n let hi = s.length;\n while (lo < hi) {\n const mid = Math.ceil((lo + hi) / 2);\n if (Buffer.byteLength(s.slice(s.length - mid), 'utf8') <= maxBytes) lo = mid;\n else hi = mid - 1;\n }\n return s.slice(s.length - lo);\n}\n\n/**\n * Truncate to `maxBytes` keeping BOTH ends — the head (what ran / early context)\n * and the tail (errors and summaries usually land last), biased ~45/55 toward\n * the tail. The result never exceeds `maxBytes`.\n */\nexport function truncateHeadTail(s: string, maxBytes: number): string {\n const total = Buffer.byteLength(s, 'utf8');\n if (total <= maxBytes) return s;\n // Reserve a fixed allowance for the marker so the final string can't exceed\n // the cap even though the dropped-byte count's digit width varies.\n const MARKER_RESERVE = 64;\n const avail = Math.max(0, maxBytes - MARKER_RESERVE);\n const headBudget = Math.floor(avail * 0.45);\n const head = takeHeadBytes(s, headBudget);\n const tail = takeTailBytes(s, avail - Buffer.byteLength(head, 'utf8'));\n const kept = Buffer.byteLength(head, 'utf8') + Buffer.byteLength(tail, 'utf8');\n return `${head}\\n…[truncated ${total - kept} bytes]…\\n${tail}`;\n}\n\n/**\n * Full token-saving pipeline for command tool output: strip ANSI → collapse\n * carriage-return progress → trim trailing whitespace → collapse identical\n * consecutive lines → squeeze blank-line runs → head+tail truncate to the cap.\n */\nexport function normalizeCommandOutput(\n raw: string,\n opts: { maxBytes?: number | undefined } = {},\n): string {\n if (!raw) return raw;\n let text = Core.stripAnsi(raw);\n text = collapseCarriageReturns(text);\n text = text.replace(/[ \\t]+$/gm, ''); // trailing whitespace per line\n text = collapseConsecutiveDuplicates(text);\n text = text.replace(/\\n{3,}/g, '\\n\\n'); // >=2 blank lines → 1\n return truncateHeadTail(text, opts.maxBytes ?? COMMAND_OUTPUT_MAX_BYTES);\n}\n","import type { Tool, ToolStreamEvent } from '@wrongstack/core';\nimport { spawnStream } from './_spawn-stream.js';\nimport { detectPackageManager, safeResolve } from './_util.js';\n\ninterface AuditInput {\n cwd?: string | undefined;\n level?: 'low' | 'moderate' | 'high' | 'critical' | undefined;\n fix?: boolean | undefined;\n packages?: string | string[] | undefined;\n}\n\ninterface AuditVulnerability {\n severity: string;\n package: string;\n title: string;\n url: string;\n}\n\ninterface AuditOutput {\n exit_code: number;\n vulnerabilities: AuditVulnerability[];\n total: number;\n summary: string;\n output: string;\n truncated: boolean;\n}\n\nexport const auditTool: Tool<AuditInput, AuditOutput> = {\n name: 'audit',\n category: 'Package Management',\n description:\n 'Run a security audit against project dependencies (using pnpm/npm audit). Reports known vulnerabilities with severity.',\n usageHint:\n 'CRITICAL SECURITY TOOL:\\n\\n' +\n '- Run regularly and especially before any release.\\n' +\n '- Use `level` to focus on high/critical issues.\\n' +\n '- `fix` can attempt automatic remediation for some vulnerabilities.\\n' +\n 'This is one of the most important tools for supply chain security.',\n permission: 'confirm',\n mutating: false,\n timeoutMs: 60_000,\n inputSchema: {\n type: 'object',\n properties: {\n cwd: { type: 'string', description: 'Working directory (default: cwd)' },\n level: {\n type: 'string',\n enum: ['low', 'moderate', 'high', 'critical'],\n description: 'Minimum severity level to report',\n },\n fix: { type: 'boolean', description: 'Attempt to fix vulnerabilities (default: false)' },\n packages: { type: 'string', description: 'Specific package(s) to audit (comma-separated)' },\n },\n },\n async execute(input, ctx, opts) {\n let final: AuditOutput | undefined;\n const executeStream = auditTool.executeStream;\n if (!executeStream) throw new Error('auditTool: stream execution unavailable');\n for await (const ev of executeStream(input, ctx, opts)) {\n if (ev.type === 'final') final = ev.output;\n }\n if (!final) throw new Error('audit: stream ended without final event');\n return final;\n },\n async *executeStream(input, ctx, opts): AsyncGenerator<ToolStreamEvent<AuditOutput>> {\n const cwd = input.cwd ? safeResolve(input.cwd, ctx) : ctx.cwd;\n const manager = await detectPackageManager(cwd);\n yield { type: 'log', text: `Auditing with ${manager}…`, data: { manager } };\n\n const args = ['audit', '--json'];\n if (input.fix) args.push('--fix');\n if (input.packages) {\n const pkgs = Array.isArray(input.packages) ? input.packages : input.packages.split(',');\n args.push(...pkgs.map((p: string) => p.trim()));\n }\n\n const result = yield* spawnStream({\n cmd: manager,\n args,\n cwd,\n signal: opts.signal,\n maxBytes: 100_000,\n });\n\n yield { type: 'final', output: parseAuditOutput(result.stdout, result.exitCode) };\n },\n};\n\nfunction parseAuditOutput(json: string, exitCode: number): AuditOutput {\n if (!json) {\n return {\n exit_code: exitCode,\n vulnerabilities: [],\n total: 0,\n summary: exitCode === 0 ? 'No vulnerabilities found' : 'Audit failed',\n output: '',\n truncated: false,\n };\n }\n\n try {\n const data = JSON.parse(json);\n const advisories: AuditVulnerability[] = [];\n const ads = data.advisories ?? {};\n for (const id of Object.keys(ads)) {\n const adv = ads[id];\n advisories.push({\n severity: adv.severity ?? 'unknown',\n package: adv.module_name ?? id,\n title: adv.title ?? 'Unknown vulnerability',\n url: adv.url ?? '',\n });\n }\n\n const total = advisories.length;\n const summary =\n total === 0\n ? 'No vulnerabilities found'\n : `Found ${total} vulnerabilities: ${advisories.filter((a) => a.severity === 'critical').length} critical, ${advisories.filter((a) => a.severity === 'high').length} high`;\n\n return {\n exit_code: exitCode,\n vulnerabilities: advisories,\n total,\n summary,\n output: json,\n truncated: json.length >= 100_000,\n };\n } catch {\n return {\n exit_code: exitCode,\n vulnerabilities: [],\n total: 0,\n summary: 'Could not parse audit output',\n output: json,\n truncated: false,\n };\n }\n}\n"]}
1
+ {"version":3,"sources":["../src/_win32-resolve.ts","../src/_spawn-stream.ts","../src/_util.ts","../src/audit.ts"],"names":["path","resolve"],"mappings":"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","file":"audit.js","sourcesContent":["import * as fs from 'node:fs';\nimport * as path from 'node:path';\n\n/**\n * On Windows, Node.js `spawn()` without a shell does NOT resolve .cmd/.bat\n * extensions through PATHEXT — it only auto-resolves .exe. Most Node.js CLI\n * tools (npx, pnpm, biome, tsc, vitest, etc.) ship as .cmd wrappers on\n * Windows. This function resolves the command name to its full path so spawn\n * can find it without relying on shell-mode argument concatenation.\n *\n * On non-Windows, returns the command unchanged.\n */\nexport function resolveWin32Command(cmd: string): string {\n if (process.platform !== 'win32') return cmd;\n\n // Already has a path or extension — use as-is\n if (cmd.includes('/') || cmd.includes('\\\\') || path.extname(cmd)) {\n return cmd;\n }\n\n const pathext = (process.env['PATHEXT'] ?? '.COM;.EXE;.BAT;.CMD;.VBS;.JS;.WS;.MSC')\n .toLowerCase()\n .split(';');\n\n const pathDirs = (process.env['PATH'] ?? '').split(path.delimiter);\n\n for (const dir of pathDirs) {\n const base = path.join(dir, cmd);\n // Check extensions in PATHEXT order. .EXE should win first because\n // it's typically listed first, and .exe doesn't need shell: true.\n for (const ext of pathext) {\n const full = `${base}${ext}`;\n try {\n fs.accessSync(full, fs.constants.X_OK);\n return full;\n } catch {\n // Not found with this extension — try next\n }\n }\n }\n\n // Not found — return original; let spawn report ENOENT with the\n // expected error message so tools can surface it properly.\n return cmd;\n}\n","import { spawn } from 'node:child_process';\nimport { buildChildEnv } from '@wrongstack/core';\nimport type { ToolProgressEvent } from '@wrongstack/core';\nimport { resolveWin32Command } from './_win32-resolve.js';\nexport interface SpawnStreamResult {\n stdout: string;\n stderr: string;\n exitCode: number;\n truncated: boolean;\n error?: string | undefined;\n}\n\nexport interface SpawnStreamOptions {\n cmd: string;\n args: string[];\n cwd: string;\n signal: AbortSignal;\n maxBytes?: number | undefined;\n /** Bytes of new stdout/stderr to accumulate before yielding a `partial_output` event. */\n flushBytes?: number | undefined;\n /** Maximum chunks to buffer before applying backpressure to the child. Default 500. */\n maxQueueSize?: number | undefined;\n}\n\n/**\n * Spawn a child process and yield `partial_output` progress events as\n * stdout/stderr arrive (batched by byte threshold), then return the full\n * buffered result. Shared between install/lint/format/typecheck/test/audit\n * so the TUI live tail sees consistent progress regardless of which tool\n * is running.\n */\nexport async function* spawnStream(\n opts: SpawnStreamOptions,\n): AsyncGenerator<ToolProgressEvent, SpawnStreamResult> {\n const max = opts.maxBytes ?? 200_000;\n const flushAt = opts.flushBytes ?? 4 * 1024;\n const maxQueue = opts.maxQueueSize ?? 500;\n let stdout = '';\n let stderr = '';\n let pending = '';\n let error: string | undefined;\n\n const cmd = resolveWin32Command(opts.cmd);\n const needsShell = process.platform === 'win32' && (cmd.endsWith('.cmd') || cmd.endsWith('.bat'));\n\n const child = spawn(cmd, opts.args, {\n cwd: opts.cwd,\n signal: opts.signal,\n env: buildChildEnv(),\n stdio: ['ignore', 'pipe', 'pipe'],\n windowsHide: true,\n ...(needsShell ? { shell: true, windowsVerbatimArguments: true } : {}),\n });\n\n type Chunk = { kind: 'out' | 'err' | 'close' | 'error'; data: string; code?: number | undefined };\n const queue: Chunk[] = [];\n let waiter: (() => void) | undefined;\n let paused = false;\n const wake = () => {\n if (waiter) {\n const w = waiter;\n waiter = undefined;\n w();\n }\n };\n\n // Resume the stream when there's room in the queue\n const resume = () => {\n if (paused && queue.length < maxQueue) {\n paused = false;\n child.stdout?.resume();\n child.stderr?.resume();\n }\n };\n\n // Note: chunks may still arrive briefly after pause() (already in flight) —\n // they are accumulated and queued rather than dropped, so the queue can\n // overshoot maxQueue by a few entries but no output is silently lost.\n child.stdout?.on('data', (c) => {\n const s = c.toString();\n if (stdout.length < max) stdout += s;\n queue.push({ kind: 'out', data: s });\n wake();\n // Apply backpressure if queue is growing faster than we consume\n if (!paused && queue.length >= maxQueue) {\n paused = true;\n child.stdout?.pause();\n child.stderr?.pause();\n }\n });\n child.stderr?.on('data', (c) => {\n const s = c.toString();\n if (stderr.length < max) stderr += s;\n queue.push({ kind: 'err', data: s });\n wake();\n if (!paused && queue.length >= maxQueue) {\n paused = true;\n child.stdout?.pause();\n child.stderr?.pause();\n }\n });\n child.on('error', (e) => {\n error = e.message;\n queue.push({ kind: 'error', data: e.message });\n wake();\n });\n child.on('close', (code) => {\n queue.push({ kind: 'close', data: '', code: code ?? 0 });\n wake();\n });\n\n let exitCode = 0;\n let spawnFailed = false;\n for (;;) {\n while (queue.length === 0) {\n await new Promise<void>((resolve) => {\n waiter = resolve;\n });\n }\n const chunk = queue.shift()!;\n // Resume reading after consuming a chunk\n resume();\n if (chunk.kind === 'close') {\n // If we already saw a spawn error (ENOENT etc.), keep exitCode=1\n // rather than the negative platform code Node fabricates.\n if (!spawnFailed) exitCode = chunk.code ?? 0;\n break;\n }\n if (chunk.kind === 'error') {\n spawnFailed = true;\n exitCode = 1;\n // close usually follows\n continue;\n }\n pending += chunk.data;\n if (pending.length >= flushAt) {\n yield { type: 'partial_output', text: pending };\n pending = '';\n }\n }\n if (pending.length > 0) {\n yield { type: 'partial_output', text: pending };\n }\n\n return {\n stdout,\n stderr,\n exitCode,\n truncated: stdout.length >= max || stderr.length >= max,\n error,\n };\n}\n","import * as fsp from 'node:fs/promises';\nimport * as path from 'node:path';\nimport * as Core from '@wrongstack/core';\nimport type { Context } from '@wrongstack/core';\n/** Detected package manager for a project directory. */\nexport type PackageManager = 'pnpm' | 'yarn' | 'npm';\n\n/**\n * Detect the project's package manager by inspecting lockfiles in `cwd`.\n * Order: pnpm → yarn → npm (default). Missing or unreadable directories fall\n * back to `npm` rather than throwing, so a `safeResolve`-checked cwd that\n * happens to be empty never aborts the tool.\n */\nexport async function detectPackageManager(cwd: string): Promise<PackageManager> {\n const { stat } = await import('node:fs/promises');\n try {\n await stat(`${cwd}/pnpm-lock.yaml`);\n return 'pnpm';\n } catch {\n /* not pnpm */\n }\n try {\n await stat(`${cwd}/yarn.lock`);\n return 'yarn';\n } catch {\n /* not yarn */\n }\n return 'npm';\n}\n\nexport function resolvePath(input: string, ctx: Context): string {\n return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.workingDir ?? ctx.cwd, input);\n}\n\nexport function ensureInsideRoot(absPath: string, ctx: Context): string {\n const root = path.resolve(ctx.projectRoot);\n const target = path.resolve(absPath);\n const rel = path.relative(root, target);\n if (rel.startsWith('..') || path.isAbsolute(rel)) {\n throw new Error(`Path \"${absPath}\" is outside project root \"${root}\"`);\n }\n return target;\n}\n\nexport function safeResolve(input: string, ctx: Context): string {\n return ensureInsideRoot(resolvePath(input, ctx), ctx);\n}\n\n/**\n * Defense against in-root→out-of-root symlink escape (CWE-59). `safeResolve`\n * only does a syntactic `../` check, so a symlink that lives *inside* the\n * project root but points outside still passes it. This resolves the path\n * through `fs.realpath` and re-verifies containment against the realpath of\n * the project root (comparing like-for-like, since the root itself may be a\n * symlink — macOS `/var`→`/private/var`, Windows 8.3 short names). For a path\n * that does not exist yet (e.g. a `write` to a new file) the nearest existing\n * ancestor directory is checked instead. Throws if the real target escapes.\n *\n * Mirrors the per-file guard already used in `replace.ts`/`grep.ts`; applied\n * to single-file `read`/`edit`/`write` it throws (rather than skips) because\n * the caller named exactly one file.\n */\nexport async function assertRealInsideRoot(absPath: string, ctx: Context): Promise<void> {\n const realRoot = await fsp.realpath(ctx.projectRoot).catch(() => path.resolve(ctx.projectRoot));\n let probe = absPath;\n for (;;) {\n let real: string;\n try {\n real = await fsp.realpath(probe);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') {\n const parent = path.dirname(probe);\n if (parent === probe) return; // reached fs root without escaping\n probe = parent;\n continue;\n }\n throw err;\n }\n const rel = path.relative(realRoot, real);\n if (rel.startsWith('..') || path.isAbsolute(rel)) {\n throw new Error(\n `Path \"${absPath}\" resolves through a symlink outside project root \"${realRoot}\"`,\n );\n }\n return;\n }\n}\n\n/** `safeResolve` + symlink realpath containment check. Async. */\nexport async function safeResolveReal(input: string, ctx: Context): Promise<string> {\n const abs = safeResolve(input, ctx);\n await assertRealInsideRoot(abs, ctx);\n return abs;\n}\n\nexport function truncateMiddle(s: string, max: number): string {\n if (Buffer.byteLength(s, 'utf8') <= max) return s;\n const half = Math.floor(max / 2);\n return (\n s.slice(0, half) +\n `\\n…[truncated ${Buffer.byteLength(s, 'utf8') - max} bytes from middle]…\\n` +\n s.slice(-half)\n );\n}\n\nexport function isBinaryBuffer(buf: Buffer): boolean {\n const len = Math.min(buf.length, 8192);\n for (let i = 0; i < len; i++) {\n if (buf[i] === 0) return true;\n }\n return false;\n}\n\n// ─── Command-output normalization (token-saving) ────────────────────────────\n//\n// Raw process output is full of tokens the model gains nothing from: ANSI\n// escapes, carriage-return progress spam, runs of identical warning lines, and\n// huge tails of build noise. These helpers strip that noise before the output\n// reaches the LLM. They are scoped to COMMAND tools (bash/git/exec and the\n// _spawn-stream consumers) — never applied to structured/code outputs.\n\n/** Unified byte cap for all command tool output fed to the model. */\nexport const COMMAND_OUTPUT_MAX_BYTES = 32_768;\n\n/** Runs of >= this many identical consecutive lines are collapsed. */\nconst REPEAT_RUN_THRESHOLD = 3;\n\n/**\n * Collapse carriage-return overwrites the way a terminal would: `\\r\\n` becomes\n * `\\n`, and a bare `\\r` (progress redraw) keeps only the text after the LAST\n * `\\r` on its physical line. Without this, a single progress bar that redraws\n * 200 times explodes into 200 lines.\n */\nexport function collapseCarriageReturns(text: string): string {\n const lf = text.replace(/\\r\\n/g, '\\n');\n if (!lf.includes('\\r')) return lf;\n return lf\n .split('\\n')\n .map((line) => (line.includes('\\r') ? line.slice(line.lastIndexOf('\\r') + 1) : line))\n .join('\\n');\n}\n\n/**\n * Collapse a run of `minRun`+ identical consecutive lines into the line once\n * plus a marker. Consecutive-only — it never reorders or dedups non-adjacent\n * lines, so diffs/source stay intact.\n */\nexport function collapseConsecutiveDuplicates(text: string, minRun = REPEAT_RUN_THRESHOLD): string {\n const lines = text.split('\\n');\n const out: string[] = [];\n let i = 0;\n while (i < lines.length) {\n let j = i + 1;\n while (j < lines.length && lines[j] === lines[i]) j++;\n const run = j - i;\n if (run >= minRun) {\n out.push(lines[i]!, `… ⟨repeated ${run}×⟩`);\n } else {\n for (let k = i; k < j; k++) out.push(lines[k]!);\n }\n i = j;\n }\n return out.join('\\n');\n}\n\n/** Largest prefix of `s` whose UTF-8 byte length is <= `maxBytes`. */\nfunction takeHeadBytes(s: string, maxBytes: number): string {\n if (maxBytes <= 0) return '';\n if (Buffer.byteLength(s, 'utf8') <= maxBytes) return s;\n let lo = 0;\n let hi = s.length;\n while (lo < hi) {\n const mid = Math.ceil((lo + hi) / 2);\n if (Buffer.byteLength(s.slice(0, mid), 'utf8') <= maxBytes) lo = mid;\n else hi = mid - 1;\n }\n return s.slice(0, lo);\n}\n\n/** Largest suffix of `s` whose UTF-8 byte length is <= `maxBytes`. */\nfunction takeTailBytes(s: string, maxBytes: number): string {\n if (maxBytes <= 0) return '';\n if (Buffer.byteLength(s, 'utf8') <= maxBytes) return s;\n let lo = 0;\n let hi = s.length;\n while (lo < hi) {\n const mid = Math.ceil((lo + hi) / 2);\n if (Buffer.byteLength(s.slice(s.length - mid), 'utf8') <= maxBytes) lo = mid;\n else hi = mid - 1;\n }\n return s.slice(s.length - lo);\n}\n\n/**\n * Truncate to `maxBytes` keeping BOTH ends — the head (what ran / early context)\n * and the tail (errors and summaries usually land last), biased ~45/55 toward\n * the tail. The result never exceeds `maxBytes`.\n */\nexport function truncateHeadTail(s: string, maxBytes: number): string {\n const total = Buffer.byteLength(s, 'utf8');\n if (total <= maxBytes) return s;\n // Reserve a fixed allowance for the marker so the final string can't exceed\n // the cap even though the dropped-byte count's digit width varies.\n const MARKER_RESERVE = 64;\n const avail = Math.max(0, maxBytes - MARKER_RESERVE);\n const headBudget = Math.floor(avail * 0.45);\n const head = takeHeadBytes(s, headBudget);\n const tail = takeTailBytes(s, avail - Buffer.byteLength(head, 'utf8'));\n const kept = Buffer.byteLength(head, 'utf8') + Buffer.byteLength(tail, 'utf8');\n return `${head}\\n…[truncated ${total - kept} bytes]…\\n${tail}`;\n}\n\n/**\n * Full token-saving pipeline for command tool output: strip ANSI → collapse\n * carriage-return progress → trim trailing whitespace → collapse identical\n * consecutive lines → squeeze blank-line runs → head+tail truncate to the cap.\n */\nexport function normalizeCommandOutput(\n raw: string,\n opts: { maxBytes?: number | undefined } = {},\n): string {\n if (!raw) return raw;\n let text = Core.stripAnsi(raw);\n text = collapseCarriageReturns(text);\n text = text.replace(/[ \\t]+$/gm, ''); // trailing whitespace per line\n text = collapseConsecutiveDuplicates(text);\n text = text.replace(/\\n{3,}/g, '\\n\\n'); // >=2 blank lines → 1\n return truncateHeadTail(text, opts.maxBytes ?? COMMAND_OUTPUT_MAX_BYTES);\n}\n","import type { Tool, ToolStreamEvent } from '@wrongstack/core';\nimport { spawnStream } from './_spawn-stream.js';\nimport { detectPackageManager, safeResolve } from './_util.js';\n\ninterface AuditInput {\n cwd?: string | undefined;\n level?: 'low' | 'moderate' | 'high' | 'critical' | undefined;\n fix?: boolean | undefined;\n packages?: string | string[] | undefined;\n}\n\ninterface AuditVulnerability {\n severity: string;\n package: string;\n title: string;\n url: string;\n}\n\ninterface AuditOutput {\n exit_code: number;\n vulnerabilities: AuditVulnerability[];\n total: number;\n summary: string;\n output: string;\n truncated: boolean;\n}\n\nexport const auditTool: Tool<AuditInput, AuditOutput> = {\n name: 'audit',\n category: 'Package Management',\n description:\n 'Run a security audit against project dependencies (using pnpm/npm audit). Reports known vulnerabilities with severity.',\n usageHint:\n 'CRITICAL SECURITY TOOL:\\n\\n' +\n '- Run regularly and especially before any release.\\n' +\n '- Use `level` to focus on high/critical issues.\\n' +\n '- `fix` can attempt automatic remediation for some vulnerabilities.\\n' +\n 'This is one of the most important tools for supply chain security.',\n permission: 'confirm',\n mutating: false,\n timeoutMs: 60_000,\n inputSchema: {\n type: 'object',\n properties: {\n cwd: { type: 'string', description: 'Working directory (default: cwd)' },\n level: {\n type: 'string',\n enum: ['low', 'moderate', 'high', 'critical'],\n description: 'Minimum severity level to report',\n },\n fix: { type: 'boolean', description: 'Attempt to fix vulnerabilities (default: false)' },\n packages: { type: 'string', description: 'Specific package(s) to audit (comma-separated)' },\n },\n },\n async execute(input, ctx, opts) {\n let final: AuditOutput | undefined;\n const executeStream = auditTool.executeStream;\n if (!executeStream) throw new Error('auditTool: stream execution unavailable');\n for await (const ev of executeStream(input, ctx, opts)) {\n if (ev.type === 'final') final = ev.output;\n }\n if (!final) throw new Error('audit: stream ended without final event');\n return final;\n },\n async *executeStream(input, ctx, opts): AsyncGenerator<ToolStreamEvent<AuditOutput>> {\n const cwd = input.cwd ? safeResolve(input.cwd, ctx) : ctx.cwd;\n const manager = await detectPackageManager(cwd);\n yield { type: 'log', text: `Auditing with ${manager}…`, data: { manager } };\n\n const args = ['audit', '--json'];\n if (input.fix) args.push('--fix');\n if (input.packages) {\n const pkgs = Array.isArray(input.packages) ? input.packages : input.packages.split(',');\n args.push(...pkgs.map((p: string) => p.trim()));\n }\n\n const result = yield* spawnStream({\n cmd: manager,\n args,\n cwd,\n signal: opts.signal,\n maxBytes: 100_000,\n });\n\n yield { type: 'final', output: parseAuditOutput(result.stdout, result.exitCode) };\n },\n};\n\nfunction parseAuditOutput(json: string, exitCode: number): AuditOutput {\n if (!json) {\n return {\n exit_code: exitCode,\n vulnerabilities: [],\n total: 0,\n summary: exitCode === 0 ? 'No vulnerabilities found' : 'Audit failed',\n output: '',\n truncated: false,\n };\n }\n\n try {\n const data = JSON.parse(json);\n const advisories: AuditVulnerability[] = [];\n const ads = data.advisories ?? {};\n for (const id of Object.keys(ads)) {\n const adv = ads[id];\n advisories.push({\n severity: adv.severity ?? 'unknown',\n package: adv.module_name ?? id,\n title: adv.title ?? 'Unknown vulnerability',\n url: adv.url ?? '',\n });\n }\n\n const total = advisories.length;\n const summary =\n total === 0\n ? 'No vulnerabilities found'\n : `Found ${total} vulnerabilities: ${advisories.filter((a) => a.severity === 'critical').length} critical, ${advisories.filter((a) => a.severity === 'high').length} high`;\n\n return {\n exit_code: exitCode,\n vulnerabilities: advisories,\n total,\n summary,\n output: json,\n truncated: json.length >= 100_000,\n };\n } catch {\n return {\n exit_code: exitCode,\n vulnerabilities: [],\n total: 0,\n summary: 'Could not parse audit output',\n output: json,\n truncated: false,\n };\n }\n}\n"]}
@@ -0,0 +1,373 @@
1
+ import { Tool } from '@wrongstack/core';
2
+
3
+ declare const codebaseIndexTool: Tool<CodebaseIndexInput, CodebaseIndexOutput>;
4
+ interface CodebaseIndexInput {
5
+ force?: boolean | undefined;
6
+ langs?: string[] | undefined;
7
+ }
8
+ interface CodebaseIndexOutput {
9
+ filesIndexed: number;
10
+ symbolsIndexed: number;
11
+ langStats: Record<string, number>;
12
+ durationMs: number;
13
+ errors: string[];
14
+ /** Advisory note when the indexer was skipped (e.g. another index in progress). */
15
+ note?: string | undefined;
16
+ }
17
+
18
+ /** Language a symbol belongs to. */
19
+ type SymbolLang = 'ts' | 'js' | 'tsx' | 'jsx' | 'go' | 'py' | 'rs' | 'json' | 'yaml';
20
+ /** What kind of symbol this is. */
21
+ type SymbolKind = 'class' | 'interface' | 'enum' | 'type' | 'function' | 'method' | 'var' | 'const' | 'let' | 'property' | 'parameter' | 'namespace' | 'object' | 'literal' | 'schema' | 'struct' | 'trait' | 'impl' | 'static' | 'mod';
22
+ /** A single indexed code symbol. */
23
+ interface Symbol {
24
+ id: number;
25
+ lang: SymbolLang;
26
+ kind: SymbolKind;
27
+ name: string;
28
+ file: string;
29
+ line: number;
30
+ col: number;
31
+ signature: string;
32
+ docComment: string;
33
+ scope: string;
34
+ text: string;
35
+ }
36
+ /** Extracted symbols and cross-references for one file. */
37
+ interface FileSymbols {
38
+ file: string;
39
+ lang: SymbolLang;
40
+ symbols: Symbol[];
41
+ refs?: Ref[] | undefined;
42
+ mtimeMs: number;
43
+ }
44
+ /** Source file metadata tracked for incremental indexing. */
45
+ interface FileMeta {
46
+ file: string;
47
+ lang: SymbolLang;
48
+ mtimeMs: number;
49
+ symbolCount: number;
50
+ lastIndexed: number;
51
+ }
52
+ /** Statistics about the index. */
53
+ interface IndexStats {
54
+ totalSymbols: number;
55
+ totalFiles: number;
56
+ byLang: Record<SymbolLang, number>;
57
+ byKind: Record<SymbolKind, number>;
58
+ indexPath: string;
59
+ lastIndexed: number | null;
60
+ sizeBytes: number;
61
+ version: number;
62
+ }
63
+ /** Result of a search query. */
64
+ interface SearchResult {
65
+ id: number;
66
+ name: string;
67
+ kind: SymbolKind;
68
+ lang: SymbolLang;
69
+ file: string;
70
+ line: number;
71
+ col: number;
72
+ signature: string;
73
+ docComment: string;
74
+ score: number;
75
+ snippet: string;
76
+ /** Original LSP SymbolKind number if the result was filtered by an LSP kind. */
77
+ lspKind?: number | undefined;
78
+ }
79
+ /** Result of a full reindex. */
80
+ interface IndexResult {
81
+ filesIndexed: number;
82
+ symbolsIndexed: number;
83
+ langStats: Record<SymbolLang, number>;
84
+ durationMs: number;
85
+ errors: string[];
86
+ }
87
+ /** What kind of reference this is. */
88
+ type CallType = 'call' | 'type_ref' | 'inherit' | 'implement' | 'import';
89
+ /** A cross-reference between two symbols (who references whom). */
90
+ interface Ref {
91
+ id?: number | undefined;
92
+ fromId: number;
93
+ toName: string;
94
+ toId?: number | undefined;
95
+ callType: CallType;
96
+ line: number;
97
+ }
98
+ declare const SCHEMA_VERSION = 2;
99
+
100
+ /**
101
+ * `codebase-search` tool — search the symbol index with BM25 ranking.
102
+ *
103
+ * Usage: codebase-search({
104
+ * query: string, // search terms
105
+ * kind?: string, // class|function|interface|method|const|...
106
+ * lang?: string, // ts|tsx|js|jsx|go|py|rs
107
+ * file?: string, // filter to a specific file path (substring match)
108
+ * limit?: number, // max results (default 20, max 100)
109
+ * })
110
+ *
111
+ * Returns: [{ name, kind, lang, file, line, signature, snippet, score }, ...]
112
+ *
113
+ * The query executes in the index worker via FTS5 (`MATCH` + native `bm25()`)
114
+ * — the main thread never opens SQLite, so a contended or wedged index can
115
+ * slow this tool down but can never freeze the terminal.
116
+ */
117
+
118
+ declare const codebaseSearchTool: Tool<CodebaseSearchInput, CodebaseSearchOutput>;
119
+ interface CodebaseSearchInput {
120
+ query: string;
121
+ kind?: string | undefined;
122
+ lang?: string | undefined;
123
+ file?: string | undefined;
124
+ limit?: number | undefined;
125
+ lspKind?: number | undefined;
126
+ }
127
+ interface CodebaseSearchOutput {
128
+ results: SearchResult[];
129
+ total: number;
130
+ query: string;
131
+ /** Non-empty when the index blocked the search (not ready, indexing, failed). */
132
+ indexStatus?: string | undefined;
133
+ }
134
+
135
+ /**
136
+ * `codebase-stats` tool — report index health and statistics.
137
+ *
138
+ * Usage: codebase-stats({})
139
+ *
140
+ * Returns: { totalSymbols, totalFiles, byLang, byKind, lastIndexed, sizeBytes, version }
141
+ */
142
+
143
+ declare const codebaseStatsTool: Tool<Record<string, never>, CodebaseStatsOutput>;
144
+ interface CodebaseStatsOutput {
145
+ totalSymbols: number;
146
+ totalFiles: number;
147
+ byLang: Record<string, number>;
148
+ byKind: Record<string, number>;
149
+ lastIndexed: number | null;
150
+ sizeBytes: number;
151
+ indexPath: string;
152
+ version: number;
153
+ /** Non-empty when the index is not ready or is still building. */
154
+ indexStatus?: string | undefined;
155
+ }
156
+
157
+ /**
158
+ * Circuit breaker for the codebase indexer.
159
+ *
160
+ * The indexer can wedge: a hung filesystem, a parser pathology, or another
161
+ * wstack process holding the SQLite write lock (several surfaces — TUI, WebUI,
162
+ * parallel terminals — share one per-project `index.db`). Without protection,
163
+ * every queued reindex piles up behind the process-wide mutex, `isIndexing()`
164
+ * stays true forever, and anything that awaits an index run (the startup scan,
165
+ * `/codebase-reindex`) locks its terminal.
166
+ *
167
+ * Standard three-state breaker:
168
+ *
169
+ * closed — normal operation; consecutive failures are counted.
170
+ * open — after `failureThreshold` consecutive failures, every request
171
+ * is rejected fast ({@link CircuitOpenError}) for `cooldownMs`.
172
+ * half-open — after the cooldown exactly one probe run is admitted;
173
+ * success closes the circuit, failure re-opens it.
174
+ *
175
+ * Watchdog timeouts ({@link IndexTimeoutError}) count as failures;
176
+ * caller-initiated aborts (session teardown) do not — the background indexer
177
+ * makes that distinction before recording.
178
+ *
179
+ * Lock conflicts ({@link LockError}) do NOT count as failures — they are expected
180
+ * transient conditions when multiple wstack surfaces share the same `index.db`.
181
+ * The index store retries automatically; a LockError only reaches the circuit
182
+ * breaker when all retries are exhausted.
183
+ */
184
+ type CircuitState = 'closed' | 'open' | 'half-open';
185
+ interface CircuitSnapshot {
186
+ state: CircuitState;
187
+ consecutiveFailures: number;
188
+ lastFailure: string | null;
189
+ /** ms until an open circuit admits a half-open probe (0 unless open). */
190
+ cooldownRemainingMs: number;
191
+ }
192
+ /** Thrown when a run is rejected because the circuit is open. */
193
+ declare class CircuitOpenError extends Error {
194
+ readonly name = "CircuitOpenError";
195
+ }
196
+ /** Thrown by the background indexer's watchdog when a run exceeds its timeout. */
197
+ declare class IndexTimeoutError extends Error {
198
+ readonly name = "IndexTimeoutError";
199
+ }
200
+ interface CircuitBreakerOptions {
201
+ /** Consecutive failures before the circuit opens. Default: 3. */
202
+ failureThreshold?: number | undefined;
203
+ /** How long an open circuit rejects requests before allowing a probe. Default: 60s. */
204
+ cooldownMs?: number | undefined;
205
+ /** Injectable clock for tests. Default: Date.now. */
206
+ now?: (() => number) | undefined;
207
+ }
208
+ declare class IndexCircuitBreaker {
209
+ private readonly failureThreshold;
210
+ private readonly cooldownMs;
211
+ private readonly now;
212
+ private state;
213
+ private consecutiveFailures;
214
+ private openedAt;
215
+ private lastFailure;
216
+ private probeInFlight;
217
+ constructor(opts?: CircuitBreakerOptions);
218
+ /**
219
+ * True when a run may proceed. An open circuit transitions to half-open once
220
+ * the cooldown has elapsed, admitting exactly one probe; further requests
221
+ * are rejected until that probe settles via recordSuccess/recordFailure.
222
+ */
223
+ allowRequest(): boolean;
224
+ recordSuccess(): void;
225
+ recordFailure(err: unknown): void;
226
+ /** Force-close the circuit (manual recovery: `/codebase-reindex`). */
227
+ reset(): void;
228
+ snapshot(): CircuitSnapshot;
229
+ }
230
+ /**
231
+ * Process-wide breaker shared by every index path (startup scan, per-edit
232
+ * incremental, external watcher, the `codebase-index` tool). Module-level for
233
+ * the same reason the mutex is: there is one `index.db` per project and one
234
+ * indexing pipeline per process.
235
+ */
236
+ declare const indexCircuitBreaker: IndexCircuitBreaker;
237
+ /** Reset the shared breaker — used by `/codebase-reindex` and tests. */
238
+ declare function resetIndexCircuitBreaker(): void;
239
+
240
+ /**
241
+ * Message protocol between the index host (main thread) and the index worker.
242
+ *
243
+ * Plain structured-cloneable shapes only — no class instances, no functions.
244
+ * Errors cross the boundary as strings and are re-wrapped by the host.
245
+ */
246
+
247
+ interface SearchOpArgs {
248
+ projectRoot: string;
249
+ indexDir?: string | undefined;
250
+ query: string;
251
+ kind?: string | undefined;
252
+ lang?: string | undefined;
253
+ file?: string | undefined;
254
+ lspKind?: number | undefined;
255
+ limit: number;
256
+ }
257
+ interface StatsOpArgs {
258
+ projectRoot: string;
259
+ indexDir?: string | undefined;
260
+ }
261
+ interface SearchOpResult {
262
+ results: SearchResult[];
263
+ total: number;
264
+ }
265
+
266
+ /**
267
+ * Index host — the main-thread coordinator for all codebase-index operations.
268
+ *
269
+ * Production mode runs every operation (full scans, per-file reindexes,
270
+ * searches, stats) in a dedicated worker thread (`worker.ts`), so the
271
+ * synchronous `node:sqlite` calls and the TypeScript parser can never block
272
+ * the main event loop — the failure mode that used to freeze terminals is
273
+ * structurally impossible. When the built worker file is not present (tests
274
+ * run from source, exotic runtimes) or `WRONGSTACK_INDEX_INLINE=1` is set,
275
+ * operations fall back to running inline through the same service layer.
276
+ *
277
+ * Concerns owned here, in front of either execution mode:
278
+ *
279
+ * 1. **Serialization** — every write run (startup scan, per-edit incremental,
280
+ * external file-watch, manual reindex) goes through one process-wide
281
+ * promise-chain mutex so two runs never race the same `index.db` writer.
282
+ * 2. **Debounce** — rapid successive edits to the same file coalesce into a
283
+ * single reindex, keyed per `(indexDir, file)`.
284
+ * 3. **Watchdog** — every operation is raced against a timeout. In worker
285
+ * mode a timeout hard-terminates the worker (it respawns lazily on the
286
+ * next request); inline it aborts the run's signal. Either way the mutex
287
+ * chain always advances and the promise always settles.
288
+ * 4. **Circuit breaker** — repeated failures/timeouts pause indexing instead
289
+ * of queuing more work behind a wedged pipeline. See circuit-breaker.ts.
290
+ * 5. **State tracking** — ready/indexing/progress flags + change listeners
291
+ * for the TUI status chip and the search/stats tools' gating.
292
+ */
293
+
294
+ /** True once the first full-project index has completed (success or failure). */
295
+ declare function isIndexReady(): boolean;
296
+ /** True while an index build is actively running. */
297
+ declare function isIndexing(): boolean;
298
+ /** Current indexing progress: { currentFile, totalFiles, ready, indexing, circuit }. */
299
+ declare function getIndexState(): {
300
+ ready: boolean;
301
+ indexing: boolean;
302
+ currentFile: number;
303
+ totalFiles: number;
304
+ lastError: string | null;
305
+ /** Circuit-breaker state — `open` means indexing is paused after repeated failures. */
306
+ circuit: CircuitSnapshot;
307
+ };
308
+ /**
309
+ * Optional callback fired on every lifecycle transition (started, progress,
310
+ * completed, failed). Plug into the event bus or a TUI dispatcher to surface
311
+ * the indexing state in real time.
312
+ */
313
+ type IndexStateListener = (state: ReturnType<typeof getIndexState>) => void;
314
+ declare function onIndexStateChange(listener: IndexStateListener): () => void;
315
+ /**
316
+ * Tear down the index host (worker + pending debounces). Call on process
317
+ * shutdown; safe to call when nothing is running.
318
+ */
319
+ declare function shutdownCodebaseIndexHost(): void;
320
+ /** True when the file's extension maps to a language the indexer can parse. */
321
+ declare function isIndexableFile(filePath: string): boolean;
322
+ /**
323
+ * Run a full-project scan and await it. Used at session start and by the manual
324
+ * `/codebase-reindex` command. Incremental by default (unchanged files skipped
325
+ * via mtime, so repeat runs are cheap); pass `force` to clear and rebuild.
326
+ *
327
+ * Sets the global `_ready` flag on completion so downstream tools know the
328
+ * index is usable.
329
+ */
330
+ declare function runStartupIndex(opts: {
331
+ projectRoot: string;
332
+ indexDir?: string | undefined;
333
+ force?: boolean | undefined;
334
+ langs?: string[] | undefined;
335
+ signal?: AbortSignal | undefined;
336
+ /** Watchdog timeout for the whole run. Default: 120s. */
337
+ timeoutMs?: number | undefined;
338
+ }): Promise<IndexResult>;
339
+ /**
340
+ * Debounced, fire-and-forget incremental reindex of specific files. Used by the
341
+ * per-edit toolCall middleware and the external file watcher. Non-indexable
342
+ * paths are dropped. Errors are reported via the optional `onError` callback and
343
+ * never thrown to the caller (background work must not crash a turn).
344
+ */
345
+ declare function enqueueReindex(opts: {
346
+ projectRoot: string;
347
+ files: string[];
348
+ indexDir?: string | undefined;
349
+ debounceMs?: number | undefined;
350
+ /** Watchdog timeout per file. Default: 30s. */
351
+ timeoutMs?: number | undefined;
352
+ onError?: ((err: unknown) => void) | undefined;
353
+ }): void;
354
+ /** Cancel all pending debounced reindexes. For teardown / tests. */
355
+ declare function cancelPendingReindexes(): void;
356
+ /**
357
+ * Ranked symbol search against the index. The query runs in the index worker
358
+ * (or inline in fallback mode) — the main thread never opens SQLite. Reads
359
+ * don't take the write mutex (WAL readers don't block the writer) and don't
360
+ * feed the circuit breaker; a wedged read still trips the watchdog, which
361
+ * recycles the worker.
362
+ */
363
+ declare function searchCodebaseIndex(args: SearchOpArgs, opts?: {
364
+ timeoutMs?: number | undefined;
365
+ signal?: AbortSignal | undefined;
366
+ }): Promise<SearchOpResult>;
367
+ /** Index health/statistics, fetched off the main thread like searches. */
368
+ declare function codebaseIndexStats(args: StatsOpArgs, opts?: {
369
+ timeoutMs?: number | undefined;
370
+ signal?: AbortSignal | undefined;
371
+ }): Promise<IndexStats>;
372
+
373
+ export { CircuitOpenError as C, type FileMeta as F, IndexCircuitBreaker as I, type Ref as R, type Symbol as S, type CircuitSnapshot as a, type CircuitState as b, IndexTimeoutError as c, cancelPendingReindexes as d, codebaseIndexStats as e, codebaseIndexTool as f, codebaseSearchTool as g, codebaseStatsTool as h, enqueueReindex as i, getIndexState as j, indexCircuitBreaker as k, isIndexReady as l, isIndexableFile as m, isIndexing as n, onIndexStateChange as o, runStartupIndex as p, shutdownCodebaseIndexHost as q, resetIndexCircuitBreaker as r, searchCodebaseIndex as s, type IndexResult as t, type SymbolKind as u, type SymbolLang as v, type SearchResult as w, type IndexStats as x, type FileSymbols as y, SCHEMA_VERSION as z };