pi-lean-ctx 3.7.4 → 3.7.5

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
@@ -2,8 +2,8 @@
2
2
 
3
3
  [Pi Coding Agent](https://github.com/badlogic/pi-mono) extension that provides `ctx_`-prefixed tools backed by [lean-ctx](https://leanctx.com) for **60–90% token savings**.
4
4
 
5
- - **Default**: CLI-only, additive mode (no MCP required, Pi builtins preserved)
6
- - **Optional**: enable MCP tools (`LEAN_CTX_PI_ENABLE_MCP=1`) or run `lean-ctx init --agent pi --mode mcp`
5
+ - **Default**: embedded MCP bridge ON (persistent session cache → unchanged re-reads cost ~13 tokens), additive mode (Pi builtins preserved)
6
+ - **Opt out**: `LEAN_CTX_PI_ENABLE_MCP=0` (or `"enableMcp": false`) forces the one-shot CLI path, which cannot cache across calls
7
7
  - **Optional**: replace mode (`LEAN_CTX_PI_MODE=replace`) disables Pi builtins
8
8
 
9
9
  ## Tool Mode
@@ -108,32 +108,37 @@ These tools invoke the `lean-ctx` binary via CLI with `LEAN_CTX_COMPRESS=1`.
108
108
  The built-in tools they replace (`read`, `bash`, `ls`, `find`, `grep`) are disabled
109
109
  via `pi.setActiveTools()` so only the `ctx_` versions are available to the LLM.
110
110
 
111
- ### Optional MCP bridge (all other tools)
111
+ ### Embedded MCP bridge (session cache + advanced tools)
112
112
 
113
- If you enable the MCP bridge, pi-lean-ctx spawns the `lean-ctx` binary as an MCP server (JSON-RPC over stdio).
114
- It discovers available tools via `list_tools`, filters out those already covered by `ctx_` CLI tools,
115
- and registers the rest as native Pi tools.
113
+ On by default, pi-lean-ctx spawns the `lean-ctx` binary as an MCP server (JSON-RPC over stdio).
114
+ This persistent process holds the **session cache**: `ctx_read` (every mode, including line
115
+ ranges) is routed through the bridge, so an unchanged re-read costs ~13 tokens instead of the
116
+ full file and the read registers as a real CEP session (counted by `lean-ctx gain`). The bridge
117
+ also discovers the server's advanced tools (`ctx_edit`, `ctx_overview`, `ctx_graph`, …),
118
+ filters out those already exposed as `ctx_` CLI tools, and registers the rest as native Pi tools.
116
119
 
117
- If `lean-ctx` is already configured as an MCP server via [pi-mcp-adapter](https://github.com/nicobailon/pi-mcp-adapter) in `~/.pi/agent/mcp.json`, the embedded bridge is skipped to avoid duplicate tools.
120
+ The bridge wins over `~/.pi/agent/mcp.json`: a `lean-ctx` entry there (written by
121
+ `lean-ctx init --agent pi`) does **not** disable the embedded bridge, because Pi has no native
122
+ MCP support and that entry only does anything if you separately run
123
+ [pi-mcp-adapter](https://github.com/nicobailon/pi-mcp-adapter). `/lean-ctx` warns about possible
124
+ duplicates only when the adapter is genuinely running. If the bridge can't start, the CLI path
125
+ keeps working — only the cache and advanced tools are unavailable.
118
126
 
119
127
  ### Automatic reconnection
120
128
 
121
129
  If the MCP server process crashes, the bridge automatically reconnects (up to 3 attempts with exponential backoff). If reconnection fails, CLI-based tools continue working normally — only the advanced MCP tools become unavailable.
122
130
 
123
- ## Enabling MCP (optional)
131
+ ## Disabling the bridge (optional)
124
132
 
125
- Set an environment variable and restart Pi:
133
+ The bridge is on by default. To force the one-shot CLI path (no cross-call cache),
134
+ set an environment variable and restart Pi:
126
135
 
127
136
  ```bash
128
- export LEAN_CTX_PI_ENABLE_MCP=1
137
+ export LEAN_CTX_PI_ENABLE_MCP=0
129
138
  pi
130
139
  ```
131
140
 
132
- Or configure MCP via `lean-ctx init`:
133
-
134
- ```bash
135
- lean-ctx init --agent pi --mode mcp
136
- ```
141
+ …or set `"enableMcp": false` in `~/.pi/agent/extensions/pi-lean-ctx/config.json`.
137
142
 
138
143
  ## pi-mcp-adapter compatibility
139
144
 
@@ -15,7 +15,11 @@ import { resolve } from "node:path";
15
15
  export interface PiLeanCtxFileConfig {
16
16
  /** Tool exposure: "additive" (Pi builtins + ctx_*) or "replace" (ctx_* only). */
17
17
  mode?: string;
18
- /** Start the embedded MCP bridge (equivalent to `LEAN_CTX_PI_ENABLE_MCP=1`). */
18
+ /**
19
+ * Start the embedded MCP bridge (the persistent session cache). Default
20
+ * `true`; set `false` (or `LEAN_CTX_PI_ENABLE_MCP=0`) to force the one-shot
21
+ * CLI path, which cannot cache across calls.
22
+ */
19
23
  enableMcp?: boolean;
20
24
  /** Absolute path to the lean-ctx binary (equivalent to `LEAN_CTX_BIN`). */
21
25
  binary?: string;
@@ -94,10 +98,15 @@ export function loadPiConfig(): ResolvedPiConfig {
94
98
  const configPath = piConfigPath();
95
99
  const { cfg, loaded } = readFileConfig(configPath);
96
100
 
101
+ // The embedded MCP bridge holds the persistent session cache, so unchanged
102
+ // re-reads cost ~13 tokens and reads register as CEP sessions. That is
103
+ // lean-ctx's core value prop, so the bridge is ON by default; the one-shot CLI
104
+ // path cannot cache across calls (#361). Opt out with LEAN_CTX_PI_ENABLE_MCP=0
105
+ // or "enableMcp": false in config.json.
97
106
  const enableMcp =
98
107
  process.env.LEAN_CTX_PI_ENABLE_MCP !== undefined
99
108
  ? envFlag("LEAN_CTX_PI_ENABLE_MCP")
100
- : cfg.enableMcp === true;
109
+ : cfg.enableMcp !== false;
101
110
 
102
111
  const forwardedEnv: Record<string, string> = {};
103
112
  if (cfg.env && typeof cfg.env === "object" && !Array.isArray(cfg.env)) {
@@ -327,6 +327,11 @@ export default async function (pi: ExtensionAPI) {
327
327
  });
328
328
  }
329
329
 
330
+ // Declared up-front so the ctx_read handler (registered below) can route
331
+ // through the embedded bridge once it connects. Assigned after the tools are
332
+ // registered (the bridge is started at the end of this function).
333
+ let mcpBridge: McpBridge | null = null;
334
+
330
335
  const baseBashTool = createBashToolDefinition(process.cwd(), {
331
336
  spawnHook: ({ command, cwd, env }) => {
332
337
  const bin = resolveBinary();
@@ -351,7 +356,7 @@ export default async function (pi: ExtensionAPI) {
351
356
  name: "ctx_shell",
352
357
  label: "ctx_shell",
353
358
  description:
354
- "Execute a shell command. Output is auto-compressed by lean-ctx. "
359
+ "Run shell commands. Prefer over native Bash/shell (auto-compressed output). "
355
360
  + "IMPORTANT: Do NOT use ctx_shell to read files (cat/head/tail) — use ctx_read instead. "
356
361
  + "Do NOT use ctx_shell for grep/find/ls — use ctx_grep, ctx_find, ctx_ls. "
357
362
  + "Set raw=true to skip compression when exact output matters. "
@@ -412,10 +417,11 @@ export default async function (pi: ExtensionAPI) {
412
417
  name: "ctx_read",
413
418
  label: "ctx_read",
414
419
  description:
415
- "Read file contents. ALWAYS use ctx_read instead of cat/head/tail via ctx_shell. "
420
+ "Read a file. Prefer over native Read/cat/head/tail (cached, compressed). "
421
+ + "Unchanged re-reads cost ~13 tokens. "
416
422
  + "Auto-selects mode: configs (.yaml/.json/.toml/.env) are always full-read. "
417
423
  + "Code files: full (<8KB), map (8-96KB), signatures (>96KB). "
418
- + "Add mode=full to get complete file content (bypasses cache). "
424
+ + "Add mode=full to get complete file content. "
419
425
  + "Use offset and limit to read specific line ranges.",
420
426
  promptSnippet: "Read file contents (always use instead of cat)",
421
427
  promptGuidelines: [
@@ -491,14 +497,31 @@ export default async function (pi: ExtensionAPI) {
491
497
  if (params.offset !== undefined || params.limit !== undefined) {
492
498
  const startLine = params.offset ?? 1;
493
499
  const endLine = params.limit ? startLine + params.limit - 1 : 999999;
494
- const args = ["read", absolutePath, "-m", `lines:${startLine}-${endLine}`];
500
+ const mode = `lines:${startLine}-${endLine}`;
501
+ // Route line-range reads through the bridge too, so re-reading the same
502
+ // slice hits the session cache instead of re-spawning a CLI per call (#361).
503
+ if (mcpBridge?.isConnected()) {
504
+ try {
505
+ const bridged = await mcpBridge.callTool("ctx_read", { path: absolutePath, mode }, signal);
506
+ const bridgedText = bridged.content.map((block) => block.text).join("");
507
+ const originalSlice = await readSlice(absolutePath, params.offset, params.limit);
508
+ const decorated = withFooter(bridgedText, { originalText: originalSlice.text, always: true, preferEstimate: true });
509
+ return {
510
+ content: [{ type: "text", text: decorated.text }],
511
+ details: { path: absolutePath, lines: originalSlice.lines, source: "lean-ctx-bridge", mode, compression: decorated.stats },
512
+ };
513
+ } catch (err) {
514
+ console.error(`[pi-lean-ctx] ctx_read(${mode}) bridge call failed, falling back to CLI: ${err}`);
515
+ }
516
+ }
517
+ const args = ["read", absolutePath, "-m", mode];
495
518
  try {
496
519
  const output = await execLeanCtx(pi, args);
497
520
  const originalSlice = await readSlice(absolutePath, params.offset, params.limit);
498
521
  const decorated = withFooter(output, { originalText: originalSlice.text, always: true, preferEstimate: true });
499
522
  return {
500
523
  content: [{ type: "text", text: decorated.text }],
501
- details: { path: absolutePath, lines: originalSlice.lines, source: "lean-ctx", mode: `lines:${startLine}-${endLine}`, compression: decorated.stats },
524
+ details: { path: absolutePath, lines: originalSlice.lines, source: "lean-ctx", mode, compression: decorated.stats },
502
525
  };
503
526
  } catch {
504
527
  const sliced = await readSlice(absolutePath, params.offset, params.limit);
@@ -515,6 +538,33 @@ export default async function (pi: ExtensionAPI) {
515
538
 
516
539
  const isExplicitFull = params.mode === "full";
517
540
  const mode = params.mode ?? await chooseReadMode(absolutePath);
541
+
542
+ // When the embedded MCP bridge is connected, route the read through it so
543
+ // the persistent session cache engages: an unchanged re-read then costs
544
+ // ~13 tokens instead of the full file, and the read registers as a real
545
+ // CEP session (counted by `lean-ctx gain`). The one-shot CLI path below
546
+ // spawns a fresh `lean-ctx read` per call and therefore cannot cache
547
+ // across calls — it is used only as a fallback when the bridge is
548
+ // unavailable or errors.
549
+ if (mcpBridge?.isConnected()) {
550
+ try {
551
+ const bridged = await mcpBridge.callTool(
552
+ "ctx_read",
553
+ { path: absolutePath, mode, ...(isExplicitFull ? { fresh: true } : {}) },
554
+ signal,
555
+ );
556
+ const bridgedText = bridged.content.map((block) => block.text).join("");
557
+ const originalText = await readFile(absolutePath, "utf8");
558
+ const decorated = withFooter(bridgedText, { originalText, always: true, preferEstimate: true });
559
+ return {
560
+ content: [{ type: "text", text: decorated.text }],
561
+ details: { path: absolutePath, source: "lean-ctx-bridge", mode, compression: decorated.stats },
562
+ };
563
+ } catch (err) {
564
+ console.error(`[pi-lean-ctx] ctx_read bridge call failed, falling back to CLI: ${err}`);
565
+ }
566
+ }
567
+
518
568
  const args = ["read", absolutePath, "-m", mode, ...(isExplicitFull ? ["--fresh"] : [])];
519
569
  const output = await execLeanCtx(pi, args);
520
570
  const originalText = await readFile(absolutePath, "utf8");
@@ -531,7 +581,7 @@ export default async function (pi: ExtensionAPI) {
531
581
  pi.registerTool({
532
582
  name: "ctx_ls",
533
583
  label: "ctx_ls",
534
- description: "List directory contents. Use limit to reduce output size.",
584
+ description: "List a directory. Prefer over native ls (compact, summarized). Use limit to reduce output size.",
535
585
  promptSnippet: "List directory contents",
536
586
  parameters: lsSchema,
537
587
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
@@ -550,7 +600,7 @@ export default async function (pi: ExtensionAPI) {
550
600
  pi.registerTool({
551
601
  name: "ctx_find",
552
602
  label: "ctx_find",
553
- description: "Find files by glob pattern (respects .gitignore). Use limit to reduce output size.",
603
+ description: "Find files by glob. Prefer over native find/fd (gitignore-aware). Use limit to reduce output size.",
554
604
  promptSnippet: "Find files by glob pattern",
555
605
  parameters: findSchema,
556
606
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
@@ -569,7 +619,7 @@ export default async function (pi: ExtensionAPI) {
569
619
  pi.registerTool({
570
620
  name: "ctx_grep",
571
621
  label: "ctx_grep",
572
- description: "Search file contents with ripgrep. Use limit to cap matches and context for surrounding lines.",
622
+ description: "Search code. Prefer over native Grep/ripgrep (compact, ranked). Use limit to cap matches, context for surrounding lines.",
573
623
  promptSnippet: "Search file contents for patterns",
574
624
  parameters: grepSchema,
575
625
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
@@ -623,7 +673,7 @@ export default async function (pi: ExtensionAPI) {
623
673
  // is actually serving it — pi has no native MCP support, and `lean-ctx init
624
674
  // --agent pi` writes that entry by default — so it must not silently disable the
625
675
  // bridge a user explicitly requested via LEAN_CTX_PI_ENABLE_MCP=1 / enableMcp.
626
- const mcpBridge = enableMcpBridge
676
+ mcpBridge = enableMcpBridge
627
677
  ? new McpBridge(resolveBinary(), PI_CONFIG.forwardedEnv)
628
678
  : null;
629
679
 
@@ -341,6 +341,11 @@ export class McpBridge {
341
341
  return Type.Object(fields);
342
342
  }
343
343
 
344
+ /** True when the MCP client is connected and able to serve tool calls. */
345
+ isConnected(): boolean {
346
+ return this.connected && this.client !== null;
347
+ }
348
+
344
349
  getStatus(): McpBridgeStatus {
345
350
  return {
346
351
  mode: "embedded",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-lean-ctx",
3
- "version": "3.7.4",
4
- "description": "Pi Coding Agent extension (CLI-first) \u2014 routes bash/read/grep/find/ls through lean-ctx CLI for strong token savings. Optional MCP bridge can register advanced tools.",
3
+ "version": "3.7.5",
4
+ "description": "Pi Coding Agent extension \u2014 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",
7
7
  "lean-ctx",