pi-hashline-edit-pro 4.2.1 → 4.2.2

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.
@@ -174,11 +174,13 @@ export function planEdit(
174
174
  skipBoundaryDedup?: boolean;
175
175
  strictBoundaryDedup?: boolean;
176
176
  signal?: AbortSignal;
177
+ baseFileLines?: string[];
177
178
  },
178
179
  ): PlannedEdit {
179
180
  const signal = options?.signal;
180
181
  abortIf(signal);
181
- const lineIndex = buildIdx(content);
182
+ const fileLines = options?.baseFileLines ?? buildIdx(content).fileLines;
183
+ const lineIndex = { fileLines };
182
184
  const fileHashes = precomputedHashes;
183
185
  const warnings: string[] = [];
184
186
 
@@ -540,7 +540,7 @@ export function assertRangeServed(
540
540
  ? `\n\n[The range has ${rangeLength} lines; showing the first ${shownLength}. Call read()${filePath ? ` on ${filePath}` : ""} with offset=${startLine + shownLength} to see the rest.]`
541
541
  : "\n\nRetry with the fresh anchors above without a read.";
542
542
  const message =
543
- `[E_RANGE_STALE] ${mismatchText} what was shown. Nothing was modified. Current range with fresh anchors:\n\n${rows.join("\n")}${capHint}`;
543
+ `[E_RANGE_STALE] ${mismatchText} what was shown. Current range with fresh anchors:\n\n${rows.join("\n")}${capHint}`;
544
544
  throw new RangeStaleError(message, shownHashes, shownMap);
545
545
  }
546
546
 
package/src/insert.ts CHANGED
@@ -3,7 +3,7 @@ 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
+ import { batchMemberFor, ensureBatchBase, executeBatchMember, noteBatchFailure, suffixPoisonCause } from "./batch";
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";
@@ -193,6 +193,7 @@ export function buildInsertToolDef(flags: EditToolFlags = DEFAULT_EDIT_FLAGS): I
193
193
  }).catch((error: unknown) => {
194
194
  const member = batchMemberFor(_toolCallId);
195
195
  if (member) noteBatchFailure(member, error);
196
+ else suffixPoisonCause(_toolCallId, error);
196
197
  throw error;
197
198
  });
198
199
  let ref: Anchor;
@@ -207,51 +208,54 @@ export function buildInsertToolDef(flags: EditToolFlags = DEFAULT_EDIT_FLAGS): I
207
208
  }
208
209
  return queuedEdit(targetPath, ctx.cwd, signal, async (absolutePath, mutationTargetPath) => {
209
210
  const member = batchMemberFor(_toolCallId);
211
+ if (member) {
212
+ const base = await ensureBatchBase({ member, targetPath, mutationTargetPath, cwd: ctx.cwd, signal });
213
+ const basePreload = { normalized: base.content, fileHashes: base.hashes } as NormFile;
214
+ const built = buildInsertEdit(req, basePreload, ref, targetPath);
215
+ let hedit: HEdit;
216
+ const resWarnings: string[] = [];
217
+ try {
218
+ hedit = resEdit(built.editParams, resWarnings);
219
+ } catch (error) {
220
+ noteBatchFailure(member, error);
221
+ throw error;
222
+ }
223
+ return executeBatchMember({
224
+ kind: "insert",
225
+ member,
226
+ targetPath,
227
+ mutationTargetPath,
228
+ cwd: ctx.cwd,
229
+ signal,
230
+ hedit,
231
+ extraWarnings: [...anchorWarnings, ...insertWarnings, ...resWarnings],
232
+ skipBoundaryDedup: true,
233
+ strictBoundaryDedup: false,
234
+ foldedLines: built.anchorLine === undefined ? 0 : 1,
235
+ });
236
+ }
210
237
  const preload = await readNormFile(targetPath, ctx.cwd, {
211
238
  signal,
212
239
  accessMode: constants.R_OK | constants.W_OK,
213
240
  maxLines: MAX_HASH_LINES,
214
241
  });
215
242
  const { editParams, anchorLine } = buildInsertEdit(req, preload, ref, targetPath);
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,
247
- mutationTargetPath,
248
- cwd: ctx.cwd,
243
+ const pipe = await execPipeline(targetPath, editParams, ctx.cwd, {
244
+ accessMode: constants.R_OK | constants.W_OK,
249
245
  signal,
250
- hedit,
251
- extraWarnings: [...anchorWarnings, ...insertWarnings, ...resWarnings],
246
+ preloadedNorm: preload,
252
247
  skipBoundaryDedup: true,
253
- strictBoundaryDedup: false,
254
- foldedLines: anchorLine === undefined ? 0 : 1,
248
+ });
249
+ return commitEdit(pipe, {
250
+ path: pipe.path,
251
+ absolutePath,
252
+ mutationTargetPath,
253
+ signal,
254
+ verb: "inserted",
255
+ noopNoun: "Insertion",
256
+ foldedAnchorLines: anchorLine === undefined ? 0 : 1,
257
+ prefixWarnings: [...anchorWarnings, ...insertWarnings],
258
+ onApplied: () => clearBoundaryBypass(mutationTargetPath),
255
259
  });
256
260
  });
257
261
  },
package/src/replace.ts CHANGED
@@ -34,7 +34,7 @@ import { toCwd } from "./paths";
34
34
  import { noopPayloadKey, markBoundaryNoop, consumeBoundaryBypass, clearBoundaryBypass } from "./boundary-bypass";
