@victor-software-house/pi-acp 0.9.0 → 0.11.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.
@@ -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, 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
@@ -1652,6 +1679,121 @@ function tryFromFile(path, source, diagnostics) {
1652
1679
  return null;
1653
1680
  }
1654
1681
  //#endregion
1682
+ //#region src/resources/sources/http.ts
1683
+ const DEFAULT_CACHE_TTL_SECONDS = 300;
1684
+ const DEFAULT_TIMEOUT_MS$1 = 5e3;
1685
+ var HttpBackend = class {
1686
+ id;
1687
+ kind = "http";
1688
+ baseUrl;
1689
+ paths;
1690
+ cacheTtlMs;
1691
+ timeoutMs;
1692
+ fetchImpl;
1693
+ urlCache = /* @__PURE__ */ new Map();
1694
+ cache = null;
1695
+ constructor(opts) {
1696
+ if (!opts.baseUrl.startsWith("https://")) throw new Error(`pi-acp http source '${opts.id}': baseUrl must use https:// (got "${opts.baseUrl}")`);
1697
+ this.id = opts.id;
1698
+ this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
1699
+ this.paths = opts.paths ?? {};
1700
+ this.cacheTtlMs = (opts.cacheTtlSeconds ?? DEFAULT_CACHE_TTL_SECONDS) * 1e3;
1701
+ this.timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS$1;
1702
+ this.fetchImpl = opts.fetchImpl ?? globalThis.fetch.bind(globalThis);
1703
+ }
1704
+ async reload() {
1705
+ const diagnostics = [];
1706
+ for (const kind of [
1707
+ "skills",
1708
+ "prompts",
1709
+ "extensions"
1710
+ ]) if (this.paths[kind] !== void 0) diagnostics.push(this.unsupportedDiagnostic(kind));
1711
+ const list = this.paths.agentsFiles ?? [];
1712
+ const files = [];
1713
+ if (list.length > 0) {
1714
+ const results = await Promise.all(list.map((path) => this.fetchPath(path).then((content) => ({
1715
+ path,
1716
+ content,
1717
+ error: null
1718
+ }), (err) => ({
1719
+ path,
1720
+ content: null,
1721
+ error: err instanceof Error ? err.message : String(err)
1722
+ }))));
1723
+ for (const r of results) {
1724
+ if (r.content !== null) {
1725
+ files.push({
1726
+ path: this.qualifyPath(r.path),
1727
+ content: r.content
1728
+ });
1729
+ continue;
1730
+ }
1731
+ diagnostics.push({
1732
+ type: "warning",
1733
+ message: `pi-acp http source '${this.id}' (${this.baseUrl}): agentsFile '${r.path}' unreadable — ${r.error ?? "(unknown)"}`,
1734
+ path: r.path
1735
+ });
1736
+ }
1737
+ }
1738
+ this.cache = {
1739
+ files,
1740
+ diagnostics
1741
+ };
1742
+ }
1743
+ getAgentsFiles() {
1744
+ return this.cache?.files ?? [];
1745
+ }
1746
+ getSkills() {
1747
+ return {
1748
+ skills: [],
1749
+ diagnostics: this.cache?.diagnostics ?? []
1750
+ };
1751
+ }
1752
+ getPrompts() {
1753
+ return {
1754
+ prompts: [],
1755
+ diagnostics: []
1756
+ };
1757
+ }
1758
+ getSystemPrompt() {}
1759
+ getAppendSystemPrompt() {
1760
+ return [];
1761
+ }
1762
+ qualifyPath(path) {
1763
+ return `${this.baseUrl}/${path.replace(/^\/+/, "")}`;
1764
+ }
1765
+ unsupportedDiagnostic(kind) {
1766
+ return {
1767
+ type: "warning",
1768
+ message: `pi-acp http source '${this.id}' (${this.baseUrl}): ${kind} discovery over HTTP not yet implemented — declare individual files via paths.agentsFiles for now, or omit paths.${kind}.`
1769
+ };
1770
+ }
1771
+ async fetchPath(path) {
1772
+ const url = this.qualifyPath(path);
1773
+ const now = Date.now();
1774
+ const cached = this.urlCache.get(url);
1775
+ if (cached !== void 0 && cached.expiresAt > now) return cached.content;
1776
+ const controller = new AbortController();
1777
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
1778
+ let response;
1779
+ try {
1780
+ response = await this.fetchImpl(url, { signal: controller.signal });
1781
+ } catch (err) {
1782
+ if (err instanceof Error && err.name === "AbortError") throw new Error(`fetch timed out after ${this.timeoutMs}ms`);
1783
+ throw err;
1784
+ } finally {
1785
+ clearTimeout(timer);
1786
+ }
1787
+ if (!response.ok) throw new Error(`HTTP ${response.status} ${response.statusText || ""}`.trim());
1788
+ const content = await response.text();
1789
+ this.urlCache.set(url, {
1790
+ content,
1791
+ expiresAt: now + this.cacheTtlMs
1792
+ });
1793
+ return content;
1794
+ }
1795
+ };
1796
+ //#endregion
1655
1797
  //#region src/resources/sources/ssh.ts
