@phnx-labs/agents-cli 1.22.41 → 1.22.43

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 (253) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/README.md +10 -4
  3. package/dist/bin/agents +0 -0
  4. package/dist/bootstrap.js +25 -7
  5. package/dist/cli/command-registry.d.ts +2 -3
  6. package/dist/cli/command-registry.js +4 -8
  7. package/dist/commands/accounts.d.ts +29 -0
  8. package/dist/commands/accounts.js +220 -29
  9. package/dist/commands/alias.d.ts +7 -4
  10. package/dist/commands/alias.js +34 -9
  11. package/dist/commands/auth.d.ts +9 -0
  12. package/dist/commands/auth.js +108 -0
  13. package/dist/commands/browser-sessions-picker.js +42 -62
  14. package/dist/commands/browser.js +30 -38
  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 +22 -8
  20. package/dist/commands/daemon.js +1 -1
  21. package/dist/commands/doctor.d.ts +5 -3
  22. package/dist/commands/doctor.js +23 -8
  23. package/dist/commands/exec.js +3 -2
  24. package/dist/commands/feed.js +2 -31
  25. package/dist/commands/harness.js +3 -3
  26. package/dist/commands/hooks.js +1 -1
  27. package/dist/commands/import.js +1 -1
  28. package/dist/commands/insights.js +163 -85
  29. package/dist/commands/inspect.js +2 -2
  30. package/dist/commands/mine.js +1 -1
  31. package/dist/commands/monitors.js +1 -1
  32. package/dist/commands/org.d.ts +6 -0
  33. package/dist/commands/org.js +175 -0
  34. package/dist/commands/packages.js +1 -1
  35. package/dist/commands/profiles.js +2 -2
  36. package/dist/commands/prune.js +1 -1
  37. package/dist/commands/routines.js +44 -26
  38. package/dist/commands/run-account-picker.d.ts +19 -0
  39. package/dist/commands/run-account-picker.js +81 -0
  40. package/dist/commands/sessions-picker-factory.d.ts +40 -0
  41. package/dist/commands/sessions-picker-factory.js +67 -0
  42. package/dist/commands/sessions-picker.js +124 -122
  43. package/dist/commands/setup-browser.js +4 -4
  44. package/dist/commands/setup-mine.js +1 -1
  45. package/dist/commands/setup-preferences.js +2 -2
  46. package/dist/commands/setup.js +10 -4
  47. package/dist/commands/snapshot.d.ts +1 -1
  48. package/dist/commands/snapshot.js +4 -4
  49. package/dist/commands/status.d.ts +4 -2
  50. package/dist/commands/status.js +10 -7
  51. package/dist/commands/sync.d.ts +1 -0
  52. package/dist/commands/sync.js +30 -3
  53. package/dist/commands/versions.js +1 -1
  54. package/dist/commands/view.js +7 -1
  55. package/dist/commands/webhook.js +1 -1
  56. package/dist/lib/account-capabilities.d.ts +10 -0
  57. package/dist/lib/account-capabilities.js +34 -0
  58. package/dist/lib/accounting/rotate.d.ts +5 -3
  59. package/dist/lib/accounting/rotate.js +36 -7
  60. package/dist/lib/accounting/usage.d.ts +18 -4
  61. package/dist/lib/accounting/usage.js +138 -27
  62. package/dist/lib/agent-spec/agents.d.ts +584 -0
  63. package/dist/lib/agent-spec/agents.js +3127 -0
  64. package/dist/lib/agent-spec/provider.js +1 -1
  65. package/dist/lib/agents.d.ts +2 -584
  66. package/dist/lib/agents.js +2 -3127
  67. package/dist/lib/browser/drivers/local.d.ts +8 -2
  68. package/dist/lib/browser/drivers/local.js +8 -2
  69. package/dist/lib/browser/drivers/ssh.d.ts +8 -2
  70. package/dist/lib/browser/drivers/ssh.js +10 -4
  71. package/dist/lib/browser/hygiene.d.ts +1 -1
  72. package/dist/lib/browser/hygiene.js +5 -1
  73. package/dist/lib/browser/profiles.d.ts +74 -1
  74. package/dist/lib/browser/profiles.js +114 -5
  75. package/dist/lib/browser/runtime-state.d.ts +11 -5
  76. package/dist/lib/browser/runtime-state.js +13 -9
  77. package/dist/lib/browser/service.d.ts +68 -30
  78. package/dist/lib/browser/service.js +305 -194
  79. package/dist/lib/browser/types.d.ts +95 -3
  80. package/dist/lib/browser/types.js +51 -0
  81. package/dist/lib/catchup.js +1 -1
  82. package/dist/lib/cloud/rush.d.ts +0 -15
  83. package/dist/lib/cloud/rush.js +0 -35
  84. package/dist/lib/computer/actions.d.ts +1 -1
  85. package/dist/lib/computer/actions.js +1 -1
  86. package/dist/lib/{computer-rpc.js → computer/computer-rpc.js} +5 -5
  87. package/dist/lib/computer/dispatch.d.ts +1 -1
  88. package/dist/lib/computer/download.d.ts +1 -1
  89. package/dist/lib/computer/download.js +3 -3
  90. package/dist/lib/{ssh-tunnel.d.ts → computer/ssh-tunnel.d.ts} +1 -1
  91. package/dist/lib/{ssh-tunnel.js → computer/ssh-tunnel.js} +12 -12
  92. package/dist/lib/crabbox/runtimes.d.ts +6 -2
  93. package/dist/lib/crabbox/runtimes.js +21 -5
  94. package/dist/lib/daemon/daemon.js +87 -59
  95. package/dist/lib/{runner.d.ts → daemon/runner.d.ts} +28 -11
  96. package/dist/lib/{runner.js → daemon/runner.js} +143 -123
  97. package/dist/lib/devices/doctor-findings.d.ts +1 -1
  98. package/dist/lib/devices/doctor-findings.js +4 -4
  99. package/dist/lib/devices/fleet-inventory.js +1 -1
  100. package/dist/lib/devices/harness-inventory.js +1 -1
  101. package/dist/lib/devices/health-report.js +1 -1
  102. package/dist/lib/devices/registry.js +4 -22
  103. package/dist/lib/doctor-diff.d.ts +1 -1
  104. package/dist/lib/doctor-diff.js +1 -1
  105. package/dist/lib/drift-sync.d.ts +1 -1
  106. package/dist/lib/drift-sync.js +2 -2
  107. package/dist/lib/drift.d.ts +1 -1
  108. package/dist/lib/drift.js +1 -1
  109. package/dist/lib/entitlement.d.ts +31 -0
  110. package/dist/lib/entitlement.js +137 -0
  111. package/dist/lib/exec.js +52 -205
  112. package/dist/lib/feed/feed.js +2 -8
  113. package/dist/lib/format.d.ts +9 -0
  114. package/dist/lib/format.js +16 -0
  115. package/dist/lib/fs-atomic.d.ts +7 -0
  116. package/dist/lib/fs-atomic.js +9 -0
  117. package/dist/lib/github/pr-mergeable.d.ts +49 -0
  118. package/dist/lib/github/pr-mergeable.js +157 -0
  119. package/dist/lib/github/pr-verdict.d.ts +67 -0
  120. package/dist/lib/github/pr-verdict.js +72 -0
  121. package/dist/lib/harness/adapter.d.ts +161 -0
  122. package/dist/lib/harness/adapter.js +47 -0
  123. package/dist/lib/harness/adapters/claude.d.ts +2 -0
  124. package/dist/lib/harness/adapters/claude.js +110 -0
  125. package/dist/lib/harness/adapters/codex.d.ts +2 -0
  126. package/dist/lib/harness/adapters/codex.js +83 -0
  127. package/dist/lib/harness/adapters/copilot.d.ts +2 -0
  128. package/dist/lib/harness/adapters/copilot.js +23 -0
  129. package/dist/lib/harness/adapters/cursor.d.ts +2 -0
  130. package/dist/lib/harness/adapters/cursor.js +38 -0
  131. package/dist/lib/harness/adapters/droid.d.ts +2 -0
  132. package/dist/lib/harness/adapters/droid.js +17 -0
  133. package/dist/lib/harness/adapters/grok.d.ts +2 -0
  134. package/dist/lib/harness/adapters/grok.js +14 -0
  135. package/dist/lib/harness/adapters/kimi.d.ts +2 -0
  136. package/dist/lib/harness/adapters/kimi.js +49 -0
  137. package/dist/lib/harness/adapters/muse.d.ts +2 -0
  138. package/dist/lib/harness/adapters/muse.js +44 -0
  139. package/dist/lib/harness/adapters/opencode.d.ts +2 -0
  140. package/dist/lib/harness/adapters/opencode.js +14 -0
  141. package/dist/lib/harness/exec-config-version.d.ts +8 -0
  142. package/dist/lib/harness/exec-config-version.js +22 -0
  143. package/dist/lib/harness/index.d.ts +1 -0
  144. package/dist/lib/harness/index.js +34 -0
  145. package/dist/lib/{hooks.d.ts → hooks/install.d.ts} +1 -1
  146. package/dist/lib/{hooks.js → hooks/install.js} +11 -11
  147. package/dist/lib/hosts/passthrough.d.ts +10 -0
  148. package/dist/lib/hosts/passthrough.js +28 -6
  149. package/dist/lib/hosts/session-index.js +1 -1
  150. package/dist/lib/import.js +1 -1
  151. package/dist/lib/installations/index.d.ts +4 -4
  152. package/dist/lib/installations/index.js +3 -4
  153. package/dist/lib/installations/migrate.js +6 -6
  154. package/dist/lib/{shims.d.ts → installations/shims.d.ts} +2 -2
  155. package/dist/lib/{shims.js → installations/shims.js} +13 -115
  156. package/dist/lib/installations/store.d.ts +141 -3
  157. package/dist/lib/installations/store.js +475 -5
  158. package/dist/lib/installations/versions.d.ts +40 -125
  159. package/dist/lib/installations/versions.js +57 -462
  160. package/dist/lib/isolation-boundary-report.d.ts +1 -1
  161. package/dist/lib/isolation-boundary-report.js +1 -1
  162. package/dist/lib/mailbox-gc.js +2 -3
  163. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  164. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  165. package/dist/lib/menubar/install-menubar.js +19 -8
  166. package/dist/lib/monitors/config.d.ts +2 -2
  167. package/dist/lib/monitors/config.js +3 -2
  168. package/dist/lib/monitors/dispatch.d.ts +1 -1
  169. package/dist/lib/monitors/dispatch.js +2 -2
  170. package/dist/lib/monitors/state.d.ts +1 -1
  171. package/dist/lib/origin-machine.d.ts +18 -0
  172. package/dist/lib/origin-machine.js +34 -0
  173. package/dist/lib/perf/db.js +1 -1
  174. package/dist/lib/perf/spool.js +1 -1
  175. package/dist/lib/plugins/plugins.js +1 -1
  176. package/dist/lib/plugins/skills.js +1 -1
  177. package/dist/lib/prix-account.d.ts +158 -0
  178. package/dist/lib/prix-account.js +214 -0
  179. package/dist/lib/profiles.d.ts +7 -0
  180. package/dist/lib/profiles.js +26 -1
  181. package/dist/lib/project-key.js +2 -2
  182. package/dist/lib/project-launch.js +1 -1
  183. package/dist/lib/refresh.js +2 -2
  184. package/dist/lib/resource-inventory.d.ts +1 -1
  185. package/dist/lib/resource-inventory.js +1 -1
  186. package/dist/lib/rules/rules.js +1 -1
  187. package/dist/lib/sandbox.d.ts +24 -0
  188. package/dist/lib/sandbox.js +101 -0
  189. package/dist/lib/scheduling/routines.d.ts +18 -9
  190. package/dist/lib/scheduling/routines.js +17 -9
  191. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  192. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  193. package/dist/lib/secrets/agent.js +9 -1
  194. package/dist/lib/secrets/index.js +1 -1
  195. package/dist/lib/secrets/list-filter.js +1 -1
  196. package/dist/lib/self-heal/checks/hook-runtime.js +1 -1
  197. package/dist/lib/self-heal/checks/path.js +1 -1
  198. package/dist/lib/self-heal/checks/shadowing.js +1 -1
  199. package/dist/lib/self-heal/checks/shims.js +1 -1
  200. package/dist/lib/service-manifest.d.ts +20 -0
  201. package/dist/lib/service-manifest.js +34 -0
  202. package/dist/lib/session/cloud.js +1 -1
  203. package/dist/lib/session/db.js +1 -1
  204. package/dist/lib/session/discover.d.ts +39 -0
  205. package/dist/lib/session/discover.js +3 -3
  206. package/dist/lib/session/fork.js +1 -1
  207. package/dist/lib/session/live-metadata.js +1 -1
  208. package/dist/lib/session/origin-machine.d.ts +1 -18
  209. package/dist/lib/session/origin-machine.js +4 -34
  210. package/dist/lib/session/parse.js +24 -42
  211. package/dist/lib/session/recovery.js +1 -1
  212. package/dist/lib/session/relative-time.d.ts +1 -44
  213. package/dist/lib/session/relative-time.js +4 -120
  214. package/dist/lib/session/remote-list.js +1 -1
  215. package/dist/lib/session/remote.js +2 -2
  216. package/dist/lib/session/short-id.d.ts +1 -17
  217. package/dist/lib/session/short-id.js +4 -20
  218. package/dist/lib/session/width.d.ts +1 -29
  219. package/dist/lib/session/width.js +4 -101
  220. package/dist/lib/snapshot.d.ts +2 -2
  221. package/dist/lib/snapshot.js +1 -1
  222. package/dist/lib/staleness/prune.d.ts +1 -1
  223. package/dist/lib/staleness/prune.js +1 -1
  224. package/dist/lib/staleness/writers/hooks.js +1 -1
  225. package/dist/lib/startup/command-registry.d.ts +4 -1
  226. package/dist/lib/startup/command-registry.js +12 -5
  227. package/dist/lib/teams/agents.js +2 -19
  228. package/dist/lib/teams/parsers.js +20 -26
  229. package/dist/lib/teams/placement-probe.js +1 -1
  230. package/dist/lib/teams/registry.js +2 -21
  231. package/dist/lib/teams/scheduler.js +1 -1
  232. package/dist/lib/text/relative-time.d.ts +44 -0
  233. package/dist/lib/text/relative-time.js +120 -0
  234. package/dist/lib/text/short-id.d.ts +17 -0
  235. package/dist/lib/text/short-id.js +20 -0
  236. package/dist/lib/text/width.d.ts +29 -0
  237. package/dist/lib/text/width.js +101 -0
  238. package/dist/lib/triggers/handlers.d.ts +7 -0
  239. package/dist/lib/triggers/handlers.js +9 -1
  240. package/dist/lib/triggers/webhook.d.ts +1 -0
  241. package/dist/lib/triggers/webhook.js +3 -2
  242. package/dist/lib/uninstall.js +1 -1
  243. package/dist/lib/version.d.ts +9 -2
  244. package/dist/lib/version.js +11 -9
  245. package/dist/lib/view-types.d.ts +4 -0
  246. package/package.json +1 -1
  247. package/dist/commands/tickets.d.ts +0 -2
  248. package/dist/commands/tickets.js +0 -43
  249. package/dist/lib/observe-aliases.d.ts +0 -30
  250. package/dist/lib/observe-aliases.js +0 -38
  251. package/dist/lib/tickets/list.d.ts +0 -53
  252. package/dist/lib/tickets/list.js +0 -153
  253. /package/dist/lib/{computer-rpc.d.ts → computer/computer-rpc.d.ts} +0 -0
