@phnx-labs/agents-cli 1.22.53 → 1.22.55

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 (166) hide show
  1. package/CHANGELOG.md +208 -0
  2. package/README.md +59 -9
  3. package/dist/bootstrap.js +55 -154
  4. package/dist/cli/command-registry.d.ts +5 -0
  5. package/dist/cli/command-registry.js +8 -1
  6. package/dist/commands/accounts.js +219 -173
  7. package/dist/commands/apply.js +6 -3
  8. package/dist/commands/auth-mint.d.ts +8 -0
  9. package/dist/commands/auth-mint.js +96 -0
  10. package/dist/commands/auth.js +5 -1
  11. package/dist/commands/browser.js +1 -1
  12. package/dist/commands/cost.js +8 -2
  13. package/dist/commands/daemon.js +2 -2
  14. package/dist/commands/doctor.js +6 -1
  15. package/dist/commands/exec.js +10 -8
  16. package/dist/commands/focus.d.ts +1 -0
  17. package/dist/commands/focus.js +2 -2
  18. package/dist/commands/go.d.ts +5 -4
  19. package/dist/commands/go.js +7 -7
  20. package/dist/commands/insights.js +9 -0
  21. package/dist/commands/monitors.js +85 -30
  22. package/dist/commands/output.js +8 -2
  23. package/dist/commands/repo.js +18 -0
  24. package/dist/commands/routines.js +31 -2
  25. package/dist/commands/secrets.js +33 -14
  26. package/dist/commands/sessions.d.ts +20 -12
  27. package/dist/commands/sessions.js +64 -20
  28. package/dist/commands/setup-accounts.d.ts +8 -0
  29. package/dist/commands/setup-accounts.js +47 -0
  30. package/dist/commands/setup.d.ts +1 -1
  31. package/dist/commands/setup.js +11 -2
  32. package/dist/commands/share.d.ts +79 -3
  33. package/dist/commands/share.js +347 -18
  34. package/dist/commands/ssh.d.ts +7 -0
  35. package/dist/commands/ssh.js +18 -2
  36. package/dist/commands/status.js +14 -0
  37. package/dist/commands/view.d.ts +11 -1
  38. package/dist/commands/view.js +35 -7
  39. package/dist/lib/account-registry.js +15 -3
  40. package/dist/lib/accounting/rotate.d.ts +20 -6
  41. package/dist/lib/accounting/rotate.js +38 -7
  42. package/dist/lib/accounting/usage.d.ts +68 -1
  43. package/dist/lib/accounting/usage.js +116 -10
  44. package/dist/lib/agent-spec/agents.d.ts +5 -2
  45. package/dist/lib/agent-spec/agents.js +25 -7
  46. package/dist/lib/analytics/mix-commands.js +12 -6
  47. package/dist/lib/auth-mint.d.ts +150 -0
  48. package/dist/lib/auth-mint.js +434 -0
  49. package/dist/lib/browser/cdp.d.ts +1 -1
  50. package/dist/lib/browser/cdp.js +1 -1
  51. package/dist/lib/browser/ffmpeg.d.ts +12 -0
  52. package/dist/lib/browser/ffmpeg.js +184 -0
  53. package/dist/lib/browser/remote-control.d.ts +9 -7
  54. package/dist/lib/browser/remote-control.js +9 -7
  55. package/dist/lib/browser/service.js +119 -25
  56. package/dist/lib/claude-account-token.d.ts +10 -0
  57. package/dist/lib/claude-account-token.js +14 -4
  58. package/dist/lib/config-drift.d.ts +37 -0
  59. package/dist/lib/config-drift.js +72 -0
  60. package/dist/lib/daemon/auth-sync-service.d.ts +19 -0
  61. package/dist/lib/daemon/auth-sync-service.js +34 -0
  62. package/dist/lib/daemon/browser-task-reap-service.d.ts +14 -0
  63. package/dist/lib/daemon/browser-task-reap-service.js +26 -0
  64. package/dist/lib/daemon/daemon.js +87 -176
  65. package/dist/lib/daemon/heartbeat-service.d.ts +13 -0
  66. package/dist/lib/daemon/heartbeat-service.js +26 -0
  67. package/dist/lib/daemon/monitor-engine-service.d.ts +9 -5
  68. package/dist/lib/daemon/monitor-engine-service.js +15 -7
  69. package/dist/lib/daemon/runner.d.ts +15 -0
  70. package/dist/lib/daemon/runner.js +23 -0
  71. package/dist/lib/daemon/secrets-broker-service.d.ts +5 -4
  72. package/dist/lib/daemon/secrets-broker-service.js +17 -32
  73. package/dist/lib/daemon/service.d.ts +2 -2
  74. package/dist/lib/daemon/service.js +1 -1
  75. package/dist/lib/daemon/session-state-service.d.ts +21 -0
  76. package/dist/lib/daemon/session-state-service.js +34 -0
  77. package/dist/lib/daemon/supervisor.d.ts +17 -7
  78. package/dist/lib/daemon/supervisor.js +87 -14
  79. package/dist/lib/daemon/tmux-reap-service.d.ts +11 -0
  80. package/dist/lib/daemon/tmux-reap-service.js +28 -0
  81. package/dist/lib/daemon/webhook-receiver-service.d.ts +9 -0
  82. package/dist/lib/daemon/webhook-receiver-service.js +17 -0
  83. package/dist/lib/daemon-services.d.ts +1 -1
  84. package/dist/lib/daemon-services.js +25 -0
  85. package/dist/lib/device-config.d.ts +3 -3
  86. package/dist/lib/device-config.js +5 -5
  87. package/dist/lib/devices/connect.d.ts +26 -0
  88. package/dist/lib/devices/connect.js +48 -1
  89. package/dist/lib/devices/doctor-findings.d.ts +5 -1
  90. package/dist/lib/devices/doctor-findings.js +19 -1
  91. package/dist/lib/devices/harness-inventory.js +5 -2
  92. package/dist/lib/exec.d.ts +28 -0
  93. package/dist/lib/exec.js +73 -7
  94. package/dist/lib/feed/feed.d.ts +1 -1
  95. package/dist/lib/feed/feed.js +23 -1
  96. package/dist/lib/feed-broadcast.js +1 -1
  97. package/dist/lib/fleet/apply.d.ts +11 -0
  98. package/dist/lib/fleet/apply.js +23 -3
  99. package/dist/lib/fleet/auth-sync.js +5 -3
  100. package/dist/lib/help.d.ts +9 -0
  101. package/dist/lib/help.js +29 -1
  102. package/dist/lib/hosts/passthrough.d.ts +1 -10
  103. package/dist/lib/hosts/passthrough.js +1 -13
  104. package/dist/lib/installations/versions.js +9 -1
  105. package/dist/lib/linux-userns.d.ts +58 -0
  106. package/dist/lib/linux-userns.js +116 -0
  107. package/dist/lib/memory.d.ts +26 -0
  108. package/dist/lib/memory.js +80 -1
  109. package/dist/lib/monitors/config.d.ts +11 -0
  110. package/dist/lib/monitors/config.js +8 -0
  111. package/dist/lib/monitors/engine.d.ts +5 -1
  112. package/dist/lib/monitors/engine.js +13 -4
  113. package/dist/lib/monitors/state.d.ts +37 -1
  114. package/dist/lib/monitors/state.js +79 -4
  115. package/dist/lib/permissions-registry.d.ts +2 -0
  116. package/dist/lib/permissions-registry.js +116 -14
  117. package/dist/lib/permissions.d.ts +5 -3
  118. package/dist/lib/permissions.js +25 -27
  119. package/dist/lib/profiles.d.ts +8 -7
  120. package/dist/lib/profiles.js +12 -0
  121. package/dist/lib/project-key.d.ts +9 -0
  122. package/dist/lib/project-key.js +11 -0
  123. package/dist/lib/scheduling/routines.d.ts +47 -0
  124. package/dist/lib/scheduling/routines.js +70 -1
  125. package/dist/lib/secrets/bundles.d.ts +35 -0
  126. package/dist/lib/secrets/bundles.js +78 -1
  127. package/dist/lib/secrets/push.d.ts +3 -8
  128. package/dist/lib/secrets/push.js +18 -14
  129. package/dist/lib/secrets/remote.d.ts +9 -18
  130. package/dist/lib/secrets/remote.js +11 -26
  131. package/dist/lib/secrets/reserved-sync.d.ts +65 -0
  132. package/dist/lib/secrets/reserved-sync.js +129 -0
  133. package/dist/lib/self-heal/checks/hook-manifest.d.ts +2 -0
  134. package/dist/lib/self-heal/checks/hook-manifest.js +56 -0
  135. package/dist/lib/self-heal/registry.js +4 -0
  136. package/dist/lib/self-heal/types.d.ts +1 -1
  137. package/dist/lib/session/active.js +1 -4
  138. package/dist/lib/session/db.d.ts +41 -5
  139. package/dist/lib/session/db.js +132 -30
  140. package/dist/lib/session/discover.d.ts +32 -4
  141. package/dist/lib/session/discover.js +119 -25
  142. package/dist/lib/session/insights.d.ts +14 -0
  143. package/dist/lib/session/insights.js +25 -2
  144. package/dist/lib/session/linear.js +1 -1
  145. package/dist/lib/session/shell-programs.d.ts +17 -0
  146. package/dist/lib/session/shell-programs.js +21 -0
  147. package/dist/lib/session/state.js +2 -1
  148. package/dist/lib/session/stream-render.js +2 -1
  149. package/dist/lib/session/tool-calls.js +2 -5
  150. package/dist/lib/session/trajectory-html.js +2 -1
  151. package/dist/lib/session/trajectory.js +3 -12
  152. package/dist/lib/session/types.d.ts +8 -0
  153. package/dist/lib/share/capture.js +11 -2
  154. package/dist/lib/share/publish.d.ts +56 -5
  155. package/dist/lib/share/publish.js +126 -18
  156. package/dist/lib/share/worker-template.d.ts +3 -12
  157. package/dist/lib/share/worker-template.js +860 -59
  158. package/dist/lib/startup/root-command.js +2 -1
  159. package/dist/lib/state.d.ts +16 -0
  160. package/dist/lib/state.js +178 -46
  161. package/dist/lib/sync-status.d.ts +4 -0
  162. package/dist/lib/sync-status.js +3 -0
  163. package/dist/lib/traces/classify.js +24 -19
  164. package/dist/lib/usage-refresh.js +2 -1
  165. package/dist/lib/view-types.d.ts +7 -0
  166. package/package.json +9 -2
