pi-readseek 0.4.30 → 0.5.1

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/src/read.ts CHANGED
@@ -26,16 +26,16 @@ import { buildReadOutput } from "./read-output.js";
26
26
 
27
27
  import { buildLocalBundle } from "./read-local-bundle.js";
28
28
  import { coerceObviousBase10Int } from "./coerce-obvious-int.js";
29
- import { readseekRead, readseekDetect, type ReadSeekDetection } from "./readseek-client.js";
29
+ import { readSeekRead, readSeekDetect, readSeekImage, type ReadSeekDetection } from "./readseek-client.js";
30
30
  import { formatReadCallText, formatReadResultText } from "./read-render-helpers.js";
31
- import { resolveReadSeekOcrMode } from "./readseek-settings.js";
31
+ import { resolveReadSeekImageMode } from "./readseek-settings.js";
32
32
  import { clampLineToWidth, clampLinesToWidth, linkToolPath, renderPendingResult, renderToolLabel, resolveRenderResultContext, summaryLine, wrapReadHashlinesForWidthCached } from "./tui-render-utils.js";
33
33
  import type { FileAnchoredCallback } from "./tool-types.js";
34
34
  import { filePathParam, mapParam, optionalIntOrString, registerReadSeekTool } from "./register-tool.js";
35
35
 
36
36
  const READ_PROMPT_METADATA = defineToolPromptMetadata({
37
37
  promptUrl: new URL("../prompts/read.md", import.meta.url),
38
- promptSnippet: "Read text files or images; text reads include hashline anchors and optional maps/symbol lookup, image reads include the attachment plus OCR text",
38
+ promptSnippet: "Read text files or images; text reads include hashline anchors and optional maps/symbol lookup, image reads include the attachment plus OCR text, an image caption, and detected objects",
39
39
  });
40
40
 
