@phnx-labs/agents-cli 1.20.72 → 1.20.73

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 (219) hide show
  1. package/CHANGELOG.md +299 -0
  2. package/README.md +40 -4
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/activity.d.ts +12 -0
  5. package/dist/commands/activity.js +68 -0
  6. package/dist/commands/apply.d.ts +13 -0
  7. package/dist/commands/apply.js +28 -3
  8. package/dist/commands/browser.js +1 -1
  9. package/dist/commands/cloud.js +21 -11
  10. package/dist/commands/doctor.js +8 -4
  11. package/dist/commands/events.d.ts +11 -10
  12. package/dist/commands/events.js +30 -21
  13. package/dist/commands/exec.d.ts +34 -0
  14. package/dist/commands/exec.js +337 -39
  15. package/dist/commands/feed.d.ts +13 -0
  16. package/dist/commands/feed.js +130 -29
  17. package/dist/commands/hq.d.ts +2 -0
  18. package/dist/commands/hq.js +58 -0
  19. package/dist/commands/lease.d.ts +19 -0
  20. package/dist/commands/lease.js +138 -11
  21. package/dist/commands/login.d.ts +2 -0
  22. package/dist/commands/login.js +123 -0
  23. package/dist/commands/logs.js +1 -1
  24. package/dist/commands/mine.d.ts +27 -0
  25. package/dist/commands/mine.js +207 -0
  26. package/dist/commands/monitors.js +79 -49
  27. package/dist/commands/plugins.js +1 -0
  28. package/dist/commands/profiles.js +158 -0
  29. package/dist/commands/repo.js +2 -60
  30. package/dist/commands/resources.d.ts +5 -0
  31. package/dist/commands/resources.js +95 -0
  32. package/dist/commands/routines.d.ts +2 -0
  33. package/dist/commands/routines.js +139 -37
  34. package/dist/commands/secrets.js +234 -47
  35. package/dist/commands/send.d.ts +15 -0
  36. package/dist/commands/send.js +62 -0
  37. package/dist/commands/sessions-tail.d.ts +1 -0
  38. package/dist/commands/sessions-tail.js +20 -9
  39. package/dist/commands/sessions.js +18 -3
  40. package/dist/commands/setup-mine.d.ts +18 -0
  41. package/dist/commands/setup-mine.js +106 -0
  42. package/dist/commands/setup-share.js +2 -2
  43. package/dist/commands/setup.js +3 -1
  44. package/dist/commands/share.d.ts +8 -1
  45. package/dist/commands/share.js +122 -37
  46. package/dist/commands/ssh.d.ts +9 -0
  47. package/dist/commands/ssh.js +196 -26
  48. package/dist/commands/sync.js +1 -1
  49. package/dist/commands/teams.js +11 -2
  50. package/dist/index.js +79 -10
  51. package/dist/lib/activity.d.ts +124 -0
  52. package/dist/lib/activity.js +542 -0
  53. package/dist/lib/agents.js +16 -5
  54. package/dist/lib/artifact-actions.d.ts +1 -1
  55. package/dist/lib/artifact-actions.js +1 -1
  56. package/dist/lib/ask-classifier.d.ts +2 -1
  57. package/dist/lib/ask-classifier.js +3 -3
  58. package/dist/lib/auth-health.d.ts +32 -4
  59. package/dist/lib/auth-health.js +40 -1
  60. package/dist/lib/auto-pull.js +8 -1
  61. package/dist/lib/brand.d.ts +40 -0
  62. package/dist/lib/brand.js +122 -0
  63. package/dist/lib/browser/drivers/ssh.js +4 -7
  64. package/dist/lib/browser/service.js +6 -8
  65. package/dist/lib/channels/providers/index.d.ts +2 -0
  66. package/dist/lib/channels/providers/index.js +20 -0
  67. package/dist/lib/channels/providers/mailbox.d.ts +2 -0
  68. package/dist/lib/channels/providers/mailbox.js +26 -0
  69. package/dist/lib/channels/providers/openclaw-telegram.d.ts +2 -0
  70. package/dist/lib/channels/providers/openclaw-telegram.js +34 -0
  71. package/dist/lib/channels/providers/rush.d.ts +6 -0
  72. package/dist/lib/channels/providers/rush.js +55 -0
  73. package/dist/lib/channels/registry.d.ts +42 -0
  74. package/dist/lib/channels/registry.js +20 -0
  75. package/dist/lib/channels/resolve.d.ts +11 -0
  76. package/dist/lib/channels/resolve.js +12 -0
  77. package/dist/lib/cli-resources.d.ts +18 -0
  78. package/dist/lib/cli-resources.js +53 -4
  79. package/dist/lib/cloud/codex.js +6 -3
  80. package/dist/lib/cloud/factory.d.ts +1 -0
  81. package/dist/lib/cloud/factory.js +10 -4
  82. package/dist/lib/cloud/rush.d.ts +3 -1
  83. package/dist/lib/cloud/rush.js +6 -1
  84. package/dist/lib/cloud/types.d.ts +2 -0
  85. package/dist/lib/codex-home.d.ts +35 -0
  86. package/dist/lib/codex-home.js +136 -0
  87. package/dist/lib/crabbox/cli.d.ts +44 -0
  88. package/dist/lib/crabbox/cli.js +120 -20
  89. package/dist/lib/crabbox/lease.d.ts +52 -1
  90. package/dist/lib/crabbox/lease.js +117 -16
  91. package/dist/lib/crabbox/progress.d.ts +31 -0
  92. package/dist/lib/crabbox/progress.js +76 -0
  93. package/dist/lib/crabbox/runtimes.d.ts +4 -0
  94. package/dist/lib/crabbox/runtimes.js +33 -8
  95. package/dist/lib/crabbox/setup-copy.d.ts +87 -0
  96. package/dist/lib/crabbox/setup-copy.js +127 -0
  97. package/dist/lib/daemon.d.ts +5 -5
  98. package/dist/lib/daemon.js +9 -9
  99. package/dist/lib/drive-sync.js +6 -3
  100. package/dist/lib/event-stream.d.ts +36 -0
  101. package/dist/lib/event-stream.js +68 -0
  102. package/dist/lib/events.d.ts +1 -1
  103. package/dist/lib/exec.d.ts +11 -0
  104. package/dist/lib/exec.js +23 -2
  105. package/dist/lib/feed-policy.d.ts +1 -1
  106. package/dist/lib/feed-policy.js +14 -9
  107. package/dist/lib/feed-ranking.d.ts +32 -0
  108. package/dist/lib/feed-ranking.js +224 -0
  109. package/dist/lib/feed.d.ts +35 -2
  110. package/dist/lib/feed.js +66 -3
  111. package/dist/lib/fleet/auth-sync.d.ts +53 -0
  112. package/dist/lib/fleet/auth-sync.js +92 -0
  113. package/dist/lib/fleet/remote-login.d.ts +187 -0
  114. package/dist/lib/fleet/remote-login.js +556 -0
  115. package/dist/lib/format.d.ts +30 -3
  116. package/dist/lib/format.js +34 -5
  117. package/dist/lib/fs-atomic.d.ts +7 -2
  118. package/dist/lib/fs-atomic.js +8 -12
  119. package/dist/lib/git.d.ts +16 -0
  120. package/dist/lib/git.js +79 -2
  121. package/dist/lib/heal.js +11 -3
  122. package/dist/lib/hosts/progress.d.ts +28 -10
  123. package/dist/lib/hosts/progress.js +79 -22
  124. package/dist/lib/hosts/remote-cmd.js +4 -0
  125. package/dist/lib/hq/floor.d.ts +87 -0
  126. package/dist/lib/hq/floor.js +226 -0
  127. package/dist/lib/menubar/install-menubar.js +14 -20
  128. package/dist/lib/notify.d.ts +1 -0
  129. package/dist/lib/notify.js +3 -3
  130. package/dist/lib/open-url.d.ts +2 -0
  131. package/dist/lib/open-url.js +19 -0
  132. package/dist/lib/openclaw-keychain.d.ts +56 -0
  133. package/dist/lib/openclaw-keychain.js +236 -0
  134. package/dist/lib/overdue.js +10 -0
  135. package/dist/lib/permissions.d.ts +44 -1
  136. package/dist/lib/permissions.js +284 -7
  137. package/dist/lib/project-launch.d.ts +6 -12
  138. package/dist/lib/project-launch.js +13 -228
  139. package/dist/lib/project-resources.d.ts +7 -0
  140. package/dist/lib/project-resources.js +291 -0
  141. package/dist/lib/pty-client.d.ts +27 -0
  142. package/dist/lib/pty-client.js +136 -10
  143. package/dist/lib/refresh.js +14 -10
  144. package/dist/lib/resource-profiles.d.ts +26 -0
  145. package/dist/lib/resource-profiles.js +157 -0
  146. package/dist/lib/resources/permissions.js +7 -1
  147. package/dist/lib/resources/types.d.ts +1 -1
  148. package/dist/lib/resources.d.ts +1 -1
  149. package/dist/lib/resources.js +32 -3
  150. package/dist/lib/routines.d.ts +11 -0
  151. package/dist/lib/routines.js +56 -15
  152. package/dist/lib/runner.d.ts +1 -0
  153. package/dist/lib/runner.js +66 -5
  154. package/dist/lib/secrets/bundles.d.ts +16 -3
  155. package/dist/lib/secrets/bundles.js +206 -37
  156. package/dist/lib/secrets/icloud-import.d.ts +2 -2
  157. package/dist/lib/secrets/icloud-import.js +9 -6
  158. package/dist/lib/secrets/mcp.js +1 -1
  159. package/dist/lib/secrets/vault-age-helper.d.ts +1 -0
  160. package/dist/lib/secrets/vault-age-helper.js +34 -0
  161. package/dist/lib/secrets/vault.d.ts +49 -0
  162. package/dist/lib/secrets/vault.js +397 -0
  163. package/dist/lib/self-heal/checks/path.js +3 -1
  164. package/dist/lib/self-heal/checks/shadowing.js +10 -2
  165. package/dist/lib/self-heal/checks/shims.js +19 -11
  166. package/dist/lib/session/cloud.d.ts +2 -2
  167. package/dist/lib/session/cloud.js +2 -2
  168. package/dist/lib/session/db.d.ts +4 -0
  169. package/dist/lib/session/db.js +44 -7
  170. package/dist/lib/session/discover.d.ts +2 -0
  171. package/dist/lib/session/discover.js +114 -3
  172. package/dist/lib/session/stream-render.d.ts +3 -0
  173. package/dist/lib/session/stream-render.js +130 -0
  174. package/dist/lib/session/types.d.ts +6 -0
  175. package/dist/lib/share/analytics.d.ts +13 -0
  176. package/dist/lib/share/analytics.js +45 -0
  177. package/dist/lib/share/config.d.ts +19 -5
  178. package/dist/lib/share/config.js +44 -8
  179. package/dist/lib/share/provision.d.ts +58 -6
  180. package/dist/lib/share/provision.js +97 -22
  181. package/dist/lib/share/publish.d.ts +36 -13
  182. package/dist/lib/share/publish.js +55 -8
  183. package/dist/lib/share/worker-template.js +83 -17
  184. package/dist/lib/shims.d.ts +9 -0
  185. package/dist/lib/shims.js +91 -13
  186. package/dist/lib/ssh-exec.d.ts +14 -0
  187. package/dist/lib/ssh-exec.js +57 -0
  188. package/dist/lib/staleness/detectors/hooks.js +25 -1
  189. package/dist/lib/staleness/detectors/permissions.js +66 -0
  190. package/dist/lib/staleness/detectors/workflows.js +20 -0
  191. package/dist/lib/staleness/writers/hooks.js +58 -4
  192. package/dist/lib/staleness/writers/permissions.js +2 -2
  193. package/dist/lib/staleness/writers/skills.js +1 -1
  194. package/dist/lib/staleness/writers/sources.d.ts +10 -1
  195. package/dist/lib/staleness/writers/sources.js +68 -1
  196. package/dist/lib/star-nudge.d.ts +45 -0
  197. package/dist/lib/star-nudge.js +91 -0
  198. package/dist/lib/startup/command-registry.d.ts +6 -1
  199. package/dist/lib/startup/command-registry.js +17 -3
  200. package/dist/lib/state.d.ts +2 -0
  201. package/dist/lib/state.js +3 -0
  202. package/dist/lib/subagents-registry.d.ts +2 -0
  203. package/dist/lib/subagents-registry.js +3 -0
  204. package/dist/lib/teams/parsers.js +214 -6
  205. package/dist/lib/teams/supervisor.d.ts +7 -0
  206. package/dist/lib/teams/supervisor.js +2 -1
  207. package/dist/lib/template.d.ts +1 -1
  208. package/dist/lib/template.js +1 -1
  209. package/dist/lib/triggers/webhook.js +36 -3
  210. package/dist/lib/types.d.ts +73 -0
  211. package/dist/lib/version.d.ts +40 -0
  212. package/dist/lib/version.js +94 -16
  213. package/dist/lib/versions.d.ts +11 -0
  214. package/dist/lib/versions.js +208 -76
  215. package/dist/lib/workflows.d.ts +2 -0
  216. package/dist/lib/workflows.js +88 -0
  217. package/package.json +2 -1
  218. package/dist/commands/daemon.d.ts +0 -10
  219. package/dist/commands/daemon.js +0 -121
