@oh-my-pi/pi-coding-agent 11.2.2 → 11.2.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-coding-agent",
3
- "version": "11.2.2",
3
+ "version": "11.2.3",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "ompConfig": {
@@ -87,15 +87,15 @@
87
87
  "test": "bun test"
88
88
  },
89
89
  "dependencies": {
90
- "@oclif/core": "^4.5.6",
91
- "@oclif/plugin-autocomplete": "^3.2.23",
90
+ "@oclif/core": "^4.8.0",
91
+ "@oclif/plugin-autocomplete": "^3.2.40",
92
92
  "@mozilla/readability": "0.6.0",
93
- "@oh-my-pi/omp-stats": "11.2.2",
94
- "@oh-my-pi/pi-agent-core": "11.2.2",
95
- "@oh-my-pi/pi-ai": "11.2.2",
96
- "@oh-my-pi/pi-natives": "11.2.2",
97
- "@oh-my-pi/pi-tui": "11.2.2",
98
- "@oh-my-pi/pi-utils": "11.2.2",
93
+ "@oh-my-pi/omp-stats": "11.2.3",
94
+ "@oh-my-pi/pi-agent-core": "11.2.3",
95
+ "@oh-my-pi/pi-ai": "11.2.3",
96
+ "@oh-my-pi/pi-natives": "11.2.3",
97
+ "@oh-my-pi/pi-tui": "11.2.3",
98
+ "@oh-my-pi/pi-utils": "11.2.3",
99
99
  "@sinclair/typebox": "^0.34.48",
100
100
  "ajv": "^8.17.1",
101
101
  "chalk": "^5.6.2",
@@ -107,7 +107,7 @@
107
107
  "jsdom": "28.0.0",
108
108
  "marked": "^17.0.1",
109
109
  "node-html-parser": "^7.0.2",
110
- "puppeteer": "^24.36.1",
110
+ "puppeteer": "^24.37.1",
111
111
  "smol-toml": "^1.6.0",
112
112
  "zod": "^4.3.6"
113
113
  },
@@ -4,6 +4,8 @@
4
4
  * Implements JSON-RPC 2.0 over subprocess stdin/stdout.
5
5
  * Messages are newline-delimited JSON.
6
6
  */
7
+
8
+ import { readLines } from "@oh-my-pi/pi-utils";
7
9
  import { type Subprocess, spawn } from "bun";
8
10
  import type { JsonRpcResponse, MCPStdioServerConfig, MCPTransport } from "../../mcp/types";
9
11
 
@@ -25,7 +27,6 @@ export class StdioTransport implements MCPTransport {
25
27
  reject: (error: Error) => void;
26
28
  }
27
29
  >();
28
- private buffer = "";
29
30
  private _connected = false;
30
31
  private readLoop: Promise<void> | null = null;
31
32
 
