@plannotator/pi-extension 0.15.0 → 0.15.2
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/README.md +1 -1
- package/generated/ai/base-session.ts +95 -0
- package/generated/ai/context.ts +212 -0
- package/generated/ai/endpoints.ts +309 -0
- package/generated/ai/index.ts +106 -0
- package/generated/ai/provider.ts +104 -0
- package/generated/ai/providers/claude-agent-sdk.ts +441 -0
- package/generated/ai/providers/codex-sdk.ts +430 -0
- package/generated/ai/providers/opencode-sdk.ts +491 -0
- package/generated/ai/providers/pi-events.ts +111 -0
- package/generated/ai/providers/pi-sdk-node.ts +377 -0
- package/generated/ai/providers/pi-sdk.ts +442 -0
- package/generated/ai/session-manager.ts +196 -0
- package/generated/ai/types.ts +370 -0
- package/generated/resolve-file.ts +28 -0
- package/index.ts +74 -45
- package/package.json +2 -2
- package/plannotator.html +70 -70
- package/review-editor.html +2 -2
- package/server/serverAnnotate.ts +2 -1
- package/server/serverReview.ts +5 -5
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
// @generated — DO NOT EDIT. Source: packages/ai/providers/pi-sdk-node.ts
|
|
2
|
+
/**
|
|
3
|
+
* Pi SDK provider — Node.js variant.
|
|
4
|
+
*
|
|
5
|
+
* Identical to pi-sdk.ts except PiProcess uses child_process.spawn()
|
|
6
|
+
* instead of Bun.spawn(). Everything else (PiSDKProvider, PiSDKSession,
|
|
7
|
+
* mapPiEvent) is re-exported from the Bun version unchanged.
|
|
8
|
+
*
|
|
9
|
+
* Used by the Pi extension which runs under jiti (Node.js).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { spawn, type ChildProcess } from "node:child_process";
|
|
13
|
+
import { BaseSession } from "../base-session.ts";
|
|
14
|
+
import { buildEffectivePrompt, buildSystemPrompt } from "../context.ts";
|
|
15
|
+
import type {
|
|
16
|
+
AIMessage,
|
|
17
|
+
AIProvider,
|
|
18
|
+
AIProviderCapabilities,
|
|
19
|
+
CreateSessionOptions,
|
|
20
|
+
PiSDKConfig,
|
|
21
|
+
} from "../types.ts";
|
|
22
|
+
import { registerProviderFactory } from "../provider.ts";
|
|
23
|
+
|
|
24
|
+
// Re-export mapPiEvent from shared (runtime-agnostic)
|
|
25
|
+
export { mapPiEvent } from "./pi-events.ts";
|
|
26
|
+
|
|
27
|
+
const PROVIDER_NAME = "pi-sdk";
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// JSONL subprocess wrapper (Node.js)
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
type EventListener = (event: Record<string, unknown>) => void;
|
|
34
|
+
|
|
35
|
+
class PiProcessNode {
|
|
36
|
+
private proc: ChildProcess | null = null;
|
|
37
|
+
private listeners: EventListener[] = [];
|
|
38
|
+
private pendingRequests = new Map<
|
|
39
|
+
string,
|
|
40
|
+
{
|
|
41
|
+
resolve: (data: Record<string, unknown>) => void;
|
|
42
|
+
reject: (err: Error) => void;
|
|
43
|
+
}
|
|
44
|
+
>();
|
|
45
|
+
private nextId = 0;
|
|
46
|
+
private buffer = "";
|
|
47
|
+
private _alive = false;
|
|
48
|
+
|
|
49
|
+
async spawn(piPath: string, cwd: string): Promise<void> {
|
|
50
|
+
this.proc = spawn(piPath, ["--mode", "rpc"], {
|
|
51
|
+
cwd,
|
|
52
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
53
|
+
});
|
|
54
|
+
this._alive = true;
|
|
55
|
+
|
|
56
|
+
this.readStream();
|
|
57
|
+
|
|
58
|
+
this.proc.on("exit", () => {
|
|
59
|
+
this._alive = false;
|
|
60
|
+
for (const [, pending] of this.pendingRequests) {
|
|
61
|
+
pending.reject(new Error("Pi process exited unexpectedly"));
|
|
62
|
+
}
|
|
63
|
+
this.pendingRequests.clear();
|
|
64
|
+
for (const listener of this.listeners) {
|
|
65
|
+
listener({ type: "process_exited" });
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private readStream(): void {
|
|
71
|
+
if (!this.proc?.stdout) return;
|
|
72
|
+
|
|
73
|
+
this.proc.stdout.on("data", (chunk: Buffer) => {
|
|
74
|
+
this.buffer += chunk.toString();
|
|
75
|
+
const lines = this.buffer.split("\n");
|
|
76
|
+
this.buffer = lines.pop() ?? "";
|
|
77
|
+
|
|
78
|
+
for (const line of lines) {
|
|
79
|
+
const trimmed = line.replace(/\r$/, "");
|
|
80
|
+
if (!trimmed) continue;
|
|
81
|
+
try {
|
|
82
|
+
const parsed = JSON.parse(trimmed);
|
|
83
|
+
this.routeMessage(parsed);
|
|
84
|
+
} catch {
|
|
85
|
+
// Ignore malformed lines
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private routeMessage(msg: Record<string, unknown>): void {
|
|
92
|
+
if (msg.type === "response" && typeof msg.id === "string") {
|
|
93
|
+
const pending = this.pendingRequests.get(msg.id);
|
|
94
|
+
if (pending) {
|
|
95
|
+
this.pendingRequests.delete(msg.id);
|
|
96
|
+
if (msg.success === false) {
|
|
97
|
+
pending.reject(new Error((msg.error as string) ?? "RPC error"));
|
|
98
|
+
} else {
|
|
99
|
+
pending.resolve((msg.data as Record<string, unknown>) ?? {});
|
|
100
|
+
}
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
for (const listener of this.listeners) {
|
|
106
|
+
listener(msg);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
send(command: Record<string, unknown>): void {
|
|
111
|
+
if (!this.proc?.stdin || this.proc.stdin.destroyed) return;
|
|
112
|
+
this.proc.stdin.write(`${JSON.stringify(command)}\n`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
sendAndWait(
|
|
116
|
+
command: Record<string, unknown>,
|
|
117
|
+
): Promise<Record<string, unknown>> {
|
|
118
|
+
const id = `req_${++this.nextId}`;
|
|
119
|
+
return new Promise((resolve, reject) => {
|
|
120
|
+
this.pendingRequests.set(id, { resolve, reject });
|
|
121
|
+
this.send({ ...command, id });
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
onEvent(listener: EventListener): () => void {
|
|
126
|
+
this.listeners.push(listener);
|
|
127
|
+
return () => {
|
|
128
|
+
const idx = this.listeners.indexOf(listener);
|
|
129
|
+
if (idx >= 0) this.listeners.splice(idx, 1);
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
get alive(): boolean {
|
|
134
|
+
return this._alive;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
kill(): void {
|
|
138
|
+
this._alive = false;
|
|
139
|
+
if (this.proc) {
|
|
140
|
+
this.proc.kill();
|
|
141
|
+
this.proc = null;
|
|
142
|
+
}
|
|
143
|
+
this.listeners.length = 0;
|
|
144
|
+
for (const [, pending] of this.pendingRequests) {
|
|
145
|
+
pending.reject(new Error("Process killed"));
|
|
146
|
+
}
|
|
147
|
+
this.pendingRequests.clear();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
// Provider (identical to pi-sdk.ts, using PiProcessNode)
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
|
|
155
|
+
export class PiSDKNodeProvider implements AIProvider {
|
|
156
|
+
readonly name = PROVIDER_NAME;
|
|
157
|
+
readonly capabilities: AIProviderCapabilities = {
|
|
158
|
+
fork: false,
|
|
159
|
+
resume: false,
|
|
160
|
+
streaming: true,
|
|
161
|
+
tools: true,
|
|
162
|
+
};
|
|
163
|
+
models?: Array<{ id: string; label: string; default?: boolean }>;
|
|
164
|
+
|
|
165
|
+
private config: PiSDKConfig;
|
|
166
|
+
private sessions = new Map<string, PiSDKNodeSession>();
|
|
167
|
+
|
|
168
|
+
constructor(config: PiSDKConfig) {
|
|
169
|
+
this.config = config;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async createSession(options: CreateSessionOptions): Promise<PiSDKNodeSession> {
|
|
173
|
+
const session = new PiSDKNodeSession({
|
|
174
|
+
systemPrompt: buildSystemPrompt(options.context),
|
|
175
|
+
cwd: options.cwd ?? this.config.cwd ?? process.cwd(),
|
|
176
|
+
parentSessionId: null,
|
|
177
|
+
piExecutablePath: this.config.piExecutablePath ?? "pi",
|
|
178
|
+
model: options.model ?? this.config.model,
|
|
179
|
+
});
|
|
180
|
+
this.sessions.set(session.id, session);
|
|
181
|
+
return session;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async forkSession(): Promise<never> {
|
|
185
|
+
throw new Error(
|
|
186
|
+
"Pi does not support session forking. " +
|
|
187
|
+
"The endpoint layer should fall back to createSession().",
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async resumeSession(): Promise<never> {
|
|
192
|
+
throw new Error("Pi does not support session resuming.");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
dispose(): void {
|
|
196
|
+
for (const session of this.sessions.values()) {
|
|
197
|
+
session.killProcess();
|
|
198
|
+
}
|
|
199
|
+
this.sessions.clear();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async fetchModels(): Promise<void> {
|
|
203
|
+
const piPath = this.config.piExecutablePath ?? "pi";
|
|
204
|
+
let proc: PiProcessNode | undefined;
|
|
205
|
+
try {
|
|
206
|
+
proc = new PiProcessNode();
|
|
207
|
+
await proc.spawn(piPath, this.config.cwd ?? process.cwd());
|
|
208
|
+
const data = await Promise.race([
|
|
209
|
+
proc.sendAndWait({ type: "get_available_models" }),
|
|
210
|
+
new Promise<never>((_, reject) =>
|
|
211
|
+
setTimeout(() => reject(new Error("Timeout")), 10_000),
|
|
212
|
+
),
|
|
213
|
+
]);
|
|
214
|
+
const rawModels = (
|
|
215
|
+
data as { models?: Array<{ provider: string; id: string; name?: string }> }
|
|
216
|
+
).models;
|
|
217
|
+
if (rawModels && rawModels.length > 0) {
|
|
218
|
+
this.models = rawModels.map((m, i) => ({
|
|
219
|
+
id: `${m.provider}/${m.id}`,
|
|
220
|
+
label: m.name ?? m.id,
|
|
221
|
+
...(i === 0 && { default: true }),
|
|
222
|
+
}));
|
|
223
|
+
}
|
|
224
|
+
} catch {
|
|
225
|
+
// Pi not configured or no models available
|
|
226
|
+
} finally {
|
|
227
|
+
proc?.kill();
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ---------------------------------------------------------------------------
|
|
233
|
+
// Session (identical to pi-sdk.ts, using PiProcessNode)
|
|
234
|
+
// ---------------------------------------------------------------------------
|
|
235
|
+
|
|
236
|
+
interface SessionConfig {
|
|
237
|
+
systemPrompt: string;
|
|
238
|
+
cwd: string;
|
|
239
|
+
parentSessionId: string | null;
|
|
240
|
+
piExecutablePath: string;
|
|
241
|
+
model?: string;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
class PiSDKNodeSession extends BaseSession {
|
|
245
|
+
private config: SessionConfig;
|
|
246
|
+
private process: PiProcessNode | null = null;
|
|
247
|
+
|
|
248
|
+
constructor(config: SessionConfig) {
|
|
249
|
+
super({ parentSessionId: config.parentSessionId });
|
|
250
|
+
this.config = config;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async *query(prompt: string): AsyncIterable<AIMessage> {
|
|
254
|
+
const { mapPiEvent } = await import("./pi-events.ts");
|
|
255
|
+
|
|
256
|
+
const started = this.startQuery();
|
|
257
|
+
if (!started) {
|
|
258
|
+
yield BaseSession.BUSY_ERROR;
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
const { gen } = started;
|
|
262
|
+
|
|
263
|
+
try {
|
|
264
|
+
if (!this.process || !this.process.alive) {
|
|
265
|
+
this.process = new PiProcessNode();
|
|
266
|
+
await this.process.spawn(this.config.piExecutablePath, this.config.cwd);
|
|
267
|
+
|
|
268
|
+
if (this.config.model) {
|
|
269
|
+
const [provider, ...rest] = this.config.model.split("/");
|
|
270
|
+
const modelId = rest.join("/");
|
|
271
|
+
if (provider && modelId) {
|
|
272
|
+
try {
|
|
273
|
+
await this.process.sendAndWait({ type: "set_model", provider, modelId });
|
|
274
|
+
} catch { /* Continue with Pi's default model */ }
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
try {
|
|
279
|
+
const state = await this.process.sendAndWait({ type: "get_state" });
|
|
280
|
+
if (typeof state.sessionId === "string") {
|
|
281
|
+
this.resolveId(state.sessionId);
|
|
282
|
+
}
|
|
283
|
+
} catch { /* Continue with placeholder ID */ }
|
|
284
|
+
|
|
285
|
+
if (!this.process.alive) {
|
|
286
|
+
yield {
|
|
287
|
+
type: "error",
|
|
288
|
+
error: "Pi process exited during startup. Check that Pi is configured correctly (API keys, models).",
|
|
289
|
+
code: "pi_startup_error",
|
|
290
|
+
};
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const effectivePrompt = buildEffectivePrompt(
|
|
296
|
+
prompt,
|
|
297
|
+
this.config.systemPrompt,
|
|
298
|
+
this._firstQuerySent,
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
const queue: AIMessage[] = [];
|
|
302
|
+
let resolve: (() => void) | null = null;
|
|
303
|
+
let done = false;
|
|
304
|
+
|
|
305
|
+
const push = (msg: AIMessage) => { queue.push(msg); resolve?.(); };
|
|
306
|
+
const finish = () => { done = true; resolve?.(); };
|
|
307
|
+
|
|
308
|
+
const unsubscribe = this.process.onEvent((event) => {
|
|
309
|
+
const mapped = mapPiEvent(event, this.id);
|
|
310
|
+
for (const msg of mapped) {
|
|
311
|
+
push(msg);
|
|
312
|
+
if (
|
|
313
|
+
msg.type === "result" ||
|
|
314
|
+
(msg.type === "error" && (event.type === "agent_end" || event.type === "process_exited"))
|
|
315
|
+
) {
|
|
316
|
+
finish();
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
try {
|
|
322
|
+
await this.process.sendAndWait({ type: "prompt", message: effectivePrompt });
|
|
323
|
+
} catch (err) {
|
|
324
|
+
unsubscribe();
|
|
325
|
+
yield {
|
|
326
|
+
type: "error",
|
|
327
|
+
error: `Pi rejected prompt: ${err instanceof Error ? err.message : String(err)}`,
|
|
328
|
+
code: "pi_prompt_rejected",
|
|
329
|
+
};
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
this._firstQuerySent = true;
|
|
333
|
+
|
|
334
|
+
try {
|
|
335
|
+
while (!done || queue.length > 0) {
|
|
336
|
+
if (queue.length > 0) {
|
|
337
|
+
yield queue.shift()!;
|
|
338
|
+
} else {
|
|
339
|
+
await new Promise<void>((r) => { resolve = r; });
|
|
340
|
+
resolve = null;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
} finally {
|
|
344
|
+
unsubscribe();
|
|
345
|
+
}
|
|
346
|
+
} catch (err) {
|
|
347
|
+
yield {
|
|
348
|
+
type: "error",
|
|
349
|
+
error: err instanceof Error ? err.message : String(err),
|
|
350
|
+
code: "provider_error",
|
|
351
|
+
};
|
|
352
|
+
} finally {
|
|
353
|
+
this.endQuery(gen);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
abort(): void {
|
|
358
|
+
if (this.process?.alive) {
|
|
359
|
+
this.process.send({ type: "abort" });
|
|
360
|
+
}
|
|
361
|
+
super.abort();
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
killProcess(): void {
|
|
365
|
+
this.process?.kill();
|
|
366
|
+
this.process = null;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// ---------------------------------------------------------------------------
|
|
371
|
+
// Factory registration
|
|
372
|
+
// ---------------------------------------------------------------------------
|
|
373
|
+
|
|
374
|
+
registerProviderFactory(
|
|
375
|
+
PROVIDER_NAME,
|
|
376
|
+
async (config) => new PiSDKNodeProvider(config as PiSDKConfig),
|
|
377
|
+
);
|