@victor-software-house/pi-acp 0.2.0 → 0.4.0

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.mjs CHANGED
@@ -4,8 +4,7 @@ import { AgentSideConnection, RequestError, ndJsonStream } from "@agentclientpro
4
4
  import { spawnSync } from "node:child_process";
5
5
  import { existsSync, readFileSync, realpathSync } from "node:fs";
6
6
  import { dirname, isAbsolute, join, resolve } from "node:path";
7
- import { fileURLToPath } from "node:url";
8
- import { SessionManager, VERSION, createAgentSession } from "@mariozechner/pi-coding-agent";
7
+ import { SessionManager, createAgentSession } from "@mariozechner/pi-coding-agent";
9
8
  import * as z from "zod";
10
9
  //#region src/acp/auth.ts
11
10
  const AUTH_METHOD_ID = "pi_terminal_login";
@@ -64,6 +63,135 @@ function detectAuthError(err) {
64
63
  return RequestError.authRequired({ authMethods: buildAuthMethods() }, "Configure an API key or log in with an OAuth provider.");
65
64
  }
66
65
  //#endregion
66
+ //#region src/acp/client-capabilities.ts
67
+ /**
68
+ * Extract well-known capability flags from ACP `ClientCapabilities`.
69
+ *
70
+ * Reads from:
71
+ * - `_meta.terminal_output` (terminal output rendering)
72
+ * - `_meta.terminal-auth` (terminal auth with command metadata)
73
+ * - `auth._meta.gateway` (gateway auth, future use)
74
+ */
75
+ function parseClientCapabilities(caps) {
76
+ if (caps === void 0 || caps === null) return {
77
+ terminalOutput: false,
78
+ terminalAuth: false,
79
+ gatewayAuth: false
80
+ };
81
+ const meta = caps._meta;
82
+ const terminalOutput = typeof meta === "object" && meta !== null && meta["terminal_output"] === true;
83
+ const terminalAuth = typeof meta === "object" && meta !== null && meta["terminal-auth"] === true;
84
+ let gatewayAuth = false;
85
+ if ("auth" in caps) {
86
+ const auth = caps.auth;
87
+ if (typeof auth === "object" && auth !== null && "_meta" in auth) {
88
+ const authMeta = auth._meta;
89
+ if (typeof authMeta === "object" && authMeta !== null && "gateway" in authMeta) gatewayAuth = authMeta["gateway"] === true;
90
+ }
91
+ }
92
+ return {
93
+ terminalOutput,
94
+ terminalAuth,
95
+ gatewayAuth
96
+ };
97
+ }
98
+ //#endregion
99
+ //#region src/acp/model-alias.ts
100
+ /**
101
+ * Tokenize a string: split on non-alphanumeric, lowercase, strip "claude".
102
+ */
103
+ function tokenize(input) {
104
+ return input.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t !== "" && t !== "claude");
105
+ }
106
+ /**
107
+ * Extract a context hint in square brackets, e.g. "opus[1m]" -> { base: "opus", hint: "1m" }.
108
+ */
109
+ function extractContextHint(input) {
110
+ const match = /^(.+?)\[([^\]]+)\]$/.exec(input);
111
+ if (match !== null && match[1] !== void 0 && match[2] !== void 0) return {
112
+ base: match[1],
113
+ hint: match[2]
114
+ };
115
+ return {
116
+ base: input,
117
+ hint: null
118
+ };
119
+ }
120
+ /** Check if a string is purely numeric. */
121
+ function isNumeric(s) {
122
+ return /^\d+$/.test(s);
123
+ }
124
+ /**
125
+ * Score how well a model matches the given preference tokens.
126
+ *
127
+ * Returns a score >= 0 (higher is better), or -1 for no match.
128
+ * Requires at least one non-numeric token to match to avoid false positives
129
+ * from bare version numbers (e.g. "4" matching model version suffixes).
130
+ */
131
+ function scoreModel(model, prefTokens, hint) {
132
+ const modelStr = `${model.provider}/${model.id}/${model.name ?? ""}`.toLowerCase();
133
+ const modelTokens = tokenize(modelStr);
134
+ let matched = 0;
135
+ let hasNonNumericMatch = false;
136
+ for (const pt of prefTokens) if (modelTokens.some((mt) => mt.includes(pt) || pt.includes(mt))) {
137
+ matched++;
138
+ if (!isNumeric(pt)) hasNonNumericMatch = true;
139
+ }
140
+ if (matched === 0) return -1;
141
+ if (!hasNonNumericMatch) return -1;
142
+ let score = matched / prefTokens.length;
143
+ if (hint !== null && modelStr.includes(hint.toLowerCase())) score += .5;
144
+ const pref = prefTokens.join("");
145
+ if (model.id.toLowerCase().includes(pref)) score += .25;
146
+ return score;
147
+ }
148
+ /**
149
+ * Resolve a user-friendly model preference to a concrete model.
150
+ *
151
+ * Matching strategy (in order):
152
+ * 1. Exact match on "provider/id"
153
+ * 2. Exact match on "id" alone
154
+ * 3. Tokenized scored match with optional context hint
155
+ *
156
+ * Returns null if no model matches.
157
+ */
158
+ function resolveModelPreference(models, preference) {
159
+ const trimmed = preference.trim();
160
+ if (trimmed === "") return null;
161
+ if (trimmed.includes("/")) {
162
+ const [p, ...rest] = trimmed.split("/");
163
+ const provider = p ?? "";
164
+ const id = rest.join("/");
165
+ const exact = models.find((m) => m.provider.toLowerCase() === provider.toLowerCase() && m.id.toLowerCase() === id.toLowerCase());
166
+ if (exact !== void 0) return {
167
+ provider: exact.provider,
168
+ id: exact.id
169
+ };
170
+ }
171
+ const byId = models.find((m) => m.id.toLowerCase() === trimmed.toLowerCase());
172
+ if (byId !== void 0) return {
173
+ provider: byId.provider,
174
+ id: byId.id
175
+ };
176
+ const { base, hint } = extractContextHint(trimmed);
177
+ const prefTokens = tokenize(base);
178
+ if (prefTokens.length === 0) return null;
179
+ let bestModel = null;
180
+ let bestScore = -1;
181
+ for (const model of models) {
182
+ const s = scoreModel(model, prefTokens, hint);
183
+ if (s > bestScore) {
184
+ bestScore = s;
185
+ bestModel = model;
186
+ }
187
+ }
188
+ if (bestModel === null || bestScore < .5) return null;
189
+ return {
190
+ provider: bestModel.provider,
191
+ id: bestModel.id
192
+ };
193
+ }
194
+ //#endregion
67
195
  //#region src/acp/pi-settings.ts
