// Generated by typebulb — do not edit; overwritten on each typebulb run. // cli/agents/pi/server/piPatcherExtension.ts import { existsSync, readFileSync, writeFileSync } from "node:fs"; import { resolve } from "node:path"; import { Type } from "typebox"; // ../node_modules/.pnpm/matchu-patchu@0.3.3/node_modules/matchu-patchu/dist/utils/textUtils.js var TextUtils = class _TextUtils { static NormalizeToLf(text) { return (text ?? "").replace(/\r\n/g, "\n").replace(/\r/g, "\n"); } static DetectNewline(text) { if (!text || text.length === 0) return "\n"; const idx = text.search(/[\r\n]/); if (idx < 0) return "\n"; return text[idx] === "\r" ? idx + 1 < text.length && text[idx + 1] === "\n" ? "\r\n" : "\r" : "\n"; } /// Normalize + split to LF-only lines. static ToLines(text) { return _TextUtils.NormalizeToLf(text).split("\n"); } /// Count of leading characters drawn from `chars` (default: space or tab). static CountLeadingWhitespace(s, chars = " ") { let i = 0; while (i < s.length && chars.includes(s[i])) i++; return i; } /// Maps chat-layer typography (smart quotes, Unicode dashes, special /// spaces) to ASCII. Match-only: output text is never built from this. /// Numeric code points throughout: invisible/lookalike literals would make this /// file unmatchable by byte-exact edit tools, including this patcher's own MCP. static NormalizeHomoglyphs(line) { return [...line].some((c) => c.charCodeAt(0) >= 160) ? [...line].map((c) => _TextUtils.MapHomoglyph(c)).join("") : line; } static MapHomoglyph(c) { const cp = c.codePointAt(0); if (cp >= 8216 && cp <= 8219) return "'"; if (cp >= 8220 && cp <= 8223) return '"'; if (cp >= 8208 && cp <= 8213 || cp === 8722) return "-"; if (cp === 160 || cp >= 8194 && cp <= 8202 || cp === 8239 || cp === 8287 || cp === 12288) return " "; return c; } /// Removes zero-width/control code points for match-tolerant /// comparison. Match-only, like NormalizeHomoglyphs: output text is never /// built from this. static StripInvisibles(line) { return [...line].some((c) => _TextUtils.IsInvisible(c)) ? [...line].filter((c) => !_TextUtils.IsInvisible(c)).join("") : line; } /// Folds NFC canonical equivalence and fullwidth ASCII forms /// (U+FF01..U+FF5E) for match-tolerant comparison. Match-only, like /// NormalizeHomoglyphs. The NFKC compat remainder (ligatures, superscripts) /// stays unfolded: visually distinct content must not cross-match. static FoldCanonicalAndFullwidth(line) { if (![...line].some((c) => c.charCodeAt(0) >= 768)) return line; const folded = [...line].map((c) => { const cp = c.charCodeAt(0); return cp >= 65281 && cp <= 65374 ? String.fromCharCode(cp - 65248) : c; }).join(""); return _TextUtils.HasUnpairedSurrogate(folded) ? folded : folded.normalize("NFC"); } static HasUnpairedSurrogate(s) { for (let i = 0; i < s.length; i++) { const cp = s.charCodeAt(i); if (cp < 55296 || cp > 57343) continue; const next = i + 1 < s.length ? s.charCodeAt(i + 1) : 0; if (cp > 56319 || next < 56320 || next > 57343) return true; i++; } return false; } // Zero-width/control code points that render as nothing (or reorder text): // the classes JSON-escape mangling degrades into. Tab excluded: real content. // Numeric code points, not escapes or literals: they survive tool boundaries // that decode backslash escapes or eat raw control chars. static IsInvisible(c) { const cp = c.codePointAt(0); if (cp === 9) return false; return cp < 32 || cp >= 127 && cp <= 159 || cp === 173 || cp === 1564 || cp === 6158 || cp >= 8203 && cp <= 8207 || cp >= 8234 && cp <= 8238 || cp >= 8288 && cp <= 8292 || cp >= 8294 && cp <= 8297 || cp === 65279; } // En/em dashes are exempt from the audit: this authoring stack emits them in all // comment prose, so warning on them fires on nearly every apply and trains readers // to skip the audit (warning-blindness). They stay in the homoglyph table, so match // tolerance is unchanged. Curly quotes still warn: the motivating mangle was a quote // lookalike inside code, and prose here uses straight quotes — a curly quote is more // likely damage than intent. static IsIntentionalTypography(c) { const cp = c.codePointAt(0); return cp === 8211 || cp === 8212; } /// Names the invisible/lookalike code points in the given lines, or null if /// there are none. Escape mangling at a tool-call boundary writes unintended bytes /// silently; intent is unrecoverable afterwards, so the audit states the facts (which /// code point, what it resembles, how many) and leaves the judgment to the caller. /// static DescribeSuspectCodePoints(lines) { const counts = /* @__PURE__ */ new Map(); for (const line of lines) for (const c of line) if (_TextUtils.IsInvisible(c) || _TextUtils.MapHomoglyph(c) !== c && !_TextUtils.IsIntentionalTypography(c)) counts.set(c, (counts.get(c) ?? 0) + 1); if (counts.size === 0) return null; return [...counts.entries()].sort((a, b) => a[0].codePointAt(0) - b[0].codePointAt(0)).map(([c, n]) => { const mapped = _TextUtils.MapHomoglyph(c); const what = _TextUtils.IsInvisible(c) ? "invisible" : mapped === " " ? "looks like a space" : `looks like ${mapped}`; const hex = c.codePointAt(0).toString(16).toUpperCase().padStart(4, "0"); return `U+${hex} (${what}) x${n}`; }).join(", "); } /// Round-trip variant that accepts updated LF lines. static RoundTripWhitespace(originalSnapshot, updatedLines) { const joined = updatedLines.join("\n"); const origHadTrailing = originalSnapshot.endsWith("\n") || originalSnapshot.endsWith("\r\n") || originalSnapshot.length === 0; let result = joined; if (origHadTrailing && !joined.endsWith("\n") && joined.length > 0) result += "\n"; else if (!origHadTrailing && joined.endsWith("\n")) result = result.slice(0, -1); const newline = _TextUtils.DetectNewline(originalSnapshot); return newline === "\n" ? result : result.replace(/\n/g, newline); } }; // ../node_modules/.pnpm/matchu-patchu@0.3.3/node_modules/matchu-patchu/dist/utils/arrayUtils.js var ArrayUtils = class { /// /// Groups items by key, preserving first-seen key order (like LINQ's GroupBy). /// static GroupBy(items, keySelector) { const map = /* @__PURE__ */ new Map(); for (const item of items) { const key = keySelector(item); const arr = map.get(key) || []; arr.push(item); map.set(key, arr); } const result = []; for (const [key, values] of map.entries()) result.push({ key, values }); return result; } /// /// Returns a new array sorted ascending by key (stable, non-mutating, like LINQ's OrderBy). /// static OrderBy(items, keySelector) { return [...items].sort((a, b) => { const ka = keySelector(a), kb = keySelector(b); return ka < kb ? -1 : ka > kb ? 1 : 0; }); } /// /// Returns a new array sorted descending by key (stable, non-mutating, like LINQ's OrderByDescending). /// static OrderByDescending(items, keySelector) { return [...items].sort((a, b) => { const ka = keySelector(a), kb = keySelector(b); return ka > kb ? -1 : ka < kb ? 1 : 0; }); } }; // ../node_modules/.pnpm/matchu-patchu@0.3.3/node_modules/matchu-patchu/dist/models.js var PatchInputFile = class { Key; InputFullText; InputSelectedText; constructor(Key, InputFullText = "", InputSelectedText = "") { this.Key = Key; this.InputFullText = InputFullText; this.InputSelectedText = InputSelectedText; } }; var PatchOptions = class { ContinueOnError; MaxErrorIterations; ContextWindowMax; MaxChunksPerHunk; SanitizeDiff; Truncation; ControlChars; constructor(ContinueOnError = true, MaxErrorIterations = 10, ContextWindowMax = 8, MaxChunksPerHunk = 64, SanitizeDiff = true, Truncation = "warn", ControlChars = "error") { this.ContinueOnError = ContinueOnError; this.MaxErrorIterations = MaxErrorIterations; this.ContextWindowMax = ContextWindowMax; this.MaxChunksPerHunk = MaxChunksPerHunk; this.SanitizeDiff = SanitizeDiff; this.Truncation = Truncation; this.ControlChars = ControlChars; } }; var PatchOutputFile = class { Key; Fuzz; Edits; InputSelectedText; InputFullText; OutputFullText; Errors; AlreadyAppliedCount; Notices; constructor(Key, Fuzz, Edits, InputSelectedText, InputFullText, OutputFullText, Errors, AlreadyAppliedCount = 0, Notices = []) { this.Key = Key; this.Fuzz = Fuzz; this.Edits = Edits; this.InputSelectedText = InputSelectedText; this.InputFullText = InputFullText; this.OutputFullText = OutputFullText; this.Errors = Errors; this.AlreadyAppliedCount = AlreadyAppliedCount; this.Notices = Notices; } }; var PatchOutput = class { Files; constructor(Files) { this.Files = Files; } }; var LineType; (function(LineType2) { LineType2[LineType2["Context"] = 0] = "Context"; LineType2[LineType2["Delete"] = 1] = "Delete"; LineType2[LineType2["Insert"] = 2] = "Insert"; })(LineType || (LineType = {})); var DiffLine = class { Type; Text; CollapsedFrom; constructor(Type2, Text, CollapsedFrom = null) { this.Type = Type2; this.Text = Text; this.CollapsedFrom = CollapsedFrom; } }; var Hunk = class { Key; OldText; NewText; OldStart; constructor(Key, OldText, NewText, OldStart = -1) { this.Key = Key; this.OldText = OldText; this.NewText = NewText; this.OldStart = OldStart; } Lines = []; /// /// 1-based start position for the new side when available (from +c,d header), otherwise -1. /// Unused, but here for Unified Diff completeness. /// NewStart = -1; /// /// 0-based line number in the original diff text where this hunk's body (first diff body line) starts. /// -1 when unavailable (e.g., parser could not determine it). /// DiffBodyStartLine = -1; // Set by the parser under TruncationPolicy 'warn' on the final hunk when it // carried the tear signature. TruncationSuspected = false; // Set by the parser under ControlCharPolicy 'warn' when this hunk's insert // lines carried raw control characters. ControlCharsSuspected = false; // Error-reporting stand-in Chunk for a hunk that was never anchored: its lines // and diff location, no match. ToUnanchoredChunk() { return new Chunk([], this.Lines.filter((l) => l.Type == LineType.Delete).map((l) => l.Text), this.Lines.filter((l) => l.Type == LineType.Insert).map((l) => l.Text), [], UniqueMatch.NotFound, new DiffLocation(this)); } }; var Edit = class _Edit { LineIndex; DeleteLines; InsertLines; Fuzz; constructor(LineIndex, DeleteLines, InsertLines, Fuzz = 0) { this.LineIndex = LineIndex; this.DeleteLines = DeleteLines; this.InsertLines = InsertLines; this.Fuzz = Fuzz; } Shift(lineDelta) { return new _Edit(this.LineIndex + lineDelta, this.DeleteLines, this.InsertLines, this.Fuzz); } ApplyTo(lines) { if (this.LineIndex < 0 || this.LineIndex > lines.length) return; const deleteCount = Math.min(this.DeleteLines.length, lines.length - this.LineIndex); if (deleteCount > 0) lines.splice(this.LineIndex, deleteCount); if (this.InsertLines.length > 0) lines.splice(this.LineIndex, 0, ...this.InsertLines); } }; var DiffLocation = class { StartLine; Length; constructor(startOrHunk, lengthOrStartLine, endLine) { if (typeof startOrHunk === "number") { this.StartLine = startOrHunk; this.Length = lengthOrStartLine ?? 0; } else { const hunk = startOrHunk; const startLine = lengthOrStartLine ?? 0; const end = endLine ?? hunk.Lines.length - 1; this.StartLine = hunk.DiffBodyStartLine + startLine + 1; this.Length = end - startLine + 1; } } }; var FileHunkGroup = class { Key; Hunks; ContextOnlyHunkLines; constructor(Key, Hunks, ContextOnlyHunkLines = []) { this.Key = Key; this.Hunks = Hunks; this.ContextOnlyHunkLines = ContextOnlyHunkLines; } }; var Chunk = class _Chunk { ContextBefore; DeleteLines; InsertLines; ContextAfter; Match; DiffLocation; constructor(ContextBefore, DeleteLines, InsertLines, ContextAfter, Match2, DiffLocation2) { this.ContextBefore = ContextBefore; this.DeleteLines = DeleteLines; this.InsertLines = InsertLines; this.ContextAfter = ContextAfter; this.Match = Match2; this.DiffLocation = DiffLocation2; } get IsPureInsert() { return this.DeleteLines.length == 0 && this.InsertLines.length > 0; } get IsPureDelete() { return this.DeleteLines.length > 0 && this.InsertLines.length == 0; } HasContextLines() { return this.ContextBefore.length > 0 || this.ContextAfter.length > 0; } DeleteLinesWithContext() { return [...this.ContextBefore, ...this.DeleteLines, ...this.ContextAfter]; } InsertLinesWithContext() { return [...this.ContextBefore, ...this.InsertLines, ...this.ContextAfter]; } with(values) { return new _Chunk(values.ContextBefore ?? this.ContextBefore, values.DeleteLines ?? this.DeleteLines, values.InsertLines ?? this.InsertLines, values.ContextAfter ?? this.ContextAfter, values.Match ?? this.Match, values.DiffLocation ?? this.DiffLocation); } }; var MatchState; (function(MatchState2) { MatchState2[MatchState2["Success"] = 0] = "Success"; MatchState2[MatchState2["Ambiguous"] = 1] = "Ambiguous"; MatchState2[MatchState2["NotFound"] = 2] = "NotFound"; })(MatchState || (MatchState = {})); var UniqueMatch = class _UniqueMatch { State; LineIndex; Fuzz; constructor(State, LineIndex = -1, Fuzz = 0) { this.State = State; this.LineIndex = LineIndex; this.Fuzz = Fuzz; } get IsAmbiguous() { return this.State == MatchState.Ambiguous; } get IsNotFound() { return this.State == MatchState.NotFound; } get IsSuccess() { return this.State == MatchState.Success; } static get Ambiguous() { return new _UniqueMatch(MatchState.Ambiguous); } static get NotFound() { return new _UniqueMatch(MatchState.NotFound); } with(values) { return new _UniqueMatch(values.State ?? this.State, values.LineIndex ?? this.LineIndex, values.Fuzz ?? this.Fuzz); } }; // ../node_modules/.pnpm/matchu-patchu@0.3.3/node_modules/matchu-patchu/dist/utils/yaml.js var Yaml = class _Yaml { _sb = []; // -------- Public API -------- AppendLine(text) { this._sb.push(text + "\n"); } Section(keyWithIndent) { const [indent, key] = _Yaml.SplitIndentAndKey(keyWithIndent); this._sb.push(`${indent}${key}: `); } Scalar(keyWithIndent, value) { const [indent, key] = _Yaml.SplitIndentAndKey(keyWithIndent); this._sb.push(`${indent}${key}: ${value} `); } Folded(keyWithIndent, value, contentIndent = 2, chompStrip = true) { this.WriteBlockScalar(">", keyWithIndent, _Yaml.SplitLines(value), contentIndent, chompStrip); } Block(keyWithIndent, lines, contentIndent = 2, chompStrip = true) { this.WriteBlockScalar("|", keyWithIndent, lines ?? [], contentIndent, chompStrip); } ToString() { return this._sb.join(""); } static SplitIndentAndKey(keyWithIndent) { const leadingSpaces = TextUtils.CountLeadingWhitespace(keyWithIndent, " "); const indentStr = " ".repeat(leadingSpaces); const key = keyWithIndent.replace(/^\s+/, ""); return [indentStr, key]; } static SplitLines(value) { if (value == null || value.length == 0) return []; const lf = value.indexOf("\r") >= 0 ? value.replace(/\r\n/g, "\n").replace(/\r/g, "\n") : value; return lf.split("\n"); } WriteBlockScalar(style, keyWithIndent, lines, contentIndent, chompStrip) { const [indent, key] = _Yaml.SplitIndentAndKey(keyWithIndent); const chomp = chompStrip ? "-" : "+"; this._sb.push(`${indent}${key}: ${style}${contentIndent}${chomp} `); const pad = " ".repeat(contentIndent); let any = false; for (const line of lines) { any = true; this._sb.push(`${indent}${pad}${line} `); } if (!any) { this._sb.push(`${indent}${pad} `); } } }; // ../node_modules/.pnpm/matchu-patchu@0.3.3/node_modules/matchu-patchu/dist/exceptions.js var PatchException = class extends Error { Error; constructor(error) { super(error.SuggestedFixYaml); this.Error = error; } }; var PatchParserException = class extends Error { constructor(message) { super(message ?? "Malformed diff patch"); } }; var PatchError = class { Type; FailedMatch; // The file key(s) the error concerns: the foreign key a FileMismatch named, or // the comma-joined candidate keys of an ambiguous headerless routing; null otherwise. FileKey; // Diagnosis computed against the file at error time (e.g. the quoted line // occurs inside one longer file line); null when no confident diagnosis. Hint; constructor(errorType, chunk, fileKey = null, hint = null) { this.Type = errorType; this.FailedMatch = chunk; this.FileKey = fileKey; this.Hint = hint; } toString() { return `Failed to patch text with the diff provided. ${this.SuggestedFixYaml}`; } get SuggestedFixYaml() { const SummaryFor = (type) => ({ "MatchNotFound": "Matching lines not found; make sure the diff's context and deleted lines exactly match the original text.", "MatchAmbiguous": "Matched multiple locations; make sure there are enough context lines to uniquely identify an edit.", "ChunkDuplicated": "Duplicate chunks.", "ChunkOverlapping": "Overlapping chunks.", "FileMismatch": "The hunk's file header names a file that is not being patched, so it was not applied. Send hunks only for the file(s) being patched." })[type] ?? "Patch failed."; const diffLoc = this.FailedMatch.DiffLocation; const y = new Yaml(); y.AppendLine("PatchError:"); y.Scalar(" Type", this.Type.toString()); if (this.FileKey != null) y.Scalar(" File", this.FileKey); y.Folded(" Summary", SummaryFor(this.Type)); if (this.Hint != null) y.Folded(" Hint", this.Hint); y.Section(" Details"); y.Section(" FailedMatch"); y.Block(" ContextBefore", this.FailedMatch.ContextBefore); y.Block(" DeleteLines", this.FailedMatch.DeleteLines); y.Block(" InsertLines", this.FailedMatch.InsertLines); y.Block(" ContextAfter", this.FailedMatch.ContextAfter); y.Section(" DiffLocation"); y.Scalar(" StartLine", diffLoc.StartLine.toString()); y.Scalar(" Length", diffLoc.Length.toString()); return y.ToString(); } }; // ../node_modules/.pnpm/matchu-patchu@0.3.3/node_modules/matchu-patchu/dist/applier/guards.js var DuplicateGuard = class _DuplicateGuard { // Two chunks that resolve to the same anchor with the same deletes and inserts are // provably the same edit (an LLM re-emitting a hunk). Applying it once is // unambiguously correct, so keep the first occurrence and drop the rest — the lenient // move, with no false-positive risk: genuinely distinct edits differ in key // (anchor or content) and fall through to OverlapGuard. static Dedupe(args) { const seen = /* @__PURE__ */ new Set(); const kept = []; for (const chunk of args.Chunks) { const key = _DuplicateGuard.BuildKey(chunk); if (!seen.has(key)) { seen.add(key); kept.push(chunk); } } return new InputChunks(args.InputFullText, kept, args.Options); } static BuildKey(c) { let sb = ""; sb += `${c.Match?.LineIndex ?? -1} `; for (const l of c.DeleteLines) sb += `${l} `; sb += `\u2192 `; for (const l of c.InsertLines) sb += `${l} `; return sb.toString(); } }; var OverlapGuard = class { static Guard(args) { const chunks = ArrayUtils.OrderBy(args.Chunks, (c) => c.Match?.LineIndex ?? -1); let currentEnd = 0; for (const chunk of chunks) { const idx = chunk.Match?.LineIndex ?? -1; if (idx < 0) continue; if (idx < currentEnd) throw new PatchException(new PatchError("ChunkOverlapping", chunk)); if (chunk.DeleteLines.length > 0) currentEnd = idx + chunk.DeleteLines.length; } } }; // ../node_modules/.pnpm/matchu-patchu@0.3.3/node_modules/matchu-patchu/dist/applier/editMinimizer.js var EditMinimizer = class _EditMinimizer { static Same(a, b) { return a === b; } /// /// Convert an edit into a sequence of minimal edits, by excluding all unchanged lines. /// static *ToMinimalEdits(edit) { const deleted = edit.DeleteLines; const inserted = edit.InsertLines; const maxLength = Math.max(deleted.length, inserted.length); let diffStart = -1; for (let i = 0; i <= maxLength; i++) { const withinDeleted = i < deleted.length; const withinInserted = i < inserted.length; const atEnd = i == maxLength; const isMatch = withinDeleted && withinInserted && _EditMinimizer.Same(deleted[i], inserted[i]); const isDiff = !atEnd && (!withinDeleted || !withinInserted || !isMatch); const isBoundary = atEnd || isMatch; if (diffStart == -1 && isDiff) { diffStart = i; } else if (diffStart != -1 && isBoundary) { const delCount = Math.max(0, Math.min(i, deleted.length) - diffStart); const insCount = Math.max(0, Math.min(i, inserted.length) - diffStart); const delBlock = delCount <= 0 ? [] : deleted.slice(diffStart, diffStart + delCount); const insBlock = insCount <= 0 ? [] : inserted.slice(diffStart, diffStart + insCount); yield new Edit(edit.LineIndex + diffStart, delBlock, insBlock, edit.Fuzz); diffStart = -1; } } } }; // ../node_modules/.pnpm/matchu-patchu@0.3.3/node_modules/matchu-patchu/dist/applier/indenter.js var Anchor = class { BaseIndent; Shift; constructor(BaseIndent, Shift) { this.BaseIndent = BaseIndent; this.Shift = Shift; } }; var Indenter = class _Indenter { static DefaultTabSize = 4; /// /// Aligns an inserted block () to the file’s /// indent context at , leveraging /// ContextBefore / ContextAfter when available. /// static AlignInsert(lines, chunk) { const insert = chunk.InsertLines; if (insert.length == 0) return insert; const tabSize = _Indenter.InferTabSize(lines.concat(insert)); const anchor = _Indenter.FindAnchor(lines, chunk.ContextBefore, chunk.Match.LineIndex - 1, true, tabSize) ?? _Indenter.FindAnchor(lines, chunk.ContextAfter, chunk.Match.LineIndex, false, tabSize) ?? new Anchor("", 0); const reindented = new Set(chunk.DeleteLines.map((l) => _Indenter.SplitIndent(l)[1])); const blankKeep = new Set(insert.filter((l) => !_Indenter.IsBlank(l)).map(_Indenter.LeadingWs).concat([anchor.BaseIndent])); const tabStyle = anchor.BaseIndent.length > 0 ? anchor.BaseIndent.indexOf(" ") >= 0 : lines.some((l) => l.startsWith(" ")); return insert.map((l) => _Indenter.Adjust(l, anchor, tabSize, tabStyle, reindented, blankKeep)); } static FindAnchor(lines, contextLines, startIndex, backward, tabSize) { if (contextLines.length == 0) return null; const [ctxWs, ctxTrim] = _Indenter.SplitIndent(contextLines[backward ? contextLines.length - 1 : 0]); const ctxW = _Indenter.VisualWidth(ctxWs, tabSize); const step = backward ? -1 : 1; const end = backward ? -1 : lines.length; for (let i = startIndex; i != end; i += step) { const [ws, trim] = _Indenter.SplitIndent(lines[i]); if (trim != ctxTrim) continue; return new Anchor(ws, _Indenter.VisualWidth(ws, tabSize) - ctxW); } return null; } static Adjust(line, anchor, tabSize, tabStyle, reindented, blankKeep) { if (_Indenter.IsBlank(line)) return anchor.Shift == 0 && blankKeep.has(line) ? line : ""; const [ws, content] = _Indenter.SplitIndent(line); const wsWidth = _Indenter.VisualWidth(ws, tabSize); const target = Math.max(0, wsWidth + anchor.Shift); if (wsWidth == target && (ws.indexOf(" ") >= 0 == tabStyle || reindented.has(content))) return ws + content; return _Indenter.BuildIndent(anchor.BaseIndent, target, tabSize) + content; } static LeadingWs(line) { return _Indenter.SplitIndent(line)[0]; } static IsBlank(s) { return s == null || s.length == 0 || Array.from(s).every((c) => c === " " || c === " "); } static SplitIndent(line) { if (line == null || line.length == 0) return ["", ""]; const i = TextUtils.CountLeadingWhitespace(line); return [line.substring(0, i), line.substring(i)]; } static InferTabSize(lines) { for (const ws of Array.from(lines).map(_Indenter.LeadingWs)) { const lastTab = ws.lastIndexOf(" "); if (lastTab >= 0) { const spaces = ws.length - lastTab - 1; if (spaces > 0) return spaces; } } return _Indenter.DefaultTabSize; } static VisualWidth(indent, tabSize) { return Array.from(indent).map((c) => c == " " ? tabSize : 1).reduce((a, b) => a + b, 0); } static BuildIndent(baseIndent, targetW, tabSize) { if (targetW <= 0) return ""; const baseW = _Indenter.VisualWidth(baseIndent, tabSize); const delta = targetW - baseW; if (delta == 0) return baseIndent; if (delta > 0) return baseIndent + " ".repeat(delta); return baseIndent.indexOf(" ") >= 0 ? " ".repeat(Math.floor(targetW / tabSize)) + " ".repeat(targetW % tabSize) : " ".repeat(targetW); } }; // ../node_modules/.pnpm/matchu-patchu@0.3.3/node_modules/matchu-patchu/dist/applier/matchHint.js var MatchHint = class _MatchHint { // A quoted line inside exactly one longer file line is the prose line-model // mistake: the author's "line" (a sentence of an unwrapped paragraph, or a // re-flowed hard wrap) is a slice of the file's physical line. Matching stays // refused — a head fragment is byte-identical to an elided whole-line delete, // so either auto-reading risks silent corruption — but naming the line is // correct under both readings: the retry re-quotes it whole. static SubLine(chunk, lines) { for (const q of chunk.DeleteLinesWithContext()) { if (q.trim().length < _MatchHint.MinHintLineContent) continue; if (lines.includes(q)) continue; const containing = []; for (let i = 0; i < lines.length && containing.length < 2; i++) if (lines[i].length > q.length && lines[i].includes(q)) containing.push(i); if (containing.length != 1) continue; return `The diff line "${_MatchHint.Preview(q)}" matches only PART of line ${containing[0] + 1}, which is ${lines[containing[0]].length} characters long \u2014 in prose, a whole paragraph is often one physical line. Re-send the diff quoting entire lines exactly.`; } return null; } static Preview(s) { const t = s.trim(); return t.length <= 60 ? t : t.slice(0, 57) + "..."; } // Trimmed length a quoted line must reach before containment counts as // evidence: short strings occur inside longer lines by coincidence. static MinHintLineContent = 12; }; // ../node_modules/.pnpm/matchu-patchu@0.3.3/node_modules/matchu-patchu/dist/applier/chunkApplier.js var InputChunks = class { InputFullText; Chunks; Options; constructor(InputFullText, Chunks, Options) { this.InputFullText = InputFullText; this.Chunks = Chunks; this.Options = Options; } }; var OutputEdits = class { OutputText; Edits; Errors; constructor(OutputText, Edits, Errors) { this.OutputText = OutputText; this.Edits = Edits; this.Errors = Errors; } }; var ChunkApplier = class _ChunkApplier { // NOTE: on partial failure, Apply is all-or-nothing — it returns the ORIGINAL // text untouched alongside the errors, never a best-effort partial application. static Apply(targetText, chunks, options) { const inputChunks = new InputChunks(targetText, chunks, options); if (!options.ContinueOnError) return _ChunkApplier.ApplyChunks(inputChunks); const errors = []; for (let attempt = 0; attempt < options.MaxErrorIterations; attempt++) { try { const r = _ChunkApplier.ApplyChunks(inputChunks); if (errors.length) return new OutputEdits(targetText, [], errors); return r; } catch (ex) { if (ex instanceof PatchException) { errors.push(ex.Error); const idx = chunks.indexOf(ex.Error.FailedMatch); if (idx >= 0) chunks.splice(idx, 1); } else throw ex; } } return new OutputEdits(targetText, [], errors); } static ApplyChunks(args) { args = DuplicateGuard.Dedupe(args); OverlapGuard.Guard(args); const lines = TextUtils.ToLines(args.InputFullText); const edits = args.Chunks.map((chunk) => _ChunkApplier.CreateEdit(chunk, lines)); const workingLines = Array.from(lines); for (const edit of ArrayUtils.OrderByDescending(edits, (e) => e.LineIndex)) edit.ApplyTo(workingLines); const patched = TextUtils.RoundTripWhitespace(args.InputFullText, workingLines); const minimalEdits = edits.flatMap((e) => Array.from(EditMinimizer.ToMinimalEdits(e))); return new OutputEdits(patched, minimalEdits, []); } static CreateEdit(chunk, lines) { const match = chunk.Match; if (match.IsAmbiguous) throw new PatchException(new PatchError("MatchAmbiguous", chunk)); if (match.IsNotFound) throw new PatchException(new PatchError("MatchNotFound", chunk, null, MatchHint.SubLine(chunk, lines))); return new Edit(match.LineIndex, Array.from(chunk.DeleteLines), Indenter.AlignInsert(lines, chunk), match.Fuzz); } }; // ../node_modules/.pnpm/matchu-patchu@0.3.3/node_modules/matchu-patchu/dist/parser/heuristics.js var DiffContentHeuristics = class _DiffContentHeuristics { /** * Checks if a line starts with a doubled marker (++ or --) */ static IsDoubleMarker(line) { return line.length > 1 && line[0] == line[1] && (line[0] == "-" || line[0] == "+"); } /** * Determines if a doubled marker represents content rather than diff syntax. * * Examples: * - `--bg-color` → true (CSS variable, content) * - `--` → true (exactly two hyphens, content) * - `-- text` → false (deletion of markdown list `- text`, diff syntax) * - `---comment` → false (deletion of `--comment`, diff syntax) * - `----` → false (deletion of `--`, diff syntax) * - `++var` → true (C++ code, content) * - `++ text` → false (insertion of `+ text`, diff syntax) * - `+++` → false (insertion of `+`, diff syntax) */ static IsDoubleMarkerContent(line) { if (!_DiffContentHeuristics.IsDoubleMarker(line)) return false; if (line.length == 2) return true; return line[2] != line[0] && line[2] != " "; } /** * A '++X' line that is a candidate for the sloppy doubled-insert-marker * collapse ('++X' → insert 'X'). Tripled markers are proper syntax * ('+++X' = insert '++X') and never candidates. */ static IsSloppyDoublePlus(line) { return line.length > 1 && line[0] == "+" && line[1] == "+" && (line.length == 2 || line[2] != "+"); } /** * True when the hunk body proves the edit region is itself diff-shaped: * a context or deletion line whose CONTENT starts with '+' or '-' exists * verbatim in the file. In such a region, doubled markers are diff syntax * over diff-shaped content, not sloppy doubling. */ static IsDiffShapedRegion(rawLines, fileLines) { if (!fileLines || fileLines.length == 0) return false; for (const l of rawLines) { if (l.length < 2 || l[0] != " " && l[0] != "-") continue; if (l[1] != "+" && l[1] != "-") continue; const content = l.substring(1); if (fileLines.some((f) => f == content)) return true; } return false; } /** * Validates whether a line (after stripping indentation) represents valid diff syntax. * Used by StripIndent to determine if indentation should be removed. * * Returns false for content that looks like diff markers: * - `--text` (CSS variables) * - `-abc-*` (hyphenated identifiers like -webkit-*) */ static IsValidDiffLineStart(line) { if (line.length == 0) return false; const first = line[0]; if (first == " " || first == "+") return true; if (first == "-") { if (line.length == 1) return true; if (line[1] == "-") return line.length > 2 && line[2] == "-"; if (_DiffContentHeuristics.IsHyphenatedIdentifier(line)) return false; return true; } return false; } /** * Pattern: -[a-z]+- (e.g., -webkit-*, -moz-*) * If hyphen is followed by letters then another hyphen, it's structural content. */ static IsHyphenatedIdentifier(line) { if (line.length < 3 || line[0] != "-") return false; let i = 1; while (i < line.length && line[i] >= "a" && line[i] <= "z") i++; return i > 1 && i < line.length && line[i] == "-"; } /** * Applies heuristics to classify raw lines (lines without explicit diff markers). * * Heuristic: Raw lines are treated as context until we've seen explicit context markers. * Once explicit context has been seen, a raw line followed by an insertion before a * context/deletion boundary is treated as a deletion. * * This helps handle sloppy diffs where deletion markers are omitted but can be inferred. */ static ApplyRawLineHeuristic(raw, rawLines, index, seenExplicitContext, fileLines) { const insAhead = _DiffContentHeuristics.HasInsertionAheadBeforeContextOrDeletion(rawLines, index); if (insAhead && seenExplicitContext) { return new DiffLine(LineType.Delete, _DiffContentHeuristics.StripInlineComment(raw, fileLines)); } if (insAhead) { const verified = _DiffContentHeuristics.TryVerifiedCommentStrip(raw, fileLines); if (verified !== null) return new DiffLine(LineType.Delete, verified); } return new DiffLine(LineType.Context, raw); } static InlineCommentMarkers = [" //", " --", " #"]; /** * Returns the comment-stripped form of a line when file content verifies it: * the full line must NOT exist in the file and the stripped variant MUST — so * real content containing " --" (SQL) or " #" (Python) is never mangled. * Returns null when no verified strip exists. */ static TryVerifiedCommentStrip(input, fileLines) { if (!fileLines || fileLines.length == 0) return null; if (fileLines.some((l) => l === input)) return null; for (const marker of _DiffContentHeuristics.InlineCommentMarkers) { for (let idx = input.indexOf(marker); idx >= 0; idx = input.indexOf(marker, idx + 1)) { const candidate = input.substring(0, idx).trimEnd(); if (fileLines.some((l) => l === candidate)) return candidate; } } return null; } /** * Strips an inline comment the LLM appended to an inferred deletion line * (e.g., "Line2 // Missing - marker" → "Line2"). * Prefers a content-verified strip; without file content (or when * verification is inconclusive), falls back to stripping C-style " //" only. */ static StripInlineComment(input, fileLines) { if (fileLines?.some((l) => l === input)) return input; const verified = _DiffContentHeuristics.TryVerifiedCommentStrip(input, fileLines); if (verified !== null) return verified; const idx = input.indexOf(" //"); return idx >= 0 ? input.substring(0, idx) : input; } /** * Checks if there's an insertion (line starting with '+') ahead of the current position, * before encountering a context line, deletion line, or blank line (boundaries). * * Used to infer whether a raw line should be treated as a deletion. */ static HasInsertionAheadBeforeContextOrDeletion(rawLines, currentIndex) { for (let i = currentIndex + 1; i < rawLines.length; i++) { const l = rawLines[i]; if (l.length == 0) break; const c = l[0]; if (c == " ") break; if (c == "-" && !_DiffContentHeuristics.IsDoubleMarkerContent(l)) break; if (c == "+") return true; } return false; } /** * Checks if a line starting with "-" or "+" is likely content with a missing context prefix. * Uses file content to disambiguate: if file has the line but NOT the would-be-modified content. * * Examples: * - "- list item" might be markdown bullet, not deletion of " list item" * - "+ Add feature" might be changelog entry, not insertion of " Add feature" * * @returns true if line should be treated as context, false if it should be treated as diff operation */ static IsMissingContextPrefix(line, fileLines) { if (line.length < 2) return false; const marker = line[0]; if (marker !== "-" && marker !== "+") return false; if (marker === "+" && line[1] !== " ") return false; const hasContentMatch = fileLines.some((l) => l === line); const hasOperationMatch = fileLines.some((l) => l === line.substring(1)); return hasContentMatch && !hasOperationMatch; } /** * Resolves ambiguity when stripping indentation reveals "- " (hyphen + space). * * This pattern is ambiguous: * - Could be an indented DELETE line (deleting content starting with a space) * - Could be a properly formatted CONTEXT line containing a markdown bullet * * Example: " - Label them..." could mean: * - DELETE: Remove line " Label them..." (space + content) from file * - CONTEXT: Keep line " - Label them..." (3 spaces + bullet) in file * * Resolution: Check which interpretation matches the actual file content. * If only one matches, use that. If both or neither, prefer CONTEXT (conservative). * * @param original - The original line before stripping (e.g., " - Label them...") * @param stripped - The line after stripping indentation (e.g., "- Label them...") * @param fileLines - Lines from the actual file being patched * @returns The line to use (original if CONTEXT, stripped if DELETE) */ static ResolveHyphenSpaceAmbiguity(original, stripped, fileLines) { const contextContent = original.length > 0 && original[0] === " " ? original.substring(1) : original; const deleteContent = stripped.substring(1); const hasContextMatch = fileLines.some((line) => line === contextContent); const hasDeleteMatch = fileLines.some((line) => line === deleteContent); if (hasContextMatch && !hasDeleteMatch) return original; if (hasDeleteMatch && !hasContextMatch) return stripped; return original; } }; // ../node_modules/.pnpm/matchu-patchu@0.3.3/node_modules/matchu-patchu/dist/parser/hunkBuilder.js var Header = class _Header { Path; OldStart; OldCount; NewStart; NewCount; DiffBodyStartLine; constructor(Path = null, OldStart = -1, OldCount = 0, NewStart = -1, NewCount = 0, DiffBodyStartLine = -1) { this.Path = Path; this.OldStart = OldStart; this.OldCount = OldCount; this.NewStart = NewStart; this.NewCount = NewCount; this.DiffBodyStartLine = DiffBodyStartLine; } static WithParsed(h, parsedHdr, diffBodyStartLine) { return new _Header(h.Path, parsedHdr.OldStart, parsedHdr.OldCount, parsedHdr.NewStart, parsedHdr.NewCount, diffBodyStartLine); } static ParseSide(header, part, prefix, isOld) { if (!part.startsWith(prefix)) return header; const split = part.substring(1).split(","); const start = split.length > 0 && !isNaN(parseInt(split[0])) ? parseInt(split[0]) : -1; const count = split.length > 1 && !isNaN(parseInt(split[1])) ? parseInt(split[1]) : 1; return isOld ? new _Header(header.Path, start, count, header.NewStart, header.NewCount, header.DiffBodyStartLine) : new _Header(header.Path, header.OldStart, header.OldCount, start, count, header.DiffBodyStartLine); } }; var HunkBuilder = class _HunkBuilder { hunks = []; bodySeen = /* @__PURE__ */ new Map(); get Hunks() { return this.hunks; } // Only the diff's FINAL hunk can be torn by a cutoff, so the parser reads this // once, after the last commit; mid-diff the same signature is just sloppy counting. LastHunkTruncated = false; // A closing ``` fence is completion evidence a token cutoff cannot emit — the // tear signature on a fence-closed hunk is miscount slop, not truncation. // (Trailing prose already clears the flag via the pure-context commit path.) ClearTruncation() { this.LastHunkTruncated = false; } // Diff line of the first @@-headed body with real content but no change lines. // Such a hunk can never be intentional (its '+'/'-' markers were lost, and the // loss is ambiguous — adds or deletes?), so the parser rejects the whole diff // instead of silently dropping it. Bodies outside any hunk (prose before or // after a diff) and blank-only bodies keep flowing through silently. // Diff line of each @@-headed body that had real content but no change lines. Such a // hunk states no change, so it is skipped and its siblings apply — but the lines are // surfaced so the caller can name the skip rather than report a clean success. Bodies // outside any hunk (prose before or after a diff) and blank-only bodies are not hunks // at all and are never recorded. ContextOnlyHunkLines = []; CommitIfAny(hdr, body, fileLines) { if (body.length == 0) return; const blankContextDeletion = _HunkBuilder.IsBlankContextDeletion(hdr.OldCount, hdr.NewCount, body); if (!blankContextDeletion && !body.some((l) => l.length > 0 && (l[0] == "-" || l[0] == "+"))) { this.LastHunkTruncated = false; if (hdr.DiffBodyStartLine >= 0 && body.some((l) => l.trim().length > 0)) this.ContextOnlyHunkLines.push(hdr.DiffBodyStartLine); body.length = 0; return; } const diffLines = _HunkBuilder.BuildLines(body, blankContextDeletion, fileLines); this.LastHunkTruncated = _HunkBuilder.IsTruncated(hdr, diffLines); const oldSb = []; const newSb = []; let oldLines = 0, newLines = 0; for (const dl of diffLines) { switch (dl.Type) { case LineType.Insert: newSb.push(dl.Text, "\n"); newLines++; break; case LineType.Delete: oldSb.push(dl.Text, "\n"); oldLines++; break; case LineType.Context: oldSb.push(dl.Text, "\n"); oldLines++; newSb.push(dl.Text, "\n"); newLines++; break; } } const oldTxt = oldLines > 0 ? oldSb.join("") : ""; const newTxt = newLines > 0 ? newSb.join("") : ""; if (oldTxt == newTxt) { body.length = 0; return; } const hunk = new Hunk(hdr.Path ?? "", oldTxt, newTxt, hdr.OldStart); hunk.NewStart = hdr.NewStart; hunk.Lines = diffLines; hunk.DiffBodyStartLine = hdr.DiffBodyStartLine; this.AddHunk(hunk); body.length = 0; } // Token-limit cutoff: body ends mid-change-run and the header declares more lines // than delivered, asymmetrically. Symmetric deficits are stripped context (the // anchorer recovers) and never count. An insert-ending body is torn only when the // old side is ALSO short (promised trailing context never arrived): an old side // fully delivered is complete by its own header, and a new-side deficit alone is // overcount slop that must still apply — a complete append with a miscounted // header is indistinguishable from a mid-run cut, and counts are tie-breakers, // never sole grounds for rejection. A delete-ending body is the mirror: torn only // when the NEW side is also short (newDeficit > 0) AND the old side is shorter // still (the delete run was cut), so an over-counted-but-complete delete hunk // (newDeficit == 0) applies as overcount slop. Cost (both branches): a cut with no // trailing context owed on the completed side goes undetected. static IsTruncated(hdr, lines) { if (hdr.OldStart < 0 || hdr.NewStart < 0 || lines.length == 0) return false; const last = lines[lines.length - 1].Type; if (last == LineType.Context) return false; const actualOld = lines.filter((l) => l.Type != LineType.Insert).length; const actualNew = lines.filter((l) => l.Type != LineType.Delete).length; const oldDeficit = hdr.OldCount - actualOld, newDeficit = hdr.NewCount - actualNew; return last == LineType.Insert ? oldDeficit > 0 && newDeficit > oldDeficit : newDeficit > 0 && oldDeficit > newDeficit; } // Identical bodies collapse when the repeat adds no placement info (bare @@ or a // start line already seen) — LLM-slop duplication. But git legitimately emits // byte-identical hunks at distinct line numbers when a file repeats a pattern; // those are separate edits, and the anchorer's line-number tiebreak places each // at its own site. Distinct bodies always survive. AddHunk(h) { const bodyKey = `${h.OldText} \u241E ${h.NewText}`; const starts = this.bodySeen.get(bodyKey); if (starts) { if (h.OldStart < 0 || starts.has(-1) || starts.has(h.OldStart)) return; starts.add(h.OldStart); } else { this.bodySeen.set(bodyKey, /* @__PURE__ */ new Set([h.OldStart])); } this.hunks.push(h); } static BuildLines(rawLines, blankContextDeletion, fileLines) { const result = []; let seenExplicitContext = false; const diffShaped = DiffContentHeuristics.IsDiffShapedRegion(rawLines, fileLines); for (let i = 0; i < rawLines.length; i++) { const raw = rawLines[i]; if (raw.length == 0) { if (i == rawLines.length - 1) continue; result.push(new DiffLine(blankContextDeletion ? LineType.Delete : LineType.Context, "")); continue; } if (blankContextDeletion) { const c = raw[0]; result.push(new DiffLine(LineType.Delete, c == " " || c == "-" ? _HunkBuilder.RemoveDiffPrefix(raw) : raw)); continue; } switch (raw[0]) { case "+": case "-": if (fileLines && DiffContentHeuristics.IsMissingContextPrefix(raw, fileLines)) { result.push(new DiffLine(LineType.Context, raw)); seenExplicitContext = true; break; } if (raw[0] === "+") { if (DiffContentHeuristics.IsSloppyDoublePlus(raw)) { result.push(diffShaped ? new DiffLine(LineType.Insert, raw.substring(1)) : new DiffLine(LineType.Insert, _HunkBuilder.RemoveDiffPrefix(raw), raw)); break; } result.push(new DiffLine(LineType.Insert, _HunkBuilder.RemoveDiffPrefix(raw))); break; } if (DiffContentHeuristics.IsDoubleMarkerContent(raw)) { if (fileLines && fileLines.some((l) => l == raw.substring(1)) && !fileLines.some((l) => l == raw)) { result.push(new DiffLine(LineType.Delete, raw.substring(1))); break; } if (seenExplicitContext && raw.length > 2 && raw[2] === " ") { result.push(new DiffLine(LineType.Delete, raw.substring(1))); } else { result.push(DiffContentHeuristics.ApplyRawLineHeuristic(raw, rawLines, i, seenExplicitContext, fileLines)); } } else { result.push(new DiffLine(LineType.Delete, _HunkBuilder.RemoveDiffPrefix(raw))); } break; case " ": result.push(new DiffLine(LineType.Context, _HunkBuilder.RemoveDiffPrefix(raw))); seenExplicitContext = true; break; default: result.push(DiffContentHeuristics.ApplyRawLineHeuristic(raw, rawLines, i, seenExplicitContext, fileLines)); break; } } return result; } static IsBlankContextDeletion(oldCount, newCount, rawLines) { return oldCount > 0 && newCount == 0 && rawLines.every((l) => l.length == 0 || l[0] == " "); } // Strip diff prefix: ` foo` → `foo`, `-bar` → `bar`, `+baz` → `baz` // Sloppy doubled markers: `++code` → `code`, `++ code` → `code` // But NOT tripled: `---comment` → `--comment` (proper syntax for deleting `--comment`) // And NOT `-- text` which is deletion of `- text` (markdown list), not sloppy static RemoveDiffPrefix(s) { if (!s || s.length <= 1) return ""; let result = s.substring(1); if (DiffContentHeuristics.IsSloppyDoublePlus(s)) { result = result.substring(1); if (result[0] === " " || result[0] === " ") result = result.substring(1); } return result; } }; // ../node_modules/.pnpm/matchu-patchu@0.3.3/node_modules/matchu-patchu/dist/parser/unifiedDiffParser.js var UnifiedDiffParser = class _UnifiedDiffParser { /** * Parse a unified diff into structured hunks. * @param diff - The diff text to parse * @param files - Map of filename to content (content used for disambiguation of ambiguous lines) * @param truncation - Verdict for a surviving tear signature on the final hunk * @param controlChars - Verdict for raw control characters in insert lines */ static Parse(diff, files, truncation = "warn", controlChars = "error") { const parsed = diff == null || diff.trim().length == 0 ? { Hunks: [], ContextOnlyLines: [] } : _UnifiedDiffParser.ParseHunks(diff, files, truncation, controlChars); const hunks = parsed.Hunks; const ctxOnly = parsed.ContextOnlyLines; const fileKeySet = new Set(files.keys()); const headerless = hunks.filter((h) => h.Key == null || h.Key == ""); const explicitKnown = hunks.filter((h) => h.Key != null && h.Key != "" && fileKeySet.has(h.Key)); const explicitFallback = hunks.filter((h) => h.Key != null && h.Key != "" && !fileKeySet.has(h.Key)); const groups = []; if (headerless.length > 0) groups.push(new FileHunkGroup("", headerless, ctxOnly)); if (explicitKnown.length > 0) ArrayUtils.GroupBy(explicitKnown, (h) => h.Key).map((g) => new FileHunkGroup(g.key, g.values, ctxOnly)).forEach((x) => groups.push(x)); const treatAsSingleFile = fileKeySet.size == 1 && fileKeySet.has("") && explicitKnown.length == 0 && headerless.length == 0 && Array.from(new Set(explicitFallback.map((h) => h.Key))).length == 1; if (treatAsSingleFile && explicitFallback.length > 0) groups.push(new FileHunkGroup("", explicitFallback, ctxOnly)); else if (explicitFallback.length > 0) ArrayUtils.GroupBy(explicitFallback, (h) => h.Key).map((g) => new FileHunkGroup(g.key, g.values, ctxOnly)).forEach((x) => groups.push(x)); if (groups.length == 0 && ctxOnly.length > 0) groups.push(new FileHunkGroup("", [], ctxOnly)); return groups; } static ParseHunks(diff, fileContents, truncation = "warn", controlChars = "error") { const hunkBuilder = new HunkBuilder(); const body = []; let header = new Header(); let inFence = false; let inHunkBody = false; let lineNo = -1; const defaultFileLines = fileContents?.size == 1 && fileContents.has("") ? _UnifiedDiffParser.ToFileLines(fileContents.get("")) : void 0; let currentFileLines = defaultFileLines; let fileLineSet = _UnifiedDiffParser.ToLineSet(currentFileLines); for (const raw of _UnifiedDiffParser.StripCommonIndent(diff.replace(/\r\n/g, "\n")).split("\n")) { lineNo++; if (raw.length > 1 && raw[0] == " " && fileLineSet != null && fileLineSet.has(raw.slice(1).replace(/\s+$/, ""))) { body.push(raw); continue; } const line = _UnifiedDiffParser.StripIndent(raw, currentFileLines); if (line.startsWith("```diff")) { inFence = true; continue; } if (!inFence && line.startsWith("```")) { continue; } if (inFence && line.startsWith("```")) { hunkBuilder.CommitIfAny(header, body, currentFileLines); hunkBuilder.ClearTruncation(); header.DiffBodyStartLine = -1; inFence = false; inHunkBody = false; continue; } if (_UnifiedDiffParser.IsMeta(line)) continue; const path = _UnifiedDiffParser.TryParseFileHeader(line); if (path && _UnifiedDiffParser.IsFileHeader(line, path, inHunkBody, fileContents, fileLineSet, body, currentFileLines)) { hunkBuilder.CommitIfAny(header, body, currentFileLines); header = new Header(path); currentFileLines = _UnifiedDiffParser.ToFileLines(fileContents?.get(path)) ?? defaultFileLines; fileLineSet = _UnifiedDiffParser.ToLineSet(currentFileLines); inHunkBody = false; continue; } const parsedHdr = _UnifiedDiffParser.TryParseHunkHeader(line); if (parsedHdr) { hunkBuilder.CommitIfAny(header, body, currentFileLines); header = Header.WithParsed(header, parsedHdr, lineNo + 1); inHunkBody = true; continue; } body.push(line); } hunkBuilder.CommitIfAny(header, body, currentFileLines); const hunks = hunkBuilder.Hunks; if (hunkBuilder.LastHunkTruncated && truncation != "ignore") { if (truncation == "error") throw new PatchParserException("The diff appears truncated mid-hunk: the final hunk's header declares more lines than its body delivers. Nothing was applied; re-send the complete diff."); if (hunks.length > 0) hunks[hunks.length - 1].TruncationSuspected = true; } if (controlChars != "ignore") { for (const hunk of hunks) { const suspect = _UnifiedDiffParser.DescribeControlChars(hunk.Lines.filter((l) => l.Type == LineType.Insert).map((l) => l.Text)); if (suspect == null) continue; if (controlChars == "error") throw new PatchParserException(`Insert lines contain raw control characters (${suspect}) \u2014 almost certainly transport damage, not intended content. Nothing was applied; re-send the diff with the intended text (or opt into the 'warn'/'ignore' policy).`); hunk.ControlCharsSuspected = true; } } return { Hunks: hunks, ContextOnlyLines: hunkBuilder.ContextOnlyHunkLines }; } static IsSuspectControlChar(c) { const cp = c.charCodeAt(0); return cp < 32 && c != " " && c != "\n" && c != "\r" && c != "\f" || cp == 127; } // "U+0000 x2, U+001B x1" — names the invisible damage so the caller can see it. static DescribeControlChars(lines) { const counts = /* @__PURE__ */ new Map(); for (const line of lines) for (const c of line) if (_UnifiedDiffParser.IsSuspectControlChar(c)) counts.set(c, (counts.get(c) ?? 0) + 1); if (counts.size == 0) return null; return [...counts.entries()].sort((a, b) => a[0].charCodeAt(0) - b[0].charCodeAt(0)).map(([c, n]) => `U+${c.charCodeAt(0).toString(16).toUpperCase().padStart(4, "0")} x${n}`).join(", "); } static MetaPrefixes = [ "index ", "new file mode ", "deleted file mode ", "similarity index ", "rename from ", "rename to ", "Binary files ", "GIT binary patch", "mode change " ]; static IsMeta(line) { return _UnifiedDiffParser.MetaPrefixes.some((p) => line.startsWith(p)); } static ToFileLines(content) { return content ? content.replace(/\r\n/g, "\n").split("\n") : void 0; } static ToLineSet(lines) { return lines ? new Set(lines.map((l) => l.replace(/\s+$/, ""))) : void 0; } // Strips the longest common whitespace prefix from all non-empty diff body lines. // This normalizes diffs that are uniformly indented (e.g. pasted inside a markdown block // or another indented structure), while preserving the single-space context markers on // lines whose content happens to start with '+' or '-'. // // @@ hunk headers are excluded from the prefix calculation because an LLM may indent // body lines but not the header (or vice versa). The strip is applied only to lines // that actually start with the computed prefix, so unindented headers are left intact. // // Runs before the per-line StripIndent pass: this handles uniform indentation // (including a single-space indent that per-line stripping must not touch), while // StripIndent handles ragged 2+ whitespace indentation line by line. static StripCommonIndent(diff) { const lines = diff.split("\n"); let common = null; for (const line of lines) { if (line.length == 0) continue; const i = TextUtils.CountLeadingWhitespace(line); if (line.startsWith("@@", i)) continue; const prefix = line.substring(0, i); if (common == null) { common = prefix; continue; } const minLen = Math.min(common.length, prefix.length); let j = 0; while (j < minLen && common[j] == prefix[j]) j++; common = common.substring(0, j); if (common.length == 0) return diff; } if (common == null || common.length == 0) return diff; const c = common; if (!lines.some((l) => l.startsWith(c) && l.length > c.length && (l[c.length] == "+" || l[c.length] == "-"))) return diff; return lines.map((l) => l.startsWith(c) ? l.substring(c.length) : l).join("\n"); } static StripIndent(s, fileLines) { if (s == null || s.length == 0) return s; const stripped = s.replace(/^[ \t]{2,}/, ""); if (fileLines && stripped.length > 1 && stripped[0] === "-" && stripped[1] === " ") { return DiffContentHeuristics.ResolveHyphenSpaceAmbiguity(s, stripped, fileLines); } if (fileLines && stripped.length > 0 && stripped[0] === "+" && fileLines.some((f) => f.trim() == stripped.trimEnd())) return s; return DiffContentHeuristics.IsValidDiffLineStart(stripped) ? stripped : s; } static TryParseFileHeader(line) { if (!(line.startsWith("diff --git ") || line.startsWith("--- ") || line.startsWith("+++ "))) { return null; } const path = line.startsWith("diff --git ") ? _UnifiedDiffParser.Canon(line.split(" ").filter((s) => s.length > 0)[2]) : _UnifiedDiffParser.Canon(line.length >= 4 ? line.substring(4) : ""); return path; } // Inside a hunk body, `--- foo` could be deletion of `-- foo` (SQL comment, // prose) and `+++ foo` insertion of `++ foo` — or the next file's header. // Evidence ladder: a transition to a file we HOLD // is a real header; otherwise content decides — the delete reading wins when // the current file contains `-- foo` verbatim, the insert reading when the // body so far is a verified diff-shaped region (real header pairs never hit // this: their `+++` follows a header-read `---`, which ends the body). Only // in an evidence vacuum does the structure heuristic decide: real paths have // `.` (extension) or `/` (directory); prose doesn't. static IsFileHeader(line, path, inHunkBody, files, fileLineSet, body, fileLines) { if (!inHunkBody) return true; if (line.startsWith("diff --git ")) return true; if (path != "" && files?.has(path)) return true; if (line.startsWith("--- ") && fileLineSet != null && fileLineSet.has(line.slice(1).replace(/\s+$/, ""))) return false; if (line.startsWith("+++ ") && body != null && DiffContentHeuristics.IsDiffShapedRegion(body, fileLines)) return false; return _UnifiedDiffParser.LooksLikePath(path); } static LooksLikePath(path) { return path.includes(".") || path.includes("/") || path.includes("\\") || path === "/dev/null"; } static Canon(p) { if (p == null || p.length == 0) return p; p = p.trim(); if (p.startsWith("a/") || p.startsWith("b/")) p = p.substring(2); return p; } static TryParseHunkHeader(line) { const t = line.trim(); if (!t.startsWith("@@")) return null; let header = new Header(); const parts = t.replace(/^[@ ]+|[@ ]+$/g, "").split(" ").filter((s) => s.length > 0); if (parts.length > 0) header = Header.ParseSide(header, parts[0], "-", true); if (parts.length > 1) header = Header.ParseSide(header, parts[1], "+", false); return header; } }; // ../node_modules/.pnpm/matchu-patchu@0.3.3/node_modules/matchu-patchu/dist/anchorer/hunkSlicer.js var Block = class { StartIndex; EndIndex; DeleteLines; InsertLines; constructor(StartIndex, EndIndex, DeleteLines, InsertLines) { this.StartIndex = StartIndex; this.EndIndex = EndIndex; this.DeleteLines = DeleteLines; this.InsertLines = InsertLines; } }; var SlicedHunk = class { Hunk; Blocks; constructor(Hunk2, Blocks) { this.Hunk = Hunk2; this.Blocks = Blocks; } }; var HunkSlicer = class _HunkSlicer { static Slice(hunks) { return Array.from(hunks).map((h) => new SlicedHunk(h, Array.from(_HunkSlicer.SliceHunk(h)))); } static *SliceHunk(hunk) { const lines = hunk.Lines; if (lines == null || lines.length == 0) { return; } let i = 0; while (i < lines.length) { if (lines[i].Type == LineType.Context) { i++; continue; } const start = i; const del = []; const ins = []; while (i < lines.length && lines[i].Type != LineType.Context) { const l = lines[i++]; if (l.Type == LineType.Delete) del.push(l.Text); else if (l.Type == LineType.Insert) ins.push(l.Text); } const end = i - 1; if (del.length > 0 || ins.length > 0) yield new Block(start, end, del, ins); } } }; // ../node_modules/.pnpm/matchu-patchu@0.3.3/node_modules/matchu-patchu/dist/anchorer/search.js var Match = class { LineIndex; Fuzz; constructor(LineIndex, Fuzz) { this.LineIndex = LineIndex; this.Fuzz = Fuzz; } }; var Search = class _Search { // Whitespace matching modes with their per-line fuzz static StrictnessPasses = [ { Transform: (s) => s, Fuzz: 0 }, // exact { Transform: (s) => s.trimEnd(), Fuzz: 1 }, // rstrip { Transform: (s) => s.trim(), Fuzz: 100 }, // strip { Transform: (s) => TextUtils.NormalizeHomoglyphs(s).trim(), Fuzz: 200 }, // homoglyphs + strip { Transform: (s) => TextUtils.NormalizeHomoglyphs(TextUtils.StripInvisibles(s)).trim(), Fuzz: 300 }, // invisibles stripped + homoglyphs + strip { Transform: (s) => TextUtils.NormalizeHomoglyphs(TextUtils.FoldCanonicalAndFullwidth(TextUtils.StripInvisibles(s))).trim(), Fuzz: 400 } // + NFC and fullwidth folds ]; /// /// Attempts to locate inside , /// applying the strictness passes in order, strictest first. /// Returns a FindResult with Index == -1 if no match. /// Passes whose per-line fuzz exceeds are skipped: /// callers that treat a match as evidence the file already contains specific text /// (rather than as an anchor to edit) pass 100 to exclude the homoglyph and /// invisible-stripping passes. /// static Find(haystack, needle, startIndex = 0, maxPassFuzz = Number.MAX_SAFE_INTEGER) { if (needle.length == 0) return new Match(Math.min(Math.max(startIndex, 0), haystack.length), 0); if (haystack.length < needle.length) return new Match(-1, 0); for (const { Transform: transform, Fuzz: fuzz } of _Search.StrictnessPasses.filter((p) => p.Fuzz <= maxPassFuzz)) { const transformedNeedle = Array.from(needle).map(transform); for (let i = startIndex; i <= haystack.length - needle.length; i++) { let match = true; for (let j = 0; j < needle.length; j++) { if (transform(haystack[i + j]) != transformedNeedle[j]) { match = false; break; } } if (!match) continue; let affected = 0; for (let k = 0; k < needle.length; k++) { if (transform(haystack[i + k]) != haystack[i + k] || transformedNeedle[k] != needle[k]) affected++; } return new Match(i, fuzz * affected); } } return new Match(-1, 0); } /// /// True when two lines are equal under any of the strictness passes the anchor /// search itself uses. Used to revalidate context at a line-number-tiebroken slot: /// the revalidation must not be stricter than the search that produced the /// candidates, or slots the search considered equivalent (indent slop, homoglyphs) /// get rejected loudly instead of applied. /// static LinesEquivalent(a, b) { return _Search.StrictnessPasses.some((p) => p.Transform(a) == p.Transform(b)); } /// /// Attempts to find a unique match and reports ambiguity without requiring a separate call. /// Success=true when exactly one match; Ambiguous=true when more than one match exists; both false when none. /// static FindUnique(haystack, needle) { const first = _Search.Find(haystack, needle); if (first.LineIndex < 0) return UniqueMatch.NotFound; const second = _Search.Find(haystack, needle, first.LineIndex + 1); if (second.LineIndex >= 0) return UniqueMatch.Ambiguous; return new UniqueMatch(MatchState.Success, first.LineIndex, first.Fuzz); } /// /// Finds the best insertion anchor for a pure-insert hunk using provided context lines. /// Attempts, in order: full pattern (pre+post), pre-only (insert after pre), post-only (insert before post). /// If none uniquely match, returns a non-success result with Ambiguous flag when any side matches somewhere. /// static FindContextAnchor(haystack, contextBefore, contextAfter) { const preCount = contextBefore.length; let sawAmbiguity = false; const searchPatterns = [ { pattern: contextBefore.concat(contextAfter), insertIndex: preCount, condition: preCount + contextAfter.length > 0 }, { pattern: contextBefore, insertIndex: preCount, condition: preCount > 0 }, { pattern: contextAfter, insertIndex: 0, condition: contextAfter.length > 0 } ]; for (const { pattern, insertIndex, condition } of searchPatterns) { if (!condition) continue; const match = _Search.FindUnique(haystack, pattern); if (match.IsSuccess) return new UniqueMatch(MatchState.Success, match.LineIndex + insertIndex, match.Fuzz); if (match.IsAmbiguous) sawAmbiguity = true; } return new UniqueMatch(sawAmbiguity ? MatchState.Ambiguous : MatchState.NotFound); } /// /// Returns every position in where /// matches exactly (no whitespace fuzz). Used for line-number tiebreaking among /// ambiguous candidates — we deliberately exclude fuzzy matches so the candidate set /// reflects only positions the LLM could plausibly have meant. /// static FindAllExact(haystack, needle) { const results = []; if (needle.length == 0) return results; for (let i = 0; i + needle.length <= haystack.length; i++) if (needle.every((v, j) => haystack[i + j] === v)) results.push(i); return results; } static TiebreakByLineNumber(candidates, expectedLine) { if (candidates.length == 0) return null; if (candidates.length == 1) return candidates[0]; const sorted = ArrayUtils.OrderBy(candidates.map((c) => ({ line: c, dist: Math.abs(c - expectedLine) })), (x) => x.dist); if (sorted[0].dist == 0) return sorted[0].line; const gapToNext = sorted[1].dist - sorted[0].dist; if (gapToNext > sorted[0].dist * 10) return sorted[0].line; return null; } }; // ../node_modules/.pnpm/matchu-patchu@0.3.3/node_modules/matchu-patchu/dist/anchorer/blockAnchorer.js var BlockAnchorer = class _BlockAnchorer { static AnchorBlocks(hunk, blocks, lines, options) { return blocks.map((focus, index) => { let aboveEdge = focus.StartIndex; let belowEdge = focus.EndIndex; let aboveBlockIdx = index - 1; let belowBlockIdx = index + 1; let chunk = null; while (true) { const aboveBound = aboveBlockIdx >= 0 ? blocks[aboveBlockIdx].EndIndex + 1 : 0; const belowBound = belowBlockIdx < blocks.length ? blocks[belowBlockIdx].StartIndex - 1 : hunk.Lines.length - 1; for (let k = 1; k <= options.ContextWindowMax; k++) { const ctxAbove = _BlockAnchorer.ContextLines(hunk, aboveEdge - 1, aboveBound, k, true); const ctxBelow = _BlockAnchorer.ContextLines(hunk, belowEdge + 1, belowBound, k, false); const needle = _BlockAnchorer.BuildNeedle(hunk, aboveEdge, belowEdge); const match = _BlockAnchorer.Anchor(lines, ctxAbove, needle, ctxBelow); chunk = new Chunk(aboveEdge == focus.StartIndex ? ctxAbove : _BlockAnchorer.ContextLines(hunk, focus.StartIndex - 1, 0, k, true), focus.DeleteLines, focus.InsertLines, belowEdge == focus.EndIndex ? ctxBelow : _BlockAnchorer.ContextLines(hunk, focus.EndIndex + 1, hunk.Lines.length - 1, k, false), _BlockAnchorer.AlignMatchToFocus(match, hunk, aboveEdge, focus.StartIndex), new DiffLocation(hunk, focus.StartIndex, focus.EndIndex)); if (!chunk.Match.IsAmbiguous) return chunk; if (ctxAbove.length < k && ctxBelow.length < k) break; } if (aboveBlockIdx < 0 && belowBlockIdx >= blocks.length) break; const ctxAboveCount = aboveBlockIdx >= 0 ? aboveEdge - (blocks[aboveBlockIdx].EndIndex + 1) : Number.MAX_SAFE_INTEGER; const ctxBelowCount = belowBlockIdx < blocks.length ? blocks[belowBlockIdx].StartIndex - (belowEdge + 1) : Number.MAX_SAFE_INTEGER; if (ctxBelowCount <= ctxAboveCount && belowBlockIdx < blocks.length) belowEdge = blocks[belowBlockIdx++].EndIndex; else aboveEdge = blocks[aboveBlockIdx--].StartIndex; } if (chunk.Match.IsAmbiguous && hunk.OldStart >= 0) { const tiebroken = _BlockAnchorer.TryLineNumberTiebreak(hunk, focus, lines); if (tiebroken != null) chunk = chunk.with({ Match: tiebroken }); } return chunk; }); } static TryLineNumberTiebreak(hunk, focus, lines) { let expectedLine = hunk.OldStart - 1; for (let i = 0; i < focus.StartIndex; i++) if (hunk.Lines[i].Type != LineType.Insert) expectedLine++; if (focus.DeleteLines.length == 0) { if (expectedLine < 0 || expectedLine > lines.length) return null; if (!_BlockAnchorer.CtxMatchesAtSlot(hunk, focus, lines, expectedLine)) return null; return new UniqueMatch(MatchState.Success, expectedLine); } const candidates = Search.FindAllExact(lines, focus.DeleteLines); const resolved = Search.TiebreakByLineNumber(candidates, expectedLine); return resolved != null && _BlockAnchorer.CtxMatchesAtSlot(hunk, focus, lines, resolved) ? new UniqueMatch(MatchState.Success, resolved) : null; } // Validates the hunk's immediate context lines against a resolved slot's // neighbours so a stale or shifted header errs loudly instead of silently // mis-placing the edit. Content-bearing context compares under the search's own // strictness ladder — the revalidation must not be stricter than the search that // produced the candidates, or indent-slopped slots the search matched get // rejected loudly. But a line that trims to near-nothing (brace/paren-only) // carries its signal IN the indentation, which every ladder pass beyond rstrip // erases — such context stays whitespace-right-trimmed strict, or a shifted // header walks the edit to any same-shaped brace. Both tolerances corpus-measured // 2026-07-06 (ladder-everywhere: 3 silent mis-applies; this gate: 0, +14 recall). // Serves both callers: a pure insert (DeleteLines empty, after-context sits at // `slot`) and a delete-block tiebreak (deletes occupy [slot, slot + DeleteLines.length)). static CtxMatchesAtSlot(hunk, focus, lines, slot) { const MinLadderContent = 3; const matches = (hunkIdx, lineIdx) => { if (hunkIdx < 0 || hunkIdx >= hunk.Lines.length || hunk.Lines[hunkIdx].Type != LineType.Context) return true; if (lineIdx < 0 || lineIdx >= lines.length) return false; const text = hunk.Lines[hunkIdx].Text; return text.trim().length >= MinLadderContent ? Search.LinesEquivalent(lines[lineIdx], text) : lines[lineIdx].trimEnd() == text.trimEnd(); }; return matches(focus.StartIndex - 1, slot - 1) && matches(focus.EndIndex + 1, slot + focus.DeleteLines.length); } static AlignMatchToFocus(match, hunk, needleStartIndex, focusStartIndex) { if (!match.IsSuccess) return match; return new UniqueMatch(match.State, match.LineIndex + hunk.Lines.slice(needleStartIndex, focusStartIndex).filter((l) => l.Type != LineType.Insert).length, match.Fuzz); } static ContextLines(hunk, startLine, bound, maxTake, isAbove) { if (maxTake <= 0) return []; const lines = []; for (let i = 0; i < maxTake; i++) { const index = startLine + (isAbove ? -i : i); if (!(index >= 0 && index < hunk.Lines.length)) break; if (!(isAbove ? index >= bound : index <= bound)) break; const line = hunk.Lines[index]; if (line.Type != LineType.Context) break; lines.push(line.Text); } return isAbove ? [...lines].reverse() : lines; } static BuildNeedle(h, startIndex, endIndex) { return h.Lines.slice(startIndex, endIndex + 1).filter((line) => line.Type != LineType.Insert).map((line) => line.Text); } static Anchor(lines, contextAbove, deleteLines, contextBelow) { if (deleteLines.length == 0) return Search.FindContextAnchor(lines, contextAbove, contextBelow); const pattern = contextAbove.concat(deleteLines).concat(contextBelow); const match = Search.FindUnique(lines, pattern); if (match.IsAmbiguous) return UniqueMatch.Ambiguous; if (match.IsSuccess) return new UniqueMatch(match.State, match.LineIndex + contextAbove.length, match.Fuzz); return Search.FindUnique(lines, deleteLines); } }; // ../node_modules/.pnpm/matchu-patchu@0.3.3/node_modules/matchu-patchu/dist/anchorer/hunkAnchorer.js var AnchorOutcome = class { Chunks; AlreadyAppliedCount; constructor(Chunks, AlreadyAppliedCount) { this.Chunks = Chunks; this.AlreadyAppliedCount = AlreadyAppliedCount; } }; var HunkAnchorer = class _HunkAnchorer { static Anchor(originalText, hunks, options) { const lines = TextUtils.ToLines(originalText); const slicedHunks = HunkSlicer.Slice(hunks); const chunks = []; for (const sliced of slicedHunks) for (const chunk of BlockAnchorer.AnchorBlocks(sliced.Hunk, sliced.Blocks, lines, options)) chunks.push(_HunkAnchorer.UseFallbackAnchorIfNecessary(sliced.Hunk, chunk, lines, slicedHunks.length == 1)); const deleteCounts = /* @__PURE__ */ new Map(); for (const l of chunks.flatMap((c) => c.DeleteLines)) if (l.trim().length > 0) deleteCounts.set(l, (deleteCounts.get(l) ?? 0) + 1); const movedFromElsewhere = (c, line) => (deleteCounts.get(line) ?? 0) > c.DeleteLines.filter((d) => d === line).length; const kept = chunks.filter((c) => !_HunkAnchorer.AppliedAtAnchor(lines, c) && (c.InsertLines.some((l) => movedFromElsewhere(c, l)) || !_HunkAnchorer.AlreadyApplied(lines, c))).map((c) => _HunkAnchorer.TrySubLineSplice(c, lines)); return new AnchorOutcome(kept, chunks.length - kept.length); } static UseFallbackAnchorIfNecessary(hunk, chunk, lines, soleHunk) { const lineCount = lines.length; if (chunk.Match.IsNotFound && hunk.Lines.length == chunk.InsertLines.length) { const targetIsEmpty = lineCount == 0 || lineCount == 1 && lines[0].length == 0; if (hunk.OldStart < 0 && !soleHunk && !targetIsEmpty) return chunk; const anchorLine = hunk.OldStart <= 1 ? 0 : hunk.OldStart; if (anchorLine > lineCount) return chunk; return new Chunk(chunk.ContextBefore, chunk.DeleteLines, chunk.InsertLines, chunk.ContextAfter, new UniqueMatch(MatchState.Success, anchorLine), chunk.DiffLocation); } return chunk; } // Sub-line splice fallback: a delete line that matches no file line but occurs // as a substring of exactly one line is an author-quoted fragment — an author // cannot intend to delete text they never mentioned, so the only coherent // reading replaces the fragment within the line and preserves the rest. Every // guard failure means fragment-vs-mangled-whole-line is undecidable: the chunk // stays NotFound and errors loudly. The insert-side guards catch damaged // whole-line edits: an insert that echoes text from outside the fragment, or // dwarfs it, was authored by someone who saw the whole line. // MUST run only on chunks that survived the already-applied filter: an applied // edit that wrapped the old line (comment-out, prefix insertion) leaves the // delete line as a substring of the new line, and splicing it again would // stack the wrapper (corpus wave-1 case 177: "#x" became "##x"). static TrySubLineSplice(chunk, lines) { if (!chunk.Match.IsNotFound || chunk.DeleteLines.length != 1 || chunk.InsertLines.length > 1) return chunk; const frag = chunk.DeleteLines[0]; if (frag.trim().length < _HunkAnchorer.MinSpliceFragmentContent) return chunk; let lineIndex = -1; for (let i = 0; i < lines.length; i++) { const first = lines[i].indexOf(frag); if (first < 0) continue; const again = lines[i].indexOf(frag, first + 1) >= 0; if (lineIndex >= 0 || again) return chunk; lineIndex = i; } if (lineIndex < 0) return chunk; const target = lines[lineIndex]; const at = target.indexOf(frag); const prefix = target.slice(0, at), suffix = target.slice(at + frag.length); const insert = chunk.InsertLines.length == 1 ? chunk.InsertLines[0] : ""; if (prefix.length == 0 && suffix.length > 0) return chunk; if (insert.length == 0 && prefix.length > 0 && suffix.length > 0 && /\s/.test(prefix[prefix.length - 1]) && /\s/.test(suffix[0])) return chunk; const remnantOk = (r) => r.length == 0 || r.trim().length >= _HunkAnchorer.MinSpliceRemnantContent; const echoes = (r) => r.length > 0 && insert.includes(r.trim()); if (!remnantOk(prefix) || !remnantOk(suffix) || echoes(prefix) || echoes(suffix) || insert.length > frag.length * 2 + 8 && _HunkAnchorer.SharedStem(frag, insert) < _HunkAnchorer.MinSpliceStemContent) return chunk; return new Chunk([], [target], [prefix + insert + suffix], [], new UniqueMatch(MatchState.Success, lineIndex, _HunkAnchorer.SubLineSpliceFuzz), chunk.DiffLocation); } // Trimmed length a fragment must reach before substring evidence counts. static MinSpliceFragmentContent = 8; // Trimmed length a non-empty out-of-fragment remnant must carry. static MinSpliceRemnantContent = 4; // Trimmed length the fragment/insert shared stem must reach to waive the dwarf cap. static MinSpliceStemContent = 8; // The longest run the insert shares with the fragment's start or end, in trimmed // chars — the fragment-edit evidence that waives the dwarf cap. static SharedStem(frag, insert) { let p = 0; while (p < frag.length && p < insert.length && frag[p] == insert[p]) p++; let s = 0; while (s < frag.length && s < insert.length && frag[frag.length - 1 - s] == insert[insert.length - 1 - s]) s++; return Math.max(frag.slice(0, p).trim().length, s == 0 ? 0 : frag.slice(-s).trim().length); } // Reported fuzz for a spliced edit: looser than every line-match pass. static SubLineSpliceFuzz = 500; // A sanity check to avoid errors for a chunk that is already applied. // Asymmetric strictness: insert-side evidence is capped at whitespace-level // passes (a homoglyph variant of the post-image is not proof the edit landed, // and would turn a loud MatchNotFound into a silent no-op), while the delete // side stays uncapped (finding the pre-image even loosely proves the edit is // still pending, blocking a false already-applied verdict). static AlreadyApplied(lines, c) { if (!c.HasContextLines() && (c.IsPureDelete || c.IsPureInsert)) return false; const insertImage = c.HasContextLines() ? c.InsertLinesWithContext() : c.InsertLines; const deleteImage = c.HasContextLines() ? c.DeleteLinesWithContext() : c.DeleteLines; const insertAt = Search.Find(lines, insertImage, 0, _HunkAnchorer.MaxAppliedEvidenceFuzz); return insertAt.LineIndex != -1 && Search.Find(lines, deleteImage).LineIndex == -1 && !_HunkAnchorer.OldImagePresentElsewhere(lines, c.DeleteLines, c.IsPureDelete ? null : insertImage, insertAt.LineIndex); } // Veto on the already-applied verdict: "old image absent" cannot // distinguish an applied edit from a delete block damaged in transit — one bad // char defeats every search pass — so a window nearly matching the delete block // proves the old image is still in the file and the edit pending: keep the chunk // and let it error loudly. Refinements, each forced by a corpus counter-example: // - Windows overlapping an insert-image occurrence never veto: after a genuine // apply the region shares most lines with the old image (rewrites re-state // unchanged lines as -/+), and vetoing there would break idempotent re-apply. // - Pure deletes skip that exclusion (exclusionImage = null): their post-image // is thin context that occurs everywhere — even inside the pending block // itself — so its occurrences prove nothing. // - The window is the delete block WITHOUT context: context sits in both images // and would glue the pending old image to a context match, masking it. // - Only content-bearing lines vote, with an absolute floor: blank/brace lines // near-match everywhere and tiny blocks carry too little signal either way. static OldImagePresentElsewhere(lines, dels, exclusionImage, insertIndex) { const contentIdx = [...dels.keys()].filter((i) => dels[i].trim().length > 0); if (contentIdx.length < _HunkAnchorer.MinNearImageMatches) return false; const insertStarts = exclusionImage == null ? [] : [...Search.FindAllExact(lines, exclusionImage), insertIndex]; const overlapsInsert = (start) => insertStarts.some((s) => start < s + exclusionImage.length && s < start + dels.length); for (let start = 0; start + dels.length <= lines.length; start++) { if (overlapsInsert(start)) continue; const matched = contentIdx.filter((j) => lines[start + j] === dels[j]).length; if (matched >= _HunkAnchorer.MinNearImageMatches && matched * 2 > contentIdx.length) return true; } return false; } // Exact-matching content lines required before a near-old-image window counts. static MinNearImageMatches = 3; // Highest per-line fuzz (strip pass) that still counts as already-applied evidence. static MaxAppliedEvidenceFuzz = 100; // A successfully anchored pure-insert chunk whose full post-image (context + // inserted lines + context) already sits at the resolved slot — or immediately // before it — would duplicate itself if applied: the file already reflects this // chunk. Including the context lines in the comparison distinguishes "already // applied" from a diff that intentionally duplicates adjacent lines (there the // old-side context sits where the copy would be, so the post-image cannot match). static AppliedAtAnchor(lines, c) { if (!c.IsPureInsert || !c.Match.IsSuccess) return false; const post = c.InsertLinesWithContext(); const slot = c.Match.LineIndex; return _HunkAnchorer.RegionEquals(lines, slot - c.ContextBefore.length, post) || _HunkAnchorer.RegionEquals(lines, slot - c.ContextBefore.length - c.InsertLines.length, post); } static RegionEquals(lines, start, region) { if (start < 0 || start + region.length > lines.length) return false; for (let i = 0; i < region.length; i++) if (lines[start + i] !== region[i]) return false; return true; } }; // ../node_modules/.pnpm/matchu-patchu@0.3.3/node_modules/matchu-patchu/dist/selectionTarget.js var SelectionTarget = class { FullText; SelectedText; UseSelection = false; LineOffset = 0; get TargetText() { return this.UseSelection ? this.SelectedText : this.FullText; } constructor(fullText, selection = "") { this.FullText = fullText; this.SelectedText = selection; if (!(this.SelectedText == null || this.SelectedText.length == 0)) { const hay = TextUtils.ToLines(this.FullText); const needle = TextUtils.ToLines(this.SelectedText); const result = Search.Find(hay, needle); this.UseSelection = result.LineIndex >= 0; this.LineOffset = this.UseSelection ? Math.max(0, result.LineIndex) : 0; } } Replace(replace) { if (!this.UseSelection) return replace; const fullLines = TextUtils.ToLines(this.FullText); const selectionLines = TextUtils.ToLines(this.SelectedText); const replaceLines = TextUtils.ToLines(replace); const edit = new Edit(this.LineOffset, selectionLines, replaceLines); edit.ApplyTo(fullLines); return TextUtils.RoundTripWhitespace(this.FullText, fullLines); } }; // ../node_modules/.pnpm/matchu-patchu@0.3.3/node_modules/matchu-patchu/dist/parser/sanitizers/shared.js function isDiffMarker(line) { return line.startsWith("--- ") || line.startsWith("+++ ") || line.startsWith("@@") || line.startsWith("diff --git"); } function isHunkBoundary(line, trimmed) { return isDiffMarker(line) || trimmed.startsWith("```"); } function isValidPrefix(char) { return char === "+" || char === "-" || char === " "; } function transformOutsideHunkBodies(text, transform) { let remainingOld = 0, remainingNew = 0; let inBareBody = false; const kept = []; for (const line of text.split("\n")) { const trimmed = line.trim(); const m = /^@@ -\d+(?:,(\d+))? \+\d+(?:,(\d+))? @@/.exec(trimmed); if (m) { remainingOld = m[1] !== void 0 ? parseInt(m[1]) : 1; remainingNew = m[2] !== void 0 ? parseInt(m[2]) : 1; inBareBody = false; kept.push(line); continue; } if (remainingOld > 0 || remainingNew > 0) { const c = line.length > 0 ? line[0] : " "; if (c === "-") remainingOld--; else if (c === "+") remainingNew--; else { remainingOld--; remainingNew--; } kept.push(line); continue; } if (trimmed.startsWith("@@")) { inBareBody = true; kept.push(line); continue; } if (inBareBody && line.length > 0 && isValidPrefix(line[0]) && !isLikelyFileHeader(trimmed)) { kept.push(line); continue; } inBareBody = false; const t = transform(line); if (t !== null) kept.push(t); } return kept.join("\n"); } function isLikelyFileHeader(trimmed) { return trimmed.startsWith("diff --git") || (trimmed.startsWith("--- ") || trimmed.startsWith("+++ ")) && (trimmed.includes(".") || trimmed.includes("/") || trimmed.includes("\\")); } // ../node_modules/.pnpm/matchu-patchu@0.3.3/node_modules/matchu-patchu/dist/parser/sanitizers/customFormatSanitizer.js var CustomFormatSanitizer = class _CustomFormatSanitizer { static OPERATIONS = [ { header: /^\s*\*\*\* Update File: (.+)$/i, fromPath: (p) => `a/${p}`, toPath: (p) => `b/${p}` }, { header: /^\s*\*\*\* Add File: (.+)$/i, fromPath: (_p) => "/dev/null", toPath: (p) => `b/${p}` }, { header: /^\s*\*\*\* Delete File: (.+)$/i, fromPath: (p) => `a/${p}`, toPath: (_p) => "/dev/null" } ]; static process(text) { return transformOutsideHunkBodies(text, (line) => { const hadCr = line.endsWith("\r"); const core = hadCr ? line.slice(0, -1) : line; for (const op of _CustomFormatSanitizer.OPERATIONS) { const m = core.match(op.header); if (!m) continue; const path = m[1].trim(); const tail = hadCr ? "\r" : ""; return `--- ${op.fromPath(path)}${tail} +++ ${op.toPath(path)}${tail}`; } return line; }); } }; // ../node_modules/.pnpm/matchu-patchu@0.3.3/node_modules/matchu-patchu/dist/parser/sanitizers/decoratedHeaderSanitizer.js var DecoratedHeaderSanitizer = class _DecoratedHeaderSanitizer { static DECORATED_HEADER_REGEX = /^(---|\+\+\+)\s+(.+?)\s+(---|\+\+\+)+\s*$/; static process(text) { return transformOutsideHunkBodies(text, (line) => { const hadCr = line.endsWith("\r"); const core = hadCr ? line.slice(0, -1) : line; const replaced = core.replace(_DecoratedHeaderSanitizer.DECORATED_HEADER_REGEX, "$1 $2"); return replaced === core ? line : hadCr ? replaced + "\r" : replaced; }); } }; // ../node_modules/.pnpm/matchu-patchu@0.3.3/node_modules/matchu-patchu/dist/parser/sanitizers/decorativeMarkerSanitizer.js var DecorativeMarkerSanitizer = class _DecorativeMarkerSanitizer { static process(text) { return transformOutsideHunkBodies(text, (line) => _DecorativeMarkerSanitizer.isDecorativeMarker(line.trim()) ? null : line); } static isDecorativeMarker(line) { if (!line) return false; if (/^diff --git\s+/.test(line)) return false; if (/^[*#_]{3,}$/.test(line)) return true; if (/^[=\-]{5,}$/.test(line)) return true; const wholeLineKeyword = /^[*=\-#_\s]*(BEGIN|END|START|STOP)\s+(PATCH|DIFF|HUNK|SECTION|BLOCK)[*=\-#_\s]*$/i.test(line); if (wholeLineKeyword && /[*=\-#_]{2,}/.test(line)) return true; return false; } }; // ../node_modules/.pnpm/matchu-patchu@0.3.3/node_modules/matchu-patchu/dist/parser/sanitizers/backtickSanitizer.js var BacktickSanitizer = class { static process(text) { return text.replace(/^```+\s*diff\b/gmi, "```diff").replace(/^```+$/gm, "```"); } }; // ../node_modules/.pnpm/matchu-patchu@0.3.3/node_modules/matchu-patchu/dist/parser/sanitizers/diffBlockReunifier.js var DiffBlockReunifier = class { static process(text) { const lines = text.split("\n"); const result = []; let inDiff = false; for (let i = 0; i < lines.length; i++) { const line = lines[i]; const trimmed = line.trim(); if (trimmed.startsWith("```diff")) { inDiff = true; result.push(line); } else if (trimmed === "```" && line.startsWith("```")) { if (inDiff && this.hasDiffContentBeforeNextFence(lines, i)) { continue; } inDiff = false; result.push(line); } else { result.push(line); } } return result.join("\n"); } static hasDiffContentBeforeNextFence(lines, currentIndex) { for (let j = currentIndex + 1; j < lines.length; j++) { const ahead = lines[j].trim(); if (lines[j].startsWith("```")) return false; if (isDiffMarker(ahead)) return true; } return false; } }; // ../node_modules/.pnpm/matchu-patchu@0.3.3/node_modules/matchu-patchu/dist/parser/sanitizers/devNullSanitizer.js var DevNullSanitizer = class { static process(text, fileKeys) { if (!fileKeys?.length) return text; const existingFiles = new Set(fileKeys); const lines = text.split("\n"); const result = []; for (let i = 0; i < lines.length; i++) { const line = lines[i]; const trimmed = line.trim(); if (this.isNewFileModeForExistingFile(trimmed, lines, i, existingFiles)) { continue; } if (trimmed.startsWith("--- /dev/null")) { const targetFile = this.getTargetFileFromNextLine(lines[i + 1]); if (targetFile && existingFiles.has(targetFile)) { result.push(line.replace("/dev/null", `a/${targetFile}`)); continue; } } result.push(line); } return result.join("\n"); } static isNewFileModeForExistingFile(line, allLines, currentIndex, existingFiles) { if (!line.startsWith("new file mode")) return false; const prevLine = allLines[currentIndex - 1]?.trim() || ""; if (!prevLine.startsWith("diff --git")) return false; const match = prevLine.match(/diff --git\s+[ab]\/(\S+)\s+[ab]\/\S+/); const filePath = match?.[1]; return filePath ? existingFiles.has(filePath) : false; } static getTargetFileFromNextLine(nextLine) { if (!nextLine?.trim().startsWith("+++ ")) return null; const match = nextLine.match(/^\+\+\+\s+(?:[ab]\/)?(\S+)/); return match?.[1] ?? null; } }; // ../node_modules/.pnpm/matchu-patchu@0.3.3/node_modules/matchu-patchu/dist/parser/sanitizers/prefixInjector.js var PrefixInjector = class { static process(text) { const lines = text.split("\n"); let inHunk = false; let deletes = { remaining: 0, consumed: 0 }; let adds = { remaining: 0, consumed: 0 }; for (let i = 0; i < lines.length; i++) { const line = lines[i]; if (line.length === 0 && i === lines.length - 1) continue; const trimmed = line.trim(); if (trimmed.startsWith("@@")) { const { deleteCount, addCount } = this.parseHunkHeader(trimmed); inHunk = true; deletes = { remaining: deleteCount, consumed: 0 }; adds = { remaining: addCount, consumed: 0 }; continue; } if (isHunkBoundary(line, trimmed)) { inHunk = false; continue; } if (inHunk) { const first = line[0] ?? ""; const hasValidPrefix = isValidPrefix(first); const prefix = hasValidPrefix ? first : this.inferPrefix(deletes.remaining - deletes.consumed, adds.remaining - adds.consumed); if (!hasValidPrefix && prefix !== null) { lines[i] = prefix + line; } if (prefix === "+") adds.consumed++; else if (prefix === "-") deletes.consumed++; else { adds.consumed++; deletes.consumed++; } } } return lines.join("\n"); } static parseHunkHeader(header) { const match = header.match(/@@\s*-(\d+)(?:,(\d+))?\s*\+(\d+)(?:,(\d+))?\s*@@/); return match ? { deleteCount: parseInt(match[2] ?? "1"), addCount: parseInt(match[4] ?? "1") } : { deleteCount: 0, addCount: 0 }; } // Only inject when the header arithmetic is unambiguous. When both deletes and // adds remain (or the header had no usable counts), leave the line untouched — // the parser's raw-line heuristics use content evidence and insertion-lookahead // to classify it better than count-based guessing can. static inferPrefix(remainingDeletes, remainingAdds) { if (remainingDeletes > 0 && remainingAdds <= 0) return "-"; if (remainingAdds > 0 && remainingDeletes <= 0) return "+"; return null; } }; // ../node_modules/.pnpm/matchu-patchu@0.3.3/node_modules/matchu-patchu/dist/parser/diffSanitizer.js var DiffSanitizer = class { static Process(diff, fileKeys) { if (!diff?.trim()) return diff; let result = CustomFormatSanitizer.process(diff); result = DecoratedHeaderSanitizer.process(result); result = DecorativeMarkerSanitizer.process(result); result = BacktickSanitizer.process(result); result = DiffBlockReunifier.process(result); result = DevNullSanitizer.process(result, fileKeys); result = PrefixInjector.process(result); return result; } }; // ../node_modules/.pnpm/matchu-patchu@0.3.3/node_modules/matchu-patchu/dist/headerlessRouter.js var HeaderlessRouter = class { static Route(fileHunks, files, options) { const group = fileHunks.find((g) => g.Key == ""); if (group == null || files.some((f) => f.Key == "")) return []; const probeOptions = Object.assign(new PatchOptions(), options, { ContinueOnError: true }); const candidates = files.filter((f) => { const target = new SelectionTarget(f.InputFullText, f.InputSelectedText).TargetText; const anchored = HunkAnchorer.Anchor(target, group.Hunks, probeOptions); return ChunkApplier.Apply(target, anchored.Chunks, probeOptions).Errors.length == 0; }); if (candidates.length == 1) { fileHunks.splice(fileHunks.indexOf(group), 1); const existing = fileHunks.find((g) => g.Key == candidates[0].Key); if (existing) existing.Hunks.push(...group.Hunks); else fileHunks.push(new FileHunkGroup(candidates[0].Key, group.Hunks)); return []; } return [candidates.length == 0 ? new PatchError("MatchNotFound", group.Hunks[0].ToUnanchoredChunk(), null, "The diff has no file headers and its content did not match any file being patched. Make sure context and deleted lines match the target file, and add '--- ' / '+++ ' headers naming it.") : new PatchError("MatchAmbiguous", group.Hunks[0].ToUnanchoredChunk(), candidates.map((f) => f.Key).join(", "), "The diff has no file headers and its content matched more than one file being patched. Add '--- ' / '+++ ' headers naming the intended file.")]; } }; // ../node_modules/.pnpm/matchu-patchu@0.3.3/node_modules/matchu-patchu/dist/patchNotices.js var PatchNotices = class { static For(hunks, file, contextOnlyHunkLines) { const notices = []; if (file.Edits.length === 0) notices.push(file.AlreadyAppliedCount > 0 ? `Note: no edits were applied because the file already contains this change (${file.AlreadyAppliedCount} chunk(s) detected as already applied).` : "Note: no edits were applied \u2014 the diff parsed but expressed no change for this file."); if (file.Fuzz > 0) notices.push(`Note: applied with fuzz factor ${file.Fuzz} \u2014 the diff did not match exactly, so confirm the edits landed where you intended.`); const suspects = TextUtils.DescribeSuspectCodePoints(file.Edits.flatMap((e) => e.InsertLines)); if (suspects) notices.push(`Warning: written lines contain invisible or lookalike characters: ${suspects}. If these were authored as \\uXXXX escapes or plain ASCII, the tool-call layer may have mangled them; verify the file content.`); const collapsed = hunks.flatMap((h) => h.Lines).filter((l) => l.CollapsedFrom != null).map((l) => l.CollapsedFrom); if (collapsed.length > 0) notices.push(`Note: ${collapsed.length} body line(s) starting '++' were read as sloppy doubled insert markers and written without the leading '+' (first: "${collapsed[0]}"). If the inserted content was meant to keep a '+' prefix (diff-shaped payload), verify the file.`); if (hunks.some((h) => h.TruncationSuspected)) notices.push("Note: the diff's final hunk looks truncated (its header declares more lines than the body delivered). The delivered lines were applied; verify the tail of that change landed."); if (hunks.some((h) => h.ControlCharsSuspected)) notices.push("Note: insert lines contained raw control characters and were written verbatim. That is usually transport damage rather than intended content; verify the written bytes."); if (contextOnlyHunkLines.length > 0) notices.push(`Note: the hunk${contextOnlyHunkLines.length === 1 ? "" : "s"} at diff line ${contextOnlyHunkLines.join(", ")} stated no change (context only), so nothing was applied there while the other edits landed. If an edit was intended, its '+'/'-' lines were lost \u2014 re-send that hunk and verify.`); return notices; } }; // ../node_modules/.pnpm/matchu-patchu@0.3.3/node_modules/matchu-patchu/dist/patcher.js var Patcher = class { static Apply(diff, files, options) { options = options ?? new PatchOptions(); const fileKeys = files.map((f) => f.Key); const fileContents = new Map(files.map((f) => [f.Key, f.InputFullText])); diff = options.SanitizeDiff ? DiffSanitizer.Process(diff, fileKeys) : diff; const fileHunks = UnifiedDiffParser.Parse(diff, fileContents, options.Truncation, options.ControlChars); const routingErrors = HeaderlessRouter.Route(fileHunks, files, options); const namedKeys = fileKeys.filter((k) => k != ""); const rosterHint = namedKeys.length > 0 ? `Files being patched: ${namedKeys.join(", ")}.` : null; const reportEntry = (key, errors) => new PatchOutputFile(key, 0, [], "", "", "", errors); const reportFiles = fileHunks.filter((g) => g.Key != "" && !fileContents.has(g.Key)).map((g) => reportEntry(g.Key, [new PatchError("FileMismatch", g.Hunks[0].ToUnanchoredChunk(), g.Key, rosterHint)])); if (routingErrors.length > 0) reportFiles.unshift(reportEntry("", routingErrors)); const reportErrors = reportFiles.flatMap((f) => f.Errors); if (reportErrors.length > 0 && !options.ContinueOnError) throw new PatchException(reportErrors[0]); const patchOutputFiles = files.map((f) => { const targetSelection = new SelectionTarget(f.InputFullText, f.InputSelectedText); const targetText = targetSelection.TargetText; const group = fileHunks.find((h) => h.Key == f.Key); const hunks = group?.Hunks ?? []; const anchored = HunkAnchorer.Anchor(targetText, hunks, options); const outputEdits = ChunkApplier.Apply(targetText, anchored.Chunks, options); const file = new PatchOutputFile(f.Key, outputEdits.Edits.map((h) => h.Fuzz).reduce((a, b) => a + b, 0), outputEdits.Edits.map((e) => e.Shift(targetSelection.LineOffset)), f.InputSelectedText, f.InputFullText, targetSelection.Replace(outputEdits.OutputText), [...outputEdits.Errors], anchored.AlreadyAppliedCount); file.Notices = PatchNotices.For(hunks, file, group?.ContextOnlyHunkLines ?? []); return file; }); return new PatchOutput([...patchOutputFiles, ...reportFiles]); } }; // cli/agents/pi/server/piPatcherExtension.ts var DESCRIPTION = true ? "Apply unified diffs to files \u2014 tolerant of form, strict about intent. Repairs sloppy AI-generated diffs (mangled headers, whitespace drift, missing prefixes; line numbers can be wrong or absent \u2014 hunks anchor by fuzzy-matched context lines) and applies all hunks atomically when the intent is unambiguous; fails with a precise, typed error when it isn't. Use for multi-hunk edits in one step, or when exact string-replacement editing fails on whitespace or invisible characters." : "Apply unified diffs to files."; var BUILD_TAG = true ? "matchu-patchu-pi 0.3.3, built 2026-07-27 14:58:21" : "matchu-patchu-pi dev"; var errorBlocks = (errors) => { const parts = [`Patch failed with ${errors.length} error(s):`]; for (const e of errors) parts.push("", e.toString()); return parts.join("\n"); }; var outcomeMessage = (target, out) => `Applied ${out.Edits.length} edit(s) to ${target}.` + (out.Edits.length > 0 ? "" : " File left unmodified.") + out.Notices.map((n) => " " + n).join(""); function discoverDiffKeys(diff) { const keys = []; for (const f of Patcher.Apply(diff, []).Files) { if (f.Key && !keys.includes(f.Key)) keys.push(f.Key); } return keys; } function applySingle(cwd, filePath, diff, dryRun) { const abs = resolve(cwd, filePath); if (!existsSync(abs)) return { ok: false, message: `Error: file not found: ${filePath}` }; const out = Patcher.Apply(diff, [new PatchInputFile("", readFileSync(abs, "utf-8"))]).Files[0]; if (out.Errors.length > 0) return { ok: false, message: errorBlocks(out.Errors) }; if (out.Edits.length > 0 && !dryRun) writeFileSync(abs, out.OutputFullText); return { ok: true, message: outcomeMessage(filePath, out) + (dryRun ? ` --- patched output --- ${out.OutputFullText}` : "") }; } function applyMulti(cwd, diff, dryRun) { const keys = discoverDiffKeys(diff); if (keys.length === 0) return { ok: false, message: "Error: no file headers (---/+++) found in the diff \u2014 pass filePath to patch a headerless diff into one file" }; const missing = keys.filter((k) => !existsSync(resolve(cwd, k))); if (missing.length > 0) return { ok: false, message: missing.map((k) => `Error: file not found: ${k}`).join("\n") }; const inputs = keys.map((k) => new PatchInputFile(k, readFileSync(resolve(cwd, k), "utf-8"))); const result = Patcher.Apply(diff, inputs); const errors = result.Files.flatMap((f) => f.Errors); if (errors.length > 0) return { ok: false, message: errorBlocks(errors) }; const lines = []; const dryOutputs = []; for (const out of result.Files) { if (out.Edits.length > 0) { if (!dryRun) writeFileSync(resolve(cwd, out.Key), out.OutputFullText); else dryOutputs.push(`--- patched output: ${out.Key} --- ${out.OutputFullText}`); } lines.push(outcomeMessage(out.Key, out)); } return { ok: true, message: [lines.join("\n"), ...dryOutputs].join("\n\n") }; } function applyPatch(cwd, args) { if (!args.diff || !args.diff.trim()) return { ok: false, message: "Error: patch is empty" }; try { const out = args.filePath ? applySingle(cwd, args.filePath, args.diff, args.dryRun === true) : applyMulti(cwd, args.diff, args.dryRun === true); return out.ok ? { ok: true, message: `${out.message} [${BUILD_TAG}]` } : out; } catch (ex) { if (ex instanceof PatchParserException) return { ok: false, message: `Error: failed to parse patch \u2014 ${ex.message}` }; return { ok: false, message: `Error: ${ex instanceof Error ? ex.message : String(ex)}` }; } } function logErr(...a) { try { console.error("[matchu-patchu]", ...a); } catch { } } function patchOwnership(tools, registeredByUs) { const patch = tools.find((t) => t.name === "patch"); if (!patch) return "absent"; return registeredByUs || /matchu-patchu/i.test(patch.sourceInfo?.path ?? "") ? "ours" : "foreign"; } var PATCH_TOOL = { name: "patch", label: "Patch", description: DESCRIPTION, promptSnippet: "Modify one or more existing files by applying a unified diff", promptGuidelines: [ "Use patch for every edit to an existing file; write is only for creating new files or full rewrites.", "patch accepts lenient unified diffs: line numbers are optional and hunks anchor by context lines. One diff may target several files via ---/+++ headers; pass filePath instead for a headerless single-file diff." ], parameters: Type.Object({ diff: Type.String({ description: "Unified diff content to apply" }), filePath: Type.Optional(Type.String({ description: "Path of the single file to patch (absolute or relative to the project root); the diff's headers are then ignored. Omit to target the file(s) named by the diff's ---/+++ headers." })), dryRun: Type.Optional(Type.Boolean({ description: "If true, return the patched content without writing to disk" })) }), async execute(_id, params, _signal, _onUpdate, ctx) { const { ok, message } = applyPatch(ctx.cwd, params); if (!ok) throw new Error(message); return { content: [{ type: "text", text: message }], details: {} }; } }; function piPatcherExtension_default(pi) { let registeredPatch = false; try { pi.on("session_start", () => { try { const owner = patchOwnership(pi.getAllTools(), registeredPatch); if (owner === "foreign") { logErr('another extension already provides "patch" \u2014 typebulb patcher standing down (built-in edit kept)'); return; } if (owner === "absent") { pi.registerTool(PATCH_TOOL); registeredPatch = true; } const active = pi.getActiveTools(); if (active.includes("edit")) pi.setActiveTools(active.filter((n) => n !== "edit")); } catch (e) { logErr("session_start", e); } }); } catch (e) { logErr("load", e); } } export { applyPatch, piPatcherExtension_default as default, discoverDiffKeys, patchOwnership };