pi-lean-ctx 3.7.2 → 3.7.4
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 +24 -4
- package/extensions/config.ts +120 -0
- package/extensions/index.ts +79 -23
- package/extensions/mcp-bridge.ts +20 -12
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -16,6 +16,25 @@ To switch to **replace mode** (disables Pi builtins, only `ctx_*` tools availabl
|
|
|
16
16
|
export LEAN_CTX_PI_MODE=replace
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
+
## Config file
|
|
20
|
+
|
|
21
|
+
If you only use lean-ctx through Pi, keep every setting in one file instead of
|
|
22
|
+
env vars — `~/.pi/agent/extensions/pi-lean-ctx/config.json`:
|
|
23
|
+
|
|
24
|
+
```json
|
|
25
|
+
{
|
|
26
|
+
"mode": "replace",
|
|
27
|
+
"enableMcp": true,
|
|
28
|
+
"binary": "/opt/lean-ctx/bin/lean-ctx",
|
|
29
|
+
"env": { "LEAN_CTX_COMPRESSION": "aggressive" }
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`mode` → `LEAN_CTX_PI_MODE`, `enableMcp` → `LEAN_CTX_PI_ENABLE_MCP`,
|
|
34
|
+
`binary` → `LEAN_CTX_BIN`. The `env` map is forwarded to every `lean-ctx`
|
|
35
|
+
subprocess, so it can override `~/.lean-ctx/config.toml` engine settings.
|
|
36
|
+
Explicit env vars still win over the file; the file wins over defaults.
|
|
37
|
+
|
|
19
38
|
## What it does
|
|
20
39
|
|
|
21
40
|
### ctx_ Tools (CLI-backed)
|
|
@@ -146,10 +165,11 @@ When pi-mcp-adapter manages the lean-ctx MCP server, pi-lean-ctx detects this an
|
|
|
146
165
|
The extension locates the `lean-ctx` binary in this order:
|
|
147
166
|
|
|
148
167
|
1. `LEAN_CTX_BIN` environment variable
|
|
149
|
-
2. `~/.
|
|
150
|
-
3. `~/.
|
|
151
|
-
4.
|
|
152
|
-
5.
|
|
168
|
+
2. `binary` in `~/.pi/agent/extensions/pi-lean-ctx/config.json`
|
|
169
|
+
3. `~/.cargo/bin/lean-ctx`
|
|
170
|
+
4. `~/.local/bin/lean-ctx` (Linux) or `%APPDATA%\Local\lean-ctx\lean-ctx.exe` (Windows)
|
|
171
|
+
5. `/usr/local/bin/lean-ctx` (macOS/Linux)
|
|
172
|
+
6. `lean-ctx` on PATH
|
|
153
173
|
|
|
154
174
|
## Smart Read Modes
|
|
155
175
|
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Shape of the optional Pi override file
|
|
7
|
+
* `~/.pi/agent/extensions/pi-lean-ctx/config.json`.
|
|
8
|
+
*
|
|
9
|
+
* It lets users who only run lean-ctx through Pi keep every setting inside
|
|
10
|
+
* their Pi configuration instead of juggling `LEAN_CTX_PI_*` environment
|
|
11
|
+
* variables and `~/.lean-ctx/config.toml` (see issue #344). All fields are
|
|
12
|
+
* optional; an absent or malformed file simply falls back to env vars and
|
|
13
|
+
* built-in defaults.
|
|
14
|
+
*/
|
|
15
|
+
export interface PiLeanCtxFileConfig {
|
|
16
|
+
/** Tool exposure: "additive" (Pi builtins + ctx_*) or "replace" (ctx_* only). */
|
|
17
|
+
mode?: string;
|
|
18
|
+
/** Start the embedded MCP bridge (equivalent to `LEAN_CTX_PI_ENABLE_MCP=1`). */
|
|
19
|
+
enableMcp?: boolean;
|
|
20
|
+
/** Absolute path to the lean-ctx binary (equivalent to `LEAN_CTX_BIN`). */
|
|
21
|
+
binary?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Extra environment forwarded to every lean-ctx subprocess. Use this to
|
|
24
|
+
* override `~/.lean-ctx/config.toml` engine settings without touching that
|
|
25
|
+
* file, since the engine honours `LEAN_CTX_*` env vars
|
|
26
|
+
* (e.g. `{ "LEAN_CTX_COMPRESSION": "aggressive" }`).
|
|
27
|
+
*/
|
|
28
|
+
env?: Record<string, string>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type PiMode = "additive" | "replace";
|
|
32
|
+
|
|
33
|
+
/** Fully resolved configuration after merging file, env vars and defaults. */
|
|
34
|
+
export interface ResolvedPiConfig {
|
|
35
|
+
mode: PiMode;
|
|
36
|
+
enableMcp: boolean;
|
|
37
|
+
/** Binary path from the file; `LEAN_CTX_BIN` still takes precedence at use time. */
|
|
38
|
+
binaryOverride?: string;
|
|
39
|
+
/** Engine env overrides forwarded to lean-ctx subprocesses. */
|
|
40
|
+
forwardedEnv: Record<string, string>;
|
|
41
|
+
/** Absolute path the loader looked at (whether or not it existed). */
|
|
42
|
+
configPath: string;
|
|
43
|
+
/** True when the file existed and parsed into a JSON object. */
|
|
44
|
+
loaded: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Absolute path to the Pi override file (Pi's per-extension config convention). */
|
|
48
|
+
export function piConfigPath(): string {
|
|
49
|
+
return resolve(
|
|
50
|
+
homedir(),
|
|
51
|
+
".pi",
|
|
52
|
+
"agent",
|
|
53
|
+
"extensions",
|
|
54
|
+
"pi-lean-ctx",
|
|
55
|
+
"config.json",
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function envFlag(name: string): boolean {
|
|
60
|
+
const raw = process.env[name];
|
|
61
|
+
if (!raw) return false;
|
|
62
|
+
const v = raw.trim().toLowerCase();
|
|
63
|
+
return v === "1" || v === "true" || v === "yes" || v === "on";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function readFileConfig(path: string): { cfg: PiLeanCtxFileConfig; loaded: boolean } {
|
|
67
|
+
if (!existsSync(path)) return { cfg: {}, loaded: false };
|
|
68
|
+
try {
|
|
69
|
+
const parsed: unknown = JSON.parse(readFileSync(path, "utf8"));
|
|
70
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
71
|
+
return { cfg: parsed as PiLeanCtxFileConfig, loaded: true };
|
|
72
|
+
}
|
|
73
|
+
console.error(`[pi-lean-ctx] ${path}: expected a JSON object — ignoring.`);
|
|
74
|
+
} catch (err) {
|
|
75
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
76
|
+
console.error(`[pi-lean-ctx] ${path}: invalid JSON (${msg}) — ignoring.`);
|
|
77
|
+
}
|
|
78
|
+
return { cfg: {}, loaded: false };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function resolveMode(fileMode: string | undefined): PiMode {
|
|
82
|
+
const raw = (process.env.LEAN_CTX_PI_MODE ?? fileMode ?? "additive").toLowerCase();
|
|
83
|
+
return raw === "replace" ? "replace" : "additive";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Loads and resolves the Pi override config. Precedence per setting is
|
|
88
|
+
* "most explicit wins": an explicit `LEAN_CTX_PI_*` / `LEAN_CTX_BIN` env var
|
|
89
|
+
* overrides `config.json`, which overrides the built-in default. This keeps
|
|
90
|
+
* shareable, file-only setups working (no env vars needed) while still
|
|
91
|
+
* allowing ad-hoc env overrides on a single machine.
|
|
92
|
+
*/
|
|
93
|
+
export function loadPiConfig(): ResolvedPiConfig {
|
|
94
|
+
const configPath = piConfigPath();
|
|
95
|
+
const { cfg, loaded } = readFileConfig(configPath);
|
|
96
|
+
|
|
97
|
+
const enableMcp =
|
|
98
|
+
process.env.LEAN_CTX_PI_ENABLE_MCP !== undefined
|
|
99
|
+
? envFlag("LEAN_CTX_PI_ENABLE_MCP")
|
|
100
|
+
: cfg.enableMcp === true;
|
|
101
|
+
|
|
102
|
+
const forwardedEnv: Record<string, string> = {};
|
|
103
|
+
if (cfg.env && typeof cfg.env === "object" && !Array.isArray(cfg.env)) {
|
|
104
|
+
for (const [key, value] of Object.entries(cfg.env)) {
|
|
105
|
+
if (typeof value === "string") forwardedEnv[key] = value;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const binaryOverride =
|
|
110
|
+
typeof cfg.binary === "string" && cfg.binary.length > 0 ? cfg.binary : undefined;
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
mode: resolveMode(cfg.mode),
|
|
114
|
+
enableMcp,
|
|
115
|
+
binaryOverride,
|
|
116
|
+
forwardedEnv,
|
|
117
|
+
configPath,
|
|
118
|
+
loaded,
|
|
119
|
+
};
|
|
120
|
+
}
|
package/extensions/index.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
AgentToolResult,
|
|
3
|
+
BashToolDetails,
|
|
4
|
+
ExtensionAPI,
|
|
5
|
+
ReadToolDetails,
|
|
6
|
+
} from "@earendil-works/pi-coding-agent";
|
|
2
7
|
import {
|
|
3
8
|
createBashToolDefinition,
|
|
4
9
|
createReadToolDefinition,
|
|
@@ -8,12 +13,13 @@ import {
|
|
|
8
13
|
truncateHead,
|
|
9
14
|
} from "@earendil-works/pi-coding-agent";
|
|
10
15
|
import { Text } from "@earendil-works/pi-tui";
|
|
11
|
-
import { Type } from "
|
|
16
|
+
import { Type } from "typebox";
|
|
12
17
|
import { existsSync, readFileSync } from "node:fs";
|
|
13
18
|
import { readFile, stat } from "node:fs/promises";
|
|
14
19
|
import { extname, resolve } from "node:path";
|
|
15
20
|
import { homedir, platform } from "node:os";
|
|
16
21
|
import { McpBridge } from "./mcp-bridge.js";
|
|
22
|
+
import { loadPiConfig } from "./config.js";
|
|
17
23
|
import type { CompressionStats } from "./types.js";
|
|
18
24
|
|
|
19
25
|
const CODE_EXTENSIONS = new Set([
|
|
@@ -36,11 +42,13 @@ const CODE_FULL_READ_MAX_BYTES = 8 * 1024;
|
|
|
36
42
|
const CODE_SIGNATURES_MIN_BYTES = 96 * 1024;
|
|
37
43
|
|
|
38
44
|
// Pi builtins that can be replaced with ctx_ prefixed versions.
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
// "
|
|
45
|
+
// Settings resolve from (most explicit first): LEAN_CTX_PI_* env vars, then
|
|
46
|
+
// ~/.pi/agent/extensions/pi-lean-ctx/config.json, then defaults (issue #344).
|
|
47
|
+
// mode "additive" (default) — keep Pi builtins, add ctx_* alongside
|
|
48
|
+
// mode "replace" — disable Pi builtins, only expose ctx_*
|
|
42
49
|
const DISABLED_BUILTIN_TOOLS = new Set(["read", "bash", "ls", "find", "grep"]);
|
|
43
|
-
const
|
|
50
|
+
const PI_CONFIG = loadPiConfig();
|
|
51
|
+
const PI_MODE = PI_CONFIG.mode;
|
|
44
52
|
// Max bytes constant for truncation warnings (same as Pi's DEFAULT_MAX_BYTES)
|
|
45
53
|
const DEFAULT_MAX_BYTES = 8192;
|
|
46
54
|
|
|
@@ -93,16 +101,23 @@ function shellQuote(value: string): string {
|
|
|
93
101
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
94
102
|
}
|
|
95
103
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
104
|
+
// Environment for every lean-ctx subprocess: config.json `env` overrides
|
|
105
|
+
// (lowest precedence) < the caller's env < the flags lean-ctx must always see.
|
|
106
|
+
function leanCtxEnv(base: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv {
|
|
107
|
+
return {
|
|
108
|
+
...PI_CONFIG.forwardedEnv,
|
|
109
|
+
...base,
|
|
110
|
+
LEAN_CTX_COMPRESS: "1",
|
|
111
|
+
LEAN_CTX_SAVINGS_FOOTER: "always",
|
|
112
|
+
};
|
|
101
113
|
}
|
|
102
114
|
|
|
103
115
|
function resolveBinary(): string {
|
|
104
116
|
const envBin = process.env.LEAN_CTX_BIN;
|
|
105
117
|
if (envBin && existsSync(envBin)) return envBin;
|
|
118
|
+
if (PI_CONFIG.binaryOverride && existsSync(PI_CONFIG.binaryOverride)) {
|
|
119
|
+
return PI_CONFIG.binaryOverride;
|
|
120
|
+
}
|
|
106
121
|
|
|
107
122
|
const home = homedir();
|
|
108
123
|
const isWin = platform() === "win32";
|
|
@@ -282,7 +297,7 @@ function isMcpAdapterConfigured(): boolean {
|
|
|
282
297
|
|
|
283
298
|
async function execLeanCtx(pi: ExtensionAPI, args: string[]) {
|
|
284
299
|
const bin = resolveBinary();
|
|
285
|
-
const result = await pi.exec(bin, args
|
|
300
|
+
const result = await pi.exec(bin, args);
|
|
286
301
|
if (result.code !== 0) {
|
|
287
302
|
const msg = (result.stderr || result.stdout || `lean-ctx failed: ${args.join(" ")}`).trim();
|
|
288
303
|
throw new Error(msg);
|
|
@@ -291,6 +306,17 @@ async function execLeanCtx(pi: ExtensionAPI, args: string[]) {
|
|
|
291
306
|
}
|
|
292
307
|
|
|
293
308
|
export default async function (pi: ExtensionAPI) {
|
|
309
|
+
// pi.exec()'s ExecOptions carries no `env`, so lean-ctx subprocesses inherit
|
|
310
|
+
// THIS process's environment. Seed it once with the config.json `env` overrides
|
|
311
|
+
// (issue #344) plus the flags lean-ctx must always see, so every path — pi.exec,
|
|
312
|
+
// the bash spawnHook, and the MCP bridge — shares one environment. An explicitly
|
|
313
|
+
// set environment variable always wins over the config file.
|
|
314
|
+
for (const [key, value] of Object.entries(PI_CONFIG.forwardedEnv)) {
|
|
315
|
+
if (process.env[key] === undefined) process.env[key] = value;
|
|
316
|
+
}
|
|
317
|
+
process.env.LEAN_CTX_COMPRESS = "1";
|
|
318
|
+
process.env.LEAN_CTX_SAVINGS_FOOTER ??= "always";
|
|
319
|
+
|
|
294
320
|
// Defer setActiveTools to session_start — runtime actions aren't available during extension load
|
|
295
321
|
// In "replace" mode, disable Pi builtins and only expose ctx_* tools.
|
|
296
322
|
// In "additive" mode (default), keep Pi builtins alongside ctx_* tools.
|
|
@@ -307,7 +333,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
307
333
|
return {
|
|
308
334
|
command: `${shellQuote(bin)} -c ${shellQuote(command)}`,
|
|
309
335
|
cwd,
|
|
310
|
-
env:
|
|
336
|
+
env: leanCtxEnv(env),
|
|
311
337
|
};
|
|
312
338
|
},
|
|
313
339
|
});
|
|
@@ -341,8 +367,15 @@ export default async function (pi: ExtensionAPI) {
|
|
|
341
367
|
: (context.lastComponent ?? new Text("", 0, 0));
|
|
342
368
|
},
|
|
343
369
|
renderResult(result, options, theme, context) {
|
|
370
|
+
// ctx_shell wraps Pi's bash tool; its renderer is typed for BashToolDetails,
|
|
371
|
+
// while our result adds compression stats on top of the same shape.
|
|
344
372
|
return baseBashTool.renderResult
|
|
345
|
-
? baseBashTool.renderResult(
|
|
373
|
+
? baseBashTool.renderResult(
|
|
374
|
+
result as AgentToolResult<BashToolDetails | undefined>,
|
|
375
|
+
options,
|
|
376
|
+
theme,
|
|
377
|
+
context,
|
|
378
|
+
)
|
|
346
379
|
: (context.lastComponent ?? new Text("", 0, 0));
|
|
347
380
|
},
|
|
348
381
|
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
@@ -397,8 +430,14 @@ export default async function (pi: ExtensionAPI) {
|
|
|
397
430
|
},
|
|
398
431
|
renderResult(result, options, theme, context) {
|
|
399
432
|
if (result.content.some((block) => block.type === "image")) {
|
|
433
|
+
// Reuse Pi's read renderer for images; its detail type is ReadToolDetails.
|
|
400
434
|
return nativeReadTool.renderResult
|
|
401
|
-
? nativeReadTool.renderResult(
|
|
435
|
+
? nativeReadTool.renderResult(
|
|
436
|
+
result as AgentToolResult<ReadToolDetails | undefined>,
|
|
437
|
+
options,
|
|
438
|
+
theme,
|
|
439
|
+
context,
|
|
440
|
+
)
|
|
402
441
|
: (context.lastComponent ?? new Text("", 0, 0));
|
|
403
442
|
}
|
|
404
443
|
|
|
@@ -438,7 +477,10 @@ export default async function (pi: ExtensionAPI) {
|
|
|
438
477
|
text += `\n\n${theme.fg("muted", footer)}`;
|
|
439
478
|
}
|
|
440
479
|
|
|
441
|
-
|
|
480
|
+
// setText only exists on Text; lastComponent is the wider Component type.
|
|
481
|
+
const component = context.lastComponent instanceof Text
|
|
482
|
+
? context.lastComponent
|
|
483
|
+
: new Text("", 0, 0);
|
|
442
484
|
component.setText(text);
|
|
443
485
|
return component;
|
|
444
486
|
},
|
|
@@ -542,7 +584,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
542
584
|
searchArgs.push(params.pattern, absolutePath);
|
|
543
585
|
|
|
544
586
|
const bin = resolveBinary();
|
|
545
|
-
const result = await pi.exec(bin, ["-c", ...searchArgs]
|
|
587
|
+
const result = await pi.exec(bin, ["-c", ...searchArgs]);
|
|
546
588
|
if (result.code >= 2) {
|
|
547
589
|
const msg = (result.stderr || result.stdout || `lean-ctx grep failed: ${params.pattern}`).trim();
|
|
548
590
|
throw new Error(msg);
|
|
@@ -574,9 +616,16 @@ export default async function (pi: ExtensionAPI) {
|
|
|
574
616
|
},
|
|
575
617
|
});
|
|
576
618
|
|
|
577
|
-
const enableMcpBridge =
|
|
619
|
+
const enableMcpBridge = PI_CONFIG.enableMcp;
|
|
578
620
|
const adapterConfigured = isMcpAdapterConfigured();
|
|
579
|
-
|
|
621
|
+
// An explicit opt-in to the embedded bridge wins over mcp.json detection (#361).
|
|
622
|
+
// A `lean-ctx` entry in ~/.pi/agent/mcp.json does NOT prove that pi-mcp-adapter
|
|
623
|
+
// is actually serving it — pi has no native MCP support, and `lean-ctx init
|
|
624
|
+
// --agent pi` writes that entry by default — so it must not silently disable the
|
|
625
|
+
// bridge a user explicitly requested via LEAN_CTX_PI_ENABLE_MCP=1 / enableMcp.
|
|
626
|
+
const mcpBridge = enableMcpBridge
|
|
627
|
+
? new McpBridge(resolveBinary(), PI_CONFIG.forwardedEnv)
|
|
628
|
+
: null;
|
|
580
629
|
|
|
581
630
|
if (mcpBridge) {
|
|
582
631
|
try {
|
|
@@ -595,11 +644,13 @@ export default async function (pi: ExtensionAPI) {
|
|
|
595
644
|
|
|
596
645
|
const lines: string[] = [];
|
|
597
646
|
lines.push(found ? `Binary: ${bin}` : "Binary: NOT FOUND — install: cargo install lean-ctx");
|
|
598
|
-
if (
|
|
599
|
-
lines.push(
|
|
600
|
-
}
|
|
647
|
+
if (PI_CONFIG.loaded) {
|
|
648
|
+
lines.push(`Config: ${PI_CONFIG.configPath}`);
|
|
649
|
+
}
|
|
650
|
+
lines.push(`Mode: ${PI_MODE}`);
|
|
651
|
+
if (!enableMcpBridge) {
|
|
601
652
|
lines.push("MCP bridge: disabled (CLI-first)");
|
|
602
|
-
lines.push(
|
|
653
|
+
lines.push(' Enable: LEAN_CTX_PI_ENABLE_MCP=1 or "enableMcp": true in config.json, then restart Pi');
|
|
603
654
|
} else if (status) {
|
|
604
655
|
lines.push(`MCP bridge: ${status.mode} (${status.connected ? "connected" : "disconnected"})`);
|
|
605
656
|
lines.push(`Reconnect attempts: ${status.reconnectAttempts}`);
|
|
@@ -607,6 +658,11 @@ export default async function (pi: ExtensionAPI) {
|
|
|
607
658
|
if (status.toolNames.length > 0) {
|
|
608
659
|
lines.push(` ${status.toolNames.join(", ")}`);
|
|
609
660
|
}
|
|
661
|
+
if (adapterConfigured) {
|
|
662
|
+
lines.push(
|
|
663
|
+
" 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.",
|
|
664
|
+
);
|
|
665
|
+
}
|
|
610
666
|
if (status.lastHungTool) {
|
|
611
667
|
lines.push(`Last hung tool: ${status.lastHungTool}`);
|
|
612
668
|
}
|
package/extensions/mcp-bridge.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
2
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
3
3
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
4
|
-
import { Type } from "
|
|
4
|
+
import { type TSchema, Type } from "typebox";
|
|
5
5
|
import type { McpBridgeRetryState, McpBridgeStatus } from "./types.js";
|
|
6
6
|
|
|
7
|
+
/** Result shape returned by the MCP client's `callTool`. */
|
|
8
|
+
type McpCallResult = Awaited<ReturnType<Client["callTool"]>>;
|
|
9
|
+
|
|
7
10
|
const CLI_OVERRIDE_TOOLS = new Set([
|
|
8
11
|
"ctx_read",
|
|
9
12
|
"ctx_multi_read",
|
|
@@ -56,13 +59,15 @@ export class McpBridge {
|
|
|
56
59
|
private registeredTools: string[] = [];
|
|
57
60
|
private connected = false;
|
|
58
61
|
private binary: string;
|
|
62
|
+
private extraEnv: Record<string, string>;
|
|
59
63
|
private reconnectAttempts = 0;
|
|
60
64
|
private lastError: string | undefined;
|
|
61
65
|
private lastHungTool: string | undefined;
|
|
62
66
|
private lastRetry: McpBridgeRetryState | undefined;
|
|
63
67
|
|
|
64
|
-
constructor(binary: string) {
|
|
68
|
+
constructor(binary: string, extraEnv: Record<string, string> = {}) {
|
|
65
69
|
this.binary = binary;
|
|
70
|
+
this.extraEnv = extraEnv;
|
|
66
71
|
}
|
|
67
72
|
|
|
68
73
|
async start(pi: ExtensionAPI): Promise<void> {
|
|
@@ -80,7 +85,8 @@ export class McpBridge {
|
|
|
80
85
|
this.transport = new StdioClientTransport({
|
|
81
86
|
command: this.binary,
|
|
82
87
|
args: [],
|
|
83
|
-
env
|
|
88
|
+
// config.json `env` (lowest) < process env < the forced compress flag.
|
|
89
|
+
env: { ...this.extraEnv, ...process.env, LEAN_CTX_COMPRESS: "1" },
|
|
84
90
|
stderr: "pipe",
|
|
85
91
|
});
|
|
86
92
|
|
|
@@ -163,12 +169,14 @@ export class McpBridge {
|
|
|
163
169
|
description: tool.description ?? `lean-ctx MCP tool: ${tool.name}`,
|
|
164
170
|
promptSnippet: tool.description ?? tool.name,
|
|
165
171
|
parameters: schema,
|
|
166
|
-
async execute(_toolCallId, params, signal) {
|
|
167
|
-
|
|
172
|
+
async execute(_toolCallId, params, signal, _onUpdate, _ctx) {
|
|
173
|
+
const result = await bridge.callTool(
|
|
168
174
|
tool.name,
|
|
169
175
|
params as Record<string, unknown>,
|
|
170
176
|
signal,
|
|
171
177
|
);
|
|
178
|
+
// Pi's AgentToolResult requires a `details` field; MCP tool output has none.
|
|
179
|
+
return { ...result, details: undefined };
|
|
172
180
|
},
|
|
173
181
|
});
|
|
174
182
|
|
|
@@ -179,7 +187,7 @@ export class McpBridge {
|
|
|
179
187
|
name: string,
|
|
180
188
|
args: Record<string, unknown>,
|
|
181
189
|
signal?: AbortSignal,
|
|
182
|
-
): Promise<{ content: Array<{ type:
|
|
190
|
+
): Promise<{ content: Array<{ type: "text"; text: string }> }> {
|
|
183
191
|
if (!this.client || !this.connected) {
|
|
184
192
|
throw new Error(
|
|
185
193
|
`lean-ctx MCP bridge not connected. Tool "${name}" unavailable.`,
|
|
@@ -221,7 +229,7 @@ export class McpBridge {
|
|
|
221
229
|
name: string,
|
|
222
230
|
args: Record<string, unknown>,
|
|
223
231
|
signal?: AbortSignal,
|
|
224
|
-
) {
|
|
232
|
+
): Promise<McpCallResult> {
|
|
225
233
|
const call = this.client?.callTool({ name, arguments: args });
|
|
226
234
|
if (!call) {
|
|
227
235
|
throw new Error(`lean-ctx MCP bridge not connected. Tool "${name}" unavailable.`);
|
|
@@ -239,7 +247,7 @@ export class McpBridge {
|
|
|
239
247
|
}, TOOL_CALL_TIMEOUT_MS);
|
|
240
248
|
});
|
|
241
249
|
|
|
242
|
-
const promises: Promise<
|
|
250
|
+
const promises: Promise<McpCallResult>[] = [call, timeout];
|
|
243
251
|
|
|
244
252
|
if (signal) {
|
|
245
253
|
let onAbort: (() => void) | undefined;
|
|
@@ -271,8 +279,8 @@ export class McpBridge {
|
|
|
271
279
|
}
|
|
272
280
|
|
|
273
281
|
private toTextBlocks(
|
|
274
|
-
result:
|
|
275
|
-
): { content: Array<{ type:
|
|
282
|
+
result: McpCallResult,
|
|
283
|
+
): { content: Array<{ type: "text"; text: string }> } {
|
|
276
284
|
const content = (
|
|
277
285
|
result.content as Array<{ type: string; text?: string }>
|
|
278
286
|
).map((block) => ({
|
|
@@ -297,13 +305,13 @@ export class McpBridge {
|
|
|
297
305
|
const required = new Set(
|
|
298
306
|
(schema.required as string[] | undefined) ?? [],
|
|
299
307
|
);
|
|
300
|
-
const fields: Record<string,
|
|
308
|
+
const fields: Record<string, TSchema> = {};
|
|
301
309
|
|
|
302
310
|
for (const [key, prop] of Object.entries(properties)) {
|
|
303
311
|
const desc = (prop.description as string) ?? undefined;
|
|
304
312
|
const jsonType = prop.type as string | undefined;
|
|
305
313
|
|
|
306
|
-
let field;
|
|
314
|
+
let field: TSchema;
|
|
307
315
|
switch (jsonType) {
|
|
308
316
|
case "number":
|
|
309
317
|
case "integer":
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-lean-ctx",
|
|
3
|
-
"version": "3.7.
|
|
3
|
+
"version": "3.7.4",
|
|
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.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -25,8 +25,9 @@
|
|
|
25
25
|
"@modelcontextprotocol/sdk": "^1.29.0"
|
|
26
26
|
},
|
|
27
27
|
"peerDependencies": {
|
|
28
|
-
"@earendil-works/pi-coding-agent": ">=0.
|
|
29
|
-
"@earendil-works/pi-tui": "*"
|
|
28
|
+
"@earendil-works/pi-coding-agent": ">=0.74.0",
|
|
29
|
+
"@earendil-works/pi-tui": "*",
|
|
30
|
+
"typebox": "^1.0.0"
|
|
30
31
|
},
|
|
31
32
|
"pi": {
|
|
32
33
|
"extensions": [
|