pi-readseek 0.8.15 → 0.8.17

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 CHANGED
@@ -56,9 +56,13 @@ Add an optional `readseek` section to `~/.pi/agent/settings.json` (global) or
56
56
 
57
57
  - `replacedTools`: built-in tools backed by ReadSeek. Valid values are `"read"`,
58
58
  `"edit"`, `"write"`, and `"grep"`.
59
- - `imageMode`: `"auto"` exposes `none`, `all`, `ocr`, `caption`, and `objects`;
60
- `"on"` omits `none`; `"off"` skips images and PDFs. Omitting `image` also skips
61
- visual files.
59
+ - `imageMode`: controls standalone images and images embedded in PDFs. `"auto"`
60
+ exposes `none`, `all`, `ocr`, `caption`, and `objects`. When the active model
61
+ accepts images, `"auto"` silently passes prepared images to the model instead
62
+ of running local analysis. `"on"` always runs the selected local analysis and
63
+ omits `none`; `"off"` skips standalone and embedded images. PDF text is always
64
+ read regardless of `imageMode`. Omitting `image` skips standalone images and,
65
+ unless native vision is available in `"auto"` mode, PDF embedded images.
62
66
  - `syntaxValidation`: syntax-regression handling for `readSeek_edit`. `"warn"`
63
67
  writes with a warning, `"block"` aborts, and `"off"` disables the check.
64
68
  - `timeoutMs`: ReadSeek command timeout in milliseconds.
package/dist/index.ts CHANGED
@@ -3251,6 +3251,16 @@ ${details.join(`
3251
3251
 
3252
3252
  `);
3253
3253
  }
3254
+ function preparedPdfContent(pdf) {
3255
+ const content = [{ type: "text", text: truncateDocumentText(pdf.markdown) }];
3256
+ for (const image of pdf.images) {
3257
+ if (image.encoding !== "base64" || image.data === undefined)
3258
+ continue;
3259
+ content.push({ type: "text", text: `[PDF page ${image.page} image]` });
3260
+ content.push({ type: "image", data: image.data, mimeType: image.mime });
3261
+ }
3262
+ return content;
3263
+ }
3254
3264
  function truncateDocumentText(text) {
3255
3265
  const truncation = truncateHead2(text, { maxLines: DEFAULT_MAX_LINES3, maxBytes: DEFAULT_MAX_BYTES3 });
3256
3266
  if (!truncation.truncated)
@@ -3258,9 +3268,9 @@ function truncateDocumentText(text) {
3258
3268
  return `${truncation.content}
3259
3269
  [… document output truncated; use readSeek_view with page or node selectors]`;
3260
3270
  }
3261
- function skippedVisualFile(path2) {
3271
+ function skippedImageFile(path2) {
3262
3272
  return {
3263
- content: [{ type: "text", text: `[Skipped image/PDF: ${path2}; no image mode selected]` }],
3273
+ content: [{ type: "text", text: `[Skipped image: ${path2}; no image mode selected]` }],
3264
3274
  details: {}
3265
3275
  };
3266
3276
  }
@@ -3361,7 +3371,7 @@ async function executeRead(opts) {
3361
3371
  path: rawParams.path
3362
3372
  });
3363
3373
  }
