@pushary/agent-hooks 0.83.2 → 0.84.1

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.
Files changed (64) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/dist/bin/pushary-bell-hook.js +5 -5
  3. package/dist/bin/pushary-bell.js +15 -14
  4. package/dist/bin/pushary-claude.js +93 -143
  5. package/dist/bin/pushary-clean.js +13 -12
  6. package/dist/bin/pushary-codex-bridge.d.ts +1 -0
  7. package/dist/bin/pushary-codex-bridge.js +349 -0
  8. package/dist/bin/pushary-codex-hook.js +18 -14
  9. package/dist/bin/pushary-codex.js +7 -7
  10. package/dist/bin/pushary-connect.js +13 -12
  11. package/dist/bin/pushary-daemon.js +331 -41
  12. package/dist/bin/pushary-doctor.js +48 -25
  13. package/dist/bin/pushary-gemini-bridge.d.ts +1 -0
  14. package/dist/bin/pushary-gemini-bridge.js +299 -0
  15. package/dist/bin/pushary-gemini-hook.js +28 -21
  16. package/dist/bin/pushary-hook.js +14 -13
  17. package/dist/bin/pushary-login.js +15 -14
  18. package/dist/bin/pushary-logout.js +12 -11
  19. package/dist/bin/pushary-mode.js +10 -9
  20. package/dist/bin/pushary-notification-hook.js +5 -5
  21. package/dist/bin/pushary-permission-denied-hook.js +9 -9
  22. package/dist/bin/pushary-permission-hook.js +9 -9
  23. package/dist/bin/pushary-post-hook.js +5 -5
  24. package/dist/bin/pushary-prompt-hook.js +5 -5
  25. package/dist/bin/pushary-session-end-hook.js +5 -5
  26. package/dist/bin/pushary-session-start-hook.js +5 -5
  27. package/dist/bin/pushary-setup.js +42 -36
  28. package/dist/bin/pushary-stats.js +6 -5
  29. package/dist/bin/pushary-status.js +17 -16
  30. package/dist/bin/pushary-stop-hook.js +5 -5
  31. package/dist/bin/pushary-stopfailure-hook.js +5 -5
  32. package/dist/bin/pushary-suggestions.js +7 -6
  33. package/dist/bin/pushary-upgrade.js +18 -17
  34. package/dist/bin/pushary-wait.js +10 -9
  35. package/dist/bin/pushary.js +8 -6
  36. package/dist/{chunk-SAA3VBDK.js → chunk-437T77TY.js} +3 -3
  37. package/dist/{chunk-U27BZWFT.js → chunk-6LC4WSCL.js} +1 -1
  38. package/dist/{chunk-7WWZPII5.js → chunk-7PZPJSWI.js} +1 -1
  39. package/dist/{chunk-4QQOKEOS.js → chunk-BLDDXHHA.js} +6 -6
  40. package/dist/{chunk-7PO2VEA4.js → chunk-CCCJ6BPW.js} +40 -34
  41. package/dist/chunk-CCH775Y6.js +343 -0
  42. package/dist/chunk-DINDL2CF.js +45 -0
  43. package/dist/{chunk-TXF4QZSQ.js → chunk-DOXSWT4Q.js} +1 -1
  44. package/dist/chunk-ERYKGH4J.js +140 -0
  45. package/dist/{chunk-QR6MVVQ7.js → chunk-F52SIIDG.js} +4 -4
  46. package/dist/{chunk-26D2YMDU.js → chunk-FBGBJXVC.js} +1 -1
  47. package/dist/chunk-GCKAWHFP.js +124 -0
  48. package/dist/{chunk-QF7WTF2C.js → chunk-HZNOAJGB.js} +2 -2
  49. package/dist/chunk-ILB6UOAF.js +192 -0
  50. package/dist/{chunk-RRZZ7TFJ.js → chunk-J4KFVOCQ.js} +4 -2
  51. package/dist/{chunk-WDRRULBH.js → chunk-K7CSRGKZ.js} +4 -4
  52. package/dist/{chunk-ZUKH2NPJ.js → chunk-MGJBC6DD.js} +1 -44
  53. package/dist/{chunk-GQLB2GBJ.js → chunk-MHBC2XUP.js} +1 -1
  54. package/dist/{chunk-YWRJGYUH.js → chunk-MQMPG5X4.js} +28 -2
  55. package/dist/{chunk-3AGM3TDM.js → chunk-NJ7JT26G.js} +1 -1
  56. package/dist/chunk-NRGRUOAR.js +25 -0
  57. package/dist/{chunk-EHLTWCDZ.js → chunk-SEAHGHCE.js} +1 -1
  58. package/dist/{chunk-NDC3TBHI.js → chunk-UKSQWTBH.js} +1 -1
  59. package/dist/{chunk-UVAC2KHH.js → chunk-UXWEE3QI.js} +1 -1
  60. package/dist/{chunk-MUEW424A.js → chunk-VT4IVERX.js} +50 -3
  61. package/dist/{chunk-DHAZ6SOT.js → chunk-YR5TSPXB.js} +81 -25
  62. package/dist/src/index.d.ts +6 -3
  63. package/dist/src/index.js +9 -9
  64. package/package.json +3 -1
