@phnx-labs/agents-cli 1.22.41 → 1.22.42

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 (167) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +9 -3
  3. package/dist/bin/agents +0 -0
  4. package/dist/cli/command-registry.d.ts +2 -0
  5. package/dist/cli/command-registry.js +4 -0
  6. package/dist/commands/accounts.d.ts +29 -0
  7. package/dist/commands/accounts.js +220 -29
  8. package/dist/commands/alias.js +1 -1
  9. package/dist/commands/auth.d.ts +9 -0
  10. package/dist/commands/auth.js +104 -0
  11. package/dist/commands/browser-sessions-picker.js +42 -62
  12. package/dist/commands/browser.js +30 -38
  13. package/dist/commands/computer-actions.d.ts +1 -1
  14. package/dist/commands/computer-actions.js +1 -1
  15. package/dist/commands/computer-sessions-picker.js +32 -55
  16. package/dist/commands/computer.d.ts +1 -1
  17. package/dist/commands/computer.js +2 -2
  18. package/dist/commands/doctor.d.ts +1 -1
  19. package/dist/commands/doctor.js +3 -3
  20. package/dist/commands/feed.js +1 -1
  21. package/dist/commands/hooks.js +1 -1
  22. package/dist/commands/import.js +1 -1
  23. package/dist/commands/insights.js +163 -85
  24. package/dist/commands/inspect.js +2 -2
  25. package/dist/commands/mine.js +1 -1
  26. package/dist/commands/org.d.ts +6 -0
  27. package/dist/commands/org.js +174 -0
  28. package/dist/commands/packages.js +1 -1
  29. package/dist/commands/prune.js +1 -1
  30. package/dist/commands/routines.js +1 -1
  31. package/dist/commands/run-account-picker.d.ts +19 -0
  32. package/dist/commands/run-account-picker.js +81 -0
  33. package/dist/commands/sessions-picker-factory.d.ts +40 -0
  34. package/dist/commands/sessions-picker-factory.js +67 -0
  35. package/dist/commands/setup-browser.js +4 -4
  36. package/dist/commands/setup-mine.js +1 -1
  37. package/dist/commands/setup-preferences.js +2 -2
  38. package/dist/commands/setup.js +5 -3
  39. package/dist/commands/sync.js +1 -1
  40. package/dist/commands/versions.js +1 -1
  41. package/dist/commands/view.js +1 -1
  42. package/dist/lib/account-capabilities.d.ts +10 -0
  43. package/dist/lib/account-capabilities.js +34 -0
  44. package/dist/lib/agent-spec/agents.d.ts +584 -0
  45. package/dist/lib/agent-spec/agents.js +3127 -0
  46. package/dist/lib/agent-spec/provider.js +1 -1
  47. package/dist/lib/agents.d.ts +2 -584
  48. package/dist/lib/agents.js +2 -3127
  49. package/dist/lib/browser/drivers/local.d.ts +8 -2
  50. package/dist/lib/browser/drivers/local.js +8 -2
  51. package/dist/lib/browser/drivers/ssh.d.ts +8 -2
  52. package/dist/lib/browser/drivers/ssh.js +10 -4
  53. package/dist/lib/browser/hygiene.d.ts +1 -1
  54. package/dist/lib/browser/hygiene.js +5 -1
  55. package/dist/lib/browser/profiles.d.ts +74 -1
  56. package/dist/lib/browser/profiles.js +114 -5
  57. package/dist/lib/browser/runtime-state.d.ts +11 -5
  58. package/dist/lib/browser/runtime-state.js +13 -9
  59. package/dist/lib/browser/service.d.ts +68 -30
  60. package/dist/lib/browser/service.js +305 -194
  61. package/dist/lib/browser/types.d.ts +95 -3
  62. package/dist/lib/browser/types.js +51 -0
  63. package/dist/lib/catchup.js +1 -1
  64. package/dist/lib/computer/actions.d.ts +1 -1
  65. package/dist/lib/computer/actions.js +1 -1
  66. package/dist/lib/{computer-rpc.js → computer/computer-rpc.js} +5 -5
  67. package/dist/lib/computer/dispatch.d.ts +1 -1
  68. package/dist/lib/computer/download.d.ts +1 -1
  69. package/dist/lib/computer/download.js +3 -3
  70. package/dist/lib/{ssh-tunnel.d.ts → computer/ssh-tunnel.d.ts} +1 -1
  71. package/dist/lib/{ssh-tunnel.js → computer/ssh-tunnel.js} +12 -12
  72. package/dist/lib/daemon/daemon.js +1 -1
  73. package/dist/lib/{runner.d.ts → daemon/runner.d.ts} +10 -10
  74. package/dist/lib/{runner.js → daemon/runner.js} +40 -114
  75. package/dist/lib/devices/doctor-findings.d.ts +1 -1
  76. package/dist/lib/devices/doctor-findings.js +3 -3
  77. package/dist/lib/devices/fleet-inventory.js +1 -1
  78. package/dist/lib/devices/harness-inventory.js +1 -1
  79. package/dist/lib/devices/registry.js +4 -22
  80. package/dist/lib/doctor-diff.d.ts +1 -1
  81. package/dist/lib/doctor-diff.js +1 -1
  82. package/dist/lib/drift.js +1 -1
  83. package/dist/lib/entitlement.d.ts +31 -0
  84. package/dist/lib/entitlement.js +137 -0
  85. package/dist/lib/exec.js +39 -202
  86. package/dist/lib/feed/feed.js +2 -8
  87. package/dist/lib/fs-atomic.d.ts +7 -0
  88. package/dist/lib/fs-atomic.js +9 -0
  89. package/dist/lib/harness/adapter.d.ts +160 -0
  90. package/dist/lib/harness/adapter.js +47 -0
  91. package/dist/lib/harness/adapters/claude.d.ts +2 -0
  92. package/dist/lib/harness/adapters/claude.js +110 -0
  93. package/dist/lib/harness/adapters/codex.d.ts +2 -0
  94. package/dist/lib/harness/adapters/codex.js +83 -0
  95. package/dist/lib/harness/adapters/copilot.d.ts +2 -0
  96. package/dist/lib/harness/adapters/copilot.js +23 -0
  97. package/dist/lib/harness/adapters/cursor.d.ts +2 -0
  98. package/dist/lib/harness/adapters/cursor.js +38 -0
  99. package/dist/lib/harness/adapters/droid.d.ts +2 -0
  100. package/dist/lib/harness/adapters/droid.js +17 -0
  101. package/dist/lib/harness/adapters/grok.d.ts +2 -0
  102. package/dist/lib/harness/adapters/grok.js +14 -0
  103. package/dist/lib/harness/adapters/kimi.d.ts +2 -0
  104. package/dist/lib/harness/adapters/kimi.js +49 -0
  105. package/dist/lib/harness/adapters/muse.d.ts +2 -0
  106. package/dist/lib/harness/adapters/muse.js +44 -0
  107. package/dist/lib/harness/adapters/opencode.d.ts +2 -0
  108. package/dist/lib/harness/adapters/opencode.js +14 -0
  109. package/dist/lib/harness/exec-config-version.d.ts +8 -0
  110. package/dist/lib/harness/exec-config-version.js +22 -0
  111. package/dist/lib/harness/index.d.ts +1 -0
  112. package/dist/lib/harness/index.js +34 -0
  113. package/dist/lib/{hooks.d.ts → hooks/install.d.ts} +1 -1
  114. package/dist/lib/{hooks.js → hooks/install.js} +11 -11
  115. package/dist/lib/import.js +1 -1
  116. package/dist/lib/installations/index.d.ts +4 -4
  117. package/dist/lib/installations/index.js +3 -4
  118. package/dist/lib/installations/migrate.js +6 -6
  119. package/dist/lib/{shims.d.ts → installations/shims.d.ts} +2 -2
  120. package/dist/lib/{shims.js → installations/shims.js} +12 -114
  121. package/dist/lib/installations/store.d.ts +141 -3
  122. package/dist/lib/installations/store.js +475 -5
  123. package/dist/lib/installations/versions.d.ts +1 -126
  124. package/dist/lib/installations/versions.js +5 -465
  125. package/dist/lib/isolation-boundary-report.d.ts +1 -1
  126. package/dist/lib/isolation-boundary-report.js +1 -1
  127. package/dist/lib/mailbox-gc.js +2 -3
  128. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  129. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  130. package/dist/lib/monitors/dispatch.d.ts +1 -1
  131. package/dist/lib/monitors/dispatch.js +2 -2
  132. package/dist/lib/monitors/state.d.ts +1 -1
  133. package/dist/lib/plugins/plugins.js +1 -1
  134. package/dist/lib/plugins/skills.js +1 -1
  135. package/dist/lib/prix-account.d.ts +158 -0
  136. package/dist/lib/prix-account.js +214 -0
  137. package/dist/lib/project-key.js +2 -2
  138. package/dist/lib/project-launch.js +1 -1
  139. package/dist/lib/refresh.js +2 -2
  140. package/dist/lib/resource-inventory.d.ts +1 -1
  141. package/dist/lib/resource-inventory.js +1 -1
  142. package/dist/lib/rules/rules.js +1 -1
  143. package/dist/lib/scheduling/routines.js +1 -1
  144. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  145. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  146. package/dist/lib/self-heal/checks/hook-runtime.js +1 -1
  147. package/dist/lib/self-heal/checks/path.js +1 -1
  148. package/dist/lib/self-heal/checks/shadowing.js +1 -1
  149. package/dist/lib/self-heal/checks/shims.js +1 -1
  150. package/dist/lib/session/discover.d.ts +39 -0
  151. package/dist/lib/session/discover.js +3 -3
  152. package/dist/lib/session/parse.js +24 -42
  153. package/dist/lib/session/recovery.js +1 -1
  154. package/dist/lib/staleness/prune.d.ts +1 -1
  155. package/dist/lib/staleness/prune.js +1 -1
  156. package/dist/lib/staleness/writers/hooks.js +1 -1
  157. package/dist/lib/startup/command-registry.js +1 -1
  158. package/dist/lib/teams/agents.js +2 -19
  159. package/dist/lib/teams/parsers.js +20 -26
  160. package/dist/lib/teams/registry.js +2 -21
  161. package/dist/lib/triggers/handlers.js +1 -1
  162. package/dist/lib/triggers/webhook.js +1 -1
  163. package/dist/lib/uninstall.js +1 -1
  164. package/dist/lib/version.d.ts +9 -2
  165. package/dist/lib/version.js +11 -9
  166. package/package.json +1 -1
  167. /package/dist/lib/{computer-rpc.d.ts → computer/computer-rpc.d.ts} +0 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.22.42
