@victor-software-house/pi-acp 0.3.0 → 0.5.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 "@earendil-works/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";
@@ -19,13 +18,10 @@ function buildAuthMethods(opts) {
19
18
  args: ["--terminal-login"],
20
19
  env: {}
21
20
  };
22
- if (supportsTerminalAuthMeta) {
23
- const launch = resolveTerminalLaunchCommand();
24
- method._meta = { "terminal-auth": {
25
- ...launch,
26
- label: "Launch pi"
27
- } };
28
- }
21
+ if (supportsTerminalAuthMeta) method._meta = { "terminal-auth": {
22
+ ...resolveTerminalLaunchCommand(),
23
+ label: "Launch pi"
24
+ } };
29
25
  return [method];
30
26
  }
31
27
  function resolveTerminalLaunchCommand() {
@@ -97,6 +93,102 @@ function parseClientCapabilities(caps) {
97
93
  };
98
94
  }
99
95
  //#endregion
96
+ //#region src/acp/model-alias.ts
97
+ /**
98
+ * Tokenize a string: split on non-alphanumeric, lowercase, strip "claude".
99
+ */
100
+ function tokenize(input) {
101
+ return input.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t !== "" && t !== "claude");
102
+ }
103
+ /**
104
+ * Extract a context hint in square brackets, e.g. "opus[1m]" -> { base: "opus", hint: "1m" }.
105
+ */
106
+ function extractContextHint(input) {
107
+ const match = /^(.+?)\[([^\]]+)\]$/.exec(input);
108
+ if (match !== null && match[1] !== void 0 && match[2] !== void 0) return {
109
+ base: match[1],
110
+ hint: match[2]
111
+ };
112
+ return {
113
+ base: input,
114
+ hint: null
115
+ };
116
+ }
117
+ /** Check if a string is purely numeric. */
118
+ function isNumeric(s) {
119
+ return /^\d+$/.test(s);
120
+ }
121
+ /**
122
+ * Score how well a model matches the given preference tokens.
123
+ *
124
+ * Returns a score >= 0 (higher is better), or -1 for no match.
125
+ * Requires at least one non-numeric token to match to avoid false positives
126
+ * from bare version numbers (e.g. "4" matching model version suffixes).
127
+ */
128
+ function scoreModel(model, prefTokens, hint) {
129
+ const modelStr = `${model.provider}/${model.id}/${model.name ?? ""}`.toLowerCase();
130
+ const modelTokens = tokenize(modelStr);
131
+ let matched = 0;
132
+ let hasNonNumericMatch = false;
133
+ for (const pt of prefTokens) if (modelTokens.some((mt) => mt.includes(pt) || pt.includes(mt))) {
134
+ matched++;
135
+ if (!isNumeric(pt)) hasNonNumericMatch = true;
136
+ }
137
+ if (matched === 0) return -1;
138
+ if (!hasNonNumericMatch) return -1;
139
+ let score = matched / prefTokens.length;
140
+ if (hint !== null && modelStr.includes(hint.toLowerCase())) score += .5;
141
+ const pref = prefTokens.join("");
142
+ if (model.id.toLowerCase().includes(pref)) score += .25;
143
+ return score;
144
+ }
145
+ /**
146
+ * Resolve a user-friendly model preference to a concrete model.
147
+ *
148
+ * Matching strategy (in order):
149
+ * 1. Exact match on "provider/id"
150
+ * 2. Exact match on "id" alone
151
+ * 3. Tokenized scored match with optional context hint
152
+ *
153
+ * Returns null if no model matches.
154
+ */
155
+ function resolveModelPreference(models, preference) {
156
+ const trimmed = preference.trim();
157
+ if (trimmed === "") return null;
158
+ if (trimmed.includes("/")) {
159
+ const [p, ...rest] = trimmed.split("/");
160
+ const provider = p ?? "";
161
+ const id = rest.join("/");
162
+ const exact = models.find((m) => m.provider.toLowerCase() === provider.toLowerCase() && m.id.toLowerCase() === id.toLowerCase());
163
+ if (exact !== void 0) return {
164
+ provider: exact.provider,
165
+ id: exact.id
166
+ };
167
+ }
168
+ const byId = models.find((m) => m.id.toLowerCase() === trimmed.toLowerCase());
169
+ if (byId !== void 0) return {
170
+ provider: byId.provider,
171
+ id: byId.id
172
+ };
173
+ const { base, hint } = extractContextHint(trimmed);
174
+ const prefTokens = tokenize(base);
175
+ if (prefTokens.length === 0) return null;
176
+ let bestModel = null;
177
+ let bestScore = -1;
178
+ for (const model of models) {
179
+ const s = scoreModel(model, prefTokens, hint);
180
+ if (s > bestScore) {
181
+ bestScore = s;
182
+ bestModel = model;
183
+ }
184
+ }
185
+ if (bestModel === null || bestScore < .5) return null;
186
+ return {
187
+ provider: bestModel.provider,
188
+ id: bestModel.id
189
+ };
190
+ }
191
+ //#endregion
100
192
  //#region src/acp/pi-settings.ts
