pi-acp 0.0.20 → 0.0.22

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/README.md CHANGED
@@ -2,12 +2,14 @@
2
2
 
3
3
  ACP ([Agent Client Protocol](https://agentclientprotocol.com/overview/introduction)) adapter for [`pi`](https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent) coding agent (fka shitty coding agent).
4
4
 
5
- `pi-acp` communicates **ACP JSON-RPC 2.0 over stdio** to an ACP client (e.g. an editor) and spawns `pi --mode rpc`, bridging requests/events between the two.
5
+ `pi-acp` communicates **ACP JSON-RPC 2.0 over stdio** to an ACP client (e.g. Zed editor) and spawns `pi --mode rpc`, bridging requests/events between the two.
6
6
 
7
7
  ## Status
8
8
 
9
9
  This is an MVP-style adapter intended to be useful today and easy to iterate on. Some ACP features may be not implemented or are not supported (see [Limitations](#limitations)). Development is centered around [Zed](https://zed.dev) editor support, other clients may have varying levels of compatibility.
10
10
 
11
+ Expect some minor breaking changes.
12
+
11
13
  ## Features
12
14
 
13
15
  - Streams assistant output as ACP `agent_message_chunk`
@@ -21,7 +23,7 @@ This is an MVP-style adapter intended to be useful today and easy to iterate on.
21
23
  - Adds a small set of built-in commands for headless/editor usage
22
24
  - Supports skill commands (if enabled in pi settings, they appear as `/skill:skill-name` in the ACP client)
23
25
  - Skills are loaded by pi directly and are available in ACP sessions
24
- - (Zed) By default, `pi-acp` emits a short markdown “startup info” block into the session (pi version, context, skills, prompts, extensions - similar to `pi` in the terminal). You can disable it by setting: `PI_ACP_STARTUP_INFO=false` (see below)
26
+ - (Zed) `pi-acp` emits “startup info” block into the session (pi version, context, skills, prompts, extensions - similar to `pi` in the terminal). You can disable it by setting `quietStartup: true` in pi settings (`~/.pi/agent/settings.json` or `<project>/.pi/settings.json`). When `quietStartup` is enabled, `pi-acp` will still emit a 'New version available' message if the installed pi version is outdated.
25
27
  - (Zed) Session history is supported in Zed starting with [`v0.225.0`](https://zed.dev/releases/preview/0.225.0). Session loading / history maps to pi's session files. Sessions can be resumed both in `pi` and in the ACP client.
26
28
 
27
29
  ## Prerequisites
@@ -40,19 +42,29 @@ npm install -g @mariozechner/pi-coding-agent
40
42
 
41
43
  ### Add pi-acp to your ACP client, e.g. [Zed](https://zed.dev/docs/agents/external-agents/)
42
44
 
43
- Add the following to your Zed `settngs.json`:
45
+ #### Using ACP Registry in Zed or other clients that support it:
46
+
47
+ In Zed launch the registry with `zed: acp registry` command and select `pi ACP` adapter from the list. This will automatically add the agent server configuration to your `settings.json` and keep it up to date:
48
+
49
+ ```json
50
+ "agent_servers": {
51
+ "pi-acp": {
52
+ "type": "registry",
53
+ },
54
+ }
55
+ ```
44
56
 
45
57
  #### Using with `npx` (no global install needed, always loads the latest version):
46
58
 
59
+ Add the following to your Zed `settings.json`:
60
+
47
61
  ```json
48
62
  "agent_servers": {
49
63
  "pi": {
50
64
  "type": "custom",
51
65
  "command": "npx",
52
66
  "args": ["-y", "pi-acp"],
53
- "env": {
54
- "PI_ACP_STARTUP_INFO": "true" // optional, "true" by default
55
- }
67
+ "env": {}
56
68
  }
57
69
  }
58
70
  ```
@@ -98,14 +110,12 @@ Point your ACP client to the built `dist/index.js`:
98
110
 
99
111
  `pi-acp` supports slash commands:
100
112
 
101
- #### 1) File-based commands (compatible with pi)
113
+ #### 1) File-based commands (aka prompts)
102
114
 
103
115
  Loaded from:
104
116
 
105
- - User commands: `~/.pi/agent/commands/**/*.md`
106
- - Project commands: `<cwd>/.pi/commands/**/*.md`
107
-
108
- These are expanded adapter-side (pi RPC mode doesn’t expand them).
117
+ - User commands: `~/.pi/agent/prompts/**/*.md`
118
+ - Project commands: `<cwd>/.pi/prompts/**/*.md`
109
119
 
110
120
  #### 2) Built-in commands
111
121
 
package/dist/index.js CHANGED
@@ -146,7 +146,7 @@ var PiRpcProcess = class _PiRpcProcess {
146
146
  }
147
147
  static async spawn(params) {
148
148
  const cmd = params.piCommand ?? "pi";
149
- const args = ["--mode", "rpc"];
149
+ const args = ["--mode", "rpc", "--no-themes"];
150
150
  if (params.sessionPath) args.push("--session", params.sessionPath);
151
151
  const child = spawn(cmd, args, {
152
152
  cwd: params.cwd,
@@ -527,10 +527,34 @@ function expandSlashCommand(text, fileCommands) {
527
527
  var SessionManager = class {
528
528
  sessions = /* @__PURE__ */ new Map();
529
529
  store = new SessionStore();
530
+ /** Dispose all sessions and their underlying pi subprocesses. */
531
+ disposeAll() {
532
+ for (const [id] of this.sessions) this.close(id);
533
+ }
530
534
  /** Get a registered session if it exists (no throw). */
531
535
  maybeGet(sessionId) {
532
536
  return this.sessions.get(sessionId);
533
537
  }
538
+ /**
539
+ * Dispose a session's underlying pi process and remove it from the manager.
540
+ * Used when clients explicitly reload a session and we want a fresh pi subprocess.
541
+ */
542
+ close(sessionId) {
543
+ const s = this.sessions.get(sessionId);
544
+ if (!s) return;
545
+ try {
546
+ s.proc.dispose?.();
547
+ } catch {
548
+ }
549
+ this.sessions.delete(sessionId);
550
+ }
551
+ /** Close all sessions except the one with `keepSessionId`. */
552
+ closeAllExcept(keepSessionId) {
553
+ for (const [id] of this.sessions) {
554
+ if (id === keepSessionId) continue;
555
+ this.close(id);
556
+ }
557
+ }
534
558
  async create(params) {
535
559
  let proc;
536
560
  try {
@@ -645,13 +669,6 @@ var PiAcpSession = class {
645
669
  });
646
670
  }
647
671
  async prompt(message, images = []) {
648
- if (!this.startupInfoSent && this.startupInfo) {
649
- this.startupInfoSent = true;
650
- this.emit({
651
- sessionUpdate: "agent_message_chunk",
652
- content: { type: "text", text: this.startupInfo }
653
- });
654
- }
655
672
  const expandedMessage = expandSlashCommand(message, this.fileCommands);
656
673
  const turnPromise = new Promise((resolve3, reject) => {
657
674
  const queued = { message: expandedMessage, images, resolve: resolve3, reject };
@@ -1254,21 +1271,32 @@ function readJsonFile(path) {
1254
1271
  return {};
1255
1272
  }
1256
1273
  }
1257
- function getAgentDir() {
1258
- return process.env.PI_CODING_AGENT_DIR ? resolve2(process.env.PI_CODING_AGENT_DIR) : join4(homedir4(), ".pi", "agent");
1259
- }
1260
- function getEnableSkillCommands(cwd) {
1274
+ function getMergedSettings(cwd) {
1261
1275
  const globalSettingsPath = join4(getAgentDir(), "settings.json");
1262
1276
  const projectSettingsPath = resolve2(cwd, ".pi", "settings.json");
1263
1277
  const global = readJsonFile(globalSettingsPath);
1264
1278
  const project = readJsonFile(projectSettingsPath);
1265
- const merged = deepMerge(global, project);
1279
+ return deepMerge(global, project);
1280
+ }
1281
+ function getAgentDir() {
1282
+ return process.env.PI_CODING_AGENT_DIR ? resolve2(process.env.PI_CODING_AGENT_DIR) : join4(homedir4(), ".pi", "agent");
1283
+ }
1284
+ function getEnableSkillCommands(cwd) {
1285
+ const merged = getMergedSettings(cwd);
1266
1286
  const direct = merged.enableSkillCommands;
1267
1287
  if (typeof direct === "boolean") return direct;
1268
1288
  const nested = isObject(merged.skills) ? merged.skills.enableSkillCommands : void 0;
1269
1289
  if (typeof nested === "boolean") return nested;
1270
1290
  return true;
1271
1291
  }
1292
+ function getQuietStartup(cwd) {
1293
+ const merged = getMergedSettings(cwd);
1294
+ const direct = merged.quietStartup;
1295
+ if (typeof direct === "boolean") return direct;
1296
+ const legacy = merged.quietStart;
1297
+ if (typeof legacy === "boolean") return legacy;
1298
+ return false;
1299
+ }
1272
1300
 
1273
1301
  // src/acp/pi-commands.ts
1274
1302
  function describeFallback(c) {
@@ -1377,14 +1405,6 @@ function hasAnyPiAuthConfigured() {
1377
1405
 
1378
1406
  // src/acp/agent.ts
1379
1407
  import { fileURLToPath } from "url";
1380
- function booleanEnv(name, defaultValue) {
1381
- const raw = process.env[name];
1382
- if (raw == null) return defaultValue;
1383
- const v = String(raw).trim().toLowerCase();
1384
- if (v === "true") return true;
1385
- if (v === "false") return false;
1386
- return defaultValue;
1387
- }
1388
1408
  function builtinAvailableCommands() {
1389
1409
  return [
1390
1410
  {
@@ -1441,6 +1461,9 @@ var PiAcpAgent = class {
1441
1461
  conn;
1442
1462
  sessions = new SessionManager();
1443
1463
  store = new SessionStore();
1464
+ dispose() {
1465
+ this.sessions.disposeAll();
1466
+ }
1444
1467
  // Remember recent session cwd and use it as the default filter.
1445
1468
  lastSessionCwd = null;
1446
1469
  constructor(conn, _config) {
@@ -1498,12 +1521,21 @@ var PiAcpAgent = class {
1498
1521
  fileCommands,
1499
1522
  piCommand: process.env.PI_ACP_PI_COMMAND
1500
1523
  });
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
- }
1524
+ let state = null;
1525
+ let availableModels = null;
1526
+ await Promise.all([
1527
+ session.proc.getState().then((s) => {
1528
+ state = s;
1529
+ }).catch(() => {
1530
+ state = null;
1531
+ }),
1532
+ session.proc.getAvailableModels().then((m) => {
1533
+ availableModels = m;
1534
+ }).catch(() => {
1535
+ availableModels = null;
1536
+ })
1537
+ ]);
1538
+ const rawModelsCount = Array.isArray(availableModels?.models) ? availableModels.models.length : 0;
1507
1539
  if (rawModelsCount === 0) {
1508
1540
  try {
1509
1541
  session.proc.dispose?.();
@@ -1514,11 +1546,18 @@ var PiAcpAgent = class {
1514
1546
  "Configure an API key or log in with an OAuth provider."
1515
1547
  );
1516
1548
  }
1517
- const models = await getModelState(session.proc);
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);
1549
+ const models = await getModelState(session.proc, { state, availableModels });
1550
+ const thinking = await getThinkingState(session.proc, { state });
1551
+ const quietStartup = getQuietStartup(params.cwd);
1552
+ const updateNotice = buildUpdateNotice();
1553
+ const preludeText = quietStartup ? updateNotice ? updateNotice + "\n" : "" : buildStartupInfo({
1554
+ cwd: params.cwd,
1555
+ fileCommands,
1556
+ updateNotice
1557
+ });
1558
+ if (preludeText)
1559
+ session.setStartupInfo(preludeText);
1560
+ this.sessions.closeAllExcept?.(session.sessionId);
1522
1561
  const response = {
1523
1562
  sessionId: session.sessionId,
1524
1563
  models,
@@ -1529,7 +1568,7 @@ var PiAcpAgent = class {
1529
1568
  }
1530
1569
  }
1531
1570
  };
1532
- if (showStartupInfo) setTimeout(() => session.sendStartupInfoIfPending(), 0);
1571
+ if (preludeText) setTimeout(() => session.sendStartupInfoIfPending(), 0);
1533
1572
  setTimeout(() => {
1534
1573
  void (async () => {
1535
1574
  try {
@@ -1960,6 +1999,7 @@ ${JSON.stringify(stats, null, 2)}`;
1960
1999
  if (!isAbsolute2(params.cwd)) {
1961
2000
  throw RequestError3.invalidParams(`cwd must be an absolute path: ${params.cwd}`);
1962
2001
  }
2002
+ this.sessions.close(params.sessionId);
1963
2003
  this.lastSessionCwd = params.cwd;
1964
2004
  const stored = this.store.get(params.sessionId);
1965
2005
  const sessionFile = stored?.sessionFile ?? findPiSessionFile(params.sessionId);
@@ -1988,6 +2028,7 @@ ${JSON.stringify(stats, null, 2)}`;
1988
2028
  proc,
1989
2029
  fileCommands
1990
2030
  });
2031
+ this.sessions.closeAllExcept?.(session.sessionId);
1991
2032
  this.store.upsert({
1992
2033
  sessionId: params.sessionId,
1993
2034
  cwd: params.cwd,
@@ -2135,14 +2176,17 @@ ${JSON.stringify(stats, null, 2)}`;
2135
2176
  function isThinkingLevel(x) {
2136
2177
  return x === "off" || x === "minimal" || x === "low" || x === "medium" || x === "high" || x === "xhigh";
2137
2178
  }
2138
- async function getThinkingState(proc) {
2179
+ async function getThinkingState(proc, pre) {
2139
2180
  let current = "medium";
2140
- try {
2141
- const state = await proc.getState();
2142
- const tl = typeof state?.thinkingLevel === "string" ? state.thinkingLevel : null;
2143
- if (tl && isThinkingLevel(tl)) current = tl;
2144
- } catch {
2145
- }
2181
+ const state = pre?.state ?? await (async () => {
2182
+ try {
2183
+ return await proc.getState();
2184
+ } catch {
2185
+ return null;
2186
+ }
2187
+ })();
2188
+ const tl = typeof state?.thinkingLevel === "string" ? state.thinkingLevel : null;
2189
+ if (tl && isThinkingLevel(tl)) current = tl;
2146
2190
  const available = ["off", "minimal", "low", "medium", "high", "xhigh"];
2147
2191
  return {
2148
2192
  currentModeId: current,
@@ -2153,34 +2197,40 @@ async function getThinkingState(proc) {
2153
2197
  }))
2154
2198
  };
2155
2199
  }
2156
- async function getModelState(proc) {
2200
+ async function getModelState(proc, pre) {
2157
2201
  let availableModels = [];
2158
- try {
2159
- const data = await proc.getAvailableModels();
2160
- const models = Array.isArray(data?.models) ? data.models : [];
2161
- availableModels = models.map((m) => {
2162
- const provider = String(m?.provider ?? "").trim();
2163
- const id = String(m?.id ?? "").trim();
2164
- if (!provider || !id) return null;
2165
- const name = String(m?.name ?? id);
2166
- return {
2167
- modelId: `${provider}/${id}`,
2168
- name: `${provider}/${name}`,
2169
- description: null
2170
- };
2171
- }).filter(Boolean);
2172
- } catch {
2173
- }
2202
+ const data = pre?.availableModels ?? await (async () => {
2203
+ try {
2204
+ return await proc.getAvailableModels();
2205
+ } catch {
2206
+ return null;
2207
+ }
2208
+ })();
2209
+ const models = Array.isArray(data?.models) ? data.models : [];
2210
+ availableModels = models.map((m) => {
2211
+ const provider = String(m?.provider ?? "").trim();
2212
+ const id = String(m?.id ?? "").trim();
2213
+ if (!provider || !id) return null;
2214
+ const name = String(m?.name ?? id);
2215
+ return {
2216
+ modelId: `${provider}/${id}`,
2217
+ name: `${provider}/${name}`,
2218
+ description: null
2219
+ };
2220
+ }).filter(Boolean);
2174
2221
  let currentModelId = null;
2175
- try {
2176
- const state = await proc.getState();
2177
- const model = state?.model;
2178
- if (model && typeof model === "object") {
2179
- const provider = String(model.provider ?? "").trim();
2180
- const id = String(model.id ?? "").trim();
2181
- if (provider && id) currentModelId = `${provider}/${id}`;
2222
+ const state = pre?.state ?? await (async () => {
2223
+ try {
2224
+ return await proc.getState();
2225
+ } catch {
2226
+ return null;
2182
2227
  }
2183
- } catch {
2228
+ })();
2229
+ const model = state?.model;
2230
+ if (model && typeof model === "object") {
2231
+ const provider = String(model.provider ?? "").trim();
2232
+ const id = String(model.id ?? "").trim();
2233
+ if (provider && id) currentModelId = `${provider}/${id}`;
2184
2234
  }
2185
2235
  if (!availableModels.length && !currentModelId) return null;
2186
2236
  if (!currentModelId) currentModelId = availableModels[0]?.modelId ?? "default";
@@ -2203,10 +2253,26 @@ function compareSemver(a, b) {
2203
2253
  }
2204
2254
  return 0;
2205
2255
  }
2256
+ function buildUpdateNotice() {
2257
+ try {
2258
+ const piVersion = spawnSync("pi", ["--version"], { encoding: "utf-8" });
2259
+ const installed = String(piVersion.stdout ?? "").trim().replace(/^v/i, "");
2260
+ if (!installed || !isSemver(installed)) return null;
2261
+ const latestRes = spawnSync("npm", ["view", "@mariozechner/pi-coding-agent", "version"], {
2262
+ encoding: "utf-8",
2263
+ timeout: 800
2264
+ });
2265
+ const latest = String(latestRes.stdout ?? "").trim().replace(/^v/i, "");
2266
+ if (!latest || !isSemver(latest)) return null;
2267
+ if (compareSemver(latest, installed) <= 0) return null;
2268
+ return `New version available: v${latest} (installed v${installed}). Run: \`npm i -g @mariozechner/pi-coding-agent\``;
2269
+ } catch {
2270
+ return null;
2271
+ }
2272
+ }
2206
2273
  function buildStartupInfo(opts) {
2207
2274
  void opts.fileCommands;
2208
2275
  const md = [];
2209
- let updateNotice = null;
2210
2276
  try {
2211
2277
  const piVersion = spawnSync("pi", ["--version"], { encoding: "utf-8" });
2212
2278
  const installed = String(piVersion.stdout ?? "").trim().replace(/^v/i, "");
@@ -2214,17 +2280,6 @@ function buildStartupInfo(opts) {
2214
2280
  md.push(`pi v${installed}`);
2215
2281
  md.push("---");
2216
2282
  md.push("");
2217
- try {
2218
- const latestRes = spawnSync("npm", ["view", "@mariozechner/pi-coding-agent", "version"], {
2219
- encoding: "utf-8",
2220
- timeout: 800
2221
- });
2222
- const latest = String(latestRes.stdout ?? "").trim().replace(/^v/i, "");
2223
- if (latest && isSemver(latest) && isSemver(installed) && compareSemver(latest, installed) > 0) {
2224
- updateNotice = `New version available: v${latest} (installed v${installed}). Run: \`npm i -g @mariozechner/pi-coding-agent\``;
2225
- }
2226
- } catch {
2227
- }
2228
2283
  }
2229
2284
  } catch {
2230
2285
  }
@@ -2318,9 +2373,9 @@ function buildStartupInfo(opts) {
2318
2373
  } catch {
2319
2374
  }
2320
2375
  addSection("Extensions", extItems);
2321
- if (updateNotice) {
2376
+ if (opts.updateNotice) {
2322
2377
  md.push("---");
2323
- md.push(updateNotice);
2378
+ md.push(opts.updateNotice);
2324
2379
  md.push("");
2325
2380
  }
2326
2381
  return md.join("\n").trim() + "\n";
@@ -2378,10 +2433,23 @@ var output = new ReadableStream({
2378
2433
  }
2379
2434
  });
2380
2435
  var stream = ndJsonStream(input, output);
2381
- new AgentSideConnection((conn) => new PiAcpAgent(conn), stream);
2436
+ var agent = new AgentSideConnection((conn) => new PiAcpAgent(conn), stream);
2437
+ function shutdown() {
2438
+ try {
2439
+ ;
2440
+ agent?.agent?.dispose?.();
2441
+ } catch {
2442
+ }
2443
+ try {
2444
+ process.exit(0);
2445
+ } catch {
2446
+ }
2447
+ }
2448
+ process.stdin.on("end", shutdown);
2449
+ process.stdin.on("close", shutdown);
2382
2450
  process.stdin.resume();
2383
- process.on("SIGINT", () => process.exit(0));
2384
- process.on("SIGTERM", () => process.exit(0));
2451
+ process.on("SIGINT", shutdown);
2452
+ process.on("SIGTERM", shutdown);
2385
2453
  process.stdout.on("error", () => {
2386
2454
  try {
2387
2455
  process.exit(0);