@phnx-labs/agents-cli 1.22.47 → 1.22.49

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 (249) hide show
  1. package/CHANGELOG.md +401 -17
  2. package/README.md +66 -32
  3. package/dist/bootstrap.js +4 -2
  4. package/dist/cli/command-registry.d.ts +1 -0
  5. package/dist/cli/command-registry.js +2 -0
  6. package/dist/commands/accounts.d.ts +58 -0
  7. package/dist/commands/accounts.js +206 -32
  8. package/dist/commands/artifacts.js +7 -6
  9. package/dist/commands/attach.js +1 -1
  10. package/dist/commands/browser.js +501 -103
  11. package/dist/commands/config.d.ts +0 -1
  12. package/dist/commands/config.js +0 -20
  13. package/dist/commands/daemon.d.ts +21 -0
  14. package/dist/commands/daemon.js +147 -12
  15. package/dist/commands/exec.js +52 -15
  16. package/dist/commands/focus.d.ts +14 -0
  17. package/dist/commands/focus.js +48 -5
  18. package/dist/commands/go.d.ts +3 -0
  19. package/dist/commands/go.js +16 -3
  20. package/dist/commands/inspect.js +2 -2
  21. package/dist/commands/lease.d.ts +1 -1
  22. package/dist/commands/lease.js +4 -3
  23. package/dist/commands/mailboxes.js +4 -3
  24. package/dist/commands/menubar.js +11 -4
  25. package/dist/commands/projects.d.ts +16 -1
  26. package/dist/commands/projects.js +58 -64
  27. package/dist/commands/resume.js +1 -1
  28. package/dist/commands/route.js +4 -4
  29. package/dist/commands/routines.js +3 -2
  30. package/dist/commands/run-broadcast.d.ts +1 -1
  31. package/dist/commands/run-broadcast.js +1 -1
  32. package/dist/commands/sessions-picker.d.ts +22 -0
  33. package/dist/commands/sessions-picker.js +48 -9
  34. package/dist/commands/sessions-trace.js +1 -1
  35. package/dist/commands/sessions.js +1 -0
  36. package/dist/commands/setup-secrets.js +23 -0
  37. package/dist/commands/share.d.ts +20 -5
  38. package/dist/commands/share.js +152 -52
  39. package/dist/commands/ssh.js +54 -7
  40. package/dist/commands/traces.d.ts +2 -0
  41. package/dist/commands/traces.js +184 -0
  42. package/dist/commands/view.d.ts +1 -1
  43. package/dist/commands/view.js +17 -34
  44. package/dist/index.d.ts +1 -0
  45. package/dist/index.js +5 -0
  46. package/dist/lib/accounting/usage.d.ts +14 -3
  47. package/dist/lib/accounting/usage.js +64 -18
  48. package/dist/lib/agent-spec/agents.js +11 -9
  49. package/dist/lib/browser/drivers/ssh.d.ts +16 -2
  50. package/dist/lib/browser/drivers/ssh.js +36 -9
  51. package/dist/lib/browser/ipc.js +8 -3
  52. package/dist/lib/browser/profiles.d.ts +35 -101
  53. package/dist/lib/browser/profiles.js +130 -301
  54. package/dist/lib/browser/registry.d.ts +39 -0
  55. package/dist/lib/browser/registry.js +149 -0
  56. package/dist/lib/browser/remote-control.d.ts +8 -6
  57. package/dist/lib/browser/remote-control.js +8 -6
  58. package/dist/lib/browser/resolve-target.d.ts +68 -0
  59. package/dist/lib/browser/resolve-target.js +250 -0
  60. package/dist/lib/browser/runtime-state.d.ts +40 -16
  61. package/dist/lib/browser/runtime-state.js +92 -25
  62. package/dist/lib/browser/service.d.ts +28 -9
  63. package/dist/lib/browser/service.js +139 -62
  64. package/dist/lib/browser/task-index.d.ts +70 -0
  65. package/dist/lib/browser/task-index.js +180 -0
  66. package/dist/lib/byok-usage.js +2 -2
  67. package/dist/lib/claude-statusline.d.ts +29 -0
  68. package/dist/lib/claude-statusline.js +139 -0
  69. package/dist/lib/cloud/host.d.ts +1 -1
  70. package/dist/lib/cloud/host.js +1 -1
  71. package/dist/lib/cloud/rush.js +1 -1
  72. package/dist/lib/computer/computer-rpc.js +2 -2
  73. package/dist/lib/computer/download.d.ts +15 -15
  74. package/dist/lib/computer/download.js +35 -98
  75. package/dist/lib/computer/ssh-tunnel.d.ts +0 -3
  76. package/dist/lib/computer/ssh-tunnel.js +12 -22
  77. package/dist/lib/config-keys.d.ts +2 -8
  78. package/dist/lib/config-keys.js +2 -13
  79. package/dist/lib/crabbox/cli.d.ts +1 -1
  80. package/dist/lib/crabbox/cli.js +2 -2
  81. package/dist/lib/crabbox/lease.d.ts +9 -0
  82. package/dist/lib/crabbox/lease.js +70 -15
  83. package/dist/lib/crabbox/runtimes.js +1 -1
  84. package/dist/lib/daemon/account-state-daemon-service.d.ts +16 -0
  85. package/dist/lib/daemon/account-state-daemon-service.js +26 -0
  86. package/dist/lib/daemon/browser-ipc-service.d.ts +20 -0
  87. package/dist/lib/daemon/browser-ipc-service.js +46 -0
  88. package/dist/lib/daemon/daemon.d.ts +3 -0
  89. package/dist/lib/daemon/daemon.js +212 -444
  90. package/dist/lib/daemon/device-probe-service.d.ts +26 -0
  91. package/dist/lib/daemon/device-probe-service.js +47 -0
  92. package/dist/lib/daemon/keychain-reap-service.d.ts +17 -0
  93. package/dist/lib/daemon/keychain-reap-service.js +32 -0
  94. package/dist/lib/daemon/monitor-engine-service.d.ts +22 -0
  95. package/dist/lib/daemon/monitor-engine-service.js +29 -0
  96. package/dist/lib/daemon/secrets-broker-service.d.ts +20 -0
  97. package/dist/lib/daemon/secrets-broker-service.js +66 -0
  98. package/dist/lib/daemon/self-heal-service.d.ts +27 -0
  99. package/dist/lib/daemon/self-heal-service.js +48 -0
  100. package/dist/lib/daemon/service.d.ts +102 -0
  101. package/dist/lib/daemon/service.js +98 -0
  102. package/dist/lib/daemon/session-index-service.d.ts +22 -0
  103. package/dist/lib/daemon/session-index-service.js +40 -0
  104. package/dist/lib/daemon/state-dir-check-service.d.ts +43 -0
  105. package/dist/lib/daemon/state-dir-check-service.js +63 -0
  106. package/dist/lib/daemon/supervisor.d.ts +87 -0
  107. package/dist/lib/daemon/supervisor.js +298 -0
  108. package/dist/lib/daemon/watchdog-service.d.ts +19 -0
  109. package/dist/lib/daemon/watchdog-service.js +40 -0
  110. package/dist/lib/daemon-health.d.ts +15 -0
  111. package/dist/lib/daemon-health.js +29 -5
  112. package/dist/lib/daemon-services.d.ts +8 -1
  113. package/dist/lib/daemon-services.js +55 -0
  114. package/dist/lib/daemon-ticks.d.ts +13 -2
  115. package/dist/lib/daemon-ticks.js +20 -18
  116. package/dist/lib/device-config.d.ts +0 -2
  117. package/dist/lib/device-config.js +1 -23
  118. package/dist/lib/devices/registry.d.ts +1 -1
  119. package/dist/lib/devices/registry.js +1 -1
  120. package/dist/lib/devices/stats-cache.d.ts +20 -0
  121. package/dist/lib/devices/stats-cache.js +49 -5
  122. package/dist/lib/devices/worker-pick.d.ts +48 -0
  123. package/dist/lib/devices/worker-pick.js +113 -0
  124. package/dist/lib/exec.d.ts +67 -13
  125. package/dist/lib/exec.js +85 -29
  126. package/dist/lib/harness/adapters/cursor.js +8 -11
  127. package/dist/lib/helper-download.d.ts +108 -0
  128. package/dist/lib/helper-download.js +194 -0
  129. package/dist/lib/helper-versions.d.ts +52 -0
  130. package/dist/lib/helper-versions.js +58 -0
  131. package/dist/lib/hooks/cache.js +11 -0
  132. package/dist/lib/hooks/install.js +2 -2
  133. package/dist/lib/hosts/credentials.d.ts +1 -1
  134. package/dist/lib/hosts/credentials.js +1 -1
  135. package/dist/lib/hosts/dispatch.d.ts +7 -1
  136. package/dist/lib/hosts/dispatch.js +24 -5
  137. package/dist/lib/hosts/passthrough.d.ts +8 -15
  138. package/dist/lib/hosts/passthrough.js +19 -16
  139. package/dist/lib/hosts/providers/devices.d.ts +1 -1
  140. package/dist/lib/hosts/providers/devices.js +1 -1
  141. package/dist/lib/hosts/reconnect.d.ts +179 -21
  142. package/dist/lib/hosts/reconnect.js +266 -45
  143. package/dist/lib/installations/migrate.d.ts +4 -5
  144. package/dist/lib/installations/migrate.js +6 -7
  145. package/dist/lib/installations/shims.d.ts +1 -1
  146. package/dist/lib/installations/shims.js +23 -13
  147. package/dist/lib/installations/versions.js +25 -2
  148. package/dist/lib/linear-autoclose.d.ts +1 -1
  149. package/dist/lib/linear-autoclose.js +1 -1
  150. package/dist/lib/loop.d.ts +2 -2
  151. package/dist/lib/loop.js +1 -1
  152. package/dist/lib/menubar/download-menubar.d.ts +47 -0
  153. package/dist/lib/menubar/download-menubar.js +60 -0
  154. package/dist/lib/menubar/install-menubar.d.ts +31 -7
  155. package/dist/lib/menubar/install-menubar.js +79 -21
  156. package/dist/lib/menubar/notify-desktop.d.ts +3 -3
  157. package/dist/lib/menubar/notify-desktop.js +6 -6
  158. package/dist/lib/model-tiers.d.ts +1 -1
  159. package/dist/lib/project-probe.d.ts +1 -1
  160. package/dist/lib/project-probe.js +1 -1
  161. package/dist/lib/project-pull.d.ts +1 -1
  162. package/dist/lib/project-pull.js +2 -2
  163. package/dist/lib/projects.d.ts +1 -1
  164. package/dist/lib/projects.js +1 -1
  165. package/dist/lib/sandbox.js +6 -7
  166. package/dist/lib/secrets/download-keychain.d.ts +47 -0
  167. package/dist/lib/secrets/download-keychain.js +70 -0
  168. package/dist/lib/secrets/install-helper.d.ts +20 -1
  169. package/dist/lib/secrets/install-helper.js +44 -3
  170. package/dist/lib/session/active.js +17 -1
  171. package/dist/lib/session/db.d.ts +11 -1
  172. package/dist/lib/session/db.js +138 -6
  173. package/dist/lib/session/discover.d.ts +21 -1
  174. package/dist/lib/session/discover.js +33 -5
  175. package/dist/lib/session/highlights.d.ts +49 -1
  176. package/dist/lib/session/highlights.js +95 -0
  177. package/dist/lib/session/host-link.d.ts +16 -2
  178. package/dist/lib/session/host-link.js +7 -1
  179. package/dist/lib/session/parse.js +33 -1
  180. package/dist/lib/session/remote/remote-list.d.ts +9 -0
  181. package/dist/lib/session/remote/remote-list.js +19 -2
  182. package/dist/lib/session/remote/watch.js +11 -2
  183. package/dist/lib/session/session-cache.d.ts +53 -0
  184. package/dist/lib/session/session-cache.js +88 -0
  185. package/dist/lib/session/state.js +1 -1
  186. package/dist/lib/session/tool-calls.d.ts +2 -0
  187. package/dist/lib/session/tool-calls.js +1 -1
  188. package/dist/lib/session/trajectory-html.d.ts +5 -1
  189. package/dist/lib/session/trajectory-html.js +42 -22
  190. package/dist/lib/session/trajectory-text.js +9 -2
  191. package/dist/lib/session/trajectory.js +53 -4
  192. package/dist/lib/session/types.d.ts +15 -0
  193. package/dist/lib/sha256-asset.d.ts +26 -0
  194. package/dist/lib/sha256-asset.js +41 -0
  195. package/dist/lib/share/backend.d.ts +95 -0
  196. package/dist/lib/share/backend.js +139 -0
  197. package/dist/lib/share/delete.d.ts +6 -2
  198. package/dist/lib/share/delete.js +8 -9
  199. package/dist/lib/share/html.d.ts +22 -0
  200. package/dist/lib/share/html.js +88 -0
  201. package/dist/lib/share/provision.d.ts +18 -4
  202. package/dist/lib/share/provision.js +38 -4
  203. package/dist/lib/share/publish.d.ts +39 -15
  204. package/dist/lib/share/publish.js +55 -26
  205. package/dist/lib/share/worker-template.js +171 -11
  206. package/dist/lib/sqlite.js +16 -3
  207. package/dist/lib/ssh-exec.d.ts +65 -0
  208. package/dist/lib/ssh-exec.js +120 -4
  209. package/dist/lib/startup/command-registry.js +1 -1
  210. package/dist/lib/startup/dev-build.js +3 -3
  211. package/dist/lib/state.d.ts +1 -1
  212. package/dist/lib/state.js +7 -3
  213. package/dist/lib/subagents-registry.js +1 -1
  214. package/dist/lib/terminal/index.d.ts +1 -1
  215. package/dist/lib/terminal/types.d.ts +1 -1
  216. package/dist/lib/terminal/types.js +1 -1
  217. package/dist/lib/tmux/orphan-reap.js +1 -1
  218. package/dist/lib/tmux/session.js +31 -0
  219. package/dist/lib/traces/backend.d.ts +29 -0
  220. package/dist/lib/traces/backend.js +46 -0
  221. package/dist/lib/traces/classify.d.ts +52 -0
  222. package/dist/lib/traces/classify.js +72 -0
  223. package/dist/lib/traces/config.d.ts +10 -0
  224. package/dist/lib/traces/config.js +2 -0
  225. package/dist/lib/traces/provision.d.ts +19 -0
  226. package/dist/lib/traces/provision.js +30 -0
  227. package/dist/lib/traces/sync.d.ts +150 -0
  228. package/dist/lib/traces/sync.js +447 -0
  229. package/dist/lib/traces/worker-template.d.ts +4 -0
  230. package/dist/lib/traces/worker-template.js +159 -0
  231. package/dist/lib/types.d.ts +18 -19
  232. package/dist/lib/types.js +14 -0
  233. package/dist/lib/usage-refresh.d.ts +1 -1
  234. package/dist/lib/usage-refresh.js +11 -8
  235. package/dist/lib/workflows.d.ts +1 -1
  236. package/package.json +4 -6
  237. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  238. package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +0 -24
  239. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  240. package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
  241. package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +0 -128
  242. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  243. package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +0 -26
  244. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  245. package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
  246. package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +0 -135
  247. package/dist/lib/secrets/Agents CLI.app/Contents/embedded.provisionprofile +0 -0
  248. package/dist/lib/usage-fleet.d.ts +0 -32
  249. package/dist/lib/usage-fleet.js +0 -125
