@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 +10 -10
- package/src/mcp/transports/stdio.ts +9 -33
- package/src/modes/rpc/rpc-client.ts +21 -36
- package/src/modes/rpc/rpc-mode.ts +19 -29
- package/src/session/session-manager.ts +5 -31
- package/src/session/session-storage.ts +1 -1
- package/src/tools/read.ts +1 -1
- package/src/utils/mime.ts +1 -1
- package/src/web/scrapers/utils.ts +8 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
3
|
-
"version": "11.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.
|
|
91
|
-
"@oclif/plugin-autocomplete": "^3.2.
|
|
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.
|
|
94
|
-
"@oh-my-pi/pi-agent-core": "11.2.
|
|
95
|
-
"@oh-my-pi/pi-ai": "11.2.
|
|
96
|
-
"@oh-my-pi/pi-natives": "11.2.
|
|
97
|
-
"@oh-my-pi/pi-tui": "11.2.
|
|
98
|
-
"@oh-my-pi/pi-utils": "11.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.
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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 {
|
|
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.
|
|
123
|
-
this.lineReader = lines;
|
|
122
|
+
const lines = readJsonl(this.process.stdout, this.abortController.signal);
|
|
124
123
|
void (async () => {
|
|
125
|
-
|
|
126
|
-
|
|
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(
|
|
465
|
-
|
|
466
|
-
if (
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
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
|
-
|
|
467
|
+
if (!isAgentEvent(data)) return;
|
|
482
468
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
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 {
|
|
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
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
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
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
648
|
+
// Handle regular commands
|
|
649
|
+
const command = parsed as RpcCommand;
|
|
650
|
+
const response = await handleCommand(command);
|
|
651
|
+
output(response);
|
|
661
652
|
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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.
|
|
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 =
|
|
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.
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
133
|
+
return { buffer: kEmptyBuffer, contentType: "", ok: false, error: "aborted" };
|
|
132
134
|
}
|
|
133
135
|
return {
|
|
134
|
-
buffer:
|
|
136
|
+
buffer: kEmptyBuffer,
|
|
135
137
|
contentType: "",
|
|
136
138
|
ok: false,
|
|
137
139
|
error: `request failed: ${String(err)}`,
|