@victor-software-house/pi-acp 0.7.0 → 0.8.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.
@@ -49,36 +49,6 @@ function autoSpawnDaemon() {
49
49
  }).unref();
50
50
  }
51
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 };
52
+ export { tryConnect as n, waitForSocket as r, autoSpawnDaemon as t };
83
53
 
84
- //# sourceMappingURL=client-CTg5Oiz5.mjs.map
54
+ //# sourceMappingURL=auto-spawn-CQ_aaNZA.mjs.map
@@ -0,0 +1 @@
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"}
@@ -0,0 +1,35 @@
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
@@ -0,0 +1 @@
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,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
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";
3
+ import { n as version, t as serveAcp } from "./serve-DmuHYqF-.mjs";
4
4
  import { createServer } from "node:net";
5
5
  //#region src/daemon/session-registry.ts
6
6
  function createSessionRegistry() {
@@ -101,6 +101,126 @@ function createDaemonContext() {
101
101
  };
102
102
  }
103
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
104
224
  //#region src/daemon/index.ts
105
225
  /**
106
226
  * Daemon entry point. Invoked when pi-acp is launched with `--daemon`.
@@ -108,9 +228,11 @@ function createDaemonContext() {
108
228
  * Lifecycle:
109
229
  * 1. Acquire per-UID lockfile (refuses if another daemon alive).
110
230
  * 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.
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.
114
236
  */
115
237
  async function runDaemon() {
116
238
  const lockResult = acquireLock();
@@ -120,14 +242,59 @@ async function runDaemon() {
120
242
  }
121
243
  ensureSocketParentDir();
122
244
  removeStaleSocketIfAny();
123
- const ctx = createDaemonContext();
124
245
  const connections = /* @__PURE__ */ new Set();
125
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
+ };
126
278
  const server = createServer((socket) => {
127
279
  if (shuttingDown) {
128
280
  socket.destroy();
129
281
  return;
130
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);
131
298
  const handle = serveAcp({
132
299
  input: socket,
133
300
  output: socket,
@@ -148,7 +315,7 @@ async function runDaemon() {
148
315
  };
149
316
  socket.on("close", cleanup);
150
317
  socket.on("error", cleanup);
151
- });
318
+ };
152
319
  server.on("error", (err) => {
153
320
  process.stderr.write(`pi-acp daemon: server error: ${err.message}\n`);
154
321
  });
@@ -158,24 +325,6 @@ async function runDaemon() {
158
325
  server.once("error", reject);
159
326
  });
160
327
  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
328
  process.on("SIGINT", () => {
180
329
  shutdown();
181
330
  });
@@ -186,4 +335,4 @@ async function runDaemon() {
186
335
  //#endregion
187
336
  export { runDaemon };
188
337
 
189
- //# sourceMappingURL=daemon-irIzm1zJ.mjs.map
338
+ //# sourceMappingURL=daemon-D76_nP59.mjs.map
@@ -0,0 +1 @@
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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"}
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as serveAcp } from "./serve-DLukbpF4.mjs";
2
+ import { t as serveAcp } from "./serve-DmuHYqF-.mjs";
3
3
  //#region src/runtime/in-process.ts