35
35
  import { queuedEdit, editToolBase, editRenderCallWrapper, editRenderResultWrapper, resolveEditTargetWithRequirement, throwIfStrictInput, getBoundaryDedupMode, withReplacePrompts, DEFAULT_EDIT_FLAGS, type EditToolFlags } from "./edit-common";
36
36
  import { commitEdit } from "./commit";
37
- import { batchMemberFor, executeBatchMember, noteBatchFailure } from "./batch";
37
+ import { batchMemberFor, executeBatchMember, noteBatchFailure, suffixPoisonCause } from "./batch";
38
38
 
39
39
  export { editToolSchema, type ReqParams, assertReq };
40
40
 
@@ -286,6 +286,7 @@ export function buildToolDef(flags: EditToolFlags = DEFAULT_EDIT_FLAGS): ToolDef
286
286
  }).catch((error: unknown) => {
287
287
  const member = batchMemberFor(_toolCallId);
288
288
  if (member) noteBatchFailure(member, error);
289
+ else suffixPoisonCause(_toolCallId, error);
289
290
  throw error;
290
291
  });
291
292
  return queuedEdit(targetPath, ctx.cwd, signal, async (absolutePath, mutationTargetPath) => {
@@ -296,31 +297,38 @@ export function buildToolDef(flags: EditToolFlags = DEFAULT_EDIT_FLAGS): ToolDef
296
297
  const strictBoundaryDedup = dedupMode === "strict" && !boundaryBypass;
297
298
  const member = batchMemberFor(_toolCallId);
298
299
  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
- });
300
+ try {
301
+ const pipe = await execPipeline(
302
+ targetPath,
303
+ normalizedParams,
304
+ ctx.cwd,
305
+ { accessMode: constants.R_OK | constants.W_OK, signal, skipBoundaryDedup: boundaryBypass },
306
+ );
307
+ const appliedWarnings = boundaryBypass
308
+ ? ["[W_BOUNDARY_BYPASS] Boundary dedup was off for this call and is back on."]
309
+ : [];
310
+ return await commitEdit(pipe, {
311
+ path: pipe.path,
312
+ absolutePath,
313
+ mutationTargetPath,
314
+ editAnchors: [normalizedParams.remove_from, normalizedParams.remove_to],
315
+ signal,
316
+ appliedWarnings,
317
+ onApplied: () => { if (dedupOn) clearBoundaryBypass(mutationTargetPath); },
318
+ onNoopDedup: dedupOn ? () => markBoundaryNoop(mutationTargetPath, noopPayload) : undefined,
319
+ });
320
+ } catch (error) {
321
+ const detail = error instanceof Error ? error.message : String(error);
322
+ if (boundaryBypass && !detail.includes("File was written;")) markBoundaryNoop(mutationTargetPath, noopPayload);
323
+ throw error;
324
+ }
318
325
  }
319
326
  let built: { edit: HEdit; warnings: string[] };
320
327
  try {
321
328
  built = buildReplaceHEdit(normalizedParams);
322
329
  } catch (error) {
323
330
  noteBatchFailure(member, error);
331
+ if (boundaryBypass) markBoundaryNoop(mutationTargetPath, noopPayload);
324
332
  throw error;
325
333
  }
326
334
  const appliedWarnings = boundaryBypass
@@ -338,6 +346,7 @@ export function buildToolDef(flags: EditToolFlags = DEFAULT_EDIT_FLAGS): ToolDef
338
346
  skipBoundaryDedup: boundaryBypass,
339
347
  strictBoundaryDedup,
340
348
  noopPayload,
349
+ bypassConsumed: boundaryBypass,
341
350
  });
342
351
  });
343
352
  },
package/src/validation.ts CHANGED
@@ -32,10 +32,10 @@ export function valKind(file: LFile, path: string): asserts file is { kind: "tex
32
32
  throw new Error(`[E_NOT_TEXT] Path is a directory: ${path}. Use ls to inspect directories.`);
33
33
  }
34
34
  if (file.kind === "binary") {
35
- throw new Error(`[E_NOT_TEXT] Path is a binary file: ${path} (${file.description}). Hashline edit only supports text files.`);
35
+ throw new Error(`[E_NOT_TEXT] Path is a binary file: ${path} (${file.description}).`);
36
36
  }
37
37
  if (file.kind === "image") {
38
- throw new Error(`[E_NOT_TEXT] Path is an image file: ${path}. Hashline edit only supports text files.`);
38
+ throw new Error(`[E_NOT_TEXT] Path is an image file: ${path}.`);
39
39
  }
40
40
  if (file.kind === "too_large") {
41
41
  throw new Error(
package/src/write-hook.ts CHANGED
@@ -31,7 +31,7 @@ export async function servedHashEchoDenial(rawPath: string, content: string, cwd
31
31
  if (!served || served.size === 0) return undefined;
32
32
  const echo = findServedHashEcho(content, served);
33
33
  if (!echo) return undefined;
34
- return `[E_WRITE_HASH_ECHO] Refused write to ${rawPath}: line ${echo.line} begins with the exact ${echo.hash}${HASH_SEP} anchor served for this file. Remove the copied anchors and retry. Nothing was written.`;
34
+ return `[E_WRITE_HASH_ECHO] Refused write to ${rawPath}: line ${echo.line} begins with the exact ${echo.hash}${HASH_SEP} anchor served for this file. Remove the copied anchors and retry.`;
35
35
  }
36
36
 
37
37
  export function registerWriteHook(pi: ExtensionAPI): void {