@phnx-labs/agents-cli 1.22.29 → 1.22.30

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 (73) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/README.md +39 -1
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/accounts.d.ts +13 -0
  5. package/dist/commands/accounts.js +32 -0
  6. package/dist/commands/daemon.d.ts +18 -0
  7. package/dist/commands/daemon.js +581 -0
  8. package/dist/commands/exec.js +66 -20
  9. package/dist/commands/routines.js +29 -11
  10. package/dist/commands/secrets.d.ts +17 -0
  11. package/dist/commands/secrets.js +30 -15
  12. package/dist/commands/sessions-browser.js +6 -6
  13. package/dist/commands/sessions-favorite.d.ts +7 -7
  14. package/dist/commands/sessions-favorite.js +30 -30
  15. package/dist/commands/sessions-picker.d.ts +33 -1
  16. package/dist/commands/sessions-picker.js +102 -27
  17. package/dist/commands/sessions.d.ts +12 -1
  18. package/dist/commands/sessions.js +259 -20
  19. package/dist/commands/view.d.ts +11 -0
  20. package/dist/commands/view.js +56 -29
  21. package/dist/index.js +37 -2
  22. package/dist/lib/account-labels.d.ts +24 -0
  23. package/dist/lib/account-labels.js +72 -0
  24. package/dist/lib/agents.d.ts +32 -1
  25. package/dist/lib/agents.js +96 -31
  26. package/dist/lib/daemon-health.d.ts +24 -0
  27. package/dist/lib/daemon-health.js +84 -0
  28. package/dist/lib/daemon-ticks.d.ts +81 -0
  29. package/dist/lib/daemon-ticks.js +190 -0
  30. package/dist/lib/daemon.d.ts +68 -18
  31. package/dist/lib/daemon.js +303 -338
  32. package/dist/lib/device-config.d.ts +10 -0
  33. package/dist/lib/device-config.js +27 -0
  34. package/dist/lib/exec.d.ts +27 -0
  35. package/dist/lib/exec.js +49 -2
  36. package/dist/lib/hosts/dispatch.d.ts +4 -0
  37. package/dist/lib/hosts/dispatch.js +4 -0
  38. package/dist/lib/hosts/remote-cmd.js +1 -0
  39. package/dist/lib/hosts/run-target.d.ts +1 -0
  40. package/dist/lib/hosts/run-target.js +1 -0
  41. package/dist/lib/import.js +7 -6
  42. package/dist/lib/memory-cache.d.ts +19 -0
  43. package/dist/lib/memory-cache.js +31 -0
  44. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  45. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  46. package/dist/lib/migrate.d.ts +1 -1
  47. package/dist/lib/migrate.js +7 -2
  48. package/dist/lib/routine-activation.d.ts +2 -0
  49. package/dist/lib/routine-activation.js +16 -0
  50. package/dist/lib/runner.d.ts +18 -0
  51. package/dist/lib/runner.js +52 -0
  52. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  53. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  54. package/dist/lib/secrets/agent.d.ts +19 -0
  55. package/dist/lib/secrets/agent.js +32 -2
  56. package/dist/lib/secrets/scope.d.ts +3 -3
  57. package/dist/lib/secrets/scope.js +3 -3
  58. package/dist/lib/session/db.d.ts +15 -0
  59. package/dist/lib/session/db.js +90 -15
  60. package/dist/lib/session/discover.js +91 -39
  61. package/dist/lib/session/favorites.d.ts +2 -2
  62. package/dist/lib/session/favorites.js +2 -2
  63. package/dist/lib/session/parse.d.ts +63 -0
  64. package/dist/lib/session/parse.js +165 -20
  65. package/dist/lib/session/session-cache.d.ts +9 -6
  66. package/dist/lib/session/session-cache.js +23 -6
  67. package/dist/lib/shims.js +12 -0
  68. package/dist/lib/startup/command-registry.d.ts +15 -1
  69. package/dist/lib/startup/command-registry.js +49 -0
  70. package/dist/lib/usage-refresh.js +3 -2
  71. package/dist/lib/usage.d.ts +12 -10
  72. package/dist/lib/usage.js +63 -144
  73. package/package.json +4 -1
