@agent-idle/cli 0.1.7 → 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 +91 -64
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -173,19 +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("/
|
|
206
|
+
const url = new URL("/device", AUTH_URL);
|
|
183
207
|
if (userCode2) url.searchParams.set("code", userCode2);
|
|
184
208
|
return url.toString();
|
|
185
209
|
} catch {
|
|
186
210
|
const base = AUTH_URL.replace(/\/$/, "");
|
|
187
211
|
const code = userCode2 ? `?code=${encodeURIComponent(userCode2)}` : "";
|
|
188
|
-
return `${base}/
|
|
212
|
+
return `${base}/device${code}`;
|
|
189
213
|
}
|
|
190
214
|
}
|
|
191
215
|
function ensureDir(path, mode) {
|
|
@@ -198,22 +222,31 @@ function ensureDir(path, mode) {
|
|
|
198
222
|
}
|
|
199
223
|
}
|
|
200
224
|
}
|
|
201
|
-
function
|
|
225
|
+
function readTokens() {
|
|
202
226
|
if (!existsSync(AUTH_TOKEN_PATH)) return null;
|
|
203
227
|
try {
|
|
204
|
-
|
|
228
|
+
const parsed = JSON.parse(readFileSync(AUTH_TOKEN_PATH, "utf8"));
|
|
229
|
+
return parsed.token ? { token: parsed.token, refreshToken: parsed.refreshToken } : null;
|
|
205
230
|
} catch {
|
|
206
231
|
return null;
|
|
207
232
|
}
|
|
208
233
|
}
|
|
209
|
-
function
|
|
234
|
+
function readToken() {
|
|
235
|
+
return readTokens()?.token ?? null;
|
|
236
|
+
}
|
|
237
|
+
function writeTokens(tokens) {
|
|
210
238
|
ensureDir(AUTH_TOKEN_PATH, 448);
|
|
211
|
-
|
|
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 });
|
|
212
242
|
try {
|
|
213
243
|
chmodSync(AUTH_TOKEN_PATH, 384);
|
|
214
244
|
} catch {
|
|
215
245
|
}
|
|
216
246
|
}
|
|
247
|
+
function writeToken(token) {
|
|
248
|
+
writeTokens({ token });
|
|
249
|
+
}
|
|
217
250
|
function clearToken() {
|
|
218
251
|
try {
|
|
219
252
|
if (existsSync(AUTH_TOKEN_PATH)) rmSync(AUTH_TOKEN_PATH);
|
|
@@ -460,6 +493,8 @@ function topicWord(prompt) {
|
|
|
460
493
|
}
|
|
461
494
|
var ingestEvent = makeFunctionReference("events:ingestEvent");
|
|
462
495
|
var ingestEvents = makeFunctionReference("events:ingestEvents");
|
|
496
|
+
var authSignIn = makeFunctionReference("auth:signIn");
|
|
497
|
+
var TOKEN_REFRESH_SKEW_MS = 2 * 6e4;
|
|
463
498
|
var FLUSH_CHUNK = 100;
|
|
464
499
|
function isPureRenewal(item) {
|
|
465
500
|
if (item.type !== "activity") return false;
|
|
@@ -860,18 +895,41 @@ function startDaemon() {
|
|
|
860
895
|
debug(`liveness: agent pid=${proc.pid} session=${tag(session)} gone \u2192 ended (killed)`);
|
|
861
896
|
}
|
|
862
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
|
+
}
|
|
863
921
|
async function flush() {
|
|
864
922
|
if (flushing) return;
|
|
865
|
-
const token = readToken();
|
|
866
923
|
const items = readOutbox();
|
|
867
924
|
if (items.length === 0) return;
|
|
868
|
-
if (!token) {
|
|
869
|
-
debug(`flush: ${items.length} event(s) queued but no auth token \u2014 sign in via the app`);
|
|
870
|
-
return;
|
|
871
|
-
}
|
|
872
925
|
flushing = true;
|
|
873
926
|
try {
|
|
874
|
-
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();
|
|
875
933
|
debug(`flush: posting ${items.length} event(s) to ${convexUrl}`);
|
|
876
934
|
const client = new ConvexHttpClient(convexUrl);
|
|
877
935
|
client.setAuth(token);
|
|
@@ -1277,46 +1335,16 @@ import { cancel, isCancel, multiselect } from "@clack/prompts";
|
|
|
1277
1335
|
|
|
1278
1336
|
// src/auth.ts
|
|
1279
1337
|
import { spawn as spawn2 } from "child_process";
|
|
1280
|
-
import { randomBytes, randomUUID as randomUUID2
|
|
1338
|
+
import { randomBytes, randomUUID as randomUUID2 } from "crypto";
|
|
1281
1339
|
import { ConvexHttpClient as ConvexHttpClient2 } from "convex/browser";
|
|
1282
1340
|
import { makeFunctionReference as makeFunctionReference2 } from "convex/server";
|
|
1283
1341
|
var sleep3 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
1284
1342
|
var createDeviceAuth = makeFunctionReference2("deviceAuth:create");
|
|
1285
|
-
var
|
|
1343
|
+
var statusDeviceAuth = makeFunctionReference2("deviceAuth:status");
|
|
1344
|
+
var authSignIn2 = makeFunctionReference2("auth:signIn");
|
|
1286
1345
|
function userCode() {
|
|
1287
1346
|
return randomBytes(5).toString("hex").toUpperCase();
|
|
1288
1347
|
}
|
|
1289
|
-
function b64ToBytes(value) {
|
|
1290
|
-
return Buffer.from(value, "base64");
|
|
1291
|
-
}
|
|
1292
|
-
async function createDeviceKeyPair() {
|
|
1293
|
-
const keyPair = await webcrypto.subtle.generateKey(
|
|
1294
|
-
{
|
|
1295
|
-
name: "RSA-OAEP",
|
|
1296
|
-
modulusLength: 2048,
|
|
1297
|
-
publicExponent: new Uint8Array([1, 0, 1]),
|
|
1298
|
-
hash: "SHA-256"
|
|
1299
|
-
},
|
|
1300
|
-
true,
|
|
1301
|
-
["encrypt", "decrypt"]
|
|
1302
|
-
);
|
|
1303
|
-
const publicJwk = await webcrypto.subtle.exportKey("jwk", keyPair.publicKey);
|
|
1304
|
-
return { publicKeyJwk: JSON.stringify(publicJwk), privateKey: keyPair.privateKey };
|
|
1305
|
-
}
|
|
1306
|
-
async function decryptToken(payload, privateKey) {
|
|
1307
|
-
const rawKey = await webcrypto.subtle.decrypt(
|
|
1308
|
-
{ name: "RSA-OAEP" },
|
|
1309
|
-
privateKey,
|
|
1310
|
-
b64ToBytes(payload.encryptedKey)
|
|
1311
|
-
);
|
|
1312
|
-
const key = await webcrypto.subtle.importKey("raw", rawKey, { name: "AES-GCM" }, false, ["decrypt"]);
|
|
1313
|
-
const plaintext = await webcrypto.subtle.decrypt(
|
|
1314
|
-
{ name: "AES-GCM", iv: b64ToBytes(payload.iv) },
|
|
1315
|
-
key,
|
|
1316
|
-
b64ToBytes(payload.ciphertext)
|
|
1317
|
-
);
|
|
1318
|
-
return new TextDecoder().decode(plaintext);
|
|
1319
|
-
}
|
|
1320
1348
|
function openBrowser(url) {
|
|
1321
1349
|
const [bin, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
|
|
1322
1350
|
try {
|
|
@@ -1329,9 +1357,7 @@ function openBrowser(url) {
|
|
|
1329
1357
|
}
|
|
1330
1358
|
async function signIn() {
|
|
1331
1359
|
if (readToken()) {
|
|
1332
|
-
console.log(
|
|
1333
|
-
"\u2713 Already signed in (shared session at ~/.agent-idle/auth.json)."
|
|
1334
|
-
);
|
|
1360
|
+
console.log("\u2713 Already signed in (session at ~/.agent-idle/auth.json).");
|
|
1335
1361
|
return;
|
|
1336
1362
|
}
|
|
1337
1363
|
spawnDaemon();
|
|
@@ -1340,32 +1366,33 @@ async function signIn() {
|
|
|
1340
1366
|
const client = new ConvexHttpClient2(resolveConvexUrl());
|
|
1341
1367
|
const deviceId = randomUUID2();
|
|
1342
1368
|
const code = userCode();
|
|
1343
|
-
|
|
1344
|
-
await client.mutation(createDeviceAuth, { deviceId, userCode: code, publicKeyJwk });
|
|
1369
|
+
await client.mutation(createDeviceAuth, { deviceId, userCode: code });
|
|
1345
1370
|
const authUrl = cliAuthUrl(code);
|
|
1346
1371
|
console.log(`
|
|
1347
1372
|
Opening ${authUrl} to sign in (GitHub or email + password)\u2026`);
|
|
1348
1373
|
console.log(`Your device code is ${code}.`);
|
|
1349
|
-
console.log(
|
|
1350
|
-
"If it doesn't open, visit that URL manually. Waiting for sign-in\u2026 (Ctrl-C to cancel)"
|
|
1351
|
-
);
|
|
1374
|
+
console.log("If it doesn't open, visit that URL manually. Waiting for sign-in\u2026 (Ctrl-C to cancel)");
|
|
1352
1375
|
openBrowser(authUrl);
|
|
1353
|
-
const deadline = Date.now() +
|
|
1376
|
+
const deadline = Date.now() + 3e5;
|
|
1354
1377
|
while (Date.now() < deadline) {
|
|
1355
1378
|
await sleep3(1500);
|
|
1356
|
-
const res = await client.mutation(
|
|
1379
|
+
const res = await client.mutation(statusDeviceAuth, { deviceId });
|
|
1357
1380
|
if (res.status === "approved") {
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
);
|
|
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.");
|
|
1362
1391
|
return;
|
|
1363
1392
|
}
|
|
1364
|
-
if (res.status === "expired") break;
|
|
1393
|
+
if (res.status === "expired" || res.status === "consumed") break;
|
|
1365
1394
|
}
|
|
1366
|
-
console.log(
|
|
1367
|
-
"Timed out waiting for sign-in. Re-run `agent-idle setup` once you've signed in."
|
|
1368
|
-
);
|
|
1395
|
+
console.log("Timed out waiting for sign-in. Re-run `agent-idle setup` once you've signed in.");
|
|
1369
1396
|
}
|
|
1370
1397
|
|
|
1371
1398
|
// src/setup.ts
|