@phnx-labs/agents-cli 1.22.114 → 1.22.115

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 (176) hide show
  1. package/CHANGELOG.md +110 -0
  2. package/README.md +22 -102
  3. package/dist/browser.js +0 -0
  4. package/dist/cli/command-registry.js +0 -7
  5. package/dist/commands/accounts.d.ts +3 -77
  6. package/dist/commands/accounts.js +13 -366
  7. package/dist/commands/audit.js +1 -1
  8. package/dist/commands/auth.js +0 -2
  9. package/dist/commands/browser.d.ts +53 -0
  10. package/dist/commands/browser.js +199 -9
  11. package/dist/commands/doctor.d.ts +2 -3
  12. package/dist/commands/doctor.js +0 -11
  13. package/dist/commands/events.d.ts +1 -3
  14. package/dist/commands/events.js +4 -7
  15. package/dist/commands/exec.d.ts +0 -1
  16. package/dist/commands/exec.js +2 -7
  17. package/dist/commands/feed-watch.js +52 -8
  18. package/dist/commands/focus.js +1 -1
  19. package/dist/commands/go.d.ts +0 -17
  20. package/dist/commands/go.js +2 -19
  21. package/dist/commands/logs.js +1 -1
  22. package/dist/commands/mcp.js +8 -83
  23. package/dist/commands/memory.js +4 -47
  24. package/dist/commands/message.js +4 -4
  25. package/dist/commands/plugins.js +1 -93
  26. package/dist/commands/repo.js +0 -44
  27. package/dist/commands/resume.js +4 -1
  28. package/dist/commands/secrets-passthrough.js +2 -2
  29. package/dist/commands/send.d.ts +4 -4
  30. package/dist/commands/send.js +6 -51
  31. package/dist/commands/sessions-backup-setup.js +1 -1
  32. package/dist/commands/sessions-resume.js +0 -1
  33. package/dist/commands/sessions-share.d.ts +5 -7
  34. package/dist/commands/sessions-share.js +98 -49
  35. package/dist/commands/sessions.js +1 -8
  36. package/dist/commands/setup-browser.js +18 -2
  37. package/dist/commands/setup-computer.js +20 -6
  38. package/dist/commands/setup-secrets.js +24 -4
  39. package/dist/commands/setup-terminal.d.ts +3 -0
  40. package/dist/commands/setup-terminal.js +22 -0
  41. package/dist/commands/setup.d.ts +1 -1
  42. package/dist/commands/setup.js +20 -10
  43. package/dist/commands/skills.js +0 -8
  44. package/dist/commands/ssh.d.ts +6 -0
  45. package/dist/commands/ssh.js +92 -233
  46. package/dist/commands/sync.js +14 -5
  47. package/dist/commands/teams.js +1 -1
  48. package/dist/commands/traces.js +1 -1
  49. package/dist/lib/accounts/add.d.ts +0 -5
  50. package/dist/lib/accounts/add.js +1 -7
  51. package/dist/lib/artifacts-client.d.ts +20 -0
  52. package/dist/lib/artifacts-client.js +46 -0
  53. package/dist/lib/auth-mint.js +2 -2
  54. package/dist/lib/browser/runtime-state.d.ts +55 -0
  55. package/dist/lib/browser/runtime-state.js +99 -18
  56. package/dist/lib/browser/service.js +21 -1
  57. package/dist/lib/cli-resources.js +3 -1
  58. package/dist/lib/cloud/dispatch.js +1 -1
  59. package/dist/lib/cloudflare/creds.d.ts +10 -0
  60. package/dist/lib/cloudflare/creds.js +46 -0
  61. package/dist/lib/cloudflare/provision.d.ts +35 -0
  62. package/dist/lib/cloudflare/provision.js +144 -0
  63. package/dist/lib/computer/sessions-list.d.ts +55 -0
  64. package/dist/lib/computer/sessions-list.js +168 -1
  65. package/dist/lib/daemon/daemon.js +8 -1
  66. package/dist/lib/daemon/feed-stream-service.d.ts +23 -0
  67. package/dist/lib/daemon/feed-stream-service.js +40 -0
  68. package/dist/lib/daemon-services.d.ts +1 -1
  69. package/dist/lib/daemon-services.js +5 -0
  70. package/dist/lib/devices/connect.d.ts +49 -5
  71. package/dist/lib/devices/connect.js +169 -21
  72. package/dist/lib/feed/envelope.d.ts +79 -0
  73. package/dist/lib/feed/envelope.js +23 -0
  74. package/dist/lib/feed/events.d.ts +6 -0
  75. package/dist/lib/feed/events.js +8 -0
  76. package/dist/lib/feed/hub-server.d.ts +86 -0
  77. package/dist/lib/feed/hub-server.js +334 -0
  78. package/dist/lib/feed/hub.d.ts +95 -0
  79. package/dist/lib/feed/hub.js +255 -0
  80. package/dist/lib/feed/tool-activity.d.ts +108 -0
  81. package/dist/lib/feed/tool-activity.js +313 -0
  82. package/dist/lib/feed/tools.d.ts +198 -0
  83. package/dist/lib/feed/tools.js +265 -0
  84. package/dist/lib/feed/watch.d.ts +50 -50
  85. package/dist/lib/feed/watch.js +147 -16
  86. package/dist/lib/format.d.ts +1 -1
  87. package/dist/lib/format.js +1 -1
  88. package/dist/lib/git.d.ts +0 -16
  89. package/dist/lib/git.js +0 -58
  90. package/dist/lib/helper-versions.js +1 -1
  91. package/dist/lib/hosts/remote-cmd.d.ts +51 -1
  92. package/dist/lib/hosts/remote-cmd.js +125 -8
  93. package/dist/lib/hosts/remote-cmd.test-fixture.d.ts +2 -0
  94. package/dist/lib/hosts/remote-cmd.test-fixture.js +22 -0
  95. package/dist/lib/mcp.js +17 -11
  96. package/dist/lib/probe.d.ts +4 -1
  97. package/dist/lib/probe.js +5 -2
  98. package/dist/lib/pwsh.d.ts +33 -0
  99. package/dist/lib/pwsh.js +56 -0
  100. package/dist/lib/redact.d.ts +8 -0
  101. package/dist/lib/redact.js +11 -0
  102. package/dist/lib/refresh.d.ts +6 -2
  103. package/dist/lib/refresh.js +92 -72
  104. package/dist/lib/secrets-cli.d.ts +11 -0
  105. package/dist/lib/secrets-cli.js +30 -0
  106. package/dist/lib/secrets-client.js +3 -2
  107. package/dist/lib/session/detached.d.ts +7 -0
  108. package/dist/lib/session/detached.js +29 -0
  109. package/dist/lib/session/remote/peer-stream.d.ts +24 -2
  110. package/dist/lib/session/remote/peer-stream.js +33 -6
  111. package/dist/lib/session/sync/backend.d.ts +3 -3
  112. package/dist/lib/session/sync/backend.js +3 -3
  113. package/dist/lib/session/sync/provision.d.ts +1 -1
  114. package/dist/lib/session/sync/provision.js +2 -2
  115. package/dist/lib/sessions-client.js +0 -3
  116. package/dist/lib/setup-tool-install.d.ts +3 -0
  117. package/dist/lib/setup-tool-install.js +26 -0
  118. package/dist/lib/setup-tool-status.d.ts +22 -0
  119. package/dist/lib/setup-tool-status.js +215 -0
  120. package/dist/lib/share-runtime.d.ts +11 -0
  121. package/dist/lib/share-runtime.js +63 -0
  122. package/dist/lib/smart-launch.d.ts +1 -5
  123. package/dist/lib/smart-launch.js +3 -11
  124. package/dist/lib/ssh-exec.d.ts +44 -0
  125. package/dist/lib/ssh-exec.js +119 -0
  126. package/dist/lib/startup/command-registry.d.ts +6 -4
  127. package/dist/lib/startup/command-registry.js +10 -7
  128. package/dist/lib/state.js +2 -2
  129. package/dist/lib/storage/selection.d.ts +2 -2
  130. package/dist/lib/storage/selection.js +2 -2
  131. package/dist/lib/sync-umbrella.d.ts +5 -0
  132. package/dist/lib/sync-umbrella.js +18 -10
  133. package/dist/lib/traces/backend.d.ts +1 -2
  134. package/dist/lib/traces/backend.js +1 -2
  135. package/dist/lib/traces/provision.d.ts +1 -1
  136. package/dist/lib/traces/provision.js +2 -2
  137. package/dist/lib/types.d.ts +8 -6
  138. package/package.json +2 -3
  139. package/dist/commands/artifacts-setup.d.ts +0 -53
  140. package/dist/commands/artifacts-setup.js +0 -161
  141. package/dist/commands/artifacts.d.ts +0 -18
  142. package/dist/commands/artifacts.js +0 -58
  143. package/dist/commands/attach.d.ts +0 -12
  144. package/dist/commands/attach.js +0 -86
  145. package/dist/commands/auth-mint.d.ts +0 -12
  146. package/dist/commands/auth-mint.js +0 -108
  147. package/dist/commands/reconnect.d.ts +0 -46
  148. package/dist/commands/reconnect.js +0 -115
  149. package/dist/commands/share.d.ts +0 -293
  150. package/dist/commands/share.js +0 -1424
  151. package/dist/lib/share/analytics.d.ts +0 -13
  152. package/dist/lib/share/analytics.js +0 -45
  153. package/dist/lib/share/backend.d.ts +0 -120
  154. package/dist/lib/share/backend.js +0 -176
  155. package/dist/lib/share/capture.d.ts +0 -31
  156. package/dist/lib/share/capture.js +0 -174
  157. package/dist/lib/share/config.d.ts +0 -72
  158. package/dist/lib/share/config.js +0 -211
  159. package/dist/lib/share/delete.d.ts +0 -123
  160. package/dist/lib/share/delete.js +0 -173
  161. package/dist/lib/share/html.d.ts +0 -20
  162. package/dist/lib/share/html.js +0 -88
  163. package/dist/lib/share/http-error.d.ts +0 -53
  164. package/dist/lib/share/http-error.js +0 -65
  165. package/dist/lib/share/og.d.ts +0 -26
  166. package/dist/lib/share/og.js +0 -84
  167. package/dist/lib/share/provision.d.ts +0 -127
  168. package/dist/lib/share/provision.js +0 -285
  169. package/dist/lib/share/publish.d.ts +0 -379
  170. package/dist/lib/share/publish.js +0 -818
  171. package/dist/lib/share/worker-template.d.ts +0 -27
  172. package/dist/lib/share/worker-template.js +0 -2424
  173. package/dist/lib/storage/index.d.ts +0 -14
  174. package/dist/lib/storage/index.js +0 -14
  175. package/dist/lib/storage/visibility.d.ts +0 -82
  176. package/dist/lib/storage/visibility.js +0 -99
