pi-hashline-edit-pro 0.4.3 → 0.6.0

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.
@@ -14,8 +14,8 @@ import {
14
14
  normalizeToLF,
15
15
  restoreLineEndings,
16
16
  stripBom,
17
- } from "./edit-diff";
18
- import { normalizeEditRequest } from "./edit-normalize";
17
+ } from "./replace-diff";
18
+ import { normalizeReplaceRequest } from "./replace-normalize";
19
19
  import { isRecord, hasOwn } from "./utils";
20
20
  import { resolveMutationTargetPath, writeFileAtomically } from "./fs-write";
21
21
  import {
@@ -33,9 +33,9 @@ import {
33
33
  buildFullResponse,
34
34
  buildNoopResponse,
35
35
  buildRangesResponse,
36
- type EditMeta,
36
+ type ReplaceMeta,
37
37
  type ReturnMode,
38
- } from "./edit-response";
38
+ } from "./replace-response";
39
39
  import {
40
40
  buildAppliedChangedResultText,
41
41
  createRenderedEditMarkdownTheme,
@@ -44,9 +44,9 @@ import {
44
44
  getRenderablePreviewInput,
45
45
  getRenderedEditTextContent,
46
46
  isAppliedChangedResult,
47
- type EditPreview,
48
- type EditRenderState,
49
- } from "./edit-render";
47
+ type ReplacePreview,
48
+ type ReplaceRenderState,
49
+ } from "./replace-render";
50
50
 
