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