pi-hashline-edit-pro 6.1.2 → 6.2.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.
Files changed (66) hide show
  1. package/README.md +14 -12
  2. package/index.ts +20 -12
  3. package/package.json +3 -2
  4. package/src/auto-read-all.ts +39 -21
  5. package/src/config-ui.ts +135 -55
  6. package/src/config.ts +27 -9
  7. package/src/copy-move.ts +4 -4
  8. package/src/grep.ts +3 -3
  9. package/src/insert.ts +3 -3
  10. package/src/outline/index.ts +45 -0
  11. package/src/outline/parsers.ts +42 -0
  12. package/src/outline/render.ts +166 -0
  13. package/src/outline/vendor/LICENSE +201 -0
  14. package/src/outline/vendor/README.md +17 -0
  15. package/src/outline/vendor/parsers/blade.ts +138 -0
  16. package/src/outline/vendor/parsers/index.ts +75 -0
  17. package/src/outline/vendor/parsers/java.ts +501 -0
  18. package/src/outline/vendor/parsers/manager.ts +1721 -0
  19. package/src/outline/vendor/parsers/php.ts +332 -0
  20. package/src/outline/vendor/parsers/sass.ts +416 -0
  21. package/src/outline/vendor/parsers/scss.ts +566 -0
  22. package/src/outline/vendor/types.ts +82 -0
  23. package/src/outline/vendor/wasm/LICENSES.md +37 -0
  24. package/src/outline/vendor/wasm/tree-sitter-blade.wasm +0 -0
  25. package/src/outline/vendor/wasm/tree-sitter-go.wasm +0 -0
  26. package/src/outline/vendor/wasm/tree-sitter-java.wasm +0 -0
  27. package/src/outline/vendor/wasm/tree-sitter-javascript.wasm +0 -0
  28. package/src/outline/vendor/wasm/tree-sitter-php.wasm +0 -0
  29. package/src/outline/vendor/wasm/tree-sitter-python.wasm +0 -0
  30. package/src/outline/vendor/wasm/tree-sitter-rust.wasm +0 -0
  31. package/src/outline/vendor/wasm/tree-sitter-sass.wasm +0 -0
  32. package/src/outline/vendor/wasm/tree-sitter-scss.wasm +0 -0
  33. package/src/outline/vendor/wasm/tree-sitter-solidity.wasm +0 -0
  34. package/src/outline/vendor/wasm/tree-sitter-tsx.wasm +0 -0
  35. package/src/outline/vendor/wasm/tree-sitter-typescript.wasm +0 -0
  36. package/src/read.ts +108 -36
  37. package/src/replace-match.ts +3 -3
  38. package/src/replace-undo.ts +3 -3
  39. package/src/replace.ts +3 -3
  40. package/tool-prompts/read-guidelines.md +3 -0
  41. package/{prompts → tool-prompts}/read.md +2 -0
  42. package/prompts/read-guidelines.md +0 -1
  43. package/src/auto-read-all-state.ts +0 -27
  44. /package/{prompts → tool-prompts}/copy-guidelines.md +0 -0
  45. /package/{prompts → tool-prompts}/copy-snippet.md +0 -0
  46. /package/{prompts → tool-prompts}/copy.md +0 -0
  47. /package/{prompts → tool-prompts}/grep-guidelines.md +0 -0
  48. /package/{prompts → tool-prompts}/grep-snippet.md +0 -0
  49. /package/{prompts → tool-prompts}/grep.md +0 -0
  50. /package/{prompts → tool-prompts}/insert-guidelines.md +0 -0
  51. /package/{prompts → tool-prompts}/insert-snippet.md +0 -0
  52. /package/{prompts → tool-prompts}/insert.md +0 -0
  53. /package/{prompts → tool-prompts}/move-guidelines.md +0 -0
  54. /package/{prompts → tool-prompts}/move-snippet.md +0 -0
  55. /package/{prompts → tool-prompts}/move.md +0 -0
  56. /package/{prompts → tool-prompts}/read-snippet.md +0 -0
  57. /package/{prompts → tool-prompts}/replace-guidelines.md +0 -0
  58. /package/{prompts → tool-prompts}/replace-match-guidelines.md +0 -0
  59. /package/{prompts → tool-prompts}/replace-match-snippet.md +0 -0
  60. /package/{prompts → tool-prompts}/replace-match.md +0 -0
  61. /package/{prompts → tool-prompts}/replace-snippet.md +0 -0
  62. /package/{prompts → tool-prompts}/replace.md +0 -0
  63. /package/{prompts → tool-prompts}/transfer-guidelines.md +0 -0
  64. /package/{prompts → tool-prompts}/undo-last-change-guidelines.md +0 -0
  65. /package/{prompts → tool-prompts}/undo-last-change-snippet.md +0 -0
  66. /package/{prompts → tool-prompts}/undo-last-change.md +0 -0
