@phnx-labs/agents-cli 1.20.63 → 1.20.65

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 (197) hide show
  1. package/CHANGELOG.md +56 -3
  2. package/README.md +46 -2
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/apply.d.ts +12 -0
  5. package/dist/commands/apply.js +274 -0
  6. package/dist/commands/browser.js +2 -2
  7. package/dist/commands/cloud.js +32 -2
  8. package/dist/commands/doctor.js +4 -1
  9. package/dist/commands/exec.js +149 -77
  10. package/dist/commands/feed.d.ts +4 -0
  11. package/dist/commands/feed.js +52 -19
  12. package/dist/commands/hosts.js +44 -6
  13. package/dist/commands/lease.d.ts +23 -0
  14. package/dist/commands/lease.js +201 -0
  15. package/dist/commands/mailboxes.d.ts +20 -0
  16. package/dist/commands/mailboxes.js +390 -0
  17. package/dist/commands/mcp.js +55 -5
  18. package/dist/commands/monitors.d.ts +12 -0
  19. package/dist/commands/monitors.js +740 -0
  20. package/dist/commands/output.js +2 -2
  21. package/dist/commands/routines.js +43 -16
  22. package/dist/commands/secrets.d.ts +16 -0
  23. package/dist/commands/secrets.js +215 -64
  24. package/dist/commands/serve.js +31 -0
  25. package/dist/commands/sessions-export.d.ts +2 -0
  26. package/dist/commands/sessions-export.js +284 -0
  27. package/dist/commands/sessions-import.d.ts +2 -0
  28. package/dist/commands/sessions-import.js +230 -0
  29. package/dist/commands/sessions.d.ts +16 -0
  30. package/dist/commands/sessions.js +40 -6
  31. package/dist/commands/ssh.js +143 -5
  32. package/dist/commands/usage.d.ts +2 -0
  33. package/dist/commands/usage.js +7 -2
  34. package/dist/commands/versions.js +7 -3
  35. package/dist/commands/view.d.ts +27 -1
  36. package/dist/commands/view.js +32 -9
  37. package/dist/commands/webhook.js +10 -2
  38. package/dist/index.js +35 -14
  39. package/dist/lib/agents.d.ts +36 -0
  40. package/dist/lib/agents.js +80 -20
  41. package/dist/lib/auto-dispatch-provider.js +7 -2
  42. package/dist/lib/auto-dispatch.d.ts +3 -0
  43. package/dist/lib/auto-dispatch.js +3 -0
  44. package/dist/lib/browser/chrome.js +2 -2
  45. package/dist/lib/browser/drivers/ssh.js +19 -2
  46. package/dist/lib/cloud/antigravity.js +2 -2
  47. package/dist/lib/cloud/host.d.ts +59 -0
  48. package/dist/lib/cloud/host.js +224 -0
  49. package/dist/lib/cloud/registry.js +4 -0
  50. package/dist/lib/cloud/types.d.ts +6 -4
  51. package/dist/lib/comms-render.d.ts +37 -0
  52. package/dist/lib/comms-render.js +89 -0
  53. package/dist/lib/computer-rpc.js +3 -1
  54. package/dist/lib/crabbox/cli.d.ts +72 -0
  55. package/dist/lib/crabbox/cli.js +162 -9
  56. package/dist/lib/crabbox/runtimes.d.ts +13 -0
  57. package/dist/lib/crabbox/runtimes.js +35 -2
  58. package/dist/lib/daemon.d.ts +20 -4
  59. package/dist/lib/daemon.js +68 -20
  60. package/dist/lib/devices/fleet.d.ts +3 -2
  61. package/dist/lib/devices/fleet.js +9 -0
  62. package/dist/lib/devices/health.d.ts +77 -0
  63. package/dist/lib/devices/health.js +186 -0
  64. package/dist/lib/devices/registry.d.ts +15 -0
  65. package/dist/lib/devices/registry.js +9 -0
  66. package/dist/lib/exec.d.ts +19 -2
  67. package/dist/lib/exec.js +41 -13
  68. package/dist/lib/fleet/apply.d.ts +63 -0
  69. package/dist/lib/fleet/apply.js +214 -0
  70. package/dist/lib/fleet/auth-sync.d.ts +67 -0
  71. package/dist/lib/fleet/auth-sync.js +142 -0
  72. package/dist/lib/fleet/manifest.d.ts +29 -0
  73. package/dist/lib/fleet/manifest.js +127 -0
  74. package/dist/lib/fleet/types.d.ts +129 -0
  75. package/dist/lib/fleet/types.js +13 -0
  76. package/dist/lib/git.d.ts +27 -0
  77. package/dist/lib/git.js +34 -2
  78. package/dist/lib/hosts/dispatch.d.ts +29 -8
  79. package/dist/lib/hosts/dispatch.js +46 -18
  80. package/dist/lib/hosts/passthrough.js +2 -0
  81. package/dist/lib/hosts/providers/devices.d.ts +27 -0
  82. package/dist/lib/hosts/providers/devices.js +98 -0
  83. package/dist/lib/hosts/registry.d.ts +10 -16
  84. package/dist/lib/hosts/registry.js +17 -50
  85. package/dist/lib/hosts/remote-cmd.d.ts +23 -0
  86. package/dist/lib/hosts/remote-cmd.js +71 -0
  87. package/dist/lib/hosts/run-target.d.ts +84 -0
  88. package/dist/lib/hosts/run-target.js +99 -0
  89. package/dist/lib/hosts/types.d.ts +23 -5
  90. package/dist/lib/hosts/types.js +22 -4
  91. package/dist/lib/linear-autoclose.d.ts +30 -0
  92. package/dist/lib/linear-autoclose.js +22 -0
  93. package/dist/lib/mailbox.d.ts +39 -0
  94. package/dist/lib/mailbox.js +112 -0
  95. package/dist/lib/mcp.d.ts +27 -1
  96. package/dist/lib/mcp.js +126 -12
  97. package/dist/lib/monitors/config.d.ts +161 -0
  98. package/dist/lib/monitors/config.js +372 -0
  99. package/dist/lib/monitors/dispatch.d.ts +28 -0
  100. package/dist/lib/monitors/dispatch.js +91 -0
  101. package/dist/lib/monitors/engine.d.ts +61 -0
  102. package/dist/lib/monitors/engine.js +201 -0
  103. package/dist/lib/monitors/sources/command.d.ts +11 -0
  104. package/dist/lib/monitors/sources/command.js +31 -0
  105. package/dist/lib/monitors/sources/device.d.ts +13 -0
  106. package/dist/lib/monitors/sources/device.js +35 -0
  107. package/dist/lib/monitors/sources/file.d.ts +14 -0
  108. package/dist/lib/monitors/sources/file.js +57 -0
  109. package/dist/lib/monitors/sources/http.d.ts +10 -0
  110. package/dist/lib/monitors/sources/http.js +34 -0
  111. package/dist/lib/monitors/sources/index.d.ts +14 -0
  112. package/dist/lib/monitors/sources/index.js +31 -0
  113. package/dist/lib/monitors/sources/poll.d.ts +9 -0
  114. package/dist/lib/monitors/sources/poll.js +9 -0
  115. package/dist/lib/monitors/sources/types.d.ts +18 -0
  116. package/dist/lib/monitors/sources/types.js +9 -0
  117. package/dist/lib/monitors/sources/webhook.d.ts +23 -0
  118. package/dist/lib/monitors/sources/webhook.js +47 -0
  119. package/dist/lib/monitors/sources/ws.d.ts +14 -0
  120. package/dist/lib/monitors/sources/ws.js +45 -0
  121. package/dist/lib/monitors/state.d.ts +69 -0
  122. package/dist/lib/monitors/state.js +144 -0
  123. package/dist/lib/paths.d.ts +13 -0
  124. package/dist/lib/paths.js +26 -4
  125. package/dist/lib/platform/exec.d.ts +16 -0
  126. package/dist/lib/platform/exec.js +17 -0
  127. package/dist/lib/plugins.js +101 -2
  128. package/dist/lib/redact.d.ts +14 -1
  129. package/dist/lib/redact.js +47 -1
  130. package/dist/lib/remote-agents-json.js +7 -1
  131. package/dist/lib/rotate.d.ts +6 -3
  132. package/dist/lib/rotate.js +0 -1
  133. package/dist/lib/routines.d.ts +37 -2
  134. package/dist/lib/routines.js +54 -12
  135. package/dist/lib/runner.d.ts +1 -0
  136. package/dist/lib/runner.js +348 -13
  137. package/dist/lib/sandbox.d.ts +9 -1
  138. package/dist/lib/sandbox.js +11 -2
  139. package/dist/lib/secrets/agent.d.ts +48 -10
  140. package/dist/lib/secrets/agent.js +123 -15
  141. package/dist/lib/secrets/bundles.d.ts +26 -0
  142. package/dist/lib/secrets/bundles.js +59 -8
  143. package/dist/lib/secrets/mcp.js +4 -2
  144. package/dist/lib/secrets/remote.d.ts +17 -0
  145. package/dist/lib/secrets/remote.js +40 -0
  146. package/dist/lib/self-update.d.ts +20 -0
  147. package/dist/lib/self-update.js +54 -1
  148. package/dist/lib/serve/control.d.ts +95 -0
  149. package/dist/lib/serve/control.js +260 -0
  150. package/dist/lib/serve/server.d.ts +35 -1
  151. package/dist/lib/serve/server.js +106 -76
  152. package/dist/lib/serve/stream.d.ts +43 -0
  153. package/dist/lib/serve/stream.js +116 -0
  154. package/dist/lib/serve/token.d.ts +35 -0
  155. package/dist/lib/serve/token.js +85 -0
  156. package/dist/lib/session/bundle.d.ts +164 -0
  157. package/dist/lib/session/bundle.js +200 -0
  158. package/dist/lib/session/remote-bundle.d.ts +12 -0
  159. package/dist/lib/session/remote-bundle.js +61 -0
  160. package/dist/lib/session/remote-list.js +5 -1
  161. package/dist/lib/session/state.d.ts +7 -25
  162. package/dist/lib/session/state.js +16 -6
  163. package/dist/lib/session/sync/agents.d.ts +54 -6
  164. package/dist/lib/session/sync/agents.js +0 -0
  165. package/dist/lib/session/sync/config.js +8 -2
  166. package/dist/lib/session/sync/manifest.d.ts +14 -3
  167. package/dist/lib/session/sync/manifest.js +4 -0
  168. package/dist/lib/session/sync/sync.d.ts +23 -2
  169. package/dist/lib/session/sync/sync.js +177 -74
  170. package/dist/lib/session/types.d.ts +30 -0
  171. package/dist/lib/ssh-tunnel.d.ts +19 -1
  172. package/dist/lib/ssh-tunnel.js +99 -8
  173. package/dist/lib/staleness/detectors/subagents.d.ts +5 -0
  174. package/dist/lib/staleness/detectors/subagents.js +5 -192
  175. package/dist/lib/staleness/writers/subagents.d.ts +10 -0
  176. package/dist/lib/staleness/writers/subagents.js +11 -102
  177. package/dist/lib/startup/command-registry.d.ts +4 -0
  178. package/dist/lib/startup/command-registry.js +9 -0
  179. package/dist/lib/state.d.ts +5 -0
  180. package/dist/lib/state.js +12 -0
  181. package/dist/lib/subagents-registry.d.ts +85 -0
  182. package/dist/lib/subagents-registry.js +393 -0
  183. package/dist/lib/subagents.d.ts +8 -8
  184. package/dist/lib/subagents.js +32 -663
  185. package/dist/lib/sync-umbrella.d.ts +1 -0
  186. package/dist/lib/sync-umbrella.js +14 -3
  187. package/dist/lib/tmux/session.d.ts +7 -0
  188. package/dist/lib/tmux/session.js +3 -1
  189. package/dist/lib/triggers/webhook.d.ts +18 -0
  190. package/dist/lib/triggers/webhook.js +105 -0
  191. package/dist/lib/types.d.ts +35 -1
  192. package/dist/lib/usage.d.ts +42 -3
  193. package/dist/lib/usage.js +163 -21
  194. package/dist/lib/versions.js +14 -11
  195. package/dist/lib/workflows.d.ts +20 -0
  196. package/dist/lib/workflows.js +24 -0
  197. package/package.json +2 -1
