@agent-idle/cli 0.1.6 → 0.1.8
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 +93 -67
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -173,20 +173,43 @@ var BUILD_CONVEX_URL = "https://handsome-camel-783.convex.cloud";
|
|
|
173
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
|
-
return process.env.CONVEX_URL ?? persistedConvexUrl() ??
|
|
176
|
+
return process.env.CONVEX_URL ?? BUILD_CONVEX_URL ?? persistedConvexUrl() ?? LOCAL_CONVEX_URL;
|
|
177
|
+
}
|
|
178
|
+
function convexUrlFromToken(token) {
|
|
179
|
+
if (!token) return null;
|
|
180
|
+
const payload = token.split(".")[1];
|
|
181
|
+
if (!payload) return null;
|
|
182
|
+
try {
|
|
183
|
+
const iss = JSON.parse(Buffer.from(payload, "base64url").toString("utf8")).iss;
|
|
184
|
+
if (typeof iss !== "string") return null;
|
|
185
|
+
const match = /^https:\/\/([a-z0-9-]+)\.convex\.site\/?$/.exec(iss);
|
|
186
|
+
return match ? `https://${match[1]}.convex.cloud` : null;
|
|
187
|
+
} catch {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function jwtExpiryMs(token) {
|
|
192
|
+
if (!token) return null;
|
|
193
|
+
const payload = token.split(".")[1];
|
|
194
|
+
if (!payload) return null;
|
|
195
|
+
try {
|
|
196
|
+
const exp = JSON.parse(Buffer.from(payload, "base64url").toString("utf8")).exp;
|
|
197
|
+
return typeof exp === "number" ? exp * 1e3 : null;
|
|
198
|
+
} catch {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
177
201
|
}
|
|
178
202
|
var DEFAULT_CONVEX_URL = resolveConvexUrl();
|
|
179
203
|
var AUTH_URL = process.env.AGENT_IDLE_AUTH_URL ?? BUILD_AUTH_URL ?? "http://localhost:1420";
|
|
180
204
|
function cliAuthUrl(userCode2) {
|
|
181
205
|
try {
|
|
182
|
-
const url = new URL(AUTH_URL);
|
|
183
|
-
url.searchParams.set("login", "1");
|
|
206
|
+
const url = new URL("/device", AUTH_URL);
|
|
184
207
|
if (userCode2) url.searchParams.set("code", userCode2);
|
|
185
208
|
return url.toString();
|
|
186
209
|
} catch {
|
|
187
|
-
const
|
|
188
|
-
const code = userCode2 ?
|
|
189
|
-
return `${
|
|
210
|
+
const base = AUTH_URL.replace(/\/$/, "");
|
|
211
|
+
const code = userCode2 ? `?code=${encodeURIComponent(userCode2)}` : "";
|
|
212
|
+
return `${base}/device${code}`;
|
|
190
213
|
}
|
|
191
214
|
}
|
|
192
215
|
function ensureDir(path, mode) {
|
|
@@ -199,22 +222,31 @@ function ensureDir(path, mode) {
|
|
|
199
222
|
}
|
|
200
223
|
}
|
|
201
224
|
}
|
|
202
|
-
function
|
|
225
|
+
function readTokens() {
|
|
203
226
|
if (!existsSync(AUTH_TOKEN_PATH)) return null;
|
|
204
227
|
try {
|
|
205
|
-
|
|
228
|
+
const parsed = JSON.parse(readFileSync(AUTH_TOKEN_PATH, "utf8"));
|
|
229
|
+
return parsed.token ? { token: parsed.token, refreshToken: parsed.refreshToken } : null;
|
|
206
230
|
} catch {
|
|
207
231
|
return null;
|
|
208
232
|
}
|
|
209
233
|
}
|
|
210
|
-
function
|
|
234
|
+
function readToken() {
|
|
235
|
+
return readTokens()?.token ?? null;
|
|
236
|
+
}
|
|
237
|
+
function writeTokens(tokens) {
|
|
211
238
|
ensureDir(AUTH_TOKEN_PATH, 448);
|
|
212
|
-
|
|
239
|
+
const payload = { token: tokens.token };
|
|
240
|
+
if (tokens.refreshToken) payload.refreshToken = tokens.refreshToken;
|
|
241
|
+
writeFileSync(AUTH_TOKEN_PATH, JSON.stringify(payload) + "\n", { mode: 384 });
|
|
213
242
|
try {
|
|
214
243
|
chmodSync(AUTH_TOKEN_PATH, 384);
|
|
215
244
|
} catch {
|
|
216
245
|
}
|
|
217
246
|
}
|
|
247
|
+
function writeToken(token) {
|
|
248
|
+
writeTokens({ token });
|
|
249
|
+
}
|
|
218
250
|
function clearToken() {
|
|
219
251
|
try {
|
|
220
252
|
if (existsSync(AUTH_TOKEN_PATH)) rmSync(AUTH_TOKEN_PATH);
|
|
@@ -461,6 +493,8 @@ function topicWord(prompt) {
|
|
|
461
493
|
}
|
|
462
494
|
var ingestEvent = makeFunctionReference("events:ingestEvent");
|
|
463
495
|
var ingestEvents = makeFunctionReference("events:ingestEvents");
|
|
496
|
+
var authSignIn = makeFunctionReference("auth:signIn");
|
|
497
|
+
var TOKEN_REFRESH_SKEW_MS = 2 * 6e4;
|
|
464
498
|
var FLUSH_CHUNK = 100;
|
|
465
499
|
function isPureRenewal(item) {
|
|
466
500
|
if (item.type !== "activity") return false;
|
|
@@ -861,18 +895,41 @@ function startDaemon() {
|
|
|
861
895
|
debug(`liveness: agent pid=${proc.pid} session=${tag(session)} gone \u2192 ended (killed)`);
|
|
862
896
|
}
|
|
863
897
|
}
|
|
898
|
+
async function ensureFreshToken() {
|
|
899
|
+
const tokens = readTokens();
|
|
900
|
+
if (!tokens) return null;
|
|
901
|
+
const expMs = jwtExpiryMs(tokens.token);
|
|
902
|
+
if (expMs !== null && expMs - Date.now() > TOKEN_REFRESH_SKEW_MS) return tokens.token;
|
|
903
|
+
if (!tokens.refreshToken) return tokens.token;
|
|
904
|
+
try {
|
|
905
|
+
const url = convexUrlFromToken(tokens.token) ?? resolveConvexUrl();
|
|
906
|
+
const res = await new ConvexHttpClient(url).action(authSignIn, {
|
|
907
|
+
refreshToken: tokens.refreshToken
|
|
908
|
+
});
|
|
909
|
+
if (!res?.tokens) {
|
|
910
|
+
debug("refresh: session expired \u2014 re-auth needed (agent-idle setup)");
|
|
911
|
+
return null;
|
|
912
|
+
}
|
|
913
|
+
writeTokens({ token: res.tokens.token, refreshToken: res.tokens.refreshToken });
|
|
914
|
+
debug("refresh: rotated machine session token");
|
|
915
|
+
return res.tokens.token;
|
|
916
|
+
} catch (err) {
|
|
917
|
+
debug(`refresh: failed (${err?.message ?? err}) \u2014 using existing token`);
|
|
918
|
+
return tokens.token;
|
|
919
|
+
}
|
|
920
|
+
}
|
|
864
921
|
async function flush() {
|
|
865
922
|
if (flushing) return;
|
|
866
|
-
const token = readToken();
|
|
867
923
|
const items = readOutbox();
|
|
868
924
|
if (items.length === 0) return;
|
|
869
|
-
if (!token) {
|
|
870
|
-
debug(`flush: ${items.length} event(s) queued but no auth token \u2014 sign in via the app`);
|
|
871
|
-
return;
|
|
872
|
-
}
|
|
873
925
|
flushing = true;
|
|
874
926
|
try {
|
|
875
|
-
const
|
|
927
|
+
const token = await ensureFreshToken();
|
|
928
|
+
if (!token) {
|
|
929
|
+
debug(`flush: ${items.length} event(s) queued but no usable auth token \u2014 run \`agent-idle setup\``);
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
const convexUrl = convexUrlFromToken(token) ?? resolveConvexUrl();
|
|
876
933
|
debug(`flush: posting ${items.length} event(s) to ${convexUrl}`);
|
|
877
934
|
const client = new ConvexHttpClient(convexUrl);
|
|
878
935
|
client.setAuth(token);
|
|
@@ -1278,46 +1335,16 @@ import { cancel, isCancel, multiselect } from "@clack/prompts";
|
|
|
1278
1335
|
|
|
1279
1336
|
// src/auth.ts
|
|
1280
1337
|
import { spawn as spawn2 } from "child_process";
|
|
1281
|
-
import { randomBytes, randomUUID as randomUUID2
|
|
1338
|
+
import { randomBytes, randomUUID as randomUUID2 } from "crypto";
|
|
1282
1339
|
import { ConvexHttpClient as ConvexHttpClient2 } from "convex/browser";
|
|
1283
1340
|
import { makeFunctionReference as makeFunctionReference2 } from "convex/server";
|
|
1284
1341
|
var sleep3 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
1285
1342
|
var createDeviceAuth = makeFunctionReference2("deviceAuth:create");
|
|
1286
|
-
var
|
|
1343
|
+
var statusDeviceAuth = makeFunctionReference2("deviceAuth:status");
|
|
1344
|
+
var authSignIn2 = makeFunctionReference2("auth:signIn");
|
|
1287
1345
|
function userCode() {
|
|
1288
1346
|
return randomBytes(5).toString("hex").toUpperCase();
|
|
1289
1347
|
}
|
|
1290
|
-
function b64ToBytes(value) {
|
|
1291
|
-
return Buffer.from(value, "base64");
|
|
1292
|
-
}
|
|
1293
|
-
async function createDeviceKeyPair() {
|
|
1294
|
-
const keyPair = await webcrypto.subtle.generateKey(
|
|
1295
|
-
{
|
|
1296
|
-
name: "RSA-OAEP",
|
|
1297
|
-
modulusLength: 2048,
|
|
1298
|
-
publicExponent: new Uint8Array([1, 0, 1]),
|
|
1299
|
-
hash: "SHA-256"
|
|
1300
|
-
},
|
|
1301
|
-
true,
|
|
1302
|
-
["encrypt", "decrypt"]
|
|
1303
|
-
);
|
|
1304
|
-
const publicJwk = await webcrypto.subtle.exportKey("jwk", keyPair.publicKey);
|
|
1305
|
-
return { publicKeyJwk: JSON.stringify(publicJwk), privateKey: keyPair.privateKey };
|
|
1306
|
-
}
|
|
1307
|
-
async function decryptToken(payload, privateKey) {
|
|
1308
|
-
const rawKey = await webcrypto.subtle.decrypt(
|
|
1309
|
-
{ name: "RSA-OAEP" },
|
|
1310
|
-
privateKey,
|
|
1311
|
-
b64ToBytes(payload.encryptedKey)
|
|
1312
|
-
);
|
|
1313
|
-
const key = await webcrypto.subtle.importKey("raw", rawKey, { name: "AES-GCM" }, false, ["decrypt"]);
|
|
1314
|
-
const plaintext = await webcrypto.subtle.decrypt(
|
|
1315
|
-
{ name: "AES-GCM", iv: b64ToBytes(payload.iv) },
|
|
1316
|
-
key,
|
|
1317
|
-
b64ToBytes(payload.ciphertext)
|
|
1318
|
-
);
|
|
1319
|
-
return new TextDecoder().decode(plaintext);
|
|
1320
|
-
}
|
|
1321
1348
|
function openBrowser(url) {
|
|
1322
1349
|
const [bin, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
|
|
1323
1350
|
try {
|
|
@@ -1330,9 +1357,7 @@ function openBrowser(url) {
|
|
|
1330
1357
|
}
|
|
1331
1358
|
async function signIn() {
|
|
1332
1359
|
if (readToken()) {
|
|
1333
|
-
console.log(
|
|
1334
|
-
"\u2713 Already signed in (shared session at ~/.agent-idle/auth.json)."
|
|
1335
|
-
);
|
|
1360
|
+
console.log("\u2713 Already signed in (session at ~/.agent-idle/auth.json).");
|
|
1336
1361
|
return;
|
|
1337
1362
|
}
|
|
1338
1363
|
spawnDaemon();
|
|
@@ -1341,32 +1366,33 @@ async function signIn() {
|
|
|
1341
1366
|
const client = new ConvexHttpClient2(resolveConvexUrl());
|
|
1342
1367
|
const deviceId = randomUUID2();
|
|
1343
1368
|
const code = userCode();
|
|
1344
|
-
|
|
1345
|
-
await client.mutation(createDeviceAuth, { deviceId, userCode: code, publicKeyJwk });
|
|
1369
|
+
await client.mutation(createDeviceAuth, { deviceId, userCode: code });
|
|
1346
1370
|
const authUrl = cliAuthUrl(code);
|
|
1347
1371
|
console.log(`
|
|
1348
1372
|
Opening ${authUrl} to sign in (GitHub or email + password)\u2026`);
|
|
1349
1373
|
console.log(`Your device code is ${code}.`);
|
|
1350
|
-
console.log(
|
|
1351
|
-
"If it doesn't open, visit that URL manually. Waiting for sign-in\u2026 (Ctrl-C to cancel)"
|
|
1352
|
-
);
|
|
1374
|
+
console.log("If it doesn't open, visit that URL manually. Waiting for sign-in\u2026 (Ctrl-C to cancel)");
|
|
1353
1375
|
openBrowser(authUrl);
|
|
1354
|
-
const deadline = Date.now() +
|
|
1376
|
+
const deadline = Date.now() + 3e5;
|
|
1355
1377
|
while (Date.now() < deadline) {
|
|
1356
1378
|
await sleep3(1500);
|
|
1357
|
-
const res = await client.mutation(
|
|
1379
|
+
const res = await client.mutation(statusDeviceAuth, { deviceId });
|
|
1358
1380
|
if (res.status === "approved") {
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
);
|
|
1381
|
+
const signedIn = await client.action(authSignIn2, {
|
|
1382
|
+
provider: "device",
|
|
1383
|
+
params: { deviceId }
|
|
1384
|
+
});
|
|
1385
|
+
if (!signedIn?.tokens) {
|
|
1386
|
+
console.log("Sign-in could not be completed. Re-run `agent-idle setup`.");
|
|
1387
|
+
return;
|
|
1388
|
+
}
|
|
1389
|
+
writeTokens({ token: signedIn.tokens.token, refreshToken: signedIn.tokens.refreshToken });
|
|
1390
|
+
console.log("\u2713 Signed in \u2014 this machine has its own session; the daemon keeps it fresh on its own.");
|
|
1363
1391
|
return;
|
|
1364
1392
|
}
|
|
1365
|
-
if (res.status === "expired") break;
|
|
1393
|
+
if (res.status === "expired" || res.status === "consumed") break;
|
|
1366
1394
|
}
|
|
1367
|
-
console.log(
|
|
1368
|
-
"Timed out waiting for sign-in. Re-run `agent-idle setup` once you've signed in."
|
|
1369
|
-
);
|
|
1395
|
+
console.log("Timed out waiting for sign-in. Re-run `agent-idle setup` once you've signed in.");
|
|
1370
1396
|
}
|
|
1371
1397
|
|
|
1372
1398
|
// src/setup.ts
|