@@ -42,8 +42,14 @@ The burn is split into input / cache-read / cache-write where the harness record
42
42
  (Claude/Codex/Gemini/Droid). --pricing no-cache reprices cached tokens at the input rate,
43
43
  so you can see what caching is saving. --json always carries both actual and no-cache costs.
44
44
  `)
45
- .action(async (options) => {
46
- await outputAction(options);
45
+ // Read opts via optsWithGlobals(): `--json`/`--since`/`--by` collide by name
46
+ // with the `insights` parent's own options, so commander binds them to the
47
+ // parent at parse time and the leaf's plain opts() never sees them. Merging
48
+ // ancestor opts is what the sibling `insights mix` recipes already do
49
+ // (mix-commands.ts) — without it every flag on this command is silently
50
+ // dropped (e.g. `agents insights output --json` printed the human table).
51
+ .action(async (_options, command) => {
52
+ await outputAction(command.optsWithGlobals());
47
53
  });
48
54
  }
49
55
  function resolveGroup(by) {
@@ -1052,6 +1052,14 @@ export function registerRepoCommands(program) {
1052
1052
  ].filter(Boolean);
1053
1053
  if (parts.length > 0)
1054
1054
  console.log(chalk.gray(`Device policy: ${parts.join(' · ')}`));
1055
+ const { syncReservedAuthBundle } = await import('../lib/secrets/reserved-sync.js');
1056
+ const auth = syncReservedAuthBundle();
1057
+ if (auth.pushed.length > 0) {
1058
+ console.log(chalk.gray(`Auth bundle: pushed to ${auth.pushed.join(', ')}`));
1059
+ }
1060
+ for (const err of auth.errors) {
1061
+ console.error(chalk.yellow(`Auth bundle: ${err.device}: ${err.message}`));
1062
+ }
1055
1063
  }
1056
1064
  });
1057
1065
  repoCmd
@@ -1091,6 +1099,16 @@ export function registerRepoCommands(program) {
1091
1099
  const result = await commitAndPush(t.dir, options.message);
1092
1100
  if (result.success) {
1093
1101
  spinner.succeed(`${formatRepoTarget(t.alias, t.dir, result.branch)}: ${result.detail ?? 'pushed'}`);
1102
+ if (t.alias === 'user') {
1103
+ const { syncReservedAuthBundle } = await import('../lib/secrets/reserved-sync.js');
1104
+ const auth = syncReservedAuthBundle();
1105
+ if (auth.pushed.length > 0) {
1106
+ console.log(chalk.gray(`Auth bundle: pushed to ${auth.pushed.join(', ')}`));
1107
+ }
1108
+ for (const err of auth.errors) {
1109
+ console.error(chalk.yellow(`Auth bundle: ${err.device}: ${err.message}`));
1110
+ }
1111
+ }
1094
1112
  }
1095
1113
  else {
1096
1114
  spinner.fail(`${formatRepoTarget(t.alias, t.dir)}: ${result.error}`);
@@ -16,7 +16,7 @@ import { parseAgentVersionSpec, isAgentHardDeprecated, hardDeprecationError, ROU
16
16
  import { isCustomHarnessName } from '../lib/profiles.js';
17
17
  import { RUN_STRATEGIES, normalizeRunStrategy } from '../lib/accounting/rotate.js';
18
18
  import { formatRepoLink, REPO_DISPLAY_MAX } from '../lib/routines-format.js';
19
- import { listJobs as listAllJobs, deleteJob, readJob, validateJob, writeJob, setJobEnabled, listRuns, routineStats, getLatestRun, getRunDir, getJobPath, isCanonicalRoutineSource, parseAtTime, hasCompletedOneShotRun, isOneShotLikeSchedule, isOneShotRoutine, jobRunsOnThisDevice, checkJobDeviceEligibility, normalizeTriggerEvent, parseHostStrategy, resolveHostStrategy, HOST_STRATEGIES, computeProjectGroupKind, projectGroupKey, projectGroupTitle, projectGroupOrder, normalizeProjects, buildRoutineListJson, fireConditionLabel, nextRunForDisplay, nextRunLabel, localLatestRun, listJobsForDisplay, } from '../lib/scheduling/routines.js';
19
+ import { listJobs as listAllJobs, deleteJob, readJob, validateJob, writeJob, setJobEnabled, listRuns, routineStats, getLatestRun, getRunDir, getJobPath, isCanonicalRoutineSource, parseAtTime, hasCompletedOneShotRun, isOneShotLikeSchedule, isOneShotRoutine, jobRunsOnThisDevice, checkJobDeviceEligibility, normalizeTriggerEvent, parseHostStrategy, resolveHostStrategy, HOST_STRATEGIES, computeProjectGroupKind, projectGroupKey, projectGroupTitle, projectGroupOrder, normalizeProjects, buildRoutineListJson, buildRoutineStatusRows, fireConditionLabel, nextRunForDisplay, nextRunLabel, localLatestRun, listJobsForDisplay, } from '../lib/scheduling/routines.js';
20
20
  export { buildRoutineListJson } from '../lib/scheduling/routines.js';
21
21
  import { listProjectDefs, isSafeProjectName } from '../lib/projects.js';
22
22
  import { evaluateActivationReadinessLive } from '../lib/routine-readiness.js';
@@ -831,6 +831,9 @@ export function registerRoutinesCommands(program) {
831
831
 
832
832
  # Check whether the scheduler is running
833
833
  agents routines status
834
+
835
+ # Machine-readable scheduler + per-routine status: owner device, last fire, last error, in-flight run
836
+ agents routines status --json
834
837
  `,
