pi-hashline-edit-pro 0.16.13 → 0.16.14
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/hashline/apply.ts +1 -16
- package/src/hashline/resolve.ts +1 -15
- package/src/replace-response.ts +8 -6
- package/src/replace.ts +58 -37
- package/src/utils.ts +24 -0
package/package.json
CHANGED
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[];
|
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/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"]);
|
|
@@ -202,6 +204,21 @@ export async function execPipeline(
|
|
|
202
204
|
}, hashStore);
|
|
203
205
|
|
|
204
206
|
const warnings = [...(anchorResult.warnings ?? [])];
|
|
207
|
+
|
|
208
|
+
let totalAddedLines = 0;
|
|
209
|
+
let totalRemovedLines = 0;
|
|
210
|
+
const noopIndices = new Set(anchorResult.noopEdits?.map((n) => n.editIndex) ?? []);
|
|
211
|
+
for (let i = 0; i < resolved.length; i++) {
|
|
212
|
+
if (noopIndices.has(i)) continue;
|
|
213
|
+
const edit = resolved[i]!;
|
|
214
|
+
const startLine = originalHashes.indexOf(edit.hash_range_inclusive[0].hash);
|
|
215
|
+
const endLine = originalHashes.indexOf(edit.hash_range_inclusive[1].hash);
|
|
216
|
+
if (startLine >= 0 && endLine >= 0) {
|
|
217
|
+
totalRemovedLines += endLine - startLine + 1;
|
|
218
|
+
}
|
|
219
|
+
totalAddedLines += edit.content_lines.length;
|
|
220
|
+
}
|
|
221
|
+
|
|
205
222
|
return {
|
|
206
223
|
path,
|
|
207
224
|
toolEdits,
|
|
@@ -216,6 +233,8 @@ export async function execPipeline(
|
|
|
216
233
|
lastChangedLine: anchorResult.lastChangedLine,
|
|
217
234
|
resultHashes,
|
|
218
235
|
originalHashes,
|
|
236
|
+
totalAddedLines,
|
|
237
|
+
totalRemovedLines,
|
|
219
238
|
};
|
|
220
239
|
}
|
|
221
240
|
|
|
@@ -266,6 +285,32 @@ export function reuseMarkdown(context: any, content: string, theme: any): Markdo
|
|
|
266
285
|
m.setText(content);
|
|
267
286
|
return m;
|
|
268
287
|
}
|
|
288
|
+
const MODE_CFG = {
|
|
289
|
+
flat: {
|
|
290
|
+
desc: " Only one edit per call. The `hash_range_inclusive` and `content_lines` fields sit at the top level of the request object.",
|
|
291
|
+
examples: [
|
|
292
|
+
"", "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\" }",
|
|
293
|
+
].join("\n"),
|
|
294
|
+
rules: "",
|
|
295
|
+
requestStructure: [
|
|
296
|
+
"Flat mode:", "```json", "{ \"content_lines\": [...], \"hash_range_inclusive\": [\"aB3\", \"xY7\"], \"path\": \"...\" }", "```",
|
|
297
|
+
].join("\n"),
|
|
298
|
+
prefix: "one edit per call (flat mode)",
|
|
299
|
+
guidePrefix: "- Use `replace` with HASH anchors for all file changes. Only one edit per call.",
|
|
300
|
+
},
|
|
301
|
+
bulk: {
|
|
302
|
+
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.",
|
|
303
|
+
examples: [
|
|
304
|
+
"", "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\" }",
|
|
305
|
+
].join("\n"),
|
|
306
|
+
rules: "- Multiple edits in one call must not overlap. Overlapping ranges are rejected with [E_EDIT_CONFLICT].",
|
|
307
|
+
requestStructure: [
|
|
308
|
+
"Bulk mode (default):", "```json", "{ \"changes\": [{ \"content_lines\": [...], \"hash_range_inclusive\": [\"aB3\", \"xY7\"] }], \"path\": \"...\" }", "```",
|
|
309
|
+
].join("\n"),
|
|
310
|
+
prefix: "batching all changes to a file in one call",
|
|
311
|
+
guidePrefix: "- Use `replace` with HASH anchors for all file changes; batch every change to one file into a single `replace` call.",
|
|
312
|
+
},
|
|
313
|
+
} as const;
|
|
269
314
|
|
|
270
315
|
export function buildToolDef(opts: { flat: boolean; autoRead?: boolean }): ToolDef {
|
|
271
316
|
const autoRead = opts.autoRead ?? false;
|
|
@@ -273,50 +318,20 @@ export function buildToolDef(opts: { flat: boolean; autoRead?: boolean }): ToolD
|
|
|
273
318
|
? "Anchors are provided automatically after write and replace operations when auto-read is enabled."
|
|
274
319
|
: "Call `read` to get fresh anchors for follow-up edits.";
|
|
275
320
|
|
|
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."
|
|
321
|
+
const cfg = MODE_CFG[opts.flat ? "flat" : "bulk"];
|
|
307
322
|
|
|
308
323
|
const E_DESC = loadP("../prompts/replace.md", {
|
|
309
|
-
MODE_DESCRIPTION:
|
|
310
|
-
MODE_EXAMPLES:
|
|
311
|
-
MODE_RULES:
|
|
312
|
-
MODE_REQUEST_STRUCTURE:
|
|
324
|
+
MODE_DESCRIPTION: cfg.desc,
|
|
325
|
+
MODE_EXAMPLES: cfg.examples,
|
|
326
|
+
MODE_RULES: cfg.rules,
|
|
327
|
+
MODE_REQUEST_STRUCTURE: cfg.requestStructure,
|
|
313
328
|
AUTO_READ_GUIDANCE: readGuidance,
|
|
314
329
|
});
|
|
315
330
|
const E_SNIPPET = loadP("../prompts/replace-snippet.md", {
|
|
316
|
-
MODE_PREFIX:
|
|
331
|
+
MODE_PREFIX: cfg.prefix,
|
|
317
332
|
});
|
|
318
333
|
const E_GUIDE = loadGuide("../prompts/replace-guidelines.md", {
|
|
319
|
-
MODE_PREFIX:
|
|
334
|
+
MODE_PREFIX: cfg.guidePrefix,
|
|
320
335
|
AUTO_READ_GUIDANCE: readGuidance,
|
|
321
336
|
});
|
|
322
337
|
|
|
@@ -449,6 +464,8 @@ export function buildToolDef(opts: { flat: boolean; autoRead?: boolean }): ToolD
|
|
|
449
464
|
firstChangedLine,
|
|
450
465
|
lastChangedLine,
|
|
451
466
|
resultHashes,
|
|
467
|
+
totalAddedLines,
|
|
468
|
+
totalRemovedLines,
|
|
452
469
|
} = await execPipeline(
|
|
453
470
|
normalizedParams,
|
|
454
471
|
ctx.cwd,
|
|
@@ -471,6 +488,8 @@ export function buildToolDef(opts: { flat: boolean; autoRead?: boolean }): ToolD
|
|
|
471
488
|
editMeta: {
|
|
472
489
|
editsAttempted,
|
|
473
490
|
noopEditsCount: noopEdits?.length ?? 0,
|
|
491
|
+
addedLines: 0,
|
|
492
|
+
removedLines: 0,
|
|
474
493
|
},
|
|
475
494
|
warnings,
|
|
476
495
|
});
|
|
@@ -501,6 +520,8 @@ export function buildToolDef(opts: { flat: boolean; autoRead?: boolean }): ToolD
|
|
|
501
520
|
noopEditsCount: noopEdits?.length ?? 0,
|
|
502
521
|
firstChangedLine,
|
|
503
522
|
lastChangedLine,
|
|
523
|
+
addedLines: totalAddedLines,
|
|
524
|
+
removedLines: totalRemovedLines,
|
|
504
525
|
};
|
|
505
526
|
|
|
506
527
|
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
|
+
}
|