pi-readseek 0.3.17 → 0.3.19

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-readseek",
3
- "version": "0.3.17",
3
+ "version": "0.3.19",
4
4
  "description": "Pi extension for readseek-backed hash-anchored read/edit/grep, structural code maps, structural search, and file exploration",
5
5
  "type": "module",
6
6
  "exports": {
@@ -52,7 +52,7 @@
52
52
  },
53
53
  "devDependencies": {
54
54
  "@earendil-works/pi-ai": "^0.78.0",
55
- "@earendil-works/pi-coding-agent": "^0.78.0",
55
+ "@earendil-works/pi-coding-agent": "^0.79.6",
56
56
  "@earendil-works/pi-tui": "^0.78.0",
57
57
  "@sinclair/typebox": "^0.34.48",
58
58
  "@types/node": "^25.5.0",
@@ -4,16 +4,7 @@ type CoercedIntResult =
4
4
  | { ok: true; value: number | undefined }
5
5
  | { ok: false; message: string };
6
6
 
7
- export function coerceObviousBase10Int(value: unknown): unknown;
8
- export function coerceObviousBase10Int(value: unknown, name: string): CoercedIntResult;
9
- export function coerceObviousBase10Int(value: unknown, name?: string): unknown | CoercedIntResult {
10
- if (name === undefined) {
11
- if (typeof value === "string" && BASE10_INT_RE.test(value)) {
12
- return Number.parseInt(value, 10);
13
- }
14
- return value;
15
- }
16
-
7
+ export function coerceObviousBase10Int(value: unknown, name: string): CoercedIntResult {
17
8
  if (value === undefined) {
18
9
  return { ok: true, value: undefined };
19
10
  }
package/src/diff-data.ts CHANGED
@@ -99,7 +99,6 @@ function parseFullDiffEntries(diff: string): DiffEntry[] {
99
99
  entries.push({ kind: "meta", text: line });
100
100
  }
101
101
 
102
- void nextOldLine;
103
102
  return entries;
104
103
  }
105
104
 
package/src/edit-diff.ts CHANGED
@@ -4,10 +4,19 @@ import { CONFUSABLE_HYPHENS_RE } from "./confusable-hyphens.js";
4
4
 
5
5
 
6
6
  export function detectLineEnding(content: string): "\r\n" | "\n" {
7
- const crlfIdx = content.indexOf("\r\n");
8
- const lfIdx = content.indexOf("\n");
9
- if (lfIdx === -1 || crlfIdx === -1) return "\n";
10
- return crlfIdx < lfIdx ? "\r\n" : "\n";
7
+ let crlf = 0;
8
+ let lf = 0;
9
+ for (let i = 0; i < content.length; i++) {
10
+ if (content[i] === "\r" && i + 1 < content.length && content[i + 1] === "\n") {
11
+ crlf++;
12
+ i++;
13
+ } else if (content[i] === "\n") {
14
+ lf++;
15
+ }
16
+ }
17
+ if (lf === 0) return "\r\n";
18
+ if (crlf === 0) return "\n";
19
+ return crlf > lf ? "\r\n" : "\n";
11
20
  }
12
21
 
13
22
  export function normalizeToLF(text: string): string {
@@ -51,7 +51,7 @@ export async function validateSyntaxRegression(
51
51
  return null;
52
52
  }
53
53
 
54
- const newErrorCount = Math.max(0, after.errorCount - before.errorCount - 1);
54
+ const newErrorCount = Math.max(0, after.errorCount - before.errorCount);
55
55
  const newMissingCount = Math.max(0, after.missingCount - before.missingCount);
56
56
 
57
57
  if (newErrorCount === 0 && newMissingCount === 0) return null;
package/src/edit.ts CHANGED
@@ -116,6 +116,26 @@ function buildEditError(
116
116
  };
117
117
  }
118
118
 
119
+ function mapEditFileError(err: any, filePath: string, displayPath: string, phase: "read" | "write"): ReturnType<typeof buildEditError> {
120
+ const code = err?.code;
121
+ if (code === "EISDIR") {
122
+ return buildEditError(filePath, "path-is-directory",
123
+ `Path is a directory: ${displayPath}`,
124
+ `Use ls(${JSON.stringify(displayPath)}) to inspect directories.`);
125
+ }
126
+ if (code === "ENOENT") {
127
+ return buildEditError(filePath, "file-not-found",
128
+ `${phase === "write" ? "Failed to write file" : "File not found"}: ${displayPath}`);
129
+ }
130
+ if (code === "EACCES" || code === "EPERM") {
131
+ return buildEditError(filePath, "permission-denied", `Permission denied: ${displayPath}`);
132
+ }
133
+ const prefix = phase === "write" ? "Failed to write file" : "File not readable";
134
+ const message = `${prefix}: ${displayPath}${err?.message ? ` — ${err.message}` : ""}`;
135
+ return buildEditError(filePath, "fs-error", message, undefined,
136
+ { fsCode: code, fsMessage: err?.message });
137
+ }
138
+
119
139
  export interface EditToolOptions {
120
140
  wasReadInSession?: (absolutePath: string) => boolean;
121
141
  syntaxValidate?: SyntaxValidateOptions["syntaxValidate"];
@@ -227,27 +247,7 @@ export function registerEditTool(pi: ExtensionAPI, options: EditToolOptions = {}
227
247
  try {
228
248
  rawBuffer = await fsReadFile(absolutePath);
229
249
  } catch (err: any) {
230
- const code = err?.code;
231
- let errCode: string;
232
- let message: string;
233
- let hint: string | undefined;
234
- let errorDetails: { fsCode?: string; fsMessage?: string } | undefined;
235
- if (code === "EISDIR") {
236
- errCode = "path-is-directory";
237
- message = `Path is a directory: ${path}`;
238
- hint = `Use ls(${JSON.stringify(path)}) to inspect directories.`;
239
- } else if (code === "ENOENT") {
240
- errCode = "file-not-found";
241
- message = `File not found: ${path}`;
242
- } else if (code === "EACCES" || code === "EPERM") {
243
- errCode = "permission-denied";
244
- message = `Permission denied: ${path}`;
245
- } else {
246
- errCode = "fs-error";
247
- message = `File not readable: ${path}${err?.message ? ` — ${err.message}` : ""}`;
248
- errorDetails = { fsCode: code, fsMessage: err?.message };
249
- }
250
- return buildEditError(absolutePath, errCode, message, hint, errorDetails);
250
+ return mapEditFileError(err, absolutePath, path, "read");
251
251
  }
252
252
  if (isBinaryBuffer(rawBuffer)) {
253
253
  const message = `Cannot edit binary file: ${path}`;
@@ -258,6 +258,27 @@ export function registerEditTool(pi: ExtensionAPI, options: EditToolOptions = {}
258
258
  const { bom, text: content } = stripBom(raw);
259
259
  const originalEnding = detectLineEnding(content);
260
260
  const originalNormalized = normalizeToLF(content);
261
+ const origLines = originalNormalized.split("\n");
262
+ const anchorSnapshots = new Map<string, string>();
263
+ for (const edit of anchorEdits) {
264
+ const refs: string[] = [];
265
+ if ("set_line" in edit) refs.push(edit.set_line.anchor);
266
+ else if ("replace_lines" in edit) {
267
+ refs.push(edit.replace_lines.start_anchor, edit.replace_lines.end_anchor);
268
+ } else if ("insert_after" in edit) refs.push(edit.insert_after.anchor);
269
+ for (const ref of refs) {
270
+ try {
271
+ const parsed = parseLineRef(ref);
272
+ if (parsed.line >= 1 && parsed.line <= origLines.length) {
273
+ const lineContent = origLines[parsed.line - 1];
274
+ const hash = computeLineHash(parsed.line, lineContent);
275
+ anchorSnapshots.set(ref, `${parsed.line}:${hash}|${escapeControlCharsForDisplay(lineContent)}`);
276
+ }
277
+ } catch {
278
+ /* skip malformed refs */
279
+ }
280
+ }
281
+ }
261
282
  let preAnchorContent = originalNormalized;
262
283
  // AC 26: reject anchored edits that target a line inside any replace_symbol
263
284
  // pre-replace range. Resolve each target against the ORIGINAL content so the
@@ -302,8 +323,8 @@ export function registerEditTool(pi: ExtensionAPI, options: EditToolOptions = {}
302
323
  let startLine: number | undefined;
303
324
  let endLine: number | undefined;
304
325
  try {
305
- startLine = parseLineRef((edit as any).replace_lines.start_anchor).line;
306
- endLine = parseLineRef((edit as any).replace_lines.end_anchor).line;
326
+ startLine = parseLineRef(edit.replace_lines.start_anchor).line;
327
+ endLine = parseLineRef(edit.replace_lines.end_anchor).line;
307
328
  } catch {
308
329
  // Let the normal anchored edit validation report malformed anchors later.
309
330
  }
@@ -319,10 +340,10 @@ export function registerEditTool(pi: ExtensionAPI, options: EditToolOptions = {}
319
340
  }
320
341
  }
321
342
  const refs: string[] = [];
322
- if ("set_line" in edit) refs.push((edit as any).set_line.anchor);
343
+ if ("set_line" in edit) refs.push(edit.set_line.anchor);
323
344
  else if ("replace_lines" in edit) {
324
- refs.push((edit as any).replace_lines.start_anchor, (edit as any).replace_lines.end_anchor);
325
- } else if ("insert_after" in edit) refs.push((edit as any).insert_after.anchor);
345
+ refs.push(edit.replace_lines.start_anchor, edit.replace_lines.end_anchor);
346
+ } else if ("insert_after" in edit) refs.push(edit.insert_after.anchor);
326
347
  for (const ref of refs) {
327
348
  let parsedLine: number | undefined;
328
349
  try {
@@ -412,30 +433,9 @@ export function registerEditTool(pi: ExtensionAPI, options: EditToolOptions = {}
412
433
  .join("\n");
413
434
  diagnostic += "\nRe-read the file to see the current state.";
414
435
  } else {
415
- // Edits were not literally identical but heuristics normalized them back
416
- const lines = result.split("\n");
417
- const targetLines: string[] = [];
418
- for (const edit of edits) {
419
- const refs: string[] = [];
420
- if ("set_line" in edit) refs.push((edit as any).set_line.anchor);
421
- else if ("replace_lines" in edit) {
422
- refs.push((edit as any).replace_lines.start_anchor, (edit as any).replace_lines.end_anchor);
423
- } else if ("insert_after" in edit) refs.push((edit as any).insert_after.anchor);
424
- for (const ref of refs) {
425
- try {
426
- const parsed = parseLineRef(ref);
427
- if (parsed.line >= 1 && parsed.line <= lines.length) {
428
- const lineContent = lines[parsed.line - 1];
429
- const hash = computeLineHash(parsed.line, lineContent);
430
- targetLines.push(`${parsed.line}:${hash}|${escapeControlCharsForDisplay(lineContent)}`);
431
- }
432
- } catch {
433
- /* skip malformed refs */
434
- }
435
- }
436
- }
436
+ const targetLines = [...new Set(anchorSnapshots.values())];
437
437
  if (targetLines.length > 0) {
438
- const preview = [...new Set(targetLines)].slice(0, 5).join("\n");
438
+ const preview = targetLines.slice(0, 5).join("\n");
439
439
  diagnostic += `\nThe file currently contains:\n${preview}\nYour edits were normalized back to the original content. Ensure your replacement changes actual code, not just formatting.`;
440
440
  }
441
441
  }
@@ -467,18 +467,7 @@ export function registerEditTool(pi: ExtensionAPI, options: EditToolOptions = {}
467
467
  try {
468
468
  await fsWriteFile(absolutePath, writeContent, "utf-8");
469
469
  } catch (err: any) {
470
- const wrapped = wrapWriteError(err, path);
471
- const code =
472
- err?.code === "EACCES" || err?.code === "EPERM"
473
- ? "permission-denied"
474
- : err?.code === "ENOENT"
475
- ? "file-not-found"
476
- : "fs-error";
477
- const message =
478
- code === "fs-error" && err?.message ? `${wrapped.message} — ${err.message}` : wrapped.message;
479
- return buildEditError(absolutePath, code, message, undefined, code === "fs-error"
480
- ? { fsCode: err?.code, fsMessage: err?.message }
481
- : undefined);
470
+ return mapEditFileError(err, absolutePath, path, "write");
482
471
  }
483
472
 
484
473
  if (input.postEditVerify === true) {
@@ -555,7 +544,6 @@ export function registerEditTool(pi: ExtensionAPI, options: EditToolOptions = {}
555
544
  semanticSummary,
556
545
  });
557
546
 
558
- const warn = warnings.length ? `\n\nWarnings:\n${warnings.join("\n")}` : "";
559
547
  return {
560
548
  content: [{ type: "text", text: builtOutput.text }],
561
549
  details: {
@@ -581,10 +569,8 @@ export function registerEditTool(pi: ExtensionAPI, options: EditToolOptions = {}
581
569
  };
582
570
  });
583
571
  } catch (err: any) {
584
- const code = err?.code;
585
- if (typeof code === "string") {
586
- const message = `File not readable: ${path}${err?.message ? ` — ${err.message}` : ""}`;
587
- return buildEditError(absolutePath, "fs-error", message, undefined, { fsCode: code, fsMessage: err?.message });
572
+ if (typeof err?.code === "string") {
573
+ return mapEditFileError(err, absolutePath, path, "read");
588
574
  }
589
575
  throw err;
590
576
  }
@@ -49,18 +49,6 @@ function readPositive(
49
49
  return undefined;
50
50
  }
51
51
 
52
- function readBoolean(
53
- raw: Record<string, unknown>,
54
- key: string,
55
- path: string,
56
- source: string,
57
- warnings: ReadseekSettingsWarning[],
58
- ): boolean | undefined {
59
- if (!(key in raw)) return undefined;
60
- if (typeof raw[key] === "boolean") return raw[key];
61
- warnings.push(invalid(source, path));
62
- return undefined;
63
- }
64
52
 
65
53
  function validateSettings(raw: unknown, source: string): ReadseekSettingsResult {
66
54
  const settings: ReadseekJsonSettings = {};
@@ -53,9 +53,11 @@ export async function replaceSymbol(input: ReplaceSymbolInput): Promise<ReplaceS
53
53
  const reindented = reindent(dedent(input.newBody), indent);
54
54
  const warnings: string[] = [];
55
55
  const leaf = input.symbol.replace(/@\d+$/, "").split(".").pop() ?? "";
56
+ const declNameRe =
57
+ /\b(?:(?:export\s+(?:default\s+)?|async\s+|public\s+|private\s+|protected\s+|static\s+)*)(?:function\*?|class|const|let|var|fn|def|method|type|interface|enum)\s+([A-Za-z_$][\w$]*)/s;
56
58
  const firstDeclName =
57
- reindented.match(/\b(?:function|class|method|const|let|var)\s+([A-Za-z_$][\w$]*)/)?.[1]
58
- ?? reindented.match(/^\s*(?:[\w$<>,?\s]+\s+)?([A-Za-z_$][\w$]*)\s*\(/)?.[1];
59
+ reindented.match(declNameRe)?.[1]
60
+ ?? reindented.match(/^\s*(?:(?:async\s+)?[\w$<>,?\s]+\s+)?([A-Za-z_$][\w$]*)\s*\(/)?.[1];
59
61
  if (leaf && firstDeclName && firstDeclName !== leaf) {
60
62
  warnings.push(`name-mismatch: expected ${leaf}, got ${firstDeclName}`);
61
63
  }
package/src/write.ts CHANGED
@@ -294,7 +294,7 @@ export async function executeWrite(opts: {
294
294
  warnings: readseekWarnings,
295
295
  diff: diffResult.diff,
296
296
  diffData,
297
- ...(requestMap !== undefined ? { map: { appended: mapAppended } } : {}),
297
+ ...(requestMap ? { map: { appended: mapAppended } } : {}),
298
298
  },
299
299
  };
300
300
  }