pi-hashline-edit-pro 4.1.0 → 4.2.1

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/src/insert.ts CHANGED
@@ -3,43 +3,17 @@ import { Type } from "typebox";
3
3
  import { constants } from "fs";
4
4
  import { execPipeline, type ReqParams, type ReplaceDetails, previewFromPipe, previewError } from "./replace";
5
5
  import { commitEdit } from "./commit";
6
+ import { batchMemberFor, executeBatchMember, noteBatchFailure } from "./batch";
6
7
  import { readNormFile, type NormFile } from "./file-reader";
7
- import { MAX_HASH_LINES, parseHashRef, resolveAnchorLine, type Anchor } from "./hashline";
8
+ import { MAX_HASH_LINES, parseHashRef, resEdit, resolveAnchorLine, type Anchor, type HEdit } from "./hashline";
8
9
  import { stripAnchorRow } from "./hashline/resolve";
9
10
  import { loadP, loadGuide } from "./prompts";
10
- import { normReq } from "./payload-contract";
11
- import { decodeStringArray, isRec, rejectUnknownFields, splitLines } from "./utils";
11
+ import { assertInsertReq, normReq, type InsertReq } from "./payload-contract";
12
+ import { decodeStringArray, isRec, splitLines } from "./utils";
12
13
  import { clearBoundaryBypass } from "./boundary-bypass";
13
14
  import { queuedEdit, editToolBase, editRenderCallWrapper, editRenderResultWrapper, resolveEditTargetWithRequirement, throwIfStrictInput, withInsertPrompts, DEFAULT_EDIT_FLAGS, type EditToolFlags } from "./edit-common";
14
15
  import type { RPreview, RRState } from "./replace-render";
15
-
16
- const INSERT_KS = new Set(["path", "anchor", "direction", "lines"]);
17
-
18
- export interface InsertReq {
19
- path?: string;
20
- anchor: string;
21
- direction: "before" | "after";
22
- lines: string[];
23
- }
24
-
25
- export function assertInsertReq(request: unknown): asserts request is InsertReq {
26
- if (!isRec(request)) {
27
- throw new Error("[E_BAD_SHAPE] Insert request must be an object.");
28
- }
29
- rejectUnknownFields(request, INSERT_KS, "Insert request");
30
- if (request.path !== undefined && typeof request.path !== "string") {
31
- throw new Error('[E_BAD_SHAPE] Insert request field "path" must be a string when provided.');
32
- }
33
- if (typeof request.anchor !== "string" || request.anchor.length === 0) {
34
- throw new Error('[E_BAD_SHAPE] Insert request requires an "anchor" string (4-char anchor from read output).');
35
- }
36
- if (request.direction !== "before" && request.direction !== "after") {
37
- throw new Error('[E_BAD_SHAPE] Insert request "direction" must be "before" or "after".');
38
- }
39
- if (!Array.isArray(request.lines) || request.lines.some((line) => typeof line !== "string")) {
40
- throw new Error('[E_BAD_SHAPE] Insert request requires "lines" as an array of strings, one element per line.');
41
- }
42
- }
16
+ export { assertInsertReq, type InsertReq };
43
17
 
44
18
  const insertAnchorSchema = Type.String({
45
19
  description:
@@ -71,7 +45,7 @@ const insertToolSchema = Type.Object(
71
45
  direction: insertDirectionSchema,
72
46
  lines: insertLinesSchema,
73
47
  },
74
- { additionalProperties: false },
48
+ { additionalProperties: true },
75
49
  );
76
50
 
77
51
  export function buildInsertToolSchema(requirePath: boolean): typeof insertToolSchema {
@@ -83,18 +57,18 @@ export function buildInsertToolSchema(requirePath: boolean): typeof insertToolSc
83
57
  direction: insertDirectionSchema,
84
58
  lines: insertLinesSchema,
85
59
  },
86
- { additionalProperties: false },
60
+ { additionalProperties: true },
87
61
  ) as typeof insertToolSchema;
