@phnx-labs/agents-cli 1.20.15 → 1.20.17

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 (49) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/commands/secrets.js +53 -1
  3. package/dist/commands/sessions-sync.d.ts +13 -0
  4. package/dist/commands/sessions-sync.js +73 -0
  5. package/dist/commands/sessions.js +2 -0
  6. package/dist/commands/sync.d.ts +10 -3
  7. package/dist/commands/sync.js +72 -9
  8. package/dist/commands/view.js +11 -3
  9. package/dist/index.js +1 -1
  10. package/dist/lib/agents.d.ts +11 -0
  11. package/dist/lib/agents.js +11 -9
  12. package/dist/lib/daemon.d.ts +19 -0
  13. package/dist/lib/daemon.js +97 -2
  14. package/dist/lib/hooks.js +12 -0
  15. package/dist/lib/migrate.d.ts +22 -0
  16. package/dist/lib/migrate.js +99 -1
  17. package/dist/lib/plugin-marketplace.d.ts +15 -0
  18. package/dist/lib/plugin-marketplace.js +54 -0
  19. package/dist/lib/secrets/drivers/rush.d.ts +14 -0
  20. package/dist/lib/secrets/drivers/rush.js +84 -0
  21. package/dist/lib/secrets/index.js +20 -0
  22. package/dist/lib/secrets/linux.js +88 -10
  23. package/dist/lib/secrets/sync-backend.d.ts +48 -0
  24. package/dist/lib/secrets/sync-backend.js +13 -0
  25. package/dist/lib/secrets/sync.d.ts +15 -23
  26. package/dist/lib/secrets/sync.js +31 -66
  27. package/dist/lib/session/parse.d.ts +2 -0
  28. package/dist/lib/session/parse.js +168 -2
  29. package/dist/lib/session/sync/agents.d.ts +46 -0
  30. package/dist/lib/session/sync/agents.js +94 -0
  31. package/dist/lib/session/sync/config.d.ts +30 -0
  32. package/dist/lib/session/sync/config.js +58 -0
  33. package/dist/lib/session/sync/crdt.d.ts +44 -0
  34. package/dist/lib/session/sync/crdt.js +119 -0
  35. package/dist/lib/session/sync/manifest.d.ts +51 -0
  36. package/dist/lib/session/sync/manifest.js +96 -0
  37. package/dist/lib/session/sync/r2.d.ts +32 -0
  38. package/dist/lib/session/sync/r2.js +121 -0
  39. package/dist/lib/session/sync/sync.d.ts +82 -0
  40. package/dist/lib/session/sync/sync.js +251 -0
  41. package/dist/lib/shims.d.ts +1 -1
  42. package/dist/lib/shims.js +17 -1
  43. package/dist/lib/sync-umbrella.d.ts +76 -0
  44. package/dist/lib/sync-umbrella.js +125 -0
  45. package/dist/lib/teams/parsers.js +159 -1
  46. package/dist/lib/usage.d.ts +18 -0
  47. package/dist/lib/usage.js +25 -0
  48. package/dist/lib/versions.js +30 -13
  49. package/package.json +2 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,15 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ **Headless Linux: `agents secrets` works out of the box when the keyring is locked**
