@phnx-labs/agents-cli 1.22.29 → 1.22.31

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 (82) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/README.md +44 -5
  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/focus.d.ts +4 -1
  10. package/dist/commands/focus.js +19 -4
  11. package/dist/commands/routines.js +29 -11
  12. package/dist/commands/secrets.d.ts +37 -0
  13. package/dist/commands/secrets.js +86 -105
  14. package/dist/commands/sessions-bookmark.d.ts +20 -0
  15. package/dist/commands/{sessions-favorite.js → sessions-bookmark.js} +42 -42
  16. package/dist/commands/sessions-browser.d.ts +10 -8
  17. package/dist/commands/sessions-browser.js +61 -32
  18. package/dist/commands/sessions-picker.d.ts +33 -1
  19. package/dist/commands/sessions-picker.js +102 -27
  20. package/dist/commands/sessions-stats.js +1 -1
  21. package/dist/commands/sessions.d.ts +21 -8
  22. package/dist/commands/sessions.js +328 -74
  23. package/dist/commands/view.d.ts +11 -0
  24. package/dist/commands/view.js +56 -29
  25. package/dist/index.js +37 -2
  26. package/dist/lib/account-labels.d.ts +24 -0
  27. package/dist/lib/account-labels.js +72 -0
  28. package/dist/lib/agents.d.ts +32 -1
  29. package/dist/lib/agents.js +96 -31
  30. package/dist/lib/daemon-health.d.ts +24 -0
  31. package/dist/lib/daemon-health.js +84 -0
  32. package/dist/lib/daemon-ticks.d.ts +81 -0
  33. package/dist/lib/daemon-ticks.js +190 -0
  34. package/dist/lib/daemon.d.ts +68 -18
  35. package/dist/lib/daemon.js +303 -338
  36. package/dist/lib/device-config.d.ts +10 -0
  37. package/dist/lib/device-config.js +27 -0
  38. package/dist/lib/exec.d.ts +27 -0
  39. package/dist/lib/exec.js +49 -2
  40. package/dist/lib/hosts/dispatch.d.ts +4 -0
  41. package/dist/lib/hosts/dispatch.js +4 -0
  42. package/dist/lib/hosts/remote-cmd.js +1 -0
  43. package/dist/lib/hosts/run-target.d.ts +1 -0
  44. package/dist/lib/hosts/run-target.js +1 -0
  45. package/dist/lib/import.js +7 -6
  46. package/dist/lib/memory-cache.d.ts +19 -0
  47. package/dist/lib/memory-cache.js +31 -0
  48. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  49. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  50. package/dist/lib/migrate.d.ts +1 -1
  51. package/dist/lib/migrate.js +13 -2
  52. package/dist/lib/picker.d.ts +6 -3
  53. package/dist/lib/picker.js +7 -2
  54. package/dist/lib/routine-activation.d.ts +2 -0
  55. package/dist/lib/routine-activation.js +16 -0
  56. package/dist/lib/runner.d.ts +18 -0
  57. package/dist/lib/runner.js +52 -0
  58. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  59. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  60. package/dist/lib/secrets/agent.d.ts +19 -1
  61. package/dist/lib/secrets/agent.js +32 -6
  62. package/dist/lib/secrets/scope.d.ts +3 -3
  63. package/dist/lib/secrets/scope.js +3 -3
  64. package/dist/lib/secrets/session-store.d.ts +0 -4
  65. package/dist/lib/secrets/session-store.js +0 -5
  66. package/dist/lib/session/{favorites.d.ts → bookmarks.d.ts} +15 -15
  67. package/dist/lib/session/{favorites.js → bookmarks.js} +23 -23
  68. package/dist/lib/session/db.d.ts +15 -0
  69. package/dist/lib/session/db.js +90 -15
  70. package/dist/lib/session/discover.js +91 -39
  71. package/dist/lib/session/parse.d.ts +63 -0
  72. package/dist/lib/session/parse.js +165 -20
  73. package/dist/lib/session/session-cache.d.ts +9 -6
  74. package/dist/lib/session/session-cache.js +23 -6
  75. package/dist/lib/shims.js +12 -0
  76. package/dist/lib/startup/command-registry.d.ts +15 -1
  77. package/dist/lib/startup/command-registry.js +49 -0
  78. package/dist/lib/usage-refresh.js +3 -2
  79. package/dist/lib/usage.d.ts +12 -10
  80. package/dist/lib/usage.js +63 -144
  81. package/package.json +4 -1
  82. package/dist/commands/sessions-favorite.d.ts +0 -20
@@ -18,7 +18,7 @@
18
18
  * versions (which creates `prune <specs...>`) and prune.js (which attaches the
19
19
  * `cleanup` subcommand to it), in that order — see commands/prune.ts.
20
20
  */
21
- import type { Command } from 'commander';
21
+ import { Command } from 'commander';
22
22
  /** A function that registers one or more commands onto the root program. */
23
23
  export type Registrar = (program: Command) => void;
24
24
  /** A thunk that dynamically imports a command module and returns its registrar. */
