@phnx-labs/agents-cli 1.20.72 → 1.20.74

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 (234) hide show
  1. package/CHANGELOG.md +395 -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 +21 -5
  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/export.d.ts +11 -0
  16. package/dist/commands/export.js +215 -0
  17. package/dist/commands/feed.d.ts +13 -0
  18. package/dist/commands/feed.js +130 -29
  19. package/dist/commands/hq.d.ts +2 -0
  20. package/dist/commands/hq.js +58 -0
  21. package/dist/commands/import.js +51 -3
  22. package/dist/commands/lease.d.ts +19 -0
  23. package/dist/commands/lease.js +138 -11
  24. package/dist/commands/login.d.ts +2 -0
  25. package/dist/commands/login.js +123 -0
  26. package/dist/commands/logs.js +1 -1
  27. package/dist/commands/mine.d.ts +27 -0
  28. package/dist/commands/mine.js +207 -0
  29. package/dist/commands/monitors.js +79 -49
  30. package/dist/commands/plugins.js +1 -0
  31. package/dist/commands/profiles.js +158 -0
  32. package/dist/commands/repo.js +2 -60
  33. package/dist/commands/resources.d.ts +5 -0
  34. package/dist/commands/resources.js +95 -0
  35. package/dist/commands/routines.d.ts +2 -0
  36. package/dist/commands/routines.js +139 -37
  37. package/dist/commands/secrets.js +234 -47
  38. package/dist/commands/send.d.ts +15 -0
  39. package/dist/commands/send.js +62 -0
  40. package/dist/commands/sessions-tail.d.ts +1 -0
  41. package/dist/commands/sessions-tail.js +20 -9
  42. package/dist/commands/sessions.js +18 -3
  43. package/dist/commands/setup-mine.d.ts +18 -0
  44. package/dist/commands/setup-mine.js +106 -0
  45. package/dist/commands/setup-share.js +2 -2
  46. package/dist/commands/setup.js +9 -2
  47. package/dist/commands/share.d.ts +8 -1
  48. package/dist/commands/share.js +122 -37
  49. package/dist/commands/ssh.d.ts +9 -0
  50. package/dist/commands/ssh.js +196 -26
  51. package/dist/commands/sync.js +1 -1
  52. package/dist/commands/teams.js +11 -2
  53. package/dist/commands/versions.js +36 -5
  54. package/dist/commands/view.js +75 -27
  55. package/dist/index.js +80 -10
  56. package/dist/lib/activity.d.ts +124 -0
  57. package/dist/lib/activity.js +542 -0
  58. package/dist/lib/agents.d.ts +35 -0
  59. package/dist/lib/agents.js +79 -6
  60. package/dist/lib/artifact-actions.d.ts +1 -1
  61. package/dist/lib/artifact-actions.js +1 -1
  62. package/dist/lib/ask-classifier.d.ts +2 -1
  63. package/dist/lib/ask-classifier.js +3 -3
  64. package/dist/lib/auth-health.d.ts +32 -4
  65. package/dist/lib/auth-health.js +40 -1
  66. package/dist/lib/auto-pull.js +8 -1
  67. package/dist/lib/brand.d.ts +40 -0
  68. package/dist/lib/brand.js +122 -0
  69. package/dist/lib/browser/drivers/ssh.js +4 -7
  70. package/dist/lib/browser/service.js +6 -8
  71. package/dist/lib/channels/providers/index.d.ts +2 -0
  72. package/dist/lib/channels/providers/index.js +20 -0
  73. package/dist/lib/channels/providers/mailbox.d.ts +2 -0
  74. package/dist/lib/channels/providers/mailbox.js +26 -0
  75. package/dist/lib/channels/providers/openclaw-telegram.d.ts +2 -0
  76. package/dist/lib/channels/providers/openclaw-telegram.js +34 -0
  77. package/dist/lib/channels/providers/rush.d.ts +6 -0
  78. package/dist/lib/channels/providers/rush.js +55 -0
  79. package/dist/lib/channels/registry.d.ts +42 -0
  80. package/dist/lib/channels/registry.js +20 -0
  81. package/dist/lib/channels/resolve.d.ts +11 -0
  82. package/dist/lib/channels/resolve.js +12 -0
  83. package/dist/lib/cli-resources.d.ts +18 -0
  84. package/dist/lib/cli-resources.js +53 -4
  85. package/dist/lib/cloud/codex.js +6 -3
  86. package/dist/lib/cloud/factory.d.ts +1 -0
  87. package/dist/lib/cloud/factory.js +10 -4
  88. package/dist/lib/cloud/rush.d.ts +3 -1
  89. package/dist/lib/cloud/rush.js +6 -1
  90. package/dist/lib/cloud/types.d.ts +2 -0
  91. package/dist/lib/codex-home.d.ts +35 -0
  92. package/dist/lib/codex-home.js +136 -0
  93. package/dist/lib/config-transfer.d.ts +16 -0
  94. package/dist/lib/config-transfer.js +56 -0
  95. package/dist/lib/crabbox/cli.d.ts +44 -0
  96. package/dist/lib/crabbox/cli.js +120 -20
  97. package/dist/lib/crabbox/lease.d.ts +52 -1
  98. package/dist/lib/crabbox/lease.js +117 -16
  99. package/dist/lib/crabbox/progress.d.ts +31 -0
  100. package/dist/lib/crabbox/progress.js +76 -0
  101. package/dist/lib/crabbox/runtimes.d.ts +4 -0
  102. package/dist/lib/crabbox/runtimes.js +33 -8
  103. package/dist/lib/crabbox/setup-copy.d.ts +87 -0
  104. package/dist/lib/crabbox/setup-copy.js +127 -0
  105. package/dist/lib/daemon.d.ts +5 -5
  106. package/dist/lib/daemon.js +9 -9
  107. package/dist/lib/drive-sync.js +6 -3
  108. package/dist/lib/event-stream.d.ts +36 -0
  109. package/dist/lib/event-stream.js +68 -0
  110. package/dist/lib/events.d.ts +1 -1
  111. package/dist/lib/exec.d.ts +11 -0
  112. package/dist/lib/exec.js +23 -2
  113. package/dist/lib/export.d.ts +72 -0
  114. package/dist/lib/export.js +269 -0
  115. package/dist/lib/feed-policy.d.ts +1 -1
  116. package/dist/lib/feed-policy.js +14 -9
  117. package/dist/lib/feed-ranking.d.ts +32 -0
  118. package/dist/lib/feed-ranking.js +224 -0
  119. package/dist/lib/feed.d.ts +35 -2
  120. package/dist/lib/feed.js +66 -3
  121. package/dist/lib/fleet/auth-sync.d.ts +53 -0
  122. package/dist/lib/fleet/auth-sync.js +92 -0
  123. package/dist/lib/fleet/remote-login.d.ts +187 -0
  124. package/dist/lib/fleet/remote-login.js +556 -0
  125. package/dist/lib/format.d.ts +30 -3
  126. package/dist/lib/format.js +34 -5
  127. package/dist/lib/fs-atomic.d.ts +7 -2
  128. package/dist/lib/fs-atomic.js +8 -12
  129. package/dist/lib/git.d.ts +16 -0
  130. package/dist/lib/git.js +79 -2
  131. package/dist/lib/heal.js +11 -3
  132. package/dist/lib/hosts/progress.d.ts +28 -10
  133. package/dist/lib/hosts/progress.js +79 -22
  134. package/dist/lib/hosts/remote-cmd.js +4 -0
  135. package/dist/lib/hq/floor.d.ts +87 -0
  136. package/dist/lib/hq/floor.js +226 -0
  137. package/dist/lib/import.d.ts +23 -0
  138. package/dist/lib/import.js +74 -2
  139. package/dist/lib/isolation-boundary-report.d.ts +10 -0
  140. package/dist/lib/isolation-boundary-report.js +35 -0
  141. package/dist/lib/menubar/install-menubar.js +14 -20
  142. package/dist/lib/notify.d.ts +1 -0
  143. package/dist/lib/notify.js +3 -3
  144. package/dist/lib/open-url.d.ts +2 -0
  145. package/dist/lib/open-url.js +19 -0
  146. package/dist/lib/openclaw-keychain.d.ts +56 -0
  147. package/dist/lib/openclaw-keychain.js +236 -0
  148. package/dist/lib/overdue.js +10 -0
  149. package/dist/lib/permissions.d.ts +44 -1
  150. package/dist/lib/permissions.js +284 -7
  151. package/dist/lib/project-launch.d.ts +6 -12
  152. package/dist/lib/project-launch.js +13 -228
  153. package/dist/lib/project-resources.d.ts +7 -0
  154. package/dist/lib/project-resources.js +306 -0
  155. package/dist/lib/pty-client.d.ts +27 -0
  156. package/dist/lib/pty-client.js +136 -10
  157. package/dist/lib/refresh.js +14 -10
  158. package/dist/lib/resource-profiles.d.ts +26 -0
  159. package/dist/lib/resource-profiles.js +157 -0
  160. package/dist/lib/resources/permissions.js +7 -1
  161. package/dist/lib/resources/types.d.ts +1 -1
  162. package/dist/lib/resources.d.ts +1 -1
  163. package/dist/lib/resources.js +32 -3
  164. package/dist/lib/routines.d.ts +11 -0
  165. package/dist/lib/routines.js +56 -15
  166. package/dist/lib/runner.d.ts +1 -0
  167. package/dist/lib/runner.js +66 -5
  168. package/dist/lib/secrets/bundles.d.ts +16 -3
  169. package/dist/lib/secrets/bundles.js +206 -37
  170. package/dist/lib/secrets/icloud-import.d.ts +2 -2
  171. package/dist/lib/secrets/icloud-import.js +9 -6
  172. package/dist/lib/secrets/mcp.js +1 -1
  173. package/dist/lib/secrets/vault-age-helper.d.ts +1 -0
  174. package/dist/lib/secrets/vault-age-helper.js +34 -0
  175. package/dist/lib/secrets/vault.d.ts +49 -0
  176. package/dist/lib/secrets/vault.js +397 -0
  177. package/dist/lib/self-heal/checks/path.js +3 -1
  178. package/dist/lib/self-heal/checks/shadowing.js +10 -2
  179. package/dist/lib/self-heal/checks/shims.js +19 -11
  180. package/dist/lib/session/cloud.d.ts +2 -2
  181. package/dist/lib/session/cloud.js +2 -2
  182. package/dist/lib/session/db.d.ts +4 -0
  183. package/dist/lib/session/db.js +44 -7
  184. package/dist/lib/session/discover.d.ts +2 -0
  185. package/dist/lib/session/discover.js +114 -3
  186. package/dist/lib/session/stream-render.d.ts +3 -0
  187. package/dist/lib/session/stream-render.js +130 -0
  188. package/dist/lib/session/types.d.ts +6 -0
  189. package/dist/lib/share/analytics.d.ts +13 -0
  190. package/dist/lib/share/analytics.js +45 -0
  191. package/dist/lib/share/config.d.ts +19 -5
  192. package/dist/lib/share/config.js +44 -8
  193. package/dist/lib/share/provision.d.ts +58 -6
  194. package/dist/lib/share/provision.js +97 -22
  195. package/dist/lib/share/publish.d.ts +36 -13
  196. package/dist/lib/share/publish.js +55 -8
  197. package/dist/lib/share/worker-template.js +83 -17
  198. package/dist/lib/shims.d.ts +35 -0
  199. package/dist/lib/shims.js +159 -13
  200. package/dist/lib/ssh-exec.d.ts +14 -0
  201. package/dist/lib/ssh-exec.js +57 -0
  202. package/dist/lib/staleness/detectors/hooks.js +25 -1
  203. package/dist/lib/staleness/detectors/permissions.js +66 -0
  204. package/dist/lib/staleness/detectors/workflows.js +20 -0
  205. package/dist/lib/staleness/writers/hooks.js +58 -4
  206. package/dist/lib/staleness/writers/permissions.js +2 -2
  207. package/dist/lib/staleness/writers/skills.js +1 -1
  208. package/dist/lib/staleness/writers/sources.d.ts +10 -1
  209. package/dist/lib/staleness/writers/sources.js +68 -1
  210. package/dist/lib/star-nudge.d.ts +45 -0
  211. package/dist/lib/star-nudge.js +91 -0
  212. package/dist/lib/startup/command-registry.d.ts +7 -1
  213. package/dist/lib/startup/command-registry.js +19 -3
  214. package/dist/lib/state.d.ts +2 -0
  215. package/dist/lib/state.js +17 -4
  216. package/dist/lib/subagents-registry.d.ts +2 -0
  217. package/dist/lib/subagents-registry.js +3 -0
  218. package/dist/lib/teams/parsers.js +214 -6
  219. package/dist/lib/teams/supervisor.d.ts +7 -0
  220. package/dist/lib/teams/supervisor.js +2 -1
  221. package/dist/lib/template.d.ts +1 -1
  222. package/dist/lib/template.js +1 -1
  223. package/dist/lib/triggers/webhook.js +36 -3
  224. package/dist/lib/types.d.ts +84 -0
  225. package/dist/lib/uninstall.js +1 -45
  226. package/dist/lib/version.d.ts +40 -0
  227. package/dist/lib/version.js +94 -16
  228. package/dist/lib/versions.d.ts +24 -0
  229. package/dist/lib/versions.js +269 -78
  230. package/dist/lib/workflows.d.ts +2 -0
  231. package/dist/lib/workflows.js +88 -0
  232. package/package.json +2 -1
  233. package/dist/commands/daemon.d.ts +0 -10
  234. package/dist/commands/daemon.js +0 -121