6
+
7
+ - On a headless server the libsecret/GNOME-keyring collection is locked, so the encrypted-file fallback is the only option — but it previously hard-failed unless `AGENTS_SECRETS_PASSPHRASE` was set, leaving `agents secrets` silently unusable. Now, on a headless run with no passphrase set, a random machine-local passphrase is auto-provisioned once at `~/.agents/.cache/secrets/.passphrase` (mode 0600) so the encrypted-file store just works. `AGENTS_SECRETS_PASSPHRASE` still takes precedence (off-disk key), an existing `.passphrase` is reused for stable interactive/headless behavior, and interactive TTY sessions are still prompted. Security model + resolution order documented in `docs/secrets.md`. (#371)
8
+
9
+ **`agents secrets get/set <item>`: raw, cross-platform keychain access for hooks**
10
+
11
+ - New `agents secrets get <item>` / `agents secrets set <item>` read and write a single keychain item **by bare name** (outside the bundle namespace), so shell hooks and automation have one platform-agnostic credential primitive to call instead of hardcoding `/usr/bin/security` (macOS-only) or `secret-tool` (Linux-only). `get` prints the value to stdout (newline-terminated for clean `$(…)` capture), sends diagnostics to stderr, and exits 1 with empty stdout when the item is missing — exactly what a `SessionStart` hook needs to probe-and-fallback quietly. Routing goes through the existing cross-platform keychain layer: macOS via `/usr/bin/security`, Linux via `secret-tool` with the encrypted-file fallback.
12
+ - `setKeychainToken` now writes bare (non-`agents-cli.`) items on macOS **without** the biometry ACL, mirroring the existing no-prompt read path for such items. This is what lets a hook read e.g. `linear-api-key` silently on every launch — routing it through the Touch ID helper would attach an ACL the `/usr/bin/security` read can't satisfy without popping the legacy password sheet. The change is purely additive: every existing caller passes an `agents-cli.`-namespaced item and is unaffected (still biometry-gated via the signed helper).
13
+
5
14
  **`agents inspect` summary: expanded detail for hooks, plugins, and MCP**
6
15
 
7
16
  - The bare `agents inspect <agent>` / `agents inspect <repo>` summary no longer collapses everything to a count table. Simple kinds (commands, skills, rules, subagents, workflows) keep a count line but now preview a few names; the rich kinds get their own expanded sections: **hooks** show their events + `matches:` predicates + cache (`PreToolUse(Bash) · git_dirty · prompt~"deploy" (5m cache)`), **plugins** show version + bundle contents (`v2.1.0 skills:6 commands:5 hooks:2 mcp:1`), and **MCP** show transport + url/command. Drill-down flags (`--hooks`, `--plugins`, `--mcp`) and `--brief` are unchanged; `--json` gains the structured detail additively (existing keys retained).
@@ -8,7 +8,7 @@
8
8
  import chalk from 'chalk';
9
9
  import * as fs from 'fs';
10
10
  import { bundleExists, deleteBundle, describeBundle, keychainItemsForBundle, keychainRef, listBundles, migrateLegacyBundles, parseDotenv, readBundle, renameBundle, rotateBundleSecret, validateBundleName, validateEnvKey, validateExpiresFutureDated, validateSecretType, writeBundle, } from '../lib/secrets/bundles.js';
11
- import { deleteKeychainToken, getKeychainTokens, hasKeychainToken, secretsKeychainItem, setKeychainToken, } from '../lib/secrets/index.js';
11
+ import { deleteKeychainToken, getKeychainToken, getKeychainTokens, hasKeychainToken, secretsKeychainItem, setKeychainToken, } from '../lib/secrets/index.js';
12
12
  import { assertOpAvailable, createPasswordItem, deleteItemByTitle, extractSecrets, itemExistsByTitle, listItems, listVaults, } from '../lib/onepassword.js';
13
13
  import { registerCommandGroups, setHelpSections } from '../lib/help.js';
14
14
  import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
@@ -365,6 +365,7 @@ export function registerSecretsCommands(program) {
365
365
  registerCommandGroups(cmd, [
366
366
  { title: 'Bundle commands', names: ['list', 'view', 'create', 'rename', 'describe', 'delete'] },
367
367
  { title: 'Secret commands', names: ['add', 'rotate', 'remove', 'import', 'export'] },
368
+ { title: 'Raw item commands', names: ['get', 'set'] },
368
369
  { title: 'Sync commands', names: ['push', 'pull', 'remote-list'] },
369
370
  { title: 'Utilities', names: ['exec', 'generate', 'migrate-acl'] },
370
371
  ]);
@@ -465,6 +466,57 @@ export function registerSecretsCommands(program) {
465
466
  process.exit(1);
466
467
  }
467
468
  });
