@victor-software-house/pi-acp 0.7.0 → 0.9.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,449 @@ 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/ssh.ts
1656
+ const DEFAULT_TIMEOUT_MS = 5e3;
1657
+ var SshBackend = class {
1658
+ id;
1659
+ kind = "ssh";
1660
+ host;
1661
+ user;
1662
+ paths;
1663
+ timeoutMs;
1664
+ sshCommand;
1665
+ cache = null;
1666
+ constructor(opts) {
1667
+ this.id = opts.id;
1668
+ this.host = opts.host;
1669
+ this.user = opts.user;
1670
+ this.paths = opts.paths ?? {};
1671
+ this.timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
1672
+ this.sshCommand = opts.sshCommand ?? "ssh";
1673
+ }
1674
+ async reload() {
1675
+ const diagnostics = [];
1676
+ for (const kind of [
1677
+ "skills",
1678
+ "prompts",
1679
+ "extensions"
1680
+ ]) if (this.paths[kind] !== void 0) diagnostics.push(this.unsupportedDiagnostic(kind));
1681
+ const list = this.paths.agentsFiles ?? [];
1682
+ const files = [];
1683
+ if (list.length > 0) {
1684
+ const results = await Promise.all(list.map((path) => this.cat(path).then((content) => ({
1685
+ path,
1686
+ content,
1687
+ error: null
1688
+ }), (err) => ({
1689
+ path,
1690
+ content: null,
1691
+ error: err instanceof Error ? err.message : String(err)
1692
+ }))));
1693
+ for (const r of results) {
1694
+ if (r.content !== null) {
1695
+ files.push({
1696
+ path: this.qualifyPath(r.path),
1697
+ content: r.content
1698
+ });
1699
+ continue;
1700
+ }
1701
+ diagnostics.push({
1702
+ type: "warning",
1703
+ message: `pi-acp ssh source '${this.id}' (${this.target()}): agentsFile '${r.path}' unreadable — ${r.error ?? "(unknown)"}`,
1704
+ path: r.path
1705
+ });
1706
+ }
1707
+ }
1708
+ this.cache = {
1709
+ files,
1710
+ diagnostics
1711
+ };
1712
+ }
1713
+ getAgentsFiles() {
1714
+ return this.cache?.files ?? [];
1715
+ }
1716
+ getSkills() {
1717
+ return {
1718
+ skills: [],
1719
+ diagnostics: this.cache?.diagnostics ?? []
1720
+ };
1721
+ }
1722
+ getPrompts() {
1723
+ return {
1724
+ prompts: [],
1725
+ diagnostics: []
1726
+ };
1727
+ }
1728
+ getSystemPrompt() {}
1729
+ getAppendSystemPrompt() {
1730
+ return [];
1731
+ }
1732
+ target() {
1733
+ return this.user !== void 0 && this.user.length > 0 ? `${this.user}@${this.host}` : this.host;
1734
+ }
1735
+ qualifyPath(path) {
1736
+ return `ssh://${this.target()}/${path.replace(/^\//, "")}`;
1737
+ }
1738
+ unsupportedDiagnostic(kind) {
1739
+ return {
1740
+ type: "warning",
1741
+ 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}.`
1742
+ };
1743
+ }
1744
+ async cat(path) {
1745
+ const seconds = Math.max(1, Math.ceil(this.timeoutMs / 1e3));
1746
+ const aliveCount = Math.max(1, Math.floor(seconds / 2));
1747
+ const result = await $`${this.sshCommand} -o BatchMode=yes -o ConnectTimeout=${seconds} -o ServerAliveInterval=2 -o ServerAliveCountMax=${aliveCount} ${this.target()} -- cat ${path}`.quiet().nothrow();
1748
+ if (result.exitCode !== 0) {
1749
+ const stderr = result.stderr.toString().trim();
1750
+ throw new Error(`ssh exited ${result.exitCode}: ${stderr || "(no stderr)"}`);
1751
+ }
1752
+ return result.stdout.toString();
1753
+ }
1754
+ };
1755
+ //#endregion
1139
1756
  //#region package.json
1140
1757
  var name = "@victor-software-house/pi-acp";
1141
- var version = "0.7.0";
1758
+ var version = "0.9.0";
1142
1759
  //#endregion
1143
1760
  //#region src/acp/agent.ts
1144
1761
  /** Builtin ACP slash commands handled directly by the adapter. */
@@ -1259,6 +1876,64 @@ var PiAcpAgent = class {
1259
1876
  if (this.daemonContext === void 0) return { disposed: true };
1260
1877
  return { disposed: this.daemonContext.sessionRegistry.release(sessionId, this.connectionId).kind === "disposed" };
1261
1878
  }
1879
+ /**
1880
+ * Build a VirtualResourceLoader for a new pi session. Reads the
1881
+ * `.pi-acp.yaml` manifest cascade (ACP params > project > user-global >
1882
+ * default), turns each declared root into a ResourceSource, and ensures at
1883
+ * least one LocalBackend is present for the extension / theme passthrough.
1884
+ *
1885
+ * Phase 5 materializes `kind: "local"`. Phase 6 adds `kind: "ssh"`.
1886
+ * `http` and `acp-fs` still parse fine but surface as diagnostics until
1887
+ * their backends land in subsequent phases.
1888
+ */
1889
+ async buildResourceLoader(cwd, sessionParams) {
1890
+ const loaded = await loadManifest({
1891
+ cwd,
1892
+ sessionParams
1893
+ });
1894
+ const diagnostics = [...loaded.diagnostics];
1895
+ const sources = [];
1896
+ for (const root of loaded.manifest.roots) {
1897
+ if (root.kind === "local") {
1898
+ sources.push(new LocalBackend({
1899
+ id: root.id,
1900
+ cwd: root.paths.cwd ?? cwd,
1901
+ agentDir: root.paths.agentDir ?? getAgentDir()
1902
+ }));
1903
+ continue;
1904
+ }
1905
+ if (root.kind === "ssh") {
1906
+ const sshOpts = {
1907
+ id: root.id,
1908
+ host: root.host,
1909
+ paths: root.paths
1910
+ };
1911
+ if (root.user !== void 0) sshOpts.user = root.user;
1912
+ sources.push(new SshBackend(sshOpts));
1913
+ continue;
1914
+ }
1915
+ const diag = {
1916
+ source: loaded.source,
1917
+ message: `root "${root.id}" kind="${root.kind}" not yet supported in this build (skipped)`
1918
+ };
1919
+ if (loaded.path !== void 0) diag.path = loaded.path;
1920
+ diagnostics.push(diag);
1921
+ }
1922
+ if (!sources.some((s) => s.kind === "local")) sources.unshift(new LocalBackend({
1923
+ cwd,
1924
+ agentDir: getAgentDir()
1925
+ }));
1926
+ const loader = new VirtualResourceLoader({
1927
+ sources,
1928
+ mergeStrategy: loaded.manifest.mergeStrategy
1929
+ });
1930
+ await loader.reload();
1931
+ if (diagnostics.length > 0 && process.env["PI_ACP_DAEMON_DEBUG"] === "1") for (const d of diagnostics) {
1932
+ const where = d.path !== void 0 ? ` ${d.path}` : "";
1933
+ process.stderr.write(`pi-acp manifest [${d.source}${where}]: ${d.message}\n`);
1934
+ }
1935
+ return loader;
1936
+ }
1262
1937
  async initialize(params) {
1263
1938
  const supportedVersion = 1;
1264
1939
  const requested = params.protocolVersion;
@@ -1295,7 +1970,11 @@ var PiAcpAgent = class {
1295
1970
  if (!isAbsolute(params.cwd)) throw RequestError.invalidParams(`cwd must be an absolute path: ${params.cwd}`);
1296
1971
  let result;
1297
1972
  try {
1298
- result = await createAgentSession({ cwd: params.cwd });
1973
+ const resourceLoader = await this.buildResourceLoader(params.cwd, params);
1974
+ result = await createAgentSession({
1975
+ cwd: params.cwd,
1976
+ resourceLoader
1977
+ });
1299
1978
  } catch (e) {
1300
1979
  const authErr = detectAuthError(e);
1301
1980
  if (authErr !== null) throw authErr;
@@ -1565,9 +2244,11 @@ var PiAcpAgent = class {
1565
2244
  let result;
1566
2245
  try {
1567
2246
  const sm = SessionManager.open(sessionFile);
2247
+ const resourceLoader = await this.buildResourceLoader(params.cwd, params);
1568
2248
  result = await createAgentSession({
1569
2249
  cwd: params.cwd,
1570
- sessionManager: sm
2250
+ sessionManager: sm,
2251
+ resourceLoader
1571
2252
  });
1572
2253
  } catch (e) {
1573
2254
  const authErr = detectAuthError(e);
@@ -1663,9 +2344,11 @@ var PiAcpAgent = class {
1663
2344
  let result;
1664
2345
  try {
1665
2346
  const sm = SessionManager.open(sessionFile);
2347
+ const resourceLoader = await this.buildResourceLoader(params.cwd, params);
1666
2348
  result = await createAgentSession({
1667
2349
  cwd: params.cwd,
1668
- sessionManager: sm
2350
+ sessionManager: sm,
2351
+ resourceLoader
1669
2352
  });
1670
2353
  } catch (e) {
1671
2354
  const authErr = detectAuthError(e);
@@ -1720,9 +2403,11 @@ var PiAcpAgent = class {
1720
2403
  let result;
1721
2404
  try {
1722
2405
  const sm = SessionManager.forkFrom(sourceFile, params.cwd);
2406
+ const resourceLoader = await this.buildResourceLoader(params.cwd, params);
1723
2407
  result = await createAgentSession({
1724
2408
  cwd: params.cwd,
1725
- sessionManager: sm
2409
+ sessionManager: sm,
2410
+ resourceLoader
1726
2411
  });
1727
2412
  } catch (e) {
1728
2413
  const authErr = detectAuthError(e);
@@ -2191,20 +2876,8 @@ function buildCommandList(piSession, enableSkillCommands) {
2191
2876
  }
2192
2877
  function findChangelog() {
2193
2878
  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
- }
2879
+ const p = piChangelogPath();
2880
+ if (existsSync(p)) return p;
2208
2881
  } catch {}
2209
2882
  return null;
2210
2883
  }
@@ -2258,6 +2931,108 @@ function toWebWritable(dst) {
2258
2931
  } });
2259
2932
  }
2260
2933
  //#endregion
2261
- export { serveAcp as t };
2934
+ //#region src/daemon/index.ts
2935
+ /**
2936
+ * Daemon entry point. Invoked when pi-acp is launched with `--daemon`.
2937
+ *
2938
+ * Lifecycle:
2939
+ * 1. Acquire per-UID lockfile (refuses if another daemon alive).
2940
+ * 2. Remove stale socket files left by a dead prior daemon.
2941
+ * 3. Construct DaemonContext shared singletons.
2942
+ * 4. Bind ACP socket (raw NDJSON via node:net).
2943
+ * 5. Bind control socket (HTTP via Bun.serve + Hono).
2944
+ * 6. SIGINT/SIGTERM/idle-timeout trigger graceful shutdown.
2945
+ */
2946
+ async function runDaemon() {
2947
+ const lockResult = acquireLock();
2948
+ if (!lockResult.ok) {
2949
+ process.stderr.write(`pi-acp daemon: already running (pid ${lockResult.heldByPid ?? "unknown"})\n`);
2950
+ process.exit(1);
2951
+ }
2952
+ ensureSocketParentDir();
2953
+ removeStaleSocketIfAny();
2954
+ const connections = /* @__PURE__ */ new Set();
2955
+ let shuttingDown = false;
2956
+ const startedAt = Date.now();
2957
+ const shutdown = () => {
2958
+ if (shuttingDown) return;
2959
+ shuttingDown = true;
2960
+ server.close();
2961
+ controlServer.stop();
2962
+ for (const entry of connections) {
2963
+ try {
2964
+ entry.handle.dispose();
2965
+ } catch {}
2966
+ try {
2967
+ entry.socket.destroy();
2968
+ } catch {}
2969
+ }
2970
+ connections.clear();
2971
+ ctx.idleTracker.dispose();
2972
+ removeStaleSocketIfAny();
2973
+ releaseLock();
2974
+ process.exit(0);
2975
+ };
2976
+ const ctx = createDaemonContext();
2977
+ ctx.idleTracker = createIdleTracker({
2978
+ idleMs: resolveIdleMs(),
2979
+ onIdle: shutdown
2980
+ });
2981
+ const controlCtx = {
2982
+ ctx,
2983
+ startedAt,
2984
+ pid: process.pid,
2985
+ version,
2986
+ activeConnections: () => connections.size,
2987
+ onShutdown: shutdown
2988
+ };
2989
+ const server = createServer((socket) => {
2990
+ if (shuttingDown) {
2991
+ socket.destroy();
2992
+ return;
2993
+ }
2994
+ onAccept(socket);
2995
+ });
2996
+ const onAccept = (socket) => {
2997
+ const handle = serveAcp({
2998
+ input: socket,
2999
+ output: socket,
3000
+ daemonContext: ctx
3001
+ });
3002
+ const entry = {
3003
+ socket,
3004
+ handle
3005
+ };
3006
+ connections.add(entry);
3007
+ ctx.idleTracker.bump(1);
3008
+ const cleanup = () => {
3009
+ if (!connections.delete(entry)) return;
3010
+ try {
3011
+ handle.dispose();
3012
+ } catch {}
3013
+ ctx.idleTracker.bump(-1);
3014
+ };
3015
+ socket.on("close", cleanup);
3016
+ socket.on("error", cleanup);
3017
+ };
3018
+ server.on("error", (err) => {
3019
+ process.stderr.write(`pi-acp daemon: server error: ${err.message}\n`);
3020
+ });
3021
+ await new Promise((resolve, reject) => {
3022
+ const path = socketPath();
3023
+ server.listen(path, () => resolve());
3024
+ server.once("error", reject);
3025
+ });
3026
+ const controlServer = serveControl(buildControlApp(controlCtx), controlSocketPath());
3027
+ if (process.env["PI_ACP_DAEMON_DEBUG"] === "1") process.stderr.write(`pi-acp daemon: acp=${socketPath()} control=${controlSocketPath()} pid=${process.pid}\n`);
3028
+ process.on("SIGINT", () => {
3029
+ shutdown();
3030
+ });
3031
+ process.on("SIGTERM", () => {
3032
+ shutdown();
3033
+ });
3034
+ }
3035
+ //#endregion
3036
+ export { runDaemon };
2262
3037
 
2263
- //# sourceMappingURL=serve-DLukbpF4.mjs.map
3038
+ //# sourceMappingURL=daemon-tHPrf3qs.mjs.map