@@ -8,6 +8,25 @@
8
8
  * so a box a concurrent run just reused is never stopped.
9
9
  */
10
10
  import type { Command } from 'commander';
11
+ import { type CrabboxBox } from '../lib/crabbox/cli.js';
12
+ /** Compact human duration: "45s", "12m", "2h", "1h 5m". Clamps negatives to 0. */
13
+ export declare function fmtDurationShort(secs: number): string;
14
+ /** Idle time since the box was last touched, e.g. "idle 5m" / "idle ?". */
15
+ export declare function fmtIdleShort(box: CrabboxBox, nowSecs: number): string;
16
+ /** Time until the lease expires, e.g. "expires 42m" / "expires ?" / "expired". */
17
+ export declare function fmtExpiresShort(box: CrabboxBox, nowSecs: number): string;
18
+ /** Reachable address for a leased box: tailnet FQDN/IP first, else public IP. */
19
+ export declare function boxAddress(box: CrabboxBox): string | undefined;
20
+ /** Human status: "ready" when usable, else the raw bootstrap state/status. */
21
+ export declare function boxStatus(box: CrabboxBox): string;
22
+ /**
23
+ * Warm boxes eligible for reuse: `ready` and the lease has not expired.
24
+ * Sorted most-recently-touched first so `--reuse` / the auto-pick lands on the
25
+ * freshest box (an untouched `lastTouchedAt` sorts last).
26
+ */
27
+ export declare function reusableBoxes(boxes: CrabboxBox[], nowSecs: number): CrabboxBox[];
28
+ /** One aligned row for the reuse picker / `agents lease list`. */
29
+ export declare function formatBoxRow(box: CrabboxBox, nowSecs: number): string;
11
30
  /** Validate a Hetzner token against the live API. Exported for unit tests (fetch injectable). */