88
62
  }
89
63
 
90
- function parseInsertAnchor(raw: string): { ref: Anchor; warnings: string[] } {
64
+ export function parseInsertAnchor(raw: string): { ref: Anchor; warnings: string[] } {
91
65
  const trimmedAnchor = raw.trim();
92
66
  const warnings: string[] = [];
93
67
  const anchorText = stripAnchorRow(trimmedAnchor, "anchor entry", warnings);
94
68
  return { ref: parseHashRef(anchorText), warnings };
95
69
  }
96
70
 
97
- function buildInsertEdit(
71
+ export function buildInsertEdit(
98
72
  req: InsertReq,
99
73
  preload: NormFile,
100
74
  ref: Anchor,
@@ -216,32 +190,68 @@ export function buildInsertToolDef(flags: EditToolFlags = DEFAULT_EDIT_FLAGS): I
216
190
  anchor: req.anchor,
217
191
  providedPath: req.path,
218
192
  cwd: ctx.cwd,
193
+ }).catch((error: unknown) => {
194
+ const member = batchMemberFor(_toolCallId);
195
+ if (member) noteBatchFailure(member, error);
196
+ throw error;
219
197
  });
220
- const { ref, warnings: anchorWarnings } = parseInsertAnchor(req.anchor);
221
- await throwIfStrictInput([...anchorWarnings, ...insertWarnings]);
198
+ let ref: Anchor;
199
+ let anchorWarnings: string[];
200
+ try {
201
+ ({ ref, warnings: anchorWarnings } = parseInsertAnchor(req.anchor));
202
+ await throwIfStrictInput([...anchorWarnings, ...insertWarnings]);
203
+ } catch (error) {
204
+ const member = batchMemberFor(_toolCallId);
205
+ if (member) noteBatchFailure(member, error);
206
+ throw error;
207
+ }
222
208
  return queuedEdit(targetPath, ctx.cwd, signal, async (absolutePath, mutationTargetPath) => {
209
+ const member = batchMemberFor(_toolCallId);
223
210
  const preload = await readNormFile(targetPath, ctx.cwd, {
224
211
  signal,
225
212
  accessMode: constants.R_OK | constants.W_OK,
226
213
  maxLines: MAX_HASH_LINES,
227
214
  });
228
215
  const { editParams, anchorLine } = buildInsertEdit(req, preload, ref, targetPath);
229
- const pipe = await execPipeline(targetPath, editParams, ctx.cwd, {
230
- accessMode: constants.R_OK | constants.W_OK,
231
- signal,
232
- preloadedNorm: preload,
233
- skipBoundaryDedup: true,
234
- });
235
- return commitEdit(pipe, {
236
- path: pipe.path,
237
- absolutePath,
216
+ if (!member) {
217
+ const pipe = await execPipeline(targetPath, editParams, ctx.cwd, {
218
+ accessMode: constants.R_OK | constants.W_OK,
219
+ signal,
220
+ preloadedNorm: preload,
221
+ skipBoundaryDedup: true,
222
+ });
223
+ return commitEdit(pipe, {
224
+ path: pipe.path,
225
+ absolutePath,
226
+ mutationTargetPath,
227
+ signal,
228
+ verb: "inserted",
229
+ noopNoun: "Insertion",
230
+ foldedAnchorLines: anchorLine === undefined ? 0 : 1,
231
+ prefixWarnings: [...anchorWarnings, ...insertWarnings],
232
+ onApplied: () => clearBoundaryBypass(mutationTargetPath),
233
+ });
234
+ }
235
+ let hedit: HEdit;
236
+ const resWarnings: string[] = [];
237
+ try {
238
+ hedit = resEdit(editParams, resWarnings);
239
+ } catch (error) {
240
+ noteBatchFailure(member, error);
241
+ throw error;
242
+ }
243
+ return executeBatchMember({
244
+ kind: "insert",
245
+ member,
246
+ targetPath,
238
247
  mutationTargetPath,
248
+ cwd: ctx.cwd,
239
249
  signal,
240
- verb: "inserted",
241
- noopNoun: "Insertion",
242
- foldedAnchorLines: anchorLine === undefined ? 0 : 1,
243
- prefixWarnings: [...anchorWarnings, ...insertWarnings],
244
- onApplied: () => clearBoundaryBypass(mutationTargetPath),
250
+ hedit,
251
+ extraWarnings: [...anchorWarnings, ...insertWarnings, ...resWarnings],
252
+ skipBoundaryDedup: true,
253
+ strictBoundaryDedup: false,
254
+ foldedLines: anchorLine === undefined ? 0 : 1,
245
255
  });
246
256
  });
247
257
  },
@@ -32,7 +32,7 @@ export const editToolSchema = Type.Object(
32
32
  remove_to: removeToSchema,
33
33
  replacement_lines: replacementLinesSchema,
34
34
  },
35
- { additionalProperties: false },
35
+ { additionalProperties: true },
36
36
  );
37
37
 
38
38
  export function buildEditToolSchema(requirePath: boolean): typeof editToolSchema {
@@ -44,7 +44,7 @@ export function buildEditToolSchema(requirePath: boolean): typeof editToolSchema
44
44
  remove_to: removeToSchema,
45
45
  replacement_lines: replacementLinesSchema,
46
46
  },
47
- { additionalProperties: false },
47
+ { additionalProperties: true },
48
48
  ) as typeof editToolSchema;