469
+ cmd
470
+ .command('get <item>')
471
+ .description('Print a raw keychain item by name (for shell hooks/automation). Cross-platform; no bundle required.')
472
+ .action((item) => {
473
+ try {
474
+ // Routes through the platform keychain layer: macOS reads bare items
475
+ // via /usr/bin/security (no Touch ID), Linux via secret-tool with the
476
+ // encrypted-file fallback. The value goes to stdout (newline-terminated
477
+ // so `$(agents secrets get NAME)` captures it cleanly); diagnostics go
478
+ // to stderr so they never pollute the captured value.
479
+ const value = getKeychainToken(item);
480
+ process.stdout.write(value.endsWith('\n') ? value : `${value}\n`);
481
+ }
482
+ catch {
483
+ // Missing item is a normal, quiet outcome for a hook probe: exit 1,
484
+ // print nothing to stdout. Callers test the exit code / empty capture.
485
+ process.exit(1);
486
+ }
487
+ });
488
+ cmd
489
+ .command('set <item>')
490
+ .description('Store a raw keychain item by name (for shell hooks/automation). Cross-platform; no bundle required.')
491
+ .option('--value <v>', 'Value to store (omit to read from stdin or be prompted)')
492
+ .option('--value-stdin', 'Read the value from stdin')
493
+ .action(async (item, opts) => {
494
+ try {
495
+ let value;
496
+ if (opts.value !== undefined) {
497
+ value = opts.value;
498
+ }
499
+ else if (opts.valueStdin) {
500
+ value = readStdinSync();
501
+ if (!value)
502
+ throw new Error('No value received on stdin.');
503
+ }
504
+ else {
505
+ value = await promptForSecret(`Enter value for ${item}`);
506
+ }
507
+ // setKeychainToken stores bare items WITHOUT the biometry ACL on macOS
508
+ // so `agents secrets get` can read them back without a password sheet;
509
+ // on Linux it goes through secret-tool / encrypted-file fallback.
510
+ setKeychainToken(item, value);
511
+ console.error(chalk.green(`Stored keychain item '${item}'.`));
512
+ }
513
+ catch (err) {
514
+ if (isPromptCancelled(err))
515
+ return;
516
+ console.error(chalk.red(err.message));
517
+ process.exit(1);
518
+ }
519
+ });
468
520
  cmd
469
521
  .command('create [name]')
470
522
  .description('Create an empty bundle')
