pi-lean-ctx 1.0.0

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 ADDED
@@ -0,0 +1,59 @@
1
+ # pi-lean-ctx
2
+
3
+ [Pi Coding Agent](https://github.com/badlogic/pi-mono) extension that routes all tool output through [lean-ctx](https://leanctx.com) for **60–90% token savings**.
4
+
5
+ ## What it does
6
+
7
+ Overrides Pi's built-in tools to route them through `lean-ctx`:
8
+
9
+ | Tool | Compression |
10
+ |------|------------|
11
+ | `bash` | All shell commands compressed via lean-ctx's 90+ patterns |
12
+ | `read` | Smart mode selection (full/map/signatures) based on file type and size |
13
+ | `grep` | Results grouped and compressed via ripgrep + lean-ctx |
14
+ | `find` | File listings compressed and .gitignore-aware |
15
+ | `ls` | Directory output compressed |
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ # 1. Install lean-ctx (if not already installed)
21
+ cargo install lean-ctx
22
+ # or: brew tap yvgude/lean-ctx && brew install lean-ctx
23
+
24
+ # 2. Install the Pi package
25
+ pi install pi-lean-ctx
26
+ ```
27
+
28
+ ## Binary Resolution
29
+
30
+ The extension locates the `lean-ctx` binary in this order:
31
+
32
+ 1. `LEAN_CTX_BIN` environment variable
33
+ 2. `~/.cargo/bin/lean-ctx`
34
+ 3. `~/.local/bin/lean-ctx` (Linux) or `%APPDATA%\Local\lean-ctx\lean-ctx.exe` (Windows)
35
+ 4. `/usr/local/bin/lean-ctx` (macOS/Linux)
36
+ 5. `lean-ctx` on PATH
37
+
38
+ ## Smart Read Modes
39
+
40
+ The `read` tool automatically selects the optimal lean-ctx mode:
41
+
42
+ | File Type | Size | Mode |
43
+ |-----------|------|------|
44
+ | `.md`, `.json`, `.toml`, `.yaml`, etc. | Any | `full` |
45
+ | Code files (`.rs`, `.ts`, `.py`, etc.) | < 24 KB | `full` |
46
+ | Code files | 24–160 KB | `map` (deps + API signatures) |
47
+ | Code files | > 160 KB | `signatures` (AST extraction) |
48
+ | Other files | < 48 KB | `full` |
49
+ | Other files | > 48 KB | `map` |
50
+
51
+ ## Slash Command
52
+
53
+ Use `/lean-ctx` in Pi to check which binary is being used.
54
+
55
+ ## Links
56
+
57
+ - [lean-ctx](https://leanctx.com) — The Cognitive Filter for AI Engineering
58
+ - [GitHub](https://github.com/yvgude/lean-ctx)
59
+ - [Discord](https://discord.gg/pTHkG9Hew9)
@@ -0,0 +1,449 @@
1
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+ import {
3
+ createBashToolDefinition,
4
+ createReadToolDefinition,
5
+ DEFAULT_MAX_BYTES,
6
+ DEFAULT_MAX_LINES,
7
+ getLanguageFromPath,
8
+ highlightCode,
9
+ truncateHead,
10
+ } from "@mariozechner/pi-coding-agent";
11
+ import { Text } from "@mariozechner/pi-tui";
12
+ import { Type } from "@sinclair/typebox";
13
+ import { existsSync } from "node:fs";
14
+ import { readFile, stat } from "node:fs/promises";
15
+ import { dirname, extname, resolve } from "node:path";
16
+ import { homedir, platform } from "node:os";
17
+
18
+ const CODE_EXTENSIONS = new Set([
19
+ ".rs", ".ts", ".tsx", ".js", ".jsx", ".php", ".py", ".go",
20
+ ".java", ".c", ".cc", ".cpp", ".cxx", ".cs", ".kt", ".swift", ".rb",
21
+ ]);
22
+
23
+ const FULL_READ_EXTENSIONS = new Set([
24
+ ".md", ".txt", ".json", ".json5", ".yaml", ".yml", ".toml",
25
+ ".env", ".ini", ".xml", ".lock",
26
+ ]);
27
+
28
+ const IMAGE_EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".gif", ".webp"]);
29
+
30
+ const readSchema = Type.Object({
31
+ path: Type.String({ description: "Path to the file to read (relative or absolute)" }),
32
+ offset: Type.Optional(Type.Number({ description: "Line number to start reading from (1-indexed)" })),
33
+ limit: Type.Optional(Type.Number({ description: "Maximum number of lines to read" })),
34
+ });
35
+
36
+ const lsSchema = Type.Object({
37
+ path: Type.Optional(Type.String({ description: "Directory to list (default: current directory)" })),
38
+ limit: Type.Optional(Type.Number({ description: "Maximum number of entries to return (default: 500)" })),
39
+ });
40
+
41
+ const findSchema = Type.Object({
42
+ pattern: Type.String({ description: "Glob pattern to match files" }),
43
+ path: Type.Optional(Type.String({ description: "Directory to search in (default: current directory)" })),
44
+ limit: Type.Optional(Type.Number({ description: "Maximum number of results (default: 1000)" })),
45
+ });
46
+
47
+ const grepSchema = Type.Object({
48
+ pattern: Type.String({ description: "Search pattern (regex or literal string)" }),
49
+ path: Type.Optional(Type.String({ description: "Directory or file to search (default: current directory)" })),
50
+ glob: Type.Optional(Type.String({ description: "Filter files by glob pattern, e.g. '*.ts'" })),
51
+ ignoreCase: Type.Optional(Type.Boolean({ description: "Case-insensitive search (default: false)" })),
52
+ literal: Type.Optional(Type.Boolean({ description: "Treat pattern as literal string (default: false)" })),
53
+ context: Type.Optional(Type.Number({ description: "Lines of context around each match (default: 0)" })),
54
+ limit: Type.Optional(Type.Number({ description: "Maximum number of matches (default: 100)" })),
55
+ });
56
+
57
+ function shellQuote(value: string): string {
58
+ if (!value) return "''";
59
+ if (/^[A-Za-z0-9_./=:@,+%^-]+$/.test(value)) return value;
60
+ return `'${value.replace(/'/g, `'\\''`)}'`;
61
+ }
62
+
63
+ function resolveBinary(): string {
64
+ const envBin = process.env.LEAN_CTX_BIN;
65
+ if (envBin && existsSync(envBin)) return envBin;
66
+
67
+ const home = homedir();
68
+ const isWin = platform() === "win32";
69
+ const candidates = isWin
70
+ ? [
71
+ resolve(home, ".cargo", "bin", "lean-ctx.exe"),
72
+ resolve(home, "AppData", "Local", "lean-ctx", "lean-ctx.exe"),
73
+ ]
74
+ : [
75
+ resolve(home, ".cargo", "bin", "lean-ctx"),
76
+ resolve(home, ".local", "bin", "lean-ctx"),
77
+ "/usr/local/bin/lean-ctx",
78
+ ];
79
+
80
+ for (const candidate of candidates) {
81
+ if (existsSync(candidate)) return candidate;
82
+ }
83
+
84
+ return "lean-ctx";
85
+ }
86
+
87
+ function normalizePathArg(path: string): string {
88
+ return path.startsWith("@") ? path.slice(1) : path;
89
+ }
90
+
91
+ async function chooseReadMode(path: string): Promise<"full" | "map" | "signatures"> {
92
+ const ext = extname(path).toLowerCase();
93
+ if (FULL_READ_EXTENSIONS.has(ext)) return "full";
94
+
95
+ const fileStat = await stat(path);
96
+ const size = fileStat.size;
97
+
98
+ if (!CODE_EXTENSIONS.has(ext)) return size > 48 * 1024 ? "map" : "full";
99
+ if (size >= 160 * 1024) return "signatures";
100
+ if (size >= 24 * 1024) return "map";
101
+ return "full";
102
+ }
103
+
104
+ async function readSlice(path: string, offset?: number, limit?: number) {
105
+ const content = await readFile(path, "utf8");
106
+ const lines = content.split("\n");
107
+ const startLine = offset ? Math.max(0, offset - 1) : 0;
108
+ const endLine = limit ? startLine + limit : lines.length;
109
+ const selected = lines.slice(startLine, endLine).join("\n");
110
+ const truncation = truncateHead(selected, {
111
+ maxLines: DEFAULT_MAX_LINES,
112
+ maxBytes: DEFAULT_MAX_BYTES,
113
+ });
114
+ return { text: truncation.content, lines: lines.length, truncated: truncation.truncated };
115
+ }
116
+
117
+ type CompressionStats = {
118
+ originalTokens: number;
119
+ compressedTokens: number;
120
+ percentSaved: number;
121
+ };
122
+
123
+ function estimateTokens(text: string) {
124
+ return Math.ceil(text.length / 4);
125
+ }
126
+
127
+ function clampStats(original: number, compressed: number): CompressionStats {
128
+ const orig = Math.max(0, original);
129
+ const comp = Math.max(0, Math.min(orig, compressed));
130
+ const saved = Math.max(0, orig - comp);
131
+ const percentSaved = orig > 0 ? Math.round((saved / orig) * 100) : 0;
132
+ return { originalTokens: orig, compressedTokens: comp, percentSaved };
133
+ }
134
+
135
+ function parseLeanCtxOutput(text: string) {
136
+ const lines = text.replace(/\r\n/g, "\n").split("\n");
137
+ let stats: CompressionStats | undefined;
138
+ const kept: string[] = [];
139
+
140
+ for (const line of lines) {
141
+ const trimmed = line.trim();
142
+
143
+ const shellMatch = trimmed.match(/^\[lean-ctx:\s*(\d+)\s*→\s*(\d+)\s*tok,\s*-?(\d+)%\]$/);
144
+ if (shellMatch) {
145
+ stats = clampStats(Number(shellMatch[1]), Number(shellMatch[2]));
146
+ continue;
147
+ }
148
+
149
+ const savedMatch = trimmed.match(/^\[(\d+)\s+tok saved(?:\s+\((\d+)%\))?\]$/);
150
+ if (savedMatch) {
151
+ const saved = Number(savedMatch[1]);
152
+ const pct = savedMatch[2] ? Number(savedMatch[2]) : 0;
153
+ if (pct > 0) {
154
+ const original = Math.round((saved * 100) / pct);
155
+ stats = clampStats(original, Math.max(0, original - saved));
156
+ } else {
157
+ stats = clampStats(saved, saved);
158
+ }
159
+ continue;
160
+ }
161
+
162
+ kept.push(line);
163
+ }
164
+
165
+ return { text: kept.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd(), stats };
166
+ }
167
+
168
+ function formatFooter(stats: CompressionStats) {
169
+ const pct = stats.percentSaved > 0 ? `-${stats.percentSaved}%` : "0%";
170
+ return `Compressed ${stats.originalTokens} → ${stats.compressedTokens} tokens (${pct})`;
171
+ }
172
+
173
+ function withFooter(text: string, opts?: {
174
+ originalText?: string;
175
+ limit?: number;
176
+ always?: boolean;
177
+ preferEstimate?: boolean;
178
+ }) {
179
+ const parsed = parseLeanCtxOutput(text);
180
+ const limited = limitLines(parsed.text, opts?.limit);
181
+
182
+ let stats = parsed.stats;
183
+ if (opts?.originalText !== undefined && (opts.preferEstimate || !stats)) {
184
+ stats = clampStats(estimateTokens(opts.originalText), estimateTokens(limited.text));
185
+ }
186
+ if (!stats && opts?.always) {
187
+ const tokens = estimateTokens(limited.text);
188
+ stats = clampStats(tokens, tokens);
189
+ }
190
+ if (!stats) return { text: limited.text, stats: undefined, truncated: limited.truncated };
191
+
192
+ const footer = formatFooter(stats);
193
+ const base = limited.text.trimEnd();
194
+ return {
195
+ text: base ? `${base}\n\n${footer}` : footer,
196
+ stats,
197
+ truncated: limited.truncated,
198
+ };
199
+ }
200
+
201
+ function limitLines(text: string, limit?: number) {
202
+ if (!limit || limit <= 0) return { text, truncated: false };
203
+ const lines = text.split("\n");
204
+ if (lines.length <= limit) return { text, truncated: false };
205
+ return {
206
+ text: lines.slice(0, limit).join("\n") + `\n\n[Output truncated to ${limit} lines]`,
207
+ truncated: true,
208
+ };
209
+ }
210
+
211
+ function replaceTabs(text: string) {
212
+ return text.replace(/\t/g, " ");
213
+ }
214
+
215
+ function trimTrailingEmpty(lines: string[]) {
216
+ let end = lines.length;
217
+ while (end > 0 && lines[end - 1] === "") end--;
218
+ return lines.slice(0, end);
219
+ }
220
+
221
+ function splitFooter(text: string) {
222
+ const normalized = text.replace(/\r\n/g, "\n").trimEnd();
223
+ const match = normalized.match(/\n\n(Compressed \d+ → \d+ tokens \((?:-?\d+|0)%\))$/);
224
+ if (!match) return { body: normalized, footer: undefined as string | undefined };
225
+ return { body: normalized.slice(0, -match[0].length), footer: match[1] };
226
+ }
227
+
228
+ async function execLeanCtx(pi: ExtensionAPI, args: string[]) {
229
+ const bin = resolveBinary();
230
+ const result = await pi.exec(bin, args, {});
231
+ if (result.code !== 0) {
232
+ const msg = (result.stderr || result.stdout || `lean-ctx failed: ${args.join(" ")}`).trim();
233
+ throw new Error(msg);
234
+ }
235
+ return result.stdout;
236
+ }
237
+
238
+ export default function (pi: ExtensionAPI) {
239
+ const baseBashTool = createBashToolDefinition(process.cwd(), {
240
+ spawnHook: ({ command, cwd, env }) => {
241
+ const bin = resolveBinary();
242
+ return {
243
+ command: `${shellQuote(bin)} -c sh -lc ${shellQuote(command)}`,
244
+ cwd,
245
+ env: { ...env },
246
+ };
247
+ },
248
+ });
249
+
250
+ pi.registerTool({
251
+ ...baseBashTool,
252
+ description:
253
+ "Execute a bash command through lean-ctx compression for 60-90% smaller output.",
254
+ promptSnippet: "Run shell commands through lean-ctx compression.",
255
+ promptGuidelines: [
256
+ "Use bash normally — commands are automatically routed through lean-ctx.",
257
+ "lean-ctx compresses verbose CLI output (git, cargo, npm, docker, kubectl, etc.) automatically.",
258
+ ],
259
+ async execute(toolCallId, params, signal, onUpdate, ctx) {
260
+ try {
261
+ const result = await baseBashTool.execute(toolCallId, params, signal, onUpdate, ctx);
262
+ const text = result.content?.[0]?.type === "text" ? result.content[0].text : "";
263
+ const decorated = withFooter(text, { always: true });
264
+ return {
265
+ ...result,
266
+ content: [{ type: "text", text: decorated.text }],
267
+ details: { ...(result.details ?? {}), compression: decorated.stats },
268
+ };
269
+ } catch (error) {
270
+ if (error instanceof Error) {
271
+ const decorated = withFooter(error.message, { always: true });
272
+ throw new Error(decorated.text);
273
+ }
274
+ throw error;
275
+ }
276
+ },
277
+ });
278
+
279
+ const nativeReadTool = createReadToolDefinition(process.cwd());
280
+
281
+ pi.registerTool({
282
+ name: "read",
283
+ label: "Read",
284
+ description:
285
+ "Read file contents through lean-ctx with automatic mode selection (full/map/signatures) based on file type and size.",
286
+ promptSnippet: "Read files through lean-ctx compression with smart mode selection.",
287
+ promptGuidelines: [
288
+ "Use read normally — lean-ctx automatically selects the optimal compression mode.",
289
+ "Small files get full reads, large code files get map/signatures mode.",
290
+ ],
291
+ parameters: readSchema,
292
+ renderCall(args, theme, context) {
293
+ return nativeReadTool.renderCall
294
+ ? nativeReadTool.renderCall(args, theme, context)
295
+ : (context.lastComponent ?? new Text("", 0, 0));
296
+ },
297
+ renderResult(result, options, theme, context) {
298
+ if (result.content.some((block) => block.type === "image")) {
299
+ return nativeReadTool.renderResult
300
+ ? nativeReadTool.renderResult(result, options, theme, context)
301
+ : (context.lastComponent ?? new Text("", 0, 0));
302
+ }
303
+
304
+ const textBlock = result.content.find((block) => block.type === "text");
305
+ const rawText = textBlock?.type === "text" ? textBlock.text : "";
306
+ const { body, footer } = splitFooter(rawText);
307
+ const rawPath = typeof context.args?.path === "string" ? context.args.path : undefined;
308
+ const lang = rawPath ? getLanguageFromPath(rawPath) : undefined;
309
+ const renderedLines = lang ? highlightCode(replaceTabs(body), lang) : body.split("\n");
310
+ const lines = trimTrailingEmpty(renderedLines);
311
+ const maxLines = options.expanded ? lines.length : 10;
312
+ const displayLines = lines.slice(0, maxLines);
313
+ const remaining = lines.length - maxLines;
314
+
315
+ let text = `\n${displayLines
316
+ .map((line) => (lang ? replaceTabs(line) : theme.fg("toolOutput", replaceTabs(line))))
317
+ .join("\n")}`;
318
+
319
+ if (remaining > 0) {
320
+ text += `${theme.fg("muted", `\n... (${remaining} more lines, ctrl+o to expand)`)}`;
321
+ }
322
+
323
+ const truncation = (result.details as Record<string, unknown> | undefined)?.truncation as
324
+ | { truncated?: boolean; firstLineExceedsLimit?: boolean; truncatedBy?: string; outputLines?: number; totalLines?: number; maxLines?: number; maxBytes?: number }
325
+ | undefined;
326
+ if (truncation?.truncated) {
327
+ if (truncation.firstLineExceedsLimit) {
328
+ text += `\n${theme.fg("warning", `[First line exceeds ${Math.round((truncation.maxBytes ?? DEFAULT_MAX_BYTES) / 1024)}KB limit]`)}`;
329
+ } else if (truncation.truncatedBy === "lines") {
330
+ text += `\n${theme.fg("warning", `[Truncated: ${truncation.outputLines} of ${truncation.totalLines} lines]`)}`;
331
+ } else {
332
+ text += `\n${theme.fg("warning", `[Truncated: ${truncation.outputLines} lines (${Math.round((truncation.maxBytes ?? DEFAULT_MAX_BYTES) / 1024)}KB limit)]`)}`;
333
+ }
334
+ }
335
+
336
+ if (footer) {
337
+ text += `\n\n${theme.fg("muted", footer)}`;
338
+ }
339
+
340
+ const component = context.lastComponent ?? new Text("", 0, 0);
341
+ component.setText(text);
342
+ return component;
343
+ },
344
+ async execute(_toolCallId, params, signal, onUpdate, ctx) {
345
+ const requestedPath = normalizePathArg(params.path);
346
+ const absolutePath = resolve(ctx.cwd, requestedPath);
347
+
348
+ if (params.offset !== undefined || params.limit !== undefined) {
349
+ const sliced = await readSlice(absolutePath, params.offset, params.limit);
350
+ return {
351
+ content: [{ type: "text", text: sliced.text }],
352
+ details: { path: absolutePath, lines: sliced.lines, source: "local-slice", truncated: sliced.truncated },
353
+ };
354
+ }
355
+
356
+ if (IMAGE_EXTENSIONS.has(extname(absolutePath).toLowerCase())) {
357
+ return nativeReadTool.execute(_toolCallId, { ...params, path: absolutePath }, signal, onUpdate, ctx);
358
+ }
359
+
360
+ const mode = await chooseReadMode(absolutePath);
361
+ const args = mode === "full" ? ["read", absolutePath] : ["read", absolutePath, "-m", mode];
362
+ const output = await execLeanCtx(pi, args);
363
+ const originalText = await readFile(absolutePath, "utf8");
364
+ const decorated = withFooter(output, { originalText, always: true, preferEstimate: true });
365
+
366
+ return {
367
+ content: [{ type: "text", text: decorated.text }],
368
+ details: { path: absolutePath, source: "lean-ctx", mode, compression: decorated.stats },
369
+ };
370
+ },
371
+ });
372
+
373
+ pi.registerTool({
374
+ name: "ls",
375
+ label: "ls",
376
+ description: "List directory contents through lean-ctx compression.",
377
+ promptSnippet: "List directory contents with token-optimized output.",
378
+ promptGuidelines: ["Use ls normally — output is automatically compressed by lean-ctx."],
379
+ parameters: lsSchema,
380
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
381
+ const requestedPath = normalizePathArg(params.path || ".");
382
+ const absolutePath = resolve(ctx.cwd, requestedPath);
383
+ const output = await execLeanCtx(pi, ["ls", absolutePath]);
384
+ const decorated = withFooter(output, { limit: params.limit, always: true });
385
+ return {
386
+ content: [{ type: "text", text: decorated.text }],
387
+ details: { path: absolutePath, source: "lean-ctx", truncated: decorated.truncated, compression: decorated.stats },
388
+ };
389
+ },
390
+ });
391
+
392
+ pi.registerTool({
393
+ name: "find",
394
+ label: "find",
395
+ description: "Find files by glob pattern through lean-ctx compression.",
396
+ promptSnippet: "Find files with compressed output.",
397
+ promptGuidelines: ["Use find normally — output respects .gitignore and is compressed by lean-ctx."],
398
+ parameters: findSchema,
399
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
400
+ const requestedPath = normalizePathArg(params.path || ".");
401
+ const absolutePath = resolve(ctx.cwd, requestedPath);
402
+ const output = await execLeanCtx(pi, ["find", params.pattern, absolutePath]);
403
+ const decorated = withFooter(output, { limit: params.limit, always: true });
404
+ return {
405
+ content: [{ type: "text", text: decorated.text }],
406
+ details: { path: absolutePath, pattern: params.pattern, source: "lean-ctx", truncated: decorated.truncated, compression: decorated.stats },
407
+ };
408
+ },
409
+ });
410
+
411
+ pi.registerTool({
412
+ name: "grep",
413
+ label: "grep",
414
+ description: "Search file contents through ripgrep + lean-ctx compression.",
415
+ promptSnippet: "Search code with compressed, grouped results.",
416
+ promptGuidelines: ["Use grep normally — results are compressed and grouped by lean-ctx."],
417
+ parameters: grepSchema,
418
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
419
+ const requestedPath = normalizePathArg(params.path || ".");
420
+ const absolutePath = resolve(ctx.cwd, requestedPath);
421
+ const searchArgs = ["rg", "--line-number", "--color=never"];
422
+ if (params.ignoreCase) searchArgs.push("-i");
423
+ if (params.literal) searchArgs.push("-F");
424
+ if (params.context && params.context > 0) searchArgs.push(`-C${params.context}`);
425
+ if (params.glob) searchArgs.push("--glob", params.glob);
426
+ if (params.limit && params.limit > 0) searchArgs.push("-m", String(params.limit));
427
+ searchArgs.push(params.pattern, absolutePath);
428
+
429
+ const output = await execLeanCtx(pi, ["-c", ...searchArgs]);
430
+ const decorated = withFooter(output, { always: true });
431
+ return {
432
+ content: [{ type: "text", text: decorated.text }],
433
+ details: { path: absolutePath, pattern: params.pattern, source: "lean-ctx", compression: decorated.stats },
434
+ };
435
+ },
436
+ });
437
+
438
+ pi.registerCommand("lean-ctx", {
439
+ description: "Show the lean-ctx binary currently used by the Pi integration",
440
+ handler: async (_args, ctx) => {
441
+ const bin = resolveBinary();
442
+ const found = existsSync(bin);
443
+ ctx.ui.notify(
444
+ found ? `pi-lean-ctx using: ${bin}` : `lean-ctx not found. Install: cargo install lean-ctx`,
445
+ found ? "info" : "warning",
446
+ );
447
+ },
448
+ });
449
+ }
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "pi-lean-ctx",
3
+ "version": "1.0.0",
4
+ "description": "Pi Coding Agent extension that routes bash, read, grep, find, and ls through lean-ctx for 60-90% token savings",
5
+ "keywords": ["pi-package", "lean-ctx", "token-optimization", "compression"],
6
+ "license": "MIT",
7
+ "author": "Yves Gugger",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/yvgude/lean-ctx.git",
11
+ "directory": "packages/pi-lean-ctx"
12
+ },
13
+ "homepage": "https://leanctx.com",
14
+ "bugs": {
15
+ "url": "https://github.com/yvgude/lean-ctx/issues"
16
+ },
17
+ "peerDependencies": {
18
+ "@mariozechner/pi-coding-agent": ">=0.50.0",
19
+ "@mariozechner/pi-tui": "*"
20
+ },
21
+ "pi": {
22
+ "extensions": ["./extensions"]
23
+ },
24
+ "files": [
25
+ "extensions/",
26
+ "README.md"
27
+ ]
28
+ }