@wrongstack/tools 0.32.0 → 0.51.3

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":";;;;;;AAIO,SAAS,WAAA,CAAY,OAAe,GAAA,EAAsB;AAC/D,EAAA,OAAY,IAAA,CAAA,UAAA,CAAW,KAAK,CAAA,GAAS,IAAA,CAAA,SAAA,CAAU,KAAK,CAAA,GAAS,IAAA,CAAA,OAAA,CAAQ,GAAA,CAAI,GAAA,EAAK,KAAK,CAAA;AACrF;AAEO,SAAS,gBAAA,CAAiB,SAAiB,GAAA,EAAsB;AACtE,EAAA,MAAM,IAAA,GAAY,IAAA,CAAA,OAAA,CAAQ,GAAA,CAAI,WAAW,CAAA;AACzC,EAAA,MAAM,MAAA,GAAc,aAAQ,OAAO,CAAA;AACnC,EAAA,MAAM,GAAA,GAAW,IAAA,CAAA,QAAA,CAAS,IAAA,EAAM,MAAM,CAAA;AACtC,EAAA,IAAI,IAAI,UAAA,CAAW,IAAI,CAAA,IAAU,IAAA,CAAA,UAAA,CAAW,GAAG,CAAA,EAAG;AAChD,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,MAAA,EAAS,OAAO,CAAA,2BAAA,EAA8B,IAAI,CAAA,CAAA,CAAG,CAAA;AAAA,EACvE;AACA,EAAA,OAAO,MAAA;AACT;AAEO,SAAS,WAAA,CAAY,OAAe,GAAA,EAAsB;AAC/D,EAAA,OAAO,gBAAA,CAAiB,WAAA,CAAY,KAAA,EAAO,GAAG,GAAG,GAAG,CAAA;AACtD;;;ACUO,IAAM,YAAA,GAAoD;AAAA,EAC/D,IAAA,EAAM,UAAA;AAAA,EACN,QAAA,EAAU,oBAAA;AAAA,EACV,WAAA,EACE,wHAAA;AAAA,EACF,SAAA,EACE,qSAAA;AAAA,EAKF,UAAA,EAAY,MAAA;AAAA,EACZ,QAAA,EAAU,IAAA;AAAA,EACV,SAAA,EAAW,GAAA;AAAA,EACX,WAAA,EAAa;AAAA,IACX,IAAA,EAAM,QAAA;AAAA,IACN,UAAA,EAAY;AAAA,MACV,GAAA,EAAK,EAAE,IAAA,EAAM,QAAA,EAAU,aAAa,kCAAA,EAAmC;AAAA,MACvE,MAAA,EAAQ;AAAA,QACN,IAAA,EAAM,QAAA;AAAA,QACN,IAAA,EAAM,CAAC,MAAA,EAAQ,OAAO,CAAA;AAAA,QACtB,WAAA,EAAa;AAAA,OACf;AAAA,MACA,kBAAA,EAAoB;AAAA,QAClB,IAAA,EAAM,SAAA;AAAA,QACN,WAAA,EAAa;AAAA,OACf;AAAA,MACA,KAAA,EAAO;AAAA,QACL,IAAA,EAAM,QAAA;AAAA,QACN,WAAA,EAAa;AAAA;AACf;AACF,GACF;AAAA,EACA,MAAM,OAAA,CAAQ,KAAA,EAAO,GAAA,EAAK,IAAA,EAAM;AAC9B,IAAA,MAAM,GAAA,GAAM,MAAM,GAAA,GAAM,WAAA,CAAY,MAAM,GAAA,EAAK,GAAG,IAAI,GAAA,CAAI,GAAA;AAC1D,IAAA,MAAM,OAAA,GAAU,MAAM,aAAA,CAAc,GAAG,CAAA;AAEvC,IAAA,MAAM,IAAA,GAAiB,CAAC,UAAA,EAAY,QAAQ,CAAA;AAC5C,IAAA,IAAI,KAAA,CAAM,MAAA,KAAW,OAAA,EAAS,IAAA,CAAK,KAAK,SAAS,CAAA;AACjD,IAAA,IAAI,KAAA,CAAM,kBAAA,EAAoB,IAAA,CAAK,IAAA,CAAK,aAAa,YAAY,CAAA;AAEjE,IAAA,OAAO,WAAA,CAAY,OAAA,EAAS,IAAA,EAAM,GAAA,EAAK,KAAK,MAAM,CAAA;AAAA,EACpD;AACF;AAEA,eAAe,cAAc,GAAA,EAA8B;AACzD,EAAA,IAAI;AACF,IAAA,MAAM,IAAA,CAAK,CAAA,EAAG,GAAG,CAAA,eAAA,CAAiB,CAAA;AAClC,IAAA,OAAO,MAAA;AAAA,EACT,CAAA,CAAA,MAAQ;AAAA,EAER;AACA,EAAA,IAAI;AACF,IAAA,MAAM,IAAA,CAAK,CAAA,EAAG,GAAG,CAAA,UAAA,CAAY,CAAA;AAC7B,IAAA,OAAO,MAAA;AAAA,EACT,CAAA,CAAA,MAAQ;AAAA,EAER;AACA,EAAA,OAAO,KAAA;AACT;AAEA,SAAS,WAAA,CACP,OAAA,EACA,IAAA,EACA,GAAA,EACA,MAAA,EACyB;AACzB,EAAA,OAAO,IAAI,OAAA,CAAQ,CAACA,QAAAA,KAAY;AAC9B,IAAA,IAAI,MAAA,GAAS,EAAA;AACb,IAAA,IAAI,MAAA,GAAS,EAAA;AACb,IAAA,MAAM,GAAA,GAAM,GAAA;AAEZ,IAAA,MAAM,QAAQ,KAAA,CAAM,OAAA,EAAS,IAAA,EAAM,EAAE,KAAK,MAAA,EAAQ,GAAA,EAAK,aAAA,EAAc,EAAG,OAAO,CAAC,QAAA,EAAU,MAAA,EAAQ,MAAM,GAAG,CAAA;AAC3G,IAAA,KAAA,CAAM,MAAA,EAAQ,EAAA,CAAG,MAAA,EAAQ,CAAC,CAAA,KAAM;AAC9B,MAAA,IAAI,MAAA,CAAO,MAAA,GAAS,GAAA,EAAK,MAAA,IAAU,EAAE,QAAA,EAAS;AAAA,IAChD,CAAC,CAAA;AACD,IAAA,KAAA,CAAM,MAAA,EAAQ,EAAA,CAAG,MAAA,EAAQ,CAAC,CAAA,KAAM;AAC9B,MAAA,IAAI,MAAA,CAAO,MAAA,GAAS,GAAA,EAAK,MAAA,IAAU,EAAE,QAAA,EAAS;AAAA,IAChD,CAAC,CAAA;AACD,IAAA,KAAA,CAAM,EAAA,CAAG,OAAA,EAAS,CAAC,IAAA,KAAS;AAC1B,MAAA,MAAM,MAAA,GAAS,mBAAA,CAAoB,MAAA,EAAQ,IAAA,IAAQ,CAAC,CAAA;AACpD,MAAAA,SAAQ,MAAM,CAAA;AAAA,IAChB,CAAC,CAAA;AACD,IAAA,KAAA,CAAM,EAAA,CAAG,OAAA,EAAS,CAAC,CAAA,KAAM;AACvB,MAAAA,QAAAA,CAAQ;AAAA,QACN,SAAA,EAAW,CAAA;AAAA,QACX,UAAU,EAAC;AAAA,QACX,KAAA,EAAO,CAAA;AAAA,QACP,QAAQ,CAAA,CAAE,OAAA;AAAA,QACV,SAAA,EAAW;AAAA,OACZ,CAAA;AAAA,IACH,CAAC,CAAA;AAAA,EACH,CAAC,CAAA;AACH;AAEA,SAAS,mBAAA,CAAoB,MAAc,QAAA,EAAkC;AAC3E,EAAA,MAAM,WAA8B,EAAC;AAErC,EAAA,IAAI,CAAC,IAAA,EAAM;AACT,IAAA,OAAO;AAAA,MACL,SAAA,EAAW,QAAA;AAAA,MACX,UAAU,EAAC;AAAA,MACX,KAAA,EAAO,CAAA;AAAA,MACP,MAAA,EAAQ,QAAA,KAAa,CAAA,GAAI,yBAAA,GAA4B,mCAAA;AAAA,MACrD,SAAA,EAAW;AAAA,KACb;AAAA,EACF;AAEA,EAAA,IAAI;AACF,IAAA,MAAM,IAAA,GAAO,IAAA,CAAK,KAAA,CAAM,IAAI,CAAA;AAC5B,IAAA,KAAA,MAAW,IAAA,IAAQ,MAAA,CAAO,IAAA,CAAK,IAAI,CAAA,EAAG;AACpC,MAAA,MAAM,IAAA,GAAO,KAAK,IAAI,CAAA;AACtB,MAAA,QAAA,CAAS,IAAA,CAAK;AAAA,QACZ,IAAA;AAAA,QACA,OAAA,EAAS,KAAK,OAAA,IAAW,SAAA;AAAA,QACzB,MAAA,EAAQ,KAAK,MAAA,IAAU,SAAA;AAAA,QACvB,MAAA,EAAQ,KAAK,MAAA,IAAU,SAAA;AAAA,QACvB,IAAA,EAAM,KAAK,IAAA,IAAQ,SAAA;AAAA,QACnB,QAAA,EAAU,KAAK,QAAA,IAAY;AAAA,OAC5B,CAAA;AAAA,IACH;AAAA,EACF,CAAA,CAAA,MAAQ;AAAA,EAER;AAEA,EAAA,OAAO;AAAA,IACL,SAAA,EAAW,QAAA;AAAA,IACX,QAAA;AAAA,IACA,OAAO,QAAA,CAAS,MAAA;AAAA,IAChB,MAAA,EAAQ,IAAA;AAAA,IACR,SAAA,EAAW,KAAK,MAAA,IAAU;AAAA,GAC5B;AACF","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 * as Core from '@wrongstack/core';\nimport type { Context } 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 = 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 { 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,7 @@
1
1
  import { spawn, execFileSync, spawnSync } from 'node:child_process';
2
- import { buildChildEnv, detectNewlineStyle, normalizeToLf, toStyle, atomicWrite, unifiedDiff, compileGlob, loadPlan, emptyPlan, clearPlan, savePlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, setPlanItemStatus, formatPlan, stripAnsi, resolveWstackPaths } from '@wrongstack/core';
2
+ import * as Core from '@wrongstack/core';
3
+ import { buildChildEnv, detectNewlineStyle, normalizeToLf, toStyle, atomicWrite, unifiedDiff, compileGlob, loadPlan, emptyPlan, clearPlan, savePlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, setPlanItemStatus, formatPlan, resolveWstackPaths } from '@wrongstack/core';
3
4
  import * as fs11 from 'node:fs/promises';
4
- import { stat } from 'node:fs/promises';
5
5
  import * as path from 'node:path';
6
6
  import { resolve, sep, dirname } from 'node:path';
7
7
  import * as os from 'node:os';
@@ -97,6 +97,20 @@ async function* spawnStream(opts) {
97
97
  error
98
98
  };
99
99
  }
