pi-acp 0.0.15 → 0.0.16

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 CHANGED
@@ -5,17 +5,95 @@ import { AgentSideConnection, ndJsonStream } from "@agentclientprotocol/sdk";
5
5
 
6
6
  // src/acp/agent.ts
7
7
  import {
8
- RequestError as RequestError2
8
+ RequestError as RequestError3
9
9
  } from "@agentclientprotocol/sdk";
10
10
 
11
+ // src/acp/auth.ts
12
+ var PI_SETUP_METHOD_ID = "pi_terminal_login";
13
+ function getAuthMethods(opts) {
14
+ const supportsTerminalAuthMeta = opts?.supportsTerminalAuthMeta ?? true;
15
+ const method = {
16
+ id: PI_SETUP_METHOD_ID,
17
+ name: "Launch pi in the terminal",
18
+ description: "Start pi in an interactive terminal to configure API keys or login",
19
+ // Registry-required fields
20
+ type: "terminal",
21
+ args: ["--terminal-login"],
22
+ env: {}
23
+ };
24
+ if (supportsTerminalAuthMeta) {
25
+ const launch = terminalAuthLaunchSpec();
26
+ method._meta = {
27
+ ...method._meta ?? {},
28
+ "terminal-auth": {
29
+ ...launch,
30
+ label: "Launch pi"
31
+ }
32
+ };
33
+ }
34
+ return [method];
35
+ }
36
+ function terminalAuthLaunchSpec() {
37
+ const argv0 = process.argv[0] || "node";
38
+ const argv1 = process.argv[1];
39
+ if (argv1 && argv0) {
40
+ const isNode = argv0.includes("node");
41
+ const isJs = argv1.endsWith(".js");
42
+ if (isNode && isJs) {
43
+ return { command: argv0, args: [argv1, "--terminal-login"] };
44
+ }
45
+ }
46
+ return { command: "pi-acp", args: ["--terminal-login"] };
47
+ }
48
+
11
49
  // src/acp/session.ts
50
+ import { RequestError as RequestError2 } from "@agentclientprotocol/sdk";
51
+
52
+ // src/acp/auth-required.ts
12
53
  import { RequestError } from "@agentclientprotocol/sdk";
54
+ function maybeAuthRequiredError(err) {
55
+ const msg = String(err?.message ?? err ?? "");
56
+ const s = msg.toLowerCase();
57
+ const patterns = [
58
+ "api key",
59
+ "apikey",
60
+ "missing key",
61
+ "no key",
62
+ "not configured",
63
+ "unauthorized",
64
+ "authentication",
65
+ "permission denied",
66
+ "forbidden",
67
+ "401",
68
+ "403"
69
+ ];
70
+ const hit = patterns.some((p) => s.includes(p));
71
+ if (!hit) return null;
72
+ return RequestError.authRequired(
73
+ {
74
+ authMethods: getAuthMethods()
75
+ },
76
+ "Configure an API key or log in with an OAuth provider."
77
+ );
78
+ }
79
+
80
+ // src/acp/session.ts
13
81
  import { readFileSync as readFileSync3 } from "fs";
14
82
  import { isAbsolute, resolve as resolvePath } from "path";
15
83
 
16
84
  // src/pi-rpc/process.ts
17
85
  import { spawn } from "child_process";
18
86
  import * as readline from "readline";
87
+ var PiRpcSpawnError = class extends Error {
88
+ /** Underlying spawn error code, e.g. ENOENT, EACCES */
89
+ code;
90
+ constructor(message, opts) {
91
+ super(message);
92
+ this.name = "PiRpcSpawnError";
93
+ this.code = opts?.code;
94
+ this.cause = opts?.cause;
95
+ }
96
+ };
19
97
  function stripAnsi(s) {
20
98
  return s.replace(/[\u001B\u009B][[\]()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, "");
21
99
  }
@@ -55,6 +133,10 @@ var PiRpcProcess = class _PiRpcProcess {
55
133
  for (const [, p] of this.pending) p.reject(err);
56
134
  this.pending.clear();
57
135
  });
136
+ child.on("error", (err) => {
137
+ for (const [, p] of this.pending) p.reject(err);
138
+ this.pending.clear();
139
+ });
58
140
  }
59
141
  static async spawn(params) {
60
142
  const cmd = params.piCommand ?? "pi";
@@ -65,6 +147,36 @@ var PiRpcProcess = class _PiRpcProcess {
65
147
  stdio: "pipe",
66
148
  env: process.env
67
149
  });
150
+ try {
151
+ await new Promise((resolve2, reject) => {
152
+ const onSpawn = () => {
153
+ cleanup();
154
+ resolve2();
155
+ };
156
+ const onError = (err) => {
157
+ cleanup();
158
+ reject(err);
159
+ };
160
+ const cleanup = () => {
161
+ child.off("spawn", onSpawn);
162
+ child.off("error", onError);
163
+ };
164
+ child.once("spawn", onSpawn);
165
+ child.once("error", onError);
166
+ });
167
+ } catch (e) {
168
+ const code = typeof e?.code === "string" ? e.code : void 0;
169
+ if (code === "ENOENT") {
170
+ throw new PiRpcSpawnError(
171
+ `Could not start pi: executable not found (command: ${cmd}). Pi needs to be installed before it can run in ACP clients. Install it via \`npm install -g @mariozechner/pi-coding-agent\` or ensure \`pi\` is on your PATH. Then try again.`,
172
+ { code, cause: e }
173
+ );
174
+ }
175
+ if (code === "EACCES") {
176
+ throw new PiRpcSpawnError(`Could not start pi: permission denied (command: ${cmd}).`, { code, cause: e });
177
+ }
178
+ throw new PiRpcSpawnError(`Could not start pi (command: ${cmd}).`, { code, cause: e });
179
+ }
68
180
  child.stderr.on("data", () => {
69
181
  });