@@ -1,12 +1,15 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
1
3
  import chalk from 'chalk';
2
- import { password } from '@inquirer/prompts';
4
+ import { password, select } from '@inquirer/prompts';
5
+ import { resolveClaudeSetupToken } from '../lib/claude-account-token.js';
3
6
  import { setHelpSections } from '../lib/help.js';
4
7
  import { readMeta, updateMeta } from '../lib/state.js';
5
8
  import { ALL_AGENT_IDS, getAccountInfo, resolveAgentName } from '../lib/agents.js';
6
9
  import { getGlobalDefault, getVersionHomePath, listInstalledVersions } from '../lib/installations/versions.js';
7
10
  import { assertNativeAccountNameable, nativeAccountCapability, nativeIdentityKey } from '../lib/account-capabilities.js';
8
11
  import { collectRunCandidates } from '../lib/accounting/rotate.js';
9
- import { isInteractiveTerminal } from './utils.js';
12
+ import { isInteractiveTerminal, isPromptCancelled, requireInteractiveSelection } from './utils.js';
10
13
  import { buildSwitchAccountChoices, formatAccountLimits, pickSwitchAccount } from './run-account-picker.js';
11
14
  import { profileExists, readProfile } from '../lib/profiles.js';
12
15
  import { pushBundleToHost } from '../lib/secrets/push.js';