@@ -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
  }
@@ -1,11 +1,14 @@
1
1
  /**
2
- * Alias command -- create top-level shim binaries that expand to `agents <subcommand>`.
2
+ * `agents setup alias` -- create top-level shim binaries that expand to
3
+ * `agents <subcommand>`. Nested under setup because the user is configuring
4
+ * this machine's PATH shortcuts, not operating a separate noun (RUSH-2965).
3
5
  *
4
- * Generates executable scripts in ~/.agents/shims/ (already on PATH from the
5
- * standard agents-cli install). Running `teams` becomes equivalent to
6
+ * Generates executable scripts in ~/.agents/.cache/shims/ (already on PATH from
7
+ * the standard agents-cli install). Running `teams` becomes equivalent to
6
8
  * `agents teams`. Aliases are tracked in ~/.agents/aliases.json so we can
7
9
  * list, regenerate, or remove them without scanning the shims directory
8
10
  * (which also holds agent CLI shims like claude/codex/gemini).
9
11
  */
10
12
  import type { Command } from 'commander';
11
- export declare function registerAliasCommand(program: Command): void;
13
+ /** Register `agents setup alias` under the parent `setup` command. */
14
+ export declare function registerAliasCommand(setupCmd: Command): void;
@@ -2,9 +2,12 @@ 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
- const ALIASES_FILE = path.join(getUserAgentsDir(), 'aliases.json');
7
+ import { setHelpSections } from '../lib/help.js';
8
+ function aliasesFile() {
9
+ return path.join(getUserAgentsDir(), 'aliases.json');
10
+ }
8
11
  /** Names that would clobber an agent CLI shim or the `agents` binary itself. */