@@ -19,6 +19,7 @@ import { getCliVersion } from '../lib/version.js';
19
19
  import { readAndResolveBundleEnv, isHeadlessSecretsContext } from '../lib/secrets/bundles.js';
20
20
  import { machineId } from '../lib/session/sync/config.js';
21
21
  import { registerFleetCaptureCommand } from './fleet-capture.js';
22
+ import { registerFleetApplyAlias } from './apply.js';
22
23
  import { addIgnored, getDevice, loadDevices, loadIgnored, removeDevice, removeIgnored, upsertDevice, } from '../lib/devices/registry.js';
23
24
  import { addControlToken } from '../lib/serve/token.js';
24
25
  import { DEFAULT_SERVE_PORT } from '../lib/serve/server.js';
@@ -40,7 +41,10 @@ import { checkAllClis } from '../lib/teams/agents.js';
40
41
  import { buildRemoteAgentsInvocation } from '../lib/hosts/remote-cmd.js';
41
42
  import { sshExecAsync } from '../lib/ssh-exec.js';
42
43
  import { ALL_AGENT_IDS } from '../lib/agents.js';
43
- import { formatCheckedAge, isDeadVerdict, probeLocalFleetAuth, readAuthHealthCache, summarizeHostAuth, summarizeVerdicts, verdictLabel, writeFleetAuthRows, } from '../lib/auth-health.js';
44
+ import { crabboxList, crabboxFind, crabboxSshArgv } from '../lib/crabbox/cli.js';
45
+ import { boxAddress, boxStatus, fmtIdleShort, fmtExpiresShort } from './lease.js';
46
+ import { authCellColor, formatCheckedAge, isDeadVerdict, probeLocalFleetAuth, readAuthHealthCache, summarizeHostAuth, summarizeVerdicts, verdictColor, verdictLabel, writeFleetAuthRows, } from '../lib/auth-health.js';
47
+ import { runFleetLogin } from '../lib/fleet/remote-login.js';
44
48
  /** One-line summary of a device for `list`. `isSelf` marks the machine this
45
49
  * command is running on so it stands out from the rest of the tailnet. */