835
838
  notes: `
836
839
  A routine is a YAML file that schedules an agent invocation. It specifies:
@@ -2148,12 +2151,38 @@ export function registerRoutinesCommands(program) {
2148
2151
  routinesCmd
2149
2152
  .command('status')
2150
2153
  .description('Show scheduler status, enabled routines, and when each one fires next.')
2151
- .action(() => {
2154
+ .option('--json', 'Emit machine-readable scheduler + per-routine status (owner device, last fire, last error, in-flight run)')
2155
+ .action((options) => {
2152
2156
  try {
2153
2157
  monitorRunningJobs();
2154
2158
  }
2155
2159
  catch { /* best-effort orphan reap */ }
2156
2160
  const status = getDaemonStatus();
2161
+ // The daemon-owned status surface (PHNX-3215): scheduler truth plus, per
2162
+ // routine, its single owner device, last fire outcome + error, and any
2163
+ // in-flight spawn — the fields `list --json` (definition-shaped) does not
2164
+ // carry. Emitted from THIS device, so per-device fields read as this box's.
2165
+ if (options.json) {
2166
+ const rows = buildRoutineStatusRows();
2167
+ writeJson({
2168
+ device: machineId(),
2169
+ scheduler: {
2170
+ state: status.state,
2171
+ pid: status.pid,
2172
+ binaryPath: status.binaryPath,
2173
+ heartbeat: status.heartbeat
2174
+ ? {
2175
+ lastTick: status.heartbeat.lastTick,
2176
+ ageSec: Math.round((Date.now() - Date.parse(status.heartbeat.lastTick)) / 1000),
2177
+ }
2178
+ : null,
2179
+ enabled: rows.filter((r) => r.enabled).length,
2180
+ total: rows.length,
2181
+ },
2182
+ routines: rows,
2183
+ });
2184
+ return;
2185
+ }
2157
2186
  console.log(chalk.bold('Scheduler\n'));
2158
2187
  const stateLabel = status.state === 'running'
2159
2188
  ? chalk.green('running')
@@ -18,7 +18,7 @@ import { ensureDaemonStarted, isDaemonRunning } from '../lib/daemon/daemon.js';
18
18
  import { parseHostsOption, remoteResolveEnv, remoteSecretsRaw, remoteSecretsStream, resolveHostSshTarget, } from '../lib/secrets/remote.js';
19
19
  import { resolveBundleForPush, pushResolvedBundleToHost, bundleEnvToDotenv } from '../lib/secrets/push.js';
20
20
  export { bundleEnvToDotenv };
21
- import { bundleBackend, bundleExists, bundleItemStore, bundlePolicy, deleteBundle, describeBundle, keychainItemsForBundle, keychainRef, listBundles, healKeychainBundleMetadataAclOnce, isHeadlessSecretsContext, isAgentInvocationContext, migrateLegacyBundles, parseDotenv, readAndResolveBundleEnv, readBundle, readBundleIfDecryptable, reAclBundleItems, renameBundle, rotateBundleSecret, sanitizeProcessEnv, validateBundleName, validateEnvKey, validateExpiresFutureDated, validateSecretType, writeBundle, writeBundleWithItems, SECRET_TYPES, } from '../lib/secrets/bundles.js';
21
+ import { bundleBackend, bundleExists, bundleItemStore, bundlePolicy, deleteBundle, describeBundle, keychainItemsForBundle, keychainRef, listBundles, healKeychainBundleMetadataAclOnce, isHeadlessSecretsContext, isAgentInvocationContext, migrateLegacyBundles, parseDotenv, readAndResolveBundleEnv, readBundle, readBundleIfDecryptable, reAclBundleItems, renameBundle, rotateBundleSecret, sanitizeProcessEnv, validateBundleName, validateEnvKey, validateExpiresFutureDated, validateSecretType, writeBundle, writeBundleWithItems, assertFileBundleDecryptable, isReservedBundleName, ReservedBundleWrongBackendError, SECRET_TYPES, } from '../lib/secrets/bundles.js';
22
22
  import { parseListFilters, bundleMatchesFilter, bundleExpiry, filterIsActive, describeFilter, parseSortField, sortBundles, SORT_FIELDS, REF_KINDS, DEFAULT_EXPIRING_DAYS, } from '../lib/secrets/list-filter.js';
23
23
  import { encryptForFallback, decryptForFallback } from '../lib/secrets/filestore.js';
24
24
  import { getKeychainToken, hasKeychainToken, secretsKeychainItem, setKeychainToken, maybeAutoRekey, } from '../lib/secrets/index.js';
@@ -846,7 +846,12 @@ export function renderExpiringCol(b, now = Date.now()) {
846
846
  * exists to describe. `--force` already means "overwrite what is there".
847
847
  */
848
848
  export function resolveImportBundle(name, backendOpt, synced = false, force = false) {
849
- const requestedBackend = synced ? 'vault' : resolveBackendOpt(backendOpt);
849
+ if (isReservedBundleName(name) && (synced || (backendOpt && backendOpt !== 'file'))) {
850
+ throw new ReservedBundleWrongBackendError(name, synced ? 'vault' : resolveBackendOpt(backendOpt));
851
+ }
852
+ const requestedBackend = isReservedBundleName(name)
853
+ ? 'file'
854
+ : (synced ? 'vault' : resolveBackendOpt(backendOpt));
850
855
  if (bundleExists(name)) {
851
856
  // readBundleIfDecryptable nulls ONLY on BundleUndecryptableError; a locked
852
857
  // keychain or logged-out vault still throws, so a recoverable state can
@@ -892,6 +897,9 @@ function applyEnvToBundle(bundle, env, opts) {
892
897
  else {
893
898
  writeBundle(bundle);
894
899
  }
900
+ if (added > 0) {
901
+ assertFileBundleDecryptable(bundle.name, Object.keys(env).filter((k) => k in bundle.vars));
902
+ }
895
903
  return { added, skipped };
896
904
  }
897
905
  /** Register the `agents secrets` command tree. */
@@ -1581,7 +1589,16 @@ export function registerSecretsCommands(program) {
1581
1589
  // inherits the configured default (`hold`) instead of being pinned.
1582
1590
  const policyOpt = opts.policy ?? opts.tier;
1583
1591
  const policy = policyOpt ? parsePolicyOpt(policyOpt) : undefined;
1584
- const backend = opts.synced ? 'vault' : resolveBackendOpt(opts.backend);
1592
+ let backend = opts.synced ? 'vault' : resolveBackendOpt(opts.backend);
1593
+ if (isReservedBundleName(resolvedName)) {
1594
+ // Reserved `auth` is file-backed by construction. Omitting --backend
1595
+ // still forces file even when agents.yaml defaults to keychain; an
1596
+ // explicit non-file backend fails loud (SEC-GAP-3).
1597
+ if (opts.synced || (opts.backend && opts.backend !== 'file')) {
1598
+ throw new ReservedBundleWrongBackendError(resolvedName, backend);
1599
+ }
1600
+ backend = 'file';
1601
+ }
1585
1602
  if (bundleExists(resolvedName) && !opts.force) {
1586
1603
  console.error(chalk.red(`Bundle '${resolvedName}' already exists. Use --force to overwrite.`));
1587
1604
  process.exit(1);
@@ -2134,7 +2151,7 @@ Examples:
2134
2151
  .option('--to-1password', 'Push every key in the bundle as a PASSWORD item in a 1Password vault')
2135
2152
  .option('--vault <name>', '1Password vault name (used with --to-1password)')
2136
2153
  .option('--device <target...>', 'Push the bundle over SSH to this device (host alias or user@host); repeatable for multiple machines')
2137
- .option('--remote-backend <backend>', 'Backend for the bundle on the remote device (with --device): keychain (default) or file. file is headless-readable via the remote\'s machine-local key; it forwards AGENTS_SECRETS_PASSPHRASE over stdin only if set (opt-in).', 'keychain')
2154
+ .option('--remote-backend <backend>', 'Backend for the bundle on the remote device (with --device): keychain (default) or file. file is headless-readable via the remote\'s machine-local key and never forwards AGENTS_SECRETS_PASSPHRASE.', 'keychain')
2138
2155
  .option('--force', 'Overwrite existing keys/items on the target (used with --to-1password and --device)')
2139
2156
  .option('--to-file <path>', `Write the bundle as an AES-256-GCM encrypted offline file (needs ${SYNC_PASSPHRASE_ENV}; symmetric counterpart of import --from-file)`)
2140
2157
  .action(async (bundleName, opts) => {
@@ -2170,15 +2187,18 @@ Examples:
2170
2187
  console.error(chalk.red("--remote-backend vault is not supported; use 'keychain' or 'file'."));
2171
2188
  process.exit(1);
2172
2189
  }
2173
- const remoteBackend = parsedBackend;
2174
- // For a file-backed remote bundle a passphrase is OPTIONAL. The file
2175
- // store is passphrase-free by default: with AGENTS_SECRETS_PASSPHRASE
2176
- // unset the remote `import --backend file` auto-provisions the remote's
2177
- // own machine-local key (0600 under ~/.agents/.secrets-key/), so reads
2178
- // are HEADLESS. We forward the LOCAL AGENTS_SECRETS_PASSPHRASE only when
2179
- // the operator opts in by setting it. Forcing a shared passphrase would
2180
- // defeat headless reads, so we no longer require one.
2181
- const remotePassphrase = remoteBackend === 'file' ? (process.env.AGENTS_SECRETS_PASSPHRASE ?? '') : '';
2190
+ let remoteBackend = parsedBackend;
2191
+ if (isReservedBundleName(resolvedBundleName)) {
2192
+ if (opts.remoteBackend && opts.remoteBackend !== 'file') {
2193
+ throw new ReservedBundleWrongBackendError(resolvedBundleName, parsedBackend);
2194
+ }
2195
+ remoteBackend = 'file';
2196
+ }
2197
+ // File-backend export NEVER forwards AGENTS_SECRETS_PASSPHRASE
2198
+ // (PHNX-2371). Forwarding it keys the remote ciphertext to a secret
2199
+ // the destination daemon does not hold, so later headless reads fail
2200
+ // while import still prints "Imported N key(s)". The remote
2201
+ // auto-provisions its own machine-local key instead.
2182
2202
  // Resolve ONCE for N hosts — reading a bundle can prompt, and doing it
2183
2203
  // per host would prompt per host.
2184
2204
  //
@@ -2197,7 +2217,6 @@ Examples:
2197
2217
  const out = pushResolvedBundleToHost(resolvedForPush, resolvedBundleName, host, {
2198
2218
  remoteBackend,
2199
2219
  force: opts.force,
2200
- passphrase: remotePassphrase,
2201
2220
  operation: 'ssh export',
2202
2221
  });
2203
2222
  if (!out.ok) {
@@ -676,7 +676,7 @@ export declare function formatPickerLabel(s: SessionMeta, query: string, cols?:
676
676
  * so it stays fixed across the picker's re-renders within a single run.
677
677
  */
678
678
  export declare function formatPickerTip(sessions: SessionMeta[]): string;
679
- export declare function pickSessionInteractive(sessions: SessionMeta[], message?: string, initialSearch?: string, hiddenCount?: number, enterHint?: string): Promise<PickedSession | null>;
679
+ export declare function pickSessionInteractive(sessions: SessionMeta[], message?: string, initialSearch?: string, hiddenCount?: number, enterHint?: string, scope?: SessionSearchScope): Promise<PickedSession | null>;
680
680
  /**
681
681
  * Row-id prefix for a live session whose agent has not reported a session id yet
682
682
  * (a booting harness, a queued teammate). The browser lists these so nothing live
@@ -736,6 +736,16 @@ interface AgentFilter {
736
736
  * alias and single-typo rules. Kept pure so focus and the browser cannot drift. */
737
737
  export declare function resolveSessionAgentName(name: string): SessionAgentId | null;
738
738
  export declare function parseAgentFilter(agentName?: string): AgentFilter;
739
+ /**
740
+ * Explicit `--agent` / `--project` / `--routine` flags. Distinct from the
741
+ * listing pool (cwd-scoped, default-capped) that PHNX-2767 hydrates past:
742
+ * these flags MUST still exclude FTS hits after that union.
743
+ */
744
+ export type SessionSearchScope = {
745
+ agent?: string;
746
+ project?: string;
747
+ routine?: boolean | string;
748
+ };
739
749
  /**
740
750
  * How a `sessions <query>` argument was resolved against the pool.
741
751
  *
@@ -759,29 +769,27 @@ export interface SessionQueryResolution {
759
769
  */
760
770
  export declare function resolveSessionQuery(pool: SessionMeta[], query: string, options?: {
761
771
  indexFallback?: boolean;
772
+ scope?: SessionSearchScope;
762
773
  }): SessionQueryResolution;
763
774
  /** Honest result of a fleet sweep that found nothing: it says what the sweep
764
775
  * actually did — how many peers answered, which were unreachable — instead of
765
776
  * dead-ending on "search with --device <host>" after the search already ran. */
766
777
  export declare function fleetNotFoundMessage(query: string, deviceCount: number, unreachable: string[]): string[];
767
778
  /** Filter and rank sessions by a multi-term search query across metadata and content. */
768
- export declare function filterSessionsByQuery(sessions: SessionMeta[], query: string | undefined): SessionMeta[];
779
+ export declare function filterSessionsByQuery(sessions: SessionMeta[], query: string | undefined, scope?: SessionSearchScope): SessionMeta[];
769
780
  /**
770
781
  * Narrow a session list by --project and --agent before search resolution.
771
782
  * Without this, a query like "scoped search" could match sessions in BOTH
772
783
  * the project you specified AND elsewhere, producing an ambiguity error
773
784
  * even though the user already pointed at the correct scope.
785
+ *
786
+ * PHNX-2767 hydrates FTS hits missing from the listing pool *after* this
787
+ * runs, so callers that search must also pass the same scope into
788
+ * `filterSessionsByQuery` / `scopedContentIndex` or the union
789
+ * reintroduces the rows this function just dropped.
774
790
  */
775
- export declare function applyScopeFilters(sessions: SessionMeta[], scope: {
776
- agent?: string;
777
- project?: string;
778
- routine?: boolean | string;
779
- }): SessionMeta[];
780
- export declare function artifactLookupScope(agent?: string, project?: string, routine?: boolean | string): {
781
- agent?: string;
782
- project?: string;
783
- routine?: boolean | string;
784
- };
791
+ export declare function applyScopeFilters(sessions: SessionMeta[], scope: SessionSearchScope): SessionMeta[];
792
+ export declare function artifactLookupScope(agent?: string, project?: string, routine?: boolean | string): SessionSearchScope;
785
793
  /**
786
794
  * Whether a missed local id lookup should widen to a cross-machine sweep.
787
795
  *
@@ -1978,7 +1978,7 @@ export async function renderSessionPreview(query, scope) {
1978
1978
  if (outcome.kind !== 'resolved'
1979
1979
  && (!scope.hosts?.length || shouldIncludeLocal(scope.hosts, machineId()))) {
1980
1980
  const discovered = applyScopeFilters(await discoverSessions({ all: true, cwd: process.cwd(), limit: 5000, waitForScan: true }), scope);
1981
- const localMatches = resolveSessionQuery(discovered, query, { indexFallback: false }).matches
1981
+ const localMatches = resolveSessionQuery(discovered, query, { indexFallback: false, scope }).matches
1982
1982
  .map(session => ({ ...session, machine: session.machine || machineId() }));
1983
1983
  const exact = localMatches.find(session => selectorAllowsEarlyExit(query)
1984
1984
  && session.id.toLowerCase() === query.trim().toLowerCase());
@@ -2674,7 +2674,11 @@ limitSource) {
2674
2674
  if (toolEvidenceMode) {
2675
2675
  const self = toolSelf;
2676
2676
  const selectedSessions = searchQuery
2677
- ? filterSessionsByQuery(sessions, searchQuery)
2677
+ ? filterSessionsByQuery(sessions, searchQuery, {
2678
+ agent: options.agent,
2679
+ project: options.project,
2680
+ routine: options.routine,
2681
+ })
2678
2682
  : sessions;
2679
2683
  const localSessions = selectedSessions;
2680
2684
  const mayFanOut = options.local !== true && process.env[NO_FANOUT_ENV] !== '1';
@@ -2771,7 +2775,9 @@ limitSource) {
2771
2775
  // that merely MENTIONS the id, defeating exact remote resolution. A genuine
2772
2776
  // search phrase keeps the ranked metadata+content path.
2773
2777
  const filtered = searchQuery
2774
- ? resolveSessionQuery(sessions, searchQuery).matches
2778
+ ? resolveSessionQuery(sessions, searchQuery, {
2779
+ scope: { agent: options.agent, project: options.project, routine: options.routine },
2780
+ }).matches
2775
2781
  : sessions;
2776
2782
  // JSON is the canonical picker contract: enrich the durable rows once
2777
2783
  // from the shared live cache so every consumer gets lifecycle/recovery
@@ -2880,7 +2886,7 @@ limitSource) {
2880
2886
  const message = pathFilter
2881
2887
  ? `Search sessions (${path.basename(pathFilter)}):`
2882
2888
  : formatSearchMessage(options);
2883
- const picked = await pickSessionInteractive(sessions, message, searchQuery, hiddenCount);
2889
+ const picked = await pickSessionInteractive(sessions, message, searchQuery, hiddenCount, undefined, { agent: options.agent, project: options.project, routine: options.routine });
2884
2890
  if (picked) {
2885
2891
  await handlePickedSession(picked);
2886
2892
  return;
@@ -2888,7 +2894,13 @@ limitSource) {
2888
2894
  return;
2889
2895
  }
2890
2896
  // Non-interactive fallback (piped output) or --flat/--tree.
2891
- const filtered = searchQuery ? filterSessionsByQuery(sessions, searchQuery) : sessions;
2897
+ const filtered = searchQuery
2898
+ ? filterSessionsByQuery(sessions, searchQuery, {
2899
+ agent: options.agent,
2900
+ project: options.project,
2901
+ routine: options.routine,
2902
+ })
2903
+ : sessions;
2892
2904
  const liveIndex = await maybeLiveIndex(options);
2893
2905
  printSessionTable(filtered, hiddenCount, options.tree === true, liveIndex);
2894
2906
  // Every listing path must say what it dropped — a hidden default that stays
@@ -3764,7 +3776,7 @@ const PICKER_TIPS = [
3764
3776
  export function formatPickerTip(sessions) {
3765
3777
  return chalk.gray(PICKER_TIPS[sessions.length % PICKER_TIPS.length]);
3766
3778
  }
3767
- export async function pickSessionInteractive(sessions, message = 'Search sessions:', initialSearch, hiddenCount = 0, enterHint) {
3779
+ export async function pickSessionInteractive(sessions, message = 'Search sessions:', initialSearch, hiddenCount = 0, enterHint, scope) {
3768
3780
  // The hidden-session footer is console.log'd above the Inquirer prompt, so it
3769
3781
  // scrolls the viewport the picker can't measure; tell the picker to reserve for
3770
3782
  // it (see pickerPageSize) so the preview and the footer stay on screen together.
@@ -3784,7 +3796,7 @@ export async function pickSessionInteractive(sessions, message = 'Search session
3784
3796
  // Typing: search the full pool.
3785
3797
  if (!query.trim())
3786
3798
  return sessions;
3787
- return filterSessionsByQuery(sessions, query);
3799
+ return filterSessionsByQuery(sessions, query, scope);
3788
3800
  },
3789
3801
  labelFor: (s, query) => formatPickerLabel(s, query, cols),
3790
3802
  pageSize: PICKER_RECENT_COUNT,
@@ -4179,7 +4191,7 @@ export function resolveSessionQuery(pool, query, options = {}) {
4179
4191
  const matches = options.indexFallback === false ? [] : findSessionsById(normalized);
4180
4192
  return { matches, byId: true, completeId };
4181
4193
  }
4182
- return { matches: filterSessionsByQuery(pool, normalized), byId: false, completeId };
4194
+ return { matches: filterSessionsByQuery(pool, normalized, options.scope), byId: false, completeId };
4183
4195
  }
4184
4196
  /** Explain an ambiguous resolution. Only a short id can be lengthened: a complete
4185
4197
  * id is already maximal, and a search phrase was never an id to begin with. */
@@ -4218,7 +4230,7 @@ export function fleetNotFoundMessage(query, deviceCount, unreachable) {
4218
4230
  return lines;
4219
4231
  }
4220
4232
  /** Filter and rank sessions by a multi-term search query across metadata and content. */
4221
- export function filterSessionsByQuery(sessions, query) {
4233
+ export function filterSessionsByQuery(sessions, query, scope) {
4222
4234
  const trimmed = query?.trim().toLowerCase() || '';
4223
4235
  if (!trimmed)
4224
4236
  return sessions;
@@ -4228,7 +4240,9 @@ export function filterSessionsByQuery(sessions, query) {
4228
4240
  return sessions.filter((session) => session.agent === agent && session.version === version);
4229
4241
  }
4230
4242
  const terms = trimmed.split(/\s+/).filter(Boolean);
4231
- const contentIndex = searchContentIndex(sessions, trimmed);
4243
+ // Hydrate FTS hits missing from the pool (PHNX-2767), then drop hits that
4244
+ // fail --project/--agent/--routine so those flags are not undone by the union.
4245
+ const contentIndex = scopedContentIndex(sessions, trimmed, scope);
4232
4246
  // If the query exactly matches a session label, short-circuit the structural
4233
4247
  // scorer (which would otherwise surface every session whose topic happens to
4234
4248
  // contain the same words) and return only the label hits.
@@ -4237,7 +4251,15 @@ export function filterSessionsByQuery(sessions, query) {
4237
4251
  if (exactLabelHits.length > 0) {
4238
4252
  return exactLabelHits.sort((a, b) => (b._bm25Score ?? 0) - (a._bm25Score ?? 0));
4239
4253
  }
4240
- return sessions
4254
+ // The listing pool is a page of the index. FTS hits the whole index. Union
4255
+ // so a grep-visible transcript is returned even when it missed the page
4256
+ // (PHNX-2767).
4257
+ const poolById = new Map(sessions.map(s => [s.id, s]));
4258
+ for (const [id, hit] of contentIndex) {
4259
+ if (!poolById.has(id))
4260
+ poolById.set(id, hit);
4261
+ }
4262
+ return [...poolById.values()]
4241
4263
  .map(session => ({ session, score: scoreSessionQuery(session, terms) }))
4242
4264
  .filter(entry => {
4243
4265
  // Include if scored by topic/project/etc, or matched by content search
@@ -4314,6 +4336,11 @@ function scoreSessionQuery(session, terms) {
4314
4336
  * Without this, a query like "scoped search" could match sessions in BOTH
4315
4337
  * the project you specified AND elsewhere, producing an ambiguity error
4316
4338
  * even though the user already pointed at the correct scope.
4339
+ *
4340
+ * PHNX-2767 hydrates FTS hits missing from the listing pool *after* this
4341
+ * runs, so callers that search must also pass the same scope into
4342
+ * `filterSessionsByQuery` / `scopedContentIndex` or the union
4343
+ * reintroduces the rows this function just dropped.
4317
4344
  */
4318
4345
  export function applyScopeFilters(sessions, scope) {
4319
4346
  let filtered = sessions;
@@ -4350,6 +4377,23 @@ export function applyScopeFilters(sessions, scope) {
4350
4377
  }
4351
4378
  return filtered;
4352
4379
  }
4380
+ /**
4381
+ * PHNX-2767 hydrates FTS hits missing from the listing pool. That union is
4382
+ * unscoped on purpose (cwd/limit are a page of the index, not a filter).
4383
+ * `--project` / `--agent` / `--routine` ARE filters: drop hydrated hits that
4384
+ * fail them so content search cannot undo a scope the user already set.
4385
+ */
4386
+ function scopedContentIndex(sessions, query, scope) {
4387
+ const hits = searchContentIndex(sessions, query);
4388
+ if (!scope || (!scope.agent && !scope.project && !scope.routine))
4389
+ return hits;
4390
+ const kept = new Map();
4391
+ for (const [id, session] of hits) {
4392
+ if (applyScopeFilters([session], scope).length > 0)
4393
+ kept.set(id, session);
4394
+ }
4395
+ return kept;
4396
+ }
4353
4397
  export function artifactLookupScope(agent, project, routine) {
4354
4398
  return { agent, project, routine };
4355
4399
  }
@@ -4365,7 +4409,7 @@ async function renderArtifactsGlobal(query, listAll, name, scope) {
4365
4409
  });
4366
4410
  tracker.stop();
4367
4411
  const allSessions = applyScopeFilters(discovered, scope);
4368
- const { matches: queryMatches, byId, completeId } = resolveSessionQuery(allSessions, query);
4412
+ const { matches: queryMatches, byId, completeId } = resolveSessionQuery(allSessions, query, { scope });
4369
4413
  if (queryMatches.length === 0) {
4370
4414
  spinner.stop();
4371
4415
  if (byId)
@@ -4462,7 +4506,7 @@ async function renderOneSession(query, mode, scope) {
4462
4506
  tracker.stop();
4463
4507
  const allSessions = applyScopeFilters(discovered, scope);
4464
4508
  let session;
4465
- const resolution = resolveSessionQuery(allSessions, query);
4509
+ const resolution = resolveSessionQuery(allSessions, query, { scope });
4466
4510
  let queryMatches = resolution.matches;
4467
4511
  let byId = resolution.byId;
4468
4512
  const completeId = resolution.completeId;
@@ -4474,7 +4518,7 @@ async function renderOneSession(query, mode, scope) {
4474
4518
  // echoes the id in a resume prompt. Gate on looksLikeSessionId, not just
4475
4519
  // completeId, so a short id resolves to "no match" rather than fuzzy content.
4476
4520
  if (queryMatches.length === 0 && !looksLikeSessionId(query)) {
4477
- const contentResults = searchContentIndex(allSessions, query);
4521
+ const contentResults = scopedContentIndex(allSessions, query, scope);
4478
4522
  if (contentResults.size > 0) {
4479
4523
  const matchedSessions = Array.from(contentResults.values())
4480
4524
  .sort((a, b) => (b._bm25Score ?? 0) - (a._bm25Score ?? 0));
@@ -4669,16 +4713,16 @@ export function fleetCandidatesByQuery(rows, query, trustResolvedRows = false) {
4669
4713
  });
4670
4714
  }
4671
4715
  /** Resolve through the canonical metadata+content union used by keyword search. */
4672
- function resolveIndexedMetadataRows(indexed, selector) {
4716
+ function resolveIndexedMetadataRows(indexed, selector, scope) {
4673
4717
  const alias = resolveSessionAlias(selector);
4674
4718
  if (alias.kind === 'resolved') {
4675
- return resolveSessionQuery(indexed, alias.sessionId, { indexFallback: false }).matches;
4719
+ return resolveSessionQuery(indexed, alias.sessionId, { indexFallback: false, scope }).matches;
4676
4720
  }
4677
4721
  if (alias.kind === 'ambiguous') {
4678
4722
  const ids = new Set(alias.sessionIds.map(id => id.toLowerCase()));
4679
4723
  return indexed.filter(session => ids.has(session.id.toLowerCase()));
4680
4724
  }
4681
- return resolveSessionQuery(indexed, selector, { indexFallback: false }).matches;
4725
+ return resolveSessionQuery(indexed, selector, { indexFallback: false, scope }).matches;
4682
4726
  }
4683
4727
  /**
4684
4728
  * ID-shaped selectors go straight to SQLite's primary-key/short-id lookup.
@@ -4789,7 +4833,7 @@ export async function computeLocalMetadataMatches(selector, scope, deps = {}) {
4789
4833
  const includeLocal = !scope.hosts?.length || shouldIncludeLocal(scope.hosts, localMachine);
4790
4834
  if (!includeLocal)
4791
4835
  return [];
4792
- const indexed = resolveIndexedMetadataRows(indexedRowsForSelector(selector, scope), selector);
4836
+ const indexed = resolveIndexedMetadataRows(indexedRowsForSelector(selector, scope), selector, scope);
4793
4837
  if (indexed.length > 0 || !looksLikeSessionId(selector)) {
4794
4838
  return indexed.map(session => ({ ...session, machine: session.machine || localMachine }));
4795
4839
  }
@@ -4806,7 +4850,7 @@ export async function computeLocalMetadataMatches(selector, scope, deps = {}) {
4806
4850
  */
4807
4851
  export async function liveMetadataMatches(selector, scope, self, deps = {}) {
4808
4852
  const load = deps.loadActive ?? loadLocalActiveSessions;
4809
- const match = (metas) => resolveIndexedMetadataRows(applyScopeFilters(metas, scope), selector);
4853
+ const match = (metas) => resolveIndexedMetadataRows(applyScopeFilters(metas, scope), selector, scope);
4810
4854
  try {
4811
4855
  const cached = liveSessionMetas((await load()).sessions, self, Date.now());
4812
4856
  const hit = match(cached);
@@ -5023,7 +5067,7 @@ export function registerSessionsCommands(program) {
5023
5067
  .option('--preview', 'With a session id/query: print a compact preview and exit (no pager)');
5024
5068
  setHelpSections(sessionsCmd, {
5025
5069
  examples: `
