pi-read-chunks 1.0.1 → 1.0.2
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 +3 -2
- package/package.json +1 -1
- package/read-chunks.ts +100 -4
package/README.md
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
A Pi Agent extension that enhances read functionality for large text files to reduce context bloat, rot and lost in the middle issues. Pi Agent's built in 'read()' tool truncates results over a certain size. 'read-chunks()' instead splits larger files into overlapping chunks snapped to natural boundaries (function endings for code, paragraph breaks for prose), and each chunk is summarised by the active model, chaining the running summary forward as the file is consumed. Files under a configurable size threshold have contents returned verbatim.
|
|
4
4
|
|
|
5
|
-
Replaces the built-in `read()` for text files. Images and other binaries still pass through to the built-in `read`. A precise query stops the scan early at the first chunk that answers it
|
|
5
|
+
Replaces the built-in `read()` for text files. Images and other binaries still pass through to the built-in `read`. A precise query stops the scan early at the first chunk that answers it.
|
|
6
6
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
9
|
-
**Built-in `read()` routing** — A `tool_call` listener intercepts the model's `read` calls. Image files (`png`, `jpg`, `gif`, `webp`, `svg`, `tiff`, `ico`, `heic`, `heif`) and other binaries (`pdf`, archives, Office docs, executables, media) pass through to `read()` unchanged. Text files and unknown extensions are blocked and the model is rerouted to `read-chunks` with the reason surfaced as the block message.
|
|
9
|
+
**Built-in `read()` routing** — A `tool_call` listener intercepts the model's `read` calls. Image files (`png`, `jpg`, `gif`, `webp`, `svg`, `tiff`, `ico`, `heic`, `heif`) and other binaries (`pdf`, archives, Office docs, executables, media) pass through to `read()` unchanged. Text files and unknown extensions are blocked and the model is rerouted to `read-chunks` with the reason surfaced as the block message — **except** when the path carries a trailing numeric line-range suffix (`:N` or `:START-END`): those bypass the block entirely and reach the built-in `read()` verbatim, since native read already serves them with no summarisation needed.
|
|
10
10
|
|
|
11
11
|
**Three read modes, one tool** — `read-chunks` selects automatically based on args:
|
|
12
12
|
- *Full* — file is at or below `thresholdKB`. Returned verbatim. Matches built-in `read` semantics.
|
|
@@ -90,6 +90,7 @@ Examples:
|
|
|
90
90
|
- `read-chunks({ path: "src/big.ts:2000-2089" })` — exact line range, no summarisation.
|
|
91
91
|
- `read-chunks({ path: "src/big.ts:300" })` — start at line 300, read to EOF.
|
|
92
92
|
- `read-chunks({ path: "diagram.png" })` — blocked at `read()` level; the model is rerouted to use the built-in `read` for images.
|
|
93
|
+
- `read("src/file.ts:388-437")` — passes through the extension unblocked; native `read` returns those lines verbatim (line-range suffixes are never routed to `read-chunks`).
|
|
93
94
|
|
|
94
95
|
### Slash command
|
|
95
96
|
|
package/package.json
CHANGED
package/read-chunks.ts
CHANGED
|
@@ -38,6 +38,9 @@
|
|
|
38
38
|
|
|
39
39
|
import { existsSync, readFileSync, statSync, writeFileSync, appendFileSync } from "node:fs";
|
|
40
40
|
import { extname, join, resolve } from "node:path";
|
|
41
|
+
import * as os from "node:os";
|
|
42
|
+
import { pathToFileURL } from "node:url";
|
|
43
|
+
import { Text, hyperlink, getCapabilities } from "@earendil-works/pi-tui";
|
|
41
44
|
import { Type } from "typebox";
|
|
42
45
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
43
46
|
|
|
@@ -406,7 +409,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
406
409
|
name: "read-chunks",
|
|
407
410
|
label: "read-chunks (chunked scan)",
|
|
408
411
|
description:
|
|
409
|
-
"TEXT-ONLY. Use instead of the built-in read() for text files.
|
|
412
|
+
"TEXT-ONLY. Use instead of the built-in read() for text files.",
|
|
410
413
|
parameters: ReadParams,
|
|
411
414
|
|
|
412
415
|
async execute(_toolCallId, params, _signal, onUpdate, ctx) {
|
|
@@ -613,6 +616,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
613
616
|
};
|
|
614
617
|
},
|
|
615
618
|
|
|
619
|
+
// Mirror the built-in read() call header so the UI shows
|
|
620
|
+
// `read-chunks <path>:<line-start-line-end>` (+ optional `[query: ...]`)
|
|
621
|
+
// instead of just the bare tool name. Built-in tools render their header via
|
|
622
|
+
// a custom renderCall; custom tools fall back to the plain tool-name fallback,
|
|
623
|
+
// so we supply one here.
|
|
624
|
+
renderCall(args, theme, context) {
|
|
625
|
+
const text = context.lastComponent ?? new Text("", 0, 0);
|
|
626
|
+
text.setText(formatReadChunksCall(args, theme, context.cwd));
|
|
627
|
+
return text;
|
|
628
|
+
},
|
|
629
|
+
|
|
616
630
|
// Full-file results keep built-in read presentation behavior. Chunked results
|
|
617
631
|
// are emitted as JSON because the model needs structured diagnostic metadata.
|
|
618
632
|
});
|
|
@@ -620,17 +634,56 @@ export default function (pi: ExtensionAPI) {
|
|
|
620
634
|
// Calls to the built-in read() are routed as follows:
|
|
621
635
|
// - Image files (png/jpg/gif/...) → pass through; read() returns image content.
|
|
622
636
|
// - Other binary files (pdf/zip/docx/...) → pass through; read() delivers bytes.
|
|
623
|
-
// -
|
|
637
|
+
// - Targeted line-range reads (offset and/or limit set) → pass through;
|
|
638
|
+
// native read() serves them. These are scoped, summarisation-free.
|
|
639
|
+
// - Line-range suffixes (:N or :START-END) on `path` → strip suffix,
|
|
640
|
+
// translate to native read()'s offset/limit by mutating event.input,
|
|
641
|
+
// and let native read() execute. Native read() does not understand the
|
|
642
|
+
// suffix form, so the rewrite is required.
|
|
643
|
+
// - Text files (and unknown extensions) without a range → block, route
|
|
644
|
+
// model to read-chunks.
|
|
624
645
|
// The returned reason is surfaced to the model for its continuation; omitting
|
|
625
646
|
// terminate keeps the turn alive so the model reroutes to read-chunks.
|
|
626
647
|
pi.on("tool_call", (event) => {
|
|
627
648
|
if (event.toolName !== "read") return;
|
|
628
|
-
const input = event.input as { path?: unknown } | undefined;
|
|
649
|
+
const input = event.input as { path?: unknown; offset?: number; limit?: number } | undefined;
|
|
629
650
|
const path = typeof input?.path === "string" ? input.path : "";
|
|
630
651
|
if (path && (isImagePath(path) || isBinaryPath(path))) return;
|
|
652
|
+
|
|
653
|
+
// Targeted line-range read: native read() handles it cheaply and the model
|
|
654
|
+
// already uses this form (offset/limit) because the read tool's schema
|
|
655
|
+
// documents it. Bypass the block — these are scoped, summarisation-free
|
|
656
|
+
// reads, exactly the kind we want read() to serve directly.
|
|
657
|
+
const hasExplicitRange = typeof input?.offset === "number" || typeof input?.limit === "number";
|
|
658
|
+
if (hasExplicitRange) return;
|
|
659
|
+
|
|
660
|
+
// Numeric line-range suffix (:N or :START-END): strip it from `path` and
|
|
661
|
+
// translate to native read()'s offset/limit, mutating event.input in place.
|
|
662
|
+
// Per the extension API contract, in-place mutation patches the args that
|
|
663
|
+
// the tool will execute with — no re-validation occurs after.
|
|
664
|
+
//
|
|
665
|
+
// Requires at least one char before the `:digits` (`.+`, not `.*?`) so
|
|
666
|
+
// that bare `:50` is rejected (a path cannot be `:50`) and so the engine
|
|
667
|
+
// picks the *last* colon-separator when the path itself contains colons
|
|
668
|
+
// (e.g. Windows `C:\Users\x\file.txt:10` or any URL-like prefix).
|
|
669
|
+
const m = path.match(/^(.+):(\d+)(?:-(\d+))?$/);
|
|
670
|
+
if (m) {
|
|
671
|
+
const newPath = m[1];
|
|
672
|
+
const startLine = Number(m[2]);
|
|
673
|
+
const endLine = m[3] !== undefined ? Number(m[3]) : undefined;
|
|
674
|
+
input!.path = newPath;
|
|
675
|
+
input!.offset = startLine;
|
|
676
|
+
if (endLine !== undefined) {
|
|
677
|
+
input!.limit = Math.max(1, endLine - startLine + 1);
|
|
678
|
+
} else {
|
|
679
|
+
delete input!.limit;
|
|
680
|
+
}
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
|
|
631
684
|
return {
|
|
632
685
|
block: true,
|
|
633
|
-
reason: "Built-in read() is disabled for text files. Use read-chunks(path, query) with a concise query describing what you are looking for.
|
|
686
|
+
reason: "Built-in read() is disabled for text files. Use read-chunks(path/file, query) with a concise query describing what you are looking for.",
|
|
634
687
|
};
|
|
635
688
|
});
|
|
636
689
|
|
|
@@ -663,6 +716,49 @@ export default function (pi: ExtensionAPI) {
|
|
|
663
716
|
});
|
|
664
717
|
}
|
|
665
718
|
|
|
719
|
+
// ---------- Call-header formatting ----------
|
|
720
|
+
|
|
721
|
+
/** Shorten a path by replacing the home dir prefix with ~ (matches pi's built-in read header). */
|
|
722
|
+
function shortenPath(p: string): string {
|
|
723
|
+
const home = os.homedir();
|
|
724
|
+
return p.startsWith(home) ? `~${p.slice(home.length)}` : p;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/** Render a path accent-colored and hyperlinked when the terminal supports it. Mirrors pi's renderToolPath(). */
|
|
728
|
+
function renderToolPath(rawPath: string | null, theme: any, cwd: string): string {
|
|
729
|
+
if (rawPath === null) return theme.fg("error", "[invalid arg]");
|
|
730
|
+
const value = rawPath || "";
|
|
731
|
+
if (!value) return theme.fg("toolOutput", "...");
|
|
732
|
+
const styled = theme.fg("accent", shortenPath(value));
|
|
733
|
+
if (!getCapabilities().hyperlinks) return styled;
|
|
734
|
+
return hyperlink(styled, pathToFileURL(resolve(cwd, value)).href);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* Build the read-chunks call header: `read-chunks <path>:<range>` plus an optional
|
|
739
|
+
* `[query: ...]` suffix. Derives the line-range from the same `:N` / `:START-END`
|
|
740
|
+
* shorthand execute() parses, so the header matches what was actually requested.
|
|
741
|
+
*/
|
|
742
|
+
function formatReadChunksCall(args: any, theme: any, cwd: string): string {
|
|
743
|
+
const rawPath = typeof args?.path === "string" ? args.path : "";
|
|
744
|
+
|
|
745
|
+
let pathPart = rawPath;
|
|
746
|
+
let rangeSuffix = "";
|
|
747
|
+
const m = rawPath.match(/^(.*?):(\d+)(?:-(\d+))?$/);
|
|
748
|
+
if (m && m.index !== undefined) {
|
|
749
|
+
rangeSuffix = `:${m[2]}${m[3] !== undefined ? `-${m[3]}` : ""}`;
|
|
750
|
+
pathPart = m[1];
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
const pathDisplay = renderToolPath(pathPart || null, theme, cwd);
|
|
754
|
+
let text = `${theme.fg("toolTitle", theme.bold("read-chunks"))} ${pathDisplay}${theme.fg("warning", rangeSuffix)}`;
|
|
755
|
+
|
|
756
|
+
if (typeof args?.query === "string" && args.query.trim()) {
|
|
757
|
+
text += theme.fg("muted", ` [query: ${args.query.trim()}]`);
|
|
758
|
+
}
|
|
759
|
+
return text;
|
|
760
|
+
}
|
|
761
|
+
|
|
666
762
|
// ---------- Summary builder ----------
|
|
667
763
|
|
|
668
764
|
/** Compact the raw chunked payload for in-context consumption. */
|