46
50
  function deviceSummary(d, isSelf = false) {
@@ -130,6 +134,74 @@ function renderDeviceTable(reg, names, self, statsMap, full = false) {
130
134
  }
131
135
  return lines;
132
136
  }
137
+ /**
138
+ * Live "Leased boxes" section for `agents devices` (F4, RUSH-1923), computed
139
+ * from `crabboxList()` — these are ephemeral crabbox leases, NEVER written into
140
+ * the device registry. Returns [] when crabbox is unavailable / has no creds /
141
+ * reports no boxes, so the section is simply omitted. `nowSecs` is injected so
142
+ * the row formatting is deterministic in tests.
143
+ */
144
+ export function renderLeasedBoxesSection(boxes, nowSecs) {
145
+ if (boxes.length === 0)
146
+ return [];
147
+ const lines = [];
148
+ lines.push('');
149
+ lines.push(chalk.bold('Leased boxes') + chalk.gray(' (ephemeral · via crabbox)'));
150
+ lines.push(' ' +
151
+ chalk.gray('box'.padEnd(16)) +
152
+ chalk.gray('class'.padEnd(10)) +
153
+ chalk.gray('address'.padEnd(24)) +
154
+ chalk.gray('status'.padEnd(9)) +
155
+ chalk.gray('idle'.padEnd(12)) +
156
+ chalk.gray('expires'));
157
+ for (const b of boxes) {
158
+ const addr = boxAddress(b) ?? '—';
159
+ lines.push(' ' +
160
+ chalk.cyan(b.slug.padEnd(16)) +
161
+ (b.class ?? '?').padEnd(10) +
162
+ addr.padEnd(24) +
163
+ boxStatus(b).padEnd(9) +
164
+ chalk.gray(fmtIdleShort(b, nowSecs).padEnd(12)) +
165
+ chalk.gray(fmtExpiresShort(b, nowSecs)));
166
+ }
167
+ lines.push(chalk.gray(' Reuse a box with `agents run --box <slug>` · stop with `agents lease stop <slug>`'));
168
+ return lines;
169
+ }
170
+ /** The leased-box rows for the devices list, or [] when crabbox can't be read. */
171
+ function loadLeasedBoxesSection() {
172
+ try {
173
+ const boxes = crabboxList({ secretsBundle: process.env.AGENTS_LEASE_SECRETS_BUNDLE, timeoutMs: 5000 });
174
+ return renderLeasedBoxesSection(boxes, Math.floor(Date.now() / 1000));
175
+ }
176
+ catch {
177
+ return []; // crabbox not installed / no provider creds — omit the section
178
+ }
179
+ }
180
+ /**
181
+ * `agents ssh <slug>` targeting a leased crabbox box. crabbox provisions a
182
+ * per-lease identity key, so we ssh via crabbox's OWN emitted invocation
183
+ * (`crabboxSshArgv`) — a raw `ssh crabbox@ip` fails publickey. Returns false when
184
+ * `name` is not a known crabbox slug so the caller can fall through to the normal
185
+ * "Unknown device" error.
186
+ */
187
+ function trySshLeasedBox(name, cmd) {
188
+ let box;
189
+ try {
190
+ box = crabboxFind(name, { secretsBundle: process.env.AGENTS_LEASE_SECRETS_BUNDLE, timeoutMs: 5000 });
191
+ }
192
+ catch {
193
+ return false; // crabbox unavailable — not a leased-box target
194
+ }
195
+ if (!box)
196
+ return false;
197
+ const sshArgv = crabboxSshArgv(name, { secretsBundle: process.env.AGENTS_LEASE_SECRETS_BUNDLE, timeoutMs: 8000 });
198
+ if (!sshArgv) {
199
+ console.error(chalk.red(`Leased box '${name}' is not reachable yet (status: ${boxStatus(box)}).`));
200
+ process.exit(1);
201
+ }
202
+ const res = spawnSync(sshArgv[0], [...sshArgv.slice(1), ...cmd], { stdio: 'inherit' });
203
+ process.exit(res.status ?? 1);
204
+ }
133
205
  /** Resolve a device or exit with a clear error. */
