pi-lean-ctx 3.8.6 → 3.8.8

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.
@@ -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 [];
@@ -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 that can be replaced with ctx_ prefixed versions.
48
- // Settings resolve from (most explicit first): LEAN_CTX_PI_* env vars, then
49
- // ~/.pi/agent/extensions/pi-lean-ctx/config.json, then defaults (issue #344).
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 === "replace"
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", () => {
@@ -555,17 +557,22 @@ export default async function (pi: ExtensionAPI) {
555
557
  const requestedPath = normalizePathArg(params.path);
556
558
  const absolutePath = resolve(ctx.cwd, requestedPath);
557
559
 
558
- if (params.offset !== undefined || params.limit !== undefined) {
559
- const startLine = params.offset ?? 1;
560
+ // `start_line` is the canonical name; `offset` is the historical Pi alias
561
+ // (GH #432). `fresh` forces a disk re-read past the session cache.
562
+ const startOffset = params.offset ?? params.start_line;
563
+ const forceFresh = params.fresh === true;
564
+
565
+ if (startOffset !== undefined || params.limit !== undefined) {
566
+ const startLine = startOffset ?? 1;
560
567
  const endLine = params.limit ? startLine + params.limit - 1 : 999999;
561
568
  const mode = `lines:${startLine}-${endLine}`;
562
569
  // Route line-range reads through the bridge too, so re-reading the same
563
570
  // slice hits the session cache instead of re-spawning a CLI per call (#361).
564
571
  if (mcpBridge?.isConnected()) {
565
572
  try {
566
- const bridged = await mcpBridge.callTool("ctx_read", { path: absolutePath, mode }, signal);
573
+ const bridged = await mcpBridge.callTool("ctx_read", { path: absolutePath, mode, ...(forceFresh ? { fresh: true } : {}) }, signal);
567
574
  const bridgedText = bridged.content.map((block) => block.text).join("");
568
- const originalSlice = await readSlice(absolutePath, params.offset, params.limit);
575
+ const originalSlice = await readSlice(absolutePath, startOffset, params.limit);
569
576
  const decorated = withFooter(bridgedText, { originalText: originalSlice.text, always: true, preferEstimate: true, suppressIfNoSaving: true });
570
577
  return {
571
578
  content: [{ type: "text", text: decorated.text }],
@@ -575,17 +582,17 @@ export default async function (pi: ExtensionAPI) {
575
582
  console.error(`[pi-lean-ctx] ctx_read(${mode}) bridge call failed, falling back to CLI: ${err}`);
576
583
  }
577
584
  }
578
- const args = ["read", absolutePath, "-m", mode];
585
+ const args = ["read", absolutePath, "-m", mode, ...(forceFresh ? ["--fresh"] : [])];
579
586
  try {
580
587
  const output = await execLeanCtx(pi, args);
581
- const originalSlice = await readSlice(absolutePath, params.offset, params.limit);
588
+ const originalSlice = await readSlice(absolutePath, startOffset, params.limit);
582
589
  const decorated = withFooter(output, { originalText: originalSlice.text, always: true, preferEstimate: true, suppressIfNoSaving: true });
583
590
  return {
584
591
  content: [{ type: "text", text: decorated.text }],
585
592
  details: { path: absolutePath, lines: originalSlice.lines, source: "lean-ctx", mode, compression: decorated.stats },
586
593
  };
587
594
  } catch {
588
- const sliced = await readSlice(absolutePath, params.offset, params.limit);
595
+ const sliced = await readSlice(absolutePath, startOffset, params.limit);
589
596
  return {
590
597
  content: [{ type: "text", text: sliced.text }],
591
598
  details: { path: absolutePath, lines: sliced.lines, source: "local-slice-fallback", truncated: sliced.truncated },
@@ -598,6 +605,7 @@ export default async function (pi: ExtensionAPI) {
598
605
  }
599
606
 
600
607
  const isExplicitFull = params.mode === "full";
608
+ const wantsFresh = forceFresh || isExplicitFull;
601
609
  const mode = params.mode ?? await chooseReadMode(absolutePath);
602
610
 
603
611
  // When the embedded MCP bridge is connected, route the read through it so
@@ -611,7 +619,7 @@ export default async function (pi: ExtensionAPI) {
611
619
  try {
612
620
  const bridged = await mcpBridge.callTool(
613
621
  "ctx_read",
614
- { path: absolutePath, mode, ...(isExplicitFull ? { fresh: true } : {}) },
622
+ { path: absolutePath, mode, ...(wantsFresh ? { fresh: true } : {}) },
615
623
  signal,
616
624
  );
617
625
  const bridgedText = bridged.content.map((block) => block.text).join("");
@@ -626,7 +634,7 @@ export default async function (pi: ExtensionAPI) {
626
634
  }
627
635
  }
628
636
 
629
- const args = ["read", absolutePath, "-m", mode, ...(isExplicitFull ? ["--fresh"] : [])];
637
+ const args = ["read", absolutePath, "-m", mode, ...(wantsFresh ? ["--fresh"] : [])];
630
638
  const output = await execLeanCtx(pi, args);
631
639
  const originalText = await readFile(absolutePath, "utf8");
632
640
  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.6",
3
+ "version": "3.8.8",
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",