@@ -72,23 +73,21 @@ export class StdioTransport implements MCPTransport {
72
73
  private async startReadLoop(): Promise<void> {
73
74
  if (!this.process?.stdout) return;
74
75
 
75
- const reader = this.process.stdout.getReader();
76
76
  const decoder = new TextDecoder();
77
-
78
77
  try {
79
- while (this._connected) {
80
- const { done, value } = await reader.read();
81
- if (done) break;
82
-
83
- this.buffer += decoder.decode(value, { stream: true });
84
- this.processBuffer();
78
+ for await (const line of readLines(this.process.stdout)) {
79
+ if (!this._connected) break;
80
+ try {
81
+ this.handleMessage(JSON.parse(decoder.decode(line)) as JsonRpcResponse);
82
+ } catch {
83
+ // Skip malformed lines
84
+ }
85
85
  }
86
86
  } catch (error) {
87
87
  if (this._connected) {
88
88
  this.onError?.(error instanceof Error ? error : new Error(String(error)));
89
89
  }
90
90
  } finally {
91
- reader.releaseLock();
92
91
  this.handleClose();
93
92
  }
94
93
  }
@@ -117,29 +116,6 @@ export class StdioTransport implements MCPTransport {
117
116
  }
118
117
  }
119
118
 
120
- private processBuffer(): void {
121
- while (this.buffer.length > 0) {
122
- const result = Bun.JSONL.parseChunk(this.buffer);
123
- for (const message of result.values) {
124
- this.handleMessage(message as JsonRpcResponse);
125
- }
126
-
127
- if (result.error) {
128
- const nextNewline = this.buffer.indexOf("\n", result.read);
129
- if (nextNewline === -1) {
130
- this.buffer = "";
131
- break;
132
- }
133
- this.buffer = this.buffer.slice(nextNewline + 1);
134
- continue;
135
- }
136
-
137
- if (result.read === 0) break;
138
- this.buffer = this.buffer.slice(result.read);
139
- if (result.done) break;
140
- }
141
- }
142
-
143
119
  private handleMessage(message: JsonRpcResponse): void {
144
120
  // Check if it's a response (has id)
145
121
  if ("id" in message && message.id !== null) {
@@ -5,7 +5,7 @@
5
5
  */
6
6
  import type { AgentEvent, AgentMessage, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
7
7
  import type { ImageContent } from "@oh-my-pi/pi-ai";
8
- import { createTextLineSplitter, ptree } from "@oh-my-pi/pi-utils";
8
+ import { ptree, readJsonl } from "@oh-my-pi/pi-utils";
9
9
  import type { BashResult } from "../../exec/bash-executor";
10
10
  import type { SessionStats } from "../../session/agent-session";
11
11
  import type { CompactionResult } from "../../session/compaction";
@@ -83,11 +83,11 @@ function isAgentEvent(value: unknown): value is AgentEvent {
83
83
 
84
84
  export class RpcClient {
85
85
  private process: ptree.ChildProcess | null = null;
86
- private lineReader: ReadableStream<string> | null = null;
87
86
  private eventListeners: RpcEventListener[] = [];
88
87
  private pendingRequests: Map<string, { resolve: (response: RpcResponse) => void; reject: (error: Error) => void }> =
89
88
  new Map();
90
89
  private requestId = 0;
90
+ private abortController = new AbortController();
91
91
 
92
92
  constructor(private options: RpcClientOptions = {}) {}
93
93
 
@@ -119,19 +119,12 @@ export class RpcClient {
119
119
  });
120
120
 
121
121
  // Process lines in background
122
- const lines = this.process.stdout.pipeThrough(createTextLineSplitter(true));
123
- this.lineReader = lines;
122
+ const lines = readJsonl(this.process.stdout, this.abortController.signal);
124
123
  void (async () => {
125
- try {
126
- for await (const line of lines) {
127
- this.handleLine(line);
128
- }
129
- } catch {
130
- // Stream closed
131
- } finally {
132
- lines.cancel();
124
+ for await (const line of lines) {
125
+ this.handleLine(line);
133
126
  }
134
- })();
127
+ })().catch(() => {});
135
128
 
136
129
  // Wait a moment for process to initialize
137
130
  await Bun.sleep(100);
@@ -154,11 +147,9 @@ export class RpcClient {
154
147
  stop() {
155
148
  if (!this.process) return;
156
149
 
157
- this.lineReader?.cancel();
158
150
  this.process.kill();
159
-
151
+ this.abortController.abort();
160
152
  this.process = null;
161
- this.lineReader = null;
162
153
  this.pendingRequests.clear();
163
154
  }
164
155
 
@@ -461,29 +452,23 @@ export class RpcClient {
461
452
  // Internal
462
453
  // =========================================================================
463
454
 
464
- private handleLine(line: string): void {
465
- const result = Bun.JSONL.parseChunk(line);
466
- if (result.error) return;
467
-
468
- for (const data of result.values) {
469
- // Check if it's a response to a pending request
470
- if (isRpcResponse(data)) {
471
- const id = data.id;
472
- if (id && this.pendingRequests.has(id)) {
473
- const pending = this.pendingRequests.get(id)!;
474
- this.pendingRequests.delete(id);
475
- pending.resolve(data);
476
- return;
477
- }
478
- continue;
455
+ private handleLine(data: unknown): void {
456
+ // Check if it's a response to a pending request
457
+ if (isRpcResponse(data)) {
458
+ const id = data.id;
459
+ if (id && this.pendingRequests.has(id)) {
460
+ const pending = this.pendingRequests.get(id)!;
461
+ this.pendingRequests.delete(id);
462
+ pending.resolve(data);
463
+ return;
479
464
  }
465
+ }
480
466
 
481
- if (!isAgentEvent(data)) continue;
467
+ if (!isAgentEvent(data)) return;
482
468
 
483
- // Otherwise it's an event
484
- for (const listener of this.eventListeners) {
485
- listener(data);
486
- }
469
+ // Otherwise it's an event
470
+ for (const listener of this.eventListeners) {
471
+ listener(data);
487
472
  }
488
473
  }
489
474
 
@@ -10,7 +10,7 @@
10
10
  * - Events: AgentSessionEvent objects streamed as they occur
11
11
  * - Extension UI: Extension UI requests are emitted, client responds with extension_ui_response
12
12
  */
13
- import { createTextLineSplitter, Snowflake } from "@oh-my-pi/pi-utils";
13
+ import { readJsonl, Snowflake } from "@oh-my-pi/pi-utils";
14
14
  import type { ExtensionUIContext, ExtensionUIDialogOptions } from "../../extensibility/extensions";
15
15
  import { type Theme, theme } from "../../modes/theme/theme";
16
16
  import type { AgentSession } from "../../session/agent-session";
@@ -633,37 +633,27 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
633
633
  }
634
634
 
635
635
  // Listen for JSON input using Bun's stdin
636
- for await (const line of Bun.stdin.stream().pipeThrough(createTextLineSplitter())) {
637
- if (!line.trim()) continue;
638
-
639
- const result = Bun.JSONL.parseChunk(`${line}\n`);
640
- if (result.error) {
641
- output(error(undefined, "parse", `Failed to parse command: ${result.error.message}`));
642
- continue;
643
- }
644
-
645
- for (const parsed of result.values) {
646
- try {
647
- // Handle extension UI responses
648
- if ((parsed as RpcExtensionUIResponse).type === "extension_ui_response") {
649
- const response = parsed as RpcExtensionUIResponse;
650
- const pending = pendingExtensionRequests.get(response.id);
651
- if (pending) {
652
- pending.resolve(response);
653
- }
654
- continue;
636
+ for await (const parsed of readJsonl(Bun.stdin.stream())) {
637
+ try {
638
+ // Handle extension UI responses
639
+ if ((parsed as RpcExtensionUIResponse).type === "extension_ui_response") {
640
+ const response = parsed as RpcExtensionUIResponse;
641
+ const pending = pendingExtensionRequests.get(response.id);
642
+ if (pending) {
643
+ pending.resolve(response);
655
644
  }
645
+ continue;
646
+ }
656
647
 
657
- // Handle regular commands
658
- const command = parsed as RpcCommand;
659
- const response = await handleCommand(command);
660
- output(response);
648
+ // Handle regular commands
649
+ const command = parsed as RpcCommand;
650
+ const response = await handleCommand(command);
651
+ output(response);
661
652
 
662
- // Check for deferred shutdown request (idle between commands)
663
- await checkShutdownRequested();
664
- } catch (e: any) {
665
- output(error(undefined, "parse", `Failed to parse command: ${e.message}`));
666
- }
653
+ // Check for deferred shutdown request (idle between commands)
654
+ await checkShutdownRequested();
655
+ } catch (e: any) {
656
+ output(error(undefined, "parse", `Failed to parse command: ${e.message}`));
667
657
  }
668
658
  }
669
659
 
@@ -1,7 +1,7 @@
1
1
  import * as path from "node:path";
2
2
  import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
3
3
  import type { ImageContent, Message, TextContent, Usage } from "@oh-my-pi/pi-ai";
4
- import { isEnoent, logger, Snowflake } from "@oh-my-pi/pi-utils";
4
+ import { isEnoent, logger, parseJsonlLenient, Snowflake } from "@oh-my-pi/pi-utils";
5
5
  import { getAgentDir as getDefaultAgentDir } from "../config";
6
6
  import { resizeImage } from "../utils/image-resize";
7
7
  import {
@@ -279,32 +279,6 @@ function migrateToCurrentVersion(entries: FileEntry[]): boolean {
279
279
  return true;
280
280
  }
281
281
 
282
- function parseJsonlEntries<T>(buffer: string): T[] {
283
- let entries: T[] | undefined;
284
-
285
- while (buffer.length > 0) {
286
- const { values, error, read, done } = Bun.JSONL.parseChunk(buffer);
287
- if (values.length > 0) {
288
- const ext = values as T[];
289
- if (!entries) {
290
- entries = ext;
291
- } else {
292
- entries.push(...ext);
293
- }
294
- }
295
- if (error) {
296
- const nextNewline = buffer.indexOf("\n", read);
297
- if (nextNewline === -1) break;
298
- buffer = buffer.substring(nextNewline + 1);
299
- continue;
300
- }
301
- if (read === 0) break;
302
- buffer = buffer.substring(read);
303
- if (done) break;
304
- }
305
- return entries ?? [];
306
- }
307
-
308
282
  /** Exported for testing */
309
283
  export function migrateSessionEntries(entries: FileEntry[]): void {
310
284
  migrateToCurrentVersion(entries);
@@ -312,7 +286,7 @@ export function migrateSessionEntries(entries: FileEntry[]): void {
312
286
 
313
287
  /** Exported for compaction.test.ts */
314
288
  export function parseSessionEntries(content: string): FileEntry[] {
315
- return parseJsonlEntries<FileEntry>(content);
289
+ return parseJsonlLenient<FileEntry>(content);
316
290
  }
317
291
 
318
292
  export function getLatestCompactionEntry(entries: SessionEntry[]): CompactionEntry | null {
@@ -485,7 +459,7 @@ export async function loadEntriesFromFile(
485
459
  if (isEnoent(err)) return [];
486
460
  throw err;
487
461
  }
488
- const entries = parseJsonlEntries<FileEntry>(content);
462
+ const entries = parseJsonlLenient<FileEntry>(content);
489
463
 
490
464
  // Validate session header
491
465
  if (entries.length === 0) return entries;
@@ -587,7 +561,7 @@ async function getSortedSessions(sessionDir: string, storage: SessionStorage): P
587
561
  files.map(async (path: string) => {
588
562
  try {
589
563
  const content = await storage.readTextPrefix(path, 4096);
590
- const entries = parseJsonlEntries<Record<string, unknown>>(content);
564
+ const entries = parseJsonlLenient<Record<string, unknown>>(content);
591
565
  if (entries.length === 0) return;
592
566
  const header = entries[0] as Record<string, unknown>;
593
567
  if (header.type !== "session" || typeof header.id !== "string") return;
@@ -915,7 +889,7 @@ async function collectSessionsFromFiles(files: string[], storage: SessionStorage
915
889
  files.map(async file => {
916
890
  try {
917
891
  const content = await storage.readText(file);
918
- const entries = parseJsonlEntries<Record<string, unknown>>(content);
892
+ const entries = parseJsonlLenient<Record<string, unknown>>(content);
919
893
  if (entries.length === 0) return;
920
894
 
921
895
  // Check first entry for valid session header
@@ -169,7 +169,7 @@ export class FileSessionStorage implements SessionStorage {
169
169
  async readTextPrefix(path: string, maxBytes: number): Promise<string> {
170
170
  const handle = await fs.promises.open(path, "r");
171
171
  try {
172
- const buffer = Buffer.alloc(maxBytes);
172
+ const buffer = Buffer.allocUnsafe(maxBytes);
173
173
  const { bytesRead } = await handle.read(buffer, 0, maxBytes, 0);
174
174
  return buffer.subarray(0, bytesRead).toString("utf-8");
175
175
  } finally {
package/src/tools/read.ts CHANGED
@@ -43,7 +43,7 @@ function isRemoteMountPath(absolutePath: string): boolean {
43
43
  return absolutePath.startsWith(REMOTE_MOUNT_PREFIX);
44
44
  }
45
45
 
46
- const READ_CHUNK_SIZE = 64 * 1024;
46
+ const READ_CHUNK_SIZE = 8 * 1024;
47
47
 
48
48
  async function streamLinesFromFile(
49
49
  filePath: string,
package/src/utils/mime.ts CHANGED
@@ -8,7 +8,7 @@ const FILE_TYPE_SNIFF_BYTES = 4100;
8
8
  export async function detectSupportedImageMimeTypeFromFile(filePath: string): Promise<string | null> {
9
9
  const fileHandle = await fs.open(filePath, "r");
10
10
  try {
11
- const buffer = Buffer.alloc(FILE_TYPE_SNIFF_BYTES);
11
+ const buffer = Buffer.allocUnsafe(FILE_TYPE_SNIFF_BYTES);
12
12
  const { bytesRead } = await fileHandle.read(buffer, 0, FILE_TYPE_SNIFF_BYTES, 0);
13
13
  if (bytesRead === 0) {
14
14
  return null;
@@ -68,9 +68,11 @@ export async function convertWithMarkitdown(
68
68
  }
69
69
  }
70
70
 
71
+ const kEmptyBuffer = Buffer.alloc(0);
72
+
71
73
  export async function fetchBinary(url: string, timeout: number, userSignal?: AbortSignal): Promise<BinaryFetchResult> {
72
74
  if (userSignal?.aborted) {
73
- return { buffer: Buffer.alloc(0), contentType: "", ok: false, error: "aborted" };
75
+ return { buffer: kEmptyBuffer, contentType: "", ok: false, error: "aborted" };
74
76
  }
75
77
 
76
78
  const signal = ptree.combineSignals(userSignal, timeout * 1000);
@@ -89,7 +91,7 @@ export async function fetchBinary(url: string, timeout: number, userSignal?: Abo
89
91
 
90
92
  if (!response.ok) {
91
93
  return {
92
- buffer: Buffer.alloc(0),
94
+ buffer: kEmptyBuffer,
93
95
  contentType,
94
96
  contentDisposition,
95
97
  ok: false,
@@ -103,7 +105,7 @@ export async function fetchBinary(url: string, timeout: number, userSignal?: Abo
103
105
  const size = Number.parseInt(contentLength, 10);
104
106
  if (Number.isFinite(size) && size > MAX_BYTES) {
105
107
  return {
106
- buffer: Buffer.alloc(0),
108
+ buffer: kEmptyBuffer,
107
109
  contentType,
108
110
  contentDisposition,
109
111
  ok: false,
@@ -116,7 +118,7 @@ export async function fetchBinary(url: string, timeout: number, userSignal?: Abo
116
118
  const buffer = Buffer.from(await response.arrayBuffer());
117
119
  if (buffer.length > MAX_BYTES) {
118
120
  return {
119
- buffer: Buffer.alloc(0),
121
+ buffer: kEmptyBuffer,
120
122
  contentType,
121
123
  contentDisposition,
122
124
  ok: false,
@@ -128,10 +130,10 @@ export async function fetchBinary(url: string, timeout: number, userSignal?: Abo
128
130
  return { buffer, contentType, contentDisposition, ok: true, status: response.status };
129
131
  } catch (err) {
130
132
  if (signal?.aborted) {
131
- return { buffer: Buffer.alloc(0), contentType: "", ok: false, error: "aborted" };
133
+ return { buffer: kEmptyBuffer, contentType: "", ok: false, error: "aborted" };
132
134
  }
133
135
  return {
134
- buffer: Buffer.alloc(0),
136
+ buffer: kEmptyBuffer,
135
137
  contentType: "",
136
138
  ok: false,
137
139
  error: `request failed: ${String(err)}`,