@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.
- package/CHANGELOG.md +94 -0
- package/dist/bin/pushary-bell-hook.js +5 -5
- package/dist/bin/pushary-bell.js +15 -14
- package/dist/bin/pushary-claude.js +93 -143
- 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 +349 -0
- package/dist/bin/pushary-codex-hook.js +18 -14
- package/dist/bin/pushary-codex.js +7 -7
- package/dist/bin/pushary-connect.js +13 -12
- package/dist/bin/pushary-daemon.js +331 -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 +299 -0
- package/dist/bin/pushary-gemini-hook.js +28 -21
- 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-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-4QQOKEOS.js → chunk-BLDDXHHA.js} +6 -6
- package/dist/{chunk-7PO2VEA4.js → chunk-CCCJ6BPW.js} +40 -34
- 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-ERYKGH4J.js +140 -0
- package/dist/{chunk-QR6MVVQ7.js → chunk-F52SIIDG.js} +4 -4
- package/dist/{chunk-26D2YMDU.js → chunk-FBGBJXVC.js} +1 -1
- package/dist/chunk-GCKAWHFP.js +124 -0
- package/dist/{chunk-QF7WTF2C.js → chunk-HZNOAJGB.js} +2 -2
- package/dist/chunk-ILB6UOAF.js +192 -0
- 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-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-DHAZ6SOT.js → chunk-YR5TSPXB.js} +81 -25
- package/dist/src/index.d.ts +6 -3
- package/dist/src/index.js +9 -9
- package/package.json +3 -1
|
@@ -2,43 +2,67 @@
|
|
|
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
|
+
findLiveProcessBySpawnId,
|
|
19
|
+
ownsLiveProcess
|
|
20
|
+
} from "../chunk-ILB6UOAF.js";
|
|
6
21
|
import {
|
|
7
22
|
spawnClaude
|
|
8
23
|
} from "../chunk-XHKBHWLX.js";
|
|
9
24
|
import {
|
|
10
|
-
|
|
11
|
-
|
|
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-
|
|
18
|
-
import "../chunk-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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 =
|
|
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 = {
|
|
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
|
-
|
|
287
|
+
codexBridgeEntry,
|
|
288
|
+
geminiBridgeEntry
|
|
128
289
|
});
|
|
129
290
|
if (!plan) {
|
|
130
|
-
|
|
131
|
-
return;
|
|
291
|
+
return Promise.reject(new Error("The prompt looks like a command-line flag"));
|
|
132
292
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
141
|
-
|
|
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
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
return
|
|
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
|
-
|
|
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: [
|
|
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 =
|
|
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-
|
|
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
|