@@ -1,18 +1,14 @@
1
- import * as fs from 'fs';
2
- import * as path from 'path';
3
1
  import chalk from 'chalk';
4
- import { password, select } from '@inquirer/prompts';
5
- import { readClaudeAccountEmail, resolveClaudeSetupToken, resolveClaudeSetupTokenForEmail, seedClaudeWorkerHomeIdentity } from '../lib/claude-account-token.js';
2
+ import { password } from '@inquirer/prompts';
6
3
  import { setHelpSections } from '../lib/help.js';
7
4
  import { readMeta, updateMeta } from '../lib/state.js';
8
5
  import { machineId } from '../lib/machine-id.js';
9
- import { ALL_AGENT_IDS, getAccountInfo, resolveAgentName } from '../lib/agents.js';
10
- import { getGlobalDefault, getVersionHomePath, listInstalledVersions } from '../lib/installations/versions.js';
11
- import { assertNativeAccountNameable, nativeAccountCapability, nativeIdentityKey } from '../lib/account-capabilities.js';
6
+ import { ALL_AGENT_IDS, resolveAgentName } from '../lib/agents.js';
7
+ import { getGlobalDefault, listInstalledVersions } from '../lib/installations/versions.js';
8
+ import { assertNativeAccountNameable } from '../lib/account-capabilities.js';
12
9
  import { collectRunCandidates } from '../lib/accounting/rotate.js';