@@ -110,6 +110,8 @@ export declare const loadAudit: ModuleLoader;
110
110
  export declare const loadWebhook: ModuleLoader;
111
111
  export declare const loadFunnel: ModuleLoader;
112
112
  export declare const loadHumans: ModuleLoader;
113
+ export declare const loadAccounts: ModuleLoader;
114
+ export declare const loadDaemon: ModuleLoader;
113
115
  /**
114
116
  * Commands whose modules pull in the SQLite-backed session/cloud stack. They are
115
117
  * registered AFTER `applyGlobalHelpConventions` (mirroring main's order: help
@@ -148,3 +150,15 @@ export declare const COMMAND_LOADERS: Record<string, ModuleLoader[]>;
148
150
  export declare const KNOWN_TOP_LEVEL_COMMANDS: ReadonlySet<string>;
149
151
  /** Whether `name` is a top-level command this CLI registers. See {@link KNOWN_TOP_LEVEL_COMMANDS}. */
150
152
  export declare function isKnownTopLevelCommand(name: string): boolean;
153
+ /**
154
+ * Register every module in {@link COMMAND_LOADERS} onto one fresh program and
155
+ * return it — the full public command tree, deduped by loader identity so a
156
+ * loader mapped to several names (e.g. `add`/`use`/`list` -> versions) runs once.
157
+ *
158
+ * Off the hot path only: the command-index generator (`scripts/gen-command-index.ts`)
159
+ * and the tests build the tree from this. Startup never calls it — src/index.ts
160
+ * registers just the one requested command via `registerEagerForRequest`. The
161
+ * inline aliases/tombstones ({@link INLINE_COMMAND_NAMES}) are NOT included: they
162
+ * are closures over entry-point state that src/index.ts registers directly.
163
+ */
164
+ export declare function buildFullCommandTree(): Promise<Command>;
@@ -1,3 +1,24 @@
1
+ /**
2
+ * Lazy command registry.
3
+ *
4
+ * The CLI entry point (src/index.ts) used to statically import every command
5
+ * module and call its `registerXCommand(program)` on every invocation. That
6
+ * loaded the entire command tree (~50 modules) before the first line of output,
7
+ * dominating cold-start latency.
8
+ *
9
+ * This module maps each user-typed top-level command name to a thunk that
10
+ * dynamically imports ONLY the module(s) that command needs. Fast commands
11
+ * (`--version`, `view`, ...) now pay for just the one module they use; the full
12
+ * tree is loaded only on the rare slow paths (unknown-command spellcheck, bare
13
+ * help) via `registerAllEagerCommands` in src/index.ts.
14
+ *
15
+ * Parity is non-negotiable: the name -> loader map below mirrors exactly which
16
+ * module registers which top-level command on `main`. Multi-command modules
17
+ * (versions, packages) map several names to the same loader; `prune` needs BOTH
18
+ * versions (which creates `prune <specs...>`) and prune.js (which attaches the
19
+ * `cleanup` subcommand to it), in that order — see commands/prune.ts.
20
+ */
21
+ import { Command } from 'commander';
1
22
  // One loader per command module. Each dynamically imports the module and hands
2
23
  // back its register function. Kept as named consts so src/index.ts can compose
3
24
  // them into the exact main-branch registration order for the slow path.
@@ -89,6 +110,8 @@ export const loadAudit = async () => (await import('../../commands/audit.js')).r
89
110
  export const loadWebhook = async () => (await import('../../commands/webhook.js')).registerWebhookCommand;
90
111
  export const loadFunnel = async () => (await import('../../commands/funnel.js')).registerFunnelCommand;
91
112
  export const loadHumans = async () => (await import('../../commands/humans.js')).registerHumansCommands;
113
+ export const loadAccounts = async () => (await import('../../commands/accounts.js')).registerAccountsCommand;
114
+ export const loadDaemon = async () => (await import('../../commands/daemon.js')).registerDaemonCommand;
92
115
  /**
93
116
  * Commands whose modules pull in the SQLite-backed session/cloud stack. They are
94
117
  * registered AFTER `applyGlobalHelpConventions` (mirroring main's order: help
@@ -123,6 +146,7 @@ export const LAZY_COMMAND_NAMES = new Set([
123
146
  * are handled directly in src/index.ts.
124
147
  */