5026
- # Search prior sessions in this project by topic, file path, or command
5070
+ # Search indexed transcripts by topic, file path, or command
5027
5071
  agents sessions "add auth middleware"
5028
5072
 
5029
5073
  # Read a session as markdown (user + assistant + thinking + tools)
@@ -0,0 +1,8 @@
1
+ /**
2
+ * `agents setup accounts` — mint a Claude setup-token so unattended usage/probe
3
+ * is not stuck on "usage pending". Delegates to the same mint engine as
4
+ * `agents accounts mint` / `agents auth mint`.
5
+ */
6
+ import type { Command } from 'commander';
7
+ export declare function runAccountsSetupWizard(): Promise<void>;
8
+ export declare function registerSetupAccountsCommand(setupCmd: Command): void;
@@ -0,0 +1,47 @@
1
+ import chalk from 'chalk';
2
+ import { confirm } from '@inquirer/prompts';
3
+ import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
4
+ import { hasMintedSetupToken, mintAndSeed } from '../lib/auth-mint.js';
5
+ export async function runAccountsSetupWizard() {
6
+ const status = hasMintedSetupToken();
7
+ if (status.ready) {
8
+ console.log(chalk.green(`Setup-token already minted (${status.detail}).`));
9
+ console.log(chalk.gray('Re-mint with: agents accounts mint claude'));
10
+ return;
11
+ }
12
+ if (!isInteractiveTerminal()) {
13
+ console.log(chalk.yellow('No Claude setup-token on this machine.'));
14
+ console.log(chalk.gray('Mint one with: agents accounts mint claude'));
15
+ console.log(chalk.gray('Already have a token: agents accounts mint claude --token-stdin'));
16
+ process.exitCode = 1;
17
+ return;
18
+ }
19
+ const proceed = await confirm({
20
+ message: 'Mint a Claude setup-token now? (opens a browser; seeds a named account + the reserved auth bundle)',
21
+ default: true,
22
+ });
23
+ if (!proceed) {
24
+ console.log(chalk.gray('Skipped. Run `agents accounts mint claude` when ready.'));
25
+ return;
26
+ }
27
+ const result = await mintAndSeed({ harness: 'claude' });
28
+ console.log(chalk.green(`Minted account '${result.account}' for ${result.email}.`));
29
+ }
30
+ export function registerSetupAccountsCommand(setupCmd) {
31
+ setupCmd
32
+ .command('accounts')
33
+ .description('Mint a Claude setup-token into a named account (unattended usage/probe).')
34
+ .action(async () => {
35
+ try {
36
+ await runAccountsSetupWizard();
37
+ }
38
+ catch (err) {
39
+ if (isPromptCancelled(err)) {
40
+ console.log(chalk.yellow('\nCancelled'));
41
+ return;
42
+ }
43
+ console.error(chalk.red(err instanceof Error ? err.message : String(err)));
44
+ process.exitCode = 1;
45
+ }
46
+ });
47
+ }
@@ -31,7 +31,7 @@ export declare function ensureInitialized(program: Command): Promise<void>;
31
31
  * wizard. Never throws — a cancel or an optional wizard's error just skips the