70
182
  const proc = new _PiRpcProcess(child);
@@ -86,6 +198,13 @@ var PiRpcProcess = class _PiRpcProcess {
86
198
  this.eventHandlers = this.eventHandlers.filter((h) => h !== handler);
87
199
  };
88
200
  }
201
+ dispose(signal = "SIGTERM") {
202
+ if (this.child.killed) return;
203
+ try {
204
+ this.child.kill(signal);
205
+ } catch {
206
+ }
207
+ }
89
208
  /**
90
209
  * Human-readable stdout lines emitted before RPC NDJSON begins (e.g. Context/Skills/Extensions info).
91
210
  * Themes are typically noisy/less useful for ACP, so callers can filter as needed.
@@ -164,12 +283,17 @@ var PiRpcProcess = class _PiRpcProcess {
164
283
  const line = JSON.stringify(withId) + "\n";
165
284
  return new Promise((resolve2, reject) => {
166
285
  this.pending.set(id, { resolve: resolve2, reject });
167
- this.child.stdin.write(line, (err) => {
168
- if (err) {
169
- this.pending.delete(id);
170
- reject(err);
171
- }
172
- });
286
+ try {
287
+ this.child.stdin.write(line, (err) => {
288
+ if (err) {
289
+ this.pending.delete(id);
290
+ reject(err);
291
+ }
292
+ });
293
+ } catch (e) {
294
+ this.pending.delete(id);
295
+ reject(e);
296
+ }
173
297
  });
174
298
  }
175
299
  };
@@ -393,10 +517,24 @@ var SessionManager = class {
393
517
  return this.sessions.get(sessionId);
394
518
  }
395
519
  async create(params) {
396
- const proc = await PiRpcProcess.spawn({
397
- cwd: params.cwd
398
- });
399
- const state = await proc.getState();
520
+ let proc;
521
+ try {
522
+ proc = await PiRpcProcess.spawn({
523
+ cwd: params.cwd,
524
+ piCommand: params.piCommand
525
+ });
526
+ } catch (e) {
527
+ if (e instanceof PiRpcSpawnError) {
528
+ throw RequestError2.internalError({ code: e.code }, e.message);
529
+ }
530
+ throw e;
531
+ }
532
+ let state = null;
533
+ try {
534
+ state = await proc.getState();
535
+ } catch {
536
+ state = null;
537
+ }
400
538
  const sessionId = typeof state?.sessionId === "string" ? state.sessionId : crypto.randomUUID();
401
539
  const sessionFile = typeof state?.sessionFile === "string" ? state.sessionFile : null;
402
540
  if (sessionFile) {
@@ -415,7 +553,7 @@ var SessionManager = class {
415
553
  }
416
554
  get(sessionId) {
417
555
  const s = this.sessions.get(sessionId);
418
- if (!s) throw RequestError.invalidParams(`Unknown sessionId: ${sessionId}`);
556
+ if (!s) throw RequestError2.invalidParams(`Unknown sessionId: ${sessionId}`);
419
557
  return s;
420
558
  }
421
559
  /**
@@ -562,8 +700,13 @@ var PiAcpSession = class {
562
700
  });
563
701
  this.proc.prompt(t.message, t.attachments).catch((err) => {
564
702
  void this.flushEmits().finally(() => {
565
- const reason = this.cancelRequested ? "cancelled" : "error";
566
- this.pendingTurn?.resolve(reason);
703
+ const authErr = maybeAuthRequiredError(err);
704
+ if (authErr) {
705
+ this.pendingTurn?.reject(authErr);
706
+ } else {
707
+ const reason = this.cancelRequested ? "cancelled" : "error";
708
+ this.pendingTurn?.resolve(reason);
709
+ }
567
710
  this.pendingTurn = null;
568
711
  this.inAgentLoop = false;
569
712
  this.emit({
@@ -767,6 +910,245 @@ function toToolKind(toolName) {
767
910
  }
768
911
  }
769
912
 
913
+ // src/acp/pi-sessions.ts
914
+ import { readdirSync as readdirSync2, readFileSync as readFileSync4, statSync, openSync, readSync, closeSync } from "fs";
915
+ import { homedir as homedir3 } from "os";
916
+ import { join as join3 } from "path";
917
+ var DEFAULT_TAIL_BYTES = 256 * 1024;
918
+ var DEFAULT_HEAD_BYTES = 64 * 1024;
919
+ function getPiAgentDir() {
920
+ return process.env.PI_CODING_AGENT_DIR ?? join3(homedir3(), ".pi", "agent");
921
+ }
922
+ function getPiSessionsDir() {
923
+ return join3(getPiAgentDir(), "sessions");
924
+ }
925
+ function walkJsonlFiles(dir, out) {
926
+ let entries;
927
+ try {
928
+ entries = readdirSync2(dir, { withFileTypes: true, encoding: "utf8" });
929
+ } catch {
930
+ return;
931
+ }
932
+ for (const e of entries) {
933
+ const name = typeof e.name === "string" ? e.name : String(e.name);
934
+ const p = join3(dir, name);
935
+ if (e.isDirectory()) walkJsonlFiles(p, out);
936
+ else if (e.isFile() && name.endsWith(".jsonl")) out.push(p);
937
+ }
938
+ }
939
+ function readFirstLine(path) {
940
+ const fd = openSync(path, "r");
941
+ try {
942
+ const buf = Buffer.alloc(DEFAULT_HEAD_BYTES);
943
+ const n = readSync(fd, buf, 0, buf.length, 0);
944
+ if (n <= 0) return null;
945
+ const s = buf.subarray(0, n).toString("utf-8");
946
+ const idx = s.indexOf("\n");
947
+ return idx === -1 ? s.trim() : s.slice(0, idx).trim();
948
+ } catch {
949
+ return null;
950
+ } finally {
951
+ try {
952
+ closeSync(fd);
953
+ } catch {
954
+ }
955
+ }
956
+ }
957
+ function readTail(path, tailBytes = DEFAULT_TAIL_BYTES) {
958
+ const st = statSync(path);
959
+ const start = Math.max(0, st.size - tailBytes);
960
+ const len = st.size - start;
961
+ const fd = openSync(path, "r");
962
+ try {
963
+ const buf = Buffer.alloc(len);
964
+ const n = readSync(fd, buf, 0, buf.length, start);
965
+ return buf.subarray(0, n).toString("utf-8");
966
+ } finally {
967
+ try {
968
+ closeSync(fd);
969
+ } catch {
970
+ }
971
+ }
972
+ }
973
+ function parseSessionHeader(firstLine) {
974
+ try {
975
+ const obj = JSON.parse(firstLine);
976
+ if (obj?.type !== "session") return null;
977
+ const sessionId = typeof obj?.id === "string" ? obj.id : null;
978
+ const cwd = typeof obj?.cwd === "string" ? obj.cwd : null;
979
+ if (!sessionId || !cwd) return null;
980
+ return { sessionId, cwd };
981
+ } catch {
982
+ return null;
983
+ }
984
+ }
985
+ function pickTitleFromTail(tail) {
986
+ const lines = tail.split(/\r?\n/);
987
+ for (let i = lines.length - 1; i >= 0; i--) {
988
+ const line = lines[i].trim();
989
+ if (!line) continue;
990
+ try {
991
+ const obj = JSON.parse(line);
992
+ if (obj?.type === "session_info" && typeof obj?.name === "string" && obj.name.trim()) {
993
+ return obj.name.trim();
994
+ }
995
+ } catch {
996
+ }
997
+ }
998
+ return null;
999
+ }
1000
+ function scanSessionInfoNameFromFile(path) {
1001
+ const fd = openSync(path, "r");
1002
+ try {
1003
+ const buf = Buffer.alloc(256 * 1024);
1004
+ let leftover = "";
1005
+ let offset = 0;
1006
+ let lastName = null;
1007
+ while (true) {
1008
+ const n = readSync(fd, buf, 0, buf.length, offset);
1009
+ if (n <= 0) break;
1010
+ offset += n;
1011
+ const chunk = leftover + buf.subarray(0, n).toString("utf8");
1012
+ const lines = chunk.split(/\r?\n/);
1013
+ leftover = lines.pop() ?? "";
1014
+ for (const line0 of lines) {
1015
+ const line = line0.trim();
1016
+ if (!line) continue;
1017
+ try {
1018
+ const obj = JSON.parse(line);
1019
+ if (obj?.type === "session_info" && typeof obj?.name === "string" && obj.name.trim()) {
1020
+ lastName = obj.name.trim();
1021
+ }
1022
+ } catch {
1023
+ }
1024
+ }
1025
+ }
1026
+ const tailLine = leftover.trim();
1027
+ if (tailLine) {
1028
+ try {
1029
+ const obj = JSON.parse(tailLine);
1030
+ if (obj?.type === "session_info" && typeof obj?.name === "string" && obj.name.trim()) {
1031
+ lastName = obj.name.trim();
1032
+ }
1033
+ } catch {
1034
+ }
1035
+ }
1036
+ return lastName;
1037
+ } catch {
1038
+ return null;
1039
+ } finally {
1040
+ try {
1041
+ closeSync(fd);
1042
+ } catch {
1043
+ }
1044
+ }
1045
+ }
1046
+ function pickUpdatedAtFromTail(tail) {
1047
+ const lines = tail.split(/\r?\n/);
1048
+ for (let i = lines.length - 1; i >= 0; i--) {
1049
+ const line = lines[i].trim();
1050
+ if (!line) continue;
1051
+ try {
1052
+ const obj = JSON.parse(line);
1053
+ if (obj?.type !== "message") continue;
1054
+ const ts = typeof obj?.timestamp === "string" ? obj.timestamp : null;
1055
+ if (!ts) continue;
1056
+ const d = new Date(ts);
1057
+ if (Number.isFinite(d.getTime())) return d.toISOString();
1058
+ } catch {
1059
+ }
1060
+ }
1061
+ for (let i = lines.length - 1; i >= 0; i--) {
1062
+ const line = lines[i].trim();
1063
+ if (!line) continue;
1064
+ try {
1065
+ const obj = JSON.parse(line);
1066
+ const ts = typeof obj?.timestamp === "string" ? obj.timestamp : null;
1067
+ if (!ts) continue;
1068
+ const d = new Date(ts);
1069
+ if (Number.isFinite(d.getTime())) return d.toISOString();
1070
+ } catch {
1071
+ }
1072
+ }
1073
+ return null;
1074
+ }
1075
+ function pickFallbackTitleFromHead(path) {
1076
+ try {
1077
+ const raw = readFileSync4(path, { encoding: "utf8" });
1078
+ const lines = raw.split(/\r?\n/);
1079
+ for (const line0 of lines) {
1080
+ const line = line0.trim();
1081
+ if (!line) continue;
1082
+ try {
1083
+ const obj = JSON.parse(line);
1084
+ if (obj?.type === "message" && obj?.message?.role === "user") {
1085
+ const content = obj?.message?.content;
1086
+ if (typeof content === "string") return content.slice(0, 80);
1087
+ if (Array.isArray(content)) {
1088
+ const t = content.find((c) => c?.type === "text" && typeof c?.text === "string");
1089
+ if (t?.text) return String(t.text).slice(0, 80);
1090
+ }
1091
+ }
1092
+ } catch {
1093
+ }
1094
+ if (lines.length > 2e3) break;
1095
+ }
1096
+ } catch {
1097
+ }
1098
+ return null;
1099
+ }
1100
+ function listPiSessions() {
1101
+ const sessionsDir = getPiSessionsDir();
1102
+ const files = [];
1103
+ walkJsonlFiles(sessionsDir, files);
1104
+ const items = [];
1105
+ for (const file of files) {
1106
+ const first = readFirstLine(file);
1107
+ if (!first) continue;
1108
+ const header = parseSessionHeader(first);
1109
+ if (!header) continue;
1110
+ let updatedAt = null;
1111
+ let title = null;
1112
+ try {
1113
+ const tail = readTail(file);
1114
+ title = pickTitleFromTail(tail);
1115
+ updatedAt = pickUpdatedAtFromTail(tail);
1116
+ } catch {
1117
+ }
1118
+ if (!title) {
1119
+ title = scanSessionInfoNameFromFile(file);
1120
+ }
1121
+ if (!updatedAt) {
1122
+ try {
1123
+ updatedAt = statSync(file).mtime.toISOString();
1124
+ } catch {
1125
+ updatedAt = null;
1126
+ }
1127
+ }
1128
+ if (!title) {
1129
+ title = pickFallbackTitleFromHead(file);
1130
+ }
1131
+ items.push({
1132
+ sessionId: header.sessionId,
1133
+ cwd: header.cwd,
1134
+ title,
1135
+ updatedAt,
1136
+ sessionFile: file
1137
+ });
1138
+ }
1139
+ items.sort((a, b) => {
1140
+ const aa = a.updatedAt ?? "";
1141
+ const bb = b.updatedAt ?? "";
1142
+ return bb.localeCompare(aa);
1143
+ });
1144
+ return items;
1145
+ }
1146
+ function findPiSessionFile(sessionId) {
1147
+ const all = listPiSessions();
1148
+ const found = all.find((s) => s.sessionId === sessionId);
1149
+ return found?.sessionFile ?? null;
1150
+ }
1151
+
770
1152
  // src/acp/translate/pi-messages.ts
771
1153
  function normalizePiMessageText(content) {
772
1154
  if (typeof content === "string") return content;
@@ -842,9 +1224,80 @@ ${r.text}`;
842
1224
 
843
1225
  // src/acp/agent.ts
844
1226
  import { isAbsolute as isAbsolute2 } from "path";
845
- import { existsSync as existsSync2, readFileSync as readFileSync4, realpathSync, readdirSync as readdirSync2, statSync } from "fs";
846
- import { join as join3, dirname as dirname2, basename } from "path";
1227
+ import { existsSync as existsSync3, readFileSync as readFileSync6, realpathSync, readdirSync as readdirSync3, statSync as statSync2 } from "fs";
1228
+ import { join as join5, dirname as dirname2, basename } from "path";
847
1229
  import { spawnSync } from "child_process";
1230
+
1231
+ // src/pi-auth/status.ts
1232
+ import { existsSync as existsSync2, readFileSync as readFileSync5 } from "fs";
1233
+ import { homedir as homedir4 } from "os";
1234
+ import { join as join4 } from "path";
1235
+ function safeReadJson(path) {
1236
+ try {
1237
+ if (!existsSync2(path)) return null;
1238
+ const raw = readFileSync5(path, "utf-8");
1239
+ if (!raw.trim()) return null;
1240
+ return JSON.parse(raw);
1241
+ } catch {
1242
+ return null;
1243
+ }
1244
+ }
1245
+ function getPiAgentDir2() {
1246
+ const envDir = process.env.PI_CODING_AGENT_DIR;
1247
+ if (envDir) {
1248
+ if (envDir === "~") return homedir4();
1249
+ if (envDir.startsWith("~/")) return homedir4() + envDir.slice(1);
1250
+ return envDir;
1251
+ }
1252
+ return join4(homedir4(), ".pi", "agent");
1253
+ }
1254
+ function hasAnyPiAuthConfigured() {
1255
+ const agentDir = getPiAgentDir2();
1256
+ const authPath = join4(agentDir, "auth.json");
1257
+ const auth = safeReadJson(authPath);
1258
+ if (auth && typeof auth === "object" && Object.keys(auth).length > 0) return true;
1259
+ const modelsPath = join4(agentDir, "models.json");
1260
+ const models = safeReadJson(modelsPath);
1261
+ const providers = models?.providers;
1262
+ if (providers && typeof providers === "object") {
1263
+ for (const p of Object.values(providers)) {
1264
+ if (p && typeof p === "object" && typeof p.apiKey === "string" && p.apiKey.trim()) {
1265
+ return true;
1266
+ }
1267
+ }
1268
+ }
1269
+ const envVars = [
1270
+ "OPENAI_API_KEY",
1271
+ "AZURE_OPENAI_API_KEY",
1272
+ "GEMINI_API_KEY",
1273
+ "GROQ_API_KEY",
1274
+ "CEREBRAS_API_KEY",
1275
+ "XAI_API_KEY",
1276
+ "OPENROUTER_API_KEY",
1277
+ "AI_GATEWAY_API_KEY",
1278
+ "ZAI_API_KEY",
1279
+ "MISTRAL_API_KEY",
1280
+ "MINIMAX_API_KEY",
1281
+ "MINIMAX_CN_API_KEY",
1282
+ "HF_TOKEN",
1283
+ "OPENCODE_API_KEY",
1284
+ "KIMI_API_KEY",
1285
+ // Copilot/github
1286
+ "COPILOT_GITHUB_TOKEN",
1287
+ "GH_TOKEN",
1288
+ "GITHUB_TOKEN",
1289
+ // Anthropic oauth
1290
+ "ANTHROPIC_OAUTH_TOKEN",
1291
+ "ANTHROPIC_API_KEY"
1292
+ ];
1293
+ for (const k of envVars) {
1294
+ const v = process.env[k];
1295
+ if (typeof v === "string" && v.trim()) return true;
1296
+ }
1297
+ return false;
1298
+ }
1299
+
1300
+ // src/acp/agent.ts
848
1301
  import { fileURLToPath } from "url";
849
1302
  function booleanEnv(name, defaultValue) {
850
1303
  const raw = process.env[name];
@@ -905,6 +1358,8 @@ var PiAcpAgent = class {
905
1358
  conn;
906
1359
  sessions = new SessionManager();
907
1360
  store = new SessionStore();
1361
+ // Remember recent session cwd and use it as the default filter.
1362
+ lastSessionCwd = null;
908
1363
  constructor(conn, _config) {
909
1364
  this.conn = conn;
910
1365
  void _config;
@@ -919,7 +1374,11 @@ var PiAcpAgent = class {
919
1374
  title: "pi ACP adapter",
920
1375
  version: pkg.version ?? "0.0.0"
921
1376
  },
922
- authMethods: [],
1377
+ // Zed currently uses ClientCapabilities._meta["terminal-auth"] to decide whether to show
1378
+ // the "Authenticate" banner/button. If not supported, we still return the method for the registry.
1379
+ authMethods: getAuthMethods({
1380
+ supportsTerminalAuthMeta: params?.clientCapabilities?._meta?.["terminal-auth"] === true
1381
+ }),
923
1382
  agentCapabilities: {
924
1383
  loadSession: true,
925
1384
  mcpCapabilities: { http: false, sse: false },
@@ -928,21 +1387,49 @@ var PiAcpAgent = class {
928
1387
  audio: false,
929
1388
  embeddedContext: false
930
1389
  },
931
- sessionCapabilities: {}
1390
+ sessionCapabilities: {
1391
+ // **UNSTABLE** ACP capability used by Zed's codex-acp adapter.
1392
+ // Enables a native session picker in clients that support it.
1393
+ list: {}
1394
+ }
932
1395
  }
933
1396
  };
934
1397
  }
935
1398
  async newSession(params) {
936
1399
  if (!isAbsolute2(params.cwd)) {
937
- throw RequestError2.invalidParams(`cwd must be an absolute path: ${params.cwd}`);
1400
+ throw RequestError3.invalidParams(`cwd must be an absolute path: ${params.cwd}`);
1401
+ }
1402
+ this.lastSessionCwd = params.cwd;
1403
+ if (!hasAnyPiAuthConfigured()) {
1404
+ throw RequestError3.authRequired(
1405
+ { authMethods: getAuthMethods() },
1406
+ "Configure an API key or log in with an OAuth provider."
1407
+ );
938
1408
  }
939
1409
  const fileCommands = loadSlashCommands(params.cwd);
940
1410
  const session = await this.sessions.create({
941
1411
  cwd: params.cwd,
942
1412
  mcpServers: params.mcpServers,
943
1413
  conn: this.conn,
944
- fileCommands
1414
+ fileCommands,
1415
+ piCommand: process.env.PI_ACP_PI_COMMAND
945
1416
  });
1417
+ let rawModelsCount = 0;
1418
+ try {
1419
+ const data = await session.proc.getAvailableModels();
1420
+ rawModelsCount = Array.isArray(data?.models) ? data.models.length : 0;
1421
+ } catch {
1422
+ }
1423
+ if (rawModelsCount === 0) {
1424
+ try {
1425
+ session.proc.dispose?.();
1426
+ } catch {
1427
+ }
1428
+ throw RequestError3.authRequired(
1429
+ { authMethods: getAuthMethods() },
1430
+ "Configure an API key or log in with an OAuth provider."
1431
+ );
1432
+ }
946
1433
  const models = await getModelState(session.proc);
947
1434
  const thinking = await getThinkingState(session.proc);
948
1435
  const showStartupInfo = booleanEnv("PI_ACP_STARTUP_INFO", true);
@@ -1121,8 +1608,8 @@ ${JSON.stringify(stats, null, 2)}`;
1121
1608
  if (piPath) {
1122
1609
  const resolved = realpathSync(piPath);
1123
1610
  const pkgRoot = dirname2(dirname2(resolved));
1124
- const p = join3(pkgRoot, "CHANGELOG.md");
1125
- if (existsSync2(p)) return p;
1611
+ const p = join5(pkgRoot, "CHANGELOG.md");
1612
+ if (existsSync3(p)) return p;
1126
1613
  }
1127
1614
  } catch {
1128
1615
  }
@@ -1130,8 +1617,8 @@ ${JSON.stringify(stats, null, 2)}`;
1130
1617
  const npmRoot = spawnSync("npm", ["root", "-g"], { encoding: "utf-8" });
1131
1618
  const root = String(npmRoot.stdout ?? "").trim();
1132
1619
  if (root) {
1133
- const p = join3(root, "@mariozechner", "pi-coding-agent", "CHANGELOG.md");
1134
- if (existsSync2(p)) return p;
1620
+ const p = join5(root, "@mariozechner", "pi-coding-agent", "CHANGELOG.md");
1621
+ if (existsSync3(p)) return p;
1135
1622
  }
1136
1623
  } catch {
1137
1624
  }
@@ -1150,7 +1637,7 @@ ${JSON.stringify(stats, null, 2)}`;
1150
1637
  }
1151
1638
  let text = "";
1152
1639
  try {
1153
- text = readFileSync4(changelogPath, "utf-8");
1640
+ text = readFileSync6(changelogPath, "utf-8");
1154
1641
  } catch (e) {
1155
1642
  await this.conn.sessionUpdate({
1156
1643
  sessionId: session.sessionId,
@@ -1176,7 +1663,7 @@ ${JSON.stringify(stats, null, 2)}`;
1176
1663
  const state = await session.proc.getState();
1177
1664
  const sessionFile = typeof state?.sessionFile === "string" ? state.sessionFile : null;
1178
1665
  const messageCount = typeof state?.messageCount === "number" ? state.messageCount : 0;
1179
- if (!sessionFile || messageCount === 0 || !existsSync2(sessionFile)) {
1666
+ if (!sessionFile || messageCount === 0 || !existsSync3(sessionFile)) {
1180
1667
  await this.conn.sessionUpdate({
1181
1668
  sessionId: session.sessionId,
1182
1669
  update: {
@@ -1190,7 +1677,7 @@ ${JSON.stringify(stats, null, 2)}`;
1190
1677
  return { stopReason: "end_turn" };
1191
1678
  }
1192
1679
  try {
1193
- const raw = readFileSync4(sessionFile, "utf-8");
1680
+ const raw = readFileSync6(sessionFile, "utf-8");
1194
1681
  if (raw.trim().length === 0) {
1195
1682
  await this.conn.sessionUpdate({
1196
1683
  sessionId: session.sessionId,
@@ -1218,7 +1705,7 @@ ${JSON.stringify(stats, null, 2)}`;
1218
1705
  return { stopReason: "end_turn" };
1219
1706
  }
1220
1707
  const safeSessionId = session.sessionId.replace(/[^a-zA-Z0-9_-]/g, "_");
1221
- const outputPath = join3(session.cwd, `pi-session-${safeSessionId}.html`);
1708
+ const outputPath = join5(session.cwd, `pi-session-${safeSessionId}.html`);
1222
1709
  let resultPath = "";
1223
1710
  try {
1224
1711
  const result2 = await session.proc.exportHtml(outputPath);
@@ -1307,18 +1794,46 @@ ${JSON.stringify(stats, null, 2)}`;
1307
1794
  const session = this.sessions.get(params.sessionId);
1308
1795
  await session.cancel();
1309
1796
  }
1797
+ async unstable_listSessions(params) {
1798
+ const all = listPiSessions();
1799
+ const effectiveCwd = params.cwd ?? this.lastSessionCwd;
1800
+ const filtered = effectiveCwd ? all.filter((s) => s.cwd === effectiveCwd) : all;
1801
+ const offset = params.cursor ? Number.parseInt(params.cursor, 10) : 0;
1802
+ const start = Number.isFinite(offset) && offset > 0 ? offset : 0;
1803
+ const PAGE_SIZE = 50;
1804
+ const page = filtered.slice(start, start + PAGE_SIZE);
1805
+ const sessions = page.map((s) => ({
1806
+ sessionId: s.sessionId,
1807
+ cwd: s.cwd,
1808
+ title: s.title,
1809
+ updatedAt: s.updatedAt
1810
+ }));
1811
+ const nextCursor = start + PAGE_SIZE < filtered.length ? String(start + PAGE_SIZE) : null;
1812
+ return { sessions, nextCursor, _meta: {} };
1813
+ }
1310
1814
  async loadSession(params) {
1311
1815
  if (!isAbsolute2(params.cwd)) {
1312
- throw RequestError2.invalidParams(`cwd must be an absolute path: ${params.cwd}`);
1816
+ throw RequestError3.invalidParams(`cwd must be an absolute path: ${params.cwd}`);
1313
1817
  }
1818
+ this.lastSessionCwd = params.cwd;
1314
1819
  const stored = this.store.get(params.sessionId);
1315
- if (!stored) {
1316
- throw RequestError2.invalidParams(`Unknown sessionId: ${params.sessionId}`);
1820
+ const sessionFile = stored?.sessionFile ?? findPiSessionFile(params.sessionId);
1821
+ if (!sessionFile) {
1822
+ throw RequestError3.invalidParams(`Unknown sessionId: ${params.sessionId}`);
1823
+ }
1824
+ let proc;
1825
+ try {
1826
+ proc = await PiRpcProcess.spawn({
1827
+ cwd: params.cwd,
1828
+ sessionPath: sessionFile,
1829
+ piCommand: process.env.PI_ACP_PI_COMMAND
1830
+ });
1831
+ } catch (e) {
1832
+ if (e?.name === "PiRpcSpawnError") {
1833
+ throw RequestError3.internalError({ code: e?.code }, String(e?.message ?? e));
1834
+ }
1835
+ throw e;
1317
1836
  }
1318
- const proc = await PiRpcProcess.spawn({
1319
- cwd: params.cwd,
1320
- sessionPath: stored.sessionFile
1321
- });
1322
1837
  const fileCommands = loadSlashCommands(params.cwd);
1323
1838
  const session = this.sessions.getOrCreate(params.sessionId, {
1324
1839
  cwd: params.cwd,
@@ -1330,7 +1845,7 @@ ${JSON.stringify(stats, null, 2)}`;
1330
1845
  this.store.upsert({
1331
1846
  sessionId: params.sessionId,
1332
1847
  cwd: params.cwd,
1333
- sessionFile: stored.sessionFile
1848
+ sessionFile
1334
1849
  });
1335
1850
  const data = await proc.getMessages();
1336
1851
  const messages = Array.isArray(data?.messages) ? data.messages : [];
@@ -1360,22 +1875,46 @@ ${JSON.stringify(stats, null, 2)}`;
1360
1875
  });
1361
1876
  }
1362
1877
  }
1878
+ if (role === "toolResult") {
1879
+ const toolName = String(m?.toolName ?? "tool");
1880
+ const toolCallId = String(m?.toolCallId ?? crypto.randomUUID());
1881
+ const isError = Boolean(m?.isError);
1882
+ await this.conn.sessionUpdate({
1883
+ sessionId: session.sessionId,
1884
+ update: {
1885
+ sessionUpdate: "tool_call",
1886
+ toolCallId,
1887
+ title: toolName,
1888
+ kind: toolName === "read" ? "read" : toolName === "write" || toolName === "edit" ? "edit" : "other",
1889
+ status: "completed",
1890
+ rawInput: null,
1891
+ rawOutput: m
1892
+ }
1893
+ });
1894
+ const text = toolResultToText(m);
1895
+ await this.conn.sessionUpdate({
1896
+ sessionId: session.sessionId,
1897
+ update: {
1898
+ sessionUpdate: "tool_call_update",
1899
+ toolCallId,
1900
+ status: isError ? "failed" : "completed",
1901
+ content: text ? [{ type: "content", content: { type: "text", text } }] : null,
1902
+ rawOutput: m
1903
+ }
1904
+ });
1905
+ }
1363
1906
  }
1364
1907
  const models = await getModelState(proc);
1365
1908
  const thinking = await getThinkingState(proc);
1366
- const showStartupInfo = booleanEnv("PI_ACP_STARTUP_INFO", true);
1367
- const preludeText = showStartupInfo ? buildStartupInfo({ cwd: params.cwd, fileCommands }) : "";
1368
- if (preludeText) session.setStartupInfo(preludeText);
1369
1909
  const response = {
1370
1910
  models,
1371
1911
  modes: thinking,
1372
1912
  _meta: {
1373
1913
  piAcp: {
1374
- startupInfo: preludeText || null
1914
+ startupInfo: null
1375
1915
  }
1376
1916
  }
1377
1917
  };
1378
- if (showStartupInfo) setTimeout(() => session.sendStartupInfoIfPending(), 0);
1379
1918
  setTimeout(() => {
1380
1919
  void this.conn.sessionUpdate({
1381
1920
  sessionId: session.sessionId,
@@ -1408,7 +1947,7 @@ ${JSON.stringify(stats, null, 2)}`;
1408
1947
  }
1409
1948
  }
1410
1949
  if (!provider || !modelId) {
1411
- throw RequestError2.invalidParams(`Unknown modelId: ${params.modelId}`);
1950
+ throw RequestError3.invalidParams(`Unknown modelId: ${params.modelId}`);
1412
1951
  }
1413
1952
  await session.proc.setModel(provider, modelId);
1414
1953
  }
@@ -1416,7 +1955,7 @@ ${JSON.stringify(stats, null, 2)}`;
1416
1955
  const session = this.sessions.get(params.sessionId);
1417
1956
  const mode = String(params.modeId);
1418
1957
  if (!isThinkingLevel(mode)) {
1419
- throw RequestError2.invalidParams(`Unknown modeId: ${mode}`);
1958
+ throw RequestError3.invalidParams(`Unknown modeId: ${mode}`);
1420
1959
  }
1421
1960
  await session.proc.setThinkingLevel(mode);
1422
1961
  void this.conn.sessionUpdate({
@@ -1533,16 +2072,16 @@ function buildStartupInfo(opts) {
1533
2072
  md.push("");
1534
2073
  };
1535
2074
  const contextItems = [];
1536
- const contextPath = join3(opts.cwd, "AGENTS.md");
1537
- if (existsSync2(contextPath)) contextItems.push(contextPath);
2075
+ const contextPath = join5(opts.cwd, "AGENTS.md");
2076
+ if (existsSync3(contextPath)) contextItems.push(contextPath);
1538
2077
  addSection("Context", contextItems);
1539
2078
  const skillsItems = [];
1540
2079
  const pushSkillFromRoot = (root) => {
1541
2080
  try {
1542
- for (const e of readdirSync2(root)) {
1543
- const p = join3(root, e);
2081
+ for (const e of readdirSync3(root)) {
2082
+ const p = join5(root, e);
1544
2083
  try {
1545
- const st = statSync(p);
2084
+ const st = statSync2(p);
1546
2085
  if (st.isFile() && e.toLowerCase().endsWith(".md")) {
1547
2086
  skillsItems.push(p);
1548
2087
  }
@@ -1554,16 +2093,16 @@ function buildStartupInfo(opts) {
1554
2093
  const dir = stack.pop();
1555
2094
  let entries = [];
1556
2095
  try {
1557
- entries = readdirSync2(dir);
2096
+ entries = readdirSync3(dir);
1558
2097
  } catch {
1559
2098
  continue;
1560
2099
  }
1561
2100
  for (const name of entries) {
1562
2101
  if (name === "node_modules" || name === ".git") continue;
1563
- const p = join3(dir, name);
2102
+ const p = join5(dir, name);
1564
2103
  let st;
1565
2104
  try {
1566
- st = statSync(p);
2105
+ st = statSync2(p);
1567
2106
  } catch {
1568
2107
  continue;
1569
2108
  }
@@ -1577,29 +2116,29 @@ function buildStartupInfo(opts) {
1577
2116
  } catch {
1578
2117
  }
1579
2118
  };
1580
- const globalSkillsDir = join3(process.env.HOME ?? "", ".pi", "agent", "skills");
2119
+ const globalSkillsDir = join5(process.env.HOME ?? "", ".pi", "agent", "skills");
1581
2120
  pushSkillFromRoot(globalSkillsDir);
1582
- const projectSkillsDir = join3(opts.cwd, ".pi", "skills");
2121
+ const projectSkillsDir = join5(opts.cwd, ".pi", "skills");
1583
2122
  pushSkillFromRoot(projectSkillsDir);
1584
2123
  addSection("Skills", skillsItems);
1585
2124
  const promptsItems = [];
1586
- const promptsDir = join3(process.env.HOME ?? "", ".pi", "agent", "prompts");
2125
+ const promptsDir = join5(process.env.HOME ?? "", ".pi", "agent", "prompts");
1587
2126
  try {
1588
- const prompts = readdirSync2(promptsDir).filter((f) => f.endsWith(".md"));
2127
+ const prompts = readdirSync3(promptsDir).filter((f) => f.endsWith(".md"));
1589
2128
  for (const f of prompts) promptsItems.push(`/${basename(f, ".md")}`);
1590
2129
  } catch {
1591
2130
  }
1592
2131
  addSection("Prompts", promptsItems);
1593
2132
  const extItems = [];
1594
- const extDir = join3(process.env.HOME ?? "", ".pi", "agent", "extensions");
2133
+ const extDir = join5(process.env.HOME ?? "", ".pi", "agent", "extensions");
1595
2134
  try {
1596
- const exts = readdirSync2(extDir).filter((f) => f.endsWith(".ts") || f.endsWith(".js"));
1597
- for (const f of exts) extItems.push(join3(extDir, f));
2135
+ const exts = readdirSync3(extDir).filter((f) => f.endsWith(".ts") || f.endsWith(".js"));
2136
+ for (const f of exts) extItems.push(join5(extDir, f));
1598
2137
  } catch {
1599
2138
  }
1600
2139
  try {
1601
- const settingsPath = join3(process.env.HOME ?? "", ".pi", "agent", "settings.json");
1602
- const settings = JSON.parse(readFileSync4(settingsPath, "utf-8"));
2140
+ const settingsPath = join5(process.env.HOME ?? "", ".pi", "agent", "settings.json");
2141
+ const settings = JSON.parse(readFileSync6(settingsPath, "utf-8"));
1603
2142
  const pkgs = Array.isArray(settings?.packages) ? settings.packages : [];
1604
2143
  for (const pkg2 of pkgs) {
1605
2144
  const s = String(pkg2);
@@ -1624,9 +2163,9 @@ function readNearestPackageJson(metaUrl) {
1624
2163
  try {
1625
2164
  let dir = dirname2(fileURLToPath(metaUrl));
1626
2165
  for (let i = 0; i < 6; i++) {
1627
- const p = join3(dir, "package.json");
1628
- if (existsSync2(p)) {
1629
- const json = JSON.parse(readFileSync4(p, "utf-8"));
2166
+ const p = join5(dir, "package.json");
2167
+ if (existsSync3(p)) {
2168
+ const json = JSON.parse(readFileSync6(p, "utf-8"));
1630
2169
  return { name: json?.name, version: json?.version };
1631
2170
  }
1632
2171
  dir = dirname2(dir);
@@ -1637,13 +2176,31 @@ function readNearestPackageJson(metaUrl) {
1637
2176
  }
1638
2177
 
1639
2178
  // src/index.ts
2179
+ if (process.argv.includes("--terminal-login")) {
2180
+ const { spawnSync: spawnSync2 } = await import("child_process");
2181
+ const cmd = process.env.PI_ACP_PI_COMMAND ?? "pi";
2182
+ const res = spawnSync2(cmd, [], { stdio: "inherit", env: process.env });
2183
+ if (res.error && res.error.code === "ENOENT") {
2184
+ process.stderr.write(
2185
+ `pi-acp: could not start pi (command not found: ${cmd}). Install it via \`npm install -g @mariozechner/pi-coding-agent\` or ensure \`pi\` is on your PATH.
2186
+ `
2187
+ );
2188
+ process.exit(1);
2189
+ }
2190
+ process.exit(typeof res.status === "number" ? res.status : 1);
2191
+ }
1640
2192
  var input = new WritableStream({
1641
2193
  write(chunk) {
1642
- return new Promise((resolve2, reject) => {
1643
- process.stdout.write(chunk, (err) => {
1644
- if (err) reject(err);
1645
- else resolve2();
1646
- });
2194
+ return new Promise((resolve2) => {
2195
+ if (process.stdout.destroyed || !process.stdout.writable) return resolve2();
2196
+ try {
2197
+ process.stdout.write(chunk, (err) => {
2198
+ void err;
2199
+ resolve2();
2200
+ });
2201
+ } catch {
2202
+ resolve2();
2203
+ }
1647
2204
  });
1648
2205
  }
1649
2206
  });
@@ -1659,4 +2216,10 @@ new AgentSideConnection((conn) => new PiAcpAgent(conn), stream);
1659
2216
  process.stdin.resume();
1660
2217
  process.on("SIGINT", () => process.exit(0));
1661
2218
  process.on("SIGTERM", () => process.exit(0));
2219
+ process.stdout.on("error", () => {
2220
+ try {
2221
+ process.exit(0);
2222
+ } catch {
2223
+ }
2224
+ });
1662
2225
  //# sourceMappingURL=index.js.map