@wrongstack/tools 0.236.0 → 0.250.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/audit.js +1 -0
  2. package/dist/audit.js.map +1 -1
  3. package/dist/background-indexer-DwJsyAB0.d.ts +373 -0
  4. package/dist/bash.js +5 -0
  5. package/dist/bash.js.map +1 -1
  6. package/dist/builtin.js +865 -327
  7. package/dist/builtin.js.map +1 -1
  8. package/dist/codebase-index/index.d.ts +53 -2
  9. package/dist/codebase-index/index.js +854 -364
  10. package/dist/codebase-index/index.js.map +1 -1
  11. package/dist/codebase-index/worker.d.ts +2 -0
  12. package/dist/codebase-index/worker.js +2321 -0
  13. package/dist/codebase-index/worker.js.map +1 -0
  14. package/dist/diff.js +2 -1
  15. package/dist/diff.js.map +1 -1
  16. package/dist/exec.js +1 -0
  17. package/dist/exec.js.map +1 -1
  18. package/dist/format.js +1 -0
  19. package/dist/format.js.map +1 -1
  20. package/dist/git.js +2 -1
  21. package/dist/git.js.map +1 -1
  22. package/dist/grep.js +2 -2
  23. package/dist/grep.js.map +1 -1
  24. package/dist/index.d.ts +1 -1
  25. package/dist/index.js +886 -386
  26. package/dist/index.js.map +1 -1
  27. package/dist/install.js +1 -0
  28. package/dist/install.js.map +1 -1
  29. package/dist/lint.js +1 -0
  30. package/dist/lint.js.map +1 -1
  31. package/dist/logs.js +1 -1
  32. package/dist/logs.js.map +1 -1
  33. package/dist/outdated.js +1 -1
  34. package/dist/outdated.js.map +1 -1
  35. package/dist/pack.js +865 -327
  36. package/dist/pack.js.map +1 -1
  37. package/dist/patch.js +1 -1
  38. package/dist/patch.js.map +1 -1
  39. package/dist/replace.js +3 -2
  40. package/dist/replace.js.map +1 -1
  41. package/dist/test.js +1 -0
  42. package/dist/test.js.map +1 -1
  43. package/dist/typecheck.js +1 -0
  44. package/dist/typecheck.js.map +1 -1
  45. package/package.json +2 -2
  46. package/dist/background-indexer-CtbgPExj.d.ts +0 -228
