@victor-software-house/pi-acp 0.10.0 → 0.12.0

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.
@@ -2,13 +2,13 @@
2
2
  import { a as removeStaleSocketIfAny, i as releaseLock, n as controlSocketPath, o as socketPath, r as ensureSocketParentDir, t as acquireLock } from "./socket-wvV053VI.mjs";
3
3
  import { t as piChangelogPath } from "./pi-package-aHs6rWNo.mjs";
4
4
  import { createServer } from "node:net";
5
- import { existsSync, readFileSync } from "node:fs";
6
- import { homedir } from "node:os";
5
+ import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
6
+ import { homedir, tmpdir } from "node:os";
7
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, getAgentDir } from "@earendil-works/pi-coding-agent";
11
+ import { DefaultResourceLoader, SessionManager, createAgentSession, 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";
@@ -184,6 +184,30 @@ function resolveIdleMs() {
184
184
  return n * 1e3;
185
185
  }
186
186
  //#endregion
187
+ //#region src/acp/acp-read-operations.ts
188
+ function createAcpReadOperations(deps) {
189
+ const { conn, getSessionId } = deps;
190
+ return {
191
+ async readFile(absolutePath) {
192
+ const sessionId = getSessionId();
193
+ if (sessionId === "") throw new Error("pi-acp acp-fs read: sessionId not yet bound");
194
+ const response = await conn.readTextFile({
195
+ sessionId,
196
+ path: absolutePath
197
+ });
198
+ return Buffer.from(response.content, "utf8");
199
+ },
200
+ async access(absolutePath) {
201
+ const sessionId = getSessionId();
202
+ if (sessionId === "") throw new Error("pi-acp acp-fs access: sessionId not yet bound");
203
+ await conn.readTextFile({
204
+ sessionId,
205
+ path: absolutePath
206
+ });
207
+ }
208
+ };
209
+ }
210
+ //#endregion
187
211
  //#region src/acp/auth.ts
188
212
  const AUTH_METHOD_ID = "pi_terminal_login";
189
213
  function buildAuthMethods(opts) {
@@ -251,7 +275,8 @@ function parseClientCapabilities(caps) {
251
275
  if (caps === void 0 || caps === null) return {
252
276
  terminalOutput: false,
253
277
  terminalAuth: false,
254
- gatewayAuth: false
278
+ gatewayAuth: false,
279
+ fsReadTextFile: false
255
280
  };
256
281
  const meta = caps._meta;
257
282
  const terminalOutput = typeof meta === "object" && meta !== null && meta["terminal_output"] === true;
@@ -264,10 +289,12 @@ function parseClientCapabilities(caps) {
264
289
  if (typeof authMeta === "object" && authMeta !== null && "gateway" in authMeta) gatewayAuth = authMeta["gateway"] === true;
265
290
  }
266
291
  }
292
+ const fsReadTextFile = caps.fs?.readTextFile === true;
267
293
  return {
268
294
  terminalOutput,
269
295
  terminalAuth,
270
- gatewayAuth
296
+ gatewayAuth,
297
+ fsReadTextFile
271
298
  };
272
299
  }
273
300
  //#endregion
@@ -849,6 +876,7 @@ var PiAcpSession = class {
849
876
  lastAssistantStopReason = null;
850
877
  lastEmit = Promise.resolve();
851
878
  unsubscribe;
879
+ cleanups;
852
880
  constructor(opts) {
853
881
  this.sessionId = opts.sessionId;
854
882
  this.cwd = opts.cwd;
@@ -856,11 +884,15 @@ var PiAcpSession = class {
856
884
  this.piSession = opts.piSession;
857
885
  this.conn = opts.conn;
858
886
  this.supportsTerminalOutput = opts.supportsTerminalOutput ?? false;
887
+ this.cleanups = opts.cleanups ?? [];
859
888
  this.unsubscribe = this.piSession.subscribe((ev) => this.handlePiEvent(ev));
860
889
  }
861
890
  dispose() {
862
891
  this.unsubscribe?.();
863
892
  this.piSession.dispose();
893
+ for (const cleanup of this.cleanups) try {
894
+ cleanup();
895
+ } catch {}
864
896
  }
865
897
  /**
866
898
  * Drop event subscription without disposing the underlying piSession.
@@ -1652,6 +1684,58 @@ function tryFromFile(path, source, diagnostics) {
1652
1684
  return null;
1653
1685
  }
1654
1686
  //#endregion
1687
+ //#region src/resources/modes.ts
1688
+ /**
1689
+ * Cwd-independence modes (PRD-002 §FR-5).
1690
+ *
1691
+ * | Mode | cwd handling | Tool target |
1692
+ * |-----------|--------------------------------------------|---------------|
1693
+ * | `local` | ACP params.cwd (must be absolute) | params.cwd |
1694
+ * | `overlay` | same as local — manifest aux roots compose | params.cwd |
1695
+ * | `none` | substitute ephemeral tmpdir | tmpdir |
1696
+ *
1697
+ * `local` and `overlay` are functionally identical in the current substrate
1698
+ * because `VirtualResourceLoader` always overlays manifest roots on top of
1699
+ * the implicit local root. The `overlay` keyword is retained for
1700
+ * forward-compatibility and operator clarity (declaring `mode: overlay`
1701
+ * documents the intent even if the runtime path is the same).
1702
+ *
1703
+ * `none` mints `mkdtemp(...)` under the OS tmpdir and returns a cleanup
1704
+ * thunk the caller must invoke at session close. The cleanup is
1705
+ * best-effort — never throws — so a session dispose path that runs it
1706
+ * after the directory has already been removed is safe.
1707
+ */
1708
+ const TMPDIR_PREFIX = "pi-acp-session-";
1709
+ function resolveMode(input) {
1710
+ const mode = input.manifest.mode;
1711
+ if (mode === "none") {
1712
+ const dir = mkdtempSync(join(tmpdir(), TMPDIR_PREFIX));
1713
+ let removed = false;
1714
+ const cleanup = () => {
1715
+ if (removed) return;
1716
+ removed = true;
1717
+ try {
1718
+ rmSync(dir, {
1719
+ recursive: true,
1720
+ force: true
1721
+ });
1722
+ } catch {}
1723
+ };
1724
+ return {
1725
+ mode,
1726
+ cwd: dir,
1727
+ cleanup,
1728
+ ephemeral: true
1729
+ };
1730
+ }
1731
+ return {
1732
+ mode,
1733
+ cwd: input.requestedCwd,
1734
+ cleanup: () => {},
1735
+ ephemeral: false
1736
+ };
1737
+ }
1738
+ //#endregion
1655
1739
  //#region src/resources/sources/http.ts
1656
1740
  const DEFAULT_CACHE_TTL_SECONDS = 300;
1657
1741
  const DEFAULT_TIMEOUT_MS$1 = 5e3;
@@ -1870,7 +1954,7 @@ var SshBackend = class {
1870
1954
  //#endregion
1871
1955
  //#region package.json
1872
1956
  var name = "@victor-software-house/pi-acp";
1873
- var version = "0.10.0";
1957
+ var version = "0.12.0";
1874
1958
  //#endregion
1875
1959
  //#region src/acp/agent.ts
1876
1960
  /** Builtin ACP slash commands handled directly by the adapter. */
@@ -1959,7 +2043,8 @@ var PiAcpAgent = class {
1959
2043
  clientCapabilities = {
1960
2044
  terminalOutput: false,
1961
2045
  terminalAuth: false,
1962
- gatewayAuth: false
2046
+ gatewayAuth: false,
2047
+ fsReadTextFile: false
1963
2048
  };
1964
2049
  daemonContext;
1965
2050
  /** Unique ID for this ACP connection. Used as the ownership key in the daemon SessionRegistry. */
@@ -2006,13 +2091,18 @@ var PiAcpAgent = class {
2006
2091
  cwd,
2007
2092
  sessionParams
2008
2093
  });
2094
+ const modeResult = resolveMode({
2095
+ manifest: loaded.manifest,
2096
+ requestedCwd: cwd
2097
+ });
2098
+ const effectiveCwd = modeResult.cwd;
2009
2099
  const diagnostics = [...loaded.diagnostics];
2010
2100
  const sources = [];
2011
2101
  for (const root of loaded.manifest.roots) {
2012
2102
  if (root.kind === "local") {
2013
2103
  sources.push(new LocalBackend({
2014
2104
  id: root.id,
2015
- cwd: root.paths.cwd ?? cwd,
2105
+ cwd: root.paths.cwd ?? effectiveCwd,
2016
2106
  agentDir: root.paths.agentDir ?? getAgentDir()
2017
2107
  }));
2018
2108
  continue;
@@ -2045,7 +2135,7 @@ var PiAcpAgent = class {
2045
2135
  diagnostics.push(diag);
2046
2136
  }
2047
2137
  if (!sources.some((s) => s.kind === "local")) sources.unshift(new LocalBackend({
2048
- cwd,
2138
+ cwd: effectiveCwd,
2049
2139
  agentDir: getAgentDir()
2050
2140
  }));
2051
2141
  const loader = new VirtualResourceLoader({
@@ -2057,7 +2147,48 @@ var PiAcpAgent = class {
2057
2147
  const where = d.path !== void 0 ? ` ${d.path}` : "";
2058
2148
  process.stderr.write(`pi-acp manifest [${d.source}${where}]: ${d.message}\n`);
2059
2149
  }
2060
- return loader;
2150
+ return {
2151
+ loader,
2152
+ modeResult
2153
+ };
2154
+ }
2155
+ /**
2156
+ * PRD-002 §FR-6 — `read` tool ACP-FS delegation overlay.
2157
+ *
2158
+ * When the client advertises `fs.readTextFile`, we override pi's
2159
+ * built-in `read` with a custom `read` tool that proxies to
2160
+ * `connection.fs.readTextFile`. The allowlist MUST include "read" so
2161
+ * pi's customTool registration loop (which filters by name) can
2162
+ * register the override; the override then shadows the builtin via
2163
+ * the tool-definition `Map.set` path inside AgentSession.
2164
+ *
2165
+ * The sessionId ref is mutated by the caller right after
2166
+ * `createAgentSession` returns, before any model turn — the tool
2167
+ * isn't invoked until prompt-time, so the late binding is safe.
2168
+ *
2169
+ * Returns `null` when the client doesn't advertise the capability;
2170
+ * callers then skip the overlay and pi's built-in `read` handles
2171
+ * everything locally.
2172
+ */
2173
+ buildAcpReadOverlay(cwd) {
2174
+ if (!this.clientCapabilities.fsReadTextFile) return null;
2175
+ const sessionIdRef = { current: "" };
2176
+ return {
2177
+ sessionIdRef,
2178
+ tools: [
2179
+ "read",
2180
+ "bash",
2181
+ "edit",
2182
+ "write",
2183
+ "grep",
2184
+ "find",
2185
+ "ls"
2186
+ ],
2187
+ customTools: [createReadToolDefinition(cwd, { operations: createAcpReadOperations({
2188
+ conn: this.conn,
2189
+ getSessionId: () => sessionIdRef.current
2190
+ }) })]
2191
+ };
2061
2192
  }
2062
2193
  async initialize(params) {
2063
2194
  const supportedVersion = 1;
@@ -2092,23 +2223,40 @@ var PiAcpAgent = class {
2092
2223
  };
2093
2224
  }
2094
2225
  async newSession(params) {
2095
- if (!isAbsolute(params.cwd)) throw RequestError.invalidParams(`cwd must be an absolute path: ${params.cwd}`);
2226
+ const { loader: resourceLoader, modeResult } = await this.buildResourceLoader(params.cwd, params).catch((e) => {
2227
+ const authErr = detectAuthError(e);
2228
+ if (authErr !== null) throw authErr;
2229
+ const msg = e instanceof Error ? e.message : String(e);
2230
+ throw RequestError.internalError({}, `Failed to load pi-acp manifest: ${msg}`);
2231
+ });
2232
+ const effectiveCwd = modeResult.cwd;
2233
+ if (modeResult.mode !== "none" && !isAbsolute(params.cwd)) {
2234
+ modeResult.cleanup();
2235
+ throw RequestError.invalidParams(`cwd must be an absolute path: ${params.cwd}`);
2236
+ }
2237
+ const acpReadOverlay = this.buildAcpReadOverlay(effectiveCwd);
2096
2238
  let result;
2097
2239
  try {
2098
- const resourceLoader = await this.buildResourceLoader(params.cwd, params);
2099
2240
  result = await createAgentSession({
2100
- cwd: params.cwd,
2101
- resourceLoader
2241
+ cwd: effectiveCwd,
2242
+ resourceLoader,
2243
+ ...acpReadOverlay ? {
2244
+ tools: acpReadOverlay.tools,
2245
+ customTools: acpReadOverlay.customTools
2246
+ } : {}
2102
2247
  });
2103
2248
  } catch (e) {
2249
+ modeResult.cleanup();
2104
2250
  const authErr = detectAuthError(e);
2105
2251
  if (authErr !== null) throw authErr;
2106
2252
  const msg = e instanceof Error ? e.message : String(e);
2107
2253
  throw RequestError.internalError({}, `Failed to create pi session: ${msg}`);
2108
2254
  }
2109
2255
  const piSession = result.session;
2256
+ if (acpReadOverlay !== null) acpReadOverlay.sessionIdRef.current = piSession.sessionManager.getSessionId();
2110
2257
  if (piSession.modelRegistry.getAvailable().length === 0) {
2111
2258
  piSession.dispose();
2259
+ modeResult.cleanup();
2112
2260
  throw RequestError.authRequired({ authMethods: buildAuthMethods() }, "Configure an API key or log in with an OAuth provider.");
2113
2261
  }
2114
2262
  const sessionId = piSession.sessionManager.getSessionId();
@@ -2116,11 +2264,12 @@ var PiAcpAgent = class {
2116
2264
  if (sessionFile !== void 0) this.sessionPaths.set(sessionId, sessionFile);
2117
2265
  const session = new PiAcpSession({
2118
2266
  sessionId,
2119
- cwd: params.cwd,
2267
+ cwd: effectiveCwd,
2120
2268
  mcpServers: params.mcpServers,
2121
2269
  piSession,
2122
2270
  conn: this.conn,
2123
- supportsTerminalOutput: this.clientCapabilities.terminalOutput
2271
+ supportsTerminalOutput: this.clientCapabilities.terminalOutput,
2272
+ cleanups: modeResult.ephemeral ? [modeResult.cleanup] : []
2124
2273
  });
2125
2274
  this.sessions.register(session);
2126
2275
  this.registerWithDaemon({
@@ -2366,14 +2515,19 @@ var PiAcpAgent = class {
2366
2515
  this.sessions.close(params.sessionId);
2367
2516
  const sessionFile = await this.resolveSessionFile(params.sessionId);
2368
2517
  if (sessionFile === null) throw RequestError.invalidParams(`Unknown sessionId: ${params.sessionId}`);
2518
+ const acpReadOverlay = this.buildAcpReadOverlay(params.cwd);
2369
2519
  let result;
2370
2520
  try {
2371
2521
  const sm = SessionManager.open(sessionFile);
2372
- const resourceLoader = await this.buildResourceLoader(params.cwd, params);
2522
+ const { loader: resourceLoader } = await this.buildResourceLoader(params.cwd, params);
2373
2523
  result = await createAgentSession({
2374
2524
  cwd: params.cwd,
2375
2525
  sessionManager: sm,
2376
- resourceLoader
2526
+ resourceLoader,
2527
+ ...acpReadOverlay ? {
2528
+ tools: acpReadOverlay.tools,
2529
+ customTools: acpReadOverlay.customTools
2530
+ } : {}
2377
2531
  });
2378
2532
  } catch (e) {
2379
2533
  const authErr = detectAuthError(e);
@@ -2382,6 +2536,7 @@ var PiAcpAgent = class {
2382
2536
  throw RequestError.internalError({}, `Failed to load pi session: ${msg}`);
2383
2537
  }
2384
2538
  const piSession = result.session;
2539
+ if (acpReadOverlay !== null) acpReadOverlay.sessionIdRef.current = piSession.sessionManager.getSessionId();
2385
2540
  const session = new PiAcpSession({
2386
2541
  sessionId: params.sessionId,
2387
2542
  cwd: params.cwd,
@@ -2466,14 +2621,19 @@ var PiAcpAgent = class {
2466
2621
  }
2467
2622
  const sessionFile = await this.resolveSessionFile(params.sessionId);
2468
2623
  if (sessionFile === null) throw RequestError.invalidParams(`Unknown sessionId: ${params.sessionId}`);
2624
+ const acpReadOverlay = this.buildAcpReadOverlay(params.cwd);
2469
2625
  let result;
2470
2626
  try {
2471
2627
  const sm = SessionManager.open(sessionFile);
2472
- const resourceLoader = await this.buildResourceLoader(params.cwd, params);
2628
+ const { loader: resourceLoader } = await this.buildResourceLoader(params.cwd, params);
2473
2629
  result = await createAgentSession({
2474
2630
  cwd: params.cwd,
2475
2631
  sessionManager: sm,
2476
- resourceLoader
2632
+ resourceLoader,
2633
+ ...acpReadOverlay ? {
2634
+ tools: acpReadOverlay.tools,
2635
+ customTools: acpReadOverlay.customTools
2636
+ } : {}
2477
2637
  });
2478
2638
  } catch (e) {
2479
2639
  const authErr = detectAuthError(e);
@@ -2482,6 +2642,7 @@ var PiAcpAgent = class {
2482
2642
  throw RequestError.internalError({}, `Failed to resume pi session: ${msg}`);
2483
2643
  }
2484
2644
  const piSession = result.session;
2645
+ if (acpReadOverlay !== null) acpReadOverlay.sessionIdRef.current = piSession.sessionManager.getSessionId();
2485
2646
  const session = new PiAcpSession({
2486
2647
  sessionId: params.sessionId,
2487
2648
  cwd: params.cwd,
@@ -2525,14 +2686,19 @@ var PiAcpAgent = class {
2525
2686
  if (!isAbsolute(params.cwd)) throw RequestError.invalidParams(`cwd must be an absolute path: ${params.cwd}`);
2526
2687
  const sourceFile = await this.resolveSessionFile(params.sessionId);
2527
2688
  if (sourceFile === null) throw RequestError.invalidParams(`Unknown sessionId: ${params.sessionId}`);
2689
+ const acpReadOverlay = this.buildAcpReadOverlay(params.cwd);
2528
2690
  let result;
2529
2691
  try {
2530
2692
  const sm = SessionManager.forkFrom(sourceFile, params.cwd);
2531
- const resourceLoader = await this.buildResourceLoader(params.cwd, params);
2693
+ const { loader: resourceLoader } = await this.buildResourceLoader(params.cwd, params);
2532
2694
  result = await createAgentSession({
2533
2695
  cwd: params.cwd,
2534
2696
  sessionManager: sm,
2535
- resourceLoader
2697
+ resourceLoader,
2698
+ ...acpReadOverlay ? {
2699
+ tools: acpReadOverlay.tools,
2700
+ customTools: acpReadOverlay.customTools
2701
+ } : {}
2536
2702
  });
2537
2703
  } catch (e) {
2538
2704
  const authErr = detectAuthError(e);
@@ -2542,6 +2708,7 @@ var PiAcpAgent = class {
2542
2708
  }
2543
2709
  const piSession = result.session;
2544
2710
  const newSessionId = piSession.sessionManager.getSessionId();
2711
+ if (acpReadOverlay !== null) acpReadOverlay.sessionIdRef.current = newSessionId;
2545
2712
  const newSessionFile = piSession.sessionManager.getSessionFile();
2546
2713
  if (newSessionFile !== void 0) this.sessionPaths.set(newSessionId, newSessionFile);
2547
2714
  const session = new PiAcpSession({
@@ -3160,4 +3327,4 @@ async function runDaemon() {
3160
3327
  //#endregion
3161
3328
  export { runDaemon };
3162
3329
 
3163
- //# sourceMappingURL=daemon-xclwSgis.mjs.map
3330
+ //# sourceMappingURL=daemon-D6QKWz5C.mjs.map