@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 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.0",
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.3.0",
88
- "@oh-my-pi/pi-agent-core": "12.3.0",
89
- "@oh-my-pi/pi-ai": "12.3.0",
90
- "@oh-my-pi/pi-natives": "12.3.0",
91
- "@oh-my-pi/pi-tui": "12.3.0",
92
- "@oh-my-pi/pi-utils": "12.3.0",
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 WASM highlighter.
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;
@@ -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 = this.#normalizeOutput(chunk);
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] += newLines[0];
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
- #normalizeOutput(text: string): string {
188
- // Strip ANSI codes and normalize line endings
189
- // Note: binary data is already sanitized in tui-renderer.ts executeBashCommand
190
- return Bun.stripANSI(text).replace(/\r\n/g, "\n").replace(/\r/g, "\n");
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 = this.#normalizeOutput(output);
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 = this.#normalizeOutput(chunk);
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] += newLines[0];
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
- #normalizeOutput(text: string): string {
172
- return Bun.stripANSI(text).replace(/\r\n/g, "\n").replace(/\r/g, "\n");
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 = this.#normalizeOutput(output);
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, sanitizeText } from "@oh-my-pi/pi-utils";
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,4 +1,4 @@
1
- import { sanitizeText } from "@oh-my-pi/pi-utils";
1
+ import { sanitizeText } from "@oh-my-pi/pi-natives";
2
2
  import { DEFAULT_MAX_BYTES } from "../tools/truncate";
3
3
 
4
4
  export interface OutputSummary {
@@ -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 noAnsi = Bun.stripANSI(chunk);
27
- const normalized = noAnsi.replace(/\r\n/gu, "\n").replace(/\r/gu, "\n");
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
- component.appendOutput(chunk);
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();
@@ -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
 
@@ -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
- notice += ` (${formatSize(t.outputBytes)} limit)`;
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",