@agent-idle/cli 0.1.1 → 0.1.3
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/index.js +137 -46
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -170,7 +170,7 @@ function persistedConvexUrl() {
|
|
|
170
170
|
}
|
|
171
171
|
}
|
|
172
172
|
var BUILD_CONVEX_URL = "https://handsome-camel-783.convex.cloud";
|
|
173
|
-
var BUILD_AUTH_URL = "https://agent-idle
|
|
173
|
+
var BUILD_AUTH_URL = "https://agent-idle.com";
|
|
174
174
|
var LOCAL_CONVEX_URL = "http://127.0.0.1:3210";
|
|
175
175
|
function resolveConvexUrl() {
|
|
176
176
|
return process.env.CONVEX_URL ?? persistedConvexUrl() ?? BUILD_CONVEX_URL ?? LOCAL_CONVEX_URL;
|
|
@@ -210,6 +210,54 @@ function clearToken() {
|
|
|
210
210
|
}
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
+
// src/daemonControl.ts
|
|
214
|
+
import { spawn } from "child_process";
|
|
215
|
+
import { statSync } from "fs";
|
|
216
|
+
import { fileURLToPath } from "url";
|
|
217
|
+
function buildFingerprint() {
|
|
218
|
+
const script = fileURLToPath(new URL("./index.js", import.meta.url));
|
|
219
|
+
try {
|
|
220
|
+
return { script, mtimeMs: statSync(script).mtimeMs };
|
|
221
|
+
} catch {
|
|
222
|
+
return { script, mtimeMs: 0 };
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function isNewerBuild(callerMtimeMs, own) {
|
|
226
|
+
return own.mtimeMs > 0 && Number.isFinite(callerMtimeMs) && callerMtimeMs > own.mtimeMs + 1;
|
|
227
|
+
}
|
|
228
|
+
function spawnDaemon() {
|
|
229
|
+
const indexPath = fileURLToPath(new URL("./index.js", import.meta.url));
|
|
230
|
+
const child = spawn(process.execPath, [indexPath, "daemon"], {
|
|
231
|
+
detached: true,
|
|
232
|
+
stdio: "ignore"
|
|
233
|
+
});
|
|
234
|
+
child.unref();
|
|
235
|
+
}
|
|
236
|
+
async function pingDaemon() {
|
|
237
|
+
try {
|
|
238
|
+
const controller = new AbortController();
|
|
239
|
+
const timer = setTimeout(() => controller.abort(), 500);
|
|
240
|
+
const res = await fetch(`${DAEMON_URL}/token`, { signal: controller.signal });
|
|
241
|
+
clearTimeout(timer);
|
|
242
|
+
return res.ok;
|
|
243
|
+
} catch {
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
async function daemonToken() {
|
|
248
|
+
try {
|
|
249
|
+
const controller = new AbortController();
|
|
250
|
+
const timer = setTimeout(() => controller.abort(), 800);
|
|
251
|
+
const res = await fetch(`${DAEMON_URL}/token`, { signal: controller.signal });
|
|
252
|
+
clearTimeout(timer);
|
|
253
|
+
if (!res.ok) return null;
|
|
254
|
+
const { token } = await res.json();
|
|
255
|
+
return token ?? null;
|
|
256
|
+
} catch {
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
213
261
|
// src/outbox.ts
|
|
214
262
|
import { appendFileSync, chmodSync as chmodSync2, existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
215
263
|
function tighten() {
|
|
@@ -332,6 +380,7 @@ var RENEW_MS = 12e3;
|
|
|
332
380
|
var HELPER_CREDIT_FLUSH_MS = 2e3;
|
|
333
381
|
var HELPER_RETAIN_MS = 6e3;
|
|
334
382
|
var LIVENESS_POLL_MS = 5e3;
|
|
383
|
+
var OWN_BUILD = buildFingerprint();
|
|
335
384
|
var DEBUG = Boolean(process.env.AGENT_IDLE_DEBUG) || process.argv.includes("--debug");
|
|
336
385
|
function debug(...args) {
|
|
337
386
|
if (DEBUG) console.log("[agent-idle]", ...args);
|
|
@@ -492,8 +541,17 @@ var ALLOWED_ORIGINS = (() => {
|
|
|
492
541
|
// Vite dev / Tauri dev webview
|
|
493
542
|
"tauri://localhost",
|
|
494
543
|
"http://tauri.localhost",
|
|
495
|
-
"https://tauri.localhost"
|
|
544
|
+
"https://tauri.localhost",
|
|
496
545
|
// Tauri production webview schemes (platform-dependent)
|
|
546
|
+
// ALL first-party hosted-app origins, unconditionally. The daemon that owns the port
|
|
547
|
+
// is not always the build that opened the auth page (a dev daemon vs the npm CLI's
|
|
548
|
+
// prod page, or an old install vs a newer canonical domain) — if it only trusted its
|
|
549
|
+
// OWN AUTH_URL origin, the page's token post would 403 and `login` would hang. These
|
|
550
|
+
// endpoints only RECEIVE a token / serve local display hints; the secret-reading
|
|
551
|
+
// /token stays browser-denied regardless of origin.
|
|
552
|
+
"https://agent-idle.com",
|
|
553
|
+
"https://www.agent-idle.com",
|
|
554
|
+
"https://agent-idle-app.vercel.app"
|
|
497
555
|
]);
|
|
498
556
|
try {
|
|
499
557
|
out.add(new URL(AUTH_URL).origin);
|
|
@@ -874,6 +932,15 @@ function startDaemon() {
|
|
|
874
932
|
res.end(JSON.stringify({ token: readToken() }));
|
|
875
933
|
return;
|
|
876
934
|
}
|
|
935
|
+
if (req.method === "GET" && req.url === "/build") {
|
|
936
|
+
if (req.headers.origin) {
|
|
937
|
+
res.writeHead(403).end();
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
941
|
+
res.end(JSON.stringify({ ...OWN_BUILD, pid: process.pid }));
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
877
944
|
if (req.method === "GET" && req.url === "/sessions") {
|
|
878
945
|
res.writeHead(200, { ...cors, "Content-Type": "application/json" });
|
|
879
946
|
res.end(JSON.stringify(Object.fromEntries(sessionMeta)));
|
|
@@ -899,6 +966,11 @@ function startDaemon() {
|
|
|
899
966
|
setTimeout(() => process.exit(0), 50);
|
|
900
967
|
return;
|
|
901
968
|
}
|
|
969
|
+
const callerBuild = Number(req.headers["x-agent-idle-build"]);
|
|
970
|
+
if (isNewerBuild(callerBuild, OWN_BUILD)) {
|
|
971
|
+
debug(`caller build ${callerBuild} > own ${OWN_BUILD.mtimeMs} \u2014 retiring after this request`);
|
|
972
|
+
setTimeout(() => process.exit(0), 250);
|
|
973
|
+
}
|
|
902
974
|
if (req.method !== "POST") {
|
|
903
975
|
res.writeHead(404, cors).end();
|
|
904
976
|
return;
|
|
@@ -920,7 +992,59 @@ function startDaemon() {
|
|
|
920
992
|
res.writeHead(204, cors).end();
|
|
921
993
|
});
|
|
922
994
|
});
|
|
923
|
-
|
|
995
|
+
async function isOurPreGuardDaemon() {
|
|
996
|
+
try {
|
|
997
|
+
const controller = new AbortController();
|
|
998
|
+
const timer = setTimeout(() => controller.abort(), 800);
|
|
999
|
+
const res = await fetch(`${DAEMON_URL}/token`, { signal: controller.signal });
|
|
1000
|
+
clearTimeout(timer);
|
|
1001
|
+
if (!res.ok) return false;
|
|
1002
|
+
const body = await res.json();
|
|
1003
|
+
return "token" in body;
|
|
1004
|
+
} catch {
|
|
1005
|
+
return false;
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
async function takeoverIfStale() {
|
|
1009
|
+
try {
|
|
1010
|
+
const controller = new AbortController();
|
|
1011
|
+
const timer = setTimeout(() => controller.abort(), 800);
|
|
1012
|
+
const res = await fetch(`${DAEMON_URL}/build`, { signal: controller.signal });
|
|
1013
|
+
clearTimeout(timer);
|
|
1014
|
+
if (res.ok) {
|
|
1015
|
+
const incumbent = await res.json();
|
|
1016
|
+
if (!isNewerBuild(OWN_BUILD.mtimeMs, { script: "", mtimeMs: incumbent.mtimeMs ?? 0 })) {
|
|
1017
|
+
process.exit(0);
|
|
1018
|
+
}
|
|
1019
|
+
debug(`incumbent build ${incumbent.mtimeMs} is stale \u2014 asking it to retire`);
|
|
1020
|
+
} else {
|
|
1021
|
+
if (!await isOurPreGuardDaemon()) process.exit(0);
|
|
1022
|
+
debug("incumbent predates the build guard \u2014 asking it to retire");
|
|
1023
|
+
}
|
|
1024
|
+
await fetch(`${DAEMON_URL}/shutdown`, { method: "POST" }).catch(() => {
|
|
1025
|
+
});
|
|
1026
|
+
for (let i = 0; i < 20; i++) {
|
|
1027
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
1028
|
+
if (!await pingDaemon()) {
|
|
1029
|
+
server.listen(DAEMON_PORT, "127.0.0.1", () => {
|
|
1030
|
+
console.log(`agent-idle daemon took over port ${DAEMON_PORT} from a stale build`);
|
|
1031
|
+
});
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
} catch {
|
|
1036
|
+
}
|
|
1037
|
+
process.exit(0);
|
|
1038
|
+
}
|
|
1039
|
+
let takeoverAttempted = false;
|
|
1040
|
+
server.on("error", (err) => {
|
|
1041
|
+
if (err.code === "EADDRINUSE" && !takeoverAttempted) {
|
|
1042
|
+
takeoverAttempted = true;
|
|
1043
|
+
void takeoverIfStale();
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
process.exit(0);
|
|
1047
|
+
});
|
|
924
1048
|
server.listen(DAEMON_PORT, "127.0.0.1", () => {
|
|
925
1049
|
console.log(`agent-idle daemon listening on http://127.0.0.1:${DAEMON_PORT}`);
|
|
926
1050
|
console.log(
|
|
@@ -934,44 +1058,6 @@ function startDaemon() {
|
|
|
934
1058
|
|
|
935
1059
|
// src/hook.ts
|
|
936
1060
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
937
|
-
|
|
938
|
-
// src/daemonControl.ts
|
|
939
|
-
import { spawn } from "child_process";
|
|
940
|
-
import { fileURLToPath } from "url";
|
|
941
|
-
function spawnDaemon() {
|
|
942
|
-
const indexPath = fileURLToPath(new URL("./index.js", import.meta.url));
|
|
943
|
-
const child = spawn(process.execPath, [indexPath, "daemon"], {
|
|
944
|
-
detached: true,
|
|
945
|
-
stdio: "ignore"
|
|
946
|
-
});
|
|
947
|
-
child.unref();
|
|
948
|
-
}
|
|
949
|
-
async function pingDaemon() {
|
|
950
|
-
try {
|
|
951
|
-
const controller = new AbortController();
|
|
952
|
-
const timer = setTimeout(() => controller.abort(), 500);
|
|
953
|
-
const res = await fetch(`${DAEMON_URL}/token`, { signal: controller.signal });
|
|
954
|
-
clearTimeout(timer);
|
|
955
|
-
return res.ok;
|
|
956
|
-
} catch {
|
|
957
|
-
return false;
|
|
958
|
-
}
|
|
959
|
-
}
|
|
960
|
-
async function daemonToken() {
|
|
961
|
-
try {
|
|
962
|
-
const controller = new AbortController();
|
|
963
|
-
const timer = setTimeout(() => controller.abort(), 800);
|
|
964
|
-
const res = await fetch(`${DAEMON_URL}/token`, { signal: controller.signal });
|
|
965
|
-
clearTimeout(timer);
|
|
966
|
-
if (!res.ok) return null;
|
|
967
|
-
const { token } = await res.json();
|
|
968
|
-
return token ?? null;
|
|
969
|
-
} catch {
|
|
970
|
-
return null;
|
|
971
|
-
}
|
|
972
|
-
}
|
|
973
|
-
|
|
974
|
-
// src/hook.ts
|
|
975
1061
|
var SHELL_COMMS = /* @__PURE__ */ new Set([
|
|
976
1062
|
"sh",
|
|
977
1063
|
"bash",
|
|
@@ -1052,7 +1138,13 @@ async function postToDaemon(url, body) {
|
|
|
1052
1138
|
const timer = setTimeout(() => controller.abort(), 800);
|
|
1053
1139
|
await fetch(url, {
|
|
1054
1140
|
method: "POST",
|
|
1055
|
-
headers: {
|
|
1141
|
+
headers: {
|
|
1142
|
+
"Content-Type": "application/json",
|
|
1143
|
+
// Stale-daemon guard: the hook is spawned fresh per event, so this mtime always
|
|
1144
|
+
// reflects the build ON DISK. A daemon seeing a newer caller retires itself and
|
|
1145
|
+
// the next hook respawns the new build — no manual `kill` after updates.
|
|
1146
|
+
"x-agent-idle-build": String(buildFingerprint().mtimeMs)
|
|
1147
|
+
},
|
|
1056
1148
|
body,
|
|
1057
1149
|
signal: controller.signal
|
|
1058
1150
|
});
|
|
@@ -1205,10 +1297,9 @@ async function signIn() {
|
|
|
1205
1297
|
);
|
|
1206
1298
|
return;
|
|
1207
1299
|
}
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
}
|
|
1300
|
+
spawnDaemon();
|
|
1301
|
+
const settleBy = Date.now() + 5e3;
|
|
1302
|
+
while (Date.now() < settleBy && !await pingDaemon()) await sleep3(250);
|
|
1212
1303
|
console.log(`
|
|
1213
1304
|
Opening ${AUTH_URL} to sign in (GitHub or email + password)\u2026`);
|
|
1214
1305
|
console.log(
|