@@ -0,0 +1,13 @@
1
+ /**
2
+ * `agents sessions sync` — push this machine's transcripts to R2 and pull every
3
+ * other machine's, merging copies of the same session via CRDT union. The local
4
+ * sessions index is rebuilt from the synced-in mirror by the normal scanner.
5
+ */
6
+ import type { Command } from 'commander';
7
+ interface SyncCmdOptions {
8
+ verbose?: boolean;
9
+ json?: boolean;
10
+ }
11
+ export declare function runSessionsSync(options: SyncCmdOptions): Promise<void>;
12
+ export declare function registerSessionsSyncCommand(sessionsCmd: Command): void;
13
+ export {};
@@ -0,0 +1,73 @@
1
+ /**
2
+ * `agents sessions sync` — push this machine's transcripts to R2 and pull every
3
+ * other machine's, merging copies of the same session via CRDT union. The local
4
+ * sessions index is rebuilt from the synced-in mirror by the normal scanner.
5
+ */
6
+ import chalk from 'chalk';
7
+ import { setHelpSections } from '../lib/help.js';
8
+ import { isSyncConfigured, SYNC_BUNDLE } from '../lib/session/sync/config.js';
9
+ import { syncSessions } from '../lib/session/sync/sync.js';
10
+ export async function runSessionsSync(options) {
11
+ if (!isSyncConfigured()) {
12
+ console.error(chalk.red(`Sessions sync is not configured.`) +
13
+ `\nAdd R2 credentials to the '${SYNC_BUNDLE}' bundle:\n` +
14
+ ` agents secrets add ${SYNC_BUNDLE} R2_ACCOUNT_ID\n` +
15
+ ` agents secrets add ${SYNC_BUNDLE} R2_BUCKET_NAME\n` +
16
+ ` agents secrets add ${SYNC_BUNDLE} R2_ACCESS_KEY_ID\n` +
17
+ ` agents secrets add ${SYNC_BUNDLE} R2_SECRET_ACCESS_KEY`);
18
+ process.exitCode = 1;
19
+ return;
20
+ }
21
+ try {
22
+ const result = await syncSessions({
23
+ verbose: options.verbose,
24
+ log: msg => console.error(chalk.dim(msg)),
25
+ });
26
+ if (options.json) {
27
+ console.log(JSON.stringify(result, null, 2));
28
+ }
29
+ else {
30
+ const parts = [
31
+ `pushed ${result.pushed}`,
32
+ `pulled ${result.pulled}`,
33
+ result.merged > 0 ? `merged ${result.merged}` : null,
34
+ ].filter(Boolean);
35
+ console.log(chalk.green('synced') + ` ${result.machine}: ` + parts.join(', ') +
36
+ chalk.dim(` (${result.pushSkipped + result.pullSkipped} unchanged)`));
37
+ }
38
+ if (result.errors.length > 0) {
39
+ for (const e of result.errors)
40
+ console.error(chalk.yellow(` ! ${e}`));
41
+ process.exitCode = 1;
42
+ }
43
+ }
44
+ catch (err) {
45
+ console.error(chalk.red(`sync failed: ${err.message}`));
46
+ process.exitCode = 1;
47
+ }
48
+ }
49
+ export function registerSessionsSyncCommand(sessionsCmd) {
50
+ const syncCmd = sessionsCmd
51
+ .command('sync')
52
+ .description('Sync session transcripts across machines via R2 (CRDT merge). Claude and Codex.')
53
+ .option('-v, --verbose', 'Log each pushed and pulled session')
54
+ .option('--json', 'Output the sync result as JSON');
55
+ setHelpSections(syncCmd, {
56
+ examples: `
57
+ # One sync cycle (push local changes, pull + merge from other machines)
58
+ agents sessions sync
59
+
60
+ # See exactly what moved
61
+ agents sessions sync --verbose
62
+ `,
63
+ notes: `
64
+ - Credentials come from the '${SYNC_BUNDLE}' secrets bundle (R2 S3 API, read+write).
65
+ - Each machine writes only its own prefix; conflicts are impossible by construction.
66
+ - The daemon runs this automatically (~90s); this command forces an immediate cycle.
67
+ - Sessions present locally always win; synced-in copies fill in other machines' sessions.
68
+ `,
69
+ });
70
+ syncCmd.action(async (options) => {
71
+ await runSessionsSync(options);
72
+ });
73
+ }
@@ -29,6 +29,7 @@ import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
29
29
  import { sessionPicker } from './sessions-picker.js';
30
30
  import { setHelpSections } from '../lib/help.js';
31
31
  import { registerSessionsTailCommand } from './sessions-tail.js';
32
+ import { registerSessionsSyncCommand } from './sessions-sync.js';
32
33
  const SESSION_AGENT_FILTER_HELP = `Filter by agent, e.g. claude, codex, claude@2.0.65`;
33
34
  const CLAUDE_RESUME_MATCH_WINDOW_MS = 10 * 60_000;
34
35
  const LOAD_VERBS = ['Loading', 'Scanning', 'Gathering', 'Indexing', 'Reading'];
@@ -1144,6 +1145,7 @@ export function registerSessionsCommands(program) {
1144
1145
  await sessionsAction(query, options);
1145
1146
  });
1146
1147
  registerSessionsTailCommand(sessionsCmd);
1148
+ registerSessionsSyncCommand(sessionsCmd);
1147
1149
  }
