@victor-software-house/pi-acp 0.16.0 → 0.17.1

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
@@ -62,6 +62,11 @@ Active development. ACP compliance is improving steadily. Development is centere
62
62
  - `local` (default) / `overlay` — ACP `params.cwd` used as session cwd; manifest roots compose
63
63
  - `none` — pi-acp mints an ephemeral tmpdir under `os.tmpdir()/pi-acp-session-*`, cleaned up at session dispose. For one-shot Q&A sessions that shouldn't pollute any project directory.
64
64
  - **ACP-FS `read` delegation** (PRD-002 §FR-6) — When the client advertises `clientCapabilities.fs.readTextFile`, pi-acp routes pi's built-in `read` tool through `connection.fs.readTextFile` instead of local disk. Lets Zed Remote read the actual remote workspace files (the ones the user is editing) while pi runs locally.
65
+ - **ACP terminal delegation** (PRD-002 §FR-6.5) — When the client advertises `clientCapabilities.terminal`, pi-acp overrides pi's built-in `bash` tool with an ACP `createTerminal`-backed implementation. Commands run on the client's machine via `terminal/*` lifecycle, so Zed Remote workflows execute `bash` on the remote workspace where the user actually edits. Pairs with `read` delegation so the full read/bash pair lands consistently remote.
66
+ - **ACP provider config** — `agentCapabilities.providers = {}` advertises `providers/list`, `providers/set`, `providers/disable`. Soft-disable on top of pi's `unregisterProvider`. Per-process; no `models.json` writer.
67
+ - **ACP logout** — `agentCapabilities.auth.logout = {}` advertises `logout`. Clears every provider's credentials from the shared `AuthStorage` in one call.
68
+ - **ACP session delete** — implemented but DISABLED by default (see Limitations). Direct invocation returns `methodNotFound`; capability not advertised. Flip `PiAcpAgent.SESSION_DELETE_ENABLED` to enable.
69
+ - **ACP `extMethod` / `extNotification`** — dispatcher under the `pi-acp/` namespace. Built-ins: `pi-acp/ping`, `pi-acp/runtime-info`.
65
70
 
66
71
  ## Resource composition (`.pi-acp.yaml`)
67
72
 
@@ -208,7 +213,7 @@ bun run dev # run from src
208
213
  bun run build # tsdown -> dist/index.mjs
209
214
  bun run typecheck # tsc --noEmit
210
215
  bun run lint # biome + oxlint
211
- bun test # 277 tests
216
+ bun test # 308 tests
212
217
  ```
213
218
 
214
219
  Project layout:
@@ -249,7 +254,17 @@ test/
249
254
 
250
255
  - **`agent_plan`** -- plan updates not emitted before tool execution. pi has no equivalent planning surface.
251
256
  - **ACP filesystem `write` delegation** (`fs/write_text_file`) -- pi writes locally. Not advertised. `fs/read_text_file` IS routed through ACP when the client advertises the capability (see Features → ACP-FS `read` delegation).
252
- - **ACP terminal delegation** (`terminal/*`) -- pi executes commands locally. Not advertised.
257
+ - **ACP terminal delegation** (`terminal/*`) -- DELEGATED. When the client advertises `clientCapabilities.terminal`, pi-acp overrides pi's built-in `bash` tool with an ACP `createTerminal`-backed implementation so commands run on the client's machine (Zed Remote routes `terminal/*` to the remote workspace). See Features → ACP terminal delegation.
258
+
259
+ ### ACP optional methods implemented (substrate completion at v0.16.0+)
260
+
261
+ - **`providers/list` / `providers/set` / `providers/disable`** -- advertised via `agentCapabilities.providers = {}`. Operates on every live `ModelRegistry`. Soft-disable is layered on top of pi's destructive `unregisterProvider`. Mutations are per-process (no models.json writer in pi).
262
+ - **`logout`** -- advertised via `agentCapabilities.auth.logout = {}`. Clears every provider's credentials from the shared AuthStorage. Sessions stay live; subsequent prompts may surface `auth_required`.
263
+ - **`extMethod` / `extNotification`** -- dispatcher under the `pi-acp/` method-name namespace. Built-in handlers: `pi-acp/ping`, `pi-acp/runtime-info`. Unknown methods → `methodNotFound`.
264
+
265
+ ### Implemented but DISABLED by default
266
+
267
+ - **`session/delete`** -- the implementation (release-from-daemon → `fs.rmSync` → cache purge) is in place, but the capability is NOT advertised in `initialize()` and direct invocations return `methodNotFound`. Gated behind `PiAcpAgent.SESSION_DELETE_ENABLED = false`. Rationale: ACP `session/delete` takes a single sessionId, has no confirmation surface, no trash, no recovery — easy to misfire from a UI button or a mistaken script. Re-enable only after a client-layer confirmation flow exists.
253
268
 
254
269
  ### Design decisions
255
270
 
@@ -8,7 +8,7 @@ import { isAbsolute, join, resolve } from "node:path";
8
8
  import { Hono } from "hono";
9
9
  import { AgentSideConnection, RequestError, ndJsonStream } from "@agentclientprotocol/sdk";
10
10
  import { randomUUID } from "node:crypto";
11
- import { DefaultResourceLoader, SessionManager, createAgentSession, createBashToolDefinition, createReadToolDefinition, getAgentDir } from "@earendil-works/pi-coding-agent";
11
+ import { AuthStorage, DefaultResourceLoader, SessionManager, createAgentSession, createBashToolDefinition, createReadToolDefinition, getAgentDir } from "@earendil-works/pi-coding-agent";
12
12
  import * as z from "zod";
13
13
  import { parse } from "yaml";
14
14
  import { $ } from "bun";
@@ -2195,7 +2195,7 @@ var SshBackend = class {
2195
2195
  //#endregion
2196
2196
  //#region package.json
2197
2197
  var name = "@victor-software-house/pi-acp";
2198
- var version = "0.16.0";
2198
+ var version = "0.17.1";
2199
2199
  //#endregion
2200
2200
  //#region src/acp/agent.ts
2201
2201
  /** Builtin ACP slash commands handled directly by the adapter. */
