pi-acp 0.0.17 → 0.0.18

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,21 +5,109 @@ 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
+ };
97
+ var ESC = String.fromCharCode(27);
98
+ var CSI = String.fromCharCode(155);
99
+ var ANSI_ESCAPE_REGEX = new RegExp(
100
+ `[${ESC}${CSI}][[\\]()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]`,
101
+ "g"
102
+ );
103
+ function stripAnsi(s) {
104
+ return s.replace(ANSI_ESCAPE_REGEX, "");
105
+ }
19
106
  var PiRpcProcess = class _PiRpcProcess {
20
107
  child;
21
108
  pending = /* @__PURE__ */ new Map();
22
109
  eventHandlers = [];
110
+ preludeLines = [];
23
111
  constructor(child) {
24
112
  this.child = child;
25
113
  const rl = readline.createInterface({ input: child.stdout });
@@ -29,6 +117,8 @@ var PiRpcProcess = class _PiRpcProcess {
29
117
  try {
30
118
  msg = JSON.parse(line);
31
119
  } catch {
120
+ const cleaned = stripAnsi(String(line)).trimEnd();
121
+ if (cleaned) this.preludeLines.push(cleaned);
32
122
  return;
33
123
  }
34
124
  if (msg?.type === "response") {
@@ -49,6 +139,10 @@ var PiRpcProcess = class _PiRpcProcess {
49
139
  for (const [, p] of this.pending) p.reject(err);
50
140
  this.pending.clear();
51
141
  });
142
+ child.on("error", (err) => {
143
+ for (const [, p] of this.pending) p.reject(err);
144
+ this.pending.clear();
145
+ });
52
146
  }
53
147
  static async spawn(params) {
54
148
  const cmd = params.piCommand ?? "pi";
@@ -59,6 +153,36 @@ var PiRpcProcess = class _PiRpcProcess {
59
153
  stdio: "pipe",
60
154
  env: process.env
61
155
  });
156
+ try {
157
+ await new Promise((resolve3, reject) => {
158
+ const onSpawn = () => {
159
+ cleanup();
160
+ resolve3();
161
+ };
162
+ const onError = (err) => {
163
+ cleanup();
164
+ reject(err);
165
+ };
166
+ const cleanup = () => {
167
+ child.off("spawn", onSpawn);
168
+ child.off("error", onError);
169
+ };
170
+ child.once("spawn", onSpawn);
171
+ child.once("error", onError);
172
+ });
173
+ } catch (e) {
174
+ const code = typeof e?.code === "string" ? e.code : void 0;
175
+ if (code === "ENOENT") {
176
+ throw new PiRpcSpawnError(
177
+ `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.`,
178
+ { code, cause: e }
179
+ );
180
+ }
181
+ if (code === "EACCES") {
182
+ throw new PiRpcSpawnError(`Could not start pi: permission denied (command: ${cmd}).`, { code, cause: e });
183
+ }
184
+ throw new PiRpcSpawnError(`Could not start pi (command: ${cmd}).`, { code, cause: e });
185
+ }
62
186
  child.stderr.on("data", () => {
63
187
  });
64
188
  const proc = new _PiRpcProcess(child);
@@ -80,6 +204,21 @@ var PiRpcProcess = class _PiRpcProcess {
80
204
  this.eventHandlers = this.eventHandlers.filter((h) => h !== handler);
81
205
  };
82
206
  }
207
+ dispose(signal = "SIGTERM") {
208
+ if (this.child.killed) return;
209
+ try {
210
+ this.child.kill(signal);
211
+ } catch {
212
+ }
213
+ }
214
+ /**
215
+ * Human-readable stdout lines emitted before RPC NDJSON begins (e.g. Context/Skills/Extensions info).
216
+ * Themes are typically noisy/less useful for ACP, so callers can filter as needed.
217
+ */
218
+ consumePreludeLines() {
219
+ const lines = this.preludeLines.splice(0, this.preludeLines.length);
220
+ return lines;
221
+ }
83
222
  async prompt(message, attachments = []) {
84
223
  const res = await this.request({ type: "prompt", message, attachments });
85
224
  if (!res.success) throw new Error(`pi prompt failed: ${res.error ?? JSON.stringify(res.data)}`);
@@ -144,18 +283,28 @@ var PiRpcProcess = class _PiRpcProcess {
144
283
  if (!res.success) throw new Error(`pi get_messages failed: ${res.error ?? JSON.stringify(res.data)}`);
145
284
  return res.data;
146
285
  }
286
+ async getCommands() {
287
+ const res = await this.request({ type: "get_commands" });
288
+ if (!res.success) throw new Error(`pi get_commands failed: ${res.error ?? JSON.stringify(res.data)}`);
289
+ return res.data;
290
+ }
147
291
  request(cmd) {
148
292
  const id = crypto.randomUUID();
149
293
  const withId = { ...cmd, id };
150
294
  const line = JSON.stringify(withId) + "\n";
151
- return new Promise((resolve2, reject) => {
152
- this.pending.set(id, { resolve: resolve2, reject });
153
- this.child.stdin.write(line, (err) => {
154
- if (err) {
155
- this.pending.delete(id);
156
- reject(err);
157
- }
158
- });
295
+ return new Promise((resolve3, reject) => {
296
+ this.pending.set(id, { resolve: resolve3, reject });
297
+ try {
298
+ this.child.stdin.write(line, (err) => {
299
+ if (err) {
300
+ this.pending.delete(id);
301
+ reject(err);
302
+ }
303
+ });
304
+ } catch (e) {
305
+ this.pending.delete(id);
306
+ reject(e);
307
+ }
159
308
  });
160
309
  }
161
310
  };
@@ -379,10 +528,24 @@ var SessionManager = class {
379
528
  return this.sessions.get(sessionId);
380
529
  }
381
530
  async create(params) {
382
- const proc = await PiRpcProcess.spawn({
383
- cwd: params.cwd
384
- });
385
- const state = await proc.getState();
531
+ let proc;
532
+ try {
533
+ proc = await PiRpcProcess.spawn({
534
+ cwd: params.cwd,
535
+ piCommand: params.piCommand
536
+ });
537
+ } catch (e) {
538
+ if (e instanceof PiRpcSpawnError) {
539
+ throw RequestError2.internalError({ code: e.code }, e.message);
540
+ }
541
+ throw e;
542
+ }
543
+ let state = null;
544
+ try {
545
+ state = await proc.getState();
546
+ } catch {
547
+ state = null;
548
+ }
386
549
  const sessionId = typeof state?.sessionId === "string" ? state.sessionId : crypto.randomUUID();
387
550
  const sessionFile = typeof state?.sessionFile === "string" ? state.sessionFile : null;
388
551
  if (sessionFile) {
@@ -401,7 +564,7 @@ var SessionManager = class {
401
564
  }
402
565
  get(sessionId) {
403
566
  const s = this.sessions.get(sessionId);
404
- if (!s) throw RequestError.invalidParams(`Unknown sessionId: ${sessionId}`);
567
+ if (!s) throw RequestError2.invalidParams(`Unknown sessionId: ${sessionId}`);
405
568
  return s;
406
569
  }
407
570
  /**
@@ -427,6 +590,8 @@ var PiAcpSession = class {
427
590
  sessionId;
428
591
  cwd;
429
592
  mcpServers;
593
+ startupInfo = null;
594
+ startupInfoSent = false;
430
595
  proc;
431
596
  conn;
432
597
  fileCommands;
@@ -459,10 +624,33 @@ var PiAcpSession = class {
459
624
  this.fileCommands = opts.fileCommands ?? [];
460
625
  this.proc.onEvent((ev) => this.handlePiEvent(ev));
461
626
  }
627
+ setStartupInfo(text) {
628
+ this.startupInfo = text;
629
+ }
630
+ /**
631
+ * Best-effort attempt to send startup info outside of a prompt turn.
632
+ * Some clients (e.g. Zed) may only render agent messages once the UI is ready;
633
+ * callers can invoke this shortly after session/new returns.
634
+ */
635
+ sendStartupInfoIfPending() {
636
+ if (this.startupInfoSent || !this.startupInfo) return;
637
+ this.startupInfoSent = true;
638
+ this.emit({
639
+ sessionUpdate: "agent_message_chunk",
640
+ content: { type: "text", text: this.startupInfo }
641
+ });
642
+ }
462
643
  async prompt(message, attachments = []) {
644
+ if (!this.startupInfoSent && this.startupInfo) {
645
+ this.startupInfoSent = true;
646
+ this.emit({
647
+ sessionUpdate: "agent_message_chunk",
648
+ content: { type: "text", text: this.startupInfo }
649
+ });
650
+ }
463
651
  const expandedMessage = expandSlashCommand(message, this.fileCommands);
464
- const turnPromise = new Promise((resolve2, reject) => {
465
- const queued = { message: expandedMessage, attachments, resolve: resolve2, reject };
652
+ const turnPromise = new Promise((resolve3, reject) => {
653
+ const queued = { message: expandedMessage, attachments, resolve: resolve3, reject };
466
654
  if (this.pendingTurn) {
467
655
  this.turnQueue.push(queued);
468
656
  this.emit({
@@ -523,8 +711,13 @@ var PiAcpSession = class {
523
711
  });
524
712
  this.proc.prompt(t.message, t.attachments).catch((err) => {
525
713
  void this.flushEmits().finally(() => {
526
- const reason = this.cancelRequested ? "cancelled" : "error";
527
- this.pendingTurn?.resolve(reason);
714
+ const authErr = maybeAuthRequiredError(err);
715
+ if (authErr) {
716
+ this.pendingTurn?.reject(authErr);
717
+ } else {
718
+ const reason = this.cancelRequested ? "cancelled" : "error";
719
+ this.pendingTurn?.resolve(reason);
720
+ }
528
721
  this.pendingTurn = null;
529
722
  this.inAgentLoop = false;
530
723
  this.emit({
@@ -728,6 +921,245 @@ function toToolKind(toolName) {
728
921
  }
729
922
  }
730
923
 
924
+ // src/acp/pi-sessions.ts
925
+ import { readdirSync as readdirSync2, readFileSync as readFileSync4, statSync, openSync, readSync, closeSync } from "fs";
926
+ import { homedir as homedir3 } from "os";
927
+ import { join as join3 } from "path";
928
+ var DEFAULT_TAIL_BYTES = 256 * 1024;
929
+ var DEFAULT_HEAD_BYTES = 64 * 1024;
930
+ function getPiAgentDir() {
931
+ return process.env.PI_CODING_AGENT_DIR ?? join3(homedir3(), ".pi", "agent");
932
+ }
933
+ function getPiSessionsDir() {
934
+ return join3(getPiAgentDir(), "sessions");
935
+ }
936
+ function walkJsonlFiles(dir, out) {
937
+ let entries;
938
+ try {
939
+ entries = readdirSync2(dir, { withFileTypes: true, encoding: "utf8" });
940
+ } catch {
941
+ return;
942
+ }
943
+ for (const e of entries) {
944
+ const name = typeof e.name === "string" ? e.name : String(e.name);
945
+ const p = join3(dir, name);
946
+ if (e.isDirectory()) walkJsonlFiles(p, out);
947
+ else if (e.isFile() && name.endsWith(".jsonl")) out.push(p);
948
+ }
949
+ }
950
+ function readFirstLine(path) {
951
+ const fd = openSync(path, "r");
952
+ try {
953
+ const buf = Buffer.alloc(DEFAULT_HEAD_BYTES);
954
+ const n = readSync(fd, buf, 0, buf.length, 0);
955
+ if (n <= 0) return null;
956
+ const s = buf.subarray(0, n).toString("utf-8");
957
+ const idx = s.indexOf("\n");
958
+ return idx === -1 ? s.trim() : s.slice(0, idx).trim();
959
+ } catch {
960
+ return null;
961
+ } finally {
962
+ try {
963
+ closeSync(fd);
964
+ } catch {
965
+ }
966
+ }
967
+ }
968
+ function readTail(path, tailBytes = DEFAULT_TAIL_BYTES) {
969
+ const st = statSync(path);
970
+ const start = Math.max(0, st.size - tailBytes);
971
+ const len = st.size - start;
972
+ const fd = openSync(path, "r");
973
+ try {
974
+ const buf = Buffer.alloc(len);
975
+ const n = readSync(fd, buf, 0, buf.length, start);
976
+ return buf.subarray(0, n).toString("utf-8");
977
+ } finally {
978
+ try {
979
+ closeSync(fd);
980
+ } catch {
981
+ }
982
+ }
983
+ }
984
+ function parseSessionHeader(firstLine) {
985
+ try {
986
+ const obj = JSON.parse(firstLine);
987
+ if (obj?.type !== "session") return null;
988
+ const sessionId = typeof obj?.id === "string" ? obj.id : null;
989
+ const cwd = typeof obj?.cwd === "string" ? obj.cwd : null;
990
+ if (!sessionId || !cwd) return null;
991
+ return { sessionId, cwd };
992
+ } catch {
993
+ return null;
994
+ }
995
+ }
996
+ function pickTitleFromTail(tail) {
997
+ const lines = tail.split(/\r?\n/);
998
+ for (let i = lines.length - 1; i >= 0; i--) {
999
+ const line = lines[i].trim();
1000
+ if (!line) continue;
1001
+ try {
1002
+ const obj = JSON.parse(line);
1003
+ if (obj?.type === "session_info" && typeof obj?.name === "string" && obj.name.trim()) {
1004
+ return obj.name.trim();
1005
+ }
1006
+ } catch {
1007
+ }
1008
+ }
1009
+ return null;
1010
+ }
1011
+ function scanSessionInfoNameFromFile(path) {
1012
+ const fd = openSync(path, "r");
1013
+ try {
1014
+ const buf = Buffer.alloc(256 * 1024);
1015
+ let leftover = "";
1016
+ let offset = 0;
1017
+ let lastName = null;
1018
+ while (true) {
1019
+ const n = readSync(fd, buf, 0, buf.length, offset);
1020
+ if (n <= 0) break;
1021
+ offset += n;
1022
+ const chunk = leftover + buf.subarray(0, n).toString("utf8");
1023
+ const lines = chunk.split(/\r?\n/);
1024
+ leftover = lines.pop() ?? "";
1025
+ for (const line0 of lines) {
1026
+ const line = line0.trim();
1027
+ if (!line) continue;
1028
+ try {
1029
+ const obj = JSON.parse(line);
1030
+ if (obj?.type === "session_info" && typeof obj?.name === "string" && obj.name.trim()) {
1031
+ lastName = obj.name.trim();
1032
+ }
1033
+ } catch {
1034
+ }
1035
+ }
1036
+ }
1037
+ const tailLine = leftover.trim();
1038
+ if (tailLine) {
1039
+ try {
1040
+ const obj = JSON.parse(tailLine);
1041
+ if (obj?.type === "session_info" && typeof obj?.name === "string" && obj.name.trim()) {
1042
+ lastName = obj.name.trim();
1043
+ }
1044
+ } catch {
1045
+ }
1046
+ }
1047
+ return lastName;
1048
+ } catch {
1049
+ return null;
1050
+ } finally {
1051
+ try {
1052
+ closeSync(fd);
1053
+ } catch {
1054
+ }
1055
+ }
1056
+ }
1057
+ function pickUpdatedAtFromTail(tail) {
1058
+ const lines = tail.split(/\r?\n/);
1059
+ for (let i = lines.length - 1; i >= 0; i--) {
1060
+ const line = lines[i].trim();
1061
+ if (!line) continue;
1062
+ try {
1063
+ const obj = JSON.parse(line);
1064
+ if (obj?.type !== "message") continue;
1065
+ const ts = typeof obj?.timestamp === "string" ? obj.timestamp : null;
1066
+ if (!ts) continue;
1067
+ const d = new Date(ts);
1068
+ if (Number.isFinite(d.getTime())) return d.toISOString();
1069
+ } catch {
1070
+ }
1071
+ }
1072
+ for (let i = lines.length - 1; i >= 0; i--) {
1073
+ const line = lines[i].trim();
1074
+ if (!line) continue;
1075
+ try {
1076
+ const obj = JSON.parse(line);
1077
+ const ts = typeof obj?.timestamp === "string" ? obj.timestamp : null;
1078
+ if (!ts) continue;
1079
+ const d = new Date(ts);
1080
+ if (Number.isFinite(d.getTime())) return d.toISOString();
1081
+ } catch {
1082
+ }
1083
+ }
1084
+ return null;
1085
+ }
1086
+ function pickFallbackTitleFromHead(path) {
1087
+ try {
1088
+ const raw = readFileSync4(path, { encoding: "utf8" });
1089
+ const lines = raw.split(/\r?\n/);
1090
+ for (const line0 of lines) {
1091
+ const line = line0.trim();
1092
+ if (!line) continue;
1093
+ try {
1094
+ const obj = JSON.parse(line);
1095
+ if (obj?.type === "message" && obj?.message?.role === "user") {
1096
+ const content = obj?.message?.content;
1097
+ if (typeof content === "string") return content.slice(0, 80);
1098
+ if (Array.isArray(content)) {
1099
+ const t = content.find((c) => c?.type === "text" && typeof c?.text === "string");
1100
+ if (t?.text) return String(t.text).slice(0, 80);
1101
+ }
1102
+ }
1103
+ } catch {
1104
+ }
1105
+ if (lines.length > 2e3) break;
1106
+ }
1107
+ } catch {
1108
+ }
1109
+ return null;
1110
+ }
1111
+ function listPiSessions() {
1112
+ const sessionsDir = getPiSessionsDir();
1113
+ const files = [];
1114
+ walkJsonlFiles(sessionsDir, files);
1115
+ const items = [];
1116
+ for (const file of files) {
1117
+ const first = readFirstLine(file);
1118
+ if (!first) continue;
1119
+ const header = parseSessionHeader(first);
1120
+ if (!header) continue;
1121
+ let updatedAt = null;
1122
+ let title = null;
1123
+ try {
1124
+ const tail = readTail(file);
1125
+ title = pickTitleFromTail(tail);
1126
+ updatedAt = pickUpdatedAtFromTail(tail);
1127
+ } catch {
1128
+ }
1129
+ if (!title) {
1130
+ title = scanSessionInfoNameFromFile(file);
1131
+ }
1132
+ if (!updatedAt) {
1133
+ try {
1134
+ updatedAt = statSync(file).mtime.toISOString();
1135
+ } catch {
1136
+ updatedAt = null;
1137
+ }
1138
+ }
1139
+ if (!title) {
1140
+ title = pickFallbackTitleFromHead(file);
1141
+ }
1142
+ items.push({
1143
+ sessionId: header.sessionId,
1144
+ cwd: header.cwd,
1145
+ title,
1146
+ updatedAt,
1147
+ sessionFile: file
1148
+ });
1149
+ }
1150
+ items.sort((a, b) => {
1151
+ const aa = a.updatedAt ?? "";
1152
+ const bb = b.updatedAt ?? "";
1153
+ return bb.localeCompare(aa);
1154
+ });
1155
+ return items;
1156
+ }
1157
+ function findPiSessionFile(sessionId) {
1158
+ const all = listPiSessions();
1159
+ const found = all.find((s) => s.sessionId === sessionId);
1160
+ return found?.sessionFile ?? null;
1161
+ }
1162
+
731
1163
  // src/acp/translate/pi-messages.ts
732
1164
  function normalizePiMessageText(content) {
733
1165
  if (typeof content === "string") return content;
@@ -801,12 +1233,163 @@ ${r.text}`;
801
1233
  return { message, attachments };
802
1234
  }
803
1235
 
1236
+ // src/acp/pi-settings.ts
1237
+ import { existsSync as existsSync2, readFileSync as readFileSync5 } from "fs";
1238
+ import { homedir as homedir4 } from "os";
1239
+ import { join as join4, resolve as resolve2 } from "path";
1240
+ function isObject(x) {
1241
+ return Boolean(x) && typeof x === "object" && !Array.isArray(x);
1242
+ }
1243
+ function deepMerge(a, b) {
1244
+ const out = { ...a };
1245
+ for (const [k, v] of Object.entries(b)) {
1246
+ const av = out[k];
1247
+ if (isObject(av) && isObject(v)) out[k] = deepMerge(av, v);
1248
+ else out[k] = v;
1249
+ }
1250
+ return out;
1251
+ }
1252
+ function readJsonFile(path) {
1253
+ try {
1254
+ if (!existsSync2(path)) return {};
1255
+ const raw = readFileSync5(path, "utf-8");
1256
+ const data = JSON.parse(raw);
1257
+ return isObject(data) ? data : {};
1258
+ } catch {
1259
+ return {};
1260
+ }
1261
+ }
1262
+ function getAgentDir() {
1263
+ return process.env.PI_CODING_AGENT_DIR ? resolve2(process.env.PI_CODING_AGENT_DIR) : join4(homedir4(), ".pi", "agent");
1264
+ }
1265
+ function getEnableSkillCommands(cwd) {
1266
+ const globalSettingsPath = join4(getAgentDir(), "settings.json");
1267
+ const projectSettingsPath = resolve2(cwd, ".pi", "settings.json");
1268
+ const global = readJsonFile(globalSettingsPath);
1269
+ const project = readJsonFile(projectSettingsPath);
1270
+ const merged = deepMerge(global, project);
1271
+ const direct = merged.enableSkillCommands;
1272
+ if (typeof direct === "boolean") return direct;
1273
+ const nested = isObject(merged.skills) ? merged.skills.enableSkillCommands : void 0;
1274
+ if (typeof nested === "boolean") return nested;
1275
+ return true;
1276
+ }
1277
+
1278
+ // src/acp/pi-commands.ts
1279
+ function describeFallback(c) {
1280
+ const source = typeof c.source === "string" ? c.source : "";
1281
+ const location = typeof c.location === "string" ? c.location : "";
1282
+ const parts = [];
1283
+ if (source) parts.push(source);
1284
+ if (location) parts.push(location);
1285
+ return parts.length ? `(${parts.join(":")})` : "(command)";
1286
+ }
1287
+ function toAvailableCommandsFromPiGetCommands(data, opts) {
1288
+ const enableSkillCommands = opts?.enableSkillCommands ?? true;
1289
+ const includeExtensionCommands = opts?.includeExtensionCommands ?? false;
1290
+ const root = data;
1291
+ const commandsRaw = Array.isArray(root?.commands) ? root.commands : Array.isArray(root?.data?.commands) ? root.data.commands : [];
1292
+ const out = [];
1293
+ for (const c of commandsRaw) {
1294
+ const name = typeof c?.name === "string" ? c.name.trim() : "";
1295
+ if (!name) continue;
1296
+ const source = typeof c?.source === "string" ? c.source : "";
1297
+ if (!includeExtensionCommands && source === "extension") continue;
1298
+ if (!enableSkillCommands && name.startsWith("skill:")) continue;
1299
+ const desc = typeof c?.description === "string" ? c.description.trim() : "";
1300
+ out.push({
1301
+ name,
1302
+ description: desc || describeFallback(c)
1303
+ });
1304
+ }
1305
+ return { commands: out, raw: commandsRaw };
1306
+ }
1307
+
804
1308
  // src/acp/agent.ts
805
1309
  import { isAbsolute as isAbsolute2 } from "path";
806
- import { existsSync as existsSync2, readFileSync as readFileSync4, realpathSync } from "fs";
807
- import { join as join3, dirname as dirname2 } from "path";
1310
+ import { existsSync as existsSync4, readFileSync as readFileSync7, realpathSync, readdirSync as readdirSync3, statSync as statSync2 } from "fs";
1311
+ import { join as join6, dirname as dirname2, basename } from "path";
808
1312
  import { spawnSync } from "child_process";
1313
+
1314
+ // src/pi-auth/status.ts
1315
+ import { existsSync as existsSync3, readFileSync as readFileSync6 } from "fs";
1316
+ import { homedir as homedir5 } from "os";
1317
+ import { join as join5 } from "path";
1318
+ function safeReadJson(path) {
1319
+ try {
1320
+ if (!existsSync3(path)) return null;
1321
+ const raw = readFileSync6(path, "utf-8");
1322
+ if (!raw.trim()) return null;
1323
+ return JSON.parse(raw);
1324
+ } catch {
1325
+ return null;
1326
+ }
1327
+ }
1328
+ function getPiAgentDir2() {
1329
+ const envDir = process.env.PI_CODING_AGENT_DIR;
1330
+ if (envDir) {
1331
+ if (envDir === "~") return homedir5();
1332
+ if (envDir.startsWith("~/")) return homedir5() + envDir.slice(1);
1333
+ return envDir;
1334
+ }
1335
+ return join5(homedir5(), ".pi", "agent");
1336
+ }
1337
+ function hasAnyPiAuthConfigured() {
1338
+ const agentDir = getPiAgentDir2();
1339
+ const authPath = join5(agentDir, "auth.json");
1340
+ const auth = safeReadJson(authPath);
1341
+ if (auth && typeof auth === "object" && Object.keys(auth).length > 0) return true;
1342
+ const modelsPath = join5(agentDir, "models.json");
1343
+ const models = safeReadJson(modelsPath);
1344
+ const providers = models?.providers;
1345
+ if (providers && typeof providers === "object") {
1346
+ for (const p of Object.values(providers)) {
1347
+ if (p && typeof p === "object" && typeof p.apiKey === "string" && p.apiKey.trim()) {
1348
+ return true;
1349
+ }
1350
+ }
1351
+ }
1352
+ const envVars = [
1353
+ "OPENAI_API_KEY",
1354
+ "AZURE_OPENAI_API_KEY",
1355
+ "GEMINI_API_KEY",
1356
+ "GROQ_API_KEY",
1357
+ "CEREBRAS_API_KEY",
1358
+ "XAI_API_KEY",
1359
+ "OPENROUTER_API_KEY",
1360
+ "AI_GATEWAY_API_KEY",
1361
+ "ZAI_API_KEY",
1362
+ "MISTRAL_API_KEY",
1363
+ "MINIMAX_API_KEY",
1364
+ "MINIMAX_CN_API_KEY",
1365
+ "HF_TOKEN",
1366
+ "OPENCODE_API_KEY",
1367
+ "KIMI_API_KEY",
1368
+ // Copilot/github
1369
+ "COPILOT_GITHUB_TOKEN",
1370
+ "GH_TOKEN",
1371
+ "GITHUB_TOKEN",
1372
+ // Anthropic oauth
1373
+ "ANTHROPIC_OAUTH_TOKEN",
1374
+ "ANTHROPIC_API_KEY"
1375
+ ];
1376
+ for (const k of envVars) {
1377
+ const v = process.env[k];
1378
+ if (typeof v === "string" && v.trim()) return true;
1379
+ }
1380
+ return false;
1381
+ }
1382
+
1383
+ // src/acp/agent.ts
809
1384
  import { fileURLToPath } from "url";
1385
+ function booleanEnv(name, defaultValue) {
1386
+ const raw = process.env[name];
1387
+ if (raw == null) return defaultValue;
1388
+ const v = String(raw).trim().toLowerCase();
1389
+ if (v === "true") return true;
1390
+ if (v === "false") return false;
1391
+ return defaultValue;
1392
+ }
810
1393
  function builtinAvailableCommands() {
811
1394
  return [
812
1395
  {
@@ -858,8 +1441,11 @@ var PiAcpAgent = class {
858
1441
  conn;
859
1442
  sessions = new SessionManager();
860
1443
  store = new SessionStore();
861
- constructor(conn) {
1444
+ // Remember recent session cwd and use it as the default filter.
1445
+ lastSessionCwd = null;
1446
+ constructor(conn, _config) {
862
1447
  this.conn = conn;
1448
+ void _config;
863
1449
  }
864
1450
  async initialize(params) {
865
1451
  const supportedVersion = 1;
@@ -871,7 +1457,11 @@ var PiAcpAgent = class {
871
1457
  title: "pi ACP adapter",
872
1458
  version: pkg.version ?? "0.0.0"
873
1459
  },
874
- authMethods: [],
1460
+ // Zed currently uses ClientCapabilities._meta["terminal-auth"] to decide whether to show
1461
+ // the "Authenticate" banner/button. If not supported, we still return the method for the registry.
1462
+ authMethods: getAuthMethods({
1463
+ supportsTerminalAuthMeta: params?.clientCapabilities?._meta?.["terminal-auth"] === true
1464
+ }),
875
1465
  agentCapabilities: {
876
1466
  loadSession: true,
877
1467
  mcpCapabilities: { http: false, sse: false },
@@ -880,37 +1470,92 @@ var PiAcpAgent = class {
880
1470
  audio: false,
881
1471
  embeddedContext: false
882
1472
  },
883
- sessionCapabilities: {}
1473
+ sessionCapabilities: {
1474
+ // **UNSTABLE** ACP capability used by Zed's codex-acp adapter.
1475
+ // Enables a native session picker in clients that support it.
1476
+ list: {}
1477
+ }
884
1478
  }
885
1479
  };
886
1480
  }
887
1481
  async newSession(params) {
888
1482
  if (!isAbsolute2(params.cwd)) {
889
- throw RequestError2.invalidParams(`cwd must be an absolute path: ${params.cwd}`);
1483
+ throw RequestError3.invalidParams(`cwd must be an absolute path: ${params.cwd}`);
1484
+ }
1485
+ this.lastSessionCwd = params.cwd;
1486
+ if (!hasAnyPiAuthConfigured()) {
1487
+ throw RequestError3.authRequired(
1488
+ { authMethods: getAuthMethods() },
1489
+ "Configure an API key or log in with an OAuth provider."
1490
+ );
890
1491
  }
891
1492
  const fileCommands = loadSlashCommands(params.cwd);
1493
+ const enableSkillCommands = getEnableSkillCommands(params.cwd);
892
1494
  const session = await this.sessions.create({
893
1495
  cwd: params.cwd,
894
1496
  mcpServers: params.mcpServers,
895
1497
  conn: this.conn,
896
- fileCommands
1498
+ fileCommands,
1499
+ piCommand: process.env.PI_ACP_PI_COMMAND
897
1500
  });
1501
+ let rawModelsCount = 0;
1502
+ try {
1503
+ const data = await session.proc.getAvailableModels();
1504
+ rawModelsCount = Array.isArray(data?.models) ? data.models.length : 0;
1505
+ } catch {
1506
+ }
1507
+ if (rawModelsCount === 0) {
1508
+ try {
1509
+ session.proc.dispose?.();
1510
+ } catch {
1511
+ }
1512
+ throw RequestError3.authRequired(
1513
+ { authMethods: getAuthMethods() },
1514
+ "Configure an API key or log in with an OAuth provider."
1515
+ );
1516
+ }
898
1517
  const models = await getModelState(session.proc);
899
1518
  const thinking = await getThinkingState(session.proc);
1519
+ const showStartupInfo = booleanEnv("PI_ACP_STARTUP_INFO", true);
1520
+ const preludeText = showStartupInfo ? buildStartupInfo({ cwd: params.cwd, fileCommands }) : "";
1521
+ if (preludeText) session.setStartupInfo(preludeText);
900
1522
  const response = {
901
1523
  sessionId: session.sessionId,
902
1524
  models,
903
1525
  modes: thinking,
904
- _meta: {}
1526
+ _meta: {
1527
+ piAcp: {
1528
+ startupInfo: preludeText || null
1529
+ }
1530
+ }
905
1531
  };
1532
+ if (showStartupInfo) setTimeout(() => session.sendStartupInfoIfPending(), 0);
906
1533
  setTimeout(() => {
907
- void this.conn.sessionUpdate({
908
- sessionId: session.sessionId,
909
- update: {
910
- sessionUpdate: "available_commands_update",
911
- availableCommands: mergeCommands(toAvailableCommands(fileCommands), builtinAvailableCommands())
1534
+ void (async () => {
1535
+ try {
1536
+ const pi = await session.proc.getCommands();
1537
+ const { commands } = toAvailableCommandsFromPiGetCommands(pi, {
1538
+ enableSkillCommands,
1539
+ includeExtensionCommands: false
1540
+ });
1541
+ await this.conn.sessionUpdate({
1542
+ sessionId: session.sessionId,
1543
+ update: {
1544
+ sessionUpdate: "available_commands_update",
1545
+ availableCommands: mergeCommands(commands, builtinAvailableCommands())
1546
+ }
1547
+ });
1548
+ return;
1549
+ } catch {
912
1550
  }
913
- });
1551
+ await this.conn.sessionUpdate({
1552
+ sessionId: session.sessionId,
1553
+ update: {
1554
+ sessionUpdate: "available_commands_update",
1555
+ availableCommands: mergeCommands(toAvailableCommands(fileCommands), builtinAvailableCommands())
1556
+ }
1557
+ });
1558
+ })();
914
1559
  }, 0);
915
1560
  return response;
916
1561
  }
@@ -1065,8 +1710,8 @@ ${JSON.stringify(stats, null, 2)}`;
1065
1710
  if (piPath) {
1066
1711
  const resolved = realpathSync(piPath);
1067
1712
  const pkgRoot = dirname2(dirname2(resolved));
1068
- const p = join3(pkgRoot, "CHANGELOG.md");
1069
- if (existsSync2(p)) return p;
1713
+ const p = join6(pkgRoot, "CHANGELOG.md");
1714
+ if (existsSync4(p)) return p;
1070
1715
  }
1071
1716
  } catch {
1072
1717
  }
@@ -1074,8 +1719,8 @@ ${JSON.stringify(stats, null, 2)}`;
1074
1719
  const npmRoot = spawnSync("npm", ["root", "-g"], { encoding: "utf-8" });
1075
1720
  const root = String(npmRoot.stdout ?? "").trim();
1076
1721
  if (root) {
1077
- const p = join3(root, "@mariozechner", "pi-coding-agent", "CHANGELOG.md");
1078
- if (existsSync2(p)) return p;
1722
+ const p = join6(root, "@mariozechner", "pi-coding-agent", "CHANGELOG.md");
1723
+ if (existsSync4(p)) return p;
1079
1724
  }
1080
1725
  } catch {
1081
1726
  }
@@ -1094,7 +1739,7 @@ ${JSON.stringify(stats, null, 2)}`;
1094
1739
  }
1095
1740
  let text = "";
1096
1741
  try {
1097
- text = readFileSync4(changelogPath, "utf-8");
1742
+ text = readFileSync7(changelogPath, "utf-8");
1098
1743
  } catch (e) {
1099
1744
  await this.conn.sessionUpdate({
1100
1745
  sessionId: session.sessionId,
@@ -1120,7 +1765,7 @@ ${JSON.stringify(stats, null, 2)}`;
1120
1765
  const state = await session.proc.getState();
1121
1766
  const sessionFile = typeof state?.sessionFile === "string" ? state.sessionFile : null;
1122
1767
  const messageCount = typeof state?.messageCount === "number" ? state.messageCount : 0;
1123
- if (!sessionFile || messageCount === 0 || !existsSync2(sessionFile)) {
1768
+ if (!sessionFile || messageCount === 0 || !existsSync4(sessionFile)) {
1124
1769
  await this.conn.sessionUpdate({
1125
1770
  sessionId: session.sessionId,
1126
1771
  update: {
@@ -1134,7 +1779,7 @@ ${JSON.stringify(stats, null, 2)}`;
1134
1779
  return { stopReason: "end_turn" };
1135
1780
  }
1136
1781
  try {
1137
- const raw = readFileSync4(sessionFile, "utf-8");
1782
+ const raw = readFileSync7(sessionFile, "utf-8");
1138
1783
  if (raw.trim().length === 0) {
1139
1784
  await this.conn.sessionUpdate({
1140
1785
  sessionId: session.sessionId,
@@ -1162,7 +1807,7 @@ ${JSON.stringify(stats, null, 2)}`;
1162
1807
  return { stopReason: "end_turn" };
1163
1808
  }
1164
1809
  const safeSessionId = session.sessionId.replace(/[^a-zA-Z0-9_-]/g, "_");
1165
- const outputPath = join3(session.cwd, `pi-session-${safeSessionId}.html`);
1810
+ const outputPath = join6(session.cwd, `pi-session-${safeSessionId}.html`);
1166
1811
  let resultPath = "";
1167
1812
  try {
1168
1813
  const result2 = await session.proc.exportHtml(outputPath);
@@ -1251,19 +1896,48 @@ ${JSON.stringify(stats, null, 2)}`;
1251
1896
  const session = this.sessions.get(params.sessionId);
1252
1897
  await session.cancel();
1253
1898
  }
1899
+ async unstable_listSessions(params) {
1900
+ const all = listPiSessions();
1901
+ const effectiveCwd = params.cwd ?? this.lastSessionCwd;
1902
+ const filtered = effectiveCwd ? all.filter((s) => s.cwd === effectiveCwd) : all;
1903
+ const offset = params.cursor ? Number.parseInt(params.cursor, 10) : 0;
1904
+ const start = Number.isFinite(offset) && offset > 0 ? offset : 0;
1905
+ const PAGE_SIZE = 50;
1906
+ const page = filtered.slice(start, start + PAGE_SIZE);
1907
+ const sessions = page.map((s) => ({
1908
+ sessionId: s.sessionId,
1909
+ cwd: s.cwd,
1910
+ title: s.title,
1911
+ updatedAt: s.updatedAt
1912
+ }));
1913
+ const nextCursor = start + PAGE_SIZE < filtered.length ? String(start + PAGE_SIZE) : null;
1914
+ return { sessions, nextCursor, _meta: {} };
1915
+ }
1254
1916
  async loadSession(params) {
1255
1917
  if (!isAbsolute2(params.cwd)) {
1256
- throw RequestError2.invalidParams(`cwd must be an absolute path: ${params.cwd}`);
1918
+ throw RequestError3.invalidParams(`cwd must be an absolute path: ${params.cwd}`);
1257
1919
  }
1920
+ this.lastSessionCwd = params.cwd;
1258
1921
  const stored = this.store.get(params.sessionId);
1259
- if (!stored) {
1260
- throw RequestError2.invalidParams(`Unknown sessionId: ${params.sessionId}`);
1922
+ const sessionFile = stored?.sessionFile ?? findPiSessionFile(params.sessionId);
1923
+ if (!sessionFile) {
1924
+ throw RequestError3.invalidParams(`Unknown sessionId: ${params.sessionId}`);
1925
+ }
1926
+ let proc;
1927
+ try {
1928
+ proc = await PiRpcProcess.spawn({
1929
+ cwd: params.cwd,
1930
+ sessionPath: sessionFile,
1931
+ piCommand: process.env.PI_ACP_PI_COMMAND
1932
+ });
1933
+ } catch (e) {
1934
+ if (e?.name === "PiRpcSpawnError") {
1935
+ throw RequestError3.internalError({ code: e?.code }, String(e?.message ?? e));
1936
+ }
1937
+ throw e;
1261
1938
  }
1262
- const proc = await PiRpcProcess.spawn({
1263
- cwd: params.cwd,
1264
- sessionPath: stored.sessionFile
1265
- });
1266
1939
  const fileCommands = loadSlashCommands(params.cwd);
1940
+ const enableSkillCommands = getEnableSkillCommands(params.cwd);
1267
1941
  const session = this.sessions.getOrCreate(params.sessionId, {
1268
1942
  cwd: params.cwd,
1269
1943
  mcpServers: params.mcpServers,
@@ -1274,7 +1948,7 @@ ${JSON.stringify(stats, null, 2)}`;
1274
1948
  this.store.upsert({
1275
1949
  sessionId: params.sessionId,
1276
1950
  cwd: params.cwd,
1277
- sessionFile: stored.sessionFile
1951
+ sessionFile
1278
1952
  });
1279
1953
  const data = await proc.getMessages();
1280
1954
  const messages = Array.isArray(data?.messages) ? data.messages : [];
@@ -1304,22 +1978,72 @@ ${JSON.stringify(stats, null, 2)}`;
1304
1978
  });
1305
1979
  }
1306
1980
  }
1981
+ if (role === "toolResult") {
1982
+ const toolName = String(m?.toolName ?? "tool");
1983
+ const toolCallId = String(m?.toolCallId ?? crypto.randomUUID());
1984
+ const isError = Boolean(m?.isError);
1985
+ await this.conn.sessionUpdate({
1986
+ sessionId: session.sessionId,
1987
+ update: {
1988
+ sessionUpdate: "tool_call",
1989
+ toolCallId,
1990
+ title: toolName,
1991
+ kind: toolName === "read" ? "read" : toolName === "write" || toolName === "edit" ? "edit" : "other",
1992
+ status: "completed",
1993
+ rawInput: null,
1994
+ rawOutput: m
1995
+ }
1996
+ });
1997
+ const text = toolResultToText(m);
1998
+ await this.conn.sessionUpdate({
1999
+ sessionId: session.sessionId,
2000
+ update: {
2001
+ sessionUpdate: "tool_call_update",
2002
+ toolCallId,
2003
+ status: isError ? "failed" : "completed",
2004
+ content: text ? [{ type: "content", content: { type: "text", text } }] : null,
2005
+ rawOutput: m
2006
+ }
2007
+ });
2008
+ }
1307
2009
  }
1308
2010
  const models = await getModelState(proc);
1309
2011
  const thinking = await getThinkingState(proc);
1310
2012
  const response = {
1311
2013
  models,
1312
2014
  modes: thinking,
1313
- _meta: {}
2015
+ _meta: {
2016
+ piAcp: {
2017
+ startupInfo: null
2018
+ }
2019
+ }
1314
2020
  };
1315
2021
  setTimeout(() => {
1316
- void this.conn.sessionUpdate({
1317
- sessionId: session.sessionId,
1318
- update: {
1319
- sessionUpdate: "available_commands_update",
1320
- availableCommands: mergeCommands(toAvailableCommands(fileCommands), builtinAvailableCommands())
2022
+ void (async () => {
2023
+ try {
2024
+ const pi = await proc.getCommands();
2025
+ const { commands } = toAvailableCommandsFromPiGetCommands(pi, {
2026
+ enableSkillCommands,
2027
+ includeExtensionCommands: false
2028
+ });
2029
+ await this.conn.sessionUpdate({
2030
+ sessionId: session.sessionId,
2031
+ update: {
2032
+ sessionUpdate: "available_commands_update",
2033
+ availableCommands: mergeCommands(commands, builtinAvailableCommands())
2034
+ }
2035
+ });
2036
+ return;
2037
+ } catch {
1321
2038
  }
1322
- });
2039
+ await this.conn.sessionUpdate({
2040
+ sessionId: session.sessionId,
2041
+ update: {
2042
+ sessionUpdate: "available_commands_update",
2043
+ availableCommands: mergeCommands(toAvailableCommands(fileCommands), builtinAvailableCommands())
2044
+ }
2045
+ });
2046
+ })();
1323
2047
  }, 0);
1324
2048
  return response;
1325
2049
  }
@@ -1344,7 +2068,7 @@ ${JSON.stringify(stats, null, 2)}`;
1344
2068
  }
1345
2069
  }
1346
2070
  if (!provider || !modelId) {
1347
- throw RequestError2.invalidParams(`Unknown modelId: ${params.modelId}`);
2071
+ throw RequestError3.invalidParams(`Unknown modelId: ${params.modelId}`);
1348
2072
  }
1349
2073
  await session.proc.setModel(provider, modelId);
1350
2074
  }
@@ -1352,7 +2076,7 @@ ${JSON.stringify(stats, null, 2)}`;
1352
2076
  const session = this.sessions.get(params.sessionId);
1353
2077
  const mode = String(params.modeId);
1354
2078
  if (!isThinkingLevel(mode)) {
1355
- throw RequestError2.invalidParams(`Unknown modeId: ${mode}`);
2079
+ throw RequestError3.invalidParams(`Unknown modeId: ${mode}`);
1356
2080
  }
1357
2081
  await session.proc.setThinkingLevel(mode);
1358
2082
  void this.conn.sessionUpdate({
@@ -1422,13 +2146,149 @@ async function getModelState(proc) {
1422
2146
  currentModelId
1423
2147
  };
1424
2148
  }
2149
+ function isSemver(v) {
2150
+ return /^\d+\.\d+\.\d+(?:[-+].+)?$/.test(v);
2151
+ }
2152
+ function compareSemver(a, b) {
2153
+ const pa = a.split(/[.-]/).slice(0, 3).map((n) => Number(n));
2154
+ const pb = b.split(/[.-]/).slice(0, 3).map((n) => Number(n));
2155
+ for (let i = 0; i < 3; i++) {
2156
+ const da = pa[i] ?? 0;
2157
+ const db = pb[i] ?? 0;
2158
+ if (da > db) return 1;
2159
+ if (da < db) return -1;
2160
+ }
2161
+ return 0;
2162
+ }
2163
+ function buildStartupInfo(opts) {
2164
+ void opts.fileCommands;
2165
+ const md = [];
2166
+ let updateNotice = null;
2167
+ try {
2168
+ const piVersion = spawnSync("pi", ["--version"], { encoding: "utf-8" });
2169
+ const installed = String(piVersion.stdout ?? "").trim().replace(/^v/i, "");
2170
+ if (installed) {
2171
+ md.push(`pi v${installed}`);
2172
+ md.push("---");
2173
+ md.push("");
2174
+ try {
2175
+ const latestRes = spawnSync("npm", ["view", "@mariozechner/pi-coding-agent", "version"], {
2176
+ encoding: "utf-8",
2177
+ timeout: 800
2178
+ });
2179
+ const latest = String(latestRes.stdout ?? "").trim().replace(/^v/i, "");
2180
+ if (latest && isSemver(latest) && isSemver(installed) && compareSemver(latest, installed) > 0) {
2181
+ updateNotice = `New version available: v${latest} (installed v${installed}). Run: \`npm i -g @mariozechner/pi-coding-agent\``;
2182
+ }
2183
+ } catch {
2184
+ }
2185
+ }
2186
+ } catch {
2187
+ }
2188
+ const addSection = (title, items) => {
2189
+ const cleaned = items.map((s) => s.trim()).filter(Boolean);
2190
+ if (!cleaned.length) return;
2191
+ md.push(`## ${title}`);
2192
+ for (const item of cleaned) md.push(`- ${item}`);
2193
+ md.push("");
2194
+ };
2195
+ const contextItems = [];
2196
+ const contextPath = join6(opts.cwd, "AGENTS.md");
2197
+ if (existsSync4(contextPath)) contextItems.push(contextPath);
2198
+ addSection("Context", contextItems);
2199
+ const skillsItems = [];
2200
+ const pushSkillFromRoot = (root) => {
2201
+ try {
2202
+ for (const e of readdirSync3(root)) {
2203
+ const p = join6(root, e);
2204
+ try {
2205
+ const st = statSync2(p);
2206
+ if (st.isFile() && e.toLowerCase().endsWith(".md")) {
2207
+ skillsItems.push(p);
2208
+ }
2209
+ } catch {
2210
+ }
2211
+ }
2212
+ const stack = [root];
2213
+ while (stack.length) {
2214
+ const dir = stack.pop();
2215
+ let entries = [];
2216
+ try {
2217
+ entries = readdirSync3(dir);
2218
+ } catch {
2219
+ continue;
2220
+ }
2221
+ for (const name of entries) {
2222
+ if (name === "node_modules" || name === ".git") continue;
2223
+ const p = join6(dir, name);
2224
+ let st;
2225
+ try {
2226
+ st = statSync2(p);
2227
+ } catch {
2228
+ continue;
2229
+ }
2230
+ if (st.isDirectory()) {
2231
+ stack.push(p);
2232
+ } else if (st.isFile() && name === "SKILL.md") {
2233
+ skillsItems.push(p);
2234
+ }
2235
+ }
2236
+ }
2237
+ } catch {
2238
+ }
2239
+ };
2240
+ const globalSkillsDir = join6(getAgentDir(), "skills");
2241
+ pushSkillFromRoot(globalSkillsDir);
2242
+ const legacyAgentsSkillsDir = join6(process.env.HOME ?? "", ".agents", "skills");
2243
+ pushSkillFromRoot(legacyAgentsSkillsDir);
2244
+ const projectSkillsDir = join6(opts.cwd, ".pi", "skills");
2245
+ pushSkillFromRoot(projectSkillsDir);
2246
+ addSection("Skills", skillsItems);
2247
+ const promptsItems = [];
2248
+ const promptsDir = join6(process.env.HOME ?? "", ".pi", "agent", "prompts");
2249
+ try {
2250
+ const prompts = readdirSync3(promptsDir).filter((f) => f.endsWith(".md"));
2251
+ for (const f of prompts) promptsItems.push(`/${basename(f, ".md")}`);
2252
+ } catch {
2253
+ }
2254
+ addSection("Prompts", promptsItems);
2255
+ const extItems = [];
2256
+ const extDir = join6(process.env.HOME ?? "", ".pi", "agent", "extensions");
2257
+ try {
2258
+ const exts = readdirSync3(extDir).filter((f) => f.endsWith(".ts") || f.endsWith(".js"));
2259
+ for (const f of exts) extItems.push(join6(extDir, f));
2260
+ } catch {
2261
+ }
2262
+ try {
2263
+ const settingsPath = join6(process.env.HOME ?? "", ".pi", "agent", "settings.json");
2264
+ const settings = JSON.parse(readFileSync7(settingsPath, "utf-8"));
2265
+ const pkgs = Array.isArray(settings?.packages) ? settings.packages : [];
2266
+ for (const pkg2 of pkgs) {
2267
+ const s = String(pkg2);
2268
+ if (s.startsWith("npm:")) {
2269
+ extItems.push(`${s}
2270
+ - index.ts`);
2271
+ } else {
2272
+ extItems.push(s);
2273
+ }
2274
+ }
2275
+ } catch {
2276
+ }
2277
+ addSection("Extensions", extItems);
2278
+ if (updateNotice) {
2279
+ md.push("---");
2280
+ md.push(updateNotice);
2281
+ md.push("");
2282
+ }
2283
+ return md.join("\n").trim() + "\n";
2284
+ }
1425
2285
  function readNearestPackageJson(metaUrl) {
1426
2286
  try {
1427
2287
  let dir = dirname2(fileURLToPath(metaUrl));
1428
2288
  for (let i = 0; i < 6; i++) {
1429
- const p = join3(dir, "package.json");
1430
- if (existsSync2(p)) {
1431
- const json = JSON.parse(readFileSync4(p, "utf-8"));
2289
+ const p = join6(dir, "package.json");
2290
+ if (existsSync4(p)) {
2291
+ const json = JSON.parse(readFileSync7(p, "utf-8"));
1432
2292
  return { name: json?.name, version: json?.version };
1433
2293
  }
1434
2294
  dir = dirname2(dir);
@@ -1439,13 +2299,31 @@ function readNearestPackageJson(metaUrl) {
1439
2299
  }
1440
2300
 
1441
2301
  // src/index.ts
2302
+ if (process.argv.includes("--terminal-login")) {
2303
+ const { spawnSync: spawnSync2 } = await import("child_process");
2304
+ const cmd = process.env.PI_ACP_PI_COMMAND ?? "pi";
2305
+ const res = spawnSync2(cmd, [], { stdio: "inherit", env: process.env });
2306
+ if (res.error && res.error.code === "ENOENT") {
2307
+ process.stderr.write(
2308
+ `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.
2309
+ `
2310
+ );
2311
+ process.exit(1);
2312
+ }
2313
+ process.exit(typeof res.status === "number" ? res.status : 1);
2314
+ }
1442
2315
  var input = new WritableStream({
1443
2316
  write(chunk) {
1444
- return new Promise((resolve2, reject) => {
1445
- process.stdout.write(chunk, (err) => {
1446
- if (err) reject(err);
1447
- else resolve2();
1448
- });
2317
+ return new Promise((resolve3) => {
2318
+ if (process.stdout.destroyed || !process.stdout.writable) return resolve3();
2319
+ try {
2320
+ process.stdout.write(chunk, (err) => {
2321
+ void err;
2322
+ resolve3();
2323
+ });
2324
+ } catch {
2325
+ resolve3();
2326
+ }
1449
2327
  });
1450
2328
  }
1451
2329
  });
@@ -1461,4 +2339,10 @@ new AgentSideConnection((conn) => new PiAcpAgent(conn), stream);
1461
2339
  process.stdin.resume();
1462
2340
  process.on("SIGINT", () => process.exit(0));
1463
2341
  process.on("SIGTERM", () => process.exit(0));
2342
+ process.stdout.on("error", () => {
2343
+ try {
2344
+ process.exit(0);
2345
+ } catch {
2346
+ }
2347
+ });
1464
2348
  //# sourceMappingURL=index.js.map