4
+
5
+ - **Plan-tier gates for `agents accounts` and `agents insights` (RUSH-2424).** A new `apps/cli/src/lib/entitlement.ts` reads the live subscription tier from `GET /api/v1/billing/subscription?agent=agi-cli` (the session token in `~/.rush/user.yaml`), caches it on disk for 15 minutes, and stays offline-tolerant: a stale cache is honored over a failed network call, and no session file at all resolves straight to the free tier. `agents accounts add` / `name` / `attach` now cap registered accounts at 3 per harness on free, 10 on paid/admin — a 4th add on free refuses before any write (`free plan is capped at 3 claude accounts (3/3). agents upgrade — up to 10 per harness.`) and the 3rd prints a one-line notice. Downgrading a plan never deletes a credential: over-cap accounts fall out of `accounts switch`/`set-default` (excluded from `listSwitchableAccounts`) and are listed `dormant (upgrade to reactivate)` in `agents accounts`. `agents insights` keeps top-line counts, harness mix, `insights mix`, and `agents perf` free on every tier; the Friction / Friction-thrash / Dissatisfaction-corrections sections, grouping `--by account` (the default), and `--narrative` are paid — a gated section is replaced by the in-voice notice `Friction and account-split analysis are on the paid plan.` in both the text report and `--json` (a new `plan: {tierName, isPaid}` field, `groups: null` when the account breakdown is gated, and the friction/correction facet keys stripped per group otherwise). Source: `apps/cli/src/lib/entitlement.ts`, `apps/cli/src/commands/accounts.ts`, `apps/cli/src/commands/insights.ts`.
6
+
7
+ - **`--profile <name>` means the same profile in every browser command, and a profile's name never carries its endpoint (RUSH-2709).** `BrowserService.start` used to overwrite `BrowserProfile.name` with the composite `<profile>@<endpoint>` runtime key, so the key leaked into `profiles list`, `status`, `history`, and the feed, and six consumers each invented their own rule for turning one back into the other — which is why `status --profile default` could report "No browser tasks running" while that profile was running. The two concepts are now distinct types: `ProfileName` (user-facing, always bare), `EndpointName`, and a **branded** `ConnectionKey` built at exactly one site, so `connections.get(someProfileName)` — the original miss — no longer compiles. One rule, `keyBelongsToProfile`, replaces the four ad-hoc reconciliations in `status`, `stopProfile`, `findTask`, and `listProfileCacheDirs`, which also fixes a legacy `<name>.<n>` fork directory being matched by some of those paths and missed by others. `status` renders `comet-local (endpoint: endpoint-0, port 9222, pid …)` instead of the raw key, and `status --json` carries `name` (bare), `endpoint`, and `key` as separate fields. Source: `apps/cli/src/lib/browser/{types,service,profiles,runtime-state}.ts`, `apps/cli/src/lib/browser/drivers/{local,ssh}.ts`, `apps/cli/src/commands/browser.ts`, `apps/cli/docs/browser.md`.
8
+
9
+ - **`default` is now purely an alias, and the auto-detected browser profile is named `auto-chrome` (RUSH-2709).** `default` used to name BOTH a concrete auto-detected profile and the "whatever you configured" alias, and only `start` honored the alias (its own comment: "narrowly scoped … only here in `start`") — so `--profile default` reached one profile in `start` and a different one, or nothing, in `stop` / `status` / `navigate`. One resolver, `resolveProfileRef`, now serves every command. Nothing existing breaks: a `browser.profile` config value of `default` still resolves (to the auto-detected profile), a profile you literally named `default` still resolves to itself and outranks the alias, and a machine that already carries a `default` profile keeps using it — `ensureDefaultBrowserProfile` reuses and regenerates it in place rather than creating a second one, so a running browser and its on-disk runtime dirs are not orphaned. Source: `apps/cli/src/lib/browser/profiles.ts`, `apps/cli/src/commands/{browser,setup,setup-browser,setup-preferences}.ts`, `apps/cli/src/lib/installations/migrate.ts`.
10
+
11
+ - **`agents auth login/whoami/logout` and `agents org create/list/view/invite/members/role/remove/leave` — the account-layer client over the existing `api.prix.dev` backend.** `agents auth login` runs the already-shipped RFC 8628 device-code flow (`POST /api/v1/auth/device/authorization` + poll `/token`) and writes its own session file, separate from `rush login`'s `~/.rush/user.yaml` — so `agents auth logout` never signs you out of `rush`, while `agents auth whoami`/`agents org` fall back to a live `rush login` session automatically if you never ran `agents auth login`. `agents org` maps to the backend's `/api/v1/spaces` routes (not `/api/v1/orgs` — spaces already enforce the free-tier caps of 1 owned space / 3 members and can exist standalone). Every subcommand takes `--json`; `--space <id-or-slug>` is optional and defaults to your sole space. Source: `apps/cli/src/lib/prix-account.ts`, `apps/cli/src/commands/{auth,org}.ts`.
12
+
13
+ - **`agents accounts switch <harness> [account]` picks the default account for a harness.** Interactive mode reuses the run-account picker layout (usage %, headroom, signed-out / rate-limited) and writes the existing per-harness default — the same binding as `accounts set-default`, which balanced rotation already honors. Pass an account name to skip the picker; `--json` lists or reports the result. Native `accounts name` / `attach` now refuse an unsupported harness with a named reason (`kimi accounts can't be isolated by agents-cli yet (device-scoped login). Supported today: claude, codex, grok.`); provider `accounts add --provider` is unchanged. Source: `apps/cli/src/commands/accounts.ts`, `apps/cli/src/commands/run-account-picker.ts`, `apps/cli/src/lib/account-capabilities.ts`.
14
+
15
+ - **`agents browser navigate` now reuses a tab in Arc instead of refusing outright (#2786, #2778).** Arc crashes on `Target.createTarget`, so every navigate was answered with "use a Chromium-family browser" — which meant the document was never shown at all and callers fell back to a raw `open`, a new tab per call, i.e. exactly the tab-spam #2779 set out to end. Arc does expose page targets and does honor `Page.navigate` on them (measured against a live Arc: 33 targets, navigate reused one, tab count unchanged, no crash), so navigate now reuses in place, the same shape as the existing Electron branch. Reuse is narrow and non-destructive: only a tab ALREADY showing the requested URL, or an empty new-tab page, and a target another task owns is left alone. A reused tab is recorded as **borrowed** — the task drives it but never closes it, so a tab that was open before the task survives `done` (the rule `adoptTabShowing` states for unowned pages, kept here where reuse is unavoidable). When nothing is safe to reuse the actionable refusal stands. The `Target.createTarget` guard from #2778 is unchanged. Source: `apps/cli/src/lib/browser/service.ts`.
16
+
3
17
  ## 1.22.41
4
18
 
5
19
  - **BREAKING: the top-level `agents timeline` command is removed — use `agents feed --filter updates` (RUSH-2692).** `timeline` was a pure alias of `agents feed --filter updates` (its own description said so), a second door to one stream against the CLI-surface convention of one owned noun with no duplicated surface. It is gone entirely, not deprecated: `agents timeline` now reports `unknown command`, and `timeline` is retired from distance-1 auto-correct so a stale invocation fails loudly instead of running a neighbouring command. `agents feed --filter updates` is unchanged, and the sibling observe aliases `agents inbox` and `agents roster` are unaffected. Source: `apps/cli/src/commands/feed.ts`, `apps/cli/src/lib/observe-aliases.ts`, `apps/cli/src/lib/startup/command-registry.ts`, `apps/cli/src/bootstrap.ts`.
package/README.md CHANGED
@@ -39,7 +39,11 @@
39
39
  <a href="https://docs.warp.dev/reference/cli" title="Warp Agent CLI (Oz)"><picture><source media="(prefers-color-scheme: dark)" srcset="assets/harnesses/warp.svg"><img src="assets/harnesses/warp-light.svg" height="32" alt="Warp Agent CLI" /></picture></a>
40
40
  </p>
41
41
 
42
- https://agi-cli.sh/demo.mp4
42
+ <p align="center">
43
+ <img src="assets/demos/hero.gif" alt="agents CLI: sessions across every agent and project, chaining agents in a Unix pipeline, dispatching to the cloud, and one team spread across machines" width="860" />
44
+ </p>
45
+
46
+ <p align="center"><a href="https://agi-cli.sh/demo.mp4">Watch the full 56-second demo</a></p>
43
47
 
44
48
  ## Quickstart
45
49
 
@@ -1006,15 +1010,17 @@ agents accounts add gateway --provider openrouter --auth api-key \
1006
1010
  --from-secrets openrouter.ai:OPENROUTER_API_KEY # import from an existing secrets bundle
1007
1011
  agents accounts add deepinfra --provider deepinfra --auth api-key
1008
1012
 
1013
+ agents accounts switch claude # picker: usage %, headroom, signed-out / rate-limited
1014
+ agents accounts switch claude work # skip the picker; same write as set-default
1009
1015
  agents accounts set-default claude work # claude uses `work` when --account is omitted
1010
1016
  agents accounts sync work --device yosemite-s0 # explicitly copy the bundle to a worker device
1011
1017
  agents run claude --account work
1012
1018
  agents harness add deepinfra --account deepinfra
1013
1019
  ```
1014
1020
 
1015
- One provider account **is** one `agents secrets` bundle -- `agents accounts add` creates it with secrets policy `never`, so a background agent launch on that account never raises Touch ID. `agents accounts` (no subcommand) lists provider bundles next to harness-native signed-in identities so you see both kinds of credential together; `accounts list` / `inspect <name>` / `set-key <name>` (rotate) / `rename` / `remove` manage a bundle by its stable id, independent of its current label.
1021
+ One provider account **is** one `agents secrets` bundle -- `agents accounts add` creates it with secrets policy `never`, so a background agent launch on that account never raises Touch ID. `agents accounts` (no subcommand) lists provider bundles next to harness-native signed-in identities so you see both kinds of credential together; `accounts list` / `inspect <name>` / `set-key <name>` (rotate) / `rename` / `remove` manage a bundle by its stable id, independent of its current label. `accounts switch <harness>` is the fast picker over that default -- it writes the same binding as `set-default`, and balanced rotation already honors it.
1016
1022
 
1017
- Harness-native OAuth logins (Claude Code's own `/login`, `codex login`, and so on) stay exactly where the harness put them -- agi-cli discovers and displays them but never copies, renames, or converts them into a provider bundle. `accounts sync <name> --device <device>` is the only way a provider account crosses machines, and it's explicit: nothing syncs automatically. Selection order for a run is explicit `--account`, then `accounts set-default` for that harness, then the harness's native/balanced account behavior.
1023
+ Harness-native OAuth logins (Claude Code's own `/login`, `codex login`, and so on) stay exactly where the harness put them -- agi-cli discovers and displays them but never copies, renames, or converts them into a provider bundle. Native `accounts name` / `attach` is only for harnesses agents-cli can isolate today (claude, codex, grok; muse when an email is present). A device-scoped login (kimi, droid, …) is refused with a named reason; provider `accounts add --provider` is unrestricted. `accounts sync <name> --device <device>` is the only way a provider account crosses machines, and it's explicit: nothing syncs automatically. Selection order for a run is explicit `--account`, then `accounts set-default` / `switch` for that harness, then the harness's native/balanced account behavior.
1018
1024
 
1019
1025
  ---
1020
1026
 
package/dist/bin/agents CHANGED
Binary file
@@ -98,6 +98,8 @@ export declare const loadWebhooks: ModuleLoader;
98
98
  export declare const loadHumans: ModuleLoader;
99
99
  export declare const loadAccounts: ModuleLoader;
100
100
  export declare const loadDaemon: ModuleLoader;
101
+ export declare const loadAuth: ModuleLoader;
102
+ export declare const loadOrg: ModuleLoader;
101
103
  /**
102
104
  * Commands whose modules pull in the SQLite-backed session/cloud stack. They are
103
105
  * registered AFTER `applyGlobalHelpConventions` (mirroring main's order: help
@@ -103,6 +103,8 @@ export const loadWebhooks = async () => (await import('../commands/webhook.js'))
103
103
  export const loadHumans = async () => (await import('../commands/humans.js')).registerHumansCommands;
104
104
  export const loadAccounts = async () => (await import('../commands/accounts.js')).registerAccountsCommand;
105
105
  export const loadDaemon = async () => (await import('../commands/daemon.js')).registerDaemonCommand;
106
+ export const loadAuth = async () => (await import('../commands/auth.js')).registerAuthCommand;
107
+ export const loadOrg = async () => (await import('../commands/org.js')).registerOrgCommand;
106
108
  /**
107
109
  * Commands whose modules pull in the SQLite-backed session/cloud stack. They are
108
110
  * registered AFTER `applyGlobalHelpConventions` (mirroring main's order: help
@@ -135,6 +137,8 @@ export const LAZY_COMMAND_NAMES = new Set([
135
137
  */
136
138
  export const COMMAND_LOADERS = {
137
139
  accounts: [loadAccounts],
140
+ auth: [loadAuth],
141
+ org: [loadOrg],
138
142
  view: [loadView],
139
143
  inspect: [loadInspect],
140
144
  feedback: [loadFeedback],
@@ -1,6 +1,14 @@
1
1
  import type { Command } from 'commander';
2
2
  import type { AgentId } from '../lib/types.js';
3
3
  import { type Profile } from '../lib/profiles.js';
4
+ import { type UnifiedAccount } from '../lib/account-registry.js';
5
+ export declare function nativeIdentityFromSource(raw: string): Promise<{
6
+ agent: AgentId;
7
+ version: string;
8
+ identityKey: string;
9
+ identityLabel?: string;
10
+ scope: 'version' | 'device';
11
+ }>;
4
12
  type AttachTarget = {
5
13
  kind: 'installation';
6
14
  agent: AgentId;
@@ -22,5 +30,26 @@ export declare function parseBundleKey(raw: string): {
22
30
  bundle: string;
23
31
  key: string;
24
32
  };
33
+ /**
34
+ * Named accounts that `set-default` / `switch` can pin for this harness —
35
+ * capped to the caller's plan tier (RUSH-2424). Downgrading never deletes a
36
+ * credential: an over-cap account simply falls out of this list (see
37
+ * {@link dormantAccountsForHarness}) and stops being a switch/rotation target
38
+ * until the plan is upgraded.
39
+ */
40
+ export declare function listSwitchableAccounts(agent: AgentId): Promise<UnifiedAccount[]>;
41
+ /** Accounts registered for this harness beyond the plan's cap — kept, never deleted, excluded from switch/rotation, reactivated by an upgrade. */
42
+ export declare function dormantAccountsForHarness(agent: AgentId): Promise<UnifiedAccount[]>;
43
+ /**
44
+ * Pin the per-harness default. Shared by `accounts set-default` and `accounts switch`.
45
+ * Provider accounts must authenticate the harness; native accounts must belong to it.
46
+ */
47
+ export declare function setDefaultAccount(agentRaw: string, name: string): {
48
+ agent: AgentId;
49
+ account: UnifiedAccount;
50
+ };
51
+ export declare function runAccountsSwitch(harness: string, accountName: string | undefined, opts: {
52
+ json: boolean;
53
+ }): Promise<void>;
25
54
  export declare function registerAccountsCommand(program: Command): void;
26
55
  export {};
@@ -4,7 +4,10 @@ import { setHelpSections } from '../lib/help.js';
4
4
  import { readMeta, updateMeta } from '../lib/state.js';
5
5
  import { ALL_AGENT_IDS, getAccountInfo, resolveAgentName } from '../lib/agents.js';
6
6
  import { getVersionHomePath, listInstalledVersions } from '../lib/installations/versions.js';
7
- import { nativeAccountCapability, nativeAccountNameable, nativeIdentityKey } from '../lib/account-capabilities.js';
7
+ import { assertNativeAccountNameable, nativeAccountCapability, nativeIdentityKey } from '../lib/account-capabilities.js';
8
+ import { collectRunCandidates } from '../lib/accounting/rotate.js';
9
+ import { isInteractiveTerminal } from './utils.js';
10
+ import { buildSwitchAccountChoices, formatAccountLimits, pickSwitchAccount } from './run-account-picker.js';
8
11
  import { profileExists, readProfile } from '../lib/profiles.js';
9
12
  import { pushBundleToHost } from '../lib/secrets/push.js';
10
13
  import { assertCredentialTransportHostPinned, resolveHostSshTarget } from '../lib/secrets/remote.js';
@@ -14,6 +17,7 @@ import { discoverNativeAccounts } from '../lib/account-catalog.js';
14
17
  import { readAndResolveBundleEnv } from '../lib/secrets/bundles.js';
15
18
  import { getAccountProvider, listAccountProviders } from '../lib/account-provider-registry.js';
16
19
  import { accountBindings, addAccount, addNativeAccount, bindAccount, findAccount, findUnifiedAccount, inspectAccount, listNativeAccounts, readAccountRegistry, removeAccount, renameAccount, setAccountSecret, unbindAccount } from '../lib/account-registry.js';
20
+ import { accountCapForTier, getTier } from '../lib/entitlement.js';
17
21
  function parseInstallation(raw) {
18
22
  const at = raw.lastIndexOf('@');
19
23
  if (at < 1 || at === raw.length - 1)
@@ -23,12 +27,10 @@ function parseInstallation(raw) {
23
27
  throw new Error(`Unknown agent '${raw.slice(0, at)}'.`);
24
28
  return { agent, version: raw.slice(at + 1) };
25
29
  }
26
- async function nativeIdentityFromSource(raw) {
30
+ export async function nativeIdentityFromSource(raw) {
27
31
  const parsed = parseInstallation(raw);
28
32
  const capability = nativeAccountCapability(parsed.agent);
29
- if (!nativeAccountNameable(parsed.agent)) {
30
- throw new Error(`${parsed.agent} native accounts are ${capability.status}; agents-cli cannot safely name or attach this login.`);
31
- }
33
+ assertNativeAccountNameable(parsed.agent);
32
34
  if (!listInstalledVersions(parsed.agent).includes(parsed.version))
33
35
  throw new Error(`${raw} is not installed.`);
34
36
  const info = await getAccountInfo(parsed.agent, getVersionHomePath(parsed.agent, parsed.version));
@@ -72,8 +74,8 @@ function secretFromBundle(raw) {
72
74
  const { bundle, key } = parseBundleKey(raw);
73
75
  return readAndResolveBundleEnv(bundle, { keys: [key], keyMode: 'storage', agentOnly: true, caller: 'accounts import' }).env[key];
74
76
  }
75
- function publicAccount(account) {
76
- return { kind: 'provider', id: account.id, name: account.name, provider: account.provider, auth: account.auth, baseUrl: account.baseUrl, policy: account.policy, secretPresent: account.secretPresent };
77
+ function publicAccount(account, dormant) {
78
+ return { kind: 'provider', id: account.id, name: account.name, provider: account.provider, auth: account.auth, baseUrl: account.baseUrl, policy: account.policy, secretPresent: account.secretPresent, dormant };
77
79
  }
78
80
  async function printAccounts(json, fleet = false) {
79
81
  if (fleet)
@@ -81,12 +83,16 @@ async function printAccounts(json, fleet = false) {
81
83
  const records = Object.values(readAccountRegistry().accounts).sort((a, b) => a.name.localeCompare(b.name));
82
84
  const discovered = await discoverNativeAccounts();
83
85
  const savedNative = listNativeAccounts(readMeta());
86
+ const dormantIds = new Set();
87
+ for (const agent of ALL_AGENT_IDS)
88
+ for (const account of await dormantAccountsForHarness(agent))
89
+ dormantIds.add(account.id);
84
90
  const native = discovered.map(row => {
85
91
  const saved = savedNative.find(account => account.agent === row.agent && account.identityKey === row.id);
86
- return { ...row, name: saved?.name, id: saved?.id ?? row.id };
92
+ return { ...row, name: saved?.name, id: saved?.id ?? row.id, dormant: !!saved && dormantIds.has(saved.id) };
87
93
  });
88
94
  if (json) {
89
- console.log(JSON.stringify([...records.map(account => publicAccount(inspectAccount(account.name))), ...native], null, 2));
95
+ console.log(JSON.stringify([...records.map(account => publicAccount(inspectAccount(account.name), dormantIds.has(account.id))), ...native], null, 2));
90
96
  return;
91
97
  }
92
98
  console.log(chalk.bold('Provider account bundles\n'));
@@ -94,19 +100,168 @@ async function printAccounts(json, fleet = false) {
94
100
  console.log(chalk.gray(" None. Add one with 'agents accounts add <name> --provider <provider> --auth <type>'."));
95
101
  for (const account of records) {
96
102
  const present = inspectAccount(account.name).secretPresent ? chalk.green('ready') : chalk.red('missing on this device');
97
- console.log(` ${chalk.cyan(account.name)} ${account.provider} ${account.auth} ${present}`);
103
+ const dormantSuffix = dormantIds.has(account.id) ? chalk.gray(' — dormant (upgrade to reactivate)') : '';
104
+ console.log(` ${chalk.cyan(account.name)} ${account.provider} ${account.auth} ${present}${dormantSuffix}`);
98
105
  }
99
106
  console.log(chalk.bold('\nNative harness logins\n'));
100
107
  if (!native.length)
101
108
  console.log(chalk.gray(' No signed-in native accounts found.'));
102
109
  for (const account of native)
103
- console.log(` ${account.name ? `${chalk.cyan(account.name)} · ` : ''}${account.display} ${account.agent} ${account.versions.join(', ')}`);
110
+ console.log(` ${account.name ? `${chalk.cyan(account.name)} · ` : ''}${account.display} ${account.agent} ${account.versions.join(', ')}${account.dormant ? chalk.gray(' — dormant (upgrade to reactivate)') : ''}`);
104
111
  }
105
112
  function parseAuth(raw) {
106
113
  if (raw === 'api-key' || raw === 'setup-token' || raw === 'bearer-token')
107
114
  return raw;
108
115
  throw new Error(`Unsupported auth type '${raw}'. Use api-key, setup-token, or bearer-token.`);
109
116
  }
117
+ function parseHarness(agentRaw) {
118
+ if (!ALL_AGENT_IDS.includes(agentRaw))
119
+ throw new Error(`Unknown agent '${agentRaw}'.`);
120
+ return agentRaw;
121
+ }
122
+ function providerAuthenticatesHarness(provider, auth, agent) {
123
+ try {
124
+ getAccountProvider(provider).envFor(agent, auth);
125
+ return true;
126
+ }
127
+ catch (err) {
128
+ const message = err instanceof Error ? err.message : String(err);
129
+ if (message.includes('cannot authenticate'))
130
+ return false;
131
+ throw err;
132
+ }
133
+ }
134
+ /** Every account (native + provider) registered/usable for this harness, oldest-first by name — the full set a plan-tier cap slices into active vs dormant. */
135
+ function accountsForHarness(agent) {
136
+ const meta = readMeta();
137
+ const native = listNativeAccounts(meta).filter(account => account.agent === agent);
138
+ const providers = Object.values(readAccountRegistry().accounts)
139
+ .filter(account => providerAuthenticatesHarness(account.provider, account.auth, agent))
140
+ .map(account => ({ ...account, kind: 'provider' }));
141
+ return [...native, ...providers].sort((a, b) => a.name.localeCompare(b.name));
142
+ }
143
+ /**
144
+ * Named accounts that `set-default` / `switch` can pin for this harness —
145
+ * capped to the caller's plan tier (RUSH-2424). Downgrading never deletes a
146
+ * credential: an over-cap account simply falls out of this list (see
147
+ * {@link dormantAccountsForHarness}) and stops being a switch/rotation target
148
+ * until the plan is upgraded.
149
+ */
150
+ export async function listSwitchableAccounts(agent) {
151
+ const cap = accountCapForTier(await getTier());
152
+ return accountsForHarness(agent).slice(0, cap);
153
+ }
154
+ /** Accounts registered for this harness beyond the plan's cap — kept, never deleted, excluded from switch/rotation, reactivated by an upgrade. */
155
+ export async function dormantAccountsForHarness(agent) {
156
+ const cap = accountCapForTier(await getTier());
157
+ return accountsForHarness(agent).slice(cap);
158
+ }
159
+ function accountCapRefusalMessage(agent, tier, cap) {
160
+ if (!tier.isPaid)
161
+ return `free plan is capped at 3 ${agent} accounts (3/3). agents upgrade — up to 10 per harness.`;
162
+ return `${tier.tierName} plan is capped at ${cap} ${agent} accounts (${cap}/${cap}).`;
163
+ }
164
+ /**
165
+ * Refuse BEFORE any write when adding a new account would push one of
166
+ * `harnesses` over the caller's plan cap. Returns the tier/cap/pre-add counts
167
+ * so the caller can print the exactly-at-cap notice after a successful write.
168
+ */
169
+ async function assertAccountCapacityFor(harnesses) {
170
+ const tier = await getTier();
171
+ const cap = accountCapForTier(tier);
172
+ const existing = new Map();
173
+ for (const harness of harnesses)
174
+ existing.set(harness, accountsForHarness(harness).length);
175
+ const capped = harnesses.find(harness => (existing.get(harness) ?? 0) >= cap);
176
+ if (capped)
177
+ throw new Error(accountCapRefusalMessage(capped, tier, cap));
178
+ return { tier, cap, existing };
179
+ }
180
+ /** One-line, non-blocking notice for every harness a free-tier add just brought to exactly its cap. */
181
+ function printCapNoticesFor(harnesses, tier, cap, existing) {
182
+ if (tier.isPaid)
183
+ return;
184
+ for (const harness of harnesses) {
185
+ if ((existing.get(harness) ?? 0) + 1 === cap) {
186
+ console.log(chalk.yellow(`${cap}/${cap} ${harness} accounts on the free plan. agents upgrade — up to 10 per harness.`));
187
+ }
188
+ }
189
+ }
190
+ /**
191
+ * Pin the per-harness default. Shared by `accounts set-default` and `accounts switch`.
192
+ * Provider accounts must authenticate the harness; native accounts must belong to it.
193
+ */
194
+ export function setDefaultAccount(agentRaw, name) {
195
+ const agent = parseHarness(agentRaw);
196
+ const account = findUnifiedAccount(name, readMeta());
197
+ if (!account)
198
+ throw new Error(`Unknown account '${name}'.`);
199
+ if (account.kind === 'provider') {
200
+ getAccountProvider(account.provider).envFor(agent, account.auth);
201
+ }
202
+ else {
203
+ if (account.agent !== agent) {
204
+ throw new Error(`Account '${account.name}' is a ${account.agent} login and cannot be the default for ${agent}.`);
205
+ }
206
+ assertNativeAccountNameable(account.agent);
207
+ }
208
+ updateMeta(meta => ({ ...meta, accounts: { ...meta.accounts, defaults: { ...meta.accounts?.defaults, [agent]: account.id } } }));
209
+ return { agent, account };
210
+ }
211
+ async function switchAccountRows(agent) {
212
+ const accounts = await listSwitchableAccounts(agent);
213
+ const candidates = await collectRunCandidates(agent);
214
+ const defaultId = readMeta().accounts?.defaults?.[agent];
215
+ return accounts.map(account => {
216
+ const candidate = account.kind === 'native'
217
+ ? candidates.find(row => row.accountKey === account.identityKey) ?? null
218
+ : null;
219
+ return {
220
+ accountName: account.name,
221
+ kind: account.kind,
222
+ detail: account.kind === 'provider' ? account.provider : (account.identityLabel ?? account.identityKey),
223
+ current: account.id === defaultId,
224
+ candidate,
225
+ };
226
+ });
227
+ }
228
+ export async function runAccountsSwitch(harness, accountName, opts) {
229
+ const agent = parseHarness(harness);
230
+ const rows = await switchAccountRows(agent);
231
+ if (accountName) {
232
+ const { account } = setDefaultAccount(agent, accountName);
233
+ if (opts.json) {
234
+ console.log(JSON.stringify({ harness: agent, account: account.name, id: account.id }, null, 2));
235
+ return;
236
+ }
237
+ console.log(chalk.green(`${agent} now uses account '${account.name}' unless --account overrides it.`));
238
+ return;
239
+ }
240
+ if (opts.json) {
241
+ const choices = buildSwitchAccountChoices(rows);
242
+ console.log(JSON.stringify({
243
+ harness: agent,
244
+ default: rows.find(row => row.current)?.accountName ?? null,
245
+ accounts: rows.map((row, i) => ({
246
+ name: row.accountName,
247
+ kind: row.kind,
248
+ detail: row.detail,
249
+ current: row.current,
250
+ ready: choices[i].ready,
251
+ limits: row.candidate ? formatAccountLimits(row.candidate) : 'limits unavailable',
252
+ })),
253
+ }, null, 2));
254
+ return;
255
+ }
256
+ if (!isInteractiveTerminal()) {
257
+ throw new Error(`Selecting a ${agent} account requires an interactive terminal.\nUse: agents accounts switch ${agent} <account>\nOr: agents accounts set-default ${agent} <account>`);
258
+ }
259
+ const selected = await pickSwitchAccount(agent, rows);
260
+ if (!selected)
261
+ return;
262
+ const { account } = setDefaultAccount(agent, selected);
263
+ console.log(chalk.green(`${agent} now uses account '${account.name}' unless --account overrides it.`));
264
+ }
110
265
  export function registerAccountsCommand(program) {
111
266
  const accounts = program.command('accounts').description('Browse native logins and manage provider account bundles')
112
267
  .option('--json', 'Machine-readable account metadata')
@@ -124,10 +279,16 @@ export function registerAccountsCommand(program) {
124
279
  const provider = getAccountProvider(o.provider);
125
280
  if (!provider.authKinds.includes(auth))
126
281
  throw new Error(`Provider '${provider.provider}' does not support ${auth}. Supported: ${provider.authKinds.join(', ')}.`);
282
+ // A provider account may authenticate more than one harness (e.g. openrouter);
283
+ // it counts toward every harness it can authenticate, so refuse before any
284
+ // write if adding it would push ANY of those harnesses over the plan cap.
285
+ const harnesses = ALL_AGENT_IDS.filter(h => providerAuthenticatesHarness(provider.provider, auth, h));
286
+ const { tier, cap, existing } = await assertAccountCapacityFor(harnesses);
127
287
  const secret = o.fromSecrets ? secretFromBundle(o.fromSecrets) : await password({ message: `Enter ${provider.provider} ${auth} for '${name}':` });
128
288
  const account = addAccount(name, provider.provider, auth, secret, undefined, { baseUrl: o.baseUrl });
129
289
  console.log(chalk.green(`Added ${account.provider} ${account.auth} account '${account.name}'.`));
130
290
  console.log(chalk.gray(`Secret bundle '${account.name}' is the account and uses policy never, so agent launches never request Touch ID.`));
291
+ printCapNoticesFor(harnesses, tier, cap, existing);
131
292
  });
132
293
  accounts.command('set-key <name>')
133
294
  .description('Rotate an account credential without changing its identity')
@@ -140,17 +301,27 @@ export function registerAccountsCommand(program) {
140
301
  setAccountSecret(name, secret);
141
302
  console.log(chalk.green(`Updated credential for account '${name}'.`));
142
303
  });
143
- accounts.command('view <name>').alias('inspect').description('Show safe account metadata, custody, and attachments').option('--json', 'Machine-readable output').action((name, o, command) => {
304
+ accounts.command('view <name>').alias('inspect').description('Show safe account metadata, custody, and attachments').option('--json', 'Machine-readable output').action(async (name, o, command) => {
144
305
  const meta = readMeta();
145
306
  const unified = findUnifiedAccount(name, meta);
146
307
  if (!unified)
147
308
  throw new Error(`Unknown account '${name}'.`);
309
+ const harnesses = unified.kind === 'provider'
310
+ ? ALL_AGENT_IDS.filter(h => providerAuthenticatesHarness(unified.provider, unified.auth, h))
311
+ : [unified.agent];
312
+ let dormant = false;
313
+ for (const harness of harnesses) {
314
+ if ((await dormantAccountsForHarness(harness)).some(a => a.id === unified.id)) {
315
+ dormant = true;
316
+ break;
317
+ }
318
+ }
148
319
  const account = unified.kind === 'provider'
149
- ? { ...publicAccount(inspectAccount(unified.name)), custody: 'agents secrets (policy never)', attached: accountBindings(unified.id, meta) }
150
- : { ...unified, custody: `${unified.agent} (not stored by agents-cli)`, attached: accountBindings(unified.id, meta) };
320
+ ? { ...publicAccount(inspectAccount(unified.name), dormant), custody: 'agents secrets (policy never)', attached: accountBindings(unified.id, meta) }
321
+ : { ...unified, dormant, custody: `${unified.agent} (not stored by agents-cli)`, attached: accountBindings(unified.id, meta) };
151
322
  if (o.json || command.optsWithGlobals().json)
152
323
  return console.log(JSON.stringify(account, null, 2));
153
- console.log(chalk.bold(account.name));
324
+ console.log(chalk.bold(account.name) + (account.dormant ? chalk.gray(' — dormant (upgrade to reactivate)') : ''));
154
325
  console.log(` kind: ${account.kind}`);
155
326
  console.log(` id: ${account.id}`);
156
327
  console.log(` custody: ${account.custody}`);
@@ -169,10 +340,12 @@ export function registerAccountsCommand(program) {
169
340
  .description('Name a signed-in native installation without copying its OAuth credentials')
170
341
  .action(async (source, name) => {
171
342
  const identity = await nativeIdentityFromSource(source);
343
+ const { tier, cap, existing } = await assertAccountCapacityFor([identity.agent]);
172
344
  const account = addNativeAccount(name, identity.agent, identity.identityKey, identity.identityLabel, identity.scope);
173
345
  console.log(chalk.green(`Named ${source} as ${account.name}.`));
174
346
  if (account.scope === 'device')
175
347
  console.log(chalk.gray(`${identity.agent} authentication is device-scoped; attach '${account.name}' to '${identity.agent}', not an individual version.`));
348
+ printCapNoticesFor([identity.agent], tier, cap, existing);
176
349
  });
177
350
  accounts.command('attach <account> <target>')
178
351
  .description('Attach a named account to a native installation or custom harness')
@@ -181,6 +354,8 @@ export function registerAccountsCommand(program) {
181
354
  const account = findUnifiedAccount(name, meta);
182
355
  if (!account)
183
356
  throw new Error(`Unknown account '${name}'.`);
357
+ if (account.kind === 'native')
358
+ assertNativeAccountNameable(account.agent);
184
359
  // Validate the target exists before mutating any binding.
185
360
  const t = classifyAttachTarget(target);
186
361
  const targetAgent = t.kind === 'profile' ? t.profile.host.agent : t.agent;
@@ -208,6 +383,17 @@ export function registerAccountsCommand(program) {
208
383
  // Provider account: it must be able to authenticate the target's harness.
209
384
  getAccountProvider(account.provider).envFor(targetAgent, account.auth);
210
385
  }
386
+ // Defense-in-depth cap check: by the point we get here `account` already
387
+ // authenticates/belongs to `targetAgent` (validated above), so it is
388
+ // already counted in accountsForHarness(targetAgent) from its `add`/`name`
389
+ // time — this can only refuse if that invariant is ever violated, never a
390
+ // normal re-attach of an account the harness is already at cap with.
391
+ const tier = await getTier();
392
+ const cap = accountCapForTier(tier);
393
+ const alreadyCounted = accountsForHarness(targetAgent).some(existing => existing.id === account.id);
394
+ const otherCount = accountsForHarness(targetAgent).length - (alreadyCounted ? 1 : 0);
395
+ if (otherCount >= cap)
396
+ throw new Error(accountCapRefusalMessage(targetAgent, tier, cap));
211
397
  bindAccount(name, target);
212
398
  console.log(chalk.green(`Attached ${account.name} to ${target}.`));
213
399
  });
@@ -220,25 +406,21 @@ export function registerAccountsCommand(program) {
220
406
  accounts.command('rename <old> <new>').description('Rename an account without changing its stable id').action((oldName, newName) => renameAccount(oldName, newName));
221
407
  accounts.command('remove <name>').description('Remove an account and its device-local credential').action((name) => removeAccount(name));
222
408
  accounts.command('set-default <agent> <name>')
223
- .description('Use a provider account for a harness when --account is omitted')
409
+ .description('Use this account for a harness when --account is omitted')
224
410
  .action((agentRaw, name) => {
225
- if (!ALL_AGENT_IDS.includes(agentRaw))
226
- throw new Error(`Unknown agent '${agentRaw}'.`);
227
- const agent = agentRaw;
228
- const account = findAccount(name);
229
- if (!account)
230
- throw new Error(`Unknown provider account '${name}'.`);
231
- getAccountProvider(account.provider).envFor(agent, account.auth);
232
- // Defaults follow the stable account id, so renaming the bundle cannot
233
- // strand bare runs on a deleted label. findAccount accepts ids and names.
234
- updateMeta(meta => ({ ...meta, accounts: { ...meta.accounts, defaults: { ...meta.accounts?.defaults, [agent]: account.id } } }));
411
+ const { agent, account } = setDefaultAccount(agentRaw, name);
235
412
  console.log(chalk.green(`${agent} now uses account '${account.name}' unless --account overrides it.`));
236
413
  });
414
+ const switchCmd = accounts.command('switch <harness> [account]')
415
+ .description('Pick the default account for a harness')
416
+ .option('--json', 'Machine-readable account list or the resulting default')
417
+ .action(async (harness, account, o, command) => {
418
+ await runAccountsSwitch(harness, account, { json: !!(o.json || command.optsWithGlobals().json) });
419
+ });
237
420
  accounts.command('clear-default <agent>')
238
421
  .description('Return a harness to native login or balanced account selection')
239
422
  .action((agentRaw) => {
240
- if (!ALL_AGENT_IDS.includes(agentRaw))
241
- throw new Error(`Unknown agent '${agentRaw}'.`);
423
+ parseHarness(agentRaw);
242
424
  updateMeta(meta => {
243
425
  const defaults = { ...meta.accounts?.defaults };
244
426
  delete defaults[agentRaw];
@@ -309,6 +491,13 @@ export function registerAccountsCommand(program) {
309
491
  throw new Error(`${result.message}\nRetry: agents accounts sync ${account.name} ${device}${o.force ? ' --force' : ''}`);
310
492
  console.log(chalk.green(`${account.name} synced to ${device} (${result.keyCount} keys, ${remoteBackend} backend, policy never).`));
311
493
  });
494
+ setHelpSections(switchCmd, {
495
+ examples: `agents accounts switch claude
496
+ agents accounts switch claude work
497
+ agents accounts switch claude --json
498
+ agents run claude`,
499
+ notes: 'Switch writes the existing per-harness default (same binding as set-default). Rotation already honors it. Pass an account name to skip the picker. Native name/attach is only for harnesses agents-cli can isolate (claude, codex, grok); provider add is unrestricted.',
500
+ });
312
501
  setHelpSections(accounts, {
313
502
  examples: `agents accounts add work --provider anthropic --auth setup-token
314
503
  agents accounts add openrouter-work --provider openrouter --auth api-key --from-secrets openrouter.ai:OPENROUTER_API_KEY
@@ -316,10 +505,12 @@ agents accounts set-key work
316
505
  agents accounts name claude@2.1.220 work
317
506
  agents accounts attach work claude@2.1.225
318
507
  agents accounts view work --json
508
+ agents accounts switch claude
509
+ agents accounts switch claude work
319
510
  agents accounts set-default claude work
320
511
  agents accounts sync openrouter-work yosemite-s0
321
512
  agents run claude --account work
322
513
  agents accounts logout claude`,
323
- notes: 'Native account records contain metadata only; harness-owned OAuth credentials are never copied. Provider accounts are explicit portable bundles with policy never. Harness-native OAuth sign-out is `agents accounts logout <harness>` (API-key accounts use `accounts remove`). Synced vault unlock is `agents secrets vault unlock`.',
514
+ notes: 'Native account records contain metadata only; harness-owned OAuth credentials are never copied. Provider accounts are explicit portable bundles with policy never. `accounts switch` is the fast picker over the same default `set-default` writes. Harness-native OAuth sign-out is `agents accounts logout <harness>` (API-key accounts use `accounts remove`). Synced vault unlock is `agents secrets vault unlock`.',
324
515
  });
325
516
  }
@@ -2,7 +2,7 @@ import chalk from 'chalk';
2
2
  import * as fs from 'fs';
3
3
  import * as path from 'path';
4
4
  import { getUserAgentsDir } from '../lib/state.js';
5
- import { getShimsDir } from '../lib/shims.js';
5
+ import { getShimsDir } from '../lib/installations/shims.js';
6
6
  import { ALL_AGENT_IDS, AGENTS } from '../lib/agents.js';
7
7
  const ALIASES_FILE = path.join(getUserAgentsDir(), 'aliases.json');
8
8
  /** Names that would clobber an agent CLI shim or the `agents` binary itself. */
@@ -0,0 +1,9 @@
1
+ import type { Command } from 'commander';
2
+ /**
3
+ * `agents auth login` — RFC 8628 device-code flow against the already-shipped
4
+ * `/api/v1/auth/device/*` routes. If the live API rejects the request shape
5
+ * (a route this spec missed, or the endpoint moved), this fails loud with the
6
+ * `rush login` fallback rather than silently stubbing a fake session.
7
+ */
8
+ export declare function runAuthLogin(): Promise<void>;
9
+ export declare function registerAuthCommand(program: Command): void;