134
206
  async function mustGetDevice(name) {
135
207
  const d = await getDevice(name);
@@ -423,17 +495,28 @@ async function runFleetPing(opts) {
423
495
  if (opts.strict && anyBad)
424
496
  process.exitCode = 1;
425
497
  }
426
- /** Color a per-host×agent cell: green all-live, red any revoked/expired, yellow degraded. */
498
+ /** Resolve an {@link AuthCellColor} to a chalk painter. Single map for cells + labels. */
499
+ const CELL_PAINT = {
500
+ green: chalk.green,
501
+ yellow: chalk.yellow,
502
+ red: chalk.red,
503
+ gray: chalk.gray,
504
+ dim: chalk.dim,
505
+ };
506
+ /**
507
+ * Color a per-host×agent cell. The numerator counts accounts that are usable
508
+ * right now — live-verified PLUS signed-in-but-unverifiable (codex/grok) — over
509
+ * the total, so a logged-in codex fleet reads "1/1", not a scary "0/1". Color is
510
+ * the shared {@link authCellColor}: red only for revoked (re-login), yellow for
511
+ * soft/expired (self-refreshes), gray for present-but-unverifiable, green when
512
+ * all live.
513
+ */
427
514
  function authCell(summary, width) {
428
515
  if (summary.total === 0)
429
516
  return chalk.dim('·'.padEnd(width));
430
- const text = `${summary.live}/${summary.total}`;
431
- const padded = text.padEnd(width);
432
- if (summary.bad > 0)
433
- return chalk.red(padded);
434
- if (summary.warn > 0)
435
- return chalk.yellow(padded);
436
- return chalk.green(padded);
517
+ const ok = summary.live + summary.present;
518
+ const padded = `${ok}/${summary.total}`.padEnd(width);
519
+ return CELL_PAINT[authCellColor(summary)](padded);
437
520
  }
438
521
  /** Render the fleet auth matrix (device rows × agent columns) plus an optional per-account breakdown. */
439
522
  function renderAuthMatrix(results, opts) {
@@ -466,7 +549,7 @@ function renderAuthMatrix(results, opts) {
466
549
  lines.push(` ${r.host.padEnd(nameW)} ${cells.join(' ')}${note}`);
467
550
  }
468
551
  lines.push('');
469
- lines.push(chalk.gray(' cell = live/total accounts · green all live · red revoked (re-login) · yellow expired/unverified/limited'));
552
+ lines.push(chalk.gray(' cell = signed-in/total accounts · green live · gray signed-in (unverifiable: codex/grok) · yellow expired (self-refreshes) · red revoked (re-login)'));
470
553
  if (opts?.verbose) {
471
554
  lines.push('');
472
555
  lines.push(chalk.bold('Accounts'));
@@ -475,9 +558,7 @@ function renderAuthMatrix(results, opts) {
475
558
  continue;
476
559
  for (const row of r.rows.slice().sort((x, y) => (x.agent + x.version).localeCompare(y.agent + y.version))) {
477
560
  const v = row.health.verdict;
478
- const label = v === 'live' ? chalk.green(verdictLabel(v))
479
- : (v === 'revoked' || v === 'expired') ? chalk.red(verdictLabel(v))
480
- : chalk.yellow(verdictLabel(v));
561
+ const label = CELL_PAINT[verdictColor(v)](verdictLabel(v));
481
562
  const acctRaw = row.account ?? '—';
482
563
  const acct = row.account ? chalk.cyan(acctRaw.padEnd(28)) : chalk.dim(acctRaw.padEnd(28));
483
564
  const detail = row.health.detail ? chalk.dim(` ${row.health.detail}`) : '';
@@ -488,6 +569,46 @@ function renderAuthMatrix(results, opts) {
488
569
  }
489
570
  return lines;
490
571
  }
572
+ /** Render the final `fleet login` result summary in the house auth-matrix style. */
573
+ function renderLoginMatrix(results) {
574
+ const lines = [chalk.bold('Fleet login')];
575
+ if (results.length === 0) {
576
+ lines.push(chalk.gray(' Nothing pending — every requested account is already logged in.'));
577
+ return lines;
578
+ }
579
+ const nameW = Math.max(6, ...results.map((r) => `${r.agent}@${r.device}`.length));
580
+ for (const r of results) {
581
+ const who = `${r.agent}@${r.device}`.padEnd(nameW);
582
+ let badge;
583
+ switch (r.state) {
584
+ case 'authorized':
585
+ badge = chalk.green('authorized');
586
+ break;
587
+ case 'ready':
588
+ badge = chalk.cyan('code ready');
589
+ break;
590
+ case 'driving':
591
+ badge = chalk.yellow('driving');
592
+ break;
593
+ case 'skipped':
594
+ badge = chalk.gray('not remotable');
595
+ break;
596
+ case 'error':
597
+ badge = chalk.red('error');
598
+ break;
599
+ default:
600
+ badge = chalk.gray(r.state);
601
+ break;
602
+ }
603
+ const note = r.detail ? chalk.dim(` ${r.detail}`) : (r.reason && !r.remotable ? chalk.dim(` ${r.reason}`) : '');
604
+ lines.push(` ${who} ${badge}${note}`);
605
+ }
606
+ const auth = results.filter((r) => r.state === 'authorized').length;
607
+ const remotable = results.filter((r) => r.remotable).length;
608
+ lines.push('');
609
+ lines.push(chalk.gray(` ${auth}/${remotable} remotable logins authorized`));
610
+ return lines;
611
+ }
491
612
  /** Register the `agents devices` command tree (also aliased as `fleet`). */
492
613
  function registerDevicesCommands(program) {
493
614
  const devicesCmd = program
@@ -529,6 +650,9 @@ Typical workflow:
529
650
  });
530
651
  // `agents fleet capture` — snapshot live state into agents.yaml fleet:.
531
652
  registerFleetCaptureCommand(devicesCmd);
653
+ // `agents fleet apply` — same reconcile engine as the top-level `agents apply`,
654
+ // surfaced under the fleet tree for discoverability.
655
+ registerFleetApplyAlias(devicesCmd);
532
656
  devicesCmd
533
657
  .command('register <name>')
534
658
  .description('Register a discovered (pending) node by name — used by the menu-bar "NEW DEVICES → Register" action.')
@@ -576,16 +700,7 @@ Typical workflow:
576
700
  }
577
701
  console.log(chalk.green(`No longer ignoring '${name}'`) + chalk.gray(' — run `agents devices sync` to register it.'));
578
702
  });