41
41
  interface ReadParams {
@@ -58,16 +58,16 @@ export interface ExecuteReadOptions {
58
58
  onUpdate: any;
59
59
  cwd: string;
60
60
  onSuccessfulRead?: FileAnchoredCallback;
61
- /** Whether the active model accepts image input natively. Used when OCR mode is auto. */
61
+ /** Whether the active model accepts image input natively. Used when imageMode is auto. */
62
62
  modelSupportsImages?: boolean;
63
63
  }
64
64
 
65
65
  function hasReadAnchors(result: AgentToolResult<any>): boolean {
66
66
  const details = (result as { details?: unknown }).details;
67
67
  if (!details || typeof details !== "object") return false;
68
- const readseekValue = (details as { readseekValue?: unknown }).readseekValue;
69
- if (!readseekValue || typeof readseekValue !== "object") return false;
70
- const lines = (readseekValue as { lines?: unknown }).lines;
68
+ const readSeekValue = (details as { readSeekValue?: unknown }).readSeekValue;
69
+ if (!readSeekValue || typeof readSeekValue !== "object") return false;
70
+ const lines = (readSeekValue as { lines?: unknown }).lines;
71
71
  return Array.isArray(lines) && lines.length > 0;
72
72
  }
73
73
 
@@ -171,24 +171,18 @@ export async function executeRead(opts: ExecuteReadOptions): Promise<AgentToolRe
171
171
  if (hasBinaryContent) {
172
172
  let detection: ReadSeekDetection | undefined;
173
173
  try {
174
- detection = await readseekDetect(absolutePath, { signal });
174
+ detection = await readSeekDetect(absolutePath, { signal });
175
175
  } catch {
176
176
  }
177
177
  if (detection?.kind === "image") {
178
178
  const builtinRead = createReadTool(cwd);
179
179
  const builtinResult = await builtinRead.execute(toolCallId, p, signal, onUpdate);
180
- const ocrMode = resolveReadSeekOcrMode();
181
- const shouldRunVision = ocrMode === "on" || (ocrMode === "auto" && !opts.modelSupportsImages);
180
+ const imageMode = resolveReadSeekImageMode();
181
+ const shouldRunVision = imageMode === "force" || (imageMode === "auto" && !opts.modelSupportsImages);
182
182
  if (!shouldRunVision) return succeed(builtinResult);
183
183
 
184
- let ocrFailed = false;
185
184
  try {
186
- const ocrDetection = await readseekDetect(absolutePath, {
187
- ocr: true,
188
- caption: true,
189
- objects: true,
190
- signal,
191
- });
185
+ const ocrDetection = await readSeekImage(absolutePath, ["ocr", "caption", "objects"], { signal });
192
186
  const imageAnalysis = formatImageAnalysis(ocrDetection);
193
187
  if (imageAnalysis) {
194
188
  return succeed({
@@ -200,14 +194,11 @@ export async function executeRead(opts: ExecuteReadOptions): Promise<AgentToolRe
200
194
  });
201
195
  }
202
196
  } catch {
203
- ocrFailed = true;
204
- }
205
- if (ocrFailed) {
206
197
  return succeed({
207
198
  ...builtinResult,
208
199
  content: [
209
200
  ...(builtinResult.content ?? []),
210
- { type: "text" as const, text: "[Warning: local image analysis (OCR) unavailable — showing image attachment only]" },
201
+ { type: "text" as const, text: "[Warning: local image analysis unavailable — showing image attachment only]" },
211
202
  ],
212
203
  });
213
204
  }
@@ -356,33 +347,33 @@ export async function executeRead(opts: ExecuteReadOptions): Promise<AgentToolRe
356
347
  }
357
348
 
358
349
  throwIfAborted(signal);
359
- let readseekOutput: Awaited<ReturnType<typeof readseekRead>>;
350
+ let readSeekOutput: Awaited<ReturnType<typeof readSeekRead>>;
360
351
  try {
361
- readseekOutput = total === 0
362
- ? await readseekRead(absolutePath, undefined, undefined, { signal })
363
- : await readseekRead(absolutePath, startLine, endIdx, { signal });
352
+ readSeekOutput = total === 0
353
+ ? await readSeekRead(absolutePath, undefined, undefined, { signal })
354
+ : await readSeekRead(absolutePath, startLine, endIdx, { signal });
364
355
  } catch (err: any) {
365
356
  const detail = err?.message ? ` — ${err.message}` : "";
366
357
  const message = `readseek failed while reading ${rawPath}${detail}`;
367
358
  return buildToolErrorResult("read", "readseek-error", message, { path: rawParams.path, hint: "Ensure @jarkkojs/readseek and its npm platform package are installed.", details: { message: err?.message } });
368
359
  }
369
360
  const expectedLineCount = Math.max(0, endIdx - startLine + 1);
370
- const invalidLine = readseekOutput.hashlines.find((line, index) => line.line !== startLine + index);
371
- if (readseekOutput.hashlines.length !== expectedLineCount || invalidLine) {
361
+ const invalidLine = readSeekOutput.hashlines.find((line, index) => line.line !== startLine + index);
362
+ if (readSeekOutput.hashlines.length !== expectedLineCount || invalidLine) {
372
363
  const message = invalidLine
373
364
  ? `readseek returned non-sequential line ${invalidLine.line} for requested range ${startLine}-${endIdx}`
374
- : `readseek returned ${readseekOutput.hashlines.length} lines for requested range ${startLine}-${endIdx} (${expectedLineCount} expected)`;
365
+ : `readseek returned ${readSeekOutput.hashlines.length} lines for requested range ${startLine}-${endIdx} (${expectedLineCount} expected)`;
375
366
  return buildToolErrorResult("read", "readseek-output-mismatch", message, { path: rawParams.path });
376
367
  }
377
- const readseekLines: ReadSeekLine[] = readseekOutput.hashlines.map((line) => ({
368
+ const readSeekLines: ReadSeekLine[] = readSeekOutput.hashlines.map((line) => ({
378
369
  line: line.line,
379
370
  hash: line.hash,
380
371
  anchor: `${line.line}:${line.hash}`,
381
372
  raw: line.text,
382
373
  display: escapeControlCharsForDisplay(line.text),
383
374
  }));
384
- const selected = readseekLines.map((line) => line.raw);
385
- const formatted = renderReadSeekLines(readseekLines);
375
+ const selected = readSeekLines.map((line) => line.raw);
376
+ const formatted = renderReadSeekLines(readSeekLines);
386
377
 
387
378
  const truncation = truncateHead(formatted, { maxLines: DEFAULT_MAX_LINES, maxBytes: DEFAULT_MAX_BYTES });
388
379
 
@@ -423,7 +414,7 @@ export async function executeRead(opts: ExecuteReadOptions): Promise<AgentToolRe
423
414
  endLine: endIdx,
424
415
  totalLines: total,
425
416
  selectedLines: selected,
426
- lines: readseekLines,
417
+ lines: readSeekLines,
427
418
  warnings: structuredWarnings,
428
419
  truncation: truncation.truncated
429
420
  ? {
@@ -456,7 +447,7 @@ export async function executeRead(opts: ExecuteReadOptions): Promise<AgentToolRe
456
447
  content: [{ type: "text", text: readOutput.text }],
457
448
  details: {
458
449
  truncation: truncation.truncated ? truncation : undefined,
459
- readseekValue: readOutput.readseekValue,
450
+ readSeekValue: readOutput.readSeekValue,
460
451
  },
461
452
  });
462
453
  }
@@ -469,11 +460,7 @@ function splitReadSeekLines(text: string): string[] {
469
460
 
470
461
  export function registerReadTool(pi: ExtensionAPI, options: ReadToolOptions = {}) {
471
462
  const tool = registerReadSeekTool(pi, {
472
- policy: "read-only",
473
- pythonName: "read",
474
- defaultExposure: "safe-by-default",
475
- }, {
476
- name: "read",
463
+ name: "readSeek_read",
477
464
  label: "Read",
478
465
  description: READ_PROMPT_METADATA.description,
479
466
  promptSnippet: READ_PROMPT_METADATA.promptSnippet,
@@ -529,16 +516,16 @@ export function registerReadTool(pi: ExtensionAPI, options: ReadToolOptions = {}
529
516
  return new Text(clampLinesToWidth([summaryLine(errorText)], width).join("\n"), 0, 0);
530
517
  }
531
518
 
532
- const readseekValue = (result.details as any)?.readseekValue as { range: { startLine: number; endLine: number; totalLines: number }; truncation: any; symbol: any; map: any; warnings: ReadSeekWarning[] } | undefined;
533
- if (!readseekValue) {
519
+ const readSeekValue = (result.details as any)?.readSeekValue as { range: { startLine: number; endLine: number; totalLines: number }; truncation: any; symbol: any; map: any; warnings: ReadSeekWarning[] } | undefined;
520
+ if (!readSeekValue) {
534
521
  const lines = textContent.split("\n").filter(Boolean).length || textContent.split("\n").length;
535
522
  return new Text(summaryLine(`loaded ${lines} ${lines === 1 ? "line" : "lines"}`, { hidden: !!textContent && !expanded }), 0, 0);
536
523
  }
537
524
 
538
- const info = formatReadResultText({ range: readseekValue.range, truncation: readseekValue.truncation, symbol: readseekValue.symbol, map: readseekValue.map, warnings: readseekValue.warnings });
539
- const visibleLines = info.truncated && readseekValue.truncation ? readseekValue.truncation.outputLines : readseekValue.range.endLine - readseekValue.range.startLine + 1;
525
+ const info = formatReadResultText({ range: readSeekValue.range, truncation: readSeekValue.truncation, symbol: readSeekValue.symbol, map: readSeekValue.map, warnings: readSeekValue.warnings });
526
+ const visibleLines = info.truncated && readSeekValue.truncation ? readSeekValue.truncation.outputLines : readSeekValue.range.endLine - readSeekValue.range.startLine + 1;
540
527
  const loadedWord = visibleLines === 1 ? "line" : "lines";
541
- const summaryParts: string[] = [info.truncated ? `loaded ${visibleLines} of ${readseekValue.truncation?.totalLines ?? readseekValue.range.totalLines} ${loadedWord} (truncated)` : `loaded ${visibleLines} ${loadedWord}`];
528
+ const summaryParts: string[] = [info.truncated ? `loaded ${visibleLines} of ${readSeekValue.truncation?.totalLines ?? readSeekValue.range.totalLines} ${loadedWord} (truncated)` : `loaded ${visibleLines} ${loadedWord}`];
542
529
  if (info.symbolBadge) summaryParts.push(info.symbolBadge);
543
530
  for (const badge of info.badges) summaryParts.push(badge);
544
531
  const summary = summaryParts.join(" • ");
@@ -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" })),