32
32
  * rest and lets core setup complete.
33
33
  */
34
- export type SetupPhase = 'browser' | 'computer' | 'share' | 'secrets' | 'fleet' | 'watchdog' | 'preferences';
34
+ export type SetupPhase = 'browser' | 'computer' | 'share' | 'secrets' | 'accounts' | 'fleet' | 'watchdog' | 'preferences';
35
35
  export type SetupStatusState = 'ready' | 'missing' | 'n/a';
36
36
  export interface SetupStatusRow {
37
37
  phase: 'core' | SetupPhase;
@@ -24,7 +24,9 @@ import { runShareWizard } from './artifacts-setup.js';
24
24
  import { registerSetupMineCommand } from './setup-mine.js';
25
25
  import { registerSetupSecretsCommand } from './setup-secrets.js';
26
26
  import { registerSetupFleetCommand } from './setup-fleet.js';
27
+ import { registerSetupAccountsCommand, runAccountsSetupWizard } from './setup-accounts.js';
27
28
  import { registerSetupWatchdogCommand, runWatchdogSetupWizard } from './setup-watchdog.js';
29
+ import { hasMintedSetupToken } from '../lib/auth-mint.js';
28
30
  import { registerAliasCommand } from './alias.js';
29
31
  import { registerBetaCommands } from './beta.js';
30
32
  import { runPreferencesStep } from './setup-preferences.js';
@@ -276,6 +278,7 @@ export async function getSetupStatus() {
276
278
  const devices = await loadDevices();
277
279
  const coreReady = isGitRepo(getAgentsDir());
278
280
  const secretsReady = fs.existsSync(setupSecretsPrefsPath());
281
+ const minted = hasMintedSetupToken();
279
282
  const shareConfig = readShareConfig();
280
283
  const watchdogEnabled = getConfigValue('watchdog.enabled').value === true;
281
284
  const interactiveHost = getConfigValue('interactive.host').value;
@@ -285,6 +288,7 @@ export async function getSetupStatus() {
285
288
  { phase: 'browser', state: browserReady ? 'ready' : 'missing', detail: browserReady ? `profile ${browserProfile.name}` : browserProfile ? `profile ${browserProfile.name} cannot launch here` : installedBrowsers.length ? 'no default profile' : 'no supported browser found' },
286
289
  { phase: 'computer', state: computerState, detail: computerState === 'ready' ? 'helper trusted' : computerState === 'n/a' ? 'macOS local setup only' : 'helper not running or not trusted' },
287
290
  { phase: 'secrets', state: secretsReady ? 'ready' : 'missing', detail: secretsReady ? 'defaults chosen' : 'defaults not chosen' },
291
+ { phase: 'accounts', state: minted.ready ? 'ready' : 'missing', detail: minted.detail },
288
292
  { phase: 'fleet', state: Object.keys(devices).length ? 'ready' : 'missing', detail: Object.keys(devices).length ? `${Object.keys(devices).length} device${Object.keys(devices).length === 1 ? '' : 's'} registered` : 'no devices registered' },
289
293
  { phase: 'share', state: shareConfig ? 'ready' : 'missing', detail: shareConfig?.baseUrl ?? 'endpoint not configured' },
290
294
  { phase: 'watchdog', state: watchdogEnabled ? 'ready' : 'missing', detail: watchdogEnabled ? 'enabled on this device' : 'disabled on this device' },
@@ -308,6 +312,8 @@ async function runSetupPhase(phase) {
308
312
  await runShareWizard();
309
313
  else if (phase === 'secrets')
310
314
  await import('./setup-secrets.js').then((m) => m.runSecretsSetupWizard());
315
+ else if (phase === 'accounts')
316
+ await runAccountsSetupWizard();
311
317
  else if (phase === 'fleet')
312
318
  await import('./setup-fleet.js').then((m) => m.runFleetSetupWizard());
313
319
  else if (phase === 'watchdog')
@@ -364,19 +370,20 @@ export function registerSetupCommand(program) {
364
370
  .description('Set up agents-cli, or re-open the capability onboarding hub.')
365
371
  .option('-f, --force', 'Re-run setup even if ~/.agents/.system/ already exists (use with caution)')
366
372
  .option('--no-system-repo', 'Skip cloning the system repo (you must populate ~/.agents/.system/ yourself)');
367
- // Capability subcommands: `agents setup browser|computer|mine|secrets|fleet|alias|beta`.
373
+ // Capability subcommands: `agents setup browser|computer|mine|secrets|accounts|fleet|alias|beta`.
368
374
  // Share/artifact publishing is set up by `agents artifacts setup` (RUSH-2580);
369
375
  // the hub below still offers it as a phase via runShareWizard.
370
376
  registerSetupBrowserCommand(setupCmd);
371
377
  registerSetupComputerCommand(setupCmd);
372
378
  registerSetupMineCommand(setupCmd);
373
379
  registerSetupSecretsCommand(setupCmd);
380
+ registerSetupAccountsCommand(setupCmd);
374
381
  registerSetupFleetCommand(setupCmd);
375
382
  registerSetupWatchdogCommand(setupCmd);
376
383
  registerAliasCommand(setupCmd);
377
384
  registerBetaCommands(setupCmd);
378
385
  setupCmd.command('status')
379
- .description('Show setup readiness for core, browser, computer, secrets, fleet, share, watchdog, and preferences.')
386
+ .description('Show setup readiness for core, browser, computer, secrets, accounts, fleet, share, watchdog, and preferences.')
380
387
  .option('--json', 'print machine-readable JSON')
381
388
  .action(async (options) => {
382
389
  const rows = await getSetupStatus();
@@ -399,6 +406,7 @@ export function registerSetupCommand(program) {
399
406
  agents setup browser
400
407
  agents setup computer
401
408
  agents setup secrets
409
+ agents setup accounts
402
410
  agents setup fleet
403
411
  agents setup watchdog
404
412
  agents setup alias add teams
@@ -416,6 +424,7 @@ export function registerSetupCommand(program) {
416
424
  agents setup computer # install the signed macOS helper + grant permissions
417
425
  agents artifacts setup # provision or join a Cloudflare share endpoint
418
426
  agents setup secrets # choose secrets backend/policy defaults + import
427
+ agents setup accounts # mint a Claude setup-token for unattended usage/probe
419
428
  agents setup fleet # discover Tailscale devices + configure SSH access
420
429
  agents setup watchdog # choose which devices run the daemon watchdog pass
421
430
  agents setup alias add teams # PATH shorthand: teams runs agents teams