101
193
  /**
102
194
  * Read pi settings from global and project config files.
@@ -149,12 +241,6 @@ function skillCommandsEnabled(cwd) {
149
241
  if (typeof settings.skills?.enableSkillCommands === "boolean") return settings.skills.enableSkillCommands;
150
242
  return true;
151
243
  }
152
- function quietStartupEnabled(cwd) {
153
- const settings = resolvedSettings(cwd);
154
- if (typeof settings.quietStartup === "boolean") return settings.quietStartup;
155
- if (typeof settings.quietStart === "boolean") return settings.quietStart;
156
- return false;
157
- }
158
244
  //#endregion
159
245
  //#region src/acp/translate/tool-content.ts
160
246
  const textBlockSchema = z.object({
@@ -249,13 +335,31 @@ function extractContentBlocks(result) {
249
335
  return blocks;
250
336
  }
251
337
  /**
252
- * Escape text that would be interpreted as markdown formatting.
338
+ * Find the longest consecutive backtick sequence in a string.
339
+ */
340
+ function longestBacktickRun(text) {
341
+ let max = 0;
342
+ let current = 0;
343
+ for (const ch of text) if (ch === "`") {
344
+ current++;
345
+ if (current > max) max = current;
346
+ } else current = 0;
347
+ return max;
348
+ }
349
+ /**
350
+ * Wrap text in a dynamically-sized backtick fence to prevent markdown rendering.
253
351
  *
254
- * Prevents file content from being rendered as headings, links, code
255
- * blocks, or horizontal rules when displayed in an ACP client.
352
+ * Instead of character-level escaping (which fails on files containing backtick
353
+ * sequences, indented code blocks, blockquotes, and list markers), this wraps
354
+ * the entire text in a backtick fence whose length exceeds any backtick sequence
355
+ * in the content. This approach is simpler and strictly more correct (following
356
+ * the claude-agent-acp pattern).
256
357
  */
