@victor-software-house/pi-acp 0.8.0 → 0.10.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.
- package/dist/{auto-spawn-CQ_aaNZA.mjs → client-BjK2BnaD.mjs} +34 -4
- package/dist/client-BjK2BnaD.mjs.map +1 -0
- package/dist/{serve-DmuHYqF-.mjs → daemon-xclwSgis.mjs} +926 -26
- package/dist/daemon-xclwSgis.mjs.map +1 -0
- package/dist/index.mjs +16 -25
- package/dist/index.mjs.map +1 -1
- package/dist/operator-AtBT_SZT.mjs +52 -0
- package/dist/operator-AtBT_SZT.mjs.map +1 -0
- package/dist/pi-package-DFOfbtij.mjs +3 -0
- package/dist/pi-package-aHs6rWNo.mjs +29 -0
- package/dist/pi-package-aHs6rWNo.mjs.map +1 -0
- package/dist/{socket-BUNWxnAN.mjs → socket-wvV053VI.mjs} +28 -25
- package/dist/socket-wvV053VI.mjs.map +1 -0
- package/package.json +5 -3
- package/dist/auto-spawn-CQ_aaNZA.mjs.map +0 -1
- package/dist/client-P4T6wITz.mjs +0 -35
- package/dist/client-P4T6wITz.mjs.map +0 -1
- package/dist/daemon-D76_nP59.mjs +0 -338
- package/dist/daemon-D76_nP59.mjs.map +0 -1
- package/dist/in-process-Byo-O30j.mjs +0 -31
- package/dist/in-process-Byo-O30j.mjs.map +0 -1
- package/dist/operator-DURoHk8w.mjs +0 -85
- package/dist/operator-DURoHk8w.mjs.map +0 -1
- package/dist/serve-DmuHYqF-.mjs.map +0 -1
- package/dist/socket-BUNWxnAN.mjs.map +0 -1
|
@@ -1,12 +1,189 @@
|
|
|
1
|
-
#!/usr/bin/env
|
|
1
|
+
#!/usr/bin/env bun
|
|
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
|
+
import { t as piChangelogPath } from "./pi-package-aHs6rWNo.mjs";
|
|
4
|
+
import { createServer } from "node:net";
|
|
5
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
6
|
import { homedir } from "node:os";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
7
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
8
|
+
import { Hono } from "hono";
|
|
6
9
|
import { AgentSideConnection, RequestError, ndJsonStream } from "@agentclientprotocol/sdk";
|
|
7
10
|
import { randomUUID } from "node:crypto";
|
|
8
|
-
import { SessionManager, createAgentSession } from "@earendil-works/pi-coding-agent";
|
|
11
|
+
import { DefaultResourceLoader, SessionManager, createAgentSession, getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
9
12
|
import * as z from "zod";
|
|
13
|
+
import { parse } from "yaml";
|
|
14
|
+
import { $ } from "bun";
|
|
15
|
+
//#region src/daemon/session-registry.ts
|
|
16
|
+
function createSessionRegistry() {
|
|
17
|
+
const map = /* @__PURE__ */ new Map();
|
|
18
|
+
return {
|
|
19
|
+
register(input) {
|
|
20
|
+
const entry = {
|
|
21
|
+
sessionId: input.sessionId,
|
|
22
|
+
piSession: input.piSession,
|
|
23
|
+
ownerConnectionId: input.ownerConnectionId,
|
|
24
|
+
alsoHeldBy: /* @__PURE__ */ new Set(),
|
|
25
|
+
cwd: input.cwd,
|
|
26
|
+
sessionFile: input.sessionFile,
|
|
27
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
28
|
+
};
|
|
29
|
+
map.set(input.sessionId, entry);
|
|
30
|
+
},
|
|
31
|
+
attach(sessionId, connectionId) {
|
|
32
|
+
const entry = map.get(sessionId);
|
|
33
|
+
if (entry === void 0) return void 0;
|
|
34
|
+
if (entry.ownerConnectionId !== connectionId) {
|
|
35
|
+
entry.alsoHeldBy.add(connectionId);
|
|
36
|
+
entry.updatedAt = /* @__PURE__ */ new Date();
|
|
37
|
+
}
|
|
38
|
+
return entry;
|
|
39
|
+
},
|
|
40
|
+
release(sessionId, connectionId) {
|
|
41
|
+
const entry = map.get(sessionId);
|
|
42
|
+
if (entry === void 0) return { kind: "unknown" };
|
|
43
|
+
if (entry.alsoHeldBy.delete(connectionId)) {
|
|
44
|
+
entry.updatedAt = /* @__PURE__ */ new Date();
|
|
45
|
+
if (entry.ownerConnectionId === connectionId && entry.alsoHeldBy.size === 0) {
|
|
46
|
+
map.delete(sessionId);
|
|
47
|
+
return {
|
|
48
|
+
kind: "disposed",
|
|
49
|
+
entry
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
kind: "still-held",
|
|
54
|
+
entry
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
if (entry.ownerConnectionId === connectionId) {
|
|
58
|
+
if (entry.alsoHeldBy.size > 0) {
|
|
59
|
+
const next = entry.alsoHeldBy.values().next().value;
|
|
60
|
+
if (next !== void 0) {
|
|
61
|
+
entry.alsoHeldBy.delete(next);
|
|
62
|
+
entry.ownerConnectionId = next;
|
|
63
|
+
entry.updatedAt = /* @__PURE__ */ new Date();
|
|
64
|
+
return {
|
|
65
|
+
kind: "still-held",
|
|
66
|
+
entry
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
map.delete(sessionId);
|
|
71
|
+
return {
|
|
72
|
+
kind: "disposed",
|
|
73
|
+
entry
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
kind: "still-held",
|
|
78
|
+
entry
|
|
79
|
+
};
|
|
80
|
+
},
|
|
81
|
+
get(sessionId) {
|
|
82
|
+
return map.get(sessionId);
|
|
83
|
+
},
|
|
84
|
+
listAll() {
|
|
85
|
+
return Array.from(map.values());
|
|
86
|
+
},
|
|
87
|
+
listOwnedBy(connectionId) {
|
|
88
|
+
return Array.from(map.values()).filter((e) => e.ownerConnectionId === connectionId || e.alsoHeldBy.has(connectionId));
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
//#endregion
|
|
93
|
+
//#region src/daemon/context.ts
|
|
94
|
+
/**
|
|
95
|
+
* Daemon-level shared state injected into per-connection PiAcpAgent instances.
|
|
96
|
+
*
|
|
97
|
+
* Phase 1 landed the interface + stub IdleTracker.
|
|
98
|
+
* Phase 2 wires the real SessionRegistry.
|
|
99
|
+
* Phase 3 will replace IdleTracker.
|
|
100
|
+
*/
|
|
101
|
+
function createNoopIdleTracker() {
|
|
102
|
+
return {
|
|
103
|
+
bump() {},
|
|
104
|
+
dispose() {}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function createDaemonContext() {
|
|
108
|
+
return {
|
|
109
|
+
sessionRegistry: createSessionRegistry(),
|
|
110
|
+
idleTracker: createNoopIdleTracker()
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
//#endregion
|
|
114
|
+
//#region src/daemon/control.ts
|
|
115
|
+
function buildControlApp(control) {
|
|
116
|
+
const app = new Hono();
|
|
117
|
+
app.get("/status", (c) => c.json({
|
|
118
|
+
uptimeSeconds: Math.round((Date.now() - control.startedAt) / 1e3),
|
|
119
|
+
connections: control.activeConnections(),
|
|
120
|
+
sessions: control.ctx.sessionRegistry.listAll().length,
|
|
121
|
+
pid: control.pid,
|
|
122
|
+
version: control.version
|
|
123
|
+
}));
|
|
124
|
+
app.post("/shutdown", (c) => {
|
|
125
|
+
setImmediate(control.onShutdown);
|
|
126
|
+
return c.json({ ok: true });
|
|
127
|
+
});
|
|
128
|
+
app.get("/sessions", (c) => c.json({ sessions: control.ctx.sessionRegistry.listAll().map((entry) => ({
|
|
129
|
+
sessionId: entry.sessionId,
|
|
130
|
+
cwd: entry.cwd,
|
|
131
|
+
owner: entry.ownerConnectionId,
|
|
132
|
+
alsoHeldBy: [...entry.alsoHeldBy],
|
|
133
|
+
updatedAt: entry.updatedAt
|
|
134
|
+
})) }));
|
|
135
|
+
return app;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Bind the control app to a Unix domain socket. Uses Bun.serve's `unix` option.
|
|
139
|
+
*/
|
|
140
|
+
function serveControl(app, socketPath) {
|
|
141
|
+
const server = Bun.serve({
|
|
142
|
+
unix: socketPath,
|
|
143
|
+
fetch: app.fetch
|
|
144
|
+
});
|
|
145
|
+
return { stop() {
|
|
146
|
+
try {
|
|
147
|
+
server.stop(true);
|
|
148
|
+
} catch {}
|
|
149
|
+
} };
|
|
150
|
+
}
|
|
151
|
+
//#endregion
|
|
152
|
+
//#region src/daemon/idle.ts
|
|
153
|
+
const DEFAULT_IDLE_SECONDS = 600;
|
|
154
|
+
function createIdleTracker(opts) {
|
|
155
|
+
let active = 0;
|
|
156
|
+
let timer = null;
|
|
157
|
+
const startTimer = () => {
|
|
158
|
+
if (timer !== null) return;
|
|
159
|
+
timer = setTimeout(opts.onIdle, opts.idleMs);
|
|
160
|
+
timer.unref?.();
|
|
161
|
+
};
|
|
162
|
+
const stopTimer = () => {
|
|
163
|
+
if (timer === null) return;
|
|
164
|
+
clearTimeout(timer);
|
|
165
|
+
timer = null;
|
|
166
|
+
};
|
|
167
|
+
startTimer();
|
|
168
|
+
return {
|
|
169
|
+
bump(delta) {
|
|
170
|
+
active = Math.max(0, active + delta);
|
|
171
|
+
if (active === 0) startTimer();
|
|
172
|
+
else stopTimer();
|
|
173
|
+
},
|
|
174
|
+
dispose() {
|
|
175
|
+
stopTimer();
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
function resolveIdleMs() {
|
|
180
|
+
const raw = process.env["PI_ACP_DAEMON_IDLE_SECONDS"];
|
|
181
|
+
if (raw === void 0 || raw === "") return DEFAULT_IDLE_SECONDS * 1e3;
|
|
182
|
+
const n = Number.parseInt(raw, 10);
|
|
183
|
+
if (!Number.isFinite(n) || n <= 0) return DEFAULT_IDLE_SECONDS * 1e3;
|
|
184
|
+
return n * 1e3;
|
|
185
|
+
}
|
|
186
|
+
//#endregion
|
|
10
187
|
//#region src/acp/auth.ts
|
|
11
188
|
const AUTH_METHOD_ID = "pi_terminal_login";
|
|
12
189
|
function buildAuthMethods(opts) {
|
|
@@ -1136,9 +1313,564 @@ function acpPromptToPiMessage(blocks) {
|
|
|
1136
1313
|
};
|
|
1137
1314
|
}
|
|
1138
1315
|
//#endregion
|
|
1316
|
+
//#region src/resources/sources/local.ts
|
|
1317
|
+
/**
|
|
1318
|
+
* LocalBackend: wraps pi's DefaultResourceLoader for one (cwd, agentDir) root.
|
|
1319
|
+
* Phase 4 skeleton — manifest support (multiple local roots) lands in Phase 5.
|
|
1320
|
+
*/
|
|
1321
|
+
var LocalBackend = class {
|
|
1322
|
+
id;
|
|
1323
|
+
kind = "local";
|
|
1324
|
+
loader;
|
|
1325
|
+
constructor(opts) {
|
|
1326
|
+
this.id = opts.id ?? "local";
|
|
1327
|
+
this.loader = new DefaultResourceLoader({
|
|
1328
|
+
cwd: opts.cwd,
|
|
1329
|
+
agentDir: opts.agentDir
|
|
1330
|
+
});
|
|
1331
|
+
}
|
|
1332
|
+
async reload() {
|
|
1333
|
+
await this.loader.reload();
|
|
1334
|
+
}
|
|
1335
|
+
getAgentsFiles() {
|
|
1336
|
+
return this.loader.getAgentsFiles().agentsFiles;
|
|
1337
|
+
}
|
|
1338
|
+
getSkills() {
|
|
1339
|
+
return this.loader.getSkills();
|
|
1340
|
+
}
|
|
1341
|
+
getPrompts() {
|
|
1342
|
+
return this.loader.getPrompts();
|
|
1343
|
+
}
|
|
1344
|
+
getExtensions() {
|
|
1345
|
+
return this.loader.getExtensions();
|
|
1346
|
+
}
|
|
1347
|
+
getSystemPrompt() {
|
|
1348
|
+
return this.loader.getSystemPrompt();
|
|
1349
|
+
}
|
|
1350
|
+
getAppendSystemPrompt() {
|
|
1351
|
+
return this.loader.getAppendSystemPrompt();
|
|
1352
|
+
}
|
|
1353
|
+
/**
|
|
1354
|
+
* Expose the wrapped DefaultResourceLoader for VirtualResourceLoader's
|
|
1355
|
+
* extension/theme passthrough. Other backends don't expose this.
|
|
1356
|
+
*/
|
|
1357
|
+
inner() {
|
|
1358
|
+
return this.loader;
|
|
1359
|
+
}
|
|
1360
|
+
};
|
|
1361
|
+
//#endregion
|
|
1362
|
+
//#region src/resources/loader.ts
|
|
1363
|
+
var VirtualResourceLoader = class {
|
|
1364
|
+
sources;
|
|
1365
|
+
mergeStrategy;
|
|
1366
|
+
primary;
|
|
1367
|
+
constructor(opts) {
|
|
1368
|
+
if (opts.sources.length === 0) throw new Error("VirtualResourceLoader requires at least one source");
|
|
1369
|
+
this.sources = opts.sources;
|
|
1370
|
+
this.mergeStrategy = opts.mergeStrategy ?? "append";
|
|
1371
|
+
const primary = resolvePrimary(opts.sources, opts.primarySourceId);
|
|
1372
|
+
this.primary = primary;
|
|
1373
|
+
}
|
|
1374
|
+
async reload() {
|
|
1375
|
+
await Promise.all(this.sources.map((s) => s.reload()));
|
|
1376
|
+
}
|
|
1377
|
+
getAgentsFiles() {
|
|
1378
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1379
|
+
const merged = [];
|
|
1380
|
+
for (const source of this.sources) for (const file of source.getAgentsFiles()) {
|
|
1381
|
+
if (seen.has(file.path)) continue;
|
|
1382
|
+
seen.add(file.path);
|
|
1383
|
+
merged.push(file);
|
|
1384
|
+
}
|
|
1385
|
+
return { agentsFiles: merged };
|
|
1386
|
+
}
|
|
1387
|
+
getSkills() {
|
|
1388
|
+
const merge = createMerger(this.mergeStrategy, (s) => s.name);
|
|
1389
|
+
const diagnostics = [];
|
|
1390
|
+
for (const source of this.sources) {
|
|
1391
|
+
const result = source.getSkills();
|
|
1392
|
+
merge.absorb(result.skills);
|
|
1393
|
+
diagnostics.push(...result.diagnostics);
|
|
1394
|
+
}
|
|
1395
|
+
return {
|
|
1396
|
+
skills: merge.list(),
|
|
1397
|
+
diagnostics
|
|
1398
|
+
};
|
|
1399
|
+
}
|
|
1400
|
+
getPrompts() {
|
|
1401
|
+
const merge = createMerger(this.mergeStrategy, (p) => p.name);
|
|
1402
|
+
const diagnostics = [];
|
|
1403
|
+
for (const source of this.sources) {
|
|
1404
|
+
const result = source.getPrompts();
|
|
1405
|
+
merge.absorb(result.prompts);
|
|
1406
|
+
diagnostics.push(...result.diagnostics);
|
|
1407
|
+
}
|
|
1408
|
+
return {
|
|
1409
|
+
prompts: merge.list(),
|
|
1410
|
+
diagnostics
|
|
1411
|
+
};
|
|
1412
|
+
}
|
|
1413
|
+
getThemes() {
|
|
1414
|
+
return this.primary.inner().getThemes();
|
|
1415
|
+
}
|
|
1416
|
+
getExtensions() {
|
|
1417
|
+
return this.primary.getExtensions();
|
|
1418
|
+
}
|
|
1419
|
+
getSystemPrompt() {
|
|
1420
|
+
for (const source of this.sources) {
|
|
1421
|
+
const sp = source.getSystemPrompt();
|
|
1422
|
+
if (sp !== void 0) return sp;
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
getAppendSystemPrompt() {
|
|
1426
|
+
const merged = [];
|
|
1427
|
+
for (const source of this.sources) merged.push(...source.getAppendSystemPrompt());
|
|
1428
|
+
return merged;
|
|
1429
|
+
}
|
|
1430
|
+
extendResources(paths) {
|
|
1431
|
+
this.primary.inner().extendResources(paths);
|
|
1432
|
+
}
|
|
1433
|
+
/** Returns the active source list. Useful for diagnostics. */
|
|
1434
|
+
listSources() {
|
|
1435
|
+
return [...this.sources];
|
|
1436
|
+
}
|
|
1437
|
+
};
|
|
1438
|
+
function resolvePrimary(sources, preferredId) {
|
|
1439
|
+
if (preferredId !== void 0) {
|
|
1440
|
+
const found = sources.find((s) => s.id === preferredId);
|
|
1441
|
+
if (found === void 0) throw new Error(`VirtualResourceLoader: primarySourceId "${preferredId}" not in sources`);
|
|
1442
|
+
if (!(found instanceof LocalBackend)) throw new Error(`VirtualResourceLoader: primary source "${preferredId}" must be a LocalBackend`);
|
|
1443
|
+
return found;
|
|
1444
|
+
}
|
|
1445
|
+
const firstLocal = sources.find((s) => s instanceof LocalBackend);
|
|
1446
|
+
if (firstLocal === void 0) throw new Error("VirtualResourceLoader: at least one LocalBackend is required (for extensions + themes)");
|
|
1447
|
+
return firstLocal;
|
|
1448
|
+
}
|
|
1449
|
+
function createMerger(strategy, key) {
|
|
1450
|
+
if (strategy === "append") {
|
|
1451
|
+
const out = [];
|
|
1452
|
+
return {
|
|
1453
|
+
absorb(items) {
|
|
1454
|
+
out.push(...items);
|
|
1455
|
+
},
|
|
1456
|
+
list() {
|
|
1457
|
+
return out;
|
|
1458
|
+
}
|
|
1459
|
+
};
|
|
1460
|
+
}
|
|
1461
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
1462
|
+
return {
|
|
1463
|
+
absorb(items) {
|
|
1464
|
+
for (const item of items) byKey.set(key(item), item);
|
|
1465
|
+
},
|
|
1466
|
+
list() {
|
|
1467
|
+
return Array.from(byKey.values());
|
|
1468
|
+
}
|
|
1469
|
+
};
|
|
1470
|
+
}
|
|
1471
|
+
//#endregion
|
|
1472
|
+
//#region src/resources/manifest.schema.ts
|
|
1473
|
+
/**
|
|
1474
|
+
* Zod schema for the `.pi-acp.yaml` resource composition manifest (ADR-0008).
|
|
1475
|
+
*
|
|
1476
|
+
* Backend kinds: `local`, `ssh`, `http`, `acp-fs`. Phase 5 ships parsing and
|
|
1477
|
+
* validation for all four; only `local` is honored by the loader until the
|
|
1478
|
+
* remote-backend phases land — unknown kinds parse fine and surface as a
|
|
1479
|
+
* diagnostic at load time.
|
|
1480
|
+
*/
|
|
1481
|
+
const IdSchema = z.string().trim().min(1, "id is required");
|
|
1482
|
+
const LocalPathsSchema = z.object({
|
|
1483
|
+
cwd: z.string().trim().optional(),
|
|
1484
|
+
agentDir: z.string().trim().optional()
|
|
1485
|
+
}).strict();
|
|
1486
|
+
const RemotePathsSchema = z.object({
|
|
1487
|
+
skills: z.string().trim().optional(),
|
|
1488
|
+
prompts: z.string().trim().optional(),
|
|
1489
|
+
agentsFiles: z.array(z.string().trim()).optional(),
|
|
1490
|
+
extensions: z.string().trim().optional()
|
|
1491
|
+
}).strict();
|
|
1492
|
+
const LocalRootSchema = z.object({
|
|
1493
|
+
id: IdSchema,
|
|
1494
|
+
kind: z.literal("local"),
|
|
1495
|
+
paths: LocalPathsSchema.default({})
|
|
1496
|
+
}).strict();
|
|
1497
|
+
const SshRootSchema = z.object({
|
|
1498
|
+
id: IdSchema,
|
|
1499
|
+
kind: z.literal("ssh"),
|
|
1500
|
+
host: z.string().trim().min(1),
|
|
1501
|
+
user: z.string().trim().optional(),
|
|
1502
|
+
paths: RemotePathsSchema.default({})
|
|
1503
|
+
}).strict();
|
|
1504
|
+
const HttpRootSchema = z.object({
|
|
1505
|
+
id: IdSchema,
|
|
1506
|
+
kind: z.literal("http"),
|
|
1507
|
+
baseUrl: z.url().refine((u) => u.startsWith("https://"), { error: "baseUrl must use https://" }),
|
|
1508
|
+
cache: z.object({ ttl: z.int().nonnegative() }).strict().optional(),
|
|
1509
|
+
paths: RemotePathsSchema.default({})
|
|
1510
|
+
}).strict();
|
|
1511
|
+
const AcpFsRootSchema = z.object({
|
|
1512
|
+
id: IdSchema,
|
|
1513
|
+
kind: z.literal("acp-fs"),
|
|
1514
|
+
paths: RemotePathsSchema.default({})
|
|
1515
|
+
}).strict();
|
|
1516
|
+
const RootSchema = z.discriminatedUnion("kind", [
|
|
1517
|
+
LocalRootSchema,
|
|
1518
|
+
SshRootSchema,
|
|
1519
|
+
HttpRootSchema,
|
|
1520
|
+
AcpFsRootSchema
|
|
1521
|
+
]);
|
|
1522
|
+
const AutoImportSchema = z.object({
|
|
1523
|
+
source: IdSchema,
|
|
1524
|
+
paths: z.array(z.string().trim()).min(1)
|
|
1525
|
+
}).strict();
|
|
1526
|
+
const ManifestSchema = z.object({
|
|
1527
|
+
version: z.literal(1),
|
|
1528
|
+
mode: z.enum([
|
|
1529
|
+
"local",
|
|
1530
|
+
"overlay",
|
|
1531
|
+
"none"
|
|
1532
|
+
]).default("local"),
|
|
1533
|
+
roots: z.array(RootSchema).default([]),
|
|
1534
|
+
mergeStrategy: z.enum(["append", "override-by-name"]).default("append"),
|
|
1535
|
+
autoImport: z.array(AutoImportSchema).optional(),
|
|
1536
|
+
diagnostics: z.boolean().default(false)
|
|
1537
|
+
}).strict();
|
|
1538
|
+
const DEFAULT_MANIFEST = {
|
|
1539
|
+
version: 1,
|
|
1540
|
+
mode: "local",
|
|
1541
|
+
roots: [],
|
|
1542
|
+
mergeStrategy: "append",
|
|
1543
|
+
diagnostics: false
|
|
1544
|
+
};
|
|
1545
|
+
//#endregion
|
|
1546
|
+
//#region src/resources/manifest.ts
|
|
1547
|
+
/**
|
|
1548
|
+
* Cascade resolver for the `.pi-acp.yaml` resource composition manifest
|
|
1549
|
+
* (ADR-0008, PRD-002 §FR-3).
|
|
1550
|
+
*
|
|
1551
|
+
* Precedence (highest first):
|
|
1552
|
+
* 1. ACP session params: `params._meta.piAcp.manifest`
|
|
1553
|
+
* — either an inline manifest object or a string path to a YAML file
|
|
1554
|
+
* 2. Project-level: `<cwd>/.pi-acp.yaml`
|
|
1555
|
+
* 3. User-global: `~/.pi-acp/config.yaml`
|
|
1556
|
+
* 4. Synthesized default
|
|
1557
|
+
*
|
|
1558
|
+
* Parse errors at any layer fall through to the next; the caller never gets
|
|
1559
|
+
* an exception. Errors collect into the returned `diagnostics` list so they
|
|
1560
|
+
* can be surfaced to the operator.
|
|
1561
|
+
*/
|
|
1562
|
+
const USER_MANIFEST_PATH = join(homedir(), ".pi-acp", "config.yaml");
|
|
1563
|
+
const PROJECT_MANIFEST_BASENAME = ".pi-acp.yaml";
|
|
1564
|
+
async function loadManifest(input) {
|
|
1565
|
+
const diagnostics = [];
|
|
1566
|
+
const fromParams = await tryFromSessionParams(input.sessionParams, diagnostics);
|
|
1567
|
+
if (fromParams !== null) {
|
|
1568
|
+
const result = {
|
|
1569
|
+
manifest: fromParams.manifest,
|
|
1570
|
+
source: "session-params",
|
|
1571
|
+
diagnostics
|
|
1572
|
+
};
|
|
1573
|
+
if (fromParams.path !== void 0) result.path = fromParams.path;
|
|
1574
|
+
return result;
|
|
1575
|
+
}
|
|
1576
|
+
const projectPath = join(input.cwd, PROJECT_MANIFEST_BASENAME);
|
|
1577
|
+
const fromProject = tryFromFile(projectPath, "project", diagnostics);
|
|
1578
|
+
if (fromProject !== null) return {
|
|
1579
|
+
manifest: fromProject,
|
|
1580
|
+
source: "project",
|
|
1581
|
+
path: projectPath,
|
|
1582
|
+
diagnostics
|
|
1583
|
+
};
|
|
1584
|
+
const fromUser = tryFromFile(USER_MANIFEST_PATH, "user-global", diagnostics);
|
|
1585
|
+
if (fromUser !== null) return {
|
|
1586
|
+
manifest: fromUser,
|
|
1587
|
+
source: "user-global",
|
|
1588
|
+
path: USER_MANIFEST_PATH,
|
|
1589
|
+
diagnostics
|
|
1590
|
+
};
|
|
1591
|
+
return {
|
|
1592
|
+
manifest: DEFAULT_MANIFEST,
|
|
1593
|
+
source: "default",
|
|
1594
|
+
diagnostics
|
|
1595
|
+
};
|
|
1596
|
+
}
|
|
1597
|
+
async function tryFromSessionParams(params, diagnostics) {
|
|
1598
|
+
if (typeof params !== "object" || params === null) return null;
|
|
1599
|
+
const meta = params._meta;
|
|
1600
|
+
if (typeof meta !== "object" || meta === null) return null;
|
|
1601
|
+
const piAcp = meta.piAcp;
|
|
1602
|
+
if (typeof piAcp !== "object" || piAcp === null) return null;
|
|
1603
|
+
const manifestRef = piAcp.manifest;
|
|
1604
|
+
if (manifestRef === void 0) return null;
|
|
1605
|
+
if (typeof manifestRef === "string") {
|
|
1606
|
+
const parsed = tryFromFile(manifestRef, "session-params", diagnostics);
|
|
1607
|
+
if (parsed !== null) return {
|
|
1608
|
+
manifest: parsed,
|
|
1609
|
+
path: manifestRef
|
|
1610
|
+
};
|
|
1611
|
+
return null;
|
|
1612
|
+
}
|
|
1613
|
+
const result = ManifestSchema.safeParse(manifestRef);
|
|
1614
|
+
if (result.success) return { manifest: result.data };
|
|
1615
|
+
diagnostics.push({
|
|
1616
|
+
source: "session-params",
|
|
1617
|
+
message: `inline manifest validation failed: ${result.error.message}`
|
|
1618
|
+
});
|
|
1619
|
+
return null;
|
|
1620
|
+
}
|
|
1621
|
+
function tryFromFile(path, source, diagnostics) {
|
|
1622
|
+
if (!existsSync(path)) return null;
|
|
1623
|
+
let raw;
|
|
1624
|
+
try {
|
|
1625
|
+
raw = readFileSync(path, "utf8");
|
|
1626
|
+
} catch (err) {
|
|
1627
|
+
diagnostics.push({
|
|
1628
|
+
source,
|
|
1629
|
+
path,
|
|
1630
|
+
message: `read failed: ${err instanceof Error ? err.message : String(err)}`
|
|
1631
|
+
});
|
|
1632
|
+
return null;
|
|
1633
|
+
}
|
|
1634
|
+
let parsed;
|
|
1635
|
+
try {
|
|
1636
|
+
parsed = parse(raw);
|
|
1637
|
+
} catch (err) {
|
|
1638
|
+
diagnostics.push({
|
|
1639
|
+
source,
|
|
1640
|
+
path,
|
|
1641
|
+
message: `YAML parse failed: ${err instanceof Error ? err.message : String(err)}`
|
|
1642
|
+
});
|
|
1643
|
+
return null;
|
|
1644
|
+
}
|
|
1645
|
+
const result = ManifestSchema.safeParse(parsed);
|
|
1646
|
+
if (result.success) return result.data;
|
|
1647
|
+
diagnostics.push({
|
|
1648
|
+
source,
|
|
1649
|
+
path,
|
|
1650
|
+
message: `schema validation failed: ${result.error.message}`
|
|
1651
|
+
});
|
|
1652
|
+
return null;
|
|
1653
|
+
}
|
|
1654
|
+
//#endregion
|
|
1655
|
+
//#region src/resources/sources/http.ts
|
|
1656
|
+
const DEFAULT_CACHE_TTL_SECONDS = 300;
|
|
1657
|
+
const DEFAULT_TIMEOUT_MS$1 = 5e3;
|
|
1658
|
+
var HttpBackend = class {
|
|
1659
|
+
id;
|
|
1660
|
+
kind = "http";
|
|
1661
|
+
baseUrl;
|
|
1662
|
+
paths;
|
|
1663
|
+
cacheTtlMs;
|
|
1664
|
+
timeoutMs;
|
|
1665
|
+
fetchImpl;
|
|
1666
|
+
urlCache = /* @__PURE__ */ new Map();
|
|
1667
|
+
cache = null;
|
|
1668
|
+
constructor(opts) {
|
|
1669
|
+
if (!opts.baseUrl.startsWith("https://")) throw new Error(`pi-acp http source '${opts.id}': baseUrl must use https:// (got "${opts.baseUrl}")`);
|
|
1670
|
+
this.id = opts.id;
|
|
1671
|
+
this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
|
|
1672
|
+
this.paths = opts.paths ?? {};
|
|
1673
|
+
this.cacheTtlMs = (opts.cacheTtlSeconds ?? DEFAULT_CACHE_TTL_SECONDS) * 1e3;
|
|
1674
|
+
this.timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS$1;
|
|
1675
|
+
this.fetchImpl = opts.fetchImpl ?? globalThis.fetch.bind(globalThis);
|
|
1676
|
+
}
|
|
1677
|
+
async reload() {
|
|
1678
|
+
const diagnostics = [];
|
|
1679
|
+
for (const kind of [
|
|
1680
|
+
"skills",
|
|
1681
|
+
"prompts",
|
|
1682
|
+
"extensions"
|
|
1683
|
+
]) if (this.paths[kind] !== void 0) diagnostics.push(this.unsupportedDiagnostic(kind));
|
|
1684
|
+
const list = this.paths.agentsFiles ?? [];
|
|
1685
|
+
const files = [];
|
|
1686
|
+
if (list.length > 0) {
|
|
1687
|
+
const results = await Promise.all(list.map((path) => this.fetchPath(path).then((content) => ({
|
|
1688
|
+
path,
|
|
1689
|
+
content,
|
|
1690
|
+
error: null
|
|
1691
|
+
}), (err) => ({
|
|
1692
|
+
path,
|
|
1693
|
+
content: null,
|
|
1694
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1695
|
+
}))));
|
|
1696
|
+
for (const r of results) {
|
|
1697
|
+
if (r.content !== null) {
|
|
1698
|
+
files.push({
|
|
1699
|
+
path: this.qualifyPath(r.path),
|
|
1700
|
+
content: r.content
|
|
1701
|
+
});
|
|
1702
|
+
continue;
|
|
1703
|
+
}
|
|
1704
|
+
diagnostics.push({
|
|
1705
|
+
type: "warning",
|
|
1706
|
+
message: `pi-acp http source '${this.id}' (${this.baseUrl}): agentsFile '${r.path}' unreadable — ${r.error ?? "(unknown)"}`,
|
|
1707
|
+
path: r.path
|
|
1708
|
+
});
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
this.cache = {
|
|
1712
|
+
files,
|
|
1713
|
+
diagnostics
|
|
1714
|
+
};
|
|
1715
|
+
}
|
|
1716
|
+
getAgentsFiles() {
|
|
1717
|
+
return this.cache?.files ?? [];
|
|
1718
|
+
}
|
|
1719
|
+
getSkills() {
|
|
1720
|
+
return {
|
|
1721
|
+
skills: [],
|
|
1722
|
+
diagnostics: this.cache?.diagnostics ?? []
|
|
1723
|
+
};
|
|
1724
|
+
}
|
|
1725
|
+
getPrompts() {
|
|
1726
|
+
return {
|
|
1727
|
+
prompts: [],
|
|
1728
|
+
diagnostics: []
|
|
1729
|
+
};
|
|
1730
|
+
}
|
|
1731
|
+
getSystemPrompt() {}
|
|
1732
|
+
getAppendSystemPrompt() {
|
|
1733
|
+
return [];
|
|
1734
|
+
}
|
|
1735
|
+
qualifyPath(path) {
|
|
1736
|
+
return `${this.baseUrl}/${path.replace(/^\/+/, "")}`;
|
|
1737
|
+
}
|
|
1738
|
+
unsupportedDiagnostic(kind) {
|
|
1739
|
+
return {
|
|
1740
|
+
type: "warning",
|
|
1741
|
+
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}.`
|
|
1742
|
+
};
|
|
1743
|
+
}
|
|
1744
|
+
async fetchPath(path) {
|
|
1745
|
+
const url = this.qualifyPath(path);
|
|
1746
|
+
const now = Date.now();
|
|
1747
|
+
const cached = this.urlCache.get(url);
|
|
1748
|
+
if (cached !== void 0 && cached.expiresAt > now) return cached.content;
|
|
1749
|
+
const controller = new AbortController();
|
|
1750
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
1751
|
+
let response;
|
|
1752
|
+
try {
|
|
1753
|
+
response = await this.fetchImpl(url, { signal: controller.signal });
|
|
1754
|
+
} catch (err) {
|
|
1755
|
+
if (err instanceof Error && err.name === "AbortError") throw new Error(`fetch timed out after ${this.timeoutMs}ms`);
|
|
1756
|
+
throw err;
|
|
1757
|
+
} finally {
|
|
1758
|
+
clearTimeout(timer);
|
|
1759
|
+
}
|
|
1760
|
+
if (!response.ok) throw new Error(`HTTP ${response.status} ${response.statusText || ""}`.trim());
|
|
1761
|
+
const content = await response.text();
|
|
1762
|
+
this.urlCache.set(url, {
|
|
1763
|
+
content,
|
|
1764
|
+
expiresAt: now + this.cacheTtlMs
|
|
1765
|
+
});
|
|
1766
|
+
return content;
|
|
1767
|
+
}
|
|
1768
|
+
};
|
|
1769
|
+
//#endregion
|
|
1770
|
+
//#region src/resources/sources/ssh.ts
|
|
1771
|
+
const DEFAULT_TIMEOUT_MS = 5e3;
|
|
1772
|
+
var SshBackend = class {
|
|
1773
|
+
id;
|
|
1774
|
+
kind = "ssh";
|
|
1775
|
+
host;
|
|
1776
|
+
user;
|
|
1777
|
+
paths;
|
|
1778
|
+
timeoutMs;
|
|
1779
|
+
sshCommand;
|
|
1780
|
+
cache = null;
|
|
1781
|
+
constructor(opts) {
|
|
1782
|
+
this.id = opts.id;
|
|
1783
|
+
this.host = opts.host;
|
|
1784
|
+
this.user = opts.user;
|
|
1785
|
+
this.paths = opts.paths ?? {};
|
|
1786
|
+
this.timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
1787
|
+
this.sshCommand = opts.sshCommand ?? "ssh";
|
|
1788
|
+
}
|
|
1789
|
+
async reload() {
|
|
1790
|
+
const diagnostics = [];
|
|
1791
|
+
for (const kind of [
|
|
1792
|
+
"skills",
|
|
1793
|
+
"prompts",
|
|
1794
|
+
"extensions"
|
|
1795
|
+
]) if (this.paths[kind] !== void 0) diagnostics.push(this.unsupportedDiagnostic(kind));
|
|
1796
|
+
const list = this.paths.agentsFiles ?? [];
|
|
1797
|
+
const files = [];
|
|
1798
|
+
if (list.length > 0) {
|
|
1799
|
+
const results = await Promise.all(list.map((path) => this.cat(path).then((content) => ({
|
|
1800
|
+
path,
|
|
1801
|
+
content,
|
|
1802
|
+
error: null
|
|
1803
|
+
}), (err) => ({
|
|
1804
|
+
path,
|
|
1805
|
+
content: null,
|
|
1806
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1807
|
+
}))));
|
|
1808
|
+
for (const r of results) {
|
|
1809
|
+
if (r.content !== null) {
|
|
1810
|
+
files.push({
|
|
1811
|
+
path: this.qualifyPath(r.path),
|
|
1812
|
+
content: r.content
|
|
1813
|
+
});
|
|
1814
|
+
continue;
|
|
1815
|
+
}
|
|
1816
|
+
diagnostics.push({
|
|
1817
|
+
type: "warning",
|
|
1818
|
+
message: `pi-acp ssh source '${this.id}' (${this.target()}): agentsFile '${r.path}' unreadable — ${r.error ?? "(unknown)"}`,
|
|
1819
|
+
path: r.path
|
|
1820
|
+
});
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
this.cache = {
|
|
1824
|
+
files,
|
|
1825
|
+
diagnostics
|
|
1826
|
+
};
|
|
1827
|
+
}
|
|
1828
|
+
getAgentsFiles() {
|
|
1829
|
+
return this.cache?.files ?? [];
|
|
1830
|
+
}
|
|
1831
|
+
getSkills() {
|
|
1832
|
+
return {
|
|
1833
|
+
skills: [],
|
|
1834
|
+
diagnostics: this.cache?.diagnostics ?? []
|
|
1835
|
+
};
|
|
1836
|
+
}
|
|
1837
|
+
getPrompts() {
|
|
1838
|
+
return {
|
|
1839
|
+
prompts: [],
|
|
1840
|
+
diagnostics: []
|
|
1841
|
+
};
|
|
1842
|
+
}
|
|
1843
|
+
getSystemPrompt() {}
|
|
1844
|
+
getAppendSystemPrompt() {
|
|
1845
|
+
return [];
|
|
1846
|
+
}
|
|
1847
|
+
target() {
|
|
1848
|
+
return this.user !== void 0 && this.user.length > 0 ? `${this.user}@${this.host}` : this.host;
|
|
1849
|
+
}
|
|
1850
|
+
qualifyPath(path) {
|
|
1851
|
+
return `ssh://${this.target()}/${path.replace(/^\//, "")}`;
|
|
1852
|
+
}
|
|
1853
|
+
unsupportedDiagnostic(kind) {
|
|
1854
|
+
return {
|
|
1855
|
+
type: "warning",
|
|
1856
|
+
message: `pi-acp ssh source '${this.id}' (${this.target()}): ${kind} discovery over SSH not yet implemented — declare individual files via paths.agentsFiles for now, or omit paths.${kind}.`
|
|
1857
|
+
};
|
|
1858
|
+
}
|
|
1859
|
+
async cat(path) {
|
|
1860
|
+
const seconds = Math.max(1, Math.ceil(this.timeoutMs / 1e3));
|
|
1861
|
+
const aliveCount = Math.max(1, Math.floor(seconds / 2));
|
|
1862
|
+
const result = await $`${this.sshCommand} -o BatchMode=yes -o ConnectTimeout=${seconds} -o ServerAliveInterval=2 -o ServerAliveCountMax=${aliveCount} ${this.target()} -- cat ${path}`.quiet().nothrow();
|
|
1863
|
+
if (result.exitCode !== 0) {
|
|
1864
|
+
const stderr = result.stderr.toString().trim();
|
|
1865
|
+
throw new Error(`ssh exited ${result.exitCode}: ${stderr || "(no stderr)"}`);
|
|
1866
|
+
}
|
|
1867
|
+
return result.stdout.toString();
|
|
1868
|
+
}
|
|
1869
|
+
};
|
|
1870
|
+
//#endregion
|
|
1139
1871
|
//#region package.json
|
|
1140
1872
|
var name = "@victor-software-house/pi-acp";
|
|
1141
|
-
var version = "0.
|
|
1873
|
+
var version = "0.10.0";
|
|
1142
1874
|
//#endregion
|
|
1143
1875
|
//#region src/acp/agent.ts
|
|
1144
1876
|
/** Builtin ACP slash commands handled directly by the adapter. */
|
|
@@ -1259,6 +1991,74 @@ var PiAcpAgent = class {
|
|
|
1259
1991
|
if (this.daemonContext === void 0) return { disposed: true };
|
|
1260
1992
|
return { disposed: this.daemonContext.sessionRegistry.release(sessionId, this.connectionId).kind === "disposed" };
|
|
1261
1993
|
}
|
|
1994
|
+
/**
|
|
1995
|
+
* Build a VirtualResourceLoader for a new pi session. Reads the
|
|
1996
|
+
* `.pi-acp.yaml` manifest cascade (ACP params > project > user-global >
|
|
1997
|
+
* default), turns each declared root into a ResourceSource, and ensures at
|
|
1998
|
+
* least one LocalBackend is present for the extension / theme passthrough.
|
|
1999
|
+
*
|
|
2000
|
+
* Phase 5 materializes `kind: "local"`. Phase 6 adds `kind: "ssh"`.
|
|
2001
|
+
* Phase 7 adds `kind: "http"`. `acp-fs` still parses fine but surfaces as
|
|
2002
|
+
* a diagnostic until its backend lands in a subsequent phase.
|
|
2003
|
+
*/
|
|
2004
|
+
async buildResourceLoader(cwd, sessionParams) {
|
|
2005
|
+
const loaded = await loadManifest({
|
|
2006
|
+
cwd,
|
|
2007
|
+
sessionParams
|
|
2008
|
+
});
|
|
2009
|
+
const diagnostics = [...loaded.diagnostics];
|
|
2010
|
+
const sources = [];
|
|
2011
|
+
for (const root of loaded.manifest.roots) {
|
|
2012
|
+
if (root.kind === "local") {
|
|
2013
|
+
sources.push(new LocalBackend({
|
|
2014
|
+
id: root.id,
|
|
2015
|
+
cwd: root.paths.cwd ?? cwd,
|
|
2016
|
+
agentDir: root.paths.agentDir ?? getAgentDir()
|
|
2017
|
+
}));
|
|
2018
|
+
continue;
|
|
2019
|
+
}
|
|
2020
|
+
if (root.kind === "ssh") {
|
|
2021
|
+
const sshOpts = {
|
|
2022
|
+
id: root.id,
|
|
2023
|
+
host: root.host,
|
|
2024
|
+
paths: root.paths
|
|
2025
|
+
};
|
|
2026
|
+
if (root.user !== void 0) sshOpts.user = root.user;
|
|
2027
|
+
sources.push(new SshBackend(sshOpts));
|
|
2028
|
+
continue;
|
|
2029
|
+
}
|
|
2030
|
+
if (root.kind === "http") {
|
|
2031
|
+
const httpOpts = {
|
|
2032
|
+
id: root.id,
|
|
2033
|
+
baseUrl: root.baseUrl,
|
|
2034
|
+
paths: root.paths
|
|
2035
|
+
};
|
|
2036
|
+
if (root.cache !== void 0) httpOpts.cacheTtlSeconds = root.cache.ttl;
|
|
2037
|
+
sources.push(new HttpBackend(httpOpts));
|
|
2038
|
+
continue;
|
|
2039
|
+
}
|
|
2040
|
+
const diag = {
|
|
2041
|
+
source: loaded.source,
|
|
2042
|
+
message: `root "${root.id}" kind="${root.kind}" not yet supported in this build (skipped)`
|
|
2043
|
+
};
|
|
2044
|
+
if (loaded.path !== void 0) diag.path = loaded.path;
|
|
2045
|
+
diagnostics.push(diag);
|
|
2046
|
+
}
|
|
2047
|
+
if (!sources.some((s) => s.kind === "local")) sources.unshift(new LocalBackend({
|
|
2048
|
+
cwd,
|
|
2049
|
+
agentDir: getAgentDir()
|
|
2050
|
+
}));
|
|
2051
|
+
const loader = new VirtualResourceLoader({
|
|
2052
|
+
sources,
|
|
2053
|
+
mergeStrategy: loaded.manifest.mergeStrategy
|
|
2054
|
+
});
|
|
2055
|
+
await loader.reload();
|
|
2056
|
+
if (diagnostics.length > 0 && process.env["PI_ACP_DAEMON_DEBUG"] === "1") for (const d of diagnostics) {
|
|
2057
|
+
const where = d.path !== void 0 ? ` ${d.path}` : "";
|
|
2058
|
+
process.stderr.write(`pi-acp manifest [${d.source}${where}]: ${d.message}\n`);
|
|
2059
|
+
}
|
|
2060
|
+
return loader;
|
|
2061
|
+
}
|
|
1262
2062
|
async initialize(params) {
|
|
1263
2063
|
const supportedVersion = 1;
|
|
1264
2064
|
const requested = params.protocolVersion;
|
|
@@ -1295,7 +2095,11 @@ var PiAcpAgent = class {
|
|
|
1295
2095
|
if (!isAbsolute(params.cwd)) throw RequestError.invalidParams(`cwd must be an absolute path: ${params.cwd}`);
|
|
1296
2096
|
let result;
|
|
1297
2097
|
try {
|
|
1298
|
-
|
|
2098
|
+
const resourceLoader = await this.buildResourceLoader(params.cwd, params);
|
|
2099
|
+
result = await createAgentSession({
|
|
2100
|
+
cwd: params.cwd,
|
|
2101
|
+
resourceLoader
|
|
2102
|
+
});
|
|
1299
2103
|
} catch (e) {
|
|
1300
2104
|
const authErr = detectAuthError(e);
|
|
1301
2105
|
if (authErr !== null) throw authErr;
|
|
@@ -1565,9 +2369,11 @@ var PiAcpAgent = class {
|
|
|
1565
2369
|
let result;
|
|
1566
2370
|
try {
|
|
1567
2371
|
const sm = SessionManager.open(sessionFile);
|
|
2372
|
+
const resourceLoader = await this.buildResourceLoader(params.cwd, params);
|
|
1568
2373
|
result = await createAgentSession({
|
|
1569
2374
|
cwd: params.cwd,
|
|
1570
|
-
sessionManager: sm
|
|
2375
|
+
sessionManager: sm,
|
|
2376
|
+
resourceLoader
|
|
1571
2377
|
});
|
|
1572
2378
|
} catch (e) {
|
|
1573
2379
|
const authErr = detectAuthError(e);
|
|
@@ -1663,9 +2469,11 @@ var PiAcpAgent = class {
|
|
|
1663
2469
|
let result;
|
|
1664
2470
|
try {
|
|
1665
2471
|
const sm = SessionManager.open(sessionFile);
|
|
2472
|
+
const resourceLoader = await this.buildResourceLoader(params.cwd, params);
|
|
1666
2473
|
result = await createAgentSession({
|
|
1667
2474
|
cwd: params.cwd,
|
|
1668
|
-
sessionManager: sm
|
|
2475
|
+
sessionManager: sm,
|
|
2476
|
+
resourceLoader
|
|
1669
2477
|
});
|
|
1670
2478
|
} catch (e) {
|
|
1671
2479
|
const authErr = detectAuthError(e);
|
|
@@ -1720,9 +2528,11 @@ var PiAcpAgent = class {
|
|
|
1720
2528
|
let result;
|
|
1721
2529
|
try {
|
|
1722
2530
|
const sm = SessionManager.forkFrom(sourceFile, params.cwd);
|
|
2531
|
+
const resourceLoader = await this.buildResourceLoader(params.cwd, params);
|
|
1723
2532
|
result = await createAgentSession({
|
|
1724
2533
|
cwd: params.cwd,
|
|
1725
|
-
sessionManager: sm
|
|
2534
|
+
sessionManager: sm,
|
|
2535
|
+
resourceLoader
|
|
1726
2536
|
});
|
|
1727
2537
|
} catch (e) {
|
|
1728
2538
|
const authErr = detectAuthError(e);
|
|
@@ -2191,20 +3001,8 @@ function buildCommandList(piSession, enableSkillCommands) {
|
|
|
2191
3001
|
}
|
|
2192
3002
|
function findChangelog() {
|
|
2193
3003
|
try {
|
|
2194
|
-
const
|
|
2195
|
-
|
|
2196
|
-
if (piPath !== void 0 && piPath !== "") {
|
|
2197
|
-
const p = join(dirname(dirname(realpathSync(piPath))), "CHANGELOG.md");
|
|
2198
|
-
if (existsSync(p)) return p;
|
|
2199
|
-
}
|
|
2200
|
-
} catch {}
|
|
2201
|
-
try {
|
|
2202
|
-
const npmRoot = spawnSync("npm", ["root", "-g"], { encoding: "utf-8" });
|
|
2203
|
-
const root = String(npmRoot.stdout ?? "").trim();
|
|
2204
|
-
if (root) {
|
|
2205
|
-
const p = join(root, "@mariozechner", "pi-coding-agent", "CHANGELOG.md");
|
|
2206
|
-
if (existsSync(p)) return p;
|
|
2207
|
-
}
|
|
3004
|
+
const p = piChangelogPath();
|
|
3005
|
+
if (existsSync(p)) return p;
|
|
2208
3006
|
} catch {}
|
|
2209
3007
|
return null;
|
|
2210
3008
|
}
|
|
@@ -2258,6 +3056,108 @@ function toWebWritable(dst) {
|
|
|
2258
3056
|
} });
|
|
2259
3057
|
}
|
|
2260
3058
|
//#endregion
|
|
2261
|
-
|
|
3059
|
+
//#region src/daemon/index.ts
|
|
3060
|
+
/**
|
|
3061
|
+
* Daemon entry point. Invoked when pi-acp is launched with `--daemon`.
|
|
3062
|
+
*
|
|
3063
|
+
* Lifecycle:
|
|
3064
|
+
* 1. Acquire per-UID lockfile (refuses if another daemon alive).
|
|
3065
|
+
* 2. Remove stale socket files left by a dead prior daemon.
|
|
3066
|
+
* 3. Construct DaemonContext shared singletons.
|
|
3067
|
+
* 4. Bind ACP socket (raw NDJSON via node:net).
|
|
3068
|
+
* 5. Bind control socket (HTTP via Bun.serve + Hono).
|
|
3069
|
+
* 6. SIGINT/SIGTERM/idle-timeout trigger graceful shutdown.
|
|
3070
|
+
*/
|
|
3071
|
+
async function runDaemon() {
|
|
3072
|
+
const lockResult = acquireLock();
|
|
3073
|
+
if (!lockResult.ok) {
|
|
3074
|
+
process.stderr.write(`pi-acp daemon: already running (pid ${lockResult.heldByPid ?? "unknown"})\n`);
|
|
3075
|
+
process.exit(1);
|
|
3076
|
+
}
|
|
3077
|
+
ensureSocketParentDir();
|
|
3078
|
+
removeStaleSocketIfAny();
|
|
3079
|
+
const connections = /* @__PURE__ */ new Set();
|
|
3080
|
+
let shuttingDown = false;
|
|
3081
|
+
const startedAt = Date.now();
|
|
3082
|
+
const shutdown = () => {
|
|
3083
|
+
if (shuttingDown) return;
|
|
3084
|
+
shuttingDown = true;
|
|
3085
|
+
server.close();
|
|
3086
|
+
controlServer.stop();
|
|
3087
|
+
for (const entry of connections) {
|
|
3088
|
+
try {
|
|
3089
|
+
entry.handle.dispose();
|
|
3090
|
+
} catch {}
|
|
3091
|
+
try {
|
|
3092
|
+
entry.socket.destroy();
|
|
3093
|
+
} catch {}
|
|
3094
|
+
}
|
|
3095
|
+
connections.clear();
|
|
3096
|
+
ctx.idleTracker.dispose();
|
|
3097
|
+
removeStaleSocketIfAny();
|
|
3098
|
+
releaseLock();
|
|
3099
|
+
process.exit(0);
|
|
3100
|
+
};
|
|
3101
|
+
const ctx = createDaemonContext();
|
|
3102
|
+
ctx.idleTracker = createIdleTracker({
|
|
3103
|
+
idleMs: resolveIdleMs(),
|
|
3104
|
+
onIdle: shutdown
|
|
3105
|
+
});
|
|
3106
|
+
const controlCtx = {
|
|
3107
|
+
ctx,
|
|
3108
|
+
startedAt,
|
|
3109
|
+
pid: process.pid,
|
|
3110
|
+
version,
|
|
3111
|
+
activeConnections: () => connections.size,
|
|
3112
|
+
onShutdown: shutdown
|
|
3113
|
+
};
|
|
3114
|
+
const server = createServer((socket) => {
|
|
3115
|
+
if (shuttingDown) {
|
|
3116
|
+
socket.destroy();
|
|
3117
|
+
return;
|
|
3118
|
+
}
|
|
3119
|
+
onAccept(socket);
|
|
3120
|
+
});
|
|
3121
|
+
const onAccept = (socket) => {
|
|
3122
|
+
const handle = serveAcp({
|
|
3123
|
+
input: socket,
|
|
3124
|
+
output: socket,
|
|
3125
|
+
daemonContext: ctx
|
|
3126
|
+
});
|
|
3127
|
+
const entry = {
|
|
3128
|
+
socket,
|
|
3129
|
+
handle
|
|
3130
|
+
};
|
|
3131
|
+
connections.add(entry);
|
|
3132
|
+
ctx.idleTracker.bump(1);
|
|
3133
|
+
const cleanup = () => {
|
|
3134
|
+
if (!connections.delete(entry)) return;
|
|
3135
|
+
try {
|
|
3136
|
+
handle.dispose();
|
|
3137
|
+
} catch {}
|
|
3138
|
+
ctx.idleTracker.bump(-1);
|
|
3139
|
+
};
|
|
3140
|
+
socket.on("close", cleanup);
|
|
3141
|
+
socket.on("error", cleanup);
|
|
3142
|
+
};
|
|
3143
|
+
server.on("error", (err) => {
|
|
3144
|
+
process.stderr.write(`pi-acp daemon: server error: ${err.message}\n`);
|
|
3145
|
+
});
|
|
3146
|
+
await new Promise((resolve, reject) => {
|
|
3147
|
+
const path = socketPath();
|
|
3148
|
+
server.listen(path, () => resolve());
|
|
3149
|
+
server.once("error", reject);
|
|
3150
|
+
});
|
|
3151
|
+
const controlServer = serveControl(buildControlApp(controlCtx), controlSocketPath());
|
|
3152
|
+
if (process.env["PI_ACP_DAEMON_DEBUG"] === "1") process.stderr.write(`pi-acp daemon: acp=${socketPath()} control=${controlSocketPath()} pid=${process.pid}\n`);
|
|
3153
|
+
process.on("SIGINT", () => {
|
|
3154
|
+
shutdown();
|
|
3155
|
+
});
|
|
3156
|
+
process.on("SIGTERM", () => {
|
|
3157
|
+
shutdown();
|
|
3158
|
+
});
|
|
3159
|
+
}
|
|
3160
|
+
//#endregion
|
|
3161
|
+
export { runDaemon };
|
|
2262
3162
|
|
|
2263
|
-
//# sourceMappingURL=
|
|
3163
|
+
//# sourceMappingURL=daemon-xclwSgis.mjs.map
|