579
- devicesCmd
580
- .command('list')
581
- .alias('ls')
582
- .description('List registered devices with platform, address, reachability, and live resource headroom.')
583
- .option('--json', 'output the registry as a JSON array (for scripts and hooks)')
584
- .option('--no-stats', 'skip the live resource probe (instant; names/addresses only)')
585
- .option('--refresh', 'force a live probe of every device, bypassing the cache')
586
- .option('--live', 'alias of --refresh (shorter to type)')
587
- .option('-f, --full', 'full mode: add per-device core count and free/total memory')
588
- .action(async (opts) => {
703
+ const runList = async (opts = {}) => {
589
704
  const reg = await loadDevices();
590
705
  const names = Object.keys(reg).sort();
591
706
  if (opts.json) {
@@ -629,7 +744,26 @@ Typical workflow:
629
744
  if (freshness?.servedFromCache && freshness.oldestFetchedAt != null) {
630
745
  console.log(chalk.gray(` updated ${formatCheckedAge(freshness.oldestFetchedAt)} — pass --refresh (--live) for a live probe`));
631
746
  }
632
- });
747
+ // Ephemeral crabbox leases live alongside the registered fleet but are never
748
+ // written into the registry — surface them as their own live section. This is
749
+ // a live provider call, so honor --no-stats (the explicit "instant, no probes"
750
+ // opt-out) and bound it so a slow provider can't hang `agents devices`.
751
+ if (opts.stats !== false) {
752
+ for (const line of loadLeasedBoxesSection())
753
+ console.log(line);
754
+ }
755
+ };
756
+ devicesCmd.action(runList);
757
+ devicesCmd
758
+ .command('list')
759
+ .alias('ls')
760
+ .description('List registered devices with platform, address, reachability, and live resource headroom.')
761
+ .option('--json', 'output the registry as a JSON array (for scripts and hooks)')
762
+ .option('--no-stats', 'skip the live resource probe (instant; names/addresses only)')
763
+ .option('--refresh', 'force a live probe of every device, bypassing the cache')
764
+ .option('--live', 'alias of --refresh (shorter to type)')
765
+ .option('-f, --full', 'full mode: add per-device core count and free/total memory')
766
+ .action(runList);
633
767
  devicesCmd
634
768
  .command('status')
635
769
  .description('Show fleet health: warnings rollup, per-device sync drift, CLI readiness, version skew, and resource headroom.')
@@ -648,8 +782,41 @@ Typical workflow:
648
782
  .option('--local', 'probe only this host (used internally for fan-out)')
649
783
  .option('--verbose', 'show a per-account breakdown, not just the per-host rollup')
650
784
  .option('--strict', 'exit non-zero when any account is revoked (expired is soft — it self-refreshes)')
785
+ .action(async (opts, cmd) => {
786
+ // The root program also defines a global `--verbose` (startup self-heal
787
+ // detail), and commander binds a shared long flag to the program, not the
788
+ // leaf — so `fleet ping --verbose` never set opts.verbose and the
789
+ // per-account breakdown was silently unreachable. Read the effective value
790
+ // from the merged globals so the flag works at either level.
791
+ const verbose = opts.verbose ?? Boolean(cmd.optsWithGlobals().verbose);
792
+ await runFleetPing({ ...opts, verbose });
793
+ });
794
+ devicesCmd
795
+ .command('login')
796
+ .description('Log agent CLIs into fleet boxes over SSH: drive each box\'s device-code OAuth, scrape the URL + code, and surface every pending login in one local browser page. Default drives all codes at once; --interactive walks one box at a time (codes requested just-in-time so they don\'t expire).')
797
+ .option('--agents <csv>', 'only these agents (comma-separated); default: every agent with a device-code flow')
798
+ .option('--devices <csv>', 'only these devices (comma-separated); default: every online box')
799
+ .option('--all', 'target every device-code pair regardless of cached login state (cold cache / forced re-login)')
800
+ .option('--interactive', 'guided one-box-at-a-time wizard (codes requested just-in-time)')
801
+ .option('--json', 'output the final result matrix as JSON')
651
802
  .action(async (opts) => {
652
- await runFleetPing(opts);
803
+ const csv = (s) => (s ? s.split(',').map((x) => x.trim()).filter(Boolean) : undefined);
804
+ const results = await runFleetLogin({
805
+ agents: csv(opts.agents),
806
+ devices: csv(opts.devices),
807
+ all: opts.all,
808
+ interactive: opts.interactive,
809
+ json: opts.json,
810
+ });
811
+ if (opts.json) {
812
+ console.log(JSON.stringify(results, null, 2));
813
+ }
814
+ else {
815
+ for (const line of renderLoginMatrix(results))
816
+ console.log(line);
817
+ }
818
+ if (results.some((r) => r.remotable && r.state !== 'authorized'))
819
+ process.exitCode = 1;
653
820
  });
654
821
  devicesCmd
655
822
  .command('show <name>')
@@ -849,6 +1016,9 @@ secrets bundle via an askpass shim — the password never touches argv.
849
1016
  // A bare unregistered alias still errors as "Unknown device".
850
1017
  const device = resolveDeviceTarget(name, await loadDevices());
851
1018
  if (!device) {
1019
+ // Not a registered device — it may be a leased crabbox box slug. ssh into
1020
+ // it directly (crabbox@<tailnet|ip>:2222) before giving up.
1021
+ trySshLeasedBox(name, cmd); // exits the process on a match
852
1022
  console.error(chalk.red(`Unknown device '${name}'. See 'agents devices list'.`));
853
1023
  process.exit(1);
854
1024
  }
@@ -907,7 +1077,7 @@ async function runAskpass() {
907
1077
  process.exit(1);
908
1078
  }
909
1079
  try {
910
- const { env } = readAndResolveBundleEnv(bundle, { caller: 'agents ssh', agentOnly: isHeadlessSecretsContext() });
1080
+ const { env } = readAndResolveBundleEnv(bundle, { caller: 'agents ssh', keys: [key], keyMode: 'storage', agentOnly: isHeadlessSecretsContext() });
911
1081
  const value = env[key];
912
1082
  if (value === undefined) {
913
1083
  console.error(`askpass: key '${key}' not found in bundle '${bundle}'`);
@@ -544,7 +544,7 @@ function runLaunchMode(agent, version, cwd, quiet) {
544
544
  if (result.rulesCompiled)
545
545
  bits.push('rules');
546
546
  if (result.workspaceLinks > 0)
547
- bits.push(`${result.workspaceLinks} workspace link(s)`);
547
+ bits.push(`${result.workspaceLinks} project resource(s)`);
548
548
  const mpCount = Object.keys(result.marketplaces).length;
549
549
  if (mpCount > 0) {
550
550
  const pluginCount = Object.values(result.marketplaces).reduce((acc, names) => acc + names.length, 0);
@@ -7,6 +7,8 @@ import { AgentManager, AgentStatus, checkCliSignedIn, collectTeamsDoctorData, ge
7
7
  import { mailboxDir, enqueue } from '../lib/mailbox.js';
8
8
  import { resolveProvider } from '../lib/cloud/registry.js';
9
9
  import { emit } from '../lib/events.js';
10
+ import { maybeShowStarNudge } from '../lib/star-nudge.js';
11
+ import { shareRuntimeEnv } from '../lib/share/config.js';
10
12
  import { runSupervisor } from '../lib/teams/supervisor.js';
11
13
  import { debug } from '../lib/teams/debug.js';
12
14
  import { runPrWatch, DEFAULT_MAX_WAVES, } from '../lib/teams/pr-watch.js';
@@ -49,8 +51,6 @@ const VALID_AGENTS = Object.keys(AGENT_NAMES);
49
51
  const VALID_MODES = ['plan', 'edit', 'auto', 'skip', 'full'];
50
52
  const VALID_EFFORTS = ['low', 'medium', 'high', 'xhigh', 'max', 'auto'];
51
53
  const VALID_CLOUD_PROVIDERS = ['rush', 'codex', 'factory'];
52
- // Auto-enable JSON mode when piped / not a TTY so AI agent consumers get
53
- // parseable output by default.
54
54
  function statusColor(status) {
55
55
  switch (status) {
56
56
  case 'pending': return chalk.blue;
@@ -1352,6 +1352,7 @@ export function registerTeamsCommands(program) {
1352
1352
  repo: opts.repo,
1353
1353
  branch: opts.branch,
1354
1354
  model: a.model ?? undefined,
1355
+ env: shareRuntimeEnv({ agentOnly: true }),
1355
1356
  };
1356
1357
  const cloudTask = await prov.dispatch(dispatchOpts);
1357
1358
  return { cloudSessionId: cloudTask.id };
@@ -1369,6 +1370,7 @@ export function registerTeamsCommands(program) {
1369
1370
  repo: opts.repo,
1370
1371
  branch: opts.branch,
1371
1372
  model: opts.model,
1373
+ env: shareRuntimeEnv({ agentOnly: true }),
1372
1374
  };
1373
1375
  try {
1374
1376
  const cloudTask = await prov.dispatch(dispatchOpts);
@@ -1594,6 +1596,13 @@ export function registerTeamsCommands(program) {
1594
1596
  emit('teams.complete', { module: 'teams', team, stoppedBy: result.stoppedBy, waves: result.waves, durationMs: result.elapsed_ms });
1595
1597
  if (result.stoppedBy === 'drained') {
1596
1598
  console.log(chalk.green(`Factory drained in ${elapsed}s (${result.waves} waves).`));
1599
+ // First-successful-team star nudge (one-time, non-nagging). "drained"
1600
+ // only means nothing is pending/running — teammates may have failed — so
1601
+ // gate on a clean drain (failed === 0). A team where every teammate
1602
+ // failed is not the success this nudge celebrates.
1603
+ if ((result.failed ?? 0) === 0) {
1604
+ maybeShowStarNudge({ quiet: opts.json });
1605
+ }
1597
1606
  }
1598
1607
  else if (result.stoppedBy === 'max-waves') {
1599
1608
  console.error(chalk.yellow(`Hit --max-waves=${maxWaves}; stopping. Re-run to continue.`));
package/dist/index.js CHANGED
@@ -26,6 +26,11 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
26
26
  const packageJsonPath = path.join(__dirname, '..', 'package.json');
27
27
  const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
28
28
  const VERSION = packageJson.version;
29
+ if (process.argv[2] === '__vault-age-helper') {
30
+ const { runVaultAgeHelperCli } = await import('./lib/secrets/vault-age-helper.js');
31
+ await runVaultAgeHelperCli();
32
+ process.exit(process.exitCode ?? 0);
33
+ }
29
34
  import { NPM_PACKAGE_NAME, deriveGlobalPrefix, detectPackageManager, installPackageIntoPrefix, installPackageWithBun, verifyInstalledVersion, refreshAliasShims, downloadVerifiedTarball, } from './lib/self-update.js';
30
35
  // Detect dev/working-tree builds and default the noisy startup steps off.
31
36
  // Three cases trip this:
@@ -50,7 +55,7 @@ if (IS_DEV_BUILD) {
50
55
  // module on each invocation (which loaded the whole ~50-module tree before the
51
56
  // first byte of output), the registry maps a command name to a thunk that
52
57
  // imports only what that command needs. See src/lib/startup/command-registry.ts.
53
- import { COMMAND_LOADERS, LAZY_COMMAND_NAMES, loadView, loadInspect, loadFeedback, loadCommands, loadHooks, loadSkills, loadRules, loadMemory, loadPermissions, loadMcp, loadCli, loadSubagents, loadPlugins, loadWorkflows, loadWorktree, loadVersions, loadImport, loadPackages, loadDaemon, loadRoutines, loadMonitors, loadRun, loadFork, loadDefaults, loadModels, loadPrune, loadTrash, loadRestore, loadDoctor, loadApply, loadCheck, loadStatus, loadProfiles, loadSecrets, loadWallet, loadHelper, loadMenubar, loadBeta, loadSync, loadLock, loadRefreshRules, loadDrive, loadFactory, loadUsage, loadCost, loadOutput, loadBudget, loadAlias, loadPty, loadTmux, loadWatchdog, loadBrowser, loadComputer, loadHosts, loadLogs, loadEvents, loadAudit, loadWebhook, loadFunnel, loadSsh, loadPull, loadPush, loadRepo, loadSetup, loadUninstall, loadShare, loadFeed, loadMailboxes, } from './lib/startup/command-registry.js';
58
+ import { COMMAND_LOADERS, LAZY_COMMAND_NAMES, loadView, loadInspect, loadResources, loadFeedback, loadCommands, loadHooks, loadSkills, loadRules, loadMemory, loadPermissions, loadMcp, loadCli, loadSubagents, loadPlugins, loadWorkflows, loadWorktree, loadVersions, loadImport, loadPackages, loadRoutines, loadMonitors, loadRun, loadFork, loadDefaults, loadModels, loadPrune, loadTrash, loadRestore, loadDoctor, loadApply, loadCheck, loadStatus, loadProfiles, loadSecrets, loadLogin, loadWallet, loadHelper, loadMenubar, loadBeta, loadSync, loadLock, loadRefreshRules, loadDrive, loadFactory, loadUsage, loadCost, loadOutput, loadBudget, loadAlias, loadMine, loadPty, loadTmux, loadWatchdog, loadBrowser, loadComputer, loadHosts, loadLogs, loadEvents, loadAudit, loadWebhook, loadFunnel, loadSsh, loadPull, loadPush, loadRepo, loadSetup, loadUninstall, loadShare, loadSend, loadHq, loadFeed, loadActivity, loadMailboxes, } from './lib/startup/command-registry.js';
54
59
  import { applyGlobalHelpConventions } from './lib/help.js';
55
60
  import { renderWhatsNew } from './lib/whats-new.js';
56
61
  import { emit, redactArgs } from './lib/events.js';
@@ -68,9 +73,18 @@ if (process.argv[2] === '__shim') {
68
73
  const code = await execShimPassthrough(agent, rawArgs, process.cwd(), pinned || undefined);
69
74
  process.exit(code);
70
75
  }
76
+ if (process.argv[2] === '__daemon-run') {
77
+ const { runDaemon } = await import('./lib/daemon.js');
78
+ await runDaemon();
79
+ process.exit(process.exitCode ?? 0);
80
+ }
81
+ // White-label: the shim for a brand (e.g. `jack`) exports AGENTS_BRAND, so the
82
+ // CLI presents its own name/help/errors as the brand. Unbranded (AGENTS_BRAND
83
+ // unset) resolves to 'agents' and everything below is byte-identical to before.
84
+ const BRAND = resolveBrandName();
71
85
  const program = new Command();
72
86
  program
73
- .name('agents')
87
+ .name(BRAND)
74
88
  .description('Environment manager for AI agents')
75
89
  .version(VERSION)
76
90
  .option('--verbose', 'Show startup self-heal details on stderr')
@@ -86,7 +100,7 @@ program
86
100
  function auditCommandPath(cmd) {
87
101
  const parts = [];
88
102
  let c = cmd;
89
- while (c && c.name() && c.name() !== 'agents') {
103
+ while (c && c.name() && c.name() !== BRAND) {
90
104
  parts.unshift(c.name());
91
105
  c = c.parent;
92
106
  }
@@ -129,11 +143,36 @@ program.hook('postAction', (_thisCommand, actionCommand) => {
129
143
  // Best-effort completion record; the start line is the durable audit fact.
130
144
  }
131
145
  });
146
+ /**
147
+ * Skin the static root help for a brand: rewrite the visible `agents` command
148
+ * examples to the brand name and drop lines for commands this brand disabled.
149
+ * A no-op for the unbranded `agents` CLI with nothing disabled.
150
+ */
151
+ function brandRootHelp(raw) {
152
+ let text = raw;
153
+ if (BRAND !== 'agents') {
154
+ text = text
155
+ .replace(/Usage: agents /g, `Usage: ${BRAND} `)
156
+ .replace(/^ {2}agents /gm, ` ${BRAND} `)
157
+ .replace(/Run 'agents /g, `Run '${BRAND} `);
158
+ }
159
+ const disabled = disabledCommandsForActiveBrand();
160
+ if (disabled.size > 0) {
161
+ text = text
162
+ .split('\n')
163
+ .filter((line) => {
164
+ const m = line.match(/^ {2}([a-z][\w-]*)/);
165
+ return !(m && disabled.has(m[1]));
166
+ })
167
+ .join('\n');
168
+ }
169
+ return text;
170
+ }
132
171
  // Custom help for the main program only
133
172
  const originalHelpInformation = program.helpInformation.bind(program);
134
173
  program.helpInformation = function () {
135
- if (this.name() === 'agents' && !this.parent) {
136
- return `Usage: agents [command] [options]
174
+ if (this.name() === BRAND && !this.parent) {
175
+ return brandRootHelp(`Usage: agents [command] [options]
137
176
 
138
177
  Install, configure, run, and dispatch AI coding agents from one place.
139
178
  Works with Claude, Codex, Gemini, Cursor, OpenCode, OpenClaw, and Droid.
@@ -154,6 +193,7 @@ Agent versions:
154
193
  trash Inspect and restore soft-deleted version directories
155
194
  view [agent[@version]] List versions, or inspect one in detail
156
195
  inspect <target> Deep details for one agent+version, or a DotAgents repo (user|system|project|alias|path)
196
+ resources Show merged DotAgents resources with their winning layer
157
197
 
158
198
  Agent configuration (synced across versions):
159
199
  rules Instructions given to agents (CLAUDE.md, etc.)
@@ -173,6 +213,7 @@ Run and dispatch:
173
213
  run <agent|profile> [prompt] Run an agent. Omit prompt for interactive mode.
174
214
  defaults Configure run defaults by agent/version selector
175
215
  teams Coordinate multiple agents on shared work
216
+ hq JSON bridge for the interactive Agents HQ floor
176
217
  routines Run agents on a cron schedule (scheduler auto-starts)
177
218
  webhook Receive signed GitHub/Linear webhooks for trigger routines
178
219
  funnel Expose a webhook receiver through Tailscale Funnel
@@ -182,6 +223,7 @@ Run and dispatch:
182
223
  pty Drive interactive terminal programs (REPLs, TUIs) via a persistent PTY session
183
224
 
184
225
  Credentials and profiles:
226
+ profile Activate resource profiles across skills, MCP, permissions, and secrets
185
227
  profiles Bundles of (host CLI, endpoint, model, auth)
186
228
  secrets Keychain-backed env bundles; use 'secrets exec <bundle> -- <cmd>' to inject into a subprocess
187
229
 
@@ -213,7 +255,7 @@ Options:
213
255
  --verbose Show startup self-heal details on stderr
214
256
 
215
257
  System config lives in ~/.agents/.system/. Run 'agents <command> --help' for details.
216
- `;
258
+ `);
217
259
  }
218
260
  return originalHelpInformation();
219
261
  };
@@ -251,6 +293,7 @@ async function showWhatsNew(fromVersion, toVersion) {
251
293
  }
252
294
  const UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours
253
295
  import { getUpdateCheckPath, getMigratedSentinelPath, getUserAgentsDir, getRuntimeStateDir } from './lib/state.js';
296
+ import { resolveBrandName, disabledCommandsForActiveBrand } from './lib/brand.js';
254
297
  import { readUpdateCache, saveUpdateCheck, dismissUpdateVersion, shouldPromptUpgrade, findAgentsCliInstalls, } from './lib/self-update.js';
255
298
  const UPDATE_CHECK_FILE = getUpdateCheckPath();
256
299
  /**
@@ -668,7 +711,9 @@ async function registerEagerForRequest(name) {
668
711
  async function registerAllEagerCommands() {
669
712
  await reg(loadView);
670
713
  await reg(loadShare);
714
+ await reg(loadSend);
671
715
  await reg(loadInspect);
716
+ await reg(loadResources);
672
717
  await reg(loadFeedback);
673
718
  await reg(loadCommands);
674
719
  await reg(loadHooks);
@@ -686,7 +731,6 @@ async function registerAllEagerCommands() {
686
731
  await reg(loadVersions);
687
732
  await reg(loadImport);
688
733
  await reg(loadPackages);
689
- await reg(loadDaemon);
690
734
  await reg(loadRoutines);
691
735
  await reg(loadMonitors);
692
736
  await reg(loadRun);
@@ -703,6 +747,7 @@ async function registerAllEagerCommands() {
703
747
  registerExecAliasCommand(program);
704
748
  await reg(loadProfiles);
705
749
  await reg(loadSecrets);
750
+ await reg(loadLogin);
706
751
  await reg(loadWallet);
707
752
  await reg(loadHelper);
708
753
  await reg(loadMenubar);
@@ -717,6 +762,7 @@ async function registerAllEagerCommands() {
717
762
  await reg(loadOutput);
718
763
  await reg(loadBudget);
719
764
  await reg(loadAlias);
765
+ await reg(loadMine);
720
766
  await reg(loadPty);
721
767
  await reg(loadTmux);
722
768
  await reg(loadWatchdog);
@@ -728,7 +774,9 @@ async function registerAllEagerCommands() {
728
774
  await reg(loadAudit);
729
775
  await reg(loadWebhook);
730
776
  await reg(loadFunnel);
777
+ await reg(loadHq);
731
778
  await reg(loadFeed);
779
+ await reg(loadActivity);
732
780
  await reg(loadMailboxes);
733
781
  await reg(loadSsh);
734
782
  registerJobsCronAliasCommand(program, 'jobs');
@@ -795,13 +843,19 @@ const verboseStartup = passedArgs.includes('--verbose');
795
843
  // Help and version output are pure documentation — they must never gate on
796
844
  // setup, otherwise `agents <cmd> --help` becomes useless on a fresh box.
797
845
  const helpOrVersionRequested = passedArgs.some((arg) => arg === '--help' || arg === '-h' || arg === '--version' || arg === '-V');
846
+ // White-label: a brand can hide built-in top-level commands. A hidden command
847
+ // must behave as if it doesn't exist under this brand (unknown-command +
848
+ // spellcheck), while `agents` itself is unaffected. `brandDisabled` is empty for
849
+ // the unbranded CLI, so all of this is a no-op there.
850
+ const brandDisabled = disabledCommandsForActiveBrand();
851
+ const requestedIsDisabled = requestedCommand !== undefined && brandDisabled.has(requestedCommand);
798
852
  // `--host` passthrough: run this invocation on a remote machine over SSH instead
799
853
  // of locally. Handled before any local command registration / update check /
800
854
  // background sync — a remote run needs none of that. Only the allowlisted
801
855
  // read-only + config + teams commands route here; `run`/`sessions` are absent
802
856
  // from the table and fall through to their own richer `--host` handling below.
803
857
  // `--help`/`--version` stay local (docs must work without a reachable host).
804
- if (requestedCommand !== undefined && !helpOrVersionRequested) {
858
+ if (requestedCommand !== undefined && !helpOrVersionRequested && !requestedIsDisabled) {
805
859
  const { maybeRunOnHost } = await import('./lib/hosts/passthrough.js');
806
860
  if (await maybeRunOnHost(requestedCommand, passedArgs)) {
807
861
  process.exit(process.exitCode ?? 0);
@@ -810,7 +864,13 @@ if (requestedCommand !== undefined && !helpOrVersionRequested) {
810
864
  // Register only the command(s) this invocation actually uses. Lazy commands
811
865
  // (sessions/teams/cloud) are handled after applyGlobalHelpConventions below.
812
866
  const isLazyRequest = requestedCommand !== undefined && LAZY_COMMAND_NAMES.has(requestedCommand);
813
- if (requestedCommand !== undefined && !isLazyRequest) {
867
+ if (requestedIsDisabled) {
868
+ // The brand turned this command off: register the full tree so the "did you
869
+ // mean" picker still works, then strip the disabled commands below so the
870
+ // request resolves as unknown.
871
+ await registerAllEagerCommands();
872
+ }
873
+ else if (requestedCommand !== undefined && !isLazyRequest) {
814
874
  const known = await registerEagerForRequest(requestedCommand);
815
875
  if (!known) {
816
876
  // Unknown top-level command: register the full tree so the "did you mean"
@@ -828,10 +888,19 @@ if (requestedCommand !== undefined && !isLazyRequest) {
828
888
  applyGlobalHelpConventions(program);
829
889
  // Lazy commands pull in the SQLite-backed session/cloud stack; register them
830
890
  // only when explicitly requested, keeping lightweight commands off that path.
831
- if (isLazyRequest) {
891
+ if (isLazyRequest && !requestedIsDisabled) {
832
892
  for (const loader of COMMAND_LOADERS[requestedCommand])
833
893
  await reg(loader);
834
894
  }
895
+ // White-label: remove any commands this brand disabled so they resolve as
896
+ // unknown (the command:* handler then reports "unknown command"). Unbranded or
897
+ // nothing-disabled → no-op. Done after all registration paths above.
898
+ if (brandDisabled.size > 0) {
899
+ const kept = program.commands.filter((c) => !brandDisabled.has(c.name()));
900
+ if (kept.length !== program.commands.length) {
901
+ program.commands = kept;
902
+ }
903
+ }
835
904
  // Pure documentation paths (--version / --help / -h) return immediately: skip
836
905
  // the update check (PATH scan + cache read) and the detached background sync
837
906
  // (spawns a child process) that every other invocation runs.