51
51
  function stringEnumSchema<const Values extends readonly string[]>(
52
52
  values: Values,
@@ -82,29 +82,16 @@ const returnRangeSchema = Type.Object(
82
82
 
83
83
  const hashlineEditItemSchema = Type.Object(
84
84
  {
85
- op: stringEnumSchema(
86
- ["replace", "append", "prepend"] as const,
87
- {
88
- description:
89
- 'edit operation. "replace" requires "start" + "end" (inclusive range); "append"/"prepend" take an optional "pos" anchor. Every edit must set op. The legacy "replace_text" op is no longer supported; use a hash-anchored "replace" instead.',
90
- },
91
- ),
92
85
  start: Type.Optional(
93
86
  Type.String({
94
87
  description:
95
- "required range-start anchor for op \"replace\" (hash anchor like \"aB3x\" copied from read output); no content may follow the anchor",
88
+ "required range-start anchor (hash anchor like \"aB3x\" copied from read output); no content may follow the anchor",
96
89
  }),
97
90
  ),
98
91
  end: Type.Optional(
99
92
  Type.String({
100
93
  description:
101
- "required range-end anchor for op \"replace\" (hash anchor like \"aB3x\"). To replace a single line, set start = end = the line's anchor",
102
- }),
103
- ),
104
- pos: Type.Optional(
105
- Type.String({
106
- description:
107
- "anchor for op \"append\" or \"prepend\" (hash anchor like \"aB3x\"). Omit for file-boundary insertion (EOF/BOF).",
94
+ "required range-end anchor (hash anchor like \"aB3x\"). To replace a single line, set start = end = the line's anchor",
108
95
  }),
109
96
  ),
110
97
  lines: Type.Optional(hashlineEditLinesSchema),
@@ -149,14 +136,14 @@ type FullContentPreview = {
149
136
  nextOffset?: number;
150
137
  };
151
138
 
152
- export type EditRequestParams = {
139
+ export type ReplaceRequestParams = {
153
140
  path: string;
154
141
  returnMode?: "changed" | "full" | "ranges";
155
142
  returnRanges?: ReturnRange[];
156
143
  edits?: HashlineToolEdit[];
157
144
  };
158
145
 
159
- type EditMetrics = {
146
+ type ReplaceMetrics = {
160
147
  edits_attempted: number;
161
148
  edits_noop: number;
162
149
  warnings: number;
@@ -167,7 +154,7 @@ type EditMetrics = {
167
154
  removed_lines?: number;
168
155
  };
169
156
 
170
- export type HashlineEditToolDetails = {
157
+ export type HashlineReplaceToolDetails = {
171
158
  diff: string;
172
159
  firstChangedLine?: number;
173
160
  /**
@@ -185,22 +172,22 @@ export type HashlineEditToolDetails = {
185
172
  * Phase 2 C — opt-in observability surface for hosts. Never echoed in text.
186
173
  * Hosts can use it for adoption/regression dashboards.
187
174
  */
188
- metrics?: EditMetrics;
175
+ metrics?: ReplaceMetrics;
189
176
  };
190
177
 
191
178
  const EDIT_DESC = readFileSync(
192
- new URL("../prompts/edit.md", import.meta.url),
179
+ new URL("../prompts/replace.md", import.meta.url),
193
180
  "utf-8",
194
181
  ).trim();
195
182
 
196
183
  const EDIT_PROMPT_SNIPPET = readFileSync(
197
- new URL("../prompts/edit-snippet.md", import.meta.url),
184
+ new URL("../prompts/replace-snippet.md", import.meta.url),
198
185
  "utf-8",
199
186
  ).trim();
200
187
 
201
188
 
202
189
  const EDIT_PROMPT_GUIDELINES = readFileSync(
203
- new URL("../prompts/edit-guidelines.md", import.meta.url),
190
+ new URL("../prompts/replace-guidelines.md", import.meta.url),
204
191
  "utf-8",
205
192
  )
206
193
  .split("\n")
@@ -209,9 +196,9 @@ const EDIT_PROMPT_GUIDELINES = readFileSync(
209
196
  .map((line) => line.slice(2));
210
197
  const ROOT_KEYS = new Set(["path", "returnMode", "returnRanges", "edits"]);
211
198
 
212
- export function assertEditRequest(
199
+ export function assertReplaceRequest(
213
200
  request: unknown,
214
- ): asserts request is EditRequestParams {
201
+ ): asserts request is ReplaceRequestParams {
215
202
  if (!isRecord(request)) {
216
203
  throw new Error("[E_BAD_SHAPE] Edit request must be an object.");
217
204
  }
@@ -219,7 +206,7 @@ export function assertEditRequest(
219
206
  for (const legacyKey of ["oldText", "newText", "old_text", "new_text"]) {
220
207
  if (hasOwn(request, legacyKey)) {
221
208
  throw new Error(
222
- `[E_LEGACY_SHAPE] "${legacyKey}" is not supported. Use {op:"replace", start:"<HASH>", end:"<HASH", lines:[...]}.`
209
+ `[E_LEGACY_SHAPE] "${legacyKey}" is not supported. Use {start:"<HASH>", end:"<HASH>", lines:[...]}.`
223
210
  );
224
211
  }
225
212
  }
@@ -320,8 +307,8 @@ async function executeEditPipeline(
320
307
  lastChangedLine?: number;
321
308
  originalHashes?: string[];
322
309
  }> {
323
- const normalized = normalizeEditRequest(request);
324
- assertEditRequest(normalized);
310
+ const normalized = normalizeReplaceRequest(request);
311
+ assertReplaceRequest(normalized);
325
312
 
326
313
  const params = normalized;
327
314
  const path = params.path;
@@ -400,10 +387,10 @@ async function executeEditPipeline(
400
387
  };
401
388
  }
402
389
 
403
- export async function computeEditPreview(
390
+ export async function computeReplacePreview(
404
391
  request: unknown,
405
392
  cwd: string,
406
- ): Promise<EditPreview> {
393
+ ): Promise<ReplacePreview> {
407
394
  try {
408
395
  const { path, originalNormalized, result } = await executeEditPipeline(
409
396
  request,
@@ -425,19 +412,19 @@ export async function computeEditPreview(
425
412
 
426
413
  type EditToolDefinition = ToolDefinition<
427
414
  typeof hashlineEditToolSchema,
428
- HashlineEditToolDetails,
429
- EditRenderState
415
+ HashlineReplaceToolDetails,
416
+ ReplaceRenderState
430
417
  > & { renderShell?: "default" | "self" };
431
418
 
432
419
  const editToolDefinition: EditToolDefinition = {
433
- name: "edit",
434
- label: "Edit",
420
+ name: "replace",
421
+ label: "Replace",
435
422
  description: EDIT_DESC,
436
423
  parameters: hashlineEditToolSchema,
437
424
  promptSnippet: EDIT_PROMPT_SNIPPET,
438
425
  promptGuidelines: EDIT_PROMPT_GUIDELINES,
439
426
  prepareArguments: (args: unknown) =>
440
- normalizeEditRequest(args) as EditRequestParams,
427
+ normalizeReplaceRequest(args) as ReplaceRequestParams,
441
428
  renderShell: "default",
442
429
  renderCall(args, theme, context) {
443
430
  const previewInput = getRenderablePreviewInput(args);
@@ -458,7 +445,7 @@ const editToolDefinition: EditToolDefinition = {
458
445
  context.state.preview = undefined;
459
446
  const previewGeneration = (context.state.previewGeneration ?? 0) + 1;
460
447
  context.state.previewGeneration = previewGeneration;
461
- computeEditPreview(previewInput, context.cwd)
448
+ computeReplacePreview(previewInput, context.cwd)
462
449
  .then((preview) => {
463
450
  if (
464
451
  context.state.argsKey === argsKey &&
@@ -486,7 +473,7 @@ const editToolDefinition: EditToolDefinition = {
486
473
  text.setText(
487
474
  formatEditCall(
488
475
  getRenderablePreviewInput(args) ?? undefined,
489
- context.state as EditRenderState,
476
+ context.state as ReplaceRenderState,
490
477
  context.expanded,
491
478
  theme,
492
479
  ),
@@ -504,11 +491,11 @@ const editToolDefinition: EditToolDefinition = {
504
491
 
505
492
  const typedResult = result as {
506
493
  content?: Array<{ type: string; text?: string }>;
507
- details?: HashlineEditToolDetails;
494
+ details?: HashlineReplaceToolDetails;
508
495
  };
509
496
  const renderedText = getRenderedEditTextContent(typedResult);
510
497
 
511
- const renderState = context.state as EditRenderState | undefined;
498
+ const renderState = context.state as ReplaceRenderState | undefined;
512
499
  const previewBeforeResult = renderState?.preview;
513
500
  if (renderState) {
514
501
  renderState.preview = undefined;
@@ -558,8 +545,8 @@ const editToolDefinition: EditToolDefinition = {
558
545
  },
559
546
 
560
547
  async execute(_toolCallId, params, signal, _onUpdate, ctx) {
561
- const normalized = normalizeEditRequest(params);
562
- assertEditRequest(normalized);
548
+ const normalized = normalizeReplaceRequest(params);
549
+ assertReplaceRequest(normalized);
563
550
  const normalizedParams = normalized;
564
551
  const path = normalizedParams.path;
565
552
  const absolutePath = resolveToCwd(path, ctx.cwd);
@@ -622,7 +609,7 @@ const editToolDefinition: EditToolDefinition = {
622
609
  const updatedSnapshotId = (await getFileSnapshot(absolutePath))
623
610
  .snapshotId;
624
611
 
625
- const editMeta: EditMeta = {
612
+ const editMeta: ReplaceMeta = {
626
613
  editsAttempted,
627
614
  noopEditsCount: noopEdits?.length ?? 0,
628
615
  firstChangedLine,
@@ -648,6 +635,6 @@ const editToolDefinition: EditToolDefinition = {
648
635
  },
649
636
  };
650
637
 
651
- export function registerEditTool(pi: ExtensionAPI): void {
638
+ export function registerReplaceTool(pi: ExtensionAPI): void {
652
639
  pi.registerTool(editToolDefinition);
653
640
  }
@@ -1,3 +0,0 @@
1
- - Use edit with HASH anchors for all file changes; batch every change to one file into a single edit call.
2
- - After a successful edit, the returned `--- Anchors ---` block replaces a re-read for nearby follow-up edits.
3
- - On `[E_STALE_ANCHOR]`, retry with the `>>>` lines quoted in the error instead of re-reading the whole file.
@@ -1 +0,0 @@
1
- Edit a text file via HASH anchors from read, batching all edits to a file in one call
package/prompts/edit.md DELETED
@@ -1,58 +0,0 @@
1
- Patch a text file using `HASH` anchors copied verbatim from `read`.
2
-
3
- Put all operations on one file in a single `edit` call. Stack every region into the `edits` 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, so you do not adjust anchors for line-number shifts between edits in the same call.
4
-
5
- Anchors are 4 characters (e.g. `aB3x`), alphabet `A-Za-z0-9-_`. The wire format for `start`/`end`/`pos` is the anchor only — no line number, no trailing content, no line content.
6
-
7
- Ops:
8
- - `replace` — replace the inclusive range `start`..`end`. Both anchors are required. Single line: `start = end`. To delete a range, use `lines: []`. Do NOT use the `pos` field on `replace`; use `start`.
9
- - `append` — insert `lines` after `pos`; omit `pos` to append at EOF.
10
- - `prepend` — insert `lines` before `pos`; omit `pos` to prepend at BOF. Use `prepend` at an anchor to insert a new block between line N-1 and N (anchor on the line *after* the insertion point).
11
-
12
- Examples:
13
-
14
- 1. Single line replace:
15
- ```json
16
- { "path": "src/main.ts", "edits": [
17
- { "op": "replace", "start": "MQXV", "end": "MQXV", "lines": ["const x = 1;"] }
18
- ] }
19
- ```
20
-
21
- 2. Range replace (3 lines → 3 new lines):
22
- ```json
23
- { "path": "src/main.ts", "edits": [
24
- { "op": "replace", "start": "ZPMQ", "end": "VRWS", "lines": [
25
- "function greet(name) {",
26
- " return `Hello, ${name}`;",
27
- "}"
28
- }
29
- ] }
30
- ```
31
-
32
- 3. Multiple regions in one call (delete two non-adjacent ranges, insert before a third anchor):
33
- ```json
34
- { "path": "src/server.ts", "edits": [
35
- { "op": "replace", "start": "aB3x", "end": "xY7q", "lines": [] },
36
- { "op": "replace", "start": "MQXV", "end": "ZPMQ", "lines": [] },
37
- { "op": "prepend", "pos": "VRWS", "lines": ["// inserted before VRWS"] }
38
- ] }
39
- ```
40
-
41
- Rules:
42
- - `replace` requires both `start` and `end`. A single-line replace is `start=X, end=X`. To replace more than one line, set `end` to a different line's anchor.
43
- - `start`, `end`, `pos` are HASH anchors only (e.g. `aB3x`). Other forms are rejected with `[E_BAD_REF]`.
44
- - `lines` is literal file content. No `HASH│` prefix, no leading `+`/`-` (those are read/diff metadata, not file content). Lines starting with 4 base64 chars + `│` are checked; if detected, the edit is rejected with `[E_BARE_HASH_PREFIX]`. For `.py` files, this becomes a `[W_BARE_HASH_PREFIX]` warning instead (Python syntax like `else:`, `except:` triggers the detector).
45
- - Copy anchors from the most recent `read` of the file. Do not guess or construct them.
46
- - All edits in one call must be non-conflicting. The runtime rejects with `[E_EDIT_CONFLICT]` if: two `replace` ranges overlap; two `append`/`prepend` target the same insertion boundary (e.g. two EOF appends on a newline-terminated file); or an `append`/`prepend` falls inside a `replace` range in the same call. Fix: merge into one, use different boundaries, or split into a follow-up `edit` call.
47
- - If `lines` matches the current content byte-for-byte, the edit is classified as `Classification: noop` (file unchanged, not an error).
48
-
49
- On success (`changed` mode, default), the response text contains an `--- Anchors ---` block with fresh `HASH│content` for the changed region (2 lines of context, capped at ~12 lines / 50 KB). Use those for nearby follow-up edits instead of re-reading. If the response says `Anchors omitted; use read for subsequent edits`, the region was too large — call `read` again. For distant follow-ups, or on any error, call `read` again. `full` and `ranges` modes put previews in `details`; the model only needs what's in the text.
50
-
51
- Errors are text starting with a bracketed code (e.g. `[E_BAD_SHAPE]`, `[E_STALE_ANCHOR]`, `[E_BAD_OP]`, `[E_INVALID_PATCH]`, `[E_LEGACY_SHAPE]`, `[E_EDIT_CONFLICT]`, `[E_BAD_REF]`, `[E_AMBIGUOUS_ANCHOR]`, `[E_BARE_HASH_PREFIX]`, `[E_WOULD_EMPTY]`). The message tells you what to retry; stale-anchor errors include `>>> HASH│content` lines, ready to copy.
52
-
53
- The legacy `oldText`/`newText` shape (top-level or as `op: "replace_text"`) is rejected with `[E_LEGACY_SHAPE]`. Use hash-anchored edits instead.
54
-
55
- Auto-read after write:
56
- - After a successful `write`, the result includes a `--- Auto-read (hashline anchors) ---` block with `HASH│content` for the written file.
57
- - Use those anchors directly for `edit` calls without a separate `read`.
58
- - This enables a seamless write → edit workflow with no extra tool calls.
File without changes