@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 #
|
|
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
|
|
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.
|
|
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
|
-
*
|
|
2912
|
-
*
|
|
2913
|
-
*
|
|
2914
|
-
*
|
|
2915
|
-
*
|
|
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-
|
|
3739
|
+
//# sourceMappingURL=daemon-DTmhT4aC.mjs.map
|