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 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. `~/.cargo/bin/lean-ctx`
150
- 3. `~/.local/bin/lean-ctx` (Linux) or `%APPDATA%\Local\lean-ctx\lean-ctx.exe` (Windows)
151
- 4. `/usr/local/bin/lean-ctx` (macOS/Linux)
152
- 5. `lean-ctx` on PATH
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
+ }
@@ -1,4 +1,9 @@
1
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
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 "@sinclair/typebox";
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
- // LEAN_CTX_PI_MODE controls behavior:
40
- // "additive" (default) keep Pi builtins, add ctx_* alongside
41
- // "replace" disable Pi builtins, only expose ctx_*
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 PI_MODE = (process.env.LEAN_CTX_PI_MODE || "additive").toLowerCase();
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
- function envFlag(name: string): boolean {
97
- const raw = process.env[name];
98
- if (!raw) return false;
99
- const v = raw.trim().toLowerCase();
100
- return v === "1" || v === "true" || v === "yes" || v === "on";
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, { env: { ...process.env, LEAN_CTX_COMPRESS: "1", LEAN_CTX_SAVINGS_FOOTER: "always" } });
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: { ...env, LEAN_CTX_COMPRESS: "1", LEAN_CTX_SAVINGS_FOOTER: "always" },
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(result, options, theme, context)
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(result, options, theme, context)
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
- const component = context.lastComponent ?? new Text("", 0, 0);
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], { env: { ...process.env, LEAN_CTX_COMPRESS: "1", LEAN_CTX_SAVINGS_FOOTER: "always" } });
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 = envFlag("LEAN_CTX_PI_ENABLE_MCP");
619
+ const enableMcpBridge = PI_CONFIG.enableMcp;
578
620
  const adapterConfigured = isMcpAdapterConfigured();
579
- const mcpBridge = enableMcpBridge && !adapterConfigured ? new McpBridge(resolveBinary()) : null;
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 (adapterConfigured) {
599
- lines.push("MCP bridge: adapter-configured (extension bridge disabled)");
600
- } else if (!enableMcpBridge) {
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(" Enable: set LEAN_CTX_PI_ENABLE_MCP=1 and restart Pi");
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
  }
@@ -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 "@sinclair/typebox";
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: { ...process.env, LEAN_CTX_COMPRESS: "1" },
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
- return bridge.callTool(
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: string; text: string }> }> {
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<unknown>[] = [call, timeout];
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: Awaited<ReturnType<Client["callTool"]>>,
275
- ): { content: Array<{ type: string; text: string }> } {
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, ReturnType<typeof Type.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.2",
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.50.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": [