13
- import { isInteractiveTerminal, isPromptCancelled, requireInteractiveSelection } from './utils.js';
10
+ import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
14
11
  import { buildSwitchAccountChoices, formatAccountLimits, pickSwitchAccount } from './run-account-picker.js';
15
- import { profileExists, readProfile } from '../lib/profiles.js';
16
12
  import { assertCredentialTransportHostPinned, resolveHostSshTarget } from '../lib/hosts/credential-transport.js';
17
13
  import { resolveRemoteOsSync } from '../lib/hosts/remote-os.js';
18
14
  import { renderAccountFleetMatrix } from '../lib/devices/harness-inventory.js';
@@ -24,23 +20,12 @@ import { ensureAdoptedDefaultRepoint } from '../lib/exec-account-home.js';
24
20
  import { acquireAuthOperationLock } from '../lib/accounts/auth-operation-lock.js';
25
21
  import { isSecretsClientError, pushBundleToHost, readAndResolveBundleEnv, readBundle } from '../lib/secrets-client.js';
26
22
  import { getAccountProvider, listAccountProviders, providerAuthenticatesHarness } from '../lib/account-provider-registry.js';
27
- import { accountBindings, addAccount, addNativeAccount, assertUnambiguousNativeAccount, bindAccount, findAccount, findUnifiedAccount, inspectAccount, labelNativeAccount, listNativeAccounts, nativeAccountHome, parseAccountSelector, readAccountRegistry, removeAccount, renameAccount, setAccountSecret, unbindAccount } from '../lib/account-registry.js';
28
- import { registerMintCommand } from './auth-mint.js';
23
+ import { accountBindings, addAccount, assertUnambiguousNativeAccount, findAccount, findUnifiedAccount, inspectAccount, listNativeAccounts, nativeAccountHome, parseAccountSelector, readAccountRegistry, removeAccount, renameAccount, setAccountSecret } from '../lib/account-registry.js';
29
24
  /** Comma-joined list of harnesses `accounts add` can drive today. */
30
25
  function addSupportedList() {
31
26
  return supportedAddHarnesses().join(', ');
32
27
  }
