@oh-my-pi/pi-coding-agent 12.3.0 → 12.4.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/CHANGELOG.md +15 -0
- package/package.json +7 -7
- package/src/config/keybindings.ts +2 -1
- package/src/lsp/render.ts +1 -1
- package/src/memories/index.ts +11 -7
- package/src/modes/components/bash-execution.ts +16 -9
- package/src/modes/components/python-execution.ts +16 -7
- package/src/modes/components/tool-execution.ts +2 -1
- package/src/modes/controllers/command-controller.ts +1 -1
- package/src/session/streaming-output.ts +1 -1
- package/src/tools/bash-interactive.ts +10 -6
- package/src/tools/fetch.ts +1 -1
- package/src/tools/output-meta.ts +6 -2
- package/src/web/scrapers/types.ts +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [12.4.0] - 2026-02-14
|
|
6
|
+
### Changed
|
|
7
|
+
|
|
8
|
+
- Moved `sanitizeText` function from `@oh-my-pi/pi-utils` to `@oh-my-pi/pi-natives` for better code organization
|
|
9
|
+
- Replaced internal `#normalizeOutput` methods with `sanitizeText` utility function in bash and Python execution components
|
|
10
|
+
- Added line length clamping (4000 characters) to bash and Python execution output to prevent display of excessively long lines
|
|
11
|
+
- Modified memory storage to isolate memories by project working directory, preventing cross-project memory contamination
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- Fixed bash interactive tool to gracefully handle malformed output chunks by normalizing them before display
|
|
16
|
+
- Fixed fetch tool incorrectly treating HTML content as plain text or markdown
|
|
17
|
+
- Fixed output truncation notice displaying incorrect byte limit when maxBytes differs from outputBytes
|
|
18
|
+
- Fixed Cloudflare returning corrupted bytes when compression is negotiated in web scraper requests
|
|
19
|
+
|
|
5
20
|
## [12.3.0] - 2026-02-14
|
|
6
21
|
### Added
|
|
7
22
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.4.0",
|
|
4
4
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -84,12 +84,12 @@
|
|
|
84
84
|
},
|
|
85
85
|
"dependencies": {
|
|
86
86
|
"@mozilla/readability": "0.6.0",
|
|
87
|
-
"@oh-my-pi/omp-stats": "12.
|
|
88
|
-
"@oh-my-pi/pi-agent-core": "12.
|
|
89
|
-
"@oh-my-pi/pi-ai": "12.
|
|
90
|
-
"@oh-my-pi/pi-natives": "12.
|
|
91
|
-
"@oh-my-pi/pi-tui": "12.
|
|
92
|
-
"@oh-my-pi/pi-utils": "12.
|
|
87
|
+
"@oh-my-pi/omp-stats": "12.4.0",
|
|
88
|
+
"@oh-my-pi/pi-agent-core": "12.4.0",
|
|
89
|
+
"@oh-my-pi/pi-ai": "12.4.0",
|
|
90
|
+
"@oh-my-pi/pi-natives": "12.4.0",
|
|
91
|
+
"@oh-my-pi/pi-tui": "12.4.0",
|
|
92
|
+
"@oh-my-pi/pi-utils": "12.4.0",
|
|
93
93
|
"@sinclair/typebox": "^0.34.48",
|
|
94
94
|
"@xterm/headless": "^6.0.0",
|
|
95
95
|
"ajv": "^8.17.1",
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
matchesKey,
|
|
9
9
|
setEditorKeybindings,
|
|
10
10
|
} from "@oh-my-pi/pi-tui";
|
|
11
|
-
import { logger } from "@oh-my-pi/pi-utils";
|
|
11
|
+
import { isEnoent, logger } from "@oh-my-pi/pi-utils";
|
|
12
12
|
import { getAgentDir } from "@oh-my-pi/pi-utils/dirs";
|
|
13
13
|
|
|
14
14
|
/**
|
|
@@ -204,6 +204,7 @@ export class KeybindingsManager {
|
|
|
204
204
|
try {
|
|
205
205
|
return await Bun.file(path).json();
|
|
206
206
|
} catch (error) {
|
|
207
|
+
if (isEnoent(error)) return {};
|
|
207
208
|
logger.warn("Failed to parse keybindings config", { path, error: String(error) });
|
|
208
209
|
return {};
|
|
209
210
|
}
|
package/src/lsp/render.ts
CHANGED
|
@@ -282,7 +282,7 @@ function renderHover(
|
|
|
282
282
|
}
|
|
283
283
|
|
|
284
284
|
/**
|
|
285
|
-
* Syntax highlight code using native
|
|
285
|
+
* Syntax highlight code using native highlighter.
|
|
286
286
|
*/
|
|
287
287
|
function highlightCode(codeText: string, language: string, theme: Theme): string[] {
|
|
288
288
|
const validLang = language && supportsLanguage(language) ? language : undefined;
|
package/src/memories/index.ts
CHANGED
|
@@ -152,7 +152,7 @@ export async function buildMemoryToolDeveloperInstructions(
|
|
|
152
152
|
): Promise<string | undefined> {
|
|
153
153
|
const cfg = loadMemoryConfig(settings);
|
|
154
154
|
if (!cfg.enabled) return undefined;
|
|
155
|
-
const memoryRoot = getMemoryRoot(agentDir);
|
|
155
|
+
const memoryRoot = getMemoryRoot(agentDir, settings.getCwd());
|
|
156
156
|
const summaryPath = path.join(memoryRoot, "memory_summary.md");
|
|
157
157
|
|
|
158
158
|
let text: string;
|
|
@@ -176,14 +176,14 @@ export async function buildMemoryToolDeveloperInstructions(
|
|
|
176
176
|
/**
|
|
177
177
|
* Clear all persisted memory state and generated artifacts.
|
|
178
178
|
*/
|
|
179
|
-
export async function clearMemoryData(agentDir: string): Promise<void> {
|
|
179
|
+
export async function clearMemoryData(agentDir: string, cwd: string): Promise<void> {
|
|
180
180
|
const db = openMemoryDb(getAgentDbPath(agentDir));
|
|
181
181
|
try {
|
|
182
182
|
clearMemoryDataInDb(db);
|
|
183
183
|
} finally {
|
|
184
184
|
closeMemoryDb(db);
|
|
185
185
|
}
|
|
186
|
-
await fs.rm(getMemoryRoot(agentDir), { recursive: true, force: true });
|
|
186
|
+
await fs.rm(getMemoryRoot(agentDir, cwd), { recursive: true, force: true });
|
|
187
187
|
}
|
|
188
188
|
|
|
189
189
|
/**
|
|
@@ -221,7 +221,7 @@ async function runPhase1(options: {
|
|
|
221
221
|
const db = openMemoryDb(getAgentDbPath(agentDir));
|
|
222
222
|
const nowSec = unixNow();
|
|
223
223
|
const workerId = `memory-${process.pid}`;
|
|
224
|
-
const memoryRoot = getMemoryRoot(agentDir);
|
|
224
|
+
const memoryRoot = getMemoryRoot(agentDir, session.sessionManager.getCwd());
|
|
225
225
|
const currentThreadId = session.sessionManager.getSessionId();
|
|
226
226
|
|
|
227
227
|
try {
|
|
@@ -345,7 +345,7 @@ async function runPhase2(options: {
|
|
|
345
345
|
const db = openMemoryDb(getAgentDbPath(agentDir));
|
|
346
346
|
const nowSec = unixNow();
|
|
347
347
|
const workerId = `memory-${process.pid}`;
|
|
348
|
-
const memoryRoot = getMemoryRoot(agentDir);
|
|
348
|
+
const memoryRoot = getMemoryRoot(agentDir, session.sessionManager.getCwd());
|
|
349
349
|
|
|
350
350
|
try {
|
|
351
351
|
const claimResult = tryClaimGlobalPhase2Job(db, {
|
|
@@ -1077,8 +1077,12 @@ function loadMemoryConfig(settings: Settings): MemoryRuntimeConfig {
|
|
|
1077
1077
|
};
|
|
1078
1078
|
}
|
|
1079
1079
|
|
|
1080
|
-
function getMemoryRoot(agentDir: string): string {
|
|
1081
|
-
return path.join(agentDir, "memories");
|
|
1080
|
+
function getMemoryRoot(agentDir: string, cwd: string): string {
|
|
1081
|
+
return path.join(agentDir, "memories", encodeProjectPath(cwd));
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
function encodeProjectPath(cwd: string): string {
|
|
1085
|
+
return `--${cwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
|
|
1082
1086
|
}
|
|
1083
1087
|
|
|
1084
1088
|
function unixNow(): number {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Component for displaying bash command execution with streaming output.
|
|
3
3
|
*/
|
|
4
|
+
|
|
5
|
+
import { sanitizeText } from "@oh-my-pi/pi-natives";
|
|
4
6
|
import { Container, Loader, Spacer, Text, type TUI } from "@oh-my-pi/pi-tui";
|
|
5
7
|
import { getSymbolTheme, theme } from "../../modes/theme/theme";
|
|
6
8
|
import type { TruncationMeta } from "../../tools/output-meta";
|
|
@@ -10,6 +12,7 @@ import { truncateToVisualLines } from "./visual-truncate";
|
|
|
10
12
|
|
|
11
13
|
// Preview line limit when not expanded (matches tool execution behavior)
|
|
12
14
|
const PREVIEW_LINES = 20;
|
|
15
|
+
const MAX_DISPLAY_LINE_CHARS = 4000;
|
|
13
16
|
|
|
14
17
|
export class BashExecutionComponent extends Container {
|
|
15
18
|
#outputLines: string[] = [];
|
|
@@ -73,13 +76,15 @@ export class BashExecutionComponent extends Container {
|
|
|
73
76
|
}
|
|
74
77
|
|
|
75
78
|
appendOutput(chunk: string): void {
|
|
76
|
-
const clean =
|
|
79
|
+
const clean = sanitizeText(chunk);
|
|
77
80
|
|
|
78
81
|
// Append to output lines
|
|
79
|
-
const newLines = clean.split("\n");
|
|
82
|
+
const newLines = clean.split("\n").map(line => this.#clampDisplayLine(line));
|
|
80
83
|
if (this.#outputLines.length > 0 && newLines.length > 0) {
|
|
81
84
|
// Append first chunk to last line (incomplete line continuation)
|
|
82
|
-
this.#outputLines[this.#outputLines.length - 1]
|
|
85
|
+
this.#outputLines[this.#outputLines.length - 1] = this.#clampDisplayLine(
|
|
86
|
+
`${this.#outputLines[this.#outputLines.length - 1]}${newLines[0]}`,
|
|
87
|
+
);
|
|
83
88
|
this.#outputLines.push(...newLines.slice(1));
|
|
84
89
|
} else {
|
|
85
90
|
this.#outputLines.push(...newLines);
|
|
@@ -184,15 +189,17 @@ export class BashExecutionComponent extends Container {
|
|
|
184
189
|
}
|
|
185
190
|
}
|
|
186
191
|
|
|
187
|
-
#
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
192
|
+
#clampDisplayLine(line: string): string {
|
|
193
|
+
if (line.length <= MAX_DISPLAY_LINE_CHARS) {
|
|
194
|
+
return line;
|
|
195
|
+
}
|
|
196
|
+
const omitted = line.length - MAX_DISPLAY_LINE_CHARS;
|
|
197
|
+
return `${line.slice(0, MAX_DISPLAY_LINE_CHARS)}… [${omitted} chars omitted]`;
|
|
191
198
|
}
|
|
192
199
|
|
|
193
200
|
#setOutput(output: string): void {
|
|
194
|
-
const clean =
|
|
195
|
-
this.#outputLines = clean ? clean.split("\n") : [];
|
|
201
|
+
const clean = sanitizeText(output);
|
|
202
|
+
this.#outputLines = clean ? clean.split("\n").map(line => this.#clampDisplayLine(line)) : [];
|
|
196
203
|
}
|
|
197
204
|
|
|
198
205
|
/**
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
* Component for displaying user-initiated Python execution with streaming output.
|
|
3
3
|
* Shares the same kernel session as the agent's Python tool.
|
|
4
4
|
*/
|
|
5
|
+
|
|
6
|
+
import { sanitizeText } from "@oh-my-pi/pi-natives";
|
|
5
7
|
import { Container, Loader, Spacer, Text, type TUI } from "@oh-my-pi/pi-tui";
|
|
6
8
|
import { getSymbolTheme, highlightCode, theme } from "../../modes/theme/theme";
|
|
7
9
|
import type { TruncationMeta } from "../../tools/output-meta";
|
|
@@ -10,6 +12,7 @@ import { DynamicBorder } from "./dynamic-border";
|
|
|
10
12
|
import { truncateToVisualLines } from "./visual-truncate";
|
|
11
13
|
|
|
12
14
|
const PREVIEW_LINES = 20;
|
|
15
|
+
const MAX_DISPLAY_LINE_CHARS = 4000;
|
|
13
16
|
|
|
14
17
|
export class PythonExecutionComponent extends Container {
|
|
15
18
|
#outputLines: string[] = [];
|
|
@@ -70,11 +73,13 @@ export class PythonExecutionComponent extends Container {
|
|
|
70
73
|
}
|
|
71
74
|
|
|
72
75
|
appendOutput(chunk: string): void {
|
|
73
|
-
const clean =
|
|
76
|
+
const clean = sanitizeText(chunk);
|
|
74
77
|
|
|
75
|
-
const newLines = clean.split("\n");
|
|
78
|
+
const newLines = clean.split("\n").map(line => this.#clampDisplayLine(line));
|
|
76
79
|
if (this.#outputLines.length > 0 && newLines.length > 0) {
|
|
77
|
-
this.#outputLines[this.#outputLines.length - 1]
|
|
80
|
+
this.#outputLines[this.#outputLines.length - 1] = this.#clampDisplayLine(
|
|
81
|
+
`${this.#outputLines[this.#outputLines.length - 1]}${newLines[0]}`,
|
|
82
|
+
);
|
|
78
83
|
this.#outputLines.push(...newLines.slice(1));
|
|
79
84
|
} else {
|
|
80
85
|
this.#outputLines.push(...newLines);
|
|
@@ -168,13 +173,17 @@ export class PythonExecutionComponent extends Container {
|
|
|
168
173
|
}
|
|
169
174
|
}
|
|
170
175
|
|
|
171
|
-
#
|
|
172
|
-
|
|
176
|
+
#clampDisplayLine(line: string): string {
|
|
177
|
+
if (line.length <= MAX_DISPLAY_LINE_CHARS) {
|
|
178
|
+
return line;
|
|
179
|
+
}
|
|
180
|
+
const omitted = line.length - MAX_DISPLAY_LINE_CHARS;
|
|
181
|
+
return `${line.slice(0, MAX_DISPLAY_LINE_CHARS)}… [${omitted} chars omitted]`;
|
|
173
182
|
}
|
|
174
183
|
|
|
175
184
|
#setOutput(output: string): void {
|
|
176
|
-
const clean =
|
|
177
|
-
this.#outputLines = clean ? clean.split("\n") : [];
|
|
185
|
+
const clean = sanitizeText(output);
|
|
186
|
+
this.#outputLines = clean ? clean.split("\n").map(line => this.#clampDisplayLine(line)) : [];
|
|
178
187
|
}
|
|
179
188
|
|
|
180
189
|
getOutput(): string {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
2
|
+
import { sanitizeText } from "@oh-my-pi/pi-natives";
|
|
2
3
|
import {
|
|
3
4
|
Box,
|
|
4
5
|
type Component,
|
|
@@ -12,7 +13,7 @@ import {
|
|
|
12
13
|
Text,
|
|
13
14
|
type TUI,
|
|
14
15
|
} from "@oh-my-pi/pi-tui";
|
|
15
|
-
import { logger
|
|
16
|
+
import { logger } from "@oh-my-pi/pi-utils";
|
|
16
17
|
import { getProjectDir } from "@oh-my-pi/pi-utils/dirs";
|
|
17
18
|
import type { Theme } from "../../modes/theme/theme";
|
|
18
19
|
import { theme } from "../../modes/theme/theme";
|
|
@@ -432,7 +432,7 @@ export class CommandController {
|
|
|
432
432
|
|
|
433
433
|
if (action === "reset" || action === "clear") {
|
|
434
434
|
try {
|
|
435
|
-
await clearMemoryData(agentDir);
|
|
435
|
+
await clearMemoryData(agentDir, this.ctx.sessionManager.getCwd());
|
|
436
436
|
await this.ctx.session.refreshBaseSystemPrompt();
|
|
437
437
|
this.ctx.showStatus("Memory data cleared and system prompt refreshed.");
|
|
438
438
|
} catch (error) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AgentToolContext } from "@oh-my-pi/pi-agent-core";
|
|
2
|
-
import { type PtyRunResult, PtySession } from "@oh-my-pi/pi-natives";
|
|
2
|
+
import { type PtyRunResult, PtySession, sanitizeText } from "@oh-my-pi/pi-natives";
|
|
3
3
|
import {
|
|
4
4
|
type Component,
|
|
5
5
|
matchesKey,
|
|
@@ -23,9 +23,8 @@ export interface BashInteractiveResult extends OutputSummary {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
function normalizeCaptureChunk(chunk: string): string {
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
return normalized.replace(/[\x00-\x08\x0B-\x1F\x7F]/gu, "");
|
|
26
|
+
const normalized = chunk.replace(/\r\n/gu, "\n").replace(/\r/gu, "\n");
|
|
27
|
+
return sanitizeText(normalized);
|
|
29
28
|
}
|
|
30
29
|
|
|
31
30
|
const XtermTerminal = xterm.Terminal;
|
|
@@ -168,7 +167,7 @@ class BashInteractiveOverlayComponent implements Component {
|
|
|
168
167
|
const visibleLines: string[] = [];
|
|
169
168
|
for (let i = 0; i < maxContentRows; i++) {
|
|
170
169
|
const line = buffer.getLine(viewportY + i)?.translateToString(true) ?? "";
|
|
171
|
-
visibleLines.push(truncateToWidth(replaceTabs(line), innerWidth));
|
|
170
|
+
visibleLines.push(truncateToWidth(replaceTabs(sanitizeText(line)), innerWidth));
|
|
172
171
|
}
|
|
173
172
|
return visibleLines;
|
|
174
173
|
}
|
|
@@ -350,7 +349,12 @@ export async function runInteractiveBashPty(
|
|
|
350
349
|
},
|
|
351
350
|
(err, chunk) => {
|
|
352
351
|
if (err || !chunk) return;
|
|
353
|
-
|
|
352
|
+
try {
|
|
353
|
+
component.appendOutput(chunk);
|
|
354
|
+
} catch {
|
|
355
|
+
const normalizedChunk = normalizeCaptureChunk(chunk);
|
|
356
|
+
component.appendOutput(normalizedChunk);
|
|
357
|
+
}
|
|
354
358
|
const normalizedChunk = normalizeCaptureChunk(chunk);
|
|
355
359
|
pendingChunks = pendingChunks.then(() => sink.push(normalizedChunk)).catch(() => {});
|
|
356
360
|
tui.requestRender();
|
package/src/tools/fetch.ts
CHANGED
|
@@ -241,7 +241,7 @@ async function tryContentNegotiation(
|
|
|
241
241
|
if (!result.ok) return null;
|
|
242
242
|
|
|
243
243
|
const mime = normalizeMime(result.contentType);
|
|
244
|
-
if (mime.includes("markdown") || mime === "text/plain") {
|
|
244
|
+
if ((mime.includes("markdown") || mime === "text/plain") && !looksLikeHtml(result.content)) {
|
|
245
245
|
return { content: result.content, type: result.contentType };
|
|
246
246
|
}
|
|
247
247
|
|
package/src/tools/output-meta.ts
CHANGED
|
@@ -21,6 +21,7 @@ export interface TruncationMeta {
|
|
|
21
21
|
totalBytes: number;
|
|
22
22
|
outputLines: number;
|
|
23
23
|
outputBytes: number;
|
|
24
|
+
maxBytes?: number;
|
|
24
25
|
/** Line range shown (1-indexed, inclusive) */
|
|
25
26
|
shownRange?: { start: number; end: number };
|
|
26
27
|
/** Artifact ID if full output was saved */
|
|
@@ -128,6 +129,7 @@ export class OutputMetaBuilder {
|
|
|
128
129
|
totalBytes: result.totalBytes,
|
|
129
130
|
outputLines: result.outputLines,
|
|
130
131
|
outputBytes: result.outputBytes,
|
|
132
|
+
maxBytes: result.maxBytes,
|
|
131
133
|
shownRange: { start: shownStart, end: shownEnd },
|
|
132
134
|
artifactId,
|
|
133
135
|
nextOffset: direction === "head" ? shownEnd + 1 : undefined,
|
|
@@ -212,6 +214,7 @@ export class OutputMetaBuilder {
|
|
|
212
214
|
totalBytes,
|
|
213
215
|
outputLines,
|
|
214
216
|
outputBytes,
|
|
217
|
+
maxBytes: options.maxBytes,
|
|
215
218
|
shownRange: { start: shownStart, end: shownEnd },
|
|
216
219
|
nextOffset: options.direction === "head" ? shownEnd + 1 : undefined,
|
|
217
220
|
};
|
|
@@ -319,14 +322,15 @@ export function formatOutputNotice(meta: OutputMeta | undefined): string {
|
|
|
319
322
|
const range = t.shownRange;
|
|
320
323
|
let notice: string;
|
|
321
324
|
|
|
322
|
-
if (range) {
|
|
325
|
+
if (range && range.end >= range.start) {
|
|
323
326
|
notice = `Showing lines ${range.start}-${range.end} of ${t.totalLines}`;
|
|
324
327
|
} else {
|
|
325
328
|
notice = `Showing ${t.outputLines} of ${t.totalLines} lines`;
|
|
326
329
|
}
|
|
327
330
|
|
|
328
331
|
if (t.truncatedBy === "bytes") {
|
|
329
|
-
|
|
332
|
+
const maxBytes = t.maxBytes ?? t.outputBytes;
|
|
333
|
+
notice += ` (${formatSize(maxBytes)} limit)`;
|
|
330
334
|
}
|
|
331
335
|
|
|
332
336
|
if (t.nextOffset != null) {
|
|
@@ -92,6 +92,7 @@ export async function loadPage(url: string, options: LoadPageOptions = {}): Prom
|
|
|
92
92
|
"User-Agent": userAgent,
|
|
93
93
|
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
94
94
|
"Accept-Language": "en-US,en;q=0.5",
|
|
95
|
+
"Accept-Encoding": "identity", // Cloudflare Markdown-for-Agents returns corrupted bytes when compression is negotiated
|
|
95
96
|
...headers,
|
|
96
97
|
},
|
|
97
98
|
redirect: "follow",
|