diff --git a/package-lock.json b/package-lock.json index 4514327de..9a9a89a5b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9221,7 +9221,6 @@ "name": "@gsd/pi-coding-agent", "version": "2.52.0", "dependencies": { - "@gsd-build/rpc-client": "^2.52.0", "@mariozechner/jiti": "^2.6.2", "@silvia-odwyer/photon-node": "^0.3.4", "chalk": "^5.5.0", diff --git a/packages/pi-coding-agent/package.json b/packages/pi-coding-agent/package.json index 9252a6196..7d3cb624e 100644 --- a/packages/pi-coding-agent/package.json +++ b/packages/pi-coding-agent/package.json @@ -20,7 +20,6 @@ "copy-assets": "node scripts/copy-assets.cjs" }, "dependencies": { - "@gsd-build/rpc-client": "^2.52.0", "@mariozechner/jiti": "^2.6.2", "@silvia-odwyer/photon-node": "^0.3.4", "chalk": "^5.5.0", diff --git a/packages/pi-coding-agent/src/modes/rpc/jsonl.ts b/packages/pi-coding-agent/src/modes/rpc/jsonl.ts index 5ef2e2473..5392defef 100644 --- a/packages/pi-coding-agent/src/modes/rpc/jsonl.ts +++ b/packages/pi-coding-agent/src/modes/rpc/jsonl.ts @@ -1 +1,64 @@ -export { serializeJsonLine, attachJsonlLineReader } from '@gsd-build/rpc-client'; +import type { Readable } from "node:stream"; +import { StringDecoder } from "node:string_decoder"; + +/** + * Serialize a single strict JSONL record. + * + * Framing is LF-only. Payload strings may contain other Unicode separators such as + * U+2028 and U+2029. Clients must split records on `\n` only. + */ +export function serializeJsonLine(value: unknown): string { + return `${JSON.stringify(value)}\n`; +} + +/** + * Attach an LF-only JSONL reader to a stream. + * + * This intentionally does not use Node readline. Readline splits on additional + * Unicode separators that are valid inside JSON strings and therefore does not + * implement strict JSONL framing. + */ +export function attachJsonlLineReader(stream: Readable, onLine: (line: string) => void): () => void { + const decoder = new StringDecoder("utf8"); + let buffer = ""; + + const emitLine = (line: string) => { + onLine(line.endsWith("\r") ? line.slice(0, -1) : line); + }; + + const onData = (chunk: string | Buffer) => { + buffer += typeof chunk === "string" ? chunk : decoder.write(chunk); + + while (true) { + const newlineIndex = buffer.indexOf("\n"); + if (newlineIndex === -1) { + return; + } + + emitLine(buffer.slice(0, newlineIndex)); + buffer = buffer.slice(newlineIndex + 1); + } + }; + + const onEnd = () => { + buffer += decoder.end(); + if (buffer.length > 0) { + emitLine(buffer); + buffer = ""; + } + }; + + const onError = (_err: Error) => { + // Stream errors are non-fatal for JSONL reading + }; + + stream.on("data", onData); + stream.on("end", onEnd); + stream.on("error", onError); + + return () => { + stream.off("data", onData); + stream.off("end", onEnd); + stream.off("error", onError); + }; +}