@@ -66,6 +69,33 @@ export function classifyAttachTarget(target) {
66
69
  return { kind: 'device-agent', agent };
67
70
  throw new Error(`Unknown attach target '${target}'. Expected an installed <agent>@<version>, a harness id, or an existing custom harness profile.`);
68
71
  }
72
+ /**
73
+ * Persist the attached setup-token to a per-version `.oauth_token` file so an
74
+ * INTERACTIVE Claude launch on a keychain-less Linux worker can authenticate from it.
75
+ * Headless runs inject the token via `buildExecEnv`, but an interactive launch defers to
76
+ * the per-version login (`claudeAdapter.applyExecConfigEnv`), and the shim's Linux fallback
77
+ * (`claudeAdapter.shimConfigEnvBash`) reads exactly this file. Without writing it, a
78
+ * freshly-attached setup-token was invisible to interactive runs on a worker. macOS keeps
79
+ * the credential in the keychain, so `resolveClaudeSetupToken` returns null there and this
80
+ * is a no-op off Linux.
81
+ */
82
+ export function writeClaudeInteractiveOauthToken(target, targetAgent) {
83
+ if (process.platform !== 'linux' || targetAgent !== 'claude' || target.kind !== 'installation')
84
+ return;
85
+ const versionHome = getVersionHomePath('claude', target.version);
86
+ const tokenPath = path.join(versionHome, '.claude', '.oauth_token');
87
+ const token = resolveClaudeSetupToken(versionHome);
88
+ // A re-point (attach B over A, or a detach) can leave no setup-token resolving for
89
+ // this version — B's may not be minted yet. A leftover file from the previous binding
90
+ // would silently authenticate interactive runs as the OLD account (the shim's Linux
91
+ // fallback reads it), so clear it rather than leave it stale.
92
+ if (!token) {
93
+ fs.rmSync(tokenPath, { force: true });
94
+ return;
95
+ }
96
+ fs.mkdirSync(path.dirname(tokenPath), { recursive: true });
97
+ fs.writeFileSync(tokenPath, token, { mode: 0o600 });
98
+ }
69
99
  export function parseBundleKey(raw) {
70
100
  const colon = raw.indexOf(':');
71
101
  if (colon < 1 || colon === raw.length - 1)
@@ -93,18 +123,59 @@ async function printAccounts(json, fleet = false) {
93
123
  console.log(JSON.stringify([...records.map(account => publicAccount(inspectAccount(account.name))), ...native], null, 2));
94
124
  return;
95
125
  }
96
- console.log(chalk.bold('Provider account bundles\n'));
97
- if (!records.length)
98
- console.log(chalk.gray(" None. Add one with 'agents accounts add <name> --provider <provider> --auth <type>'."));
99
- for (const account of records) {
100
- const present = inspectAccount(account.name).secretPresent ? chalk.green('ready') : chalk.red('missing on this device');
101
- console.log(` ${chalk.cyan(account.name)} ${account.provider} ${account.auth} ${present}`);
126
+ console.log(renderAccountList(records, native));
127
+ }
128
+ /** Pure text renderer for `agents accounts list` grouped, column-aligned, labels first-class. */
129
+ export function renderAccountList(records, native) {
130
+ const out = [];
131
+ // Native logins, grouped by harness — the selection surface for `<harness>#<label>`.
132
+ out.push(chalk.bold('Native logins') + chalk.gray(' ') + chalk.gray('run <harness>#<label>'));
133
+ if (!native.length) {
134
+ out.push(chalk.gray(' No signed-in native accounts found.'));
135
+ }
136
+ else {
137
+ const byHarness = new Map();
138
+ for (const acct of native) {
139
+ const list = byHarness.get(acct.agent) ?? [];
140
+ list.push(acct);
141
+ byHarness.set(acct.agent, list);
142
+ }
143
+ const harnessW = Math.max(6, ...[...byHarness.keys()].map(h => h.length));
144
+ const labelW = Math.max(6, ...native.map(a => (a.name ?? '—').length));
145
+ const idW = Math.max(8, ...native.map(a => a.display.length));
146
+ let sawDefault = false;
147
+ for (const [harness, list] of [...byHarness.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
148
+ const defaultVersion = getGlobalDefault(harness);
149
+ list.forEach((acct, i) => {
150
+ const harnessCell = chalk.gray((i === 0 ? harness : '').padEnd(harnessW));
151
+ const rawLabel = acct.name ?? '—';
152
+ const labelCell = acct.name ? chalk.cyan(rawLabel.padEnd(labelW)) : chalk.gray(rawLabel.padEnd(labelW));
153
+ const isDefault = defaultVersion ? acct.versions.includes(defaultVersion) : false;
154
+ if (isDefault)
155
+ sawDefault = true;
156
+ const marker = isDefault ? chalk.green('*') : ' ';
157
+ out.push(` ${harnessCell} ${labelCell}${marker} ${acct.display.padEnd(idW)} ${chalk.gray(acct.versions.join(', '))}`);
158
+ });
159
+ }
160
+ if (sawDefault)
161
+ out.push(chalk.gray('\n * default account for that harness (used when you pass neither @version nor #label)'));
102
162
  }
103
- console.log(chalk.bold('\nNative harness logins\n'));
104
- if (!native.length)
105
- console.log(chalk.gray(' No signed-in native accounts found.'));
106
- for (const account of native)
107
- console.log(` ${account.name ? `${chalk.cyan(account.name)} · ` : ''}${account.display} ${account.agent} ${account.versions.join(', ')}`);
163
+ // Provider bundles — the durable API-key/setup-token accounts, selected with `--account <name>`.
164
+ out.push('');
165
+ out.push(chalk.bold('Provider bundles') + chalk.gray(' run <harness> --account <name>'));
166
+ if (!records.length) {
167
+ out.push(chalk.gray(" None. Add one with 'agents accounts add <name> --provider <provider> --auth <type>'."));
168
+ }
169
+ else {
170
+ const nameW = Math.max(4, ...records.map(a => a.name.length));
171
+ const provW = Math.max(8, ...records.map(a => a.provider.length));
172
+ const authW = Math.max(10, ...records.map(a => a.auth.length));
173
+ for (const account of records) {
174
+ const present = inspectAccount(account.name).secretPresent ? chalk.green('ready') : chalk.red('missing on this device');
175
+ out.push(` ${chalk.cyan(account.name.padEnd(nameW))} ${account.provider.padEnd(provW)} ${account.auth.padEnd(authW)} ${present}`);
176
+ }
177
+ }
178
+ return out.join('\n');
108
179
  }
109
180
  function parseAuth(raw) {
110
181
  if (raw === 'api-key' || raw === 'setup-token' || raw === 'bearer-token')
@@ -116,6 +187,107 @@ function parseHarness(agentRaw) {
116
187
  throw new Error(`Unknown agent '${agentRaw}'.`);
117
188
  return agentRaw;
118
189
  }
190
+ /**
191
+ * Fold signed-in run candidates into distinct identities — the unit a label
192
+ * binds to. The same account signed into several versions is ONE row, so a
193
+ * multi-version single-account harness never demands a selector. Candidates
194
+ * with no stable identity (no accountKey, no email) are dropped: there is
195
+ * nothing durable for a label to bind to. Default-version identity first.
196
+ */
197
+ export function groupLabelIdentities(candidates, defaultVersion) {
198
+ const byKey = new Map();
199
+ for (const candidate of candidates) {
200
+ const key = candidate.accountKey ?? candidate.email?.toLowerCase();
201
+ if (!key)
202
+ continue;
203
+ const row = byKey.get(key) ?? { identityKey: key, email: null, versions: [], isDefault: false };
204
+ row.email ??= candidate.email;
205
+ row.versions.push(candidate.version);
206
+ if (candidate.version === defaultVersion)
207
+ row.isDefault = true;
208
+ byKey.set(key, row);
209
+ }
210
+ return [...byKey.values()].sort((a, b) => a.isDefault !== b.isDefault ? (a.isDefault ? -1 : 1) : (a.email ?? a.identityKey).localeCompare(b.email ?? b.identityKey));
211
+ }
212
+ /**
213
+ * Resolve which signed-in login a bare-harness `accounts label` call means.
214
+ * `collect` is injectable (the `resolveRunVersion` pattern) so tests drive the
215
+ * real selection path against fixture candidates.
216
+ */
217
+ export async function resolveLabelIdentity(agent, accountSelector, collect = collectRunCandidates) {
218
+ const candidates = (await collect(agent)).filter(candidate => candidate.signedIn);
219
+ const identities = groupLabelIdentities(candidates, getGlobalDefault(agent));
220
+ if (identities.length === 0)
221
+ throw new Error(`No signed-in ${agent} account with a stable identity. Run the harness and complete its login first.`);
222
+ const needle = accountSelector?.toLowerCase();
223
+ if (needle) {
224
+ const match = identities.find(identity => identity.email?.toLowerCase() === needle || identity.identityKey.toLowerCase() === needle);
225
+ if (!match)
226
+ throw new Error(`Unknown ${agent} account '${accountSelector}'.`);
227
+ return { kind: 'selected', identity: match };
228
+ }
229
+ if (identities.length === 1)
230
+ return { kind: 'selected', identity: identities[0] };
231
+ return { kind: 'ambiguous', identities };
232
+ }
233
+ /**
234
+ * Body of `accounts label`, exported with the injectable candidate collector
235
+ * so tests exercise everything but the TTY prompt itself.
236
+ */
237
+ export async function runAccountsLabel(source, label, opts, collect = collectRunCandidates) {
238
+ if (source.includes('@')) {
239
+ // <harness>@<version> pins the login by where it is signed in, so a
240
+ // second selector can only contradict it.
241
+ if (opts.account)
242
+ throw new Error(`'${source}' already selects one login; drop --account.`);
243
+ const identity = await nativeIdentityFromSource(source);
244
+ const account = labelNativeAccount(identity.agent, identity.identityKey, identity.identityLabel, label, identity.scope);
245
+ console.log(chalk.green(`Labeled ${identity.agent} account ${identity.identityLabel ?? identity.identityKey} as '${account.name}'.`));
246
+ return;
247
+ }
248
+ const agent = parseHarness(source);
249
+ assertNativeAccountNameable(agent);
250
+ const selection = await resolveLabelIdentity(agent, opts.account, collect);
251
+ let selected;
252
+ if (selection.kind === 'selected') {
253
+ selected = selection.identity;
254
+ }
255
+ else {
256
+ if (!isInteractiveTerminal()) {
257
+ requireInteractiveSelection(`Selecting the ${agent} login to label`, [
258
+ `agents accounts label ${agent} ${label ?? '<label>'} --account <email|id>`,
259
+ `agents accounts label ${agent}@<version> ${label ?? '<label>'}`,
260
+ ]);
261
+ }
262
+ const picked = await pickLabelIdentity(agent, selection.identities);
263
+ if (!picked)
264
+ return;
265
+ selected = picked;
266
+ }
267
+ const account = labelNativeAccount(agent, selected.identityKey, selected.email ?? undefined, label, nativeAccountCapability(agent).scope);
268
+ console.log(chalk.green(`Labeled ${agent} account ${selected.email ?? selected.identityKey} as '${account.name}'.`));
269
+ }
270
+ /** Prompt for the login a label should bind to. A cancelled picker writes nothing. */
271
+ async function pickLabelIdentity(agent, identities) {
272
+ const idW = Math.max(0, ...identities.map(identity => (identity.email ?? identity.identityKey).length));
273
+ const choices = identities.map(identity => ({
274
+ name: [
275
+ (identity.email ?? identity.identityKey).padEnd(idW),
276
+ identity.isDefault ? chalk.green('default') : ' ',
277
+ chalk.gray(identity.versions.join(', ')),
278
+ ].join(' '),
279
+ value: identity.identityKey,
280
+ }));
281
+ try {
282
+ const key = await select({ message: `Select the ${agent} login to label:`, choices, loop: false });
283
+ return identities.find(identity => identity.identityKey === key) ?? null;
284
+ }
285
+ catch (err) {
286
+ if (isPromptCancelled(err))
287
+ return null;
288
+ throw err;
289
+ }
290
+ }
119
291
  function providerAuthenticatesHarness(provider, auth, agent) {
120
292
  try {
121
293
  getAccountProvider(provider).envFor(agent, auth);
@@ -288,32 +460,24 @@ export function registerAccountsCommand(program) {
288
460
  cleanCommandError(command, err);
289
461
  }
290
462
  });
291
- accounts.command('label <harness> [label]')
292
- .description('Label a native login independently of its installed harness version')
463
+ const labelCmd = accounts.command('label <source> [label]')
464
+ .description('Label a native login by harness or <harness>@<version>; the label binds to the account identity, not the version')
293
465
  .option('--account <email-or-id>', 'Native identity to label when the harness has multiple logins')
294
- .action(async (harness, label, o, command) => {
466
+ .action(async (source, label, o, command) => {
295
467
  try {
296
- const agent = parseHarness(harness);
297
- assertNativeAccountNameable(agent);
298
- const candidates = (await collectRunCandidates(agent)).filter(candidate => candidate.signedIn);
299
- const needle = o.account?.toLowerCase();
300
- const selected = needle
301
- ? candidates.find(candidate => candidate.email?.toLowerCase() === needle || candidate.accountKey?.toLowerCase() === needle)
302
- : candidates.find(candidate => candidate.version === getGlobalDefault(agent)) ?? (candidates.length === 1 ? candidates[0] : undefined);
303
- if (!selected)
304
- throw new Error(needle
305
- ? `Unknown ${agent} account '${o.account}'.`
306
- : `Multiple ${agent} accounts are connected; pass --account <email|id>.`);
307
- const identityKey = selected.accountKey ?? selected.email?.toLowerCase();
308
- if (!identityKey)
309
- throw new Error(`${agent} account '${selected.version}' has no stable identity.`);
310
- const account = labelNativeAccount(agent, identityKey, selected.email ?? undefined, label, nativeAccountCapability(agent).scope);
311
- console.log(chalk.green(`Labeled ${agent} account ${selected.email ?? identityKey} as '${account.name}'.`));
468
+ await runAccountsLabel(source, label, o);
312
469
  }
313
470
  catch (err) {
314
471
  cleanCommandError(command, err);
315
472
  }
316
473
  });
474
+ setHelpSections(labelCmd, {
475
+ examples: `agents accounts label codex work
476
+ agents accounts label codex@0.146.0 personal
477
+ agents accounts label codex work --account you@example.com
478
+ agents run codex#work`,
479
+ notes: 'The label binds to the signed-in account identity, not the version — codex#work keeps selecting that account after it moves to a newer install. One signed-in login needs no selector; with several, an interactive terminal opens a picker, while scripts pass --account or <harness>@<version>.',
480
+ });
317
481
  accounts.command('attach <account> <target>')
318
482
  .description('Attach a named account to a native installation or custom harness')
319
483
  .action(async (name, target) => {
@@ -351,12 +515,21 @@ export function registerAccountsCommand(program) {
351
515
  getAccountProvider(account.provider).envFor(targetAgent, account.auth);
352
516
  }
353
517
  bindAccount(name, target);
518
+ writeClaudeInteractiveOauthToken(t, targetAgent);
354
519
  console.log(chalk.green(`Attached ${account.name} to ${target}.`));
355
520
  });
356
521
  accounts.command('detach <account> <target>')
357
522
  .description('Remove one account attachment')
358
523
  .action((name, target) => {
359
524
  unbindAccount(name, target);
525
+ // With the binding gone, no setup-token resolves for this version home, so this
526
+ // clears any .oauth_token the attach left behind (else interactive runs would keep
527
+ // authenticating as the just-detached account).
528
+ try {
529
+ const t = classifyAttachTarget(target);
530
+ writeClaudeInteractiveOauthToken(t, t.kind === 'profile' ? t.profile.host.agent : t.agent);
531
+ }
532
+ catch { /* an unresolvable target has no version home to clean */ }
360
533
  console.log(chalk.green(`Detached ${name} from ${target}.`));
361
534
  });
362
535
  accounts.command('rename <old> <new>').description('Rename an account without changing its stable id').action((oldName, newName) => renameAccount(oldName, newName));
@@ -469,6 +642,7 @@ agents run claude`,
469
642
  agents accounts add openrouter-work --provider openrouter --auth api-key --from-secrets openrouter.ai:OPENROUTER_API_KEY
470
643
  agents accounts set-key work
471
644
  agents accounts name claude@2.1.220 work
645
+ agents accounts label codex@0.146.0 personal
472
646
  agents accounts attach work claude@2.1.225
473
647
  agents accounts view work --json
474
648
  agents accounts switch claude
@@ -20,18 +20,19 @@ import { registerArtifactsSetupCommand } from './artifacts-setup.js';
20
20
  export function registerArtifactsCommands(program) {
21
21
  const artifactsCmd = program
22
22
  .command('artifacts')
23
- .description('Publish agent-made artifacts (plans, reports, visuals) to your own Cloudflare R2 and get a shareable link (~$0).');
23
+ .description('Publish agent-made artifacts (plans, reports, visuals) to a shareable link managed if signed in, otherwise your Cloudflare R2.');
24
24
  registerArtifactsSetupCommand(artifactsCmd);
25
25
  registerShareCommands(artifactsCmd);
26
26
  setHelpSections(artifactsCmd, {
27
27
  examples: `
28
- # One-time: provision your own endpoint (or join a teammate's)
28
+ # Signed in? Just publish no Cloudflare setup
29
+ agents auth login
30
+ agents artifacts share ./out/plan.html
31
+
32
+ # Or provision your own Cloudflare endpoint (or join a teammate's)
29
33
  agents artifacts setup
30
34
  agents artifacts share join https://share.agents-cli.sh
31
35
 
32
- # Publish — auto OG cover, default 30d expiry, shareable link
33
- agents artifacts share ./out/plan.html
34
-
35
36
  # What's live in your namespace, and taking one down
36
37
  agents artifacts share list
37
38
  agents artifacts unshare my-plan-a1b2
@@ -50,7 +51,7 @@ export function registerArtifactsCommands(program) {
50
51
  agents artifacts unshare <targets...> is the nested alias of
51
52
  agents artifacts share delete. Top-level \`agents unshare\` is gone.
52
53
 
53
- Full reference: apps/cli/docs/share.md.
54
+ Architecture: cli/docs/observability.md.
54
55
  `,
55
56
  });
56
57
  registerUnshareCommand(artifactsCmd);
@@ -50,7 +50,7 @@ export async function attachAction(id) {
50
50
  // process on the owning machine.
51
51
  const peer = sessionRecoveryPeer(meta);
52
52
  if (peer) {
53
- const routed = await runOnPeer(attachRecoveryArgs(meta), peer, { tty: true });
53
+ const routed = await runOnPeer(attachRecoveryArgs(meta), peer, { tty: true, sessionId: meta.id });
54
54
  if (routed === 'no-target') {
55
55
  console.error(chalk.red(`Cannot attach ${meta.shortId}: origin device ${peer} is not a registered reachable peer.`));
56
56
  process.exitCode = 1;