33
- /**
34
- * Legacy-verb retirement: the command is created with `{ hidden: true }` (help
35
- * no longer lists it) and this hook prints the pointer to its replacement when
36
- * it runs. Verbs keep executing for one release.
37
- */
38
- function retireVerb(cmd, pointer) {
39
- cmd.hook('preAction', () => {
40
- console.error(chalk.gray(`'agents accounts ${cmd.name()}' is replaced by '${pointer}' — still works this release.`));
41
- });
42
- }
43
- /** Shared result printer for `accounts add` / `accounts login` / hidden `connect`. */
28
+ /** Shared result printer for `accounts add` / `accounts login`. */
44
29
  function printAddResult(result, json) {
45
30
  if (json) {
46
31
  console.log(JSON.stringify(result, null, 2));
@@ -78,85 +63,6 @@ async function runAccountsAction(command, fn) {
78
63
  cleanCommandError(command, err);
79
64
  }
80
65
  }
81
- function parseInstallation(raw) {
82
- const at = raw.lastIndexOf('@');
83
- if (at < 1 || at === raw.length - 1)
84
- throw new Error(`Expected <agent>@<version>, got '${raw}'.`);
85
- const agent = resolveAgentName(raw.slice(0, at));
86
- if (!agent)
87
- throw new Error(`Unknown agent '${raw.slice(0, at)}'.`);
88
- return { agent, version: raw.slice(at + 1) };
89
- }
90
- export async function nativeIdentityFromSource(raw) {
91
- const parsed = parseInstallation(raw);
92
- const capability = nativeAccountCapability(parsed.agent);
93
- assertNativeAccountNameable(parsed.agent);
94
- if (!listInstalledVersions(parsed.agent).includes(parsed.version))
95
- throw new Error(`${raw} is not installed.`);
96
- const info = await getAccountInfo(parsed.agent, getVersionHomePath(parsed.agent, parsed.version));
97
- const identityKey = nativeIdentityKey(info, capability);
98
- if (!identityKey)
99
- throw new Error(`${raw} has no stable signed-in identity. Run it and complete its normal login first.`);
100
- return { agent: parsed.agent, version: parsed.version, identityKey, identityLabel: info.email ?? undefined, scope: capability.scope };
101
- }
102
- /**
103
- * Classify + validate an attach target, rejecting a typo BEFORE any binding is
104
- * written: it must be an existing custom-harness profile, an installed
105
- * `agent@version`, or a known harness id.
106
- */
107
- export function classifyAttachTarget(target) {
108
- if (profileExists(target))
109
- return { kind: 'profile', profile: readProfile(target) };
110
- if (target.includes('@')) {
111
- const at = target.lastIndexOf('@');
112
- const agent = resolveAgentName(target.slice(0, at));
113
- const version = target.slice(at + 1);
114
- if (!agent)
115
- throw new Error(`Unknown harness '${target.slice(0, at)}' in target '${target}'.`);
116
- if (!version)
117
- throw new Error(`Target '${target}' is missing a version.`);
118
- if (!listInstalledVersions(agent).includes(version))
119
- throw new Error(`${agent}@${version} is not installed.`);
120
- return { kind: 'installation', agent, version };
121
- }
122
- const agent = resolveAgentName(target);
123
- if (agent)
124
- return { kind: 'device-agent', agent };
125
- throw new Error(`Unknown attach target '${target}'. Expected an installed <agent>@<version>, a harness id, or an existing custom harness profile.`);
126
- }
127
- /**
128
- * Persist the attached setup-token to a per-version `.oauth_token` file so an
129
- * INTERACTIVE Claude launch on a keychain-less Linux worker can authenticate from it.
130
- * Headless runs inject the token via `buildExecEnv`. Since PHNX-3502 an interactive
131
- * launch on a worker-role device ALSO injects it through `claudeAdapter.applyExecConfigEnv`
132
- * (only a headed `personal`/`desktop` device defers to its per-version native login), and
133
- * the shim's Linux fallback (`claudeAdapter.shimConfigEnvBash`) reads exactly this file —
134
- * so writing it is what makes a freshly-attached setup-token visible to that shim fallback
135
- * on a worker. macOS keeps the credential in the keychain, so `resolveClaudeSetupToken`
136
- * returns null there and this is a no-op off Linux.
137
- */
138
- export function writeClaudeInteractiveOauthToken(target, targetAgent, email) {
139
- if (process.platform !== 'linux' || targetAgent !== 'claude' || target.kind !== 'installation')
140
- return;
141
- const versionHome = getVersionHomePath('claude', target.version);
142
- const tokenPath = path.join(versionHome, '.claude', '.oauth_token');
143
- // Resolve by the attached account's email when known (a freshly-seeded worker
144
- // home the `.claude.json` read below could not key on yet), else by the home's
145
- // own recorded identity for a re-point/detach.
146
- const token = email
147
- ? resolveClaudeSetupTokenForEmail(email, versionHome)
148
- : resolveClaudeSetupToken(versionHome);
149
- // A re-point (attach B over A, or a detach) can leave no setup-token resolving for
150
- // this version — B's may not be minted yet. A leftover file from the previous binding
151
- // would silently authenticate interactive runs as the OLD account (the shim's Linux
152
- // fallback reads it), so clear it rather than leave it stale.
153
- if (!token) {
154
- fs.rmSync(tokenPath, { force: true });
155
- return;
156
- }
157
- fs.mkdirSync(path.dirname(tokenPath), { recursive: true });
158
- fs.writeFileSync(tokenPath, token, { mode: 0o600 });
159
- }
160
66
  export function parseBundleKey(raw) {
161
67
  const colon = raw.indexOf(':');
162
68
  if (colon < 1 || colon === raw.length - 1)
@@ -252,107 +158,6 @@ function parseHarness(agentRaw) {
252
158
  throw new Error(`Unknown agent '${agentRaw}'.`);
253
159
  return agentRaw;
254
160
  }
255
- /**
256
- * Fold signed-in run candidates into distinct identities — the unit a label
257
- * binds to. The same account signed into several versions is ONE row, so a
258
- * multi-version single-account harness never demands a selector. Candidates
259
- * with no stable identity (no accountKey, no email) are dropped: there is
260
- * nothing durable for a label to bind to. Default-version identity first.
261
- */
262
- export function groupLabelIdentities(candidates, defaultVersion) {
263
- const byKey = new Map();
264
- for (const candidate of candidates) {
265
- const key = candidate.accountKey ?? candidate.email?.toLowerCase();
266
- if (!key)
267
- continue;
268
- const row = byKey.get(key) ?? { identityKey: key, email: null, versions: [], isDefault: false };
269
- row.email ??= candidate.email;
270
- row.versions.push(candidate.version);
271
- if (candidate.version === defaultVersion)
272
- row.isDefault = true;
273
- byKey.set(key, row);
274
- }
275
- return [...byKey.values()].sort((a, b) => a.isDefault !== b.isDefault ? (a.isDefault ? -1 : 1) : (a.email ?? a.identityKey).localeCompare(b.email ?? b.identityKey));
276
- }
277
- /**
278
- * Resolve which signed-in login a bare-harness `accounts label` call means.
279
- * `collect` is injectable (the `resolveRunVersion` pattern) so tests drive the
280
- * real selection path against fixture candidates.
281
- */
282
- export async function resolveLabelIdentity(agent, accountSelector, collect = collectRunCandidates) {
283
- const candidates = (await collect(agent)).filter(candidate => candidate.signedIn);
284
- const identities = groupLabelIdentities(candidates, getGlobalDefault(agent));
285
- if (identities.length === 0)
286
- throw new Error(`No signed-in ${agent} account with a stable identity. Run the harness and complete its login first.`);
287
- const needle = accountSelector?.toLowerCase();
288
- if (needle) {
289
- const match = identities.find(identity => identity.email?.toLowerCase() === needle || identity.identityKey.toLowerCase() === needle);
290
- if (!match)
291
- throw new Error(`Unknown ${agent} account '${accountSelector}'.`);
292
- return { kind: 'selected', identity: match };
293
- }
294
- if (identities.length === 1)
295
- return { kind: 'selected', identity: identities[0] };
296
- return { kind: 'ambiguous', identities };
297
- }
298
- /**
299
- * Body of `accounts label`, exported with the injectable candidate collector
300
- * so tests exercise everything but the TTY prompt itself.
301
- */
302
- export async function runAccountsLabel(source, label, opts, collect = collectRunCandidates) {
303
- if (source.includes('@')) {
304
- // <harness>@<version> pins the login by where it is signed in, so a
305
- // second selector can only contradict it.
306
- if (opts.account)
307
- throw new Error(`'${source}' already selects one login; drop --account.`);
308
- const identity = await nativeIdentityFromSource(source);
309
- const account = labelNativeAccount(identity.agent, identity.identityKey, identity.identityLabel, label, identity.scope);
310
- console.log(chalk.green(`Labeled ${identity.agent} account ${identity.identityLabel ?? identity.identityKey} as '${account.name}'.`));
311
- return;
312
- }
313
- const agent = parseHarness(source);
314
- assertNativeAccountNameable(agent);
315
- const selection = await resolveLabelIdentity(agent, opts.account, collect);
316
- let selected;
317
- if (selection.kind === 'selected') {
318
- selected = selection.identity;
319
- }
320
- else {
321
- if (!isInteractiveTerminal()) {
322
- requireInteractiveSelection(`Selecting the ${agent} login to label`, [
323
- `agents accounts label ${agent} ${label ?? '<label>'} --account <email|id>`,
324
- `agents accounts label ${agent}@<version> ${label ?? '<label>'}`,
325
- ]);
326
- }
327
- const picked = await pickLabelIdentity(agent, selection.identities);
328
- if (!picked)
329
- return;
330
- selected = picked;
331
- }
332
- const account = labelNativeAccount(agent, selected.identityKey, selected.email ?? undefined, label, nativeAccountCapability(agent).scope);
333
- console.log(chalk.green(`Labeled ${agent} account ${selected.email ?? selected.identityKey} as '${account.name}'.`));
334
- }
335
- /** Prompt for the login a label should bind to. A cancelled picker writes nothing. */
336
- async function pickLabelIdentity(agent, identities) {
337
- const idW = Math.max(0, ...identities.map(identity => (identity.email ?? identity.identityKey).length));
338
- const choices = identities.map(identity => ({
339
- name: [
340
- (identity.email ?? identity.identityKey).padEnd(idW),
341
- identity.isDefault ? chalk.green('default') : ' ',
342
- chalk.gray(identity.versions.join(', ')),
343
- ].join(' '),
344
- value: identity.identityKey,
345
- }));
346
- try {
347
- const key = await select({ message: `Select the ${agent} login to label:`, choices, loop: false });
348
- return identities.find(identity => identity.identityKey === key) ?? null;
349
- }
350
- catch (err) {
351
- if (isPromptCancelled(err))
352
- return null;
353
- throw err;
354
- }
355
- }
356
161
  /** Every account (native + provider) registered/usable for this harness, oldest-first by name. */
357
162
  function accountsForHarness(agent) {
358
163
  const meta = readMeta();
@@ -362,12 +167,12 @@ function accountsForHarness(agent) {
362
167
  .map(account => ({ ...account, kind: 'provider' }));
363
168
  return [...native, ...providers].sort((a, b) => a.name.localeCompare(b.name));
364
169
  }
365
- /** Named accounts that `set-default` / `switch` can pin for this harness. */
170
+ /** Named accounts that `accounts default` can pin for this harness. */
366
171
  export async function listSwitchableAccounts(agent) {
367
172
  return accountsForHarness(agent);
368
173
  }
369
174
  /**
370
- * Pin the per-harness default. Shared by `accounts set-default` and `accounts switch`.
175
+ * Pin the per-harness default — the `accounts default` write path.
371
176
  * Provider accounts must authenticate the harness; native accounts must belong to it.
372
177
  */
373
178
  export function setDefaultAccount(agentRaw, name) {
@@ -418,7 +223,7 @@ async function switchAccountRows(agent) {
418
223
  };
419
224
  });
420
225
  }
421
- export async function runAccountsSwitch(harness, accountName, opts) {
226
+ export async function runAccountsDefault(harness, accountName, opts) {
422
227
  const agent = parseHarness(harness);
423
228
  const rows = await switchAccountRows(agent);
424
229
  if (accountName) {
@@ -556,7 +361,6 @@ agents accounts list --json
556
361
  agents accounts list --fleet`,
557
362
  notes: 'One row per account per harness: the account name, its usage bars (`*` stale), then nothing at all when the account is healthy. A state worth acting on trails the row — `rate-limited` (the usage snapshot via `deriveUsageStatusFromSnapshot`/`applyUsageHonesty`, never a probe 429), `expired`/`revoked`/`missing`, partial fleet coverage (`usable on 3 of 5 boxes`), then the exact repair command. `live`, `unverified`, `ready` and `per-device` are the ordinary cases and print nothing. For the identity behind an account use `agents accounts view <name>`; for per-device state use `--fleet`; for everything use `--json`. Reserved credential stores are not listed here; `agents secrets` is the place those show.',
558
363
  });
559
- registerMintCommand(accounts, undefined, { hidden: true });
560
364
  const addCmd = accounts.command('add <target> [name]')
561
365
  .description('Add an account. Harness form: add <harness> [name] runs the native login in a fresh credential slot and provisions workers. Provider form: add <name> --provider <p> --auth <t> stores a durable credential.')
562
366
  .option('--api-key <key>', 'Worker credential for api-key harnesses (codex/grok/cursor/opencode); prompted when omitted')
@@ -658,13 +462,13 @@ agents accounts login kimi#main # per-device: logs THIS bo
658
462
  .description('Set the fleet-wide default account for a harness (picker when no name)')
659
463
  .option('--json', 'Machine-readable account list or the resulting default')
660
464
  .action(async (harness, name, o, command) => {
661
- await runAccountsAction(command, () => runAccountsSwitch(harness, name, { json: !!(o.json || command.optsWithGlobals().json) }));
465
+ await runAccountsAction(command, () => runAccountsDefault(harness, name, { json: !!(o.json || command.optsWithGlobals().json) }));
662
466
  });
663
467
  setHelpSections(defaultCmd, {
664
468
  examples: `agents accounts default claude
665
469
  agents accounts default claude work
666
470
  agents accounts default claude --json`,
667
- notes: 'The one write path for the per-harness default (the hidden set-default/switch share it). Pass a name to skip the picker. Rotation already honors the default.',
471
+ notes: 'The one write path for the per-harness default. Pass a name to skip the picker. Rotation already honors the default.',
668
472
  });
669
473
  const migrateCmd = accounts.command('migrate')
670
474
  .description('Fold leftover per-account installations into 1 harness install + N credential slots')
@@ -732,24 +536,6 @@ agents accounts migrate --apply
732
536
  agents accounts migrate --dry-run --device worker-1`,
733
537
  notes: 'Folds leftover per-account installations (the old connect homes) into one managed install plus a HOME-shaped slot per identity. Empty logged-out homes and duplicate identities go to trash (agents trash restore reverses). A busy home is deferred, never moved. --apply is required to write; this upgrade only prints the dry-run report. Native OAuth files stay on this device inside the moved home.',
734
538
  });
735
- // Hidden legacy verb: connect is the old spelling of add.
736
- const connectCmd = accounts.command('connect <harness> [name]', { hidden: true })
737
- .description('Legacy alias of accounts add')
738
- .option('--json', 'Machine-readable result')
739
- .action(async (harness, name, o, command) => {
740
- await runAccountsAction(command, async () => {
741
- const agent = parseHarness(harness);
742
- const reason = addRefusal(agent);
743
- if (reason)
744
- throw new Error(reason);
745
- const result = await runAdd(agent, name, {
746
- meta: readMeta(),
747
- onProgress: (m) => console.log(chalk.gray(` ${m}`)),
748
- });
749
- printAddResult(result, !!(o.json || command.optsWithGlobals().json));
750
- });
751
- });
752
- retireVerb(connectCmd, 'agents accounts add <harness> [name]');
753
539
  accounts.command('set-key <name>')
754
540
  .description('Rotate an account credential without changing its identity')
755
541
  .option('--from-secrets <bundle:key>', 'Import from an existing agents secrets entry')
@@ -810,123 +596,6 @@ agents accounts migrate --dry-run --device worker-1`,
810
596
  agents accounts view codex#icloud`,
811
597
  notes: 'Native names are unique per harness. A bare name that exists for several harnesses is refused — pick one with <harness>#<name>.',
812
598
  });