package/dist/lib/usage.js CHANGED
@@ -681,10 +681,11 @@ export function claudeUsageAccessTokenNoRefresh(oauth) {
681
681
  /** Fetch Claude usage via the Anthropic OAuth usage API. */
682
682
  async function getClaudeUsageInfo(options) {
683
683
  try {
684
- // Opt into the no-ACL access-token cache: this is the every-60s watchdog hot
685
- // path and usage needs only the access token, so it kills the Touch ID storm.
686
- // Daemon refresh also sets fileOnly so we never fall through to the ACL
687
- // keychain item (that path is the Touch ID prompt).
684
+ // accessTokenCache: this is the every-60s watchdog hot path and usage needs
685
+ // only the access token, so it reads ONLY the file-based setup-token and never
686
+ // the interactive login (reading that ACL-bound token and firing it at the
687
+ // usage API is what got it revoked RUSH-1822). No setup-token => null =>
688
+ // "usage pending". fileOnly additionally forbids the ACL keychain path.
688
689
  const oauth = await loadClaudeOauth(options?.home, {
689
690
  accessTokenCache: true,
690
691
  fileOnly: options?.fileOnly === true,
@@ -977,9 +978,10 @@ async function getDroidUsageInfo(options) {
977
978
  }
978
979
  /** Probe Claude's OAuth token against the usage endpoint. Never refreshes — reports `expired` for a near-expiry token; see the comment below (RUSH-1822). */
979
980
  export async function probeClaudeStatus(home, cliVersion) {
980
- // Opt into the no-ACL access-token cache: the daemon warms this probe every ~3
981
- // min per account and it never refreshes (access token only), so caching is safe
982
- // and stops it from adding to the Touch ID storm.
981
+ // accessTokenCache: the daemon warms this probe every ~3 min per account, so it
982
+ // reads ONLY the file-based setup-token and never the interactive login
983
+ // transmitting that ACL-bound token to the usage API from a background loop is
984
+ // what got it revoked (RUSH-1822). No setup-token => token 'missing' below.
983
985
  const oauth = await loadClaudeOauth(home, { accessTokenCache: true });
984
986
  const accessToken = oauth?.accessToken?.trim();
985
987
  if (!accessToken)
@@ -1261,97 +1263,29 @@ function parseClaudeOauthPayload(raw) {
1261
1263
  return null;
1262
1264
  }
1263
1265
  }
1264
- // ── No-ACL cache for Claude's OAuth token (kills the macOS Touch ID storm) ──
1266
+ // ── Stale no-ACL Claude OAuth cache eviction (retired subsystem) ──
1265
1267
  //
1266
- // The source item `Claude Code-credentials-<hash>` is ACL-bound to Claude Code's
1267
- // own process, so every read agents-cli makes (via `/usr/bin/security`) pops
1268
- // Touch ID. The Factory watchdog polls `agents view --json` every 60s per agent,
1269
- // and each poll that crosses the 5-minute usage cache fires a background refresh
1270
- // -> loadClaudeOauth -> keychain read -> a biometric prompt. With many agents and
1271
- // accounts that is a prompt every few minutes, per account.
1272
- //
1273
- // Fix: after one real (prompting) read, cache the ACCESS token in a device-local
1274
- // NO-ACL keychain item — the same `set-no-acl` mechanism secrets/session-store.ts
1275
- // uses for unlocked bundles, whose reads never prompt — and serve every later read
1276
- // from it until the token's own `expiresAt`. The ACL read then happens at most once
1277
- // per token lifetime (~a few times a day), shared across every agent process.
1278
- //
1279
- // Security posture: only the short-lived access token is cached, never the refresh
1280
- // token, and the entry dies at the token's expiry (capped below). This mirrors the
1281
- // no-ACL posture session-store.ts already accepts for held bundles.
1268
+ // Earlier versions cached Claude's OAuth ACCESS token in a device-local no-ACL
1269
+ // keychain item so a read-only usage/probe read wouldn't pop the macOS Touch ID
1270
+ // prompt that the ACL-bound source item (`Claude Code-credentials-<hash>`) forces.
1271
+ // That cache is retired: read-only probes now authenticate ONLY with a file-based
1272
+ // setup-token and never read the interactive login (see loadClaudeOauth), so
1273
+ // nothing populates the cache anymore. deleteCachedClaudeOauth remains a
1274
+ // credential rotation still evicts a stale item an earlier version may have
1275
+ // written, so an old no-ACL copy of the interactive token can't linger.
1282
1276
  const CLAUDE_OAUTH_CACHE_PREFIX = 'agents-cli.claude-oauth-cache.';
1283
- /** Hard cap so a token with a distant or absent `expiresAt` can't pin a stale entry. */
1284
- const CLAUDE_OAUTH_CACHE_MAX_TTL_MS = 8 * 60 * 60 * 1000;
1285
1277
  /** The no-ACL cache item name for a Claude keychain service (hashed to stay tidy). */
1286
1278
  function claudeOauthCacheItem(service) {
1287
1279
  const hash = createHash('sha256').update(service).digest('hex').slice(0, 16);
1288
1280
  return `${CLAUDE_OAUTH_CACHE_PREFIX}${hash}`;
1289
1281
  }
1290
- /** Whether the no-ACL cache is meaningful: macOS (where the source read prompts) or
1291
- * whenever a test keychain backend is installed, so the path is exercisable on CI.
1292
- * Off macOS in production the source read is prompt-free, so there is nothing to cache. */
1293
- function claudeOauthCacheActive() {
1294
- return process.platform === 'darwin' || isKeychainBackendOverridden();
1295
- }
1296
- /** Read the cached access token (prompt-free). Null on miss, or when the cache entry
1297
- * or the token itself has expired — in which case the stale entry is dropped. */
1298
- function readCachedClaudeOauth(service) {
1299
- try {
1300
- // The cache item is written no-ACL (writeCachedClaudeOauth) — its whole purpose is
1301
- // serving the token prompt-free, so attest that to the raw-read storm guard.
1302
- const entry = JSON.parse(getKeychainToken(claudeOauthCacheItem(service), { silentNoAcl: true }));
1303
- if (!entry || typeof entry.accessToken !== 'string' || !entry.accessToken)
1304
- return null;
1305
- const now = Date.now();
1306
- const tokenExpired = typeof entry.expiresAt === 'number' && entry.expiresAt > 0 && now >= entry.expiresAt;
1307
- if (now >= entry.cacheExpiresAt || tokenExpired) {
1308
- try {
1309
- deleteKeychainToken(claudeOauthCacheItem(service));
1310
- }
1311
- catch {
1312
- /* best-effort eviction */
1313
- }
1314
- return null;
1315
- }
1316
- return {
1317
- accessToken: entry.accessToken,
1318
- refreshToken: null,
1319
- expiresAt: entry.expiresAt ?? null,
1320
- scopes: entry.scopes ?? null,
1321
- subscriptionType: entry.subscriptionType ?? null,
1322
- rateLimitTier: entry.rateLimitTier ?? null,
1323
- organizationUuid: entry.organizationUuid ?? null,
1324
- };
1325
- }
1326
- catch {
1327
- return null;
1328
- }
1329
- }
1330
- /** Populate the no-ACL cache after a real (prompting) source read. Best-effort — the
1331
- * cache is an optimization, so any failure just means the next read prompts again. */
1332
- function writeCachedClaudeOauth(service, creds) {
1333
- if (!creds.accessToken)
1334
- return;
1335
- try {
1336
- const now = Date.now();
1337
- const cap = now + CLAUDE_OAUTH_CACHE_MAX_TTL_MS;
1338
- const tokenExp = typeof creds.expiresAt === 'number' && creds.expiresAt > now ? creds.expiresAt : cap;
1339
- const entry = {
1340
- accessToken: creds.accessToken,
1341
- expiresAt: creds.expiresAt ?? null,
1342
- scopes: creds.scopes ?? null,
1343
- subscriptionType: creds.subscriptionType ?? null,
1344
- rateLimitTier: creds.rateLimitTier ?? null,
1345
- organizationUuid: creds.organizationUuid ?? null,
1346
- cacheExpiresAt: Math.min(tokenExp, cap),
1347
- };
1348
- setKeychainToken(claudeOauthCacheItem(service), JSON.stringify(entry), { noAcl: true });
1349
- }
1350
- catch {
1351
- /* best-effort — cache is an optimization */
1352
- }
1353
- }
1354
- /** Evict the no-ACL cache so a source rotation or sign-out is reflected immediately. */
1282
+ /**
1283
+ * Evict any no-ACL access-token cache item so a source rotation or sign-out is
1284
+ * reflected immediately. The cache itself is retired read-only probes no longer
1285
+ * read or write it (loadClaudeOauth returns a file-based setup-token or nothing)
1286
+ * but this eviction remains so a credential rotation still clears a stale cache
1287
+ * item that an earlier agents-cli version may have written no-ACL.
1288
+ */
1355
1289
  function deleteCachedClaudeOauth(service) {
1356
1290
  try {
1357
1291
  deleteKeychainToken(claudeOauthCacheItem(service));
@@ -1377,26 +1311,27 @@ function deleteCachedClaudeOauth(service) {
1377
1311
  * (run remotely over SSH by `--host`) rendered no usage bars even though the
1378
1312
  * account + plan — read from the plaintext `.claude.json` — showed fine.
1379
1313
  *
1380
- * `opts.accessTokenCache` opts INTO the no-ACL access-token cache (the Touch ID
1381
- * storm fix, see the cache helpers above). It is OFF by default so every caller
1382
- * keeps the full ACL-read credential the cached copy deliberately omits the
1383
- * refresh token, so callers that refresh (`isClaudeAuthValid` ->
1384
- * `getClaudeAccessToken`) or export the full blob (`readClaudeCredentialsBlob`
1385
- * for Rush Cloud dispatch) must NOT pass it. Only the read-only, high-frequency
1386
- * access-token-only consumers (the usage fetch and the auth-health probe) opt in.
1314
+ * `opts.accessTokenCache` marks a read-only, access-token-only consumer (the
1315
+ * usage fetch and the auth-health probe). Such a caller authenticates ONLY with
1316
+ * a file-based setup-token and, when none is provisioned, gets `null` — it never
1317
+ * reads Claude Code's interactive login (transmitting that ACL-bound OAuth token
1318
+ * to Anthropic's API is what gets it revoked; see the branch body and
1319
+ * docs/design/credential-management.md). It is OFF by default so full-credential
1320
+ * callers that refresh (`isClaudeAuthValid` -> `getClaudeAccessToken`) or export
1321
+ * the full blob (`readClaudeCredentialsBlob` for Rush Cloud dispatch) still read
1322
+ * the interactive login.
1387
1323
  *
1388
- * `opts.fileOnly` (implies access-token-only consumers) skips the ACL keychain
1389
- * read entirely — setup-token, no-ACL cache, and `.credentials.json` only. Used
1390
- * by the daemon usage refresher so a background tick can never pop Touch ID.
1324
+ * `opts.fileOnly` skips the ACL keychain read entirely — setup-token and
1325
+ * `.credentials.json` only. Used by the daemon usage refresher so a background
1326
+ * tick can never pop Touch ID.
1391
1327
  */
1392
1328
  export async function loadClaudeOauth(home, opts) {
1393
- // Read-only usage/probe callers (accessTokenCache) authenticate with a
1394
- // file-based setup-token when one is provisioned: the usage endpoint accepts
1395
- // any sk-ant-oat01 bearer, and the token is read from the file-based `auth`
1396
- // bundle never Claude Code's ACL-bound keychain item so it never pops
1397
- // Touch ID. Transitional: an account with no provisioned setup-token still
1398
- // falls through to the keychain/file read below (that fallback is removed once
1399
- // the fleet is fully seeded, per docs/design/credential-management.md).
1329
+ // Read-only usage/probe callers (accessTokenCache) authenticate ONLY with a
1330
+ // file-based setup-token from the `auth` bundle never Claude Code's
1331
+ // interactive login. The usage endpoint accepts any sk-ant-oat01 bearer, and
1332
+ // the file-based token never pops Touch ID. When no setup-token is provisioned
1333
+ // the probe reports unprovisioned rather than reading the interactive
1334
+ // credential (see below) that is the whole point of this branch.
1400
1335
  if (opts?.accessTokenCache === true) {
1401
1336
  const setupToken = resolveClaudeSetupToken(home);
1402
1337
  if (setupToken) {
@@ -1406,52 +1341,36 @@ export async function loadClaudeOauth(home, opts) {
1406
1341
  // the source of truth if it has actually been revoked.
1407
1342
  return { accessToken: setupToken };
1408
1343
  }
1344
+ // No provisioned setup-token: a read-only usage/health probe MUST NOT fall
1345
+ // through to Claude Code's interactive login credential. The daemon's usage
1346
+ // (~60s) and auth-health (~3min) warms would otherwise read the ACL-bound
1347
+ // OAuth token and transmit it to api.anthropic.com/api/oauth/usage — an
1348
+ // interactive credential used programmatically, which Anthropic flags and
1349
+ // revokes (the fleet-wide-logout class, RUSH-1822), and which violates the
1350
+ // invariant that the interactive/rotating login is untouchable
1351
+ // (docs/design/credential-management.md). Report unprovisioned (-> probe
1352
+ // token 'missing' -> auth-health 'unconfigured', benign for rotation); seed a
1353
+ // setup-token via the mint-auth path to restore usage/probe for the account.
1354
+ return null;
1409
1355
  }
1410
- // The OS keychain/keyring step is macOS/Linux-only. Windows skips straight
1411
- // to the .credentials.json fallback the Claude CLI has no keychain
1412
- // integration there and stores its OAuth token in that file too. An injected
1413
- // test backend is the exception: it makes the keychain path exercisable
1414
- // anywhere, so the platform check must yield to it exactly as the inner
1415
- // claudeOauthCacheActive() gate does otherwise this whole block is dead on
1416
- // a Windows runner and the tests below it read an empty home.
1417
- //
1418
- // fileOnly: daemon refresh must never open the ACL-bound item (Touch ID).
1419
- // It may still read the no-ACL cache item (set-no-acl — prompt-free).
1356
+ // Full-credential callers (isClaudeAuthValid -> getClaudeAccessToken; Rush
1357
+ // Cloud dispatch) legitimately read the interactive login to run/refresh
1358
+ // Claude. The OS keychain/keyring step is macOS/Linux-only; Windows and any
1359
+ // fileOnly caller skip to the .credentials.json read below (the Claude CLI
1360
+ // stores its OAuth token in that file too). An injected test backend makes the
1361
+ // keychain path exercisable anywhere, so the platform check yields to it.
1420
1362
  if (!opts?.fileOnly
1421
1363
  && (process.platform === 'darwin' || process.platform === 'linux' || isKeychainBackendOverridden())) {
1422
1364
  const service = getClaudeKeychainService(home);
1423
- // Serve the no-ACL cache first (opt-in, macOS/test only) so the ACL-gated read
1424
- // below — the one that pops Touch ID — happens at most once per token lifetime
1425
- // instead of on every usage refresh. Off unless the caller opts in, because the
1426
- // cached copy drops the refresh token (see the doc comment above).
1427
- const cacheActive = opts?.accessTokenCache === true && claudeOauthCacheActive();
1428
- if (cacheActive) {
1429
- const cached = readCachedClaudeOauth(service);
1430
- if (cached)
1431
- return cached;
1432
- }
1433
1365
  try {
1434
1366
  const fromKeychain = parseClaudeOauthPayload(getKeychainToken(service));
1435
- if (fromKeychain) {
1436
- if (cacheActive)
1437
- writeCachedClaudeOauth(service, fromKeychain);
1367
+ if (fromKeychain)
1438
1368
  return fromKeychain;
1439
- }
1440
1369
  }
1441
1370
  catch {
1442
1371
  // No keychain item, or no reachable keyring (headless Linux) — fall through.
1443
- // Evict any stale no-ACL cache so a sign-out/deletion isn't masked.
1444
- if (cacheActive)
1445
- deleteCachedClaudeOauth(service);
1446
1372
  }
1447
1373
  }
1448
- else if (opts?.fileOnly === true && opts?.accessTokenCache === true && claudeOauthCacheActive()) {
1449
- // fileOnly + accessTokenCache: still allow the no-ACL cache (never Touch ID).
1450
- const service = getClaudeKeychainService(home);
1451
- const cached = readCachedClaudeOauth(service);
1452
- if (cached)
1453
- return cached;
1454
- }
1455
1374
  const credsPath = path.join(home ?? os.homedir(), '.claude', '.credentials.json');
1456
1375
  try {
1457
1376
  if (fs.existsSync(credsPath)) {
@@ -1472,8 +1391,8 @@ export async function saveClaudeOauth(home, credentials) {
1472
1391
  // Windows not yet supported. An injected test backend is the exception, for
1473
1392
  // the same reason as the loadClaudeOauth guard above: it makes the keychain
1474
1393
  // path exercisable anywhere, and without it this returns before the rotated
1475
- // credential is written OR the no-ACL cache is evicted so a cached reader
1476
- // keeps serving the pre-rotation access token.
1394
+ // credential is written OR a stale no-ACL cache item an earlier version wrote
1395
+ // is evicted (deleteCachedClaudeOauth) leaving that stale item behind.
1477
1396
  if (process.platform !== 'darwin' && process.platform !== 'linux' && !isKeychainBackendOverridden()) {
1478
1397
  return false;
1479
1398
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phnx-labs/agents-cli",
3
- "version": "1.22.29",
3
+ "version": "1.22.30",
4
4
  "description": "One CLI for all your AI coding agents - versions, config, cloud dispatch, sessions, and teams (now with first-class Grok Build CLI support)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -52,6 +52,8 @@
52
52
  "postinstall": "node scripts/postinstall.js",
53
53
  "dev": "tsx src/index.ts",
54
54
  "changelog": "bun scripts/gen-changelog.ts",
55
+ "gen:index": "bun scripts/gen-command-index.ts",
56
+ "verify:index": "bash scripts/verify-command-index.sh",
55
57
  "start": "node dist/index.js",
56
58
  "test": "node ./node_modules/vitest/vitest.mjs run",
57
59
  "bench": "node ./node_modules/vitest/vitest.mjs bench --run",
@@ -97,6 +99,7 @@
97
99
  "commander": "15.0.0",
98
100
  "croner": "10.0.1",
99
101
  "diff": "9.0.0",
102
+ "lru-cache": "11.5.2",
100
103
  "marked": "15.0.12",
101
104
  "marked-terminal": "7.3.0",
102
105
  "ora": "9.4.1",