@@ -2,43 +2,67 @@
2
2
  import {
3
3
  HEARTBEAT_INTERVAL_MS,
4
4
  sendHeartbeat
5
- } from "../chunk-GQLB2GBJ.js";
5
+ } from "../chunk-MHBC2XUP.js";
6
+ import {
7
+ DAEMON_LEASE_RENEW_INTERVAL_MS,
8
+ acquireDaemonLease,
9
+ readDaemonStatus,
10
+ requestDaemonHandoff
11
+ } from "../chunk-CCH775Y6.js";
12
+ import {
13
+ CODEX_BRIDGE_CAPABILITY,
14
+ CODEX_RESUME_CAPABILITY,
15
+ geminiLiveCapabilities
16
+ } from "../chunk-NRGRUOAR.js";
17
+ import {
18
+ findLiveProcessBySpawnId,
19
+ ownsLiveProcess
20
+ } from "../chunk-ILB6UOAF.js";
6
21
  import {
7
22
  spawnClaude
8
23
  } from "../chunk-XHKBHWLX.js";
9
24
  import {
10
- resolveGlobalBinary
11
- } from "../chunk-MUEW424A.js";
25
+ resolveBinary,
26
+ resolveGlobalPackageVersion,
27
+ resolvePackageVersionFromBinary
28
+ } from "../chunk-VT4IVERX.js";
29
+ import "../chunk-SEAHGHCE.js";
12
30
  import {
13
31
  getMachineId
14
32
  } from "../chunk-RN3NOEJF.js";
15
33
  import {
16
34
  exitOnHelpFlag
17
- } from "../chunk-RRZZ7TFJ.js";
18
- import "../chunk-ZUKH2NPJ.js";
35
+ } from "../chunk-J4KFVOCQ.js";
36
+ import "../chunk-MGJBC6DD.js";
37
+ import "../chunk-DINDL2CF.js";
38
+ import "../chunk-6MTNS63X.js";
39
+ import {
40
+ EXIT
41
+ } from "../chunk-KZERVKTD.js";
19
42
  import {
20
43
  getApiKey,
21
44
  getBaseUrl
22
45
  } from "../chunk-2UMNXADU.js";
23
- import {
24
- EXIT
25
- } from "../chunk-KZERVKTD.js";
46
+ import "../chunk-MQMPG5X4.js";
26
47
 
27
48
  // src/spawn-daemon.ts
28
- import { spawn } from "child_process";
49
+ import { execFileSync, spawn } from "child_process";
29
50
  import { existsSync } from "fs";
30
51
  import { basename, dirname, join } from "path";
31
52
 
32
53
  // src/stop-requests.ts
33
54
  var STOP_CAPABILITY = "stop";
55
+ var STOP_ACK_CAPABILITY = "stop-ack";
56
+ var windowsTaskkillArgs = (pid) => ["/PID", String(pid), "/T", "/F"];
34
57
  var isStopRequest = (value) => {
35
58
  if (!value || typeof value !== "object") return false;
36
59
  const candidate = value;
37
- return typeof candidate.sessionId === "string" && candidate.sessionId.length > 0 && typeof candidate.pid === "number" && Number.isInteger(candidate.pid) && candidate.pid > 1;
60
+ return (candidate.id === void 0 || typeof candidate.id === "string" && candidate.id.length > 0 && candidate.id.length <= 64) && (candidate.spawnId === void 0 || typeof candidate.spawnId === "string" && candidate.spawnId.length > 0 && candidate.spawnId.length <= 64) && typeof candidate.sessionId === "string" && candidate.sessionId.length > 0 && typeof candidate.pid === "number" && Number.isInteger(candidate.pid) && candidate.pid > 1;
38
61
  };
39
62
  var parseStopRequests = (value) => Array.isArray(value) ? value.filter(isStopRequest) : [];
