pi-lean-ctx 3.7.3 → 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 +20 -15
- package/extensions/config.ts +11 -2
- package/extensions/index.ts +72 -15
- package/extensions/mcp-bridge.ts +5 -0
- package/package.json +2 -2
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**:
|
|
6
|
-
- **
|
|
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
|
-
###
|
|
111
|
+
### Embedded MCP bridge (session cache + advanced tools)
|
|
112
112
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
131
|
+
## Disabling the bridge (optional)
|
|
124
132
|
|
|
125
|
-
|
|
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=
|
|
137
|
+
export LEAN_CTX_PI_ENABLE_MCP=0
|
|
129
138
|
pi
|
|
130
139
|
```
|
|
131
140
|
|
|
132
|
-
|
|
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
|
|
package/extensions/config.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
|
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)) {
|
package/extensions/index.ts
CHANGED
|
@@ -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
|
-
"
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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) {
|
|
@@ -618,10 +668,14 @@ export default async function (pi: ExtensionAPI) {
|
|
|
618
668
|
|
|
619
669
|
const enableMcpBridge = PI_CONFIG.enableMcp;
|
|
620
670
|
const adapterConfigured = isMcpAdapterConfigured();
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
671
|
+
// An explicit opt-in to the embedded bridge wins over mcp.json detection (#361).
|
|
672
|
+
// A `lean-ctx` entry in ~/.pi/agent/mcp.json does NOT prove that pi-mcp-adapter
|
|
673
|
+
// is actually serving it — pi has no native MCP support, and `lean-ctx init
|
|
674
|
+
// --agent pi` writes that entry by default — so it must not silently disable the
|
|
675
|
+
// bridge a user explicitly requested via LEAN_CTX_PI_ENABLE_MCP=1 / enableMcp.
|
|
676
|
+
mcpBridge = enableMcpBridge
|
|
677
|
+
? new McpBridge(resolveBinary(), PI_CONFIG.forwardedEnv)
|
|
678
|
+
: null;
|
|
625
679
|
|
|
626
680
|
if (mcpBridge) {
|
|
627
681
|
try {
|
|
@@ -644,9 +698,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
644
698
|
lines.push(`Config: ${PI_CONFIG.configPath}`);
|
|
645
699
|
}
|
|
646
700
|
lines.push(`Mode: ${PI_MODE}`);
|
|
647
|
-
if (
|
|
648
|
-
lines.push("MCP bridge: adapter-configured (extension bridge disabled)");
|
|
649
|
-
} else if (!enableMcpBridge) {
|
|
701
|
+
if (!enableMcpBridge) {
|
|
650
702
|
lines.push("MCP bridge: disabled (CLI-first)");
|
|
651
703
|
lines.push(' Enable: LEAN_CTX_PI_ENABLE_MCP=1 or "enableMcp": true in config.json, then restart Pi');
|
|
652
704
|
} else if (status) {
|
|
@@ -656,6 +708,11 @@ export default async function (pi: ExtensionAPI) {
|
|
|
656
708
|
if (status.toolNames.length > 0) {
|
|
657
709
|
lines.push(` ${status.toolNames.join(", ")}`);
|
|
658
710
|
}
|
|
711
|
+
if (adapterConfigured) {
|
|
712
|
+
lines.push(
|
|
713
|
+
" Note: ~/.pi/agent/mcp.json also has a lean-ctx entry. The embedded bridge is serving tools; if you additionally run pi-mcp-adapter you may see duplicates.",
|
|
714
|
+
);
|
|
715
|
+
}
|
|
659
716
|
if (status.lastHungTool) {
|
|
660
717
|
lines.push(`Last hung tool: ${status.lastHungTool}`);
|
|
661
718
|
}
|
package/extensions/mcp-bridge.ts
CHANGED
|
@@ -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
|
-
"description": "Pi Coding Agent extension
|
|
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",
|