@phnx-labs/agents-cli 1.20.63 → 1.20.64

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 (69) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +9 -0
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/exec.js +55 -28
  5. package/dist/commands/feed.d.ts +4 -0
  6. package/dist/commands/feed.js +27 -8
  7. package/dist/commands/lease.d.ts +23 -0
  8. package/dist/commands/lease.js +201 -0
  9. package/dist/commands/mailboxes.d.ts +20 -0
  10. package/dist/commands/mailboxes.js +390 -0
  11. package/dist/commands/routines.js +20 -14
  12. package/dist/commands/sessions-export.d.ts +2 -0
  13. package/dist/commands/sessions-export.js +279 -0
  14. package/dist/commands/sessions-import.d.ts +2 -0
  15. package/dist/commands/sessions-import.js +230 -0
  16. package/dist/commands/sessions.js +4 -0
  17. package/dist/commands/ssh.js +98 -3
  18. package/dist/commands/usage.d.ts +2 -0
  19. package/dist/commands/usage.js +7 -2
  20. package/dist/commands/view.d.ts +1 -1
  21. package/dist/index.js +2 -1
  22. package/dist/lib/agents.d.ts +18 -0
  23. package/dist/lib/agents.js +27 -17
  24. package/dist/lib/browser/drivers/ssh.js +19 -2
  25. package/dist/lib/comms-render.d.ts +37 -0
  26. package/dist/lib/comms-render.js +89 -0
  27. package/dist/lib/crabbox/cli.d.ts +72 -0
  28. package/dist/lib/crabbox/cli.js +158 -9
  29. package/dist/lib/crabbox/runtimes.d.ts +13 -0
  30. package/dist/lib/crabbox/runtimes.js +24 -0
  31. package/dist/lib/daemon.js +6 -1
  32. package/dist/lib/devices/health.d.ts +77 -0
  33. package/dist/lib/devices/health.js +186 -0
  34. package/dist/lib/mailbox.d.ts +39 -0
  35. package/dist/lib/mailbox.js +112 -0
  36. package/dist/lib/paths.d.ts +13 -0
  37. package/dist/lib/paths.js +26 -4
  38. package/dist/lib/routines.d.ts +21 -2
  39. package/dist/lib/routines.js +35 -12
  40. package/dist/lib/runner.js +255 -13
  41. package/dist/lib/sandbox.d.ts +9 -1
  42. package/dist/lib/sandbox.js +11 -2
  43. package/dist/lib/session/bundle.d.ts +150 -0
  44. package/dist/lib/session/bundle.js +189 -0
  45. package/dist/lib/session/remote-bundle.d.ts +12 -0
  46. package/dist/lib/session/remote-bundle.js +61 -0
  47. package/dist/lib/session/sync/agents.d.ts +54 -6
  48. package/dist/lib/session/sync/agents.js +0 -0
  49. package/dist/lib/session/sync/manifest.d.ts +14 -3
  50. package/dist/lib/session/sync/manifest.js +4 -0
  51. package/dist/lib/session/sync/sync.d.ts +23 -2
  52. package/dist/lib/session/sync/sync.js +177 -74
  53. package/dist/lib/ssh-tunnel.js +13 -1
  54. package/dist/lib/staleness/detectors/subagents.d.ts +5 -0
  55. package/dist/lib/staleness/detectors/subagents.js +5 -192
  56. package/dist/lib/staleness/writers/subagents.d.ts +10 -0
  57. package/dist/lib/staleness/writers/subagents.js +11 -102
  58. package/dist/lib/startup/command-registry.d.ts +2 -0
  59. package/dist/lib/startup/command-registry.js +5 -0
  60. package/dist/lib/subagents-registry.d.ts +85 -0
  61. package/dist/lib/subagents-registry.js +393 -0
  62. package/dist/lib/subagents.d.ts +8 -8
  63. package/dist/lib/subagents.js +32 -663
  64. package/dist/lib/sync-umbrella.d.ts +1 -0
  65. package/dist/lib/sync-umbrella.js +14 -3
  66. package/dist/lib/types.d.ts +9 -0
  67. package/dist/lib/usage.d.ts +42 -3
  68. package/dist/lib/usage.js +162 -22
  69. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,11 +2,27 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 1.20.64