12
31
  export declare function validateHetznerToken(token: string, fetchImpl?: typeof fetch): Promise<'valid' | 'invalid' | 'unreachable'>;
13
32
  /**
@@ -7,9 +7,9 @@
7
7
  * that have been untouched for a safety window are eligible (see `isReapSafe`),
8
8
  * so a box a concurrent run just reused is never stopped.
9
9
  */
10
- import { spawn } from 'child_process';
11
10
  import chalk from 'chalk';
12
- import { crabboxList, reapSafeOrphans, reapOrphans, setLeaseSecretsBundle } from '../lib/crabbox/cli.js';
11
+ import { openUrl } from '../lib/open-url.js';
12
+ import { crabboxList, crabboxStop, reapSafeOrphans, reapOrphans, setLeaseSecretsBundle } from '../lib/crabbox/cli.js';
13
13
  import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
14
14
  import { bundleExists, readBundle, writeBundle, keychainRef, bundleItemStore } from '../lib/secrets/bundles.js';
15
15
  import { secretsKeychainItem } from '../lib/secrets/index.js';
@@ -19,21 +19,104 @@ function fmtIdle(box) {
19
19
  const iso = new Date(box.lastTouchedAt * 1000).toISOString().slice(0, 16).replace('T', ' ');
20
20
  return `idle since ${iso}Z`;
21
21
  }
