@victor-software-house/pi-acp 0.5.0 → 0.7.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/README.md CHANGED
@@ -6,9 +6,13 @@ ACP ([Agent Client Protocol](https://agentclientprotocol.com/get-started/introdu
6
6
 
7
7
  ## Specs and decisions
8
8
 
9
- - [`docs/prd/PRD-001-acp-v013-zed-alignment.md`](docs/prd/PRD-001-acp-v013-zed-alignment.md) — active release PRD (v0.5).
10
- - [`docs/architecture/plan-acp-v013-zed-alignment.md`](docs/architecture/plan-acp-v013-zed-alignment.md) — phased implementation plan.
11
- - [`docs/adr/`](docs/adr/) — architecture decision records (ADR-0001..ADR-0004).
9
+ - [`docs/prd/PRD-001-acp-v013-zed-alignment.md`](docs/prd/PRD-001-acp-v013-zed-alignment.md) — v0.5 release PRD (Shipped).
10
+ - [`docs/prd/PRD-002-portable-runtime.md`](docs/prd/PRD-002-portable-runtime.md) — v0.6 portable runtime + multi-host resource composition (Draft).
11
+ - [`docs/prd/PRD-003-runtime-daemon.md`](docs/prd/PRD-003-runtime-daemon.md) — v0.6 long-running daemon + thin-client binary (Draft).
12
+ - [`docs/architecture/plan-acp-v013-zed-alignment.md`](docs/architecture/plan-acp-v013-zed-alignment.md) — v0.5 phased implementation plan.
13
+ - [`docs/architecture/plan-portable-runtime.md`](docs/architecture/plan-portable-runtime.md) — v0.6 portable-runtime plan.
14
+ - [`docs/architecture/plan-runtime-daemon.md`](docs/architecture/plan-runtime-daemon.md) — v0.6 daemon plan (foundation for portable-runtime backends).
15
+ - [`docs/adr/`](docs/adr/) — architecture decision records (ADR-0001..ADR-0010).
12
16
  - [`docs/architecture/acp-conformance.md`](docs/architecture/acp-conformance.md) — ACP conformance reference.
13
17
  - [`docs/architecture/claude-acp-comparison.md`](docs/architecture/claude-acp-comparison.md) — reference comparison against `claude-agent-acp`.
14
18
 
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env node
2
+ import { a as socketPath } from "./socket-BUNWxnAN.mjs";
3
+ import { spawn } from "node:child_process";
4
+ import { connect } from "node:net";
5
+ import { setTimeout } from "node:timers/promises";
6
+ //#region src/client/auto-spawn.ts
7
+ const POLL_INTERVAL_MS = 50;
8
+ async function tryConnect() {
9
+ const path = socketPath();
10
+ return await new Promise((resolve) => {
11
+ const sock = connect(path);
12
+ const onConnect = () => {
13
+ sock.off("error", onError);
14
+ resolve(sock);
15
+ };
16
+ const onError = () => {
17
+ sock.off("connect", onConnect);
18
+ try {
19
+ sock.destroy();
20
+ } catch {}
21
+ resolve(null);
22
+ };
23
+ sock.once("connect", onConnect);
24
+ sock.once("error", onError);
25
+ });
26
+ }
27
+ async function waitForSocket(timeoutMs) {
28
+ const deadline = Date.now() + timeoutMs;
29
+ while (Date.now() < deadline) {
30
+ const sock = await tryConnect();
31
+ if (sock) return sock;
32
+ await setTimeout(POLL_INTERVAL_MS);
33
+ }
34
+ return null;
35
+ }
36
+ /**
37
+ * Fork pi-acp --daemon detached so the daemon outlives this process.
38
+ *
39
+ * Note: we resolve the entry-point script via `process.argv[1]` so the same
40
+ * bin / dev entry is reused without the client needing to know its own path.
41
+ */
42
+ function autoSpawnDaemon() {
43
+ const entry = process.argv[1];
44
+ if (entry === void 0) throw new Error("pi-acp: cannot resolve entry script for daemon spawn");
45
+ spawn(process.execPath, [entry, "--daemon"], {
46
+ detached: true,
47
+ stdio: "ignore",
48
+ env: process.env
49
+ }).unref();
50
+ }
51
+ //#endregion
52
+ //#region src/client/index.ts
53
+ const CONNECT_TIMEOUT_MS = 3e3;
54
+ async function runClient() {
55
+ let socket = await tryConnect();
56
+ if (socket === null) {
57
+ autoSpawnDaemon();
58
+ socket = await waitForSocket(CONNECT_TIMEOUT_MS);
59
+ }
60
+ if (socket === null) {
61
+ process.stderr.write("pi-acp: failed to connect to daemon socket within 3s. Try `pi-acp --daemon` manually or set PI_ACP_NO_DAEMON=1.\n");
62
+ process.exit(1);
63
+ }
64
+ process.stdin.pipe(socket);
65
+ socket.pipe(process.stdout);
66
+ let exiting = false;
67
+ const exitOnce = (code) => {
68
+ if (exiting) return;
69
+ exiting = true;
70
+ process.exit(code);
71
+ };
72
+ socket.on("close", () => exitOnce(0));
73
+ socket.on("error", (err) => {
74
+ process.stderr.write(`pi-acp: socket error: ${err.message}\n`);
75
+ exitOnce(1);
76
+ });
77
+ process.on("SIGINT", () => socket?.destroy());
78
+ process.on("SIGTERM", () => socket?.destroy());
79
+ process.stdout.on("error", () => exitOnce(0));
80
+ }
81
+ //#endregion
82
+ export { runClient };
83
+
84
+ //# sourceMappingURL=client-CTg5Oiz5.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client-CTg5Oiz5.mjs","names":["delay"],"sources":["../src/client/auto-spawn.ts","../src/client/index.ts"],"sourcesContent":["import { spawn } from \"node:child_process\";\nimport { connect, type Socket } from \"node:net\";\nimport { setTimeout as delay } from \"node:timers/promises\";\n\nimport { socketPath } from \"@pi-acp/daemon/socket\";\n\nconst POLL_INTERVAL_MS = 50;\n\nexport async function tryConnect(): Promise<Socket | null> {\n\tconst path = socketPath();\n\treturn await new Promise<Socket | null>((resolve) => {\n\t\tconst sock = connect(path);\n\t\tconst onConnect = (): void => {\n\t\t\tsock.off(\"error\", onError);\n\t\t\tresolve(sock);\n\t\t};\n\t\tconst onError = (): void => {\n\t\t\tsock.off(\"connect\", onConnect);\n\t\t\ttry {\n\t\t\t\tsock.destroy();\n\t\t\t} catch {\n\t\t\t\t/* best-effort */\n\t\t\t}\n\t\t\tresolve(null);\n\t\t};\n\t\tsock.once(\"connect\", onConnect);\n\t\tsock.once(\"error\", onError);\n\t});\n}\n\nexport async function waitForSocket(timeoutMs: number): Promise<Socket | null> {\n\tconst deadline = Date.now() + timeoutMs;\n\twhile (Date.now() < deadline) {\n\t\tconst sock = await tryConnect();\n\t\tif (sock) return sock;\n\t\tawait delay(POLL_INTERVAL_MS);\n\t}\n\treturn null;\n}\n\n/**\n * Fork pi-acp --daemon detached so the daemon outlives this process.\n *\n * Note: we resolve the entry-point script via `process.argv[1]` so the same\n * bin / dev entry is reused without the client needing to know its own path.\n */\nexport function autoSpawnDaemon(): void {\n\tconst entry = process.argv[1];\n\tif (entry === undefined) {\n\t\tthrow new Error(\"pi-acp: cannot resolve entry script for daemon spawn\");\n\t}\n\tconst child = spawn(process.execPath, [entry, \"--daemon\"], {\n\t\tdetached: true,\n\t\tstdio: \"ignore\",\n\t\tenv: process.env,\n\t});\n\tchild.unref();\n}\n","/**\n * Thin-client entry point. Connects to (or auto-spawns) the daemon, then\n * forwards stdio in both directions.\n */\n\nimport type { Socket } from \"node:net\";\nimport { autoSpawnDaemon, tryConnect, waitForSocket } from \"@pi-acp/client/auto-spawn\";\n\nconst CONNECT_TIMEOUT_MS = 3000;\n\nexport async function runClient(): Promise<void> {\n\tlet socket: Socket | null = await tryConnect();\n\tif (socket === null) {\n\t\tautoSpawnDaemon();\n\t\tsocket = await waitForSocket(CONNECT_TIMEOUT_MS);\n\t}\n\tif (socket === null) {\n\t\tprocess.stderr.write(\n\t\t\t\"pi-acp: failed to connect to daemon socket within 3s. Try `pi-acp --daemon` manually or set PI_ACP_NO_DAEMON=1.\\n\",\n\t\t);\n\t\tprocess.exit(1);\n\t}\n\n\t// Wire both pipes synchronously before yielding. The daemon won't send\n\t// frames until it receives an initialize request, so there's no window\n\t// where socket->stdout drops bytes — but don't reorder these.\n\tprocess.stdin.pipe(socket);\n\tsocket.pipe(process.stdout);\n\n\tlet exiting = false;\n\tconst exitOnce = (code: number): void => {\n\t\tif (exiting) return;\n\t\texiting = true;\n\t\tprocess.exit(code);\n\t};\n\n\tsocket.on(\"close\", () => exitOnce(0));\n\tsocket.on(\"error\", (err) => {\n\t\tprocess.stderr.write(`pi-acp: socket error: ${err.message}\\n`);\n\t\texitOnce(1);\n\t});\n\tprocess.on(\"SIGINT\", () => socket?.destroy());\n\tprocess.on(\"SIGTERM\", () => socket?.destroy());\n\tprocess.stdout.on(\"error\", () => exitOnce(0));\n}\n"],"mappings":";;;;;;AAMA,MAAM,mBAAmB;AAEzB,eAAsB,aAAqC;CAC1D,MAAM,OAAO,YAAY;AACzB,QAAO,MAAM,IAAI,SAAwB,YAAY;EACpD,MAAM,OAAO,QAAQ,KAAK;EAC1B,MAAM,kBAAwB;AAC7B,QAAK,IAAI,SAAS,QAAQ;AAC1B,WAAQ,KAAK;;EAEd,MAAM,gBAAsB;AAC3B,QAAK,IAAI,WAAW,UAAU;AAC9B,OAAI;AACH,SAAK,SAAS;WACP;AAGR,WAAQ,KAAK;;AAEd,OAAK,KAAK,WAAW,UAAU;AAC/B,OAAK,KAAK,SAAS,QAAQ;GAC1B;;AAGH,eAAsB,cAAc,WAA2C;CAC9E,MAAM,WAAW,KAAK,KAAK,GAAG;AAC9B,QAAO,KAAK,KAAK,GAAG,UAAU;EAC7B,MAAM,OAAO,MAAM,YAAY;AAC/B,MAAI,KAAM,QAAO;AACjB,QAAMA,WAAM,iBAAiB;;AAE9B,QAAO;;;;;;;;AASR,SAAgB,kBAAwB;CACvC,MAAM,QAAQ,QAAQ,KAAK;AAC3B,KAAI,UAAU,KAAA,EACb,OAAM,IAAI,MAAM,uDAAuD;AAE1D,OAAM,QAAQ,UAAU,CAAC,OAAO,WAAW,EAAE;EAC1D,UAAU;EACV,OAAO;EACP,KAAK,QAAQ;EACb,CACI,CAAC,OAAO;;;;AChDd,MAAM,qBAAqB;AAE3B,eAAsB,YAA2B;CAChD,IAAI,SAAwB,MAAM,YAAY;AAC9C,KAAI,WAAW,MAAM;AACpB,mBAAiB;AACjB,WAAS,MAAM,cAAc,mBAAmB;;AAEjD,KAAI,WAAW,MAAM;AACpB,UAAQ,OAAO,MACd,oHACA;AACD,UAAQ,KAAK,EAAE;;AAMhB,SAAQ,MAAM,KAAK,OAAO;AAC1B,QAAO,KAAK,QAAQ,OAAO;CAE3B,IAAI,UAAU;CACd,MAAM,YAAY,SAAuB;AACxC,MAAI,QAAS;AACb,YAAU;AACV,UAAQ,KAAK,KAAK;;AAGnB,QAAO,GAAG,eAAe,SAAS,EAAE,CAAC;AACrC,QAAO,GAAG,UAAU,QAAQ;AAC3B,UAAQ,OAAO,MAAM,yBAAyB,IAAI,QAAQ,IAAI;AAC9D,WAAS,EAAE;GACV;AACF,SAAQ,GAAG,gBAAgB,QAAQ,SAAS,CAAC;AAC7C,SAAQ,GAAG,iBAAiB,QAAQ,SAAS,CAAC;AAC9C,SAAQ,OAAO,GAAG,eAAe,SAAS,EAAE,CAAC"}
@@ -0,0 +1,189 @@
1
+ #!/usr/bin/env node
2
+ import { a as socketPath, i as removeStaleSocketIfAny, n as ensureSocketParentDir, r as releaseLock, t as acquireLock } from "./socket-BUNWxnAN.mjs";
3
+ import { t as serveAcp } from "./serve-DLukbpF4.mjs";
4
+ import { createServer } from "node:net";
5
+ //#region src/daemon/session-registry.ts
6
+ function createSessionRegistry() {
7
+ const map = /* @__PURE__ */ new Map();
8
+ return {
9
+ register(input) {
10
+ const entry = {
11
+ sessionId: input.sessionId,
12
+ piSession: input.piSession,
13
+ ownerConnectionId: input.ownerConnectionId,
14
+ alsoHeldBy: /* @__PURE__ */ new Set(),
15
+ cwd: input.cwd,
16
+ sessionFile: input.sessionFile,
17
+ updatedAt: /* @__PURE__ */ new Date()
18
+ };
19
+ map.set(input.sessionId, entry);
20
+ },
21
+ attach(sessionId, connectionId) {
22
+ const entry = map.get(sessionId);
23
+ if (entry === void 0) return void 0;
24
+ if (entry.ownerConnectionId !== connectionId) {
25
+ entry.alsoHeldBy.add(connectionId);
26
+ entry.updatedAt = /* @__PURE__ */ new Date();
27
+ }
28
+ return entry;
29
+ },
30
+ release(sessionId, connectionId) {
31
+ const entry = map.get(sessionId);
32
+ if (entry === void 0) return { kind: "unknown" };
33
+ if (entry.alsoHeldBy.delete(connectionId)) {
34
+ entry.updatedAt = /* @__PURE__ */ new Date();
35
+ if (entry.ownerConnectionId === connectionId && entry.alsoHeldBy.size === 0) {
36
+ map.delete(sessionId);
37
+ return {
38
+ kind: "disposed",
39
+ entry
40
+ };
41
+ }
42
+ return {
43
+ kind: "still-held",
44
+ entry
45
+ };
46
+ }
47
+ if (entry.ownerConnectionId === connectionId) {
48
+ if (entry.alsoHeldBy.size > 0) {
49
+ const next = entry.alsoHeldBy.values().next().value;
50
+ if (next !== void 0) {
51
+ entry.alsoHeldBy.delete(next);
52
+ entry.ownerConnectionId = next;
53
+ entry.updatedAt = /* @__PURE__ */ new Date();
54
+ return {
55
+ kind: "still-held",
56
+ entry
57
+ };
58
+ }
59
+ }
60
+ map.delete(sessionId);
61
+ return {
62
+ kind: "disposed",
63
+ entry
64
+ };
65
+ }
66
+ return {
67
+ kind: "still-held",
68
+ entry
69
+ };
70
+ },
71
+ get(sessionId) {
72
+ return map.get(sessionId);
73
+ },
74
+ listAll() {
75
+ return Array.from(map.values());
76
+ },
77
+ listOwnedBy(connectionId) {
78
+ return Array.from(map.values()).filter((e) => e.ownerConnectionId === connectionId || e.alsoHeldBy.has(connectionId));
79
+ }
80
+ };
81
+ }
82
+ //#endregion
83
+ //#region src/daemon/context.ts
84
+ /**
85
+ * Daemon-level shared state injected into per-connection PiAcpAgent instances.
86
+ *
87
+ * Phase 1 landed the interface + stub IdleTracker.
88
+ * Phase 2 wires the real SessionRegistry.
89
+ * Phase 3 will replace IdleTracker.
90
+ */
91
+ function createNoopIdleTracker() {
92
+ return {
93
+ bump() {},
94
+ dispose() {}
95
+ };
96
+ }
97
+ function createDaemonContext() {
98
+ return {
99
+ sessionRegistry: createSessionRegistry(),
100
+ idleTracker: createNoopIdleTracker()
101
+ };
102
+ }
103
+ //#endregion
104
+ //#region src/daemon/index.ts
105
+ /**
106
+ * Daemon entry point. Invoked when pi-acp is launched with `--daemon`.
107
+ *
108
+ * Lifecycle:
109
+ * 1. Acquire per-UID lockfile (refuses if another daemon alive).
110
+ * 2. Remove stale socket file if any (left by a dead prior daemon).
111
+ * 3. Construct DaemonContext shared singletons (Phase 1: stubs).
112
+ * 4. Bind socket; accept loop spawns a per-connection serveAcp instance.
113
+ * 5. SIGINT / SIGTERM → graceful shutdown.
114
+ */
115
+ async function runDaemon() {
116
+ const lockResult = acquireLock();
117
+ if (!lockResult.ok) {
118
+ process.stderr.write(`pi-acp daemon: already running (pid ${lockResult.heldByPid ?? "unknown"})\n`);
119
+ process.exit(1);
120
+ }
121
+ ensureSocketParentDir();
122
+ removeStaleSocketIfAny();
123
+ const ctx = createDaemonContext();
124
+ const connections = /* @__PURE__ */ new Set();
125
+ let shuttingDown = false;
126
+ const server = createServer((socket) => {
127
+ if (shuttingDown) {
128
+ socket.destroy();
129
+ return;
130
+ }
131
+ const handle = serveAcp({
132
+ input: socket,
133
+ output: socket,
134
+ daemonContext: ctx
135
+ });
136
+ const entry = {
137
+ socket,
138
+ handle
139
+ };
140
+ connections.add(entry);
141
+ ctx.idleTracker.bump(1);
142
+ const cleanup = () => {
143
+ if (!connections.delete(entry)) return;
144
+ try {
145
+ handle.dispose();
146
+ } catch {}
147
+ ctx.idleTracker.bump(-1);
148
+ };
149
+ socket.on("close", cleanup);
150
+ socket.on("error", cleanup);
151
+ });
152
+ server.on("error", (err) => {
153
+ process.stderr.write(`pi-acp daemon: server error: ${err.message}\n`);
154
+ });
155
+ await new Promise((resolve, reject) => {
156
+ const path = socketPath();
157
+ server.listen(path, () => resolve());
158
+ server.once("error", reject);
159
+ });
160
+ if (process.env["PI_ACP_DAEMON_DEBUG"] === "1") process.stderr.write(`pi-acp daemon: listening on ${socketPath()} (pid ${process.pid})\n`);
161
+ const shutdown = async () => {
162
+ if (shuttingDown) return;
163
+ shuttingDown = true;
164
+ server.close();
165
+ for (const entry of connections) {
166
+ try {
167
+ entry.handle.dispose();
168
+ } catch {}
169
+ try {
170
+ entry.socket.destroy();
171
+ } catch {}
172
+ }
173
+ connections.clear();
174
+ ctx.idleTracker.dispose();
175
+ removeStaleSocketIfAny();
176
+ releaseLock();
177
+ process.exit(0);
178
+ };
179
+ process.on("SIGINT", () => {
180
+ shutdown();
181
+ });
182
+ process.on("SIGTERM", () => {
183
+ shutdown();
184
+ });
185
+ }
186
+ //#endregion
187
+ export { runDaemon };
188
+
189
+ //# sourceMappingURL=daemon-irIzm1zJ.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"daemon-irIzm1zJ.mjs","names":[],"sources":["../src/daemon/session-registry.ts","../src/daemon/context.ts","../src/daemon/index.ts"],"sourcesContent":["/**\n * Daemon-level session registry. Maps sessionId -> live AgentSession plus\n * ownership refcount so that closing a session from one client does NOT\n * dispose the underlying pi runtime if another client also holds it.\n */\n\nimport type { AgentSession } from \"@earendil-works/pi-coding-agent\";\n\nexport interface SessionEntry {\n\tsessionId: string;\n\tpiSession: AgentSession;\n\townerConnectionId: string;\n\talsoHeldBy: Set<string>;\n\tcwd: string;\n\tsessionFile: string | undefined;\n\tupdatedAt: Date;\n}\n\nexport interface SessionRegistry {\n\tregister(entry: NewSessionEntry): void;\n\tattach(sessionId: string, connectionId: string): SessionEntry | undefined;\n\trelease(sessionId: string, connectionId: string): ReleaseResult;\n\tget(sessionId: string): SessionEntry | undefined;\n\tlistAll(): SessionEntry[];\n\tlistOwnedBy(connectionId: string): SessionEntry[];\n}\n\nexport interface NewSessionEntry {\n\tsessionId: string;\n\tpiSession: AgentSession;\n\townerConnectionId: string;\n\tcwd: string;\n\tsessionFile: string | undefined;\n}\n\nexport type ReleaseResult =\n\t| { kind: \"disposed\"; entry: SessionEntry }\n\t| { kind: \"still-held\"; entry: SessionEntry }\n\t| { kind: \"unknown\" };\n\nexport function createSessionRegistry(): SessionRegistry {\n\tconst map = new Map<string, SessionEntry>();\n\n\treturn {\n\t\tregister(input) {\n\t\t\tconst entry: SessionEntry = {\n\t\t\t\tsessionId: input.sessionId,\n\t\t\t\tpiSession: input.piSession,\n\t\t\t\townerConnectionId: input.ownerConnectionId,\n\t\t\t\talsoHeldBy: new Set<string>(),\n\t\t\t\tcwd: input.cwd,\n\t\t\t\tsessionFile: input.sessionFile,\n\t\t\t\tupdatedAt: new Date(),\n\t\t\t};\n\t\t\tmap.set(input.sessionId, entry);\n\t\t},\n\n\t\tattach(sessionId, connectionId) {\n\t\t\tconst entry = map.get(sessionId);\n\t\t\tif (entry === undefined) return undefined;\n\t\t\tif (entry.ownerConnectionId !== connectionId) {\n\t\t\t\tentry.alsoHeldBy.add(connectionId);\n\t\t\t\tentry.updatedAt = new Date();\n\t\t\t}\n\t\t\treturn entry;\n\t\t},\n\n\t\trelease(sessionId, connectionId) {\n\t\t\tconst entry = map.get(sessionId);\n\t\t\tif (entry === undefined) return { kind: \"unknown\" };\n\n\t\t\tif (entry.alsoHeldBy.delete(connectionId)) {\n\t\t\t\tentry.updatedAt = new Date();\n\t\t\t\tif (entry.ownerConnectionId === connectionId && entry.alsoHeldBy.size === 0) {\n\t\t\t\t\tmap.delete(sessionId);\n\t\t\t\t\treturn { kind: \"disposed\", entry };\n\t\t\t\t}\n\t\t\t\treturn { kind: \"still-held\", entry };\n\t\t\t}\n\n\t\t\tif (entry.ownerConnectionId === connectionId) {\n\t\t\t\tif (entry.alsoHeldBy.size > 0) {\n\t\t\t\t\t// Hand ownership to one of the still-holders so the entry\n\t\t\t\t\t// keeps a coherent owner record. Pick first by iteration.\n\t\t\t\t\tconst next = entry.alsoHeldBy.values().next().value;\n\t\t\t\t\tif (next !== undefined) {\n\t\t\t\t\t\tentry.alsoHeldBy.delete(next);\n\t\t\t\t\t\tentry.ownerConnectionId = next;\n\t\t\t\t\t\tentry.updatedAt = new Date();\n\t\t\t\t\t\treturn { kind: \"still-held\", entry };\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tmap.delete(sessionId);\n\t\t\t\treturn { kind: \"disposed\", entry };\n\t\t\t}\n\n\t\t\treturn { kind: \"still-held\", entry };\n\t\t},\n\n\t\tget(sessionId) {\n\t\t\treturn map.get(sessionId);\n\t\t},\n\n\t\tlistAll() {\n\t\t\treturn Array.from(map.values());\n\t\t},\n\n\t\tlistOwnedBy(connectionId) {\n\t\t\treturn Array.from(map.values()).filter(\n\t\t\t\t(e) => e.ownerConnectionId === connectionId || e.alsoHeldBy.has(connectionId),\n\t\t\t);\n\t\t},\n\t};\n}\n","/**\n * Daemon-level shared state injected into per-connection PiAcpAgent instances.\n *\n * Phase 1 landed the interface + stub IdleTracker.\n * Phase 2 wires the real SessionRegistry.\n * Phase 3 will replace IdleTracker.\n */\n\nimport { createSessionRegistry, type SessionRegistry } from \"@pi-acp/daemon/session-registry\";\n\nexport interface DaemonContext {\n\t/** Cross-window session registry. PRD-003 FR-5. */\n\tsessionRegistry: SessionRegistry;\n\t/** Idle-shutdown tracker. Stub in Phase 1-2; real in Phase 3. */\n\tidleTracker: IdleTracker;\n}\n\n/** Phase-3 stub. Replaced when idle shutdown lands. */\nexport interface IdleTracker {\n\tbump(delta: 1 | -1): void;\n\tdispose(): void;\n}\n\nexport type { SessionEntry, SessionRegistry } from \"@pi-acp/daemon/session-registry\";\nexport { createSessionRegistry } from \"@pi-acp/daemon/session-registry\";\n\nexport function createNoopIdleTracker(): IdleTracker {\n\treturn {\n\t\tbump() {\n\t\t\t/* phase 3 wires this */\n\t\t},\n\t\tdispose() {\n\t\t\t/* phase 3 wires this */\n\t\t},\n\t};\n}\n\nexport function createDaemonContext(): DaemonContext {\n\treturn {\n\t\tsessionRegistry: createSessionRegistry(),\n\t\tidleTracker: createNoopIdleTracker(),\n\t};\n}\n","/**\n * Daemon entry point. Invoked when pi-acp is launched with `--daemon`.\n *\n * Lifecycle:\n * 1. Acquire per-UID lockfile (refuses if another daemon alive).\n * 2. Remove stale socket file if any (left by a dead prior daemon).\n * 3. Construct DaemonContext shared singletons (Phase 1: stubs).\n * 4. Bind socket; accept loop spawns a per-connection serveAcp instance.\n * 5. SIGINT / SIGTERM → graceful shutdown.\n */\n\nimport { createServer, type Server, type Socket } from \"node:net\";\nimport { createDaemonContext, type DaemonContext } from \"@pi-acp/daemon/context\";\nimport {\n\tacquireLock,\n\tensureSocketParentDir,\n\treleaseLock,\n\tremoveStaleSocketIfAny,\n\tsocketPath,\n} from \"@pi-acp/daemon/socket\";\nimport { type ServeHandle, serveAcp } from \"@pi-acp/runtime/serve\";\n\ninterface Connection {\n\tsocket: Socket;\n\thandle: ServeHandle;\n}\n\nexport async function runDaemon(): Promise<void> {\n\tconst lockResult = acquireLock();\n\tif (!lockResult.ok) {\n\t\tprocess.stderr.write(\n\t\t\t`pi-acp daemon: already running (pid ${lockResult.heldByPid ?? \"unknown\"})\\n`,\n\t\t);\n\t\tprocess.exit(1);\n\t}\n\n\tensureSocketParentDir();\n\tremoveStaleSocketIfAny();\n\n\tconst ctx: DaemonContext = createDaemonContext();\n\tconst connections = new Set<Connection>();\n\tlet shuttingDown = false;\n\n\tconst server: Server = createServer((socket) => {\n\t\tif (shuttingDown) {\n\t\t\tsocket.destroy();\n\t\t\treturn;\n\t\t}\n\t\tconst handle = serveAcp({ input: socket, output: socket, daemonContext: ctx });\n\t\tconst entry: Connection = { socket, handle };\n\t\tconnections.add(entry);\n\t\tctx.idleTracker.bump(1);\n\n\t\tconst cleanup = (): void => {\n\t\t\tif (!connections.delete(entry)) return;\n\t\t\ttry {\n\t\t\t\thandle.dispose();\n\t\t\t} catch {\n\t\t\t\t/* best-effort */\n\t\t\t}\n\t\t\tctx.idleTracker.bump(-1);\n\t\t};\n\n\t\tsocket.on(\"close\", cleanup);\n\t\tsocket.on(\"error\", cleanup);\n\t});\n\n\tserver.on(\"error\", (err) => {\n\t\tprocess.stderr.write(`pi-acp daemon: server error: ${err.message}\\n`);\n\t});\n\n\tawait new Promise<void>((resolve, reject) => {\n\t\tconst path = socketPath();\n\t\tserver.listen(path, () => resolve());\n\t\tserver.once(\"error\", reject);\n\t});\n\n\tif (process.env[\"PI_ACP_DAEMON_DEBUG\"] === \"1\") {\n\t\tprocess.stderr.write(`pi-acp daemon: listening on ${socketPath()} (pid ${process.pid})\\n`);\n\t}\n\n\tconst shutdown = async (): Promise<void> => {\n\t\tif (shuttingDown) return;\n\t\tshuttingDown = true;\n\t\tserver.close();\n\t\tfor (const entry of connections) {\n\t\t\ttry {\n\t\t\t\tentry.handle.dispose();\n\t\t\t} catch {\n\t\t\t\t/* best-effort */\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tentry.socket.destroy();\n\t\t\t} catch {\n\t\t\t\t/* best-effort */\n\t\t\t}\n\t\t}\n\t\tconnections.clear();\n\t\tctx.idleTracker.dispose();\n\t\tremoveStaleSocketIfAny();\n\t\treleaseLock();\n\t\tprocess.exit(0);\n\t};\n\n\tprocess.on(\"SIGINT\", () => {\n\t\tvoid shutdown();\n\t});\n\tprocess.on(\"SIGTERM\", () => {\n\t\tvoid shutdown();\n\t});\n}\n"],"mappings":"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"}
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env node
2
+ import { t as serveAcp } from "./serve-DLukbpF4.mjs";
3
+ //#region src/runtime/in-process.ts
4
+ /**
5
+ * In-process ACP server. The v0.5 codepath, preserved as the `PI_ACP_NO_DAEMON`
6
+ * escape hatch and reused by the daemon's own stdio-bridge fallback.
7
+ *
8
+ * Treats process.stdin/stdout as the ACP transport. Owns the shutdown
9
+ * lifecycle (AgentSideConnection.closed + SIGINT/SIGTERM).
10
+ */
11
+ function runInProcess() {
12
+ const handle = serveAcp({
13
+ input: process.stdin,
14
+ output: process.stdout
15
+ });
16
+ let shuttingDown = false;
17
+ const shutdown = () => {
18
+ if (shuttingDown) return;
19
+ shuttingDown = true;
20
+ handle.dispose();
21
+ process.exit(0);
22
+ };
23
+ handle.connection.closed.then(shutdown);
24
+ process.on("SIGINT", shutdown);
25
+ process.on("SIGTERM", shutdown);
26
+ process.stdout.on("error", () => process.exit(0));
27
+ }
28
+ //#endregion
29
+ export { runInProcess };
30
+
31
+ //# sourceMappingURL=in-process-DcAV6Sgx.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"in-process-DcAV6Sgx.mjs","names":[],"sources":["../src/runtime/in-process.ts"],"sourcesContent":["/**\n * In-process ACP server. The v0.5 codepath, preserved as the `PI_ACP_NO_DAEMON`\n * escape hatch and reused by the daemon's own stdio-bridge fallback.\n *\n * Treats process.stdin/stdout as the ACP transport. Owns the shutdown\n * lifecycle (AgentSideConnection.closed + SIGINT/SIGTERM).\n */\n\nimport { serveAcp } from \"@pi-acp/runtime/serve\";\n\nexport function runInProcess(): void {\n\tconst handle = serveAcp({\n\t\tinput: process.stdin,\n\t\toutput: process.stdout,\n\t\t// No DaemonContext: behavior identical to v0.5.\n\t});\n\n\tlet shuttingDown = false;\n\tconst shutdown = (): void => {\n\t\tif (shuttingDown) return;\n\t\tshuttingDown = true;\n\t\thandle.dispose();\n\t\tprocess.exit(0);\n\t};\n\n\tvoid handle.connection.closed.then(shutdown);\n\n\tprocess.on(\"SIGINT\", shutdown);\n\tprocess.on(\"SIGTERM\", shutdown);\n\tprocess.stdout.on(\"error\", () => process.exit(0));\n}\n"],"mappings":";;;;;;;;;;AAUA,SAAgB,eAAqB;CACpC,MAAM,SAAS,SAAS;EACvB,OAAO,QAAQ;EACf,QAAQ,QAAQ;EAEhB,CAAC;CAEF,IAAI,eAAe;CACnB,MAAM,iBAAuB;AAC5B,MAAI,aAAc;AAClB,iBAAe;AACf,SAAO,SAAS;AAChB,UAAQ,KAAK,EAAE;;AAGX,QAAO,WAAW,OAAO,KAAK,SAAS;AAE5C,SAAQ,GAAG,UAAU,SAAS;AAC9B,SAAQ,GAAG,WAAW,SAAS;AAC/B,SAAQ,OAAO,GAAG,eAAe,QAAQ,KAAK,EAAE,CAAC"}