4
4
  /**
5
5
  * In-process ACP server. The v0.5 codepath, preserved as the `PI_ACP_NO_DAEMON`
@@ -28,4 +28,4 @@ function runInProcess() {
28
28
  //#endregion
29
29
  export { runInProcess };
30
30
 
31
- //# sourceMappingURL=in-process-DcAV6Sgx.mjs.map
31
+ //# sourceMappingURL=in-process-Byo-O30j.mjs.map
@@ -1 +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"}
1
+ {"version":3,"file":"in-process-Byo-O30j.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"}
package/dist/index.mjs CHANGED
@@ -25,13 +25,19 @@ import { platform } from "node:os";
25
25
  const argv = process.argv.slice(2);
26
26
  if (argv.includes("--terminal-login")) await runTerminalLogin();
27
27
  else if (argv.includes("--daemon")) {
28
- const { runDaemon } = await import("./daemon-irIzm1zJ.mjs");
28
+ const { runDaemon } = await import("./daemon-D76_nP59.mjs");
29
29
  await runDaemon();
30
+ } else if (argv.includes("--daemon-status")) {
31
+ const { runDaemonStatus } = await import("./operator-DURoHk8w.mjs");
32
+ await runDaemonStatus();
33
+ } else if (argv.includes("--daemon-stop")) {
34
+ const { runDaemonStop } = await import("./operator-DURoHk8w.mjs");
35
+ await runDaemonStop();
30
36
  } else if (argv.includes("--no-daemon") || process.env["PI_ACP_NO_DAEMON"] === "1") {
31
- const { runInProcess } = await import("./in-process-DcAV6Sgx.mjs");
37
+ const { runInProcess } = await import("./in-process-Byo-O30j.mjs");
32
38
  runInProcess();
33
39
  } else {
34
- const { runClient } = await import("./client-CTg5Oiz5.mjs");
40
+ const { runClient } = await import("./client-P4T6wITz.mjs");
35
41
  await runClient();
36
42
  }
37
43
  async function runTerminalLogin() {
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["/**\n * pi-acp entry point. Dispatches between four modes:\n *\n * --terminal-login → foreground pi for interactive auth (v0.5 flow)\n * --daemon → long-running orchestrator (PRD-003)\n * --no-daemon | PI_ACP_NO_DAEMON=1 → v0.5 in-process server (escape hatch)\n * (default) → thin client; auto-spawns daemon\n *\n * ACP transports JSON-RPC NDJSON over stdout. Any stray byte poisons the\n * protocol stream. Redirect console.{log,info,warn,debug} to stderr at boot\n * so transitive deps (or our own debug prints) can't corrupt it.\n */\n\nimport { platform } from \"node:os\";\n\n{\n\tconst toStderr = (...args: unknown[]): void => {\n\t\tprocess.stderr.write(\n\t\t\t`${args.map((a) => (typeof a === \"string\" ? a : JSON.stringify(a))).join(\" \")}\\n`,\n\t\t);\n\t};\n\tconsole.log = toStderr;\n\tconsole.info = toStderr;\n\tconsole.warn = toStderr;\n\tconsole.debug = toStderr;\n}\n\nconst argv = process.argv.slice(2);\n\nif (argv.includes(\"--terminal-login\")) {\n\tawait runTerminalLogin();\n} else if (argv.includes(\"--daemon\")) {\n\tconst { runDaemon } = await import(\"@pi-acp/daemon/index\");\n\tawait runDaemon();\n} else if (argv.includes(\"--no-daemon\") || process.env[\"PI_ACP_NO_DAEMON\"] === \"1\") {\n\tconst { runInProcess } = await import(\"@pi-acp/runtime/in-process\");\n\trunInProcess();\n} else {\n\tconst { runClient } = await import(\"@pi-acp/client/index\");\n\tawait runClient();\n}\n\nasync function runTerminalLogin(): Promise<void> {\n\tconst { spawnSync } = await import(\"node:child_process\");\n\tconst isWindows = platform() === \"win32\";\n\tconst cmd = process.env[\"PI_ACP_PI_COMMAND\"] ?? (isWindows ? \"pi.cmd\" : \"pi\");\n\tconst res = spawnSync(cmd, [], { stdio: \"inherit\", env: process.env });\n\n\tif (res.error && \"code\" in res.error && res.error.code === \"ENOENT\") {\n\t\tprocess.stderr.write(\n\t\t\t`pi-acp: could not start pi (command not found: ${cmd}). ` +\n\t\t\t\t\"Install via `npm install -g @earendil-works/pi-coding-agent` \" +\n\t\t\t\t\"or ensure `pi` is on your PATH.\\n\",\n\t\t);\n\t\tprocess.exit(1);\n\t}\n\n\tprocess.exit(typeof res.status === \"number\" ? res.status : 1);\n}\n"],"mappings":";;;;;;;;;;;;;;;AAeA;CACC,MAAM,YAAY,GAAG,SAA0B;AAC9C,UAAQ,OAAO,MACd,GAAG,KAAK,KAAK,MAAO,OAAO,MAAM,WAAW,IAAI,KAAK,UAAU,EAAE,CAAE,CAAC,KAAK,IAAI,CAAC,IAC9E;;AAEF,SAAQ,MAAM;AACd,SAAQ,OAAO;AACf,SAAQ,OAAO;AACf,SAAQ,QAAQ;;AAGjB,MAAM,OAAO,QAAQ,KAAK,MAAM,EAAE;AAElC,IAAI,KAAK,SAAS,mBAAmB,CACpC,OAAM,kBAAkB;SACd,KAAK,SAAS,WAAW,EAAE;CACrC,MAAM,EAAE,cAAc,MAAM,OAAO;AACnC,OAAM,WAAW;WACP,KAAK,SAAS,cAAc,IAAI,QAAQ,IAAI,wBAAwB,KAAK;CACnF,MAAM,EAAE,iBAAiB,MAAM,OAAO;AACtC,eAAc;OACR;CACN,MAAM,EAAE,cAAc,MAAM,OAAO;AACnC,OAAM,WAAW;;AAGlB,eAAe,mBAAkC;CAChD,MAAM,EAAE,cAAc,MAAM,OAAO;CACnC,MAAM,YAAY,UAAU,KAAK;CACjC,MAAM,MAAM,QAAQ,IAAI,yBAAyB,YAAY,WAAW;CACxE,MAAM,MAAM,UAAU,KAAK,EAAE,EAAE;EAAE,OAAO;EAAW,KAAK,QAAQ;EAAK,CAAC;AAEtE,KAAI,IAAI,SAAS,UAAU,IAAI,SAAS,IAAI,MAAM,SAAS,UAAU;AACpE,UAAQ,OAAO,MACd,kDAAkD,IAAI;EAGtD;AACD,UAAQ,KAAK,EAAE;;AAGhB,SAAQ,KAAK,OAAO,IAAI,WAAW,WAAW,IAAI,SAAS,EAAE"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["/**\n * pi-acp entry point. Dispatches between four modes:\n *\n * --terminal-login → foreground pi for interactive auth (v0.5 flow)\n * --daemon → long-running orchestrator (PRD-003)\n * --no-daemon | PI_ACP_NO_DAEMON=1 → v0.5 in-process server (escape hatch)\n * (default) → thin client; auto-spawns daemon\n *\n * ACP transports JSON-RPC NDJSON over stdout. Any stray byte poisons the\n * protocol stream. Redirect console.{log,info,warn,debug} to stderr at boot\n * so transitive deps (or our own debug prints) can't corrupt it.\n */\n\nimport { platform } from \"node:os\";\n\n{\n\tconst toStderr = (...args: unknown[]): void => {\n\t\tprocess.stderr.write(\n\t\t\t`${args.map((a) => (typeof a === \"string\" ? a : JSON.stringify(a))).join(\" \")}\\n`,\n\t\t);\n\t};\n\tconsole.log = toStderr;\n\tconsole.info = toStderr;\n\tconsole.warn = toStderr;\n\tconsole.debug = toStderr;\n}\n\nconst argv = process.argv.slice(2);\n\nif (argv.includes(\"--terminal-login\")) {\n\tawait runTerminalLogin();\n} else if (argv.includes(\"--daemon\")) {\n\tconst { runDaemon } = await import(\"@pi-acp/daemon/index\");\n\tawait runDaemon();\n} else if (argv.includes(\"--daemon-status\")) {\n\tconst { runDaemonStatus } = await import(\"@pi-acp/client/operator\");\n\tawait runDaemonStatus();\n} else if (argv.includes(\"--daemon-stop\")) {\n\tconst { runDaemonStop } = await import(\"@pi-acp/client/operator\");\n\tawait runDaemonStop();\n} else if (argv.includes(\"--no-daemon\") || process.env[\"PI_ACP_NO_DAEMON\"] === \"1\") {\n\tconst { runInProcess } = await import(\"@pi-acp/runtime/in-process\");\n\trunInProcess();\n} else {\n\tconst { runClient } = await import(\"@pi-acp/client/index\");\n\tawait runClient();\n}\n\nasync function runTerminalLogin(): Promise<void> {\n\tconst { spawnSync } = await import(\"node:child_process\");\n\tconst isWindows = platform() === \"win32\";\n\tconst cmd = process.env[\"PI_ACP_PI_COMMAND\"] ?? (isWindows ? \"pi.cmd\" : \"pi\");\n\tconst res = spawnSync(cmd, [], { stdio: \"inherit\", env: process.env });\n\n\tif (res.error && \"code\" in res.error && res.error.code === \"ENOENT\") {\n\t\tprocess.stderr.write(\n\t\t\t`pi-acp: could not start pi (command not found: ${cmd}). ` +\n\t\t\t\t\"Install via `npm install -g @earendil-works/pi-coding-agent` \" +\n\t\t\t\t\"or ensure `pi` is on your PATH.\\n\",\n\t\t);\n\t\tprocess.exit(1);\n\t}\n\n\tprocess.exit(typeof res.status === \"number\" ? res.status : 1);\n}\n"],"mappings":";;;;;;;;;;;;;;;AAeA;CACC,MAAM,YAAY,GAAG,SAA0B;AAC9C,UAAQ,OAAO,MACd,GAAG,KAAK,KAAK,MAAO,OAAO,MAAM,WAAW,IAAI,KAAK,UAAU,EAAE,CAAE,CAAC,KAAK,IAAI,CAAC,IAC9E;;AAEF,SAAQ,MAAM;AACd,SAAQ,OAAO;AACf,SAAQ,OAAO;AACf,SAAQ,QAAQ;;AAGjB,MAAM,OAAO,QAAQ,KAAK,MAAM,EAAE;AAElC,IAAI,KAAK,SAAS,mBAAmB,CACpC,OAAM,kBAAkB;SACd,KAAK,SAAS,WAAW,EAAE;CACrC,MAAM,EAAE,cAAc,MAAM,OAAO;AACnC,OAAM,WAAW;WACP,KAAK,SAAS,kBAAkB,EAAE;CAC5C,MAAM,EAAE,oBAAoB,MAAM,OAAO;AACzC,OAAM,iBAAiB;WACb,KAAK,SAAS,gBAAgB,EAAE;CAC1C,MAAM,EAAE,kBAAkB,MAAM,OAAO;AACvC,OAAM,eAAe;WACX,KAAK,SAAS,cAAc,IAAI,QAAQ,IAAI,wBAAwB,KAAK;CACnF,MAAM,EAAE,iBAAiB,MAAM,OAAO;AACtC,eAAc;OACR;CACN,MAAM,EAAE,cAAc,MAAM,OAAO;AACnC,OAAM,WAAW;;AAGlB,eAAe,mBAAkC;CAChD,MAAM,EAAE,cAAc,MAAM,OAAO;CACnC,MAAM,YAAY,UAAU,KAAK;CACjC,MAAM,MAAM,QAAQ,IAAI,yBAAyB,YAAY,WAAW;CACxE,MAAM,MAAM,UAAU,KAAK,EAAE,EAAE;EAAE,OAAO;EAAW,KAAK,QAAQ;EAAK,CAAC;AAEtE,KAAI,IAAI,SAAS,UAAU,IAAI,SAAS,IAAI,MAAM,SAAS,UAAU;AACpE,UAAQ,OAAO,MACd,kDAAkD,IAAI;EAGtD;AACD,UAAQ,KAAK,EAAE;;AAGhB,SAAQ,KAAK,OAAO,IAAI,WAAW,WAAW,IAAI,SAAS,EAAE"}
@@ -0,0 +1,85 @@
1
+ #!/usr/bin/env node
2
+ import { n as tryConnect } from "./auto-spawn-CQ_aaNZA.mjs";
3
+ //#region src/client/operator.ts
4
+ /**
5
+ * Operator client modes: pi-acp --daemon-status, pi-acp --daemon-stop.
6
+ *
7
+ * Both connect to the daemon socket and send a `daemon/<method>` JSON-RPC
8
+ * frame; the daemon handles them inline (see src/daemon/control.ts) before
9
+ * the normal ACP handoff.
10
+ */
11
+ const CONTROL_TIMEOUT_MS = 5e3;
12
+ async function runDaemonStatus() {
13
+ const socket = await tryConnect();
14
+ if (socket === null) {
15
+ process.stderr.write("pi-acp daemon: not running\n");
16
+ process.exit(1);
17
+ }
18
+ const frame = JSON.stringify({
19
+ jsonrpc: "2.0",
20
+ id: 1,
21
+ method: "daemon/status"
22
+ });
23
+ socket.write(`${frame}\n`);
24
+ const response = await readSingleFrame(socket);
25
+ socket.destroy();
26
+ if (response === null) {
27
+ process.stderr.write("pi-acp daemon: no response\n");
28
+ process.exit(1);
29
+ }
30
+ process.stdout.write(`${response}\n`);
31
+ process.exit(0);
32
+ }
33
+ async function runDaemonStop() {
34
+ const socket = await tryConnect();
35
+ if (socket === null) {
36
+ process.stderr.write("pi-acp daemon: not running\n");
37
+ process.exit(0);
38
+ }
39
+ const frame = JSON.stringify({
40
+ jsonrpc: "2.0",
41
+ id: 1,
42
+ method: "daemon/shutdown"
43
+ });
44
+ socket.write(`${frame}\n`);
45
+ if (await readSingleFrame(socket) === null) {
46
+ process.stderr.write("pi-acp daemon: no response (already exiting?)\n");
47
+ process.exit(0);
48
+ }
49
+ await new Promise((resolve) => {
50
+ const timer = setTimeout(resolve, CONTROL_TIMEOUT_MS);
51
+ timer.unref?.();
52
+ socket.once("close", () => {
53
+ clearTimeout(timer);
54
+ resolve();
55
+ });
56
+ });
57
+ process.stderr.write("pi-acp daemon: stopped\n");
58
+ process.exit(0);
59
+ }
60
+ async function readSingleFrame(socket) {
61
+ return new Promise((resolve) => {
62
+ let buf = Buffer.alloc(0);
63
+ const timer = setTimeout(() => {
64
+ cleanup();
65
+ resolve(null);
66
+ }, CONTROL_TIMEOUT_MS);
67
+ timer.unref?.();
68
+ const cleanup = () => {
69
+ clearTimeout(timer);
70
+ socket.off("data", onData);
71
+ };
72
+ const onData = (chunk) => {
73
+ buf = Buffer.concat([buf, chunk]);
74
+ const idx = buf.indexOf(10);
75
+ if (idx === -1) return;
76
+ cleanup();
77
+ resolve(buf.subarray(0, idx).toString("utf8"));
78
+ };
79
+ socket.on("data", onData);
80
+ });
81
+ }
82
+ //#endregion
83
+ export { runDaemonStatus, runDaemonStop };
84
+
85
+ //# sourceMappingURL=operator-DURoHk8w.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"operator-DURoHk8w.mjs","names":[],"sources":["../src/client/operator.ts"],"sourcesContent":["/**\n * Operator client modes: pi-acp --daemon-status, pi-acp --daemon-stop.\n *\n * Both connect to the daemon socket and send a `daemon/<method>` JSON-RPC\n * frame; the daemon handles them inline (see src/daemon/control.ts) before\n * the normal ACP handoff.\n */\n\nimport { tryConnect } from \"@pi-acp/client/auto-spawn\";\n\nconst CONTROL_TIMEOUT_MS = 5000;\n\nexport async function runDaemonStatus(): Promise<void> {\n\tconst socket = await tryConnect();\n\tif (socket === null) {\n\t\tprocess.stderr.write(\"pi-acp daemon: not running\\n\");\n\t\tprocess.exit(1);\n\t}\n\n\tconst id = 1;\n\tconst frame = JSON.stringify({ jsonrpc: \"2.0\", id, method: \"daemon/status\" });\n\tsocket.write(`${frame}\\n`);\n\n\tconst response = await readSingleFrame(socket);\n\tsocket.destroy();\n\n\tif (response === null) {\n\t\tprocess.stderr.write(\"pi-acp daemon: no response\\n\");\n\t\tprocess.exit(1);\n\t}\n\n\tprocess.stdout.write(`${response}\\n`);\n\tprocess.exit(0);\n}\n\nexport async function runDaemonStop(): Promise<void> {\n\tconst socket = await tryConnect();\n\tif (socket === null) {\n\t\tprocess.stderr.write(\"pi-acp daemon: not running\\n\");\n\t\tprocess.exit(0);\n\t}\n\n\tconst id = 1;\n\tconst frame = JSON.stringify({ jsonrpc: \"2.0\", id, method: \"daemon/shutdown\" });\n\tsocket.write(`${frame}\\n`);\n\n\tconst response = await readSingleFrame(socket);\n\tif (response === null) {\n\t\tprocess.stderr.write(\"pi-acp daemon: no response (already exiting?)\\n\");\n\t\tprocess.exit(0);\n\t}\n\n\t// Wait for the socket to close, which signals daemon shutdown completed.\n\tawait new Promise<void>((resolve) => {\n\t\tconst timer = setTimeout(resolve, CONTROL_TIMEOUT_MS);\n\t\ttimer.unref?.();\n\t\tsocket.once(\"close\", () => {\n\t\t\tclearTimeout(timer);\n\t\t\tresolve();\n\t\t});\n\t});\n\n\tprocess.stderr.write(\"pi-acp daemon: stopped\\n\");\n\tprocess.exit(0);\n}\n\nasync function readSingleFrame(socket: NodeJS.ReadableStream): Promise<string | null> {\n\treturn new Promise((resolve) => {\n\t\tlet buf = Buffer.alloc(0);\n\t\tconst timer = setTimeout(() => {\n\t\t\tcleanup();\n\t\t\tresolve(null);\n\t\t}, CONTROL_TIMEOUT_MS);\n\t\ttimer.unref?.();\n\t\tconst cleanup = (): void => {\n\t\t\tclearTimeout(timer);\n\t\t\tsocket.off(\"data\", onData);\n\t\t};\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\tcleanup();\n\t\t\tresolve(buf.subarray(0, idx).toString(\"utf8\"));\n\t\t};\n\t\tsocket.on(\"data\", onData);\n\t});\n}\n"],"mappings":";;;;;;;;;;AAUA,MAAM,qBAAqB;AAE3B,eAAsB,kBAAiC;CACtD,MAAM,SAAS,MAAM,YAAY;AACjC,KAAI,WAAW,MAAM;AACpB,UAAQ,OAAO,MAAM,+BAA+B;AACpD,UAAQ,KAAK,EAAE;;CAIhB,MAAM,QAAQ,KAAK,UAAU;EAAE,SAAS;EAAO,IAAA;EAAI,QAAQ;EAAiB,CAAC;AAC7E,QAAO,MAAM,GAAG,MAAM,IAAI;CAE1B,MAAM,WAAW,MAAM,gBAAgB,OAAO;AAC9C,QAAO,SAAS;AAEhB,KAAI,aAAa,MAAM;AACtB,UAAQ,OAAO,MAAM,+BAA+B;AACpD,UAAQ,KAAK,EAAE;;AAGhB,SAAQ,OAAO,MAAM,GAAG,SAAS,IAAI;AACrC,SAAQ,KAAK,EAAE;;AAGhB,eAAsB,gBAA+B;CACpD,MAAM,SAAS,MAAM,YAAY;AACjC,KAAI,WAAW,MAAM;AACpB,UAAQ,OAAO,MAAM,+BAA+B;AACpD,UAAQ,KAAK,EAAE;;CAIhB,MAAM,QAAQ,KAAK,UAAU;EAAE,SAAS;EAAO,IAAA;EAAI,QAAQ;EAAmB,CAAC;AAC/E,QAAO,MAAM,GAAG,MAAM,IAAI;AAG1B,KAAI,MADmB,gBAAgB,OAAO,KAC7B,MAAM;AACtB,UAAQ,OAAO,MAAM,kDAAkD;AACvE,UAAQ,KAAK,EAAE;;AAIhB,OAAM,IAAI,SAAe,YAAY;EACpC,MAAM,QAAQ,WAAW,SAAS,mBAAmB;AACrD,QAAM,SAAS;AACf,SAAO,KAAK,eAAe;AAC1B,gBAAa,MAAM;AACnB,YAAS;IACR;GACD;AAEF,SAAQ,OAAO,MAAM,2BAA2B;AAChD,SAAQ,KAAK,EAAE;;AAGhB,eAAe,gBAAgB,QAAuD;AACrF,QAAO,IAAI,SAAS,YAAY;EAC/B,IAAI,MAAM,OAAO,MAAM,EAAE;EACzB,MAAM,QAAQ,iBAAiB;AAC9B,YAAS;AACT,WAAQ,KAAK;KACX,mBAAmB;AACtB,QAAM,SAAS;EACf,MAAM,gBAAsB;AAC3B,gBAAa,MAAM;AACnB,UAAO,IAAI,QAAQ,OAAO;;EAE3B,MAAM,UAAU,UAAwB;AACvC,SAAM,OAAO,OAAO,CAAC,KAAK,MAAM,CAAC;GACjC,MAAM,MAAM,IAAI,QAAQ,GAAK;AAC7B,OAAI,QAAQ,GAAI;AAChB,YAAS;AACT,WAAQ,IAAI,SAAS,GAAG,IAAI,CAAC,SAAS,OAAO,CAAC;;AAE/C,SAAO,GAAG,QAAQ,OAAO;GACxB"}
@@ -1138,7 +1138,7 @@ function acpPromptToPiMessage(blocks) {
1138
1138
  //#endregion
1139
1139
  //#region package.json
1140
1140
  var name = "@victor-software-house/pi-acp";
1141
- var version = "0.7.0";
1141
+ var version = "0.8.0";
1142
1142
  //#endregion
1143
1143
  //#region src/acp/agent.ts
1144
1144
  /** Builtin ACP slash commands handled directly by the adapter. */
@@ -2258,6 +2258,6 @@ function toWebWritable(dst) {
2258
2258
  } });
2259
2259
  }
2260
2260
  //#endregion
2261
- export { serveAcp as t };
2261
+ export { version as n, serveAcp as t };
2262
2262
 
2263
- //# sourceMappingURL=serve-DLukbpF4.mjs.map
2263
+ //# sourceMappingURL=serve-DmuHYqF-.mjs.map