22
+ // ── Shared box helpers (consumed by exec.ts's reuse picker + ssh.ts's devices
23
+ // section, so the reuse/format logic lives in exactly one place) ─────────────
24
+ /** Compact human duration: "45s", "12m", "2h", "1h 5m". Clamps negatives to 0. */
25
+ export function fmtDurationShort(secs) {
26
+ const s = Math.max(0, Math.round(secs));
27
+ if (s < 60)
28
+ return `${s}s`;
29
+ const m = Math.round(s / 60);
30
+ if (m < 60)
31
+ return `${m}m`;
32
+ const h = Math.floor(m / 60);
33
+ const rem = m % 60;
34
+ return rem ? `${h}h ${rem}m` : `${h}h`;
35
+ }
36
+ /** Idle time since the box was last touched, e.g. "idle 5m" / "idle ?". */
37
+ export function fmtIdleShort(box, nowSecs) {
38
+ if (box.lastTouchedAt === null)
39
+ return 'idle ?';
40
+ return `idle ${fmtDurationShort(nowSecs - box.lastTouchedAt)}`;
41
+ }
42
+ /** Time until the lease expires, e.g. "expires 42m" / "expires ?" / "expired". */
43
+ export function fmtExpiresShort(box, nowSecs) {
44
+ if (box.expiresAt === null)
45
+ return 'expires ?';
46
+ const left = box.expiresAt - nowSecs;
47
+ return left <= 0 ? 'expired' : `expires ${fmtDurationShort(left)}`;
48
+ }
49
+ /** Reachable address for a leased box: tailnet FQDN/IP first, else public IP. */
50
+ export function boxAddress(box) {
51
+ return box.tailscaleFQDN || box.tailscaleIPv4 || box.ip || undefined;
52
+ }
53
+ /** Human status: "ready" when usable, else the raw bootstrap state/status. */
54
+ export function boxStatus(box) {
55
+ return box.ready ? 'ready' : box.state || box.status || 'pending';
56
+ }
57
+ /**
58
+ * Warm boxes eligible for reuse: `ready` and the lease has not expired.
59
+ * Sorted most-recently-touched first so `--reuse` / the auto-pick lands on the
60
+ * freshest box (an untouched `lastTouchedAt` sorts last).
61
+ */
62
+ export function reusableBoxes(boxes, nowSecs) {
63
+ return boxes
64
+ .filter((b) => b.ready && (b.expiresAt === null || b.expiresAt > nowSecs))
65
+ .sort((a, b) => (b.lastTouchedAt ?? 0) - (a.lastTouchedAt ?? 0));
66
+ }
67
+ /** One aligned row for the reuse picker / `agents lease list`. */
68
+ export function formatBoxRow(box, nowSecs) {
69
+ const slug = box.slug.padEnd(16);
70
+ const cls = (box.class ?? '?').padEnd(10);
71
+ const addr = (boxAddress(box) ?? '—').padEnd(24);
72
+ const status = boxStatus(box).padEnd(8);
73
+ const idle = fmtIdleShort(box, nowSecs).padEnd(12);
74
+ return `${slug} ${cls} ${addr} ${status} ${idle} ${fmtExpiresShort(box, nowSecs)}`;
75
+ }
22
76
  const HETZNER_BUNDLE = 'hetzner.com';
23
77
  const HCLOUD_KEY = 'HCLOUD_TOKEN';
24
78
  const HETZNER_CONSOLE_URL = 'https://console.hetzner.cloud/';