49
49
  }
50
50
 
@@ -109,3 +109,31 @@ export function getPreviewInput(args: unknown): { path?: string; remove_from: st
109
109
  replacement_lines: normalized.replacement_lines as string[],
110
110
  };
111
111
  }
112
+
113
+ const INSERT_KS = new Set(["path", "anchor", "direction", "lines"]);
114
+
115
+ export interface InsertReq {
116
+ path?: string;
117
+ anchor: string;
118
+ direction: "before" | "after";
119
+ lines: string[];
120
+ }
121
+
122
+ export function assertInsertReq(request: unknown): asserts request is InsertReq {
123
+ if (!isRec(request)) {
124
+ throw new Error("[E_BAD_SHAPE] Insert request must be an object.");
125
+ }
126
+ rejectUnknownFields(request, INSERT_KS, "Insert request");
127
+ if (request.path !== undefined && typeof request.path !== "string") {
128
+ throw new Error('[E_BAD_SHAPE] Insert request field "path" must be a string when provided.');
129
+ }
130
+ if (typeof request.anchor !== "string" || request.anchor.length === 0) {
131
+ throw new Error('[E_BAD_SHAPE] Insert request requires an "anchor" string (4-char anchor from read output).');
132
+ }
133
+ if (request.direction !== "before" && request.direction !== "after") {
134
+ throw new Error('[E_BAD_SHAPE] Insert request "direction" must be "before" or "after".');
135
+ }
136
+ if (!Array.isArray(request.lines) || request.lines.some((line) => typeof line !== "string")) {
137
+ throw new Error('[E_BAD_SHAPE] Insert request requires "lines" as an array of strings, one element per line.');
138
+ }
139
+ }
@@ -170,10 +170,10 @@ export function withDedupRows(diff: string, lineNumbers: (number | undefined)[],
170
170
  return { diff: out.join("\n"), lineNumbers: nums };
171
171
  }
172
172
 