257
358
  function markdownEscape(text) {
258
- return text.replace(/^(#{1,6})\s/gm, "\\$1 ").replace(/\[/g, "\\[").replace(/\]/g, "\\]").replace(/^([-*_])\1{2,}$/gm, "\\$1$1$1").replace(/</g, "\\<");
359
+ if (text === "") return "";
360
+ const fenceLen = Math.max(3, longestBacktickRun(text) + 1);
361
+ const fence = "`".repeat(fenceLen);
362
+ return `${fence}\n${text.endsWith("\n") ? text.slice(0, -1) : text}\n${fence}`;
259
363
  }
260
364
  /**
261
365
  * Format tool output into `ToolCallContent[]` by tool name.
@@ -363,6 +467,19 @@ function wrapStreamingBashOutput(text) {
363
467
  return `\`\`\`console\n${text}\n\`\`\``;
364
468
  }
365
469
  //#endregion
470
+ //#region src/acp/unreachable.ts
471
+ /**
472
+ * Exhaustive switch/case helper.
473
+ *
474
+ * Writes unknown values to stderr instead of silently ignoring them, aiding
475
+ * debugging when the pi SDK adds new event types. Never write to stdout: it
476
+ * carries the ACP NDJSON stream and any other byte poisons the protocol.
477
+ */
478
+ function unreachable(value, context) {
479
+ const label = context !== void 0 ? `[${context}] ` : "";
480
+ process.stderr.write(`${label}Unhandled value: ${String(value)}\n`);
481
+ }
482
+ //#endregion
366
483
  //#region src/acp/session.ts
367
484
  function findUniqueLineNumber(text, needle) {
368
485
  if (!needle) return void 0;
@@ -528,11 +645,12 @@ var PiAcpSession = class {
528
645
  mcpServers;
529
646
  piSession;
530
647
  supportsTerminalOutput;
531
- startupInfo = null;
532
- startupInfoSent = false;
533
648
  conn;
534
649
  cancelRequested = false;
650
+ promptRunning = false;
535
651
  pendingTurn = null;
652
+ /** Queued prompts waiting for the active turn to complete. */
653
+ pendingMessages = [];
536
654
  currentToolCalls = /* @__PURE__ */ new Map();
537
655
  /** Map of toolCallId -> toolName for streaming updates (Phase 5). */
538
656
  toolCallNames = /* @__PURE__ */ new Map();
@@ -553,21 +671,25 @@ var PiAcpSession = class {
553
671
  this.unsubscribe?.();
554
672
  this.piSession.dispose();
555
673
  }
556
- setStartupInfo(text) {
557
- this.startupInfo = text;
558
- }
559
- sendStartupInfoIfPending() {
560
- if (this.startupInfoSent || this.startupInfo === null) return;
561
- this.startupInfoSent = true;
562
- this.emit({
563
- sessionUpdate: "agent_message_chunk",
564
- content: {
565
- type: "text",
566
- text: this.startupInfo
567
- }
674
+ async prompt(message, images = []) {
675
+ if (this.promptRunning) return new Promise((resolve, reject) => {
676
+ this.pendingMessages.push({
677
+ message,
678
+ images,
679
+ resolve,
680
+ reject
681
+ });
568
682
  });
683
+ return this.executePrompt(message, images);
569
684
  }
570
- async prompt(message, images = []) {
685
+ async cancel() {
686
+ this.cancelRequested = true;
687
+ for (const pending of this.pendingMessages) pending.resolve("cancelled");
688
+ this.pendingMessages = [];
689
+ await this.piSession.abort();
690
+ }
691
+ executePrompt(message, images) {
692
+ this.promptRunning = true;
571
693
  const turnPromise = new Promise((resolve, reject) => {
572
694
  this.cancelRequested = false;
573
695
  this.pendingTurn = {
@@ -585,9 +707,17 @@ var PiAcpSession = class {
585
707
  });
586
708
  return turnPromise;
587
709
  }
588
- async cancel() {
589
- this.cancelRequested = true;
590
- await this.piSession.abort();
710
+ /**
711
+ * Dequeue and execute the next pending prompt, if any.
712
+ * Called after a turn completes.
713
+ */
714
+ dequeueNextPrompt() {
715
+ const next = this.pendingMessages.shift();
716
+ if (next === void 0) {
717
+ this.promptRunning = false;
718
+ return;
719
+ }
720
+ this.executePrompt(next.message, next.images).then(next.resolve, next.reject);
591
721
  }
592
722
  wasCancelRequested() {
593
723
  return this.cancelRequested;
@@ -622,7 +752,9 @@ var PiAcpSession = class {
622
752
  case "agent_end":
623
753
  this.handleAgentEnd();
624
754
  break;
625
- default: break;
755
+ default:
756
+ unreachable(ev, "handlePiEvent");
757
+ break;
626
758
  }
627
759
  }
628
760
  handleMessageUpdate(ame) {
@@ -792,11 +924,6 @@ var PiAcpSession = class {
792
924
  }, ...formatted];
793
925
  }
794
926
  } catch {}
795
- const meta = buildToolMeta(toolName, this.supportsTerminalOutput && isTerminalTool(toolName) ? { terminal_exit: {
796
- terminal_id: toolCallId,
797
- exit_code: extractExitCode(result),
798
- signal: null
799
- } } : void 0);
800
927
  if (content === null) {
801
928
  const formatted = formatToolContent(toolName, result, isError);
802
929
  content = formatted.length > 0 ? formatted : null;
@@ -811,6 +938,24 @@ var PiAcpSession = class {
811
938
  }
812
939
  }];
813
940
  }
941
+ if (this.supportsTerminalOutput && isTerminalTool(toolName)) {
942
+ const outputText = extractStreamingText(result);
943
+ if (outputText !== "") this.emit({
944
+ sessionUpdate: "tool_call_update",
945
+ toolCallId,
946
+ status: "in_progress",
947
+ _meta: buildToolMeta(toolName, { terminal_output: {
948
+ terminal_id: toolCallId,
949
+ data: outputText
950
+ } }),
951
+ rawOutput: result
952
+ });
953
+ }
954
+ const meta = buildToolMeta(toolName, this.supportsTerminalOutput && isTerminalTool(toolName) ? { terminal_exit: {
955
+ terminal_id: toolCallId,
956
+ exit_code: extractExitCode(result),
957
+ signal: null
958
+ } } : void 0);
814
959
  this.emit({
815
960
  sessionUpdate: "tool_call_update",
816
961
  toolCallId,
@@ -830,6 +975,7 @@ var PiAcpSession = class {
830
975
  this.lastAssistantStopReason = null;
831
976
  this.pendingTurn?.resolve(reason);
832
977
  this.pendingTurn = null;
978
+ this.dequeueNextPrompt();
833
979
  });
834
980
  }
835
981
  /**
@@ -967,122 +1113,58 @@ function acpPromptToPiMessage(blocks) {
967
1113
  };
968
1114
  }
969
1115
  //#endregion
970
- //#region src/pi-auth/status.ts
971
- /**
972
- * Detect whether the user has any pi authentication configured.
973
- *
974
- * Checks three sources:
975
- * 1. auth.json (API keys, OAuth credentials)
976
- * 2. models.json custom provider apiKey entries
977
- * 3. Known provider environment variables
978
- */
979
- const modelsConfigSchema = z.object({ providers: z.record(z.string().trim(), z.object({ apiKey: z.string().trim().optional() })).optional() });
980
- function agentDir() {
981
- const env = process.env.PI_CODING_AGENT_DIR;
982
- if (env === void 0) return join(homedir(), ".pi", "agent");
983
- if (env === "~") return homedir();
984
- if (env.startsWith("~/")) return homedir() + env.slice(1);
985
- return env;
986
- }
987
- function readJsonFile(path) {
988
- try {
989
- if (!existsSync(path)) return null;
990
- const raw = readFileSync(path, "utf-8").trim();
991
- if (!raw) return null;
992
- return JSON.parse(raw);
993
- } catch {
994
- return null;
995
- }
996
- }
997
- function hasAuthJson() {
998
- const data = readJsonFile(join(agentDir(), "auth.json"));
999
- return typeof data === "object" && data !== null && Object.keys(data).length > 0;
1000
- }
1001
- function hasCustomProviderKey() {
1002
- const raw = readJsonFile(join(agentDir(), "models.json"));
1003
- const result = modelsConfigSchema.safeParse(raw);
1004
- if (!result.success || !result.data.providers) return false;
1005
- return Object.values(result.data.providers).some((provider) => typeof provider.apiKey === "string" && provider.apiKey.trim().length > 0);
1006
- }
1007
- /** Environment variables that indicate a configured provider API key. */
1008
- const PROVIDER_ENV_VARS = [
1009
- "ANTHROPIC_API_KEY",
1010
- "ANTHROPIC_OAUTH_TOKEN",
1011
- "OPENAI_API_KEY",
1012
- "AZURE_OPENAI_API_KEY",
1013
- "GEMINI_API_KEY",
1014
- "GROQ_API_KEY",
1015
- "CEREBRAS_API_KEY",
1016
- "XAI_API_KEY",
1017
- "OPENROUTER_API_KEY",
1018
- "AI_GATEWAY_API_KEY",
1019
- "ZAI_API_KEY",
1020
- "MISTRAL_API_KEY",
1021
- "MINIMAX_API_KEY",
1022
- "MINIMAX_CN_API_KEY",
1023
- "HF_TOKEN",
1024
- "OPENCODE_API_KEY",
1025
- "KIMI_API_KEY",
1026
- "COPILOT_GITHUB_TOKEN",
1027
- "GH_TOKEN",
1028
- "GITHUB_TOKEN"
1029
- ];
1030
- function hasProviderEnvVar() {
1031
- return PROVIDER_ENV_VARS.some((key) => {
1032
- const val = process.env[key];
1033
- return typeof val === "string" && val.trim().length > 0;
1034
- });
1035
- }
1036
- function hasPiAuthConfigured() {
1037
- return hasAuthJson() || hasCustomProviderKey() || hasProviderEnvVar();
1038
- }
1116
+ //#region package.json
1117
+ var name = "@victor-software-house/pi-acp";
1118
+ var version = "0.5.0";
1039
1119
  //#endregion
1040
1120
  //#region src/acp/agent.ts
1041
- function builtinAvailableCommands() {
1042
- return [
1043
- {
1044
- name: "compact",
1045
- description: "Manually compact the session context",
1046
- input: { hint: "optional custom instructions" }
1047
- },
1048
- {
1049
- name: "autocompact",
1050
- description: "Toggle automatic context compaction",
1051
- input: { hint: "on|off|toggle" }
1052
- },
1053
- {
1054
- name: "export",
1055
- description: "Export session to an HTML file in the session cwd"
1056
- },
1057
- {
1058
- name: "session",
1059
- description: "Show session stats (messages, tokens, cost, session file)"
1060
- },
1061
- {
1062
- name: "name",
1063
- description: "Set session display name",
1064
- input: { hint: "<name>" }
1065
- },
1066
- {
1067
- name: "steering",
1068
- description: "Get/set pi steering message delivery mode",
1069
- input: { hint: "(no args to show) all | one-at-a-time" }
1070
- },
1071
- {
1072
- name: "follow-up",
1073
- description: "Get/set pi follow-up message delivery mode",
1074
- input: { hint: "(no args to show) all | one-at-a-time" }
1075
- },
1076
- {
1077
- name: "changelog",
1078
- description: "Show pi changelog"
1079
- }
1080
- ];
1081
- }
1082
- function mergeCommands(a, b) {
1083
- const out = [];
1121
+ /** Builtin ACP slash commands handled directly by the adapter. */
1122
+ const BUILTIN_COMMANDS = [
1123
+ {
1124
+ name: "compact",
1125
+ description: "Manually compact the session context",
1126
+ input: { hint: "optional custom instructions" }
1127
+ },
1128
+ {
1129
+ name: "autocompact",
1130
+ description: "Toggle automatic context compaction",
1131
+ input: { hint: "on|off|toggle" }
1132
+ },
1133
+ {
1134
+ name: "export",
1135
+ description: "Export session to an HTML file in the session cwd"
1136
+ },
1137
+ {
1138
+ name: "session",
1139
+ description: "Show session stats (messages, tokens, cost, session file)"
1140
+ },
1141
+ {
1142
+ name: "name",
1143
+ description: "Set session display name",
1144
+ input: { hint: "<name>" }
1145
+ },
1146
+ {
1147
+ name: "steering",
1148
+ description: "Get/set pi steering message delivery mode",
1149
+ input: { hint: "(no args to show) all | one-at-a-time" }
1150
+ },
1151
+ {
1152
+ name: "follow-up",
1153
+ description: "Get/set pi follow-up message delivery mode",
1154
+ input: { hint: "(no args to show) all | one-at-a-time" }
1155
+ },
1156
+ {
1157
+ name: "changelog",
1158
+ description: "Show pi changelog"
1159
+ }
1160
+ ];
1161
+ /**
1162
+ * Deduplicate commands by name. First occurrence wins.
1163
+ */
1164
+ function deduplicateCommands(commands) {
1084
1165
  const seen = /* @__PURE__ */ new Set();
1085
- for (const c of [...a, ...b]) {
1166
+ const out = [];
1167
+ for (const c of commands) {
1086
1168
  if (seen.has(c.name)) continue;
1087
1169
  seen.add(c.name);
1088
1170
  out.push(c);
@@ -1113,7 +1195,6 @@ function truncateSessionTitle(text) {
1113
1195
  if (oneLine.length <= SESSION_TITLE_MAX) return oneLine;
1114
1196
  return `${oneLine.slice(0, SESSION_TITLE_MAX - 1)}…`;
1115
1197
  }
1116
- const pkg = readNearestPackageJson(import.meta.url);
1117
1198
  var PiAcpAgent = class {
1118
1199
  conn;
1119
1200
  sessions = new SessionManager$1();
@@ -1138,9 +1219,9 @@ var PiAcpAgent = class {
1138
1219
  return {
1139
1220
  protocolVersion: requested === supportedVersion ? requested : supportedVersion,
1140
1221
  agentInfo: {
1141
- name: pkg.name,
1222
+ name,
1142
1223
  title: "pi ACP adapter",
1143
- version: pkg.version
1224
+ version
1144
1225
  },
1145
1226
  authMethods: buildAuthMethods({ supportsTerminalAuthMeta: this.clientCapabilities.terminalAuth }),
1146
1227
  agentCapabilities: {
@@ -1165,7 +1246,6 @@ var PiAcpAgent = class {
1165
1246
  }
1166
1247
  async newSession(params) {
1167
1248
  if (!isAbsolute(params.cwd)) throw RequestError.invalidParams(`cwd must be an absolute path: ${params.cwd}`);
1168
- if (!hasPiAuthConfigured()) throw RequestError.authRequired({ authMethods: buildAuthMethods() }, "Configure an API key or log in with an OAuth provider.");
1169
1249
  let result;
1170
1250
  try {
1171
1251
  result = await createAgentSession({ cwd: params.cwd });
@@ -1192,24 +1272,9 @@ var PiAcpAgent = class {
1192
1272
  supportsTerminalOutput: this.clientCapabilities.terminalOutput
1193
1273
  });
1194
1274
  this.sessions.register(session);
1195
- const quietStartup = quietStartupEnabled(params.cwd);
1196
- const updateNotice = buildUpdateNotice();
1197
- const preludeText = quietStartup ? updateNotice !== null ? `${updateNotice}\n` : "" : buildStartupInfo({
1198
- cwd: params.cwd,
1199
- updateNotice
1200
- });
1201
- if (preludeText) session.setStartupInfo(preludeText);
1202
1275
  const modes = buildThinkingModes(piSession);
1203
1276
  const models = buildModelState(piSession);
1204
1277
  const configOptions = buildConfigOptions(modes, models);
1205
- const response = {
1206
- sessionId: session.sessionId,
1207
- configOptions,
1208
- modes,
1209
- models,
1210
- _meta: { piAcp: { startupInfo: preludeText || null } }
1211
- };
1212
- if (preludeText) setTimeout(() => session.sendStartupInfoIfPending(), 0);
1213
1278
  const enableSkillCommands = skillCommandsEnabled(params.cwd);
1214
1279
  setTimeout(() => {
1215
1280
  (async () => {
@@ -1219,13 +1284,18 @@ var PiAcpAgent = class {
1219
1284
  sessionId: session.sessionId,
1220
1285
  update: {
1221
1286
  sessionUpdate: "available_commands_update",
1222
- availableCommands: mergeCommands(commands, builtinAvailableCommands())
1287
+ availableCommands: deduplicateCommands([...commands, ...BUILTIN_COMMANDS])
1223
1288
  }
1224
1289
  });
1225
1290
  } catch {}
1226
1291
  })();
1227
1292
  }, 0);
1228
- return response;
1293
+ return {
1294
+ sessionId: session.sessionId,
1295
+ configOptions,
1296
+ modes,
1297
+ models
1298
+ };
1229
1299
  }
1230
1300
  async authenticate(_params) {
1231
1301
  return {};
@@ -1453,7 +1523,7 @@ var PiAcpAgent = class {
1453
1523
  sessionId: session.sessionId,
1454
1524
  update: {
1455
1525
  sessionUpdate: "available_commands_update",
1456
- availableCommands: mergeCommands(commands, builtinAvailableCommands())
1526
+ availableCommands: deduplicateCommands([...commands, ...BUILTIN_COMMANDS])
1457
1527
  }
1458
1528
  });
1459
1529
  } catch {}
@@ -1462,16 +1532,15 @@ var PiAcpAgent = class {
1462
1532
  return {
1463
1533
  configOptions,
1464
1534
  modes,
1465
- models,
1466
- _meta: { piAcp: { startupInfo: null } }
1535
+ models
1467
1536
  };
1468
1537
  }
1469
- async unstable_closeSession(params) {
1538
+ async closeSession(params) {
1470
1539
  if (this.sessions.maybeGet(params.sessionId) === void 0) throw RequestError.invalidParams(`Unknown sessionId: ${params.sessionId}`);
1471
1540
  this.sessions.close(params.sessionId);
1472
1541
  return {};
1473
1542
  }
1474
- async unstable_resumeSession(params) {
1543
+ async resumeSession(params) {
1475
1544
  if (!isAbsolute(params.cwd)) throw RequestError.invalidParams(`cwd must be an absolute path: ${params.cwd}`);
1476
1545
  const existing = this.sessions.maybeGet(params.sessionId);
1477
1546
  if (existing !== void 0) {
@@ -1518,7 +1587,7 @@ var PiAcpAgent = class {
1518
1587
  sessionId: session.sessionId,
1519
1588
  update: {
1520
1589
  sessionUpdate: "available_commands_update",
1521
- availableCommands: mergeCommands(commands, builtinAvailableCommands())
1590
+ availableCommands: deduplicateCommands([...commands, ...BUILTIN_COMMANDS])
1522
1591
  }
1523
1592
  });
1524
1593
  } catch {}
@@ -1571,7 +1640,7 @@ var PiAcpAgent = class {
1571
1640
  sessionId: session.sessionId,
1572
1641
  update: {
1573
1642
  sessionUpdate: "available_commands_update",
1574
- availableCommands: mergeCommands(commands, builtinAvailableCommands())
1643
+ availableCommands: deduplicateCommands([...commands, ...BUILTIN_COMMANDS])
1575
1644
  }
1576
1645
  });
1577
1646
  } catch {}
@@ -1603,22 +1672,10 @@ var PiAcpAgent = class {
1603
1672
  }
1604
1673
  async unstable_setSessionModel(params) {
1605
1674
  const session = this.sessions.get(params.sessionId);
1606
- let provider = null;
1607
- let modelId = null;
1608
- if (params.modelId.includes("/")) {
1609
- const [p, ...rest] = params.modelId.split("/");
1610
- provider = p ?? null;
1611
- modelId = rest.join("/");
1612
- } else modelId = params.modelId;
1613
- if (provider === null) {
1614
- const found = session.piSession.modelRegistry.getAvailable().find((m) => m.id === modelId);
1615
- if (found) {
1616
- provider = found.provider;
1617
- modelId = found.id;
1618
- }
1619
- }
1620
- if (provider === null || modelId === null) throw RequestError.invalidParams(`Unknown modelId: ${params.modelId}`);
1621
- const model = session.piSession.modelRegistry.getAvailable().find((m) => m.provider === provider && m.id === modelId);
1675
+ const available = session.piSession.modelRegistry.getAvailable();
1676
+ const resolved = resolveModelPreference(available, params.modelId);
1677
+ if (resolved === null) throw RequestError.invalidParams(`Unknown modelId: ${params.modelId}`);
1678
+ const model = available.find((m) => m.provider === resolved.provider && m.id === resolved.id);
1622
1679
  if (!model) throw RequestError.invalidParams(`Unknown modelId: ${params.modelId}`);
1623
1680
  await session.piSession.setModel(model);
1624
1681
  this.emitConfigOptionUpdate(session);
@@ -1628,22 +1685,10 @@ var PiAcpAgent = class {
1628
1685
  const configId = String(params.configId);
1629
1686
  const value = String(params.value);
1630
1687
  if (configId === "model") {
1631
- let provider = null;
1632
- let modelId = null;
1633
- if (value.includes("/")) {
1634
- const [p, ...rest] = value.split("/");
1635
- provider = p ?? null;
1636
- modelId = rest.join("/");
1637
- } else modelId = value;
1638
- if (provider === null) {
1639
- const found = session.piSession.modelRegistry.getAvailable().find((m) => m.id === modelId);
1640
- if (found) {
1641
- provider = found.provider;
1642
- modelId = found.id;
1643
- }
1644
- }
1645
- if (provider === null || modelId === null) throw RequestError.invalidParams(`Unknown model: ${value}`);
1646
- const model = session.piSession.modelRegistry.getAvailable().find((m) => m.provider === provider && m.id === modelId);
1688
+ const available = session.piSession.modelRegistry.getAvailable();
1689
+ const resolved = resolveModelPreference(available, value);
1690
+ if (resolved === null) throw RequestError.invalidParams(`Unknown model: ${value}`);
1691
+ const model = available.find((m) => m.provider === resolved.provider && m.id === resolved.id);
1647
1692
  if (!model) throw RequestError.invalidParams(`Unknown model: ${value}`);
1648
1693
  await session.piSession.setModel(model);
1649
1694
  } else if (configId === "thought_level") {
@@ -2020,71 +2065,12 @@ function buildCommandList(piSession, enableSkillCommands) {
2020
2065
  description: skill.description ?? `(skill)`
2021
2066
  });
2022
2067
  }
2023
- const runner = piSession.extensionRunner;
2024
- if (runner) for (const cmd of runner.getRegisteredCommands()) commands.push({
2068
+ for (const cmd of piSession.extensionRunner.getRegisteredCommands()) commands.push({
2025
2069
  name: cmd.name,
2026
2070
  description: cmd.description ?? "(extension)"
2027
2071
  });
2028
2072
  return commands;
2029
2073
  }
2030
- let cachedUpdateNotice;
2031
- function buildUpdateNotice() {
2032
- if (cachedUpdateNotice !== void 0) return cachedUpdateNotice;
2033
- try {
2034
- const installed = VERSION;
2035
- if (!installed || !isSemver(installed)) {
2036
- cachedUpdateNotice = null;
2037
- return null;
2038
- }
2039
- const latestRes = spawnSync("npm", [
2040
- "view",
2041
- "@mariozechner/pi-coding-agent",
2042
- "version"
2043
- ], {
2044
- encoding: "utf-8",
2045
- timeout: 800
2046
- });
2047
- const latest = String(latestRes.stdout ?? "").trim().replace(/^v/i, "");
2048
- if (!latest || !isSemver(latest)) {
2049
- cachedUpdateNotice = null;
2050
- return null;
2051
- }
2052
- if (compareSemver(latest, installed) <= 0) {
2053
- cachedUpdateNotice = null;
2054
- return null;
2055
- }
2056
- cachedUpdateNotice = `New version available: v${latest} (installed v${installed}). Run: \`npm i -g @mariozechner/pi-coding-agent\``;
2057
- return cachedUpdateNotice;
2058
- } catch {
2059
- cachedUpdateNotice = null;
2060
- return null;
2061
- }
2062
- }
2063
- function buildStartupInfo(opts) {
2064
- const md = [];
2065
- if (VERSION) {
2066
- md.push(`pi v${VERSION}`);
2067
- md.push("---");
2068
- md.push("");
2069
- }
2070
- const addSection = (title, items) => {
2071
- const cleaned = items.map((s) => s.trim()).filter(Boolean);
2072
- if (cleaned.length === 0) return;
2073
- md.push(`## ${title}`);
2074
- for (const item of cleaned) md.push(`- ${item}`);
2075
- md.push("");
2076
- };
2077
- const contextItems = [];
2078
- const contextPath = join(opts.cwd, "AGENTS.md");
2079
- if (existsSync(contextPath)) contextItems.push(contextPath);
2080
- addSection("Context", contextItems);
2081
- if (opts.updateNotice !== void 0 && opts.updateNotice !== null) {
2082
- md.push("---");
2083
- md.push(opts.updateNotice);
2084
- md.push("");
2085
- }
2086
- return `${md.join("\n").trim()}\n`;
2087
- }
2088
2074
  function findChangelog() {
2089
2075
  try {
2090
2076
  const which = spawnSync(process.platform === "win32" ? "where" : "which", ["pi"], { encoding: "utf-8" });
@@ -2104,44 +2090,17 @@ function findChangelog() {
2104
2090
  } catch {}
2105
2091
  return null;
2106
2092
  }
2107
- function isSemver(v) {
2108
- return /^\d+\.\d+\.\d+(?:[-+].+)?$/.test(v);
2109
- }
2110
- function compareSemver(a, b) {
2111
- const pa = a.split(/[.-]/).slice(0, 3).map((n) => Number(n));
2112
- const pb = b.split(/[.-]/).slice(0, 3).map((n) => Number(n));
2113
- for (let i = 0; i < 3; i++) {
2114
- const da = pa[i] ?? 0;
2115
- const db = pb[i] ?? 0;
2116
- if (da > db) return 1;
2117
- if (da < db) return -1;
2118
- }
2119
- return 0;
2120
- }
2121
- function readNearestPackageJson(metaUrl) {
2122
- const fallback = {
2123
- name: "pi-acp",
2124
- version: "0.0.0"
2125
- };
2126
- try {
2127
- let dir = dirname(fileURLToPath(metaUrl));
2128
- for (let i = 0; i < 6; i++) {
2129
- const p = join(dir, "package.json");
2130
- if (existsSync(p)) {
2131
- const raw = JSON.parse(readFileSync(p, "utf-8"));
2132
- if (typeof raw !== "object" || raw === null) return fallback;
2133
- return {
2134
- name: "name" in raw && typeof raw.name === "string" ? raw.name : fallback.name,
2135
- version: "version" in raw && typeof raw.version === "string" ? raw.version : fallback.version
2136
- };
2137
- }
2138
- dir = dirname(dir);
2139
- }
2140
- } catch {}
2141
- return fallback;
2142
- }
2143
2093
  //#endregion
2144
2094
  //#region src/index.ts
2095
+ {
2096
+ const toStderr = (...args) => {
2097
+ process.stderr.write(`${args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ")}\n`);
2098
+ };
2099
+ console.log = toStderr;
2100
+ console.info = toStderr;
2101
+ console.warn = toStderr;
2102
+ console.debug = toStderr;
2103
+ }
2145
2104
  if (process.argv.includes("--terminal-login")) {
2146
2105
  const { spawnSync } = await import("node:child_process");
2147
2106
  const isWindows = platform() === "win32";
@@ -2151,7 +2110,7 @@ if (process.argv.includes("--terminal-login")) {
2151
2110
  env: process.env
2152
2111
  });
2153
2112
  if (res.error && "code" in res.error && res.error.code === "ENOENT") {
2154
- process.stderr.write(`pi-acp: could not start pi (command not found: ${cmd}). Install via \`npm install -g @mariozechner/pi-coding-agent\` or ensure \`pi\` is on your PATH.
2113
+ process.stderr.write(`pi-acp: could not start pi (command not found: ${cmd}). Install via \`npm install -g @earendil-works/pi-coding-agent\` or ensure \`pi\` is on your PATH.
2155
2114
  `);
2156
2115
  process.exit(1);
2157
2116
  }
@@ -2174,7 +2133,10 @@ const agent = new AgentSideConnection((conn) => new PiAcpAgent(conn), ndJsonStre
2174
2133
  process.stdin.on("end", () => controller.close());
2175
2134
  process.stdin.on("error", (err) => controller.error(err));
2176
2135
  } })));
2136
+ let shuttingDown = false;
2177
2137
  function shutdown() {
2138
+ if (shuttingDown) return;
2139
+ shuttingDown = true;
2178
2140
  try {
2179
2141
  if ("agent" in agent) {
2180
2142
  const inner = agent.agent;
@@ -2183,9 +2145,7 @@ function shutdown() {
2183
2145
  } catch {}
2184
2146
  process.exit(0);
2185
2147
  }
2186
- process.stdin.on("end", shutdown);
2187
- process.stdin.on("close", shutdown);
2188
- process.stdin.resume();
2148
+ agent.closed.then(shutdown);
2189
2149
  process.on("SIGINT", shutdown);
2190
2150
  process.on("SIGTERM", shutdown);
2191
2151
  process.stdout.on("error", () => process.exit(0));