@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.
- package/CHANGELOG.md +17 -0
- package/README.md +9 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/exec.js +55 -28
- package/dist/commands/feed.d.ts +4 -0
- package/dist/commands/feed.js +27 -8
- package/dist/commands/lease.d.ts +23 -0
- package/dist/commands/lease.js +201 -0
- package/dist/commands/mailboxes.d.ts +20 -0
- package/dist/commands/mailboxes.js +390 -0
- package/dist/commands/routines.js +20 -14
- package/dist/commands/sessions-export.d.ts +2 -0
- package/dist/commands/sessions-export.js +279 -0
- package/dist/commands/sessions-import.d.ts +2 -0
- package/dist/commands/sessions-import.js +230 -0
- package/dist/commands/sessions.js +4 -0
- package/dist/commands/ssh.js +98 -3
- package/dist/commands/usage.d.ts +2 -0
- package/dist/commands/usage.js +7 -2
- package/dist/commands/view.d.ts +1 -1
- package/dist/index.js +2 -1
- package/dist/lib/agents.d.ts +18 -0
- package/dist/lib/agents.js +27 -17
- package/dist/lib/browser/drivers/ssh.js +19 -2
- package/dist/lib/comms-render.d.ts +37 -0
- package/dist/lib/comms-render.js +89 -0
- package/dist/lib/crabbox/cli.d.ts +72 -0
- package/dist/lib/crabbox/cli.js +158 -9
- package/dist/lib/crabbox/runtimes.d.ts +13 -0
- package/dist/lib/crabbox/runtimes.js +24 -0
- package/dist/lib/daemon.js +6 -1
- package/dist/lib/devices/health.d.ts +77 -0
- package/dist/lib/devices/health.js +186 -0
- package/dist/lib/mailbox.d.ts +39 -0
- package/dist/lib/mailbox.js +112 -0
- package/dist/lib/paths.d.ts +13 -0
- package/dist/lib/paths.js +26 -4
- package/dist/lib/routines.d.ts +21 -2
- package/dist/lib/routines.js +35 -12
- package/dist/lib/runner.js +255 -13
- package/dist/lib/sandbox.d.ts +9 -1
- package/dist/lib/sandbox.js +11 -2
- package/dist/lib/session/bundle.d.ts +150 -0
- package/dist/lib/session/bundle.js +189 -0
- package/dist/lib/session/remote-bundle.d.ts +12 -0
- package/dist/lib/session/remote-bundle.js +61 -0
- package/dist/lib/session/sync/agents.d.ts +54 -6
- package/dist/lib/session/sync/agents.js +0 -0
- package/dist/lib/session/sync/manifest.d.ts +14 -3
- package/dist/lib/session/sync/manifest.js +4 -0
- package/dist/lib/session/sync/sync.d.ts +23 -2
- package/dist/lib/session/sync/sync.js +177 -74
- package/dist/lib/ssh-tunnel.js +13 -1
- package/dist/lib/staleness/detectors/subagents.d.ts +5 -0
- package/dist/lib/staleness/detectors/subagents.js +5 -192
- package/dist/lib/staleness/writers/subagents.d.ts +10 -0
- package/dist/lib/staleness/writers/subagents.js +11 -102
- package/dist/lib/startup/command-registry.d.ts +2 -0
- package/dist/lib/startup/command-registry.js +5 -0
- package/dist/lib/subagents-registry.d.ts +85 -0
- package/dist/lib/subagents-registry.js +393 -0
- package/dist/lib/subagents.d.ts +8 -8
- package/dist/lib/subagents.js +32 -663
- package/dist/lib/sync-umbrella.d.ts +1 -0
- package/dist/lib/sync-umbrella.js +14 -3
- package/dist/lib/types.d.ts +9 -0
- package/dist/lib/usage.d.ts +42 -3
- package/dist/lib/usage.js +162 -22
- 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
|
package/dist/commands/exec.js
CHANGED
|
@@ -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
|
-
|
|
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 {
|
|
345
|
+
const { getConfiguredRunStrategy, resolveRunVersion } = await import('../lib/rotate.js');
|
|
335
346
|
const detected = await detectSignedInRuntimes();
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
|
|
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
|
-
|
|
342
|
-
//
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
//
|
|
350
|
-
//
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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".
|
|
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 (
|
|
365
|
-
|
|
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) {
|
package/dist/commands/feed.d.ts
CHANGED
|
@@ -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[];
|
package/dist/commands/feed.js
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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(
|
|
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;
|