1656
1798
  const DEFAULT_TIMEOUT_MS = 5e3;
1657
1799
  var SshBackend = class {
@@ -1755,7 +1897,7 @@ var SshBackend = class {
1755
1897
  //#endregion
1756
1898
  //#region package.json
1757
1899
  var name = "@victor-software-house/pi-acp";
1758
- var version = "0.9.0";
1900
+ var version = "0.11.0";
1759
1901
  //#endregion
1760
1902
  //#region src/acp/agent.ts
1761
1903
  /** Builtin ACP slash commands handled directly by the adapter. */
@@ -1844,7 +1986,8 @@ var PiAcpAgent = class {
1844
1986
  clientCapabilities = {
1845
1987
  terminalOutput: false,
1846
1988
  terminalAuth: false,
1847
- gatewayAuth: false
1989
+ gatewayAuth: false,
1990
+ fsReadTextFile: false
1848
1991
  };
1849
1992
  daemonContext;
1850
1993
  /** Unique ID for this ACP connection. Used as the ownership key in the daemon SessionRegistry. */
@@ -1883,8 +2026,8 @@ var PiAcpAgent = class {
1883
2026
  * least one LocalBackend is present for the extension / theme passthrough.
1884
2027
  *
1885
2028
  * Phase 5 materializes `kind: "local"`. Phase 6 adds `kind: "ssh"`.
1886
- * `http` and `acp-fs` still parse fine but surface as diagnostics until
1887
- * their backends land in subsequent phases.
2029
+ * Phase 7 adds `kind: "http"`. `acp-fs` still parses fine but surfaces as
2030
+ * a diagnostic until its backend lands in a subsequent phase.
1888
2031
  */
1889
2032
  async buildResourceLoader(cwd, sessionParams) {
1890
2033
  const loaded = await loadManifest({
@@ -1912,6 +2055,16 @@ var PiAcpAgent = class {
1912
2055
  sources.push(new SshBackend(sshOpts));
1913
2056
  continue;
1914
2057
  }
2058
+ if (root.kind === "http") {
2059
+ const httpOpts = {
2060
+ id: root.id,
2061
+ baseUrl: root.baseUrl,
2062
+ paths: root.paths
2063
+ };
2064
+ if (root.cache !== void 0) httpOpts.cacheTtlSeconds = root.cache.ttl;
2065
+ sources.push(new HttpBackend(httpOpts));
2066
+ continue;
2067
+ }
1915
2068
  const diag = {
1916
2069
  source: loaded.source,
1917
2070
  message: `root "${root.id}" kind="${root.kind}" not yet supported in this build (skipped)`
@@ -1934,6 +2087,44 @@ var PiAcpAgent = class {
1934
2087
  }
1935
2088
  return loader;
1936
2089
  }
2090
+ /**
2091
+ * PRD-002 §FR-6 — `read` tool ACP-FS delegation overlay.
2092
+ *
2093
+ * When the client advertises `fs.readTextFile`, we override pi's
2094
+ * built-in `read` with a custom `read` tool that proxies to
2095
+ * `connection.fs.readTextFile`. The allowlist MUST include "read" so
2096
+ * pi's customTool registration loop (which filters by name) can
2097
+ * register the override; the override then shadows the builtin via
2098
+ * the tool-definition `Map.set` path inside AgentSession.
2099
+ *
2100
+ * The sessionId ref is mutated by the caller right after
2101
+ * `createAgentSession` returns, before any model turn — the tool
2102
+ * isn't invoked until prompt-time, so the late binding is safe.
2103
+ *
2104
+ * Returns `null` when the client doesn't advertise the capability;
2105
+ * callers then skip the overlay and pi's built-in `read` handles
2106
+ * everything locally.
2107
+ */
2108
+ buildAcpReadOverlay(cwd) {
2109
+ if (!this.clientCapabilities.fsReadTextFile) return null;
2110
+ const sessionIdRef = { current: "" };
2111
+ return {
2112
+ sessionIdRef,
2113
+ tools: [
2114
+ "read",
2115
+ "bash",
2116
+ "edit",
2117
+ "write",
2118
+ "grep",
2119
+ "find",
2120
+ "ls"
2121
+ ],
2122
+ customTools: [createReadToolDefinition(cwd, { operations: createAcpReadOperations({
2123
+ conn: this.conn,
2124
+ getSessionId: () => sessionIdRef.current
2125
+ }) })]
2126
+ };
2127
+ }
1937
2128
  async initialize(params) {
1938
2129
  const supportedVersion = 1;
1939
2130
  const requested = params.protocolVersion;
@@ -1968,12 +2159,17 @@ var PiAcpAgent = class {
1968
2159
  }
1969
2160
  async newSession(params) {
1970
2161
  if (!isAbsolute(params.cwd)) throw RequestError.invalidParams(`cwd must be an absolute path: ${params.cwd}`);
2162
+ const acpReadOverlay = this.buildAcpReadOverlay(params.cwd);
1971
2163
  let result;
1972
2164
  try {
1973
2165
  const resourceLoader = await this.buildResourceLoader(params.cwd, params);
1974
2166
  result = await createAgentSession({
1975
2167
  cwd: params.cwd,
1976
- resourceLoader
2168
+ resourceLoader,
2169
+ ...acpReadOverlay ? {
2170
+ tools: acpReadOverlay.tools,
2171
+ customTools: acpReadOverlay.customTools
2172
+ } : {}
1977
2173
  });
1978
2174
  } catch (e) {
1979
2175
  const authErr = detectAuthError(e);
@@ -1982,6 +2178,7 @@ var PiAcpAgent = class {
1982
2178
  throw RequestError.internalError({}, `Failed to create pi session: ${msg}`);
1983
2179
  }
1984
2180
  const piSession = result.session;
2181
+ if (acpReadOverlay !== null) acpReadOverlay.sessionIdRef.current = piSession.sessionManager.getSessionId();
1985
2182
  if (piSession.modelRegistry.getAvailable().length === 0) {
1986
2183
  piSession.dispose();
1987
2184
  throw RequestError.authRequired({ authMethods: buildAuthMethods() }, "Configure an API key or log in with an OAuth provider.");
@@ -2241,6 +2438,7 @@ var PiAcpAgent = class {
2241
2438
  this.sessions.close(params.sessionId);
2242
2439
  const sessionFile = await this.resolveSessionFile(params.sessionId);
2243
2440
  if (sessionFile === null) throw RequestError.invalidParams(`Unknown sessionId: ${params.sessionId}`);
2441
+ const acpReadOverlay = this.buildAcpReadOverlay(params.cwd);
2244
2442
  let result;
2245
2443
  try {
2246
2444
  const sm = SessionManager.open(sessionFile);
@@ -2248,7 +2446,11 @@ var PiAcpAgent = class {
2248
2446
  result = await createAgentSession({
2249
2447
  cwd: params.cwd,
2250
2448
  sessionManager: sm,
2251
- resourceLoader
2449
+ resourceLoader,
2450
+ ...acpReadOverlay ? {
2451
+ tools: acpReadOverlay.tools,
2452
+ customTools: acpReadOverlay.customTools
2453
+ } : {}
2252
2454
  });
2253
2455
  } catch (e) {
2254
2456
  const authErr = detectAuthError(e);
@@ -2257,6 +2459,7 @@ var PiAcpAgent = class {
2257
2459
  throw RequestError.internalError({}, `Failed to load pi session: ${msg}`);
2258
2460
  }
2259
2461
  const piSession = result.session;
2462
+ if (acpReadOverlay !== null) acpReadOverlay.sessionIdRef.current = piSession.sessionManager.getSessionId();
2260
2463
  const session = new PiAcpSession({
2261
2464
  sessionId: params.sessionId,
2262
2465
  cwd: params.cwd,
@@ -2341,6 +2544,7 @@ var PiAcpAgent = class {
2341
2544
  }
2342
2545
  const sessionFile = await this.resolveSessionFile(params.sessionId);
2343
2546
  if (sessionFile === null) throw RequestError.invalidParams(`Unknown sessionId: ${params.sessionId}`);
2547
+ const acpReadOverlay = this.buildAcpReadOverlay(params.cwd);
2344
2548
  let result;
2345
2549
  try {
2346
2550
  const sm = SessionManager.open(sessionFile);
@@ -2348,7 +2552,11 @@ var PiAcpAgent = class {
2348
2552
  result = await createAgentSession({
2349
2553
  cwd: params.cwd,
2350
2554
  sessionManager: sm,
2351
- resourceLoader
2555
+ resourceLoader,
2556
+ ...acpReadOverlay ? {
2557
+ tools: acpReadOverlay.tools,
2558
+ customTools: acpReadOverlay.customTools
2559
+ } : {}
2352
2560
  });
2353
2561
  } catch (e) {
2354
2562
  const authErr = detectAuthError(e);
@@ -2357,6 +2565,7 @@ var PiAcpAgent = class {
2357
2565
  throw RequestError.internalError({}, `Failed to resume pi session: ${msg}`);
2358
2566
  }
2359
2567
  const piSession = result.session;
2568
+ if (acpReadOverlay !== null) acpReadOverlay.sessionIdRef.current = piSession.sessionManager.getSessionId();
2360
2569
  const session = new PiAcpSession({
2361
2570
  sessionId: params.sessionId,
2362
2571
  cwd: params.cwd,
@@ -2400,6 +2609,7 @@ var PiAcpAgent = class {
2400
2609
  if (!isAbsolute(params.cwd)) throw RequestError.invalidParams(`cwd must be an absolute path: ${params.cwd}`);
2401
2610
  const sourceFile = await this.resolveSessionFile(params.sessionId);
2402
2611
  if (sourceFile === null) throw RequestError.invalidParams(`Unknown sessionId: ${params.sessionId}`);
2612
+ const acpReadOverlay = this.buildAcpReadOverlay(params.cwd);
2403
2613
  let result;
2404
2614
  try {
2405
2615
  const sm = SessionManager.forkFrom(sourceFile, params.cwd);
@@ -2407,7 +2617,11 @@ var PiAcpAgent = class {
2407
2617
  result = await createAgentSession({
2408
2618
  cwd: params.cwd,
2409
2619
  sessionManager: sm,
2410
- resourceLoader
2620
+ resourceLoader,
2621
+ ...acpReadOverlay ? {
2622
+ tools: acpReadOverlay.tools,
2623
+ customTools: acpReadOverlay.customTools
2624
+ } : {}
2411
2625
  });
2412
2626
  } catch (e) {
2413
2627
  const authErr = detectAuthError(e);
@@ -2417,6 +2631,7 @@ var PiAcpAgent = class {
2417
2631
  }
2418
2632
  const piSession = result.session;
2419
2633
  const newSessionId = piSession.sessionManager.getSessionId();
2634
+ if (acpReadOverlay !== null) acpReadOverlay.sessionIdRef.current = newSessionId;
2420
2635
  const newSessionFile = piSession.sessionManager.getSessionFile();
2421
2636
  if (newSessionFile !== void 0) this.sessionPaths.set(newSessionId, newSessionFile);
2422
2637
  const session = new PiAcpSession({
@@ -3035,4 +3250,4 @@ async function runDaemon() {
3035
3250
  //#endregion
3036
3251
  export { runDaemon };
3037
3252
 
3038
- //# sourceMappingURL=daemon-tHPrf3qs.mjs.map
3253
+ //# sourceMappingURL=daemon-COFaIaTB.mjs.map