pi-readseek 0.4.26 → 0.4.27
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/README.md +35 -2
- package/package.json +1 -1
- package/prompts/read.md +1 -1
- package/src/edit.ts +4 -14
- package/src/fs-error.ts +27 -24
- package/src/hover.ts +4 -2
- package/src/read.ts +51 -17
- package/src/readseek-client.ts +36 -2
- package/src/readseek-settings.ts +24 -0
- package/src/stat-search-path.ts +3 -8
- package/src/write.ts +10 -66
package/README.md
CHANGED
|
@@ -24,8 +24,9 @@ npm install --save-dev @jarkkojs/readseek
|
|
|
24
24
|
## Tools
|
|
25
25
|
|
|
26
26
|
- **read** — reads text files with `LINE:HASH` anchors for later `edit` calls;
|
|
27
|
-
images are returned as attachments
|
|
28
|
-
options powered by
|
|
27
|
+
images are returned as attachments and may include local OCR, caption, and
|
|
28
|
+
object text. Supports `symbol`, `map`, and `bundle` options powered by
|
|
29
|
+
`@jarkkojs/readseek`.
|
|
29
30
|
- **edit** — changes existing text files using fresh anchors from `read`,
|
|
30
31
|
`grep`, `search`, or `write`. Variants: `set_line`, `replace_lines`,
|
|
31
32
|
`insert_after`, `replace_symbol`, `replace`. Set `new_text` to `""` to
|
|
@@ -40,6 +41,38 @@ npm install --save-dev @jarkkojs/readseek
|
|
|
40
41
|
- **write** — creates or overwrites whole files and returns anchors for
|
|
41
42
|
immediate follow-up edits.
|
|
42
43
|
|
|
44
|
+
## Settings
|
|
45
|
+
|
|
46
|
+
`pi-readseek` reads optional JSON settings from:
|
|
47
|
+
|
|
48
|
+
| Location | Scope |
|
|
49
|
+
| --- | --- |
|
|
50
|
+
| `~/.pi/agent/readseek/settings.json` | Global |
|
|
51
|
+
| `.pi/readseek/settings.json` | Project |
|
|
52
|
+
|
|
53
|
+
Project settings override global settings. Image OCR behavior is controlled by
|
|
54
|
+
`read.ocrMode`:
|
|
55
|
+
|
|
56
|
+
```json
|
|
57
|
+
{
|
|
58
|
+
"read": {
|
|
59
|
+
"ocrMode": "on"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Modes:
|
|
65
|
+
|
|
66
|
+
- `"on"` — always run local image OCR/caption/object analysis. This is the
|
|
67
|
+
default.
|
|
68
|
+
- `"off"` — return only the image attachment. Use this as a workaround if the
|
|
69
|
+
local readseek image-analysis path crashes.
|
|
70
|
+
- `"auto"` — run local image analysis only when the active model does not
|
|
71
|
+
support native image input.
|
|
72
|
+
|
|
73
|
+
`READSEEK_READ_OCR_MODE=on|off|auto` overrides the JSON setting for one
|
|
74
|
+
process.
|
|
75
|
+
|
|
43
76
|
## Related
|
|
44
77
|
|
|
45
78
|
- [readseek.vim](https://github.com/jarkkojs/readseek.vim) — Vim 9 plugin
|
package/package.json
CHANGED
package/prompts/read.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
Read files through readseek. Text output uses `LINE:HASH|content` anchors that can be copied directly into `edit`; supported images return attachments
|
|
1
|
+
Read files through readseek. Text output uses `LINE:HASH|content` anchors that can be copied directly into `edit`; supported images return attachments and may append OCR/caption/object text depending on `read.ocrMode`. Default cap: {{DEFAULT_MAX_LINES}} lines or {{DEFAULT_MAX_BYTES}}.
|
|
2
2
|
|
|
3
3
|
## Choose the right read
|
|
4
4
|
|
package/src/edit.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { HashlineMismatchError, applyHashlineEdits, computeLineHash, ensureHashI
|
|
|
12
12
|
import { resolveToCwd } from "./path-utils.js";
|
|
13
13
|
import { looksLikeBinary } from "./binary-detect.js";
|
|
14
14
|
import { throwIfAborted } from "./runtime.js";
|
|
15
|
-
import {
|
|
15
|
+
import { formatFsError } from "./fs-error.js";
|
|
16
16
|
import { buildEditOutput } from "./edit-output.js";
|
|
17
17
|
import { classifyEdit, isDifftAvailable, runDifftastic } from "./edit-classify.js";
|
|
18
18
|
import type { SemanticSummary } from "./readseek-value.js";
|
|
@@ -104,19 +104,9 @@ function buildEditError(
|
|
|
104
104
|
};
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
function mapEditFileError(err: any, filePath: string,
|
|
108
|
-
const code = err
|
|
109
|
-
|
|
110
|
-
return buildEditError(filePath, "file-not-found", `Failed to write file: ${displayPath}`);
|
|
111
|
-
}
|
|
112
|
-
const fsError = classifyFsError(err, displayPath);
|
|
113
|
-
if (fsError) {
|
|
114
|
-
return buildEditError(filePath, fsError.code, fsError.message, fsError.hint);
|
|
115
|
-
}
|
|
116
|
-
const prefix = phase === "write" ? "Failed to write file" : "File not readable";
|
|
117
|
-
const message = `${prefix}: ${displayPath}${err?.message ? ` — ${err.message}` : ""}`;
|
|
118
|
-
return buildEditError(filePath, "fs-error", message, undefined,
|
|
119
|
-
{ fsCode: code, fsMessage: err?.message });
|
|
107
|
+
function mapEditFileError(err: any, filePath: string, _displayPath: string, _phase: "read" | "write"): ReturnType<typeof buildEditError> {
|
|
108
|
+
const { code, message } = formatFsError(err, "edit-error");
|
|
109
|
+
return buildEditError(filePath, code, message);
|
|
120
110
|
}
|
|
121
111
|
|
|
122
112
|
export interface EditToolOptions {
|
package/src/fs-error.ts
CHANGED
|
@@ -1,30 +1,33 @@
|
|
|
1
|
-
|
|
1
|
+
const SYSLOG_PATH_SUFFIX = /,\s+[a-z]+(\s+'.*')?$/;
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Derive the canonical OS strerror from a Node {@link err.message} by
|
|
5
|
+
* stripping the trailing syscall and optional path suffix so the result
|
|
6
|
+
* is portable and never duplicates information already carried in the
|
|
7
|
+
* error-envelope `path` field.
|
|
8
|
+
*/
|
|
9
|
+
function strerror(err: { message?: unknown }): string {
|
|
10
|
+
const raw = String(err?.message ?? String(err));
|
|
11
|
+
return raw.replace(SYSLOG_PATH_SUFFIX, "");
|
|
7
12
|
}
|
|
8
13
|
|
|
9
14
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
15
|
+
* Build a readseek error from a Node `fs` errno exception.
|
|
16
|
+
*
|
|
17
|
+
* Every errno maps to its own canonical code automatically — no
|
|
18
|
+
* hand-curated switch, no `"fs-error"` catch-all.
|
|
19
|
+
*
|
|
20
|
+
* @param err The caught exception (carries `err.code` and `err.message`).
|
|
21
|
+
* @param domain Tool-level prefix for the message: `"read-error"`,
|
|
22
|
+
* `"edit-error"`, `"write-error"`, `"stat-error"`.
|
|
13
23
|
*/
|
|
14
|
-
export function
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
case "EPERM":
|
|
24
|
-
return { code: "permission-denied", message: `Permission denied: ${path}` };
|
|
25
|
-
case "ENOENT":
|
|
26
|
-
return { code: "file-not-found", message: `File not found: ${path}` };
|
|
27
|
-
default:
|
|
28
|
-
return null;
|
|
29
|
-
}
|
|
24
|
+
export function formatFsError(
|
|
25
|
+
err: { code?: unknown; message?: string },
|
|
26
|
+
domain: string,
|
|
27
|
+
): { code: string; message: string } {
|
|
28
|
+
const errno = typeof err.code === "string" ? err.code : "EIO";
|
|
29
|
+
return {
|
|
30
|
+
code: errno,
|
|
31
|
+
message: `${domain}: ${errno}: ${strerror(err)}`,
|
|
32
|
+
};
|
|
30
33
|
}
|
package/src/hover.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { Text } from "@earendil-works/pi-tui";
|
|
|
7
7
|
import { defineToolPromptMetadata } from "./tool-prompt-metadata.js";
|
|
8
8
|
import { buildToolErrorResult } from "./readseek-value.js";
|
|
9
9
|
import { resolveToCwd } from "./path-utils.js";
|
|
10
|
+
import { formatFsError } from "./fs-error.js";
|
|
10
11
|
import { classifyReadSeekFailure, readseekIdentify } from "./readseek-client.js";
|
|
11
12
|
import { filePathParam, registerReadSeekTool } from "./register-tool.js";
|
|
12
13
|
|
|
@@ -48,8 +49,9 @@ export async function executeHover(opts: ExecuteHoverOptions): Promise<any> {
|
|
|
48
49
|
let content: string;
|
|
49
50
|
try {
|
|
50
51
|
content = await readFile(filePath, "utf-8");
|
|
51
|
-
} catch {
|
|
52
|
-
|
|
52
|
+
} catch (err: any) {
|
|
53
|
+
const { code, message } = formatFsError(err, "hover-error");
|
|
54
|
+
return buildToolErrorResult("hover", code, message, { path: p.path });
|
|
53
55
|
}
|
|
54
56
|
|
|
55
57
|
try {
|
package/src/read.ts
CHANGED
|
@@ -17,7 +17,7 @@ import { buildReadSeekWarning, buildToolErrorResult, renderReadSeekLines, type R
|
|
|
17
17
|
import { looksLikeBinary } from "./binary-detect.js";
|
|
18
18
|
import { resolveToCwd } from "./path-utils.js";
|
|
19
19
|
import { throwIfAborted } from "./runtime.js";
|
|
20
|
-
import {
|
|
20
|
+
import { formatFsError } from "./fs-error.js";
|
|
21
21
|
import { getOrGenerateMap } from "./file-map.js";
|
|
22
22
|
import { formatFileMapWithBudget } from "./readseek/formatter.js";
|
|
23
23
|
import { findSymbol, type SymbolMatch } from "./readseek/symbol-lookup.js";
|
|
@@ -28,6 +28,7 @@ import { buildLocalBundle } from "./read-local-bundle.js";
|
|
|
28
28
|
import { coerceObviousBase10Int } from "./coerce-obvious-int.js";
|
|
29
29
|
import { readseekRead, readseekDetect, type ReadSeekDetection } from "./readseek-client.js";
|
|
30
30
|
import { formatReadCallText, formatReadResultText } from "./read-render-helpers.js";
|
|
31
|
+
import { resolveReadSeekOcrMode } from "./readseek-settings.js";
|
|
31
32
|
import { clampLineToWidth, clampLinesToWidth, linkToolPath, renderPendingResult, renderToolLabel, resolveRenderResultContext, summaryLine, wrapReadHashlinesForWidth } from "./tui-render-utils.js";
|
|
32
33
|
import type { FileAnchoredCallback } from "./tool-types.js";
|
|
33
34
|
import { filePathParam, mapParam, optionalIntOrString, registerReadSeekTool } from "./register-tool.js";
|
|
@@ -57,6 +58,8 @@ export interface ExecuteReadOptions {
|
|
|
57
58
|
onUpdate: any;
|
|
58
59
|
cwd: string;
|
|
59
60
|
onSuccessfulRead?: FileAnchoredCallback;
|
|
61
|
+
/** Whether the active model accepts image input natively. Used when OCR mode is auto. */
|
|
62
|
+
modelSupportsImages?: boolean;
|
|
60
63
|
}
|
|
61
64
|
|
|
62
65
|
function hasReadAnchors(result: AgentToolResult<any>): boolean {
|
|
@@ -68,6 +71,20 @@ function hasReadAnchors(result: AgentToolResult<any>): boolean {
|
|
|
68
71
|
return Array.isArray(lines) && lines.length > 0;
|
|
69
72
|
}
|
|
70
73
|
|
|
74
|
+
function formatImageAnalysis(detection: ReadSeekDetection): string | undefined {
|
|
75
|
+
if (detection.kind !== "image") return undefined;
|
|
76
|
+
const sections: string[] = [];
|
|
77
|
+
const transcript = detection.transcribe?.text?.trim();
|
|
78
|
+
if (transcript) sections.push(`Transcribed text from image:\n${transcript}`);
|
|
79
|
+
const caption = detection.caption?.trim();
|
|
80
|
+
if (caption) sections.push(`Image caption:\n${caption}`);
|
|
81
|
+
if (detection.objects?.length) {
|
|
82
|
+
const lines = detection.objects.map((object) => `- ${object.label} [${object.bbox.join(", ")}]`);
|
|
83
|
+
sections.push(`Detected objects:\n${lines.join("\n")}`);
|
|
84
|
+
}
|
|
85
|
+
return sections.length > 0 ? sections.join("\n\n") : undefined;
|
|
86
|
+
}
|
|
87
|
+
|
|
71
88
|
export async function executeRead(opts: ExecuteReadOptions): Promise<AgentToolResult<any>> {
|
|
72
89
|
const { toolCallId, params, signal, onUpdate, cwd, onSuccessfulRead } = opts;
|
|
73
90
|
await ensureHashInit();
|
|
@@ -144,37 +161,53 @@ export async function executeRead(opts: ExecuteReadOptions): Promise<AgentToolRe
|
|
|
144
161
|
try {
|
|
145
162
|
rawBuffer = await fsReadFile(absolutePath);
|
|
146
163
|
} catch (err: any) {
|
|
147
|
-
const code = err
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
path: rawParams.path,
|
|
152
|
-
...(fsError.hint ? { hint: fsError.hint } : {}),
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
const message = `File not readable: ${rawPath}${err?.message ? ` — ${err.message}` : ""}`;
|
|
156
|
-
return buildToolErrorResult("read", "fs-error", message, { path: rawParams.path, details: { fsCode: code, fsMessage: err?.message } });
|
|
164
|
+
const { code, message } = formatFsError(err, "read-error");
|
|
165
|
+
return buildToolErrorResult("read", code, message, {
|
|
166
|
+
path: rawParams.path,
|
|
167
|
+
});
|
|
157
168
|
}
|
|
158
169
|
|
|
159
170
|
const hasBinaryContent = looksLikeBinary(rawBuffer);
|
|
160
171
|
if (hasBinaryContent) {
|
|
161
|
-
// Images are always binary; classify and transcribe in a single readseek detect call.
|
|
162
172
|
let detection: ReadSeekDetection | undefined;
|
|
163
173
|
try {
|
|
164
|
-
detection = await readseekDetect(absolutePath, {
|
|
174
|
+
detection = await readseekDetect(absolutePath, { signal });
|
|
165
175
|
} catch {
|
|
166
|
-
// detect unavailable — fall through to binary-as-text handling below
|
|
167
176
|
}
|
|
168
177
|
if (detection?.kind === "image") {
|
|
169
178
|
const builtinRead = createReadTool(cwd);
|
|
170
179
|
const builtinResult = await builtinRead.execute(toolCallId, p, signal, onUpdate);
|
|
171
|
-
const
|
|
172
|
-
|
|
180
|
+
const ocrMode = resolveReadSeekOcrMode();
|
|
181
|
+
const shouldTranscribe = ocrMode === "on" || (ocrMode === "auto" && !opts.modelSupportsImages);
|
|
182
|
+
if (!shouldTranscribe) return succeed(builtinResult);
|
|
183
|
+
|
|
184
|
+
let ocrFailed = false;
|
|
185
|
+
try {
|
|
186
|
+
const ocrDetection = await readseekDetect(absolutePath, {
|
|
187
|
+
transcribe: true,
|
|
188
|
+
caption: true,
|
|
189
|
+
objects: true,
|
|
190
|
+
signal,
|
|
191
|
+
});
|
|
192
|
+
const imageAnalysis = formatImageAnalysis(ocrDetection);
|
|
193
|
+
if (imageAnalysis) {
|
|
194
|
+
return succeed({
|
|
195
|
+
...builtinResult,
|
|
196
|
+
content: [
|
|
197
|
+
...(builtinResult.content ?? []),
|
|
198
|
+
{ type: "text" as const, text: imageAnalysis },
|
|
199
|
+
],
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
} catch {
|
|
203
|
+
ocrFailed = true;
|
|
204
|
+
}
|
|
205
|
+
if (ocrFailed) {
|
|
173
206
|
return succeed({
|
|
174
207
|
...builtinResult,
|
|
175
208
|
content: [
|
|
176
209
|
...(builtinResult.content ?? []),
|
|
177
|
-
{ type: "text" as const, text:
|
|
210
|
+
{ type: "text" as const, text: "[Warning: local image analysis (OCR) unavailable — showing image attachment only]" },
|
|
178
211
|
],
|
|
179
212
|
});
|
|
180
213
|
}
|
|
@@ -465,6 +498,7 @@ export function registerReadTool(pi: ExtensionAPI, options: ReadToolOptions = {}
|
|
|
465
498
|
onUpdate,
|
|
466
499
|
cwd: ctx.cwd,
|
|
467
500
|
onSuccessfulRead: options.onSuccessfulRead,
|
|
501
|
+
modelSupportsImages: ctx.model?.input.includes("image") ?? false,
|
|
468
502
|
});
|
|
469
503
|
},
|
|
470
504
|
renderCall(args: any, theme: any, ...rest: any[]) {
|
package/src/readseek-client.ts
CHANGED
|
@@ -118,6 +118,11 @@ export interface ReadSeekTranscript {
|
|
|
118
118
|
regions: ReadSeekTranscriptRegion[];
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
export interface ReadSeekDetectedObject {
|
|
122
|
+
label: string;
|
|
123
|
+
bbox: [number, number, number, number];
|
|
124
|
+
}
|
|
125
|
+
|
|
121
126
|
export type ReadSeekDetection =
|
|
122
127
|
| {
|
|
123
128
|
kind: "source";
|
|
@@ -139,6 +144,8 @@ export type ReadSeekDetection =
|
|
|
139
144
|
height: number;
|
|
140
145
|
animated: boolean;
|
|
141
146
|
transcribe?: ReadSeekTranscript;
|
|
147
|
+
caption?: string;
|
|
148
|
+
objects?: ReadSeekDetectedObject[];
|
|
142
149
|
}
|
|
143
150
|
| {
|
|
144
151
|
kind: "text";
|
|
@@ -155,6 +162,13 @@ interface ReadSeekSearchOptions {
|
|
|
155
162
|
signal?: AbortSignal;
|
|
156
163
|
}
|
|
157
164
|
|
|
165
|
+
interface ReadSeekDetectOptions {
|
|
166
|
+
transcribe?: boolean;
|
|
167
|
+
caption?: boolean;
|
|
168
|
+
objects?: boolean;
|
|
169
|
+
signal?: AbortSignal;
|
|
170
|
+
}
|
|
171
|
+
|
|
158
172
|
function normalizeLanguage(language: string): string {
|
|
159
173
|
return language === "java" ? "Java" : language;
|
|
160
174
|
}
|
|
@@ -348,10 +362,11 @@ async function spawnReadSeekRaw(args: string[], options: RunReadSeekOptions = {}
|
|
|
348
362
|
});
|
|
349
363
|
childStdin.end(stdin, "utf-8");
|
|
350
364
|
}
|
|
351
|
-
child.on("close", (code) => {
|
|
365
|
+
child.on("close", (code, signal) => {
|
|
352
366
|
const stdout = Buffer.concat(stdoutChunks).toString("utf-8");
|
|
353
367
|
const stderr = Buffer.concat(stderrChunks).toString("utf-8").trim();
|
|
354
368
|
if (code === 0) succeed(stdout);
|
|
369
|
+
else if (signal) fail(new Error((stderr || `readseek killed by signal ${signal}`).replace(/^error:\s*/i, "")));
|
|
355
370
|
else fail(new Error((stderr || `readseek exited with status ${code}`).replace(/^error:\s*/i, "")));
|
|
356
371
|
});
|
|
357
372
|
});
|
|
@@ -659,6 +674,21 @@ function parseTranscript(value: unknown): ReadSeekTranscript | undefined {
|
|
|
659
674
|
};
|
|
660
675
|
}
|
|
661
676
|
|
|
677
|
+
function parseDetectedObjects(value: unknown): ReadSeekDetectedObject[] | undefined {
|
|
678
|
+
if (value === undefined || value === null) return undefined;
|
|
679
|
+
if (!Array.isArray(value)) throw new Error("invalid readseek detect objects");
|
|
680
|
+
return value.map((object) => {
|
|
681
|
+
if (!object || typeof object !== "object") throw new Error("invalid readseek detect object");
|
|
682
|
+
const item = object as Record<string, unknown>;
|
|
683
|
+
const bbox = item.bbox;
|
|
684
|
+
if (!Array.isArray(bbox) || bbox.length !== 4) throw new Error("invalid readseek detect object.bbox");
|
|
685
|
+
return {
|
|
686
|
+
label: requireString(item.label, "object.label"),
|
|
687
|
+
bbox: bbox.map((n, i) => requireNumber(n, `object.bbox[${i}]`)) as ReadSeekDetectedObject["bbox"],
|
|
688
|
+
};
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
|
|
662
692
|
function parseDetectOutput(value: unknown): ReadSeekDetection {
|
|
663
693
|
if (!value || typeof value !== "object") throw new Error("invalid readseek detect output");
|
|
664
694
|
const output = value as Record<string, unknown>;
|
|
@@ -682,6 +712,8 @@ function parseDetectOutput(value: unknown): ReadSeekDetection {
|
|
|
682
712
|
height: requireNumber(output.height, "height"),
|
|
683
713
|
animated: requireBoolean(output.animated, "animated"),
|
|
684
714
|
transcribe: parseTranscript(output.transcribe),
|
|
715
|
+
caption: optionalString(output.caption, "caption"),
|
|
716
|
+
objects: parseDetectedObjects(output.objects),
|
|
685
717
|
};
|
|
686
718
|
}
|
|
687
719
|
if (output.language !== undefined) {
|
|
@@ -701,10 +733,12 @@ function parseDetectOutput(value: unknown): ReadSeekDetection {
|
|
|
701
733
|
|
|
702
734
|
export async function readseekDetect(
|
|
703
735
|
filePath: string,
|
|
704
|
-
options:
|
|
736
|
+
options: ReadSeekDetectOptions = {},
|
|
705
737
|
): Promise<ReadSeekDetection> {
|
|
706
738
|
const args = ["detect"];
|
|
707
739
|
if (options.transcribe) args.push("--transcribe");
|
|
740
|
+
if (options.caption) args.push("--caption");
|
|
741
|
+
if (options.objects) args.push("--objects");
|
|
708
742
|
args.push(filePath);
|
|
709
743
|
return parseDetectOutput(await runReadSeek(args, { signal: options.signal }));
|
|
710
744
|
}
|
package/src/readseek-settings.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { join } from "node:path";
|
|
|
5
5
|
interface ReadSeekJsonSettings {
|
|
6
6
|
grep?: { maxLines?: number; maxBytes?: number };
|
|
7
7
|
edit?: { diffDisplay?: "collapsed" | "expanded" };
|
|
8
|
+
read?: { ocrMode?: "on" | "off" | "auto" };
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
interface ReadSeekSettingsWarning {
|
|
@@ -75,6 +76,16 @@ function validateSettings(raw: unknown, source: string): ReadSeekSettingsResult
|
|
|
75
76
|
if (Object.keys(edit).length > 0) settings.edit = edit;
|
|
76
77
|
}
|
|
77
78
|
|
|
79
|
+
if (isRecord(raw.read)) {
|
|
80
|
+
const read: NonNullable<ReadSeekJsonSettings["read"]> = {};
|
|
81
|
+
if ("ocrMode" in raw.read) {
|
|
82
|
+
const value = raw.read.ocrMode;
|
|
83
|
+
if (value === "on" || value === "off" || value === "auto") read.ocrMode = value;
|
|
84
|
+
else warnings.push(invalid(source, "read.ocrMode"));
|
|
85
|
+
}
|
|
86
|
+
if (Object.keys(read).length > 0) settings.read = read;
|
|
87
|
+
}
|
|
88
|
+
|
|
78
89
|
return { settings, warnings };
|
|
79
90
|
}
|
|
80
91
|
|
|
@@ -96,6 +107,8 @@ function mergeSettings(base: ReadSeekJsonSettings, override: ReadSeekJsonSetting
|
|
|
96
107
|
if (Object.keys(grep).length > 0) merged.grep = grep;
|
|
97
108
|
const edit = { ...(base.edit ?? {}), ...(override.edit ?? {}) };
|
|
98
109
|
if (Object.keys(edit).length > 0) merged.edit = edit;
|
|
110
|
+
const read = { ...(base.read ?? {}), ...(override.read ?? {}) };
|
|
111
|
+
if (Object.keys(read).length > 0) merged.read = read;
|
|
99
112
|
return merged;
|
|
100
113
|
}
|
|
101
114
|
|
|
@@ -118,3 +131,14 @@ export function resolveEditDiffDisplay(env: NodeJS.ProcessEnv = process.env): "c
|
|
|
118
131
|
if (json === "expanded" || json === "collapsed") return json;
|
|
119
132
|
return "collapsed";
|
|
120
133
|
}
|
|
134
|
+
|
|
135
|
+
export function resolveReadSeekOcrMode(env: NodeJS.ProcessEnv = process.env): "on" | "off" | "auto" {
|
|
136
|
+
const raw = env.READSEEK_READ_OCR_MODE;
|
|
137
|
+
if (typeof raw === "string") {
|
|
138
|
+
const normalized = raw.trim().toLowerCase();
|
|
139
|
+
if (normalized === "on" || normalized === "off" || normalized === "auto") return normalized;
|
|
140
|
+
}
|
|
141
|
+
const json = resolveReadSeekJsonSettings().settings.read?.ocrMode;
|
|
142
|
+
if (json === "on" || json === "off" || json === "auto") return json;
|
|
143
|
+
return "on";
|
|
144
|
+
}
|
package/src/stat-search-path.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { stat as fsStat } from "node:fs/promises";
|
|
|
2
2
|
import type { Stats } from "node:fs";
|
|
3
3
|
|
|
4
4
|
import { buildToolErrorResult, type ToolErrorResult } from "./readseek-value.js";
|
|
5
|
+
import { formatFsError } from "./fs-error.js";
|
|
5
6
|
|
|
6
7
|
export type StatSearchPathResult =
|
|
7
8
|
| { ok: true; stats: Stats }
|
|
@@ -22,13 +23,7 @@ export async function statSearchPathOrError(
|
|
|
22
23
|
} catch (err: any) {
|
|
23
24
|
const display = rawPath ?? ".";
|
|
24
25
|
const path = rawPath ?? searchPath;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
if (err?.code === "EACCES" || err?.code === "EPERM") {
|
|
29
|
-
return { ok: false, error: buildToolErrorResult(tool, "permission-denied", `Permission denied for path '${display}'`, { path }) };
|
|
30
|
-
}
|
|
31
|
-
const message = `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 } }) };
|
|
26
|
+
const { code, message } = formatFsError(err, "stat-error");
|
|
27
|
+
return { ok: false, error: buildToolErrorResult(tool, code, message, { path }) };
|
|
33
28
|
}
|
|
34
29
|
}
|
package/src/write.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { resolveToCwd } from "./path-utils.js";
|
|
|
9
9
|
import { ensureHashInit, formatHashlineDisplay } from "./hashline.js";
|
|
10
10
|
import { buildReadSeekError, buildReadSeekLine, buildReadSeekWarning, buildToolErrorResult, type ReadSeekLine, type ReadSeekWarning } from "./readseek-value.js";
|
|
11
11
|
import { looksLikeBinary } from "./binary-detect.js";
|
|
12
|
-
import {
|
|
12
|
+
import { formatFsError } from "./fs-error.js";
|
|
13
13
|
import { getOrGenerateMap } from "./file-map.js";
|
|
14
14
|
import { formatFileMapWithBudget } from "./readseek/formatter.js";
|
|
15
15
|
|
|
@@ -120,57 +120,12 @@ export interface WriteToolOptions {
|
|
|
120
120
|
onFileAnchored?: FileAnchoredCallback;
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
type MappedFsError = {
|
|
124
|
-
code: FsErrorCode | "fs-error";
|
|
125
|
-
message: string;
|
|
126
|
-
hint?: string;
|
|
127
|
-
includeMeta: boolean;
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
function mapFsWriteError(err: any, path: string): MappedFsError {
|
|
131
|
-
const phase: "mkdir" | "write" | undefined = err?.__phase;
|
|
132
|
-
const fsCode = err?.code as string | undefined;
|
|
133
|
-
|
|
134
|
-
if (fsCode === "ENOENT" && phase === "mkdir") {
|
|
135
|
-
return {
|
|
136
|
-
code: "fs-error",
|
|
137
|
-
message: `Cannot create parent directories for ${path}: ${err?.message ?? String(err)}`,
|
|
138
|
-
includeMeta: true,
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
if (fsCode === "ENOSPC") {
|
|
142
|
-
return {
|
|
143
|
-
code: "fs-error",
|
|
144
|
-
message: `No space left on device — cannot write: ${path}`,
|
|
145
|
-
includeMeta: true,
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
if (fsCode === "EROFS") {
|
|
149
|
-
return {
|
|
150
|
-
code: "fs-error",
|
|
151
|
-
message: `Read-only filesystem — cannot write: ${path}`,
|
|
152
|
-
includeMeta: true,
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
const fsError = classifyFsError(err, path);
|
|
156
|
-
if (fsError) {
|
|
157
|
-
return { code: fsError.code, message: fsError.message, hint: fsError.hint, includeMeta: false };
|
|
158
|
-
}
|
|
159
|
-
return {
|
|
160
|
-
code: "fs-error",
|
|
161
|
-
message: `Error writing ${path}: ${err?.message ?? String(err)}`,
|
|
162
|
-
includeMeta: true,
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
|
|
166
123
|
function buildWriteFsErrorResult(err: any, absolutePath: string) {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
details: mapped.includeMeta ? { fsCode: err?.code, fsMessage: err?.message } : undefined,
|
|
173
|
-
});
|
|
124
|
+
const { code, message } = formatFsError(err, "write-error");
|
|
125
|
+
return buildToolErrorResult("write", code, message, {
|
|
126
|
+
path: absolutePath,
|
|
127
|
+
extra: { lines: [], warnings: [] },
|
|
128
|
+
});
|
|
174
129
|
}
|
|
175
130
|
|
|
176
131
|
export async function executeWrite(opts: {
|
|
@@ -218,21 +173,10 @@ export async function executeWrite(opts: {
|
|
|
218
173
|
const previousContent = await readPreviousTextForDiff(filePath);
|
|
219
174
|
const existedBeforeWrite = await access(filePath).then(() => true, () => false);
|
|
220
175
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
err.__phase = "mkdir";
|
|
226
|
-
throw err;
|
|
227
|
-
}
|
|
228
|
-
// Write file
|
|
229
|
-
try {
|
|
230
|
-
await writeFile(filePath, content, "utf-8");
|
|
231
|
-
} catch (err: any) {
|
|
232
|
-
err.__phase = "write";
|
|
233
|
-
throw err;
|
|
234
|
-
}
|
|
235
|
-
|
|
176
|
+
// Create parent directories
|
|
177
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
178
|
+
// Write file
|
|
179
|
+
await writeFile(filePath, content, "utf-8");
|
|
236
180
|
// Compute hashlines
|
|
237
181
|
const rawLines = content.split("\n");
|
|
238
182
|
const readseekLines: ReadSeekLine[] = [];
|