25
- /** Best-effort: open a URL in the user's default browser. Never throws. */
26
- function openUrl(url) {
27
- const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'cmd' : 'xdg-open';
28
- const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url];
79
+ const TAILSCALE_BUNDLE = 'tailscale.com';
80
+ const TAILSCALE_KEY = 'CRABBOX_TAILSCALE_AUTH_KEY';
81
+ const TAILSCALE_KEYS_URL = 'https://login.tailscale.com/admin/settings/keys';
82
+ /**
83
+ * Optional Tailscale setup for private-network leases (`--tailscale`). Collects
84
+ * an EPHEMERAL, pre-authorized, `tag:crabbox` auth key and stores it in the
85
+ * `tailscale.com` keychain bundle under `CRABBOX_TAILSCALE_AUTH_KEY` (the exact
86
+ * key `crabboxEnv` auto-injects). Blank input skips — public-IP leases still
87
+ * work with no Tailscale key. Never throws for cancel; mirrors the Hetzner
88
+ * capture above but does no live validation (Tailscale has no cheap probe).
89
+ */
90
+ async function captureTailscaleAuthKey() {
91
+ console.error(chalk.bold('\nOptional: private-network leases over Tailscale'));
92
+ console.error(chalk.dim('Mint an EPHEMERAL, pre-authorized auth key tagged `tag:crabbox` in the Tailscale admin,'));
93
+ console.error(chalk.dim('then paste it to reach leased boxes only over your tailnet (`--tailscale`). Leave blank to skip.\n'));
94
+ const { password } = await import('@inquirer/prompts');
95
+ let key;
29
96
  try {
30
- const p = spawn(cmd, args, { stdio: 'ignore', detached: true });
31
- p.on('error', () => { });
32
- p.unref();
97
+ openUrl(TAILSCALE_KEYS_URL);
98
+ key = (await password({ message: 'Paste a Tailscale auth key (blank to skip):', mask: true })).trim();
33
99
  }
34
- catch {
35
- /* best-effort */
100
+ catch (e) {
101
+ if (isPromptCancelled(e)) {
102
+ console.error(chalk.yellow('Skipped Tailscale setup.'));
103
+ return;
104
+ }
105
+ throw e;
106
+ }
107
+ if (!key) {
108
+ console.error(chalk.dim('Skipped Tailscale setup — public-IP leases only.'));
109
+ return;
36
110
  }
111
+ const bundle = bundleExists(TAILSCALE_BUNDLE)
112
+ ? readBundle(TAILSCALE_BUNDLE)
113
+ : { name: TAILSCALE_BUNDLE, description: 'Tailscale ephemeral auth key for crabbox tailnet leases', vars: {} };
114
+ const store = bundleItemStore(bundle.backend);
115
+ store.set(secretsKeychainItem(TAILSCALE_BUNDLE, TAILSCALE_KEY), key);
116
+ bundle.vars[TAILSCALE_KEY] = keychainRef(TAILSCALE_KEY);
117
+ writeBundle(bundle);
118
+ console.error(chalk.green(`✔ Stored Tailscale auth key in keychain bundle '${TAILSCALE_BUNDLE}'.`));
119
+ console.error(chalk.dim(' Add --tailscale to a lease (reuse defaults to it) to reach the box over your tailnet.'));
37
120
  }
38
121
  /** Validate a Hetzner token against the live API. Exported for unit tests (fetch injectable). */
39
122
  export async function validateHetznerToken(token, fetchImpl = fetch) {
@@ -103,6 +186,8 @@ export async function runLeaseSetup(opts = {}) {
103
186
  setLeaseSecretsBundle(HETZNER_BUNDLE);
104
187
  console.error(chalk.green(`\n✔ Stored in keychain bundle '${HETZNER_BUNDLE}' and set as the default lease provider.`));
105
188
  console.error(chalk.dim(' Run `agents run <agent> "…" --lease` — no env var, no flag needed.'));
189
+ // Also offer to capture a Tailscale auth key for private-network leases.
190
+ await captureTailscaleAuthKey();
106
191
  return true;
107
192
  }
108
193
  console.error(chalk.yellow('lease setup: no valid token after 3 attempts — aborted.'));
@@ -128,6 +213,48 @@ export function registerLeaseCommand(program) {
128
213
  const ok = await runLeaseSetup({ provider: opts.provider });
129
214
  process.exit(ok ? 0 : 1);
130
215
  });
216
+ lease
217
+ .command('list')
218
+ .alias('ls')
219
+ .description('List warm crabbox boxes you can reuse with `agents run --box <slug>`.')
220
+ .option('--json', 'Output JSON', false)
221
+ .action((opts) => {
222
+ const boxOpts = { secretsBundle: process.env.AGENTS_LEASE_SECRETS_BUNDLE };
223
+ let boxes;
224
+ try {
225
+ boxes = crabboxList(boxOpts);
226
+ }
227
+ catch (e) {
228
+ console.error(chalk.red(`lease list: ${e.message}`));
229
+ process.exit(1);
230
+ return;
231
+ }
232
+ if (opts.json) {
233
+ console.log(JSON.stringify(boxes, null, 2));
234
+ return;
235
+ }
236
+ if (boxes.length === 0) {
237
+ console.error(chalk.gray('No crabbox boxes. `agents run <agent> "…" --lease` provisions one.'));
238
+ return;
239
+ }
240
+ const nowSecs = Math.floor(Date.now() / 1000);
241
+ console.log(chalk.bold(`Warm boxes (${boxes.length})`));
242
+ for (const b of boxes)
243
+ console.log(' ' + formatBoxRow(b, nowSecs));
244
+ console.log(chalk.gray(' Reuse: agents run <agent> "…" --box <slug> · Stop: agents lease stop <slug>'));
245
+ });
246
+ lease
247
+ .command('stop <slug>')
248
+ .description('Stop (release) a leased crabbox box now.')
249
+ .action((slug) => {
250
+ const boxOpts = { secretsBundle: process.env.AGENTS_LEASE_SECRETS_BUNDLE };
251
+ const ok = crabboxStop(slug, boxOpts);
252
+ if (ok)
253
+ console.error(chalk.green(`Stopped box ${slug}.`));
254
+ else
255
+ console.error(chalk.red(`Could not stop box ${slug} (already gone, or crabbox is unavailable).`));
256
+ process.exit(ok ? 0 : 1);
257
+ });
131
258
  lease
132
259
  .command('gc')
133
260
  .description('Stop expired, idle lease boxes that are holding your provider quota. Safe: never stops a box in active use.')
@@ -0,0 +1,2 @@
1
+ import type { Command } from 'commander';
2
+ export declare function registerLoginCommands(program: Command): void;
@@ -0,0 +1,123 @@
1
+ import chalk from 'chalk';
2
+ import * as fs from 'fs';
3
+ import { readStdinSync } from '../lib/format.js';
4
+ import { clearVaultKey, createVault, getVaultSession, joinVault, unlock, vaultExists, vaultPath, } from '../lib/secrets/vault.js';
5
+ import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
6
+ async function promptPassword(message) {
7
+ if (!isInteractiveTerminal()) {
8
+ throw new Error('A password is required. Run from a TTY or pass --password-stdin.');
9
+ }
10
+ const { password } = await import('@inquirer/prompts');
11
+ return password({ message, mask: true });
12
+ }
13
+ async function readPassword(opts, message) {
14
+ if (opts.passwordStdin) {
15
+ const value = readStdinSync();
16
+ if (!value)
17
+ throw new Error('No password received on stdin.');
18
+ return value.replace(/\r?\n$/, '');
19
+ }
20
+ return promptPassword(message);
21
+ }
22
+ async function chooseFreshLoginMode(opts) {
23
+ if (opts.create && opts.join)
24
+ throw new Error('--create and --join are mutually exclusive.');
25
+ if (opts.create)
26
+ return 'create';
27
+ if (opts.join)
28
+ return 'join';
29
+ if (!isInteractiveTerminal()) {
30
+ throw new Error('No synced secrets file exists. Pass --create or --join <vault.age> in a non-interactive shell.');
31
+ }
32
+ const { select } = await import('@inquirer/prompts');
33
+ return select({
34
+ message: 'No synced secrets login found. What do you want to do?',
35
+ choices: [
36
+ { name: 'Create a new login', value: 'create' },
37
+ { name: 'Join with an existing synced secrets file', value: 'join' },
38
+ ],
39
+ });
40
+ }
41
+ function formatRemaining(expiresAt) {
42
+ const ms = expiresAt - Date.now();
43
+ if (ms <= 0)
44
+ return 'expired';
45
+ const mins = Math.round(ms / 60000);
46
+ if (mins < 60)
47
+ return `${mins} minute${mins === 1 ? '' : 's'}`;
48
+ const hours = Math.round(mins / 60);
49
+ if (hours < 24)
50
+ return `${hours} hour${hours === 1 ? '' : 's'}`;
51
+ const days = Math.round(hours / 24);
52
+ return `${days} day${days === 1 ? '' : 's'}`;
53
+ }
54
+ export function registerLoginCommands(program) {
55
+ program
56
+ .command('login')
57
+ .description('Unlock synced secrets for this shell session')
58
+ .option('--create', 'Create a new encrypted synced-secrets file at ~/.agents/vault.age')
59
+ .option('--join <path>', 'Copy and unlock an existing vault.age file')
60
+ .option('--force', 'Replace an existing synced-secrets file when used with --create or --join')
61
+ .option('--password-stdin', 'Read the master password from stdin')
62
+ .action(async (opts) => {
63
+ try {
64
+ if (opts.force && !opts.create && !opts.join) {
65
+ throw new Error('--force can only be used with --create or --join.');
66
+ }
67
+ if (vaultExists() && !opts.create && !opts.join) {
68
+ const password = await readPassword(opts, 'Master password');
69
+ unlock(password);
70
+ const session = getVaultSession();
71
+ const ttl = session.loggedIn ? formatRemaining(session.expiresAt) : '8 hours';
72
+ console.log(chalk.green(`Logged in. Synced secrets unlocked for ${ttl}.`));
73
+ return;
74
+ }
75
+ const mode = await chooseFreshLoginMode(opts);
76
+ if (mode === 'join') {
77
+ let source = opts.join;
78
+ if (!source) {
79
+ if (!isInteractiveTerminal())
80
+ throw new Error('--join needs a vault.age path.');
81
+ const { input } = await import('@inquirer/prompts');
82
+ source = await input({
83
+ message: 'Path to existing vault.age',
84
+ validate: (value) => fs.existsSync(value) || 'File not found.',
85
+ });
86
+ }
87
+ const password = await readPassword(opts, 'Master password');
88
+ joinVault(password, source, { overwrite: opts.force });
89
+ console.log(chalk.green(`Logged in. Copied synced secrets file to ${vaultPath()}.`));
90
+ return;
91
+ }
92
+ const password = await readPassword(opts, 'Choose a master password');
93
+ createVault(password, { overwrite: opts.force });
94
+ console.log(chalk.green(`Logged in. Created synced secrets file at ${vaultPath()}.`));
95
+ }
96
+ catch (err) {
97
+ if (isPromptCancelled(err))
98
+ return;
99
+ console.error(chalk.red(err.message));
100
+ process.exit(1);
101
+ }
102
+ });
103
+ program
104
+ .command('logout')
105
+ .description('Forget the cached synced-secrets key')
106
+ .action(() => {
107
+ clearVaultKey();
108
+ console.log(chalk.green('Logged out. Synced secrets are locked.'));
109
+ });
110
+ program
111
+ .command('whoami')
112
+ .description('Show synced-secrets login status')
113
+ .action(() => {
114
+ const session = getVaultSession();
115
+ if (!session.loggedIn) {
116
+ console.log(chalk.yellow('Not logged in.'));
117
+ console.log(chalk.gray(`File: ${vaultPath()}${vaultExists() ? '' : ' (not created)'}`));
118
+ return;
119
+ }
120
+ console.log(chalk.green(`Logged in (${formatRemaining(session.expiresAt)} remaining).`));
121
+ console.log(chalk.gray(`File: ${vaultPath()}`));
122
+ });
123
+ }
@@ -62,7 +62,7 @@ async function showSession(session, follow, full, json = false) {
62
62
  console.error(chalk.red(`Tailing is supported for claude and codex sessions only (got ${session.agent}).`));
63
63
  process.exit(2);
64
64
  }
65
- await streamSessionTail(session, {});
65
+ await streamSessionTail(session, { raw: full });
66
66
  return;
67
67
  }
