pi-hashline-edit-pro 0.16.12 → 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 +7 -6
- package/src/hashline/resolve.ts +17 -7
- package/src/replace-normalize.ts +27 -9
- package/src/replace-response.ts +8 -6
- package/src/replace.ts +68 -41
- 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,7 +12,6 @@ import {
|
|
|
12
12
|
type BDupWarn,
|
|
13
13
|
type AutoFix,
|
|
14
14
|
} from "./resolve";
|
|
15
|
-
import { visLines } from "../utils";
|
|
16
15
|
|
|
17
16
|
type LIdx = {
|
|
18
17
|
fileLines: string[];
|
|
@@ -306,13 +305,15 @@ export function applyEdits(
|
|
|
306
305
|
const edit = correctedEdits[bw.editIndex];
|
|
307
306
|
if (!edit) continue;
|
|
308
307
|
if (bw.kind === "trailing") {
|
|
309
|
-
const
|
|
310
|
-
if (
|
|
308
|
+
const idx = lastNonEmptyIndex(edit.content_lines);
|
|
309
|
+
if (idx >= 0) {
|
|
310
|
+
const removed = edit.content_lines.splice(idx, 1)[0];
|
|
311
311
|
autoFixes.push({ kind: "trailing", editIndex: bw.editIndex, removedLine: removed });
|
|
312
312
|
}
|
|
313
313
|
} else {
|
|
314
|
-
const
|
|
315
|
-
if (
|
|
314
|
+
const idx = firstNonEmptyIndex(edit.content_lines);
|
|
315
|
+
if (idx >= 0) {
|
|
316
|
+
const removed = edit.content_lines.splice(idx, 1)[0];
|
|
316
317
|
autoFixes.push({ kind: "leading", editIndex: bw.editIndex, removedLine: removed });
|
|
317
318
|
}
|
|
318
319
|
}
|
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";
|
|
@@ -94,7 +93,7 @@ export function fmtMismatch(
|
|
|
94
93
|
const refList = notFound.map((m) => `"${m.ref.hash}"`).join(", ");
|
|
95
94
|
if (notFound.length > 0) {
|
|
96
95
|
out.push(
|
|
97
|
-
`[E_STALE_ANCHOR] ${notFound.length} stale anchor${notFound.length > 1 ? "s" : ""}${filePath ? ` in ${filePath}` : ""}: ${refList}. Call read() to get fresh anchors, then copy the 3-char HASH of the start and end of the range you are replacing into hash_range_inclusive of your next replace call.`
|
|
96
|
+
`[E_STALE_ANCHOR] ${notFound.length} stale anchor${notFound.length > 1 ? "s" : ""}${filePath ? ` in ${filePath}` : ""}: ${refList}. The file content has changed since those anchors were read. Call read() to get fresh anchors, then copy the 3-char HASH of the start and end of the range you are replacing into hash_range_inclusive of your next replace call.`
|
|
98
97
|
);
|
|
99
98
|
}
|
|
100
99
|
if (ambiguous.length > 0) {
|
|
@@ -153,9 +152,19 @@ function assertItem(edit: Record<string, unknown>, index: number): void {
|
|
|
153
152
|
if ("content_lines" in edit && !isStrArr(edit.content_lines)) {
|
|
154
153
|
const val = edit.content_lines;
|
|
155
154
|
if (typeof val === "string") {
|
|
156
|
-
|
|
155
|
+
try {
|
|
156
|
+
const parsed = JSON.parse(val);
|
|
157
|
+
if (Array.isArray(parsed)) {
|
|
158
|
+
edit.content_lines = parsed;
|
|
159
|
+
} else {
|
|
160
|
+
throw new Error(CONTENT_LINES_NOT_STRING_MSG);
|
|
161
|
+
}
|
|
162
|
+
} catch {
|
|
163
|
+
throw new Error(CONTENT_LINES_NOT_STRING_MSG);
|
|
164
|
+
}
|
|
165
|
+
} else {
|
|
166
|
+
throw new Error(`[E_BAD_SHAPE] Edit ${index} field "content_lines" must be a string array.`);
|
|
157
167
|
}
|
|
158
|
-
throw new Error(`[E_BAD_SHAPE] Edit ${index} field "content_lines" must be a string array.`);
|
|
159
168
|
}
|
|
160
169
|
if (!isStrPair(edit.hash_range_inclusive)) {
|
|
161
170
|
throw new Error(
|
|
@@ -230,6 +239,7 @@ export function descEdit(edit: RHEdit): string {
|
|
|
230
239
|
return `replace ${edit.hash_range_inclusive[0].hash}-${edit.hash_range_inclusive[1].hash}`;
|
|
231
240
|
}
|
|
232
241
|
|
|
242
|
+
|
|
233
243
|
function checkBoundaryDup(
|
|
234
244
|
adjacentLine: string | undefined,
|
|
235
245
|
replacementEdge: string | undefined,
|
|
@@ -296,11 +306,11 @@ export function valEdits(
|
|
|
296
306
|
}
|
|
297
307
|
const endLine = endResolved.line;
|
|
298
308
|
const nextLine = fileLines[endLine];
|
|
299
|
-
const replacementLastLine = edit.content_lines
|
|
309
|
+
const replacementLastLine = lastNonEmpty(edit.content_lines);
|
|
300
310
|
const trailing = checkBoundaryDup(nextLine, replacementLastLine, "trailing", endLine, resolved.length);
|
|
301
311
|
if (trailing) boundaryWarnings.push(trailing);
|
|
302
312
|
const prevLine = fileLines[startResolved.line - 2];
|
|
303
|
-
const replacementFirstLine = edit.content_lines
|
|
313
|
+
const replacementFirstLine = firstNonEmpty(edit.content_lines);
|
|
304
314
|
const leading = checkBoundaryDup(prevLine, replacementFirstLine, "leading", startResolved.line - 2, resolved.length);
|
|
305
315
|
if (leading) boundaryWarnings.push(leading);
|
|
306
316
|
resolved.push({
|
package/src/replace-normalize.ts
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import { isRec, has } from "./utils";
|
|
2
2
|
import { CONTENT_LINES_NOT_STRING_MSG } from "./constants";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
)
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
3
|
+
|
|
4
|
+
function tryParseContentLines(record: Record<string, unknown>, key: string, label: string): void {
|
|
5
|
+
const val = record[key];
|
|
6
|
+
if (typeof val !== "string") return;
|
|
7
|
+
try {
|
|
8
|
+
const parsed = JSON.parse(val);
|
|
9
|
+
if (Array.isArray(parsed)) {
|
|
10
|
+
record[key] = parsed;
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
} catch {
|
|
14
|
+
// fall through to error
|
|
15
|
+
}
|
|
16
|
+
throw new Error(CONTENT_LINES_NOT_STRING_MSG);
|
|
10
17
|
}
|
|
11
18
|
|
|
12
19
|
export function normalizeFilePath(record: Record<string, unknown>): void {
|
|
@@ -27,6 +34,17 @@ function normalizeField(
|
|
|
27
34
|
record[to] = raw;
|
|
28
35
|
} else if (isRec(raw)) {
|
|
29
36
|
record[to] = [raw];
|
|
37
|
+
} else if (typeof raw === "string") {
|
|
38
|
+
try {
|
|
39
|
+
const parsed = JSON.parse(raw);
|
|
40
|
+
if (Array.isArray(parsed)) {
|
|
41
|
+
record[to] = parsed;
|
|
42
|
+
} else if (isRec(parsed)) {
|
|
43
|
+
record[to] = [parsed];
|
|
44
|
+
}
|
|
45
|
+
} catch {
|
|
46
|
+
// not valid JSON, leave as-is for downstream validation
|
|
47
|
+
}
|
|
30
48
|
}
|
|
31
49
|
if (from !== to) delete record[from];
|
|
32
50
|
}
|
|
@@ -41,7 +59,7 @@ export function normReq(input: unknown): unknown {
|
|
|
41
59
|
normalizeFilePath(record);
|
|
42
60
|
|
|
43
61
|
if (has(record, "content_lines") && typeof record.content_lines === "string") {
|
|
44
|
-
|
|
62
|
+
tryParseContentLines(record, "content_lines", "Top-level");
|
|
45
63
|
}
|
|
46
64
|
|
|
47
65
|
normalizeField(record, "changes", "changes");
|
|
@@ -51,7 +69,7 @@ export function normReq(input: unknown): unknown {
|
|
|
51
69
|
for (let i = 0; i < record.changes.length; i++) {
|
|
52
70
|
const item = record.changes[i];
|
|
53
71
|
if (isRec(item) && has(item, "content_lines") && typeof item.content_lines === "string") {
|
|
54
|
-
|
|
72
|
+
tryParseContentLines(item, "content_lines", `changes[${i}]`);
|
|
55
73
|
}
|
|
56
74
|
}
|
|
57
75
|
}
|
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,12 +111,15 @@ 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
|
-
const ROOT_KS = new Set(["path", "changes"]);
|
|
118
|
+
const ROOT_KS = new Set(["path", "changes", "content_lines", "hash_range_inclusive"]);
|
|
117
119
|
|
|
118
120
|
export function assertReq(
|
|
119
121
|
request: unknown,
|
|
122
|
+
flat?: boolean
|
|
120
123
|
): asserts request is ReqParams {
|
|
121
124
|
if (!isRec(request)) {
|
|
122
125
|
throw new Error("[E_BAD_SHAPE] Edit request must be an object.");
|
|
@@ -137,10 +140,14 @@ export function assertReq(
|
|
|
137
140
|
}
|
|
138
141
|
|
|
139
142
|
if (!Array.isArray(request.changes)) {
|
|
143
|
+
if (flat) {
|
|
144
|
+
throw new Error(
|
|
145
|
+
'[E_BAD_SHAPE] Edit request requires both "content_lines" and "hash_range_inclusive" at the top level.',
|
|
146
|
+
);
|
|
147
|
+
}
|
|
140
148
|
throw new Error('[E_BAD_SHAPE] Edit request requires a "changes" array. Each change is { content_lines: [...], hash_range_inclusive: ["<START>", "<END>"] }.');
|
|
141
149
|
}
|
|
142
150
|
}
|
|
143
|
-
|
|
144
151
|
export async function execPipeline(
|
|
145
152
|
params: ReqParams,
|
|
146
153
|
cwd: string,
|
|
@@ -197,6 +204,21 @@ export async function execPipeline(
|
|
|
197
204
|
}, hashStore);
|
|
198
205
|
|
|
199
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
|
+
|
|
200
222
|
return {
|
|
201
223
|
path,
|
|
202
224
|
toolEdits,
|
|
@@ -211,16 +233,19 @@ export async function execPipeline(
|
|
|
211
233
|
lastChangedLine: anchorResult.lastChangedLine,
|
|
212
234
|
resultHashes,
|
|
213
235
|
originalHashes,
|
|
236
|
+
totalAddedLines,
|
|
237
|
+
totalRemovedLines,
|
|
214
238
|
};
|
|
215
239
|
}
|
|
216
240
|
|
|
217
241
|
export async function compPreview(
|
|
218
242
|
request: unknown,
|
|
219
243
|
cwd: string,
|
|
244
|
+
flat?: boolean
|
|
220
245
|
): Promise<RPreview> {
|
|
221
246
|
try {
|
|
222
247
|
const normalized = normReq(request);
|
|
223
|
-
assertReq(normalized);
|
|
248
|
+
assertReq(normalized, flat);
|
|
224
249
|
const { path, originalNormalized, originalHashes, result, resultHashes } = await execPipeline(
|
|
225
250
|
normalized,
|
|
226
251
|
cwd,
|
|
@@ -260,6 +285,32 @@ export function reuseMarkdown(context: any, content: string, theme: any): Markdo
|
|
|
260
285
|
m.setText(content);
|
|
261
286
|
return m;
|
|
262
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;
|
|
263
314
|
|
|
264
315
|
export function buildToolDef(opts: { flat: boolean; autoRead?: boolean }): ToolDef {
|
|
265
316
|
const autoRead = opts.autoRead ?? false;
|
|
@@ -267,50 +318,20 @@ export function buildToolDef(opts: { flat: boolean; autoRead?: boolean }): ToolD
|
|
|
267
318
|
? "Anchors are provided automatically after write and replace operations when auto-read is enabled."
|
|
268
319
|
: "Call `read` to get fresh anchors for follow-up edits.";
|
|
269
320
|
|
|
270
|
-
const
|
|
271
|
-
? " Only one edit per call. The `hash_range_inclusive` and `content_lines` fields sit at the top level of the request object."
|
|
272
|
-
: "\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.";
|
|
273
|
-
|
|
274
|
-
const modeExamples = opts.flat
|
|
275
|
-
? [
|
|
276
|
-
"", "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\" }",
|
|
277
|
-
].join("\n")
|
|
278
|
-
: [
|
|
279
|
-
"", "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\" }",
|
|
280
|
-
].join("\n")
|
|
281
|
-
|
|
282
|
-
const modeRules = opts.flat
|
|
283
|
-
? ""
|
|
284
|
-
: "- Multiple edits in one call must not overlap. Overlapping ranges are rejected with [E_EDIT_CONFLICT]."
|
|
285
|
-
|
|
286
|
-
const modeRequestStructure = opts.flat
|
|
287
|
-
? [
|
|
288
|
-
"Flat mode:", "```json", "{ \"content_lines\": [...], \"hash_range_inclusive\": [\"aB3\", \"xY7\"], \"path\": \"...\" }", "```",
|
|
289
|
-
].join("\n")
|
|
290
|
-
: [
|
|
291
|
-
"Bulk mode (default):", "```json", "{ \"changes\": [{ \"content_lines\": [...], \"hash_range_inclusive\": [\"aB3\", \"xY7\"] }], \"path\": \"...\" }", "```",
|
|
292
|
-
].join("\n")
|
|
293
|
-
|
|
294
|
-
const modePrefix = opts.flat
|
|
295
|
-
? "one edit per call (flat mode)"
|
|
296
|
-
: "batching all changes to a file in one call"
|
|
297
|
-
|
|
298
|
-
const modeGuidePrefix = opts.flat
|
|
299
|
-
? "- Use `replace` with HASH anchors for all file changes. Only one edit per call."
|
|
300
|
-
: "- 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"];
|
|
301
322
|
|
|
302
323
|
const E_DESC = loadP("../prompts/replace.md", {
|
|
303
|
-
MODE_DESCRIPTION:
|
|
304
|
-
MODE_EXAMPLES:
|
|
305
|
-
MODE_RULES:
|
|
306
|
-
MODE_REQUEST_STRUCTURE:
|
|
324
|
+
MODE_DESCRIPTION: cfg.desc,
|
|
325
|
+
MODE_EXAMPLES: cfg.examples,
|
|
326
|
+
MODE_RULES: cfg.rules,
|
|
327
|
+
MODE_REQUEST_STRUCTURE: cfg.requestStructure,
|
|
307
328
|
AUTO_READ_GUIDANCE: readGuidance,
|
|
308
329
|
});
|
|
309
330
|
const E_SNIPPET = loadP("../prompts/replace-snippet.md", {
|
|
310
|
-
MODE_PREFIX:
|
|
331
|
+
MODE_PREFIX: cfg.prefix,
|
|
311
332
|
});
|
|
312
333
|
const E_GUIDE = loadGuide("../prompts/replace-guidelines.md", {
|
|
313
|
-
MODE_PREFIX:
|
|
334
|
+
MODE_PREFIX: cfg.guidePrefix,
|
|
314
335
|
AUTO_READ_GUIDANCE: readGuidance,
|
|
315
336
|
});
|
|
316
337
|
|
|
@@ -352,7 +373,7 @@ export function buildToolDef(opts: { flat: boolean; autoRead?: boolean }): ToolD
|
|
|
352
373
|
context.state.preview = undefined;
|
|
353
374
|
const previewGeneration = (context.state.previewGeneration ?? 0) + 1;
|
|
354
375
|
context.state.previewGeneration = previewGeneration;
|
|
355
|
-
compPreview(previewInput, context.cwd)
|
|
376
|
+
compPreview(previewInput, context.cwd, opts.flat)
|
|
356
377
|
.then((preview) => {
|
|
357
378
|
if (
|
|
358
379
|
context.state.argsKey === argsKey &&
|
|
@@ -443,6 +464,8 @@ export function buildToolDef(opts: { flat: boolean; autoRead?: boolean }): ToolD
|
|
|
443
464
|
firstChangedLine,
|
|
444
465
|
lastChangedLine,
|
|
445
466
|
resultHashes,
|
|
467
|
+
totalAddedLines,
|
|
468
|
+
totalRemovedLines,
|
|
446
469
|
} = await execPipeline(
|
|
447
470
|
normalizedParams,
|
|
448
471
|
ctx.cwd,
|
|
@@ -465,6 +488,8 @@ export function buildToolDef(opts: { flat: boolean; autoRead?: boolean }): ToolD
|
|
|
465
488
|
editMeta: {
|
|
466
489
|
editsAttempted,
|
|
467
490
|
noopEditsCount: noopEdits?.length ?? 0,
|
|
491
|
+
addedLines: 0,
|
|
492
|
+
removedLines: 0,
|
|
468
493
|
},
|
|
469
494
|
warnings,
|
|
470
495
|
});
|
|
@@ -495,6 +520,8 @@ export function buildToolDef(opts: { flat: boolean; autoRead?: boolean }): ToolD
|
|
|
495
520
|
noopEditsCount: noopEdits?.length ?? 0,
|
|
496
521
|
firstChangedLine,
|
|
497
522
|
lastChangedLine,
|
|
523
|
+
addedLines: totalAddedLines,
|
|
524
|
+
removedLines: totalRemovedLines,
|
|
498
525
|
};
|
|
499
526
|
|
|
500
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
|
+
}
|