40
63
  var applyStop = (request, deps) => {
41
64
  if (request.pid === deps.selfPid) return "skipped_self";
65
+ if (!deps.ownsProcess(request)) return "refused";
42
66
  try {
43
67
  deps.kill(request.pid, "SIGTERM");
44
68
  return "stopped";
@@ -47,17 +71,88 @@ var applyStop = (request, deps) => {
47
71
  return code === "ESRCH" ? "already_gone" : "refused";
48
72
  }
49
73
  };
74
+ var applyStopAndVerify = async (request, deps, attempts = 100, intervalMs = 100) => {
75
+ if (!deps.isAlive(request.pid)) return "already_gone";
76
+ if (!await deps.ownsProcess(request)) return "refused";
77
+ const outcome = applyStop(request, { ...deps, ownsProcess: () => true });
78
+ if (outcome !== "stopped") return outcome;
79
+ for (let attempt = 0; attempt < attempts; attempt++) {
80
+ await deps.sleep(intervalMs);
81
+ if (!deps.isAlive(request.pid)) return "stopped";
82
+ }
83
+ return "signal_sent";
84
+ };
50
85
 
51
86
  // src/spawn-daemon.ts
52
87
  var DRAIN_PATH = "/api/agent/spawn/drain";
88
+ var ACK_PATH = "/api/agent/spawn/ack";
89
+ var SPAWN_PROTOCOL_VERSION = 2;
53
90
  var FAST_POLL_MS = 3e3;
54
91
  var SLOW_POLL_MS = 3e4;
55
92
  var FAST_DECAY_TICKS = 3;
56
93
  var MAX_ERROR_BACKOFF_MS = 12e4;
57
94
  var MAX_ERROR_STREAK = 6;
58
95
  var REQUEST_TIMEOUT_MS = 1e4;
96
+ var STOP_PROTOCOL_VERSION = 2;
59
97
  var MAX_SPAWNS_PER_WINDOW = 5;
60
98
  var SPAWN_WINDOW_MS = 6e4;
99
+ var VERSION_CHECK_INTERVAL_MS = 2e3;
100
+ var MAX_TRACKED_PROCESSES = 100;
101
+ var MAX_COMPLETED_STOPS = 20;
102
+ var daemonSignals = (platform = process.platform) => platform === "win32" ? ["SIGINT", "SIGTERM"] : ["SIGINT", "SIGTERM", "SIGHUP"];
103
+ var parseSpawnClaim = (value) => {
104
+ if (typeof value !== "object" || value === null) return null;
105
+ const request = value;
106
+ if (typeof request.id !== "string" || typeof request.prompt !== "string") return null;
107
+ return {
108
+ id: request.id,
109
+ prompt: request.prompt,
110
+ ...typeof request.claimId === "string" ? { claimId: request.claimId } : {},
111
+ ...Number.isInteger(request.attemptCount) ? { attemptCount: request.attemptCount } : {},
112
+ ...typeof request.cwd === "string" ? { cwd: request.cwd } : {},
113
+ ...typeof request.agent === "string" ? { agent: request.agent } : {},
114
+ ...typeof request.model === "string" ? { model: request.model } : {},
115
+ ...typeof request.resumeSessionId === "string" ? { resumeSessionId: request.resumeSessionId } : {}
116
+ };
117
+ };
118
+ var stopAcknowledgement = (request, machineId, state) => request.id ? { stopId: request.id, machineId, state } : null;
119
+ var spawnAcknowledgement = (request, machineId, outcome) => {
120
+ if (!request.claimId) return null;
121
+ return outcome.state === "started" ? {
122
+ spawnId: request.id,
123
+ claimId: request.claimId,
124
+ machineId,
125
+ state: "started",
126
+ pid: outcome.pid ?? null
127
+ } : {
128
+ spawnId: request.id,
129
+ claimId: request.claimId,
130
+ machineId,
131
+ state: "failed",
132
+ failure: {
133
+ code: "launch_failed",
134
+ message: outcome.message.slice(0, 240),
135
+ retryable: false
136
+ }
137
+ };
138
+ };
139
+ var launchSpawnClaim = async (request, runtime) => {
140
+ const existingPid = await runtime.findExistingPid(request.id);
141
+ if (existingPid !== null) {
142
+ await runtime.acknowledge(spawnAcknowledgement(request, runtime.machineId, { state: "started", pid: existingPid }));
143
+ return;
144
+ }
145
+ let pid;
146
+ try {
147
+ pid = await runtime.launch(request);
148
+ } catch (err) {
149
+ const message = err instanceof Error ? err.message : String(err);
150
+ runtime.reportLaunchError(message);
151
+ await runtime.acknowledge(spawnAcknowledgement(request, runtime.machineId, { state: "failed", message }));
152
+ return;
153
+ }
154
+ await runtime.acknowledge(spawnAcknowledgement(request, runtime.machineId, { state: "started", pid }));
155
+ };
61
156
  var DrainError = class extends Error {
62
157
  constructor(status) {
63
158
  super(`drain ${status}`);
@@ -71,7 +166,6 @@ var isAuthFailure = (err) => {
71
166
  if (candidate.name !== "DrainError") return false;
72
167
  return candidate.status === 401 || candidate.status === 403;
73
168
  };
74
- var NATIVE_AGENT_BINARIES = { codex: "codex", gemini: "gemini" };
75
169
  var isFlagShapedPrompt = (prompt) => prompt.trimStart().startsWith("-");
76
170
  var buildSpawnLaunch = (req, opts) => {
77
171
  if (isFlagShapedPrompt(req.prompt)) return null;
@@ -81,9 +175,20 @@ var buildSpawnLaunch = (req, opts) => {
81
175
  const claudeArgs = ["claude", "--remote", ...model, "-p", req.prompt];
82
176
  return opts.spawnDirect ? { direct: true, command: opts.execPath, args: [opts.siblingEntry, ...claudeArgs], agent } : { direct: false, command: opts.pusharyBin, args: claudeArgs, agent };
83
177
  }
84
- const command = opts.resolveNative(NATIVE_AGENT_BINARIES[agent]);
85
- const args = agent === "codex" ? ["exec", ...model, req.prompt] : [...model, "-p", req.prompt];
86
- return { direct: false, command, args, agent };
178
+ if (agent === "codex") {
179
+ return {
180
+ direct: true,
181
+ command: opts.execPath,
182
+ args: [opts.codexBridgeEntry],
183
+ agent
184
+ };
185
+ }
186
+ return {
187
+ direct: true,
188
+ command: opts.execPath,
189
+ args: [opts.geminiBridgeEntry],
190
+ agent
191
+ };
87
192
  };
88
193
  var spawnRateAllows = (timestamps, now, maxPerWindow = MAX_SPAWNS_PER_WINDOW, windowMs = SPAWN_WINDOW_MS) => {
89
194
  while (timestamps.length > 0 && now - timestamps[0] > windowMs) timestamps.shift();
@@ -102,70 +207,227 @@ var runSpawnDaemon = async () => {
102
207
  }
103
208
  const machineId = getMachineId();
104
209
  const baseUrl = getBaseUrl();
105
- const pusharyBin = resolveGlobalBinary("pushary") ?? "pushary";
210
+ const pusharyBin = resolveBinary("pushary") ?? "pushary";
211
+ const codexBin = resolveBinary("codex");
212
+ const geminiBin = resolveBinary("gemini");
213
+ const geminiCapabilities = geminiBin ? geminiLiveCapabilities(
214
+ resolvePackageVersionFromBinary(geminiBin, "@google/gemini-cli") ?? resolveGlobalPackageVersion("@google/gemini-cli")
215
+ ) : [];
106
216
  const siblingEntry = process.argv[1] ? join(dirname(process.argv[1]), "pushary.js") : "";
217
+ const codexBridgeEntry = process.argv[1] ? join(dirname(process.argv[1]), "pushary-codex-bridge.js") : "pushary-codex-bridge";
218
+ const geminiBridgeEntry = process.argv[1] ? join(dirname(process.argv[1]), "pushary-gemini-bridge.js") : "pushary-gemini-bridge";
107
219
  const spawnDirect = siblingEntry.length > 0 && existsSync(siblingEntry);
220
+ const existing = readDaemonStatus();
221
+ let handoff;
222
+ try {
223
+ handoff = await requestDaemonHandoff(existing);
224
+ } catch (err) {
225
+ stderr(`[pushary] daemon could not request a handoff: ${err instanceof Error ? err.message : String(err)}
226
+ `);
227
+ return 1;
228
+ }
229
+ if (existing.pid !== null && handoff.pid === existing.pid && handoff.processAlive === true) {
230
+ stderr("[pushary] daemon could not replace the existing process; run `pushary doctor` for details\n");
231
+ return 1;
232
+ }
233
+ const lifecycle = acquireDaemonLease({ machineId });
234
+ if (lifecycle.kind === "already-running") {
235
+ stderr(`[pushary] daemon already online as pid ${lifecycle.status.pid ?? "unknown"}
236
+ `);
237
+ return 0;
238
+ }
239
+ if (lifecycle.kind === "unavailable") {
240
+ stderr(`[pushary] daemon could not acquire its local lease: ${lifecycle.detail}
241
+ `);
242
+ return 1;
243
+ }
244
+ const daemonLease = lifecycle.lease;
108
245
  let stopped = false;
109
246
  let idleTicks = 0;
110
247
  let errorStreak = 0;
111
248
  const spawnTimestamps = [];
249
+ const launchedProcesses = /* @__PURE__ */ new Map();
250
+ const completedStops = /* @__PURE__ */ new Map();
112
251
  let timer;
113
252
  let inflight;
114
253
  let fatalCode = null;
115
254
  let finish = () => {
116
255
  };
117
256
  let heartbeat;
257
+ let versionWatch;
258
+ let leaseRenewal;
118
259
  const launch = (req) => {
260
+ if (launchedProcesses.size >= MAX_TRACKED_PROCESSES) {
261
+ return Promise.reject(new Error("This daemon is already tracking 100 active sessions"));
262
+ }
119
263
  spawnTimestamps.push(Date.now());
120
264
  const cwd = req.cwd && existsSync(req.cwd) ? req.cwd : process.cwd();
121
- const env = { ...process.env, PUSHARY_SPAWN_ID: req.id };
265
+ const env = {
266
+ ...process.env,
267
+ PUSHARY_SPAWN_ID: req.id,
268
+ PUSHARY_SPAWN_CLAIM_ID: req.claimId,
269
+ ...req.agent === "codex" || req.agent === "gemini" ? {
270
+ PUSHARY_BRIDGE_CWD: cwd,
271
+ PUSHARY_BRIDGE_PROMPT: req.prompt || void 0,
272
+ PUSHARY_BRIDGE_MODEL: req.model || void 0,
273
+ PUSHARY_BRIDGE_RESUME_SESSION_ID: req.resumeSessionId || void 0
274
+ } : {},
275
+ ...req.agent === "codex" ? {
276
+ PUSHARY_CODEX_COMMAND: codexBin ?? "codex"
277
+ } : {},
278
+ ...req.agent === "gemini" ? {
279
+ PUSHARY_GEMINI_COMMAND: geminiBin ?? "gemini"
280
+ } : {}
281
+ };
122
282
  const plan = buildSpawnLaunch(req, {
123
283
  spawnDirect,
124
284
  execPath: process.execPath,
125
285
  siblingEntry,
126
286
  pusharyBin,
127
- resolveNative: (name) => resolveGlobalBinary(name) ?? name
287
+ codexBridgeEntry,
288
+ geminiBridgeEntry
128
289
  });
129
290
  if (!plan) {
130
- stderr("[pushary] refused a phone-requested session: the prompt looks like a command-line flag\n");
131
- return;
291
+ return Promise.reject(new Error("The prompt looks like a command-line flag"));
132
292
  }
133
- try {
134
- const child = plan.direct ? spawn(plan.command, plan.args, { cwd, detached: true, stdio: "ignore", env }) : spawnClaude(plan.command, plan.args, { cwd, detached: true, stdio: "ignore", env });
135
- child.on("error", (err) => stderr(`[pushary] could not launch session: ${err.message}
136
- `));
137
- child.unref();
138
- stderr(`[pushary] launched a phone-requested ${plan.agent} session in ${basename(cwd)}
293
+ return new Promise((resolve, reject) => {
294
+ try {
295
+ const child = plan.direct ? spawn(plan.command, plan.args, { cwd, detached: true, stdio: "ignore", env }) : spawnClaude(plan.command, plan.args, { cwd, detached: true, stdio: "ignore", env });
296
+ let started = false;
297
+ child.once("error", (err) => {
298
+ if (!started) reject(err);
299
+ else stderr(`[pushary] launched session reported an error: ${err.message}
139
300
  `);
140
- } catch (err) {
141
- stderr(`[pushary] could not launch session: ${err instanceof Error ? err.message : String(err)}
301
+ });
302
+ child.once("spawn", () => {
303
+ started = true;
304
+ if (typeof child.pid === "number") {
305
+ launchedProcesses.set(child.pid, req.id);
306
+ child.once("close", () => launchedProcesses.delete(child.pid));
307
+ }
308
+ child.unref();
309
+ stderr(`[pushary] launched a phone-requested ${plan.agent} session in ${basename(cwd)}
142
310
  `);
143
- }
311
+ resolve(typeof child.pid === "number" ? child.pid : null);
312
+ });
313
+ } catch (err) {
314
+ reject(err);
315
+ }
316
+ });
144
317
  };