68
196
  /**
69
197
  * Read pi settings from global and project config files.
@@ -116,69 +244,78 @@ function skillCommandsEnabled(cwd) {
116
244
  if (typeof settings.skills?.enableSkillCommands === "boolean") return settings.skills.enableSkillCommands;
117
245
  return true;
118
246
  }
119
- function quietStartupEnabled(cwd) {
120
- const settings = resolvedSettings(cwd);
121
- if (typeof settings.quietStartup === "boolean") return settings.quietStartup;
122
- if (typeof settings.quietStart === "boolean") return settings.quietStart;
123
- return false;
124
- }
125
247
  //#endregion
126
- //#region src/acp/translate/pi-tools.ts
127
- /**
128
- * Extract displayable text from a pi tool result.
129
- *
130
- * Pi tool results have varying shapes depending on the tool. This function
131
- * tries content blocks first, then falls back to details fields (diff, stdout/stderr),
132
- * and finally JSON serialization as a last resort.
133
- */
248
+ //#region src/acp/translate/tool-content.ts
134
249
  const textBlockSchema = z.object({
135
250
  type: z.literal("text"),
136
251
  text: z.string()
137
252
  });
138
- const toolDetailsSchema = z.object({
139
- diff: z.string().optional(),
253
+ const imageBlockSchema = z.object({ type: z.literal("image") });
254
+ const contentBlockSchema = z.union([textBlockSchema, imageBlockSchema]);
255
+ const bashDetailsSchema = z.object({
140
256
  stdout: z.string().optional(),
141
257
  stderr: z.string().optional(),
142
258
  output: z.string().optional(),
143
259
  exitCode: z.number().optional(),
144
260
  code: z.number().optional()
145
261
  });
146
- const toolResultSchema = z.object({
262
+ const bashResultSchema = z.object({
147
263
  content: z.array(z.unknown()).optional(),
148
- details: toolDetailsSchema.optional(),
264
+ details: bashDetailsSchema.optional(),
149
265
  stdout: z.string().optional(),
150
266
  stderr: z.string().optional(),
151
267
  output: z.string().optional(),
152
268
  exitCode: z.number().optional(),
153
269
  code: z.number().optional()
154
270
  });
155
- function toolResultToText(result) {
156
- if (result === null || result === void 0 || typeof result !== "object") return "";
157
- const parsed = toolResultSchema.safeParse(result);
158
- if (!parsed.success) try {
159
- return JSON.stringify(result, null, 2);
160
- } catch {
161
- return String(result);
162
- }
271
+ /**
272
+ * Extract stdout/stderr and exit code from a pi bash/tmux result.
273
+ */
274
+ function extractBashOutput(result) {
275
+ if (result === null || result === void 0 || typeof result !== "object") return {
276
+ output: "",
277
+ exitCode: void 0
278
+ };
279
+ const parsed = bashResultSchema.safeParse(result);
280
+ if (!parsed.success) return {
281
+ output: "",
282
+ exitCode: void 0
283
+ };
163
284
  const r = parsed.data;
285
+ const d = r.details;
164
286
  if (r.content !== void 0) {
165
287
  const texts = r.content.map((block) => textBlockSchema.safeParse(block)).filter((res) => res.success).map((res) => res.data.text);
166
- if (texts.length > 0) return texts.join("");
288
+ if (texts.length > 0) {
289
+ const exitCode = d?.exitCode ?? r.exitCode ?? d?.code ?? r.code;
290
+ return {
291
+ output: texts.join(""),
292
+ exitCode
293
+ };
294
+ }
167
295
  }
168
- const d = r.details;
169
- const diff = d?.diff;
170
- if (diff !== void 0 && diff.trim() !== "") return diff;
171
296
  const stdout = d?.stdout ?? r.stdout ?? d?.output ?? r.output;
172
297
  const stderr = d?.stderr ?? r.stderr;
173
298
  const exitCode = d?.exitCode ?? r.exitCode ?? d?.code ?? r.code;
174
- const hasStdout = stdout !== void 0 && stdout.trim() !== "";
175
- const hasStderr = stderr !== void 0 && stderr.trim() !== "";
176
- if (hasStdout || hasStderr) {
177
- const parts = [];
178
- if (hasStdout) parts.push(stdout);
179
- if (hasStderr) parts.push(`stderr:\n${stderr}`);
180
- if (exitCode !== void 0) parts.push(`exit code: ${exitCode}`);
181
- return parts.join("\n\n").trimEnd();
299
+ const parts = [];
300
+ if (stdout !== void 0 && stdout.trim() !== "") parts.push(stdout);
301
+ if (stderr !== void 0 && stderr.trim() !== "") parts.push(stderr);
302
+ return {
303
+ output: parts.join("\n"),
304
+ exitCode
305
+ };
306
+ }
307
+ /**
308
+ * Extract text content from a pi tool result (generic).
309
+ */
310
+ function extractTextContent(result) {
311
+ if (result === null || result === void 0 || typeof result !== "object") return "";
312
+ if ("content" in result && Array.isArray(result.content)) {
313
+ const texts = [];
314
+ for (const block of result.content) {
315
+ const parsed = textBlockSchema.safeParse(block);
316
+ if (parsed.success) texts.push(parsed.data.text);
317
+ }
318
+ if (texts.length > 0) return texts.join("");
182
319
  }
183
320
  try {
184
321
  return JSON.stringify(result, null, 2);
@@ -186,6 +323,164 @@ function toolResultToText(result) {
186
323
  return String(result);
187
324
  }
188
325
  }
326
+ /**
327
+ * Extract content blocks from a pi result, preserving type information.
328
+ * Used for read results where images need to be preserved.
329
+ */
330
+ function extractContentBlocks(result) {
331
+ if (result === null || result === void 0 || typeof result !== "object") return [];
332
+ if (!("content" in result) || !Array.isArray(result.content)) return [];
333
+ const blocks = [];
334
+ for (const raw of result.content) {
335
+ const parsed = contentBlockSchema.safeParse(raw);
336
+ if (parsed.success) blocks.push(parsed.data);
337
+ }
338
+ return blocks;
339
+ }
340
+ /**
341
+ * Find the longest consecutive backtick sequence in a string.
342
+ */
343
+ function longestBacktickRun(text) {
344
+ let max = 0;
345
+ let current = 0;
346
+ for (const ch of text) if (ch === "`") {
347
+ current++;
348
+ if (current > max) max = current;
349
+ } else current = 0;
350
+ return max;
351
+ }
352
+ /**
353
+ * Wrap text in a dynamically-sized backtick fence to prevent markdown rendering.
354
+ *
355
+ * Instead of character-level escaping (which fails on files containing backtick
356
+ * sequences, indented code blocks, blockquotes, and list markers), this wraps
357
+ * the entire text in a backtick fence whose length exceeds any backtick sequence
358
+ * in the content. This approach is simpler and strictly more correct (following
359
+ * the claude-agent-acp pattern).
360
+ */
361
+ function markdownEscape(text) {
362
+ if (text === "") return "";
363
+ const fenceLen = Math.max(3, longestBacktickRun(text) + 1);
364
+ const fence = "`".repeat(fenceLen);
365
+ return `${fence}\n${text.endsWith("\n") ? text.slice(0, -1) : text}\n${fence}`;
366
+ }
367
+ /**
368
+ * Format tool output into `ToolCallContent[]` by tool name.
369
+ *
370
+ * Returns the appropriate content shape for each tool type:
371
+ * - bash/tmux: console code fences
372
+ * - read: markdown-escaped text (images preserved)
373
+ * - edit/write: empty (diff handled separately)
374
+ * - lsp: code fences
375
+ * - errors: code fences with failed status
376
+ * - everything else: plain text
377
+ */
378
+ function formatToolContent(toolName, result, isError) {
379
+ if (isError) {
380
+ const text = extractTextContent(result);
381
+ if (text === "") return [];
382
+ return [{
383
+ type: "content",
384
+ content: {
385
+ type: "text",
386
+ text: `\`\`\`\n${text}\n\`\`\``
387
+ }
388
+ }];
389
+ }
390
+ switch (toolName) {
391
+ case "bash":
392
+ case "tmux": return formatBashContent(result);
393
+ case "read": return formatReadContent(result);
394
+ case "edit":
395
+ case "write": return [];
396
+ case "lsp": return formatLspContent(result);
397
+ default: return formatFallbackContent(result);
398
+ }
399
+ }
400
+ function formatBashContent(result) {
401
+ const { output, exitCode } = extractBashOutput(result);
402
+ if (output === "" && exitCode === void 0) return [];
403
+ const parts = [];
404
+ if (output !== "") parts.push(`\`\`\`console\n${output}\n\`\`\``);
405
+ if (exitCode !== void 0 && exitCode !== 0) parts.push(`exit code: ${exitCode}`);
406
+ const text = parts.join("\n\n");
407
+ if (text === "") return [];
408
+ return [{
409
+ type: "content",
410
+ content: {
411
+ type: "text",
412
+ text
413
+ }
414
+ }];
415
+ }
416
+ function formatReadContent(result) {
417
+ const blocks = extractContentBlocks(result);
418
+ if (blocks.length === 0) {
419
+ if (typeof result === "object" && result !== null && "content" in result && Array.isArray(result.content) && result.content.length === 0) return [];
420
+ const text = extractTextContent(result);
421
+ if (text === "") return [];
422
+ return [{
423
+ type: "content",
424
+ content: {
425
+ type: "text",
426
+ text: markdownEscape(text)
427
+ }
428
+ }];
429
+ }
430
+ const content = [];
431
+ for (const block of blocks) if (block.type === "text") content.push({
432
+ type: "content",
433
+ content: {
434
+ type: "text",
435
+ text: markdownEscape(block.text)
436
+ }
437
+ });
438
+ return content;
439
+ }
440
+ function formatLspContent(result) {
441
+ const text = extractTextContent(result);
442
+ if (text === "") return [];
443
+ return [{
444
+ type: "content",
445
+ content: {
446
+ type: "text",
447
+ text: `\`\`\`\n${text}\n\`\`\``
448
+ }
449
+ }];
450
+ }
451
+ function formatFallbackContent(result) {
452
+ const text = extractTextContent(result);
453
+ if (text === "") return [];
454
+ return [{
455
+ type: "content",
456
+ content: {
457
+ type: "text",
458
+ text
459
+ }
460
+ }];
461
+ }
462
+ /**
463
+ * Wrap streaming output text in a console code fence for bash/tmux.
464
+ *
465
+ * Each streaming update is self-contained (full accumulated buffer),
466
+ * following the codex-acp pattern.
467
+ */
468
+ function wrapStreamingBashOutput(text) {
469
+ if (text === "") return "";
470
+ return `\`\`\`console\n${text}\n\`\`\``;
471
+ }
472
+ //#endregion
473
+ //#region src/acp/unreachable.ts
474
+ /**
475
+ * Exhaustive switch/case helper.
476
+ *
477
+ * Logs unknown values instead of silently ignoring them, aiding debugging
478
+ * when the pi SDK adds new event types.
479
+ */
480
+ function unreachable(value, context) {
481
+ const label = context !== void 0 ? `[${context}] ` : "";
482
+ console.warn(`${label}Unhandled value: ${String(value)}`);
483
+ }
189
484
  //#endregion
190
485
  //#region src/acp/session.ts
191
486
  function findUniqueLineNumber(text, needle) {
@@ -210,7 +505,9 @@ function toToolKind(toolName) {
210
505
  case "read": return "read";
211
506
  case "write":
212
507
  case "edit": return "edit";
213
- case "bash": return "execute";
508
+ case "bash":
509
+ case "tmux": return "execute";
510
+ case "lsp": return "search";
214
511
  default: return "other";
215
512
  }
216
513
  }
@@ -220,6 +517,10 @@ function truncateTitle(text) {
220
517
  if (oneLine.length <= MAX_TITLE_LEN) return oneLine;
221
518
  return `${oneLine.slice(0, MAX_TITLE_LEN - 1)}…`;
222
519
  }
520
+ function capitalize(s) {
521
+ if (s.length === 0) return s;
522
+ return s.charAt(0).toUpperCase() + s.slice(1);
523
+ }
223
524
  /**
224
525
  * Build a descriptive tool title from tool name and args.
225
526
  *
@@ -235,6 +536,36 @@ function buildToolTitle(toolName, args) {
235
536
  const command = typeof args["command"] === "string" ? args["command"] : typeof args["cmd"] === "string" ? args["cmd"] : void 0;
236
537
  return command !== void 0 ? truncateTitle(`Run ${command}`) : "bash";
237
538
  }
539
+ case "lsp": {
540
+ const action = typeof args["action"] === "string" ? args["action"] : void 0;
541
+ const file = typeof args["file"] === "string" ? args["file"] : void 0;
542
+ const query = typeof args["query"] === "string" ? args["query"] : void 0;
543
+ const line = typeof args["line"] === "number" ? args["line"] : void 0;
544
+ if (action !== void 0) {
545
+ const target = file !== void 0 ? line !== void 0 ? `${file}:${line}` : file : query;
546
+ return target !== void 0 ? truncateTitle(`${capitalize(action)} ${target}`) : capitalize(action);
547
+ }
548
+ return "LSP";
549
+ }
550
+ case "tmux": {
551
+ const action = typeof args["action"] === "string" ? args["action"] : void 0;
552
+ const command = typeof args["command"] === "string" ? args["command"] : void 0;
553
+ const name = typeof args["name"] === "string" ? args["name"] : void 0;
554
+ if (action === "run" && command !== void 0) return truncateTitle(`Tmux: ${command}`);
555
+ if (action !== void 0 && name !== void 0) return truncateTitle(`Tmux ${action} ${name}`);
556
+ if (action !== void 0) return `Tmux ${action}`;
557
+ return "Tmux";
558
+ }
559
+ case "context_tag": {
560
+ const name = typeof args["name"] === "string" ? args["name"] : void 0;
561
+ return name !== void 0 ? `Tag ${name}` : "Tag";
562
+ }
563
+ case "context_log": return "Context log";
564
+ case "context_checkout": {
565
+ const target = typeof args["target"] === "string" ? args["target"] : void 0;
566
+ return target !== void 0 ? truncateTitle(`Checkout ${target}`) : "Checkout";
567
+ }
568
+ case "claudemon": return "Check quota";
238
569
  default: return toolName;
239
570
  }
240
571
  }
@@ -269,6 +600,19 @@ function toToolArgs(raw) {
269
600
  const result = toolArgsSchema.safeParse(raw);
270
601
  return result.success ? result.data : {};
271
602
  }
603
+ /** Build the `_meta.piAcp` tool name metadata. */
604
+ function buildToolMeta(toolName, extra) {
605
+ const base = { piAcp: { toolName } };
606
+ if (extra !== void 0) return {
607
+ ...base,
608
+ ...extra
609
+ };
610
+ return base;
611
+ }
612
+ /** Tools that produce terminal-style output. */
613
+ function isTerminalTool(toolName) {
614
+ return toolName === "bash" || toolName === "tmux";
615
+ }
272
616
  var SessionManager$1 = class {
273
617
  sessions = /* @__PURE__ */ new Map();
274
618
  disposeAll() {
@@ -302,12 +646,16 @@ var PiAcpSession = class {
302
646
  cwd;
303
647
  mcpServers;
304
648
  piSession;
305
- startupInfo = null;
306
- startupInfoSent = false;
649
+ supportsTerminalOutput;
307
650
  conn;
308
651
  cancelRequested = false;
652
+ promptRunning = false;
309
653
  pendingTurn = null;
654
+ /** Queued prompts waiting for the active turn to complete. */
655
+ pendingMessages = [];
310
656
  currentToolCalls = /* @__PURE__ */ new Map();
657
+ /** Map of toolCallId -> toolName for streaming updates (Phase 5). */
658
+ toolCallNames = /* @__PURE__ */ new Map();
311
659
  editSnapshots = /* @__PURE__ */ new Map();
312
660
  lastAssistantStopReason = null;
313
661
  lastEmit = Promise.resolve();
@@ -318,27 +666,32 @@ var PiAcpSession = class {
318
666
  this.mcpServers = opts.mcpServers;
319
667
  this.piSession = opts.piSession;
320
668
  this.conn = opts.conn;
669
+ this.supportsTerminalOutput = opts.supportsTerminalOutput ?? false;
321
670
  this.unsubscribe = this.piSession.subscribe((ev) => this.handlePiEvent(ev));
322
671
  }
323
672
  dispose() {
324
673
  this.unsubscribe?.();
325
674
  this.piSession.dispose();
326
675
  }
327
- setStartupInfo(text) {
328
- this.startupInfo = text;
329
- }
330
- sendStartupInfoIfPending() {
331
- if (this.startupInfoSent || this.startupInfo === null) return;
332
- this.startupInfoSent = true;
333
- this.emit({
334
- sessionUpdate: "agent_message_chunk",
335
- content: {
336
- type: "text",
337
- text: this.startupInfo
338
- }
676
+ async prompt(message, images = []) {
677
+ if (this.promptRunning) return new Promise((resolve, reject) => {
678
+ this.pendingMessages.push({
679
+ message,
680
+ images,
681
+ resolve,
682
+ reject
683
+ });
339
684
  });
685
+ return this.executePrompt(message, images);
340
686
  }
341
- async prompt(message, images = []) {
687
+ async cancel() {
688
+ this.cancelRequested = true;
689
+ for (const pending of this.pendingMessages) pending.resolve("cancelled");
690
+ this.pendingMessages = [];
691
+ await this.piSession.abort();
692
+ }
693
+ executePrompt(message, images) {
694
+ this.promptRunning = true;
342
695
  const turnPromise = new Promise((resolve, reject) => {
343
696
  this.cancelRequested = false;
344
697
  this.pendingTurn = {
@@ -356,9 +709,17 @@ var PiAcpSession = class {
356
709
  });
357
710
  return turnPromise;
358
711
  }
359
- async cancel() {
360
- this.cancelRequested = true;
361
- await this.piSession.abort();
712
+ /**
713
+ * Dequeue and execute the next pending prompt, if any.
714
+ * Called after a turn completes.
715
+ */
716
+ dequeueNextPrompt() {
717
+ const next = this.pendingMessages.shift();
718
+ if (next === void 0) {
719
+ this.promptRunning = false;
720
+ return;
721
+ }
722
+ this.executePrompt(next.message, next.images).then(next.resolve, next.reject);
362
723
  }
363
724
  wasCancelRequested() {
364
725
  return this.cancelRequested;
@@ -385,15 +746,17 @@ var PiAcpSession = class {
385
746
  this.handleToolStart(ev.toolCallId, ev.toolName, toToolArgs(ev.args));
386
747
  break;
387
748
  case "tool_execution_update":
388
- this.handleToolUpdate(ev.toolCallId, ev.partialResult);
749
+ this.handleToolUpdate(ev.toolCallId, ev.toolName, ev.partialResult);
389
750
  break;
390
751
  case "tool_execution_end":
391
- this.handleToolEnd(ev.toolCallId, ev.result, ev.isError);
752
+ this.handleToolEnd(ev.toolCallId, ev.toolName, ev.result, ev.isError);
392
753
  break;
393
754
  case "agent_end":
394
755
  this.handleAgentEnd();
395
756
  break;
396
- default: break;
757
+ default:
758
+ unreachable(ev, "handlePiEvent");
759
+ break;
397
760
  }
398
761
  }
399
762
  handleMessageUpdate(ame) {
@@ -433,14 +796,16 @@ var PiAcpSession = class {
433
796
  kind: toToolKind(toolCall.name),
434
797
  status,
435
798
  ...locations ? { locations } : {},
436
- rawInput
799
+ rawInput,
800
+ _meta: buildToolMeta(toolCall.name)
437
801
  });
438
802
  } else this.emit({
439
803
  sessionUpdate: "tool_call_update",
440
804
  toolCallId: toolCall.id,
441
805
  status,
442
806
  ...locations ? { locations } : {},
443
- rawInput
807
+ rawInput,
808
+ _meta: buildToolMeta(toolCall.name)
444
809
  });
445
810
  }
446
811
  }
@@ -448,6 +813,7 @@ var PiAcpSession = class {
448
813
  if ("role" in msg && msg.role === "assistant") this.lastAssistantStopReason = msg.stopReason;
449
814
  }
450
815
  handleToolStart(toolCallId, toolName, args) {
816
+ this.toolCallNames.set(toolCallId, toolName);
451
817
  let line;
452
818
  if ((toolName === "edit" || toolName === "write") && args.path !== void 0) try {
453
819
  const abs = isAbsolute(args.path) ? args.path : resolve(this.cwd, args.path);
@@ -462,6 +828,14 @@ var PiAcpSession = class {
462
828
  if (toolName === "edit") line = findUniqueLineNumber(oldText, args.oldText ?? "");
463
829
  } catch {}
464
830
  const locations = resolveToolPath(args, this.cwd, line);
831
+ const meta = buildToolMeta(toolName, this.supportsTerminalOutput && isTerminalTool(toolName) ? { terminal_info: {
832
+ terminal_id: toolCallId,
833
+ cwd: this.cwd
834
+ } } : void 0);
835
+ const terminalContent = this.supportsTerminalOutput && isTerminalTool(toolName) ? [{
836
+ type: "terminal",
837
+ terminalId: toolCallId
838
+ }] : void 0;
465
839
  if (!this.currentToolCalls.has(toolCallId)) {
466
840
  this.currentToolCalls.set(toolCallId, "in_progress");
467
841
  this.emit({
@@ -471,7 +845,9 @@ var PiAcpSession = class {
471
845
  kind: toToolKind(toolName),
472
846
  status: "in_progress",
473
847
  ...locations ? { locations } : {},
474
- rawInput: args
848
+ ...terminalContent !== void 0 ? { content: terminalContent } : {},
849
+ rawInput: args,
850
+ _meta: meta
475
851
  });
476
852
  } else {
477
853
  this.currentToolCalls.set(toolCallId, "in_progress");
@@ -481,61 +857,118 @@ var PiAcpSession = class {
481
857
  title: buildToolTitle(toolName, args),
482
858
  status: "in_progress",
483
859
  ...locations ? { locations } : {},
484
- rawInput: args
860
+ ...terminalContent !== void 0 ? { content: terminalContent } : {},
861
+ rawInput: args,
862
+ _meta: meta
485
863
  });
486
864
  }
487
865
  }
488
- handleToolUpdate(toolCallId, partialResult) {
489
- const text = toolResultToText(partialResult);
490
- this.emit({
491
- sessionUpdate: "tool_call_update",
492
- toolCallId,
493
- status: "in_progress",
494
- content: text ? [{
495
- type: "content",
496
- content: {
497
- type: "text",
498
- text
499
- }
500
- }] : null,
501
- rawOutput: partialResult
502
- });
866
+ handleToolUpdate(toolCallId, toolName, partialResult) {
867
+ const name = this.toolCallNames.get(toolCallId) ?? toolName;
868
+ if (this.supportsTerminalOutput && isTerminalTool(name)) {
869
+ const text = extractStreamingText(partialResult);
870
+ this.emit({
871
+ sessionUpdate: "tool_call_update",
872
+ toolCallId,
873
+ status: "in_progress",
874
+ _meta: buildToolMeta(name, { terminal_output: {
875
+ terminal_id: toolCallId,
876
+ data: text
877
+ } }),
878
+ rawOutput: partialResult
879
+ });
880
+ } else if (isTerminalTool(name)) {
881
+ const wrapped = wrapStreamingBashOutput(extractStreamingText(partialResult));
882
+ this.emit({
883
+ sessionUpdate: "tool_call_update",
884
+ toolCallId,
885
+ status: "in_progress",
886
+ content: wrapped ? [{
887
+ type: "content",
888
+ content: {
889
+ type: "text",
890
+ text: wrapped
891
+ }
892
+ }] : null,
893
+ _meta: buildToolMeta(name),
894
+ rawOutput: partialResult
895
+ });
896
+ } else {
897
+ const text = extractStreamingText(partialResult);
898
+ this.emit({
899
+ sessionUpdate: "tool_call_update",
900
+ toolCallId,
901
+ status: "in_progress",
902
+ content: text ? [{
903
+ type: "content",
904
+ content: {
905
+ type: "text",
906
+ text
907
+ }
908
+ }] : null,
909
+ _meta: buildToolMeta(name),
910
+ rawOutput: partialResult
911
+ });
912
+ }
503
913
  }
504
- handleToolEnd(toolCallId, result, isError) {
505
- const text = toolResultToText(result);
914
+ handleToolEnd(toolCallId, toolName, result, isError) {
506
915
  const snapshot = this.editSnapshots.get(toolCallId);
507
916
  let content = null;
508
917
  if (!isError && snapshot) try {
509
918
  const newText = readFileSync(snapshot.path, "utf8");
510
- if (newText !== snapshot.oldText) content = [{
511
- type: "diff",
512
- path: snapshot.path,
513
- oldText: snapshot.oldText,
514
- newText
515
- }, ...text ? [{
919
+ if (newText !== snapshot.oldText) {
920
+ const formatted = formatToolContent(toolName, result, isError);
921
+ content = [{
922
+ type: "diff",
923
+ path: snapshot.path,
924
+ oldText: snapshot.oldText,
925
+ newText
926
+ }, ...formatted];
927
+ }
928
+ } catch {}
929
+ if (content === null) {
930
+ const formatted = formatToolContent(toolName, result, isError);
931
+ content = formatted.length > 0 ? formatted : null;
932
+ }
933
+ if (content === null && !isError && toolName !== "edit" && toolName !== "write") {
934
+ const text = extractStreamingText(result);
935
+ if (text) content = [{
516
936
  type: "content",
517
937
  content: {
518
938
  type: "text",
519
939
  text
520
940
  }
521
- }] : []];
522
- } catch {}
523
- if (!content && text) content = [{
524
- type: "content",
525
- content: {
526
- type: "text",
527
- text
528
- }
529
- }];
941
+ }];
942
+ }
943
+ if (this.supportsTerminalOutput && isTerminalTool(toolName)) {
944
+ const outputText = extractStreamingText(result);
945
+ if (outputText !== "") this.emit({
946
+ sessionUpdate: "tool_call_update",
947
+ toolCallId,
948
+ status: "in_progress",
949
+ _meta: buildToolMeta(toolName, { terminal_output: {
950
+ terminal_id: toolCallId,
951
+ data: outputText
952
+ } }),
953
+ rawOutput: result
954
+ });
955
+ }
956
+ const meta = buildToolMeta(toolName, this.supportsTerminalOutput && isTerminalTool(toolName) ? { terminal_exit: {
957
+ terminal_id: toolCallId,
958
+ exit_code: extractExitCode(result),
959
+ signal: null
960
+ } } : void 0);
530
961
  this.emit({
531
962
  sessionUpdate: "tool_call_update",
532
963
  toolCallId,
533
964
  status: isError ? "failed" : "completed",
534
965
  content,
966
+ _meta: meta,
535
967
  rawOutput: result
536
968
  });
537
969
  this.currentToolCalls.delete(toolCallId);
538
970
  this.editSnapshots.delete(toolCallId);
971
+ this.toolCallNames.delete(toolCallId);
539
972
  }
540
973
  handleAgentEnd() {
541
974
  this.emitUsageUpdate();
@@ -544,6 +977,7 @@ var PiAcpSession = class {
544
977
  this.lastAssistantStopReason = null;
545
978
  this.pendingTurn?.resolve(reason);
546
979
  this.pendingTurn = null;
980
+ this.dequeueNextPrompt();
547
981
  });
548
982
  }
549
983
  /**
@@ -583,6 +1017,42 @@ var PiAcpSession = class {
583
1017
  return this.piSession.getSessionStats().cost;
584
1018
  }
585
1019
  };
1020
+ function isTextBlock$1(v) {
1021
+ return typeof v === "object" && v !== null && "type" in v && v.type === "text" && "text" in v && typeof v.text === "string";
1022
+ }
1023
+ function extractStreamingText(result) {
1024
+ if (result === null || result === void 0) return "";
1025
+ if (typeof result === "string") return result;
1026
+ if (typeof result !== "object") return String(result);
1027
+ if ("content" in result && Array.isArray(result.content)) {
1028
+ const texts = [];
1029
+ for (const raw of result.content) if (isTextBlock$1(raw)) texts.push(raw.text);
1030
+ if (texts.length > 0) return texts.join("");
1031
+ }
1032
+ if ("details" in result) {
1033
+ const details = result.details;
1034
+ if (typeof details === "object" && details !== null) {
1035
+ if ("stdout" in details && typeof details.stdout === "string" && details.stdout.trim() !== "") return details.stdout;
1036
+ if ("output" in details && typeof details.output === "string" && details.output.trim() !== "") return details.output;
1037
+ }
1038
+ }
1039
+ if ("output" in result && typeof result.output === "string" && result.output.trim() !== "") return result.output;
1040
+ if ("stdout" in result && typeof result.stdout === "string" && result.stdout.trim() !== "") return result.stdout;
1041
+ return "";
1042
+ }
1043
+ function extractExitCode(result) {
1044
+ if (result === null || result === void 0 || typeof result !== "object") return null;
1045
+ if ("details" in result) {
1046
+ const details = result.details;
1047
+ if (typeof details === "object" && details !== null) {
1048
+ if ("exitCode" in details && typeof details.exitCode === "number") return details.exitCode;
1049
+ if ("code" in details && typeof details.code === "number") return details.code;
1050
+ }
1051
+ }
1052
+ if ("exitCode" in result && typeof result.exitCode === "number") return result.exitCode;
1053
+ if ("code" in result && typeof result.code === "number") return result.code;
1054
+ return null;
1055
+ }
586
1056
  /**
587
1057
  * Type guard to narrow AgentSessionEvent to the AgentEvent subset
588
1058
  * (the variants we handle). Session-specific events like auto_compaction
@@ -715,52 +1185,58 @@ function hasPiAuthConfigured() {
715
1185
  return hasAuthJson() || hasCustomProviderKey() || hasProviderEnvVar();
716
1186
  }
717
1187
  //#endregion
1188
+ //#region package.json
1189
+ var name = "@victor-software-house/pi-acp";
1190
+ var version = "0.4.0";
1191
+ //#endregion
718
1192
  //#region src/acp/agent.ts
719
- function builtinAvailableCommands() {
720
- return [
721
- {
722
- name: "compact",
723
- description: "Manually compact the session context",
724
- input: { hint: "optional custom instructions" }
725
- },
726
- {
727
- name: "autocompact",
728
- description: "Toggle automatic context compaction",
729
- input: { hint: "on|off|toggle" }
730
- },
731
- {
732
- name: "export",
733
- description: "Export session to an HTML file in the session cwd"
734
- },
735
- {
736
- name: "session",
737
- description: "Show session stats (messages, tokens, cost, session file)"
738
- },
739
- {
740
- name: "name",
741
- description: "Set session display name",
742
- input: { hint: "<name>" }
743
- },
744
- {
745
- name: "steering",
746
- description: "Get/set pi steering message delivery mode",
747
- input: { hint: "(no args to show) all | one-at-a-time" }
748
- },
749
- {
750
- name: "follow-up",
751
- description: "Get/set pi follow-up message delivery mode",
752
- input: { hint: "(no args to show) all | one-at-a-time" }
753
- },
754
- {
755
- name: "changelog",
756
- description: "Show pi changelog"
757
- }
758
- ];
759
- }
760
- function mergeCommands(a, b) {
761
- const out = [];
1193
+ /** Builtin ACP slash commands handled directly by the adapter. */
1194
+ const BUILTIN_COMMANDS = [
1195
+ {
1196
+ name: "compact",
1197
+ description: "Manually compact the session context",
1198
+ input: { hint: "optional custom instructions" }
1199
+ },
1200
+ {
1201
+ name: "autocompact",
1202
+ description: "Toggle automatic context compaction",
1203
+ input: { hint: "on|off|toggle" }
1204
+ },
1205
+ {
1206
+ name: "export",
1207
+ description: "Export session to an HTML file in the session cwd"
1208
+ },
1209
+ {
1210
+ name: "session",
1211
+ description: "Show session stats (messages, tokens, cost, session file)"
1212
+ },
1213
+ {
1214
+ name: "name",
1215
+ description: "Set session display name",
1216
+ input: { hint: "<name>" }
1217
+ },
1218
+ {
1219
+ name: "steering",
1220
+ description: "Get/set pi steering message delivery mode",
1221
+ input: { hint: "(no args to show) all | one-at-a-time" }
1222
+ },
1223
+ {
1224
+ name: "follow-up",
1225
+ description: "Get/set pi follow-up message delivery mode",
1226
+ input: { hint: "(no args to show) all | one-at-a-time" }
1227
+ },
1228
+ {
1229
+ name: "changelog",
1230
+ description: "Show pi changelog"
1231
+ }
1232
+ ];
1233
+ /**
1234
+ * Deduplicate commands by name. First occurrence wins.
1235
+ */
1236
+ function deduplicateCommands(commands) {
762
1237
  const seen = /* @__PURE__ */ new Set();
763
- for (const c of [...a, ...b]) {
1238
+ const out = [];
1239
+ for (const c of commands) {
764
1240
  if (seen.has(c.name)) continue;
765
1241
  seen.add(c.name);
766
1242
  out.push(c);
@@ -791,12 +1267,17 @@ function truncateSessionTitle(text) {
791
1267
  if (oneLine.length <= SESSION_TITLE_MAX) return oneLine;
792
1268
  return `${oneLine.slice(0, SESSION_TITLE_MAX - 1)}…`;
793
1269
  }
794
- const pkg = readNearestPackageJson(import.meta.url);
795
1270
  var PiAcpAgent = class {
796
1271
  conn;
797
1272
  sessions = new SessionManager$1();
798
1273
  /** Cache of sessionId → file path, populated by listSessions and newSession. */
799
1274
  sessionPaths = /* @__PURE__ */ new Map();
1275
+ /** Parsed client capability flags from initialize(). */
1276
+ clientCapabilities = {
1277
+ terminalOutput: false,
1278
+ terminalAuth: false,
1279
+ gatewayAuth: false
1280
+ };
800
1281
  dispose() {
801
1282
  this.sessions.disposeAll();
802
1283
  }
@@ -806,14 +1287,15 @@ var PiAcpAgent = class {
806
1287
  async initialize(params) {
807
1288
  const supportedVersion = 1;
808
1289
  const requested = params.protocolVersion;
1290
+ this.clientCapabilities = parseClientCapabilities(params.clientCapabilities);
809
1291
  return {
810
1292
  protocolVersion: requested === supportedVersion ? requested : supportedVersion,
811
1293
  agentInfo: {
812
- name: pkg.name,
1294
+ name,
813
1295
  title: "pi ACP adapter",
814
- version: pkg.version
1296
+ version
815
1297
  },
816
- authMethods: buildAuthMethods({ supportsTerminalAuthMeta: params.clientCapabilities?._meta?.["terminal-auth"] === true }),
1298
+ authMethods: buildAuthMethods({ supportsTerminalAuthMeta: this.clientCapabilities.terminalAuth }),
817
1299
  agentCapabilities: {
818
1300
  loadSession: true,
819
1301
  mcpCapabilities: {
@@ -859,27 +1341,13 @@ var PiAcpAgent = class {
859
1341
  cwd: params.cwd,
860
1342
  mcpServers: params.mcpServers,
861
1343
  piSession,
862
- conn: this.conn
1344
+ conn: this.conn,
1345
+ supportsTerminalOutput: this.clientCapabilities.terminalOutput
863
1346
  });
864
1347
  this.sessions.register(session);
865
- const quietStartup = quietStartupEnabled(params.cwd);
866
- const updateNotice = buildUpdateNotice();
867
- const preludeText = quietStartup ? updateNotice !== null ? `${updateNotice}\n` : "" : buildStartupInfo({
868
- cwd: params.cwd,
869
- updateNotice
870
- });
871
- if (preludeText) session.setStartupInfo(preludeText);
872
1348
  const modes = buildThinkingModes(piSession);
873
1349
  const models = buildModelState(piSession);
874
1350
  const configOptions = buildConfigOptions(modes, models);
875
- const response = {
876
- sessionId: session.sessionId,
877
- configOptions,
878
- modes,
879
- models,
880
- _meta: { piAcp: { startupInfo: preludeText || null } }
881
- };
882
- if (preludeText) setTimeout(() => session.sendStartupInfoIfPending(), 0);
883
1351
  const enableSkillCommands = skillCommandsEnabled(params.cwd);
884
1352
  setTimeout(() => {
885
1353
  (async () => {
@@ -889,13 +1357,18 @@ var PiAcpAgent = class {
889
1357
  sessionId: session.sessionId,
890
1358
  update: {
891
1359
  sessionUpdate: "available_commands_update",
892
- availableCommands: mergeCommands(commands, builtinAvailableCommands())
1360
+ availableCommands: deduplicateCommands([...commands, ...BUILTIN_COMMANDS])
893
1361
  }
894
1362
  });
895
1363
  } catch {}
896
1364
  })();
897
1365
  }, 0);
898
- return response;
1366
+ return {
1367
+ sessionId: session.sessionId,
1368
+ configOptions,
1369
+ modes,
1370
+ models
1371
+ };
899
1372
  }
900
1373
  async authenticate(_params) {
901
1374
  return {};
@@ -1009,7 +1482,8 @@ var PiAcpAgent = class {
1009
1482
  kind: toToolKind(block.name),
1010
1483
  status: "completed",
1011
1484
  rawInput: args,
1012
- ...locations ? { locations } : {}
1485
+ ...locations ? { locations } : {},
1486
+ _meta: { piAcp: { toolName: block.name } }
1013
1487
  }
1014
1488
  });
1015
1489
  }
@@ -1032,25 +1506,21 @@ var PiAcpAgent = class {
1032
1506
  kind: toToolKind(toolName),
1033
1507
  status: "completed",
1034
1508
  rawInput: null,
1035
- rawOutput: m
1509
+ rawOutput: m,
1510
+ _meta: { piAcp: { toolName } }
1036
1511
  }
1037
1512
  });
1038
- const text = toolResultToText(m);
1513
+ const content = formatToolContent(toolName, m, isError);
1039
1514
  await this.conn.sessionUpdate({
1040
1515
  sessionId: session.sessionId,
1041
1516
  update: {
1042
1517
  sessionUpdate: "tool_call_update",
1043
1518
  toolCallId,
1044
1519
  status: isError ? "failed" : "completed",
1045
- content: text ? [{
1046
- type: "content",
1047
- content: {
1048
- type: "text",
1049
- text
1050
- }
1051
- }] : null,
1520
+ content: content.length > 0 ? content : null,
1052
1521
  rawOutput: m,
1053
- ...locations ? { locations } : {}
1522
+ ...locations ? { locations } : {},
1523
+ _meta: { piAcp: { toolName } }
1054
1524
  }
1055
1525
  });
1056
1526
  }
@@ -1109,7 +1579,8 @@ var PiAcpAgent = class {
1109
1579
  cwd: params.cwd,
1110
1580
  mcpServers: params.mcpServers,
1111
1581
  piSession,
1112
- conn: this.conn
1582
+ conn: this.conn,
1583
+ supportsTerminalOutput: this.clientCapabilities.terminalOutput
1113
1584
  });
1114
1585
  this.sessions.register(session);
1115
1586
  await this.replaySessionHistory(session, piSession.messages);
@@ -1125,7 +1596,7 @@ var PiAcpAgent = class {
1125
1596
  sessionId: session.sessionId,
1126
1597
  update: {
1127
1598
  sessionUpdate: "available_commands_update",
1128
- availableCommands: mergeCommands(commands, builtinAvailableCommands())
1599
+ availableCommands: deduplicateCommands([...commands, ...BUILTIN_COMMANDS])
1129
1600
  }
1130
1601
  });
1131
1602
  } catch {}
@@ -1134,8 +1605,7 @@ var PiAcpAgent = class {
1134
1605
  return {
1135
1606
  configOptions,
1136
1607
  modes,
1137
- models,
1138
- _meta: { piAcp: { startupInfo: null } }
1608
+ models
1139
1609
  };
1140
1610
  }
1141
1611
  async unstable_closeSession(params) {
@@ -1176,7 +1646,8 @@ var PiAcpAgent = class {
1176
1646
  cwd: params.cwd,
1177
1647
  mcpServers: params.mcpServers ?? [],
1178
1648
  piSession,
1179
- conn: this.conn
1649
+ conn: this.conn,
1650
+ supportsTerminalOutput: this.clientCapabilities.terminalOutput
1180
1651
  });
1181
1652
  this.sessions.register(session);
1182
1653
  this.sessionPaths.set(params.sessionId, sessionFile);
@@ -1189,7 +1660,7 @@ var PiAcpAgent = class {
1189
1660
  sessionId: session.sessionId,
1190
1661
  update: {
1191
1662
  sessionUpdate: "available_commands_update",
1192
- availableCommands: mergeCommands(commands, builtinAvailableCommands())
1663
+ availableCommands: deduplicateCommands([...commands, ...BUILTIN_COMMANDS])
1193
1664
  }
1194
1665
  });
1195
1666
  } catch {}
@@ -1229,7 +1700,8 @@ var PiAcpAgent = class {
1229
1700
  cwd: params.cwd,
1230
1701
  mcpServers: params.mcpServers ?? [],
1231
1702
  piSession,
1232
- conn: this.conn
1703
+ conn: this.conn,
1704
+ supportsTerminalOutput: this.clientCapabilities.terminalOutput
1233
1705
  });
1234
1706
  this.sessions.register(session);
1235
1707
  const enableSkillCommands = skillCommandsEnabled(params.cwd);
@@ -1241,7 +1713,7 @@ var PiAcpAgent = class {
1241
1713
  sessionId: session.sessionId,
1242
1714
  update: {
1243
1715
  sessionUpdate: "available_commands_update",
1244
- availableCommands: mergeCommands(commands, builtinAvailableCommands())
1716
+ availableCommands: deduplicateCommands([...commands, ...BUILTIN_COMMANDS])
1245
1717
  }
1246
1718
  });
1247
1719
  } catch {}
@@ -1273,22 +1745,10 @@ var PiAcpAgent = class {
1273
1745
  }
1274
1746
  async unstable_setSessionModel(params) {
1275
1747
  const session = this.sessions.get(params.sessionId);
1276
- let provider = null;
1277
- let modelId = null;
1278
- if (params.modelId.includes("/")) {
1279
- const [p, ...rest] = params.modelId.split("/");
1280
- provider = p ?? null;
1281
- modelId = rest.join("/");
1282
- } else modelId = params.modelId;
1283
- if (provider === null) {
1284
- const found = session.piSession.modelRegistry.getAvailable().find((m) => m.id === modelId);
1285
- if (found) {
1286
- provider = found.provider;
1287
- modelId = found.id;
1288
- }
1289
- }
1290
- if (provider === null || modelId === null) throw RequestError.invalidParams(`Unknown modelId: ${params.modelId}`);
1291
- const model = session.piSession.modelRegistry.getAvailable().find((m) => m.provider === provider && m.id === modelId);
1748
+ const available = session.piSession.modelRegistry.getAvailable();
1749
+ const resolved = resolveModelPreference(available, params.modelId);
1750
+ if (resolved === null) throw RequestError.invalidParams(`Unknown modelId: ${params.modelId}`);
1751
+ const model = available.find((m) => m.provider === resolved.provider && m.id === resolved.id);
1292
1752
  if (!model) throw RequestError.invalidParams(`Unknown modelId: ${params.modelId}`);
1293
1753
  await session.piSession.setModel(model);
1294
1754
  this.emitConfigOptionUpdate(session);
@@ -1298,22 +1758,10 @@ var PiAcpAgent = class {
1298
1758
  const configId = String(params.configId);
1299
1759
  const value = String(params.value);
1300
1760
  if (configId === "model") {
1301
- let provider = null;
1302
- let modelId = null;
1303
- if (value.includes("/")) {
1304
- const [p, ...rest] = value.split("/");
1305
- provider = p ?? null;
1306
- modelId = rest.join("/");
1307
- } else modelId = value;
1308
- if (provider === null) {
1309
- const found = session.piSession.modelRegistry.getAvailable().find((m) => m.id === modelId);
1310
- if (found) {
1311
- provider = found.provider;
1312
- modelId = found.id;
1313
- }
1314
- }
1315
- if (provider === null || modelId === null) throw RequestError.invalidParams(`Unknown model: ${value}`);
1316
- const model = session.piSession.modelRegistry.getAvailable().find((m) => m.provider === provider && m.id === modelId);
1761
+ const available = session.piSession.modelRegistry.getAvailable();
1762
+ const resolved = resolveModelPreference(available, value);
1763
+ if (resolved === null) throw RequestError.invalidParams(`Unknown model: ${value}`);
1764
+ const model = available.find((m) => m.provider === resolved.provider && m.id === resolved.id);
1317
1765
  if (!model) throw RequestError.invalidParams(`Unknown model: ${value}`);
1318
1766
  await session.piSession.setModel(model);
1319
1767
  } else if (configId === "thought_level") {
@@ -1697,64 +2145,6 @@ function buildCommandList(piSession, enableSkillCommands) {
1697
2145
  });
1698
2146
  return commands;
1699
2147
  }
1700
- let cachedUpdateNotice;
1701
- function buildUpdateNotice() {
1702
- if (cachedUpdateNotice !== void 0) return cachedUpdateNotice;
1703
- try {
1704
- const installed = VERSION;
1705
- if (!installed || !isSemver(installed)) {
1706
- cachedUpdateNotice = null;
1707
- return null;
1708
- }
1709
- const latestRes = spawnSync("npm", [
1710
- "view",
1711
- "@mariozechner/pi-coding-agent",
1712
- "version"
1713
- ], {
1714
- encoding: "utf-8",
1715
- timeout: 800
1716
- });
1717
- const latest = String(latestRes.stdout ?? "").trim().replace(/^v/i, "");
1718
- if (!latest || !isSemver(latest)) {
1719
- cachedUpdateNotice = null;
1720
- return null;
1721
- }
1722
- if (compareSemver(latest, installed) <= 0) {
1723
- cachedUpdateNotice = null;
1724
- return null;
1725
- }
1726
- cachedUpdateNotice = `New version available: v${latest} (installed v${installed}). Run: \`npm i -g @mariozechner/pi-coding-agent\``;
1727
- return cachedUpdateNotice;
1728
- } catch {
1729
- cachedUpdateNotice = null;
1730
- return null;
1731
- }
1732
- }
1733
- function buildStartupInfo(opts) {
1734
- const md = [];
1735
- if (VERSION) {
1736
- md.push(`pi v${VERSION}`);
1737
- md.push("---");
1738
- md.push("");
1739
- }
1740
- const addSection = (title, items) => {
1741
- const cleaned = items.map((s) => s.trim()).filter(Boolean);
1742
- if (cleaned.length === 0) return;
1743
- md.push(`## ${title}`);
1744
- for (const item of cleaned) md.push(`- ${item}`);
1745
- md.push("");
1746
- };
1747
- const contextItems = [];
1748
- const contextPath = join(opts.cwd, "AGENTS.md");
1749
- if (existsSync(contextPath)) contextItems.push(contextPath);
1750
- addSection("Context", contextItems);
1751
- if (opts.updateNotice !== void 0 && opts.updateNotice !== null) {
1752
- md.push("---");
1753
- md.push(opts.updateNotice);
1754
- md.push("");
1755
- }
1756
- return `${md.join("\n").trim()}\n`;
1757
- }
1758
2148
  function findChangelog() {
1759
2149
  try {
1760
2150
  const which = spawnSync(process.platform === "win32" ? "where" : "which", ["pi"], { encoding: "utf-8" });
@@ -1774,42 +2164,6 @@ function findChangelog() {
1774
2164
  } catch {}
1775
2165
  return null;
1776
2166
  }
1777
- function isSemver(v) {
1778
- return /^\d+\.\d+\.\d+(?:[-+].+)?$/.test(v);
1779
- }
1780
- function compareSemver(a, b) {
1781
- const pa = a.split(/[.-]/).slice(0, 3).map((n) => Number(n));
1782
- const pb = b.split(/[.-]/).slice(0, 3).map((n) => Number(n));
1783
- for (let i = 0; i < 3; i++) {
1784
- const da = pa[i] ?? 0;
1785
- const db = pb[i] ?? 0;
1786
- if (da > db) return 1;
1787
- if (da < db) return -1;
1788
- }
1789
- return 0;
1790
- }
1791
- function readNearestPackageJson(metaUrl) {
1792
- const fallback = {
1793
- name: "pi-acp",
1794
- version: "0.0.0"
1795
- };
1796
- try {
1797
- let dir = dirname(fileURLToPath(metaUrl));
1798
- for (let i = 0; i < 6; i++) {
1799
- const p = join(dir, "package.json");
1800
- if (existsSync(p)) {
1801
- const raw = JSON.parse(readFileSync(p, "utf-8"));
1802
- if (typeof raw !== "object" || raw === null) return fallback;
1803
- return {
1804
- name: "name" in raw && typeof raw.name === "string" ? raw.name : fallback.name,
1805
- version: "version" in raw && typeof raw.version === "string" ? raw.version : fallback.version
1806
- };
1807
- }
1808
- dir = dirname(dir);
1809
- }
1810
- } catch {}
1811
- return fallback;
1812
- }
1813
2167
  //#endregion
1814
2168
  //#region src/index.ts
1815
2169
  if (process.argv.includes("--terminal-login")) {