pi-lean-ctx 3.8.7 → 3.8.9
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/extensions/config.ts +26 -0
- package/extensions/index.ts +42 -25
- package/package.json +1 -1
package/extensions/config.ts
CHANGED
|
@@ -132,6 +132,32 @@ export function resolveRouteShell(mode: PiMode, fileRouteShell: unknown): boolea
|
|
|
132
132
|
return fileRouteShell === true;
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
/**
|
|
136
|
+
* The five Pi builtins lean-ctx ships compressed `ctx_*` replacements for.
|
|
137
|
+
* Suppressing one removes it from the agent's tool set, so the agent must reach
|
|
138
|
+
* for the metered ctx_* equivalent instead of the uncompressed native.
|
|
139
|
+
*/
|
|
140
|
+
export const REPLACEABLE_BUILTIN_TOOLS = ["read", "bash", "ls", "find", "grep"] as const;
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* The Pi builtins to suppress for a resolved config. Single source of truth for
|
|
144
|
+
* the R1 "102 native bash / 0 ctx_shell" fix (#361): whenever the returned set
|
|
145
|
+
* contains `bash`, the native shell is gone and the agent must route through
|
|
146
|
+
* `ctx_shell` (compressed + metered).
|
|
147
|
+
*
|
|
148
|
+
* replace → all five natives suppressed (only ctx_* exposed)
|
|
149
|
+
* additive+routeShell → only `bash` suppressed (read/ls/find/grep stay)
|
|
150
|
+
* additive → nothing suppressed (fully non-regressive default)
|
|
151
|
+
*
|
|
152
|
+
* Invariant: every suppressed name has a ctx_* replacement (a subset of
|
|
153
|
+
* REPLACEABLE_BUILTIN_TOOLS), so a builtin is never removed without a substitute.
|
|
154
|
+
*/
|
|
155
|
+
export function resolveSuppressedBuiltins(mode: PiMode, routeShell: boolean): Set<string> {
|
|
156
|
+
if (mode === "replace") return new Set(REPLACEABLE_BUILTIN_TOOLS);
|
|
157
|
+
if (routeShell) return new Set(["bash"]);
|
|
158
|
+
return new Set<string>();
|
|
159
|
+
}
|
|
160
|
+
|
|
135
161
|
/** Split a comma/whitespace-separated tool list into trimmed, non-empty names. */
|
|
136
162
|
function parseToolList(raw: string | undefined): string[] {
|
|
137
163
|
if (!raw) return [];
|
package/extensions/index.ts
CHANGED
|
@@ -22,7 +22,7 @@ import { readFile, stat } from "node:fs/promises";
|
|
|
22
22
|
import { extname, resolve } from "node:path";
|
|
23
23
|
import { homedir, platform } from "node:os";
|
|
24
24
|
import { McpBridge } from "./mcp-bridge.js";
|
|
25
|
-
import { loadPiConfig } from "./config.js";
|
|
25
|
+
import { loadPiConfig, resolveSuppressedBuiltins } from "./config.js";
|
|
26
26
|
import type { CompressionStats } from "./types.js";
|
|
27
27
|
|
|
28
28
|
const CODE_EXTENSIONS = new Set([
|
|
@@ -44,12 +44,12 @@ const IMAGE_EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".gif", ".webp"]);
|
|
|
44
44
|
const CODE_FULL_READ_MAX_BYTES = 8 * 1024;
|
|
45
45
|
const CODE_SIGNATURES_MIN_BYTES = 96 * 1024;
|
|
46
46
|
|
|
47
|
-
// Pi builtins
|
|
48
|
-
// Settings resolve from (most explicit first):
|
|
49
|
-
// ~/.pi/agent/extensions/pi-lean-ctx/config.json,
|
|
47
|
+
// Which Pi builtins to suppress is resolved by resolveSuppressedBuiltins (in
|
|
48
|
+
// config.ts, unit-tested). Settings resolve from (most explicit first):
|
|
49
|
+
// LEAN_CTX_PI_* env vars, then ~/.pi/agent/extensions/pi-lean-ctx/config.json,
|
|
50
|
+
// then defaults (issue #344).
|
|
50
51
|
// mode "additive" (default) — keep Pi builtins, add ctx_* alongside
|
|
51
52
|
// mode "replace" — disable Pi builtins, only expose ctx_*
|
|
52
|
-
const DISABLED_BUILTIN_TOOLS = new Set(["read", "bash", "ls", "find", "grep"]);
|
|
53
53
|
const PI_CONFIG = loadPiConfig();
|
|
54
54
|
const PI_MODE = PI_CONFIG.mode;
|
|
55
55
|
// Max bytes constant for truncation warnings (same as Pi's DEFAULT_MAX_BYTES)
|
|
@@ -61,11 +61,17 @@ const readModeSchema = Type.Union([
|
|
|
61
61
|
Type.Literal("signatures"),
|
|
62
62
|
], { description: "Override auto-selection: full (complete content), map (deps+API signatures), signatures (AST only)" });
|
|
63
63
|
|
|
64
|
+
// Kept field-compatible with the canonical MCP `ctx_read` schema (registry in
|
|
65
|
+
// rust/src/tools/registered/ctx_read.rs) so the tool looks identical across
|
|
66
|
+
// harnesses: Codex sees `start_line`/`fresh`, Pi must too (GH #432). `offset` is
|
|
67
|
+
// the historical Pi alias for `start_line`; both are accepted.
|
|
64
68
|
const readSchema = Type.Object({
|
|
65
69
|
path: Type.String({ description: "Path to the file to read (relative or absolute)" }),
|
|
66
|
-
offset: Type.Optional(Type.Number({ description: "Line number to start reading from (1-indexed)" })),
|
|
67
|
-
limit: Type.Optional(Type.Number({ description: "Maximum number of lines to read" })),
|
|
68
70
|
mode: Type.Optional(readModeSchema),
|
|
71
|
+
start_line: Type.Optional(Type.Number({ description: "1-based line to start reading from (alias: offset)" })),
|
|
72
|
+
offset: Type.Optional(Type.Number({ description: "Alias for start_line — 1-based line to start reading from" })),
|
|
73
|
+
limit: Type.Optional(Type.Number({ description: "Maximum number of lines to read" })),
|
|
74
|
+
fresh: Type.Optional(Type.Boolean({ description: "Force a fresh disk re-read, bypassing the session cache" })),
|
|
69
75
|
});
|
|
70
76
|
|
|
71
77
|
// `path` is REQUIRED on ls/find/grep (#395): with an optional path these tools
|
|
@@ -343,11 +349,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
343
349
|
// finding: 102 bash / 0 ctx_shell), so the heaviest addressable surface in
|
|
344
350
|
// a fix task — make/reproducer/test logs — never reaches the compressor.
|
|
345
351
|
// - "additive" without routeShell → suppress nothing (keep Pi builtins).
|
|
346
|
-
const suppressedBuiltins = PI_MODE
|
|
347
|
-
? DISABLED_BUILTIN_TOOLS
|
|
348
|
-
: PI_CONFIG.routeShell
|
|
349
|
-
? new Set(["bash"])
|
|
350
|
-
: new Set<string>();
|
|
352
|
+
const suppressedBuiltins = resolveSuppressedBuiltins(PI_MODE, PI_CONFIG.routeShell);
|
|
351
353
|
|
|
352
354
|
if (suppressedBuiltins.size > 0) {
|
|
353
355
|
pi.on("session_start", () => {
|
|
@@ -418,6 +420,9 @@ export default async function (pi: ExtensionAPI) {
|
|
|
418
420
|
label: "ctx_shell",
|
|
419
421
|
description:
|
|
420
422
|
"Run shell commands. Prefer over native Bash/shell (auto-compressed output). "
|
|
423
|
+
+ "Runs the system POSIX shell ($SHELL, sh/bash-compatible) non-login, non-interactive "
|
|
424
|
+
+ "and profile-free: no .bash_profile/.zshrc/rc files are sourced, so behavior is "
|
|
425
|
+
+ "deterministic regardless of user shell config. "
|
|
421
426
|
+ "IMPORTANT: Do NOT use ctx_shell to read files (cat/head/tail) — use ctx_read instead. "
|
|
422
427
|
+ "Do NOT use ctx_shell for grep/find/ls — use ctx_grep, ctx_find, ctx_ls. "
|
|
423
428
|
+ "Set raw=true to skip compression when exact output matters. "
|
|
@@ -427,10 +432,16 @@ export default async function (pi: ExtensionAPI) {
|
|
|
427
432
|
"Use ctx_shell only for commands with side effects: build, test, install, git, run scripts.",
|
|
428
433
|
],
|
|
429
434
|
parameters: bashSchemaWithRaw,
|
|
430
|
-
renderCall(args, theme
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
435
|
+
renderCall(args, theme) {
|
|
436
|
+
// Render with an explicit `ctx_shell` label instead of delegating to Pi's
|
|
437
|
+
// bash renderer, whose bare `$` prefix made the call look like a native
|
|
438
|
+
// bash shell and obscured that this is lean-ctx's profile-free shell (#451).
|
|
439
|
+
const command = typeof args.command === "string" ? args.command : "";
|
|
440
|
+
return new Text(
|
|
441
|
+
theme.fg("toolTitle", theme.bold("ctx_shell ")) + theme.fg("text", command),
|
|
442
|
+
0,
|
|
443
|
+
0,
|
|
444
|
+
);
|
|
434
445
|
},
|
|
435
446
|
renderResult(result, options, theme, context) {
|
|
436
447
|
// ctx_shell wraps Pi's bash tool; its renderer is typed for BashToolDetails,
|
|
@@ -555,17 +566,22 @@ export default async function (pi: ExtensionAPI) {
|
|
|
555
566
|
const requestedPath = normalizePathArg(params.path);
|
|
556
567
|
const absolutePath = resolve(ctx.cwd, requestedPath);
|
|
557
568
|
|
|
558
|
-
|
|
559
|
-
|
|
569
|
+
// `start_line` is the canonical name; `offset` is the historical Pi alias
|
|
570
|
+
// (GH #432). `fresh` forces a disk re-read past the session cache.
|
|
571
|
+
const startOffset = params.offset ?? params.start_line;
|
|
572
|
+
const forceFresh = params.fresh === true;
|
|
573
|
+
|
|
574
|
+
if (startOffset !== undefined || params.limit !== undefined) {
|
|
575
|
+
const startLine = startOffset ?? 1;
|
|
560
576
|
const endLine = params.limit ? startLine + params.limit - 1 : 999999;
|
|
561
577
|
const mode = `lines:${startLine}-${endLine}`;
|
|
562
578
|
// Route line-range reads through the bridge too, so re-reading the same
|
|
563
579
|
// slice hits the session cache instead of re-spawning a CLI per call (#361).
|
|
564
580
|
if (mcpBridge?.isConnected()) {
|
|
565
581
|
try {
|
|
566
|
-
const bridged = await mcpBridge.callTool("ctx_read", { path: absolutePath, mode }, signal);
|
|
582
|
+
const bridged = await mcpBridge.callTool("ctx_read", { path: absolutePath, mode, ...(forceFresh ? { fresh: true } : {}) }, signal);
|
|
567
583
|
const bridgedText = bridged.content.map((block) => block.text).join("");
|
|
568
|
-
const originalSlice = await readSlice(absolutePath,
|
|
584
|
+
const originalSlice = await readSlice(absolutePath, startOffset, params.limit);
|
|
569
585
|
const decorated = withFooter(bridgedText, { originalText: originalSlice.text, always: true, preferEstimate: true, suppressIfNoSaving: true });
|
|
570
586
|
return {
|
|
571
587
|
content: [{ type: "text", text: decorated.text }],
|
|
@@ -575,17 +591,17 @@ export default async function (pi: ExtensionAPI) {
|
|
|
575
591
|
console.error(`[pi-lean-ctx] ctx_read(${mode}) bridge call failed, falling back to CLI: ${err}`);
|
|
576
592
|
}
|
|
577
593
|
}
|
|
578
|
-
const args = ["read", absolutePath, "-m", mode];
|
|
594
|
+
const args = ["read", absolutePath, "-m", mode, ...(forceFresh ? ["--fresh"] : [])];
|
|
579
595
|
try {
|
|
580
596
|
const output = await execLeanCtx(pi, args);
|
|
581
|
-
const originalSlice = await readSlice(absolutePath,
|
|
597
|
+
const originalSlice = await readSlice(absolutePath, startOffset, params.limit);
|
|
582
598
|
const decorated = withFooter(output, { originalText: originalSlice.text, always: true, preferEstimate: true, suppressIfNoSaving: true });
|
|
583
599
|
return {
|
|
584
600
|
content: [{ type: "text", text: decorated.text }],
|
|
585
601
|
details: { path: absolutePath, lines: originalSlice.lines, source: "lean-ctx", mode, compression: decorated.stats },
|
|
586
602
|
};
|
|
587
603
|
} catch {
|
|
588
|
-
const sliced = await readSlice(absolutePath,
|
|
604
|
+
const sliced = await readSlice(absolutePath, startOffset, params.limit);
|
|
589
605
|
return {
|
|
590
606
|
content: [{ type: "text", text: sliced.text }],
|
|
591
607
|
details: { path: absolutePath, lines: sliced.lines, source: "local-slice-fallback", truncated: sliced.truncated },
|
|
@@ -598,6 +614,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
598
614
|
}
|
|
599
615
|
|
|
600
616
|
const isExplicitFull = params.mode === "full";
|
|
617
|
+
const wantsFresh = forceFresh || isExplicitFull;
|
|
601
618
|
const mode = params.mode ?? await chooseReadMode(absolutePath);
|
|
602
619
|
|
|
603
620
|
// When the embedded MCP bridge is connected, route the read through it so
|
|
@@ -611,7 +628,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
611
628
|
try {
|
|
612
629
|
const bridged = await mcpBridge.callTool(
|
|
613
630
|
"ctx_read",
|
|
614
|
-
{ path: absolutePath, mode, ...(
|
|
631
|
+
{ path: absolutePath, mode, ...(wantsFresh ? { fresh: true } : {}) },
|
|
615
632
|
signal,
|
|
616
633
|
);
|
|
617
634
|
const bridgedText = bridged.content.map((block) => block.text).join("");
|
|
@@ -626,7 +643,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
626
643
|
}
|
|
627
644
|
}
|
|
628
645
|
|
|
629
|
-
const args = ["read", absolutePath, "-m", mode, ...(
|
|
646
|
+
const args = ["read", absolutePath, "-m", mode, ...(wantsFresh ? ["--fresh"] : [])];
|
|
630
647
|
const output = await execLeanCtx(pi, args);
|
|
631
648
|
const originalText = await readFile(absolutePath, "utf8");
|
|
632
649
|
const decorated = withFooter(output, { originalText, always: true, preferEstimate: true, suppressIfNoSaving: true });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-lean-ctx",
|
|
3
|
-
"version": "3.8.
|
|
3
|
+
"version": "3.8.9",
|
|
4
4
|
"description": "Pi Coding Agent extension — routes bash/read/grep/find/ls through lean-ctx for strong token savings. The embedded MCP bridge (on by default) adds a persistent session cache so unchanged re-reads cost ~13 tokens.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|