@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.
Files changed (2) hide show
  1. package/index.js +93 -67
  2. 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() ?? BUILD_CONVEX_URL ?? LOCAL_CONVEX_URL;
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 join2 = AUTH_URL.includes("?") ? "&" : "?";
188
- const code = userCode2 ? `&code=${encodeURIComponent(userCode2)}` : "";
189
- return `${AUTH_URL}${join2}login=1${code}`;
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 readToken() {
225
+ function readTokens() {
203
226
  if (!existsSync(AUTH_TOKEN_PATH)) return null;
204
227
  try {
205
- return JSON.parse(readFileSync(AUTH_TOKEN_PATH, "utf8")).token ?? null;
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 writeToken(token) {
234
+ function readToken() {
235
+ return readTokens()?.token ?? null;
236
+ }
237
+ function writeTokens(tokens) {
211
238
  ensureDir(AUTH_TOKEN_PATH, 448);
212
- writeFileSync(AUTH_TOKEN_PATH, JSON.stringify({ token }) + "\n", { mode: 384 });
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 convexUrl = resolveConvexUrl();
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, webcrypto } from "crypto";
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 pollDeviceAuth = makeFunctionReference2("deviceAuth:poll");
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
- const { publicKeyJwk, privateKey } = await createDeviceKeyPair();
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() + 12e4;
1376
+ const deadline = Date.now() + 3e5;
1355
1377
  while (Date.now() < deadline) {
1356
1378
  await sleep3(1500);
1357
- const res = await client.mutation(pollDeviceAuth, { deviceId });
1379
+ const res = await client.mutation(statusDeviceAuth, { deviceId });
1358
1380
  if (res.status === "approved") {
1359
- writeToken(await decryptToken(res.encryptedToken, privateKey));
1360
- console.log(
1361
- "\u2713 Signed in \u2014 shared session established for the app, CLI, and daemon."
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-idle/cli",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "agent-idle CLI: setup, ui, and the headless sensor daemon.",
5
5
  "license": "AGPL-3.0-only",
6
6
  "type": "module",