@@ -2275,7 +2275,7 @@ function truncateSessionTitle(text) {
2275
2275
  if (oneLine.length <= SESSION_TITLE_MAX) return oneLine;
2276
2276
  return `${oneLine.slice(0, SESSION_TITLE_MAX - 1)}…`;
2277
2277
  }
2278
- var PiAcpAgent = class {
2278
+ var PiAcpAgent = class PiAcpAgent {
2279
2279
  conn;
2280
2280
  sessions = new SessionManager$1();
2281
2281
  /** Cache of sessionId → file path, populated by listSessions and newSession. */
@@ -2299,6 +2299,19 @@ var PiAcpAgent = class {
2299
2299
  * report `current: null` per ACP spec even after disable.
2300
2300
  */
2301
2301
  disabledProviders = /* @__PURE__ */ new Set();
2302
+ /**
2303
+ * Master toggle for `session/delete` advertisement + method body.
2304
+ *
2305
+ * Disabled by default: the method irreversibly removes a session file
2306
+ * via fs.rmSync, and ACP clients invoke it with a single sessionId arg
2307
+ * — no confirmation surface, no trash. Easy to misfire from a UI or
2308
+ * an extension. Re-enable only when a confirmation flow exists at the
2309
+ * client layer or when the operator opts in via configuration.
2310
+ *
2311
+ * The implementation (release-from-daemon → fs.rmSync → cache purge)
2312
+ * is kept intact so re-enabling is a one-line flip plus a re-advertise.
2313
+ */
2314
+ static SESSION_DELETE_ENABLED = false;
2302
2315
  dispose() {
2303
2316
  if (this.daemonContext !== void 0) {
2304
2317
  const registry = this.daemonContext.sessionRegistry;
@@ -2354,6 +2367,41 @@ var PiAcpAgent = class {
2354
2367
  disabled: this.disabledProviders
2355
2368
  }, params);
2356
2369
  }
2370
+ /**
2371
+ * Clears every provider's stored credentials from the shared AuthStorage.
2372
+ *
2373
+ * ACP `LogoutRequest` carries only `_meta` — no per-provider selector —
2374
+ * so this is correctly GLOBAL. Pi has no AuthStorage.clearAll(); we
2375
+ * loop `list()` + `remove()` per provider.
2376
+ *
2377
+ * Sessions stay live. Subsequent prompts may surface auth_required which
2378
+ * is the correct UX. Best-effort fanout: post an agent_message_chunk to
2379
+ * every live PiAcpSession announcing the logout for client visibility.
2380
+ *
2381
+ * Strategy: reuse an active session's AuthStorage instance when one
2382
+ * exists (every live session shares the same on-disk auth.json). When
2383
+ * no session is live, mint an ad-hoc `AuthStorage.create()` to operate
2384
+ * directly on the on-disk store.
2385
+ *
2386
+ * Gated by `agentCapabilities.auth.logout = {}`.
2387
+ */
2388
+ async unstable_logout(_params) {
2389
+ const live = this.sessions.first();
2390
+ const authStorage = live !== void 0 ? live.piSession.modelRegistry.authStorage : AuthStorage.create();
2391
+ const providers = authStorage.list();
2392
+ for (const p of providers) authStorage.remove(p);
2393
+ for (const s of this.sessions.values()) this.conn.sessionUpdate({
2394
+ sessionId: s.sessionId,
2395
+ update: {
2396
+ sessionUpdate: "agent_message_chunk",
2397
+ content: {
2398
+ type: "text",
2399
+ text: "[pi-acp] Logged out from all providers.\n"
2400
+ }
2401
+ }
2402
+ }).catch(() => {});
2403
+ return { _meta: { piAcp: { clearedProviders: providers } } };
2404
+ }
2357
2405
  registerWithDaemon(input) {
2358
2406
  if (this.daemonContext === void 0) return;
2359
2407
  this.daemonContext.sessionRegistry.register({
@@ -2516,6 +2564,7 @@ var PiAcpAgent = class {
2516
2564
  },
2517
2565
  authMethods: buildAuthMethods({ supportsTerminalAuthMeta: this.clientCapabilities.terminalAuth }),
2518
2566
  agentCapabilities: {
2567
+ auth: { logout: {} },
2519
2568
  loadSession: true,
2520
2569
  mcpCapabilities: {
2521
2570
  http: false,
@@ -2531,7 +2580,7 @@ var PiAcpAgent = class {
2531
2580
  close: {},
2532
2581
  resume: {},
2533
2582
  fork: {},
2534
- delete: {}
2583
+ ...PiAcpAgent.SESSION_DELETE_ENABLED ? { delete: {} } : {}
2535
2584
  },
2536
2585
  providers: {}
2537
2586
  }
@@ -2908,21 +2957,27 @@ var PiAcpAgent = class {
2908
2957
  /**
2909
2958
  * Deletes a session's on-disk file + releases any live state.
2910
2959
  *
2911
- * Pi's SessionManager exposes no `delete()` method (verified against
2912
- * session-manager.d.ts) sessions are append-only JSONL files. We
2913
- * unlink the file directly via `fs.rmSync`. `resolveSessionFile`
2914
- * sources paths from `PiSessionManager.listAll`, so the unlinked path
2915
- * is always inside `~/.pi/agent/sessions/...`.
2960
+ * DISABLED by default via `PiAcpAgent.SESSION_DELETE_ENABLED = false`.
2961
+ * The capability is not advertised in initialize() and any direct call
2962
+ * is refused with `methodNotFound`. Rationale: ACP `session/delete`
2963
+ * carries only a sessionId no confirmation surface, no trash, no
2964
+ * recovery. Easy to misfire from a UI button or a mistaken script,
2965
+ * and the consequence is permanent loss of session history.
2966
+ *
2967
+ * When enabled: Pi's SessionManager exposes no `delete()` method
2968
+ * (verified against session-manager.d.ts) — sessions are append-only
2969
+ * JSONL files. We unlink the file directly via `fs.rmSync`.
2970
+ * `resolveSessionFile` sources paths from `PiSessionManager.listAll`,
2971
+ * so the unlinked path is always inside `~/.pi/agent/sessions/...`.
2916
2972
  *
2917
2973
  * Refuses to delete sessions owned by ANOTHER connection in the daemon
2918
2974
  * registry — security boundary: clients may only delete sessions they
2919
2975
  * own or sessions that are not currently live. Always releases the
2920
2976
  * daemon registry entry first so the live piSession is disposed
2921
2977
  * cleanly before the file disappears.
2922
- *
2923
- * Gated by `sessionCapabilities.delete = {}` (advertised in initialize).
2924
2978
  */
2925
2979
  async unstable_deleteSession(params) {
2980
+ if (!PiAcpAgent.SESSION_DELETE_ENABLED) throw RequestError.methodNotFound("session/delete");
2926
2981
  const sessionFile = await this.resolveSessionFile(params.sessionId);
2927
2982
  if (sessionFile === null) throw RequestError.invalidParams(`Unknown sessionId: ${params.sessionId}`);
2928
2983
  const live = this.daemonContext?.sessionRegistry.get(params.sessionId);
@@ -3681,4 +3736,4 @@ async function runDaemon() {
3681
3736
  //#endregion
3682
3737
  export { runDaemon };
3683
3738
 
3684
- //# sourceMappingURL=daemon-DKl32dgA.mjs.map
3739
+ //# sourceMappingURL=daemon-DTmhT4aC.mjs.map