813
- retireVerb(accounts.command('name <source> <name>', { hidden: true })
814
- .description('Name a signed-in native installation without copying its OAuth credentials')
815
- .action(async (source, name, _o, command) => {
816
- await runAccountsAction(command, async () => {
817
- const identity = await nativeIdentityFromSource(source);
818
- const account = addNativeAccount(name, identity.agent, identity.identityKey, identity.identityLabel, identity.scope);
819
- console.log(chalk.green(`Named ${source} as ${account.name}.`));
820
- if (account.scope === 'device')
821
- console.log(chalk.gray(`${identity.agent} authentication is device-scoped; attach '${account.name}' to '${identity.agent}', not an individual version.`));
822
- });
823
- }), 'agents accounts add <harness> <name> (names are chosen at add time)');
824
- const labelCmd = accounts.command('label <source> [label]', { hidden: true })
825
- .description('Label a native login by harness or <harness>@<version>; the label binds to the account identity, not the version')
826
- .option('--account <email-or-id>', 'Native identity to label when the harness has multiple logins')
827
- .action(async (source, label, o, command) => {
828
- await runAccountsAction(command, () => runAccountsLabel(source, label, o));
829
- });
830
- retireVerb(labelCmd, 'agents accounts add <harness> <name> (rename: agents accounts rename <harness>#<old> <new>)');
831
- setHelpSections(labelCmd, {
832
- examples: `agents accounts label codex work
833
- agents accounts label codex@0.146.0 personal
834
- agents accounts label codex work --account you@example.com
835
- agents run codex#work`,
836
- 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. Labels live on the central account rows in agents.yaml, which `agents repo push/pull` already syncs fleet-wide, keyed by (agent, identityKey). One signed-in login needs no selector; with several, an interactive terminal opens a picker, while scripts pass --account or <harness>@<version>.',
837
- });
838
- retireVerb(accounts.command('attach <account> <target>', { hidden: true })
839
- .description('Attach a named account to a native installation or custom harness')
840
- .action(async (name, target, _o, command) => {
841
- await runAccountsAction(command, async () => {
842
- const meta = readMeta();
843
- // Validate the target exists before mutating any binding — and resolve it
844
- // FIRST so the account lookup can be scoped to the harness being attached
845
- // to. `identityLabel` defaults to the login's email, so a bare identity
846
- // (`muqsitnawaz@gmail.com`) matches every harness that identity is signed
847
- // into; un-scoped this resolved whichever row the store ordered first and
848
- // then rejected it below as "is a <other> login".
849
- const t = classifyAttachTarget(target);
850
- const targetAgent = t.kind === 'profile' ? t.profile.host.agent : t.agent;
851
- const account = findUnifiedAccount(name, meta, undefined, targetAgent);
852
- if (!account)
853
- throw new Error(`Unknown account '${name}'.`);
854
- if (account.kind === 'native')
855
- assertNativeAccountNameable(account.agent);
856
- if (account.kind === 'native') {
857
- // A provider-backed profile injects provider env at spawn, which would run
858
- // under a different credential than the native identity claims — refuse it.
859
- if (t.kind === 'profile' && t.profile.provider) {
860
- throw new Error(`Custom harness '${target}' is provider-backed (${t.profile.provider}); it cannot host the native login account '${account.name}'. Attach a matching provider account instead.`);
861
- }
862
- if (targetAgent !== account.agent)
863
- throw new Error(`Account '${account.name}' is a ${account.agent} login; '${target}' runs ${targetAgent}.`);
864
- if (account.scope === 'device') {
865
- if (t.kind !== 'device-agent')
866
- throw new Error(`${account.agent} authentication is device-scoped. Attach it with 'agents accounts attach ${account.name} ${account.agent}'.`);
867
- }
868
- else {
869
- if (t.kind !== 'installation')
870
- throw new Error(`${account.agent} authentication is per-version. Attach '${account.name}' to a specific ${account.agent}@<version>.`);
871
- const versionHome = getVersionHomePath(t.agent, t.version);
872
- // The literal email keys the account's `auth`-bundle setup-token. It lives
873
- // in `identityLabel` — `identityKey` is a synthetic composite
874
- // (`claude:account=<uuid>:org=<uuid>`, agents.ts nativeIdentityKey), never
875
- // the address, so it must NOT be used to derive the token key.
876
- const accountEmail = account.identityLabel;
877
- // Headless-worker bootstrap: a keychain-less Linux worker home never had
878
- // an interactive login, so its `.claude.json` carries no identity and
879
- // `nativeIdentityFromSource` would reject the attach — yet the account's
880
- // non-rotating setup-token is already fleet-synced in the `auth` bundle.
881
- // Seed the identity (email only, no rotating credential) so the token
882
- // resolves; `writeClaudeInteractiveOauthToken` then writes `.oauth_token`.
883
- if (process.platform === 'linux' &&
884
- account.agent === 'claude' &&
885
- accountEmail &&
886
- !readClaudeAccountEmail(versionHome) &&
887
- resolveClaudeSetupTokenForEmail(accountEmail)) {
888
- seedClaudeWorkerHomeIdentity(versionHome, accountEmail);
889
- }
890
- else {
891
- const identity = await nativeIdentityFromSource(target);
892
- if (identity.identityKey !== account.identityKey)
893
- throw new Error(`'${target}' is signed in to a different identity than account '${account.name}'.`);
894
- }
895
- }
896
- }
897
- else {
898
- // Provider account: it must be able to authenticate the target's harness.
899
- getAccountProvider(account.provider).envFor(targetAgent, account.auth);
900
- }
901
- bindAccount(name, target, targetAgent);
902
- writeClaudeInteractiveOauthToken(t, targetAgent, account.kind === 'native' && account.agent === 'claude' ? account.identityLabel : undefined);
903
- console.log(chalk.green(`Attached ${account.name} to ${target}.`));
904
- });
905
- }), 'agents run <harness>#<name> (accounts select by name; installation bindings are legacy)');
906
- retireVerb(accounts.command('detach <account> <target>', { hidden: true })
907
- .description('Remove one account attachment')
908
- .action(async (name, target, _o, command) => {
909
- await runAccountsAction(command, () => {
910
- // Classify the target first so the unbind is scoped to the right harness
911
- // for a colliding identity selector (same as the attach path).
912
- let targetAgent;
913
- try {
914
- const t = classifyAttachTarget(target);
915
- targetAgent = t.kind === 'profile' ? t.profile.host.agent : t.agent;
916
- }
917
- catch { /* an unresolvable target still unbinds by whatever row matches */ }
918
- unbindAccount(name, target, targetAgent);
919
- // With the binding gone, no setup-token resolves for this version home, so this
920
- // clears any .oauth_token the attach left behind (else interactive runs would keep
921
- // authenticating as the just-detached account).
922
- try {
923
- const t = classifyAttachTarget(target);
924
- writeClaudeInteractiveOauthToken(t, t.kind === 'profile' ? t.profile.host.agent : t.agent);
925
- }
926
- catch { /* an unresolvable target has no version home to clean */ }
927
- console.log(chalk.green(`Detached ${name} from ${target}.`));
928
- });
929
- }), 'agents run <harness>#<name> (accounts select by name; installation bindings are legacy)');
930
599
  const renameCmd = accounts.command('rename <old> <new>')
931
600
  .description('Rename an account without changing its stable id. Target may be <harness>#<name> when the name exists for several harnesses')
932
601
  .action(async (oldName, newName, _o, command) => {
@@ -946,21 +615,6 @@ agents accounts rename codex#icloud cloud`,
946
615
  examples: `agents accounts remove claude#icloud`,
947
616
  notes: 'Native names are unique per harness. A bare name that exists for several harnesses is refused — pick one with <harness>#<name>.',
948
617
  });
949
- retireVerb(accounts.command('set-default <agent> <name>', { hidden: true })
950
- .description('Use this account for a harness when --account is omitted')
951
- .action(async (agentRaw, name, _o, command) => {
952
- await runAccountsAction(command, () => {
953
- const { agent, account } = setDefaultAccount(agentRaw, name);
954
- console.log(chalk.green(`${agent} now uses account '${account.name}' unless --account overrides it.`));
955
- });
956
- }), 'agents accounts default <harness> <name>');
957
- const switchCmd = accounts.command('switch <harness> [account]', { hidden: true })
958
- .description('Pick the default account for a harness')
959
- .option('--json', 'Machine-readable account list or the resulting default')
960
- .action(async (harness, account, o, command) => {
961
- await runAccountsAction(command, () => runAccountsSwitch(harness, account, { json: !!(o.json || command.optsWithGlobals().json) }));
962
- });
963
- retireVerb(switchCmd, 'agents accounts default <harness> [name]');
964
618
  accounts.command('clear-default <agent>')
965
619
  .description('Return a harness to native login or balanced account selection')
966
620
  .action(async (agentRaw, _o, command) => {
@@ -1048,13 +702,6 @@ agents accounts rename codex#icloud cloud`,
1048
702
  console.log(chalk.green(`${account.name} synced to ${device} (${result.keyCount} keys, ${remoteBackend} backend, policy never).`));
1049
703
  });
1050
704
  });
1051
- setHelpSections(switchCmd, {
1052
- examples: `agents accounts switch claude
1053
- agents accounts switch claude work
1054
- agents accounts switch claude --json
1055
- agents run claude`,
1056
- 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.',
1057
- });
1058
705
  setHelpSections(accounts, {
1059
706
  examples: `agents accounts add claude work
1060
707
  agents accounts add codex personal --api-key sk-…
@@ -1070,6 +717,6 @@ agents run claude#work
1070
717
  agents accounts logout claude
1071
718
  agents accounts migrate --dry-run
1072
719
  agents accounts migrate --apply`,
1073
- notes: 'An account is a credential SLOT, not an installation: `accounts add <harness> [name]` runs the native login in a fresh HOME-shaped slot of the one managed install, registers the fleet-wide row, and mints the durable worker credential (claude: setup-token; codex/grok/cursor/opencode: --api-key or a prompt) in one step. Headed devices only — workers are provisioned automatically. `accounts login <harness>#<name>` re-auths into the same slot and re-mints; `accounts default <harness> [name]` is the one default write path. 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`). Leftover per-account installations fold with `agents accounts migrate --dry-run|--apply`. Synced vault unlock is `agents secrets vault unlock`. The legacy connect/name/label/mint/attach/detach/switch/set-default verbs still work this release and print their replacement.',
720
+ notes: 'An account is a credential SLOT, not an installation: `accounts add <harness> [name]` runs the native login in a fresh HOME-shaped slot of the one managed install, registers the fleet-wide row, and mints the durable worker credential (claude: setup-token; codex/grok/cursor/opencode: --api-key or a prompt) in one step. Headed devices only — workers are provisioned automatically. `accounts login <harness>#<name>` re-auths into the same slot and re-mints; `accounts default <harness> [name]` is the one default write path. 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`). Leftover per-account installations fold with `agents accounts migrate --dry-run|--apply`. Synced vault unlock is `agents secrets vault unlock`.',
1074
721
  });
1075
722
  }
@@ -16,7 +16,7 @@ import { addEventsReadOptions, runEventsCommand } from './events.js';
16
16
  export function registerAuditCommands(events) {
17
17
  const audit = addEventsReadOptions(events
18
18
  .command('audit')
19
- .description('Alias of `agents events --include runs` — dispatched-run outcomes'), false)
19
+ .description('Alias of `agents events --include runs` — dispatched-run outcomes'))
20
20
  .addHelpText('after', `
21
21
  Examples:
22
22
  agents events audit Same as: agents events --include runs
@@ -2,7 +2,6 @@ import chalk from 'chalk';
2
2
  import { PhoenixApiError, clearSession, createSpace, createSpaceInvite, fetchWhoAmI, listSpaceMembers, listSpaces, pollDeviceToken, readSession, refreshSessionAvatar, removeSpaceMember, resolveMemberFromList, resolveSpaceFromList, slugify, startDeviceAuthorization, updateSpaceMemberRole, writeSession, } from '../lib/identity/index.js';
3
3
  import { setHelpSections } from '../lib/help.js';
4
4
  import { runOrDie } from '../lib/format.js';
5
- import { registerMintCommand } from './auth-mint.js';
6
5
  /**
7
6
  * `agents auth` — sign in to Phoenix ID, the account layer behind team spaces.
8
7
  * Signing in is optional: every local feature works with no account. Everything
@@ -126,7 +125,6 @@ Harness worker credentials are minted by \`agents accounts add <harness> [name]\
126
125
  const json = !!o.json || !!command.optsWithGlobals().json;
127
126
  return runOrDie(() => whoami(json), { json });
128
127
  });
129
- registerMintCommand(auth, undefined, { hidden: true });
130
128
  auth
131
129
  .command('logout')
132
130
  .description("Clear this machine's session (no other device is affected)")
@@ -1,7 +1,60 @@
1
1
  import { Command } from 'commander';
2
2
  import { type BrowserProfile } from '../lib/browser/profiles.js';
3
3
  import { listInstalledBrowsers } from '../lib/browser/chrome.js';
4
+ import type { DeviceProfile } from '../lib/devices/registry.js';
4
5
  export declare function remoteStartTaskName(stdout: string, explicit?: string): string | undefined;
6
+ /**
7
+ * Fetch an absolute remote file into a private local path, bounded.
8
+ *
9
+ * Bounded for real, not by a pre-flight `stat`: the byte budget is enforced on the
10
+ * stream as it arrives (`sshExecRawStream` + `cat`), so a file that grows between
11
+ * a size check and the copy — or a peer that lies about the size — still cannot
12
+ * spend more than the budget. `scp` could not do this; it would transfer
13
+ * everything before this process got a chance to refuse.
14
+ *
15
+ * The local file is written 0600 inside a 0700 directory, because a capture can
16
+ * show anything that was on the peer's screen and a world-readable temp path
17
+ * would publish it to every local user.
18
+ */
19
+ /**
20
+ * The command that streams one file's RAW BYTES to stdout on the device's shell.
21
+ *
22
+ * Platform-correct, not one command with a portable-looking name. On Windows
23
+ * `cat` is an alias for `Get-Content`, which is a TEXT reader: it decodes the
24
+ * file, splits it into lines, and re-encodes on output — so a screenshot arrives
25
+ * corrupted rather than merely reordered. The .NET path opens a `FileStream` and
26
+ * copies it into the raw stdout handle, bypassing PowerShell's object/text
27
+ * pipeline entirely, which is the only way binary survives.
28
+ */
29
+ export declare function remoteReadFileArgv(remotePath: string, shell: DeviceProfile['shell']): string[];
30
+ /**
31
+ * Largest capture `browser show --device` will pull. A capture is a screenshot,
32
+ * a PDF or a short recording; anything past this is not something a viewer should
33
+ * stream over ssh, and the cap is what makes the transfer BOUNDED rather than
34
+ * "however big the peer's file happens to be".
35
+ */
36
+ export declare const REMOTE_VIEW_MAX_BYTES: number;
37
+ /**
38
+ * Fetch an absolute remote file into a private local path, bounded.
39
+ *
40
+ * Auth is the CANONICAL device path, not a hand-rolled ssh: `buildSshInvocation`
41
+ * supplies the askpass shim for a password-auth box, `-i`/`IdentitiesOnly` for an
42
+ * explicit identity file, and the managed known-hosts pinning. Building the ssh
43
+ * args here instead meant a password-auth device could not be reached at all and
44
+ * an explicit `identityFile` was silently ignored.
45
+ *
46
+ * Bounded for real, not by a pre-flight `stat`: the byte budget is enforced on the
47
+ * stream as it arrives, so a file that grows between a size check and the copy — or
48
+ * a peer that misreports its size — still cannot spend more than the budget.
49
+ *
50
+ * The local file is written 0600 inside a 0700 directory, because a capture can
51
+ * show anything that was on the peer's screen and a world-readable temp path
52
+ * would publish it to every local user.
53
+ */
54
+ export declare function fetchRemoteFileForViewing(device: string, remotePath: string, localPath: string, opts?: {
55
+ maxBytes?: number;
56
+ timeoutMs?: number;
57
+ }): Promise<void>;
5
58
  /**
6
59
  * The fleet browser hub this box drives by default (`browser.device`), or
7
60
  * undefined to drive locally. It is one fleet-synced value in the central