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.
- package/README.md +38 -44
- package/index.ts +45 -7
- package/package.json +2 -2
- package/prompts/def.md +2 -9
- package/prompts/edit.md +13 -13
- package/prompts/grep.md +3 -3
- package/prompts/hover.md +2 -6
- package/prompts/read.md +3 -3
- package/prompts/refs.md +1 -1
- package/prompts/rename.md +3 -7
- package/prompts/sg.md +1 -1
- package/prompts/write.md +5 -5
- package/src/def.ts +12 -19
- package/src/edit-output.ts +3 -3
- package/src/edit-syntax-validate.ts +3 -3
- package/src/edit.ts +18 -25
- package/src/file-map.ts +2 -2
- package/src/grep-budget.ts +7 -51
- package/src/grep-output.ts +4 -4
- package/src/grep.ts +19 -26
- package/src/hover.ts +4 -8
- package/src/read-output.ts +4 -4
- package/src/read.ts +25 -34
- package/src/readseek-client.ts +111 -79
- package/src/readseek-params.ts +1 -1
- package/src/readseek-settings.ts +138 -55
- package/src/readseek-value.ts +4 -4
- package/src/refs-output.ts +3 -3
- package/src/refs.ts +6 -10
- package/src/register-tool.ts +4 -42
- package/src/rename.ts +6 -10
- package/src/replace-symbol.ts +2 -2
- package/src/sg-output.ts +3 -3
- package/src/sg.ts +21 -25
- package/src/syntax-validate-mode.ts +3 -13
- package/src/tool-prompt-metadata.ts +12 -12
- package/src/tui-render-utils.ts +2 -2
- package/src/write.ts +22 -26
package/src/readseek-client.ts
CHANGED
|
@@ -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
|
|
216
|
+
function readSeekPackageDir(): string {
|
|
219
217
|
return path.dirname(require.resolve("@jarkkojs/readseek/package.json"));
|
|
220
218
|
}
|
|
221
219
|
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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: [
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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(
|
|
349
|
-
const timeoutMs = options.timeoutMs ??
|
|
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}`)
|
|
395
|
-
else fail(new Error((stderr || `readseek exited with status ${code}`)
|
|
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
|
|
424
|
+
async function readSeekInvocationArgs(args: string[]): Promise<string[]> {
|
|
415
425
|
if (isOwnReadSeekRepository()) return args;
|
|
416
426
|
|
|
417
|
-
const
|
|
418
|
-
return
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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",
|
|
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
|
|
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
|
|
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",
|
|
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
|
|
758
|
+
export async function readSeekDetect(
|
|
750
759
|
filePath: string,
|
|
751
|
-
options:
|
|
760
|
+
options: { signal?: AbortSignal } = {},
|
|
752
761
|
): Promise<ReadSeekDetection> {
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
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
|
|
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
|
|
1006
|
+
export async function readSeekIdentify(
|
|
966
1007
|
filePath: string,
|
|
967
1008
|
content: string,
|
|
968
1009
|
options: IdentifyOptions = {},
|
|
969
1010
|
): Promise<IdentifyOutput> {
|
|
970
|
-
const
|
|
971
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
1038
|
-
return parseDefCompact(await runReadSeek(args, { signal: options.signal, stdin }));
|
|
1070
|
+
return parseDefCompact(await runReadSeek(args, { signal: options.signal }));
|
|
1039
1071
|
}
|
package/src/readseek-params.ts
CHANGED
|
@@ -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
|
|
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" })),
|
package/src/readseek-settings.ts
CHANGED
|
@@ -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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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))
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
|
228
|
+
const settings: ReadSeekJsonSettings = { ...base, ...override };
|
|
132
229
|
const grep = { ...(base.grep ?? {}), ...(override.grep ?? {}) };
|
|
133
|
-
if (Object.keys(grep).length > 0)
|
|
134
|
-
|
|
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
|
|
151
|
-
|
|
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
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
}
|
package/src/readseek-value.ts
CHANGED
|
@@ -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: {
|
|
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 `
|
|
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
|
-
* `
|
|
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
|
-
|
|
154
|
+
readSeekValue: {
|
|
155
155
|
tool,
|
|
156
156
|
...(opts.extra ?? {}),
|
|
157
157
|
ok: false,
|
package/src/refs-output.ts
CHANGED
|
@@ -17,7 +17,7 @@ interface BuildRefsOutputInput {
|
|
|
17
17
|
|
|
18
18
|
interface RefsOutputResult {
|
|
19
19
|
text: string;
|
|
20
|
-
|
|
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
|
-
|
|
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
|
-
|
|
39
|
+
readSeekValue: {
|
|
40
40
|
tool: "refs",
|
|
41
41
|
files: input.files.map((file) => ({
|
|
42
42
|
path: file.path,
|