@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.
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env bun
2
+ import { n as controlSocketPath } from "./socket-wvV053VI.mjs";
3
+ import { existsSync } from "node:fs";
4
+ //#region src/client/operator.ts
5
+ /**
6
+ * Operator client modes: pi-acp --daemon-status, pi-acp --daemon-stop.
7
+ *
8
+ * Talks to the daemon's control plane (Hono over Unix-domain HTTP) using
9
+ * Bun.fetch's `unix` option. The control socket is separate from the ACP
10
+ * socket, so these commands never disturb live ACP traffic.
11
+ */
12
+ const CONTROL_TIMEOUT_MS = 5e3;
13
+ async function controlFetch(path, init = {}) {
14
+ const sock = controlSocketPath();
15
+ if (!existsSync(sock)) return null;
16
+ try {
17
+ return await Bun.fetch(`http://daemon${path}`, {
18
+ ...init,
19
+ unix: sock,
20
+ signal: AbortSignal.timeout(CONTROL_TIMEOUT_MS)
21
+ });
22
+ } catch {
23
+ return null;
24
+ }
25
+ }
26
+ async function runDaemonStatus() {
27
+ const res = await controlFetch("/status");
28
+ if (res === null) {
29
+ process.stderr.write("pi-acp daemon: not running\n");
30
+ process.exit(1);
31
+ }
32
+ const body = await res.text();
33
+ process.stdout.write(`${body}\n`);
34
+ process.exit(0);
35
+ }
36
+ async function runDaemonStop() {
37
+ const res = await controlFetch("/shutdown", { method: "POST" });
38
+ if (res === null) {
39
+ process.stderr.write("pi-acp daemon: not running\n");
40
+ process.exit(0);
41
+ }
42
+ if (!res.ok) {
43
+ process.stderr.write(`pi-acp daemon: shutdown failed (HTTP ${res.status})\n`);
44
+ process.exit(1);
45
+ }
46
+ process.stderr.write("pi-acp daemon: stopped\n");
47
+ process.exit(0);
48
+ }
49
+ //#endregion
50
+ export { runDaemonStatus, runDaemonStop };
51
+
52
+ //# sourceMappingURL=operator-AtBT_SZT.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"operator-AtBT_SZT.mjs","names":[],"sources":["../src/client/operator.ts"],"sourcesContent":["/**\n * Operator client modes: pi-acp --daemon-status, pi-acp --daemon-stop.\n *\n * Talks to the daemon's control plane (Hono over Unix-domain HTTP) using\n * Bun.fetch's `unix` option. The control socket is separate from the ACP\n * socket, so these commands never disturb live ACP traffic.\n */\n\nimport { existsSync } from \"node:fs\";\nimport { controlSocketPath } from \"@pi-acp/daemon/socket\";\n\nconst CONTROL_TIMEOUT_MS = 5000;\n\nasync function controlFetch(path: string, init: RequestInit = {}): Promise<Response | null> {\n\tconst sock = controlSocketPath();\n\tif (!existsSync(sock)) return null;\n\ttry {\n\t\treturn await Bun.fetch(`http://daemon${path}`, {\n\t\t\t...init,\n\t\t\tunix: sock,\n\t\t\tsignal: AbortSignal.timeout(CONTROL_TIMEOUT_MS),\n\t\t});\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nexport async function runDaemonStatus(): Promise<void> {\n\tconst res = await controlFetch(\"/status\");\n\tif (res === null) {\n\t\tprocess.stderr.write(\"pi-acp daemon: not running\\n\");\n\t\tprocess.exit(1);\n\t}\n\tconst body = await res.text();\n\tprocess.stdout.write(`${body}\\n`);\n\tprocess.exit(0);\n}\n\nexport async function runDaemonStop(): Promise<void> {\n\tconst res = await controlFetch(\"/shutdown\", { method: \"POST\" });\n\tif (res === null) {\n\t\tprocess.stderr.write(\"pi-acp daemon: not running\\n\");\n\t\tprocess.exit(0);\n\t}\n\tif (!res.ok) {\n\t\tprocess.stderr.write(`pi-acp daemon: shutdown failed (HTTP ${res.status})\\n`);\n\t\tprocess.exit(1);\n\t}\n\tprocess.stderr.write(\"pi-acp daemon: stopped\\n\");\n\tprocess.exit(0);\n}\n"],"mappings":";;;;;;;;;;;AAWA,MAAM,qBAAqB;AAE3B,eAAe,aAAa,MAAc,OAAoB,EAAE,EAA4B;CAC3F,MAAM,OAAO,mBAAmB;AAChC,KAAI,CAAC,WAAW,KAAK,CAAE,QAAO;AAC9B,KAAI;AACH,SAAO,MAAM,IAAI,MAAM,gBAAgB,QAAQ;GAC9C,GAAG;GACH,MAAM;GACN,QAAQ,YAAY,QAAQ,mBAAmB;GAC/C,CAAC;SACK;AACP,SAAO;;;AAIT,eAAsB,kBAAiC;CACtD,MAAM,MAAM,MAAM,aAAa,UAAU;AACzC,KAAI,QAAQ,MAAM;AACjB,UAAQ,OAAO,MAAM,+BAA+B;AACpD,UAAQ,KAAK,EAAE;;CAEhB,MAAM,OAAO,MAAM,IAAI,MAAM;AAC7B,SAAQ,OAAO,MAAM,GAAG,KAAK,IAAI;AACjC,SAAQ,KAAK,EAAE;;AAGhB,eAAsB,gBAA+B;CACpD,MAAM,MAAM,MAAM,aAAa,aAAa,EAAE,QAAQ,QAAQ,CAAC;AAC/D,KAAI,QAAQ,MAAM;AACjB,UAAQ,OAAO,MAAM,+BAA+B;AACpD,UAAQ,KAAK,EAAE;;AAEhB,KAAI,CAAC,IAAI,IAAI;AACZ,UAAQ,OAAO,MAAM,wCAAwC,IAAI,OAAO,KAAK;AAC7E,UAAQ,KAAK,EAAE;;AAEhB,SAAQ,OAAO,MAAM,2BAA2B;AAChD,SAAQ,KAAK,EAAE"}
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env bun
2
+ import { n as piCliEntry } from "./pi-package-aHs6rWNo.mjs";
3
+ export { piCliEntry };
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env bun
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ //#region src/pi-package.ts
5
+ /**
6
+ * Helpers to locate the bundled `@earendil-works/pi-coding-agent` package on
7
+ * disk so we can resolve its CLI binary and CHANGELOG without depending on a
8
+ * `pi` command being globally on PATH.
9
+ *
10
+ * Pi is a regular npm dependency — it's installed alongside pi-acp under
11
+ * node_modules. `import.meta.resolve` finds its `package.json`; we derive the
12
+ * sibling paths from there.
13
+ */
14
+ let cached;
15
+ function piPackageRoot() {
16
+ if (cached !== void 0) return cached;
17
+ cached = dirname(fileURLToPath(import.meta.resolve("@earendil-works/pi-coding-agent/package.json")));
18
+ return cached;
19
+ }
20
+ function piCliEntry() {
21
+ return join(piPackageRoot(), "dist", "cli.js");
22
+ }
23
+ function piChangelogPath() {
24
+ return join(piPackageRoot(), "CHANGELOG.md");
25
+ }
26
+ //#endregion
27
+ export { piCliEntry as n, piPackageRoot as r, piChangelogPath as t };
28
+
29
+ //# sourceMappingURL=pi-package-aHs6rWNo.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pi-package-aHs6rWNo.mjs","names":[],"sources":["../src/pi-package.ts"],"sourcesContent":["/**\n * Helpers to locate the bundled `@earendil-works/pi-coding-agent` package on\n * disk so we can resolve its CLI binary and CHANGELOG without depending on a\n * `pi` command being globally on PATH.\n *\n * Pi is a regular npm dependency — it's installed alongside pi-acp under\n * node_modules. `import.meta.resolve` finds its `package.json`; we derive the\n * sibling paths from there.\n */\n\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nlet cached: string | undefined;\n\nexport function piPackageRoot(): string {\n\tif (cached !== undefined) return cached;\n\tconst pkgUrl = import.meta.resolve(\"@earendil-works/pi-coding-agent/package.json\");\n\tcached = dirname(fileURLToPath(pkgUrl));\n\treturn cached;\n}\n\nexport function piCliEntry(): string {\n\treturn join(piPackageRoot(), \"dist\", \"cli.js\");\n}\n\nexport function piChangelogPath(): string {\n\treturn join(piPackageRoot(), \"CHANGELOG.md\");\n}\n"],"mappings":";;;;;;;;;;;;;AAaA,IAAI;AAEJ,SAAgB,gBAAwB;AACvC,KAAI,WAAW,KAAA,EAAW,QAAO;AAEjC,UAAS,QAAQ,cADF,OAAO,KAAK,QAAQ,+CACE,CAAC,CAAC;AACvC,QAAO;;AAGR,SAAgB,aAAqB;AACpC,QAAO,KAAK,eAAe,EAAE,QAAQ,SAAS;;AAG/C,SAAgB,kBAA0B;AACzC,QAAO,KAAK,eAAe,EAAE,eAAe"}
@@ -1,19 +1,23 @@
1
- #!/usr/bin/env node
2
- import { tmpdir, userInfo } from "node:os";
3
- import { closeSync, constants, openSync, readFileSync, statSync, unlinkSync, writeFileSync } from "node:fs";
1
+ #!/usr/bin/env bun
2
+ import { closeSync, constants, mkdirSync, openSync, readFileSync, statSync, unlinkSync, writeFileSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
4
  import { dirname, join } from "node:path";
5
5
  //#region src/daemon/socket.ts
6
6
  /**
7
- * Per-UID socket path. Unix family: under XDG_RUNTIME_DIR (or TMPDIR) so the
8
- * file is bounded to the user. Windows: named pipe in the per-user namespace.
7
+ * Socket + lockfile live under ~/.pi/run/ by default so everything pi-related
8
+ * stays under one tree. PI_ACP_SOCKET_DIR overrides for tests / sandboxing.
9
9
  */
10
+ function baseDir() {
11
+ return process.env["PI_ACP_SOCKET_DIR"] ?? join(homedir(), ".pi", "run");
12
+ }
10
13
  function socketPath() {
11
- if (process.platform === "win32") return `\\\\.\\pipe\\pi-acp-${process.env["USERNAME"] ?? userInfo().username}`;
12
- return join(process.env["PI_ACP_SOCKET_DIR"] ?? process.env["XDG_RUNTIME_DIR"] ?? process.env["TMPDIR"] ?? tmpdir(), `pi-acp-${typeof process.getuid === "function" ? process.getuid() : userInfo().uid !== -1 ? userInfo().uid : 0}.sock`);
14
+ return join(baseDir(), "pi-acp.sock");
15
+ }
16
+ function controlSocketPath() {
17
+ return join(baseDir(), "pi-acp-control.sock");
13
18
  }
14
19
  function lockfilePath() {
15
- if (process.platform === "win32") return join(process.env["TMPDIR"] ?? tmpdir(), `pi-acp-${process.env["USERNAME"] ?? userInfo().username}.lock`);
16
- return `${socketPath()}.lock`;
20
+ return join(baseDir(), "pi-acp.lock");
17
21
  }
18
22
  function errnoCode(err) {
19
23
  if (typeof err === "object" && err !== null && "code" in err) {
@@ -35,6 +39,7 @@ function pidIsAlive(pid) {
35
39
  * and its PID is alive, refuse. If the PID is dead, reclaim.
36
40
  */
37
41
  function acquireLock() {
42
+ ensureSocketParentDir();
38
43
  const path = lockfilePath();
39
44
  for (let attempt = 0; attempt < 2; attempt++) try {
40
45
  const fd = openSync(path, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY, 384);
@@ -71,30 +76,28 @@ function readPidFromLockfile(path) {
71
76
  return;
72
77
  }
73
78
  }
74
- /**
75
- * Remove a socket file left behind by a dead daemon. Safe no-op on Windows
76
- * (named pipes have no filesystem residue) or if the path doesn't exist.
77
- */
78
- function removeStaleSocketIfAny() {
79
- if (process.platform === "win32") return;
80
- const path = socketPath();
79
+ function removeIfExists(path) {
81
80
  try {
82
81
  statSync(path);
83
82
  unlinkSync(path);
84
83
  } catch {}
85
84
  }
86
- /**
87
- * Ensure the directory the socket lives in exists. Skip on Windows (the
88
- * named-pipe namespace is provided by the OS).
89
- */
85
+ function removeStaleSocketIfAny() {
86
+ removeIfExists(socketPath());
87
+ removeIfExists(controlSocketPath());
88
+ }
90
89
  function ensureSocketParentDir() {
91
- if (process.platform === "win32") return;
92
90
  const dir = dirname(socketPath());
93
91
  try {
94
- statSync(dir);
95
- } catch {}
92
+ mkdirSync(dir, {
93
+ recursive: true,
94
+ mode: 448
95
+ });
96
+ } catch (err) {
97
+ if (errnoCode(err) !== "EEXIST") throw err;
98
+ }
96
99
  }
97
100
  //#endregion
98
- export { socketPath as a, removeStaleSocketIfAny as i, ensureSocketParentDir as n, releaseLock as r, acquireLock as t };
101
+ export { removeStaleSocketIfAny as a, releaseLock as i, controlSocketPath as n, socketPath as o, ensureSocketParentDir as r, acquireLock as t };
99
102
 
100
- //# sourceMappingURL=socket-BUNWxnAN.mjs.map
103
+ //# sourceMappingURL=socket-wvV053VI.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"socket-wvV053VI.mjs","names":["fsConstants"],"sources":["../src/daemon/socket.ts"],"sourcesContent":["import {\n\tcloseSync,\n\tconstants as fsConstants,\n\tmkdirSync,\n\topenSync,\n\treadFileSync,\n\tstatSync,\n\tunlinkSync,\n\twriteFileSync,\n} from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\n\n/**\n * Socket + lockfile live under ~/.pi/run/ by default so everything pi-related\n * stays under one tree. PI_ACP_SOCKET_DIR overrides for tests / sandboxing.\n */\nfunction baseDir(): string {\n\treturn process.env[\"PI_ACP_SOCKET_DIR\"] ?? join(homedir(), \".pi\", \"run\");\n}\n\nexport function socketPath(): string {\n\treturn join(baseDir(), \"pi-acp.sock\");\n}\n\nexport function controlSocketPath(): string {\n\treturn join(baseDir(), \"pi-acp-control.sock\");\n}\n\nexport function lockfilePath(): string {\n\treturn join(baseDir(), \"pi-acp.lock\");\n}\n\nfunction errnoCode(err: unknown): string | undefined {\n\tif (typeof err === \"object\" && err !== null && \"code\" in err) {\n\t\tconst code = (err as { code: unknown }).code;\n\t\treturn typeof code === \"string\" ? code : undefined;\n\t}\n\treturn undefined;\n}\n\nfunction pidIsAlive(pid: number): boolean {\n\ttry {\n\t\tprocess.kill(pid, 0);\n\t\treturn true;\n\t} catch (err) {\n\t\tconst code = errnoCode(err);\n\t\tif (code === \"EPERM\") return true;\n\t\treturn false;\n\t}\n}\n\nexport interface LockAcquireResult {\n\tok: boolean;\n\theldByPid?: number;\n}\n\n/**\n * Acquire the daemon lockfile by writing our PID. If a lockfile already exists\n * and its PID is alive, refuse. If the PID is dead, reclaim.\n */\nexport function acquireLock(): LockAcquireResult {\n\tensureSocketParentDir();\n\tconst path = lockfilePath();\n\tfor (let attempt = 0; attempt < 2; attempt++) {\n\t\ttry {\n\t\t\tconst fd = openSync(\n\t\t\t\tpath,\n\t\t\t\tfsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_WRONLY,\n\t\t\t\t0o600,\n\t\t\t);\n\t\t\ttry {\n\t\t\t\twriteFileSync(fd, String(process.pid));\n\t\t\t} finally {\n\t\t\t\tcloseSync(fd);\n\t\t\t}\n\t\t\treturn { ok: true };\n\t\t} catch (err) {\n\t\t\tconst code = errnoCode(err);\n\t\t\tif (code !== \"EEXIST\") throw err;\n\t\t\tconst existing = readPidFromLockfile(path);\n\t\t\tif (existing !== undefined && pidIsAlive(existing)) {\n\t\t\t\treturn { ok: false, heldByPid: existing };\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tunlinkSync(path);\n\t\t\t} catch {\n\t\t\t\t/* ignore — next attempt */\n\t\t\t}\n\t\t}\n\t}\n\treturn { ok: false };\n}\n\nexport function releaseLock(): void {\n\ttry {\n\t\tunlinkSync(lockfilePath());\n\t} catch {\n\t\t/* ignore */\n\t}\n}\n\nfunction readPidFromLockfile(path: string): number | undefined {\n\ttry {\n\t\tconst raw = readFileSync(path, \"utf8\").trim();\n\t\tconst n = Number.parseInt(raw, 10);\n\t\treturn Number.isFinite(n) ? n : undefined;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nfunction removeIfExists(path: string): void {\n\ttry {\n\t\tstatSync(path);\n\t\tunlinkSync(path);\n\t} catch {\n\t\t/* not present — fine */\n\t}\n}\n\nexport function removeStaleSocketIfAny(): void {\n\tremoveIfExists(socketPath());\n\tremoveIfExists(controlSocketPath());\n}\n\nexport function ensureSocketParentDir(): void {\n\tconst dir = dirname(socketPath());\n\ttry {\n\t\tmkdirSync(dir, { recursive: true, mode: 0o700 });\n\t} catch (err) {\n\t\tif (errnoCode(err) !== \"EEXIST\") throw err;\n\t}\n}\n"],"mappings":";;;;;;;;;AAiBA,SAAS,UAAkB;AAC1B,QAAO,QAAQ,IAAI,wBAAwB,KAAK,SAAS,EAAE,OAAO,MAAM;;AAGzE,SAAgB,aAAqB;AACpC,QAAO,KAAK,SAAS,EAAE,cAAc;;AAGtC,SAAgB,oBAA4B;AAC3C,QAAO,KAAK,SAAS,EAAE,sBAAsB;;AAG9C,SAAgB,eAAuB;AACtC,QAAO,KAAK,SAAS,EAAE,cAAc;;AAGtC,SAAS,UAAU,KAAkC;AACpD,KAAI,OAAO,QAAQ,YAAY,QAAQ,QAAQ,UAAU,KAAK;EAC7D,MAAM,OAAQ,IAA0B;AACxC,SAAO,OAAO,SAAS,WAAW,OAAO,KAAA;;;AAK3C,SAAS,WAAW,KAAsB;AACzC,KAAI;AACH,UAAQ,KAAK,KAAK,EAAE;AACpB,SAAO;UACC,KAAK;AAEb,MADa,UAAU,IACf,KAAK,QAAS,QAAO;AAC7B,SAAO;;;;;;;AAaT,SAAgB,cAAiC;AAChD,wBAAuB;CACvB,MAAM,OAAO,cAAc;AAC3B,MAAK,IAAI,UAAU,GAAG,UAAU,GAAG,UAClC,KAAI;EACH,MAAM,KAAK,SACV,MACAA,UAAY,UAAUA,UAAY,SAASA,UAAY,UACvD,IACA;AACD,MAAI;AACH,iBAAc,IAAI,OAAO,QAAQ,IAAI,CAAC;YAC7B;AACT,aAAU,GAAG;;AAEd,SAAO,EAAE,IAAI,MAAM;UACX,KAAK;AAEb,MADa,UAAU,IACf,KAAK,SAAU,OAAM;EAC7B,MAAM,WAAW,oBAAoB,KAAK;AAC1C,MAAI,aAAa,KAAA,KAAa,WAAW,SAAS,CACjD,QAAO;GAAE,IAAI;GAAO,WAAW;GAAU;AAE1C,MAAI;AACH,cAAW,KAAK;UACT;;AAKV,QAAO,EAAE,IAAI,OAAO;;AAGrB,SAAgB,cAAoB;AACnC,KAAI;AACH,aAAW,cAAc,CAAC;SACnB;;AAKT,SAAS,oBAAoB,MAAkC;AAC9D,KAAI;EACH,MAAM,MAAM,aAAa,MAAM,OAAO,CAAC,MAAM;EAC7C,MAAM,IAAI,OAAO,SAAS,KAAK,GAAG;AAClC,SAAO,OAAO,SAAS,EAAE,GAAG,IAAI,KAAA;SACzB;AACP;;;AAIF,SAAS,eAAe,MAAoB;AAC3C,KAAI;AACH,WAAS,KAAK;AACd,aAAW,KAAK;SACT;;AAKT,SAAgB,yBAA+B;AAC9C,gBAAe,YAAY,CAAC;AAC5B,gBAAe,mBAAmB,CAAC;;AAGpC,SAAgB,wBAA8B;CAC7C,MAAM,MAAM,QAAQ,YAAY,CAAC;AACjC,KAAI;AACH,YAAU,KAAK;GAAE,WAAW;GAAM,MAAM;GAAO,CAAC;UACxC,KAAK;AACb,MAAI,UAAU,IAAI,KAAK,SAAU,OAAM"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@victor-software-house/pi-acp",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "description": "ACP adapter for pi coding agent",
5
5
  "license": "MIT",
6
6
  "author": "Victor Software House",
@@ -23,7 +23,7 @@
23
23
  "adapter"
24
24
  ],
25
25
  "engines": {
26
- "node": ">=24"
26
+ "bun": ">=1.3"
27
27
  },
28
28
  "type": "module",
29
29
  "main": "dist/index.mjs",
@@ -36,7 +36,7 @@
36
36
  "scripts": {
37
37
  "dev": "bun src/index.ts",
38
38
  "build": "tsdown",
39
- "start": "node dist/index.js",
39
+ "start": "bun dist/index.mjs",
40
40
  "typecheck": "tsc --noEmit",
41
41
  "lint": "bun run lint:biome && bun run lint:oxlint",
42
42
  "lint:biome": "biome check .",
@@ -50,6 +50,8 @@
50
50
  "dependencies": {
51
51
  "@agentclientprotocol/sdk": "^0.22.1",
52
52
  "@earendil-works/pi-coding-agent": "^0.75.3",
53
+ "hono": "^4.12.21",
54
+ "yaml": "^2.9.0",
53
55
  "zod": "^4.3.6"
54
56
  },
55
57
  "devDependencies": {
@@ -1 +0,0 @@
1
- {"version":3,"file":"auto-spawn-CQ_aaNZA.mjs","names":["delay"],"sources":["../src/client/auto-spawn.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"],"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"}
@@ -1,35 +0,0 @@
1
- #!/usr/bin/env node
2
- import { n as tryConnect, r as waitForSocket, t as autoSpawnDaemon } from "./auto-spawn-CQ_aaNZA.mjs";
3
- //#region src/client/index.ts
4
- const CONNECT_TIMEOUT_MS = 3e3;
5
- async function runClient() {
6
- let socket = await tryConnect();
7
- if (socket === null) {
8
- autoSpawnDaemon();
9
- socket = await waitForSocket(CONNECT_TIMEOUT_MS);
10
- }
11
- if (socket === null) {
12
- 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");
13
- process.exit(1);
14
- }
15
- process.stdin.pipe(socket);
16
- socket.pipe(process.stdout);
17
- let exiting = false;
18
- const exitOnce = (code) => {
19
- if (exiting) return;
20
- exiting = true;
21
- process.exit(code);
22
- };
23
- socket.on("close", () => exitOnce(0));
24
- socket.on("error", (err) => {
25
- process.stderr.write(`pi-acp: socket error: ${err.message}\n`);
26
- exitOnce(1);
27
- });
28
- process.on("SIGINT", () => socket?.destroy());
29
- process.on("SIGTERM", () => socket?.destroy());
30
- process.stdout.on("error", () => exitOnce(0));
31
- }
32
- //#endregion
33
- export { runClient };
34
-
35
- //# sourceMappingURL=client-P4T6wITz.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"client-P4T6wITz.mjs","names":[],"sources":["../src/client/index.ts"],"sourcesContent":["/**\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":";;;AAQA,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"}
@@ -1,338 +0,0 @@
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 { n as version, t as serveAcp } from "./serve-DmuHYqF-.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/control.ts
105
- const FIRST_FRAME_TIMEOUT_MS = 200;
106
- function readMethod(parsed) {
107
- if (typeof parsed !== "object" || parsed === null) return null;
108
- const raw = Reflect.get(parsed, "method");
109
- if (raw === "daemon/status") return "daemon/status";
110
- if (raw === "daemon/shutdown") return "daemon/shutdown";
111
- return null;
112
- }
113
- function readId(parsed) {
114
- if (typeof parsed !== "object" || parsed === null) return null;
115
- const raw = Reflect.get(parsed, "id");
116
- if (typeof raw === "number") return raw;
117
- if (typeof raw === "string") return raw;
118
- return null;
119
- }
120
- async function peekFirstFrame(socket) {
121
- return new Promise((resolve) => {
122
- let buf = Buffer.alloc(0);
123
- let done = false;
124
- const finish = (result) => {
125
- if (done) return;
126
- done = true;
127
- socket.off("data", onData);
128
- clearTimeout(timer);
129
- resolve(result);
130
- };
131
- const onData = (chunk) => {
132
- buf = Buffer.concat([buf, chunk]);
133
- const idx = buf.indexOf(10);
134
- if (idx === -1) return;
135
- const line = buf.subarray(0, idx).toString("utf8");
136
- try {
137
- const parsed = JSON.parse(line);
138
- const method = readMethod(parsed);
139
- if (method !== null) {
140
- finish({
141
- kind: "control",
142
- method,
143
- id: readId(parsed),
144
- buffered: buf
145
- });
146
- return;
147
- }
148
- } catch {}
149
- finish({
150
- kind: "passthrough",
151
- buffered: buf
152
- });
153
- };
154
- const timer = setTimeout(() => finish({
155
- kind: "passthrough",
156
- buffered: buf
157
- }), FIRST_FRAME_TIMEOUT_MS);
158
- timer.unref?.();
159
- socket.on("data", onData);
160
- });
161
- }
162
- function handleStatus(socket, id, control) {
163
- const response = {
164
- jsonrpc: "2.0",
165
- id,
166
- result: {
167
- uptimeSeconds: Math.round((Date.now() - control.startedAt) / 1e3),
168
- connections: control.activeConnections(),
169
- sessions: control.ctx.sessionRegistry.listAll().length,
170
- pid: control.pid,
171
- version: control.version
172
- }
173
- };
174
- socket.write(`${JSON.stringify(response)}\n`);
175
- socket.end();
176
- }
177
- function handleShutdown(socket, id, onShutdown) {
178
- const response = {
179
- jsonrpc: "2.0",
180
- id,
181
- result: {}
182
- };
183
- socket.write(`${JSON.stringify(response)}\n`, () => {
184
- socket.end();
185
- setImmediate(onShutdown);
186
- });
187
- }
188
- //#endregion
189
- //#region src/daemon/idle.ts
190
- const DEFAULT_IDLE_SECONDS = 600;
191
- function createIdleTracker(opts) {
192
- let active = 0;
193
- let timer = null;
194
- const startTimer = () => {
195
- if (timer !== null) return;
196
- timer = setTimeout(opts.onIdle, opts.idleMs);
197
- timer.unref?.();
198
- };
199
- const stopTimer = () => {
200
- if (timer === null) return;
201
- clearTimeout(timer);
202
- timer = null;
203
- };
204
- startTimer();
205
- return {
206
- bump(delta) {
207
- active = Math.max(0, active + delta);
208
- if (active === 0) startTimer();
209
- else stopTimer();
210
- },
211
- dispose() {
212
- stopTimer();
213
- }
214
- };
215
- }
216
- function resolveIdleMs() {
217
- const raw = process.env["PI_ACP_DAEMON_IDLE_SECONDS"];
218
- if (raw === void 0 || raw === "") return DEFAULT_IDLE_SECONDS * 1e3;
219
- const n = Number.parseInt(raw, 10);
220
- if (!Number.isFinite(n) || n <= 0) return DEFAULT_IDLE_SECONDS * 1e3;
221
- return n * 1e3;
222
- }
223
- //#endregion
224
- //#region src/daemon/index.ts
225
- /**
226
- * Daemon entry point. Invoked when pi-acp is launched with `--daemon`.
227
- *
228
- * Lifecycle:
229
- * 1. Acquire per-UID lockfile (refuses if another daemon alive).
230
- * 2. Remove stale socket file if any (left by a dead prior daemon).
231
- * 3. Construct DaemonContext shared singletons.
232
- * 4. Bind socket; accept loop peeks for `daemon/status`/`daemon/shutdown`
233
- * control frames, otherwise hands off to ACP serve.
234
- * 5. Idle shutdown timer fires after PI_ACP_DAEMON_IDLE_SECONDS with no
235
- * active connections. SIGINT / SIGTERM trigger graceful shutdown.
236
- */
237
- async function runDaemon() {
238
- const lockResult = acquireLock();
239
- if (!lockResult.ok) {
240
- process.stderr.write(`pi-acp daemon: already running (pid ${lockResult.heldByPid ?? "unknown"})\n`);
241
- process.exit(1);
242
- }
243
- ensureSocketParentDir();
244
- removeStaleSocketIfAny();
245
- const connections = /* @__PURE__ */ new Set();
246
- let shuttingDown = false;
247
- const startedAt = Date.now();
248
- const shutdown = () => {
249
- if (shuttingDown) return;
250
- shuttingDown = true;
251
- server.close();
252
- for (const entry of connections) {
253
- try {
254
- entry.handle.dispose();
255
- } catch {}
256
- try {
257
- entry.socket.destroy();
258
- } catch {}
259
- }
260
- connections.clear();
261
- ctx.idleTracker.dispose();
262
- removeStaleSocketIfAny();
263
- releaseLock();
264
- process.exit(0);
265
- };
266
- const ctx = createDaemonContext();
267
- ctx.idleTracker = createIdleTracker({
268
- idleMs: resolveIdleMs(),
269
- onIdle: shutdown
270
- });
271
- const controlCtx = {
272
- ctx,
273
- startedAt,
274
- pid: process.pid,
275
- version,
276
- activeConnections: () => connections.size
277
- };
278
- const server = createServer((socket) => {
279
- if (shuttingDown) {
280
- socket.destroy();
281
- return;
282
- }
283
- onAccept(socket);
284
- });
285
- const onAccept = async (socket) => {
286
- const peek = await peekFirstFrame(socket);
287
- if (peek.kind === "control") {
288
- if (peek.method === "daemon/status") {
289
- handleStatus(socket, peek.id ?? null, controlCtx);
290
- return;
291
- }
292
- if (peek.method === "daemon/shutdown") {
293
- handleShutdown(socket, peek.id ?? null, shutdown);
294
- return;
295
- }
296
- }
297
- if (peek.buffered.length > 0) socket.unshift(peek.buffered);
298
- const handle = serveAcp({
299
- input: socket,
300
- output: socket,
301
- daemonContext: ctx
302
- });
303
- const entry = {
304
- socket,
305
- handle
306
- };
307
- connections.add(entry);
308
- ctx.idleTracker.bump(1);
309
- const cleanup = () => {
310
- if (!connections.delete(entry)) return;
311
- try {
312
- handle.dispose();
313
- } catch {}
314
- ctx.idleTracker.bump(-1);
315
- };
316
- socket.on("close", cleanup);
317
- socket.on("error", cleanup);
318
- };
319
- server.on("error", (err) => {
320
- process.stderr.write(`pi-acp daemon: server error: ${err.message}\n`);
321
- });
322
- await new Promise((resolve, reject) => {
323
- const path = socketPath();
324
- server.listen(path, () => resolve());
325
- server.once("error", reject);
326
- });
327
- if (process.env["PI_ACP_DAEMON_DEBUG"] === "1") process.stderr.write(`pi-acp daemon: listening on ${socketPath()} (pid ${process.pid})\n`);
328
- process.on("SIGINT", () => {
329
- shutdown();
330
- });
331
- process.on("SIGTERM", () => {
332
- shutdown();
333
- });
334
- }
335
- //#endregion
336
- export { runDaemon };
337
-
338
- //# sourceMappingURL=daemon-D76_nP59.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"daemon-D76_nP59.mjs","names":["pkgJson.version"],"sources":["../src/daemon/session-registry.ts","../src/daemon/context.ts","../src/daemon/control.ts","../src/daemon/idle.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","import type { Socket } from \"node:net\";\nimport type { DaemonContext } from \"@pi-acp/daemon/context\";\n\n/**\n * Daemon control-frame protocol (in-band on the same socket).\n *\n * Methods recognized BEFORE the ACP handoff:\n * - `daemon/status` — returns runtime info (uptime, connection count,\n * session count, pid, version).\n * - `daemon/shutdown` — graceful shutdown.\n *\n * The first newline-terminated frame received on a new socket is sniffed\n * for these method names. Anything else hands the socket to the normal\n * ACP serve path.\n */\n\nexport interface ControlPeekResult {\n\tkind: \"control\" | \"passthrough\";\n\tmethod?: \"daemon/status\" | \"daemon/shutdown\";\n\tid?: number | string | null;\n\tbuffered: Buffer;\n}\n\nexport interface ControlContext {\n\tctx: DaemonContext;\n\tstartedAt: number;\n\tpid: number;\n\tversion: string;\n\tactiveConnections: () => number;\n}\n\nconst FIRST_FRAME_TIMEOUT_MS = 200;\n\nfunction readMethod(parsed: unknown): \"daemon/status\" | \"daemon/shutdown\" | null {\n\tif (typeof parsed !== \"object\" || parsed === null) return null;\n\tconst raw: unknown = Reflect.get(parsed, \"method\");\n\tif (raw === \"daemon/status\") return \"daemon/status\";\n\tif (raw === \"daemon/shutdown\") return \"daemon/shutdown\";\n\treturn null;\n}\n\nfunction readId(parsed: unknown): number | string | null {\n\tif (typeof parsed !== \"object\" || parsed === null) return null;\n\tconst raw: unknown = Reflect.get(parsed, \"id\");\n\tif (typeof raw === \"number\") return raw;\n\tif (typeof raw === \"string\") return raw;\n\treturn null;\n}\n\nexport async function peekFirstFrame(socket: Socket): Promise<ControlPeekResult> {\n\treturn new Promise((resolve) => {\n\t\tlet buf = Buffer.alloc(0);\n\t\tlet done = false;\n\n\t\tconst finish = (result: ControlPeekResult): void => {\n\t\t\tif (done) return;\n\t\t\tdone = true;\n\t\t\tsocket.off(\"data\", onData);\n\t\t\tclearTimeout(timer);\n\t\t\tresolve(result);\n\t\t};\n\n\t\tconst onData = (chunk: Buffer): void => {\n\t\t\tbuf = Buffer.concat([buf, chunk]);\n\t\t\tconst idx = buf.indexOf(0x0a);\n\t\t\tif (idx === -1) return;\n\t\t\tconst line = buf.subarray(0, idx).toString(\"utf8\");\n\t\t\ttry {\n\t\t\t\tconst parsed: unknown = JSON.parse(line);\n\t\t\t\tconst method = readMethod(parsed);\n\t\t\t\tif (method !== null) {\n\t\t\t\t\tconst id = readId(parsed);\n\t\t\t\t\tfinish({ kind: \"control\", method, id, buffered: buf });\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Not valid JSON — let ACP framing handle it.\n\t\t\t}\n\t\t\tfinish({ kind: \"passthrough\", buffered: buf });\n\t\t};\n\n\t\t// If we get nothing in 200ms, treat as ACP passthrough — the client\n\t\t// may be slow to send the first frame (acceptable; ACP servers idle).\n\t\tconst timer = setTimeout(\n\t\t\t() => finish({ kind: \"passthrough\", buffered: buf }),\n\t\t\tFIRST_FRAME_TIMEOUT_MS,\n\t\t);\n\t\ttimer.unref?.();\n\n\t\tsocket.on(\"data\", onData);\n\t});\n}\n\nexport function handleStatus(\n\tsocket: Socket,\n\tid: number | string | null,\n\tcontrol: ControlContext,\n): void {\n\tconst uptimeSeconds = Math.round((Date.now() - control.startedAt) / 1000);\n\tconst response = {\n\t\tjsonrpc: \"2.0\",\n\t\tid,\n\t\tresult: {\n\t\t\tuptimeSeconds,\n\t\t\tconnections: control.activeConnections(),\n\t\t\tsessions: control.ctx.sessionRegistry.listAll().length,\n\t\t\tpid: control.pid,\n\t\t\tversion: control.version,\n\t\t},\n\t};\n\tsocket.write(`${JSON.stringify(response)}\\n`);\n\tsocket.end();\n}\n\nexport function handleShutdown(\n\tsocket: Socket,\n\tid: number | string | null,\n\tonShutdown: () => void,\n): void {\n\tconst response = { jsonrpc: \"2.0\", id, result: {} };\n\tsocket.write(`${JSON.stringify(response)}\\n`, () => {\n\t\tsocket.end();\n\t\t// Defer one tick so the response is flushed before we tear the\n\t\t// listener down.\n\t\tsetImmediate(onShutdown);\n\t});\n}\n","import type { IdleTracker } from \"@pi-acp/daemon/context\";\n\nconst DEFAULT_IDLE_SECONDS = 600;\n\nexport function createIdleTracker(opts: { idleMs: number; onIdle: () => void }): IdleTracker {\n\tlet active = 0;\n\tlet timer: ReturnType<typeof setTimeout> | null = null;\n\n\tconst startTimer = (): void => {\n\t\tif (timer !== null) return;\n\t\ttimer = setTimeout(opts.onIdle, opts.idleMs);\n\t\ttimer.unref?.();\n\t};\n\n\tconst stopTimer = (): void => {\n\t\tif (timer === null) return;\n\t\tclearTimeout(timer);\n\t\ttimer = null;\n\t};\n\n\t// Cold start: no connections yet. Arm the timer so an unused daemon\n\t// shuts itself down even if the spawning client never connected.\n\tstartTimer();\n\n\treturn {\n\t\tbump(delta: 1 | -1) {\n\t\t\tactive = Math.max(0, active + delta);\n\t\t\tif (active === 0) startTimer();\n\t\t\telse stopTimer();\n\t\t},\n\t\tdispose() {\n\t\t\tstopTimer();\n\t\t},\n\t};\n}\n\nexport function resolveIdleMs(): number {\n\tconst raw = process.env[\"PI_ACP_DAEMON_IDLE_SECONDS\"];\n\tif (raw === undefined || raw === \"\") return DEFAULT_IDLE_SECONDS * 1000;\n\tconst n = Number.parseInt(raw, 10);\n\tif (!Number.isFinite(n) || n <= 0) return DEFAULT_IDLE_SECONDS * 1000;\n\treturn n * 1000;\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.\n * 4. Bind socket; accept loop peeks for `daemon/status`/`daemon/shutdown`\n * control frames, otherwise hands off to ACP serve.\n * 5. Idle shutdown timer fires after PI_ACP_DAEMON_IDLE_SECONDS with no\n * active connections. SIGINT / SIGTERM trigger graceful shutdown.\n */\n\nimport { createServer, type Server, type Socket } from \"node:net\";\nimport { createDaemonContext, type DaemonContext } from \"@pi-acp/daemon/context\";\nimport {\n\ttype ControlContext,\n\thandleShutdown,\n\thandleStatus,\n\tpeekFirstFrame,\n} from \"@pi-acp/daemon/control\";\nimport { createIdleTracker, resolveIdleMs } from \"@pi-acp/daemon/idle\";\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\nimport pkgJson from \"../../package.json\" with { type: \"json\" };\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 connections = new Set<Connection>();\n\tlet shuttingDown = false;\n\tconst startedAt = Date.now();\n\n\tconst shutdown = (): 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\tconst ctx: DaemonContext = createDaemonContext();\n\tctx.idleTracker = createIdleTracker({ idleMs: resolveIdleMs(), onIdle: shutdown });\n\n\tconst controlCtx: ControlContext = {\n\t\tctx,\n\t\tstartedAt,\n\t\tpid: process.pid,\n\t\tversion: pkgJson.version,\n\t\tactiveConnections: () => connections.size,\n\t};\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\tvoid onAccept(socket);\n\t});\n\n\tconst onAccept = async (socket: Socket): Promise<void> => {\n\t\t// Peek the first frame: control method handlers run inline; anything\n\t\t// else proceeds to the ACP path.\n\t\tconst peek = await peekFirstFrame(socket);\n\n\t\tif (peek.kind === \"control\") {\n\t\t\tif (peek.method === \"daemon/status\") {\n\t\t\t\thandleStatus(socket, peek.id ?? null, controlCtx);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (peek.method === \"daemon/shutdown\") {\n\t\t\t\thandleShutdown(socket, peek.id ?? null, shutdown);\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\t// Unshift the peeked bytes back onto the socket so the ACP framing\n\t\t// layer sees them as if they were never consumed.\n\t\tif (peek.buffered.length > 0) socket.unshift(peek.buffered);\n\n\t\tconst handle = serveAcp({\n\t\t\tinput: socket,\n\t\t\toutput: socket,\n\t\t\tdaemonContext: ctx,\n\t\t});\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\t// biome-ignore lint/complexity/useLiteralKeys: env var keys need bracket access for tsc strict mode\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\tprocess.on(\"SIGINT\", () => {\n\t\tshutdown();\n\t});\n\tprocess.on(\"SIGTERM\", () => {\n\t\tshutdown();\n\t});\n}\n"],"mappings":"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"}