pi-acp 0.0.9

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/dist/index.js ADDED
@@ -0,0 +1,1415 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { AgentSideConnection, ndJsonStream } from "@agentclientprotocol/sdk";
5
+
6
+ // src/acp/agent.ts
7
+ import {
8
+ RequestError as RequestError2
9
+ } from "@agentclientprotocol/sdk";
10
+
11
+ // src/acp/session.ts
12
+ import { RequestError } from "@agentclientprotocol/sdk";
13
+ import { readFileSync as readFileSync3 } from "fs";
14
+ import { isAbsolute, resolve as resolvePath } from "path";
15
+
16
+ // src/pi-rpc/process.ts
17
+ import { spawn } from "child_process";
18
+ import * as readline from "readline";
19
+ var PiRpcProcess = class _PiRpcProcess {
20
+ child;
21
+ pending = /* @__PURE__ */ new Map();
22
+ eventHandlers = [];
23
+ constructor(child) {
24
+ this.child = child;
25
+ const rl = readline.createInterface({ input: child.stdout });
26
+ rl.on("line", (line) => {
27
+ if (!line.trim()) return;
28
+ let msg;
29
+ try {
30
+ msg = JSON.parse(line);
31
+ } catch {
32
+ return;
33
+ }
34
+ if (msg?.type === "response") {
35
+ const id = typeof msg.id === "string" ? msg.id : void 0;
36
+ if (id) {
37
+ const pending = this.pending.get(id);
38
+ if (pending) {
39
+ this.pending.delete(id);
40
+ pending.resolve(msg);
41
+ return;
42
+ }
43
+ }
44
+ }
45
+ for (const h of this.eventHandlers) h(msg);
46
+ });
47
+ child.on("exit", (code, signal) => {
48
+ const err = new Error(`pi process exited (code=${code}, signal=${signal})`);
49
+ for (const [, p] of this.pending) p.reject(err);
50
+ this.pending.clear();
51
+ });
52
+ }
53
+ static async spawn(params) {
54
+ const cmd = params.piCommand ?? "pi";
55
+ const args = ["--mode", "rpc"];
56
+ if (params.sessionPath) args.push("--session", params.sessionPath);
57
+ const child = spawn(cmd, args, {
58
+ cwd: params.cwd,
59
+ stdio: "pipe",
60
+ env: process.env
61
+ });
62
+ child.stderr.on("data", () => {
63
+ });
64
+ const proc = new _PiRpcProcess(child);
65
+ try {
66
+ const state = await proc.getState();
67
+ const sessionFile = typeof state?.sessionFile === "string" ? state.sessionFile : null;
68
+ if (sessionFile) {
69
+ const { mkdirSync: mkdirSync2 } = await import("fs");
70
+ const { dirname: dirname3 } = await import("path");
71
+ mkdirSync2(dirname3(sessionFile), { recursive: true });
72
+ }
73
+ } catch {
74
+ }
75
+ return proc;
76
+ }
77
+ onEvent(handler) {
78
+ this.eventHandlers.push(handler);
79
+ return () => {
80
+ this.eventHandlers = this.eventHandlers.filter((h) => h !== handler);
81
+ };
82
+ }
83
+ async prompt(message, attachments = []) {
84
+ const res = await this.request({ type: "prompt", message, attachments });
85
+ if (!res.success) throw new Error(`pi prompt failed: ${res.error ?? JSON.stringify(res.data)}`);
86
+ }
87
+ async abort() {
88
+ const res = await this.request({ type: "abort" });
89
+ if (!res.success) throw new Error(`pi abort failed: ${res.error ?? JSON.stringify(res.data)}`);
90
+ }
91
+ async getState() {
92
+ const res = await this.request({ type: "get_state" });
93
+ if (!res.success) throw new Error(`pi get_state failed: ${res.error ?? JSON.stringify(res.data)}`);
94
+ return res.data;
95
+ }
96
+ async getAvailableModels() {
97
+ const res = await this.request({ type: "get_available_models" });
98
+ if (!res.success) throw new Error(`pi get_available_models failed: ${res.error ?? JSON.stringify(res.data)}`);
99
+ return res.data;
100
+ }
101
+ async setModel(provider, modelId) {
102
+ const res = await this.request({ type: "set_model", provider, modelId });
103
+ if (!res.success) throw new Error(`pi set_model failed: ${res.error ?? JSON.stringify(res.data)}`);
104
+ return res.data;
105
+ }
106
+ async setThinkingLevel(level) {
107
+ const res = await this.request({ type: "set_thinking_level", level });
108
+ if (!res.success) throw new Error(`pi set_thinking_level failed: ${res.error ?? JSON.stringify(res.data)}`);
109
+ }
110
+ async setQueueMode(mode) {
111
+ const res = await this.request({ type: "set_queue_mode", mode });
112
+ if (!res.success) throw new Error(`pi set_queue_mode failed: ${res.error ?? JSON.stringify(res.data)}`);
113
+ }
114
+ async compact(customInstructions) {
115
+ const res = await this.request({ type: "compact", customInstructions });
116
+ if (!res.success) throw new Error(`pi compact failed: ${res.error ?? JSON.stringify(res.data)}`);
117
+ return res.data;
118
+ }
119
+ async setAutoCompaction(enabled) {
120
+ const res = await this.request({ type: "set_auto_compaction", enabled });
121
+ if (!res.success) throw new Error(`pi set_auto_compaction failed: ${res.error ?? JSON.stringify(res.data)}`);
122
+ }
123
+ async getSessionStats() {
124
+ const res = await this.request({ type: "get_session_stats" });
125
+ if (!res.success) throw new Error(`pi get_session_stats failed: ${res.error ?? JSON.stringify(res.data)}`);
126
+ return res.data;
127
+ }
128
+ async exportHtml(outputPath) {
129
+ const res = await this.request({ type: "export_html", outputPath });
130
+ if (!res.success) throw new Error(`pi export_html failed: ${res.error ?? JSON.stringify(res.data)}`);
131
+ const data = res.data;
132
+ return { path: String(data?.path ?? "") };
133
+ }
134
+ async switchSession(sessionPath) {
135
+ const res = await this.request({ type: "switch_session", sessionPath });
136
+ if (!res.success) throw new Error(`pi switch_session failed: ${res.error ?? JSON.stringify(res.data)}`);
137
+ }
138
+ async getMessages() {
139
+ const res = await this.request({ type: "get_messages" });
140
+ if (!res.success) throw new Error(`pi get_messages failed: ${res.error ?? JSON.stringify(res.data)}`);
141
+ return res.data;
142
+ }
143
+ request(cmd) {
144
+ const id = crypto.randomUUID();
145
+ const withId = { ...cmd, id };
146
+ const line = JSON.stringify(withId) + "\n";
147
+ return new Promise((resolve2, reject) => {
148
+ this.pending.set(id, { resolve: resolve2, reject });
149
+ this.child.stdin.write(line, (err) => {
150
+ if (err) {
151
+ this.pending.delete(id);
152
+ reject(err);
153
+ }
154
+ });
155
+ });
156
+ }
157
+ };
158
+
159
+ // src/acp/session-store.ts
160
+ import { mkdirSync, readFileSync, writeFileSync } from "fs";
161
+ import { dirname } from "path";
162
+
163
+ // src/acp/paths.ts
164
+ import { homedir } from "os";
165
+ import { join } from "path";
166
+ function getPiAcpDir() {
167
+ return join(homedir(), ".pi", "pi-acp");
168
+ }
169
+ function getPiAcpSessionMapPath() {
170
+ return join(getPiAcpDir(), "session-map.json");
171
+ }
172
+
173
+ // src/acp/session-store.ts
174
+ function ensureParentDir(path) {
175
+ mkdirSync(dirname(path), { recursive: true });
176
+ }
177
+ function loadFile(path) {
178
+ try {
179
+ const raw = readFileSync(path, "utf-8");
180
+ const parsed = JSON.parse(raw);
181
+ if (parsed?.version !== 1 || typeof parsed.sessions !== "object" || !parsed.sessions) {
182
+ return { version: 1, sessions: {} };
183
+ }
184
+ return parsed;
185
+ } catch {
186
+ return { version: 1, sessions: {} };
187
+ }
188
+ }
189
+ function saveFile(path, data) {
190
+ ensureParentDir(path);
191
+ writeFileSync(path, JSON.stringify(data, null, 2) + "\n", "utf-8");
192
+ }
193
+ var SessionStore = class {
194
+ path;
195
+ constructor(path = getPiAcpSessionMapPath()) {
196
+ this.path = path;
197
+ }
198
+ get(sessionId) {
199
+ const db = loadFile(this.path);
200
+ return db.sessions[sessionId] ?? null;
201
+ }
202
+ upsert(entry) {
203
+ const db = loadFile(this.path);
204
+ db.sessions[entry.sessionId] = {
205
+ sessionId: entry.sessionId,
206
+ cwd: entry.cwd,
207
+ sessionFile: entry.sessionFile,
208
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
209
+ };
210
+ saveFile(this.path, db);
211
+ }
212
+ };
213
+
214
+ // src/acp/translate/pi-tools.ts
215
+ function toolResultToText(result) {
216
+ if (!result) return "";
217
+ const content = result.content;
218
+ if (Array.isArray(content)) {
219
+ const texts = content.map((c) => c?.type === "text" && typeof c.text === "string" ? c.text : "").filter(Boolean);
220
+ if (texts.length) return texts.join("");
221
+ }
222
+ const details = result?.details;
223
+ const diff = details?.diff;
224
+ if (typeof diff === "string" && diff.trim()) {
225
+ return diff;
226
+ }
227
+ const stdout = (typeof details?.stdout === "string" ? details.stdout : void 0) ?? (typeof result?.stdout === "string" ? result.stdout : void 0) ?? (typeof details?.output === "string" ? details.output : void 0) ?? (typeof result?.output === "string" ? result.output : void 0);
228
+ const stderr = (typeof details?.stderr === "string" ? details.stderr : void 0) ?? (typeof result?.stderr === "string" ? result.stderr : void 0);
229
+ const exitCode = (typeof details?.exitCode === "number" ? details.exitCode : void 0) ?? (typeof result?.exitCode === "number" ? result.exitCode : void 0) ?? (typeof details?.code === "number" ? details.code : void 0) ?? (typeof result?.code === "number" ? result.code : void 0);
230
+ if (typeof stdout === "string" && stdout.trim() || typeof stderr === "string" && stderr.trim()) {
231
+ const parts = [];
232
+ if (typeof stdout === "string" && stdout.trim()) parts.push(stdout);
233
+ if (typeof stderr === "string" && stderr.trim()) parts.push(`stderr:
234
+ ${stderr}`);
235
+ if (typeof exitCode === "number") parts.push(`exit code: ${exitCode}`);
236
+ return parts.join("\n\n").trimEnd();
237
+ }
238
+ try {
239
+ return JSON.stringify(result, null, 2);
240
+ } catch {
241
+ return String(result);
242
+ }
243
+ }
244
+
245
+ // src/acp/slash-commands.ts
246
+ import { existsSync, readdirSync, readFileSync as readFileSync2 } from "fs";
247
+ import { homedir as homedir2 } from "os";
248
+ import { join as join2, resolve } from "path";
249
+ function parseFrontmatter(content) {
250
+ const frontmatter = {};
251
+ if (!content.startsWith("---")) return { frontmatter, content };
252
+ const endIndex = content.indexOf("\n---", 3);
253
+ if (endIndex === -1) return { frontmatter, content };
254
+ const frontmatterBlock = content.slice(4, endIndex);
255
+ const remaining = content.slice(endIndex + 4).trim();
256
+ for (const line of frontmatterBlock.split("\n")) {
257
+ const match = line.match(/^(\w+):\s*(.*)$/);
258
+ if (match) frontmatter[match[1]] = match[2].trim();
259
+ }
260
+ return { frontmatter, content: remaining };
261
+ }
262
+ function loadCommandsFromDir(dir, source, subdir = "") {
263
+ const commands = [];
264
+ if (!existsSync(dir)) return commands;
265
+ try {
266
+ const entries = readdirSync(dir, { withFileTypes: true });
267
+ for (const entry of entries) {
268
+ const fullPath = join2(dir, entry.name);
269
+ if (entry.isDirectory()) {
270
+ const newSubdir = subdir ? `${subdir}:${entry.name}` : entry.name;
271
+ commands.push(...loadCommandsFromDir(fullPath, source, newSubdir));
272
+ continue;
273
+ }
274
+ if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
275
+ try {
276
+ const rawContent = readFileSync2(fullPath, "utf-8");
277
+ const { frontmatter, content } = parseFrontmatter(rawContent);
278
+ const name = entry.name.slice(0, -3);
279
+ const sourceStr = source === "user" ? subdir ? `(user:${subdir})` : "(user)" : subdir ? `(project:${subdir})` : "(project)";
280
+ let description = frontmatter.description || "";
281
+ if (!description) {
282
+ const firstLine = content.split("\n").find((l) => l.trim());
283
+ if (firstLine) {
284
+ description = firstLine.slice(0, 60);
285
+ if (firstLine.length > 60) description += "...";
286
+ }
287
+ }
288
+ description = description ? `${description} ${sourceStr}` : sourceStr;
289
+ commands.push({
290
+ name,
291
+ description,
292
+ content,
293
+ source: sourceStr
294
+ });
295
+ } catch {
296
+ }
297
+ }
298
+ } catch {
299
+ }
300
+ return commands;
301
+ }
302
+ function loadSlashCommands(cwd) {
303
+ const commands = [];
304
+ const userDir = join2(homedir2(), ".pi", "agent", "commands");
305
+ const projectDir = resolve(cwd, ".pi", "commands");
306
+ commands.push(...loadCommandsFromDir(userDir, "user"));
307
+ commands.push(...loadCommandsFromDir(projectDir, "project"));
308
+ return commands;
309
+ }
310
+ function toAvailableCommands(fileCommands) {
311
+ const seen = /* @__PURE__ */ new Set();
312
+ const out = [];
313
+ for (const c of fileCommands) {
314
+ if (seen.has(c.name)) continue;
315
+ seen.add(c.name);
316
+ out.push({
317
+ name: c.name,
318
+ description: c.description
319
+ // input: omitted for now (pi commands don't specify this)
320
+ });
321
+ }
322
+ return out;
323
+ }
324
+ function parseCommandArgs(argsString) {
325
+ const args = [];
326
+ let current = "";
327
+ let inQuote = null;
328
+ for (let i = 0; i < argsString.length; i++) {
329
+ const ch = argsString[i];
330
+ if (inQuote) {
331
+ if (ch === inQuote) inQuote = null;
332
+ else current += ch;
333
+ continue;
334
+ }
335
+ if (ch === '"' || ch === "'") {
336
+ inQuote = ch;
337
+ } else if (ch === " " || ch === " ") {
338
+ if (current) {
339
+ args.push(current);
340
+ current = "";
341
+ }
342
+ } else {
343
+ current += ch;
344
+ }
345
+ }
346
+ if (current) args.push(current);
347
+ return args;
348
+ }
349
+ function substituteArgs(content, args) {
350
+ let result = content;
351
+ result = result.replace(/\$@/g, args.join(" "));
352
+ result = result.replace(/\$(\d+)/g, (_m, num) => {
353
+ const idx = Number.parseInt(String(num), 10) - 1;
354
+ return args[idx] ?? "";
355
+ });
356
+ return result;
357
+ }
358
+ function expandSlashCommand(text, fileCommands) {
359
+ if (!text.startsWith("/")) return text;
360
+ const spaceIndex = text.indexOf(" ");
361
+ const commandName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);
362
+ const argsString = spaceIndex === -1 ? "" : text.slice(spaceIndex + 1);
363
+ const cmd = fileCommands.find((c) => c.name === commandName);
364
+ if (!cmd) return text;
365
+ const args = parseCommandArgs(argsString);
366
+ return substituteArgs(cmd.content, args);
367
+ }
368
+
369
+ // src/acp/session.ts
370
+ var SessionManager = class {
371
+ sessions = /* @__PURE__ */ new Map();
372
+ store = new SessionStore();
373
+ /** Get a registered session if it exists (no throw). */
374
+ maybeGet(sessionId) {
375
+ return this.sessions.get(sessionId);
376
+ }
377
+ async create(params) {
378
+ const proc = await PiRpcProcess.spawn({
379
+ cwd: params.cwd
380
+ });
381
+ const state = await proc.getState();
382
+ const sessionId = typeof state?.sessionId === "string" ? state.sessionId : crypto.randomUUID();
383
+ const sessionFile = typeof state?.sessionFile === "string" ? state.sessionFile : null;
384
+ if (sessionFile) {
385
+ this.store.upsert({ sessionId, cwd: params.cwd, sessionFile });
386
+ }
387
+ const session = new PiAcpSession({
388
+ sessionId,
389
+ cwd: params.cwd,
390
+ mcpServers: params.mcpServers,
391
+ proc,
392
+ conn: params.conn,
393
+ fileCommands: params.fileCommands ?? []
394
+ });
395
+ this.sessions.set(sessionId, session);
396
+ return session;
397
+ }
398
+ get(sessionId) {
399
+ const s = this.sessions.get(sessionId);
400
+ if (!s) throw RequestError.invalidParams(`Unknown sessionId: ${sessionId}`);
401
+ return s;
402
+ }
403
+ /**
404
+ * Used by session/load: create a session object bound to an existing sessionId/proc
405
+ * if it isn't already registered.
406
+ */
407
+ getOrCreate(sessionId, params) {
408
+ const existing = this.sessions.get(sessionId);
409
+ if (existing) return existing;
410
+ const session = new PiAcpSession({
411
+ sessionId,
412
+ cwd: params.cwd,
413
+ mcpServers: params.mcpServers,
414
+ proc: params.proc,
415
+ conn: params.conn,
416
+ fileCommands: params.fileCommands ?? []
417
+ });
418
+ this.sessions.set(sessionId, session);
419
+ return session;
420
+ }
421
+ };
422
+ var PiAcpSession = class {
423
+ sessionId;
424
+ cwd;
425
+ mcpServers;
426
+ proc;
427
+ conn;
428
+ fileCommands;
429
+ // Used to map abort semantics to ACP stopReason.
430
+ // Applies to the currently running turn.
431
+ cancelRequested = false;
432
+ // Current in-flight turn (if any). Additional prompts are queued.
433
+ pendingTurn = null;
434
+ turnQueue = [];
435
+ // Track tool call statuses and ensure they are monotonic (pending -> in_progress -> completed).
436
+ // Some pi events can arrive out of order (e.g. late toolcall_* deltas after execution starts),
437
+ // and clients may hide progress if we ever downgrade back to `pending`.
438
+ currentToolCalls = /* @__PURE__ */ new Map();
439
+ // pi can emit multiple `turn_end` events for a single user prompt (e.g. after tool_use).
440
+ // The overall agent loop completes when `agent_end` is emitted.
441
+ inAgentLoop = false;
442
+ // For ACP diff support: capture file contents before edits, then emit ToolCallContent {type:"diff"}.
443
+ // This is due to pi sending diff as a string as opposed to ACP expected diff format.
444
+ // Compatible format may need to be implemented in pi in the future.
445
+ editSnapshots = /* @__PURE__ */ new Map();
446
+ // Ensure `session/update` notifications are sent in order and can be awaited
447
+ // before completing a `session/prompt` request.
448
+ lastEmit = Promise.resolve();
449
+ constructor(opts) {
450
+ this.sessionId = opts.sessionId;
451
+ this.cwd = opts.cwd;
452
+ this.mcpServers = opts.mcpServers;
453
+ this.proc = opts.proc;
454
+ this.conn = opts.conn;
455
+ this.fileCommands = opts.fileCommands ?? [];
456
+ this.proc.onEvent((ev) => this.handlePiEvent(ev));
457
+ }
458
+ async prompt(message, attachments = []) {
459
+ const expandedMessage = expandSlashCommand(message, this.fileCommands);
460
+ const turnPromise = new Promise((resolve2, reject) => {
461
+ const queued = { message: expandedMessage, attachments, resolve: resolve2, reject };
462
+ if (this.pendingTurn) {
463
+ this.turnQueue.push(queued);
464
+ this.emit({
465
+ sessionUpdate: "agent_message_chunk",
466
+ content: {
467
+ type: "text",
468
+ text: `Queued message (position ${this.turnQueue.length}).`
469
+ }
470
+ });
471
+ this.emit({
472
+ sessionUpdate: "session_info_update",
473
+ _meta: { piAcp: { queueDepth: this.turnQueue.length, running: true } }
474
+ });
475
+ return;
476
+ }
477
+ this.startTurn(queued);
478
+ });
479
+ return turnPromise;
480
+ }
481
+ async cancel() {
482
+ this.cancelRequested = true;
483
+ if (this.turnQueue.length) {
484
+ const queued = this.turnQueue.splice(0, this.turnQueue.length);
485
+ for (const t of queued) t.resolve("cancelled");
486
+ this.emit({
487
+ sessionUpdate: "agent_message_chunk",
488
+ content: { type: "text", text: "Cleared queued prompts." }
489
+ });
490
+ this.emit({
491
+ sessionUpdate: "session_info_update",
492
+ _meta: { piAcp: { queueDepth: 0, running: Boolean(this.pendingTurn) } }
493
+ });
494
+ }
495
+ await this.proc.abort();
496
+ }
497
+ wasCancelRequested() {
498
+ return this.cancelRequested;
499
+ }
500
+ emit(update) {
501
+ this.lastEmit = this.lastEmit.then(
502
+ () => this.conn.sessionUpdate({
503
+ sessionId: this.sessionId,
504
+ update
505
+ })
506
+ ).catch(() => {
507
+ });
508
+ }
509
+ async flushEmits() {
510
+ await this.lastEmit;
511
+ }
512
+ startTurn(t) {
513
+ this.cancelRequested = false;
514
+ this.inAgentLoop = false;
515
+ this.pendingTurn = { resolve: t.resolve, reject: t.reject };
516
+ this.emit({
517
+ sessionUpdate: "session_info_update",
518
+ _meta: { piAcp: { queueDepth: this.turnQueue.length, running: true } }
519
+ });
520
+ this.proc.prompt(t.message, t.attachments).catch((err) => {
521
+ void this.flushEmits().finally(() => {
522
+ const reason = this.cancelRequested ? "cancelled" : "error";
523
+ this.pendingTurn?.resolve(reason);
524
+ this.pendingTurn = null;
525
+ this.inAgentLoop = false;
526
+ this.emit({
527
+ sessionUpdate: "session_info_update",
528
+ _meta: { piAcp: { queueDepth: this.turnQueue.length, running: false } }
529
+ });
530
+ });
531
+ void err;
532
+ });
533
+ }
534
+ handlePiEvent(ev) {
535
+ const type = String(ev.type ?? "");
536
+ switch (type) {
537
+ case "message_update": {
538
+ const ame = ev.assistantMessageEvent;
539
+ if (ame?.type === "text_delta" && typeof ame.delta === "string") {
540
+ this.emit({
541
+ sessionUpdate: "agent_message_chunk",
542
+ content: { type: "text", text: ame.delta }
543
+ });
544
+ break;
545
+ }
546
+ if (ame?.type === "toolcall_start" || ame?.type === "toolcall_delta" || ame?.type === "toolcall_end") {
547
+ const toolCall = (
548
+ // pi sometimes includes the tool call directly on the event
549
+ ame?.toolCall ?? // ...and always includes it in the partial assistant message at contentIndex
550
+ ame?.partial?.content?.[ame?.contentIndex ?? 0]
551
+ );
552
+ const toolCallId = String(toolCall?.id ?? "");
553
+ const toolName = String(toolCall?.name ?? "tool");
554
+ if (toolCallId) {
555
+ const rawInput = toolCall?.arguments && typeof toolCall.arguments === "object" ? toolCall.arguments : (() => {
556
+ const s = String(toolCall?.partialArgs ?? "");
557
+ if (!s) return void 0;
558
+ try {
559
+ return JSON.parse(s);
560
+ } catch {
561
+ return { partialArgs: s };
562
+ }
563
+ })();
564
+ const existingStatus = this.currentToolCalls.get(toolCallId);
565
+ const status = existingStatus ?? "pending";
566
+ if (!existingStatus) {
567
+ this.currentToolCalls.set(toolCallId, "pending");
568
+ this.emit({
569
+ sessionUpdate: "tool_call",
570
+ toolCallId,
571
+ title: toolName,
572
+ kind: toToolKind(toolName),
573
+ status,
574
+ rawInput
575
+ });
576
+ } else {
577
+ this.emit({
578
+ sessionUpdate: "tool_call_update",
579
+ toolCallId,
580
+ status,
581
+ rawInput
582
+ });
583
+ }
584
+ }
585
+ break;
586
+ }
587
+ break;
588
+ }
589
+ case "tool_execution_start": {
590
+ const toolCallId = String(ev.toolCallId ?? crypto.randomUUID());
591
+ const toolName = String(ev.toolName ?? "tool");
592
+ const args = ev.args;
593
+ if (toolName === "edit") {
594
+ const p = typeof args?.path === "string" ? args.path : void 0;
595
+ if (p) {
596
+ try {
597
+ const abs = isAbsolute(p) ? p : resolvePath(this.cwd, p);
598
+ const oldText = readFileSync3(abs, "utf8");
599
+ this.editSnapshots.set(toolCallId, { path: p, oldText });
600
+ } catch {
601
+ }
602
+ }
603
+ }
604
+ if (!this.currentToolCalls.has(toolCallId)) {
605
+ this.currentToolCalls.set(toolCallId, "in_progress");
606
+ this.emit({
607
+ sessionUpdate: "tool_call",
608
+ toolCallId,
609
+ title: toolName,
610
+ kind: toToolKind(toolName),
611
+ status: "in_progress",
612
+ rawInput: args
613
+ });
614
+ } else {
615
+ this.currentToolCalls.set(toolCallId, "in_progress");
616
+ this.emit({
617
+ sessionUpdate: "tool_call_update",
618
+ toolCallId,
619
+ status: "in_progress",
620
+ rawInput: args
621
+ });
622
+ }
623
+ break;
624
+ }
625
+ case "tool_execution_update": {
626
+ const toolCallId = String(ev.toolCallId ?? "");
627
+ if (!toolCallId) break;
628
+ const partial = ev.partialResult;
629
+ const text = toolResultToText(partial);
630
+ this.emit({
631
+ sessionUpdate: "tool_call_update",
632
+ toolCallId,
633
+ status: "in_progress",
634
+ content: text ? [{ type: "content", content: { type: "text", text } }] : void 0,
635
+ rawOutput: partial
636
+ });
637
+ break;
638
+ }
639
+ case "tool_execution_end": {
640
+ const toolCallId = String(ev.toolCallId ?? "");
641
+ if (!toolCallId) break;
642
+ const result = ev.result;
643
+ const isError = Boolean(ev.isError);
644
+ const text = toolResultToText(result);
645
+ const snapshot = this.editSnapshots.get(toolCallId);
646
+ let content;
647
+ if (!isError && snapshot) {
648
+ try {
649
+ const abs = isAbsolute(snapshot.path) ? snapshot.path : resolvePath(this.cwd, snapshot.path);
650
+ const newText = readFileSync3(abs, "utf8");
651
+ if (newText !== snapshot.oldText) {
652
+ content = [
653
+ {
654
+ type: "diff",
655
+ path: snapshot.path,
656
+ oldText: snapshot.oldText,
657
+ newText
658
+ },
659
+ ...text ? [{ type: "content", content: { type: "text", text } }] : []
660
+ ];
661
+ }
662
+ } catch {
663
+ }
664
+ }
665
+ if (!content && text) {
666
+ content = [{ type: "content", content: { type: "text", text } }];
667
+ }
668
+ this.emit({
669
+ sessionUpdate: "tool_call_update",
670
+ toolCallId,
671
+ status: isError ? "failed" : "completed",
672
+ content,
673
+ rawOutput: result
674
+ });
675
+ this.currentToolCalls.delete(toolCallId);
676
+ this.editSnapshots.delete(toolCallId);
677
+ break;
678
+ }
679
+ case "agent_start": {
680
+ this.inAgentLoop = true;
681
+ break;
682
+ }
683
+ case "turn_end": {
684
+ break;
685
+ }
686
+ case "agent_end": {
687
+ void this.flushEmits().finally(() => {
688
+ const reason = this.cancelRequested ? "cancelled" : "end_turn";
689
+ this.pendingTurn?.resolve(reason);
690
+ this.pendingTurn = null;
691
+ this.inAgentLoop = false;
692
+ const next = this.turnQueue.shift();
693
+ if (next) {
694
+ this.emit({
695
+ sessionUpdate: "agent_message_chunk",
696
+ content: { type: "text", text: `Starting queued message. (${this.turnQueue.length} remaining)` }
697
+ });
698
+ this.startTurn(next);
699
+ } else {
700
+ this.emit({
701
+ sessionUpdate: "session_info_update",
702
+ _meta: { piAcp: { queueDepth: 0, running: false } }
703
+ });
704
+ }
705
+ });
706
+ break;
707
+ }
708
+ default:
709
+ break;
710
+ }
711
+ }
712
+ };
713
+ function toToolKind(toolName) {
714
+ switch (toolName) {
715
+ case "read":
716
+ return "read";
717
+ case "write":
718
+ case "edit":
719
+ return "edit";
720
+ case "bash":
721
+ return "other";
722
+ default:
723
+ return "other";
724
+ }
725
+ }
726
+
727
+ // src/acp/translate/pi-messages.ts
728
+ function normalizePiMessageText(content) {
729
+ if (typeof content === "string") return content;
730
+ if (!Array.isArray(content)) return "";
731
+ return content.map((c) => c?.type === "text" && typeof c.text === "string" ? c.text : "").filter(Boolean).join("");
732
+ }
733
+ function normalizePiAssistantText(content) {
734
+ if (!Array.isArray(content)) return "";
735
+ return content.map((c) => c?.type === "text" && typeof c.text === "string" ? c.text : "").filter(Boolean).join("");
736
+ }
737
+
738
+ // src/acp/translate/prompt.ts
739
+ function guessFileNameFromMime(mimeType) {
740
+ const ext = mimeType === "image/png" ? "png" : mimeType === "image/jpeg" ? "jpg" : mimeType === "image/webp" ? "webp" : "bin";
741
+ return `attachment.${ext}`;
742
+ }
743
+ function promptToPiMessage(blocks) {
744
+ let message = "";
745
+ const attachments = [];
746
+ for (const b of blocks) {
747
+ switch (b.type) {
748
+ case "text":
749
+ message += b.text;
750
+ break;
751
+ case "resource_link":
752
+ message += `
753
+ [Context] ${b.uri}`;
754
+ break;
755
+ case "image": {
756
+ const id = b.uri ?? crypto.randomUUID();
757
+ const size = Buffer.byteLength(b.data, "base64");
758
+ attachments.push({
759
+ id,
760
+ type: "image",
761
+ fileName: guessFileNameFromMime(b.mimeType),
762
+ mimeType: b.mimeType,
763
+ size,
764
+ content: b.data
765
+ });
766
+ break;
767
+ }
768
+ case "resource": {
769
+ const r = b.resource;
770
+ const uri = typeof r?.uri === "string" ? r.uri : "(unknown)";
771
+ if (typeof r?.text === "string") {
772
+ const mime = typeof r?.mimeType === "string" ? r.mimeType : "text/plain";
773
+ message += `
774
+ [Embedded Context] ${uri} (${mime})
775
+ ${r.text}`;
776
+ } else if (typeof r?.blob === "string") {
777
+ const mime = typeof r?.mimeType === "string" ? r.mimeType : "application/octet-stream";
778
+ const bytes = Buffer.byteLength(r.blob, "base64");
779
+ message += `
780
+ [Embedded Context] ${uri} (${mime}, ${bytes} bytes)`;
781
+ } else {
782
+ message += `
783
+ [Embedded Context] ${uri}`;
784
+ }
785
+ break;
786
+ }
787
+ case "audio": {
788
+ const bytes = Buffer.byteLength(b.data, "base64");
789
+ message += `
790
+ [Audio] (${b.mimeType}, ${bytes} bytes) not supported by pi-acp`;
791
+ break;
792
+ }
793
+ default:
794
+ break;
795
+ }
796
+ }
797
+ return { message, attachments };
798
+ }
799
+
800
+ // src/acp/agent.ts
801
+ import { isAbsolute as isAbsolute2 } from "path";
802
+ import { existsSync as existsSync2, readFileSync as readFileSync4, realpathSync } from "fs";
803
+ import { join as join3, dirname as dirname2 } from "path";
804
+ import { spawnSync } from "child_process";
805
+ import { fileURLToPath } from "url";
806
+ function builtinAvailableCommands() {
807
+ return [
808
+ {
809
+ name: "compact",
810
+ description: "Manually compact the session context",
811
+ input: { hint: "optional custom instructions" }
812
+ },
813
+ {
814
+ name: "autocompact",
815
+ description: "Toggle automatic context compaction",
816
+ input: { hint: "on|off|toggle" }
817
+ },
818
+ {
819
+ name: "export",
820
+ description: "Export session to an HTML file in the session cwd"
821
+ },
822
+ {
823
+ name: "session",
824
+ description: "Show session stats (messages, tokens, cost, session file)"
825
+ },
826
+ {
827
+ name: "queue",
828
+ description: "Set pi message queue mode (all | one-at-a-time(recommended))",
829
+ input: { hint: "all | one-at-a-time" }
830
+ },
831
+ {
832
+ name: "changelog",
833
+ description: "Show pi changelog"
834
+ }
835
+ ];
836
+ }
837
+ function mergeCommands(a, b) {
838
+ const out = [];
839
+ const seen = /* @__PURE__ */ new Set();
840
+ for (const c of [...a, ...b]) {
841
+ if (seen.has(c.name)) continue;
842
+ seen.add(c.name);
843
+ out.push(c);
844
+ }
845
+ return out;
846
+ }
847
+ var pkg = readNearestPackageJson(import.meta.url);
848
+ var PiAcpAgent = class {
849
+ conn;
850
+ sessions = new SessionManager();
851
+ store = new SessionStore();
852
+ constructor(conn) {
853
+ this.conn = conn;
854
+ }
855
+ async initialize(params) {
856
+ const supportedVersion = 1;
857
+ const requested = params.protocolVersion;
858
+ return {
859
+ protocolVersion: requested === supportedVersion ? requested : supportedVersion,
860
+ agentInfo: {
861
+ name: pkg.name ?? "pi-acp",
862
+ title: "pi ACP adapter",
863
+ version: pkg.version ?? "0.0.0"
864
+ },
865
+ authMethods: [],
866
+ agentCapabilities: {
867
+ loadSession: true,
868
+ mcpCapabilities: { http: false, sse: false },
869
+ promptCapabilities: {
870
+ image: true,
871
+ audio: false,
872
+ embeddedContext: false
873
+ },
874
+ sessionCapabilities: {}
875
+ }
876
+ };
877
+ }
878
+ async newSession(params) {
879
+ if (!isAbsolute2(params.cwd)) {
880
+ throw RequestError2.invalidParams(`cwd must be an absolute path: ${params.cwd}`);
881
+ }
882
+ const fileCommands = loadSlashCommands(params.cwd);
883
+ const session = await this.sessions.create({
884
+ cwd: params.cwd,
885
+ mcpServers: params.mcpServers,
886
+ conn: this.conn,
887
+ fileCommands
888
+ });
889
+ const models = await getModelState(session.proc);
890
+ const thinking = await getThinkingState(session.proc);
891
+ const response = {
892
+ sessionId: session.sessionId,
893
+ models,
894
+ modes: thinking,
895
+ _meta: {}
896
+ };
897
+ setTimeout(() => {
898
+ void this.conn.sessionUpdate({
899
+ sessionId: session.sessionId,
900
+ update: {
901
+ sessionUpdate: "available_commands_update",
902
+ availableCommands: mergeCommands(toAvailableCommands(fileCommands), builtinAvailableCommands())
903
+ }
904
+ });
905
+ }, 0);
906
+ return response;
907
+ }
908
+ async authenticate(_params) {
909
+ return;
910
+ }
911
+ async prompt(params) {
912
+ const session = this.sessions.get(params.sessionId);
913
+ const { message, attachments } = promptToPiMessage(params.prompt);
914
+ if (attachments.length === 0 && message.trimStart().startsWith("/")) {
915
+ const trimmed = message.trim();
916
+ const space = trimmed.indexOf(" ");
917
+ const cmd = space === -1 ? trimmed.slice(1) : trimmed.slice(1, space);
918
+ const argsString = space === -1 ? "" : trimmed.slice(space + 1);
919
+ const args = parseCommandArgs(argsString);
920
+ if (cmd === "compact") {
921
+ const customInstructions = args.join(" ").trim() || void 0;
922
+ const res = await session.proc.compact(customInstructions);
923
+ const r = res && typeof res === "object" ? res : null;
924
+ const tokensBefore = typeof r?.tokensBefore === "number" ? r.tokensBefore : null;
925
+ const summary = typeof r?.summary === "string" ? r.summary : null;
926
+ const headerLines = [
927
+ `Compaction completed.${customInstructions ? " (custom instructions applied)" : ""}`,
928
+ tokensBefore !== null ? `Tokens before: ${tokensBefore}` : null
929
+ ].filter(Boolean);
930
+ const text = headerLines.join("\n") + (summary ? `
931
+
932
+ ${summary}` : "");
933
+ await this.conn.sessionUpdate({
934
+ sessionId: session.sessionId,
935
+ update: {
936
+ sessionUpdate: "agent_message_chunk",
937
+ content: { type: "text", text }
938
+ }
939
+ });
940
+ return { stopReason: "end_turn" };
941
+ }
942
+ if (cmd === "session") {
943
+ const stats = await session.proc.getSessionStats();
944
+ const lines = [];
945
+ if (stats?.sessionId) lines.push(`Session: ${stats.sessionId}`);
946
+ if (stats?.sessionFile) lines.push(`Session file: ${stats.sessionFile}`);
947
+ if (typeof stats?.totalMessages === "number") lines.push(`Messages: ${stats.totalMessages}`);
948
+ if (typeof stats?.cost === "number") lines.push(`Cost: ${stats.cost}`);
949
+ const t = stats?.tokens;
950
+ if (t && typeof t === "object") {
951
+ const parts = [];
952
+ if (typeof t.input === "number") parts.push(`in ${t.input}`);
953
+ if (typeof t.output === "number") parts.push(`out ${t.output}`);
954
+ if (typeof t.cacheRead === "number") parts.push(`cache read ${t.cacheRead}`);
955
+ if (typeof t.cacheWrite === "number") parts.push(`cache write ${t.cacheWrite}`);
956
+ if (typeof t.total === "number") parts.push(`total ${t.total}`);
957
+ if (parts.length) lines.push(`Tokens: ${parts.join(", ")}`);
958
+ }
959
+ const text = lines.length ? lines.join("\n") : `Session stats:
960
+ ${JSON.stringify(stats, null, 2)}`;
961
+ await this.conn.sessionUpdate({
962
+ sessionId: session.sessionId,
963
+ update: {
964
+ sessionUpdate: "agent_message_chunk",
965
+ content: { type: "text", text }
966
+ }
967
+ });
968
+ return { stopReason: "end_turn" };
969
+ }
970
+ if (cmd === "queue") {
971
+ const modeRaw = String(args[0] ?? "").toLowerCase();
972
+ const state = await session.proc.getState();
973
+ const current = String(state?.queueMode ?? "");
974
+ if (!modeRaw) {
975
+ await this.conn.sessionUpdate({
976
+ sessionId: session.sessionId,
977
+ update: {
978
+ sessionUpdate: "agent_message_chunk",
979
+ content: {
980
+ type: "text",
981
+ text: `Queue mode: ${current || "unknown"}`
982
+ }
983
+ }
984
+ });
985
+ return { stopReason: "end_turn" };
986
+ }
987
+ if (modeRaw !== "all" && modeRaw !== "one-at-a-time") {
988
+ await this.conn.sessionUpdate({
989
+ sessionId: session.sessionId,
990
+ update: {
991
+ sessionUpdate: "agent_message_chunk",
992
+ content: {
993
+ type: "text",
994
+ text: "Usage: /queue all | /queue one-at-a-time"
995
+ }
996
+ }
997
+ });
998
+ return { stopReason: "end_turn" };
999
+ }
1000
+ await session.proc.setQueueMode(modeRaw);
1001
+ await this.conn.sessionUpdate({
1002
+ sessionId: session.sessionId,
1003
+ update: {
1004
+ sessionUpdate: "agent_message_chunk",
1005
+ content: { type: "text", text: `Queue mode set to: ${modeRaw}` }
1006
+ }
1007
+ });
1008
+ return { stopReason: "end_turn" };
1009
+ }
1010
+ if (cmd === "changelog") {
1011
+ const findChangelog = () => {
1012
+ try {
1013
+ const whichCmd = process.platform === "win32" ? "where" : "which";
1014
+ const which = spawnSync(whichCmd, ["pi"], { encoding: "utf-8" });
1015
+ const piPath = String(which.stdout ?? "").split(/\r?\n/)[0]?.trim();
1016
+ if (piPath) {
1017
+ const resolved = realpathSync(piPath);
1018
+ const pkgRoot = dirname2(dirname2(resolved));
1019
+ const p = join3(pkgRoot, "CHANGELOG.md");
1020
+ if (existsSync2(p)) return p;
1021
+ }
1022
+ } catch {
1023
+ }
1024
+ try {
1025
+ const npmRoot = spawnSync("npm", ["root", "-g"], { encoding: "utf-8" });
1026
+ const root = String(npmRoot.stdout ?? "").trim();
1027
+ if (root) {
1028
+ const p = join3(root, "@mariozechner", "pi-coding-agent", "CHANGELOG.md");
1029
+ if (existsSync2(p)) return p;
1030
+ }
1031
+ } catch {
1032
+ }
1033
+ return null;
1034
+ };
1035
+ const changelogPath = findChangelog();
1036
+ if (!changelogPath) {
1037
+ await this.conn.sessionUpdate({
1038
+ sessionId: session.sessionId,
1039
+ update: {
1040
+ sessionUpdate: "agent_message_chunk",
1041
+ content: { type: "text", text: "Changelog not found (couldn't locate pi installation)." }
1042
+ }
1043
+ });
1044
+ return { stopReason: "end_turn" };
1045
+ }
1046
+ let text = "";
1047
+ try {
1048
+ text = readFileSync4(changelogPath, "utf-8");
1049
+ } catch (e) {
1050
+ await this.conn.sessionUpdate({
1051
+ sessionId: session.sessionId,
1052
+ update: {
1053
+ sessionUpdate: "agent_message_chunk",
1054
+ content: { type: "text", text: `Failed to read changelog: ${String(e?.message ?? e)}` }
1055
+ }
1056
+ });
1057
+ return { stopReason: "end_turn" };
1058
+ }
1059
+ const maxChars = 2e4;
1060
+ if (text.length > maxChars) text = text.slice(0, maxChars) + "\n\n...(truncated)...";
1061
+ await this.conn.sessionUpdate({
1062
+ sessionId: session.sessionId,
1063
+ update: {
1064
+ sessionUpdate: "agent_message_chunk",
1065
+ content: { type: "text", text }
1066
+ }
1067
+ });
1068
+ return { stopReason: "end_turn" };
1069
+ }
1070
+ if (cmd === "export") {
1071
+ const state = await session.proc.getState();
1072
+ const sessionFile = typeof state?.sessionFile === "string" ? state.sessionFile : null;
1073
+ const messageCount = typeof state?.messageCount === "number" ? state.messageCount : 0;
1074
+ if (!sessionFile || messageCount === 0 || !existsSync2(sessionFile)) {
1075
+ await this.conn.sessionUpdate({
1076
+ sessionId: session.sessionId,
1077
+ update: {
1078
+ sessionUpdate: "agent_message_chunk",
1079
+ content: {
1080
+ type: "text",
1081
+ text: "Nothing to export yet (no session messages). Send a prompt first."
1082
+ }
1083
+ }
1084
+ });
1085
+ return { stopReason: "end_turn" };
1086
+ }
1087
+ try {
1088
+ const raw = readFileSync4(sessionFile, "utf-8");
1089
+ if (raw.trim().length === 0) {
1090
+ await this.conn.sessionUpdate({
1091
+ sessionId: session.sessionId,
1092
+ update: {
1093
+ sessionUpdate: "agent_message_chunk",
1094
+ content: {
1095
+ type: "text",
1096
+ text: "Nothing to export yet (empty session file). Send a prompt first."
1097
+ }
1098
+ }
1099
+ });
1100
+ return { stopReason: "end_turn" };
1101
+ }
1102
+ } catch {
1103
+ await this.conn.sessionUpdate({
1104
+ sessionId: session.sessionId,
1105
+ update: {
1106
+ sessionUpdate: "agent_message_chunk",
1107
+ content: {
1108
+ type: "text",
1109
+ text: "Couldn't read session file for export. Try sending a prompt first."
1110
+ }
1111
+ }
1112
+ });
1113
+ return { stopReason: "end_turn" };
1114
+ }
1115
+ const safeSessionId = session.sessionId.replace(/[^a-zA-Z0-9_-]/g, "_");
1116
+ const outputPath = join3(session.cwd, `pi-session-${safeSessionId}.html`);
1117
+ let resultPath = "";
1118
+ try {
1119
+ const result2 = await session.proc.exportHtml(outputPath);
1120
+ resultPath = result2.path;
1121
+ } catch (e) {
1122
+ await this.conn.sessionUpdate({
1123
+ sessionId: session.sessionId,
1124
+ update: {
1125
+ sessionUpdate: "agent_message_chunk",
1126
+ content: {
1127
+ type: "text",
1128
+ text: `Export failed: ${String(e?.message ?? e)}`
1129
+ }
1130
+ }
1131
+ });
1132
+ return { stopReason: "end_turn" };
1133
+ }
1134
+ if (!resultPath) {
1135
+ await this.conn.sessionUpdate({
1136
+ sessionId: session.sessionId,
1137
+ update: {
1138
+ sessionUpdate: "agent_message_chunk",
1139
+ content: {
1140
+ type: "text",
1141
+ text: "Export failed: no output path returned by pi."
1142
+ }
1143
+ }
1144
+ });
1145
+ return { stopReason: "end_turn" };
1146
+ }
1147
+ const uri = `file://${resultPath}`;
1148
+ await this.conn.sessionUpdate({
1149
+ sessionId: session.sessionId,
1150
+ update: {
1151
+ sessionUpdate: "agent_message_chunk",
1152
+ content: {
1153
+ type: "text",
1154
+ text: "Session exported: "
1155
+ }
1156
+ }
1157
+ });
1158
+ await this.conn.sessionUpdate({
1159
+ sessionId: session.sessionId,
1160
+ update: {
1161
+ sessionUpdate: "agent_message_chunk",
1162
+ content: {
1163
+ type: "resource_link",
1164
+ name: `pi-session-${safeSessionId}.html`,
1165
+ uri,
1166
+ mimeType: "text/html",
1167
+ title: "Session exported"
1168
+ }
1169
+ }
1170
+ });
1171
+ return { stopReason: "end_turn" };
1172
+ }
1173
+ if (cmd === "autocompact") {
1174
+ const mode = (args[0] ?? "toggle").toLowerCase();
1175
+ let enabled = null;
1176
+ if (mode === "on" || mode === "true" || mode === "enable" || mode === "enabled") enabled = true;
1177
+ else if (mode === "off" || mode === "false" || mode === "disable" || mode === "disabled") enabled = false;
1178
+ if (enabled === null) {
1179
+ const state = await session.proc.getState();
1180
+ const current = Boolean(state?.autoCompactionEnabled);
1181
+ enabled = !current;
1182
+ }
1183
+ await session.proc.setAutoCompaction(enabled);
1184
+ await this.conn.sessionUpdate({
1185
+ sessionId: session.sessionId,
1186
+ update: {
1187
+ sessionUpdate: "agent_message_chunk",
1188
+ content: {
1189
+ type: "text",
1190
+ text: `Auto-compaction ${enabled ? "enabled" : "disabled"}.`
1191
+ }
1192
+ }
1193
+ });
1194
+ return { stopReason: "end_turn" };
1195
+ }
1196
+ }
1197
+ const result = await session.prompt(message, attachments);
1198
+ const stopReason = result === "error" ? session.wasCancelRequested() ? "cancelled" : "end_turn" : result;
1199
+ return { stopReason };
1200
+ }
1201
+ async cancel(params) {
1202
+ const session = this.sessions.get(params.sessionId);
1203
+ await session.cancel();
1204
+ }
1205
+ async loadSession(params) {
1206
+ if (!isAbsolute2(params.cwd)) {
1207
+ throw RequestError2.invalidParams(`cwd must be an absolute path: ${params.cwd}`);
1208
+ }
1209
+ const stored = this.store.get(params.sessionId);
1210
+ if (!stored) {
1211
+ throw RequestError2.invalidParams(`Unknown sessionId: ${params.sessionId}`);
1212
+ }
1213
+ const proc = await PiRpcProcess.spawn({
1214
+ cwd: params.cwd,
1215
+ sessionPath: stored.sessionFile
1216
+ });
1217
+ const fileCommands = loadSlashCommands(params.cwd);
1218
+ const session = this.sessions.getOrCreate(params.sessionId, {
1219
+ cwd: params.cwd,
1220
+ mcpServers: params.mcpServers,
1221
+ conn: this.conn,
1222
+ proc,
1223
+ fileCommands
1224
+ });
1225
+ this.store.upsert({
1226
+ sessionId: params.sessionId,
1227
+ cwd: params.cwd,
1228
+ sessionFile: stored.sessionFile
1229
+ });
1230
+ const data = await proc.getMessages();
1231
+ const messages = Array.isArray(data?.messages) ? data.messages : [];
1232
+ for (const m of messages) {
1233
+ const role = String(m?.role ?? "");
1234
+ if (role === "user") {
1235
+ const text = normalizePiMessageText(m?.content);
1236
+ if (text) {
1237
+ await this.conn.sessionUpdate({
1238
+ sessionId: session.sessionId,
1239
+ update: {
1240
+ sessionUpdate: "user_message_chunk",
1241
+ content: { type: "text", text }
1242
+ }
1243
+ });
1244
+ }
1245
+ }
1246
+ if (role === "assistant") {
1247
+ const text = normalizePiAssistantText(m?.content);
1248
+ if (text) {
1249
+ await this.conn.sessionUpdate({
1250
+ sessionId: session.sessionId,
1251
+ update: {
1252
+ sessionUpdate: "agent_message_chunk",
1253
+ content: { type: "text", text }
1254
+ }
1255
+ });
1256
+ }
1257
+ }
1258
+ }
1259
+ const models = await getModelState(proc);
1260
+ const thinking = await getThinkingState(proc);
1261
+ const response = {
1262
+ models,
1263
+ modes: thinking,
1264
+ _meta: {}
1265
+ };
1266
+ setTimeout(() => {
1267
+ void this.conn.sessionUpdate({
1268
+ sessionId: session.sessionId,
1269
+ update: {
1270
+ sessionUpdate: "available_commands_update",
1271
+ availableCommands: mergeCommands(toAvailableCommands(fileCommands), builtinAvailableCommands())
1272
+ }
1273
+ });
1274
+ }, 0);
1275
+ return response;
1276
+ }
1277
+ async unstable_setSessionModel(params) {
1278
+ const session = this.sessions.get(params.sessionId);
1279
+ let provider = null;
1280
+ let modelId = null;
1281
+ if (params.modelId.includes("/")) {
1282
+ const [p, ...rest] = params.modelId.split("/");
1283
+ provider = p;
1284
+ modelId = rest.join("/");
1285
+ } else {
1286
+ modelId = params.modelId;
1287
+ }
1288
+ if (!provider) {
1289
+ const data = await session.proc.getAvailableModels();
1290
+ const models = Array.isArray(data?.models) ? data.models : [];
1291
+ const found = models.find((m) => String(m?.id) === modelId);
1292
+ if (found) {
1293
+ provider = String(found.provider);
1294
+ modelId = String(found.id);
1295
+ }
1296
+ }
1297
+ if (!provider || !modelId) {
1298
+ throw RequestError2.invalidParams(`Unknown modelId: ${params.modelId}`);
1299
+ }
1300
+ await session.proc.setModel(provider, modelId);
1301
+ }
1302
+ async setSessionMode(params) {
1303
+ const session = this.sessions.get(params.sessionId);
1304
+ const mode = String(params.modeId);
1305
+ if (!isThinkingLevel(mode)) {
1306
+ throw RequestError2.invalidParams(`Unknown modeId: ${mode}`);
1307
+ }
1308
+ await session.proc.setThinkingLevel(mode);
1309
+ void this.conn.sessionUpdate({
1310
+ sessionId: session.sessionId,
1311
+ update: {
1312
+ sessionUpdate: "current_mode_update",
1313
+ currentModeId: mode
1314
+ }
1315
+ });
1316
+ return {};
1317
+ }
1318
+ };
1319
+ function isThinkingLevel(x) {
1320
+ return x === "off" || x === "minimal" || x === "low" || x === "medium" || x === "high" || x === "xhigh";
1321
+ }
1322
+ async function getThinkingState(proc) {
1323
+ let current = "medium";
1324
+ try {
1325
+ const state = await proc.getState();
1326
+ const tl = typeof state?.thinkingLevel === "string" ? state.thinkingLevel : null;
1327
+ if (tl && isThinkingLevel(tl)) current = tl;
1328
+ } catch {
1329
+ }
1330
+ const available = ["off", "minimal", "low", "medium", "high", "xhigh"];
1331
+ return {
1332
+ currentModeId: current,
1333
+ availableModes: available.map((id) => ({
1334
+ id,
1335
+ name: `Thinking: ${id}`,
1336
+ description: null
1337
+ }))
1338
+ };
1339
+ }
1340
+ async function getModelState(proc) {
1341
+ let availableModels = [];
1342
+ try {
1343
+ const data = await proc.getAvailableModels();
1344
+ const models = Array.isArray(data?.models) ? data.models : [];
1345
+ availableModels = models.map((m) => {
1346
+ const provider = String(m?.provider ?? "").trim();
1347
+ const id = String(m?.id ?? "").trim();
1348
+ if (!provider || !id) return null;
1349
+ const name = String(m?.name ?? id);
1350
+ return {
1351
+ modelId: `${provider}/${id}`,
1352
+ name: `${provider}/${name}`,
1353
+ description: null
1354
+ };
1355
+ }).filter(Boolean);
1356
+ } catch {
1357
+ }
1358
+ let currentModelId = null;
1359
+ try {
1360
+ const state = await proc.getState();
1361
+ const model = state?.model;
1362
+ if (model && typeof model === "object") {
1363
+ const provider = String(model.provider ?? "").trim();
1364
+ const id = String(model.id ?? "").trim();
1365
+ if (provider && id) currentModelId = `${provider}/${id}`;
1366
+ }
1367
+ } catch {
1368
+ }
1369
+ if (!availableModels.length && !currentModelId) return null;
1370
+ if (!currentModelId) currentModelId = availableModels[0]?.modelId ?? "default";
1371
+ return {
1372
+ availableModels,
1373
+ currentModelId
1374
+ };
1375
+ }
1376
+ function readNearestPackageJson(metaUrl) {
1377
+ try {
1378
+ let dir = dirname2(fileURLToPath(metaUrl));
1379
+ for (let i = 0; i < 6; i++) {
1380
+ const p = join3(dir, "package.json");
1381
+ if (existsSync2(p)) {
1382
+ const json = JSON.parse(readFileSync4(p, "utf-8"));
1383
+ return { name: json?.name, version: json?.version };
1384
+ }
1385
+ dir = dirname2(dir);
1386
+ }
1387
+ } catch {
1388
+ }
1389
+ return { name: "pi-acp", version: "0.0.0" };
1390
+ }
1391
+
1392
+ // src/index.ts
1393
+ var input = new WritableStream({
1394
+ write(chunk) {
1395
+ return new Promise((resolve2, reject) => {
1396
+ process.stdout.write(chunk, (err) => {
1397
+ if (err) reject(err);
1398
+ else resolve2();
1399
+ });
1400
+ });
1401
+ }
1402
+ });
1403
+ var output = new ReadableStream({
1404
+ start(controller) {
1405
+ process.stdin.on("data", (chunk) => controller.enqueue(new Uint8Array(chunk)));
1406
+ process.stdin.on("end", () => controller.close());
1407
+ process.stdin.on("error", (err) => controller.error(err));
1408
+ }
1409
+ });
1410
+ var stream = ndJsonStream(input, output);
1411
+ new AgentSideConnection((conn) => new PiAcpAgent(conn), stream);
1412
+ process.stdin.resume();
1413
+ process.on("SIGINT", () => process.exit(0));
1414
+ process.on("SIGTERM", () => process.exit(0));
1415
+ //# sourceMappingURL=index.js.map