@agent-idle/cli 0.1.7 → 0.1.9

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 -66
  2. 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() ?? 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("/cli", AUTH_URL);
183
- if (userCode2) url.searchParams.set("code", userCode2);
206
+ const url = new URL("/device", AUTH_URL);
207
+ if (userCode2) url.searchParams.set("user_code", userCode2);
184
208
  return url.toString();
185
209
  } catch {
186
210
  const base = AUTH_URL.replace(/\/$/, "");
187
- const code = userCode2 ? `?code=${encodeURIComponent(userCode2)}` : "";
188
- return `${base}/cli${code}`;
211
+ const code = userCode2 ? `?user_code=${encodeURIComponent(userCode2)}` : "";
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 readToken() {
225
+ function readTokens() {
202
226
  if (!existsSync(AUTH_TOKEN_PATH)) return null;
203
227
  try {
204
- 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;
205
230
  } catch {
206
231
  return null;
207
232
  }
208
233
  }
209
- function writeToken(token) {
234
+ function readToken() {
235
+ return readTokens()?.token ?? null;
236
+ }
237
+ function writeTokens(tokens) {
210
238
  ensureDir(AUTH_TOKEN_PATH, 448);
211
- 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 });
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 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();
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, webcrypto } from "crypto";
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 pollDeviceAuth = makeFunctionReference2("deviceAuth:poll");
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
- const { publicKeyJwk, privateKey } = await createDeviceKeyPair();
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() + 12e4;
1376
+ const deadline = Date.now() + 3e5;
1354
1377
  while (Date.now() < deadline) {
1355
1378
  await sleep3(1500);
1356
- const res = await client.mutation(pollDeviceAuth, { deviceId });
1379
+ const res = await client.mutation(statusDeviceAuth, { deviceId });
1357
1380
  if (res.status === "approved") {
1358
- writeToken(await decryptToken(res.encryptedToken, privateKey));
1359
- console.log(
1360
- "\u2713 Signed in \u2014 shared session established for the app, CLI, and daemon."
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-idle/cli",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "agent-idle CLI: setup, ui, and the headless sensor daemon.",
5
5
  "license": "AGPL-3.0-only",
6
6
  "type": "module",