9
12
  function reservedNames() {
10
13
  const reserved = new Set(['agents']);
@@ -13,10 +16,11 @@ function reservedNames() {
13
16
  return reserved;
14
17
  }
15
18
  function readAliases() {
16
- if (!fs.existsSync(ALIASES_FILE))
19
+ const file = aliasesFile();
20
+ if (!fs.existsSync(file))
17
21
  return {};
18
22
  try {
19
- const parsed = JSON.parse(fs.readFileSync(ALIASES_FILE, 'utf-8'));
23
+ const parsed = JSON.parse(fs.readFileSync(file, 'utf-8'));
20
24
  return parsed && typeof parsed === 'object' ? parsed : {};
21
25
  }
22
26
  catch {
@@ -24,8 +28,9 @@ function readAliases() {
24
28
  }
25
29
  }
26
30
  function writeAliases(map) {
27
- fs.mkdirSync(path.dirname(ALIASES_FILE), { recursive: true });
28
- fs.writeFileSync(ALIASES_FILE, JSON.stringify(map, null, 2) + '\n', 'utf-8');
31
+ const file = aliasesFile();
32
+ fs.mkdirSync(path.dirname(file), { recursive: true });
33
+ fs.writeFileSync(file, JSON.stringify(map, null, 2) + '\n', 'utf-8');
29
34
  }
30
35
  function shimPath(name) {
31
36
  return path.join(getShimsDir(), name);
@@ -46,10 +51,30 @@ function isAliasShim(filePath) {
46
51
  return false;
47
52
  }
48
53
  }
49
- export function registerAliasCommand(program) {
50
- const cmd = program
54
+ /** Register `agents setup alias` under the parent `setup` command. */
55
+ export function registerAliasCommand(setupCmd) {
56
+ const cmd = setupCmd
51
57
  .command('alias')
52
58
  .description('Create shorthand binaries that expand to `agents <subcommand>`');
59
+ setHelpSections(cmd, {
60
+ examples: `
61
+ # Make \`teams\` run \`agents teams\`
62
+ agents setup alias add teams
63
+
64
+ # Shorthand for a longer expansion
65
+ agents setup alias add sess sessions preview
66
+
67
+ agents setup alias list
68
+ agents setup alias remove teams
69
+ `,
70
+ notes: `
71
+ Writes an executable shim into ~/.agents/.cache/shims/ (already on PATH after
72
+ \`agents setup\`) and records it in ~/.agents/aliases.json.
73
+
74
+ Names that collide with the agents binary or an agent CLI (claude, codex,
75
+ grok, …) are refused. Top-level \`agents alias\` is gone — use this group.
76
+ `,
77
+ });
53
78
  cmd
54
79
  .command('add <name> [expansion...]')
55
80
  .description('Create an alias shim. If expansion is omitted, defaults to <name>.')
@@ -106,7 +131,7 @@ export function registerAliasCommand(program) {
106
131
  const map = readAliases();
107
132
  const entries = Object.entries(map);
108
133
  if (entries.length === 0) {
109
- console.log(chalk.gray('No aliases. Try: agents alias add teams'));
134
+ console.log(chalk.gray('No aliases. Try: agents setup alias add teams'));
110
135
  return;
111
136
  }
112
137
  const width = entries.reduce((max, [n]) => Math.max(max, n.length), 0);
@@ -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;
@@ -0,0 +1,108 @@
1
+ import chalk from 'chalk';
2
+ import { setHelpSections } from '../lib/help.js';
3
+ import { runOrDie } from '../lib/format.js';
4
+ import { clearPrixSession, fetchWhoAmI, pollDeviceToken, PrixApiError, readPrixSession, resolvePrixToken, startDeviceAuthorization, writePrixSession, } from '../lib/prix-account.js';
5
+ function sleep(ms) {
6
+ return new Promise(resolve => setTimeout(resolve, ms));
7
+ }
8
+ /**
9
+ * `agents auth login` — RFC 8628 device-code flow against the already-shipped
10
+ * `/api/v1/auth/device/*` routes. If the live API rejects the request shape
11
+ * (a route this spec missed, or the endpoint moved), this fails loud with the
12
+ * `rush login` fallback rather than silently stubbing a fake session.
13
+ */
14
+ export async function runAuthLogin() {
15
+ let authorization;
16
+ try {
17
+ authorization = await startDeviceAuthorization();
18
+ }
19
+ catch (err) {
20
+ const message = err instanceof Error ? err.message : String(err);
21
+ const existing = resolvePrixToken();
22
+ if (existing?.source === 'rush') {
23
+ console.log(chalk.yellow(`Device login is unavailable right now (${message}), but you're already signed in via 'rush login'.`));
24
+ console.log("Run 'agents auth whoami' to confirm.");
25
+ return;
26
+ }
27
+ throw new Error(`Device login is unavailable right now (${message}). Run 'rush login' instead, then 'agents auth whoami' to confirm.`);
28
+ }
29
+ console.log(`To sign in, open:\n\n ${chalk.cyan(authorization.verification_uri_complete)}\n`);
30
+ console.log(chalk.gray(`(code: ${authorization.user_code})`));
31
+ const deadline = Date.now() + authorization.expires_in * 1000;
32
+ let interval = Math.max(authorization.interval, 1) * 1000;
33
+ while (Date.now() < deadline) {
34
+ await sleep(interval);
35
+ const poll = await pollDeviceToken(authorization.device_code);
36
+ if (poll.status === 'pending')
37
+ continue;
38
+ if (poll.status === 'slow_down') {
39
+ interval += 1000;
40
+ continue;
41
+ }
42
+ if (poll.status === 'expired')
43
+ throw new Error('Login code expired. Run `agents auth login` again.');
44
+ if (poll.status === 'denied')
45
+ throw new Error('Login was denied.');
46
+ writePrixSession({
47
+ access_token: poll.access_token,
48
+ refresh_token: poll.refresh_token,
49
+ expires_at: poll.expires_in ? Date.now() + poll.expires_in * 1000 : undefined,
50
+ email: poll.user.email,
51
+ userId: poll.user.id,
52
+ });
53
+ console.log(chalk.green(`Signed in as ${poll.user.email}.`));
54
+ return;
55
+ }
56
+ throw new Error('Login code expired. Run `agents auth login` again.');
57
+ }
58
+ async function runAuthWhoami(json) {
59
+ const resolved = resolvePrixToken();
60
+ if (!resolved)
61
+ throw new Error("Not signed in. Run 'agents auth login' first.");
62
+ let who;
63
+ try {
64
+ who = await fetchWhoAmI(resolved.token);
65
+ }
66
+ catch (err) {
67
+ if (err instanceof PrixApiError && err.status === 401) {
68
+ throw new Error(`Signed-in token is no longer valid (${resolved.source === 'agents' ? "run 'agents auth login'" : "run 'rush login'"} again).`);
69
+ }
70
+ throw err;
71
+ }
72
+ if (json) {
73
+ console.log(JSON.stringify({ ...who, source: resolved.source }, null, 2));
74
+ return;
75
+ }
76
+ console.log(`${chalk.bold(who.email)} ${chalk.gray(who.userId)}`);
77
+ console.log(chalk.gray(`signed in via ${resolved.source === 'agents' ? "'agents auth login'" : "'rush login' (shared session)"}`));
78
+ }
79
+ function runAuthLogout() {
80
+ const cleared = clearPrixSession();
81
+ const rushRemains = resolvePrixToken()?.source === 'rush';
82
+ if (!cleared && !readPrixSession()) {
83
+ console.log(chalk.gray('Not signed in via `agents auth login` — nothing to clear.'));
84
+ }
85
+ else {
86
+ console.log(chalk.green('Signed out.'));
87
+ }
88
+ if (rushRemains) {
89
+ console.log(chalk.gray("You're still signed in via `rush login` — `agents auth whoami` will use that session. Run `rush logout` to clear it too."));
90
+ }
91
+ }
92
+ export function registerAuthCommand(program) {
93
+ const auth = program.command('auth').description('Sign in to your Rush account (shared by `agents org` and paid tiers)');
94
+ auth.command('login').description('Sign in via the device-code flow').action(() => runOrDie(() => runAuthLogin()));
95
+ auth.command('whoami').description('Show the signed-in account').option('--json', 'Machine-readable output')
96
+ .action((o, command) => {
97
+ const json = !!(o.json || command.optsWithGlobals().json);
98
+ return runOrDie(() => runAuthWhoami(json), { json });
99
+ });
100
+ auth.command('logout').description('Clear the local `agents auth login` session').action(() => runOrDie(() => runAuthLogout()));
101
+ setHelpSections(auth, {
102
+ examples: `agents auth login
103
+ agents auth whoami
104
+ agents auth whoami --json
105
+ agents auth logout`,
106
+ notes: "`agents auth login` writes its own session, separate from \`rush login\`'s \`~/.rush/user.yaml\` — \`agents auth logout\` never signs you out of \`rush\`. If you're already signed in via \`rush login\`, \`agents auth whoami\`/\`agents org\` use that session automatically; no separate login is required.",
107
+ });
108
+ }