package/src/read.ts CHANGED
@@ -1,6 +1,8 @@
1
+ import { realpathSync } from "node:fs";
1
2
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
3
  import {
3
4
  createReadTool,
5
+ createReadToolDefinition,
4
6
  formatSize,
5
7
  truncateHead,
6
8
  DEFAULT_MAX_BYTES,
@@ -11,23 +13,21 @@ import { Type } from "typebox";
11
13
  import { loadFileKindAndText } from "./file-kind";
12
14
  import { MAX_OVERSIZED_WARNING_LINES } from "./constants";
13
15
  import { readNormFile, safeSnapId } from "./file-reader";
14
- import { lineHashes, fmtRegion, fmtRow, HASH_SEP, MAX_HASH_LINES } from "./hashline";
16
+ import { lineHashes, fmtRegion, fmtRow, HASH_SEP, MAX_HASH_LINES, parseHashRef, resolveAnchorLine, AnchorMismatchError } from "./hashline";
15
17
  import { toCwd } from "./paths";
16
- import { abortIf, makePrepareArguments, numberedRead, visLines, splitLines } from "./utils";
18
+ import { abortIf, isRec, makePrepareArguments, numberedRead, visLines, splitLines } from "./utils";
17
19
  import { loadP, loadGuide } from "./prompts";
18
20
  import { withReadPrompts, DEFAULT_EDIT_FLAGS, type EditToolFlags } from "./edit-common";
19
21
  import { valAccess } from "./validation";
20
- import { readConfig } from "./config";
21
- import { resolveTarget } from "./fs-write";
22
- import { withAnchorSession, servedForPath, sessionKeyFor, formatAnchorReclaimNotice, takeReclaimedPaths } from "./anchor-registry";
22
+ import { withAnchorSession, adoptAnchors, ownerOf, ownersDifferingOnlyByCase, formatAnchorReclaimNotice, takeReclaimedPaths } from "./anchor-registry";
23
23
  import { serveRows } from "./served";
24
- import { getAutoReadAllSnapshot } from "./auto-read-all-state";
24
+ import { snapshotCache } from "./hash-store/cache";
25
25
  import { Text } from "@earendil-works/pi-tui";
26
26
  import { anchoredLine, readResultSchema, withStructuredErrors, type AnchoredLine, type ReadResult } from "./structured";
27
- const R_DESC = loadP("../prompts/read.md");
28
- const R_SNIPPET = loadP("../prompts/read-snippet.md");
27
+ const R_DESC = loadP("../tool-prompts/read.md");
28
+ const R_SNIPPET = loadP("../tool-prompts/read-snippet.md");
29
29
  function readGuide(): string[] {
30
- return loadGuide("../prompts/read-guidelines.md");
30
+ return loadGuide("../tool-prompts/read-guidelines.md");
31
31
  }
32
32
  function normPosInt(
33
33
  value: number | undefined,
@@ -196,6 +196,90 @@ export async function fmtReadPreview(
196
196
  };
197
197
  }
198
198
 
199
+ export function resolveReadOffset(
200
+ offset: number | string | undefined,
201
+ fileLines: string[],
202
+ fileHashes: string[],
203
+ resolvedPath: string,
204
+ ): number | undefined {
205
+ if (offset === undefined || typeof offset === "number") return offset;
206
+ if (/^\d+$/.test(offset)) return Number(offset);
207
+ const ref = parseHashRef(offset);
208
+ const owner = ownerOf(ref.hash);
209
+ if (owner === undefined) {
210
+ const folded = ownersDifferingOnlyByCase(ref.hash);
211
+ const hint = folded.length > 0 ? ` Anchors are case-sensitive; ${folded.map((match) => `"${match.anchor}"`).join(", ")} differs only in case.` : "";
212
+ throw new Error(`[E_STALE_ANCHOR] "${ref.hash}" is not owned in this session.${hint} Call read() on ${resolvedPath} first.`);
213
+ }
214
+ if (owner.path !== resolvedPath) {
215
+ throw new Error(`[E_STALE_ANCHOR] "${ref.hash}" is owned by ${owner.path}. Call read() on ${resolvedPath} for fresh anchors.`);
216
+ }
217
+ try {
218
+ return resolveAnchorLine(ref, fileLines, fileHashes, resolvedPath);
219
+ } catch (error) {
220
+ if (error instanceof AnchorMismatchError) adoptAnchors(resolvedPath, error.feedbackMap);
221
+ throw error;
222
+ }
223
+ }
224
+
225
+ const builtinReadRenderCall = createReadToolDefinition(process.cwd()).renderCall as
226
+ (args: unknown, theme: any, context: any) => Text;
227
+
228
+ function normalizeReadCallArgs(args: unknown): unknown {
229
+ if (!isRec(args)) return args;
230
+ const normalized: Record<string, unknown> = { ...args };
231
+ for (const key of ["offset", "limit"]) {
232
+ const value = normalized[key];
233
+ if (typeof value === "string" && /^\d+$/.test(value)) normalized[key] = Number(value);
234
+ }
235
+ return normalized;
236
+ }
237
+
238
+ function anchorRefOf(args: unknown): { anchor: string; limit: number | undefined; path: string | undefined } | undefined {
239
+ if (!isRec(args)) return undefined;
240
+ const { offset, limit, path } = args;
241
+ if (typeof offset !== "string" || offset.length === 0 || /^\d+$/.test(offset)) return undefined;
242
+ const validLimit = typeof limit === "number" && Number.isInteger(limit) && limit >= 1 ? limit : undefined;
243
+ if (limit !== undefined && validLimit === undefined) return undefined;
244
+ return { anchor: offset, limit: validLimit, path: typeof path === "string" ? path : undefined };
245
+ }
246
+
247
+ function cachedHashes(path: string): string[] | undefined {
248
+ const cached = snapshotCache.get(path);
249
+ if (cached !== undefined) return cached.hashes;
250
+ try {
251
+ return snapshotCache.get(realpathSync(path))?.hashes;
252
+ } catch {
253
+ return undefined;
254
+ }
255
+ }
256
+
257
+ function anchorLineOf(anchor: string, path: string | undefined, cwd: unknown): number | undefined {
258
+ if (path === undefined || typeof cwd !== "string") return undefined;
259
+ const hashes = cachedHashes(toCwd(path, cwd));
260
+ if (hashes === undefined) return undefined;
261
+ const index = hashes.indexOf(anchor);
262
+ return index < 0 ? undefined : index + 1;
263
+ }
264
+
265
+ function renderReadCall(args: unknown, theme: any, context: any): Text {
266
+ const normalized = normalizeReadCallArgs(args);
267
+ const rendered = builtinReadRenderCall(normalized, theme, context);
268
+ const ref = anchorRefOf(normalized);
269
+ if (ref === undefined) return rendered;
270
+ const marker = `:${ref.anchor}`;
271
+ const text = (rendered as unknown as { text: string }).text;
272
+ const index = text.lastIndexOf(marker);
273
+ if (index < 0) return rendered;
274
+ const line = anchorLineOf(ref.anchor, ref.path, context?.cwd);
275
+ if (line === undefined && ref.limit === undefined) return rendered;
276
+ const lineLabel = line === undefined ? "" : ` (${line})`;
277
+ const limitLabel = ref.limit === undefined ? "" : ` +${ref.limit}`;
278
+ const suffix = theme.fg("warning", `${lineLabel}${limitLabel}`);
279
+ rendered.setText(`${text.slice(0, index + marker.length)}${suffix}${text.slice(index + marker.length)}`);
280
+ return rendered;
281
+ }
282
+
199
283
  export function regRead(pi: ExtensionAPI, flags: EditToolFlags = DEFAULT_EDIT_FLAGS): void {
200
284
  const prompted = withReadPrompts({ description: R_DESC, snippet: R_SNIPPET, guidelines: readGuide() }, flags);
201
285
  pi.registerTool({
@@ -210,20 +294,26 @@ export function regRead(pi: ExtensionAPI, flags: EditToolFlags = DEFAULT_EDIT_FL
210
294
  description: "Path to the file to read (relative or absolute)",
211
295
  }),
212
296
  offset: Type.Optional(
213
- Type.Integer({
214
- minimum: 1,
215
- description: "Line number to start reading from (1-indexed)",
216
- }),
297
+ Type.Union([
298
+ Type.Integer({
299
+ minimum: 1,
300
+ description: "Line number to start reading from (1-indexed)",
301
+ }),
302
+ Type.String({
303
+ description: "4-char anchor of a served line to start reading from",
304
+ }),
305
+ ], { description: "Line number (1-indexed) or a served anchor to start reading from" }),
217
306
  ),
218
307
  limit: Type.Optional(
219
308
  Type.Integer({
220
309
  minimum: 1,
221
- description: "Maximum number of lines to read",
310
+ description: "Maximum number of lines to read from offset",
222
311
  }),
223
312
  ),
224
313
  }),
225
314
  outputSchema: readResultSchema,
226
315
  executionMode: "sequential",
316
+ renderCall: renderReadCall,
227
317
  renderResult(result, { isPartial, expanded }, theme, context) {
228
318
  if (isPartial) return new Text((theme as unknown as { fg: (a:string,b:string)=>string }).fg("warning", "Reading..."), 0, 0);
229
319
  const raw = (result.content?.[0] as { text?: string } | undefined)?.text;
@@ -243,22 +333,6 @@ export function regRead(pi: ExtensionAPI, flags: EditToolFlags = DEFAULT_EDIT_FL
243
333
 
244
334
  abortIf(signal);
245
335
  await valAccess(absolutePath, rawPath);
246
- const autoReadAllMode = (await readConfig()).autoReadAll ?? "off";
247
- if (autoReadAllMode !== "off") {
248
- const canonical = await resolveTarget(absolutePath).catch(() => undefined);
249
- if (canonical !== undefined) {
250
- const stored = getAutoReadAllSnapshot(sessionKeyFor(ctx), canonical);
251
- if (stored !== undefined) {
252
- const current = await safeSnapId(canonical, "auto-read-all guard");
253
- if (current !== undefined && current === stored) {
254
- const served = servedForPath(canonical);
255
- if (served !== undefined && served.size > 0) {
256
- throw new Error(`[E_AUTO_READ_ALL] ${rawPath} is unchanged since this session's start-of-session auto-read, so the attached content is still exact. Read succeeds on files that have changed since the full auto read.`);
257
- }
258
- }
259
- }
260
- }
261
- }
262
336
 
263
337
  abortIf(signal);
264
338
  const file = await loadFileKindAndText(absolutePath, { maxLines: MAX_HASH_LINES, displayPath: rawPath });
@@ -279,12 +353,10 @@ export function regRead(pi: ExtensionAPI, flags: EditToolFlags = DEFAULT_EDIT_FL
279
353
  rawPath, ctx.cwd, { signal, preloadedFile: file, maxLines: MAX_HASH_LINES },
280
354
  );
281
355
  const fileLines = splitLines(normalized);
356
+ const offset = resolveReadOffset(params.offset, fileLines, fileHashes, resolvedPath);
282
357
  const preview = await fmtReadPreview(
283
358
  normalized,
284
- {
285
- offset: params.offset,
286
- limit: params.limit,
287
- },
359
+ { offset, limit: params.limit },
288
360
  fileHashes,
289
361
  resolvedPath,
290
362
  );
@@ -315,12 +387,12 @@ export function regRead(pi: ExtensionAPI, flags: EditToolFlags = DEFAULT_EDIT_FL
315
387
  details: {
316
388
  truncation: preview.truncation,
317
389
  snapshotId,
318
- offset: params.offset ?? 1,
390
+ offset: preview.startLine,
319
391
  ...(preview.nextOffset !== undefined
320
392
  ? { nextOffset: preview.nextOffset }
321
393
  : {}),
322
394
  metrics: {
323
- truncated: !!preview.truncation,
395
+ truncated: preview.truncation !== undefined,
324
396
  ...(preview.nextOffset !== undefined
325
397
  ? { next_offset: preview.nextOffset }
326
398
  : {}),
@@ -148,9 +148,9 @@ type ReplaceMatchToolDef = ToolDefinition<any, ReplaceDetails, RRState> & { rend
148
148
 
149
149
  export function buildReplaceMatchToolDef(flags: EditToolFlags = DEFAULT_EDIT_FLAGS): ReplaceMatchToolDef {
150
150
  const prompted = withReplaceMatchPrompts({
151
- description: loadP("../prompts/replace-match.md"),
152
- snippet: loadP("../prompts/replace-match-snippet.md"),
153
- guidelines: loadGuide("../prompts/replace-match-guidelines.md"),
151
+ description: loadP("../tool-prompts/replace-match.md"),
152
+ snippet: loadP("../tool-prompts/replace-match-snippet.md"),
153
+ guidelines: loadGuide("../tool-prompts/replace-match-guidelines.md"),
154
154
  }, flags);
155
155
  return {
156
156
  name: "replace_match",
@@ -113,9 +113,9 @@ function fallbackFileMode(): number {
113
113
 
114
114
  export function regUndo(pi: ExtensionAPI, flags: EditToolFlags = DEFAULT_EDIT_FLAGS): void {
115
115
  const prompted = withUndoPrompts({
116
- description: loadP("../prompts/undo-last-change.md"),
117
- snippet: loadP("../prompts/undo-last-change-snippet.md"),
118
- guidelines: loadGuide("../prompts/undo-last-change-guidelines.md"),
116
+ description: loadP("../tool-prompts/undo-last-change.md"),
117
+ snippet: loadP("../tool-prompts/undo-last-change-snippet.md"),
118
+ guidelines: loadGuide("../tool-prompts/undo-last-change-guidelines.md"),
119
119
  }, flags);
120
120
  pi.registerTool({
121
121
  name: "undo_last_change",
package/src/replace.ts CHANGED
@@ -269,9 +269,9 @@ type ToolDef = ToolDefinition<
269
269
 
270
270
  export function buildToolDef(flags: EditToolFlags = DEFAULT_EDIT_FLAGS): ToolDef {
271
271
  const prompted = withReplacePrompts({
272
- description: loadP("../prompts/replace.md"),
273
- snippet: loadP("../prompts/replace-snippet.md"),
274
- guidelines: loadGuide("../prompts/replace-guidelines.md"),
272
+ description: loadP("../tool-prompts/replace.md"),
273
+ snippet: loadP("../tool-prompts/replace-snippet.md"),
274
+ guidelines: loadGuide("../tool-prompts/replace-guidelines.md"),
275
275
  }, flags);
276
276
  const parameters = buildEditToolSchema(flags.requirePath);
277
277
  return {
@@ -0,0 +1,3 @@
1
+ - `read`: call again after an edit when you need anchors you lack — post-edit diff `+anchor│`/` anchor│` rows and any served `anchor│content` rows already carry fresh anchors for the changed range.
2
+ - `read`: pass a served anchor as `offset` with `limit` to read from it without line-number math; an outline row's `[limit N]` is the span to request.
3
+ - `read`: an outline row shows a symbol label, not the file line; edit by anchor, and read the anchor (or copy `old_string` from a served row) before `replace_match`.
@@ -1 +1,3 @@
1
1
  Read a text file and return it as `anchor│content` rows, one per line: a 4-character anchor (letters only, case-sensitive), the `│` separator, then the line content. Target lines in edits by anchor, never by content or line number. Page long files with `offset` and `limit`; when the output says truncated, continue with the hinted `offset/limit`. Images attach visually; binary, directory, and UTF-16/UTF-32 text are rejected; an empty file returns one empty row you can replace to seed content.
2
+
3
+ `offset` takes a 1-indexed line number or a served anchor; with an anchor, `limit` counts the lines to return after it. A stale anchor is refused with the current context rows and fresh anchors.
@@ -1 +0,0 @@
1
- - `read`: call again after an edit when you need anchors you lack — post-edit diff `+anchor│`/` anchor│` rows and any served `anchor│content` rows already carry fresh anchors for the changed range.
@@ -1,27 +0,0 @@
1
- const completeSnapshots = new Map<string, Map<string, string>>();
2
-
3
- function scopeFor(sessionKey: string | undefined): string {
4
- return sessionKey ?? "";
5
- }
6
-
7
- export function recordAutoReadAllComplete(sessionKey: string | undefined, absolutePath: string, snapshotId: string): void {
8
- const scope = scopeFor(sessionKey);
9
- let snapshots = completeSnapshots.get(scope);
10
- if (!snapshots) {
11
- snapshots = new Map();
12
- completeSnapshots.set(scope, snapshots);
13
- }
14
- snapshots.set(absolutePath, snapshotId);
15
- }
16
-
17
- export function getAutoReadAllSnapshot(sessionKey: string | undefined, absolutePath: string): string | undefined {
18
- return completeSnapshots.get(scopeFor(sessionKey))?.get(absolutePath);
19
- }
20
-
21
- export function clearAutoReadAllComplete(sessionKey: string | undefined): void {
22
- completeSnapshots.delete(scopeFor(sessionKey));
23
- }
24
-
25
- export function clearAllAutoReadAllComplete(): void {
26
- completeSnapshots.clear();
27
- }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes