@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.
|
|
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
|
|
1887
|
-
*
|
|
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-
|
|
3253
|
+
//# sourceMappingURL=daemon-COFaIaTB.mjs.map
|