125
148
  export const COMMAND_LOADERS = {
149
+ accounts: [loadAccounts],
126
150
  view: [loadView],
127
151
  inspect: [loadInspect],
128
152
  feedback: [loadFeedback],
@@ -241,6 +265,7 @@ export const COMMAND_LOADERS = {
241
265
  webhook: [loadWebhook],
242
266
  funnel: [loadFunnel],
243
267
  humans: [loadHumans],
268
+ daemon: [loadDaemon],
244
269
  };
245
270
  /**
246
271
  * Top-level names that {@link COMMAND_LOADERS} does not carry because they are
@@ -279,3 +304,27 @@ export const KNOWN_TOP_LEVEL_COMMANDS = new Set([
279
304
  export function isKnownTopLevelCommand(name) {
280
305
  return KNOWN_TOP_LEVEL_COMMANDS.has(name);
281
306
  }
307
+ /**
308
+ * Register every module in {@link COMMAND_LOADERS} onto one fresh program and
309
+ * return it — the full public command tree, deduped by loader identity so a
310
+ * loader mapped to several names (e.g. `add`/`use`/`list` -> versions) runs once.
311
+ *
312
+ * Off the hot path only: the command-index generator (`scripts/gen-command-index.ts`)
313
+ * and the tests build the tree from this. Startup never calls it — src/index.ts
314
+ * registers just the one requested command via `registerEagerForRequest`. The
315
+ * inline aliases/tombstones ({@link INLINE_COMMAND_NAMES}) are NOT included: they
316
+ * are closures over entry-point state that src/index.ts registers directly.
317
+ */
318
+ export async function buildFullCommandTree() {
319
+ const program = new Command();
320
+ const done = new Set();
321
+ for (const loaders of Object.values(COMMAND_LOADERS)) {
322
+ for (const loader of loaders) {
323
+ if (done.has(loader))
324
+ continue;
325
+ done.add(loader);
326
+ (await loader())(program);
327
+ }
328
+ }
329
+ return program;
330
+ }
@@ -198,8 +198,9 @@ export async function buildLocalUsageAccounts() {
198
198
  usageKey,
199
199
  agentId,
200
200
  // fileOnly: never open the ACL-bound keychain item from the daemon —
201
- // that path is the Touch ID storm. Usage reads setup-token /
202
- // no-ACL cache / .credentials.json only (see loadClaudeOauth).
201
+ // that path is the Touch ID storm. Usage reads the file-based setup-token
202
+ // only, never the interactive login (see loadClaudeOauth); no setup-token
203
+ // reads as "usage pending".
203
204
  fetch: () => getUsageInfo(agentId, {
204
205
  home: fetchInput.home,
205
206
  cliVersion: fetchInput.cliVersion,
@@ -466,17 +466,19 @@ export declare function normalizeDroidWindows(data: DroidBillingLimitsResponse):
466
466
  * (run remotely over SSH by `--host`) rendered no usage bars even though the
467
467
  * account + plan — read from the plaintext `.claude.json` — showed fine.
468
468
  *
469
- * `opts.accessTokenCache` opts INTO the no-ACL access-token cache (the Touch ID
470
- * storm fix, see the cache helpers above). It is OFF by default so every caller
471
- * keeps the full ACL-read credential the cached copy deliberately omits the
472
- * refresh token, so callers that refresh (`isClaudeAuthValid` ->
473
- * `getClaudeAccessToken`) or export the full blob (`readClaudeCredentialsBlob`
474
- * for Rush Cloud dispatch) must NOT pass it. Only the read-only, high-frequency
475
- * access-token-only consumers (the usage fetch and the auth-health probe) opt in.
469
+ * `opts.accessTokenCache` marks a read-only, access-token-only consumer (the
470
+ * usage fetch and the auth-health probe). Such a caller authenticates ONLY with
471
+ * a file-based setup-token and, when none is provisioned, gets `null` — it never
472
+ * reads Claude Code's interactive login (transmitting that ACL-bound OAuth token
473
+ * to Anthropic's API is what gets it revoked; see the branch body and
474
+ * docs/design/credential-management.md). It is OFF by default so full-credential
475
+ * callers that refresh (`isClaudeAuthValid` -> `getClaudeAccessToken`) or export
476
+ * the full blob (`readClaudeCredentialsBlob` for Rush Cloud dispatch) still read
477
+ * the interactive login.
476
478
  *
477
- * `opts.fileOnly` (implies access-token-only consumers) skips the ACL keychain
478
- * read entirely — setup-token, no-ACL cache, and `.credentials.json` only. Used
479
- * by the daemon usage refresher so a background tick can never pop Touch ID.
479
+ * `opts.fileOnly` skips the ACL keychain read entirely — setup-token and
480
+ * `.credentials.json` only. Used by the daemon usage refresher so a background
481
+ * tick can never pop Touch ID.
480
482
  */
481
483
  export declare function loadClaudeOauth(home?: string, opts?: {
482
484
  accessTokenCache?: boolean;
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.31",
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",
@@ -1,20 +0,0 @@
1
- /**
2
- * `agents sessions favorite` — the non-TTY half of the star.
3
- *
4
- * The `*` hotkey in the interactive browser is how a human stars a session; this
5
- * is how a script, an agent, or a machine without a TTY does the same thing, and
6
- * it is what makes the feature testable end to end without driving a terminal UI.
7
- * Both write the one store in `lib/session/favorites.ts`.
8
- */
9
- import type { Command } from 'commander';
10
- /**
11
- * Resolve one user-typed id (usually the 8-char short id the listing prints) to
12
- * a full session id. Ambiguity is an ERROR, not a silent first-match: starring
13
- * the wrong session is invisible until the user wonders where their star went.
14
- */
15
- export declare function resolveFavoriteTarget(idQuery: string): {
16
- id: string;
17
- } | {
18
- error: string;
19
- };
20
- export declare function registerSessionsFavoriteCommand(sessionsCmd: Command): void;