3364
- if (detection.kind === "image" || detection.type === "application/pdf") {
3374
+ if (detection.kind === "image" || detection.kind === "pdf") {
3365
3375
  if (p.offset !== undefined || p.limit !== undefined) {
3366
3376
  const message = "Cannot combine offset/limit with image or PDF reads.";
3367
3377
  return buildToolErrorResult("read", "invalid-params-combo", message, { path: rawParams.path });
@@ -3370,49 +3380,55 @@ async function executeRead(opts) {
3370
3380
  const message = "The page parameter applies to PDFs only.";
3371
3381
  return buildToolErrorResult("read", "invalid-params-combo", message, { path: rawParams.path });
3372
3382
  }
3373
- const pdfPage = detection.kind === "pdf" ? p.page ?? 1 : undefined;
3374
3383
  const imageMode = resolveReadSeekImageMode();
3375
- if (imageMode === "off" || p.image === undefined) {
3376
- return skippedVisualFile(rawParams.path);
3377
- }
3384
+ const useNativeVision = imageMode === "auto" && opts.modelSupportsImages === true;
3378
3385
  if (imageMode === "on" && p.image === "none") {
3379
3386
  return buildToolErrorResult("read", "invalid-params-combo", 'image="none" is only available when imageMode is "auto".', {
3380
3387
  path: rawParams.path
3381
3388
  });
3382
3389
  }
3383
- if (p.image === "none") {
3390
+ if (detection.kind === "pdf") {
3391
+ let pdfImageMode = "none";
3392
+ if (imageMode !== "off" && p.image !== undefined && p.image !== "none" && !useNativeVision) {
3393
+ pdfImageMode = p.image;
3394
+ }
3384
3395
  try {
3385
- if (detection.kind === "pdf") {
3386
- const pdf = await readSeekPdf(absolutePath, p.image, { page: pdfPage, signal });
3387
- const content = [{ type: "text", text: truncateDocumentText(pdf.markdown) }];
3388
- for (const image2 of pdf.images) {
3389
- if (image2.encoding !== "base64" || image2.data === undefined)
3390
- continue;
3391
- content.push({ type: "text", text: `[PDF page ${image2.page} image]` });
3392
- content.push({ type: "image", data: image2.data, mimeType: image2.mime });
3393
- }
3394
- return succeed({ content, details: {} });
3396
+ const pdf = await readSeekPdf(absolutePath, pdfImageMode, { page: p.page ?? 1, signal });
3397
+ if (pdfImageMode !== "none") {
3398
+ return succeed({
3399
+ content: [{ type: "text", text: truncateDocumentText(formatPdfAnalysis(pdf)) }],
3400
+ details: {}
3401
+ });
3395
3402
  }
3396
- const image = await readSeekPreparedImage(absolutePath, { signal });
3397
3403
  return succeed({
3398
- content: [{ type: "image", data: image.data, mimeType: image.mime }],
3404
+ content: useNativeVision ? preparedPdfContent(pdf) : [{ type: "text", text: truncateDocumentText(pdf.markdown) }],
3399
3405
  details: {}
3400
3406
  });
3401
3407
  } catch (error) {
3402
3408
  const message = error instanceof Error ? error.message : String(error);
3403
- return buildToolErrorResult("read", "read-error", `Image preprocessing unavailable: ${message}`, {
3409
+ return buildToolErrorResult("read", "read-error", `PDF reading unavailable: ${message}`, {
3404
3410
  path: rawParams.path
3405
3411
  });
3406
3412
  }
3407
3413
  }
3408
- try {
3409
- if (detection.kind === "pdf") {
3410
- const pdf = await readSeekPdf(absolutePath, p.image, { page: pdfPage, signal });
3414
+ if (imageMode === "off" || p.image === undefined) {
3415
+ return skippedImageFile(rawParams.path);
3416
+ }
3417
+ if (useNativeVision || p.image === "none") {
3418
+ try {
3419
+ const image = await readSeekPreparedImage(absolutePath, { signal });
3411
3420
  return succeed({
3412
- content: [{ type: "text", text: truncateDocumentText(formatPdfAnalysis(pdf)) }],
3421
+ content: [{ type: "image", data: image.data, mimeType: image.mime }],
3413
3422
  details: {}
3414
3423
  });
3424
+ } catch (error) {
3425
+ const message = error instanceof Error ? error.message : String(error);
3426
+ return buildToolErrorResult("read", "read-error", `Image preprocessing unavailable: ${message}`, {
3427
+ path: rawParams.path
3428
+ });
3415
3429
  }
3430
+ }
3431
+ try {
3416
3432
  const analysis = await readSeekImage(absolutePath, [p.image], { signal });
3417
3433
  const imageAnalysis = formatImageAnalysis(analysis, p.image);
3418
3434
  if (imageAnalysis) {
@@ -3423,8 +3439,7 @@ async function executeRead(opts) {
3423
3439
  }
3424
3440
  } catch (error) {
3425
3441
  const message = error instanceof Error ? error.message : String(error);
3426
- const label = detection.kind === "pdf" ? "PDF" : "Image";
3427
- return buildToolErrorResult("read", "read-error", `${label} analysis unavailable: ${message}`, {
3442
+ return buildToolErrorResult("read", "read-error", `Image analysis unavailable: ${message}`, {
3428
3443
  path: rawParams.path
3429
3444
  });
3430
3445
  }
@@ -3663,11 +3678,11 @@ function registerReadTool(pi, options = {}) {
3663
3678
  const tool = registerReadSeekTool(pi, {
3664
3679
  name,
3665
3680
  label: "Read",
3666
- description: imageMode === "off" ? 'Read anchored text by range, map, or symbol; image and PDF files are skipped because imageMode is "off".' : promptMetadata.description,
3681
+ description: imageMode === "off" ? 'Read anchored text by range, map, or symbol; standalone images are skipped because imageMode is "off", while PDFs are read as text.' : promptMetadata.description,
3667
3682
  promptSnippet: promptMetadata.promptSnippet,
3668
3683
  promptGuidelines: [
3669
3684
  ...promptMetadata.promptGuidelines,
3670
- imageMode === "off" ? "The image parameter is unavailable; image and PDF files are always skipped." : `For an image or PDF, explicitly choose image: ${imageModes.join(", ")}; omitting image skips the file.`
3685
+ imageMode === "off" ? "The image parameter is unavailable; standalone images and PDF embedded images are skipped, while PDFs are read as text." : `For an image, explicitly choose image: ${imageModes.join(", ")}; omitting image skips standalone images. PDFs are always read as text, and image controls embedded images.${imageMode === "auto" ? " Native image-capable models bypass local analysis and receive standalone and PDF embedded images directly." : ""}`
3671
3686
  ],
3672
3687
  parameters: Type2.Object({
3673
3688
  path: filePathParam(),
@@ -3681,7 +3696,7 @@ function registerReadTool(pi, options = {}) {
3681
3696
  })),
3682
3697
  ...imageMode === "off" ? {} : {
3683
3698
  image: Type2.Optional(Type2.Union(imageModes.map((mode) => Type2.Literal(mode)), {
3684
- description: `Image/PDF mode: ${imageModes.join(", ")}. Must be selected explicitly.`
3699
+ description: `Standalone image/PDF embedded-image mode: ${imageModes.join(", ")}. Must be selected explicitly for standalone images.`
3685
3700
  }))
3686
3701
  }
3687
3702
  }),
@@ -3692,7 +3707,8 @@ function registerReadTool(pi, options = {}) {
3692
3707
  signal,
3693
3708
  onUpdate,
3694
3709
  cwd: ctx.cwd,
3695
- onSuccessfulRead: options.onSuccessfulRead
3710
+ onSuccessfulRead: options.onSuccessfulRead,
3711
+ modelSupportsImages: ctx.model?.input.includes("image") ?? false
3696
3712
  });
3697
3713
  },
3698
3714
  renderCall(args, theme, ...rest) {
@@ -3726,13 +3742,19 @@ function registerReadTool(pi, options = {}) {
3726
3742
  const readSeekValue = result.details?.readSeekValue;
3727
3743
  if (!readSeekValue) {
3728
3744
  const lines = textContent.split(`
3729
- `).filter(Boolean).length || textContent.split(`
3730
- `).length;
3731
- return new Text2(summaryLine(`loaded ${lines} ${lines === 1 ? "line" : "lines"}`, {
3745
+ `).filter(Boolean).length;
3746
+ let text2 = summaryLine(`loaded ${lines} ${lines === 1 ? "line" : "lines"}`, {
3732
3747
  hidden: !!textContent && !expanded,
3733
3748
  theme,
3734
3749
  style: "success"
3735
- }), 0, 0);
3750
+ });
3751
+ if (expanded && textContent) {
3752
+ text2 += `
3753
+ ${textContent}`;
3754
+ }
3755
+ return new Text2(clampLinesToWidth(text2.split(`
3756
+ `), width).join(`
3757
+ `), 0, 0);
3736
3758
  }
3737
3759
  const info = formatReadResultText({ range: readSeekValue.range, truncation: readSeekValue.truncation, symbol: readSeekValue.symbol, map: readSeekValue.map, warnings: readSeekValue.warnings });
3738
3760
  const visibleLines = info.truncated && readSeekValue.truncation ? readSeekValue.truncation.outputLines : readSeekValue.range.endLine - readSeekValue.range.startLine + 1;
@@ -6124,7 +6146,6 @@ async function statSearchPathOrError(tool, rawPath, searchPath) {
6124
6146
  try {
6125
6147
  return { ok: true, stats: await fsStat2(searchPath) };
6126
6148
  } catch (err) {
6127
- const display = rawPath ?? ".";
6128
6149
  const path3 = rawPath ?? searchPath;
6129
6150
  const { code, message } = formatFsError(err, "stat-error");
6130
6151
  return { ok: false, error: buildToolErrorResult(tool, code, message, { path: path3 }) };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-readseek",
3
- "version": "0.8.15",
3
+ "version": "0.8.17",
4
4
  "description": "Pi extension for readseek-backed hash-anchored read/edit/grep, structural code maps, structural search, and file exploration",
5
5
  "type": "module",
6
6
  "exports": {
@@ -40,7 +40,7 @@
40
40
  "node": ">=20.0.0"
41
41
  },
42
42
  "dependencies": {
43
- "@jarkkojs/readseek": "^0.8.15",
43
+ "@jarkkojs/readseek": "^0.8.17",
44
44
  "diff": "^9.0.0",
45
45
  "xxhash-wasm": "^1.1.0"
46
46
  },
package/prompts/read.md CHANGED
@@ -1,4 +1,4 @@
1
- Read anchored text by range, map, or symbol, and process images or PDFs with an explicit mode.
1
+ Read anchored text by range, map, or symbol, process images with an explicit mode, and always read PDF text.
2
2
 
3
3
  ## Choose the right read
4
4
 
@@ -14,10 +14,10 @@ Read anchored text by range, map, or symbol, and process images or PDFs with an
14
14
  - `map` — full-file map; incompatible with `symbol` or `bundle`.
15
15
  - `symbol` — `Name`, `Class.method`, or `Name@<line>`; incompatible with `offset` / `limit`.
16
16
  - `bundle` — only `"local"`; requires `symbol` and excludes `map`.
17
- - `image` — an exposed image/PDF mode; unavailable when `imageMode` is `"off"`.
17
+ - `image` — an exposed standalone-image/PDF embedded-image mode; unavailable when `imageMode` is `"off"`.
18
18
  - `page` — one-based PDF page; defaults to page 1 and cannot be combined with `offset` / `limit`.
19
19
 
20
- Default cap: {{DEFAULT_MAX_LINES}} lines or {{DEFAULT_MAX_BYTES}}. PDF reads return one page by default. Omitting `image` skips images and PDFs; when `imageMode` is `"off"`, visual files are always skipped.
20
+ Default cap: {{DEFAULT_MAX_LINES}} lines or {{DEFAULT_MAX_BYTES}}. PDF reads always return text for one page by default. Omitting `image` skips standalone images and PDF embedded images unless native vision is available in `"auto"` mode. With native vision, prepared standalone and PDF embedded images are passed directly instead of using local analysis.
21
21
 
22
22
  Truncated full-file reads append a map when available. Use its ranges for follow-up reads.
23
23