173
- export function buildChanged(input: SuccessInput, verb = "replaced"): TResult {
173
+ export function buildChanged(input: SuccessInput, verb = "replaced", diffContextLines = 1): TResult {
174
174
  const { path, result, warnings, snapshotId, originalNormalized, originalHashes, editMeta, resultHashes, boundaryDedupAbove, boundaryDedupBelow } = input;
175
175
  const resultLines = visLines(result);
176
- const baseDiff = genDiff(originalNormalized, result, 1, resultHashes, originalHashes);
176
+ const baseDiff = genDiff(originalNormalized, result, diffContextLines, resultHashes, originalHashes);
177
177
  const diffResult = withDedupRows(baseDiff.diff, baseDiff.lineNumbers, boundaryDedupAbove, boundaryDedupBelow);
178
178
  const addedLines = editMeta.addedLines;
179
179
  const removedLines = editMeta.removedLines;
@@ -11,6 +11,7 @@ import { markServed as markServedScoped, freeAnchors, adoptAnchors } from "./anc
11
11
  import { resolveInCwd, writeAtomic, type FileIdentity } from "./fs-write";
12
12
  import { toLF, stripBOM, restoreEndings, type LineEnding } from "./normalize";
13
13
  import { genDiff, genPatch } from "./replace-diff";
14
+ import { getDiffContextLines } from "./config";
14
15
  import { cntDiff, errCode, makePrepareArguments, splitLines } from "./utils";
15
16
  import { loadP, loadGuide } from "./prompts";
16
17
  import { buildMetrics } from "./replace-response";
@@ -178,13 +179,13 @@ export function regUndo(pi: ExtensionAPI): void {
178
179
  const linesAddedByReplace = cntDiff(diffResult.diff, "+");
179
180
  const linesRemovedByReplace = cntDiff(diffResult.diff, "-");
180
181
  const restoredRange = changedRange(currentNormalized, undo.content);
181
- const undoDiffResult = genDiff(currentNormalized, undo.content, 1, undo.hashes, currentHashes);
182
+ const undoDiffResult = genDiff(currentNormalized, undo.content, await getDiffContextLines(), undo.hashes, currentHashes);
182
183
  const undoDiff = undoDiffResult.diff;
183
184
 
184
185
  try {
185
186
  const store = await loadHashStore();
186
187
  const undoLines = splitLines(undo.content);
187
- persistSnapshot(store, mutationTargetPath, undo.content, undo.hashes);
188
+ persistSnapshot(store, mutationTargetPath, undo.content, undo.hashes, undoLines.map((line) => contentChecksum(hashSource(line))));
188
189
  freeAnchors(mutationTargetPath);
189
190
  adoptAnchors(
190
191
  mutationTargetPath,
package/src/replace.ts CHANGED
@@ -22,7 +22,6 @@ import { applyEdit,
22
22
  type HEdit,
23
23
  type NEdit,
24
24
  } from "./hashline";
25
- import { commitEdit } from "./commit";
26
25
  import { withDedupRows, type RMetrics } from "./replace-response";
27
26
  import {
28
27
  type RPreview,
@@ -33,7 +32,9 @@ import { adoptAnchors, servedForPath } from "./anchor-registry";
33
32
  import { resolveTarget } from "./fs-write";
34
33
  import { toCwd } from "./paths";
35
34
  import { noopPayloadKey, markBoundaryNoop, consumeBoundaryBypass, clearBoundaryBypass } from "./boundary-bypass";
36
- import { queuedEdit, editToolBase, editRenderCallWrapper, editRenderResultWrapper, resolveEditTargetWithRequirement, throwIfStrictInput, isBoundaryDedupEnabled, withReplacePrompts, DEFAULT_EDIT_FLAGS, type EditToolFlags } from "./edit-common";
35
+ import { queuedEdit, editToolBase, editRenderCallWrapper, editRenderResultWrapper, resolveEditTargetWithRequirement, throwIfStrictInput, getBoundaryDedupMode, withReplacePrompts, DEFAULT_EDIT_FLAGS, type EditToolFlags } from "./edit-common";
36
+ import { commitEdit } from "./commit";
37
+ import { batchMemberFor, executeBatchMember, noteBatchFailure } from "./batch";
37
38
 
38
39
  export { editToolSchema, type ReqParams, assertReq };
39
40
 
@@ -47,6 +48,7 @@ export type ReplaceDetails = {
47
48
  metrics?: RMetrics;
48
49
  diffLineNumbers?: (number|undefined)[];
49
50
  warnings?: string[];
51
+ batch?: { id: number; size: number; last: boolean; total: number };
50
52
  };
51
53
 
52
54
  export interface PipelineResult {
@@ -112,13 +114,7 @@ function countLineChanges(
112
114
  };
113
115
  }
114
116
 
115
- export async function execPipeline(
116
- targetPath: string,
117
- params: ReqParams,
118
- cwd: string,
119
- options?: ExecPipelineOptions,
120
- ): Promise<PipelineResult> {
121
-
117
+ export function buildReplaceHEdit(params: ReqParams): { edit: HEdit; warnings: string[] } {
122
118
  const editWarnings: string[] = [];
123
119
  let replacementLines = params.replacement_lines;
124
120
  const expandedReplacement = decodeStringArray(replacementLines);
@@ -134,7 +130,17 @@ export async function execPipeline(
134
130
  },
135
131
  editWarnings,
136
132
  );
133
+ return { edit, warnings: editWarnings };
134
+ }
137
135
 
136
+ export async function execPipeline(
137
+ targetPath: string,
138
+ params: ReqParams,
139
+ cwd: string,
140
+ options?: ExecPipelineOptions,
141
+ ): Promise<PipelineResult> {
142
+
143
+ const { edit, warnings: editWarnings } = buildReplaceHEdit(params);
138
144
  const hashStore = options?.store ?? await loadHashStore();
139
145
  const preResolvedPath = await resolveTarget(toCwd(targetPath, cwd));
140
146
  const served = servedForPath(preResolvedPath);
@@ -143,8 +149,9 @@ export async function execPipeline(
143
149
  );
144
150
  const displayPath = relative(cwd, absolutePath).replace(/\\/g, "/") || targetPath;
145
151
 
146
- const dedupEnabled = await isBoundaryDedupEnabled();
147
- const effectiveSkipBoundaryDedup = options?.skipBoundaryDedup === true || !dedupEnabled;
152
+ const dedupMode = await getBoundaryDedupMode();
153
+ const effectiveSkipBoundaryDedup = options?.skipBoundaryDedup === true || dedupMode === "off";
154
+ const strictBoundaryDedup = options?.skipBoundaryDedup !== true && dedupMode === "strict";
148
155
  let anchorResult: ReturnType<typeof applyEdit>;
149
156
  try {
150
157
  anchorResult = applyEdit(
@@ -155,6 +162,7 @@ export async function execPipeline(
155
162
  displayPath,
156
163
  served,
157
164
  effectiveSkipBoundaryDedup,
165
+ strictBoundaryDedup,
158
166
  );
159
167
  } catch (error) {
160
168
  await noteAnchorError(absolutePath, error, originalHashes, options?.noPersist);
@@ -275,29 +283,61 @@ export function buildToolDef(flags: EditToolFlags = DEFAULT_EDIT_FLAGS): ToolDef
275
283
  removeTo: normalizedParams.remove_to,
276
284
  providedPath: normalizedParams.path,
277
285
  cwd: ctx.cwd,
286
+ }).catch((error: unknown) => {
287
+ const member = batchMemberFor(_toolCallId);
288
+ if (member) noteBatchFailure(member, error);
289
+ throw error;
278
290
  });
279
291
  return queuedEdit(targetPath, ctx.cwd, signal, async (absolutePath, mutationTargetPath) => {
280
- const dedupOn = await isBoundaryDedupEnabled();
292
+ const dedupMode = await getBoundaryDedupMode();
293
+ const dedupOn = dedupMode !== "off";
281
294
  const noopPayload = noopPayloadKey(mutationTargetPath, normalizedParams.remove_from, normalizedParams.remove_to, normalizedParams.replacement_lines);
282
295
  const boundaryBypass = dedupOn ? consumeBoundaryBypass(mutationTargetPath, noopPayload) : false;
283
- const pipe = await execPipeline(
284
- targetPath,
285
- normalizedParams,
286
- ctx.cwd,
287
- { accessMode: constants.R_OK | constants.W_OK, signal, skipBoundaryDedup: boundaryBypass },
288
- );
296
+ const strictBoundaryDedup = dedupMode === "strict" && !boundaryBypass;
297
+ const member = batchMemberFor(_toolCallId);
298
+ if (!member) {
299
+ const pipe = await execPipeline(
300
+ targetPath,
301
+ normalizedParams,
302
+ ctx.cwd,
303
+ { accessMode: constants.R_OK | constants.W_OK, signal, skipBoundaryDedup: boundaryBypass },
304
+ );
305
+ const appliedWarnings = boundaryBypass
306
+ ? ["[W_BOUNDARY_BYPASS] Boundary dedup was off for this call and is back on."]
307
+ : [];
308
+ return commitEdit(pipe, {
309
+ path: pipe.path,
310
+ absolutePath,
311
+ mutationTargetPath,
312
+ editAnchors: [normalizedParams.remove_from, normalizedParams.remove_to],
313
+ signal,
314
+ appliedWarnings,
315
+ onApplied: () => { if (dedupOn) clearBoundaryBypass(mutationTargetPath); },
316
+ onNoopDedup: dedupOn ? () => markBoundaryNoop(mutationTargetPath, noopPayload) : undefined,
317
+ });
318
+ }
319
+ let built: { edit: HEdit; warnings: string[] };
320
+ try {
321
+ built = buildReplaceHEdit(normalizedParams);
322
+ } catch (error) {
323
+ noteBatchFailure(member, error);
324
+ throw error;
325
+ }
289
326
  const appliedWarnings = boundaryBypass
290
327
  ? ["[W_BOUNDARY_BYPASS] Boundary dedup was off for this call and is back on."]
291
328
  : [];
292
- return commitEdit(pipe, {
293
- path: pipe.path,
294
- absolutePath,
329
+ return executeBatchMember({
330
+ kind: "replace",
331
+ member,
332
+ targetPath,
295
333
  mutationTargetPath,
296
- editAnchors: [normalizedParams.remove_from, normalizedParams.remove_to],
334
+ cwd: ctx.cwd,
297
335
  signal,
298
- appliedWarnings,
299
- onApplied: () => { if (dedupOn) clearBoundaryBypass(mutationTargetPath); },
300
- onNoopDedup: dedupOn ? () => markBoundaryNoop(mutationTargetPath, noopPayload) : undefined,
336
+ hedit: built.edit,
337
+ extraWarnings: [...built.warnings, ...appliedWarnings],
338
+ skipBoundaryDedup: boundaryBypass,
339
+ strictBoundaryDedup,
340
+ noopPayload,
301
341
  });
302
342
  });
303
343
  },
package/src/write-hook.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { HASH_CLASS } from "./hashline/alphabet";
3
3
  import { HASH_SEP } from "./hashline/hash";
4
- import { ownersForPath } from "./anchor-registry";
4
+ import { servedForPath } from "./anchor-registry";
5
5
  import { resolveInCwd } from "./fs-write";
6
6
  import { abortIf, splitLines, isRec, normalizeFilePath } from "./utils";
7
7
 
@@ -27,7 +27,7 @@ export async function servedHashEchoDenial(rawPath: string, content: string, cwd
27
27
  abortIf(signal);
28
28
  const { resolved } = await resolveInCwd(rawPath, cwd);
29
29
  abortIf(signal);
30
- const served = ownersForPath(resolved);
30
+ const served = servedForPath(resolved);
31
31
  if (!served || served.size === 0) return undefined;
32
32
  const echo = findServedHashEcho(content, served);
33
33
  if (!echo) return undefined;