@phnx-labs/agents-cli 1.22.40 → 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 (201) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +14 -3
  3. package/dist/bin/agents +0 -0
  4. package/dist/bootstrap.js +0 -1
  5. package/dist/cli/command-registry.d.ts +2 -0
  6. package/dist/cli/command-registry.js +5 -2
  7. package/dist/commands/accounts.d.ts +29 -0
  8. package/dist/commands/accounts.js +220 -29
  9. package/dist/commands/alias.js +1 -1
  10. package/dist/commands/auth.d.ts +9 -0
  11. package/dist/commands/auth.js +104 -0
  12. package/dist/commands/browser-sessions-picker.js +42 -62
  13. package/dist/commands/browser.d.ts +15 -0
  14. package/dist/commands/browser.js +145 -78
  15. package/dist/commands/computer-actions.d.ts +1 -1
  16. package/dist/commands/computer-actions.js +1 -1
  17. package/dist/commands/computer-sessions-picker.js +32 -55
  18. package/dist/commands/computer.d.ts +1 -1
  19. package/dist/commands/computer.js +2 -2
  20. package/dist/commands/doctor.d.ts +1 -1
  21. package/dist/commands/doctor.js +3 -3
  22. package/dist/commands/feed.js +4 -12
  23. package/dist/commands/hooks.js +1 -1
  24. package/dist/commands/import.js +1 -1
  25. package/dist/commands/insights.js +163 -85
  26. package/dist/commands/inspect.js +2 -2
  27. package/dist/commands/mine.js +1 -1
  28. package/dist/commands/org.d.ts +6 -0
  29. package/dist/commands/org.js +174 -0
  30. package/dist/commands/packages.js +1 -1
  31. package/dist/commands/prune.js +1 -1
  32. package/dist/commands/routines.js +1 -1
  33. package/dist/commands/run-account-picker.d.ts +19 -0
  34. package/dist/commands/run-account-picker.js +81 -0
  35. package/dist/commands/secrets.js +87 -97
  36. package/dist/commands/sessions-picker-factory.d.ts +40 -0
  37. package/dist/commands/sessions-picker-factory.js +67 -0
  38. package/dist/commands/sessions-picker.d.ts +1 -0
  39. package/dist/commands/sessions-picker.js +27 -5
  40. package/dist/commands/sessions-share.d.ts +25 -0
  41. package/dist/commands/sessions-share.js +166 -0
  42. package/dist/commands/sessions.js +2 -0
  43. package/dist/commands/setup-browser.js +5 -5
  44. package/dist/commands/setup-mine.js +1 -1
  45. package/dist/commands/setup-preferences.js +4 -4
  46. package/dist/commands/setup.js +5 -3
  47. package/dist/commands/sync.js +1 -1
  48. package/dist/commands/versions.js +1 -1
  49. package/dist/commands/view.js +1 -1
  50. package/dist/commands/webhook.js +14 -9
  51. package/dist/lib/account-capabilities.d.ts +10 -0
  52. package/dist/lib/account-capabilities.js +34 -0
  53. package/dist/lib/agent-spec/agents.d.ts +584 -0
  54. package/dist/lib/agent-spec/agents.js +3127 -0
  55. package/dist/lib/agent-spec/provider.js +1 -1
  56. package/dist/lib/agents.d.ts +2 -584
  57. package/dist/lib/agents.js +2 -3127
  58. package/dist/lib/browser/cdp.js +4 -0
  59. package/dist/lib/browser/chrome.js +12 -1
  60. package/dist/lib/browser/drivers/local.d.ts +8 -2
  61. package/dist/lib/browser/drivers/local.js +8 -2
  62. package/dist/lib/browser/drivers/ssh.d.ts +8 -2
  63. package/dist/lib/browser/drivers/ssh.js +11 -4
  64. package/dist/lib/browser/hygiene.d.ts +1 -1
  65. package/dist/lib/browser/hygiene.js +5 -1
  66. package/dist/lib/browser/profiles.d.ts +77 -4
  67. package/dist/lib/browser/profiles.js +121 -12
  68. package/dist/lib/browser/runtime-state.d.ts +11 -5
  69. package/dist/lib/browser/runtime-state.js +13 -9
  70. package/dist/lib/browser/service.d.ts +91 -30
  71. package/dist/lib/browser/service.js +342 -207
  72. package/dist/lib/browser/types.d.ts +96 -4
  73. package/dist/lib/browser/types.js +51 -0
  74. package/dist/lib/catchup.js +1 -1
  75. package/dist/lib/computer/actions.d.ts +1 -1
  76. package/dist/lib/computer/actions.js +1 -1
  77. package/dist/lib/{computer-rpc.js → computer/computer-rpc.js} +5 -5
  78. package/dist/lib/computer/dispatch.d.ts +1 -1
  79. package/dist/lib/computer/download.d.ts +1 -1
  80. package/dist/lib/computer/download.js +3 -3
  81. package/dist/lib/{ssh-tunnel.d.ts → computer/ssh-tunnel.d.ts} +1 -1
  82. package/dist/lib/{ssh-tunnel.js → computer/ssh-tunnel.js} +12 -12
  83. package/dist/lib/daemon/daemon.js +1 -1
  84. package/dist/lib/{runner.d.ts → daemon/runner.d.ts} +10 -10
  85. package/dist/lib/{runner.js → daemon/runner.js} +59 -118
  86. package/dist/lib/daemon-webhooks.d.ts +3 -1
  87. package/dist/lib/daemon-webhooks.js +12 -7
  88. package/dist/lib/device-config.js +1 -1
  89. package/dist/lib/devices/doctor-findings.d.ts +1 -1
  90. package/dist/lib/devices/doctor-findings.js +3 -3
  91. package/dist/lib/devices/fleet-inventory.js +1 -1
  92. package/dist/lib/devices/harness-inventory.js +1 -1
  93. package/dist/lib/devices/registry.js +4 -22
  94. package/dist/lib/doctor-diff.d.ts +1 -1
  95. package/dist/lib/doctor-diff.js +1 -1
  96. package/dist/lib/drift.js +1 -1
  97. package/dist/lib/entitlement.d.ts +31 -0
  98. package/dist/lib/entitlement.js +137 -0
  99. package/dist/lib/exec.js +39 -202
  100. package/dist/lib/feed/feed.js +2 -8
  101. package/dist/lib/fs-atomic.d.ts +7 -0
  102. package/dist/lib/fs-atomic.js +9 -0
  103. package/dist/lib/harness/adapter.d.ts +160 -0
  104. package/dist/lib/harness/adapter.js +47 -0
  105. package/dist/lib/harness/adapters/claude.d.ts +2 -0
  106. package/dist/lib/harness/adapters/claude.js +110 -0
  107. package/dist/lib/harness/adapters/codex.d.ts +2 -0
  108. package/dist/lib/harness/adapters/codex.js +83 -0
  109. package/dist/lib/harness/adapters/copilot.d.ts +2 -0
  110. package/dist/lib/harness/adapters/copilot.js +23 -0
  111. package/dist/lib/harness/adapters/cursor.d.ts +2 -0
  112. package/dist/lib/harness/adapters/cursor.js +38 -0
  113. package/dist/lib/harness/adapters/droid.d.ts +2 -0
  114. package/dist/lib/harness/adapters/droid.js +17 -0
  115. package/dist/lib/harness/adapters/grok.d.ts +2 -0
  116. package/dist/lib/harness/adapters/grok.js +14 -0
  117. package/dist/lib/harness/adapters/kimi.d.ts +2 -0
  118. package/dist/lib/harness/adapters/kimi.js +49 -0
  119. package/dist/lib/harness/adapters/muse.d.ts +2 -0
  120. package/dist/lib/harness/adapters/muse.js +44 -0
  121. package/dist/lib/harness/adapters/opencode.d.ts +2 -0
  122. package/dist/lib/harness/adapters/opencode.js +14 -0
  123. package/dist/lib/harness/exec-config-version.d.ts +8 -0
  124. package/dist/lib/harness/exec-config-version.js +22 -0
  125. package/dist/lib/harness/index.d.ts +1 -0
  126. package/dist/lib/harness/index.js +34 -0
  127. package/dist/lib/{hooks.d.ts → hooks/install.d.ts} +1 -1
  128. package/dist/lib/{hooks.js → hooks/install.js} +11 -11
  129. package/dist/lib/import.js +1 -1
  130. package/dist/lib/installations/index.d.ts +4 -4
  131. package/dist/lib/installations/index.js +3 -4
  132. package/dist/lib/installations/migrate.js +6 -6
  133. package/dist/lib/{shims.d.ts → installations/shims.d.ts} +2 -2
  134. package/dist/lib/{shims.js → installations/shims.js} +12 -114
  135. package/dist/lib/installations/store.d.ts +141 -3
  136. package/dist/lib/installations/store.js +475 -5
  137. package/dist/lib/installations/versions.d.ts +1 -126
  138. package/dist/lib/installations/versions.js +5 -465
  139. package/dist/lib/isolation-boundary-report.d.ts +1 -1
  140. package/dist/lib/isolation-boundary-report.js +1 -1
  141. package/dist/lib/mailbox-gc.js +2 -3
  142. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  143. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  144. package/dist/lib/monitors/dispatch.d.ts +1 -1
  145. package/dist/lib/monitors/dispatch.js +2 -2
  146. package/dist/lib/monitors/state.d.ts +1 -1
  147. package/dist/lib/observe-aliases.d.ts +2 -2
  148. package/dist/lib/observe-aliases.js +2 -11
  149. package/dist/lib/plugins/plugins.js +1 -1
  150. package/dist/lib/plugins/skills.js +1 -1
  151. package/dist/lib/prix-account.d.ts +158 -0
  152. package/dist/lib/prix-account.js +214 -0
  153. package/dist/lib/project-key.js +7 -0
  154. package/dist/lib/project-launch.js +1 -1
  155. package/dist/lib/refresh.js +2 -2
  156. package/dist/lib/resource-inventory.d.ts +1 -1
  157. package/dist/lib/resource-inventory.js +1 -1
  158. package/dist/lib/rules/rules.js +1 -1
  159. package/dist/lib/scheduling/routines.js +1 -1
  160. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  161. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  162. package/dist/lib/secrets/bundles.d.ts +1 -1
  163. package/dist/lib/secrets/bundles.js +1 -1
  164. package/dist/lib/secrets/headless.d.ts +16 -0
  165. package/dist/lib/secrets/headless.js +21 -0
  166. package/dist/lib/secrets/remote.d.ts +9 -7
  167. package/dist/lib/secrets/remote.js +18 -9
  168. package/dist/lib/self-heal/checks/hook-runtime.js +1 -1
  169. package/dist/lib/self-heal/checks/path.js +1 -1
  170. package/dist/lib/self-heal/checks/shadowing.js +1 -1
  171. package/dist/lib/self-heal/checks/shims.js +1 -1
  172. package/dist/lib/session/discover.d.ts +39 -0
  173. package/dist/lib/session/discover.js +3 -3
  174. package/dist/lib/session/parse.js +24 -42
  175. package/dist/lib/session/recovery.js +1 -1
  176. package/dist/lib/session/share-html.d.ts +55 -0
  177. package/dist/lib/session/share-html.js +319 -0
  178. package/dist/lib/settings-manifest.d.ts +2 -0
  179. package/dist/lib/settings-manifest.js +81 -3
  180. package/dist/lib/share/publish.d.ts +38 -0
  181. package/dist/lib/share/publish.js +81 -8
  182. package/dist/lib/staleness/prune.d.ts +1 -1
  183. package/dist/lib/staleness/prune.js +1 -1
  184. package/dist/lib/staleness/writers/hooks.js +1 -1
  185. package/dist/lib/startup/command-registry.d.ts +2 -1
  186. package/dist/lib/startup/command-registry.js +5 -3
  187. package/dist/lib/teams/agents.js +2 -19
  188. package/dist/lib/teams/parsers.js +20 -26
  189. package/dist/lib/teams/registry.js +2 -21
  190. package/dist/lib/triggers/handlers.d.ts +37 -2
  191. package/dist/lib/triggers/handlers.js +57 -6
  192. package/dist/lib/triggers/webhook.d.ts +80 -6
  193. package/dist/lib/triggers/webhook.js +128 -4
  194. package/dist/lib/types.d.ts +1 -1
  195. package/dist/lib/uninstall.js +1 -1
  196. package/dist/lib/version.d.ts +9 -2
  197. package/dist/lib/version.js +11 -9
  198. package/dist/lib/wrap.d.ts +33 -0
  199. package/dist/lib/wrap.js +70 -0
  200. package/package.json +1 -1
  201. /package/dist/lib/{computer-rpc.d.ts → computer/computer-rpc.d.ts} +0 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,41 @@
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
+
17
+ ## 1.22.41
18
+
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`.
20
+
21
+ - **Session preview leads with the title and wraps the last response to the pane (RUSH-2757).** `agents sessions preview` now opens its header with the session's title (`session.label` — an agent-generated name / `/rename` / the `--name` launch handle) instead of burying it, and the agent's last response is wrapped to the terminal width rather than running off the right edge as one long line. Wrapping measures visible width (ANSI-aware), so coloured output is not miscounted, and lines that already fit keep their rendered-markdown indentation. Source: `apps/cli/src/commands/sessions-picker.ts`, `apps/cli/src/lib/wrap.ts`.
22
+
23
+ - **`release-manifest.sh` helper digests are keyed by path relative to `--repo-root`, and `release-attestation-produce.sh` now writes the helper manifest it needs (RUSH-2766).** `hash_tree` used to bake the ABSOLUTE file path into every helper's `input-digest`, so a digest recorded in one worktree could never match the same recomputation elsewhere — `require_helpers` structurally could not pass for any release, and v1.22.40 was finished only by hand-recomputing the manifest inside a fixed-path worktree on `mac-mini` and re-uploading it to the GitHub release. Paths are now hashed relative to the repo root, so the same digest reproduces across machines and worktrees (demonstrated: identical `sha256:` for all three helpers computed from two independent checkouts). Separately, `release.sh` has always consumed `release-manifest.json` (`require` at :231, `upload_release_proof` at :976) with no producer writing it — the same consumer-without-producer gap RUSH-2749 fixed for the attestation itself. `release-attestation-produce.sh` now writes/updates it alongside `ATTEST.json`: an unchanged helper carries its prior record forward, `keychain`/`menubar` are re-recorded from the fresh signed assets this script's own Darwin block builds, and a drifted `computer-mac` digest with no prior record fails the producer closed rather than shipping a stale or missing helper record. Source: `apps/cli/scripts/release-manifest.sh`, `apps/cli/scripts/release-attestation-produce.sh`, `apps/cli/docs/release.md`.
24
+
25
+ - **BREAKING: `agents secrets` no longer prints bundle values — the plaintext export and bundle-key `get` are removed; run commands under `agents secrets exec` instead (RUSH-2774).** The `eval "$(agents secrets export <bundle> --plaintext)"` one-liner put every value of a bundle onto stdout, which inside a coding-agent session means the model's context and the synced session transcript — and agents copied it reflexively from the CLI's own help and scripts. `secrets export` is now transfer-only (`--device`, `--to-1password`, `--to-file`; anything else refuses, naming the alternatives); `secrets get <bundle> <KEY>` refuses, naming `agents secrets exec <bundle> -- printenv <KEY>`; `secrets view --reveal` refuses inside an agent session (`AGENTS_RUNTIME`/`AGENT_SESSION_ID`/`CLAUDECODE` markers) and its non-TTY `--reveal --plaintext` escape is gone; the raw-item `secrets get <item>` stays available everywhere — fleet shell hooks running inside sessions capture single ad-hoc tokens into their own variables, the accepted narrower residual. The SSH remote resolve behind `secrets exec --device` and `run --secrets <b>@<host>` still works: it rides a hidden, marker-gated transport (`AGENTS_SECRETS_REMOTE_TRANSPORT`), so a newer CLI keeps resolving from an older remote during a fleet rollout, while an older CLI against a newer remote fails loud with an upgrade hint. First-party scripts (`sandbox.sh`, both `release.sh`) now re-enter themselves under `agents secrets exec`, so values ride the child environment and never touch stdout. Migration for eval scripts: `agents secrets exec <bundle> -- <your command>`, or `VAR="$(agents secrets exec <bundle> -- printenv KEY)"` for a single value. Spec: SEC-9/SEC-9b/SEC-9c, SEC-GAP-10. Source: `apps/cli/src/commands/secrets.ts`, `apps/cli/src/lib/secrets/{headless,remote}.ts`, `apps/cli/scripts/{sandbox,release}.sh`, `apps/ext/scripts/release.sh`.
26
+
27
+ - **Fix a publish crash on any non-latin1 label, meta value, or repo name (RUSH-2784).** `fetch` encodes header values as a ByteString, so a title containing an ellipsis, a curly quote, an emoji, or CJK text threw `TypeError: Cannot convert argument to a ByteString` mid-publish — an unhandled stack trace after the body had already been read and the OG cover uploaded. Every free-text header (`x-share-label`, `x-share-repo`, `x-share-host`, `x-share-meta`, and the provenance set) now goes through `toHeaderValue()`, which transliterates common typographic punctuation to ASCII and drops the rest of the non-latin1 range. Still lossy for a title in a non-latin script, which publishes as `(unnamed)`; full Unicode needs percent-encoding plus a matching Worker decode, tracked as RUSH-2786. Source: `apps/cli/src/lib/share/publish.ts`.
28
+
29
+ - **`agents sessions share <id>` publishes a session as a link (RUSH-2784).** Sharing a session took three commands and a detour through the external artifacts-cli, so nobody ran it — every piece already existed (`sessions render` produces a redacted transcript, and the R2-backed share Worker has served artifacts for a month), but nothing wired them together. One verb now renders the session as a self-contained branded HTML page and publishes it: `agents sessions share a1b2c3d4` prints `https://share.agents-cli.sh/<you>/session-a1b2c3d4`. **Unlisted by default**, unlike `agents artifacts share` — a transcript carries file paths, command output, and error text, so it stays out of the public gallery until `--public` asks for it (the URL is still world-readable: unlisted is a capability URL, not a secret). The page is escaped rather than passed through, so a session that merely printed a `<script>` tag does not ship an executable one. Publishing inherits the existing email/credential scan and the 30d default expiry; `--reasoning fold` keeps the model's reasoning in collapsible sections, `--expire never` opts out of decay. Source: `apps/cli/src/commands/sessions-share.ts`, `apps/cli/src/lib/session/share-html.ts`, `apps/cli/src/lib/share/publish.ts`.
30
+
31
+ - **`agents browser use [name]` is the one command for choosing this machine's default browser profile (RUSH-2820).** A name writes the existing device-scoped `browser.profile` config key; a bare interactive invocation opens a picker over configured profiles and installed browsers, while a headless invocation prints the current default and usage. `--unset` and `auto` restore auto-detect. `agents browser profiles use` is an alias, and the hidden `profiles set-default` compatibility alias now points users to `browser use`. Source: `apps/cli/src/commands/browser.ts`.
32
+
33
+ - **Claude workspace trust now carries across pinned version homes (#2776).** Every pinned claude version gets an isolated home, and workspace trust (`projects[<path>].hasTrustDialogAccepted` in `.claude.json`) never carried into a new one — so the first interactive launch of each newly pinned version re-showed the workspace-trust dialog once per project, complete with the "this folder pre-approves N tool permissions" warning on repos trusted for months (headless runs skip the dialog, so the re-prompt could surface weeks after pinning and read as a trust regression). `agents add` / `agents use` now carry trust via a new `claude-trust` manifest strategy that projects ONLY the accepted trust flags out of the source `.claude.json` — the login (`oauthAccount`) and per-session stats stay per-version, same posture as the removed `.codex/auth.json` carry. A target entry's stamped-default `false` is promoted (Claude Code never persists a decline), all its other keys are preserved, the target is backed up before modification, and the write is atomic (a running Claude session rewrites this exact file). Codex was already covered: `projects.*.trust_level` rides along in `config.toml`. Source: `apps/cli/src/lib/settings-manifest.ts`.
34
+
35
+ - **Arc is a recognized browser type, and `agents browser` now refuses to drive it with a clear error instead of crashing it (#2779, #2778).** `arc` joins the Chromium-family `BrowserType` (detected at `/Applications/Arc.app`, macOS-only; `agents browser profiles seed` creates an `arc-local` profile). But Arc answers `Browser.getVersion` while exposing **zero** CDP page targets and **crashing** the moment a new tab is requested (`Target.createTarget`), so it is not actually drivable. Every tab-creating path now routes through one guard (`createPageTarget`) that throws an actionable "use Comet/Chrome/Chromium/Brave" error for an Arc profile rather than crashing the user's Arc window. The browser skill documents this and teaches showing a review doc/plan/report via `agents browser navigate --url file://<path>` (one reused tab, refreshed in place) instead of a raw `open <file>` that spawns a duplicate tab every call. Source: `apps/cli/src/lib/browser/service.ts`, `apps/cli/src/lib/browser/chrome.ts`, `apps/cli/src/lib/browser/cdp.ts`, `skills/browser/SKILL.md`.
36
+
37
+ - **Slack can now trigger an agent on your box and reply in-thread.** The signed webhook receiver (`agents webhooks serve` and the daemon `webhook-receiver`) accepts a third source, `POST /hooks/slack`: it verifies Slack's `v0` request signature with a 5-minute replay guard, answers the one-time `url_verification` handshake, and parses both slash commands (`/agents AGI: …`) and `app_mention` events. A matching `~/.agents/webhooks/*.yml` handler with `source: slack` (and optional `command`/`channel` filters) runs an agent scoped to the project named in the message — a handler may now template `project`/`cwd` from the delivery (`project: "{{slack.project}}"`), with the new `{{slack.*}}` namespace exposing `prompt`, `project`, `channel`, `thread_ts`, `user`, `text`, and `command`. The agent replies into the same thread through the existing `agents send --channel slack --to <channel> --thread <ts>` — no new outbound code. The receiver bundle gains `SLACK_SIGNING_SECRET` (and `SLACK_BOT_TOKEN` when the reply uses the Slack Web API). Setup, a ready-to-paste app manifest, and an example handler are in `docs/routines.md` and `docs/examples/slack/`. Source: `apps/cli/src/lib/triggers/webhook.ts`, `apps/cli/src/lib/triggers/handlers.ts`, `apps/cli/src/lib/daemon-webhooks.ts`, `apps/cli/src/commands/webhook.ts`.
38
+
3
39
  ## 1.22.40
4
40
 
5
41
  ---
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
 
@@ -285,6 +289,9 @@ agents sessions a1b2c3d4 --markdown
285
289
  # Render a shareable, redacted Markdown transcript with the session preview on top
286
290
  agents sessions render a1b2c3d4 -o session.md
287
291
 
292
+ # Or publish it as a link in one step (unlisted + redacted by default)
293
+ agents sessions share a1b2c3d4
294
+
288
295
  # Just the last 3 turns, user messages only
289
296
  agents sessions a1b2c3d4 --last 3 --include user
290
297
 
@@ -369,6 +376,8 @@ Landing on a session cold? `agents sessions <id>` prints a catch-up digest: an i
369
376
 
370
377
  Sharing a session uses `agents sessions render <id> -o session.md`, not the raw harness JSONL. The document starts with that same preview, then presents user and assistant turns, fenced commands, structured tool arguments, and bounded tool output. Credential-shaped values and local home paths are redacted by default; `--no-redact` is for local-only inspection.
371
378
 
379
+ `agents sessions share <id>` goes one step further and publishes that document as a self-contained web page on your own share endpoint, printing the link. It is **unlisted** unless you pass `--public` — a transcript carries file paths, command output, and error text that a plan does not, so it stays out of your public gallery by default, and emails are masked on top of the render's own redaction. The slug is `session-<shortId>`, so re-sharing one session updates one URL.
380
+
372
381
  ### Resume anywhere — and stay resumed
373
382
 
374
383
  Pick up any past conversation and drop it back into a terminal:
@@ -1001,15 +1010,17 @@ agents accounts add gateway --provider openrouter --auth api-key \
1001
1010
  --from-secrets openrouter.ai:OPENROUTER_API_KEY # import from an existing secrets bundle
1002
1011
  agents accounts add deepinfra --provider deepinfra --auth api-key
1003
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
1004
1015
  agents accounts set-default claude work # claude uses `work` when --account is omitted
1005
1016
  agents accounts sync work --device yosemite-s0 # explicitly copy the bundle to a worker device
1006
1017
  agents run claude --account work
1007
1018
  agents harness add deepinfra --account deepinfra
1008
1019
  ```
1009
1020
 
1010
- 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.
1011
1022
 
1012
- 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.
1013
1024
 
1014
1025
  ---
1015
1026
 
package/dist/bin/agents CHANGED
Binary file
package/dist/bootstrap.js CHANGED
@@ -258,7 +258,6 @@ Run and dispatch:
258
258
 
259
259
  Observe (read the fleet — no store merge; aliases point at the real readers):
260
260
  feed / inbox Needs-you inbox (open blocks waiting on you)
261
- timeline Agent progress stream (= feed --filter updates)
262
261
  sessions --active Live agents (who is running right now)
263
262
  events Unified ops + activity event trail
264
263
  audit Tamper-evident run-dispatch log (not events)
@@ -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],
@@ -218,9 +222,8 @@ export const COMMAND_LOADERS = {
218
222
  send: [loadSend],
219
223
  notify: [loadSend],
220
224
  feed: [loadFeed],
221
- // Observe-umbrella aliases of feed / feed --filter updates.
225
+ // Observe-umbrella alias of feed (the `timeline` alias was removed in RUSH-2692).
222
226
  inbox: [loadFeed],
223
- timeline: [loadFeed],
224
227
  mailboxes: [loadMailboxes],
225
228
  mailbox: [loadMailboxes],
226
229
  serve: [loadServe],
@@ -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;