100
+ async function detectPackageManager(cwd) {
101
+ const { stat: stat10 } = await import('node:fs/promises');
102
+ try {
103
+ await stat10(`${cwd}/pnpm-lock.yaml`);
104
+ return "pnpm";
105
+ } catch {
106
+ }
107
+ try {
108
+ await stat10(`${cwd}/yarn.lock`);
109
+ return "yarn";
110
+ } catch {
111
+ }
112
+ return "npm";
113
+ }
100
114
  function resolvePath(input, ctx) {
101
115
  return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.cwd, input);
102
116
  }
@@ -219,7 +233,7 @@ ${tail}`;
219
233
  }
220
234
  function normalizeCommandOutput(raw, opts = {}) {
221
235
  if (!raw) return raw;
222
- let text = stripAnsi(raw);
236
+ let text = Core.stripAnsi(raw);
223
237
  text = collapseCarriageReturns(text);
224
238
  text = text.replace(/[ \t]+$/gm, "");
225
239
  text = collapseConsecutiveDuplicates(text);
@@ -259,7 +273,7 @@ var auditTool = {
259
273
  },
260
274
  async *executeStream(input, ctx, opts) {
261
275
  const cwd = input.cwd ? safeResolve(input.cwd, ctx) : ctx.cwd;
262
- const manager = await detectManager(cwd);
276
+ const manager = await detectPackageManager(cwd);
263
277
  yield { type: "log", text: `Auditing with ${manager}\u2026`, data: { manager } };
264
278
  const args = ["audit", "--json"];
265
279
  if (input.fix) args.push("--fix");
@@ -277,20 +291,6 @@ var auditTool = {
277
291
  yield { type: "final", output: parseAuditOutput(result.stdout, result.exitCode) };
278
292
  }
279
293
  };
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
294
  function parseAuditOutput(json, exitCode) {
295
295
  if (!json) {
296
296
  return {
@@ -2716,18 +2716,18 @@ async function runIndexer(_ctx, opts) {
2716
2716
  for (const meta of store.getAllFileMetas()) existingMeta.set(meta.file, meta);
2717
2717
  }
2718
2718
  for (const file of files) {
2719
- let stat11;
2719
+ let stat10;
2720
2720
  try {
2721
- stat11 = await fs11.stat(file);
2721
+ stat10 = await fs11.stat(file);
2722
2722
  } catch {
2723
2723
  store.deleteFile(file);
2724
2724
  continue;
2725
2725
  }
2726
- if (!stat11.isFile()) continue;
2726
+ if (!stat10.isFile()) continue;
2727
2727
  const lang = detectLang(file);
2728
2728
  if (!lang) continue;
2729
2729
  const meta = existingMeta.get(file);
2730
- if (!force && meta && meta.mtimeMs === Math.floor(stat11.mtimeMs)) {
2730
+ if (!force && meta && meta.mtimeMs === Math.floor(stat10.mtimeMs)) {
2731
2731
  langStats[lang] = (langStats[lang] ?? 0) + meta.symbolCount;
2732
2732
  symbolsIndexed += meta.symbolCount;
2733
2733
  filesIndexed++;
@@ -2753,7 +2753,7 @@ async function runIndexer(_ctx, opts) {
2753
2753
  store.upsertFile({
2754
2754
  file,
2755
2755
  lang,
2756
- mtimeMs: Math.floor(stat11.mtimeMs),
2756
+ mtimeMs: Math.floor(stat10.mtimeMs),
2757
2757
  symbolCount: 0,
2758
2758
  lastIndexed: Date.now()
2759
2759
  });
@@ -2779,7 +2779,7 @@ async function runIndexer(_ctx, opts) {
2779
2779
  store.upsertFile({
2780
2780
  file,
2781
2781
  lang,
2782
- mtimeMs: Math.floor(stat11.mtimeMs),
2782
+ mtimeMs: Math.floor(stat10.mtimeMs),
2783
2783
  symbolCount: count,
2784
2784
  lastIndexed: Date.now()
2785
2785
  });
@@ -2810,8 +2810,9 @@ var codebaseIndexTool = {
2810
2810
  category: "Project",
2811
2811
  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
2812
  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",
2813
+ permission: "confirm",
2814
2814
  mutating: true,
2815
+ capabilities: ["fs.write.outside-project"],
2815
2816
  timeoutMs: 12e4,
2816
2817
  inputSchema: {
2817
2818
  type: "object",
@@ -3047,8 +3048,8 @@ var codebaseStatsTool = {
3047
3048
  var diffTool = {
3048
3049
  name: "diff",
3049
3050
  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).',
3051
+ 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.",
3052
+ 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
3053
  permission: "auto",
3053
3054
  mutating: false,
3054
3055
  capabilities: ["fs.read"],
@@ -3125,8 +3126,8 @@ function findGitDir(cwd) {
3125
3126
  let dir = cwd;
3126
3127
  for (let i = 0; i < 20; i++) {
3127
3128
  try {
3128
- const stat11 = statSync(path.join(dir, ".git"));
3129
- if (stat11.isDirectory()) return dir;
3129
+ const stat10 = statSync(path.join(dir, ".git"));
3130
+ if (stat10.isDirectory()) return dir;
3130
3131
  } catch {
3131
3132
  }
3132
3133
  const parent = path.dirname(dir);
@@ -3156,7 +3157,7 @@ function runGit(args, cwd, signal) {
3156
3157
  });
3157
3158
  }
3158
3159
  async function fileDiff(input, ctx, _signal) {
3159
- input.context ?? 3;
3160
+ void input.context;
3160
3161
  const files = input.files ? (Array.isArray(input.files) ? input.files : input.files.split(",")).map((f) => f.trim()).filter(Boolean) : [];
3161
3162
  if (files.length === 0) {
3162
3163
  return {
@@ -3169,31 +3170,32 @@ async function fileDiff(input, ctx, _signal) {
3169
3170
  const results = [];
3170
3171
  for (const file of files) {
3171
3172
  const absPath = safeResolve(file, ctx);
3172
- const stat11 = await fs11.stat(absPath).catch(() => null);
3173
- if (!stat11?.isFile()) continue;
3173
+ const stat10 = await fs11.stat(absPath).catch(() => null);
3174
+ if (!stat10?.isFile()) continue;
3174
3175
  const content = await fs11.readFile(absPath, "utf8");
3175
3176
  const lines = content.split(/\r?\n/);
3176
- results.push(`--- ${file}
3177
- +++ ${file}
3178
- ${formatUnified(lines)}`);
3177
+ results.push(formatWithLineNumbers(file, lines));
3179
3178
  }
3180
3179
  return {
3181
- diff: results.join("\n"),
3180
+ diff: results.join("\n\n"),
3182
3181
  files,
3183
3182
  truncated: false,
3184
3183
  mode: input.mode ?? "unified"
3185
3184
  };
3186
3185
  }
3187
- function formatUnified(lines, _context) {
3188
- return lines.map((line, _i) => ` ${line}`).join("\n");
3186
+ function formatWithLineNumbers(file, lines) {
3187
+ const width = String(lines.length).length;
3188
+ const numbered = lines.map((line, i) => `${String(i + 1).padStart(width)} | ${line}`).join("\n");
3189
+ return `--- ${file} (line-numbered dump, not a unified diff) ---
3190
+ ${numbered}`;
3189
3191
  }
3190
3192
  var documentTool = {
3191
3193
  name: "document",
3192
3194
  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,
3195
+ 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.",
3196
+ 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.",
3197
+ permission: "auto",
3198
+ mutating: false,
3197
3199
  timeoutMs: 3e4,
3198
3200
  inputSchema: {
3199
3201
  type: "object",
@@ -3268,8 +3270,8 @@ async function resolveFiles(filesInput, cwd) {
3268
3270
  for (const f of files) {
3269
3271
  const absPath = f.trim().startsWith("/") ? f.trim() : `${cwd}/${f.trim()}`;
3270
3272
  try {
3271
- const stat11 = await fs11.stat(absPath);
3272
- if (stat11.isFile()) resolved.push(absPath);
3273
+ const stat10 = await fs11.stat(absPath);
3274
+ if (stat10.isFile()) resolved.push(absPath);
3273
3275
  } catch {
3274
3276
  }
3275
3277
  }
@@ -3360,13 +3362,13 @@ var editTool = {
3360
3362
  if (input.new_string === void 0) throw new Error("edit: new_string is required");
3361
3363
  if (input.old_string === "") throw new Error("edit: old_string cannot be empty");
3362
3364
  const absPath = await safeResolveReal(input.path, ctx);
3363
- const stat11 = await fs11.stat(absPath).catch((err) => {
3365
+ const stat10 = await fs11.stat(absPath).catch((err) => {
3364
3366
  if (err.code === "ENOENT") {
3365
3367
  throw new Error(`edit: file "${input.path}" does not exist. Use \`write\` instead.`);
