@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.
@@ -1,12 +1,189 @@
1
- #!/usr/bin/env node
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 { spawnSync } from "node:child_process";
4
- import { existsSync, readFileSync, realpathSync } from "node:fs";
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.8.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
- result = await createAgentSession({ cwd: params.cwd });
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 which = spawnSync(process.platform === "win32" ? "where" : "which", ["pi"], { encoding: "utf-8" });
2195
- const piPath = String(which.stdout ?? "").split(/\r?\n/)[0]?.trim();
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
- export { version as n, serveAcp as t };
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=serve-DmuHYqF-.mjs.map
3163
+ //# sourceMappingURL=daemon-xclwSgis.mjs.map