@victor-software-house/pi-acp 0.4.0 → 0.6.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 +18 -5
- package/dist/client-CTg5Oiz5.mjs +84 -0
- package/dist/client-CTg5Oiz5.mjs.map +1 -0
- package/dist/daemon-y7gCaXDq.mjs +128 -0
- package/dist/daemon-y7gCaXDq.mjs.map +1 -0
- package/dist/in-process-DZBoxK5h.mjs +31 -0
- package/dist/in-process-DZBoxK5h.mjs.map +1 -0
- package/dist/index.mjs +37 -2201
- package/dist/index.mjs.map +1 -1
- package/dist/serve-DgZht0tv.mjs +2148 -0
- package/dist/serve-DgZht0tv.mjs.map +1 -0
- package/dist/socket-BUNWxnAN.mjs +100 -0
- package/dist/socket-BUNWxnAN.mjs.map +1 -0
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -2,7 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
ACP ([Agent Client Protocol](https://agentclientprotocol.com/get-started/introduction)) adapter for [`pi`](https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent) coding agent.
|
|
4
4
|
|
|
5
|
-
`pi-acp` embeds pi directly via the `@
|
|
5
|
+
`pi-acp` embeds pi directly via the `@earendil-works/pi-coding-agent` SDK and exposes it as an ACP agent over stdio. Each ACP session owns one in-process `AgentSession`.
|
|
6
|
+
|
|
7
|
+
## Specs and decisions
|
|
8
|
+
|
|
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).
|
|
16
|
+
- [`docs/architecture/acp-conformance.md`](docs/architecture/acp-conformance.md) — ACP conformance reference.
|
|
17
|
+
- [`docs/architecture/claude-acp-comparison.md`](docs/architecture/claude-acp-comparison.md) — reference comparison against `claude-agent-acp`.
|
|
6
18
|
|
|
7
19
|
## Status
|
|
8
20
|
|
|
@@ -28,7 +40,8 @@ Active development. ACP compliance is improving steadily. Development is centere
|
|
|
28
40
|
- pi manages sessions in `~/.pi/agent/sessions/...`
|
|
29
41
|
- `session/list` with title fallback from first user message
|
|
30
42
|
- `session/load` replays structured history (text, thinking, tool calls)
|
|
31
|
-
- `
|
|
43
|
+
- `closeSession`, `resumeSession` (stable in ACP v0.12.2+)
|
|
44
|
+
- `unstable_forkSession` (preview)
|
|
32
45
|
- Sessions can be resumed in both `pi` CLI and ACP clients
|
|
33
46
|
- Usage and cost tracking
|
|
34
47
|
- `usage_update` emitted after each agent turn with context size and cost
|
|
@@ -43,8 +56,8 @@ Active development. ACP compliance is improving steadily. Development is centere
|
|
|
43
56
|
|
|
44
57
|
## Prerequisites
|
|
45
58
|
|
|
46
|
-
- Node.js
|
|
47
|
-
- `pi` installed globally: `npm install -g @
|
|
59
|
+
- Node.js 24+ (hard requirement, matches pi runtime)
|
|
60
|
+
- `pi` installed globally (v0.75.3+): `npm install -g @earendil-works/pi-coding-agent`
|
|
48
61
|
- Configure `pi` for your model providers/API keys
|
|
49
62
|
|
|
50
63
|
## Install
|
|
@@ -187,7 +200,7 @@ test/
|
|
|
187
200
|
- `configOptions` is the preferred configuration mechanism. Zed uses it exclusively when present.
|
|
188
201
|
- pi-acp uses direct filesystem access rather than delegating reads/writes to the ACP client. This means pi reads on-disk file versions, not unsaved editor buffers.
|
|
189
202
|
|
|
190
|
-
See [docs/
|
|
203
|
+
See [docs/architecture/acp-conformance.md](docs/architecture/acp-conformance.md) for detailed conformance status.
|
|
191
204
|
|
|
192
205
|
## Release
|
|
193
206
|
|
|
@@ -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,128 @@
|
|
|
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-DgZht0tv.mjs";
|
|
4
|
+
import { createServer } from "node:net";
|
|
5
|
+
//#region src/daemon/context.ts
|
|
6
|
+
function createStubSessionRegistry() {
|
|
7
|
+
const map = /* @__PURE__ */ new Map();
|
|
8
|
+
return {
|
|
9
|
+
register(entry) {
|
|
10
|
+
map.set(entry.sessionId, entry);
|
|
11
|
+
},
|
|
12
|
+
release(sessionId, connectionId) {
|
|
13
|
+
const entry = map.get(sessionId);
|
|
14
|
+
if (!entry) return { unknown: true };
|
|
15
|
+
entry.alsoHeldBy.delete(connectionId);
|
|
16
|
+
if (entry.ownerConnectionId === connectionId && entry.alsoHeldBy.size === 0) {
|
|
17
|
+
map.delete(sessionId);
|
|
18
|
+
return { disposed: true };
|
|
19
|
+
}
|
|
20
|
+
return { disposed: false };
|
|
21
|
+
},
|
|
22
|
+
listAll() {
|
|
23
|
+
return Array.from(map.values());
|
|
24
|
+
},
|
|
25
|
+
get(sessionId) {
|
|
26
|
+
return map.get(sessionId);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function createNoopIdleTracker() {
|
|
31
|
+
return {
|
|
32
|
+
bump() {},
|
|
33
|
+
dispose() {}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function createDaemonContext() {
|
|
37
|
+
return {
|
|
38
|
+
sessionRegistry: createStubSessionRegistry(),
|
|
39
|
+
idleTracker: createNoopIdleTracker()
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
//#endregion
|
|
43
|
+
//#region src/daemon/index.ts
|
|
44
|
+
/**
|
|
45
|
+
* Daemon entry point. Invoked when pi-acp is launched with `--daemon`.
|
|
46
|
+
*
|
|
47
|
+
* Lifecycle:
|
|
48
|
+
* 1. Acquire per-UID lockfile (refuses if another daemon alive).
|
|
49
|
+
* 2. Remove stale socket file if any (left by a dead prior daemon).
|
|
50
|
+
* 3. Construct DaemonContext shared singletons (Phase 1: stubs).
|
|
51
|
+
* 4. Bind socket; accept loop spawns a per-connection serveAcp instance.
|
|
52
|
+
* 5. SIGINT / SIGTERM → graceful shutdown.
|
|
53
|
+
*/
|
|
54
|
+
async function runDaemon() {
|
|
55
|
+
const lockResult = acquireLock();
|
|
56
|
+
if (!lockResult.ok) {
|
|
57
|
+
process.stderr.write(`pi-acp daemon: already running (pid ${lockResult.heldByPid ?? "unknown"})\n`);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
ensureSocketParentDir();
|
|
61
|
+
removeStaleSocketIfAny();
|
|
62
|
+
const ctx = createDaemonContext();
|
|
63
|
+
const connections = /* @__PURE__ */ new Set();
|
|
64
|
+
let shuttingDown = false;
|
|
65
|
+
const server = createServer((socket) => {
|
|
66
|
+
if (shuttingDown) {
|
|
67
|
+
socket.destroy();
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const handle = serveAcp({
|
|
71
|
+
input: socket,
|
|
72
|
+
output: socket,
|
|
73
|
+
daemonContext: ctx
|
|
74
|
+
});
|
|
75
|
+
const entry = {
|
|
76
|
+
socket,
|
|
77
|
+
handle
|
|
78
|
+
};
|
|
79
|
+
connections.add(entry);
|
|
80
|
+
ctx.idleTracker.bump(1);
|
|
81
|
+
const cleanup = () => {
|
|
82
|
+
if (!connections.delete(entry)) return;
|
|
83
|
+
try {
|
|
84
|
+
handle.dispose();
|
|
85
|
+
} catch {}
|
|
86
|
+
ctx.idleTracker.bump(-1);
|
|
87
|
+
};
|
|
88
|
+
socket.on("close", cleanup);
|
|
89
|
+
socket.on("error", cleanup);
|
|
90
|
+
});
|
|
91
|
+
server.on("error", (err) => {
|
|
92
|
+
process.stderr.write(`pi-acp daemon: server error: ${err.message}\n`);
|
|
93
|
+
});
|
|
94
|
+
await new Promise((resolve, reject) => {
|
|
95
|
+
const path = socketPath();
|
|
96
|
+
server.listen(path, () => resolve());
|
|
97
|
+
server.once("error", reject);
|
|
98
|
+
});
|
|
99
|
+
if (process.env["PI_ACP_DAEMON_DEBUG"] === "1") process.stderr.write(`pi-acp daemon: listening on ${socketPath()} (pid ${process.pid})\n`);
|
|
100
|
+
const shutdown = async () => {
|
|
101
|
+
if (shuttingDown) return;
|
|
102
|
+
shuttingDown = true;
|
|
103
|
+
server.close();
|
|
104
|
+
for (const entry of connections) {
|
|
105
|
+
try {
|
|
106
|
+
entry.handle.dispose();
|
|
107
|
+
} catch {}
|
|
108
|
+
try {
|
|
109
|
+
entry.socket.destroy();
|
|
110
|
+
} catch {}
|
|
111
|
+
}
|
|
112
|
+
connections.clear();
|
|
113
|
+
ctx.idleTracker.dispose();
|
|
114
|
+
removeStaleSocketIfAny();
|
|
115
|
+
releaseLock();
|
|
116
|
+
process.exit(0);
|
|
117
|
+
};
|
|
118
|
+
process.on("SIGINT", () => {
|
|
119
|
+
shutdown();
|
|
120
|
+
});
|
|
121
|
+
process.on("SIGTERM", () => {
|
|
122
|
+
shutdown();
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
//#endregion
|
|
126
|
+
export { runDaemon };
|
|
127
|
+
|
|
128
|
+
//# sourceMappingURL=daemon-y7gCaXDq.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"daemon-y7gCaXDq.mjs","names":[],"sources":["../src/daemon/context.ts","../src/daemon/index.ts"],"sourcesContent":["/**\n * Daemon-level shared state injected into per-connection PiAcpAgent instances.\n *\n * Phase 1 lands the interface plus stub implementations. Future phases (PRD-002\n * backends, PRD-003 SessionRegistry / IdleTracker) replace stubs with real\n * implementations registered at daemon startup.\n */\n\nexport interface DaemonContext {\n\t/** Cross-window session registry. Phase 2 of PRD-003. */\n\tsessionRegistry: SessionRegistry;\n\t/** Idle-shutdown tracker. Phase 3 of PRD-003. */\n\tidleTracker: IdleTracker;\n}\n\n/** Phase-1 stub. Replaced in Phase 2 (cross-window session visibility). */\nexport interface SessionRegistry {\n\tregister(entry: SessionEntry): void;\n\trelease(sessionId: string, connectionId: string): { disposed: boolean } | { unknown: true };\n\tlistAll(): SessionEntry[];\n\tget(sessionId: string): SessionEntry | undefined;\n}\n\nexport interface SessionEntry {\n\tsessionId: string;\n\townerConnectionId: string;\n\talsoHeldBy: Set<string>;\n}\n\n/** Phase-1 stub. Replaced in Phase 3 (idle shutdown timer). */\nexport interface IdleTracker {\n\tbump(delta: 1 | -1): void;\n\tdispose(): void;\n}\n\nexport function createStubSessionRegistry(): SessionRegistry {\n\tconst map = new Map<string, SessionEntry>();\n\treturn {\n\t\tregister(entry) {\n\t\t\tmap.set(entry.sessionId, entry);\n\t\t},\n\t\trelease(sessionId, connectionId) {\n\t\t\tconst entry = map.get(sessionId);\n\t\t\tif (!entry) return { unknown: true };\n\t\t\tentry.alsoHeldBy.delete(connectionId);\n\t\t\tif (entry.ownerConnectionId === connectionId && entry.alsoHeldBy.size === 0) {\n\t\t\t\tmap.delete(sessionId);\n\t\t\t\treturn { disposed: true };\n\t\t\t}\n\t\t\treturn { disposed: false };\n\t\t},\n\t\tlistAll() {\n\t\t\treturn Array.from(map.values());\n\t\t},\n\t\tget(sessionId) {\n\t\t\treturn map.get(sessionId);\n\t\t},\n\t};\n}\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: createStubSessionRegistry(),\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":";;;;;AAmCA,SAAgB,4BAA6C;CAC5D,MAAM,sBAAM,IAAI,KAA2B;AAC3C,QAAO;EACN,SAAS,OAAO;AACf,OAAI,IAAI,MAAM,WAAW,MAAM;;EAEhC,QAAQ,WAAW,cAAc;GAChC,MAAM,QAAQ,IAAI,IAAI,UAAU;AAChC,OAAI,CAAC,MAAO,QAAO,EAAE,SAAS,MAAM;AACpC,SAAM,WAAW,OAAO,aAAa;AACrC,OAAI,MAAM,sBAAsB,gBAAgB,MAAM,WAAW,SAAS,GAAG;AAC5E,QAAI,OAAO,UAAU;AACrB,WAAO,EAAE,UAAU,MAAM;;AAE1B,UAAO,EAAE,UAAU,OAAO;;EAE3B,UAAU;AACT,UAAO,MAAM,KAAK,IAAI,QAAQ,CAAC;;EAEhC,IAAI,WAAW;AACd,UAAO,IAAI,IAAI,UAAU;;EAE1B;;AAGF,SAAgB,wBAAqC;AACpD,QAAO;EACN,OAAO;EAGP,UAAU;EAGV;;AAGF,SAAgB,sBAAqC;AACpD,QAAO;EACN,iBAAiB,2BAA2B;EAC5C,aAAa,uBAAuB;EACpC;;;;;;;;;;;;;;AChDF,eAAsB,YAA2B;CAChD,MAAM,aAAa,aAAa;AAChC,KAAI,CAAC,WAAW,IAAI;AACnB,UAAQ,OAAO,MACd,uCAAuC,WAAW,aAAa,UAAU,KACzE;AACD,UAAQ,KAAK,EAAE;;AAGhB,wBAAuB;AACvB,yBAAwB;CAExB,MAAM,MAAqB,qBAAqB;CAChD,MAAM,8BAAc,IAAI,KAAiB;CACzC,IAAI,eAAe;CAEnB,MAAM,SAAiB,cAAc,WAAW;AAC/C,MAAI,cAAc;AACjB,UAAO,SAAS;AAChB;;EAED,MAAM,SAAS,SAAS;GAAE,OAAO;GAAQ,QAAQ;GAAQ,eAAe;GAAK,CAAC;EAC9E,MAAM,QAAoB;GAAE;GAAQ;GAAQ;AAC5C,cAAY,IAAI,MAAM;AACtB,MAAI,YAAY,KAAK,EAAE;EAEvB,MAAM,gBAAsB;AAC3B,OAAI,CAAC,YAAY,OAAO,MAAM,CAAE;AAChC,OAAI;AACH,WAAO,SAAS;WACT;AAGR,OAAI,YAAY,KAAK,GAAG;;AAGzB,SAAO,GAAG,SAAS,QAAQ;AAC3B,SAAO,GAAG,SAAS,QAAQ;GAC1B;AAEF,QAAO,GAAG,UAAU,QAAQ;AAC3B,UAAQ,OAAO,MAAM,gCAAgC,IAAI,QAAQ,IAAI;GACpE;AAEF,OAAM,IAAI,SAAe,SAAS,WAAW;EAC5C,MAAM,OAAO,YAAY;AACzB,SAAO,OAAO,YAAY,SAAS,CAAC;AACpC,SAAO,KAAK,SAAS,OAAO;GAC3B;AAEF,KAAI,QAAQ,IAAI,2BAA2B,IAC1C,SAAQ,OAAO,MAAM,+BAA+B,YAAY,CAAC,QAAQ,QAAQ,IAAI,KAAK;CAG3F,MAAM,WAAW,YAA2B;AAC3C,MAAI,aAAc;AAClB,iBAAe;AACf,SAAO,OAAO;AACd,OAAK,MAAM,SAAS,aAAa;AAChC,OAAI;AACH,UAAM,OAAO,SAAS;WACf;AAGR,OAAI;AACH,UAAM,OAAO,SAAS;WACf;;AAIT,cAAY,OAAO;AACnB,MAAI,YAAY,SAAS;AACzB,0BAAwB;AACxB,eAAa;AACb,UAAQ,KAAK,EAAE;;AAGhB,SAAQ,GAAG,gBAAgB;AACrB,YAAU;GACd;AACF,SAAQ,GAAG,iBAAiB;AACtB,YAAU;GACd"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { t as serveAcp } from "./serve-DgZht0tv.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-DZBoxK5h.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"in-process-DZBoxK5h.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"}
|