68
68
  await renderSessionLog(session, full ? 'markdown' : 'summary');
@@ -0,0 +1,27 @@
1
+ /**
2
+ * `agents mine` — white-label the CLI under your own name.
3
+ *
4
+ * `agents mine init <name>` mints a personally-named binary (e.g. `jack`) that
5
+ * IS agents-cli: a pure pass-through shim on PATH that runs every `agents` verb
6
+ * under the brand's name, with the brand's disabled commands and curated
7
+ * resource profile applied. `list` / `toggle` / `remove` manage brands.
8
+ *
9
+ * The discoverable entry point is the `agents setup mine` wizard
10
+ * (see setup-mine.ts), which delegates to `initBrand` here.
11
+ *
12
+ * Storage: brand config in `meta.brands` (agents.yaml); the curated resource set
13
+ * reuses the resource-profile engine — each brand owns a preset named
14
+ * `mine-<name>` in `meta.profiles.presets`. See lib/brand.ts.
15
+ */
16
+ import type { Command } from 'commander';
17
+ /**
18
+ * Create (or re-mint) a brand. Shared by `mine init` and the `setup mine`
19
+ * wizard. Writes the shim, the brand config, and an empty resource preset.
20
+ */
21
+ export declare function initBrand(name: string, opts?: {
22
+ disabledCommands?: string[];
23
+ force?: boolean;
24
+ }): {
25
+ pathWarning: boolean;
26
+ };
27
+ export declare function registerMineCommand(program: Command): void;
@@ -0,0 +1,207 @@
1
+ import chalk from 'chalk';
2
+ import { validateBrandName, listBrands, getBrandConfig, upsertBrand, removeBrand, brandPresetName, } from '../lib/brand.js';
3
+ import { createBrandShim, removeBrandShim, isShimsInPath } from '../lib/shims.js';
4
+ import { updateMeta } from '../lib/state.js';
5
+ import { COMMAND_LOADERS } from '../lib/startup/command-registry.js';
6
+ /** Built-in top-level command names, for validating `--disable <cmd>`. */
7
+ function knownCommandNames() {
8
+ const names = new Set(Object.keys(COMMAND_LOADERS));
9
+ // Inline aliases registered outside COMMAND_LOADERS (see src/index.ts).
10
+ for (const n of ['perms', 'exec', 'jobs', 'cron', 'upgrade'])
11
+ names.add(n);
12
+ return names;
13
+ }
14
+ /**
15
+ * Add/remove a `!name` exclusion on a preset's pattern list for one kind,
16
+ * keeping `*` so everything not explicitly disabled stays enabled. Deletes the
17
+ * key entirely once nothing but `*` remains, so a clean brand has an empty preset.
18
+ */
19
+ function applyResourceToggle(preset, kind, name, enable) {
20
+ // Cast to a plain string-bag: writing through the union-keyed `preset[kind]`
21
+ // collapses to `never` under strict TS, so index the bag instead.
22
+ const bag = preset;
23
+ const arr = [...(bag[kind] ?? [])];
24
+ if (arr.length === 0)
25
+ arr.push('*');
26
+ const excl = `!${name}`;
27
+ const idx = arr.indexOf(excl);
28
+ if (enable) {
29
+ if (idx !== -1)
30
+ arr.splice(idx, 1);
31
+ }
32
+ else if (idx === -1) {
33
+ arr.push(excl);
34
+ }
35
+ const onlyStar = arr.length === 1 && arr[0] === '*';
36
+ if (onlyStar)
37
+ delete bag[kind];
38
+ else
39
+ bag[kind] = arr;
40
+ }
41
+ /** Ensure the brand's preset exists in meta.profiles.presets and return a mutable copy path. */
42
+ function ensurePresetExists(name) {
43
+ const presetName = brandPresetName(name);
44
+ updateMeta((meta) => {
45
+ const profiles = meta.profiles ?? {};
46
+ const presets = { ...(profiles.presets ?? {}) };
47
+ if (!presets[presetName]) {
48
+ presets[presetName] = { description: `Resource set for the "${name}" brand` };
49
+ }
50
+ return { ...meta, profiles: { ...profiles, presets } };
51
+ });
52
+ }
53
+ /** Mutate the brand's resource preset under lock. */
54
+ function editPreset(name, fn) {
55
+ const presetName = brandPresetName(name);
56
+ updateMeta((meta) => {
57
+ const profiles = meta.profiles ?? {};
58
+ const presets = { ...(profiles.presets ?? {}) };
59
+ const preset = { ...(presets[presetName] ?? {}) };
60
+ fn(preset);
61
+ presets[presetName] = preset;
62
+ return { ...meta, profiles: { ...profiles, presets } };
63
+ });
64
+ }
65
+ /**
66
+ * Create (or re-mint) a brand. Shared by `mine init` and the `setup mine`
67
+ * wizard. Writes the shim, the brand config, and an empty resource preset.
68
+ */
69
+ export function initBrand(name, opts = {}) {
70
+ const err = validateBrandName(name);
71
+ if (err) {
72
+ console.error(chalk.red(err));
73
+ process.exit(1);
74
+ }
75
+ if (getBrandConfig(name) && !opts.force) {
76
+ console.error(chalk.red(`Brand "${name}" already exists. Use --force to re-mint, or 'agents mine toggle ${name}'.`));
77
+ process.exit(1);
78
+ }
79
+ ensurePresetExists(name);
80
+ const disabled = (opts.disabledCommands ?? []).filter((c) => c.length > 0);
81
+ const known = knownCommandNames();
82
+ for (const c of disabled) {
83
+ if (!known.has(c))
84
+ console.error(chalk.yellow(` note: "${c}" is not a known command — kept anyway.`));
85
+ }
86
+ const cfg = {
87
+ name,
88
+ enabled: true,
89
+ profile: brandPresetName(name),
90
+ ...(disabled.length > 0 ? { disabledCommands: disabled } : {}),
91
+ };
92
+ upsertBrand(cfg);
93
+ createBrandShim(name);
94
+ const pathWarning = !isShimsInPath();
95
+ return { pathWarning };
96
+ }
97
+ function printMinted(name, pathWarning) {
98
+ console.log(`${chalk.green('Minted')} ${chalk.bold(name)} — your own agents CLI.`);
99
+ console.log(chalk.dim(` try: ${name} --help ${name} run claude "hello"`));
100
+ if (pathWarning) {
101
+ console.log(chalk.yellow(` note: the shims dir isn't on your PATH yet — run 'agents setup' or open a new shell.`));
102
+ }
103
+ }
104
+ export function registerMineCommand(program) {
105
+ const cmd = program
106
+ .command('mine')
107
+ .description('White-label the CLI: your own personally-named binary (e.g. `jack`)');
108
+ cmd
109
+ .command('init <name>')
110
+ .description('Mint your own branded CLI that runs every agents verb under <name>')
111
+ .option('--disable <commands...>', 'Built-in commands to hide from this brand (e.g. teams cloud)')
112
+ .option('--force', 'Re-mint even if the brand already exists')
113
+ .action((name, options) => {
114
+ const { pathWarning } = initBrand(name, {
115
+ disabledCommands: options.disable,
116
+ force: options.force,
117
+ });
118
+ printMinted(name, pathWarning);
119
+ });
120
+ cmd
121
+ .command('list')
122
+ .alias('ls')
123
+ .description('Show your brands and what each has turned off')
124
+ .action(() => {
125
+ const brands = listBrands();
126
+ const entries = Object.entries(brands);
127
+ if (entries.length === 0) {
128
+ console.log(chalk.gray("No brands yet. Try: agents setup mine (or: agents mine init jack)"));
129
+ return;
130
+ }
131
+ const width = entries.reduce((m, [n]) => Math.max(m, n.length), 0);
132
+ for (const [name, cfg] of entries) {
133
+ const off = cfg.disabledCommands?.length ? `disabled: ${cfg.disabledCommands.join(', ')}` : 'all commands on';
134
+ const state = cfg.enabled === false ? chalk.yellow('(disabled) ') : '';
135
+ console.log(` ${chalk.bold(name.padEnd(width))} ${state}${chalk.dim(off)}`);
136
+ }
137
+ });
138
+ cmd
139
+ .command('toggle <name>')
140
+ .description('Enable/disable features for a brand')
141
+ .option('--disable <commands...>', 'Hide these built-in commands')
142
+ .option('--enable <commands...>', 'Un-hide these built-in commands')
143
+ .option('--disable-plugin <names...>', 'Disable these plugins for the brand')
144
+ .option('--enable-plugin <names...>', 'Re-enable these plugins')
145
+ .option('--disable-skill <names...>', 'Disable these skills for the brand')
146
+ .option('--enable-skill <names...>', 'Re-enable these skills')
147
+ .action((name, options) => {
148
+ const cfg = getBrandConfig(name);
149
+ if (!cfg) {
150
+ console.error(chalk.red(`No brand named "${name}". Create it with 'agents mine init ${name}'.`));
151
+ process.exit(1);
152
+ }
153
+ // Built-in command toggles → brand.disabledCommands.
154
+ const disabledSet = new Set(cfg.disabledCommands ?? []);
155
+ const known = knownCommandNames();
156
+ for (const c of options.disable ?? []) {
157
+ if (!known.has(c))
158
+ console.error(chalk.yellow(` note: "${c}" is not a known command — kept anyway.`));
159
+ disabledSet.add(c);
160
+ }
161
+ for (const c of options.enable ?? [])
162
+ disabledSet.delete(c);
163
+ const nextDisabled = [...disabledSet];
164
+ const nextCfg = { ...cfg };
165
+ if (nextDisabled.length > 0)
166
+ nextCfg.disabledCommands = nextDisabled;
167
+ else
168
+ delete nextCfg.disabledCommands;
169
+ upsertBrand(nextCfg);
170
+ // Resource toggles → the brand's profile preset.
171
+ const resourceOps = [];
172
+ for (const p of options.disablePlugin ?? [])
173
+ resourceOps.push(['plugins', p, false]);
174
+ for (const p of options.enablePlugin ?? [])
175
+ resourceOps.push(['plugins', p, true]);
176
+ for (const s of options.disableSkill ?? [])
177
+ resourceOps.push(['skills', s, false]);
178
+ for (const s of options.enableSkill ?? [])
179
+ resourceOps.push(['skills', s, true]);
180
+ if (resourceOps.length > 0) {
181
+ ensurePresetExists(name);
182
+ editPreset(name, (preset) => {
183
+ for (const [kind, n, enable] of resourceOps)
184
+ applyResourceToggle(preset, kind, n, enable);
185
+ });
186
+ }
187
+ console.log(`${chalk.green('Updated')} ${chalk.bold(name)}.`);
188
+ const finalCfg = getBrandConfig(name);
189
+ const off = finalCfg.disabledCommands?.length ? finalCfg.disabledCommands.join(', ') : '(none)';
190
+ console.log(chalk.dim(` disabled commands: ${off}`));
191
+ });
192
+ cmd
193
+ .command('remove <name>')
194
+ .alias('rm')
195
+ .description('Remove a brand (its shim + config)')
196
+ .option('--purge', "Also delete the brand's resource profile preset")
197
+ .action((name, options) => {
198
+ const existed = getBrandConfig(name) !== undefined;
199
+ const shimRemoved = removeBrandShim(name);
200
+ if (!existed && !shimRemoved) {
201
+ console.error(chalk.red(`No brand named "${name}".`));
202
+ process.exit(1);
203
+ }
204
+ removeBrand(name, options.purge === true);
205
+ console.log(`${chalk.yellow('Removed')} brand ${chalk.bold(name)}`);
206
+ });
207
+ }