pi-readseek 0.4.10 → 0.4.12

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.4.10",
3
+ "version": "0.4.12",
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": {
@@ -39,7 +39,7 @@
39
39
  "node": ">=20.0.0"
40
40
  },
41
41
  "dependencies": {
42
- "@jarkkojs/readseek": "^0.4.13",
42
+ "@jarkkojs/readseek": "^0.4.16",
43
43
  "diff": "^8.0.3",
44
44
  "ignore": "^7.0.5",
45
45
  "picomatch": "^4.0.4",
package/src/edit.ts CHANGED
@@ -16,6 +16,7 @@ import { buildEditOutput } from "./edit-output.js";
16
16
  import { classifyEdit, isDifftAvailable, runDifftastic } from "./edit-classify.js";
17
17
  import type { SemanticSummary } from "./readseek-value.js";
18
18
  import { buildReadseekError } from "./readseek-value.js";
19
+ import { classifyReadseekFailure } from "./readseek-client.js";
19
20
  import { countEditTypes, formatEditCallText, formatEditResultText } from "./edit-render-helpers.js";
20
21
  import { validateSyntaxRegression } from "./edit-syntax-validate.js";
21
22
  import { resolveSyntaxValidateMode, type SyntaxValidateOptions } from "./syntax-validate-mode.js";
@@ -276,20 +277,26 @@ export async function executeEdit(opts: ExecuteEditOptions): Promise<any> {
276
277
  // readseekMapContent is invoked at most once per replace_symbol edit.
277
278
  const replaceSymbolRanges: { start: number; end: number }[] = [];
278
279
  const rsProbeResults: { type: "ok"; content: string; replacement: string; warnings: string[]; range: { start: number; end: number } }[] = [];
279
- for (const rs of replaceSymbolEdits) {
280
- throwIfAborted(signal);
281
- const probe = await replaceSymbol({
282
- filePath: absolutePath,
283
- content: originalNormalized,
284
- symbol: rs.replace_symbol.symbol,
285
- newBody: rs.replace_symbol.new_body,
286
- });
287
- if (probe.type !== "ok") {
288
- // symbol-resolution errors surface before the overlap check.
289
- return buildEditError(absolutePath, "invalid-edit-variant", probe.message);
280
+ try {
281
+ for (const rs of replaceSymbolEdits) {
282
+ throwIfAborted(signal);
283
+ const probe = await replaceSymbol({
284
+ filePath: absolutePath,
285
+ content: originalNormalized,
286
+ symbol: rs.replace_symbol.symbol,
287
+ newBody: rs.replace_symbol.new_body,
288
+ });
289
+ if (probe.type !== "ok") {
290
+ // symbol-resolution errors surface before the overlap check.
291
+ return buildEditError(absolutePath, "invalid-edit-variant", probe.message);
292
+ }
293
+ rsProbeResults.push(probe);
294
+ replaceSymbolRanges.push(probe.range);
290
295
  }
291
- rsProbeResults.push(probe);
292
- replaceSymbolRanges.push(probe.range);
296
+ } catch (err) {
297
+ throwIfAborted(signal);
298
+ const failure = classifyReadseekFailure(err);
299
+ return buildEditError(absolutePath, failure.code, failure.message, failure.hint);
293
300
  }
294
301
 
295
302
  const sortedReplaceSymbolRanges = [...replaceSymbolRanges].sort((a, b) => a.start - b.start || a.end - b.end);
package/src/refs.ts CHANGED
@@ -2,8 +2,8 @@ import type { ExtensionAPI, ToolRenderResultOptions } from "@earendil-works/pi-c
2
2
  import { Text } from "@earendil-works/pi-tui";
3
3
  import { Type } from "@sinclair/typebox";
4
4
  import path from "node:path";
5
- import { stat as fsStat } from "node:fs/promises";
6
5
  import { defineToolPromptMetadata } from "./tool-prompt-metadata.js";
6
+ import { statSearchPathOrError } from "./stat-search-path.js";
7
7
  import { buildReadseekLineWithHash, buildToolErrorResult } from "./readseek-value.js";
8
8
  import { resolveToCwd } from "./path-utils.js";
9
9
  import { classifyReadseekFailure, readseekRefs, type ReadseekReference } from "./readseek-client.js";
@@ -91,25 +91,8 @@ export async function executeRefs(opts: ExecuteRefsOptions): Promise<any> {
91
91
  }
92
92
  const searchPath = resolveToCwd(p.path ?? ".", cwd);
93
93
 
94
- try {
95
- await fsStat(searchPath);
96
- } catch (err: any) {
97
- if (err?.code === "ENOENT") {
98
- return buildToolErrorResult("refs", "path-not-found", `Error: path '${p.path ?? "."}' does not exist`, {
99
- path: p.path ?? searchPath,
100
- });
101
- }
102
- if (err?.code === "EACCES" || err?.code === "EPERM") {
103
- return buildToolErrorResult("refs", "permission-denied", `Error: permission denied for path '${p.path ?? "."}'`, {
104
- path: p.path ?? searchPath,
105
- });
106
- }
107
- const message = `Error: could not access path '${p.path ?? "."}': ${err?.message ?? String(err)}`;
108
- return buildToolErrorResult("refs", "fs-error", message, {
109
- path: p.path ?? searchPath,
110
- details: { fsCode: err?.code, fsMessage: err?.message },
111
- });
112
- }
94
+ const statResult = await statSearchPathOrError("refs", p.path, searchPath);
95
+ if (!statResult.ok) return statResult.error;
113
96
 
114
97
  try {
115
98
  const references = await readseekRefs(searchPath, p.name, {
package/src/sg.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import path from "node:path";
2
- import { stat as fsStat } from "node:fs/promises";
3
2
 
4
3
  import type { ExtensionAPI, ToolRenderResultOptions } from "@earendil-works/pi-coding-agent";
5
4
  import { Text } from "@earendil-works/pi-tui";
@@ -8,6 +7,7 @@ import { Type } from "@sinclair/typebox";
8
7
  import { defineToolPromptMetadata } from "./tool-prompt-metadata.js";
9
8
  import { buildReadseekLineWithHash, buildToolErrorResult, type ReadseekLine } from "./readseek-value.js";
10
9
  import { resolveToCwd } from "./path-utils.js";
10
+ import { statSearchPathOrError } from "./stat-search-path.js";
11
11
  import { classifyReadseekFailure, isReadseekAvailable, readseekSearch, type ReadseekHashline, type ReadseekSearchFileOutput } from "./readseek-client.js";
12
12
  import { buildSgOutput } from "./sg-output.js";
13
13
  import type { FileAnchoredCallback } from "./tool-types.js";
@@ -112,23 +112,10 @@ export async function executeSg(opts: ExecuteSgOptions): Promise<any> {
112
112
  return buildToolErrorResult("search", "invalid-parameter", message);
113
113
  }
114
114
  const searchPath = resolveToCwd(p.path ?? ".", cwd);
115
- let searchPathIsFile = false;
116
115
 
117
- try {
118
- const stat = await fsStat(searchPath);
119
- searchPathIsFile = stat.isFile();
120
- } catch (err: any) {
121
- if (err?.code === "ENOENT") {
122
- const message = `Error: path '${p.path ?? "."}' does not exist`;
123
- return buildToolErrorResult("search", "path-not-found", message, { path: p.path ?? searchPath });
124
- }
125
- if (err?.code === "EACCES" || err?.code === "EPERM") {
126
- const message = `Error: permission denied for path '${p.path ?? "."}'`;
127
- return buildToolErrorResult("search", "permission-denied", message, { path: p.path ?? searchPath });
128
- }
129
- const message = `Error: could not access path '${p.path ?? "."}': ${err?.message ?? String(err)}`;
130
- return buildToolErrorResult("search", "fs-error", message, { path: p.path ?? searchPath, details: { fsCode: err?.code, fsMessage: err?.message } });
131
- }
116
+ const statResult = await statSearchPathOrError("search", p.path, searchPath);
117
+ if (!statResult.ok) return statResult.error;
118
+ const searchPathIsFile = statResult.stats.isFile();
132
119
 
133
120
  try {
134
121
  const effectiveLang = readseekLanguageForPath(p.lang, searchPath, searchPathIsFile);
@@ -0,0 +1,34 @@
1
+ import { stat as fsStat } from "node:fs/promises";
2
+ import type { Stats } from "node:fs";
3
+
4
+ import { buildToolErrorResult, type ToolErrorResult } from "./readseek-value.js";
5
+
6
+ export type StatSearchPathResult =
7
+ | { ok: true; stats: Stats }
8
+ | { ok: false; error: ToolErrorResult };
9
+
10
+ /**
11
+ * Stat a tool's search path, mapping access failures into the shared readseek
12
+ * error taxonomy. Used by refs and search, which reject a missing or
13
+ * unreadable path with identical codes and messages.
14
+ */
15
+ export async function statSearchPathOrError(
16
+ tool: string,
17
+ rawPath: string | undefined,
18
+ searchPath: string,
19
+ ): Promise<StatSearchPathResult> {
20
+ try {
21
+ return { ok: true, stats: await fsStat(searchPath) };
22
+ } catch (err: any) {
23
+ const display = rawPath ?? ".";
24
+ const path = rawPath ?? searchPath;
25
+ if (err?.code === "ENOENT") {
26
+ return { ok: false, error: buildToolErrorResult(tool, "path-not-found", `Error: path '${display}' does not exist`, { path }) };
27
+ }
28
+ if (err?.code === "EACCES" || err?.code === "EPERM") {
29
+ return { ok: false, error: buildToolErrorResult(tool, "permission-denied", `Error: permission denied for path '${display}'`, { path }) };
30
+ }
31
+ const message = `Error: could not access path '${display}': ${err?.message ?? String(err)}`;
32
+ return { ok: false, error: buildToolErrorResult(tool, "fs-error", message, { path, details: { fsCode: err?.code, fsMessage: err?.message } }) };
33
+ }
34
+ }
package/src/write.ts CHANGED
@@ -222,6 +222,21 @@ export async function executeWrite(opts: {
222
222
  },
223
223
  };
224
224
  }
225
+ if (looksLikeBinary(Buffer.from(content, "utf-8"))) {
226
+ warnings.push("File content appears to be binary.");
227
+ readseekWarnings.push(buildReadseekWarning("binary-content", "File content appears to be binary."));
228
+ return {
229
+ text: `Cannot write ${filePath}\n⚠️ File content appears to be binary — refusing to write.`,
230
+ warnings,
231
+ readseekValue: {
232
+ tool: "write",
233
+ path: filePath,
234
+ lines: [],
235
+ warnings: readseekWarnings,
236
+ },
237
+ };
238
+ }
239
+
225
240
  const previousContent = await readPreviousTextForDiff(filePath);
226
241
  const existedBeforeWrite = await access(filePath).then(() => true, () => false);
227
242
 
@@ -240,22 +255,6 @@ export async function executeWrite(opts: {
240
255
  throw err;
241
256
  }
242
257
 
243
- // Binary detection
244
- if (looksLikeBinary(Buffer.from(content, "utf-8"))) {
245
- warnings.push("File content appears to be binary.");
246
- readseekWarnings.push(buildReadseekWarning("binary-content", "File content appears to be binary."));
247
- return {
248
- text: `Wrote ${filePath}\n⚠️ File content appears to be binary — hashlines not generated.`,
249
- warnings,
250
- readseekValue: {
251
- tool: "write",
252
- path: filePath,
253
- lines: [],
254
- warnings: readseekWarnings,
255
- },
256
- };
257
- }
258
-
259
258
  // Compute hashlines
260
259
  const rawLines = content.split("\n");
261
260
  const readseekLines: ReadseekLine[] = [];