@wrongstack/tools 0.31.1 → 0.41.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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/_util.ts","../src/outdated.ts"],"names":["resolve"],"mappings":"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","file":"outdated.js","sourcesContent":["import * as fsp from 'node:fs/promises';\nimport * as path from 'node:path';\nimport { type Context, stripAnsi } from '@wrongstack/core';\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 } = {},\n): string {\n if (!raw) return raw;\n let text = 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 { spawn } from 'node:child_process';\nimport { stat } from 'node:fs/promises';\nimport { buildChildEnv } from '@wrongstack/core';\nimport type { Tool } from '@wrongstack/core';\nimport { safeResolve } from './_util.js';\n\ninterface OutdatedInput {\n cwd?: string;\n format?: 'list' | 'table';\n include_deprecated?: boolean;\n check?: string | string[];\n}\n\ninterface OutdatedPackage {\n name: string;\n current: string;\n latest: string;\n wanted: string;\n type: string;\n location: string;\n}\n\ninterface OutdatedOutput {\n exit_code: number;\n packages: OutdatedPackage[];\n total: number;\n output: string;\n truncated: boolean;\n}\n\nexport const outdatedTool: Tool<OutdatedInput, OutdatedOutput> = {\n name: 'outdated',\n category: 'Package Management',\n description:\n 'Check for outdated dependencies in the project. Reports current, wanted (semver range), and latest versions available.',\n usageHint:\n 'MAINTENANCE & SECURITY TOOL:\\n\\n' +\n '- Run periodically or before dependency-related work.\\n' +\n '- Helps surface packages that may need updates for security or features.\\n' +\n '- Safe, read-only operation.\\n' +\n 'Use the output to decide on upgrades. Prefer this over manual shell commands for dependency hygiene.',\n permission: 'auto',\n mutating: true,\n timeoutMs: 60_000,\n inputSchema: {\n type: 'object',\n properties: {\n cwd: { type: 'string', description: 'Working directory (default: cwd)' },\n format: {\n type: 'string',\n enum: ['list', 'table'],\n description: 'Output format (default: list)',\n },\n include_deprecated: {\n type: 'boolean',\n description: 'Include deprecated packages (default: false)',\n },\n check: {\n type: 'string',\n description: 'Specific package(s) to check (comma-separated)',\n },\n },\n },\n async execute(input, ctx, opts) {\n const cwd = input.cwd ? safeResolve(input.cwd, ctx) : ctx.cwd;\n const manager = await detectManager(cwd);\n\n const args: string[] = ['outdated', '--json'];\n if (input.format === 'table') args.push('--table');\n if (input.include_deprecated) args.push('--include', 'deprecated');\n\n return runOutdated(manager, args, cwd, opts.signal);\n },\n};\n\nasync function detectManager(cwd: string): Promise<string> {\n try {\n await stat(`${cwd}/pnpm-lock.yaml`);\n return 'pnpm';\n } catch {\n /* */\n }\n try {\n await stat(`${cwd}/yarn.lock`);\n return 'yarn';\n } catch {\n /* */\n }\n return 'npm';\n}\n\nfunction runOutdated(\n manager: string,\n args: string[],\n cwd: string,\n signal: AbortSignal,\n): Promise<OutdatedOutput> {\n return new Promise((resolve) => {\n let stdout = '';\n let stderr = '';\n const MAX = 100_000;\n\n const child = spawn(manager, args, { cwd, signal, env: buildChildEnv(), stdio: ['ignore', 'pipe', 'pipe'] });\n child.stdout?.on('data', (c) => {\n if (stdout.length < MAX) stdout += c.toString();\n });\n child.stderr?.on('data', (c) => {\n if (stderr.length < MAX) stderr += c.toString();\n });\n child.on('close', (code) => {\n const result = parseOutdatedOutput(stdout, code ?? 0);\n resolve(result);\n });\n child.on('error', (e) => {\n resolve({\n exit_code: 1,\n packages: [],\n total: 0,\n output: e.message,\n truncated: false,\n });\n });\n });\n}\n\nfunction parseOutdatedOutput(json: string, exitCode: number): OutdatedOutput {\n const packages: OutdatedPackage[] = [];\n\n if (!json) {\n return {\n exit_code: exitCode,\n packages: [],\n total: 0,\n output: exitCode === 0 ? 'All packages up to date' : 'Could not check outdated packages',\n truncated: false,\n };\n }\n\n try {\n const data = JSON.parse(json);\n for (const name of Object.keys(data)) {\n const info = data[name];\n packages.push({\n name,\n current: info.current ?? 'unknown',\n latest: info.latest ?? 'unknown',\n wanted: info.wanted ?? 'unknown',\n type: info.type ?? 'unknown',\n location: info.location ?? name,\n });\n }\n } catch {\n // JSON parse failed, return raw output\n }\n\n return {\n exit_code: exitCode,\n packages,\n total: packages.length,\n output: json,\n truncated: json.length >= 100_000,\n };\n}\n"]}
1
+ {"version":3,"sources":["../src/_util.ts","../src/outdated.ts"],"names":["resolve"],"mappings":"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","file":"outdated.js","sourcesContent":["import * as fsp from 'node:fs/promises';\nimport * as path from 'node:path';\nimport { type Context, stripAnsi } from '@wrongstack/core';\n\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 } = {},\n): string {\n if (!raw) return raw;\n let text = 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 { spawn } from 'node:child_process';\nimport { buildChildEnv } from '@wrongstack/core';\nimport type { Tool } from '@wrongstack/core';\nimport { detectPackageManager, safeResolve } from './_util.js';\n\ninterface OutdatedInput {\n cwd?: string;\n format?: 'list' | 'table';\n include_deprecated?: boolean;\n check?: string | string[];\n}\n\ninterface OutdatedPackage {\n name: string;\n current: string;\n latest: string;\n wanted: string;\n type: string;\n location: string;\n}\n\ninterface OutdatedOutput {\n exit_code: number;\n packages: OutdatedPackage[];\n total: number;\n output: string;\n truncated: boolean;\n}\n\nexport const outdatedTool: Tool<OutdatedInput, OutdatedOutput> = {\n name: 'outdated',\n category: 'Package Management',\n description:\n 'Check for outdated dependencies in the project. Reports current, wanted (semver range), and latest versions available.',\n usageHint:\n 'MAINTENANCE & SECURITY TOOL:\\n\\n' +\n '- Run periodically or before dependency-related work.\\n' +\n '- Helps surface packages that may need updates for security or features.\\n' +\n '- Safe, read-only operation.\\n' +\n 'Use the output to decide on upgrades. Prefer this over manual shell commands for dependency hygiene.',\n permission: 'auto',\n mutating: false,\n timeoutMs: 60_000,\n inputSchema: {\n type: 'object',\n properties: {\n cwd: { type: 'string', description: 'Working directory (default: cwd)' },\n format: {\n type: 'string',\n enum: ['list', 'table'],\n description: 'Output format (default: list)',\n },\n include_deprecated: {\n type: 'boolean',\n description: 'Include deprecated packages (default: false)',\n },\n check: {\n type: 'string',\n description: 'Specific package(s) to check (comma-separated)',\n },\n },\n },\n async execute(input, ctx, opts) {\n const cwd = input.cwd ? safeResolve(input.cwd, ctx) : ctx.cwd;\n const manager = await detectPackageManager(cwd);\n\n const args: string[] = ['outdated', '--json'];\n if (input.format === 'table') args.push('--table');\n if (input.include_deprecated) args.push('--include', 'deprecated');\n\n return runOutdated(manager, args, cwd, opts.signal);\n },\n};\n\nfunction runOutdated(\n manager: string,\n args: string[],\n cwd: string,\n signal: AbortSignal,\n): Promise<OutdatedOutput> {\n return new Promise((resolve) => {\n let stdout = '';\n let stderr = '';\n const MAX = 100_000;\n\n const child = spawn(manager, args, { cwd, signal, env: buildChildEnv(), stdio: ['ignore', 'pipe', 'pipe'] });\n child.stdout?.on('data', (c) => {\n if (stdout.length < MAX) stdout += c.toString();\n });\n child.stderr?.on('data', (c) => {\n if (stderr.length < MAX) stderr += c.toString();\n });\n child.on('close', (code) => {\n const result = parseOutdatedOutput(stdout, code ?? 0);\n resolve(result);\n });\n child.on('error', (e) => {\n resolve({\n exit_code: 1,\n packages: [],\n total: 0,\n output: e.message,\n truncated: false,\n });\n });\n });\n}\n\nfunction parseOutdatedOutput(json: string, exitCode: number): OutdatedOutput {\n const packages: OutdatedPackage[] = [];\n\n if (!json) {\n return {\n exit_code: exitCode,\n packages: [],\n total: 0,\n output: exitCode === 0 ? 'All packages up to date' : 'Could not check outdated packages',\n truncated: false,\n };\n }\n\n try {\n const data = JSON.parse(json);\n for (const name of Object.keys(data)) {\n const info = data[name];\n packages.push({\n name,\n current: info.current ?? 'unknown',\n latest: info.latest ?? 'unknown',\n wanted: info.wanted ?? 'unknown',\n type: info.type ?? 'unknown',\n location: info.location ?? name,\n });\n }\n } catch {\n // JSON parse failed, return raw output\n }\n\n return {\n exit_code: exitCode,\n packages,\n total: packages.length,\n output: json,\n truncated: json.length >= 100_000,\n };\n}\n"]}
package/dist/pack.js CHANGED
@@ -1,7 +1,6 @@
1
1
  import { spawn, execFileSync, spawnSync } from 'node:child_process';
2
2
  import { buildChildEnv, detectNewlineStyle, normalizeToLf, toStyle, atomicWrite, unifiedDiff, compileGlob, loadPlan, emptyPlan, clearPlan, savePlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, setPlanItemStatus, formatPlan, stripAnsi, resolveWstackPaths } from '@wrongstack/core';
3
3
  import * as fs11 from 'node:fs/promises';
4
- import { stat } from 'node:fs/promises';
5
4
  import * as path from 'node:path';
6
5
  import { resolve, sep, dirname } from 'node:path';
7
6
  import * as os from 'node:os';
@@ -97,6 +96,20 @@ async function* spawnStream(opts) {
97
96
  error
98
97
  };
99
98
  }
99
+ async function detectPackageManager(cwd) {
100
+ const { stat: stat10 } = await import('node:fs/promises');
101
+ try {
102
+ await stat10(`${cwd}/pnpm-lock.yaml`);
103
+ return "pnpm";
104
+ } catch {
105
+ }
106
+ try {
107
+ await stat10(`${cwd}/yarn.lock`);
108
+ return "yarn";
109
+ } catch {
110
+ }
111
+ return "npm";
112
+ }
100
113
  function resolvePath(input, ctx) {
101
114
  return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.cwd, input);
102
115
  }
@@ -259,7 +272,7 @@ var auditTool = {
259
272
  },
260
273
  async *executeStream(input, ctx, opts) {
261
274
  const cwd = input.cwd ? safeResolve(input.cwd, ctx) : ctx.cwd;
262
- const manager = await detectManager(cwd);
275
+ const manager = await detectPackageManager(cwd);
263
276
  yield { type: "log", text: `Auditing with ${manager}\u2026`, data: { manager } };
264
277
  const args = ["audit", "--json"];
265
278
  if (input.fix) args.push("--fix");
@@ -277,20 +290,6 @@ var auditTool = {
277
290
  yield { type: "final", output: parseAuditOutput(result.stdout, result.exitCode) };
278
291
  }
279
292
  };
280
- async function detectManager(cwd) {
281
- const { stat: stat11 } = await import('node:fs/promises');
282
- try {
283
- await stat11(`${cwd}/pnpm-lock.yaml`);
284
- return "pnpm";
285
- } catch {
286
- }
287
- try {
288
- await stat11(`${cwd}/yarn.lock`);
289
- return "yarn";
290
- } catch {
291
- }
292
- return "npm";
293
- }
294
293
  function parseAuditOutput(json, exitCode) {
295
294
  if (!json) {
296
295
  return {
@@ -2716,18 +2715,18 @@ async function runIndexer(_ctx, opts) {
2716
2715
  for (const meta of store.getAllFileMetas()) existingMeta.set(meta.file, meta);
2717
2716
  }
2718
2717
  for (const file of files) {
2719
- let stat11;
2718
+ let stat10;
2720
2719
  try {
2721
- stat11 = await fs11.stat(file);
2720
+ stat10 = await fs11.stat(file);
2722
2721
  } catch {
2723
2722
  store.deleteFile(file);
2724
2723
  continue;
2725
2724
  }
2726
- if (!stat11.isFile()) continue;
2725
+ if (!stat10.isFile()) continue;
2727
2726
  const lang = detectLang(file);
2728
2727
  if (!lang) continue;
2729
2728
  const meta = existingMeta.get(file);
2730
- if (!force && meta && meta.mtimeMs === Math.floor(stat11.mtimeMs)) {
2729
+ if (!force && meta && meta.mtimeMs === Math.floor(stat10.mtimeMs)) {
2731
2730
  langStats[lang] = (langStats[lang] ?? 0) + meta.symbolCount;
2732
2731
  symbolsIndexed += meta.symbolCount;
2733
2732
  filesIndexed++;
@@ -2753,7 +2752,7 @@ async function runIndexer(_ctx, opts) {
2753
2752
  store.upsertFile({
2754
2753
  file,
2755
2754
  lang,
2756
- mtimeMs: Math.floor(stat11.mtimeMs),
2755
+ mtimeMs: Math.floor(stat10.mtimeMs),
2757
2756
  symbolCount: 0,
2758
2757
  lastIndexed: Date.now()
2759
2758
  });
@@ -2779,7 +2778,7 @@ async function runIndexer(_ctx, opts) {
2779
2778
  store.upsertFile({
2780
2779
  file,
2781
2780
  lang,
2782
- mtimeMs: Math.floor(stat11.mtimeMs),
2781
+ mtimeMs: Math.floor(stat10.mtimeMs),
2783
2782
  symbolCount: count,
2784
2783
  lastIndexed: Date.now()
2785
2784
  });
@@ -2810,8 +2809,9 @@ var codebaseIndexTool = {
2810
2809
  category: "Project",
2811
2810
  description: "Build or incrementally update the project-wide symbol index. This powers fast codebase search and understanding. By default it only processes files that have changed since the last indexing run.",
2812
2811
  usageHint: "IMPORTANT FOR LARGE CODEBASES:\n\n- First run (or after major changes): consider `force: true` for a clean rebuild.\n- Normal usage: call without arguments for fast incremental updates.\n- Use `langs` to restrict to specific languages if you only care about certain parts of the project.\nThis tool is relatively expensive \u2014 do not call it on every turn. Use it when the index is stale or before heavy codebase-search sessions.",
2813
- permission: "auto",
2812
+ permission: "confirm",
2814
2813
  mutating: true,
2814
+ capabilities: ["fs.write.outside-project"],
2815
2815
  timeoutMs: 12e4,
2816
2816
  inputSchema: {
2817
2817
  type: "object",
@@ -3047,8 +3047,8 @@ var codebaseStatsTool = {
3047
3047
  var diffTool = {
3048
3048
  name: "diff",
3049
3049
  category: "Filesystem",
3050
- description: "Show code differences between files, commits, branches, or staged changes. A safer and more structured alternative to raw `git diff` via shell.",
3051
- usageHint: 'USE FOR CODE REVIEW AND CHANGE INSPECTION:\n\n- `files` + no `a`/`b` \u2192 diff working tree vs HEAD for those files.\n- `a` and/or `b` \u2192 git-style commit/branch diff.\n- `staged: true` \u2192 only show staged changes.\n- `mode` can be "unified", "stat", or "side-by-side".\nThis tool has important safety guards against flag injection (see previous security findings).',
3050
+ description: "Show file content with line numbers, staged/working-tree diffs via git, or commit/branch diffs. A safer and more structured alternative to raw `git diff` via shell.",
3051
+ usageHint: 'USE FOR CODE REVIEW AND CHANGE INSPECTION:\n\n- `files` + no `a`/`b` \u2192 show file content with line numbers (NOT a unified diff; no +/- prefixes).\n- `a` and/or `b` \u2192 git-style commit/branch diff (unified format, real +/- prefixes).\n- `staged: true` \u2192 only show staged changes.\n- `mode` can be "unified", "stat", or "side-by-side" (only affects the git-diff path).\n\nNOTE: For a true file-vs-file unified diff, supply `a` and `b` so the tool delegates to `git diff`. The `files`-only path is a line-numbered dump, not a diff.\n\nThis tool has important safety guards against flag injection (see previous security findings).',
3052
3052
  permission: "auto",
3053
3053
  mutating: false,
3054
3054
  capabilities: ["fs.read"],
@@ -3125,8 +3125,8 @@ function findGitDir(cwd) {
3125
3125
  let dir = cwd;
3126
3126
  for (let i = 0; i < 20; i++) {
3127
3127
  try {
3128
- const stat11 = statSync(path.join(dir, ".git"));
3129
- if (stat11.isDirectory()) return dir;
3128
+ const stat10 = statSync(path.join(dir, ".git"));
3129
+ if (stat10.isDirectory()) return dir;
3130
3130
  } catch {
3131
3131
  }
3132
3132
  const parent = path.dirname(dir);
@@ -3156,7 +3156,7 @@ function runGit(args, cwd, signal) {
3156
3156
  });
3157
3157
  }
3158
3158
  async function fileDiff(input, ctx, _signal) {
3159
- input.context ?? 3;
3159
+ void input.context;
3160
3160
  const files = input.files ? (Array.isArray(input.files) ? input.files : input.files.split(",")).map((f) => f.trim()).filter(Boolean) : [];
3161
3161
  if (files.length === 0) {
3162
3162
  return {
@@ -3169,31 +3169,32 @@ async function fileDiff(input, ctx, _signal) {
3169
3169
  const results = [];
3170
3170
  for (const file of files) {
3171
3171
  const absPath = safeResolve(file, ctx);
3172
- const stat11 = await fs11.stat(absPath).catch(() => null);
3173
- if (!stat11?.isFile()) continue;
3172
+ const stat10 = await fs11.stat(absPath).catch(() => null);
3173
+ if (!stat10?.isFile()) continue;
3174
3174
  const content = await fs11.readFile(absPath, "utf8");
3175
3175
  const lines = content.split(/\r?\n/);
3176
- results.push(`--- ${file}
3177
- +++ ${file}
3178
- ${formatUnified(lines)}`);
3176
+ results.push(formatWithLineNumbers(file, lines));
3179
3177
  }
3180
3178
  return {
3181
- diff: results.join("\n"),
3179
+ diff: results.join("\n\n"),
3182
3180
  files,
3183
3181
  truncated: false,
3184
3182
  mode: input.mode ?? "unified"
3185
3183
  };
3186
3184
  }
3187
- function formatUnified(lines, _context) {
3188
- return lines.map((line, _i) => ` ${line}`).join("\n");
3185
+ function formatWithLineNumbers(file, lines) {
3186
+ const width = String(lines.length).length;
3187
+ const numbered = lines.map((line, i) => `${String(i + 1).padStart(width)} | ${line}`).join("\n");
3188
+ return `--- ${file} (line-numbered dump, not a unified diff) ---
3189
+ ${numbered}`;
3189
3190
  }
3190
3191
  var documentTool = {
3191
3192
  name: "document",
3192
3193
  category: "Project",
3193
- description: "Automatically generate or update documentation comments (JSDoc/TSDoc style) for code. Can target specific symbols or entire files/directories.",
3194
- usageHint: "USE FOR IMPROVING CODE DOCUMENTATION:\n\n- Good for adding missing docs to public APIs or complex functions.\n- `overwrite: true` will replace existing documentation (use carefully).\n- You can target specific symbols via `target` or whole files/directories via `files`.\nAlways review the generated documentation before committing \u2014 the model can hallucinate details.",
3195
- permission: "confirm",
3196
- mutating: true,
3194
+ description: "Preview documentation comments (JSDoc/TSDoc style) that would be generated for code symbols. Returns a list of candidates with status `skipped` \u2014 the tool is currently a read-only preview and does NOT write to files.",
3195
+ usageHint: "USE FOR IMPROVING CODE DOCUMENTATION:\n\n- Good for adding missing docs to public APIs or complex functions.\n- Currently this is a PREVIEW-ONLY tool: it does not modify files.\n- Use the output to decide which symbols to document manually, or pass the candidates to `edit` / `patch`.\n- `overwrite`, `style`, and `target` parameters are accepted for future expansion but are ignored today.\nAlways review the proposed documentation before applying it \u2014 the model can hallucinate details.",
3196
+ permission: "auto",
3197
+ mutating: false,
3197
3198
  timeoutMs: 3e4,
3198
3199
  inputSchema: {
3199
3200
  type: "object",
@@ -3268,8 +3269,8 @@ async function resolveFiles(filesInput, cwd) {
3268
3269
  for (const f of files) {
3269
3270
  const absPath = f.trim().startsWith("/") ? f.trim() : `${cwd}/${f.trim()}`;
3270
3271
  try {
3271
- const stat11 = await fs11.stat(absPath);
3272
- if (stat11.isFile()) resolved.push(absPath);
3272
+ const stat10 = await fs11.stat(absPath);
3273
+ if (stat10.isFile()) resolved.push(absPath);
3273
3274
  } catch {
3274
3275
  }
3275
3276
  }
@@ -3360,13 +3361,13 @@ var editTool = {
3360
3361
  if (input.new_string === void 0) throw new Error("edit: new_string is required");
3361
3362
  if (input.old_string === "") throw new Error("edit: old_string cannot be empty");
3362
3363
  const absPath = await safeResolveReal(input.path, ctx);
3363
- const stat11 = await fs11.stat(absPath).catch((err) => {
3364
+ const stat10 = await fs11.stat(absPath).catch((err) => {
3364
3365
  if (err.code === "ENOENT") {
3365
3366
  throw new Error(`edit: file "${input.path}" does not exist. Use \`write\` instead.`);
3366
3367
  }
3367
3368
  throw err;
3368
3369
  });
3369
- if (!stat11.isFile()) throw new Error(`edit: "${input.path}" is not a regular file`);
3370
+ if (!stat10.isFile()) throw new Error(`edit: "${input.path}" is not a regular file`);
3370
3371
  if (!ctx.hasRead(absPath)) {
3371
3372
  throw new Error(`edit: file "${input.path}" was not read in this session. Read it first.`);
3372
3373
  }
@@ -4062,6 +4063,7 @@ var formatTool = {
4062
4063
  usageHint: "RUN REGULARLY:\n\n- Use on changed files before committing.\n- `check: true` verifies formatting without making changes (useful in CI-like flows).\nThis project has very consistent formatting expectations. Always ensure your changes are formatted.",
4063
4064
  permission: "confirm",
4064
4065
  mutating: true,
4066
+ capabilities: ["fs.write", "shell.exec"],
4065
4067
  timeoutMs: 6e4,
4066
4068
  inputSchema: {
4067
4069
  type: "object",
@@ -4139,13 +4141,13 @@ var formatTool = {
4139
4141
  }
4140
4142
  };
4141
4143
  async function detectFixer(cwd) {
4142
- const { stat: stat11 } = await import('node:fs/promises');
4144
+ const { stat: stat10 } = await import('node:fs/promises');
4143
4145
  try {
4144
- await stat11(`${cwd}/biome.json`);
4146
+ await stat10(`${cwd}/biome.json`);
4145
4147
  return "biome";
4146
4148
  } catch {
4147
4149
  try {
4148
- await stat11(`${cwd}/.prettierrc`);
4150
+ await stat10(`${cwd}/.prettierrc`);
4149
4151
  return "prettier";
4150
4152
  } catch {
4151
4153
  return "biome";
@@ -4275,8 +4277,8 @@ function findGitDir2(cwd, projectRoot) {
4275
4277
  let dir = cwd;
4276
4278
  for (let i = 0; i < 20; i++) {
4277
4279
  try {
4278
- const stat11 = statSync(`${dir}/.git`);
4279
- if (stat11.isDirectory() || stat11.isFile()) return dir;
4280
+ const stat10 = statSync(`${dir}/.git`);
4281
+ if (stat10.isDirectory() || stat10.isFile()) return dir;
4280
4282
  } catch {
4281
4283
  }
4282
4284
  if (dir === root) break;
@@ -4772,8 +4774,8 @@ async function runNative(input, base, mode, limit, signal) {
4772
4774
  if (globRe && !globRe.test(e.name) && !globRe.test(full)) continue;
4773
4775
  if (globRe) globRe.lastIndex = 0;
4774
4776
  try {
4775
- const stat11 = await fs11.stat(full);
4776
- if (stat11.size > 1e6) continue;
4777
+ const stat10 = await fs11.stat(full);
4778
+ if (stat10.size > 1e6) continue;
4777
4779
  const head = await fs11.readFile(full);
4778
4780
  if (isBinaryBuffer(head)) continue;
4779
4781
  const text = head.toString("utf8");
@@ -4918,20 +4920,6 @@ var installTool = {
4918
4920
  };
4919
4921
  }
4920
4922
  };
4921
- async function detectPackageManager(cwd) {
4922
- const { stat: stat11 } = await import('node:fs/promises');
4923
- try {
4924
- await stat11(`${cwd}/pnpm-lock.yaml`);
4925
- return "pnpm";
4926
- } catch {
4927
- try {
4928
- await stat11(`${cwd}/yarn.lock`);
4929
- return "yarn";
4930
- } catch {
4931
- return "npm";
4932
- }
4933
- }
4934
- }
4935
4923
  var jsonTool = {
4936
4924
  name: "json",
4937
4925
  category: "Data",
@@ -5132,11 +5120,11 @@ var lintTool = {
5132
5120
  }
5133
5121
  };
5134
5122
  async function detectLinter(cwd) {
5135
- const { stat: stat11 } = await import('node:fs/promises');
5123
+ const { stat: stat10 } = await import('node:fs/promises');
5136
5124
  const checks = ["biome.json", ".eslintrc.json", "tslint.json", ".eslintrc.js", "tsconfig.json"];
5137
5125
  for (const f of checks) {
5138
5126
  try {
5139
- await stat11(`${cwd}/${f}`);
5127
+ await stat10(`${cwd}/${f}`);
5140
5128
  if (f.includes("biome")) return "biome";
5141
5129
  if (f.includes("eslint")) return "eslint";
5142
5130
  if (f.includes("tslint")) return "tslint";
@@ -5352,7 +5340,7 @@ var outdatedTool = {
5352
5340
  description: "Check for outdated dependencies in the project. Reports current, wanted (semver range), and latest versions available.",
5353
5341
  usageHint: "MAINTENANCE & SECURITY TOOL:\n\n- Run periodically or before dependency-related work.\n- Helps surface packages that may need updates for security or features.\n- Safe, read-only operation.\nUse the output to decide on upgrades. Prefer this over manual shell commands for dependency hygiene.",
5354
5342
  permission: "auto",
5355
- mutating: true,
5343
+ mutating: false,
5356
5344
  timeoutMs: 6e4,
5357
5345
  inputSchema: {
5358
5346
  type: "object",
@@ -5375,26 +5363,13 @@ var outdatedTool = {
5375
5363
  },
5376
5364
  async execute(input, ctx, opts) {
5377
5365
  const cwd = input.cwd ? safeResolve(input.cwd, ctx) : ctx.cwd;
5378
- const manager = await detectManager2(cwd);
5366
+ const manager = await detectPackageManager(cwd);
5379
5367
  const args = ["outdated", "--json"];
5380
5368
  if (input.format === "table") args.push("--table");
5381
5369
  if (input.include_deprecated) args.push("--include", "deprecated");
5382
5370
  return runOutdated(manager, args, cwd, opts.signal);
5383
5371
  }
5384
5372
  };
5385
- async function detectManager2(cwd) {
5386
- try {
5387
- await stat(`${cwd}/pnpm-lock.yaml`);
5388
- return "pnpm";
5389
- } catch {
5390
- }
5391
- try {
5392
- await stat(`${cwd}/yarn.lock`);
5393
- return "yarn";
5394
- } catch {
5395
- }
5396
- return "npm";
5397
- }
5398
5373
  function runOutdated(manager, args, cwd, signal) {
5399
5374
  return new Promise((resolve7) => {
5400
5375
  let stdout = "";
@@ -5571,15 +5546,26 @@ var planTool = {
5571
5546
  category: "Session",
5572
5547
  description: "Manage a persistent strategic plan for the current session. Unlike todos, plans are meant for higher-level, multi-phase approaches and survive across conversation resumptions. Use this to outline big-picture work, then promote concrete items into the todo list when ready to execute.",
5573
5548
  usageHint: 'RECOMMENDED FOR COMPLEX, MULTI-PHASE WORK:\n\n- Start by creating a high-level plan with `action: "add"` or using templates (`template_use`).\n- Use `promote` to turn a plan item into actionable todos.\n- Keep plans at the "why and what" level, and todos at the "how and next step" level.\n- Common templates: "new-feature", "bug-fix", "refactor", "release", "security-audit".\n\nThis tool is excellent for maintaining long-term direction across many turns or even multiple sessions.',
5574
- permission: "auto",
5575
- mutating: false,
5549
+ permission: "confirm",
5550
+ mutating: true,
5551
+ capabilities: ["fs.write"],
5576
5552
  timeoutMs: 2e3,
5577
5553
  inputSchema: {
5578
5554
  type: "object",
5579
5555
  properties: {
5580
5556
  action: {
5581
5557
  type: "string",
5582
- enum: ["show", "add", "start", "done", "remove", "promote", "derive", "template_use", "clear"],
5558
+ enum: [
5559
+ "show",
5560
+ "add",
5561
+ "start",
5562
+ "done",
5563
+ "remove",
5564
+ "promote",
5565
+ "derive",
5566
+ "template_use",
5567
+ "clear"
5568
+ ],
5583
5569
  description: "The operation to perform on the plan board."
5584
5570
  },
5585
5571
  title: {
@@ -5672,7 +5658,12 @@ var planTool = {
5672
5658
  plan = derived.plan;
5673
5659
  await savePlan(planPath, plan);
5674
5660
  ctx.state.replaceTodos(derived.todos);
5675
- return mkResult(plan, true, `${input.action} ok \u2014 ${derived.todos.length} todo(s) created.`, derived.todos);
5661
+ return mkResult(
5662
+ plan,
5663
+ true,
5664
+ `${input.action} ok \u2014 ${derived.todos.length} todo(s) created.`,
5665
+ derived.todos
5666
+ );
5676
5667
  }
5677
5668
  case "template_use": {
5678
5669
  const templateName = input.template?.trim();
@@ -5687,7 +5678,11 @@ var planTool = {
5687
5678
  ({ plan } = addPlanItem(plan, item.title, item.details));
5688
5679
  }
5689
5680
  await savePlan(planPath, plan);
5690
- return mkResult(plan, true, `Applied template "${template.name}" \u2014 ${template.items.length} items added.`);
5681
+ return mkResult(
5682
+ plan,
5683
+ true,
5684
+ `Applied template "${template.name}" \u2014 ${template.items.length} items added.`
5685
+ );
5691
5686
  }
5692
5687
  case "clear":
5693
5688
  plan = clearPlan(plan);
@@ -5742,9 +5737,9 @@ var readTool = {
5742
5737
  async execute(input, ctx) {
5743
5738
  if (!input?.path) throw new Error("read: path is required");
5744
5739
  const absPath = await safeResolveReal(input.path, ctx);
5745
- let stat11;
5740
+ let stat10;
5746
5741
  try {
5747
- stat11 = await fs11.stat(absPath);
5742
+ stat10 = await fs11.stat(absPath);
5748
5743
  } catch (err) {
5749
5744
  const code = err.code;
5750
5745
  if (code === "ENOENT") throw new Error(`read: file not found "${input.path}"`);
@@ -5752,9 +5747,9 @@ var readTool = {
5752
5747
  `read: failed to stat "${input.path}": ${err instanceof Error ? err.message : String(err)}`
5753
5748
  );
5754
5749
  }
5755
- if (!stat11.isFile()) throw new Error(`read: "${input.path}" is not a regular file`);
5756
- if (stat11.size > MAX_BYTES2) {
5757
- throw new Error(`read: file too large (${stat11.size} bytes, limit ${MAX_BYTES2})`);
5750
+ if (!stat10.isFile()) throw new Error(`read: "${input.path}" is not a regular file`);
5751
+ if (stat10.size > MAX_BYTES2) {
5752
+ throw new Error(`read: file too large (${stat10.size} bytes, limit ${MAX_BYTES2})`);
5758
5753
  }
5759
5754
  const buf = await fs11.readFile(absPath);
5760
5755
  if (isBinaryBuffer(buf)) {
@@ -5766,14 +5761,14 @@ var readTool = {
5766
5761
  const offset = Math.max(1, input.offset ?? 1);
5767
5762
  const limit = Math.max(0, Math.min(input.limit ?? 2e3, 5e3));
5768
5763
  if (limit === 0) {
5769
- ctx.recordRead(absPath, stat11.mtimeMs);
5764
+ ctx.recordRead(absPath, stat10.mtimeMs);
5770
5765
  return { text: "", total_lines: total, encoding: "utf8", truncated: total > 0 };
5771
5766
  }
5772
5767
  const slice = allLines.slice(offset - 1, offset - 1 + limit);
5773
5768
  const truncated = offset - 1 + slice.length < total;
5774
5769
  const width = String(offset + slice.length - 1).length;
5775
5770
  const numbered = slice.map((line, i) => `${String(offset + i).padStart(width, " ")}\u2192${line}`).join("\n");
5776
- ctx.recordRead(absPath, stat11.mtimeMs);
5771
+ ctx.recordRead(absPath, stat10.mtimeMs);
5777
5772
  return {
5778
5773
  text: numbered,
5779
5774
  total_lines: total,
@@ -5842,8 +5837,8 @@ var replaceTool = {
5842
5837
  }
5843
5838
  const rel = path.relative(realRoot, realPath);
5844
5839
  if (rel.startsWith("..") || path.isAbsolute(rel)) continue;
5845
- const stat11 = await fs11.stat(realPath).catch(() => null);
5846
- if (!stat11 || !stat11.isFile()) continue;
5840
+ const stat10 = await fs11.stat(realPath).catch(() => null);
5841
+ if (!stat10 || !stat10.isFile()) continue;
5847
5842
  let content;
5848
5843
  try {
5849
5844
  const buf = await fs11.readFile(realPath);
@@ -5868,7 +5863,7 @@ var replaceTool = {
5868
5863
  totalReplacements += count;
5869
5864
  if (!dryRun) {
5870
5865
  const newContent = toStyle(newContentLf, style);
5871
- await atomicWrite(realPath, newContent, { mode: stat11.mode & 511 });
5866
+ await atomicWrite(realPath, newContent, { mode: stat10.mode & 511 });
5872
5867
  }
5873
5868
  const diff = dryRun || matches.length > 0 ? unifiedDiff(content, toStyle(newContentLf, style), {
5874
5869
  fromFile: absPath,
@@ -5898,8 +5893,8 @@ async function resolveFiles2(filesInput, ctx, extraGlob) {
5898
5893
  const resolved = [];
5899
5894
  for (const p of parts) {
5900
5895
  const absPath = safeResolve(p, ctx);
5901
- const stat11 = await fs11.stat(absPath).catch(() => null);
5902
- if (stat11?.isFile()) {
5896
+ const stat10 = await fs11.stat(absPath).catch(() => null);
5897
+ if (stat10?.isFile()) {
5903
5898
  resolved.push(absPath);
5904
5899
  }
5905
5900
  }
@@ -5957,8 +5952,8 @@ async function globNative(pattern, base, extraGlob) {
5957
5952
  if (DEFAULT_IGNORE4.includes(e.name)) continue;
5958
5953
  const full = path.join(dir, e.name);
5959
5954
  try {
5960
- const stat11 = await fs11.lstat(full);
5961
- if (stat11.isSymbolicLink()) continue;
5955
+ const stat10 = await fs11.lstat(full);
5956
+ if (stat10.isSymbolicLink()) continue;
5962
5957
  } catch {
5963
5958
  continue;
5964
5959
  }
@@ -6443,11 +6438,11 @@ var testTool = {
6443
6438
  }
6444
6439
  };
6445
6440
  async function detectRunner(cwd) {
6446
- const { stat: stat11 } = await import('node:fs/promises');
6441
+ const { stat: stat10 } = await import('node:fs/promises');
6447
6442
  const candidates = ["vitest.config.ts", "jest.config.js", ".mocharc.json"];
6448
6443
  for (const f of candidates) {
6449
6444
  try {
6450
- await stat11(path.join(cwd, f));
6445
+ await stat10(path.join(cwd, f));
6451
6446
  if (f.includes("vitest")) return "vitest";
6452
6447
  if (f.includes("jest")) return "jest";
6453
6448
  if (f.includes("mocha")) return "mocha";
@@ -7093,11 +7088,11 @@ var typecheckTool = {
7093
7088
  }
7094
7089
  };
7095
7090
  async function findTsConfig(cwd) {
7096
- const { stat: stat11 } = await import('node:fs/promises');
7091
+ const { stat: stat10 } = await import('node:fs/promises');
7097
7092
  const candidates = ["tsconfig.json", "tsconfig.base.json"];
7098
7093
  for (const f of candidates) {
7099
7094
  try {
7100
- const s = await stat11(path.join(cwd, f));
7095
+ const s = await stat10(path.join(cwd, f));
7101
7096
  if (s.isFile()) return path.join(cwd, f);
7102
7097
  } catch {
7103
7098
  }
@@ -7134,12 +7129,12 @@ var writeTool = {
7134
7129
  let existed = false;
7135
7130
  let prev = "";
7136
7131
  try {
7137
- const stat12 = await fs11.stat(absPath);
7138
- existed = stat12.isFile();
7132
+ const stat11 = await fs11.stat(absPath);
7133
+ existed = stat11.isFile();
7139
7134
  if (existed) {
7140
7135
  if (!ctx.hasRead(absPath)) {
7141
7136
  prev = await fs11.readFile(absPath, "utf8");
7142
- ctx.recordRead(absPath, stat12.mtimeMs);
7137
+ ctx.recordRead(absPath, stat11.mtimeMs);
7143
7138
  } else {
7144
7139
  prev = await fs11.readFile(absPath, "utf8");
7145
7140
  }
@@ -7152,8 +7147,8 @@ var writeTool = {
7152
7147
  await atomicWrite(absPath, input.content);
7153
7148
  const diff = existed ? unifiedDiff(prev, input.content, { fromFile: input.path, toFile: input.path }) : `+++ ${input.path}
7154
7149
  + (new file, ${input.content.split("\n").length} lines)`;
7155
- const stat11 = await fs11.stat(absPath);
7156
- ctx.recordRead(absPath, stat11.mtimeMs);
7150
+ const stat10 = await fs11.stat(absPath);
7151
+ ctx.recordRead(absPath, stat10.mtimeMs);
7157
7152
  ctx.session.recordFileChange({
7158
7153
  path: absPath,
7159
7154
  action: existed ? "modified" : "created",