pi-readseek 0.4.30 → 0.5.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.
@@ -7,6 +7,7 @@ import path from "node:path";
7
7
  import { DetailLevel } from "./readseek/enums.js";
8
8
  import type { FileMap, FileSymbol } from "./readseek/types.js";
9
9
  import { SymbolKind } from "./readseek/enums.js";
10
+ import { resolveReadSeekTimeoutMs } from "./readseek-settings.js";
10
11
 
11
12
  export interface ReadSeekHashline {
12
13
  line: number;
@@ -115,6 +116,8 @@ export interface ReadSeekDetectedObject {
115
116
  bbox: [number, number, number, number];
116
117
  }
117
118
 
119
+ export type ReadSeekImageMode = "ocr" | "caption" | "objects";
120
+
118
121
  export type ReadSeekDetection =
119
122
  | {
120
123
  kind: "source";
@@ -146,6 +149,8 @@ export type ReadSeekDetection =
146
149
  mime?: string;
147
150
  };
148
151
 
152
+ type ReadSeekImageDetection = Extract<ReadSeekDetection, { kind: "image" }>;
153
+
149
154
  interface ReadSeekSearchOptions {
150
155
  language?: string;
151
156
  cached?: boolean;
@@ -154,13 +159,6 @@ interface ReadSeekSearchOptions {
154
159
  signal?: AbortSignal;
155
160
  }
156
161
 
157
- interface ReadSeekDetectOptions {
158
- ocr?: boolean;
159
- caption?: boolean;
160
- objects?: boolean;
161
- signal?: AbortSignal;
162
- }
163
-
164
162
  function normalizeLanguage(language: string): string {
165
163
  return language === "java" ? "Java" : language;
166
164
  }
@@ -215,36 +213,43 @@ const require = createRequire(import.meta.url);
215
213
  const READSEEK_REPO_PACKAGE_NAMES = new Set(["@jarkkojs/readseek", "readseek"]);
216
214
  let defaultReadSeekDirInit: Promise<string | null> | undefined;
217
215
 
218
- function readseekPackageDir(): string {
216
+ function readSeekPackageDir(): string {
219
217
  return path.dirname(require.resolve("@jarkkojs/readseek/package.json"));
220
218
  }
221
219
 
222
- function readseekBinaryPath(): string {
223
- if (process.env.READSEEK_BIN) return process.env.READSEEK_BIN;
220
+ const READSEEK_PLATFORM_PACKAGES: Record<string, string> = {
221
+ "darwin-arm64": "@jarkkojs/readseek-darwin-arm64",
222
+ "linux-x64": "@jarkkojs/readseek-linux-x64",
223
+ "win32-x64": "@jarkkojs/readseek-win32-x64",
224
+ };
224
225
 
225
- const platformPackage = (() => {
226
- switch (process.platform) {
227
- case "darwin":
228
- return "@jarkkojs/readseek-darwin-arm64";
229
- case "linux":
230
- return "@jarkkojs/readseek-linux-x64";
231
- case "win32":
232
- return "@jarkkojs/readseek-win32-x64";
233
- default:
234
- throw new Error(`unsupported readseek platform: ${process.platform}`);
235
- }
236
- })();
226
+ function readSeekPlatform(): string {
227
+ return `${process.platform}-${process.arch}`;
228
+ }
229
+
230
+ function readSeekBinaryPath(): string {
231
+ const platform = readSeekPlatform();
232
+ const platformPackage = READSEEK_PLATFORM_PACKAGES[platform];
233
+ if (!platformPackage) {
234
+ const supported = Object.keys(READSEEK_PLATFORM_PACKAGES).join(", ");
235
+ throw new Error(`@jarkkojs/readseek ships no binary for ${platform}; it supports ${supported}`);
236
+ }
237
237
 
238
- const packageJson = require.resolve(`${platformPackage}/package.json`, { paths: [readseekPackageDir()] });
238
+ const packageJson = require.resolve(`${platformPackage}/package.json`, { paths: [readSeekPackageDir()] });
239
239
  return path.join(path.dirname(packageJson), "bin", process.platform === "win32" ? "readseek.exe" : "readseek");
240
240
  }
241
241
 
242
- export function isReadSeekAvailable(): boolean {
242
+ /**
243
+ * Report whether a readseek binary can be resolved for the running platform,
244
+ * along with the reason when it cannot. Used to keep the readseek tools out of
245
+ * the active set on hosts that readseek publishes no binary for.
246
+ */
247
+ export function readSeekBinaryAvailability(): { available: true } | { available: false; reason: string } {
243
248
  try {
244
- readseekBinaryPath();
245
- return true;
246
- } catch {
247
- return false;
249
+ readSeekBinaryPath();
250
+ return { available: true };
251
+ } catch (err) {
252
+ return { available: false, reason: classifyReadSeekFailure(err).message };
248
253
  }
249
254
  }
250
255
 
@@ -317,13 +322,18 @@ function defaultReadSeekDir(): string | null {
317
322
 
318
323
  const DEFAULT_READSEEK_TIMEOUT_MS = 120_000;
319
324
 
320
- function readseekTimeoutMs(): number {
321
- const raw = process.env.READSEEK_TIMEOUT_MS;
322
- if (raw !== undefined && raw !== "") {
323
- const parsed = Number(raw);
324
- if (Number.isFinite(parsed) && parsed > 0) return parsed;
325
- }
326
- return DEFAULT_READSEEK_TIMEOUT_MS;
325
+ function readSeekTimeoutMs(): number {
326
+ return resolveReadSeekTimeoutMs() ?? DEFAULT_READSEEK_TIMEOUT_MS;
327
+ }
328
+
329
+ const READSEEK_USAGE_HINT = /\n\s*Run readseek(?: [\w-]+)* --help for more information\.?\s*$/;
330
+
331
+ function readSeekErrorMessage(stderr: string): string {
332
+ return stderr
333
+ .replace(READSEEK_USAGE_HINT, "")
334
+ .replace(/^error:\s*/i, "")
335
+ .replace(/\s*\n\s*/g, " ")
336
+ .trim();
327
337
  }
328
338
 
329
339
  async function spawnReadSeekRaw(args: string[], options: RunReadSeekOptions = {}): Promise<string> {
@@ -345,8 +355,8 @@ async function spawnReadSeekRaw(args: string[], options: RunReadSeekOptions = {}
345
355
 
346
356
  const stdin = options.stdin;
347
357
  const stdio: StdioOptions = [stdin === undefined ? "ignore" : "pipe", "pipe", "pipe"];
348
- const child = spawn(readseekBinaryPath(), args, { stdio, signal: options.signal });
349
- const timeoutMs = options.timeoutMs ?? readseekTimeoutMs();
358
+ const child = spawn(readSeekBinaryPath(), args, { stdio, signal: options.signal });
359
+ const timeoutMs = options.timeoutMs ?? readSeekTimeoutMs();
350
360
  timeout = setTimeout(() => {
351
361
  child.kill("SIGKILL");
352
362
  fail(new Error(`readseek timed out after ${timeoutMs} ms`));
@@ -391,8 +401,8 @@ async function spawnReadSeekRaw(args: string[], options: RunReadSeekOptions = {}
391
401
  const stdout = Buffer.concat(stdoutChunks).toString("utf-8");
392
402
  const stderr = Buffer.concat(stderrChunks).toString("utf-8").trim();
393
403
  if (code === 0) succeed(stdout);
394
- else if (signal) fail(new Error((stderr || `readseek killed by signal ${signal}`).replace(/^error:\s*/i, "")));
395
- else fail(new Error((stderr || `readseek exited with status ${code}`).replace(/^error:\s*/i, "")));
404
+ else if (signal) fail(new Error(readSeekErrorMessage(stderr) || `readseek killed by signal ${signal}`));
405
+ else fail(new Error(readSeekErrorMessage(stderr) || `readseek exited with status ${code}`));
396
406
  });
397
407
  });
398
408
  }
@@ -411,11 +421,11 @@ async function ensureDefaultReadSeekDir(): Promise<string | null> {
411
421
  return defaultReadSeekDirInit;
412
422
  }
413
423
 
414
- async function readseekInvocationArgs(args: string[]): Promise<string[]> {
424
+ async function readSeekInvocationArgs(args: string[]): Promise<string[]> {
415
425
  if (isOwnReadSeekRepository()) return args;
416
426
 
417
- const readseekDir = await ensureDefaultReadSeekDir();
418
- return readseekDir ? ["--readseek-dir", readseekDir, ...args] : args;
427
+ const readSeekDir = await ensureDefaultReadSeekDir();
428
+ return readSeekDir ? ["--readseek-dir", readSeekDir, ...args] : args;
419
429
  }
420
430
 
421
431
  interface RunReadSeekOptions {
@@ -425,7 +435,7 @@ interface RunReadSeekOptions {
425
435
  }
426
436
 
427
437
  async function runReadSeekRaw(args: string[], options: RunReadSeekOptions = {}): Promise<string> {
428
- return spawnReadSeekRaw(await readseekInvocationArgs(args), options);
438
+ return spawnReadSeekRaw(await readSeekInvocationArgs(args), options);
429
439
  }
430
440
 
431
441
  async function runReadSeek(args: string[], options: RunReadSeekOptions = {}): Promise<unknown> {
@@ -548,14 +558,13 @@ function parseSearchOutput(value: unknown): ReadSeekSearchOutput {
548
558
  };
549
559
  }
550
560
 
551
- export async function readseekRead(
561
+ export async function readSeekRead(
552
562
  filePath: string,
553
563
  startLine?: number,
554
564
  endLine?: number,
555
565
  options: { signal?: AbortSignal } = {},
556
566
  ): Promise<ReadSeekReadOutput> {
557
- const args = ["read", filePath];
558
- if (startLine !== undefined) args.push("--start", String(startLine));
567
+ const args = ["read", startLine === undefined ? filePath : `${filePath}:${startLine}`];
559
568
  if (endLine !== undefined) args.push("--end", String(endLine));
560
569
  return parseReadOutput(await runReadSeek(args, { signal: options.signal }));
561
570
  }
@@ -572,7 +581,7 @@ function fileMapFromReadSeekOutput(output: ReadSeekMapOutput, filePath: string,
572
581
  };
573
582
  }
574
583
 
575
- export async function readseekMap(
584
+ export async function readSeekMap(
576
585
  filePath: string,
577
586
  totalBytes: number,
578
587
  options: { signal?: AbortSignal } = {},
@@ -581,7 +590,7 @@ export async function readseekMap(
581
590
  return fileMapFromReadSeekOutput(output, filePath, totalBytes);
582
591
  }
583
592
 
584
- export async function readseekSearch(
593
+ export async function readSeekSearch(
585
594
  target: string,
586
595
  pattern: string,
587
596
  options: ReadSeekSearchOptions = {},
@@ -594,13 +603,13 @@ export async function readseekSearch(
594
603
  return parseSearchOutput(await runReadSeek(args, { signal: options.signal })).results;
595
604
  }
596
605
 
597
- export async function readseekMapContent(
606
+ export async function readSeekMapContent(
598
607
  filePath: string,
599
608
  content: string,
600
609
  options: { signal?: AbortSignal } = {},
601
610
  ): Promise<FileMap | null> {
602
611
  const output = parseMapOutput(
603
- await runReadSeek(["map", "--stdin", filePath], { signal: options.signal, stdin: content }),
612
+ await runReadSeek(["map", `stdin:${filePath}`], { signal: options.signal, stdin: content }),
604
613
  );
605
614
  return fileMapFromReadSeekOutput(output, filePath, Buffer.byteLength(content, "utf8"));
606
615
  }
@@ -635,7 +644,7 @@ function parseRefsOutput(value: unknown): ReadSeekRefsOutput {
635
644
  };
636
645
  }
637
646
 
638
- export async function readseekRefs(
647
+ export async function readSeekRefs(
639
648
  target: string,
640
649
  name: string,
641
650
  options: ReadSeekRefsOptions = {},
@@ -675,13 +684,13 @@ function parseCheckOutput(value: unknown): ReadSeekCheckOutput {
675
684
  };
676
685
  }
677
686
 
678
- export async function readseekCheck(
687
+ export async function readSeekCheck(
679
688
  filePath: string,
680
689
  content: string,
681
690
  options: { signal?: AbortSignal } = {},
682
691
  ): Promise<ReadSeekCheckOutput> {
683
692
  return parseCheckOutput(
684
- await runReadSeek(["check", "--stdin", filePath], { signal: options.signal, stdin: content }),
693
+ await runReadSeek(["check", `stdin:${filePath}`], { signal: options.signal, stdin: content }),
685
694
  );
686
695
  }
687
696
 
@@ -746,16 +755,48 @@ function parseDetectOutput(value: unknown): ReadSeekDetection {
746
755
  return { kind: "text", type, file, mime };
747
756
  }
748
757
 
749
- export async function readseekDetect(
758
+ export async function readSeekDetect(
750
759
  filePath: string,
751
- options: ReadSeekDetectOptions = {},
760
+ options: { signal?: AbortSignal } = {},
752
761
  ): Promise<ReadSeekDetection> {
753
- const args = ["detect"];
754
- if (options.ocr) args.push("--ocr");
755
- if (options.caption) args.push("--caption");
756
- if (options.objects) args.push("--objects");
757
- args.push(filePath);
758
- return parseDetectOutput(await runReadSeek(args, { signal: options.signal }));
762
+ return parseDetectOutput(await runReadSeek(["detect", filePath], { signal: options.signal }));
763
+ }
764
+
765
+ /**
766
+ * Analyze an image with each requested vision mode and merge the payloads into
767
+ * a single detection. readseek accepts one `--image` mode per invocation, so
768
+ * modes that fail are dropped as long as at least one produced a payload;
769
+ * otherwise the first failure is rethrown.
770
+ */
771
+ export async function readSeekImage(
772
+ filePath: string,
773
+ modes: ReadSeekImageMode[],
774
+ options: { signal?: AbortSignal } = {},
775
+ ): Promise<ReadSeekDetection> {
776
+ const results = await Promise.allSettled(
777
+ modes.map(async (mode) =>
778
+ parseDetectOutput(await runReadSeek(["read", "--image", mode, filePath], { signal: options.signal })),
779
+ ),
780
+ );
781
+
782
+ let merged: ReadSeekImageDetection | undefined;
783
+ for (const result of results) {
784
+ if (result.status !== "fulfilled" || result.value.kind !== "image") continue;
785
+ const detection = result.value;
786
+ merged = merged === undefined
787
+ ? detection
788
+ : {
789
+ ...merged,
790
+ ocr: detection.ocr ?? merged.ocr,
791
+ caption: detection.caption ?? merged.caption,
792
+ objects: detection.objects ?? merged.objects,
793
+ };
794
+ }
795
+ if (merged !== undefined) return merged;
796
+
797
+ const failure = results.find((result) => result.status === "rejected");
798
+ if (failure?.status === "rejected") throw failure.reason;
799
+ throw new Error(`readseek returned no image analysis for ${filePath}`);
759
800
  }
760
801
 
761
802
  // --- Rename ---
@@ -875,7 +916,7 @@ function parseRenameOutput(value: unknown): RenameOutput {
875
916
  };
876
917
  }
877
918
 
878
- export async function readseekRename(
919
+ export async function readSeekRename(
879
920
  filePath: string,
880
921
  options: RenameOptions,
881
922
  ): Promise<RenameOutput> {
@@ -962,13 +1003,13 @@ function parseIdentifyOutput(value: unknown): IdentifyOutput {
962
1003
  };
963
1004
  }
964
1005
 
965
- export async function readseekIdentify(
1006
+ export async function readSeekIdentify(
966
1007
  filePath: string,
967
1008
  content: string,
968
1009
  options: IdentifyOptions = {},
969
1010
  ): Promise<IdentifyOutput> {
970
- const args = ["identify", "--stdin", filePath];
971
- if (options.line !== undefined) args.push("--line", String(options.line));
1011
+ const target = options.line === undefined ? `stdin:${filePath}` : `stdin:${filePath}:${options.line}`;
1012
+ const args = ["identify", target];
972
1013
  if (options.column !== undefined) args.push("--column", String(options.column));
973
1014
  if (options.language) args.push("--language", options.language);
974
1015
  return parseIdentifyOutput(await runReadSeek(args, { signal: options.signal, stdin: content }));
@@ -988,9 +1029,7 @@ export interface DefLocation {
988
1029
  }
989
1030
 
990
1031
  interface DefOptions {
991
- name?: string;
992
- fromIdentify?: boolean;
993
- identifyInput?: string;
1032
+ name: string;
994
1033
  language?: string;
995
1034
  cached?: boolean;
996
1035
  others?: boolean;
@@ -1019,21 +1058,14 @@ function parseDefCompact(value: unknown): DefLocation[] {
1019
1058
  });
1020
1059
  }
1021
1060
 
1022
- export async function readseekDef(
1061
+ export async function readSeekDef(
1023
1062
  target: string,
1024
- options: DefOptions = {},
1063
+ options: DefOptions,
1025
1064
  ): Promise<DefLocation[]> {
1026
- const args = ["def", target, "--format", "plain"];
1027
- if (options.fromIdentify) {
1028
- args.push("--from-identify");
1029
- }
1030
- if (options.name) {
1031
- args.push(options.name);
1032
- }
1065
+ const args = ["def", target, "--format", "plain", options.name];
1033
1066
  if (options.language) args.push("--language", options.language);
1034
1067
  if (options.cached) args.push("--cached");
1035
1068
  if (options.others) args.push("--others");
1036
1069
  if (options.ignored) args.push("--ignored");
1037
- const stdin = options.fromIdentify && options.identifyInput ? options.identifyInput : undefined;
1038
- return parseDefCompact(await runReadSeek(args, { signal: options.signal, stdin }));
1070
+ return parseDefCompact(await runReadSeek(args, { signal: options.signal }));
1039
1071
  }
@@ -16,7 +16,7 @@ export function langParam() {
16
16
  * TypeBox fragment for the Git-scope parameters shared by every readseek
17
17
  * search tool. Returns fresh schema instances so each tool spreads its own.
18
18
  */
19
- export function readseekGitSearchParams() {
19
+ export function readSeekGitSearchParams() {
20
20
  return {
21
21
  cached: Type.Optional(Type.Boolean({ description: "In a Git repository, search tracked/indexed files" })),
22
22
  others: Type.Optional(Type.Boolean({ description: "In a Git repository, search untracked files" })),
@@ -2,23 +2,35 @@ import { readFileSync, statSync, type Stats } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
4
 
5
+ export type ReadSeekOcrMode = "force" | "off" | "auto";
6
+ export type ReadSeekSyntaxValidationMode = "warn" | "block" | "off";
7
+
8
+ interface ReadSeekGrepSettings {
9
+ maxLines?: number;
10
+ maxBytes?: number;
11
+ }
12
+
5
13
  interface ReadSeekJsonSettings {
6
- grep?: { maxLines?: number; maxBytes?: number };
7
- edit?: { diffDisplay?: "collapsed" | "expanded" };
8
- read?: { ocrMode?: "on" | "off" | "auto" };
14
+ excludeTools?: string[];
15
+ ocrMode?: ReadSeekOcrMode;
16
+ syntaxValidation?: ReadSeekSyntaxValidationMode;
17
+ timeoutMs?: number;
18
+ grep?: ReadSeekGrepSettings;
9
19
  }
10
20
 
11
- interface ReadSeekSettingsWarning {
21
+ export interface ReadSeekSettingsWarning {
12
22
  source: string;
13
23
  message: string;
14
24
  path?: string;
15
25
  }
16
26
 
17
- interface ReadSeekSettingsResult {
27
+ export interface ReadSeekSettingsResult {
18
28
  settings: ReadSeekJsonSettings;
19
29
  warnings: ReadSeekSettingsWarning[];
20
30
  }
21
31
 
32
+ const READSEEK_KEYS = ["excludeTools", "ocrMode", "syntaxValidation", "timeoutMs", "grep"];
33
+ const READSEEK_GREP_KEYS = ["maxLines", "maxBytes"];
22
34
 
23
35
  function defaultGlobalSettingsPath(): string {
24
36
  return join(homedir(), ".pi/agent/readseek/settings.json");
@@ -36,6 +48,20 @@ function invalid(source: string, path: string): ReadSeekSettingsWarning {
36
48
  return { source, path, message: `Invalid readseek setting at ${path}` };
37
49
  }
38
50
 
51
+ function warnUnknownKeys(
52
+ raw: Record<string, unknown>,
53
+ known: string[],
54
+ prefix: string,
55
+ source: string,
56
+ warnings: ReadSeekSettingsWarning[],
57
+ ): void {
58
+ for (const key of Object.keys(raw)) {
59
+ if (known.includes(key)) continue;
60
+ const path = `${prefix}.${key}`;
61
+ warnings.push({ source, path, message: `Unknown readseek setting at ${path}` });
62
+ }
63
+ }
64
+
39
65
  function readPositive(
40
66
  raw: Record<string, unknown>,
41
67
  key: string,
@@ -50,40 +76,111 @@ function readPositive(
50
76
  return undefined;
51
77
  }
52
78
 
79
+ function readEnum<T extends string>(
80
+ raw: Record<string, unknown>,
81
+ key: string,
82
+ values: readonly T[],
83
+ path: string,
84
+ source: string,
85
+ warnings: ReadSeekSettingsWarning[],
86
+ ): T | undefined {
87
+ if (!(key in raw)) return undefined;
88
+ const val = raw[key];
89
+ if (typeof val === "string" && (values as readonly string[]).includes(val)) return val as T;
90
+ warnings.push(invalid(source, path));
91
+ return undefined;
92
+ }
93
+
94
+ function readOcrMode(
95
+ raw: Record<string, unknown>,
96
+ source: string,
97
+ warnings: ReadSeekSettingsWarning[],
98
+ ): ReadSeekOcrMode | undefined {
99
+ if (!("ocrMode" in raw)) return undefined;
100
+ const val = raw.ocrMode;
101
+ if (val === "on") return "force";
102
+ if (val === "force" || val === "off" || val === "auto") return val;
103
+ warnings.push(invalid(source, "readseek.ocrMode"));
104
+ return undefined;
105
+ }
106
+
107
+ function readStringArray(
108
+ raw: Record<string, unknown>,
109
+ key: string,
110
+ path: string,
111
+ source: string,
112
+ warnings: ReadSeekSettingsWarning[],
113
+ ): string[] | undefined {
114
+ if (!(key in raw)) return undefined;
115
+ const value = raw[key];
116
+ if (!Array.isArray(value)) {
117
+ warnings.push(invalid(source, path));
118
+ return undefined;
119
+ }
120
+
121
+ const strings: string[] = [];
122
+ value.forEach((item, index) => {
123
+ if (typeof item !== "string" || item.trim() === "") {
124
+ warnings.push(invalid(source, `${path}[${index}]`));
125
+ return;
126
+ }
127
+ strings.push(item);
128
+ });
129
+ return strings;
130
+ }
53
131
 
54
132
  function validateSettings(raw: unknown, source: string): ReadSeekSettingsResult {
55
133
  const settings: ReadSeekJsonSettings = {};
56
134
  const warnings: ReadSeekSettingsWarning[] = [];
57
- if (!isRecord(raw)) return { settings, warnings };
58
-
59
- if (isRecord(raw.grep)) {
60
- const grep: NonNullable<ReadSeekJsonSettings["grep"]> = {};
61
- const maxLines = readPositive(raw.grep, "maxLines", "grep.maxLines", source, warnings);
62
- if (maxLines !== undefined) grep.maxLines = maxLines;
63
- const maxBytes = readPositive(raw.grep, "maxBytes", "grep.maxBytes", source, warnings);
64
- if (maxBytes !== undefined) grep.maxBytes = maxBytes;
65
- if (Object.keys(grep).length > 0) settings.grep = grep;
135
+ if (!isRecord(raw)) {
136
+ warnings.push({ source, message: "Invalid readseek settings: expected a JSON object" });
137
+ return { settings, warnings };
66
138
  }
67
-
68
-
69
- if (isRecord(raw.edit)) {
70
- const edit: NonNullable<ReadSeekJsonSettings["edit"]> = {};
71
- if ("diffDisplay" in raw.edit) {
72
- const value = raw.edit.diffDisplay;
73
- if (value === "collapsed" || value === "expanded") edit.diffDisplay = value;
74
- else warnings.push(invalid(source, "edit.diffDisplay"));
139
+ if (!("readseek" in raw)) {
140
+ if (Object.keys(raw).length > 0) {
141
+ warnings.push({ source, path: "readseek", message: "Missing readseek section: every setting belongs under \"readseek\"" });
75
142
  }
76
- if (Object.keys(edit).length > 0) settings.edit = edit;
143
+ return { settings, warnings };
144
+ }
145
+ if (!isRecord(raw.readseek)) {
146
+ warnings.push(invalid(source, "readseek"));
147
+ return { settings, warnings };
77
148
  }
78
149
 
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"));
150
+ const section = raw.readseek;
151
+ warnUnknownKeys(section, READSEEK_KEYS, "readseek", source, warnings);
152
+
153
+ const excludeTools = readStringArray(section, "excludeTools", "readseek.excludeTools", source, warnings);
154
+ if (excludeTools !== undefined) settings.excludeTools = excludeTools;
155
+
156
+ const ocrMode = readOcrMode(section, source, warnings);
157
+ if (ocrMode !== undefined) settings.ocrMode = ocrMode;
158
+
159
+ const syntaxValidation = readEnum(
160
+ section,
161
+ "syntaxValidation",
162
+ ["warn", "block", "off"] as const,
163
+ "readseek.syntaxValidation",
164
+ source,
165
+ warnings,
166
+ );
167
+ if (syntaxValidation !== undefined) settings.syntaxValidation = syntaxValidation;
168
+
169
+ const timeoutMs = readPositive(section, "timeoutMs", "readseek.timeoutMs", source, warnings);
170
+ if (timeoutMs !== undefined) settings.timeoutMs = timeoutMs;
171
+
172
+ if ("grep" in section) {
173
+ if (isRecord(section.grep)) {
174
+ warnUnknownKeys(section.grep, READSEEK_GREP_KEYS, "readseek.grep", source, warnings);
175
+ const grep: ReadSeekGrepSettings = {};
176
+ const maxLines = readPositive(section.grep, "maxLines", "readseek.grep.maxLines", source, warnings);
177
+ if (maxLines !== undefined) grep.maxLines = maxLines;
178
+ const maxBytes = readPositive(section.grep, "maxBytes", "readseek.grep.maxBytes", source, warnings);
179
+ if (maxBytes !== undefined) grep.maxBytes = maxBytes;
180
+ if (Object.keys(grep).length > 0) settings.grep = grep;
181
+ } else {
182
+ warnings.push(invalid(source, "readseek.grep"));
85
183
  }
86
- if (Object.keys(read).length > 0) settings.read = read;
87
184
  }
88
185
 
89
186
  return { settings, warnings };
@@ -128,14 +225,10 @@ function readSettingsFile(path: string): ReadSeekSettingsResult {
128
225
  }
129
226
 
130
227
  function mergeSettings(base: ReadSeekJsonSettings, override: ReadSeekJsonSettings): ReadSeekJsonSettings {
131
- const merged: ReadSeekJsonSettings = {};
228
+ const settings: ReadSeekJsonSettings = { ...base, ...override };
132
229
  const grep = { ...(base.grep ?? {}), ...(override.grep ?? {}) };
133
- if (Object.keys(grep).length > 0) merged.grep = grep;
134
- const edit = { ...(base.edit ?? {}), ...(override.edit ?? {}) };
135
- if (Object.keys(edit).length > 0) merged.edit = edit;
136
- const read = { ...(base.read ?? {}), ...(override.read ?? {}) };
137
- if (Object.keys(read).length > 0) merged.read = read;
138
- return merged;
230
+ if (Object.keys(grep).length > 0) settings.grep = grep;
231
+ return settings;
139
232
  }
140
233
 
141
234
  export function resolveReadSeekJsonSettings(): ReadSeekSettingsResult {
@@ -147,24 +240,14 @@ export function resolveReadSeekJsonSettings(): ReadSeekSettingsResult {
147
240
  };
148
241
  }
149
242
 
150
- export function resolveEditDiffDisplay(env: NodeJS.ProcessEnv = process.env): "collapsed" | "expanded" {
151
- const raw = env.READSEEK_EDIT_DIFF_DISPLAY;
152
- if (typeof raw === "string") {
153
- const normalized = raw.trim().toLowerCase();
154
- if (normalized === "expanded" || normalized === "collapsed") return normalized;
155
- }
156
- const json = resolveReadSeekJsonSettings().settings.edit?.diffDisplay;
157
- if (json === "expanded" || json === "collapsed") return json;
158
- return "collapsed";
243
+ export function resolveReadSeekOcrMode(): ReadSeekOcrMode {
244
+ return resolveReadSeekJsonSettings().settings.ocrMode ?? "force";
159
245
  }
160
246
 
161
- export function resolveReadSeekOcrMode(env: NodeJS.ProcessEnv = process.env): "on" | "off" | "auto" {
162
- const raw = env.READSEEK_READ_OCR_MODE;
163
- if (typeof raw === "string") {
164
- const normalized = raw.trim().toLowerCase();
165
- if (normalized === "on" || normalized === "off" || normalized === "auto") return normalized;
166
- }
167
- const json = resolveReadSeekJsonSettings().settings.read?.ocrMode;
168
- if (json === "on" || json === "off" || json === "auto") return json;
169
- return "on";
247
+ export function resolveReadSeekSyntaxValidation(): ReadSeekSyntaxValidationMode | undefined {
248
+ return resolveReadSeekJsonSettings().settings.syntaxValidation;
249
+ }
250
+
251
+ export function resolveReadSeekTimeoutMs(): number | undefined {
252
+ return resolveReadSeekJsonSettings().settings.timeoutMs;
170
253
  }
@@ -129,16 +129,16 @@ export function buildReadSeekError(
129
129
  export interface ToolErrorResult {
130
130
  content: [{ type: "text"; text: string }];
131
131
  isError: true;
132
- details: { readseekValue: Record<string, unknown> };
132
+ details: { readSeekValue: Record<string, unknown> };
133
133
  }
134
134
 
135
135
  /**
136
136
  * Build the standard failure envelope shared by every read-family tool: a text
137
- * content block plus a `readseekValue` carrying `ok: false` and a
137
+ * content block plus a `readSeekValue` carrying `ok: false` and a
138
138
  * {@link buildReadSeekError} payload.
139
139
  *
140
140
  * `path` is included only when provided, and `extra` is merged into
141
- * `readseekValue` so callers can attach tool-specific fields (e.g. write's
141
+ * `readSeekValue` so callers can attach tool-specific fields (e.g. write's
142
142
  * `lines`/`warnings`).
143
143
  */
144
144
  export function buildToolErrorResult(
@@ -151,7 +151,7 @@ export function buildToolErrorResult(
151
151
  content: [{ type: "text", text: message }],
152
152
  isError: true,
153
153
  details: {
154
- readseekValue: {
154
+ readSeekValue: {
155
155
  tool,
156
156
  ...(opts.extra ?? {}),
157
157
  ok: false,
@@ -17,7 +17,7 @@ interface BuildRefsOutputInput {
17
17
 
18
18
  interface RefsOutputResult {
19
19
  text: string;
20
- readseekValue: {
20
+ readSeekValue: {
21
21
  tool: "refs";
22
22
  files: Array<{
23
23
  path: string;
@@ -30,13 +30,13 @@ export function buildRefsOutput(input: BuildRefsOutputInput): RefsOutputResult {
30
30
  if (input.files.length === 0) {
31
31
  return {
32
32
  text: `No references found for: ${input.name}`,
33
- readseekValue: { tool: "refs", files: [] },
33
+ readSeekValue: { tool: "refs", files: [] },
34
34
  };
35
35
  }
36
36
 
37
37
  return {
38
38
  text: formatAnchoredFileBlocks(input.files, (line) => (line.enclosingSymbol ? ` (in ${line.enclosingSymbol})` : "")),
39
- readseekValue: {
39
+ readSeekValue: {
40
40
  tool: "refs",
41
41
  files: input.files.map((file) => ({
42
42
  path: file.path,