3366
3368
  }
3367
3369
  throw err;
3368
3370
  });
3369
- if (!stat11.isFile()) throw new Error(`edit: "${input.path}" is not a regular file`);
3371
+ if (!stat10.isFile()) throw new Error(`edit: "${input.path}" is not a regular file`);
3370
3372
  if (!ctx.hasRead(absPath)) {
3371
3373
  throw new Error(`edit: file "${input.path}" was not read in this session. Read it first.`);
3372
3374
  }
@@ -4062,6 +4064,7 @@ var formatTool = {
4062
4064
  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
4065
  permission: "confirm",
4064
4066
  mutating: true,
4067
+ capabilities: ["fs.write", "shell.exec"],
4065
4068
  timeoutMs: 6e4,
4066
4069
  inputSchema: {
4067
4070
  type: "object",
@@ -4139,13 +4142,13 @@ var formatTool = {
4139
4142
  }
4140
4143
  };
4141
4144
  async function detectFixer(cwd) {
4142
- const { stat: stat11 } = await import('node:fs/promises');
4145
+ const { stat: stat10 } = await import('node:fs/promises');
4143
4146
  try {
4144
- await stat11(`${cwd}/biome.json`);
4147
+ await stat10(`${cwd}/biome.json`);
4145
4148
  return "biome";
4146
4149
  } catch {
4147
4150
  try {
4148
- await stat11(`${cwd}/.prettierrc`);
4151
+ await stat10(`${cwd}/.prettierrc`);
4149
4152
  return "prettier";
4150
4153
  } catch {
4151
4154
  return "biome";
@@ -4275,8 +4278,8 @@ function findGitDir2(cwd, projectRoot) {
4275
4278
  let dir = cwd;
4276
4279
  for (let i = 0; i < 20; i++) {
4277
4280
  try {
4278
- const stat11 = statSync(`${dir}/.git`);
4279
- if (stat11.isDirectory() || stat11.isFile()) return dir;
4281
+ const stat10 = statSync(`${dir}/.git`);
4282
+ if (stat10.isDirectory() || stat10.isFile()) return dir;
4280
4283
  } catch {
4281
4284
  }
4282
4285
  if (dir === root) break;
@@ -4772,8 +4775,8 @@ async function runNative(input, base, mode, limit, signal) {
4772
4775
  if (globRe && !globRe.test(e.name) && !globRe.test(full)) continue;
4773
4776
  if (globRe) globRe.lastIndex = 0;
4774
4777
  try {
4775
- const stat11 = await fs11.stat(full);
4776
- if (stat11.size > 1e6) continue;
4778
+ const stat10 = await fs11.stat(full);
4779
+ if (stat10.size > 1e6) continue;
4777
4780
  const head = await fs11.readFile(full);
4778
4781
  if (isBinaryBuffer(head)) continue;
4779
4782
  const text = head.toString("utf8");
@@ -4918,20 +4921,6 @@ var installTool = {
4918
4921
  };
4919
4922
  }
4920
4923
  };
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
4924
  var jsonTool = {
4936
4925
  name: "json",
4937
4926
  category: "Data",
@@ -5132,11 +5121,11 @@ var lintTool = {
5132
5121
  }
5133
5122
  };
5134
5123
  async function detectLinter(cwd) {
5135
- const { stat: stat11 } = await import('node:fs/promises');
5124
+ const { stat: stat10 } = await import('node:fs/promises');
5136
5125
  const checks = ["biome.json", ".eslintrc.json", "tslint.json", ".eslintrc.js", "tsconfig.json"];
5137
5126
  for (const f of checks) {
5138
5127
  try {
5139
- await stat11(`${cwd}/${f}`);
5128
+ await stat10(`${cwd}/${f}`);
5140
5129
  if (f.includes("biome")) return "biome";
5141
5130
  if (f.includes("eslint")) return "eslint";
5142
5131
  if (f.includes("tslint")) return "tslint";
@@ -5352,7 +5341,7 @@ var outdatedTool = {
5352
5341
  description: "Check for outdated dependencies in the project. Reports current, wanted (semver range), and latest versions available.",
5353
5342
  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
5343
  permission: "auto",
5355
- mutating: true,
5344
+ mutating: false,
5356
5345
  timeoutMs: 6e4,
5357
5346
  inputSchema: {
5358
5347
  type: "object",
@@ -5375,26 +5364,13 @@ var outdatedTool = {
5375
5364
  },
5376
5365
  async execute(input, ctx, opts) {
5377
5366
  const cwd = input.cwd ? safeResolve(input.cwd, ctx) : ctx.cwd;
5378
- const manager = await detectManager2(cwd);
5367
+ const manager = await detectPackageManager(cwd);
5379
5368
  const args = ["outdated", "--json"];
5380
5369
  if (input.format === "table") args.push("--table");
5381
5370
  if (input.include_deprecated) args.push("--include", "deprecated");
5382
5371
  return runOutdated(manager, args, cwd, opts.signal);
5383
5372
  }
5384
5373
  };
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
5374
  function runOutdated(manager, args, cwd, signal) {
5399
5375
  return new Promise((resolve7) => {
5400
5376
  let stdout = "";
@@ -5571,15 +5547,26 @@ var planTool = {
5571
5547
  category: "Session",
5572
5548
  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
5549
  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,
5550
+ permission: "confirm",
5551
+ mutating: true,
5552
+ capabilities: ["fs.write"],
5576
5553
  timeoutMs: 2e3,
5577
5554
  inputSchema: {
5578
5555
  type: "object",
5579
5556
  properties: {
5580
5557
  action: {
5581
5558
  type: "string",
5582
- enum: ["show", "add", "start", "done", "remove", "promote", "derive", "template_use", "clear"],
5559
+ enum: [
5560
+ "show",
5561
+ "add",
5562
+ "start",
5563
+ "done",
5564
+ "remove",
5565
+ "promote",
5566
+ "derive",
5567
+ "template_use",
5568
+ "clear"
5569
+ ],
5583
5570
  description: "The operation to perform on the plan board."
5584
5571
  },
5585
5572
  title: {
@@ -5672,7 +5659,12 @@ var planTool = {
5672
5659
  plan = derived.plan;
5673
5660
  await savePlan(planPath, plan);
5674
5661
  ctx.state.replaceTodos(derived.todos);
5675
- return mkResult(plan, true, `${input.action} ok \u2014 ${derived.todos.length} todo(s) created.`, derived.todos);
5662
+ return mkResult(
5663
+ plan,
5664
+ true,
5665
+ `${input.action} ok \u2014 ${derived.todos.length} todo(s) created.`,
5666
+ derived.todos
5667
+ );
5676
5668
  }
5677
5669
  case "template_use": {
5678
5670
  const templateName = input.template?.trim();
@@ -5687,7 +5679,11 @@ var planTool = {
5687
5679
  ({ plan } = addPlanItem(plan, item.title, item.details));
5688
5680
  }
5689
5681
  await savePlan(planPath, plan);
5690
- return mkResult(plan, true, `Applied template "${template.name}" \u2014 ${template.items.length} items added.`);
5682
+ return mkResult(
5683
+ plan,
5684
+ true,
5685
+ `Applied template "${template.name}" \u2014 ${template.items.length} items added.`
5686
+ );
5691
5687
  }
5692
5688
  case "clear":
5693
5689
  plan = clearPlan(plan);
@@ -5742,9 +5738,9 @@ var readTool = {
5742
5738
  async execute(input, ctx) {
5743
5739
  if (!input?.path) throw new Error("read: path is required");
5744
5740
  const absPath = await safeResolveReal(input.path, ctx);
5745
- let stat11;
5741
+ let stat10;
5746
5742
  try {
5747
- stat11 = await fs11.stat(absPath);
5743
+ stat10 = await fs11.stat(absPath);
5748
5744
  } catch (err) {
5749
5745
  const code = err.code;
5750
5746
  if (code === "ENOENT") throw new Error(`read: file not found "${input.path}"`);
@@ -5752,9 +5748,9 @@ var readTool = {
5752
5748
  `read: failed to stat "${input.path}": ${err instanceof Error ? err.message : String(err)}`
5753
5749
  );
5754
5750
  }
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})`);
5751
+ if (!stat10.isFile()) throw new Error(`read: "${input.path}" is not a regular file`);
5752
+ if (stat10.size > MAX_BYTES2) {
5753
+ throw new Error(`read: file too large (${stat10.size} bytes, limit ${MAX_BYTES2})`);
5758
5754
  }
5759
5755
  const buf = await fs11.readFile(absPath);
5760
5756
  if (isBinaryBuffer(buf)) {
@@ -5766,14 +5762,14 @@ var readTool = {
5766
5762
  const offset = Math.max(1, input.offset ?? 1);
5767
5763
  const limit = Math.max(0, Math.min(input.limit ?? 2e3, 5e3));
5768
5764
  if (limit === 0) {
5769
- ctx.recordRead(absPath, stat11.mtimeMs);
5765
+ ctx.recordRead(absPath, stat10.mtimeMs);
5770
5766
  return { text: "", total_lines: total, encoding: "utf8", truncated: total > 0 };
5771
5767
  }
5772
5768
  const slice = allLines.slice(offset - 1, offset - 1 + limit);
5773
5769
  const truncated = offset - 1 + slice.length < total;
5774
5770
  const width = String(offset + slice.length - 1).length;
5775
5771
  const numbered = slice.map((line, i) => `${String(offset + i).padStart(width, " ")}\u2192${line}`).join("\n");
5776
- ctx.recordRead(absPath, stat11.mtimeMs);
5772
+ ctx.recordRead(absPath, stat10.mtimeMs);
5777
5773
  return {
5778
5774
  text: numbered,
5779
5775
  total_lines: total,
@@ -5842,8 +5838,8 @@ var replaceTool = {
5842
5838
  }
5843
5839
  const rel = path.relative(realRoot, realPath);
5844
5840
  if (rel.startsWith("..") || path.isAbsolute(rel)) continue;
5845
- const stat11 = await fs11.stat(realPath).catch(() => null);
5846
- if (!stat11 || !stat11.isFile()) continue;
5841
+ const stat10 = await fs11.stat(realPath).catch(() => null);
5842
+ if (!stat10 || !stat10.isFile()) continue;
5847
5843
  let content;
5848
5844
  try {
5849
5845
  const buf = await fs11.readFile(realPath);
@@ -5868,7 +5864,7 @@ var replaceTool = {
5868
5864
  totalReplacements += count;
5869
5865
  if (!dryRun) {
5870
5866
  const newContent = toStyle(newContentLf, style);
5871
- await atomicWrite(realPath, newContent, { mode: stat11.mode & 511 });
5867
+ await atomicWrite(realPath, newContent, { mode: stat10.mode & 511 });
5872
5868
  }
5873
5869
  const diff = dryRun || matches.length > 0 ? unifiedDiff(content, toStyle(newContentLf, style), {
5874
5870
  fromFile: absPath,
@@ -5898,8 +5894,8 @@ async function resolveFiles2(filesInput, ctx, extraGlob) {
5898
5894
  const resolved = [];
5899
5895
  for (const p of parts) {
5900
5896
  const absPath = safeResolve(p, ctx);
5901
- const stat11 = await fs11.stat(absPath).catch(() => null);
5902
- if (stat11?.isFile()) {
5897
+ const stat10 = await fs11.stat(absPath).catch(() => null);
5898
+ if (stat10?.isFile()) {
5903
5899
  resolved.push(absPath);
5904
5900
  }
5905
5901
  }
@@ -5957,8 +5953,8 @@ async function globNative(pattern, base, extraGlob) {
5957
5953
  if (DEFAULT_IGNORE4.includes(e.name)) continue;
5958
5954
  const full = path.join(dir, e.name);
5959
5955
  try {
5960
- const stat11 = await fs11.lstat(full);
5961
- if (stat11.isSymbolicLink()) continue;
5956
+ const stat10 = await fs11.lstat(full);
5957
+ if (stat10.isSymbolicLink()) continue;
5962
5958
  } catch {
5963
5959
  continue;
5964
5960
  }
@@ -6443,11 +6439,11 @@ var testTool = {
6443
6439
  }
6444
6440
  };
6445
6441
  async function detectRunner(cwd) {
6446
- const { stat: stat11 } = await import('node:fs/promises');
6442
+ const { stat: stat10 } = await import('node:fs/promises');
6447
6443
  const candidates = ["vitest.config.ts", "jest.config.js", ".mocharc.json"];
6448
6444
  for (const f of candidates) {
6449
6445
  try {
6450
- await stat11(path.join(cwd, f));
6446
+ await stat10(path.join(cwd, f));
6451
6447
  if (f.includes("vitest")) return "vitest";
6452
6448
  if (f.includes("jest")) return "jest";
6453
6449
  if (f.includes("mocha")) return "mocha";
@@ -7093,11 +7089,11 @@ var typecheckTool = {
7093
7089
  }
7094
7090
  };
7095
7091
  async function findTsConfig(cwd) {
7096
- const { stat: stat11 } = await import('node:fs/promises');
7092
+ const { stat: stat10 } = await import('node:fs/promises');
7097
7093
  const candidates = ["tsconfig.json", "tsconfig.base.json"];
7098
7094
  for (const f of candidates) {
7099
7095
  try {
7100
- const s = await stat11(path.join(cwd, f));
7096
+ const s = await stat10(path.join(cwd, f));
7101
7097
  if (s.isFile()) return path.join(cwd, f);
7102
7098
  } catch {
7103
7099
  }
@@ -7134,12 +7130,12 @@ var writeTool = {
7134
7130
  let existed = false;
7135
7131
  let prev = "";
7136
7132
  try {
7137
- const stat12 = await fs11.stat(absPath);
7138
- existed = stat12.isFile();
7133
+ const stat11 = await fs11.stat(absPath);
7134
+ existed = stat11.isFile();
7139
7135
  if (existed) {
7140
7136
  if (!ctx.hasRead(absPath)) {
7141
7137
  prev = await fs11.readFile(absPath, "utf8");
7142
- ctx.recordRead(absPath, stat12.mtimeMs);
7138
+ ctx.recordRead(absPath, stat11.mtimeMs);
7143
7139
  } else {
7144
7140
  prev = await fs11.readFile(absPath, "utf8");
7145
7141
  }
@@ -7152,8 +7148,8 @@ var writeTool = {
7152
7148
  await atomicWrite(absPath, input.content);
7153
7149
  const diff = existed ? unifiedDiff(prev, input.content, { fromFile: input.path, toFile: input.path }) : `+++ ${input.path}
7154
7150
  + (new file, ${input.content.split("\n").length} lines)`;
7155
- const stat11 = await fs11.stat(absPath);
7156
- ctx.recordRead(absPath, stat11.mtimeMs);
7151
+ const stat10 = await fs11.stat(absPath);
7152
+ ctx.recordRead(absPath, stat10.mtimeMs);
7157
7153
  ctx.session.recordFileChange({
7158
7154
  path: absPath,
7159
7155
  action: existed ? "modified" : "created",