6
+
7
+ - **`teams` skill documents the fleet-comms surface (RUSH-1739).** The Monitoring section now points teammates-of-teams at `agents feed` (what agents need from you), `agents mailboxes` / `--watch` / `--graph` / `--between` (what agents say to each other), and the `agents message` / `agents teams message` reply path — so an operator running a team can see and answer the whole conversation. Source: `skills/teams/SKILL.md`.
8
+ - **`agents feed` reskin to shared fleet-comms visual language (RUSH-1738).** Presentation only: amber `they need you` masthead, shared `GLYPH` ask/answered markers (▲ / ✓), and `↳ ag message <id> "…"` reply hints — same product face as `agents mailboxes`. Grouping, fan-out, policy, and JSON contracts are unchanged. Source: `apps/cli/src/commands/feed.ts`.
9
+ - **`agents mailboxes` grows into the fleet-comms surface (RUSH-1737).** The overview now opens with the shared `fleet comms` masthead (`N live · M boxes`, total messages, count still awaiting delivery, last activity) plus a 24-hour hourly-volume sparkline. New views and filters, all mirrored in `--json`: `--watch`/`-f` streams cross-box messages live (`HH:MM:SS from ─→ toLabel text`, NDJSON with `--json`, clean Ctrl-C via an AbortController, `--since` backfills the window first, and mail addressed to the watching agent's own `AGENTS_MAILBOX_DIR` box renders as `▲ you` so an orchestrator sees its replies); `--between <a> <b>` reads one relationship as a chronological thread in both directions under an `a ⇄ b · N messages · span` header; `--graph` renders who-talks-to-whom `from └─▶ to ···· count` adjacency, busiest first; `--from`/`--to`/`--since` filter the overview recency log, the watch stream, and the graph. The `<id>` detail view, `--limit`, and the `mailbox` alias are unchanged. Built on the RUSH-1736 comms engine (`lib/comms-render.ts`, `watchMessages`). Source: `apps/cli/src/commands/mailboxes.ts`, `apps/cli/src/commands/mailboxes.test.ts`, `apps/cli/docs/06-observability.md`.
10
+ - **Routines can run a plain shell `command:` — no LLM agent required.** A routine (`JobConfig`) now accepts `command: <shell>` as a third execution mode alongside `agent:` and `workflow:` (exactly one is required). A command routine runs the shell string directly via `/bin/sh -c` (`cmd /c` on Windows) in the **real** environment (no sandbox overlay — it can `npm i -g` / `git pull`), honoring `timeout` with the same SIGTERM→SIGKILL kill the agent path uses, and writes the identical run record (`meta.json` + `stdout.log`, status from exit code) so `agents routines list/runs`, overdue tracking, and device scoping are unchanged. `agents routines add` gains `--command`. This exists because deterministic housekeeping routines (e.g. a built-in update checker) shouldn't depend on a logged-in agent, burn tokens, or gamble on account rotation — a real failure mode where the rotation dispatched an update-check to a logged-out agent version and the run died on "Not logged in." Source: `apps/cli/src/lib/routines.ts` (`JobConfig.command`, `validateJob`), `apps/cli/src/lib/runner.ts` (`executeCommandJob{Foreground,Detached}`), `apps/cli/src/lib/daemon.ts`, `apps/cli/src/commands/routines.ts`.
11
+ - **Shared fleet-comms rendering and mailbox streaming (RUSH-1736).** Adds the common masthead, glyph, sparkline, aggregation, hourly-volume, and route-graph helpers used by `agents mailboxes` and `agents feed`, plus an abortable spool watcher that emits each new box/message pair once without replaying history unless backfill is requested. Source: `apps/cli/src/lib/comms-render.ts`, `apps/cli/src/lib/mailbox.ts`.
12
+ - **Fix: Claude usage bars now render on Linux — so `agents view claude --host <linux-box>` shows them too.** `agents … --host X` runs the whole command on the remote box over SSH, so `agents view` executes on Linux there. Claude usage needs a live OAuth-token fetch, but `loadClaudeOauth` read the token *only* from the OS keychain — which on macOS falls through to `/usr/bin/security` and reads Claude Code's real login-keychain entry, while on Linux it routed to agents-cli's own secret store and never found the token (Claude Code on a headless Linux box writes its OAuth to the plaintext `<home>/.claude/.credentials.json` instead). The token load now falls back to that file when the keychain has no item — the same keychain-then-`.credentials.json` order `readClaudeCredentialsBlob` (`cloud/rush.ts`) already uses — so the live usage fetch succeeds and the bars render. Account + plan were unaffected because those come from the plaintext `.claude.json`. Codex (session logs), Kimi (`kimi-code.json`), and Droid (`auth.v2.file`) were already file-based and unaffected. Source: `apps/cli/src/lib/usage.ts` (`loadClaudeOauth`, `parseClaudeOauthPayload`), `apps/cli/src/lib/__tests__/usage.test.ts`.
13
+ - **`agents mailboxes` — a read-only window onto the agent mailbox spool.** The mailbox spool (`~/.agents/.history/mailbox/<id>/{inbox,processing,consumed}`) is the transport under `agents message` / `agents feed` / `agents teams message`, but until now it had no inspection surface — `agents mailboxes` failed with `unknown command`. The new command lists every box with pending/total counts, last activity, and a live-session label when the owning agent is running, then renders a recency-ordered log of the messages that flowed **between** agents — including already-`consumed` (delivered) mail — so an operator can see agent-to-agent chatter after the fact, not just what a running agent is currently blocked on. `agents mailboxes <id>` shows one box in full across all three buckets; `--json` for machine output, `-n/--limit` bounds the overview log; `mailbox` is an alias. Adds `listBoxes()` (enumerate boxes, consistent with the GC's validity contract) and `readBox()` (read all buckets, tagged by state, non-destructive — unlike `peek`, includes `consumed/`) to the mailbox lib. Source: `apps/cli/src/commands/mailboxes.ts`, `apps/cli/src/lib/mailbox.ts`, `apps/cli/src/lib/mailbox.test.ts`, `apps/cli/src/lib/startup/command-registry.ts`, `apps/cli/src/index.ts`.
14
+ - **`agents usage` now reports live usage for Droid and Kimi, matching `agents view`.** Both agents already render live usage bars in `agents view` — Droid via `GET https://api.factory.ai/api/billing/limits` (decrypted from `~/.factory/auth.v2.file`; 5-hour/weekly/monthly rolling windows), Kimi via its `/usages` API — but the standalone `agents usage` command still marked them "does not publish usage data" because its supported-agent set had drifted from the live sources. `agents usage droid` / `agents usage kimi` now show the same live windows with reset times, and the observability docs reflect that Droid exposes live usage. Source: `apps/cli/src/commands/usage.ts`, `apps/cli/docs/06-observability.md`.
15
+ - **`agents devices list` now shows live resource headroom — which box has room right now.** The list used to show only name / platform / address / reachability. It now probes every reachable device in parallel (one SSH round-trip each: `uptime` + `vm_stat`/`/proc/meminfo` + `nproc`/`hw.ncpu`), bounded by a per-probe timeout so a slow or wedged node degrades to `—` instead of hanging the table, and the local machine is measured directly (no self-SSH). Each row gains **normalized load** (`load1 / cores`), **memory pressure %**, and an **idle / light / busy / loaded** headroom badge (colored by the worse of load and memory); a trailing **fleet-capacity summary** aggregates total cores and free/total RAM (`164 cores · 421G free / 518G RAM (81% free) across 10 reachable devices`). `--full` adds per-device core count and free/total memory; `--no-stats` restores the instant registry-only view; `--json` stays registry-only and fast (the path the Factory extension polls). This is the utilization signal the teammate scheduler doesn't yet consume. Source: `apps/cli/src/lib/devices/health.ts`, `apps/cli/src/commands/ssh.ts`, `apps/cli/src/lib/devices/health.test.ts`.
16
+ - **Portable session export / import over the SSH fleet (RUSH-1710, RUSH-1711, RUSH-1712).** `agents sessions export` bundles selected sessions into a portable, self-describing archive and `agents sessions import` restores one — the user-driven successor to background R2/CRDT sync for the durable-archive / hand-off case (no daemon, no bucket). A bundle is NDJSON (a header line + one line per transcript file) so it pipes over SSH with no external archiver, stays greppable, and carries a per-file AES-256-GCM envelope under `--encrypt`; secrets are redacted by default (`--no-redact` to keep them). Selection reuses the `agents sessions` flags (`--since`, `-n/--limit`, `--all`, `-a/--agent`); dir-shaped sessions (Kimi) carry all their files. Import places each session at the cross-machine mirror keyed by its origin machine, so it shows up in `agents sessions` tagged with that machine and never overwrites your own local sessions; dedup is byte-exact (`--overwrite` to replace conflicts, `--dry-run` to preview). Multi-device transfer rides the existing SSH transport — `agents sessions import --from-host <h>` (and `export --host <h>`) run the export on the peer and stream the bundle back, equivalent to `agents ssh <h> 'agents sessions export --stdout' | agents sessions import -`; no R2, no daemon. Source: `apps/cli/src/lib/session/bundle.ts`, `apps/cli/src/lib/session/remote-bundle.ts`, `apps/cli/src/commands/sessions-export.ts`, `apps/cli/src/commands/sessions-import.ts`.
17
+ - **SSH-first recall is now the documented default; R2/CRDT background sync is demoted to an opt-in backup (RUSH-1714).** `agents sessions --host <box>` reads any online peer's sessions live (no sync, always current) and covers almost all cross-machine recall; export/import handles the offline / hand-off case. Background sync (`agents sessions sync`) stays an opt-in beta, off by default — a passive mirror for when you want offline machines' sessions to appear automatically, not the primary mechanism. Documented in `apps/cli/docs/05-sessions.md`.
18
+ - **Session sync now round-trips directory-shaped sessions (Kimi, Grok) instead of silently dropping the conversation (RUSH-1466).** A session used to be assumed to be one transcript file, so for agents that store a session as a *directory* — Kimi's `session_<id>/state.json` + `agents/<name>/wire.jsonl` + per-tool `tasks/*.json`, Grok's `<uuid>/events.jsonl` — only a single file survived and the actual conversation was never synced. `SyncAgentSpec` gains `dirShaped`/`exts`/`fileFilter`/`mergeableExts`; `listLocalTranscripts` now returns every file of a session (`LocalTranscript.files[]`), each stored under its own R2 sub-key and mirrored at its own relative path. Per-file reconciliation splits by kind: append-only logs (`wire.jsonl`) take the CRDT G-Set union; mutable blobs (`state.json`, task sidecars) take last-writer-wins by `(lastTs, hash)`, where a blob's `lastTs` is derived from its file mtime (blobs carry no event timestamp, so without this LWW degraded to an arbitrary highest-hash-wins that could keep a stale copy). The manifest entry shape is backward-compatible (`ManifestEntry | ManifestEntry[]`) so older CLIs read file-shaped entries byte-identically. Source: `apps/cli/src/lib/session/sync/agents.ts`, `apps/cli/src/lib/session/sync/sync.ts` (`deriveLastTs`, `resolveMirrorWrite`), `apps/cli/src/lib/session/sync/manifest.ts`, and their `*.test.ts`.
19
+
5
20
  ## 1.20.63
6
21
 
7
22
  - **Built-in routines: the daemon now fires routines shipped in the system repo.** Routine discovery (`listJobs`/`readJob`) unions a new system layer — `~/.agents/.system/routines/*.yml` (shipped via `gh:phnx-labs/.agents-system`, which every install pulls at `agents setup`) — under the existing project and user layers. Ordering is project > user > system with first-seen-wins, so a routine shipped as a built-in fires for every install, while a user routine of the same name overrides it and a user copy with `enabled: false` disables it. The daemon (which loads with no `cwd`) sees user + system routines; `writeJob` still only ever writes to the user layer, so built-ins are never mutated in place. This is what lets a routine like `check-updates` ship to all users centrally. Source: `apps/cli/src/lib/state.ts` (`getSystemRoutinesDir`), `apps/cli/src/lib/routines.ts`, `apps/cli/src/lib/routines.test.ts`.
8
23
 
9
24
  - **`--host` / `--device` work across virtually all first-class subcommand groups (RUSH-1691).** Previously only a handful of groups accepted the flags; `agents repos list --host yosemite-s0` (and the same with `--device`) died with commander's raw `unknown option`. Remote routing now covers `repos`/`repo`, status/inspect groups, config/resource groups (`plugins`, `skills`, `hooks`, `sync`, …), `teams`, `routines`, and more via the central `maybeRunOnHost` allowlist. Commands with their own richer host handling (`run`, `sessions`, `feed`, `computer`, `secrets`, `logs`) still fall through to their actions. Groups with no remote semantics reject the flag with a clear message instead of `unknown option`. Self-host targets strip the routing flags before the local command parses so fall-through never trips an unregistered option. Source: `apps/cli/src/lib/hosts/passthrough.ts`, `apps/cli/src/commands/repo.ts`, `apps/cli/docs/{00-concepts,hosts}.md`.
25
+ - **Subagent integrations are now a declarative capability registry — one table entry per agent instead of copy-pasted `else if (agent === …)` arms across six files (RUSH-1698).** Wiring subagents for an agent used to mean editing the same near-identical per-agent branches in `subagents.ts` (install / list / remove-from-agent / orphan-diff / soft-delete), the staleness writer, and the staleness detector — roughly O(agents × operations), and the top source of merge conflicts when new-agent PRs landed in parallel. All of it now iterates a single `SUBAGENT_TARGETS` table (`apps/cli/src/lib/subagents-registry.ts`) keyed by agent, each entry declaring the target dir, on-disk layout (`flat-file` / `dir-file` / `dir-copy`), transform, and ownership marker; the install/list/detect/orphan/remove engine is generic with zero per-agent branches. Genuinely-bespoke agents keep a handler in the same table (Kimi: two files per subagent + a managed parent index). Adding a standard integration is now one registry entry plus the `subagents` capability gate — the writer, detector, and `subagents.ts` need no new arm, and a test pins `Object.keys(SUBAGENT_TARGETS)` to `capableAgents('subagents')` so the flag and the shape can never drift. This also closed real latent gaps the old hand-written chains had left inconsistent: `droid` (synced to `.factory/droids/` but absent from every `subagents.ts` function, so its subagents could not be listed, pruned, or removed), and `copilot`/`codex` (present in some operations, missing from others) are now uniformly install/list/remove-capable. Behavior for every already-supported agent is unchanged (verified: full subagent + versions suites green, byte-identical trash/list semantics per layout). A documented **integration tier list** (`apps/cli/docs/subagents.md`) scopes future "wire X" tickets by importance instead of treating every agent equally. Source: `apps/cli/src/lib/subagents-registry.ts`, `apps/cli/src/lib/subagents.ts`, `apps/cli/src/lib/staleness/{writers,detectors}/subagents.ts`, `apps/cli/src/lib/subagents-registry.test.ts`, `apps/cli/docs/subagents.md`.
10
26
  - **Signed webhook ingress for routines via Tailscale Funnel (RUSH-1456, RUSH-1459, RUSH-1460, RUSH-1461).**
11
27
  Routine triggers now understand both GitHub and Linear event sources, including
12
28
  Linear action/team/label filters. `agents webhook serve --secrets-bundle <name>`
@@ -18,6 +34,7 @@
18
34
  `apps/cli/src/lib/routines.ts`, `apps/cli/src/lib/triggers/webhook.ts`,
19
35
  `apps/cli/src/commands/routines.ts`, `apps/cli/src/commands/webhook.ts`,
20
36
  `apps/cli/src/commands/funnel.ts`, `apps/cli/src/lib/funnel.ts`.
37
+ - **Cross-machine session sync verified end-to-end + documented (RUSH-1464).** The R2/CRDT session-sync beta (Claude + Codex) is now signed off across the full matrix: a machine push with a read-write R2 token uploads with zero errors (the read-only-token 403 from #412 is resolved), a second machine pulls and folds those sessions into its own list, CRDT G-Set union converges byte-identically regardless of sync order, and a machine that fell behind catches up automatically when it returns (a grown session's manifest hash no longer matches the puller's recorded signature, forcing a re-fetch + re-merge). Ships the previously-missing docs: a "Cross-machine sync (R2 + CRDT)" section in `apps/cli/docs/05-sessions.md` covering the single-writer prefix layout, manifest + mirror model, CRDT convergence, client-side AES-256-GCM encryption, the opt-in beta gate, and the `r2.backups` credential bundle (including the read+write scope requirement). Verification only — no runtime change. Source: `apps/cli/docs/05-sessions.md`.
21
38
  - **Guided session-sync provisioning in `agents setup` and `agents sessions sync --setup` (RUSH-1468).** Joining a machine to the cross-machine session-sync fabric no longer requires hand-running four `agents secrets add r2.backups …` commands. A new interactive step mints the `r2.backups` bundle (R2 account/bucket/access-key/secret + a generated `R2_SYNC_ENC_KEY`), probes read+write connectivity with a throwaway object, and opts the machine into the `session-sync` beta on success. The first machine mints and prints the shared encryption key; every other machine pastes it so the whole fabric shares one key (an existing key is reused, never overwritten — overwriting would orphan peers' encrypted transcripts). `agents setup` offers it opt-in (default No, never blocks setup); `agents sessions sync --setup` runs it explicitly and can re-show the shared key. Source: `apps/cli/src/lib/session/sync/provision.ts`, `apps/cli/src/lib/session/sync/provision.test.ts`, `apps/cli/src/commands/sync-provision.ts`, `apps/cli/src/commands/setup.ts`, `apps/cli/src/commands/sessions-sync.ts`.
22
39
  - **`agents fleet` alias + fleet-wide rollout (RUSH-1632).** `fleet` is an alias
23
40
  for `devices`. New subcommands `update [version]` and `run <cmd…>` roll out
package/README.md CHANGED
@@ -361,10 +361,19 @@ agents doctor --device mac-mini # same matrix, scoped to one device
361
361
 
362
362
  # Your Tailscale fleet, auto-discovered
363
363
  agents devices sync # ingest `tailscale status`
364
+ agents devices list # fleet + live headroom: load, mem, idle/busy — which box has room
365
+ agents devices list --full # add per-device cores and free/total RAM
366
+ agents devices list --no-stats # instant: names/addresses only, skip the probe
364
367
  agents ssh mac-mini # hardened SSH: fails fast if offline,
365
368
  # PowerShell on Windows, password-from-Keychain
366
369
  ```
367
370
 
371
+ `agents devices list` probes every reachable box in parallel (bounded timeout, so a
372
+ slow node degrades to `—` instead of hanging) and shows normalized load, memory
373
+ pressure, and an idle/light/busy/loaded headroom badge, plus a fleet-capacity summary
374
+ (`164 cores · 421G free / 518G RAM`). It answers "which machine has room right now?" —
375
+ the utilization signal the teammate scheduler doesn't yet see.
376
+
368
377
  **Hosts** (`agents hosts`) are git-synced dispatch targets in `agents.yaml`; **devices** (`agents devices`) are your Tailscale machines in a local registry. Both ride SSH. See [docs/00-concepts.md](apps/cli/docs/00-concepts.md#devices--hosts).
369
378
 
370
379
  Every `--host` command rides one multiplexed SSH engine, tuned for driving a fleet from a small laptop: the first call to a machine opens a control socket and every later call reuses it (no repeat TCP+auth handshake), connections carry keepalive so a dropped link dies in ~45 s instead of zombying, and following a remote run polls in a single round-trip per cycle. Measured against a Tailscale-relayed host: repeated calls **~6–7× faster**, dispatch readiness **~2×**, and the follow loop **~21× faster with 50% fewer local ssh spawns**. Design: [docs/09-ssh-transport.md](apps/cli/docs/09-ssh-transport.md) · reproduce: `node scripts/bench-ssh.mjs <host>`.
package/dist/bin/agents CHANGED
Binary file
@@ -329,41 +329,69 @@ export function registerRunCommand(program) {
329
329
  process.exit(1);
330
330
  }
331
331
  const backend = typeof options.lease === 'string' ? options.lease : undefined;
332
- const { detectSignedInRuntimes, pickRuntimes, resolveClaudeCredentialsBlob } = await import('../lib/crabbox/runtimes.js');
332
+ // First-run: no provider credential resolves (no env var, no config, no
333
+ // detectable bundle) → guide the user through one-time setup, then continue.
334
+ const { resolveLeaseBundle } = await import('../lib/crabbox/cli.js');
335
+ if (!resolveLeaseBundle()) {
336
+ const { runLeaseSetup } = await import('./lease.js');
337
+ const ok = await runLeaseSetup({ provider: backend ?? 'hetzner' });
338
+ if (!ok) {
339
+ console.error(chalk.yellow('Leasing needs a cloud provider set up. Run `agents lease setup` and retry.'));
340
+ process.exit(1);
341
+ }
342
+ }
343
+ const { detectSignedInRuntimes, resolveClaudeCredentialsBlob, inferLeaseRuntime } = await import('../lib/crabbox/runtimes.js');
333
344
  const { leaseAndRun } = await import('../lib/crabbox/lease.js');
334
- const { confirm } = await import('@inquirer/prompts');
345
+ const { getConfiguredRunStrategy, resolveRunVersion } = await import('../lib/rotate.js');
335
346
  const detected = await detectSignedInRuntimes();
336
- const runtimes = await pickRuntimes(detected);
337
- if (runtimes.length === 0) {
338
- console.error(chalk.yellow('No runtimes selected. Sign into one locally (e.g. run `claude` once) then retry.'));
347
+ const agentName = agentSpec.split('@')[0];
348
+ const leaseCwd = options.cwd ?? process.cwd();
349
+ // `--lease` requires a prompt (guarded above), so it is headless by
350
+ // contract — never block on an interactive picker. Provision exactly the
351
+ // one runtime this run needs, inferred from the agent, not every
352
+ // signed-in CLI (which would ship unrelated tokens to a throwaway box).
353
+ const runtime = inferLeaseRuntime(agentName, detected);
354
+ if (!runtime) {
355
+ console.error(chalk.yellow('No signed-in runtime to provision on the box. Sign into one locally (e.g. run `claude` once) then retry.'));
339
356
  process.exit(1);
340
357
  }
341
- // Security gate: copying auth tokens to an ephemeral cloud box is opt-in
342
- // per run. Name the runtimes + accounts so the user sees what ships.
343
- const names = runtimes
344
- .map((id) => {
345
- const d = detected.find((x) => x.id === id);
346
- return `${d?.label ?? id}${d?.email ? ` (${d.email})` : ''}`;
347
- })
348
- .join(', ');
349
- // Claude ships its OAuth token (not just the .claude.json config) name it
350
- // explicitly so the consent covers the actual credential transferred.
351
- const whatShips = runtimes.includes('claude') ? 'credentials + Claude OAuth token' : 'credentials';
352
- const ok = await confirm({
353
- message: `Copy ${whatShips} for ${names} to a disposable cloud box, run there, then destroy it?`,
354
- default: false,
355
- });
356
- if (!ok) {
357
- console.error(chalk.yellow('Aborted no credentials pushed, no box leased.'));
358
- process.exit(1);
358
+ const runtimes = [runtime];
359
+ // Copy the account the run's OWN strategy would pick (default `balanced`:
360
+ // a healthy, non-rate-limited account weighted by remaining headroom) —
361
+ // never the raw default signed-in one, which could be throttled or out of
362
+ // credits and would boot the box straight into a wall.
363
+ //
364
+ // Only claude's token is account-switched: resolveClaudeCredentialsBlob
365
+ // honors preferEmail across version-homes. The other runtimes'
366
+ // buildCredentialScript copies the single currently-active credential file
367
+ // with no switching, so run the balanced picker only for claude — otherwise
368
+ // the notice below would name a balanced-picked account that is NOT the one
369
+ // actually shipped. Best-effort: fall back to the default account on error.
370
+ let leaseEmail = detected.find((d) => d.id === runtime)?.email ?? null;
371
+ if (runtime === 'claude') {
372
+ try {
373
+ const strategy = getConfiguredRunStrategy(runtime, leaseCwd);
374
+ const { rotation } = await resolveRunVersion(runtime, strategy, leaseCwd);
375
+ if (rotation?.picked.email)
376
+ leaseEmail = rotation.picked.email;
377
+ }
378
+ catch {
379
+ /* strategy resolution is best-effort; keep the default signed-in account */
380
+ }
359
381
  }
382
+ // Headless-by-contract: don't prompt, but print exactly what ships and
383
+ // where — copying an auth token to a cloud box is a credential transfer.
384
+ // The box is destroyed after the run, so the credential's lifetime is
385
+ // bounded by the run.
386
+ const whatShips = runtime === 'claude' ? 'credentials + Claude OAuth token' : 'credentials';
387
+ console.error(chalk.gray(`Leasing a ${backend ?? 'hetzner'} box · shipping ${runtime}${leaseEmail ? ` (${leaseEmail})` : ''} ${whatShips}; the box is destroyed after the run.`));
360
388
  // Read the Claude OAuth token from the local Keychain (silent) so it can be
361
389
  // written to ~/.claude/.credentials.json on the box — otherwise Claude boots
362
- // "Not logged in". Consent above already covered this transfer.
390
+ // "Not logged in". `preferEmail` targets the strategy-picked account so the
391
+ // token matches the account this run resolved to.
363
392
  let claudeCredentialsJson = null;
364
- if (runtimes.includes('claude')) {
365
- const claudeEmail = detected.find((d) => d.id === 'claude')?.email ?? null;
366
- claudeCredentialsJson = await resolveClaudeCredentialsBlob({ preferEmail: claudeEmail });
393
+ if (runtime === 'claude') {
394
+ claudeCredentialsJson = await resolveClaudeCredentialsBlob({ preferEmail: leaseEmail });
367
395
  if (!claudeCredentialsJson) {
368
396
  console.error(chalk.yellow('Warning: could not read the local Claude OAuth token — the box may come up "Not logged in".'));
369
397
  }
@@ -374,7 +402,6 @@ export function registerRunCommand(program) {
374
402
  // box-side marker. Rule: only ONE spinner phase is active at a time, and no
375
403
  // other output is written to stderr while it spins — so it can never storm.
376
404
  const { createLeaseOutputRouter, createSpinner } = await import('../lib/crabbox/progress.js');
377
- const agentName = agentSpec.split('@')[0];
378
405
  const spinner = createSpinner({ stream: process.stderr });
379
406
  let warmupTimer;
380
407
  const stopTimer = () => { if (warmupTimer) {
@@ -13,6 +13,10 @@ import type { Command } from 'commander';
13
13
  import { type OpenBlock } from '../lib/feed.js';
14
14
  import { type OutcomeGroup, type SessionOutcomeHint } from '../lib/feed-outcome.js';
15
15
  export declare const FEED_NO_FANOUT_ENV = "AGENTS_FEED_LOCAL";
16
+ /** Right-hand masthead summary: `N blocks · M agents`. */
17
+ export declare function formatFeedMastheadRight(blocks: OpenBlock[]): string;
18
+ /** Reply hint matching the shared fleet-comms reply line. */
19
+ export declare function formatFeedReplyHint(mailboxId: string): string;
16
20
  export declare function parseRemoteFeed(stdout: string, machine: string): OpenBlock[];
17
21
  /** Merge local and remote rows, keeping the first copy of a host/session block. */
18
22
  export declare function mergeFeedBlocks(...groups: OpenBlock[][]): OpenBlock[];
@@ -10,7 +10,17 @@ import { notifyUrgentBlock } from '../lib/notify.js';
10
10
  import { gcMailbox } from '../lib/mailbox-gc.js';
11
11
  import { getActiveSessions } from '../lib/session/active.js';
12
12
  import { mailboxIdForActiveSession } from '../lib/mailbox-target.js';
13
+ import { GLYPH, masthead } from '../lib/comms-render.js';
13
14
  export const FEED_NO_FANOUT_ENV = 'AGENTS_FEED_LOCAL';
15
+ /** Right-hand masthead summary: `N blocks · M agents`. */
16
+ export function formatFeedMastheadRight(blocks) {
17
+ const agents = new Set(blocks.map((b) => b.mailboxId)).size;
18
+ return `${blocks.length} block${blocks.length === 1 ? '' : 's'} · ${agents} agent${agents === 1 ? '' : 's'}`;
19
+ }
20
+ /** Reply hint matching the shared fleet-comms reply line. */
21
+ export function formatFeedReplyHint(mailboxId) {
22
+ return `↳ ag message ${mailboxId} "…"`;
23
+ }
14
24
  export function parseRemoteFeed(stdout, machine) {
15
25
  let parsed;
16
26
  try {
@@ -60,7 +70,13 @@ function renderBlock(b, localHost, indent = '') {
60
70
  const cls = b.blockClass ? chalk.gray(`(${b.blockClass})`) : '';
61
71
  const consequence = b.consequence && b.consequence !== 'normal' ? chalk.red(`[${b.consequence}]`) : '';
62
72
  const cost = b.costOfDelay ? chalk.gray(`cost:${b.costOfDelay}`) : '';
63
- console.log(`${indent}${chalk.cyan(b.mailboxId)}${host} ${runtime} ${age} ${cls} ${consequence} ${cost}`.trimEnd());
73
+ // Shared fleet-comms glyphs: ▲ open ask, ✓ answered (see comms-render GLYPH).
74
+ const marker = b.answer
75
+ ? chalk.green(GLYPH.delivered)
76
+ : !b.parkedAt
77
+ ? chalk.yellow(GLYPH.ask)
78
+ : ' ';
79
+ console.log(`${indent}${marker} ${chalk.cyan(b.mailboxId)}${host} ${runtime} ${age} ${cls} ${consequence} ${cost}`.trimEnd());
64
80
  for (const question of b.questions) {
65
81
  const header = question.header ? chalk.gray(`[${question.header}] `) : '';
66
82
  console.log(`${indent} ${header}${question.text}`);
@@ -77,7 +93,7 @@ function renderBlock(b, localHost, indent = '') {
77
93
  console.log(`${indent} ${chalk.gray(meta)}`);
78
94
  }
79
95
  if (b.answer) {
80
- const verified = b.answer.verified ? chalk.green('✓') : chalk.yellow('?');
96
+ const verified = b.answer.verified ? chalk.green(GLYPH.delivered) : chalk.yellow('?');
81
97
  const who = b.answer.answeredFrom + (b.answer.answeredBy ? ` (${b.answer.answeredBy})` : '');
82
98
  console.log(`${indent} ${chalk.green('answered')} by ${who} ${verified}`);
83
99
  }
@@ -98,7 +114,7 @@ function renderBlock(b, localHost, indent = '') {
98
114
  console.log(`${indent} ${chalk.dim('notified')} ${relTime(b.notifiedAt)}`);
99
115
  }
100
116
  if (!b.answer && !b.parkedAt) {
101
- console.log(`${indent} ${chalk.dim('reply:')} agents message ${b.mailboxId} "<answer>"`);
117
+ console.log(`${indent} ${chalk.dim(formatFeedReplyHint(b.mailboxId))}`);
102
118
  }
103
119
  console.log();
104
120
  }
@@ -257,17 +273,20 @@ export function registerFeedCommand(program) {
257
273
  console.log(chalk.gray(digest ? 'No open blocks after stall suppression.' : 'No open blocks.'));
258
274
  return;
259
275
  }
276
+ // Shared fleet-comms masthead (same family as `agents mailboxes`).
277
+ console.log(masthead({
278
+ title: 'they need you',
279
+ accent: 'amber',
280
+ host: self,
281
+ right: formatFeedMastheadRight(blocks),
282
+ }));
283
+ console.log();
260
284
  if (opts.flat) {
261
- console.log(chalk.bold(`${blocks.length} open block${blocks.length === 1 ? '' : 's'}:\n`));
262
285
  for (const b of blocks)
263
286
  renderBlock(b, self);
264
287
  return;
265
288
  }
266
289
  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
290
  for (const g of groups)
272
291
  renderOutcomeGroup(g, self);
273
292
  });
@@ -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;