@wrongstack/tools 0.283.1 → 0.284.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/audit.js.map +1 -1
- package/dist/bash.js +4 -0
- package/dist/bash.js.map +1 -1
- package/dist/builtin.js +685 -253
- package/dist/builtin.js.map +1 -1
- package/dist/codebase-index/index.js +2 -2
- package/dist/codebase-index/index.js.map +1 -1
- package/dist/diff.js.map +1 -1
- package/dist/document.js.map +1 -1
- package/dist/edit.d.ts +43 -0
- package/dist/edit.js +386 -60
- package/dist/edit.js.map +1 -1
- package/dist/{exec-Ca3fnpUh.d.ts → exec-2OKoT6tk.d.ts} +1 -1
- package/dist/exec.d.ts +1 -1
- package/dist/exec.js +13 -14
- package/dist/exec.js.map +1 -1
- package/dist/fetch.js.map +1 -1
- package/dist/format.js.map +1 -1
- package/dist/git.js.map +1 -1
- package/dist/glob.js +10 -1
- package/dist/glob.js.map +1 -1
- package/dist/grep.js +4 -0
- package/dist/grep.js.map +1 -1
- package/dist/index.d.ts +13 -2
- package/dist/index.js +730 -268
- package/dist/index.js.map +1 -1
- package/dist/install.js.map +1 -1
- package/dist/json.js.map +1 -1
- package/dist/lint.js.map +1 -1
- package/dist/logs.js.map +1 -1
- package/dist/next-steps.d.ts +66 -0
- package/dist/next-steps.js +116 -0
- package/dist/next-steps.js.map +1 -0
- package/dist/outdated.js.map +1 -1
- package/dist/pack.js +685 -253
- package/dist/pack.js.map +1 -1
- package/dist/patch.js +43 -0
- package/dist/patch.js.map +1 -1
- package/dist/read.js +13 -4
- package/dist/read.js.map +1 -1
- package/dist/replace.js +14 -0
- package/dist/replace.js.map +1 -1
- package/dist/scaffold.js.map +1 -1
- package/dist/test.js.map +1 -1
- package/dist/tool-diff.d.ts +88 -0
- package/dist/tool-diff.js +229 -0
- package/dist/tool-diff.js.map +1 -0
- package/dist/tool-summary.d.ts +24 -0
- package/dist/tool-summary.js +208 -0
- package/dist/tool-summary.js.map +1 -0
- package/dist/tree.js.map +1 -1
- package/dist/typecheck.js.map +1 -1
- package/dist/write.d.ts +6 -0
- package/dist/write.js +118 -19
- package/dist/write.js.map +1 -1
- package/package.json +15 -2
package/dist/edit.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/_util.ts","../src/edit.ts"],"names":["fsp","stat"],"mappings":"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","file":"edit.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\n/**\n * Roots every file tool may always reach, even in restricted mode: the\n * project root and the user-global `~/.wrongstack` directory (config, memory,\n * sessions, skills). `~/.wrongstack` honors the `WRONGSTACK_HOME` override.\n */\nfunction allowedRoots(ctx: Context): string[] {\n return [path.resolve(ctx.projectRoot), path.resolve(Core.wstackGlobalRoot())];\n}\n\n/** True if `target` is `root` itself or nested inside any of `roots`. */\nfunction isInsideAny(target: string, roots: string[]): boolean {\n return roots.some((root) => {\n const rel = path.relative(root, target);\n return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));\n });\n}\n\nexport function ensureInsideRoot(absPath: string, ctx: Context): string {\n const target = path.resolve(absPath);\n // Unrestricted filesystem access: skip the project-root containment check.\n if (ctx.allowOutsideProjectRoot) return target;\n if (isInsideAny(target, allowedRoots(ctx))) return target;\n throw new Error(`Path \"${absPath}\" is outside project root \"${path.resolve(ctx.projectRoot)}\"`);\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 // Unrestricted filesystem access: no symlink-escape check to perform.\n if (ctx.allowOutsideProjectRoot) return;\n // Compare like-for-like against the realpath of each always-allowed root\n // (project root + ~/.wrongstack), since a root may itself be a symlink.\n const realRoots = await Promise.all(\n allowedRoots(ctx).map((r) => fsp.realpath(r).catch(() => path.resolve(r))),\n );\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 if (isInsideAny(real, realRoots)) return;\n throw new Error(\n `Path \"${absPath}\" resolves through a symlink outside project root \"${realRoots[0]}\"`,\n );\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 /* v8 ignore next -- only caller (truncateHeadTail) passes a budget smaller than s; defensive. */\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 /* v8 ignore next -- only caller (truncateHeadTail) passes a budget smaller than s; defensive. */\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 fs from 'node:fs/promises';\nimport type { Tool } from '@wrongstack/core';\nimport {\n atomicWrite,\n detectNewlineStyle,\n normalizeToLf,\n ToolValidationError,\n toStyle,\n unifiedDiff,\n} from '@wrongstack/core';\nimport { safeResolveReal } from './_util.js';\n\ninterface EditInput {\n path: string;\n old_string: string;\n new_string: string;\n /**\n * When true, replaces all occurrences of `old_string`.\n * When false (default), replaces only the first occurrence and errors\n * if more than one match exists — use this to ensure you target the\n * right location.\n */\n replace_all?: boolean | undefined;\n}\n\ninterface EditOutput {\n path: string;\n replacements: number;\n diff: string;\n note?: string | undefined;\n}\n\nexport const editTool: Tool<EditInput, EditOutput> = {\n name: 'edit',\n category: 'Filesystem',\n description:\n 'Perform a precise, surgical text replacement in a file. This is the preferred tool for modifying existing code. ' +\n 'It works best after a prior `read`, but can auto-read the current file when the replacement is still unambiguous. ' +\n 'Fails safely if the `old_string` appears more than once unless `replace_all` is set.',\n usageHint:\n 'RECOMMENDED WORKFLOW:\\n' +\n '1. Prefer calling `read` on the target file first when planning an edit.\\n' +\n '2. Use a sufficiently unique `old_string` (include surrounding lines/context if needed).\\n' +\n '3. If the string appears multiple times and you want to change all of them, set `replace_all: true`.\\n' +\n '4. `new_string` must be the exact replacement text.\\n\\n' +\n 'If no prior read is recorded, the tool auto-reads the current file and only applies the edit after the same ambiguity checks pass.',\n permission: 'confirm',\n mutating: true,\n capabilities: ['fs.write'],\n icon: 'edit',\n timeoutMs: 5_000,\n inputSchema: {\n type: 'object',\n properties: {\n path: { type: 'string' },\n old_string: { type: 'string' },\n new_string: { type: 'string' },\n replace_all: { type: 'boolean' },\n },\n required: ['path', 'old_string', 'new_string'],\n },\n async execute(input, ctx) {\n if (!input?.path) {\n throw new ToolValidationError({ message: 'edit: path is required', field: 'path' });\n }\n if (input.old_string === undefined) {\n throw new ToolValidationError({\n message: 'edit: old_string is required',\n field: 'old_string',\n });\n }\n if (input.new_string === undefined) {\n throw new ToolValidationError({\n message: 'edit: new_string is required',\n field: 'new_string',\n });\n }\n if (input.old_string === '') {\n throw new ToolValidationError({\n message: 'edit: old_string cannot be empty',\n field: 'old_string',\n });\n }\n\n const absPath = await safeResolveReal(input.path, ctx);\n const stat = await fs.stat(absPath).catch((err) => {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') {\n throw new ToolValidationError({\n message: `edit: file \"${input.path}\" does not exist. Use \\`write\\` instead.`,\n field: 'path',\n context: { exists: false },\n });\n }\n throw err;\n });\n if (!stat.isFile()) {\n throw new ToolValidationError({\n message: `edit: \"${input.path}\" is not a regular file`,\n field: 'path',\n });\n }\n\n const autoRead = !ctx.hasRead(absPath);\n // Read BEFORE mtime check to eliminate TOCTOU window.\n // The sequence must be: read content → check mtime → apply edit.\n // If we check mtime first, a concurrent modification between the\n // stat call and the read gives us stale content to search/replace.\n const original = await fs.readFile(absPath, 'utf8');\n const updated = await fs.stat(absPath);\n const mtimeTolerance = process.platform === 'win32' ? 2000 : 1;\n const lastReadMtime = ctx.lastReadMtime(absPath);\n if (lastReadMtime !== undefined && updated.mtimeMs > lastReadMtime + mtimeTolerance) {\n throw new ToolValidationError({\n message: `edit: file \"${input.path}\" was modified externally. Re-read it first.`,\n field: 'path',\n context: { reason: 'external_modification' },\n });\n }\n if (autoRead && updated.mtimeMs > stat.mtimeMs + mtimeTolerance) {\n throw new ToolValidationError({\n message: `edit: file \"${input.path}\" changed while being auto-read. Retry the edit.`,\n field: 'path',\n context: { reason: 'auto_read_race' },\n });\n }\n const autoReadNote = autoRead\n ? `No prior read was recorded for \"${input.path}\"; edit auto-read the current file and applied the replacement only after the ambiguity checks passed.`\n : undefined;\n const style = detectNewlineStyle(original);\n const fileLf = normalizeToLf(original);\n const oldLf = normalizeToLf(input.old_string);\n const newLf = normalizeToLf(input.new_string);\n\n if (oldLf === newLf) {\n if (autoRead) ctx.recordRead(absPath, updated.mtimeMs);\n return {\n path: absPath,\n replacements: 0,\n diff: '(no-op: old and new are identical)',\n note: autoReadNote,\n };\n }\n\n let count = 0;\n let idx = fileLf.indexOf(oldLf);\n const matches: number[] = [];\n while (idx !== -1) {\n matches.push(idx);\n count++;\n idx = fileLf.indexOf(oldLf, idx + 1);\n }\n\n if (count === 0) {\n const hint = findSimilarity(fileLf, oldLf);\n throw new ToolValidationError({\n message: `edit: no match for old_string in \"${input.path}\".${\n hint ? ` Nearest match near line ${hint}.` : ''\n }`,\n field: 'old_string',\n });\n }\n\n if (count > 1 && !input.replace_all) {\n const lines = lineNumbersFor(fileLf, matches);\n throw new ToolValidationError({\n message:\n `edit: old_string matched ${count} times in \"${input.path}\" (lines: ${lines.join(', ')}). ` +\n `Add more context to make it unique, or set replace_all: true.`,\n field: 'old_string',\n context: { occurrences: count },\n });\n }\n\n const newFileLf = input.replace_all\n ? fileLf.split(oldLf).join(newLf)\n : fileLf.replace(oldLf, newLf);\n const newFile = toStyle(newFileLf, style);\n\n await atomicWrite(absPath, newFile, { mode: updated.mode & 0o777 });\n const written = await fs.stat(absPath);\n // Record mtime so a later edit detects external modification, but tag as\n // 'write' so the permission policy's write-smart-bypass does NOT treat\n // this as \"user already saw the content\" (P1 #1).\n ctx.recordRead(absPath, written.mtimeMs, 'write');\n\n // Record for session rewind\n ctx.session.recordFileChange({\n path: absPath,\n action: 'modified',\n before: original,\n after: newFile,\n });\n\n const diff = unifiedDiff(original, newFile, {\n fromFile: input.path,\n toFile: input.path,\n });\n\n return {\n path: absPath,\n replacements: input.replace_all ? count : 1,\n diff,\n note: autoReadNote,\n };\n },\n};\n\nfunction lineNumbersFor(text: string, indices: number[]): number[] {\n const out: number[] = [];\n let pos = 0;\n let line = 1;\n for (const target of indices) {\n while (pos < target) {\n if (text.charCodeAt(pos) === 0x0a) line++;\n pos++;\n }\n out.push(line);\n }\n return out;\n}\n\nfunction findSimilarity(haystack: string, needle: string): number | undefined {\n if (needle.length < 20) return undefined;\n const probe = needle.slice(0, Math.min(40, needle.length));\n const idx = haystack.indexOf(probe);\n if (idx === -1) return undefined;\n let line = 1;\n for (let i = 0; i < idx; i++) {\n if (haystack.charCodeAt(i) === 0x0a) line++;\n }\n return line;\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/_edit-match.ts","../src/_syntax-check.ts","../src/_util.ts","../src/edit.ts"],"names":["path","ts","fsp","stat"],"mappings":"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","file":"edit.js","sourcesContent":["/**\n * Matching ladder for the `edit` tool.\n *\n * Models routinely hallucinate whitespace when reproducing code they read\n * many turns ago: trailing spaces appear, indentation shifts by a level,\n * tabs become spaces. A strict exact-match `old_string` fails on all of\n * these even though the intended target is unambiguous. This module\n * implements a graded fallback chain — each tier is strictly more permissive\n * and strictly lower-confidence than the previous one:\n *\n * 1. `exact` — substring match (the classic behavior).\n * 2. `trailing-whitespace` — whole-line window match ignoring trailing\n * whitespace per line. Zero ambiguity risk\n * beyond exact (trailing blanks never carry\n * meaning in the languages we edit).\n * 3. `whitespace-normalized` — whole-line window match ignoring leading\n * AND trailing whitespace per line. The\n * replacement is re-indented to the file's\n * actual indentation.\n * 4. `fuzzy` — block-anchor match: first and last lines\n * must match (trimmed), interior lines are\n * compared by Levenshtein similarity. Only a\n * single candidate with a clear margin over\n * the runner-up is accepted.\n *\n * The caller (edit.ts) enforces per-tier uniqueness, restricts `replace_all`\n * to tiers 1–2, and surfaces the tier + confidence in the tool output so the\n * model and the session log both see when a fallback fired.\n */\n\nexport type MatchTier = 'exact' | 'trailing-whitespace' | 'whitespace-normalized' | 'fuzzy';\n\nexport interface LadderMatch {\n /** Char offset (inclusive) of the match start in the LF-normalized file. */\n start: number;\n /** Char offset (exclusive) of the match end. */\n end: number;\n /** 1-based line number of the first matched line (for error messages). */\n startLine: number;\n}\n\nexport interface LadderResult {\n tier: MatchTier;\n matches: LadderMatch[];\n /** Similarity score of the best candidate — only set for the fuzzy tier. */\n score?: number | undefined;\n /**\n * Fuzzy tier only: true when two candidates scored within the ambiguity\n * margin of each other, so no single target can be trusted. `matches`\n * then contains the rival candidates for the error message.\n */\n ambiguous?: boolean | undefined;\n}\n\n/** Human-readable label per tier, used in notes and error messages. */\nexport const TIER_LABEL: Record<MatchTier, string> = {\n exact: 'exact match',\n 'trailing-whitespace': 'whitespace-normalized match (trailing whitespace ignored)',\n 'whitespace-normalized':\n 'whitespace-normalized match (indentation ignored, replacement re-indented)',\n fuzzy: 'fuzzy match (block anchors exact, interior ≥90% similar)',\n};\n\n/** Confidence per tier, recorded in the tool output note. */\nexport const TIER_CONFIDENCE: Record<MatchTier, 'exact' | 'high' | 'medium' | 'low'> = {\n exact: 'exact',\n 'trailing-whitespace': 'high',\n 'whitespace-normalized': 'medium',\n fuzzy: 'low',\n};\n\n/** Below this many non-whitespace chars, indent-insensitive tiers are too\n * ambiguous to trust (`}` would match everywhere). */\nconst MIN_NORMALIZED_NEEDLE_CHARS = 16;\n/** Fuzzy interior similarity threshold. */\nconst FUZZY_MIN_SIMILARITY = 0.9;\n/** A fuzzy best-candidate must beat the runner-up by at least this margin. */\nconst FUZZY_AMBIGUITY_MARGIN = 0.05;\n/** Cost guard: skip fuzzy comparison on interiors larger than this. */\nconst FUZZY_MAX_INTERIOR_CHARS = 2000;\n\n/**\n * Walk the ladder and return the first tier that produces at least one\n * match. Returns `undefined` when no tier matches.\n */\nexport function findLadderMatches(fileLf: string, oldLf: string): LadderResult | undefined {\n // Tier 1 — exact substring scan (supports sub-line needles).\n const exact: LadderMatch[] = [];\n let idx = fileLf.indexOf(oldLf);\n while (idx !== -1) {\n exact.push({ start: idx, end: idx + oldLf.length, startLine: lineAt(fileLf, idx) });\n idx = fileLf.indexOf(oldLf, idx + 1);\n }\n if (exact.length > 0) return { tier: 'exact', matches: exact };\n\n // Line-based tiers operate on whole-line windows.\n const fileLines = fileLf.split('\\n');\n const needleLines = oldLf.split('\\n');\n if (needleLines.length > fileLines.length) return undefined;\n const offsets = lineOffsets(fileLines);\n\n const trailing = windowScan(\n fileLines,\n needleLines,\n offsets,\n (a, b) => a.trimEnd() === b.trimEnd(),\n );\n if (trailing.length > 0) return { tier: 'trailing-whitespace', matches: trailing };\n\n const normalizedLen = needleLines.reduce((n, l) => n + l.trim().length, 0);\n if (normalizedLen < MIN_NORMALIZED_NEEDLE_CHARS) return undefined;\n\n const normalized = windowScan(fileLines, needleLines, offsets, (a, b) => a.trim() === b.trim());\n if (normalized.length > 0) return { tier: 'whitespace-normalized', matches: normalized };\n\n return fuzzyScan(fileLines, needleLines, offsets);\n}\n\n/** 1-based line number containing char offset `pos`. */\nfunction lineAt(text: string, pos: number): number {\n let line = 1;\n for (let i = 0; i < pos; i++) {\n if (text.charCodeAt(i) === 0x0a) line++;\n }\n return line;\n}\n\n/** Start char offset of each line in the LF-normalized text. */\nfunction lineOffsets(lines: string[]): number[] {\n const out = new Array<number>(lines.length);\n let pos = 0;\n for (let i = 0; i < lines.length; i++) {\n out[i] = pos;\n pos += (lines[i] as string).length + 1; // +1 for the '\\n'\n }\n return out;\n}\n\nfunction windowToMatch(\n fileLines: string[],\n offsets: number[],\n start: number,\n windowLen: number,\n): LadderMatch {\n const lastLine = start + windowLen - 1;\n return {\n start: offsets[start] as number,\n end: (offsets[lastLine] as number) + (fileLines[lastLine] as string).length,\n startLine: start + 1,\n };\n}\n\n/** Slide a needle-sized window over the file lines with a per-line comparator.\n * Matched windows never overlap (scan resumes after the window). */\nfunction windowScan(\n fileLines: string[],\n needleLines: string[],\n offsets: number[],\n eq: (fileLine: string, needleLine: string) => boolean,\n): LadderMatch[] {\n const n = needleLines.length;\n const out: LadderMatch[] = [];\n for (let i = 0; i + n <= fileLines.length; i++) {\n let all = true;\n for (let j = 0; j < n; j++) {\n if (!eq(fileLines[i + j] as string, needleLines[j] as string)) {\n all = false;\n break;\n }\n }\n if (all) {\n out.push(windowToMatch(fileLines, offsets, i, n));\n i += n - 1; // no overlapping windows\n }\n }\n return out;\n}\n\n/** Block-anchor fuzzy tier: first/last lines trim-exact, interior by\n * Levenshtein similarity. Requires ≥3 needle lines so anchors exist. */\nfunction fuzzyScan(\n fileLines: string[],\n needleLines: string[],\n offsets: number[],\n): LadderResult | undefined {\n const n = needleLines.length;\n if (n < 3) return undefined;\n const firstNeedle = (needleLines[0] as string).trim();\n const lastNeedle = (needleLines[n - 1] as string).trim();\n const needleInterior = needleLines\n .slice(1, -1)\n .map((l) => l.trim())\n .join('\\n');\n if (needleInterior.length > FUZZY_MAX_INTERIOR_CHARS) return undefined;\n\n const candidates: Array<{ match: LadderMatch; score: number }> = [];\n for (let i = 0; i + n <= fileLines.length; i++) {\n if ((fileLines[i] as string).trim() !== firstNeedle) continue;\n if ((fileLines[i + n - 1] as string).trim() !== lastNeedle) continue;\n const windowInterior = fileLines\n .slice(i + 1, i + n - 1)\n .map((l) => l.trim())\n .join('\\n');\n if (windowInterior.length > FUZZY_MAX_INTERIOR_CHARS) continue;\n const score = similarity(needleInterior, windowInterior);\n if (score >= FUZZY_MIN_SIMILARITY) {\n candidates.push({ match: windowToMatch(fileLines, offsets, i, n), score });\n }\n }\n\n if (candidates.length === 0) return undefined;\n candidates.sort((a, b) => b.score - a.score);\n const best = candidates[0] as { match: LadderMatch; score: number };\n const runnerUp = candidates[1];\n if (runnerUp && best.score - runnerUp.score < FUZZY_AMBIGUITY_MARGIN) {\n return {\n tier: 'fuzzy',\n matches: candidates.map((c) => c.match),\n score: best.score,\n ambiguous: true,\n };\n }\n return { tier: 'fuzzy', matches: [best.match], score: best.score };\n}\n\n/** Normalized similarity in [0, 1]: 1 − levenshtein / max-length. */\nexport function similarity(a: string, b: string): number {\n if (a === b) return 1;\n const max = Math.max(a.length, b.length);\n if (max === 0) return 1;\n // Quick reject: a length gap larger than the allowed edit budget can\n // never reach the threshold — skip the O(n·m) DP entirely.\n if (Math.abs(a.length - b.length) / max > 1 - FUZZY_MIN_SIMILARITY + 0.05) {\n return 1 - Math.abs(a.length - b.length) / max;\n }\n return 1 - levenshtein(a, b) / max;\n}\n\n/** Two-row Levenshtein distance. */\nfunction levenshtein(a: string, b: string): number {\n if (a.length === 0) return b.length;\n if (b.length === 0) return a.length;\n let prev = new Array<number>(b.length + 1);\n let cur = new Array<number>(b.length + 1);\n for (let j = 0; j <= b.length; j++) prev[j] = j;\n for (let i = 1; i <= a.length; i++) {\n cur[0] = i;\n const ca = a.charCodeAt(i - 1);\n for (let j = 1; j <= b.length; j++) {\n const cost = ca === b.charCodeAt(j - 1) ? 0 : 1;\n cur[j] = Math.min(\n (prev[j] as number) + 1,\n (cur[j - 1] as number) + 1,\n (prev[j - 1] as number) + cost,\n );\n }\n [prev, cur] = [cur, prev];\n }\n return prev[b.length] as number;\n}\n\n/** Leading whitespace (spaces/tabs) of the first non-empty line, or ''. */\nexport function firstLineIndent(text: string): string {\n for (const line of text.split('\\n')) {\n if (line.trim() === '') continue;\n return (/^[ \\t]*/.exec(line) as RegExpExecArray)[0];\n }\n return '';\n}\n\n/**\n * Shift `newLf` from the needle's indentation to the matched block's actual\n * indentation. Only clean prefix relationships are adjusted (purely adding\n * or removing leading whitespace); a tabs-vs-spaces mismatch is left alone\n * rather than guessed at — the diff in the tool output makes the result\n * visible either way.\n */\nexport function adjustIndent(\n newLf: string,\n fromIndent: string,\n toIndent: string,\n): { text: string; adjusted: boolean } {\n if (fromIndent === toIndent) return { text: newLf, adjusted: false };\n const lines = newLf.split('\\n');\n if (toIndent.startsWith(fromIndent)) {\n const extra = toIndent.slice(fromIndent.length);\n return {\n text: lines.map((l) => (l.trim() === '' ? l : extra + l)).join('\\n'),\n adjusted: true,\n };\n }\n if (fromIndent.startsWith(toIndent)) {\n const remove = fromIndent.slice(toIndent.length);\n return {\n text: lines.map((l) => (l.startsWith(remove) ? l.slice(remove.length) : l)).join('\\n'),\n adjusted: true,\n };\n }\n return { text: newLf, adjusted: false };\n}\n\n/**\n * Best-effort locator for the no-match error: find the window whose trimmed\n * lines are most similar to the needle (cheap common-prefix scoring — this\n * runs on the failure path where the needle by definition doesn't match, so\n * precision matters less than never being slow). Returns the 1-based line\n * and a short snippet the model can use to correct itself without re-reading.\n */\nexport function nearestMatchHint(\n fileLf: string,\n oldLf: string,\n): { line: number; snippet: string } | undefined {\n const fileLines = fileLf.split('\\n');\n const needleLines = oldLf.split('\\n');\n if (needleLines.length > fileLines.length) return undefined;\n\n const needleTrimmed = needleLines.map((l) => l.trim());\n let bestScore = 0;\n let bestStart = -1;\n for (let i = 0; i + needleLines.length <= fileLines.length; i++) {\n let sum = 0;\n for (let j = 0; j < needleLines.length; j++) {\n sum += prefixSimilarity(needleTrimmed[j] as string, (fileLines[i + j] as string).trim());\n }\n const score = sum / needleLines.length;\n if (score > bestScore) {\n bestScore = score;\n bestStart = i;\n }\n }\n if (bestStart === -1 || bestScore < 0.5) return undefined;\n\n const snippet = fileLines\n .slice(bestStart, bestStart + Math.min(3, needleLines.length))\n .map((l) => (l.length > 120 ? `${l.slice(0, 120)}…` : l))\n .join('\\n');\n return { line: bestStart + 1, snippet };\n}\n\n/** Cheap similarity proxy: shared prefix length over max length. */\nfunction prefixSimilarity(a: string, b: string): number {\n if (a === b) return 1;\n const max = Math.max(a.length, b.length);\n if (max === 0) return 1;\n let p = 0;\n const lim = Math.min(a.length, b.length);\n while (p < lim && a.charCodeAt(p) === b.charCodeAt(p)) p++;\n return p / max;\n}\n","/**\n * Post-edit syntax validation for `edit` and `write`.\n *\n * After a mutation lands on disk, the new content is parsed (TS/JS via the\n * TypeScript compiler's parser, JSON via JSON.parse with a JSONC fallback)\n * and any parse errors are surfaced in the tool output. The edit is NOT\n * rolled back — reverting would desynchronize the model's picture of the\n * file — but the errors come back in the same turn so the model fixes the\n * breakage before the user ever opens a broken file.\n *\n * The check is purely syntactic (no type checking, no project program), so\n * it costs single-digit milliseconds per file. Semantic type errors remain\n * the `type-gate` plugin's job.\n */\nimport * as path from 'node:path';\n\nconst TS_LIKE = new Set(['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs']);\n/** Skip pathologically large files — the model never writes these in one go. */\nconst MAX_CHECK_CHARS = 1_500_000;\n/** Cap reported errors so a mangled file doesn't flood the context. */\nconst MAX_ERRORS = 5;\n\ntype Ts = typeof import('typescript');\n\nlet tsLoad: Promise<Ts | null> | null = null;\n/** Lazy-load the TypeScript compiler (already a dependency via the codebase\n * indexer) so tools that never touch TS files don't pay its import cost. */\nfunction loadTypescript(): Promise<Ts | null> {\n tsLoad ??= import('typescript').then(\n (m) => ((m as unknown as { default?: Ts }).default ?? m) as Ts,\n () => null,\n );\n return tsLoad;\n}\n\nexport interface SyntaxCheckResult {\n /** Parse errors in the new content (capped at {@link MAX_ERRORS}). */\n errors: string[];\n /** True when the pre-edit content already failed to parse — the edit did\n * not introduce the breakage (though it didn't fix it either). */\n preExisting: boolean;\n}\n\n/**\n * Check `content` for syntax errors based on the file extension.\n * Returns `undefined` when the file type is not checkable (or the checker\n * is unavailable) and a result with an empty `errors` array when clean.\n * Pass `previousContent` to distinguish newly-introduced errors from\n * pre-existing ones.\n */\nexport async function checkSyntax(\n filePath: string,\n content: string,\n previousContent?: string,\n): Promise<SyntaxCheckResult | undefined> {\n const errors = await parseErrors(filePath, content);\n if (errors === undefined) return undefined;\n if (errors.length === 0) return { errors, preExisting: false };\n let preExisting = false;\n if (previousContent !== undefined) {\n const prevErrors = await parseErrors(filePath, previousContent);\n preExisting = prevErrors !== undefined && prevErrors.length > 0;\n }\n return { errors, preExisting };\n}\n\n/** Files that are conventionally JSONC (comments + trailing commas allowed). */\nfunction isJsoncFile(filePath: string): boolean {\n const base = path.basename(filePath).toLowerCase();\n if (base.endsWith('.jsonc')) return true;\n if (/^(tsconfig|jsconfig)([.-].*)?\\.json$/.test(base)) return true;\n const dir = path.basename(path.dirname(filePath)).toLowerCase();\n return dir === '.vscode';\n}\n\nasync function parseErrors(filePath: string, content: string): Promise<string[] | undefined> {\n if (content.length > MAX_CHECK_CHARS) return undefined;\n const ext = path.extname(filePath).toLowerCase();\n\n if (ext === '.json' || ext === '.jsonc') {\n try {\n JSON.parse(content);\n return [];\n } catch (err) {\n // Known-JSONC files (tsconfig.json, VS Code settings, *.jsonc) accept\n // comments and trailing commas — re-parse with the TS config parser\n // before declaring them broken. Plain .json stays strict: a trailing\n // comma there IS an error the model should fix.\n if (isJsoncFile(filePath)) {\n const ts = await loadTypescript();\n if (ts) {\n const jsonc = ts.parseConfigFileTextToJson(filePath, content);\n if (!jsonc.error) return [];\n return [formatDiag(ts, jsonc.error, content)];\n }\n }\n return [`JSON parse error: ${(err as Error).message}`];\n }\n }\n\n if (!TS_LIKE.has(ext)) return undefined;\n const ts = await loadTypescript();\n if (!ts) return undefined;\n\n const scriptKind =\n ext === '.tsx'\n ? ts.ScriptKind.TSX\n : ext === '.ts' || ext === '.mts' || ext === '.cts'\n ? ts.ScriptKind.TS\n : // Plain JS may legitimately contain JSX; the JSX grammar is a\n // superset for untyped code, so parsing .js/.jsx as JSX avoids\n // false positives on React files.\n ts.ScriptKind.JSX;\n\n const sourceFile = ts.createSourceFile(\n path.basename(filePath),\n content,\n ts.ScriptTarget.Latest,\n /* setParentNodes */ false,\n scriptKind,\n );\n // parseDiagnostics is not in the public .d.ts but has been the stable\n // home of the parser's syntactic diagnostics for a decade (the public\n // alternative, program.getSyntacticDiagnostics, needs a full Program).\n const diags =\n (sourceFile as unknown as { parseDiagnostics?: import('typescript').Diagnostic[] })\n .parseDiagnostics ?? [];\n return diags.slice(0, MAX_ERRORS).map((d) => formatDiag(ts, d, content, sourceFile));\n}\n\nfunction formatDiag(\n ts: Ts,\n diag: import('typescript').Diagnostic,\n content: string,\n sourceFile?: import('typescript').SourceFile,\n): string {\n const message = ts.flattenDiagnosticMessageText(diag.messageText, ' ');\n if (diag.start === undefined) return message;\n let line: number;\n if (sourceFile) {\n line = sourceFile.getLineAndCharacterOfPosition(diag.start).line + 1;\n } else {\n line = 1;\n for (let i = 0; i < diag.start && i < content.length; i++) {\n if (content.charCodeAt(i) === 0x0a) line++;\n }\n }\n return `line ${line}: ${message}`;\n}\n","import { createHash } from 'node:crypto';\nimport * as fsp from 'node:fs/promises';\nimport * as path from 'node:path';\nimport * as Core from '@wrongstack/core';\nimport type { Context } from '@wrongstack/core';\n\n/**\n * sha-256 hex of a UTF-8 string. Used by the file tools to record a content\n * hash alongside the mtime in `ctx.recordRead` — the hash is the authoritative\n * staleness arbiter for `edit` (mtime has a 2 s tolerance window on Windows).\n */\nexport function sha256hex(content: string): string {\n return createHash('sha256').update(content, 'utf8').digest('hex');\n}\n/** Detected package manager for a project directory. */\nexport type PackageManager = 'pnpm' | 'yarn' | 'npm';\n\n/**\n * Detect the project's package manager by inspecting lockfiles in `cwd`.\n * Order: pnpm → yarn → npm (default). Missing or unreadable directories fall\n * back to `npm` rather than throwing, so a `safeResolve`-checked cwd that\n * happens to be empty never aborts the tool.\n */\nexport async function detectPackageManager(cwd: string): Promise<PackageManager> {\n const { stat } = await import('node:fs/promises');\n try {\n await stat(`${cwd}/pnpm-lock.yaml`);\n return 'pnpm';\n } catch {\n /* not pnpm */\n }\n try {\n await stat(`${cwd}/yarn.lock`);\n return 'yarn';\n } catch {\n /* not yarn */\n }\n return 'npm';\n}\n\nexport function resolvePath(input: string, ctx: Context): string {\n return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.workingDir ?? ctx.cwd, input);\n}\n\n/**\n * Roots every file tool may always reach, even in restricted mode: the\n * project root and the user-global `~/.wrongstack` directory (config, memory,\n * sessions, skills). `~/.wrongstack` honors the `WRONGSTACK_HOME` override.\n */\nfunction allowedRoots(ctx: Context): string[] {\n return [path.resolve(ctx.projectRoot), path.resolve(Core.wstackGlobalRoot())];\n}\n\n/** True if `target` is `root` itself or nested inside any of `roots`. */\nfunction isInsideAny(target: string, roots: string[]): boolean {\n return roots.some((root) => {\n const rel = path.relative(root, target);\n return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));\n });\n}\n\nexport function ensureInsideRoot(absPath: string, ctx: Context): string {\n const target = path.resolve(absPath);\n // Unrestricted filesystem access: skip the project-root containment check.\n if (ctx.allowOutsideProjectRoot) return target;\n if (isInsideAny(target, allowedRoots(ctx))) return target;\n throw new Error(`Path \"${absPath}\" is outside project root \"${path.resolve(ctx.projectRoot)}\"`);\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 // Unrestricted filesystem access: no symlink-escape check to perform.\n if (ctx.allowOutsideProjectRoot) return;\n // Compare like-for-like against the realpath of each always-allowed root\n // (project root + ~/.wrongstack), since a root may itself be a symlink.\n const realRoots = await Promise.all(\n allowedRoots(ctx).map((r) => fsp.realpath(r).catch(() => path.resolve(r))),\n );\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 if (isInsideAny(real, realRoots)) return;\n throw new Error(\n `Path \"${absPath}\" resolves through a symlink outside project root \"${realRoots[0]}\"`,\n );\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 /* v8 ignore next -- only caller (truncateHeadTail) passes a budget smaller than s; defensive. */\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 /* v8 ignore next -- only caller (truncateHeadTail) passes a budget smaller than s; defensive. */\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 fs from 'node:fs/promises';\nimport type { Tool } from '@wrongstack/core';\nimport {\n atomicWrite,\n detectNewlineStyle,\n normalizeToLf,\n ToolValidationError,\n toStyle,\n unifiedDiff,\n} from '@wrongstack/core';\nimport {\n adjustIndent,\n findLadderMatches,\n firstLineIndent,\n type MatchTier,\n nearestMatchHint,\n TIER_CONFIDENCE,\n TIER_LABEL,\n} from './_edit-match.js';\nimport { checkSyntax } from './_syntax-check.js';\nimport { safeResolveReal, sha256hex } from './_util.js';\n\ninterface EditInput {\n path: string;\n old_string: string;\n new_string: string;\n /**\n * When true, replaces all occurrences of `old_string`.\n * When false (default), replaces only the first occurrence and errors\n * if more than one match exists — use this to ensure you target the\n * right location.\n */\n replace_all?: boolean | undefined;\n}\n\ninterface EditOutput {\n path: string;\n replacements: number;\n diff: string;\n /**\n * How `old_string` was located. Anything other than 'exact' means a\n * whitespace/fuzzy fallback fired — the note carries the confidence level\n * and the diff shows exactly what was replaced.\n */\n matched_by: MatchTier;\n /**\n * Parse errors found in the file AFTER the edit was applied (TS/JS/JSON\n * only). The edit is still on disk — fix these in a follow-up edit now,\n * before doing anything else.\n */\n syntax_errors?: string[] | undefined;\n note?: string | undefined;\n}\n\nexport const editTool: Tool<EditInput, EditOutput> = {\n name: 'edit',\n category: 'Filesystem',\n description:\n 'Perform a precise, surgical text replacement in a file. This is the preferred tool for modifying existing code. ' +\n 'It works best after a prior `read`, but can auto-read the current file when the replacement is still unambiguous. ' +\n 'Fails safely if the `old_string` appears more than once unless `replace_all` is set.',\n usageHint:\n 'RECOMMENDED WORKFLOW:\\n' +\n '1. Prefer calling `read` on the target file first when planning an edit.\\n' +\n '2. Use a sufficiently unique `old_string` (include surrounding lines/context if needed).\\n' +\n '3. If the string appears multiple times and you want to change all of them, set `replace_all: true`.\\n' +\n '4. `new_string` must be the exact replacement text.\\n\\n' +\n 'If no prior read is recorded, the tool auto-reads the current file and only applies the edit after the same ambiguity checks pass.\\n' +\n 'If `old_string` differs from the file only in whitespace (trailing spaces, indentation), a lower-confidence fallback match is applied and reported in `matched_by`/`note` — always verify the diff when this happens.\\n' +\n 'After the edit, TS/JS/JSON files are syntax-checked; if `syntax_errors` is present in the output, fix those errors immediately with a follow-up edit.',\n selection: {\n doNotUseWhen: 'creating a new file, replacing the whole file, or applying an existing unified diff.',\n useInstead: ['write', 'patch'],\n },\n permission: 'confirm',\n mutating: true,\n capabilities: ['fs.write'],\n icon: 'edit',\n timeoutMs: 5_000,\n inputSchema: {\n type: 'object',\n properties: {\n path: { type: 'string' },\n old_string: { type: 'string' },\n new_string: { type: 'string' },\n replace_all: { type: 'boolean' },\n },\n required: ['path', 'old_string', 'new_string'],\n },\n async execute(input, ctx, opts) {\n if (!input?.path) {\n throw new ToolValidationError({ message: 'edit: path is required', field: 'path' });\n }\n if (input.old_string === undefined) {\n throw new ToolValidationError({\n message: 'edit: old_string is required',\n field: 'old_string',\n });\n }\n if (input.new_string === undefined) {\n throw new ToolValidationError({\n message: 'edit: new_string is required',\n field: 'new_string',\n });\n }\n if (input.old_string === '') {\n throw new ToolValidationError({\n message: 'edit: old_string cannot be empty',\n field: 'old_string',\n });\n }\n\n const absPath = await safeResolveReal(input.path, ctx);\n const stat = await fs.stat(absPath).catch((err) => {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') {\n throw new ToolValidationError({\n message: `edit: file \"${input.path}\" does not exist. Use \\`write\\` instead.`,\n field: 'path',\n context: { exists: false },\n });\n }\n throw err;\n });\n if (!stat.isFile()) {\n throw new ToolValidationError({\n message: `edit: \"${input.path}\" is not a regular file`,\n field: 'path',\n });\n }\n\n const autoRead = !ctx.hasRead(absPath);\n // Read BEFORE mtime check to eliminate TOCTOU window.\n // The sequence must be: read content → check mtime → apply edit.\n // If we check mtime first, a concurrent modification between the\n // stat call and the read gives us stale content to search/replace.\n const original = await fs.readFile(absPath, 'utf8');\n const updated = await fs.stat(absPath);\n const mtimeTolerance = process.platform === 'win32' ? 2000 : 1;\n const originalHash = sha256hex(original);\n // Optional call: embedders and older test fixtures may hand in a\n // duck-typed Context without hash tracking — they fall back to mtime.\n const lastReadHash = ctx.lastReadHash?.(absPath);\n if (lastReadHash !== undefined) {\n // Content hash is the authoritative arbiter when available: it closes\n // the mtime tolerance window (an external write within 2 s of the read\n // on Windows is invisible to mtime) and ignores content-preserving\n // mtime bumps (touch, checkout of identical content).\n if (lastReadHash !== originalHash) {\n throw new ToolValidationError({\n message: `edit: file \"${input.path}\" was modified externally. Re-read it first.`,\n field: 'path',\n context: { reason: 'external_modification' },\n });\n }\n } else {\n const lastReadMtime = ctx.lastReadMtime(absPath);\n if (lastReadMtime !== undefined && updated.mtimeMs > lastReadMtime + mtimeTolerance) {\n throw new ToolValidationError({\n message: `edit: file \"${input.path}\" was modified externally. Re-read it first.`,\n field: 'path',\n context: { reason: 'external_modification' },\n });\n }\n }\n if (autoRead && updated.mtimeMs > stat.mtimeMs + mtimeTolerance) {\n throw new ToolValidationError({\n message: `edit: file \"${input.path}\" changed while being auto-read. Retry the edit.`,\n field: 'path',\n context: { reason: 'auto_read_race' },\n });\n }\n const autoReadNote = autoRead\n ? `No prior read was recorded for \"${input.path}\"; edit auto-read the current file and applied the replacement only after the ambiguity checks passed.`\n : undefined;\n const style = detectNewlineStyle(original);\n const fileLf = normalizeToLf(original);\n const oldLf = normalizeToLf(input.old_string);\n const newLf = normalizeToLf(input.new_string);\n\n if (oldLf === newLf) {\n if (autoRead) ctx.recordRead(absPath, updated.mtimeMs, 'user', originalHash);\n return {\n path: absPath,\n replacements: 0,\n diff: '(no-op: old and new are identical)',\n matched_by: 'exact',\n note: autoReadNote,\n };\n }\n\n const ladder = findLadderMatches(fileLf, oldLf);\n\n if (!ladder) {\n const hint = nearestMatchHint(fileLf, oldLf);\n throw new ToolValidationError({\n message: `edit: no match for old_string in \"${input.path}\".${\n hint\n ? ` Nearest match near line ${hint.line}:\\n${hint.snippet}\\n` +\n `Compare this against your old_string and retry with the file's actual text.`\n : ''\n }`,\n field: 'old_string',\n });\n }\n\n const { tier, matches } = ladder;\n const count = matches.length;\n\n if (ladder.ambiguous) {\n const lines = matches.map((m) => m.startLine);\n throw new ToolValidationError({\n message:\n `edit: old_string only matched fuzzily and ${count} candidate blocks scored too close ` +\n `to distinguish (lines: ${lines.join(', ')}) in \"${input.path}\". ` +\n `Re-read the file and use the exact text of the intended block.`,\n field: 'old_string',\n context: { occurrences: count, matchTier: tier },\n });\n }\n\n // Fuzzy/indent-insensitive matches are single-target operations: applying\n // them in bulk multiplies a low-confidence guess. replace_all therefore\n // requires at least a trailing-whitespace-level match.\n if (input.replace_all && tier !== 'exact' && tier !== 'trailing-whitespace') {\n throw new ToolValidationError({\n message:\n `edit: old_string only matched via ${TIER_LABEL[tier]} in \"${input.path}\", ` +\n `but replace_all requires an exact (or trailing-whitespace) match. ` +\n `Re-read the file and use its exact text.`,\n field: 'old_string',\n context: { matchTier: tier },\n });\n }\n\n if (count > 1 && !input.replace_all) {\n const lines = matches.map((m) => m.startLine);\n throw new ToolValidationError({\n message:\n `edit: old_string matched ${count} times in \"${input.path}\" (lines: ${lines.join(', ')})` +\n `${tier === 'exact' ? '' : ` via ${TIER_LABEL[tier]}`}. ` +\n `Add more context to make it unique, or set replace_all: true.`,\n field: 'old_string',\n context: { occurrences: count, matchTier: tier },\n });\n }\n\n let newFileLf: string;\n let tierNote: string | undefined;\n if (tier === 'exact') {\n newFileLf = input.replace_all\n ? fileLf.split(oldLf).join(newLf)\n : fileLf.replace(oldLf, newLf);\n } else {\n // Fallback tiers matched whole-line windows — splice by char offsets,\n // right to left so earlier offsets stay valid. For indent-insensitive\n // tiers, shift the replacement to the file's actual indentation.\n let replacement = newLf;\n let indentSuffix = '';\n if (tier === 'whitespace-normalized' || tier === 'fuzzy') {\n const first = matches[0] as (typeof matches)[number];\n const matchedText = fileLf.slice(first.start, first.end);\n const adjusted = adjustIndent(newLf, firstLineIndent(oldLf), firstLineIndent(matchedText));\n replacement = adjusted.text;\n if (adjusted.adjusted) indentSuffix = '; replacement re-indented to match the file';\n }\n newFileLf = fileLf;\n const applied = input.replace_all ? matches : matches.slice(0, 1);\n for (let i = applied.length - 1; i >= 0; i--) {\n const m = applied[i] as (typeof matches)[number];\n newFileLf = newFileLf.slice(0, m.start) + replacement + newFileLf.slice(m.end);\n }\n const scoreSuffix =\n ladder.score !== undefined ? `, similarity ${(ladder.score * 100).toFixed(1)}%` : '';\n tierNote =\n `old_string did not match exactly; applied via ${TIER_LABEL[tier]} at line ` +\n `${(matches[0] as (typeof matches)[number]).startLine} ` +\n `(confidence: ${TIER_CONFIDENCE[tier]}${scoreSuffix}${indentSuffix}). ` +\n `Review the diff to confirm the intended target was edited.`;\n }\n const newFile = toStyle(newFileLf, style);\n\n // Last exit before mutating the filesystem: a run aborted during the\n // read/match phase must not leave the edit behind. (atomicWrite itself\n // is all-or-nothing — the file is old or new, never partial.)\n opts?.signal?.throwIfAborted();\n await atomicWrite(absPath, newFile, { mode: updated.mode & 0o777 });\n const written = await fs.stat(absPath);\n // Record mtime + content hash so a later edit detects external\n // modification, but tag as 'write' so the permission policy's\n // write-smart-bypass does NOT treat this as \"user already saw the\n // content\" (P1 #1).\n ctx.recordRead(absPath, written.mtimeMs, 'write', sha256hex(newFile));\n\n // Record for session rewind\n ctx.session.recordFileChange({\n path: absPath,\n action: 'modified',\n before: original,\n after: newFile,\n });\n\n const diff = unifiedDiff(original, newFile, {\n fromFile: input.path,\n toFile: input.path,\n });\n\n // Post-edit syntax validation (TS/JS/JSON). The edit stays on disk —\n // errors come back in the same turn so the model fixes them immediately.\n const syntax = await checkSyntax(absPath, newFile, original).catch(() => undefined);\n let syntaxNote: string | undefined;\n if (syntax && syntax.errors.length > 0) {\n syntaxNote = syntax.preExisting\n ? `Syntax check: the file still has parse errors (they pre-date this edit) — see syntax_errors.`\n : `Syntax check: this edit introduced ${syntax.errors.length} parse error(s) — fix them now, see syntax_errors.`;\n }\n\n const notes = [autoReadNote, tierNote, syntaxNote].filter(Boolean);\n\n return {\n path: absPath,\n replacements: input.replace_all ? count : 1,\n diff,\n matched_by: tier,\n syntax_errors: syntax && syntax.errors.length > 0 ? syntax.errors : undefined,\n note: notes.length > 0 ? notes.join('\\n') : undefined,\n };\n },\n};\n"]}
|
|
@@ -23,7 +23,7 @@ import { Tool } from '@wrongstack/core';
|
|
|
23
23
|
*
|
|
24
24
|
* Caution rules are deliberately permissive — they execute and emit a
|
|
25
25
|
* warning rather than blocking. The rationale: many of these patterns
|
|
26
|
-
* (python -c, sudo
|
|
26
|
+
* (python -c, sudo) are part of legitimate dev workflows,
|
|
27
27
|
* so a hard deny would block too much. A warning gives the user a
|
|
28
28
|
* chance to notice "wait, I didn't mean to do that" without forcing
|
|
29
29
|
* them to add a config override for every script.
|
package/dist/exec.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import '@wrongstack/core';
|
|
2
|
-
export { c as configureDangerBypass, b as configureExecPolicy, e as execTool, g as getDangerBypass, f as getExecAllowlist, i as isExecCommandAllowed, r as resetDangerBypass, h as resetExecPolicy } from './exec-
|
|
2
|
+
export { c as configureDangerBypass, b as configureExecPolicy, e as execTool, g as getDangerBypass, f as getExecAllowlist, i as isExecCommandAllowed, r as resetDangerBypass, h as resetExecPolicy } from './exec-2OKoT6tk.js';
|
package/dist/exec.js
CHANGED
|
@@ -1064,23 +1064,18 @@ var RULES = [
|
|
|
1064
1064
|
},
|
|
1065
1065
|
reason: "inline script evaluation (-c / -e / --eval)"
|
|
1066
1066
|
},
|
|
1067
|
-
// ----- pipe-to-shell (
|
|
1067
|
+
// ----- pipe-to-shell (destructive — download-and-run pattern) -----
|
|
1068
1068
|
// The classic `curl https://... | sh` download-and-run vector. Detected by
|
|
1069
|
-
// looking for a known fetcher
|
|
1070
|
-
//
|
|
1071
|
-
//
|
|
1069
|
+
// looking for a known fetcher piped into a shell or expression evaluator.
|
|
1070
|
+
// A shell command passed through `bash -c` / `pwsh -Command` arrives as one
|
|
1071
|
+
// argv string, so scan the reconstructed argv text rather than individual
|
|
1072
|
+
// tokens only.
|
|
1072
1073
|
{
|
|
1073
1074
|
id: "pipe-to-shell",
|
|
1074
|
-
level: "
|
|
1075
|
-
test: (
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
);
|
|
1079
|
-
const hasShellSink = args.some(
|
|
1080
|
-
(a) => a === "sh" || a === "bash" || a === "zsh" || a === "fish" || a === "pwsh" || a === "powershell" || a.endsWith("/sh") || a.endsWith("/bash") || a.endsWith("/zsh") || a.endsWith("/pwsh")
|
|
1081
|
-
);
|
|
1082
|
-
return hasFetcher && hasShellSink;
|
|
1083
|
-
},
|
|
1075
|
+
level: "destructive",
|
|
1076
|
+
test: (cmd, args) => /\b(?:curl|wget|fetch|httpie|http|irm|iwr|Invoke-WebRequest|Invoke-RestMethod)\b[\s\S]{0,300}\|\s*(?:sudo\s+)?(?:sh|bash|zsh|fish|pwsh|powershell|iex|Invoke-Expression)\b/i.test(
|
|
1077
|
+
[cmd, ...args].join(" ")
|
|
1078
|
+
),
|
|
1084
1079
|
reason: "network fetch piped to a shell (download-and-run pattern)"
|
|
1085
1080
|
},
|
|
1086
1081
|
// ----- privilege escalation (caution) -----
|
|
@@ -1871,6 +1866,10 @@ var execTool = {
|
|
|
1871
1866
|
category: "Shell",
|
|
1872
1867
|
description: "Execute a **whitelisted, restricted set of commands** with strict argument validation. This is the **preferred and safer** alternative to the `bash` tool for running development tools (node, npm, pnpm, tsc, git, tests, linters, etc.). It prevents arbitrary command injection and limits what the model can do.",
|
|
1873
1868
|
usageHint: "PREFERRED SHELL TOOL for most cases.\n\nUse this instead of `bash` whenever possible.\n- `command` must be in the allowlist. Defaults cover JS (node/npm/pnpm/yarn/bun/deno/tsc/vitest/eslint/biome), Go (`go build`/`go test`), Rust (cargo), Python (python/pip), Ruby (gem/bundle), JVM (java/mvn/gradle), .NET (dotnet), native (make/cmake), and git. Users can extend it via `tools.exec.allow` in config.\n- Arguments are passed as a clean array (no shell interpretation).\n- `cwd` is validated to stay inside the project.\n- If a command is not allowlisted, the error explains how to add it; for one-off arbitrary commands, fall back to `bash` (with strong justification).\nThis tool significantly reduces the risk compared to full shell access.",
|
|
1869
|
+
selection: {
|
|
1870
|
+
doNotUseWhen: "the operation requires pipes, redirection, shell expansion, or a non-allowlisted command.",
|
|
1871
|
+
useInstead: ["bash"]
|
|
1872
|
+
},
|
|
1874
1873
|
permission: "confirm",
|
|
1875
1874
|
mutating: true,
|
|
1876
1875
|
riskTier: "standard",
|