@dyyz1993/pi-coding-agent 0.74.25 → 0.74.27
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/CHANGELOG.md +9 -0
- package/dist/core/session-manager.d.ts +5 -0
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +8 -0
- package/dist/core/session-manager.js.map +1 -1
- package/dist/extensions/lsp/lsp/index.ts +40 -37
- package/dist/extensions/output-guard/index.ts +126 -64
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/package.json +4 -4
|
@@ -1,31 +1,45 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Output Guard Extension - Global fallback truncation + tool limit optimization.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Aligns pi-momo-fork's truncation strategy with OpenCode's approach:
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* OpenCode has a global truncation layer in `Tool.define()` that checks
|
|
7
|
+
* `metadata.truncated` - if undefined, applies 50KB/2000-line truncation
|
|
8
|
+
* and saves full output to disk. Plugin/MCP tools are wrapped in
|
|
9
|
+
* `fromPlugin()` with `Truncate.output()` built in.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* Pi lacks this global layer. This extension fills the gap via `tool_result`
|
|
12
|
+
* event hooks, providing equivalent protection for:
|
|
13
|
+
* - Extension/plugin tools (no built-in truncation)
|
|
14
|
+
* - MCP tools (no built-in truncation)
|
|
15
|
+
* - Any future tool that forgets to self-manage
|
|
14
16
|
*
|
|
15
|
-
*
|
|
16
|
-
* from PDF files using pdf-parse, since the built-in read tool does not support PDFs.
|
|
17
|
+
* Three capabilities:
|
|
17
18
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
19
|
+
* 1. **Global truncation fallback**: Intercepts `tool_result` for tools that
|
|
20
|
+
* don't self-manage truncation. Applies 50KB/2000-line limit, saves full
|
|
21
|
+
* output to `<sessionDataDir>/tool-output/`, returns truncated preview
|
|
22
|
+
* with actionable file path hint.
|
|
23
|
+
*
|
|
24
|
+
* 2. **Tool limit optimization**: Intercepts `tool_call` to enforce lower
|
|
25
|
+
* result limits on find (1000 -> 100) and ls (500 -> 100), matching
|
|
26
|
+
* OpenCode's glob/ls defaults. Reduces unnecessary context consumption.
|
|
27
|
+
*
|
|
28
|
+
* 3. **PDF text extraction**: Registers a `pdf_read` tool that extracts text
|
|
29
|
+
* from PDF files. OpenCode sends PDFs as raw base64 to the model; Pi's
|
|
30
|
+
* read tool doesn't support PDFs at all. This tool uses pdf-parse for
|
|
31
|
+
* text extraction, which is more token-efficient than base64 encoding.
|
|
32
|
+
*
|
|
33
|
+
* Configuration (via .pi/settings.json `outputGuard` key):
|
|
34
|
+
* maxLines: number (default: 2000)
|
|
35
|
+
* maxBytes: number (default: 51200 = 50KB)
|
|
36
|
+
* findLimit: number (default: 100)
|
|
37
|
+
* lsLimit: number (default: 100)
|
|
38
|
+
* saveToFile: boolean (default: true)
|
|
24
39
|
*/
|
|
25
40
|
|
|
26
41
|
import { randomBytes } from "node:crypto";
|
|
27
|
-
import {
|
|
28
|
-
import { writeFile } from "node:fs/promises";
|
|
42
|
+
import { mkdirSync, existsSync, writeFileSync as fsWriteFileSync } from "node:fs";
|
|
29
43
|
import { tmpdir } from "node:os";
|
|
30
44
|
import { join } from "node:path";
|
|
31
45
|
import { Type } from "typebox";
|
|
@@ -39,14 +53,30 @@ import type {
|
|
|
39
53
|
} from "@dyyz1993/pi-coding-agent";
|
|
40
54
|
|
|
41
55
|
// ============================================================================
|
|
42
|
-
//
|
|
56
|
+
// Constants
|
|
43
57
|
// ============================================================================
|
|
44
58
|
|
|
59
|
+
/** Matches OpenCode's MAX_LINES */
|
|
45
60
|
const DEFAULT_MAX_LINES = 2000;
|
|
61
|
+
/** Matches OpenCode's MAX_BYTES */
|
|
46
62
|
const DEFAULT_MAX_BYTES = 50 * 1024; // 50KB
|
|
63
|
+
/** Matches OpenCode's glob limit of 100 */
|
|
47
64
|
const DEFAULT_FIND_LIMIT = 100;
|
|
65
|
+
/** Matches OpenCode's ls limit of 100 */
|
|
48
66
|
const DEFAULT_LS_LIMIT = 100;
|
|
49
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Built-in tools that self-manage truncation.
|
|
70
|
+
* These tools set details.truncation and handle their own size limits,
|
|
71
|
+
* so the global fallback must skip them (matches OpenCode's
|
|
72
|
+
* `metadata.truncated !== undefined` check).
|
|
73
|
+
*/
|
|
74
|
+
const SELF_MANAGED_TOOLS = new Set(["read", "bash", "grep", "find", "ls"]);
|
|
75
|
+
|
|
76
|
+
// ============================================================================
|
|
77
|
+
// Configuration
|
|
78
|
+
// ============================================================================
|
|
79
|
+
|
|
50
80
|
interface OutputGuardConfig {
|
|
51
81
|
maxLines: number;
|
|
52
82
|
maxBytes: number;
|
|
@@ -68,7 +98,7 @@ function loadConfig(ctx: ExtensionContext): OutputGuardConfig {
|
|
|
68
98
|
}
|
|
69
99
|
|
|
70
100
|
// ============================================================================
|
|
71
|
-
// Truncation Logic
|
|
101
|
+
// Truncation Logic (mirrors OpenCode's Truncate.output)
|
|
72
102
|
// ============================================================================
|
|
73
103
|
|
|
74
104
|
interface TruncationInfo {
|
|
@@ -76,13 +106,17 @@ interface TruncationInfo {
|
|
|
76
106
|
content: string;
|
|
77
107
|
totalLines: number;
|
|
78
108
|
totalBytes: number;
|
|
109
|
+
outputLines: number;
|
|
110
|
+
outputBytes: number;
|
|
79
111
|
truncatedBy: "lines" | "bytes" | null;
|
|
80
112
|
fullOutputPath?: string;
|
|
81
113
|
}
|
|
82
114
|
|
|
83
115
|
/**
|
|
84
|
-
* Truncate text content
|
|
85
|
-
*
|
|
116
|
+
* Truncate text content, keeping the tail (last N lines).
|
|
117
|
+
* Mirrors OpenCode's `Truncate.output()` with direction="tail".
|
|
118
|
+
* Saves full content to `<sessionDataDir>/tool-output/` when truncated
|
|
119
|
+
* (matches OpenCode's `<data-dir>/tool-output/` pattern).
|
|
86
120
|
*/
|
|
87
121
|
function truncateOutput(
|
|
88
122
|
content: string,
|
|
@@ -100,36 +134,39 @@ function truncateOutput(
|
|
|
100
134
|
content,
|
|
101
135
|
totalLines,
|
|
102
136
|
totalBytes,
|
|
137
|
+
outputLines: totalLines,
|
|
138
|
+
outputBytes: totalBytes,
|
|
103
139
|
truncatedBy: null,
|
|
104
140
|
};
|
|
105
141
|
}
|
|
106
142
|
|
|
107
|
-
// Collect lines from the end
|
|
108
|
-
const
|
|
109
|
-
let
|
|
143
|
+
// Collect lines from the end (tail direction)
|
|
144
|
+
const outputLinesArr: string[] = [];
|
|
145
|
+
let outputBytesCount = 0;
|
|
110
146
|
let truncatedBy: "lines" | "bytes" = "lines";
|
|
111
147
|
|
|
112
|
-
for (let i = lines.length - 1; i >= 0 &&
|
|
148
|
+
for (let i = lines.length - 1; i >= 0 && outputLinesArr.length < config.maxLines; i--) {
|
|
113
149
|
const line = lines[i];
|
|
114
|
-
const lineBytes = Buffer.byteLength(line, "utf-8") + (
|
|
150
|
+
const lineBytes = Buffer.byteLength(line, "utf-8") + (outputLinesArr.length > 0 ? 1 : 0);
|
|
115
151
|
|
|
116
|
-
if (
|
|
152
|
+
if (outputBytesCount + lineBytes > config.maxBytes) {
|
|
117
153
|
truncatedBy = "bytes";
|
|
118
154
|
break;
|
|
119
155
|
}
|
|
120
156
|
|
|
121
|
-
|
|
122
|
-
|
|
157
|
+
outputLinesArr.unshift(line);
|
|
158
|
+
outputBytesCount += lineBytes;
|
|
123
159
|
}
|
|
124
160
|
|
|
125
|
-
if (
|
|
161
|
+
if (outputLinesArr.length >= config.maxLines && outputBytesCount <= config.maxBytes) {
|
|
126
162
|
truncatedBy = "lines";
|
|
127
163
|
}
|
|
128
164
|
|
|
129
|
-
const truncatedContent =
|
|
165
|
+
const truncatedContent = outputLinesArr.join("\n");
|
|
166
|
+
const finalOutputBytes = Buffer.byteLength(truncatedContent, "utf-8");
|
|
130
167
|
let fullOutputPath: string | undefined;
|
|
131
168
|
|
|
132
|
-
// Save full output to disk
|
|
169
|
+
// Save full output to disk (matches OpenCode's behavior)
|
|
133
170
|
if (config.saveToFile) {
|
|
134
171
|
fullOutputPath = saveFullOutput(content, ctx);
|
|
135
172
|
}
|
|
@@ -139,38 +176,42 @@ function truncateOutput(
|
|
|
139
176
|
content: truncatedContent,
|
|
140
177
|
totalLines,
|
|
141
178
|
totalBytes,
|
|
179
|
+
outputLines: outputLinesArr.length,
|
|
180
|
+
outputBytes: finalOutputBytes,
|
|
142
181
|
truncatedBy,
|
|
143
182
|
fullOutputPath,
|
|
144
183
|
};
|
|
145
184
|
}
|
|
146
185
|
|
|
147
186
|
/**
|
|
148
|
-
* Save full output
|
|
187
|
+
* Save full output to disk.
|
|
188
|
+
* Uses sessionDataDir/tool-output/ to match OpenCode's <data-dir>/tool-output/.
|
|
189
|
+
* Falls back to tmpdir if sessionDataDir is unavailable.
|
|
149
190
|
*/
|
|
150
191
|
function saveFullOutput(content: string, ctx: ExtensionContext): string | undefined {
|
|
151
192
|
try {
|
|
152
|
-
const id = randomBytes(
|
|
153
|
-
|
|
193
|
+
const id = `output-${Date.now()}-${randomBytes(4).toString("hex")}`;
|
|
194
|
+
// Prefer sessionDataDir if it's an absolute path (production),
|
|
195
|
+
// otherwise fall back to tmpdir (works reliably in tests too)
|
|
196
|
+
const rawBaseDir = ctx.sessionDataDir;
|
|
197
|
+
let baseDir: string;
|
|
198
|
+
if (rawBaseDir && rawBaseDir.startsWith("/")) {
|
|
199
|
+
baseDir = rawBaseDir;
|
|
200
|
+
} else {
|
|
201
|
+
baseDir = join(tmpdir(), "pi-output-guard");
|
|
202
|
+
}
|
|
203
|
+
const dir = join(baseDir, "tool-output");
|
|
154
204
|
if (!existsSync(dir)) {
|
|
155
205
|
mkdirSync(dir, { recursive: true });
|
|
156
206
|
}
|
|
157
|
-
const filePath = join(dir,
|
|
158
|
-
|
|
207
|
+
const filePath = join(dir, `${id}.log`);
|
|
208
|
+
fsWriteFileSync(filePath, content);
|
|
159
209
|
return filePath;
|
|
160
210
|
} catch {
|
|
161
211
|
return undefined;
|
|
162
212
|
}
|
|
163
213
|
}
|
|
164
214
|
|
|
165
|
-
/**
|
|
166
|
-
* Synchronous write for saveFullOutput.
|
|
167
|
-
*/
|
|
168
|
-
function writeFileSync(filePath: string, content: string): void {
|
|
169
|
-
const stream = createWriteStream(filePath);
|
|
170
|
-
stream.write(content);
|
|
171
|
-
stream.end();
|
|
172
|
-
}
|
|
173
|
-
|
|
174
215
|
// ============================================================================
|
|
175
216
|
// Extension Entry Point
|
|
176
217
|
// ============================================================================
|
|
@@ -178,6 +219,15 @@ function writeFileSync(filePath: string, content: string): void {
|
|
|
178
219
|
export default function outputGuard(pi: ExtensionAPI) {
|
|
179
220
|
// ------------------------------------------------------------------
|
|
180
221
|
// 1. Global truncation fallback via tool_result hook
|
|
222
|
+
//
|
|
223
|
+
// Mirrors OpenCode's Tool.define() wrapper:
|
|
224
|
+
// if (result.metadata.truncated === undefined) {
|
|
225
|
+
// result.output = Truncate.output(result.output)
|
|
226
|
+
// }
|
|
227
|
+
//
|
|
228
|
+
// In pi, the equivalent is: if a tool's details doesn't have a
|
|
229
|
+
// truncation field AND the tool isn't a known self-managing tool,
|
|
230
|
+
// apply truncation.
|
|
181
231
|
// ------------------------------------------------------------------
|
|
182
232
|
pi.on("tool_result", async (event: ToolResultEvent, ctx: ExtensionContext): Promise<ToolResultEventResult | void> => {
|
|
183
233
|
const config = loadConfig(ctx);
|
|
@@ -186,10 +236,12 @@ export default function outputGuard(pi: ExtensionAPI) {
|
|
|
186
236
|
const textParts = event.content.filter((p): p is { type: "text"; text: string } => p.type === "text");
|
|
187
237
|
if (textParts.length === 0) return;
|
|
188
238
|
|
|
189
|
-
//
|
|
239
|
+
// Skip tools that self-manage truncation
|
|
240
|
+
// (matches OpenCode's `metadata.truncated !== undefined` check)
|
|
190
241
|
if (hasSelfManagedTruncation(event)) return;
|
|
191
242
|
|
|
192
|
-
//
|
|
243
|
+
// Skip image content - images have their own size management
|
|
244
|
+
// (matches OpenCode's `metadata.truncated = false` for images)
|
|
193
245
|
const hasImages = event.content.some((p) => p.type === "image");
|
|
194
246
|
if (hasImages) return;
|
|
195
247
|
|
|
@@ -217,6 +269,10 @@ export default function outputGuard(pi: ExtensionAPI) {
|
|
|
217
269
|
|
|
218
270
|
// ------------------------------------------------------------------
|
|
219
271
|
// 2. Tool limit optimization via tool_call hook
|
|
272
|
+
//
|
|
273
|
+
// OpenCode: glob=100, ls=100
|
|
274
|
+
// Pi default: find=1000, ls=500
|
|
275
|
+
// This hook reduces Pi's limits to match OpenCode.
|
|
220
276
|
// ------------------------------------------------------------------
|
|
221
277
|
pi.on("tool_call", async (event: ToolCallEvent, ctx: ExtensionContext): Promise<ToolCallEventResult | void> => {
|
|
222
278
|
const config = loadConfig(ctx);
|
|
@@ -240,12 +296,16 @@ export default function outputGuard(pi: ExtensionAPI) {
|
|
|
240
296
|
|
|
241
297
|
// ------------------------------------------------------------------
|
|
242
298
|
// 3. PDF text extraction tool
|
|
299
|
+
//
|
|
300
|
+
// OpenCode sends PDFs as raw base64 attachments (no text extraction).
|
|
301
|
+
// Pi's read tool doesn't support PDFs at all (outputs binary garbage).
|
|
302
|
+
// This tool uses pdf-parse to extract text, which is more token-efficient.
|
|
243
303
|
// ------------------------------------------------------------------
|
|
244
304
|
pi.registerTool({
|
|
245
305
|
name: "pdf_read",
|
|
246
306
|
description:
|
|
247
307
|
"Read and extract text content from a PDF file. " +
|
|
248
|
-
"Returns the text content of the PDF
|
|
308
|
+
"Returns the text content of the PDF with metadata. " +
|
|
249
309
|
"Use this instead of the read tool for PDF files.",
|
|
250
310
|
parameters: Type.Object({
|
|
251
311
|
path: Type.String({ description: "Path to the PDF file" }),
|
|
@@ -337,14 +397,15 @@ export default function outputGuard(pi: ExtensionAPI) {
|
|
|
337
397
|
// ============================================================================
|
|
338
398
|
|
|
339
399
|
/**
|
|
340
|
-
* Check if a tool already self-manages truncation
|
|
341
|
-
*
|
|
342
|
-
*
|
|
400
|
+
* Check if a tool already self-manages truncation.
|
|
401
|
+
*
|
|
402
|
+
* Mirrors OpenCode's check: `result.metadata.truncated !== undefined`.
|
|
403
|
+
* In pi, built-in tools set `details.truncation`, and any tool can opt in
|
|
404
|
+
* by including a `truncation` field in its details.
|
|
343
405
|
*/
|
|
344
406
|
function hasSelfManagedTruncation(event: ToolResultEvent): boolean {
|
|
345
407
|
// Built-in tools that self-manage truncation
|
|
346
|
-
|
|
347
|
-
if (selfManagedTools.has(event.toolName)) return true;
|
|
408
|
+
if (SELF_MANAGED_TOOLS.has(event.toolName)) return true;
|
|
348
409
|
|
|
349
410
|
// Check if details has a truncation field (any tool can opt in)
|
|
350
411
|
const details = event.details as Record<string, unknown> | undefined;
|
|
@@ -354,27 +415,28 @@ function hasSelfManagedTruncation(event: ToolResultEvent): boolean {
|
|
|
354
415
|
}
|
|
355
416
|
|
|
356
417
|
/**
|
|
357
|
-
* Build a
|
|
418
|
+
* Build a truncation notice with actionable file path hint.
|
|
419
|
+
* Matches OpenCode's output format which tells the model where to find
|
|
420
|
+
* the full output and suggests using read/grep tools.
|
|
358
421
|
*/
|
|
359
422
|
function buildTruncationNotice(info: TruncationInfo, config: OutputGuardConfig): string {
|
|
360
423
|
const parts: string[] = [];
|
|
361
424
|
|
|
362
425
|
if (info.truncatedBy === "lines") {
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
);
|
|
426
|
+
const omitted = info.totalLines - info.outputLines;
|
|
427
|
+
parts.push(`...${omitted} lines truncated.`);
|
|
428
|
+
parts.push(`Output exceeded ${config.maxLines} line limit (${info.totalLines} total lines).`);
|
|
366
429
|
} else if (info.truncatedBy === "bytes") {
|
|
367
|
-
parts.push(
|
|
368
|
-
|
|
369
|
-
);
|
|
430
|
+
parts.push(`...output truncated at ${formatBytes(info.outputBytes)}.`);
|
|
431
|
+
parts.push(`Output exceeded ${formatBytes(config.maxBytes)} byte limit (${formatBytes(info.totalBytes)} total).`);
|
|
370
432
|
}
|
|
371
433
|
|
|
372
434
|
if (info.fullOutputPath) {
|
|
373
435
|
parts.push(`Full output saved to: ${info.fullOutputPath}`);
|
|
374
|
-
parts.push(
|
|
436
|
+
parts.push("Use the read tool to view the full output.");
|
|
375
437
|
}
|
|
376
438
|
|
|
377
|
-
return parts.join("
|
|
439
|
+
return parts.join("\n");
|
|
378
440
|
}
|
|
379
441
|
|
|
380
442
|
function formatBytes(bytes: number): string {
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-extension-custom-provider",
|
|
3
|
-
"version": "0.74.
|
|
3
|
+
"version": "0.74.16",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "pi-extension-custom-provider",
|
|
9
|
-
"version": "0.74.
|
|
9
|
+
"version": "0.74.16",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@anthropic-ai/sdk": "^0.52.0"
|
|
12
12
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-extension-sandbox",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.16",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "pi-extension-sandbox",
|
|
9
|
-
"version": "1.4.
|
|
9
|
+
"version": "1.4.16",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@anthropic-ai/sandbox-runtime": "^0.0.26"
|
|
12
12
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-extension-with-deps",
|
|
3
|
-
"version": "0.74.
|
|
3
|
+
"version": "0.74.16",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "pi-extension-with-deps",
|
|
9
|
-
"version": "0.74.
|
|
9
|
+
"version": "0.74.16",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"ms": "^2.1.3"
|
|
12
12
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dyyz1993/pi-coding-agent",
|
|
3
|
-
"version": "0.74.
|
|
3
|
+
"version": "0.74.27",
|
|
4
4
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"piConfig": {
|
|
@@ -39,9 +39,9 @@
|
|
|
39
39
|
"prepublishOnly": "npm run clean && npm run build"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@dyyz1993/pi-agent-core": "^0.74.
|
|
43
|
-
"@dyyz1993/pi-ai": "^0.74.
|
|
44
|
-
"@dyyz1993/pi-tui": "^0.74.
|
|
42
|
+
"@dyyz1993/pi-agent-core": "^0.74.27",
|
|
43
|
+
"@dyyz1993/pi-ai": "^0.74.27",
|
|
44
|
+
"@dyyz1993/pi-tui": "^0.74.27",
|
|
45
45
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
46
46
|
"@silvia-odwyer/photon-node": "^0.3.4",
|
|
47
47
|
"chalk": "^5.5.0",
|