@@ -8,9 +8,20 @@ import { gatherRemoteAgentsJson } from '../lib/remote-agents-json.js';
8
8
  import { loadPolicy, applyPolicyToBlock, isPhoneUrgent } from '../lib/feed-policy.js';
9
9
  import { notifyUrgentBlock } from '../lib/notify.js';
10
10
  import { gcMailbox } from '../lib/mailbox-gc.js';
11
+ import { isValidMailboxId } from '../lib/mailbox.js';
11
12
  import { getActiveSessions } from '../lib/session/active.js';
12
13
  import { mailboxIdForActiveSession } from '../lib/mailbox-target.js';
14
+ import { GLYPH, masthead } from '../lib/comms-render.js';
13
15
  export const FEED_NO_FANOUT_ENV = 'AGENTS_FEED_LOCAL';
16
+ /** Right-hand masthead summary: `N blocks · M agents`. */
17
+ export function formatFeedMastheadRight(blocks) {
18
+ const agents = new Set(blocks.map((b) => b.mailboxId)).size;
19
+ return `${blocks.length} block${blocks.length === 1 ? '' : 's'} · ${agents} agent${agents === 1 ? '' : 's'}`;
20
+ }
21
+ /** Reply hint matching the shared fleet-comms reply line. */
22
+ export function formatFeedReplyHint(mailboxId) {
23
+ return `↳ ag message ${mailboxId} "…"`;
24
+ }
14
25
  export function parseRemoteFeed(stdout, machine) {
15
26
  let parsed;
16
27
  try {
@@ -28,6 +39,11 @@ export function parseRemoteFeed(stdout, machine) {
28
39
  const block = item;
29
40
  if (!block.blockId || !block.sessionId || !block.mailboxId || !block.questions?.length)
30
41
  continue;
42
+ // A crafted mailboxId (path separators, `.`/`..`) would throw inside
43
+ // mailboxDir() when policy runs against the block, aborting the whole
44
+ // dispatch loop — drop it here so a malicious peer can't smuggle one in.
45
+ if (!isValidMailboxId(block.mailboxId))
46
+ continue;
31
47
  blocks.push({ ...block, host: machine });
32
48
  }
33
49
  return blocks;
@@ -60,7 +76,13 @@ function renderBlock(b, localHost, indent = '') {
60
76
  const cls = b.blockClass ? chalk.gray(`(${b.blockClass})`) : '';
61
77
  const consequence = b.consequence && b.consequence !== 'normal' ? chalk.red(`[${b.consequence}]`) : '';
62
78
  const cost = b.costOfDelay ? chalk.gray(`cost:${b.costOfDelay}`) : '';
63
- console.log(`${indent}${chalk.cyan(b.mailboxId)}${host} ${runtime} ${age} ${cls} ${consequence} ${cost}`.trimEnd());
79
+ // Shared fleet-comms glyphs: ▲ open ask, ✓ answered (see comms-render GLYPH).
80
+ const marker = b.answer
81
+ ? chalk.green(GLYPH.delivered)
82
+ : !b.parkedAt
83
+ ? chalk.yellow(GLYPH.ask)
84
+ : ' ';
85
+ console.log(`${indent}${marker} ${chalk.cyan(b.mailboxId)}${host} ${runtime} ${age} ${cls} ${consequence} ${cost}`.trimEnd());
64
86
  for (const question of b.questions) {
65
87
  const header = question.header ? chalk.gray(`[${question.header}] `) : '';
66
88
  console.log(`${indent} ${header}${question.text}`);
@@ -77,7 +99,7 @@ function renderBlock(b, localHost, indent = '') {
77
99
  console.log(`${indent} ${chalk.gray(meta)}`);
78
100
  }
79
101
  if (b.answer) {
80
- const verified = b.answer.verified ? chalk.green('✓') : chalk.yellow('?');
102
+ const verified = b.answer.verified ? chalk.green(GLYPH.delivered) : chalk.yellow('?');
81
103
  const who = b.answer.answeredFrom + (b.answer.answeredBy ? ` (${b.answer.answeredBy})` : '');
82
104
  console.log(`${indent} ${chalk.green('answered')} by ${who} ${verified}`);
83
105
  }
@@ -98,7 +120,7 @@ function renderBlock(b, localHost, indent = '') {
98
120
  console.log(`${indent} ${chalk.dim('notified')} ${relTime(b.notifiedAt)}`);
99
121
  }
100
122
  if (!b.answer && !b.parkedAt) {
101
- console.log(`${indent} ${chalk.dim('reply:')} agents message ${b.mailboxId} "<answer>"`);
123
+ console.log(`${indent} ${chalk.dim(formatFeedReplyHint(b.mailboxId))}`);
102
124
  }
103
125
  console.log();
104
126
  }
@@ -224,20 +246,28 @@ export function registerFeedCommand(program) {
224
246
  const policy = loadPolicy();
225
247
  const now = new Date();
226
248
  for (const b of blocks) {
227
- const result = applyPolicyToBlock(b, policy, now);
228
- if (result.action !== 'none') {
229
- console.log(`${chalk.yellow('policy')} ${b.blockId}: ${result.action}`);
230
- }
231
- if (isPhoneUrgent(b, policy)) {
232
- const notifyResult = await notifyUrgentBlock(b, { dryRun: opts.json });
233
- if (notifyResult.ok && !notifyResult.skipped) {
234
- recordNotified(b.blockId);
235
- console.log(`${chalk.green('notified')} ${b.blockId}`);
249
+ // Wrap per-block policy so one malformed block (e.g. a crafted
250
+ // mailboxId that throws in mailboxDir) can't abort the whole loop and
251
+ // strand every remaining block's dispatch.
252
+ try {
253
+ const result = applyPolicyToBlock(b, policy, now);
254
+ if (result.action !== 'none') {
255
+ console.log(`${chalk.yellow('policy')} ${b.blockId}: ${result.action}`);
236
256
  }
237
- else if (notifyResult.error) {
238
- console.error(chalk.yellow(`Notification failed for ${b.blockId}: ${notifyResult.error}`));
257
+ if (isPhoneUrgent(b, policy)) {
258
+ const notifyResult = await notifyUrgentBlock(b, { dryRun: opts.json });
259
+ if (notifyResult.ok && !notifyResult.skipped) {
260
+ recordNotified(b.blockId);
261
+ console.log(`${chalk.green('notified')} ${b.blockId}`);
262
+ }
263
+ else if (notifyResult.error) {
264
+ console.error(chalk.yellow(`Notification failed for ${b.blockId}: ${notifyResult.error}`));
265
+ }
239
266
  }
240
267
  }
268
+ catch (err) {
269
+ console.error(chalk.yellow(`Skipped block ${b.blockId}: ${err.message}`));
270
+ }
241
271
  }
242
272
  }
243
273
  for (const warning of setupWarnings) {
@@ -257,17 +287,20 @@ export function registerFeedCommand(program) {
257
287
  console.log(chalk.gray(digest ? 'No open blocks after stall suppression.' : 'No open blocks.'));
258
288
  return;
259
289
  }
290
+ // Shared fleet-comms masthead (same family as `agents mailboxes`).
291
+ console.log(masthead({
292
+ title: 'they need you',
293
+ accent: 'amber',
294
+ host: self,
295
+ right: formatFeedMastheadRight(blocks),
296
+ }));
297
+ console.log();
260
298
  if (opts.flat) {
261
- console.log(chalk.bold(`${blocks.length} open block${blocks.length === 1 ? '' : 's'}:\n`));
262
299
  for (const b of blocks)
263
300
  renderBlock(b, self);
264
301
  return;
265
302
  }
266
303
  const groups = groupBlocksByOutcome(blocks);
267
- const openOutcomes = groups.filter((g) => g.counts.open > 0).length;
268
- console.log(chalk.bold(`${groups.length} outcome${groups.length === 1 ? '' : 's'} · ${blocks.length} block${blocks.length === 1 ? '' : 's'}` +
269
- (openOutcomes > 0 ? ` · ${openOutcomes} need you` : '') +
270
- ':\n'));
271
304
  for (const g of groups)
272
305
  renderOutcomeGroup(g, self);
273
306
  });
@@ -11,6 +11,7 @@ import { terminalWidth, truncateToWidth, stringWidth } from '../lib/session/widt
11
11
  import { checkbox, confirm } from '@inquirer/prompts';
12
12
  import { assertValidSshTarget } from '../lib/ssh-exec.js';
13
13
  import { getProvider, listAllHosts, resolveHost } from '../lib/hosts/registry.js';
14
+ import { getDevice } from '../lib/devices/registry.js';
14
15
  import { sshTargetFor } from '../lib/hosts/types.js';
15
16
  import { listSshConfigHosts, listKnownHosts, isSshConfigHost } from '../lib/hosts/ssh-config.js';
16
17
  import { probeHost, remoteAgentsVersion, bootstrapAgentsCli, localCliVersion, } from '../lib/hosts/ready.js';
@@ -93,7 +94,7 @@ async function doAdd(name, target, opts) {
93
94
  return;
94
95
  }
95
96
  let spec;
96
- let sshTarget;
97
+ let sshTarget = '';
97
98
  if (target) {
98
99
  assertValidSshTarget(target);
99
100
  const { address, user } = parseTarget(target);
@@ -105,9 +106,36 @@ async function doAdd(name, target, opts) {
105
106
  sshTarget = name;
106
107
  }
107
108
  else {
108
- console.log(chalk.red(`"${name}" is not in ~/.ssh/config. Pass a target: agents hosts add ${name} <user@host>`));
109
- process.exitCode = 1;
110
- return;
109
+ // A registered device enrolls with no target connection details come from
110
+ // the device profile. The main reason to enroll one at all is the overlay
111
+ // metadata (capability tags); plain dispatch already works via the devices
112
+ // provider.
113
+ const device = await getDevice(name);
114
+ if (device && device.auth.method !== 'password') {
115
+ const address = device.address.dnsName ?? device.address.ip;
116
+ if (address) {
117
+ spec = {
118
+ name,
119
+ provider: 'local',
120
+ source: 'inline',
121
+ address,
122
+ user: device.user,
123
+ caps: opts.cap,
124
+ os: opts.os ?? (device.platform !== 'unknown' ? device.platform : undefined),
125
+ };
126
+ sshTarget = device.user ? `${device.user}@${address}` : address;
127
+ }
128
+ }
129
+ if (!spec) {
130
+ if (device?.auth.method === 'password') {
131
+ console.log(chalk.red(`Device "${name}" uses password auth — dispatch needs key auth. Switch it first: agents devices set ${name} --auth key`));
132
+ }
133
+ else {
134
+ console.log(chalk.red(`"${name}" is not in ~/.ssh/config or the devices registry. Pass a target: agents hosts add ${name} <user@host>`));
135
+ }
136
+ process.exitCode = 1;
137
+ return;
138
+ }
111
139
  }
112
140
  const probe = probeHost(sshTarget);
113
141
  if (!spec.os && probe.os)
@@ -133,8 +161,12 @@ async function doList(json) {
133
161
  // Cap TARGET at its column so a long user@host can't shove CAPS out of alignment.
134
162
  const tgtW = stringWidth(tgt);
135
163
  const tgtCol = tgtW > 28 ? truncateToWidth(tgt, 28) : tgt + ' '.repeat(28 - tgtW);
136
- const mark = h.enrolled ? '' : chalk.gray(' ·available');
137
- console.log(h.name.padEnd(20) + h.source.padEnd(13) + tgtCol + (h.caps?.join(',') ?? '') + mark);
164
+ // Devices surface their registry as SOURCE (they're synced, not enrolled here).
165
+ const source = h.provider === 'devices' ? 'devices' : h.source;
166
+ const mark = h.dispatchable === false
167
+ ? chalk.yellow(' ·password-auth (no dispatch)')
168
+ : h.enrolled ? '' : chalk.gray(' ·available');
169
+ console.log(h.name.padEnd(20) + source.padEnd(13) + tgtCol + (h.caps?.join(',') ?? '') + mark);
138
170
  }
139
171
  }
140
172
  async function doCheck(name) {
@@ -163,6 +195,12 @@ async function doRemove(name) {
163
195
  console.log(chalk.yellow(`"${name}" is not enrolled (nothing to remove).`));
164
196
  return;
165
197
  }
198
+ if (host.provider === 'devices') {
199
+ // The devices registry owns this entry — hosts remove would silently no-op.
200
+ console.log(chalk.yellow(`"${name}" comes from the devices registry. Remove it there: agents devices rm ${name}`));
201
+ process.exitCode = 1;
202
+ return;
203
+ }
166
204
  await getProvider('local').remove(name);
167
205
  console.log(chalk.green(`Removed ${name}`));
168
206
  }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * `agents lease` — manage the disposable cloud boxes used by `agents run --lease`.
3
+ *
4
+ * Today: `agents lease gc`, which stops expired + idle "orphan" boxes that are
5
+ * holding a provider's server quota (the cause of the `server_limit` 403 a new
6
+ * lease hits). Reaping is conservative: only boxes whose lease has expired AND
7
+ * that have been untouched for a safety window are eligible (see `isReapSafe`),
8
+ * so a box a concurrent run just reused is never stopped.
9
+ */
10
+ import type { Command } from 'commander';
11
+ /** Validate a Hetzner token against the live API. Exported for unit tests (fetch injectable). */
12
+ export declare function validateHetznerToken(token: string, fetchImpl?: typeof fetch): Promise<'valid' | 'invalid' | 'unreachable'>;
13
+ /**
14
+ * One-time credential setup for `agents run --lease` (Hetzner today). Opens the
15
+ * token page, collects a token, validates it against the live API, stores it in
16
+ * the keychain bundle `hetzner.com`, and persists it as the default lease bundle
17
+ * (so `--lease` needs no env var or flag afterward). Returns true on success.
18
+ * Never throws for expected outcomes (non-interactive, cancel, repeated failure).
19
+ */
20
+ export declare function runLeaseSetup(opts?: {
21
+ provider?: string;
22
+ }): Promise<boolean>;
23
+ export declare function registerLeaseCommand(program: Command): void;
@@ -0,0 +1,201 @@
1
+ /**
2
+ * `agents lease` — manage the disposable cloud boxes used by `agents run --lease`.
3
+ *
4
+ * Today: `agents lease gc`, which stops expired + idle "orphan" boxes that are
5
+ * holding a provider's server quota (the cause of the `server_limit` 403 a new
6
+ * lease hits). Reaping is conservative: only boxes whose lease has expired AND
7
+ * that have been untouched for a safety window are eligible (see `isReapSafe`),
8
+ * so a box a concurrent run just reused is never stopped.
9
+ */
10
+ import { spawn } from 'child_process';
11
+ import chalk from 'chalk';
12
+ import { crabboxList, reapSafeOrphans, reapOrphans, setLeaseSecretsBundle } from '../lib/crabbox/cli.js';
13
+ import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
14
+ import { bundleExists, readBundle, writeBundle, keychainRef, bundleItemStore } from '../lib/secrets/bundles.js';
15
+ import { secretsKeychainItem } from '../lib/secrets/index.js';
16
+ function fmtIdle(box) {
17
+ if (box.lastTouchedAt === null)
18
+ return 'idle ?';
19
+ const iso = new Date(box.lastTouchedAt * 1000).toISOString().slice(0, 16).replace('T', ' ');
20
+ return `idle since ${iso}Z`;
21
+ }
22
+ const HETZNER_BUNDLE = 'hetzner.com';
23
+ const HCLOUD_KEY = 'HCLOUD_TOKEN';
24
+ 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];
29
+ try {
30
+ const p = spawn(cmd, args, { stdio: 'ignore', detached: true });
31
+ p.on('error', () => { });
32
+ p.unref();
33
+ }
34
+ catch {
35
+ /* best-effort */
36
+ }
37
+ }
38
+ /** Validate a Hetzner token against the live API. Exported for unit tests (fetch injectable). */
39
+ export async function validateHetznerToken(token, fetchImpl = fetch) {
40
+ try {
41
+ const res = await fetchImpl('https://api.hetzner.cloud/v1/servers?per_page=1', {
42
+ headers: { Authorization: `Bearer ${token}` },
43
+ });
44
+ if (res.ok)
45
+ return 'valid';
46
+ if (res.status === 401 || res.status === 403)
47
+ return 'invalid';
48
+ return 'unreachable';
49
+ }
50
+ catch {
51
+ return 'unreachable';
52
+ }
53
+ }
54
+ /**
55
+ * One-time credential setup for `agents run --lease` (Hetzner today). Opens the
56
+ * token page, collects a token, validates it against the live API, stores it in
57
+ * the keychain bundle `hetzner.com`, and persists it as the default lease bundle
58
+ * (so `--lease` needs no env var or flag afterward). Returns true on success.
59
+ * Never throws for expected outcomes (non-interactive, cancel, repeated failure).
60
+ */
61
+ export async function runLeaseSetup(opts = {}) {
62
+ const provider = opts.provider ?? 'hetzner';
63
+ if (provider !== 'hetzner') {
64
+ console.error(chalk.yellow(`lease setup: only 'hetzner' is supported today (got '${provider}').`));
65
+ return false;
66
+ }
67
+ if (!isInteractiveTerminal()) {
68
+ console.error(chalk.red('lease setup needs an interactive terminal. For CI/headless, set AGENTS_LEASE_SECRETS_BUNDLE ' +
69
+ 'or store HCLOUD_TOKEN in a keychain bundle.'));
70
+ return false;
71
+ }
72
+ console.error(chalk.bold('\nSet up leasing (Hetzner) — one time (~30s):'));
73
+ console.error(chalk.dim('Opening the Hetzner console. Create/select a project, then Security → API Tokens →'));
74
+ console.error(chalk.dim('Generate a token with Read & Write permission, and copy it.\n'));
75
+ openUrl(HETZNER_CONSOLE_URL);
76
+ const { password } = await import('@inquirer/prompts');
77
+ const ora = (await import('ora')).default;
78
+ try {
79
+ for (let attempt = 0; attempt < 3; attempt++) {
80
+ const token = (await password({ message: 'Paste your Hetzner API token:', mask: true })).trim();
81
+ if (!token) {
82
+ console.error(chalk.yellow('No token entered.'));
83
+ continue;
84
+ }
85
+ const spinner = ora('Validating token against the Hetzner API…').start();
86
+ const result = await validateHetznerToken(token);
87
+ if (result === 'invalid') {
88
+ spinner.fail('Token rejected by Hetzner (401/403). Try again.');
89
+ continue;
90
+ }
91
+ if (result === 'unreachable')
92
+ spinner.warn('Could not reach the Hetzner API to validate — storing anyway.');
93
+ else
94
+ spinner.succeed('Token valid — Hetzner API reachable.');
95
+ // Store into the `hetzner.com` keychain bundle (mirrors writeSyncBundle).
96
+ const bundle = bundleExists(HETZNER_BUNDLE)
97
+ ? readBundle(HETZNER_BUNDLE)
98
+ : { name: HETZNER_BUNDLE, description: 'Hetzner Cloud API token for crabbox leases', vars: {} };
99
+ const store = bundleItemStore(bundle.backend);
100
+ store.set(secretsKeychainItem(HETZNER_BUNDLE, HCLOUD_KEY), token);
101
+ bundle.vars[HCLOUD_KEY] = keychainRef(HCLOUD_KEY);
102
+ writeBundle(bundle);
103
+ setLeaseSecretsBundle(HETZNER_BUNDLE);
104
+ console.error(chalk.green(`\n✔ Stored in keychain bundle '${HETZNER_BUNDLE}' and set as the default lease provider.`));
105
+ console.error(chalk.dim(' Run `agents run <agent> "…" --lease` — no env var, no flag needed.'));
106
+ return true;
107
+ }
108
+ console.error(chalk.yellow('lease setup: no valid token after 3 attempts — aborted.'));
109
+ return false;
110
+ }
111
+ catch (e) {
112
+ if (isPromptCancelled(e)) {
113
+ console.error(chalk.yellow('lease setup cancelled.'));
114
+ return false;
115
+ }
116
+ throw e;
117
+ }
118
+ }
119
+ export function registerLeaseCommand(program) {
120
+ const lease = program
121
+ .command('lease')
122
+ .description('Manage the disposable cloud boxes used by `agents run --lease`.');
123
+ lease
124
+ .command('setup')
125
+ .description('One-time credential setup so `agents run --lease` works with no env var or flag.')
126
+ .option('--provider <name>', 'Cloud provider (only hetzner today)', 'hetzner')
127
+ .action(async (opts) => {
128
+ const ok = await runLeaseSetup({ provider: opts.provider });
129
+ process.exit(ok ? 0 : 1);
130
+ });
131
+ lease
132
+ .command('gc')
133
+ .description('Stop expired, idle lease boxes that are holding your provider quota. Safe: never stops a box in active use.')
134
+ .option('--dry-run', 'List reap-safe orphan boxes without stopping any', false)
135
+ .option('--yes', 'Stop them without the interactive confirm', false)
136
+ .option('--json', 'Output JSON', false)
137
+ .action(async (opts) => {
138
+ const boxOpts = { secretsBundle: process.env.AGENTS_LEASE_SECRETS_BUNDLE };
139
+ const nowSecs = Math.floor(Date.now() / 1000);
140
+ let candidates;
141
+ try {
142
+ candidates = reapSafeOrphans(crabboxList(boxOpts), nowSecs);
143
+ }
144
+ catch (e) {
145
+ console.error(chalk.red(`lease gc: ${e.message}`));
146
+ process.exit(1);
147
+ return;
148
+ }
149
+ if (candidates.length === 0) {
150
+ if (opts.json)
151
+ console.log(JSON.stringify({ candidates: [], reaped: [] }));
152
+ else
153
+ console.error(chalk.gray('No reap-safe orphan boxes — nothing to collect.'));
154
+ return;
155
+ }
156
+ if (!opts.json) {
157
+ console.error(chalk.bold(`${candidates.length} reap-safe orphan box(es):`));
158
+ for (const b of candidates) {
159
+ console.error(` ${chalk.cyan(b.slug)} ${chalk.dim(`(${b.class ?? '?'}, ${fmtIdle(b)})`)}`);
160
+ }
161
+ }
162
+ if (opts.dryRun) {
163
+ if (opts.json)
164
+ console.log(JSON.stringify({ candidates, reaped: [] }, null, 2));
165
+ else
166
+ console.error(chalk.gray('\n--dry-run: nothing stopped. Re-run with --yes to stop them.'));
167
+ return;
168
+ }
169
+ // Destructive: stopping boxes the agent did not create needs an explicit yes.
170
+ if (!opts.yes) {
171
+ const { isInteractiveTerminal, isPromptCancelled } = await import('./utils.js');
172
+ if (!isInteractiveTerminal()) {
173
+ console.error(chalk.yellow('Refusing to stop boxes without --yes in a non-interactive shell.'));
174
+ process.exit(1);
175
+ return;
176
+ }
177
+ try {
178
+ const { confirm } = await import('@inquirer/prompts');
179
+ const ok = await confirm({ message: `Stop these ${candidates.length} box(es)?`, default: false });
180
+ if (!ok) {
181
+ console.error(chalk.yellow('Aborted — no boxes stopped.'));
182
+ return;
183
+ }
184
+ }
185
+ catch (e) {
186
+ if (isPromptCancelled(e)) {
187
+ console.error(chalk.yellow('Aborted — no boxes stopped.'));
188
+ return;
189
+ }
190
+ throw e;
191
+ }
192
+ }
193
+ // Re-list at stop time (freshness re-check) so a box touched since the
194
+ // preview is not stopped out from under an active run.
195
+ const { candidates: stopped, reaped } = reapOrphans({ ...boxOpts, nowSecs });
196
+ if (opts.json)
197
+ console.log(JSON.stringify({ candidates: stopped, reaped }, null, 2));
198
+ else
199
+ console.error(chalk.green(`Stopped ${reaped.length}/${stopped.length} box(es): ${reaped.join(', ') || '(none)'}`));
200
+ });
201
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * `agents mailboxes` — fleet comms: a read-only window onto the agent mailbox spool.
3
+ *
4
+ * The mailbox spool (`~/.agents/.history/mailbox/<id>/{inbox,processing,consumed}`)
5
+ * is the transport under `agents message` / `agents feed` / `agents teams message`:
6
+ * one box per logical agent (session UUID / teams agentId / loop runId). This
7
+ * command surfaces which boxes exist, how much mail each holds, the messages that
8
+ * flowed BETWEEN agents (including already-consumed ones), and — with `--watch` —
9
+ * the live stream as it happens. Rendering rides the shared comms engine
10
+ * (`lib/comms-render.ts`): masthead, sparkline, aggregate, graphEdges.
11
+ *
12
+ * agents mailboxes overview: masthead + 24h sparkline + boxes + recent log
13
+ * agents mailboxes <id> one box in full (inbox / processing / consumed)
14
+ * agents mailboxes --watch live tail of cross-box traffic until ⌃C
15
+ * agents mailboxes --between <a> <b> one relationship as a thread, either direction
16
+ * agents mailboxes --graph who-talks-to-whom adjacency, busiest first
17
+ * --from/--to/--since filter the overview log and the --watch stream
18
+ */
19
+ import type { Command } from 'commander';
20
+ export declare function registerMailboxesCommand(program: Command): void;