package/dist/patch.js CHANGED
@@ -113,7 +113,7 @@ function runPatch(args, cwd, signal) {
113
113
  let stdout = "";
114
114
  let stderr = "";
115
115
  const env = { ...buildChildEnv(), LANG: "C", LC_ALL: "C" };
116
- const child = spawn("patch", args, { cwd, signal, env, stdio: ["pipe", "pipe", "pipe"] });
116
+ const child = spawn("patch", args, { cwd, signal, env, stdio: ["pipe", "pipe", "pipe"], windowsHide: true });
117
117
  child.stdout?.on("data", (c) => {
118
118
  stdout += c.toString();
119
119
  });
package/dist/patch.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/_util.ts","../src/patch.ts"],"names":["path2","resolve"],"mappings":";;;;;;;AA8BO,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,UAAA,IAAc,GAAA,CAAI,GAAA,EAAK,KAAK,CAAA;AACvG;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;;;ACvBO,IAAM,SAAA,GAA2C;AAAA,EACtD,IAAA,EAAM,OAAA;AAAA,EACN,QAAA,EAAU,YAAA;AAAA,EACV,WAAA,EACE,+JAAA;AAAA,EACF,SAAA,EACE,6SAAA;AAAA,EAIF,UAAA,EAAY,SAAA;AAAA,EACZ,QAAA,EAAU,IAAA;AAAA,EACV,YAAA,EAAc,CAAC,UAAU,CAAA;AAAA,EACzB,SAAA,EAAW,GAAA;AAAA,EACX,WAAA,EAAa;AAAA,IACX,IAAA,EAAM,QAAA;AAAA,IACN,UAAA,EAAY;AAAA,MACV,KAAA,EAAO,EAAE,IAAA,EAAM,QAAA,EAAU,aAAa,4BAAA,EAA6B;AAAA,MACnE,SAAA,EAAW,EAAE,IAAA,EAAM,QAAA,EAAU,aAAa,yCAAA,EAA0C;AAAA,MACpF,KAAA,EAAO,EAAE,IAAA,EAAM,SAAA,EAAW,aAAa,4CAAA,EAA6C;AAAA,MACpF,OAAA,EAAS,EAAE,IAAA,EAAM,SAAA,EAAW,aAAa,0BAAA;AAA2B,KACtE;AAAA,IACA,QAAA,EAAU,CAAC,OAAO;AAAA,GACpB;AAAA,EACA,MAAM,OAAA,CAAQ,KAAA,EAAO,GAAA,EAAK,IAAA,EAAM;AAC9B,IAAA,IAAI,CAAC,KAAA,EAAO,KAAA,EAAO,MAAM,IAAI,MAAM,kCAAkC,CAAA;AAErE,IAAA,MAAM,GAAA,GAAM,MAAM,SAAA,GAAY,WAAA,CAAY,MAAM,SAAA,EAAW,GAAG,IAAI,GAAA,CAAI,GAAA;AAGtE,IAAA,MAAM,QAAQ,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,KAAA,CAAM,SAAS,CAAC,CAAA;AAC1C,IAAA,MAAM,MAAA,GAAS,MAAM,OAAA,IAAW,KAAA;AAKhC,IAAA,MAAM,OAAA,GAAU,kBAAA,CAAmB,KAAA,CAAM,KAAK,CAAA;AAC9C,IAAA,KAAA,MAAW,KAAK,OAAA,EAAS;AACvB,MAAA,MAAM,QAAA,GAAW,mBAAA,CAAoB,CAAA,EAAG,KAAK,CAAA;AAC7C,MAAA,IAAI,CAAC,QAAA,EAAU;AACf,MAAA,MAAM,SAAA,GAAiBA,IAAA,CAAA,OAAA,CAAQ,GAAA,EAAK,QAAQ,CAAA;AAC5C,MAAA,MAAM,GAAA,GAAWA,IAAA,CAAA,QAAA,CAAS,GAAA,CAAI,WAAA,EAAa,SAAS,CAAA;AACpD,MAAA,IAAI,IAAI,UAAA,CAAW,IAAI,CAAA,IAAUA,IAAA,CAAA,UAAA,CAAW,GAAG,CAAA,EAAG;AAChD,QAAA,OAAO;AAAA,UACL,OAAA,EAAS,CAAA;AAAA,UACT,QAAA,EAAU,CAAA;AAAA,UACV,OAAO,EAAC;AAAA,UACR,OAAA,EAAS,MAAA;AAAA,UACT,OAAA,EAAS,0BAA0B,CAAC,CAAA,+BAAA;AAAA,SACtC;AAAA,MACF;AAAA,IACF;AAKA,IAAA,MAAM,SAAS,MAAS,EAAA,CAAA,OAAA,CAAaA,UAAQ,EAAA,CAAA,MAAA,EAAO,EAAG,gBAAgB,CAAC,CAAA;AACxE,IAAA,IAAI;AACF,MAAA,MAAS,EAAA,CAAA,KAAA,CAAM,MAAA,EAAQ,GAAK,CAAA,CAAE,MAAM,MAAM;AAAA,MAE1C,CAAC,CAAA;AACD,MAAA,MAAM,SAAA,GAAiBA,IAAA,CAAA,IAAA,CAAK,MAAA,EAAQ,SAAS,CAAA;AAC7C,MAAA,MAAS,aAAU,SAAA,EAAW,KAAA,CAAM,OAAO,EAAE,IAAA,EAAM,KAAO,CAAA;AAE1D,MAAA,MAAM,IAAA,GAAO,CAAC,CAAA,EAAA,EAAK,KAAK,IAAI,SAAA,EAAW,GAAI,MAAA,GAAS,CAAC,WAAW,CAAA,GAAI,EAAC,EAAI,MAAM,SAAS,CAAA;AAExF,MAAA,MAAM,SAAS,MAAM,QAAA,CAAS,IAAA,EAAM,GAAA,EAAK,KAAK,MAAM,CAAA;AAEpD,MAAA,IAAI,MAAA,CAAO,QAAA,KAAa,CAAA,IAAK,CAAC,MAAA,EAAQ;AACpC,QAAA,OAAO;AAAA,UACL,OAAA,EAAS,CAAA;AAAA,UACT,QAAA,EAAU,CAAA;AAAA,UACV,OAAO,EAAC;AAAA,UACR,OAAA,EAAS,MAAA;AAAA,UACT,OAAA,EAAS,CAAA,cAAA,EAAiB,MAAA,CAAO,MAAA,IAAU,OAAO,MAAM,CAAA;AAAA,SAC1D;AAAA,MACF;AAEA,MAAA,MAAM,OAAA,GAAU,mBAAA,CAAoB,MAAA,CAAO,MAAM,CAAA;AACjD,MAAA,OAAO;AAAA,QACL,SAAS,OAAA,CAAQ,MAAA;AAAA,QACjB,QAAA,EAAU,CAAA;AAAA,QACV,KAAA,EAAO,OAAA;AAAA,QACP,OAAA,EAAS,MAAA;AAAA,QACT,OAAA,EAAS,OAAO,MAAA,IAAU;AAAA,OAC5B;AAAA,IACF,CAAA,SAAE;AACA,MAAA,MAAS,EAAA,CAAA,EAAA,CAAG,MAAA,EAAQ,EAAE,SAAA,EAAW,IAAA,EAAM,OAAO,IAAA,EAAM,CAAA,CAAE,KAAA,CAAM,MAAM;AAAA,MAAC,CAAC,CAAA;AAAA,IACtE;AAAA,EACF;AACF;AAGA,SAAS,mBAAmB,KAAA,EAAyB;AACnD,EAAA,MAAM,MAAgB,EAAC;AAGvB,EAAA,MAAM,EAAA,GAAK,0BAAA;AACX,EAAA,KAAA,MAAW,CAAA,IAAK,KAAA,CAAM,QAAA,CAAS,EAAE,CAAA,EAAG;AAClC,IAAA,MAAM,MAAA,GAAS,CAAA,CAAE,CAAC,CAAA,EAAG,IAAA,EAAK;AAC1B,IAAA,IAAI,CAAC,MAAA,IAAU,MAAA,KAAW,WAAA,EAAa;AACvC,IAAA,GAAA,CAAI,KAAK,MAAM,CAAA;AAAA,EACjB;AACA,EAAA,OAAO,GAAA;AACT;AAIA,SAAS,mBAAA,CAAoB,GAAW,KAAA,EAAmC;AAGzE,EAAA,MAAM,QAAQ,CAAA,CAAE,OAAA,CAAQ,OAAO,GAAG,CAAA,CAAE,MAAM,GAAG,CAAA;AAC7C,EAAA,IAAI,KAAA,CAAM,MAAA,IAAU,KAAA,EAAO,OAAO,MAAA;AAClC,EAAA,OAAO,KAAA,CAAM,KAAA,CAAM,KAAK,CAAA,CAAE,KAAK,GAAG,CAAA;AACpC;AAEA,SAAS,QAAA,CACP,IAAA,EACA,GAAA,EACA,MAAA,EAC+D;AAC/D,EAAA,OAAO,IAAI,OAAA,CAAQ,CAACC,QAAAA,KAAY;AAC9B,IAAA,IAAI,MAAA,GAAS,EAAA;AACb,IAAA,IAAI,MAAA,GAAS,EAAA;AAMb,IAAA,MAAM,GAAA,GAAM,EAAE,GAAG,aAAA,IAAiB,IAAA,EAAM,GAAA,EAAK,QAAQ,GAAA,EAAI;AACzD,IAAA,MAAM,KAAA,GAAQ,KAAA,CAAM,OAAA,EAAS,IAAA,EAAM,EAAE,GAAA,EAAK,MAAA,EAAQ,GAAA,EAAK,KAAA,EAAO,CAAC,MAAA,EAAQ,MAAA,EAAQ,MAAM,GAAG,CAAA;AACxF,IAAA,KAAA,CAAM,MAAA,EAAQ,EAAA,CAAG,MAAA,EAAQ,CAAC,CAAA,KAAM;AAC9B,MAAA,MAAA,IAAU,EAAE,QAAA,EAAS;AAAA,IACvB,CAAC,CAAA;AACD,IAAA,KAAA,CAAM,MAAA,EAAQ,EAAA,CAAG,MAAA,EAAQ,CAAC,CAAA,KAAM;AAC9B,MAAA,MAAA,IAAU,EAAE,QAAA,EAAS;AAAA,IACvB,CAAC,CAAA;AACD,IAAA,KAAA,CAAM,EAAA,CAAG,OAAA,EAAS,CAAC,IAAA,KAASA,QAAAA,CAAQ,EAAE,QAAA,EAAU,IAAA,IAAQ,CAAA,EAAG,MAAA,EAAQ,MAAA,EAAQ,CAAC,CAAA;AAC5E,IAAA,KAAA,CAAM,EAAA,CAAG,OAAA,EAAS,CAAC,CAAA,KAAMA,SAAQ,EAAE,QAAA,EAAU,CAAA,EAAG,MAAA,EAAQ,EAAA,EAAI,MAAA,EAAQ,CAAA,CAAE,OAAA,EAAS,CAAC,CAAA;AAAA,EAClF,CAAC,CAAA;AACH;AAEA,SAAS,oBAAoB,MAAA,EAA0B;AACrD,EAAA,MAAM,QAAkB,EAAC;AACzB,EAAA,MAAM,EAAA,GAAK,sBAAA;AACX,EAAA,KAAA,MAAW,CAAA,IAAK,MAAA,CAAO,QAAA,CAAS,EAAE,CAAA,EAAG;AACnC,IAAA,IAAI,EAAE,CAAC,CAAA,QAAS,IAAA,CAAK,CAAA,CAAE,CAAC,CAAC,CAAA;AAAA,EAC3B;AACA,EAAA,OAAO,KAAA;AACT","file":"patch.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/** Detected package manager for a project directory. */\nexport type PackageManager = 'pnpm' | 'yarn' | 'npm';\n\n/**\n * Detect the project's package manager by inspecting lockfiles in `cwd`.\n * Order: pnpm → yarn → npm (default). Missing or unreadable directories fall\n * back to `npm` rather than throwing, so a `safeResolve`-checked cwd that\n * happens to be empty never aborts the tool.\n */\nexport async function detectPackageManager(cwd: string): Promise<PackageManager> {\n const { stat } = await import('node:fs/promises');\n try {\n await stat(`${cwd}/pnpm-lock.yaml`);\n return 'pnpm';\n } catch {\n /* not pnpm */\n }\n try {\n await stat(`${cwd}/yarn.lock`);\n return 'yarn';\n } catch {\n /* not yarn */\n }\n return 'npm';\n}\n\nexport function resolvePath(input: string, ctx: Context): string {\n return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.workingDir ?? ctx.cwd, input);\n}\n\nexport function ensureInsideRoot(absPath: string, ctx: Context): string {\n const root = path.resolve(ctx.projectRoot);\n const target = path.resolve(absPath);\n const rel = path.relative(root, target);\n if (rel.startsWith('..') || path.isAbsolute(rel)) {\n throw new Error(`Path \"${absPath}\" is outside project root \"${root}\"`);\n }\n return target;\n}\n\nexport function safeResolve(input: string, ctx: Context): string {\n return ensureInsideRoot(resolvePath(input, ctx), ctx);\n}\n\n/**\n * Defense against in-root→out-of-root symlink escape (CWE-59). `safeResolve`\n * only does a syntactic `../` check, so a symlink that lives *inside* the\n * project root but points outside still passes it. This resolves the path\n * through `fs.realpath` and re-verifies containment against the realpath of\n * the project root (comparing like-for-like, since the root itself may be a\n * symlink — macOS `/var`→`/private/var`, Windows 8.3 short names). For a path\n * that does not exist yet (e.g. a `write` to a new file) the nearest existing\n * ancestor directory is checked instead. Throws if the real target escapes.\n *\n * Mirrors the per-file guard already used in `replace.ts`/`grep.ts`; applied\n * to single-file `read`/`edit`/`write` it throws (rather than skips) because\n * the caller named exactly one file.\n */\nexport async function assertRealInsideRoot(absPath: string, ctx: Context): Promise<void> {\n const realRoot = await fsp.realpath(ctx.projectRoot).catch(() => path.resolve(ctx.projectRoot));\n let probe = absPath;\n for (;;) {\n let real: string;\n try {\n real = await fsp.realpath(probe);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') {\n const parent = path.dirname(probe);\n if (parent === probe) return; // reached fs root without escaping\n probe = parent;\n continue;\n }\n throw err;\n }\n const rel = path.relative(realRoot, real);\n if (rel.startsWith('..') || path.isAbsolute(rel)) {\n throw new Error(\n `Path \"${absPath}\" resolves through a symlink outside project root \"${realRoot}\"`,\n );\n }\n return;\n }\n}\n\n/** `safeResolve` + symlink realpath containment check. Async. */\nexport async function safeResolveReal(input: string, ctx: Context): Promise<string> {\n const abs = safeResolve(input, ctx);\n await assertRealInsideRoot(abs, ctx);\n return abs;\n}\n\nexport function truncateMiddle(s: string, max: number): string {\n if (Buffer.byteLength(s, 'utf8') <= max) return s;\n const half = Math.floor(max / 2);\n return (\n s.slice(0, half) +\n `\\n…[truncated ${Buffer.byteLength(s, 'utf8') - max} bytes from middle]…\\n` +\n s.slice(-half)\n );\n}\n\nexport function isBinaryBuffer(buf: Buffer): boolean {\n const len = Math.min(buf.length, 8192);\n for (let i = 0; i < len; i++) {\n if (buf[i] === 0) return true;\n }\n return false;\n}\n\n// ─── Command-output normalization (token-saving) ────────────────────────────\n//\n// Raw process output is full of tokens the model gains nothing from: ANSI\n// escapes, carriage-return progress spam, runs of identical warning lines, and\n// huge tails of build noise. These helpers strip that noise before the output\n// reaches the LLM. They are scoped to COMMAND tools (bash/git/exec and the\n// _spawn-stream consumers) — never applied to structured/code outputs.\n\n/** Unified byte cap for all command tool output fed to the model. */\nexport const COMMAND_OUTPUT_MAX_BYTES = 32_768;\n\n/** Runs of >= this many identical consecutive lines are collapsed. */\nconst REPEAT_RUN_THRESHOLD = 3;\n\n/**\n * Collapse carriage-return overwrites the way a terminal would: `\\r\\n` becomes\n * `\\n`, and a bare `\\r` (progress redraw) keeps only the text after the LAST\n * `\\r` on its physical line. Without this, a single progress bar that redraws\n * 200 times explodes into 200 lines.\n */\nexport function collapseCarriageReturns(text: string): string {\n const lf = text.replace(/\\r\\n/g, '\\n');\n if (!lf.includes('\\r')) return lf;\n return lf\n .split('\\n')\n .map((line) => (line.includes('\\r') ? line.slice(line.lastIndexOf('\\r') + 1) : line))\n .join('\\n');\n}\n\n/**\n * Collapse a run of `minRun`+ identical consecutive lines into the line once\n * plus a marker. Consecutive-only — it never reorders or dedups non-adjacent\n * lines, so diffs/source stay intact.\n */\nexport function collapseConsecutiveDuplicates(text: string, minRun = REPEAT_RUN_THRESHOLD): string {\n const lines = text.split('\\n');\n const out: string[] = [];\n let i = 0;\n while (i < lines.length) {\n let j = i + 1;\n while (j < lines.length && lines[j] === lines[i]) j++;\n const run = j - i;\n if (run >= minRun) {\n out.push(lines[i]!, `… ⟨repeated ${run}×⟩`);\n } else {\n for (let k = i; k < j; k++) out.push(lines[k]!);\n }\n i = j;\n }\n return out.join('\\n');\n}\n\n/** Largest prefix of `s` whose UTF-8 byte length is <= `maxBytes`. */\nfunction takeHeadBytes(s: string, maxBytes: number): string {\n if (maxBytes <= 0) return '';\n if (Buffer.byteLength(s, 'utf8') <= maxBytes) return s;\n let lo = 0;\n let hi = s.length;\n while (lo < hi) {\n const mid = Math.ceil((lo + hi) / 2);\n if (Buffer.byteLength(s.slice(0, mid), 'utf8') <= maxBytes) lo = mid;\n else hi = mid - 1;\n }\n return s.slice(0, lo);\n}\n\n/** Largest suffix of `s` whose UTF-8 byte length is <= `maxBytes`. */\nfunction takeTailBytes(s: string, maxBytes: number): string {\n if (maxBytes <= 0) return '';\n if (Buffer.byteLength(s, 'utf8') <= maxBytes) return s;\n let lo = 0;\n let hi = s.length;\n while (lo < hi) {\n const mid = Math.ceil((lo + hi) / 2);\n if (Buffer.byteLength(s.slice(s.length - mid), 'utf8') <= maxBytes) lo = mid;\n else hi = mid - 1;\n }\n return s.slice(s.length - lo);\n}\n\n/**\n * Truncate to `maxBytes` keeping BOTH ends — the head (what ran / early context)\n * and the tail (errors and summaries usually land last), biased ~45/55 toward\n * the tail. The result never exceeds `maxBytes`.\n */\nexport function truncateHeadTail(s: string, maxBytes: number): string {\n const total = Buffer.byteLength(s, 'utf8');\n if (total <= maxBytes) return s;\n // Reserve a fixed allowance for the marker so the final string can't exceed\n // the cap even though the dropped-byte count's digit width varies.\n const MARKER_RESERVE = 64;\n const avail = Math.max(0, maxBytes - MARKER_RESERVE);\n const headBudget = Math.floor(avail * 0.45);\n const head = takeHeadBytes(s, headBudget);\n const tail = takeTailBytes(s, avail - Buffer.byteLength(head, 'utf8'));\n const kept = Buffer.byteLength(head, 'utf8') + Buffer.byteLength(tail, 'utf8');\n return `${head}\\n…[truncated ${total - kept} bytes]…\\n${tail}`;\n}\n\n/**\n * Full token-saving pipeline for command tool output: strip ANSI → collapse\n * carriage-return progress → trim trailing whitespace → collapse identical\n * consecutive lines → squeeze blank-line runs → head+tail truncate to the cap.\n */\nexport function normalizeCommandOutput(\n raw: string,\n opts: { maxBytes?: number | undefined } = {},\n): string {\n if (!raw) return raw;\n let text = Core.stripAnsi(raw);\n text = collapseCarriageReturns(text);\n text = text.replace(/[ \\t]+$/gm, ''); // trailing whitespace per line\n text = collapseConsecutiveDuplicates(text);\n text = text.replace(/\\n{3,}/g, '\\n\\n'); // >=2 blank lines → 1\n return truncateHeadTail(text, opts.maxBytes ?? COMMAND_OUTPUT_MAX_BYTES);\n}\n","import { spawn } from 'node:child_process';\nimport * as fs from 'node:fs/promises';\nimport * as os from 'node:os';\nimport * as path from 'node:path';\nimport { buildChildEnv } from '@wrongstack/core';\nimport type { Tool } from '@wrongstack/core';\nimport { safeResolve } from './_util.js';\n\ninterface PatchInput {\n patch: string;\n directory?: string | undefined;\n strip?: number | undefined;\n dry_run?: boolean | undefined;\n}\n\ninterface PatchOutput {\n applied: number;\n rejected: number;\n files: string[];\n dry_run: boolean;\n message: string;\n}\n\nexport const patchTool: Tool<PatchInput, PatchOutput> = {\n name: 'patch',\n category: 'Filesystem',\n description:\n 'Apply a unified diff (patch) to the project. This is the correct tool when you have a diff that needs to be applied precisely, including handling of rejects.',\n usageHint:\n 'Best used when you already have a diff (from generation, external source, or previous step).\\n' +\n '- Use `dry_run: true` to see what would happen without modifying files.\\n' +\n '- On failure it creates .rej and .orig files for manual review.\\n' +\n 'Often cleaner than many small `edit` operations for larger changes.',\n permission: 'confirm',\n mutating: true,\n capabilities: ['fs.write'],\n timeoutMs: 30_000,\n inputSchema: {\n type: 'object',\n properties: {\n patch: { type: 'string', description: 'Unified diff patch content' },\n directory: { type: 'string', description: 'Root directory for patch (default: cwd)' },\n strip: { type: 'integer', description: 'Strip leading path components (default: 1)' },\n dry_run: { type: 'boolean', description: 'Preview without applying' },\n },\n required: ['patch'],\n },\n async execute(input, ctx, opts) {\n if (!input?.patch) throw new Error('patch: patch content is required');\n\n const dir = input.directory ? safeResolve(input.directory, ctx) : ctx.cwd;\n // strip=0 lets a diff address absolute paths like /etc/passwd and\n // escape the project root entirely. Force >= 1.\n const strip = Math.max(1, input.strip ?? 1);\n const dryRun = input.dry_run ?? false;\n\n // Pre-flight: scan diff target paths and reject any that resolve outside\n // the project root. This catches `../../../etc/passwd`-style escapes\n // before we hand the diff to GNU patch.\n const targets = extractDiffTargets(input.patch);\n for (const t of targets) {\n const stripped = stripPathComponents(t, strip);\n if (!stripped) continue;\n const candidate = path.resolve(dir, stripped);\n const rel = path.relative(ctx.projectRoot, candidate);\n if (rel.startsWith('..') || path.isAbsolute(rel)) {\n return {\n applied: 0,\n rejected: 1,\n files: [],\n dry_run: dryRun,\n message: `patch refused: target \"${t}\" resolves outside project root`,\n };\n }\n }\n\n // Write the diff into a private 0700 temp directory rather than into\n // the user-controlled `dir` with a predictable timestamp name. Avoids\n // symlink-bait races on shared work trees.\n const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), '.wstack_patch_'));\n try {\n await fs.chmod(tmpDir, 0o700).catch(() => {\n /* best-effort on Windows */\n });\n const patchFile = path.join(tmpDir, 'in.diff');\n await fs.writeFile(patchFile, input.patch, { mode: 0o600 });\n\n const args = [`-p${strip}`, '--merge', ...(dryRun ? ['--dry-run'] : []), '-i', patchFile];\n\n const result = await runPatch(args, dir, opts.signal);\n\n if (result.exitCode !== 0 && !dryRun) {\n return {\n applied: 0,\n rejected: 1,\n files: [],\n dry_run: dryRun,\n message: `patch failed: ${result.stderr || result.stdout}`,\n };\n }\n\n const patched = extractPatchedFiles(result.stdout);\n return {\n applied: patched.length,\n rejected: 0,\n files: patched,\n dry_run: dryRun,\n message: result.stdout || 'patch applied',\n };\n } finally {\n await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {});\n }\n },\n};\n\n/** Extract every `+++ <path>` target from a unified diff. */\nfunction extractDiffTargets(patch: string): string[] {\n const out: string[] = [];\n // Matches `+++ path/to/file` and `+++ b/path/to/file` (also `a/`). Strips\n // optional tab-prefixed timestamp suffixes that some diff tools emit.\n const re = /^\\+\\+\\+\\s+([^\\t\\r\\n]+)/gm;\n for (const m of patch.matchAll(re)) {\n const target = m[1]?.trim();\n if (!target || target === '/dev/null') continue;\n out.push(target);\n }\n return out;\n}\n\n/** Mimic `patch -pN` path stripping on a single target. Returns undefined\n * if the path has fewer segments than `strip`. */\nfunction stripPathComponents(p: string, strip: number): string | undefined {\n // Normalize separators so the count works on both POSIX and Windows-style\n // paths embedded in LLM-generated diffs.\n const parts = p.replace(/\\\\/g, '/').split('/');\n if (parts.length <= strip) return undefined;\n return parts.slice(strip).join('/');\n}\n\nfunction runPatch(\n args: string[],\n cwd: string,\n signal: AbortSignal,\n): Promise<{ exitCode: number; stdout: string; stderr: string }> {\n return new Promise((resolve) => {\n let stdout = '';\n let stderr = '';\n\n // Force C locale so `extractPatchedFiles` (which greps for the English\n // \"patching file\" prefix) doesn't silently miss-count on systems with\n // localized GNU patch output (fr/de/es etc.). Use buildChildEnv to\n // strip API keys and other secrets from the parent environment.\n const env = { ...buildChildEnv(), LANG: 'C', LC_ALL: 'C' };\n const child = spawn('patch', args, { cwd, signal, env, stdio: ['pipe', 'pipe', 'pipe'] });\n child.stdout?.on('data', (c) => {\n stdout += c.toString();\n });\n child.stderr?.on('data', (c) => {\n stderr += c.toString();\n });\n child.on('close', (code) => resolve({ exitCode: code ?? 1, stdout, stderr }));\n child.on('error', (e) => resolve({ exitCode: 1, stdout: '', stderr: e.message }));\n });\n}\n\nfunction extractPatchedFiles(output: string): string[] {\n const files: string[] = [];\n const re = /patching file (.+)/gi;\n for (const m of output.matchAll(re)) {\n if (m[1]) files.push(m[1]);\n }\n return files;\n}\n"]}
1
+ {"version":3,"sources":["../src/_util.ts","../src/patch.ts"],"names":["path2","resolve"],"mappings":"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","file":"patch.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/** Detected package manager for a project directory. */\nexport type PackageManager = 'pnpm' | 'yarn' | 'npm';\n\n/**\n * Detect the project's package manager by inspecting lockfiles in `cwd`.\n * Order: pnpm → yarn → npm (default). Missing or unreadable directories fall\n * back to `npm` rather than throwing, so a `safeResolve`-checked cwd that\n * happens to be empty never aborts the tool.\n */\nexport async function detectPackageManager(cwd: string): Promise<PackageManager> {\n const { stat } = await import('node:fs/promises');\n try {\n await stat(`${cwd}/pnpm-lock.yaml`);\n return 'pnpm';\n } catch {\n /* not pnpm */\n }\n try {\n await stat(`${cwd}/yarn.lock`);\n return 'yarn';\n } catch {\n /* not yarn */\n }\n return 'npm';\n}\n\nexport function resolvePath(input: string, ctx: Context): string {\n return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.workingDir ?? ctx.cwd, input);\n}\n\nexport function ensureInsideRoot(absPath: string, ctx: Context): string {\n const root = path.resolve(ctx.projectRoot);\n const target = path.resolve(absPath);\n const rel = path.relative(root, target);\n if (rel.startsWith('..') || path.isAbsolute(rel)) {\n throw new Error(`Path \"${absPath}\" is outside project root \"${root}\"`);\n }\n return target;\n}\n\nexport function safeResolve(input: string, ctx: Context): string {\n return ensureInsideRoot(resolvePath(input, ctx), ctx);\n}\n\n/**\n * Defense against in-root→out-of-root symlink escape (CWE-59). `safeResolve`\n * only does a syntactic `../` check, so a symlink that lives *inside* the\n * project root but points outside still passes it. This resolves the path\n * through `fs.realpath` and re-verifies containment against the realpath of\n * the project root (comparing like-for-like, since the root itself may be a\n * symlink — macOS `/var`→`/private/var`, Windows 8.3 short names). For a path\n * that does not exist yet (e.g. a `write` to a new file) the nearest existing\n * ancestor directory is checked instead. Throws if the real target escapes.\n *\n * Mirrors the per-file guard already used in `replace.ts`/`grep.ts`; applied\n * to single-file `read`/`edit`/`write` it throws (rather than skips) because\n * the caller named exactly one file.\n */\nexport async function assertRealInsideRoot(absPath: string, ctx: Context): Promise<void> {\n const realRoot = await fsp.realpath(ctx.projectRoot).catch(() => path.resolve(ctx.projectRoot));\n let probe = absPath;\n for (;;) {\n let real: string;\n try {\n real = await fsp.realpath(probe);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') {\n const parent = path.dirname(probe);\n if (parent === probe) return; // reached fs root without escaping\n probe = parent;\n continue;\n }\n throw err;\n }\n const rel = path.relative(realRoot, real);\n if (rel.startsWith('..') || path.isAbsolute(rel)) {\n throw new Error(\n `Path \"${absPath}\" resolves through a symlink outside project root \"${realRoot}\"`,\n );\n }\n return;\n }\n}\n\n/** `safeResolve` + symlink realpath containment check. Async. */\nexport async function safeResolveReal(input: string, ctx: Context): Promise<string> {\n const abs = safeResolve(input, ctx);\n await assertRealInsideRoot(abs, ctx);\n return abs;\n}\n\nexport function truncateMiddle(s: string, max: number): string {\n if (Buffer.byteLength(s, 'utf8') <= max) return s;\n const half = Math.floor(max / 2);\n return (\n s.slice(0, half) +\n `\\n…[truncated ${Buffer.byteLength(s, 'utf8') - max} bytes from middle]…\\n` +\n s.slice(-half)\n );\n}\n\nexport function isBinaryBuffer(buf: Buffer): boolean {\n const len = Math.min(buf.length, 8192);\n for (let i = 0; i < len; i++) {\n if (buf[i] === 0) return true;\n }\n return false;\n}\n\n// ─── Command-output normalization (token-saving) ────────────────────────────\n//\n// Raw process output is full of tokens the model gains nothing from: ANSI\n// escapes, carriage-return progress spam, runs of identical warning lines, and\n// huge tails of build noise. These helpers strip that noise before the output\n// reaches the LLM. They are scoped to COMMAND tools (bash/git/exec and the\n// _spawn-stream consumers) — never applied to structured/code outputs.\n\n/** Unified byte cap for all command tool output fed to the model. */\nexport const COMMAND_OUTPUT_MAX_BYTES = 32_768;\n\n/** Runs of >= this many identical consecutive lines are collapsed. */\nconst REPEAT_RUN_THRESHOLD = 3;\n\n/**\n * Collapse carriage-return overwrites the way a terminal would: `\\r\\n` becomes\n * `\\n`, and a bare `\\r` (progress redraw) keeps only the text after the LAST\n * `\\r` on its physical line. Without this, a single progress bar that redraws\n * 200 times explodes into 200 lines.\n */\nexport function collapseCarriageReturns(text: string): string {\n const lf = text.replace(/\\r\\n/g, '\\n');\n if (!lf.includes('\\r')) return lf;\n return lf\n .split('\\n')\n .map((line) => (line.includes('\\r') ? line.slice(line.lastIndexOf('\\r') + 1) : line))\n .join('\\n');\n}\n\n/**\n * Collapse a run of `minRun`+ identical consecutive lines into the line once\n * plus a marker. Consecutive-only — it never reorders or dedups non-adjacent\n * lines, so diffs/source stay intact.\n */\nexport function collapseConsecutiveDuplicates(text: string, minRun = REPEAT_RUN_THRESHOLD): string {\n const lines = text.split('\\n');\n const out: string[] = [];\n let i = 0;\n while (i < lines.length) {\n let j = i + 1;\n while (j < lines.length && lines[j] === lines[i]) j++;\n const run = j - i;\n if (run >= minRun) {\n out.push(lines[i]!, `… ⟨repeated ${run}×⟩`);\n } else {\n for (let k = i; k < j; k++) out.push(lines[k]!);\n }\n i = j;\n }\n return out.join('\\n');\n}\n\n/** Largest prefix of `s` whose UTF-8 byte length is <= `maxBytes`. */\nfunction takeHeadBytes(s: string, maxBytes: number): string {\n if (maxBytes <= 0) return '';\n if (Buffer.byteLength(s, 'utf8') <= maxBytes) return s;\n let lo = 0;\n let hi = s.length;\n while (lo < hi) {\n const mid = Math.ceil((lo + hi) / 2);\n if (Buffer.byteLength(s.slice(0, mid), 'utf8') <= maxBytes) lo = mid;\n else hi = mid - 1;\n }\n return s.slice(0, lo);\n}\n\n/** Largest suffix of `s` whose UTF-8 byte length is <= `maxBytes`. */\nfunction takeTailBytes(s: string, maxBytes: number): string {\n if (maxBytes <= 0) return '';\n if (Buffer.byteLength(s, 'utf8') <= maxBytes) return s;\n let lo = 0;\n let hi = s.length;\n while (lo < hi) {\n const mid = Math.ceil((lo + hi) / 2);\n if (Buffer.byteLength(s.slice(s.length - mid), 'utf8') <= maxBytes) lo = mid;\n else hi = mid - 1;\n }\n return s.slice(s.length - lo);\n}\n\n/**\n * Truncate to `maxBytes` keeping BOTH ends — the head (what ran / early context)\n * and the tail (errors and summaries usually land last), biased ~45/55 toward\n * the tail. The result never exceeds `maxBytes`.\n */\nexport function truncateHeadTail(s: string, maxBytes: number): string {\n const total = Buffer.byteLength(s, 'utf8');\n if (total <= maxBytes) return s;\n // Reserve a fixed allowance for the marker so the final string can't exceed\n // the cap even though the dropped-byte count's digit width varies.\n const MARKER_RESERVE = 64;\n const avail = Math.max(0, maxBytes - MARKER_RESERVE);\n const headBudget = Math.floor(avail * 0.45);\n const head = takeHeadBytes(s, headBudget);\n const tail = takeTailBytes(s, avail - Buffer.byteLength(head, 'utf8'));\n const kept = Buffer.byteLength(head, 'utf8') + Buffer.byteLength(tail, 'utf8');\n return `${head}\\n…[truncated ${total - kept} bytes]…\\n${tail}`;\n}\n\n/**\n * Full token-saving pipeline for command tool output: strip ANSI → collapse\n * carriage-return progress → trim trailing whitespace → collapse identical\n * consecutive lines → squeeze blank-line runs → head+tail truncate to the cap.\n */\nexport function normalizeCommandOutput(\n raw: string,\n opts: { maxBytes?: number | undefined } = {},\n): string {\n if (!raw) return raw;\n let text = Core.stripAnsi(raw);\n text = collapseCarriageReturns(text);\n text = text.replace(/[ \\t]+$/gm, ''); // trailing whitespace per line\n text = collapseConsecutiveDuplicates(text);\n text = text.replace(/\\n{3,}/g, '\\n\\n'); // >=2 blank lines → 1\n return truncateHeadTail(text, opts.maxBytes ?? COMMAND_OUTPUT_MAX_BYTES);\n}\n","import { spawn } from 'node:child_process';\nimport * as fs from 'node:fs/promises';\nimport * as os from 'node:os';\nimport * as path from 'node:path';\nimport { buildChildEnv } from '@wrongstack/core';\nimport type { Tool } from '@wrongstack/core';\nimport { safeResolve } from './_util.js';\n\ninterface PatchInput {\n patch: string;\n directory?: string | undefined;\n strip?: number | undefined;\n dry_run?: boolean | undefined;\n}\n\ninterface PatchOutput {\n applied: number;\n rejected: number;\n files: string[];\n dry_run: boolean;\n message: string;\n}\n\nexport const patchTool: Tool<PatchInput, PatchOutput> = {\n name: 'patch',\n category: 'Filesystem',\n description:\n 'Apply a unified diff (patch) to the project. This is the correct tool when you have a diff that needs to be applied precisely, including handling of rejects.',\n usageHint:\n 'Best used when you already have a diff (from generation, external source, or previous step).\\n' +\n '- Use `dry_run: true` to see what would happen without modifying files.\\n' +\n '- On failure it creates .rej and .orig files for manual review.\\n' +\n 'Often cleaner than many small `edit` operations for larger changes.',\n permission: 'confirm',\n mutating: true,\n capabilities: ['fs.write'],\n timeoutMs: 30_000,\n inputSchema: {\n type: 'object',\n properties: {\n patch: { type: 'string', description: 'Unified diff patch content' },\n directory: { type: 'string', description: 'Root directory for patch (default: cwd)' },\n strip: { type: 'integer', description: 'Strip leading path components (default: 1)' },\n dry_run: { type: 'boolean', description: 'Preview without applying' },\n },\n required: ['patch'],\n },\n async execute(input, ctx, opts) {\n if (!input?.patch) throw new Error('patch: patch content is required');\n\n const dir = input.directory ? safeResolve(input.directory, ctx) : ctx.cwd;\n // strip=0 lets a diff address absolute paths like /etc/passwd and\n // escape the project root entirely. Force >= 1.\n const strip = Math.max(1, input.strip ?? 1);\n const dryRun = input.dry_run ?? false;\n\n // Pre-flight: scan diff target paths and reject any that resolve outside\n // the project root. This catches `../../../etc/passwd`-style escapes\n // before we hand the diff to GNU patch.\n const targets = extractDiffTargets(input.patch);\n for (const t of targets) {\n const stripped = stripPathComponents(t, strip);\n if (!stripped) continue;\n const candidate = path.resolve(dir, stripped);\n const rel = path.relative(ctx.projectRoot, candidate);\n if (rel.startsWith('..') || path.isAbsolute(rel)) {\n return {\n applied: 0,\n rejected: 1,\n files: [],\n dry_run: dryRun,\n message: `patch refused: target \"${t}\" resolves outside project root`,\n };\n }\n }\n\n // Write the diff into a private 0700 temp directory rather than into\n // the user-controlled `dir` with a predictable timestamp name. Avoids\n // symlink-bait races on shared work trees.\n const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), '.wstack_patch_'));\n try {\n await fs.chmod(tmpDir, 0o700).catch(() => {\n /* best-effort on Windows */\n });\n const patchFile = path.join(tmpDir, 'in.diff');\n await fs.writeFile(patchFile, input.patch, { mode: 0o600 });\n\n const args = [`-p${strip}`, '--merge', ...(dryRun ? ['--dry-run'] : []), '-i', patchFile];\n\n const result = await runPatch(args, dir, opts.signal);\n\n if (result.exitCode !== 0 && !dryRun) {\n return {\n applied: 0,\n rejected: 1,\n files: [],\n dry_run: dryRun,\n message: `patch failed: ${result.stderr || result.stdout}`,\n };\n }\n\n const patched = extractPatchedFiles(result.stdout);\n return {\n applied: patched.length,\n rejected: 0,\n files: patched,\n dry_run: dryRun,\n message: result.stdout || 'patch applied',\n };\n } finally {\n await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {});\n }\n },\n};\n\n/** Extract every `+++ <path>` target from a unified diff. */\nfunction extractDiffTargets(patch: string): string[] {\n const out: string[] = [];\n // Matches `+++ path/to/file` and `+++ b/path/to/file` (also `a/`). Strips\n // optional tab-prefixed timestamp suffixes that some diff tools emit.\n const re = /^\\+\\+\\+\\s+([^\\t\\r\\n]+)/gm;\n for (const m of patch.matchAll(re)) {\n const target = m[1]?.trim();\n if (!target || target === '/dev/null') continue;\n out.push(target);\n }\n return out;\n}\n\n/** Mimic `patch -pN` path stripping on a single target. Returns undefined\n * if the path has fewer segments than `strip`. */\nfunction stripPathComponents(p: string, strip: number): string | undefined {\n // Normalize separators so the count works on both POSIX and Windows-style\n // paths embedded in LLM-generated diffs.\n const parts = p.replace(/\\\\/g, '/').split('/');\n if (parts.length <= strip) return undefined;\n return parts.slice(strip).join('/');\n}\n\nfunction runPatch(\n args: string[],\n cwd: string,\n signal: AbortSignal,\n): Promise<{ exitCode: number; stdout: string; stderr: string }> {\n return new Promise((resolve) => {\n let stdout = '';\n let stderr = '';\n\n // Force C locale so `extractPatchedFiles` (which greps for the English\n // \"patching file\" prefix) doesn't silently miss-count on systems with\n // localized GNU patch output (fr/de/es etc.). Use buildChildEnv to\n // strip API keys and other secrets from the parent environment.\n const env = { ...buildChildEnv(), LANG: 'C', LC_ALL: 'C' };\n const child = spawn('patch', args, { cwd, signal, env, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });\n child.stdout?.on('data', (c) => {\n stdout += c.toString();\n });\n child.stderr?.on('data', (c) => {\n stderr += c.toString();\n });\n child.on('close', (code) => resolve({ exitCode: code ?? 1, stdout, stderr }));\n child.on('error', (e) => resolve({ exitCode: 1, stdout: '', stderr: e.message }));\n });\n}\n\nfunction extractPatchedFiles(output: string): string[] {\n const files: string[] = [];\n const re = /patching file (.+)/gi;\n for (const m of output.matchAll(re)) {\n if (m[1]) files.push(m[1]);\n }\n return files;\n}\n"]}
package/dist/replace.js CHANGED
@@ -206,7 +206,7 @@ async function globFiles(pattern, base, extraGlob) {
206
206
  function checkRg() {
207
207
  return new Promise((resolve2) => {
208
208
  try {
209
- const p = spawn("rg", ["--version"], { env: buildChildEnv(), stdio: "ignore" });
209
+ const p = spawn("rg", ["--version"], { env: buildChildEnv(), stdio: "ignore", windowsHide: true });
210
210
  p.on("error", () => resolve2(false));
211
211
  p.on("close", (code) => resolve2(code === 0));
212
212
  } catch {
@@ -219,7 +219,8 @@ function spawnRgFind(pattern, base) {
219
219
  const child = spawn("rg", args, {
220
220
  signal: AbortSignal.timeout(3e4),
221
221
  env: buildChildEnv(),
222
- stdio: ["ignore", "pipe", "pipe"]
222
+ stdio: ["ignore", "pipe", "pipe"],
223
+ windowsHide: true
223
224
  });
224
225
  let buf = "";
225
226
  child.stdout?.on("data", (chunk) => {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/_regex.ts","../src/_util.ts","../src/replace.ts"],"names":["lstat","path2","stat","resolve"],"mappings":"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","file":"replace.js","sourcesContent":["/**\n * Compile a user-supplied regex with conservative bounds against ReDoS.\n *\n * Node's regex engine (V8) is backtracking-based and cannot interrupt a\n * synchronous match — a pattern like `(a+)+$` against a sufficiently long\n * line will pin a worker for seconds. The executor's outer `timeoutMs` only\n * fires between async boundaries, so a long regex eval inside a sync loop\n * is uninterruptible.\n *\n * We can't fully prevent ReDoS without an alternative engine (re2-wasm), but\n * we can sharply limit the blast radius:\n *\n * 1. Cap pattern length — practically all legitimate user patterns are\n * under 256 characters. A 4 KB pattern is almost certainly malicious\n * or a copy-paste accident.\n * 2. Reject patterns containing the most obvious super-linear structures.\n * This is a coarse filter (false-positives are likely; we accept that\n * for hostile-input contexts).\n *\n * Callers should additionally bound the *subject* length (e.g. by capping\n * line size before matching).\n */\n\nconst MAX_PATTERN_LEN = 256;\n\n// Heuristics for catastrophic-backtracking constructs. Not exhaustive; bias\n// toward false-positives in tools that accept LLM-generated input.\nconst DANGEROUS_PATTERNS: ReadonlyArray<RegExp> = [\n // (a+)+, (.*)+, etc — nested quantifier on a group with internal quantifier\n /(\\([^)]*[+*][^)]*\\))[+*]/,\n /(\\(\\?:[^)]*[+*][^)]*\\))[+*]/,\n // Adjacent quantifiers: a++ a*+\n /[+*]{2,}/,\n // Quantifier on alternation with length 2+\n /\\([^|)]+\\|[^)]+\\)[+*][+*]/,\n // Greedy quantifier inside lookahead/lookbehind — (?!.*a+)\n /[([][^)\\]]*[+*][^)\\]]*[)\\]][^)]*\\?\\??/,\n];\n\nexport interface CompileResult {\n ok: true;\n regex: RegExp;\n}\n\nexport interface CompileFail {\n ok: false;\n reason: string;\n}\n\nexport function compileUserRegex(pattern: string, flags: string): CompileResult | CompileFail {\n if (typeof pattern !== 'string') {\n return { ok: false, reason: 'pattern must be a string' };\n }\n if (pattern.length === 0) {\n return { ok: false, reason: 'pattern is empty' };\n }\n if (pattern.length > MAX_PATTERN_LEN) {\n return { ok: false, reason: `pattern exceeds ${MAX_PATTERN_LEN} characters` };\n }\n for (const rx of DANGEROUS_PATTERNS) {\n if (rx.test(pattern)) {\n return {\n ok: false,\n reason:\n 'pattern looks vulnerable to catastrophic backtracking — rewrite without nested quantifiers',\n };\n }\n }\n try {\n return { ok: true, regex: new RegExp(pattern, flags) };\n } catch (err) {\n return {\n ok: false,\n reason: err instanceof Error ? err.message : 'invalid regex',\n };\n }\n}\n\n/**\n * Truncate a subject line to a safe length for synchronous regex eval.\n * The cap is conservative; tools that need exact-line matching against very\n * long lines should use ripgrep externally rather than the native walker.\n */\nexport const MAX_SUBJECT_LEN = 64 * 1024;\n\nexport function capSubject(line: string): string {\n return line.length > MAX_SUBJECT_LEN ? line.slice(0, MAX_SUBJECT_LEN) : line;\n}\n","import * as fsp from 'node:fs/promises';\nimport * as path from 'node:path';\nimport * as Core from '@wrongstack/core';\nimport type { Context } from '@wrongstack/core';\n/** Detected package manager for a project directory. */\nexport type PackageManager = 'pnpm' | 'yarn' | 'npm';\n\n/**\n * Detect the project's package manager by inspecting lockfiles in `cwd`.\n * Order: pnpm → yarn → npm (default). Missing or unreadable directories fall\n * back to `npm` rather than throwing, so a `safeResolve`-checked cwd that\n * happens to be empty never aborts the tool.\n */\nexport async function detectPackageManager(cwd: string): Promise<PackageManager> {\n const { stat } = await import('node:fs/promises');\n try {\n await stat(`${cwd}/pnpm-lock.yaml`);\n return 'pnpm';\n } catch {\n /* not pnpm */\n }\n try {\n await stat(`${cwd}/yarn.lock`);\n return 'yarn';\n } catch {\n /* not yarn */\n }\n return 'npm';\n}\n\nexport function resolvePath(input: string, ctx: Context): string {\n return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.workingDir ?? ctx.cwd, input);\n}\n\nexport function ensureInsideRoot(absPath: string, ctx: Context): string {\n const root = path.resolve(ctx.projectRoot);\n const target = path.resolve(absPath);\n const rel = path.relative(root, target);\n if (rel.startsWith('..') || path.isAbsolute(rel)) {\n throw new Error(`Path \"${absPath}\" is outside project root \"${root}\"`);\n }\n return target;\n}\n\nexport function safeResolve(input: string, ctx: Context): string {\n return ensureInsideRoot(resolvePath(input, ctx), ctx);\n}\n\n/**\n * Defense against in-root→out-of-root symlink escape (CWE-59). `safeResolve`\n * only does a syntactic `../` check, so a symlink that lives *inside* the\n * project root but points outside still passes it. This resolves the path\n * through `fs.realpath` and re-verifies containment against the realpath of\n * the project root (comparing like-for-like, since the root itself may be a\n * symlink — macOS `/var`→`/private/var`, Windows 8.3 short names). For a path\n * that does not exist yet (e.g. a `write` to a new file) the nearest existing\n * ancestor directory is checked instead. Throws if the real target escapes.\n *\n * Mirrors the per-file guard already used in `replace.ts`/`grep.ts`; applied\n * to single-file `read`/`edit`/`write` it throws (rather than skips) because\n * the caller named exactly one file.\n */\nexport async function assertRealInsideRoot(absPath: string, ctx: Context): Promise<void> {\n const realRoot = await fsp.realpath(ctx.projectRoot).catch(() => path.resolve(ctx.projectRoot));\n let probe = absPath;\n for (;;) {\n let real: string;\n try {\n real = await fsp.realpath(probe);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') {\n const parent = path.dirname(probe);\n if (parent === probe) return; // reached fs root without escaping\n probe = parent;\n continue;\n }\n throw err;\n }\n const rel = path.relative(realRoot, real);\n if (rel.startsWith('..') || path.isAbsolute(rel)) {\n throw new Error(\n `Path \"${absPath}\" resolves through a symlink outside project root \"${realRoot}\"`,\n );\n }\n return;\n }\n}\n\n/** `safeResolve` + symlink realpath containment check. Async. */\nexport async function safeResolveReal(input: string, ctx: Context): Promise<string> {\n const abs = safeResolve(input, ctx);\n await assertRealInsideRoot(abs, ctx);\n return abs;\n}\n\nexport function truncateMiddle(s: string, max: number): string {\n if (Buffer.byteLength(s, 'utf8') <= max) return s;\n const half = Math.floor(max / 2);\n return (\n s.slice(0, half) +\n `\\n…[truncated ${Buffer.byteLength(s, 'utf8') - max} bytes from middle]…\\n` +\n s.slice(-half)\n );\n}\n\nexport function isBinaryBuffer(buf: Buffer): boolean {\n const len = Math.min(buf.length, 8192);\n for (let i = 0; i < len; i++) {\n if (buf[i] === 0) return true;\n }\n return false;\n}\n\n// ─── Command-output normalization (token-saving) ────────────────────────────\n//\n// Raw process output is full of tokens the model gains nothing from: ANSI\n// escapes, carriage-return progress spam, runs of identical warning lines, and\n// huge tails of build noise. These helpers strip that noise before the output\n// reaches the LLM. They are scoped to COMMAND tools (bash/git/exec and the\n// _spawn-stream consumers) — never applied to structured/code outputs.\n\n/** Unified byte cap for all command tool output fed to the model. */\nexport const COMMAND_OUTPUT_MAX_BYTES = 32_768;\n\n/** Runs of >= this many identical consecutive lines are collapsed. */\nconst REPEAT_RUN_THRESHOLD = 3;\n\n/**\n * Collapse carriage-return overwrites the way a terminal would: `\\r\\n` becomes\n * `\\n`, and a bare `\\r` (progress redraw) keeps only the text after the LAST\n * `\\r` on its physical line. Without this, a single progress bar that redraws\n * 200 times explodes into 200 lines.\n */\nexport function collapseCarriageReturns(text: string): string {\n const lf = text.replace(/\\r\\n/g, '\\n');\n if (!lf.includes('\\r')) return lf;\n return lf\n .split('\\n')\n .map((line) => (line.includes('\\r') ? line.slice(line.lastIndexOf('\\r') + 1) : line))\n .join('\\n');\n}\n\n/**\n * Collapse a run of `minRun`+ identical consecutive lines into the line once\n * plus a marker. Consecutive-only — it never reorders or dedups non-adjacent\n * lines, so diffs/source stay intact.\n */\nexport function collapseConsecutiveDuplicates(text: string, minRun = REPEAT_RUN_THRESHOLD): string {\n const lines = text.split('\\n');\n const out: string[] = [];\n let i = 0;\n while (i < lines.length) {\n let j = i + 1;\n while (j < lines.length && lines[j] === lines[i]) j++;\n const run = j - i;\n if (run >= minRun) {\n out.push(lines[i]!, `… ⟨repeated ${run}×⟩`);\n } else {\n for (let k = i; k < j; k++) out.push(lines[k]!);\n }\n i = j;\n }\n return out.join('\\n');\n}\n\n/** Largest prefix of `s` whose UTF-8 byte length is <= `maxBytes`. */\nfunction takeHeadBytes(s: string, maxBytes: number): string {\n if (maxBytes <= 0) return '';\n if (Buffer.byteLength(s, 'utf8') <= maxBytes) return s;\n let lo = 0;\n let hi = s.length;\n while (lo < hi) {\n const mid = Math.ceil((lo + hi) / 2);\n if (Buffer.byteLength(s.slice(0, mid), 'utf8') <= maxBytes) lo = mid;\n else hi = mid - 1;\n }\n return s.slice(0, lo);\n}\n\n/** Largest suffix of `s` whose UTF-8 byte length is <= `maxBytes`. */\nfunction takeTailBytes(s: string, maxBytes: number): string {\n if (maxBytes <= 0) return '';\n if (Buffer.byteLength(s, 'utf8') <= maxBytes) return s;\n let lo = 0;\n let hi = s.length;\n while (lo < hi) {\n const mid = Math.ceil((lo + hi) / 2);\n if (Buffer.byteLength(s.slice(s.length - mid), 'utf8') <= maxBytes) lo = mid;\n else hi = mid - 1;\n }\n return s.slice(s.length - lo);\n}\n\n/**\n * Truncate to `maxBytes` keeping BOTH ends — the head (what ran / early context)\n * and the tail (errors and summaries usually land last), biased ~45/55 toward\n * the tail. The result never exceeds `maxBytes`.\n */\nexport function truncateHeadTail(s: string, maxBytes: number): string {\n const total = Buffer.byteLength(s, 'utf8');\n if (total <= maxBytes) return s;\n // Reserve a fixed allowance for the marker so the final string can't exceed\n // the cap even though the dropped-byte count's digit width varies.\n const MARKER_RESERVE = 64;\n const avail = Math.max(0, maxBytes - MARKER_RESERVE);\n const headBudget = Math.floor(avail * 0.45);\n const head = takeHeadBytes(s, headBudget);\n const tail = takeTailBytes(s, avail - Buffer.byteLength(head, 'utf8'));\n const kept = Buffer.byteLength(head, 'utf8') + Buffer.byteLength(tail, 'utf8');\n return `${head}\\n…[truncated ${total - kept} bytes]…\\n${tail}`;\n}\n\n/**\n * Full token-saving pipeline for command tool output: strip ANSI → collapse\n * carriage-return progress → trim trailing whitespace → collapse identical\n * consecutive lines → squeeze blank-line runs → head+tail truncate to the cap.\n */\nexport function normalizeCommandOutput(\n raw: string,\n opts: { maxBytes?: number | undefined } = {},\n): string {\n if (!raw) return raw;\n let text = Core.stripAnsi(raw);\n text = collapseCarriageReturns(text);\n text = text.replace(/[ \\t]+$/gm, ''); // trailing whitespace per line\n text = collapseConsecutiveDuplicates(text);\n text = text.replace(/\\n{3,}/g, '\\n\\n'); // >=2 blank lines → 1\n return truncateHeadTail(text, opts.maxBytes ?? COMMAND_OUTPUT_MAX_BYTES);\n}\n","import { expectDefined } from '@wrongstack/core';\nimport { spawn } from 'node:child_process';\nimport * as fs from 'node:fs/promises';\nimport * as path from 'node:path';\nimport {\n atomicWrite,\n buildChildEnv,\n compileGlob,\n detectNewlineStyle,\n normalizeToLf,\n toStyle,\n unifiedDiff,\n} from '@wrongstack/core';\nimport type { Context, Tool } from '@wrongstack/core';\nimport { compileUserRegex } from './_regex.js';\nimport { isBinaryBuffer, safeResolve } from './_util.js';\ninterface ReplaceInput {\n pattern: string;\n replacement: string;\n files: string | string[];\n glob?: string | undefined;\n replace_all?: boolean | undefined;\n dry_run?: boolean | undefined;\n}\n\ninterface ReplaceOutput {\n files_modified: number;\n total_replacements: number;\n results: { path: string; replacements: number; diff?: string | undefined }[];\n dry_run: boolean;\n}\n\nconst DEFAULT_IGNORE = ['node_modules', '.git', 'dist', 'build', '.next', 'coverage'];\n\nexport const replaceTool: Tool<ReplaceInput, ReplaceOutput> = {\n name: 'replace',\n category: 'Transform',\n description:\n 'Perform a search-and-replace across multiple files using a regex pattern. ' +\n 'This is a powerful bulk transformation tool. Always use `dry_run: true` first on anything non-trivial.',\n usageHint:\n 'DANGEROUS IF USED CARELESSLY — review the diff output carefully.\\n\\n' +\n 'Recommended workflow:\\n' +\n '1. Start with `dry_run: true` to see exactly what would change.\\n' +\n '2. Use a specific enough `pattern` (and `glob` / `files`) to avoid accidental broad changes.\\n' +\n '3. `replace_all` controls whether only the first match per file or all matches are replaced.\\n' +\n 'This tool is excellent for large-scale refactors (renaming, import updates, etc.) but must be used with caution.',\n permission: 'confirm',\n mutating: true,\n capabilities: ['fs.write'],\n timeoutMs: 30_000,\n inputSchema: {\n type: 'object',\n properties: {\n pattern: { type: 'string', description: 'Regex pattern to match' },\n replacement: { type: 'string', description: 'Replacement string' },\n files: {\n type: 'string',\n description: 'File(s) to target: single path, comma-separated list, or glob pattern',\n },\n glob: { type: 'string', description: 'Additional glob filter (e.g. \"*.ts\")' },\n replace_all: {\n type: 'boolean',\n description: 'Replace all occurrences in each file (default: true)',\n },\n dry_run: { type: 'boolean', description: 'Preview changes without writing' },\n },\n required: ['pattern', 'replacement', 'files'],\n },\n async execute(input: ReplaceInput, ctx: Context) {\n if (!input?.pattern) throw new Error('replace: pattern is required');\n if (input.replacement === undefined) throw new Error('replace: replacement is required');\n if (!input?.files) throw new Error('replace: files is required');\n\n const replaceAll = input.replace_all ?? true;\n // Always compile with 'g' so matchAll() works — matchAll throws\n // TypeError on non-global regexes. The replaceAll flag controls\n // how many matches we act on, not whether the regex is global.\n const compiled = compileUserRegex(input.pattern, 'g');\n if (!compiled.ok) {\n throw new Error(`replace: ${compiled.reason}`);\n }\n const re = compiled.regex;\n const globRe = input.glob ? compileGlob(input.glob) : null;\n const dryRun = input.dry_run ?? false;\n\n const filesInput = Array.isArray(input.files) ? input.files.join(',') : input.files;\n const fileList = await resolveFiles(filesInput, ctx, globRe);\n\n // Resolve the project root through realpath ONCE so the sandbox check\n // below compares like-for-like with realpath(file). The project root\n // itself can be a symlink or short name — e.g. macOS temp dirs live under\n // /var -> /private/var, and Windows CI runners expose an 8.3 short name\n // (C:\\Users\\RUNNER~1\\...). Comparing realpath(file) against the raw root\n // then makes every legitimately-inside file look \"outside\" and skips it.\n const realRoot = await fs.realpath(ctx.projectRoot).catch(() => ctx.projectRoot);\n\n const results: ReplaceOutput['results'] = [];\n let totalReplacements = 0;\n\n for (const absPath of fileList) {\n // Use lstat to detect symlinks. resolveFiles already applies\n // safeResolve, but a symlink with a target outside the project\n // root would still pass that string check — explicitly skip it\n // so we never read or write through a link.\n const lstat = await fs.lstat(absPath).catch((err) => {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') return null;\n throw err;\n });\n if (!lstat || !lstat.isFile()) continue;\n if (lstat.isSymbolicLink()) continue;\n\n // Cross-check via realpath: if the resolved target lives outside the\n // project root (e.g. a bind mount or a parent-dir traversal we missed),\n // skip rather than rewrite through it.\n let realPath: string;\n try {\n realPath = await fs.realpath(absPath);\n } catch {\n continue;\n }\n const rel = path.relative(realRoot, realPath);\n if (rel.startsWith('..') || path.isAbsolute(rel)) continue;\n\n // Now stat the real target so we use its mode for atomicWrite.\n const stat = await fs.stat(realPath).catch(() => null);\n if (!stat || !stat.isFile()) continue;\n\n let content: string;\n try {\n const buf = await fs.readFile(realPath);\n if (isBinaryBuffer(buf)) continue;\n content = buf.toString('utf8');\n } catch {\n continue;\n }\n\n const style = detectNewlineStyle(content);\n const contentLf = normalizeToLf(content);\n re.lastIndex = 0;\n const allMatches = [...contentLf.matchAll(re)];\n if (allMatches.length === 0) continue;\n\n // When replace_all is false, only act on the first match.\n const matches = replaceAll ? allMatches : allMatches.slice(0, 1);\n const count = matches.length;\n\n // Rebuild: splice the replacement into each match position from\n // right to left so earlier indices stay valid.\n let newContentLf = contentLf;\n for (let i = matches.length - 1; i >= 0; i--) {\n const m = expectDefined(matches[i]);\n newContentLf =\n newContentLf.slice(0, m.index) +\n input.replacement +\n newContentLf.slice(expectDefined(m.index) + m[0].length);\n }\n re.lastIndex = 0;\n totalReplacements += count;\n\n if (!dryRun) {\n const newContent = toStyle(newContentLf, style);\n // Write to the real path (already validated inside project root)\n // so atomicWrite's temp-and-rename can't be redirected through a\n // freshly-planted symlink at absPath.\n await atomicWrite(realPath, newContent, { mode: stat.mode & 0o777 });\n }\n\n const diff =\n dryRun || matches.length > 0\n ? unifiedDiff(content, toStyle(newContentLf, style), {\n fromFile: absPath,\n toFile: absPath,\n })\n : undefined;\n\n results.push({\n path: absPath,\n replacements: matches.length,\n diff,\n });\n }\n\n return {\n files_modified: results.length,\n total_replacements: totalReplacements,\n results,\n dry_run: dryRun,\n };\n },\n};\n\nasync function resolveFiles(\n filesInput: string,\n ctx: Context,\n extraGlob?: RegExp | null | undefined,\n): Promise<string[]> {\n const base = ctx.cwd;\n const normalized = filesInput.trim();\n\n if (normalized.startsWith('**/') || normalized.startsWith('*') || normalized.includes('**')) {\n return await globFiles(normalized, base, extraGlob);\n }\n\n const parts = normalized\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean);\n const resolved: string[] = [];\n\n for (const p of parts) {\n const absPath = safeResolve(p, ctx);\n const stat = await fs.stat(absPath).catch(() => null);\n if (stat?.isFile()) {\n resolved.push(absPath);\n }\n }\n\n return resolved;\n}\n\nasync function globFiles(\n pattern: string,\n base: string,\n extraGlob?: RegExp | null | undefined,\n): Promise<string[]> {\n\n const rgAvailable = await checkRg();\n if (rgAvailable) {\n try {\n const { promise } = spawnRgFind(pattern, base);\n return await promise;\n } catch {\n // fall through\n }\n }\n\n return await globNative(pattern, base, extraGlob);\n}\n\nfunction checkRg(): Promise<boolean> {\n return new Promise((resolve) => {\n try {\n const p = spawn('rg', ['--version'], { env: buildChildEnv(), stdio: 'ignore' });\n p.on('error', () => resolve(false));\n p.on('close', (code) => resolve(code === 0));\n } catch {\n resolve(false);\n }\n });\n}\n\nfunction spawnRgFind(pattern: string, base: string): { promise: Promise<string[]> } {\n const args = ['--files', '--glob', pattern, base];\n // 30-second safety net to prevent zombie rg processes. Unlike the main\n // grep tool, glob file enumeration is fast and should never need more time.\n const child = spawn('rg', args, {\n signal: AbortSignal.timeout(30_000),\n env: buildChildEnv(),\n stdio: ['ignore', 'pipe', 'pipe'],\n });\n let buf = '';\n child.stdout?.on('data', (chunk: Buffer) => {\n buf += chunk.toString();\n });\n return {\n promise: new Promise((resolve, reject) => {\n child.on('error', reject);\n child.on('close', () => {\n resolve(buf.split('\\n').filter(Boolean));\n });\n }),\n };\n}\n\nasync function globNative(\n pattern: string,\n base: string,\n extraGlob?: RegExp | null | undefined,\n): Promise<string[]> {\n const results: string[] = [];\n const globRe = compileGlob(pattern);\n\n const walk = async (dir: string): Promise<void> => {\n let entries: import('node:fs').Dirent[];\n try {\n entries = await fs.readdir(dir, { withFileTypes: true });\n } catch {\n return;\n }\n for (const e of entries) {\n if (DEFAULT_IGNORE.includes(e.name)) continue;\n const full = path.join(dir, e.name);\n // Dirent.isSymbolicLink() uses readdir's d_type, which may not detect\n // directory symlinks on Windows (d_type = DT_UNKNOWN). Defensive stat\n // call: skip any entry whose lstat shows a symlink — file or directory.\n try {\n const stat = await fs.lstat(full);\n if (stat.isSymbolicLink()) continue;\n } catch {\n // lstat fails for very unusual entries (e.g. broken symlinks to deleted\n // files on NFS); skip safely rather than surfacing an error.\n continue;\n }\n if (e.isDirectory()) {\n await walk(full);\n } else if (e.isFile()) {\n const name = e.name;\n if (globRe.test(name) || globRe.test(full)) {\n if (extraGlob && !extraGlob.test(name) && !extraGlob.test(full)) continue;\n results.push(full);\n }\n globRe.lastIndex = 0;\n if (extraGlob) extraGlob.lastIndex = 0;\n }\n }\n };\n\n await walk(base);\n return results;\n}\n"]}
1
+ {"version":3,"sources":["../src/_regex.ts","../src/_util.ts","../src/replace.ts"],"names":["lstat","path2","stat","resolve"],"mappings":"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","file":"replace.js","sourcesContent":["/**\n * Compile a user-supplied regex with conservative bounds against ReDoS.\n *\n * Node's regex engine (V8) is backtracking-based and cannot interrupt a\n * synchronous match — a pattern like `(a+)+$` against a sufficiently long\n * line will pin a worker for seconds. The executor's outer `timeoutMs` only\n * fires between async boundaries, so a long regex eval inside a sync loop\n * is uninterruptible.\n *\n * We can't fully prevent ReDoS without an alternative engine (re2-wasm), but\n * we can sharply limit the blast radius:\n *\n * 1. Cap pattern length — practically all legitimate user patterns are\n * under 256 characters. A 4 KB pattern is almost certainly malicious\n * or a copy-paste accident.\n * 2. Reject patterns containing the most obvious super-linear structures.\n * This is a coarse filter (false-positives are likely; we accept that\n * for hostile-input contexts).\n *\n * Callers should additionally bound the *subject* length (e.g. by capping\n * line size before matching).\n */\n\nconst MAX_PATTERN_LEN = 256;\n\n// Heuristics for catastrophic-backtracking constructs. Not exhaustive; bias\n// toward false-positives in tools that accept LLM-generated input.\nconst DANGEROUS_PATTERNS: ReadonlyArray<RegExp> = [\n // (a+)+, (.*)+, etc — nested quantifier on a group with internal quantifier\n /(\\([^)]*[+*][^)]*\\))[+*]/,\n /(\\(\\?:[^)]*[+*][^)]*\\))[+*]/,\n // Adjacent quantifiers: a++ a*+\n /[+*]{2,}/,\n // Quantifier on alternation with length 2+\n /\\([^|)]+\\|[^)]+\\)[+*][+*]/,\n // Greedy quantifier inside lookahead/lookbehind — (?!.*a+)\n /[([][^)\\]]*[+*][^)\\]]*[)\\]][^)]*\\?\\??/,\n];\n\nexport interface CompileResult {\n ok: true;\n regex: RegExp;\n}\n\nexport interface CompileFail {\n ok: false;\n reason: string;\n}\n\nexport function compileUserRegex(pattern: string, flags: string): CompileResult | CompileFail {\n if (typeof pattern !== 'string') {\n return { ok: false, reason: 'pattern must be a string' };\n }\n if (pattern.length === 0) {\n return { ok: false, reason: 'pattern is empty' };\n }\n if (pattern.length > MAX_PATTERN_LEN) {\n return { ok: false, reason: `pattern exceeds ${MAX_PATTERN_LEN} characters` };\n }\n for (const rx of DANGEROUS_PATTERNS) {\n if (rx.test(pattern)) {\n return {\n ok: false,\n reason:\n 'pattern looks vulnerable to catastrophic backtracking — rewrite without nested quantifiers',\n };\n }\n }\n try {\n return { ok: true, regex: new RegExp(pattern, flags) };\n } catch (err) {\n return {\n ok: false,\n reason: err instanceof Error ? err.message : 'invalid regex',\n };\n }\n}\n\n/**\n * Truncate a subject line to a safe length for synchronous regex eval.\n * The cap is conservative; tools that need exact-line matching against very\n * long lines should use ripgrep externally rather than the native walker.\n */\nexport const MAX_SUBJECT_LEN = 64 * 1024;\n\nexport function capSubject(line: string): string {\n return line.length > MAX_SUBJECT_LEN ? line.slice(0, MAX_SUBJECT_LEN) : line;\n}\n","import * as fsp from 'node:fs/promises';\nimport * as path from 'node:path';\nimport * as Core from '@wrongstack/core';\nimport type { Context } from '@wrongstack/core';\n/** Detected package manager for a project directory. */\nexport type PackageManager = 'pnpm' | 'yarn' | 'npm';\n\n/**\n * Detect the project's package manager by inspecting lockfiles in `cwd`.\n * Order: pnpm → yarn → npm (default). Missing or unreadable directories fall\n * back to `npm` rather than throwing, so a `safeResolve`-checked cwd that\n * happens to be empty never aborts the tool.\n */\nexport async function detectPackageManager(cwd: string): Promise<PackageManager> {\n const { stat } = await import('node:fs/promises');\n try {\n await stat(`${cwd}/pnpm-lock.yaml`);\n return 'pnpm';\n } catch {\n /* not pnpm */\n }\n try {\n await stat(`${cwd}/yarn.lock`);\n return 'yarn';\n } catch {\n /* not yarn */\n }\n return 'npm';\n}\n\nexport function resolvePath(input: string, ctx: Context): string {\n return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.workingDir ?? ctx.cwd, input);\n}\n\nexport function ensureInsideRoot(absPath: string, ctx: Context): string {\n const root = path.resolve(ctx.projectRoot);\n const target = path.resolve(absPath);\n const rel = path.relative(root, target);\n if (rel.startsWith('..') || path.isAbsolute(rel)) {\n throw new Error(`Path \"${absPath}\" is outside project root \"${root}\"`);\n }\n return target;\n}\n\nexport function safeResolve(input: string, ctx: Context): string {\n return ensureInsideRoot(resolvePath(input, ctx), ctx);\n}\n\n/**\n * Defense against in-root→out-of-root symlink escape (CWE-59). `safeResolve`\n * only does a syntactic `../` check, so a symlink that lives *inside* the\n * project root but points outside still passes it. This resolves the path\n * through `fs.realpath` and re-verifies containment against the realpath of\n * the project root (comparing like-for-like, since the root itself may be a\n * symlink — macOS `/var`→`/private/var`, Windows 8.3 short names). For a path\n * that does not exist yet (e.g. a `write` to a new file) the nearest existing\n * ancestor directory is checked instead. Throws if the real target escapes.\n *\n * Mirrors the per-file guard already used in `replace.ts`/`grep.ts`; applied\n * to single-file `read`/`edit`/`write` it throws (rather than skips) because\n * the caller named exactly one file.\n */\nexport async function assertRealInsideRoot(absPath: string, ctx: Context): Promise<void> {\n const realRoot = await fsp.realpath(ctx.projectRoot).catch(() => path.resolve(ctx.projectRoot));\n let probe = absPath;\n for (;;) {\n let real: string;\n try {\n real = await fsp.realpath(probe);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') {\n const parent = path.dirname(probe);\n if (parent === probe) return; // reached fs root without escaping\n probe = parent;\n continue;\n }\n throw err;\n }\n const rel = path.relative(realRoot, real);\n if (rel.startsWith('..') || path.isAbsolute(rel)) {\n throw new Error(\n `Path \"${absPath}\" resolves through a symlink outside project root \"${realRoot}\"`,\n );\n }\n return;\n }\n}\n\n/** `safeResolve` + symlink realpath containment check. Async. */\nexport async function safeResolveReal(input: string, ctx: Context): Promise<string> {\n const abs = safeResolve(input, ctx);\n await assertRealInsideRoot(abs, ctx);\n return abs;\n}\n\nexport function truncateMiddle(s: string, max: number): string {\n if (Buffer.byteLength(s, 'utf8') <= max) return s;\n const half = Math.floor(max / 2);\n return (\n s.slice(0, half) +\n `\\n…[truncated ${Buffer.byteLength(s, 'utf8') - max} bytes from middle]…\\n` +\n s.slice(-half)\n );\n}\n\nexport function isBinaryBuffer(buf: Buffer): boolean {\n const len = Math.min(buf.length, 8192);\n for (let i = 0; i < len; i++) {\n if (buf[i] === 0) return true;\n }\n return false;\n}\n\n// ─── Command-output normalization (token-saving) ────────────────────────────\n//\n// Raw process output is full of tokens the model gains nothing from: ANSI\n// escapes, carriage-return progress spam, runs of identical warning lines, and\n// huge tails of build noise. These helpers strip that noise before the output\n// reaches the LLM. They are scoped to COMMAND tools (bash/git/exec and the\n// _spawn-stream consumers) — never applied to structured/code outputs.\n\n/** Unified byte cap for all command tool output fed to the model. */\nexport const COMMAND_OUTPUT_MAX_BYTES = 32_768;\n\n/** Runs of >= this many identical consecutive lines are collapsed. */\nconst REPEAT_RUN_THRESHOLD = 3;\n\n/**\n * Collapse carriage-return overwrites the way a terminal would: `\\r\\n` becomes\n * `\\n`, and a bare `\\r` (progress redraw) keeps only the text after the LAST\n * `\\r` on its physical line. Without this, a single progress bar that redraws\n * 200 times explodes into 200 lines.\n */\nexport function collapseCarriageReturns(text: string): string {\n const lf = text.replace(/\\r\\n/g, '\\n');\n if (!lf.includes('\\r')) return lf;\n return lf\n .split('\\n')\n .map((line) => (line.includes('\\r') ? line.slice(line.lastIndexOf('\\r') + 1) : line))\n .join('\\n');\n}\n\n/**\n * Collapse a run of `minRun`+ identical consecutive lines into the line once\n * plus a marker. Consecutive-only — it never reorders or dedups non-adjacent\n * lines, so diffs/source stay intact.\n */\nexport function collapseConsecutiveDuplicates(text: string, minRun = REPEAT_RUN_THRESHOLD): string {\n const lines = text.split('\\n');\n const out: string[] = [];\n let i = 0;\n while (i < lines.length) {\n let j = i + 1;\n while (j < lines.length && lines[j] === lines[i]) j++;\n const run = j - i;\n if (run >= minRun) {\n out.push(lines[i]!, `… ⟨repeated ${run}×⟩`);\n } else {\n for (let k = i; k < j; k++) out.push(lines[k]!);\n }\n i = j;\n }\n return out.join('\\n');\n}\n\n/** Largest prefix of `s` whose UTF-8 byte length is <= `maxBytes`. */\nfunction takeHeadBytes(s: string, maxBytes: number): string {\n if (maxBytes <= 0) return '';\n if (Buffer.byteLength(s, 'utf8') <= maxBytes) return s;\n let lo = 0;\n let hi = s.length;\n while (lo < hi) {\n const mid = Math.ceil((lo + hi) / 2);\n if (Buffer.byteLength(s.slice(0, mid), 'utf8') <= maxBytes) lo = mid;\n else hi = mid - 1;\n }\n return s.slice(0, lo);\n}\n\n/** Largest suffix of `s` whose UTF-8 byte length is <= `maxBytes`. */\nfunction takeTailBytes(s: string, maxBytes: number): string {\n if (maxBytes <= 0) return '';\n if (Buffer.byteLength(s, 'utf8') <= maxBytes) return s;\n let lo = 0;\n let hi = s.length;\n while (lo < hi) {\n const mid = Math.ceil((lo + hi) / 2);\n if (Buffer.byteLength(s.slice(s.length - mid), 'utf8') <= maxBytes) lo = mid;\n else hi = mid - 1;\n }\n return s.slice(s.length - lo);\n}\n\n/**\n * Truncate to `maxBytes` keeping BOTH ends — the head (what ran / early context)\n * and the tail (errors and summaries usually land last), biased ~45/55 toward\n * the tail. The result never exceeds `maxBytes`.\n */\nexport function truncateHeadTail(s: string, maxBytes: number): string {\n const total = Buffer.byteLength(s, 'utf8');\n if (total <= maxBytes) return s;\n // Reserve a fixed allowance for the marker so the final string can't exceed\n // the cap even though the dropped-byte count's digit width varies.\n const MARKER_RESERVE = 64;\n const avail = Math.max(0, maxBytes - MARKER_RESERVE);\n const headBudget = Math.floor(avail * 0.45);\n const head = takeHeadBytes(s, headBudget);\n const tail = takeTailBytes(s, avail - Buffer.byteLength(head, 'utf8'));\n const kept = Buffer.byteLength(head, 'utf8') + Buffer.byteLength(tail, 'utf8');\n return `${head}\\n…[truncated ${total - kept} bytes]…\\n${tail}`;\n}\n\n/**\n * Full token-saving pipeline for command tool output: strip ANSI → collapse\n * carriage-return progress → trim trailing whitespace → collapse identical\n * consecutive lines → squeeze blank-line runs → head+tail truncate to the cap.\n */\nexport function normalizeCommandOutput(\n raw: string,\n opts: { maxBytes?: number | undefined } = {},\n): string {\n if (!raw) return raw;\n let text = Core.stripAnsi(raw);\n text = collapseCarriageReturns(text);\n text = text.replace(/[ \\t]+$/gm, ''); // trailing whitespace per line\n text = collapseConsecutiveDuplicates(text);\n text = text.replace(/\\n{3,}/g, '\\n\\n'); // >=2 blank lines → 1\n return truncateHeadTail(text, opts.maxBytes ?? COMMAND_OUTPUT_MAX_BYTES);\n}\n","import { expectDefined } from '@wrongstack/core';\nimport { spawn } from 'node:child_process';\nimport * as fs from 'node:fs/promises';\nimport * as path from 'node:path';\nimport {\n atomicWrite,\n buildChildEnv,\n compileGlob,\n detectNewlineStyle,\n normalizeToLf,\n toStyle,\n unifiedDiff,\n} from '@wrongstack/core';\nimport type { Context, Tool } from '@wrongstack/core';\nimport { compileUserRegex } from './_regex.js';\nimport { isBinaryBuffer, safeResolve } from './_util.js';\ninterface ReplaceInput {\n pattern: string;\n replacement: string;\n files: string | string[];\n glob?: string | undefined;\n replace_all?: boolean | undefined;\n dry_run?: boolean | undefined;\n}\n\ninterface ReplaceOutput {\n files_modified: number;\n total_replacements: number;\n results: { path: string; replacements: number; diff?: string | undefined }[];\n dry_run: boolean;\n}\n\nconst DEFAULT_IGNORE = ['node_modules', '.git', 'dist', 'build', '.next', 'coverage'];\n\nexport const replaceTool: Tool<ReplaceInput, ReplaceOutput> = {\n name: 'replace',\n category: 'Transform',\n description:\n 'Perform a search-and-replace across multiple files using a regex pattern. ' +\n 'This is a powerful bulk transformation tool. Always use `dry_run: true` first on anything non-trivial.',\n usageHint:\n 'DANGEROUS IF USED CARELESSLY — review the diff output carefully.\\n\\n' +\n 'Recommended workflow:\\n' +\n '1. Start with `dry_run: true` to see exactly what would change.\\n' +\n '2. Use a specific enough `pattern` (and `glob` / `files`) to avoid accidental broad changes.\\n' +\n '3. `replace_all` controls whether only the first match per file or all matches are replaced.\\n' +\n 'This tool is excellent for large-scale refactors (renaming, import updates, etc.) but must be used with caution.',\n permission: 'confirm',\n mutating: true,\n capabilities: ['fs.write'],\n timeoutMs: 30_000,\n inputSchema: {\n type: 'object',\n properties: {\n pattern: { type: 'string', description: 'Regex pattern to match' },\n replacement: { type: 'string', description: 'Replacement string' },\n files: {\n type: 'string',\n description: 'File(s) to target: single path, comma-separated list, or glob pattern',\n },\n glob: { type: 'string', description: 'Additional glob filter (e.g. \"*.ts\")' },\n replace_all: {\n type: 'boolean',\n description: 'Replace all occurrences in each file (default: true)',\n },\n dry_run: { type: 'boolean', description: 'Preview changes without writing' },\n },\n required: ['pattern', 'replacement', 'files'],\n },\n async execute(input: ReplaceInput, ctx: Context) {\n if (!input?.pattern) throw new Error('replace: pattern is required');\n if (input.replacement === undefined) throw new Error('replace: replacement is required');\n if (!input?.files) throw new Error('replace: files is required');\n\n const replaceAll = input.replace_all ?? true;\n // Always compile with 'g' so matchAll() works — matchAll throws\n // TypeError on non-global regexes. The replaceAll flag controls\n // how many matches we act on, not whether the regex is global.\n const compiled = compileUserRegex(input.pattern, 'g');\n if (!compiled.ok) {\n throw new Error(`replace: ${compiled.reason}`);\n }\n const re = compiled.regex;\n const globRe = input.glob ? compileGlob(input.glob) : null;\n const dryRun = input.dry_run ?? false;\n\n const filesInput = Array.isArray(input.files) ? input.files.join(',') : input.files;\n const fileList = await resolveFiles(filesInput, ctx, globRe);\n\n // Resolve the project root through realpath ONCE so the sandbox check\n // below compares like-for-like with realpath(file). The project root\n // itself can be a symlink or short name — e.g. macOS temp dirs live under\n // /var -> /private/var, and Windows CI runners expose an 8.3 short name\n // (C:\\Users\\RUNNER~1\\...). Comparing realpath(file) against the raw root\n // then makes every legitimately-inside file look \"outside\" and skips it.\n const realRoot = await fs.realpath(ctx.projectRoot).catch(() => ctx.projectRoot);\n\n const results: ReplaceOutput['results'] = [];\n let totalReplacements = 0;\n\n for (const absPath of fileList) {\n // Use lstat to detect symlinks. resolveFiles already applies\n // safeResolve, but a symlink with a target outside the project\n // root would still pass that string check — explicitly skip it\n // so we never read or write through a link.\n const lstat = await fs.lstat(absPath).catch((err) => {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') return null;\n throw err;\n });\n if (!lstat || !lstat.isFile()) continue;\n if (lstat.isSymbolicLink()) continue;\n\n // Cross-check via realpath: if the resolved target lives outside the\n // project root (e.g. a bind mount or a parent-dir traversal we missed),\n // skip rather than rewrite through it.\n let realPath: string;\n try {\n realPath = await fs.realpath(absPath);\n } catch {\n continue;\n }\n const rel = path.relative(realRoot, realPath);\n if (rel.startsWith('..') || path.isAbsolute(rel)) continue;\n\n // Now stat the real target so we use its mode for atomicWrite.\n const stat = await fs.stat(realPath).catch(() => null);\n if (!stat || !stat.isFile()) continue;\n\n let content: string;\n try {\n const buf = await fs.readFile(realPath);\n if (isBinaryBuffer(buf)) continue;\n content = buf.toString('utf8');\n } catch {\n continue;\n }\n\n const style = detectNewlineStyle(content);\n const contentLf = normalizeToLf(content);\n re.lastIndex = 0;\n const allMatches = [...contentLf.matchAll(re)];\n if (allMatches.length === 0) continue;\n\n // When replace_all is false, only act on the first match.\n const matches = replaceAll ? allMatches : allMatches.slice(0, 1);\n const count = matches.length;\n\n // Rebuild: splice the replacement into each match position from\n // right to left so earlier indices stay valid.\n let newContentLf = contentLf;\n for (let i = matches.length - 1; i >= 0; i--) {\n const m = expectDefined(matches[i]);\n newContentLf =\n newContentLf.slice(0, m.index) +\n input.replacement +\n newContentLf.slice(expectDefined(m.index) + m[0].length);\n }\n re.lastIndex = 0;\n totalReplacements += count;\n\n if (!dryRun) {\n const newContent = toStyle(newContentLf, style);\n // Write to the real path (already validated inside project root)\n // so atomicWrite's temp-and-rename can't be redirected through a\n // freshly-planted symlink at absPath.\n await atomicWrite(realPath, newContent, { mode: stat.mode & 0o777 });\n }\n\n const diff =\n dryRun || matches.length > 0\n ? unifiedDiff(content, toStyle(newContentLf, style), {\n fromFile: absPath,\n toFile: absPath,\n })\n : undefined;\n\n results.push({\n path: absPath,\n replacements: matches.length,\n diff,\n });\n }\n\n return {\n files_modified: results.length,\n total_replacements: totalReplacements,\n results,\n dry_run: dryRun,\n };\n },\n};\n\nasync function resolveFiles(\n filesInput: string,\n ctx: Context,\n extraGlob?: RegExp | null | undefined,\n): Promise<string[]> {\n const base = ctx.cwd;\n const normalized = filesInput.trim();\n\n if (normalized.startsWith('**/') || normalized.startsWith('*') || normalized.includes('**')) {\n return await globFiles(normalized, base, extraGlob);\n }\n\n const parts = normalized\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean);\n const resolved: string[] = [];\n\n for (const p of parts) {\n const absPath = safeResolve(p, ctx);\n const stat = await fs.stat(absPath).catch(() => null);\n if (stat?.isFile()) {\n resolved.push(absPath);\n }\n }\n\n return resolved;\n}\n\nasync function globFiles(\n pattern: string,\n base: string,\n extraGlob?: RegExp | null | undefined,\n): Promise<string[]> {\n\n const rgAvailable = await checkRg();\n if (rgAvailable) {\n try {\n const { promise } = spawnRgFind(pattern, base);\n return await promise;\n } catch {\n // fall through\n }\n }\n\n return await globNative(pattern, base, extraGlob);\n}\n\nfunction checkRg(): Promise<boolean> {\n return new Promise((resolve) => {\n try {\n const p = spawn('rg', ['--version'], { env: buildChildEnv(), stdio: 'ignore', windowsHide: true });\n p.on('error', () => resolve(false));\n p.on('close', (code) => resolve(code === 0));\n } catch {\n resolve(false);\n }\n });\n}\n\nfunction spawnRgFind(pattern: string, base: string): { promise: Promise<string[]> } {\n const args = ['--files', '--glob', pattern, base];\n // 30-second safety net to prevent zombie rg processes. Unlike the main\n // grep tool, glob file enumeration is fast and should never need more time.\n const child = spawn('rg', args, {\n signal: AbortSignal.timeout(30_000),\n env: buildChildEnv(),\n stdio: ['ignore', 'pipe', 'pipe'],\n windowsHide: true,\n });\n let buf = '';\n child.stdout?.on('data', (chunk: Buffer) => {\n buf += chunk.toString();\n });\n return {\n promise: new Promise((resolve, reject) => {\n child.on('error', reject);\n child.on('close', () => {\n resolve(buf.split('\\n').filter(Boolean));\n });\n }),\n };\n}\n\nasync function globNative(\n pattern: string,\n base: string,\n extraGlob?: RegExp | null | undefined,\n): Promise<string[]> {\n const results: string[] = [];\n const globRe = compileGlob(pattern);\n\n const walk = async (dir: string): Promise<void> => {\n let entries: import('node:fs').Dirent[];\n try {\n entries = await fs.readdir(dir, { withFileTypes: true });\n } catch {\n return;\n }\n for (const e of entries) {\n if (DEFAULT_IGNORE.includes(e.name)) continue;\n const full = path.join(dir, e.name);\n // Dirent.isSymbolicLink() uses readdir's d_type, which may not detect\n // directory symlinks on Windows (d_type = DT_UNKNOWN). Defensive stat\n // call: skip any entry whose lstat shows a symlink — file or directory.\n try {\n const stat = await fs.lstat(full);\n if (stat.isSymbolicLink()) continue;\n } catch {\n // lstat fails for very unusual entries (e.g. broken symlinks to deleted\n // files on NFS); skip safely rather than surfacing an error.\n continue;\n }\n if (e.isDirectory()) {\n await walk(full);\n } else if (e.isFile()) {\n const name = e.name;\n if (globRe.test(name) || globRe.test(full)) {\n if (extraGlob && !extraGlob.test(name) && !extraGlob.test(full)) continue;\n results.push(full);\n }\n globRe.lastIndex = 0;\n if (extraGlob) extraGlob.lastIndex = 0;\n }\n }\n };\n\n await walk(base);\n return results;\n}\n"]}
package/dist/test.js CHANGED
@@ -42,6 +42,7 @@ async function* spawnStream(opts) {
42
42
  signal: opts.signal,
43
43
  env: buildChildEnv(),
44
44
  stdio: ["ignore", "pipe", "pipe"],
45
+ windowsHide: true,
45
46
  ...needsShell ? { shell: true, windowsVerbatimArguments: true } : {}
46
47
  });
47
48
  const queue = [];
package/dist/test.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/_win32-resolve.ts","../src/_spawn-stream.ts","../src/_util.ts","../src/test.ts"],"names":["path","resolve","path3"],"mappings":"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","file":"test.js","sourcesContent":["import * as fs from 'node:fs';\nimport * as path from 'node:path';\n\n/**\n * On Windows, Node.js `spawn()` without a shell does NOT resolve .cmd/.bat\n * extensions through PATHEXT — it only auto-resolves .exe. Most Node.js CLI\n * tools (npx, pnpm, biome, tsc, vitest, etc.) ship as .cmd wrappers on\n * Windows. This function resolves the command name to its full path so spawn\n * can find it without relying on shell-mode argument concatenation.\n *\n * On non-Windows, returns the command unchanged.\n */\nexport function resolveWin32Command(cmd: string): string {\n if (process.platform !== 'win32') return cmd;\n\n // Already has a path or extension — use as-is\n if (cmd.includes('/') || cmd.includes('\\\\') || path.extname(cmd)) {\n return cmd;\n }\n\n const pathext = (process.env['PATHEXT'] ?? '.COM;.EXE;.BAT;.CMD;.VBS;.JS;.WS;.MSC')\n .toLowerCase()\n .split(';');\n\n const pathDirs = (process.env['PATH'] ?? '').split(path.delimiter);\n\n for (const dir of pathDirs) {\n const base = path.join(dir, cmd);\n // Check extensions in PATHEXT order. .EXE should win first because\n // it's typically listed first, and .exe doesn't need shell: true.\n for (const ext of pathext) {\n const full = `${base}${ext}`;\n try {\n fs.accessSync(full, fs.constants.X_OK);\n return full;\n } catch {\n // Not found with this extension — try next\n }\n }\n }\n\n // Not found — return original; let spawn report ENOENT with the\n // expected error message so tools can surface it properly.\n return cmd;\n}\n","import { spawn } from 'node:child_process';\nimport { buildChildEnv } from '@wrongstack/core';\nimport type { ToolProgressEvent } from '@wrongstack/core';\nimport { resolveWin32Command } from './_win32-resolve.js';\nexport interface SpawnStreamResult {\n stdout: string;\n stderr: string;\n exitCode: number;\n truncated: boolean;\n error?: string | undefined;\n}\n\nexport interface SpawnStreamOptions {\n cmd: string;\n args: string[];\n cwd: string;\n signal: AbortSignal;\n maxBytes?: number | undefined;\n /** Bytes of new stdout/stderr to accumulate before yielding a `partial_output` event. */\n flushBytes?: number | undefined;\n /** Maximum chunks to buffer before applying backpressure to the child. Default 500. */\n maxQueueSize?: number | undefined;\n}\n\n/**\n * Spawn a child process and yield `partial_output` progress events as\n * stdout/stderr arrive (batched by byte threshold), then return the full\n * buffered result. Shared between install/lint/format/typecheck/test/audit\n * so the TUI live tail sees consistent progress regardless of which tool\n * is running.\n */\nexport async function* spawnStream(\n opts: SpawnStreamOptions,\n): AsyncGenerator<ToolProgressEvent, SpawnStreamResult> {\n const max = opts.maxBytes ?? 200_000;\n const flushAt = opts.flushBytes ?? 4 * 1024;\n const maxQueue = opts.maxQueueSize ?? 500;\n let stdout = '';\n let stderr = '';\n let pending = '';\n let error: string | undefined;\n\n const cmd = resolveWin32Command(opts.cmd);\n const needsShell = process.platform === 'win32' && (cmd.endsWith('.cmd') || cmd.endsWith('.bat'));\n\n const child = spawn(cmd, opts.args, {\n cwd: opts.cwd,\n signal: opts.signal,\n env: buildChildEnv(),\n stdio: ['ignore', 'pipe', 'pipe'],\n ...(needsShell ? { shell: true, windowsVerbatimArguments: true } : {}),\n });\n\n type Chunk = { kind: 'out' | 'err' | 'close' | 'error'; data: string; code?: number | undefined };\n const queue: Chunk[] = [];\n let waiter: (() => void) | undefined;\n let paused = false;\n const wake = () => {\n if (waiter) {\n const w = waiter;\n waiter = undefined;\n w();\n }\n };\n\n // Resume the stream when there's room in the queue\n const resume = () => {\n if (paused && queue.length < maxQueue) {\n paused = false;\n child.stdout?.resume();\n child.stderr?.resume();\n }\n };\n\n // Note: chunks may still arrive briefly after pause() (already in flight) —\n // they are accumulated and queued rather than dropped, so the queue can\n // overshoot maxQueue by a few entries but no output is silently lost.\n child.stdout?.on('data', (c) => {\n const s = c.toString();\n if (stdout.length < max) stdout += s;\n queue.push({ kind: 'out', data: s });\n wake();\n // Apply backpressure if queue is growing faster than we consume\n if (!paused && queue.length >= maxQueue) {\n paused = true;\n child.stdout?.pause();\n child.stderr?.pause();\n }\n });\n child.stderr?.on('data', (c) => {\n const s = c.toString();\n if (stderr.length < max) stderr += s;\n queue.push({ kind: 'err', data: s });\n wake();\n if (!paused && queue.length >= maxQueue) {\n paused = true;\n child.stdout?.pause();\n child.stderr?.pause();\n }\n });\n child.on('error', (e) => {\n error = e.message;\n queue.push({ kind: 'error', data: e.message });\n wake();\n });\n child.on('close', (code) => {\n queue.push({ kind: 'close', data: '', code: code ?? 0 });\n wake();\n });\n\n let exitCode = 0;\n let spawnFailed = false;\n for (;;) {\n while (queue.length === 0) {\n await new Promise<void>((resolve) => {\n waiter = resolve;\n });\n }\n const chunk = queue.shift()!;\n // Resume reading after consuming a chunk\n resume();\n if (chunk.kind === 'close') {\n // If we already saw a spawn error (ENOENT etc.), keep exitCode=1\n // rather than the negative platform code Node fabricates.\n if (!spawnFailed) exitCode = chunk.code ?? 0;\n break;\n }\n if (chunk.kind === 'error') {\n spawnFailed = true;\n exitCode = 1;\n // close usually follows\n continue;\n }\n pending += chunk.data;\n if (pending.length >= flushAt) {\n yield { type: 'partial_output', text: pending };\n pending = '';\n }\n }\n if (pending.length > 0) {\n yield { type: 'partial_output', text: pending };\n }\n\n return {\n stdout,\n stderr,\n exitCode,\n truncated: stdout.length >= max || stderr.length >= max,\n error,\n };\n}\n","import * as fsp from 'node:fs/promises';\nimport * as path from 'node:path';\nimport * as Core from '@wrongstack/core';\nimport type { Context } from '@wrongstack/core';\n/** Detected package manager for a project directory. */\nexport type PackageManager = 'pnpm' | 'yarn' | 'npm';\n\n/**\n * Detect the project's package manager by inspecting lockfiles in `cwd`.\n * Order: pnpm → yarn → npm (default). Missing or unreadable directories fall\n * back to `npm` rather than throwing, so a `safeResolve`-checked cwd that\n * happens to be empty never aborts the tool.\n */\nexport async function detectPackageManager(cwd: string): Promise<PackageManager> {\n const { stat } = await import('node:fs/promises');\n try {\n await stat(`${cwd}/pnpm-lock.yaml`);\n return 'pnpm';\n } catch {\n /* not pnpm */\n }\n try {\n await stat(`${cwd}/yarn.lock`);\n return 'yarn';\n } catch {\n /* not yarn */\n }\n return 'npm';\n}\n\nexport function resolvePath(input: string, ctx: Context): string {\n return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.workingDir ?? ctx.cwd, input);\n}\n\nexport function ensureInsideRoot(absPath: string, ctx: Context): string {\n const root = path.resolve(ctx.projectRoot);\n const target = path.resolve(absPath);\n const rel = path.relative(root, target);\n if (rel.startsWith('..') || path.isAbsolute(rel)) {\n throw new Error(`Path \"${absPath}\" is outside project root \"${root}\"`);\n }\n return target;\n}\n\nexport function safeResolve(input: string, ctx: Context): string {\n return ensureInsideRoot(resolvePath(input, ctx), ctx);\n}\n\n/**\n * Defense against in-root→out-of-root symlink escape (CWE-59). `safeResolve`\n * only does a syntactic `../` check, so a symlink that lives *inside* the\n * project root but points outside still passes it. This resolves the path\n * through `fs.realpath` and re-verifies containment against the realpath of\n * the project root (comparing like-for-like, since the root itself may be a\n * symlink — macOS `/var`→`/private/var`, Windows 8.3 short names). For a path\n * that does not exist yet (e.g. a `write` to a new file) the nearest existing\n * ancestor directory is checked instead. Throws if the real target escapes.\n *\n * Mirrors the per-file guard already used in `replace.ts`/`grep.ts`; applied\n * to single-file `read`/`edit`/`write` it throws (rather than skips) because\n * the caller named exactly one file.\n */\nexport async function assertRealInsideRoot(absPath: string, ctx: Context): Promise<void> {\n const realRoot = await fsp.realpath(ctx.projectRoot).catch(() => path.resolve(ctx.projectRoot));\n let probe = absPath;\n for (;;) {\n let real: string;\n try {\n real = await fsp.realpath(probe);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') {\n const parent = path.dirname(probe);\n if (parent === probe) return; // reached fs root without escaping\n probe = parent;\n continue;\n }\n throw err;\n }\n const rel = path.relative(realRoot, real);\n if (rel.startsWith('..') || path.isAbsolute(rel)) {\n throw new Error(\n `Path \"${absPath}\" resolves through a symlink outside project root \"${realRoot}\"`,\n );\n }\n return;\n }\n}\n\n/** `safeResolve` + symlink realpath containment check. Async. */\nexport async function safeResolveReal(input: string, ctx: Context): Promise<string> {\n const abs = safeResolve(input, ctx);\n await assertRealInsideRoot(abs, ctx);\n return abs;\n}\n\nexport function truncateMiddle(s: string, max: number): string {\n if (Buffer.byteLength(s, 'utf8') <= max) return s;\n const half = Math.floor(max / 2);\n return (\n s.slice(0, half) +\n `\\n…[truncated ${Buffer.byteLength(s, 'utf8') - max} bytes from middle]…\\n` +\n s.slice(-half)\n );\n}\n\nexport function isBinaryBuffer(buf: Buffer): boolean {\n const len = Math.min(buf.length, 8192);\n for (let i = 0; i < len; i++) {\n if (buf[i] === 0) return true;\n }\n return false;\n}\n\n// ─── Command-output normalization (token-saving) ────────────────────────────\n//\n// Raw process output is full of tokens the model gains nothing from: ANSI\n// escapes, carriage-return progress spam, runs of identical warning lines, and\n// huge tails of build noise. These helpers strip that noise before the output\n// reaches the LLM. They are scoped to COMMAND tools (bash/git/exec and the\n// _spawn-stream consumers) — never applied to structured/code outputs.\n\n/** Unified byte cap for all command tool output fed to the model. */\nexport const COMMAND_OUTPUT_MAX_BYTES = 32_768;\n\n/** Runs of >= this many identical consecutive lines are collapsed. */\nconst REPEAT_RUN_THRESHOLD = 3;\n\n/**\n * Collapse carriage-return overwrites the way a terminal would: `\\r\\n` becomes\n * `\\n`, and a bare `\\r` (progress redraw) keeps only the text after the LAST\n * `\\r` on its physical line. Without this, a single progress bar that redraws\n * 200 times explodes into 200 lines.\n */\nexport function collapseCarriageReturns(text: string): string {\n const lf = text.replace(/\\r\\n/g, '\\n');\n if (!lf.includes('\\r')) return lf;\n return lf\n .split('\\n')\n .map((line) => (line.includes('\\r') ? line.slice(line.lastIndexOf('\\r') + 1) : line))\n .join('\\n');\n}\n\n/**\n * Collapse a run of `minRun`+ identical consecutive lines into the line once\n * plus a marker. Consecutive-only — it never reorders or dedups non-adjacent\n * lines, so diffs/source stay intact.\n */\nexport function collapseConsecutiveDuplicates(text: string, minRun = REPEAT_RUN_THRESHOLD): string {\n const lines = text.split('\\n');\n const out: string[] = [];\n let i = 0;\n while (i < lines.length) {\n let j = i + 1;\n while (j < lines.length && lines[j] === lines[i]) j++;\n const run = j - i;\n if (run >= minRun) {\n out.push(lines[i]!, `… ⟨repeated ${run}×⟩`);\n } else {\n for (let k = i; k < j; k++) out.push(lines[k]!);\n }\n i = j;\n }\n return out.join('\\n');\n}\n\n/** Largest prefix of `s` whose UTF-8 byte length is <= `maxBytes`. */\nfunction takeHeadBytes(s: string, maxBytes: number): string {\n if (maxBytes <= 0) return '';\n if (Buffer.byteLength(s, 'utf8') <= maxBytes) return s;\n let lo = 0;\n let hi = s.length;\n while (lo < hi) {\n const mid = Math.ceil((lo + hi) / 2);\n if (Buffer.byteLength(s.slice(0, mid), 'utf8') <= maxBytes) lo = mid;\n else hi = mid - 1;\n }\n return s.slice(0, lo);\n}\n\n/** Largest suffix of `s` whose UTF-8 byte length is <= `maxBytes`. */\nfunction takeTailBytes(s: string, maxBytes: number): string {\n if (maxBytes <= 0) return '';\n if (Buffer.byteLength(s, 'utf8') <= maxBytes) return s;\n let lo = 0;\n let hi = s.length;\n while (lo < hi) {\n const mid = Math.ceil((lo + hi) / 2);\n if (Buffer.byteLength(s.slice(s.length - mid), 'utf8') <= maxBytes) lo = mid;\n else hi = mid - 1;\n }\n return s.slice(s.length - lo);\n}\n\n/**\n * Truncate to `maxBytes` keeping BOTH ends — the head (what ran / early context)\n * and the tail (errors and summaries usually land last), biased ~45/55 toward\n * the tail. The result never exceeds `maxBytes`.\n */\nexport function truncateHeadTail(s: string, maxBytes: number): string {\n const total = Buffer.byteLength(s, 'utf8');\n if (total <= maxBytes) return s;\n // Reserve a fixed allowance for the marker so the final string can't exceed\n // the cap even though the dropped-byte count's digit width varies.\n const MARKER_RESERVE = 64;\n const avail = Math.max(0, maxBytes - MARKER_RESERVE);\n const headBudget = Math.floor(avail * 0.45);\n const head = takeHeadBytes(s, headBudget);\n const tail = takeTailBytes(s, avail - Buffer.byteLength(head, 'utf8'));\n const kept = Buffer.byteLength(head, 'utf8') + Buffer.byteLength(tail, 'utf8');\n return `${head}\\n…[truncated ${total - kept} bytes]…\\n${tail}`;\n}\n\n/**\n * Full token-saving pipeline for command tool output: strip ANSI → collapse\n * carriage-return progress → trim trailing whitespace → collapse identical\n * consecutive lines → squeeze blank-line runs → head+tail truncate to the cap.\n */\nexport function normalizeCommandOutput(\n raw: string,\n opts: { maxBytes?: number | undefined } = {},\n): string {\n if (!raw) return raw;\n let text = Core.stripAnsi(raw);\n text = collapseCarriageReturns(text);\n text = text.replace(/[ \\t]+$/gm, ''); // trailing whitespace per line\n text = collapseConsecutiveDuplicates(text);\n text = text.replace(/\\n{3,}/g, '\\n\\n'); // >=2 blank lines → 1\n return truncateHeadTail(text, opts.maxBytes ?? COMMAND_OUTPUT_MAX_BYTES);\n}\n","import * as path from 'node:path';\nimport type { Tool, ToolStreamEvent } from '@wrongstack/core';\nimport { spawnStream } from './_spawn-stream.js';\nimport { normalizeCommandOutput, safeResolve } from './_util.js';\n\ninterface TestInput {\n files?: string | string[] | undefined;\n runner?: 'vitest' | 'jest' | 'mocha' | 'auto' | undefined;\n watch?: boolean | undefined;\n coverage?: boolean | undefined;\n cwd?: string | undefined;\n grep?: string | undefined;\n timeout?: number | undefined;\n}\n\ninterface TestOutput {\n runner: string;\n exit_code: number;\n tests_run: number;\n passed: number;\n failed: number;\n duration_ms: number;\n output: string;\n truncated: boolean;\n}\n\nexport const testTool: Tool<TestInput, TestOutput> = {\n name: 'test',\n category: 'Code Quality',\n description:\n 'Execute the project\\'s test suite. This is one of the most critical tools for validating that your changes are correct.',\n usageHint:\n 'ESSENTIAL BEFORE CONSIDERING WORK DONE:\\n\\n' +\n '- Use `files` or `grep` to run only relevant tests during development.\\n' +\n '- `coverage: true` is useful when working on critical paths.\\n' +\n 'Run tests frequently. A clean test run is usually required before the task can be considered complete.',\n permission: 'confirm',\n mutating: false,\n timeoutMs: 120_000,\n inputSchema: {\n type: 'object',\n properties: {\n files: {\n type: 'string',\n description: 'Test files: single path, comma-separated list, or glob (e.g. \"**/*.test.ts\")',\n },\n runner: {\n type: 'string',\n enum: ['vitest', 'jest', 'mocha', 'auto'],\n description: 'Test runner (default: auto-detect)',\n },\n watch: { type: 'boolean', description: 'Run in watch mode (default: false)' },\n coverage: { type: 'boolean', description: 'Generate coverage report (default: false)' },\n cwd: { type: 'string', description: 'Working directory (default: cwd)' },\n grep: { type: 'string', description: 'Filter tests by name pattern (default: none)' },\n timeout: { type: 'integer', description: 'Test timeout in ms (default: 30000)' },\n },\n },\n async execute(input, ctx, opts) {\n let final: TestOutput | undefined;\n const executeStream = testTool.executeStream;\n if (!executeStream) throw new Error('testTool: stream execution unavailable');\n for await (const ev of executeStream(input, ctx, opts)) {\n if (ev.type === 'final') final = ev.output;\n }\n if (!final) throw new Error('test: stream ended without final event');\n return final;\n },\n async *executeStream(input, ctx, opts): AsyncGenerator<ToolStreamEvent<TestOutput>> {\n const cwd = input.cwd ? safeResolve(input.cwd, ctx) : ctx.cwd;\n const runner = input.runner ?? 'auto';\n\n const detected = runner === 'auto' ? await detectRunner(cwd) : runner;\n if (!detected) {\n yield {\n type: 'final',\n output: {\n runner: 'none',\n exit_code: 0,\n tests_run: 0,\n passed: 0,\n failed: 0,\n duration_ms: 0,\n output: 'No test runner found (vitest.config.ts, jest.config.js, .mocharc.json)',\n truncated: false,\n },\n };\n return;\n }\n\n yield { type: 'log', text: `Running ${detected}…`, data: { runner: detected } };\n\n const start = Date.now();\n const args = buildArgs(detected, input);\n\n const result = yield* spawnStream({\n cmd: detected,\n args,\n cwd,\n signal: opts.signal,\n maxBytes: 200_000,\n });\n const duration = Date.now() - start;\n\n yield { type: 'final', output: parseResult(detected, result, duration) };\n },\n};\n\nasync function detectRunner(cwd: string): Promise<string | null> {\n const { stat } = await import('node:fs/promises');\n const candidates = ['vitest.config.ts', 'jest.config.js', '.mocharc.json'];\n for (const f of candidates) {\n try {\n await stat(path.join(cwd, f));\n if (f.includes('vitest')) return 'vitest';\n if (f.includes('jest')) return 'jest';\n if (f.includes('mocha')) return 'mocha';\n } catch {\n // continue\n }\n }\n return null;\n}\n\nfunction buildArgs(runner: string, input: TestInput): string[] {\n const args: string[] = [];\n const timeout = input.timeout ?? 30000;\n\n switch (runner) {\n case 'vitest':\n args.push('run', '--reporter=verbose');\n if (input.watch) {\n args[1] = '';\n args.push('watch');\n }\n if (input.coverage) args.push('--coverage');\n if (input.grep) args.push('--testNamePattern', input.grep);\n args.push('--testTimeout', String(timeout));\n break;\n case 'jest':\n args.push('--verbose');\n if (input.watch) args.push('--watch');\n if (input.coverage) args.push('--coverage');\n if (input.grep) args.push('--testPathPattern', input.grep);\n args.push('--testTimeout', String(timeout));\n break;\n case 'mocha':\n args.push('--reporter', 'spec');\n if (input.grep) args.push('--grep', input.grep);\n args.push('--timeout', String(timeout));\n break;\n }\n\n if (input.files) {\n const files = Array.isArray(input.files) ? input.files : input.files.split(',');\n args.push('--', ...files.map((f) => f.trim()));\n }\n\n return args;\n}\n\nfunction parseResult(\n runner: string,\n result: { stdout: string; stderr: string; exitCode: number; truncated: boolean; error?: string | undefined },\n duration: number,\n): TestOutput {\n const out = result.stdout + result.stderr;\n\n let tests_run = 0;\n let passed = 0;\n let failed = 0;\n\n if (runner === 'vitest') {\n const passedMatch = out.match(/(\\d+) passed/);\n const failedMatch = out.match(/(\\d+) failed/);\n if (passedMatch?.[1]) passed = Number.parseInt(passedMatch[1], 10);\n if (failedMatch?.[1]) failed = Number.parseInt(failedMatch[1], 10);\n tests_run = passed + failed;\n } else if (runner === 'jest') {\n const suitesMatch = out.match(/Test Suites:\\s+(\\d+)\\s+total/);\n const passedMatch = out.match(/Tests:\\s+(\\d+)\\s+passed/);\n const failedMatch = out.match(/Tests:\\s+(\\d+)\\s+failed/);\n tests_run = Number.parseInt(suitesMatch?.[1] ?? '0', 10);\n passed = Number.parseInt(passedMatch?.[1] ?? '0', 10);\n failed = Number.parseInt(failedMatch?.[1] ?? '0', 10);\n }\n\n return {\n runner,\n exit_code: result.exitCode,\n tests_run,\n passed,\n failed,\n duration_ms: duration,\n output: normalizeCommandOutput(result.stdout || result.error || ''),\n truncated: result.truncated,\n };\n}\n"]}
1
+ {"version":3,"sources":["../src/_win32-resolve.ts","../src/_spawn-stream.ts","../src/_util.ts","../src/test.ts"],"names":["path","resolve","path3"],"mappings":"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","file":"test.js","sourcesContent":["import * as fs from 'node:fs';\nimport * as path from 'node:path';\n\n/**\n * On Windows, Node.js `spawn()` without a shell does NOT resolve .cmd/.bat\n * extensions through PATHEXT — it only auto-resolves .exe. Most Node.js CLI\n * tools (npx, pnpm, biome, tsc, vitest, etc.) ship as .cmd wrappers on\n * Windows. This function resolves the command name to its full path so spawn\n * can find it without relying on shell-mode argument concatenation.\n *\n * On non-Windows, returns the command unchanged.\n */\nexport function resolveWin32Command(cmd: string): string {\n if (process.platform !== 'win32') return cmd;\n\n // Already has a path or extension — use as-is\n if (cmd.includes('/') || cmd.includes('\\\\') || path.extname(cmd)) {\n return cmd;\n }\n\n const pathext = (process.env['PATHEXT'] ?? '.COM;.EXE;.BAT;.CMD;.VBS;.JS;.WS;.MSC')\n .toLowerCase()\n .split(';');\n\n const pathDirs = (process.env['PATH'] ?? '').split(path.delimiter);\n\n for (const dir of pathDirs) {\n const base = path.join(dir, cmd);\n // Check extensions in PATHEXT order. .EXE should win first because\n // it's typically listed first, and .exe doesn't need shell: true.\n for (const ext of pathext) {\n const full = `${base}${ext}`;\n try {\n fs.accessSync(full, fs.constants.X_OK);\n return full;\n } catch {\n // Not found with this extension — try next\n }\n }\n }\n\n // Not found — return original; let spawn report ENOENT with the\n // expected error message so tools can surface it properly.\n return cmd;\n}\n","import { spawn } from 'node:child_process';\nimport { buildChildEnv } from '@wrongstack/core';\nimport type { ToolProgressEvent } from '@wrongstack/core';\nimport { resolveWin32Command } from './_win32-resolve.js';\nexport interface SpawnStreamResult {\n stdout: string;\n stderr: string;\n exitCode: number;\n truncated: boolean;\n error?: string | undefined;\n}\n\nexport interface SpawnStreamOptions {\n cmd: string;\n args: string[];\n cwd: string;\n signal: AbortSignal;\n maxBytes?: number | undefined;\n /** Bytes of new stdout/stderr to accumulate before yielding a `partial_output` event. */\n flushBytes?: number | undefined;\n /** Maximum chunks to buffer before applying backpressure to the child. Default 500. */\n maxQueueSize?: number | undefined;\n}\n\n/**\n * Spawn a child process and yield `partial_output` progress events as\n * stdout/stderr arrive (batched by byte threshold), then return the full\n * buffered result. Shared between install/lint/format/typecheck/test/audit\n * so the TUI live tail sees consistent progress regardless of which tool\n * is running.\n */\nexport async function* spawnStream(\n opts: SpawnStreamOptions,\n): AsyncGenerator<ToolProgressEvent, SpawnStreamResult> {\n const max = opts.maxBytes ?? 200_000;\n const flushAt = opts.flushBytes ?? 4 * 1024;\n const maxQueue = opts.maxQueueSize ?? 500;\n let stdout = '';\n let stderr = '';\n let pending = '';\n let error: string | undefined;\n\n const cmd = resolveWin32Command(opts.cmd);\n const needsShell = process.platform === 'win32' && (cmd.endsWith('.cmd') || cmd.endsWith('.bat'));\n\n const child = spawn(cmd, opts.args, {\n cwd: opts.cwd,\n signal: opts.signal,\n env: buildChildEnv(),\n stdio: ['ignore', 'pipe', 'pipe'],\n windowsHide: true,\n ...(needsShell ? { shell: true, windowsVerbatimArguments: true } : {}),\n });\n\n type Chunk = { kind: 'out' | 'err' | 'close' | 'error'; data: string; code?: number | undefined };\n const queue: Chunk[] = [];\n let waiter: (() => void) | undefined;\n let paused = false;\n const wake = () => {\n if (waiter) {\n const w = waiter;\n waiter = undefined;\n w();\n }\n };\n\n // Resume the stream when there's room in the queue\n const resume = () => {\n if (paused && queue.length < maxQueue) {\n paused = false;\n child.stdout?.resume();\n child.stderr?.resume();\n }\n };\n\n // Note: chunks may still arrive briefly after pause() (already in flight) —\n // they are accumulated and queued rather than dropped, so the queue can\n // overshoot maxQueue by a few entries but no output is silently lost.\n child.stdout?.on('data', (c) => {\n const s = c.toString();\n if (stdout.length < max) stdout += s;\n queue.push({ kind: 'out', data: s });\n wake();\n // Apply backpressure if queue is growing faster than we consume\n if (!paused && queue.length >= maxQueue) {\n paused = true;\n child.stdout?.pause();\n child.stderr?.pause();\n }\n });\n child.stderr?.on('data', (c) => {\n const s = c.toString();\n if (stderr.length < max) stderr += s;\n queue.push({ kind: 'err', data: s });\n wake();\n if (!paused && queue.length >= maxQueue) {\n paused = true;\n child.stdout?.pause();\n child.stderr?.pause();\n }\n });\n child.on('error', (e) => {\n error = e.message;\n queue.push({ kind: 'error', data: e.message });\n wake();\n });\n child.on('close', (code) => {\n queue.push({ kind: 'close', data: '', code: code ?? 0 });\n wake();\n });\n\n let exitCode = 0;\n let spawnFailed = false;\n for (;;) {\n while (queue.length === 0) {\n await new Promise<void>((resolve) => {\n waiter = resolve;\n });\n }\n const chunk = queue.shift()!;\n // Resume reading after consuming a chunk\n resume();\n if (chunk.kind === 'close') {\n // If we already saw a spawn error (ENOENT etc.), keep exitCode=1\n // rather than the negative platform code Node fabricates.\n if (!spawnFailed) exitCode = chunk.code ?? 0;\n break;\n }\n if (chunk.kind === 'error') {\n spawnFailed = true;\n exitCode = 1;\n // close usually follows\n continue;\n }\n pending += chunk.data;\n if (pending.length >= flushAt) {\n yield { type: 'partial_output', text: pending };\n pending = '';\n }\n }\n if (pending.length > 0) {\n yield { type: 'partial_output', text: pending };\n }\n\n return {\n stdout,\n stderr,\n exitCode,\n truncated: stdout.length >= max || stderr.length >= max,\n error,\n };\n}\n","import * as fsp from 'node:fs/promises';\nimport * as path from 'node:path';\nimport * as Core from '@wrongstack/core';\nimport type { Context } from '@wrongstack/core';\n/** Detected package manager for a project directory. */\nexport type PackageManager = 'pnpm' | 'yarn' | 'npm';\n\n/**\n * Detect the project's package manager by inspecting lockfiles in `cwd`.\n * Order: pnpm → yarn → npm (default). Missing or unreadable directories fall\n * back to `npm` rather than throwing, so a `safeResolve`-checked cwd that\n * happens to be empty never aborts the tool.\n */\nexport async function detectPackageManager(cwd: string): Promise<PackageManager> {\n const { stat } = await import('node:fs/promises');\n try {\n await stat(`${cwd}/pnpm-lock.yaml`);\n return 'pnpm';\n } catch {\n /* not pnpm */\n }\n try {\n await stat(`${cwd}/yarn.lock`);\n return 'yarn';\n } catch {\n /* not yarn */\n }\n return 'npm';\n}\n\nexport function resolvePath(input: string, ctx: Context): string {\n return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.workingDir ?? ctx.cwd, input);\n}\n\nexport function ensureInsideRoot(absPath: string, ctx: Context): string {\n const root = path.resolve(ctx.projectRoot);\n const target = path.resolve(absPath);\n const rel = path.relative(root, target);\n if (rel.startsWith('..') || path.isAbsolute(rel)) {\n throw new Error(`Path \"${absPath}\" is outside project root \"${root}\"`);\n }\n return target;\n}\n\nexport function safeResolve(input: string, ctx: Context): string {\n return ensureInsideRoot(resolvePath(input, ctx), ctx);\n}\n\n/**\n * Defense against in-root→out-of-root symlink escape (CWE-59). `safeResolve`\n * only does a syntactic `../` check, so a symlink that lives *inside* the\n * project root but points outside still passes it. This resolves the path\n * through `fs.realpath` and re-verifies containment against the realpath of\n * the project root (comparing like-for-like, since the root itself may be a\n * symlink — macOS `/var`→`/private/var`, Windows 8.3 short names). For a path\n * that does not exist yet (e.g. a `write` to a new file) the nearest existing\n * ancestor directory is checked instead. Throws if the real target escapes.\n *\n * Mirrors the per-file guard already used in `replace.ts`/`grep.ts`; applied\n * to single-file `read`/`edit`/`write` it throws (rather than skips) because\n * the caller named exactly one file.\n */\nexport async function assertRealInsideRoot(absPath: string, ctx: Context): Promise<void> {\n const realRoot = await fsp.realpath(ctx.projectRoot).catch(() => path.resolve(ctx.projectRoot));\n let probe = absPath;\n for (;;) {\n let real: string;\n try {\n real = await fsp.realpath(probe);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') {\n const parent = path.dirname(probe);\n if (parent === probe) return; // reached fs root without escaping\n probe = parent;\n continue;\n }\n throw err;\n }\n const rel = path.relative(realRoot, real);\n if (rel.startsWith('..') || path.isAbsolute(rel)) {\n throw new Error(\n `Path \"${absPath}\" resolves through a symlink outside project root \"${realRoot}\"`,\n );\n }\n return;\n }\n}\n\n/** `safeResolve` + symlink realpath containment check. Async. */\nexport async function safeResolveReal(input: string, ctx: Context): Promise<string> {\n const abs = safeResolve(input, ctx);\n await assertRealInsideRoot(abs, ctx);\n return abs;\n}\n\nexport function truncateMiddle(s: string, max: number): string {\n if (Buffer.byteLength(s, 'utf8') <= max) return s;\n const half = Math.floor(max / 2);\n return (\n s.slice(0, half) +\n `\\n…[truncated ${Buffer.byteLength(s, 'utf8') - max} bytes from middle]…\\n` +\n s.slice(-half)\n );\n}\n\nexport function isBinaryBuffer(buf: Buffer): boolean {\n const len = Math.min(buf.length, 8192);\n for (let i = 0; i < len; i++) {\n if (buf[i] === 0) return true;\n }\n return false;\n}\n\n// ─── Command-output normalization (token-saving) ────────────────────────────\n//\n// Raw process output is full of tokens the model gains nothing from: ANSI\n// escapes, carriage-return progress spam, runs of identical warning lines, and\n// huge tails of build noise. These helpers strip that noise before the output\n// reaches the LLM. They are scoped to COMMAND tools (bash/git/exec and the\n// _spawn-stream consumers) — never applied to structured/code outputs.\n\n/** Unified byte cap for all command tool output fed to the model. */\nexport const COMMAND_OUTPUT_MAX_BYTES = 32_768;\n\n/** Runs of >= this many identical consecutive lines are collapsed. */\nconst REPEAT_RUN_THRESHOLD = 3;\n\n/**\n * Collapse carriage-return overwrites the way a terminal would: `\\r\\n` becomes\n * `\\n`, and a bare `\\r` (progress redraw) keeps only the text after the LAST\n * `\\r` on its physical line. Without this, a single progress bar that redraws\n * 200 times explodes into 200 lines.\n */\nexport function collapseCarriageReturns(text: string): string {\n const lf = text.replace(/\\r\\n/g, '\\n');\n if (!lf.includes('\\r')) return lf;\n return lf\n .split('\\n')\n .map((line) => (line.includes('\\r') ? line.slice(line.lastIndexOf('\\r') + 1) : line))\n .join('\\n');\n}\n\n/**\n * Collapse a run of `minRun`+ identical consecutive lines into the line once\n * plus a marker. Consecutive-only — it never reorders or dedups non-adjacent\n * lines, so diffs/source stay intact.\n */\nexport function collapseConsecutiveDuplicates(text: string, minRun = REPEAT_RUN_THRESHOLD): string {\n const lines = text.split('\\n');\n const out: string[] = [];\n let i = 0;\n while (i < lines.length) {\n let j = i + 1;\n while (j < lines.length && lines[j] === lines[i]) j++;\n const run = j - i;\n if (run >= minRun) {\n out.push(lines[i]!, `… ⟨repeated ${run}×⟩`);\n } else {\n for (let k = i; k < j; k++) out.push(lines[k]!);\n }\n i = j;\n }\n return out.join('\\n');\n}\n\n/** Largest prefix of `s` whose UTF-8 byte length is <= `maxBytes`. */\nfunction takeHeadBytes(s: string, maxBytes: number): string {\n if (maxBytes <= 0) return '';\n if (Buffer.byteLength(s, 'utf8') <= maxBytes) return s;\n let lo = 0;\n let hi = s.length;\n while (lo < hi) {\n const mid = Math.ceil((lo + hi) / 2);\n if (Buffer.byteLength(s.slice(0, mid), 'utf8') <= maxBytes) lo = mid;\n else hi = mid - 1;\n }\n return s.slice(0, lo);\n}\n\n/** Largest suffix of `s` whose UTF-8 byte length is <= `maxBytes`. */\nfunction takeTailBytes(s: string, maxBytes: number): string {\n if (maxBytes <= 0) return '';\n if (Buffer.byteLength(s, 'utf8') <= maxBytes) return s;\n let lo = 0;\n let hi = s.length;\n while (lo < hi) {\n const mid = Math.ceil((lo + hi) / 2);\n if (Buffer.byteLength(s.slice(s.length - mid), 'utf8') <= maxBytes) lo = mid;\n else hi = mid - 1;\n }\n return s.slice(s.length - lo);\n}\n\n/**\n * Truncate to `maxBytes` keeping BOTH ends — the head (what ran / early context)\n * and the tail (errors and summaries usually land last), biased ~45/55 toward\n * the tail. The result never exceeds `maxBytes`.\n */\nexport function truncateHeadTail(s: string, maxBytes: number): string {\n const total = Buffer.byteLength(s, 'utf8');\n if (total <= maxBytes) return s;\n // Reserve a fixed allowance for the marker so the final string can't exceed\n // the cap even though the dropped-byte count's digit width varies.\n const MARKER_RESERVE = 64;\n const avail = Math.max(0, maxBytes - MARKER_RESERVE);\n const headBudget = Math.floor(avail * 0.45);\n const head = takeHeadBytes(s, headBudget);\n const tail = takeTailBytes(s, avail - Buffer.byteLength(head, 'utf8'));\n const kept = Buffer.byteLength(head, 'utf8') + Buffer.byteLength(tail, 'utf8');\n return `${head}\\n…[truncated ${total - kept} bytes]…\\n${tail}`;\n}\n\n/**\n * Full token-saving pipeline for command tool output: strip ANSI → collapse\n * carriage-return progress → trim trailing whitespace → collapse identical\n * consecutive lines → squeeze blank-line runs → head+tail truncate to the cap.\n */\nexport function normalizeCommandOutput(\n raw: string,\n opts: { maxBytes?: number | undefined } = {},\n): string {\n if (!raw) return raw;\n let text = Core.stripAnsi(raw);\n text = collapseCarriageReturns(text);\n text = text.replace(/[ \\t]+$/gm, ''); // trailing whitespace per line\n text = collapseConsecutiveDuplicates(text);\n text = text.replace(/\\n{3,}/g, '\\n\\n'); // >=2 blank lines → 1\n return truncateHeadTail(text, opts.maxBytes ?? COMMAND_OUTPUT_MAX_BYTES);\n}\n","import * as path from 'node:path';\nimport type { Tool, ToolStreamEvent } from '@wrongstack/core';\nimport { spawnStream } from './_spawn-stream.js';\nimport { normalizeCommandOutput, safeResolve } from './_util.js';\n\ninterface TestInput {\n files?: string | string[] | undefined;\n runner?: 'vitest' | 'jest' | 'mocha' | 'auto' | undefined;\n watch?: boolean | undefined;\n coverage?: boolean | undefined;\n cwd?: string | undefined;\n grep?: string | undefined;\n timeout?: number | undefined;\n}\n\ninterface TestOutput {\n runner: string;\n exit_code: number;\n tests_run: number;\n passed: number;\n failed: number;\n duration_ms: number;\n output: string;\n truncated: boolean;\n}\n\nexport const testTool: Tool<TestInput, TestOutput> = {\n name: 'test',\n category: 'Code Quality',\n description:\n 'Execute the project\\'s test suite. This is one of the most critical tools for validating that your changes are correct.',\n usageHint:\n 'ESSENTIAL BEFORE CONSIDERING WORK DONE:\\n\\n' +\n '- Use `files` or `grep` to run only relevant tests during development.\\n' +\n '- `coverage: true` is useful when working on critical paths.\\n' +\n 'Run tests frequently. A clean test run is usually required before the task can be considered complete.',\n permission: 'confirm',\n mutating: false,\n timeoutMs: 120_000,\n inputSchema: {\n type: 'object',\n properties: {\n files: {\n type: 'string',\n description: 'Test files: single path, comma-separated list, or glob (e.g. \"**/*.test.ts\")',\n },\n runner: {\n type: 'string',\n enum: ['vitest', 'jest', 'mocha', 'auto'],\n description: 'Test runner (default: auto-detect)',\n },\n watch: { type: 'boolean', description: 'Run in watch mode (default: false)' },\n coverage: { type: 'boolean', description: 'Generate coverage report (default: false)' },\n cwd: { type: 'string', description: 'Working directory (default: cwd)' },\n grep: { type: 'string', description: 'Filter tests by name pattern (default: none)' },\n timeout: { type: 'integer', description: 'Test timeout in ms (default: 30000)' },\n },\n },\n async execute(input, ctx, opts) {\n let final: TestOutput | undefined;\n const executeStream = testTool.executeStream;\n if (!executeStream) throw new Error('testTool: stream execution unavailable');\n for await (const ev of executeStream(input, ctx, opts)) {\n if (ev.type === 'final') final = ev.output;\n }\n if (!final) throw new Error('test: stream ended without final event');\n return final;\n },\n async *executeStream(input, ctx, opts): AsyncGenerator<ToolStreamEvent<TestOutput>> {\n const cwd = input.cwd ? safeResolve(input.cwd, ctx) : ctx.cwd;\n const runner = input.runner ?? 'auto';\n\n const detected = runner === 'auto' ? await detectRunner(cwd) : runner;\n if (!detected) {\n yield {\n type: 'final',\n output: {\n runner: 'none',\n exit_code: 0,\n tests_run: 0,\n passed: 0,\n failed: 0,\n duration_ms: 0,\n output: 'No test runner found (vitest.config.ts, jest.config.js, .mocharc.json)',\n truncated: false,\n },\n };\n return;\n }\n\n yield { type: 'log', text: `Running ${detected}…`, data: { runner: detected } };\n\n const start = Date.now();\n const args = buildArgs(detected, input);\n\n const result = yield* spawnStream({\n cmd: detected,\n args,\n cwd,\n signal: opts.signal,\n maxBytes: 200_000,\n });\n const duration = Date.now() - start;\n\n yield { type: 'final', output: parseResult(detected, result, duration) };\n },\n};\n\nasync function detectRunner(cwd: string): Promise<string | null> {\n const { stat } = await import('node:fs/promises');\n const candidates = ['vitest.config.ts', 'jest.config.js', '.mocharc.json'];\n for (const f of candidates) {\n try {\n await stat(path.join(cwd, f));\n if (f.includes('vitest')) return 'vitest';\n if (f.includes('jest')) return 'jest';\n if (f.includes('mocha')) return 'mocha';\n } catch {\n // continue\n }\n }\n return null;\n}\n\nfunction buildArgs(runner: string, input: TestInput): string[] {\n const args: string[] = [];\n const timeout = input.timeout ?? 30000;\n\n switch (runner) {\n case 'vitest':\n args.push('run', '--reporter=verbose');\n if (input.watch) {\n args[1] = '';\n args.push('watch');\n }\n if (input.coverage) args.push('--coverage');\n if (input.grep) args.push('--testNamePattern', input.grep);\n args.push('--testTimeout', String(timeout));\n break;\n case 'jest':\n args.push('--verbose');\n if (input.watch) args.push('--watch');\n if (input.coverage) args.push('--coverage');\n if (input.grep) args.push('--testPathPattern', input.grep);\n args.push('--testTimeout', String(timeout));\n break;\n case 'mocha':\n args.push('--reporter', 'spec');\n if (input.grep) args.push('--grep', input.grep);\n args.push('--timeout', String(timeout));\n break;\n }\n\n if (input.files) {\n const files = Array.isArray(input.files) ? input.files : input.files.split(',');\n args.push('--', ...files.map((f) => f.trim()));\n }\n\n return args;\n}\n\nfunction parseResult(\n runner: string,\n result: { stdout: string; stderr: string; exitCode: number; truncated: boolean; error?: string | undefined },\n duration: number,\n): TestOutput {\n const out = result.stdout + result.stderr;\n\n let tests_run = 0;\n let passed = 0;\n let failed = 0;\n\n if (runner === 'vitest') {\n const passedMatch = out.match(/(\\d+) passed/);\n const failedMatch = out.match(/(\\d+) failed/);\n if (passedMatch?.[1]) passed = Number.parseInt(passedMatch[1], 10);\n if (failedMatch?.[1]) failed = Number.parseInt(failedMatch[1], 10);\n tests_run = passed + failed;\n } else if (runner === 'jest') {\n const suitesMatch = out.match(/Test Suites:\\s+(\\d+)\\s+total/);\n const passedMatch = out.match(/Tests:\\s+(\\d+)\\s+passed/);\n const failedMatch = out.match(/Tests:\\s+(\\d+)\\s+failed/);\n tests_run = Number.parseInt(suitesMatch?.[1] ?? '0', 10);\n passed = Number.parseInt(passedMatch?.[1] ?? '0', 10);\n failed = Number.parseInt(failedMatch?.[1] ?? '0', 10);\n }\n\n return {\n runner,\n exit_code: result.exitCode,\n tests_run,\n passed,\n failed,\n duration_ms: duration,\n output: normalizeCommandOutput(result.stdout || result.error || ''),\n truncated: result.truncated,\n };\n}\n"]}
package/dist/typecheck.js CHANGED
@@ -42,6 +42,7 @@ async function* spawnStream(opts) {
42
42
  signal: opts.signal,
43
43
  env: buildChildEnv(),
44
44
  stdio: ["ignore", "pipe", "pipe"],
45
+ windowsHide: true,
45
46
  ...needsShell ? { shell: true, windowsVerbatimArguments: true } : {}
46
47
  });
47
48
  const queue = [];