pi-hashline-edit-pro 4.2.6 → 4.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- package/index.ts +18 -9
- package/package.json +1 -1
- package/src/anchor-registry.ts +262 -36
- package/src/constants.ts +3 -0
- package/src/grep.ts +154 -152
- package/src/hashline/resolve.ts +2 -1
- package/src/insert.ts +77 -74
- package/src/payload-contract.ts +3 -1
- package/src/read.ts +53 -51
- package/src/replace-diff.ts +8 -1
- package/src/replace-undo.ts +109 -107
- package/src/replace.ts +71 -69
- package/src/utils.ts +6 -0
- package/src/write-hook.ts +3 -3
package/src/grep.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { toCwd } from "./paths";
|
|
|
12
12
|
import { loadP, loadGuide } from "./prompts";
|
|
13
13
|
import { normReq } from "./payload-contract";
|
|
14
14
|
import { abortIf, errCode, isRec, makePrepareArguments, rejectUnknownFields, truncateToBytes, visLines } from "./utils";
|
|
15
|
-
import { markServed as markServedScoped } from "./anchor-registry";
|
|
15
|
+
import { markServed as markServedScoped, withAnchorSession } from "./anchor-registry";
|
|
16
16
|
import { buildServedMap } from "./served";
|
|
17
17
|
import { Text } from "@earendil-works/pi-tui";
|
|
18
18
|
import { expandHint, getResultText, reuseText, type CallT, type FgT } from "./replace-render";
|
|
@@ -553,172 +553,174 @@ export function regGrep(pi: ExtensionAPI): void {
|
|
|
553
553
|
},
|
|
554
554
|
|
|
555
555
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
baseStat = await stat(base);
|
|
566
|
-
} catch (error) {
|
|
567
|
-
if (errCode(error) === "ENOENT") {
|
|
568
|
-
throw new Error(`[E_NOT_FOUND] File not found: ${req.path ?? ctx.cwd}`);
|
|
569
|
-
}
|
|
570
|
-
throw new Error(`[E_ACCESS] Cannot access path: ${req.path ?? ctx.cwd}`);
|
|
571
|
-
}
|
|
572
|
-
const globRoot = baseStat.isFile() ? dirname(base) : base;
|
|
573
|
-
const globRegex = req.glob === undefined ? undefined : globToRegex(req.glob);
|
|
574
|
-
const validatedRegex = buildRegex(req.pattern, req.literal === true, req.ignoreCase === true);
|
|
575
|
-
const rgPath = await resolveRgPath();
|
|
576
|
-
const hits: FileHit[] = [];
|
|
577
|
-
let matches = 0;
|
|
578
|
-
let limitTruncated = false;
|
|
579
|
-
let rowTruncated = false;
|
|
580
|
-
let rowCount = 0;
|
|
581
|
-
let byteCount = 0;
|
|
582
|
-
let totalRows = 0;
|
|
583
|
-
let totalBytes = 0;
|
|
584
|
-
let truncatedBy: "lines" | "bytes" | null = null;
|
|
585
|
-
let linesReplaced = 0;
|
|
586
|
-
let countOnly = false;
|
|
587
|
-
let poolSkipped = 0;
|
|
588
|
-
const makeGrepReader = (allocation: "real" | "shadow") => async (absPath: string) => {
|
|
556
|
+
return withAnchorSession(ctx, async () => {
|
|
557
|
+
const canonical = normReq(params);
|
|
558
|
+
assertGrepReq(canonical);
|
|
559
|
+
const req = canonical;
|
|
560
|
+
const context = req.context ?? 0;
|
|
561
|
+
const limit = req.limit ?? 100;
|
|
562
|
+
const base = req.path ? toCwd(req.path, ctx.cwd) : ctx.cwd;
|
|
563
|
+
abortIf(signal);
|
|
564
|
+
let baseStat;
|
|
589
565
|
try {
|
|
590
|
-
|
|
566
|
+
baseStat = await stat(base);
|
|
591
567
|
} catch (error) {
|
|
592
|
-
if (
|
|
593
|
-
|
|
594
|
-
|
|
568
|
+
if (errCode(error) === "ENOENT") {
|
|
569
|
+
throw new Error(`[E_NOT_FOUND] File not found: ${req.path ?? ctx.cwd}`);
|
|
570
|
+
}
|
|
571
|
+
throw new Error(`[E_ACCESS] Cannot access path: ${req.path ?? ctx.cwd}`);
|
|
595
572
|
}
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
573
|
+
const globRoot = baseStat.isFile() ? dirname(base) : base;
|
|
574
|
+
const globRegex = req.glob === undefined ? undefined : globToRegex(req.glob);
|
|
575
|
+
const validatedRegex = buildRegex(req.pattern, req.literal === true, req.ignoreCase === true);
|
|
576
|
+
const rgPath = await resolveRgPath();
|
|
577
|
+
const hits: FileHit[] = [];
|
|
578
|
+
let matches = 0;
|
|
579
|
+
let limitTruncated = false;
|
|
580
|
+
let rowTruncated = false;
|
|
581
|
+
let rowCount = 0;
|
|
582
|
+
let byteCount = 0;
|
|
583
|
+
let totalRows = 0;
|
|
584
|
+
let totalBytes = 0;
|
|
585
|
+
let truncatedBy: "lines" | "bytes" | null = null;
|
|
586
|
+
let linesReplaced = 0;
|
|
587
|
+
let countOnly = false;
|
|
588
|
+
let poolSkipped = 0;
|
|
589
|
+
const makeGrepReader = (allocation: "real" | "shadow") => async (absPath: string) => {
|
|
590
|
+
try {
|
|
591
|
+
return await tryReadNormFile(absPath, ctx.cwd, { maxLines: MAX_HASH_LINES, noPersist: true, allocation, signal });
|
|
592
|
+
} catch (error) {
|
|
593
|
+
if (!isPoolExhaustedError(error)) throw error;
|
|
594
|
+
poolSkipped += 1;
|
|
595
|
+
return undefined;
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
const readGrepFile = makeGrepReader("real");
|
|
599
|
+
const readGrepFileShadow = makeGrepReader("shadow");
|
|
600
|
+
const rgMatches = await collectRgMatches(rgPath, req.pattern, base, req, signal);
|
|
601
|
+
const sortedFiles = [...rgMatches.keys()].sort(cmp);
|
|
602
|
+
for (let f = 0; f < sortedFiles.length; f++) {
|
|
603
|
+
abortIf(signal);
|
|
604
|
+
const absPath = sortedFiles[f]!;
|
|
605
|
+
const allNums = rgMatches.get(absPath) ?? [];
|
|
606
|
+
const totalForFile = allNums.length;
|
|
607
|
+
const sortedNums = [...allNums].sort((a, b) => a - b);
|
|
608
|
+
const indices = sortedNums.map((n) => n - 1).filter((n) => n >= 0);
|
|
609
|
+
if (countOnly) {
|
|
610
|
+
if (globRegex) {
|
|
611
|
+
const displayPath = relative(ctx.cwd, absPath).replace(/\\/g, "/");
|
|
612
|
+
const globPath = relative(globRoot, absPath).replace(/\\/g, "/");
|
|
613
|
+
if (!globRegex.test(globPath) && !globRegex.test(displayPath)) continue;
|
|
614
|
+
}
|
|
615
|
+
const norm = await readGrepFileShadow(absPath);
|
|
616
|
+
if (!norm) continue;
|
|
617
|
+
const hit = makeHitFromIndices(norm, relative(ctx.cwd, absPath).replace(/\\/g, "/"), indices, context, validatedRegex, totalForFile, indices.length);
|
|
618
|
+
const display = displayRowsForHit(hit);
|
|
619
|
+
totalRows += display.length;
|
|
620
|
+
for (const r of display) totalBytes += Buffer.byteLength(r, "utf-8") + 1;
|
|
621
|
+
const remainingCountOnly = limit - matches;
|
|
622
|
+
if (remainingCountOnly > 0) {
|
|
623
|
+
const add = Math.min(hit.matchCount, remainingCountOnly);
|
|
624
|
+
matches += add;
|
|
625
|
+
if (hit.matchCount > remainingCountOnly) limitTruncated = true;
|
|
626
|
+
} else {
|
|
627
|
+
limitTruncated = true;
|
|
628
|
+
}
|
|
629
|
+
continue;
|
|
630
|
+
}
|
|
631
|
+
const remaining = limit - matches;
|
|
632
|
+
if (remaining <= 0) {
|
|
633
|
+
limitTruncated = true;
|
|
634
|
+
break;
|
|
635
|
+
}
|
|
609
636
|
if (globRegex) {
|
|
610
637
|
const displayPath = relative(ctx.cwd, absPath).replace(/\\/g, "/");
|
|
611
638
|
const globPath = relative(globRoot, absPath).replace(/\\/g, "/");
|
|
612
639
|
if (!globRegex.test(globPath) && !globRegex.test(displayPath)) continue;
|
|
613
640
|
}
|
|
614
|
-
const norm = await
|
|
641
|
+
const norm = await readGrepFile(absPath);
|
|
615
642
|
if (!norm) continue;
|
|
616
|
-
const hit = makeHitFromIndices(norm, relative(ctx.cwd, absPath).replace(/\\/g, "/"), indices, context, validatedRegex, totalForFile,
|
|
643
|
+
const hit = makeHitFromIndices(norm, relative(ctx.cwd, absPath).replace(/\\/g, "/"), indices, context, validatedRegex, totalForFile, Math.min(totalForFile, remaining));
|
|
644
|
+
if (!hit) continue;
|
|
617
645
|
const display = displayRowsForHit(hit);
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
const
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
646
|
+
const keptRows: string[] = [];
|
|
647
|
+
const keptHashes: string[] = [];
|
|
648
|
+
const keptLineNumbers: number[] = [];
|
|
649
|
+
const keptFragmented: boolean[] = [];
|
|
650
|
+
for (let i = 0; i < display.length; i++) {
|
|
651
|
+
const row = display[i]!;
|
|
652
|
+
const rowBytes = Buffer.byteLength(row, "utf-8") + 1;
|
|
653
|
+
if (rowCount >= DEFAULT_MAX_LINES || byteCount + rowBytes > DEFAULT_MAX_BYTES) {
|
|
654
|
+
rowTruncated = true;
|
|
655
|
+
if (truncatedBy === null) truncatedBy = byteCount + rowBytes > DEFAULT_MAX_BYTES ? "bytes" : "lines";
|
|
656
|
+
for (let j = i; j < display.length; j++) {
|
|
657
|
+
totalRows += 1;
|
|
658
|
+
totalBytes += Buffer.byteLength(display[j]!, "utf-8") + 1;
|
|
659
|
+
}
|
|
660
|
+
break;
|
|
661
|
+
}
|
|
662
|
+
keptRows.push(row);
|
|
663
|
+
keptHashes.push(hit.hashes[i]!);
|
|
664
|
+
keptLineNumbers.push(hit.lineNumbers[i]!);
|
|
665
|
+
keptFragmented.push(hit.fragmented[i]!);
|
|
666
|
+
if (hit.fragmented[i]) linesReplaced += 1;
|
|
667
|
+
rowCount += 1;
|
|
668
|
+
byteCount += rowBytes;
|
|
669
|
+
totalRows += 1;
|
|
670
|
+
totalBytes += rowBytes;
|
|
627
671
|
}
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
break;
|
|
672
|
+
if (hit.totalMatchCount > hit.matchCount) limitTruncated = true;
|
|
673
|
+
matches += hit.matchCount;
|
|
674
|
+
const displayHit: FileHit = { ...hit, rows: keptRows, hashes: keptHashes, lineNumbers: keptLineNumbers, fragmented: keptFragmented };
|
|
675
|
+
hits.push(displayHit);
|
|
676
|
+
if (rowTruncated) countOnly = true;
|
|
634
677
|
}
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
if (!globRegex.test(globPath) && !globRegex.test(displayPath)) continue;
|
|
678
|
+
hits.sort((a, b) => cmp(a.displayPath, b.displayPath));
|
|
679
|
+
for (const hit of hits) {
|
|
680
|
+
markServedScoped(hit.path, buildServedMap(hit.fileHashes, hit.fileLines, hit.hashes), new Set(hit.fileHashes));
|
|
639
681
|
}
|
|
640
|
-
const
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
const
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
682
|
+
const blocks = hits
|
|
683
|
+
.map((hit) => `=== ${hit.displayPath} ===\n${hit.rows.join("\n")}`)
|
|
684
|
+
.join("\n");
|
|
685
|
+
const notes: string[] = [];
|
|
686
|
+
if (rowTruncated) notes.push(`[grep: output truncated at ${DEFAULT_MAX_LINES} rows or ${formatSize(DEFAULT_MAX_BYTES)}; refine the pattern to see more.]`);
|
|
687
|
+
if (limitTruncated) notes.push(`[grep: showing first ${limit} matches; increase limit to see more.]`);
|
|
688
|
+
if (linesReplaced > 0) notes.push(`[grep: ${linesReplaced} line(s) exceed ${formatSize(MAX_GREP_LINE_BYTES)} and are shown as truncated fragments; use read to see the full lines.]`);
|
|
689
|
+
if (poolSkipped > 0) notes.push(`[grep: ${poolSkipped} file(s) skipped because the session's anchor pool is exhausted; narrow the search.]`);
|
|
690
|
+
const truncated = limitTruncated || rowTruncated;
|
|
691
|
+
const truncation: TruncationResult | undefined = rowTruncated
|
|
692
|
+
? {
|
|
693
|
+
content: blocks,
|
|
694
|
+
truncated: true,
|
|
695
|
+
truncatedBy,
|
|
696
|
+
totalLines: totalRows,
|
|
697
|
+
totalBytes,
|
|
698
|
+
outputLines: rowCount,
|
|
699
|
+
outputBytes: byteCount,
|
|
700
|
+
lastLinePartial: false,
|
|
701
|
+
firstLineExceedsLimit: false,
|
|
702
|
+
maxLines: DEFAULT_MAX_LINES,
|
|
703
|
+
maxBytes: DEFAULT_MAX_BYTES,
|
|
658
704
|
}
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
if (rowTruncated) countOnly = true;
|
|
676
|
-
}
|
|
677
|
-
hits.sort((a, b) => cmp(a.displayPath, b.displayPath));
|
|
678
|
-
for (const hit of hits) {
|
|
679
|
-
markServedScoped(hit.path, buildServedMap(hit.fileHashes, hit.fileLines, hit.hashes), new Set(hit.fileHashes));
|
|
680
|
-
}
|
|
681
|
-
const blocks = hits
|
|
682
|
-
.map((hit) => `=== ${hit.displayPath} ===\n${hit.rows.join("\n")}`)
|
|
683
|
-
.join("\n");
|
|
684
|
-
const notes: string[] = [];
|
|
685
|
-
if (rowTruncated) notes.push(`[grep: output truncated at ${DEFAULT_MAX_LINES} rows or ${formatSize(DEFAULT_MAX_BYTES)}; refine the pattern to see more.]`);
|
|
686
|
-
if (limitTruncated) notes.push(`[grep: showing first ${limit} matches; increase limit to see more.]`);
|
|
687
|
-
if (linesReplaced > 0) notes.push(`[grep: ${linesReplaced} line(s) exceed ${formatSize(MAX_GREP_LINE_BYTES)} and are shown as truncated fragments; use read to see the full lines.]`);
|
|
688
|
-
if (poolSkipped > 0) notes.push(`[grep: ${poolSkipped} file(s) skipped because the session's anchor pool is exhausted; narrow the search.]`);
|
|
689
|
-
const truncated = limitTruncated || rowTruncated;
|
|
690
|
-
const truncation: TruncationResult | undefined = rowTruncated
|
|
691
|
-
? {
|
|
692
|
-
content: blocks,
|
|
693
|
-
truncated: true,
|
|
694
|
-
truncatedBy,
|
|
695
|
-
totalLines: totalRows,
|
|
696
|
-
totalBytes,
|
|
697
|
-
outputLines: rowCount,
|
|
698
|
-
outputBytes: byteCount,
|
|
699
|
-
lastLinePartial: false,
|
|
700
|
-
firstLineExceedsLimit: false,
|
|
701
|
-
maxLines: DEFAULT_MAX_LINES,
|
|
702
|
-
maxBytes: DEFAULT_MAX_BYTES,
|
|
703
|
-
}
|
|
704
|
-
: undefined;
|
|
705
|
-
const text = blocks.length > 0
|
|
706
|
-
? `${blocks}${notes.length > 0 ? `\n${notes.join("\n")}` : ""}`
|
|
707
|
-
: notes.length > 0
|
|
708
|
-
? `No matches found.\n${notes.join("\n")}`
|
|
709
|
-
: "No matches found.";
|
|
710
|
-
return {
|
|
711
|
-
content: [{ type: "text", text }],
|
|
712
|
-
details: {
|
|
713
|
-
...(truncation ? { truncation } : {}),
|
|
714
|
-
...(linesReplaced > 0 ? { linesTruncated: true as const } : {}),
|
|
715
|
-
metrics: {
|
|
716
|
-
matches,
|
|
717
|
-
files: hits.length,
|
|
718
|
-
truncated,
|
|
705
|
+
: undefined;
|
|
706
|
+
const text = blocks.length > 0
|
|
707
|
+
? `${blocks}${notes.length > 0 ? `\n${notes.join("\n")}` : ""}`
|
|
708
|
+
: notes.length > 0
|
|
709
|
+
? `No matches found.\n${notes.join("\n")}`
|
|
710
|
+
: "No matches found.";
|
|
711
|
+
return {
|
|
712
|
+
content: [{ type: "text", text }],
|
|
713
|
+
details: {
|
|
714
|
+
...(truncation ? { truncation } : {}),
|
|
715
|
+
...(linesReplaced > 0 ? { linesTruncated: true as const } : {}),
|
|
716
|
+
metrics: {
|
|
717
|
+
matches,
|
|
718
|
+
files: hits.length,
|
|
719
|
+
truncated,
|
|
720
|
+
},
|
|
719
721
|
},
|
|
720
|
-
}
|
|
721
|
-
};
|
|
722
|
+
};
|
|
723
|
+
});
|
|
722
724
|
},
|
|
723
725
|
});
|
|
724
726
|
}
|
package/src/hashline/resolve.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { abortIf, rejectUnknownFields, firstNonEmptyIndex, lastNonEmptyIndex, clipLine, getCached, decodeStringArray } from "../utils";
|
|
1
|
+
import { abortIf, rejectUnknownFields, firstNonEmptyIndex, lastNonEmptyIndex, clipLine, getCached, decodeStringArray, assertNoNul } from "../utils";
|
|
2
2
|
import { parseHashRef, parseText, type Anchor } from "./parse";
|
|
3
3
|
import { HASH_SEP, stripRowPrefix, canon } from "./hash";
|
|
4
4
|
import { HASH_RUN } from "./alphabet";
|
|
@@ -172,6 +172,7 @@ export function resEdit(edit: HTEdit, warnings?: string[]): HEdit {
|
|
|
172
172
|
assertItem(edit as Record<string, unknown>);
|
|
173
173
|
|
|
174
174
|
const replaceLines = parseText(decodeStringArray(edit.replacement_lines, warnings) ?? edit.replacement_lines, warnings);
|
|
175
|
+
assertNoNul(replaceLines);
|
|
175
176
|
const bounds = [edit.remove_from, edit.remove_to].map((ref) => {
|
|
176
177
|
return stripAnchorRow(ref.trim(), "remove_from/remove_to entry", warnings);
|
|
177
178
|
}) as [string, string];
|
package/src/insert.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { batchMemberFor, ensureBatchBase, executeBatchMember, noteBatchFailure,
|
|
|
7
7
|
import { readNormFile, type NormFile } from "./file-reader";
|
|
8
8
|
import { MAX_HASH_LINES, parseHashRef, resEdit, resolveAnchorLine, type Anchor, type HEdit } from "./hashline";
|
|
9
9
|
import { stripAnchorRow } from "./hashline/resolve";
|
|
10
|
+
import { withAnchorSession } from "./anchor-registry";
|
|
10
11
|
import { loadP, loadGuide } from "./prompts";
|
|
11
12
|
import { assertInsertReq, normReq, type InsertReq } from "./payload-contract";
|
|
12
13
|
import { decodeStringArray, isRec, splitLines } from "./utils";
|
|
@@ -172,84 +173,86 @@ export function buildInsertToolDef(flags: EditToolFlags = DEFAULT_EDIT_FLAGS): I
|
|
|
172
173
|
renderCall: editRenderCallWrapper(insertPreview, getInsertInput, "insert"),
|
|
173
174
|
renderResult: editRenderResultWrapper,
|
|
174
175
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
176
|
+
return withAnchorSession(ctx, async () => {
|
|
177
|
+
const canonical = normReq(params);
|
|
178
|
+
const insertWarnings: string[] = [];
|
|
179
|
+
if (isRec(canonical)) {
|
|
180
|
+
const expanded = decodeStringArray(canonical.lines, insertWarnings, "lines");
|
|
181
|
+
if (expanded) canonical.lines = expanded;
|
|
182
|
+
}
|
|
183
|
+
assertInsertReq(canonical);
|
|
184
|
+
const req = canonical;
|
|
185
|
+
const targetPath = await resolveEditTargetWithRequirement({
|
|
186
|
+
anchor: req.anchor,
|
|
187
|
+
providedPath: req.path,
|
|
188
|
+
cwd: ctx.cwd,
|
|
189
|
+
}).catch((error: unknown) => {
|
|
190
|
+
const member = batchMemberFor(_toolCallId);
|
|
191
|
+
if (member) noteBatchFailure(member, error);
|
|
192
|
+
else suffixPoisonCause(_toolCallId, error);
|
|
193
|
+
throw error;
|
|
194
|
+
});
|
|
195
|
+
let ref: Anchor;
|
|
196
|
+
let anchorWarnings: string[];
|
|
197
|
+
try {
|
|
198
|
+
({ ref, warnings: anchorWarnings } = parseInsertAnchor(req.anchor));
|
|
199
|
+
await throwIfStrictInput([...anchorWarnings, ...insertWarnings]);
|
|
200
|
+
} catch (error) {
|
|
201
|
+
const member = batchMemberFor(_toolCallId);
|
|
202
|
+
if (member) noteBatchFailure(member, error);
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
205
|
+
return queuedEdit(targetPath, ctx.cwd, signal, async (absolutePath, mutationTargetPath) => {
|
|
206
|
+
const member = batchMemberFor(_toolCallId);
|
|
207
|
+
if (member) {
|
|
208
|
+
const base = await ensureBatchBase({ member, targetPath, mutationTargetPath, cwd: ctx.cwd, signal });
|
|
209
|
+
const basePreload = { normalized: base.content, fileHashes: base.hashes } as NormFile;
|
|
210
|
+
const built = buildInsertEdit(req, basePreload, ref, targetPath);
|
|
211
|
+
let hedit: HEdit;
|
|
212
|
+
const resWarnings: string[] = [];
|
|
213
|
+
try {
|
|
214
|
+
hedit = resEdit(built.editParams, resWarnings);
|
|
215
|
+
} catch (error) {
|
|
216
|
+
noteBatchFailure(member, error);
|
|
217
|
+
throw error;
|
|
218
|
+
}
|
|
219
|
+
return executeBatchMember({
|
|
220
|
+
kind: "insert",
|
|
221
|
+
member,
|
|
222
|
+
targetPath,
|
|
223
|
+
mutationTargetPath,
|
|
224
|
+
cwd: ctx.cwd,
|
|
225
|
+
signal,
|
|
226
|
+
hedit,
|
|
227
|
+
extraWarnings: [...anchorWarnings, ...insertWarnings, ...resWarnings],
|
|
228
|
+
skipBoundaryDedup: true,
|
|
229
|
+
strictBoundaryDedup: false,
|
|
230
|
+
foldedLines: built.anchorLine === undefined ? 0 : 1,
|
|
231
|
+
});
|
|
216
232
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
233
|
+
const preload = await readNormFile(targetPath, ctx.cwd, {
|
|
234
|
+
signal,
|
|
235
|
+
accessMode: constants.R_OK | constants.W_OK,
|
|
236
|
+
maxLines: MAX_HASH_LINES,
|
|
237
|
+
});
|
|
238
|
+
const { editParams, anchorLine } = buildInsertEdit(req, preload, ref, targetPath);
|
|
239
|
+
const pipe = await execPipeline(targetPath, editParams, ctx.cwd, {
|
|
240
|
+
accessMode: constants.R_OK | constants.W_OK,
|
|
223
241
|
signal,
|
|
224
|
-
|
|
225
|
-
extraWarnings: [...anchorWarnings, ...insertWarnings, ...resWarnings],
|
|
242
|
+
preloadedNorm: preload,
|
|
226
243
|
skipBoundaryDedup: true,
|
|
227
|
-
strictBoundaryDedup: false,
|
|
228
|
-
foldedLines: built.anchorLine === undefined ? 0 : 1,
|
|
229
244
|
});
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
skipBoundaryDedup: true,
|
|
242
|
-
});
|
|
243
|
-
return commitEdit(pipe, {
|
|
244
|
-
path: pipe.path,
|
|
245
|
-
absolutePath,
|
|
246
|
-
mutationTargetPath,
|
|
247
|
-
signal,
|
|
248
|
-
verb: "inserted",
|
|
249
|
-
noopNoun: "Insertion",
|
|
250
|
-
foldedAnchorLines: anchorLine === undefined ? 0 : 1,
|
|
251
|
-
prefixWarnings: [...anchorWarnings, ...insertWarnings],
|
|
252
|
-
onApplied: () => clearBoundaryBypass(mutationTargetPath),
|
|
245
|
+
return commitEdit(pipe, {
|
|
246
|
+
path: pipe.path,
|
|
247
|
+
absolutePath,
|
|
248
|
+
mutationTargetPath,
|
|
249
|
+
signal,
|
|
250
|
+
verb: "inserted",
|
|
251
|
+
noopNoun: "Insertion",
|
|
252
|
+
foldedAnchorLines: anchorLine === undefined ? 0 : 1,
|
|
253
|
+
prefixWarnings: [...anchorWarnings, ...insertWarnings],
|
|
254
|
+
onApplied: () => clearBoundaryBypass(mutationTargetPath),
|
|
255
|
+
});
|
|
253
256
|
});
|
|
254
257
|
});
|
|
255
258
|
},
|
package/src/payload-contract.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
|
-
import { isRec, normalizeAnchors, normalizeFilePath, rejectUnknownFields } from "./utils";
|
|
2
|
+
import { isRec, normalizeAnchors, normalizeFilePath, rejectUnknownFields, assertNoNul } from "./utils";
|
|
3
3
|
|
|
4
4
|
const replacementLinesSchema = Type.Array(
|
|
5
5
|
Type.String({
|
|
@@ -74,6 +74,7 @@ export function assertReq(request: unknown): asserts request is ReqParams {
|
|
|
74
74
|
'[E_BAD_SHAPE] Edit request requires "remove_from", "remove_to", and "replacement_lines" (array of strings, one per line; use [] to delete).',
|
|
75
75
|
);
|
|
76
76
|
}
|
|
77
|
+
assertNoNul(request.replacement_lines);
|
|
77
78
|
}
|
|
78
79
|
|
|
79
80
|
export function normReq(input: unknown): unknown {
|
|
@@ -136,4 +137,5 @@ export function assertInsertReq(request: unknown): asserts request is InsertReq
|
|
|
136
137
|
if (!Array.isArray(request.lines) || request.lines.some((line) => typeof line !== "string")) {
|
|
137
138
|
throw new Error('[E_BAD_SHAPE] Insert request requires "lines" as an array of strings, one element per line.');
|
|
138
139
|
}
|
|
140
|
+
assertNoNul(request.lines);
|
|
139
141
|
}
|