pi-hashline-edit-pro 0.16.13 → 0.16.15
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/package.json +1 -1
- package/src/file-kind.ts +1 -16
- package/src/hashline/apply.ts +3 -18
- package/src/hashline/hash.ts +1 -0
- package/src/hashline/resolve.ts +1 -15
- package/src/prompts.ts +2 -2
- package/src/replace-response.ts +8 -6
- package/src/replace.ts +60 -41
- package/src/utils.ts +24 -0
package/package.json
CHANGED
package/src/file-kind.ts
CHANGED
|
@@ -31,9 +31,6 @@ export type LFile =
|
|
|
31
31
|
| { kind: "text"; text: string; hadUtf8DecodeErrors?: true }
|
|
32
32
|
| { kind: "binary"; description: string };
|
|
33
33
|
|
|
34
|
-
function hasNull(buffer: Uint8Array): boolean {
|
|
35
|
-
return buffer.includes(0);
|
|
36
|
-
}
|
|
37
34
|
|
|
38
35
|
export async function loadFileKindAndText(
|
|
39
36
|
filePath: string,
|
|
@@ -82,12 +79,7 @@ export async function loadFileKindAndText(
|
|
|
82
79
|
description: detectedMimeType,
|
|
83
80
|
};
|
|
84
81
|
}
|
|
85
|
-
|
|
86
|
-
return {
|
|
87
|
-
kind: "binary",
|
|
88
|
-
description: "null bytes detected",
|
|
89
|
-
};
|
|
90
|
-
}
|
|
82
|
+
|
|
91
83
|
|
|
92
84
|
const decoder = new TextDecoder("utf-8");
|
|
93
85
|
const fatalDecoder = new TextDecoder("utf-8", { fatal: true });
|
|
@@ -121,17 +113,10 @@ export async function loadFileKindAndText(
|
|
|
121
113
|
}
|
|
122
114
|
|
|
123
115
|
const chunk = buffer.subarray(0, chunkBytesRead);
|
|
124
|
-
if (hasNull(chunk)) {
|
|
125
|
-
return {
|
|
126
|
-
kind: "binary",
|
|
127
|
-
description: "null bytes detected",
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
116
|
noteUtf8Err(chunk);
|
|
131
117
|
parts.push(decoder.decode(chunk, { stream: true }));
|
|
132
118
|
position += chunkBytesRead;
|
|
133
119
|
}
|
|
134
|
-
|
|
135
120
|
noteUtf8Err();
|
|
136
121
|
parts.push(decoder.decode());
|
|
137
122
|
|
package/src/hashline/apply.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { abortIf } from "../utils";
|
|
1
|
+
import { abortIf, visLines, lastNonEmptyIndex, firstNonEmptyIndex } from "../utils";
|
|
2
2
|
import { _lineHashesPure, HASH_SEP } from "./hash";
|
|
3
3
|
import {
|
|
4
4
|
valEdits,
|
|
@@ -12,21 +12,6 @@ import {
|
|
|
12
12
|
type BDupWarn,
|
|
13
13
|
type AutoFix,
|
|
14
14
|
} from "./resolve";
|
|
15
|
-
import { visLines } from "../utils";
|
|
16
|
-
|
|
17
|
-
function lastNonEmptyIndex(lines: string[]): number {
|
|
18
|
-
for (let i = lines.length - 1; i >= 0; i--) {
|
|
19
|
-
if (lines[i]!.length > 0) return i;
|
|
20
|
-
}
|
|
21
|
-
return -1;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function firstNonEmptyIndex(lines: string[]): number {
|
|
25
|
-
for (let i = 0; i < lines.length; i++) {
|
|
26
|
-
if (lines[i]!.length > 0) return i;
|
|
27
|
-
}
|
|
28
|
-
return -1;
|
|
29
|
-
}
|
|
30
15
|
|
|
31
16
|
type LIdx = {
|
|
32
17
|
fileLines: string[];
|
|
@@ -267,7 +252,7 @@ export function fmtBoundaryWarning(params: {
|
|
|
267
252
|
|
|
268
253
|
export function applyEdits(
|
|
269
254
|
content: string,
|
|
270
|
-
edits:
|
|
255
|
+
edits: HEdit[],
|
|
271
256
|
signal?: AbortSignal,
|
|
272
257
|
precomputedHashes?: string[],
|
|
273
258
|
filePath?: string,
|
|
@@ -312,7 +297,7 @@ export function applyEdits(
|
|
|
312
297
|
let autoFixes: AutoFix[] | undefined;
|
|
313
298
|
if (boundaryWarnings.length > 0) {
|
|
314
299
|
autoFixes = [];
|
|
315
|
-
const correctedEdits:
|
|
300
|
+
const correctedEdits: HEdit[] = edits.map(e => ({
|
|
316
301
|
...e,
|
|
317
302
|
content_lines: [...e.content_lines],
|
|
318
303
|
}));
|
package/src/hashline/hash.ts
CHANGED
package/src/hashline/resolve.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { abortIf } from "../utils";
|
|
2
|
-
import { rejectUnknownFields } from "../utils";
|
|
1
|
+
import { abortIf, rejectUnknownFields, lastNonEmpty, firstNonEmpty } from "../utils";
|
|
3
2
|
import { HL_BARE_PREFIX_RE } from "./hash";
|
|
4
3
|
import { parseHashRef, parseText, type Anchor } from "./parse";
|
|
5
4
|
import { CONTENT_LINES_NOT_STRING_MSG } from "../constants";
|
|
@@ -240,19 +239,6 @@ export function descEdit(edit: RHEdit): string {
|
|
|
240
239
|
return `replace ${edit.hash_range_inclusive[0].hash}-${edit.hash_range_inclusive[1].hash}`;
|
|
241
240
|
}
|
|
242
241
|
|
|
243
|
-
function lastNonEmpty(lines: string[]): string | undefined {
|
|
244
|
-
for (let i = lines.length - 1; i >= 0; i--) {
|
|
245
|
-
if (lines[i]!.length > 0) return lines[i]!;
|
|
246
|
-
}
|
|
247
|
-
return undefined;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function firstNonEmpty(lines: string[]): string | undefined {
|
|
251
|
-
for (let i = 0; i < lines.length; i++) {
|
|
252
|
-
if (lines[i]!.length > 0) return lines[i]!;
|
|
253
|
-
}
|
|
254
|
-
return undefined;
|
|
255
|
-
}
|
|
256
242
|
|
|
257
243
|
function checkBoundaryDup(
|
|
258
244
|
adjacentLine: string | undefined,
|
package/src/prompts.ts
CHANGED
|
@@ -4,7 +4,7 @@ export function loadP(relativePath: string, replacements?: Record<string, string
|
|
|
4
4
|
let content = readFileSync(new URL(relativePath, import.meta.url), "utf-8").trim();
|
|
5
5
|
if (replacements) {
|
|
6
6
|
for (const [key, value] of Object.entries(replacements)) {
|
|
7
|
-
content = content.
|
|
7
|
+
content = content.split(`{{${key}}}`).join(value);
|
|
8
8
|
}
|
|
9
9
|
}
|
|
10
10
|
return content;
|
|
@@ -14,7 +14,7 @@ export function loadGuide(relativePath: string, replacements?: Record<string, st
|
|
|
14
14
|
let content = readFileSync(new URL(relativePath, import.meta.url), "utf-8");
|
|
15
15
|
if (replacements) {
|
|
16
16
|
for (const [key, value] of Object.entries(replacements)) {
|
|
17
|
-
content = content.
|
|
17
|
+
content = content.split(`{{${key}}}`).join(value);
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
return content
|
package/src/replace-response.ts
CHANGED
|
@@ -19,10 +19,12 @@ export type RMetrics = {
|
|
|
19
19
|
};
|
|
20
20
|
|
|
21
21
|
export type RMeta = {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
editsAttempted: number;
|
|
23
|
+
noopEditsCount: number;
|
|
24
|
+
firstChangedLine?: number;
|
|
25
|
+
lastChangedLine?: number;
|
|
26
|
+
addedLines: number;
|
|
27
|
+
removedLines: number;
|
|
26
28
|
};
|
|
27
29
|
|
|
28
30
|
type NEditEntry = {
|
|
@@ -131,8 +133,8 @@ export function buildChanged(input: SuccessInput): TResult {
|
|
|
131
133
|
|
|
132
134
|
const resultLines = visLines(result);
|
|
133
135
|
const diffResult = genDiff(originalNormalized, result, 2, resultHashes, originalHashes);
|
|
134
|
-
const addedLines =
|
|
135
|
-
const removedLines =
|
|
136
|
+
const addedLines = editMeta.addedLines;
|
|
137
|
+
const removedLines = editMeta.removedLines;
|
|
136
138
|
const warningsBlock = warnBlock(warnings);
|
|
137
139
|
const successPrefix = `Successfully replaced in ${path}.`;
|
|
138
140
|
const lineSummary = addedLines > 0 || removedLines > 0
|
package/src/replace.ts
CHANGED
|
@@ -111,6 +111,8 @@ interface PipelineResult {
|
|
|
111
111
|
lastChangedLine?: number;
|
|
112
112
|
originalHashes: string[];
|
|
113
113
|
resultHashes: string[];
|
|
114
|
+
totalAddedLines: number;
|
|
115
|
+
totalRemovedLines: number;
|
|
114
116
|
}
|
|
115
117
|
|
|
116
118
|
const ROOT_KS = new Set(["path", "changes", "content_lines", "hash_range_inclusive"]);
|
|
@@ -165,12 +167,10 @@ export async function execPipeline(
|
|
|
165
167
|
|
|
166
168
|
const hashStore = store ?? await loadHashStore();
|
|
167
169
|
|
|
168
|
-
const { normalized: originalNormalized, bom, originalEnding, fileHashes: originalHashes, hadUtf8DecodeErrors } = await readNormFile(
|
|
170
|
+
const { normalized: originalNormalized, bom, originalEnding, fileHashes: originalHashes, hadUtf8DecodeErrors, absolutePath } = await readNormFile(
|
|
169
171
|
path, cwd, signal, accessMode, undefined, MAX_HASH_LINES, hashStore,
|
|
170
172
|
);
|
|
171
173
|
|
|
172
|
-
const absolutePath = toCwd(path, cwd);
|
|
173
|
-
const resolvedPath = await resolveTarget(absolutePath);
|
|
174
174
|
const resolved = resEdits(toolEdits);
|
|
175
175
|
const anchorResult = applyEdits(
|
|
176
176
|
originalNormalized,
|
|
@@ -195,13 +195,28 @@ export async function execPipeline(
|
|
|
195
195
|
}
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
-
const resultHashes = await lineHashes(result,
|
|
198
|
+
const resultHashes = await lineHashes(result, absolutePath, {
|
|
199
199
|
content: originalNormalized,
|
|
200
200
|
hashes: originalHashes,
|
|
201
201
|
removedHashes,
|
|
202
202
|
}, hashStore);
|
|
203
203
|
|
|
204
204
|
const warnings = [...(anchorResult.warnings ?? [])];
|
|
205
|
+
|
|
206
|
+
let totalAddedLines = 0;
|
|
207
|
+
let totalRemovedLines = 0;
|
|
208
|
+
const noopIndices = new Set(anchorResult.noopEdits?.map((n) => n.editIndex) ?? []);
|
|
209
|
+
for (let i = 0; i < resolved.length; i++) {
|
|
210
|
+
if (noopIndices.has(i)) continue;
|
|
211
|
+
const edit = resolved[i]!;
|
|
212
|
+
const startLine = originalHashes.indexOf(edit.hash_range_inclusive[0].hash);
|
|
213
|
+
const endLine = originalHashes.indexOf(edit.hash_range_inclusive[1].hash);
|
|
214
|
+
if (startLine >= 0 && endLine >= 0) {
|
|
215
|
+
totalRemovedLines += endLine - startLine + 1;
|
|
216
|
+
}
|
|
217
|
+
totalAddedLines += edit.content_lines.length;
|
|
218
|
+
}
|
|
219
|
+
|
|
205
220
|
return {
|
|
206
221
|
path,
|
|
207
222
|
toolEdits,
|
|
@@ -216,6 +231,8 @@ export async function execPipeline(
|
|
|
216
231
|
lastChangedLine: anchorResult.lastChangedLine,
|
|
217
232
|
resultHashes,
|
|
218
233
|
originalHashes,
|
|
234
|
+
totalAddedLines,
|
|
235
|
+
totalRemovedLines,
|
|
219
236
|
};
|
|
220
237
|
}
|
|
221
238
|
|
|
@@ -266,6 +283,32 @@ export function reuseMarkdown(context: any, content: string, theme: any): Markdo
|
|
|
266
283
|
m.setText(content);
|
|
267
284
|
return m;
|
|
268
285
|
}
|
|
286
|
+
const MODE_CFG = {
|
|
287
|
+
flat: {
|
|
288
|
+
desc: " Only one edit per call. The `hash_range_inclusive` and `content_lines` fields sit at the top level of the request object.",
|
|
289
|
+
examples: [
|
|
290
|
+
"", "Single line:", "{ \"content_lines\": [\"const x = 1;\"], \"hash_range_inclusive\": [\"MQX\", \"MQX\"], \"path\": \"src/main.ts\" }", "", "Range replace:", "{ \"content_lines\": [\"function greet() {\", \" return 1;\", \"}\"], \"hash_range_inclusive\": [\"ZPM\", \"VRW\"], \"path\": \"src/main.ts\" }",
|
|
291
|
+
].join("\n"),
|
|
292
|
+
rules: "",
|
|
293
|
+
requestStructure: [
|
|
294
|
+
"Flat mode:", "```json", "{ \"content_lines\": [...], \"hash_range_inclusive\": [\"aB3\", \"xY7\"], \"path\": \"...\" }", "```",
|
|
295
|
+
].join("\n"),
|
|
296
|
+
prefix: "one edit per call (flat mode)",
|
|
297
|
+
guidePrefix: "- Use `replace` with HASH anchors for all file changes. Only one edit per call.",
|
|
298
|
+
},
|
|
299
|
+
bulk: {
|
|
300
|
+
desc: "\n\nPut all operations on one file in a single `replace` call. Stack every region into the `changes` array, even when they are far apart. Anchors within one call must all come from the same pre-edit read; the runtime applies them atomically against that one snapshot.",
|
|
301
|
+
examples: [
|
|
302
|
+
"", "Single line:", "{ \"changes\": [{ \"content_lines\": [\"const x = 1;\"], \"hash_range_inclusive\": [\"MQX\", \"MQX\"] }], \"path\": \"src/main.ts\" }", "", "Range replace:", "{ \"changes\": [{ \"content_lines\": [\"function greet() {\", \" return 1;\", \"}\"], \"hash_range_inclusive\": [\"ZPM\", \"VRW\"] }], \"path\": \"src/main.ts\" }",
|
|
303
|
+
].join("\n"),
|
|
304
|
+
rules: "- Multiple edits in one call must not overlap. Overlapping ranges are rejected with [E_EDIT_CONFLICT].",
|
|
305
|
+
requestStructure: [
|
|
306
|
+
"Bulk mode (default):", "```json", "{ \"changes\": [{ \"content_lines\": [...], \"hash_range_inclusive\": [\"aB3\", \"xY7\"] }], \"path\": \"...\" }", "```",
|
|
307
|
+
].join("\n"),
|
|
308
|
+
prefix: "batching all changes to a file in one call",
|
|
309
|
+
guidePrefix: "- Use `replace` with HASH anchors for all file changes; batch every change to one file into a single `replace` call.",
|
|
310
|
+
},
|
|
311
|
+
} as const;
|
|
269
312
|
|
|
270
313
|
export function buildToolDef(opts: { flat: boolean; autoRead?: boolean }): ToolDef {
|
|
271
314
|
const autoRead = opts.autoRead ?? false;
|
|
@@ -273,50 +316,20 @@ export function buildToolDef(opts: { flat: boolean; autoRead?: boolean }): ToolD
|
|
|
273
316
|
? "Anchors are provided automatically after write and replace operations when auto-read is enabled."
|
|
274
317
|
: "Call `read` to get fresh anchors for follow-up edits.";
|
|
275
318
|
|
|
276
|
-
const
|
|
277
|
-
? " Only one edit per call. The `hash_range_inclusive` and `content_lines` fields sit at the top level of the request object."
|
|
278
|
-
: "\n\nPut all operations on one file in a single `replace` call. Stack every region into the `changes` array, even when they are far apart. Anchors within one call must all come from the same pre-edit read; the runtime applies them atomically against that one snapshot.";
|
|
279
|
-
|
|
280
|
-
const modeExamples = opts.flat
|
|
281
|
-
? [
|
|
282
|
-
"", "Single line:", "{ \"content_lines\": [\"const x = 1;\"], \"hash_range_inclusive\": [\"MQX\", \"MQX\"], \"path\": \"src/main.ts\" }", "", "Range replace:", "{ \"content_lines\": [\"function greet() {\", \" return 1;\", \"}\"], \"hash_range_inclusive\": [\"ZPM\", \"VRW\"], \"path\": \"src/main.ts\" }",
|
|
283
|
-
].join("\n")
|
|
284
|
-
: [
|
|
285
|
-
"", "Single line:", "{ \"changes\": [{ \"content_lines\": [\"const x = 1;\"], \"hash_range_inclusive\": [\"MQX\", \"MQX\"] }], \"path\": \"src/main.ts\" }", "", "Range replace:", "{ \"changes\": [{ \"content_lines\": [\"function greet() {\", \" return 1;\", \"}\"], \"hash_range_inclusive\": [\"ZPM\", \"VRW\"] }], \"path\": \"src/main.ts\" }",
|
|
286
|
-
].join("\n")
|
|
287
|
-
|
|
288
|
-
const modeRules = opts.flat
|
|
289
|
-
? ""
|
|
290
|
-
: "- Multiple edits in one call must not overlap. Overlapping ranges are rejected with [E_EDIT_CONFLICT]."
|
|
291
|
-
|
|
292
|
-
const modeRequestStructure = opts.flat
|
|
293
|
-
? [
|
|
294
|
-
"Flat mode:", "```json", "{ \"content_lines\": [...], \"hash_range_inclusive\": [\"aB3\", \"xY7\"], \"path\": \"...\" }", "```",
|
|
295
|
-
].join("\n")
|
|
296
|
-
: [
|
|
297
|
-
"Bulk mode (default):", "```json", "{ \"changes\": [{ \"content_lines\": [...], \"hash_range_inclusive\": [\"aB3\", \"xY7\"] }], \"path\": \"...\" }", "```",
|
|
298
|
-
].join("\n")
|
|
299
|
-
|
|
300
|
-
const modePrefix = opts.flat
|
|
301
|
-
? "one edit per call (flat mode)"
|
|
302
|
-
: "batching all changes to a file in one call"
|
|
303
|
-
|
|
304
|
-
const modeGuidePrefix = opts.flat
|
|
305
|
-
? "- Use `replace` with HASH anchors for all file changes. Only one edit per call."
|
|
306
|
-
: "- Use `replace` with HASH anchors for all file changes; batch every change to one file into a single `replace` call."
|
|
319
|
+
const cfg = MODE_CFG[opts.flat ? "flat" : "bulk"];
|
|
307
320
|
|
|
308
321
|
const E_DESC = loadP("../prompts/replace.md", {
|
|
309
|
-
MODE_DESCRIPTION:
|
|
310
|
-
MODE_EXAMPLES:
|
|
311
|
-
MODE_RULES:
|
|
312
|
-
MODE_REQUEST_STRUCTURE:
|
|
322
|
+
MODE_DESCRIPTION: cfg.desc,
|
|
323
|
+
MODE_EXAMPLES: cfg.examples,
|
|
324
|
+
MODE_RULES: cfg.rules,
|
|
325
|
+
MODE_REQUEST_STRUCTURE: cfg.requestStructure,
|
|
313
326
|
AUTO_READ_GUIDANCE: readGuidance,
|
|
314
327
|
});
|
|
315
328
|
const E_SNIPPET = loadP("../prompts/replace-snippet.md", {
|
|
316
|
-
MODE_PREFIX:
|
|
329
|
+
MODE_PREFIX: cfg.prefix,
|
|
317
330
|
});
|
|
318
331
|
const E_GUIDE = loadGuide("../prompts/replace-guidelines.md", {
|
|
319
|
-
MODE_PREFIX:
|
|
332
|
+
MODE_PREFIX: cfg.guidePrefix,
|
|
320
333
|
AUTO_READ_GUIDANCE: readGuidance,
|
|
321
334
|
});
|
|
322
335
|
|
|
@@ -449,6 +462,8 @@ export function buildToolDef(opts: { flat: boolean; autoRead?: boolean }): ToolD
|
|
|
449
462
|
firstChangedLine,
|
|
450
463
|
lastChangedLine,
|
|
451
464
|
resultHashes,
|
|
465
|
+
totalAddedLines,
|
|
466
|
+
totalRemovedLines,
|
|
452
467
|
} = await execPipeline(
|
|
453
468
|
normalizedParams,
|
|
454
469
|
ctx.cwd,
|
|
@@ -471,6 +486,8 @@ export function buildToolDef(opts: { flat: boolean; autoRead?: boolean }): ToolD
|
|
|
471
486
|
editMeta: {
|
|
472
487
|
editsAttempted,
|
|
473
488
|
noopEditsCount: noopEdits?.length ?? 0,
|
|
489
|
+
addedLines: 0,
|
|
490
|
+
removedLines: 0,
|
|
474
491
|
},
|
|
475
492
|
warnings,
|
|
476
493
|
});
|
|
@@ -501,6 +518,8 @@ export function buildToolDef(opts: { flat: boolean; autoRead?: boolean }): ToolD
|
|
|
501
518
|
noopEditsCount: noopEdits?.length ?? 0,
|
|
502
519
|
firstChangedLine,
|
|
503
520
|
lastChangedLine,
|
|
521
|
+
addedLines: totalAddedLines,
|
|
522
|
+
removedLines: totalRemovedLines,
|
|
504
523
|
};
|
|
505
524
|
|
|
506
525
|
const successInput = {
|
package/src/utils.ts
CHANGED
|
@@ -52,3 +52,27 @@ export function errCode(error: unknown): string | undefined {
|
|
|
52
52
|
}
|
|
53
53
|
return undefined;
|
|
54
54
|
}
|
|
55
|
+
|
|
56
|
+
export function lastNonEmptyIndex(lines: string[]): number {
|
|
57
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
58
|
+
if (lines[i]!.length > 0) return i;
|
|
59
|
+
}
|
|
60
|
+
return -1;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function firstNonEmptyIndex(lines: string[]): number {
|
|
64
|
+
for (let i = 0; i < lines.length; i++) {
|
|
65
|
+
if (lines[i]!.length > 0) return i;
|
|
66
|
+
}
|
|
67
|
+
return -1;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function lastNonEmpty(lines: string[]): string | undefined {
|
|
71
|
+
const idx = lastNonEmptyIndex(lines);
|
|
72
|
+
return idx >= 0 ? lines[idx] : undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function firstNonEmpty(lines: string[]): string | undefined {
|
|
76
|
+
const idx = firstNonEmptyIndex(lines);
|
|
77
|
+
return idx >= 0 ? lines[idx] : undefined;
|
|
78
|
+
}
|