1148
1150
  function formatNoSessionsMessage(showAll, project) {
1149
1151
  const projectQuery = project?.trim();
@@ -2,11 +2,18 @@
2
2
  * `agents sync` — synchronize central resources into an installed agent version.
3
3
  *
4
4
  * Forms:
5
- * agents sync claude # uses default/sole installed version
6
- * agents sync claude@2.1.142 # explicit version
7
- * agents sync claude@latest # newest installed
5
+ * agents sync # umbrella: fetch remote (repos+secrets+sessions) -> reconcile all
6
+ * agents sync --repos|--secrets|--sessions # umbrella: fetch only those, then reconcile
7
+ * agents sync --cloud # umbrella: fetch all, skip reconcile
8
+ * agents sync --local # umbrella: reconcile all, no fetch
9
+ * agents sync claude # one agent: uses default/sole installed version
10
+ * agents sync claude@2.1.142 # one agent: explicit version
11
+ * agents sync claude@latest # one agent: newest installed
8
12
  * agents sync --agent claude --agent-version 2.1.142 # legacy form, still supported
9
13
  *
14
+ * The umbrella stages live in lib/sync-umbrella.ts; this file dispatches to them
15
+ * when no agent is given.
16
+ *
10
17
  * In a TTY the command previews available/new resources and lets the user
11
18
  * select what to sync (same prompts shown after `agents add`). Pass
12
19
  * --yes for non-interactive auto-sync, --force to re-sync when nothing
@@ -2,11 +2,18 @@
2
2
  * `agents sync` — synchronize central resources into an installed agent version.
3
3
  *
4
4
  * Forms:
5
- * agents sync claude # uses default/sole installed version
6
- * agents sync claude@2.1.142 # explicit version
7
- * agents sync claude@latest # newest installed
5
+ * agents sync # umbrella: fetch remote (repos+secrets+sessions) -> reconcile all
6
+ * agents sync --repos|--secrets|--sessions # umbrella: fetch only those, then reconcile
7
+ * agents sync --cloud # umbrella: fetch all, skip reconcile
8
+ * agents sync --local # umbrella: reconcile all, no fetch
9
+ * agents sync claude # one agent: uses default/sole installed version
10
+ * agents sync claude@2.1.142 # one agent: explicit version
11
+ * agents sync claude@latest # one agent: newest installed
8
12
  * agents sync --agent claude --agent-version 2.1.142 # legacy form, still supported
9
13
  *
14
+ * The umbrella stages live in lib/sync-umbrella.ts; this file dispatches to them
15
+ * when no agent is given.
16
+ *
10
17
  * In a TTY the command previews available/new resources and lets the user
11
18
  * select what to sync (same prompts shown after `agents add`). Pass
12
19
  * --yes for non-interactive auto-sync, --force to re-sync when nothing
@@ -25,12 +32,13 @@ import { isVersionInstalled, syncResourcesToVersion, parseAgentSpec, resolveVers
25
32
  import { compileRulesForProject } from '../lib/rules/compile.js';
26
33
  import { runLaunchSync } from '../lib/project-launch.js';
27
34
  import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
35
+ import { runUmbrellaSync } from '../lib/sync-umbrella.js';
28
36
  /** Register the `agents sync` command. */
29
37
  export function registerSyncCommand(program) {
30
38
  program
31
39
  .command('sync [agentSpec]')
32
- .summary('Sync resources into an installed agent version')
33
- .description('Sync resources (commands, skills, hooks, rules, MCPs, plugins, etc.) into an installed agent version. Previews what will change and lets you pick.\n\n[agentSpec] is the agent name with an optional @version, e.g. "claude" or "claude@2.1.142". Omit the version to sync into the active (or sole installed) one.')
40
+ .summary('Make this machine current, or sync resources into one agent')
41
+ .description('With an [agentSpec], syncs resources (commands, skills, hooks, rules, MCPs, plugins, etc.) into that installed agent version previews changes and lets you pick. e.g. "claude" or "claude@2.1.142".\n\nWith NO agent, runs the umbrella verb: fetch remote state (config repos + secrets + sessions) then reconcile it into every installed agent. Scope it with --repos / --secrets / --sessions, --cloud (fetch only), or --local (reconcile only).')
34
42
  .option('--agent <agent>', 'Agent identifier (legacy form; prefer the positional spec)')
35
43
  .option('--agent-version <version>', 'Version to sync into (legacy form; prefer "agent@version")')
36
44
  .option('--project-dir <path>', 'Path to project-level .agents/ directory containing project-scoped resources')
@@ -39,10 +47,66 @@ export function registerSyncCommand(program) {
39
47
  .option('-y, --yes', 'Skip the interactive preview and auto-sync all detected resources', false)
40
48
  .option('--force', 'Re-sync even if no changes are detected since the last sync', false)
41
49
  .option('--quiet', 'Suppress all output (exit code indicates success)', false)
50
+ // Umbrella verb (no agent given): make this machine current.
51
+ .option('--repos', 'Umbrella: git-pull ~/.agents + enabled ~/.agents-* extras', false)
52
+ .option('--secrets', 'Umbrella: pull encrypted secret bundles from the remote', false)
53
+ .option('--sessions', 'Umbrella: sync session transcripts across machines', false)
54
+ .option('--cloud', 'Umbrella: fetch all remote state but skip the local reconcile', false)
55
+ .option('--local', "Umbrella: reconcile resources into installed agents only (no fetch)", false)
42
56
  .action(async (agentSpec, opts) => {
43
57
  await runSync(agentSpec, opts);
44
58
  });
45
59
  }
60
+ /**
61
+ * The umbrella verb: bare `agents sync` (no agent) makes this machine current.
62
+ * Resolves the flags + a secrets passphrase (env-only for now; tokenized auth
63
+ * arrives with `agents login`) and runs the fetch+reconcile stages, then prints
64
+ * a one-line summary. Stage failures are non-fatal and surfaced as warnings.
65
+ */
66
+ async function runUmbrella(opts, quiet, outLog, errLog) {
67
+ const flags = {
68
+ repos: opts.repos,
69
+ secrets: opts.secrets,
70
+ sessions: opts.sessions,
71
+ cloud: opts.cloud,
72
+ local: opts.local,
73
+ };
74
+ const passphrase = process.env.AGENTS_SECRETS_PASSPHRASE || undefined;
75
+ if (!quiet)
76
+ outLog(chalk.bold('Syncing this machine…'));
77
+ try {
78
+ const result = await runUmbrellaSync({
79
+ flags,
80
+ yes: !!opts.yes,
81
+ passphrase,
82
+ log: (msg) => { if (!quiet)
83
+ outLog(chalk.gray(` ${msg}`)); },
84
+ });
85
+ if (!quiet) {
86
+ const parts = [];
87
+ if (result.repos) {
88
+ parts.push(`repos ${result.repos.pulled} pulled` +
89
+ (result.repos.errors.length ? `, ${result.repos.errors.length} failed` : ''));
90
+ }
91
+ if (result.secrets) {
92
+ parts.push(result.secrets.skipped ? 'secrets skipped' : `secrets ${result.secrets.pulled} pulled`);
93
+ }
94
+ if (result.sessions) {
95
+ parts.push(result.sessions.ran ? `sessions ${result.sessions.merged} merged` : 'sessions off');
96
+ }
97
+ if (result.reconciled)
98
+ parts.push('reconciled');
99
+ outLog(chalk.green(`✓ sync: ${parts.join(' · ') || 'nothing to do'}`));
100
+ const errs = [...(result.repos?.errors ?? []), ...(result.secrets?.errors ?? [])];
101
+ for (const e of errs)
102
+ errLog(chalk.yellow(` ! ${e}`));
103
+ }
104
+ }
105
+ catch (err) {
106
+ errLog(chalk.red(`sync failed: ${err.message}`));
107
+ process.exitCode = 1;
108
+ }
109
+ }
46
110
  async function runSync(agentSpec, opts) {
47
111
  const quiet = !!opts.quiet;
48
112
  const errLog = (msg) => { if (!quiet)
@@ -77,10 +141,9 @@ async function runSync(agentSpec, opts) {
77
141
  version = opts.agentVersion;
78
142
  }
79
143
  if (!agentId) {
80
- errLog(chalk.red('Usage: agents sync <agent>[@version]'));
81
- errLog(chalk.gray(' agents sync claude'));
82
- errLog(chalk.gray(' agents sync claude@2.1.142'));
83
- process.exitCode = 1;
144
+ // No agent specified → the umbrella verb: make this machine current
145
+ // (fetch repos + secrets + sessions, then reconcile all installed agents).
146
+ await runUmbrella(opts, quiet, outLog, errLog);
84
147
  return;
85
148
  }
86
149
  // ---------- 2. Resolve version (project pin → global default → sole installed) ----------
@@ -3,7 +3,7 @@ import ora from 'ora';
3
3
  import * as fs from 'fs';
4
4
  import * as path from 'path';
5
5
  import { AGENTS, ALL_AGENT_IDS, getAllCliStates, getAccountInfo, resolveAgentName, formatAgentError, agentLabel, colorAgent, } from '../lib/agents.js';
6
- import { formatUsageSection, formatUsageSummary, formatUsageStatusBadge, getUsageInfoForIdentity, getUsageInfoByIdentity, getUsageLookupKey, } from '../lib/usage.js';
6
+ import { deriveUsageStatusFromSnapshot, formatUsageSection, formatUsageSummary, formatUsageStatusBadge, getUsageInfoForIdentity, getUsageInfoByIdentity, getUsageLookupKey, } from '../lib/usage.js';
7
7
  import { readManifest } from '../lib/manifest.js';
8
8
  import { listInstalledVersions, listInstalledVersionDirs, getGlobalDefault, getVersionHomePath, getVersionDir, resolveVersionAlias, getAvailableResources, getActuallySyncedResources, getNewResources, getProjectOnlyResources, hasNewResources, promptNewResourceSelection, syncResourcesToVersion, removeVersion, printTrashFooter, } from '../lib/versions.js';
9
9
  import { ensureVersionedAliasCurrent, removeShim, } from '../lib/shims.js';
@@ -306,7 +306,11 @@ async function showInstalledVersions(filterAgentId) {
306
306
  return {
307
307
  ...info,
308
308
  plan: canon.plan,
309
- usageStatus: canon.usageStatus,
309
+ // Throttle state comes from the live usage windows, not the pay-as-you-go
310
+ // overage flag that AccountInfo.usageStatus used to carry. A maxed window
311
+ // means rate-limited; no snapshot means no badge. See
312
+ // deriveUsageStatusFromSnapshot.
313
+ usageStatus: deriveUsageStatusFromSnapshot(usageByKey.get(key)?.snapshot),
310
314
  overageCredits: canon.overageCredits,
311
315
  };
312
316
  };
@@ -984,7 +988,11 @@ async function collectAgentsJson(filterAgentId) {
984
988
  return {
985
989
  ...info,
986
990
  plan: canon.plan,
987
- usageStatus: canon.usageStatus,
991
+ // Throttle state comes from the live usage windows, not the pay-as-you-go
992
+ // overage flag that AccountInfo.usageStatus used to carry. A maxed window
993
+ // means rate-limited; no snapshot means no badge. See
994
+ // deriveUsageStatusFromSnapshot.
995
+ usageStatus: deriveUsageStatusFromSnapshot(usageByKey.get(key)?.snapshot),
988
996
  overageCredits: canon.overageCredits,
989
997
  };
990
998
  };
package/dist/index.js CHANGED
@@ -893,7 +893,7 @@ if (process.env.AGENTS_SKIP_MIGRATION !== '1') {
893
893
  // Bumping the suffix re-runs migrations for every user; binary releases that
894
894
  // don't change the schema must NOT re-run (they would destroy user content
895
895
  // when migration steps overlap with user-authored paths). See issue #20.
896
- const sentinelValue = 'v9';
896
+ const sentinelValue = 'v10';
897
897
  let needRun = true;
898
898
  try {
899
899
  if (fs.existsSync(sentinel) && fs.readFileSync(sentinel, 'utf-8').trim() === sentinelValue) {
@@ -13,6 +13,17 @@ export interface CliState {
13
13
  export declare const CODEX_HOOKS_MIN_VERSION = "0.116.0";
14
14
  /** Minimum Gemini CLI version that supports the hooks system (v0.26.0, Jan 2026). */
15
15
  export declare const GEMINI_HOOKS_MIN_VERSION = "0.26.0";
16
+ /**
17
+ * Synchronous PATH search -- no subprocess. Returns first matching binary path.
18
+ *
19
+ * Skips our own shims dir (`~/.agents/.cache/shims/`) — those shims are
20
+ * dispatch helpers, not real installs. Counting them as installed produced a
21
+ * false positive where agents with NO real binary on the host (e.g. a
22
+ * never-installed Cursor whose only PATH entry was our `cursor-agent` shim
23
+ * dispatcher) showed up under `agents view`'s "Not Managed by Agents CLI"
24
+ * section, even though the user had nothing to import.
25
+ */
26
+ export declare function findInPath(command: string): string | null;
16
27
  /**
17
28
  * Master registry of all supported agents keyed by AgentId.
18
29
  *
@@ -66,7 +66,7 @@ function saveCliVersionCache() {
66
66
  * dispatcher) showed up under `agents view`'s "Not Managed by Agents CLI"
67
67
  * section, even though the user had nothing to import.
68
68
  */
69
- function findInPath(command) {
69
+ export function findInPath(command) {
70
70
  const pathEnv = process.env.PATH || '';
71
71
  const pathExt = process.platform === 'win32' ? (process.env.PATHEXT || '').split(';') : [''];
72
72
  const shimsDir = getShimsDir();
@@ -814,14 +814,16 @@ export async function getAccountInfo(agentId, home) {
814
814
  else if (oa?.billingType) {
815
815
  plan = oa.billingType;
816
816
  }
817
- let usageStatus = null;
818
- const reason = data.cachedExtraUsageDisabledReason;
819
- if (reason === 'out_of_credits')
820
- usageStatus = 'out_of_credits';
821
- else if (reason)
822
- usageStatus = 'rate_limited';
823
- else
824
- usageStatus = 'available';
817
+ // usageStatus is NOT derived from cachedExtraUsageDisabledReason. That
818
+ // field reports why pay-as-you-go overage is off (out_of_credits = no
819
+ // overage credits purchased; org_level_disabled = admin turned overage
820
+ // off), which says nothing about whether the account is throttled — a
821
+ // Pro account at 5% weekly usage with overage disabled is fully usable.
822
+ // Real throttle state comes from the live usage windows; callers derive
823
+ // it via deriveUsageStatusFromSnapshot(). Here we only report whether
824
+ // the account is signed in at all. Overage state stays visible through
825
+ // overageCredits below.
826
+ const usageStatus = email ? 'available' : null;
825
827
  let overageCredits = null;
826
828
  const orgId = oa?.organizationUuid;
827
829
  const creditCache = orgId && data.overageCreditGrantCache?.[orgId];
@@ -18,6 +18,16 @@ export declare function isDaemonRunning(): boolean;
18
18
  export declare function log(level: string, message: string): void;
19
19
  /** Main daemon loop: load jobs, schedule crons, monitor runs, and handle signals. */
20
20
  export declare function runDaemon(): Promise<void>;
21
+ /**
22
+ * Read the long-lived Claude OAuth token (from `claude setup-token`) that the
23
+ * user stored under the `claude` secrets bundle. Resolves the bundle the same
24
+ * way `agents run --secrets` does, so the token is found whether it was stored
25
+ * keychain-backed or as a literal. Returns null when the bundle/key isn't
26
+ * configured, the Keychain read is cancelled, or the platform has no keychain —
27
+ * the daemon then behaves exactly as before (relying on the interactive OAuth
28
+ * session). Never throws: a misconfigured token must not block daemon startup.
29
+ */
30
+ export declare function readDaemonClaudeOAuthToken(): string | null;
21
31
  /** Generate a macOS launchd plist for auto-starting the daemon. */
22
32
  export declare function generateLaunchdPlist(): string;
23
33
  /** Generate a Linux systemd user unit for auto-starting the daemon. */
@@ -27,6 +37,15 @@ export declare function startDaemon(): {
27
37
  pid: number | null;
28
38
  method: string;
29
39
  };
40
+ /**
41
+ * Environment for the detached daemon fallback. The launchd/systemd paths
42
+ * deliver the long-lived OAuth token via the service manifest's environment;
43
+ * the detached path has no manifest, so inject it here. Read happens during an
44
+ * interactive `routines start`, so a Keychain Touch ID prompt can be satisfied;
45
+ * the daemon then passes it to every routine run it spawns. An already-set
46
+ * value (e.g. inherited from launchd) is left untouched.
47
+ */
48
+ export declare function buildDetachedDaemonEnv(baseEnv?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
30
49
  /** Stop the daemon, unloading it from launchd/systemd if applicable. */
31
50
  export declare function stopDaemon(): boolean;
32
51
  /** Get current daemon status including running state, PID, and enabled job count. */