145
- const runStop = (stop) => {
146
- const outcome = applyStop(stop, { kill: process.kill.bind(process), selfPid: process.pid });
318
+ const runStop = async (stop) => {
319
+ const kill = process.platform === "win32" ? (pid) => {
320
+ execFileSync("taskkill", windowsTaskkillArgs(pid), { stdio: "ignore" });
321
+ } : process.kill.bind(process);
322
+ const outcome = stop.id ? completedStops.get(stop.id) ?? await apply() : await apply();
323
+ if (stop.id && !completedStops.has(stop.id)) {
324
+ if (completedStops.size === MAX_COMPLETED_STOPS) {
325
+ const oldest = completedStops.keys().next().value;
326
+ if (oldest) completedStops.delete(oldest);
327
+ }
328
+ completedStops.set(stop.id, outcome);
329
+ }
147
330
  const said = {
148
331
  stopped: `stopped session ${stop.sessionId}`,
149
332
  already_gone: `session ${stop.sessionId} had already exited`,
333
+ signal_sent: `sent the stop signal to session ${stop.sessionId}; shutdown is still finishing`,
150
334
  refused: `could not stop session ${stop.sessionId}, the system refused the signal`,
151
335
  skipped_self: `refused to stop session ${stop.sessionId}: that pid is this daemon`
152
336
  };
153
337
  stderr(`[pushary] ${said[outcome]}
154
338
  `);
339
+ const acknowledgement = stopAcknowledgement(stop, machineId, outcome);
340
+ if (acknowledgement) {
341
+ for (let attempt = 0; attempt < 3 && !stopped; attempt++) {
342
+ const response = await fetch(`${baseUrl}/api/agent/stop/ack`, {
343
+ method: "POST",
344
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
345
+ body: JSON.stringify(acknowledgement),
346
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
347
+ }).catch(() => null);
348
+ if (response?.ok) {
349
+ completedStops.delete(acknowledgement.stopId);
350
+ break;
351
+ }
352
+ if (attempt < 2) await new Promise((resolve) => setTimeout(resolve, 200 * (attempt + 1)));
353
+ }
354
+ }
355
+ async function apply() {
356
+ return applyStopAndVerify(stop, {
357
+ kill,
358
+ ownsProcess: (request) => {
359
+ const launched = request.spawnId ? launchedProcesses.get(request.pid) === request.spawnId : !request.id && launchedProcesses.has(request.pid);
360
+ return launched || ownsLiveProcess(request);
361
+ },
362
+ selfPid: process.pid,
363
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
364
+ isAlive: (pid) => {
365
+ try {
366
+ process.kill(pid, 0);
367
+ return true;
368
+ } catch (err) {
369
+ return err.code === "EPERM";
370
+ }
371
+ }
372
+ });
373
+ }
155
374
  };
156
375
  const drain = async (signal) => {
157
376
  const res = await fetch(`${baseUrl}${DRAIN_PATH}`, {
158
377
  method: "POST",
159
378
  headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
160
- body: JSON.stringify({ machineId }),
379
+ body: JSON.stringify({ machineId, spawnProtocol: SPAWN_PROTOCOL_VERSION, stopProtocol: STOP_PROTOCOL_VERSION }),
161
380
  signal
162
381
  });
163
382
  if (!res.ok) throw new DrainError(res.status);
164
383
  const data = await res.json();
165
- for (const stop of parseStopRequests(data.stops)) runStop(stop);
166
- const spawn2 = data.spawn;
167
- return spawn2 && typeof spawn2.prompt === "string" && typeof spawn2.id === "string" ? spawn2 : null;
384
+ const response = typeof data === "object" && data !== null ? data : {};
385
+ await Promise.all(parseStopRequests(response.stops).map(runStop));
386
+ return parseSpawnClaim(response.spawn);
168
387
  };
388
+ const acknowledge = async (body) => {
389
+ if (!body) return;
390
+ let lastError;
391
+ for (let attempt = 0; attempt < 3 && !stopped; attempt++) {
392
+ const controller = new AbortController();
393
+ inflight = controller;
394
+ const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
395
+ try {
396
+ const response = await fetch(`${baseUrl}${ACK_PATH}`, {
397
+ method: "POST",
398
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
399
+ body: JSON.stringify(body),
400
+ signal: controller.signal
401
+ });
402
+ if (response.ok) return;
403
+ const error = new Error(`spawn acknowledgement failed (${response.status})`);
404
+ if (response.status < 500) {
405
+ lastError = error;
406
+ break;
407
+ }
408
+ lastError = error;
409
+ } catch (err) {
410
+ lastError = err;
411
+ } finally {
412
+ clearTimeout(timeout);
413
+ if (inflight === controller) inflight = void 0;
414
+ }
415
+ }
416
+ throw lastError ?? new Error("spawn acknowledgement stopped");
417
+ };
418
+ const launchClaim = (request) => launchSpawnClaim(request, {
419
+ machineId,
420
+ findExistingPid: async (spawnId) => {
421
+ for (const [pid, launchedSpawnId] of launchedProcesses) {
422
+ if (launchedSpawnId === spawnId) return pid;
423
+ }
424
+ return findLiveProcessBySpawnId(spawnId);
425
+ },
426
+ launch,
427
+ acknowledge,
428
+ reportLaunchError: (message) => stderr(`[pushary] could not launch session: ${message}
429
+ `)
430
+ });
169
431
  const nextDelay = () => {
170
432
  if (errorStreak > 0) return Math.min(SLOW_POLL_MS * 2 ** (errorStreak - 1), MAX_ERROR_BACKOFF_MS);
171
433
  return idleTicks >= FAST_DECAY_TICKS ? SLOW_POLL_MS : FAST_POLL_MS;
@@ -173,6 +435,10 @@ var runSpawnDaemon = async () => {
173
435
  const jitter = (ms) => Math.max(1, Math.round(ms * (0.85 + Math.random() * 0.3)));
174
436
  const tick = async () => {
175
437
  if (stopped) return;
438
+ if (!daemonLease.isOwner()) {
439
+ finish();
440
+ return;
441
+ }
176
442
  if (!spawnRateAllows(spawnTimestamps, Date.now())) {
177
443
  timer = setTimeout(tick, jitter(nextDelay()));
178
444
  return;
@@ -182,10 +448,14 @@ var runSpawnDaemon = async () => {
182
448
  const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
183
449
  try {
184
450
  const req = await drain(controller.signal);
451
+ if (!daemonLease.isOwner()) {
452
+ finish();
453
+ return;
454
+ }
185
455
  errorStreak = 0;
186
456
  if (req) {
187
457
  idleTicks = 0;
188
- launch(req);
458
+ await launchClaim(req);
189
459
  } else {
190
460
  idleTicks++;
191
461
  }
@@ -211,14 +481,33 @@ var runSpawnDaemon = async () => {
211
481
  stopped = true;
212
482
  if (timer) clearTimeout(timer);
213
483
  if (heartbeat) clearInterval(heartbeat);
484
+ if (versionWatch) clearInterval(versionWatch);
485
+ if (leaseRenewal) clearInterval(leaseRenewal);
214
486
  inflight?.abort();
487
+ try {
488
+ daemonLease.release();
489
+ } catch (err) {
490
+ stderr(`[pushary] daemon could not release its local lease: ${err instanceof Error ? err.message : String(err)}
491
+ `);
492
+ }
215
493
  };
216
494
  const beat = async () => {
217
495
  if (stopped) return;
496
+ if (!daemonLease.isOwner()) {
497
+ finish();
498
+ return;
499
+ }
218
500
  const result = await sendHeartbeat(apiKey, {
219
501
  repoKey: basename(process.cwd()),
220
- capabilities: ["spawn", STOP_CAPABILITY]
502
+ capabilities: [
503
+ "spawn",
504
+ STOP_CAPABILITY,
505
+ STOP_ACK_CAPABILITY,
506
+ ...codexBin ? [CODEX_BRIDGE_CAPABILITY, CODEX_RESUME_CAPABILITY] : [],
507
+ ...geminiCapabilities
508
+ ]
221
509
  });
510
+ if (result.kind === "ok") daemonLease.recordSuccessfulHeartbeat();
222
511
  if (result.kind === "unauthorized" && !stopped) {
223
512
  fatalCode = EXIT.UNAUTHENTICATED;
224
513
  stderr("[pushary] daemon stopping: this machine's key was rejected. Run `pushary login` to sign in again.\n");
@@ -227,17 +516,18 @@ var runSpawnDaemon = async () => {
227
516
  };
228
517
  void beat();
229
518
  heartbeat = setInterval(() => void beat(), HEARTBEAT_INTERVAL_MS);
519
+ leaseRenewal = setInterval(() => daemonLease.renew(), DAEMON_LEASE_RENEW_INTERVAL_MS);
520
+ versionWatch = setInterval(() => {
521
+ if (daemonLease.shouldYield()) finish();
522
+ }, VERSION_CHECK_INTERVAL_MS);
230
523
  stderr(
231
524
  `[pushary] daemon online \u2014 this machine (${machineId}) can now be sent a new session from your phone. Ctrl-C to stop.
232
525
  `
233
526
  );
234
527
  timer = setTimeout(tick, FAST_POLL_MS);
235
- const signals = ["SIGINT", "SIGTERM", "SIGHUP"];
528
+ const signals = daemonSignals();
236
529
  await new Promise((resolve) => {
237
- const onSignal = () => {
238
- teardown();
239
- resolve();
240
- };
530
+ const onSignal = () => finish();
241
531
  for (const signal of signals) process.on(signal, onSignal);
242
532
  finish = () => {
243
533
  teardown();
@@ -6,7 +6,10 @@ import {
6
6
  claudeWired,
7
7
  codexWired,
8
8
  geminiWired
9
- } from "../chunk-QF7WTF2C.js";
9
+ } from "../chunk-HZNOAJGB.js";
10
+ import {
11
+ readDaemonStatus
12
+ } from "../chunk-CCH775Y6.js";
10
13
  import {
11
14
  agentProbes,
12
15
  detectAgent,
@@ -15,12 +18,16 @@ import {
15
18
  isDetected,
16
19
  isPluginRegistered,
17
20
  vscodeSettingsTargets
18
- } from "../chunk-UVAC2KHH.js";
21
+ } from "../chunk-UXWEE3QI.js";
19
22
  import {
20
23
  GEMINI_HOOK_BINARY,
21
24
  hasGeminiHooks,
22
25
  missingGeminiHookEvents
23
- } from "../chunk-TXF4QZSQ.js";
26
+ } from "../chunk-DOXSWT4Q.js";
27
+ import "../chunk-XHKBHWLX.js";
28
+ import {
29
+ execNpm
30
+ } from "../chunk-VT4IVERX.js";
24
31
  import {
25
32
  claudeJson,
26
33
  claudeSettings,
@@ -39,62 +46,60 @@ import {
39
46
  readCodexMcpAuth,
40
47
  untrustedCodexHookEvents,
41
48
  vscodePluginDir
42
- } from "../chunk-EHLTWCDZ.js";
49
+ } from "../chunk-SEAHGHCE.js";
43
50
  import {
44
51
  readLedgerSummary
45
- } from "../chunk-3AGM3TDM.js";
52
+ } from "../chunk-NJ7JT26G.js";
53
+ import {
54
+ getMachineId
55
+ } from "../chunk-RN3NOEJF.js";
46
56
  import {
47
57
  describeReach,
48
58
  describeRoundTrip,
49
59
  fetchChannels,
50
60
  reachVerdict,
51
61
  verifyRoundTrip
52
- } from "../chunk-U27BZWFT.js";
62
+ } from "../chunk-6LC4WSCL.js";
53
63
  import "../chunk-3EGEA4KH.js";
54
64
  import {
55
65
  checkApiKey,
56
66
  describeKeyCheck
57
- } from "../chunk-QR6MVVQ7.js";
67
+ } from "../chunk-F52SIIDG.js";
58
68
  import {
59
69
  createIo
60
70
  } from "../chunk-5RUIFDTP.js";
61
71
  import {
62
72
  keyPrefix,
63
73
  readKeySource
64
- } from "../chunk-WDRRULBH.js";
65
- import "../chunk-6MTNS63X.js";
74
+ } from "../chunk-K7CSRGKZ.js";
66
75
  import {
67
76
  parseFlags
68
77
  } from "../chunk-GMXKITVA.js";
78
+ import {
79
+ exitOnHelpFlag
80
+ } from "../chunk-J4KFVOCQ.js";
81
+ import "../chunk-MGJBC6DD.js";
69
82
  import "../chunk-BC3VCZ3E.js";
83
+ import {
84
+ getPackageVersion
85
+ } from "../chunk-DINDL2CF.js";
86
+ import "../chunk-6MTNS63X.js";
70
87
  import {
71
88
  callMcpTool,
72
89
  sendMcpRequest
73
90
  } from "../chunk-BSZYIAZL.js";
74
91
  import "../chunk-SAF6HGAA.js";
75
- import "../chunk-SAA3VBDK.js";
92
+ import "../chunk-437T77TY.js";
76
93
  import {
77
94
  UsageError
78
95
  } from "../chunk-7QLSKOSU.js";
79
- import "../chunk-YWRJGYUH.js";
80
- import {
81
- execNpm
82
- } from "../chunk-MUEW424A.js";
83
96
  import {
84
- getMachineId
85
- } from "../chunk-RN3NOEJF.js";
86
- import {
87
- exitOnHelpFlag
88
- } from "../chunk-RRZZ7TFJ.js";
89
- import {
90
- getPackageVersion
91
- } from "../chunk-ZUKH2NPJ.js";
97
+ EXIT
98
+ } from "../chunk-KZERVKTD.js";
92
99
  import {
93
100
  getBaseUrl
94
101
  } from "../chunk-2UMNXADU.js";
95
- import {
96
- EXIT
97
- } from "../chunk-KZERVKTD.js";
102
+ import "../chunk-MQMPG5X4.js";
98
103
 
99
104
  // bin/pushary-doctor.ts
100
105
  import { existsSync, readFileSync, writeFileSync } from "fs";
@@ -321,6 +326,23 @@ var main = async () => {
321
326
  break;
322
327
  }
323
328
  }
329
+ const daemon = readDaemonStatus();
330
+ const daemonOk = daemon.kind === "running";
331
+ const daemonFailure = daemonOk ? null : `local state is ${daemon.kind}`;
332
+ const daemonDetail = daemonFailure ? `${daemonFailure}; run \`pushary daemon\` in a terminal to inspect it` : null;
333
+ check(
334
+ daemonOk,
335
+ "Remote-control daemon",
336
+ [
337
+ daemonDetail,
338
+ daemon.kind,
339
+ `pid ${daemon.pid ?? "none"}`,
340
+ `daemon ${daemon.daemonVersion ?? "none"}`,
341
+ `installed ${daemon.installedVersion}`,
342
+ `machine ${daemon.machineId ?? "none"}`,
343
+ `heartbeat ${daemon.lastSuccessfulHeartbeatAt ?? "none yet"}`
344
+ ].filter((value) => value !== null).join(" \xB7 ")
345
+ );
324
346
  const CLAUDE_JSON = claudeJson();
325
347
  const claudeInPlay = claudeWired({
326
348
  claudeJson: readJson(CLAUDE_JSON),
@@ -799,6 +821,7 @@ var main = async () => {
799
821
  ok: exitCode === EXIT.OK,
800
822
  exitCode,
801
823
  keyCheck: keyVerdict?.kind ?? null,
824
+ daemon,
802
825
  passed: results.filter((r) => r.passed).length,
803
826
  failed: failed.length,
804
827
  checks: results.map((r) => ({ label: r.label, passed: r.passed, detail: r.detail ?? null }))
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node