@phnx-labs/agents-cli 1.20.62 → 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 +62 -0
- package/README.md +19 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/browser.js +13 -3
- package/dist/commands/exec.js +96 -28
- package/dist/commands/feed.d.ts +4 -0
- package/dist/commands/feed.js +27 -8
- package/dist/commands/funnel.d.ts +5 -0
- package/dist/commands/funnel.js +62 -0
- package/dist/commands/hosts.js +42 -0
- 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/repo.d.ts +4 -4
- package/dist/commands/repo.js +30 -19
- package/dist/commands/routines.js +92 -29
- 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-sync.d.ts +1 -0
- package/dist/commands/sessions-sync.js +16 -2
- package/dist/commands/sessions.js +12 -1
- package/dist/commands/setup.js +9 -0
- package/dist/commands/ssh.js +170 -5
- package/dist/commands/sync-provision.d.ts +23 -0
- package/dist/commands/sync-provision.js +107 -0
- 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/commands/webhook.d.ts +9 -0
- package/dist/commands/webhook.js +93 -0
- package/dist/index.js +7 -2
- package/dist/lib/agents.d.ts +44 -0
- package/dist/lib/agents.js +85 -35
- package/dist/lib/browser/drivers/ssh.js +19 -2
- package/dist/lib/browser/ipc.js +5 -4
- package/dist/lib/browser/profiles.d.ts +13 -0
- package/dist/lib/browser/profiles.js +17 -0
- package/dist/lib/browser/service.d.ts +12 -1
- package/dist/lib/browser/service.js +48 -13
- package/dist/lib/browser/sessions-list.d.ts +40 -0
- package/dist/lib/browser/sessions-list.js +190 -0
- 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 +8 -1
- package/dist/lib/devices/fleet.d.ts +62 -0
- package/dist/lib/devices/fleet.js +128 -0
- package/dist/lib/devices/health.d.ts +77 -0
- package/dist/lib/devices/health.js +186 -0
- package/dist/lib/funnel.d.ts +5 -0
- package/dist/lib/funnel.js +23 -0
- package/dist/lib/git.d.ts +21 -5
- package/dist/lib/git.js +64 -14
- package/dist/lib/hosts/credentials.d.ts +28 -0
- package/dist/lib/hosts/credentials.js +48 -0
- package/dist/lib/hosts/dispatch.d.ts +25 -0
- package/dist/lib/hosts/dispatch.js +68 -2
- package/dist/lib/hosts/passthrough.d.ts +13 -10
- package/dist/lib/hosts/passthrough.js +119 -29
- package/dist/lib/mailbox-gc.js +4 -16
- package/dist/lib/mailbox.d.ts +39 -0
- package/dist/lib/mailbox.js +112 -0
- package/dist/lib/migrate.d.ts +12 -0
- package/dist/lib/migrate.js +55 -1
- package/dist/lib/paths.d.ts +13 -0
- package/dist/lib/paths.js +26 -4
- package/dist/lib/routines.d.ts +50 -12
- package/dist/lib/routines.js +82 -27
- 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 +56 -6
- package/dist/lib/session/sync/agents.js +0 -0
- package/dist/lib/session/sync/config.d.ts +8 -0
- package/dist/lib/session/sync/config.js +6 -1
- package/dist/lib/session/sync/manifest.d.ts +14 -3
- package/dist/lib/session/sync/manifest.js +4 -0
- package/dist/lib/session/sync/provision.d.ts +49 -0
- package/dist/lib/session/sync/provision.js +91 -0
- package/dist/lib/session/sync/sync.d.ts +26 -2
- package/dist/lib/session/sync/sync.js +192 -69
- package/dist/lib/session/sync/transcript-crypto.d.ts +77 -0
- package/dist/lib/session/sync/transcript-crypto.js +147 -0
- 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 +4 -0
- package/dist/lib/startup/command-registry.js +16 -0
- package/dist/lib/state.d.ts +10 -2
- package/dist/lib/state.js +14 -2
- 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/triggers/webhook.d.ts +70 -27
- package/dist/lib/triggers/webhook.js +264 -43
- 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,8 +2,70 @@
|
|
|
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
|
+
|
|
20
|
+
## 1.20.63
|
|
21
|
+
|
|
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`.
|
|
23
|
+
|
|
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`.
|
|
26
|
+
- **Signed webhook ingress for routines via Tailscale Funnel (RUSH-1456, RUSH-1459, RUSH-1460, RUSH-1461).**
|
|
27
|
+
Routine triggers now understand both GitHub and Linear event sources, including
|
|
28
|
+
Linear action/team/label filters. `agents webhook serve --secrets-bundle <name>`
|
|
29
|
+
exposes signed localhost endpoints at `/hooks/github` and `/hooks/linear` with
|
|
30
|
+
raw-body HMAC verification, Linear timestamp checks, duplicate delivery
|
|
31
|
+
suppression, and rate limiting. `agents funnel status/up` wraps the allowed
|
|
32
|
+
Tailscale Funnel ports through the existing SSH/device path so a webhook
|
|
33
|
+
receiver can be exposed without hand-written SSH commands. Source:
|
|
34
|
+
`apps/cli/src/lib/routines.ts`, `apps/cli/src/lib/triggers/webhook.ts`,
|
|
35
|
+
`apps/cli/src/commands/routines.ts`, `apps/cli/src/commands/webhook.ts`,
|
|
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`.
|
|
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`.
|
|
39
|
+
- **`agents fleet` alias + fleet-wide rollout (RUSH-1632).** `fleet` is an alias
|
|
40
|
+
for `devices`. New subcommands `update [version]` and `run <cmd…>` roll out
|
|
41
|
+
across every online device with a per-device result table. Source:
|
|
42
|
+
`apps/cli/src/commands/ssh.ts`, `apps/cli/src/lib/devices/fleet.ts`.
|
|
43
|
+
- **`agents hosts stop <id>` (alias `kill`) terminates a detached host run from the origin machine (RUSH-1360).**
|
|
44
|
+
Sends SIGTERM to the remote process group, writes exit `143` only when a live
|
|
45
|
+
group was signaled (or no `.exit` existed), and keeps the remote log for
|
|
46
|
+
`agents hosts logs <id>`. Source: `apps/cli/src/lib/hosts/dispatch.ts`,
|
|
47
|
+
`apps/cli/src/commands/hosts.ts`.
|
|
48
|
+
- **Fix: mailbox GC actually archives expired messages on live boxes (RUSH-1611).**
|
|
49
|
+
The live-box branch of `gcMailbox` only incremented `messagesDroppedExpired`
|
|
50
|
+
without moving the file to `consumed/`, so `agents feed --dispatch` could report
|
|
51
|
+
drops while leaving expired messages in `inbox/`/`processing/`. GC now reuses
|
|
52
|
+
`sweepExpired` (the same path as drain/peek). Source: `apps/cli/src/lib/mailbox-gc.ts`.
|
|
53
|
+
- **Fix: Antigravity sign-in detection on Linux when the OAuth grant lives in Secret Service (RUSH-1329).** `agy` uses the Go keyring library, which prefers libsecret (gnome-keyring) over the file fallback whenever a Secret Service daemon is running — so `~/.gemini/antigravity-cli/antigravity-oauth-token` may be absent even when the user is signed in. After the file check, `getAccountInfo` now probes `secret-tool lookup service gemini username antigravity` (exit 0 = present; stdout discarded), mirroring the macOS `security find-generic-password` probe from #506. Missing `secret-tool`, locked collections, and timeouts all read as signed out. Opt out with `AGENTS_NO_KEYCHAIN_PROBE=1`. Source: `apps/cli/src/lib/agents.ts`, `apps/cli/src/lib/agents.test.ts`.
|
|
54
|
+
- **Client-side (zero-knowledge) encryption of session transcripts before R2 upload (RUSH-1463).** Transcripts carry secrets, tokens, and absolute file paths; R2's server-side encryption uses Cloudflare's key, so anyone with bucket-read access (or Cloudflare) could read them as plaintext NDJSON. `agents sessions sync` now seals each transcript BODY client-side with AES-256-GCM before upload and decrypts on pull, so R2 only ever stores ciphertext. The 32-byte key is a new `R2_SYNC_ENC_KEY` in the `r2.backups` bundle — shared across the sync fabric (every machine derives the identical key) and deliberately separate from the R2 access key so rotating the token never orphans encrypted objects. CRDT identity stays over plaintext (the manifest hash is cleartext; pull decrypts before the G-Set union), so cross-machine merge is unaffected. Pull transparently reads legacy plaintext objects (migration-safe); a push with no key configured still uploads but emits a loud per-cycle warning. A new `R2_ENDPOINT` override points sync at any S3-compatible store (MinIO/other providers), which is also how the flow is verified end-to-end without live R2. Source: `apps/cli/src/lib/session/sync/transcript-crypto.ts`, `apps/cli/src/lib/session/sync/transcript-crypto.test.ts`, `apps/cli/src/lib/session/sync/sync.ts`, `apps/cli/src/lib/session/sync/config.ts`, `apps/cli/src/commands/sessions-sync.ts`, `apps/cli/src/lib/daemon.ts`.
|
|
55
|
+
- **Extend session sync to Droid, Grok, Kimi, and OpenCode (RUSH-1467).** `agents sessions sync` now includes these four agents in its upload/download matrix. `SyncAgentSpec` gains an optional `ext` field so agents with non-`.jsonl` transcript files (e.g., Kimi `state.json`) are walked correctly. Droid `.jsonl` rollouts, Grok `events.jsonl` streams, and Kimi `state.json` metadata files round-trip through the R2 mirror; OpenCode is slotted in `SYNC_AGENTS` but remains a placeholder because its sessions live in a SQLite DB and still require an SQLite-to-JSONL export step. Source: `apps/cli/src/lib/session/sync/agents.ts`, `apps/cli/src/lib/session/sync/agents.test.ts`, `apps/cli/src/commands/sessions-sync.ts`.
|
|
56
|
+
- **`agents repos` is canonical (`repo` alias); push/pull echo the resolved target; push no longer no-ops when clean-but-ahead; pull rebases on diverge (RUSH-1454).** Help now prints `Usage: agents repos …`. Push/pull report `user (~/.agents → origin/main): …` instead of the bare alias. `commitAndPush` still `git push`es when the tree is clean but local is ahead of origin (previously returned success without pushing). `pullRepo` uses `git pull --rebase` so divergent branches reconcile instead of failing with a raw git error. Source: `apps/cli/src/commands/repo.ts`, `apps/cli/src/lib/git.ts`, `apps/cli/src/lib/startup/command-registry.ts`.
|
|
57
|
+
- **`agents run --host <name> --copy-creds` provisions runtime credentials on a persistent host (RUSH-1608).**
|
|
58
|
+
Reuses the `--lease` credential path (`resolveClaudeCredentialsBlob` + the
|
|
59
|
+
`~/.claude/.credentials.json` bootstrap) but makes copying tokens to a
|
|
60
|
+
persistent host strictly opt-in per run. The user picks runtimes, sees a
|
|
61
|
+
consent prompt naming accounts and the Claude OAuth token, and the files are
|
|
62
|
+
shredded after the run. Source: `apps/cli/src/commands/exec.ts`,
|
|
63
|
+
`apps/cli/src/lib/hosts/dispatch.ts`, `apps/cli/src/lib/hosts/credentials.ts`,
|
|
64
|
+
`apps/cli/src/lib/hosts/credentials.test.ts`.
|
|
65
|
+
|
|
5
66
|
## 1.20.62
|
|
6
67
|
|
|
68
|
+
- **Browser downloads land in a known per-profile dir; profile data is consolidated.** A browser profile is now one self-contained tree under `~/.agents/.cache/browser/<profile>/`: `chrome-data/`, `downloads/`, and `sessions/<task>/` (screenshots, PDFs, recordings). Previously downloads had no configured destination — the CLI only set the download dir when the agent explicitly ran `browser download --path`, so absent that call a download fell to Chromium's own default (for an attached user browser like comet, wherever *that* browser was last configured), which is how downloads escaped into random locations. The service now sets the profile's `downloads/` dir browser-global at connect time (both fresh launch and every attach path), so downloads always land somewhere agents-cli controls; `browser download --path` becomes an optional override (omit it to use the profile default) and reports the resolved path. Screenshots/PDFs/recordings moved from the old GLOBAL `browser/sessions/<task>/` root to the per-profile `browser/<profile>/sessions/<task>/`, with a one-shot migration that folds existing captures into the owning profile (attributed via each profile's `tasks.json`; unattributable captures go to a `_legacy` bucket). New `agents browser sessions [--profile <name>] [--open latest|<file>] [--json]` lists a profile's captures + downloads, aliased as `agents sessions --browser`. Source: `apps/cli/src/lib/browser/{profiles,service,ipc,sessions-list}.ts`, `apps/cli/src/lib/migrate.ts`, `apps/cli/src/commands/{browser,sessions}.ts`.
|
|
7
69
|
- **Wire Goose commands support (RUSH-1572).** `agents` now syncs slash commands to Goose as recipe YAML files under `~/.config/goose/commands/<name>.yaml`, each registered in `~/.config/goose/config.yaml` under a `slash_commands: [{ command, recipe_path }]` array (Goose has no native slash-command file format — a slash command IS a recipe). The command recipes live in a dir distinct from the workflow recipes dir (`~/.config/goose/recipes/`) so the workflow detector never treats a command recipe as a workflow. Registration is a read-modify-write that preserves every other `config.yaml` key (`mcp_servers`, `extensions`, …) and other `slash_commands` entries, and removal soft-deletes the recipe + unregisters the entry. Flip Goose's `commands` capability and add `goose` branches to install/list/match/remove, the staleness commands writer, and doctor-diff, backed by a new `goose-commands.ts` module + a `markdownToGooseRecipe` converter. Source: `apps/cli/src/lib/agents.ts`, `apps/cli/src/lib/goose-commands.ts`, `apps/cli/src/lib/commands.ts`, `apps/cli/src/lib/convert.ts`, `apps/cli/src/lib/staleness/writers/commands.ts`, `apps/cli/src/lib/doctor-diff.ts`.
|
|
8
70
|
- **Fix: Hermes plugin sync no longer disables a plugin the user explicitly enabled.** The plugin install path (RUSH-1688) unconditionally forced `plugins.enabled` to the exec-surface trust verdict on every sync. An ordinary un-flagged background re-sync computes `enable=false` for a plugin with hooks/tools, so it stripped that plugin from the `~/.hermes/config.yaml` allowlist — clobbering a plugin the user deliberately enabled with `--allow-exec-surfaces`. The install path now enables only when trusted and never down-toggles (matching the marketplace flow's add-if-trusted semantics); removal still unregisters explicitly. Source: `apps/cli/src/lib/plugins.ts`.
|
|
9
71
|
- **Wire Goose subagents support (RUSH-1573).** `agents` now syncs subagents to Goose as recipe YAML files under `~/.config/goose/agents/<name>.yaml` — Goose has no dedicated subagent format, so a named subagent IS a recipe (goose auto-discovers `~/.config/goose/agents/` and delegates to them by name in autonomous mode). `transformSubagentForGoose` emits the same recipe schema agents-cli already uses for Goose workflow recipes (`version`/`title`/`description`/`instructions`/`prompt`, plus optional `settings.goose_model`). Flip Goose's `subagents` capability and wire the install/remove/list/orphan/version-remove branches plus the staleness subagents writer and detector. Source: `apps/cli/src/lib/agents.ts`, `apps/cli/src/lib/subagents.ts`, `apps/cli/src/lib/staleness/{writers,detectors}/subagents.ts`.
|
package/README.md
CHANGED
|
@@ -347,6 +347,9 @@ agents hosts check gpu-box # reachable? which agents-cli version?
|
|
|
347
347
|
# Run there instead of locally
|
|
348
348
|
agents run claude --host gpu-box "profile this build" # headless: follows live by default
|
|
349
349
|
agents run claude --host gpu-box # no prompt → interactive TTY over SSH (tmux-backed)
|
|
350
|
+
agents run claude --host gpu-box --copy-creds "fix auth" # copy local runtime creds + Claude token, shred after
|
|
351
|
+
agents hosts ps # list dispatched runs + terminal status
|
|
352
|
+
agents hosts stop <id> # terminate a hung/detached run (alias: kill)
|
|
350
353
|
agents logs --host gpu-box # pick a dispatched run — concise summary by default
|
|
351
354
|
agents logs <id> --full # the full raw transcript / stdout (token-heavy)
|
|
352
355
|
agents logs <id> -f # re-attach to a running one and follow
|
|
@@ -358,10 +361,19 @@ agents doctor --device mac-mini # same matrix, scoped to one device
|
|
|
358
361
|
|
|
359
362
|
# Your Tailscale fleet, auto-discovered
|
|
360
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
|
|
361
367
|
agents ssh mac-mini # hardened SSH: fails fast if offline,
|
|
362
368
|
# PowerShell on Windows, password-from-Keychain
|
|
363
369
|
```
|
|
364
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
|
+
|
|
365
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).
|
|
366
378
|
|
|
367
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>`.
|
|
@@ -682,6 +694,13 @@ agents routines add nightly-drain --schedule "0 3 * * *" --agent claude \
|
|
|
682
694
|
|
|
683
695
|
agents routines devices nightly-drain --set yosemite-s0,mac-mini # update allowlist
|
|
684
696
|
agents routines list --host yosemite-s0 # query another device
|
|
697
|
+
|
|
698
|
+
# Signed webhook trigger: Linear issue labeled "agent" fires a routine
|
|
699
|
+
agents routines add agent-labeled-issue --on linear:Issue --action update \
|
|
700
|
+
--team-key RUSH --label agent --agent claude \
|
|
701
|
+
--prompt "Work the Linear issue that was just labeled agent"
|
|
702
|
+
agents webhook serve --secrets-bundle webhooks --port 8787 # /hooks/linear, /hooks/github
|
|
703
|
+
agents funnel up yosemite-s0 --local-port 8787 --port 443 # public HTTPS ingress
|
|
685
704
|
```
|
|
686
705
|
|
|
687
706
|
Jobs run sandboxed -- agents only see directories and tools you explicitly allow.
|
package/dist/bin/agents
CHANGED
|
Binary file
|
package/dist/commands/browser.js
CHANGED
|
@@ -8,6 +8,7 @@ import { readAndResolveBundleEnv, bundleExists, readBundle, describeBundle } fro
|
|
|
8
8
|
import { findBrowserPath, getPortOccupant, isLauncherScript } from '../lib/browser/chrome.js';
|
|
9
9
|
import { listProfileCacheDirs, removeProfileCache, listAllProfileSnapshots, } from '../lib/browser/runtime-state.js';
|
|
10
10
|
import { DEFAULT_VIEWPORT } from '../lib/browser/devices.js';
|
|
11
|
+
import { runBrowserSessions } from '../lib/browser/sessions-list.js';
|
|
11
12
|
import { discoverBrowserWsUrl, verifyBrowserIdentity } from '../lib/browser/cdp.js';
|
|
12
13
|
import { parseTargetFilter } from '../lib/browser/service.js';
|
|
13
14
|
import { BrowserDaemonNotRunningError, formatBrowserDaemonNotRunningError, sendIPCRequest, } from '../lib/browser/ipc.js';
|
|
@@ -1850,10 +1851,10 @@ function registerTaskCommands(browser) {
|
|
|
1850
1851
|
// ─── Downloads ───────────────────────────────────────────────────────────────
|
|
1851
1852
|
browser
|
|
1852
1853
|
.command('download')
|
|
1853
|
-
.description(
|
|
1854
|
+
.description("Set the download directory for a task (defaults to the profile's downloads dir)")
|
|
1854
1855
|
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
1855
1856
|
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
1856
|
-
.
|
|
1857
|
+
.option('-p, --path <dir>', "Download directory path (default: the profile's downloads dir)")
|
|
1857
1858
|
.action(async (opts) => {
|
|
1858
1859
|
const task = resolveTaskName(opts);
|
|
1859
1860
|
const response = await sendIPCRequest({
|
|
@@ -1866,7 +1867,16 @@ function registerTaskCommands(browser) {
|
|
|
1866
1867
|
console.error(response.error);
|
|
1867
1868
|
process.exit(1);
|
|
1868
1869
|
}
|
|
1869
|
-
console.log(`Download path set to ${
|
|
1870
|
+
console.log(response.downloadPath ? `Download path set to ${response.downloadPath}` : 'Download path set');
|
|
1871
|
+
});
|
|
1872
|
+
browser
|
|
1873
|
+
.command('sessions')
|
|
1874
|
+
.description('List a profile\'s captured screenshots, PDFs, recordings, and downloads')
|
|
1875
|
+
.option('--profile <name>', 'Only this profile (default: all profiles with captures)')
|
|
1876
|
+
.option('--open [selector]', "Open a capture in the OS default app: 'latest' or a filename")
|
|
1877
|
+
.option('--json', 'Emit machine-readable JSON')
|
|
1878
|
+
.action((opts) => {
|
|
1879
|
+
runBrowserSessions({ profile: opts.profile, open: opts.open, json: opts.json });
|
|
1870
1880
|
});
|
|
1871
1881
|
// ─── Recording ─────────────────────────────────────────────────────────────
|
|
1872
1882
|
const record = browser.command('record').description('Record a video of the page');
|
package/dist/commands/exec.js
CHANGED
|
@@ -242,6 +242,7 @@ export function registerRunCommand(program) {
|
|
|
242
242
|
.option('--remote-cwd <dir>', 'Explicit host working directory for --host runs (overrides --cwd; usually --cwd suffices).')
|
|
243
243
|
.option('--no-follow', 'With --host, dispatch detached and return immediately (track via `agents hosts ps/logs`).')
|
|
244
244
|
.option('--any', 'With --host <cap> (a capability tag), pick any matching host instead of erroring when several match.')
|
|
245
|
+
.option('--copy-creds', 'With --host, copy the picked runtime credentials (and Claude OAuth token) to the host, then shred them after the run. Opt-in per run.')
|
|
245
246
|
.option('--lease [backend]', 'Invent a disposable cloud box for this run and tear it down after (via crabbox). Optional backend selects the cloud (hetzner/aws/do). Unlike --host, no machine is registered.')
|
|
246
247
|
.option('--keep-box', 'With --lease, keep the box after the run instead of stopping it.');
|
|
247
248
|
// `--on` and `--computer` are hidden aliases of `--host` — same behavior.
|
|
@@ -328,41 +329,69 @@ export function registerRunCommand(program) {
|
|
|
328
329
|
process.exit(1);
|
|
329
330
|
}
|
|
330
331
|
const backend = typeof options.lease === 'string' ? options.lease : undefined;
|
|
331
|
-
|
|
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');
|
|
332
344
|
const { leaseAndRun } = await import('../lib/crabbox/lease.js');
|
|
333
|
-
const {
|
|
345
|
+
const { getConfiguredRunStrategy, resolveRunVersion } = await import('../lib/rotate.js');
|
|
334
346
|
const detected = await detectSignedInRuntimes();
|
|
335
|
-
const
|
|
336
|
-
|
|
337
|
-
|
|
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.'));
|
|
338
356
|
process.exit(1);
|
|
339
357
|
}
|
|
340
|
-
|
|
341
|
-
//
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
//
|
|
349
|
-
//
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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
|
+
}
|
|
358
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.`));
|
|
359
388
|
// Read the Claude OAuth token from the local Keychain (silent) so it can be
|
|
360
389
|
// written to ~/.claude/.credentials.json on the box — otherwise Claude boots
|
|
361
|
-
// "Not logged in".
|
|
390
|
+
// "Not logged in". `preferEmail` targets the strategy-picked account so the
|
|
391
|
+
// token matches the account this run resolved to.
|
|
362
392
|
let claudeCredentialsJson = null;
|
|
363
|
-
if (
|
|
364
|
-
|
|
365
|
-
claudeCredentialsJson = await resolveClaudeCredentialsBlob({ preferEmail: claudeEmail });
|
|
393
|
+
if (runtime === 'claude') {
|
|
394
|
+
claudeCredentialsJson = await resolveClaudeCredentialsBlob({ preferEmail: leaseEmail });
|
|
366
395
|
if (!claudeCredentialsJson) {
|
|
367
396
|
console.error(chalk.yellow('Warning: could not read the local Claude OAuth token — the box may come up "Not logged in".'));
|
|
368
397
|
}
|
|
@@ -373,7 +402,6 @@ export function registerRunCommand(program) {
|
|
|
373
402
|
// box-side marker. Rule: only ONE spinner phase is active at a time, and no
|
|
374
403
|
// other output is written to stderr while it spins — so it can never storm.
|
|
375
404
|
const { createLeaseOutputRouter, createSpinner } = await import('../lib/crabbox/progress.js');
|
|
376
|
-
const agentName = agentSpec.split('@')[0];
|
|
377
405
|
const spinner = createSpinner({ stream: process.stderr });
|
|
378
406
|
let warmupTimer;
|
|
379
407
|
const stopTimer = () => { if (warmupTimer) {
|
|
@@ -536,6 +564,44 @@ export function registerRunCommand(program) {
|
|
|
536
564
|
// which can't run over a detached remote dispatch — only forward a
|
|
537
565
|
// concrete id.
|
|
538
566
|
const resumeId = typeof options.resume === 'string' ? options.resume : undefined;
|
|
567
|
+
// --copy-creds: provision runtime credentials (and the Claude OAuth token)
|
|
568
|
+
// on the remote host before the run, then shred them after. Unlike
|
|
569
|
+
// --lease, a host is persistent, so this is strictly opt-in per run.
|
|
570
|
+
let hostCopyCreds;
|
|
571
|
+
if (options.copyCreds) {
|
|
572
|
+
const { detectSignedInRuntimes, pickRuntimes, resolveClaudeCredentialsBlob } = await import('../lib/crabbox/runtimes.js');
|
|
573
|
+
const { confirm } = await import('@inquirer/prompts');
|
|
574
|
+
const detected = await detectSignedInRuntimes();
|
|
575
|
+
const runtimes = await pickRuntimes(detected);
|
|
576
|
+
if (runtimes.length === 0) {
|
|
577
|
+
console.error(chalk.yellow('No runtimes selected. Sign into one locally then retry, or omit --copy-creds.'));
|
|
578
|
+
process.exit(1);
|
|
579
|
+
}
|
|
580
|
+
const names = runtimes
|
|
581
|
+
.map((id) => {
|
|
582
|
+
const d = detected.find((x) => x.id === id);
|
|
583
|
+
return `${d?.label ?? id}${d?.email ? ` (${d.email})` : ''}`;
|
|
584
|
+
})
|
|
585
|
+
.join(', ');
|
|
586
|
+
const whatShips = runtimes.includes('claude') ? 'credentials + Claude OAuth token' : 'credentials';
|
|
587
|
+
const ok = await confirm({
|
|
588
|
+
message: `Copy ${whatShips} for ${names} to host "${host.name}", run there, then shred them after?`,
|
|
589
|
+
default: false,
|
|
590
|
+
});
|
|
591
|
+
if (!ok) {
|
|
592
|
+
console.error(chalk.yellow('Aborted — no credentials pushed, no run dispatched.'));
|
|
593
|
+
process.exit(1);
|
|
594
|
+
}
|
|
595
|
+
let claudeCredentialsJson = null;
|
|
596
|
+
if (runtimes.includes('claude')) {
|
|
597
|
+
const claudeEmail = detected.find((d) => d.id === 'claude')?.email ?? null;
|
|
598
|
+
claudeCredentialsJson = await resolveClaudeCredentialsBlob({ preferEmail: claudeEmail });
|
|
599
|
+
if (!claudeCredentialsJson) {
|
|
600
|
+
console.error(chalk.yellow('Warning: could not read the local Claude OAuth token — the host may boot Claude "Not logged in".'));
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
hostCopyCreds = { runtimes, detected, claudeCredentialsJson };
|
|
604
|
+
}
|
|
539
605
|
// Decide whether this host run is interactive. No prompt always means
|
|
540
606
|
// interactive (matching local resolveInteractive); --interactive forces
|
|
541
607
|
// interactive even when a prompt is provided; --headless forces headless
|
|
@@ -587,6 +653,7 @@ export function registerRunCommand(program) {
|
|
|
587
653
|
passthroughArgs,
|
|
588
654
|
raw: options.raw || options.tmux === false || options.disableTmux === true,
|
|
589
655
|
forceInteractive: options.interactive,
|
|
656
|
+
copyCreds: hostCopyCreds,
|
|
590
657
|
});
|
|
591
658
|
process.exit(exitCode);
|
|
592
659
|
}
|
|
@@ -616,6 +683,7 @@ export function registerRunCommand(program) {
|
|
|
616
683
|
name: options.name,
|
|
617
684
|
resume: resumeId,
|
|
618
685
|
follow: options.follow !== false,
|
|
686
|
+
copyCreds: hostCopyCreds,
|
|
619
687
|
});
|
|
620
688
|
// Register the dispatched run in the LOCAL session index so it shows
|
|
621
689
|
// up in `agents sessions` and resolves by id/name, even though its
|
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,62 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { buildFunnelStatusCommand, buildFunnelUpCommand, parseFunnelPort } from '../lib/funnel.js';
|
|
3
|
+
import { resolveHost } from '../lib/hosts/registry.js';
|
|
4
|
+
import { resolveRemoteOsSync } from '../lib/hosts/remote-os.js';
|
|
5
|
+
import { sshTargetFor } from '../lib/hosts/types.js';
|
|
6
|
+
import { sshExec } from '../lib/ssh-exec.js';
|
|
7
|
+
async function resolveIngressHost(name) {
|
|
8
|
+
const host = await resolveHost(name);
|
|
9
|
+
if (!host)
|
|
10
|
+
throw new Error(`Unknown host "${name}". Enroll it with agents hosts add, or sync devices first.`);
|
|
11
|
+
const os = host.os ?? resolveRemoteOsSync(host.name);
|
|
12
|
+
if (os === 'windows') {
|
|
13
|
+
throw new Error('Tailscale Funnel requires a host with the Tailscale CLI; Windows cannot host this ingress path.');
|
|
14
|
+
}
|
|
15
|
+
return host;
|
|
16
|
+
}
|
|
17
|
+
async function runOnHost(hostName, command) {
|
|
18
|
+
const host = await resolveIngressHost(hostName);
|
|
19
|
+
const target = sshTargetFor(host);
|
|
20
|
+
const result = sshExec(target, command, { timeoutMs: 30_000, multiplex: true });
|
|
21
|
+
if (result.code !== 0) {
|
|
22
|
+
throw new Error((result.stderr || result.stdout).trim() || `ssh exited ${result.code ?? 'without a code'}`);
|
|
23
|
+
}
|
|
24
|
+
const out = result.stdout.trim();
|
|
25
|
+
if (out)
|
|
26
|
+
console.log(out);
|
|
27
|
+
}
|
|
28
|
+
export function registerFunnelCommand(program) {
|
|
29
|
+
const funnel = program
|
|
30
|
+
.command('funnel')
|
|
31
|
+
.description('Manage Tailscale Funnel exposure for a fleet webhook receiver.');
|
|
32
|
+
funnel
|
|
33
|
+
.command('status <host>')
|
|
34
|
+
.description('Show Tailscale Funnel status on a fleet host.')
|
|
35
|
+
.action(async (host) => {
|
|
36
|
+
try {
|
|
37
|
+
await runOnHost(host, buildFunnelStatusCommand());
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
console.error(chalk.red(err.message));
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
funnel
|
|
45
|
+
.command('up <host>')
|
|
46
|
+
.description('Expose a localhost webhook receiver through Tailscale Funnel.')
|
|
47
|
+
.requiredOption('--local-port <n>', 'Local receiver port on the remote host')
|
|
48
|
+
.option('--port <n>', 'Public Funnel port: 443, 8443, or 10000', '443')
|
|
49
|
+
.action(async (host, opts) => {
|
|
50
|
+
try {
|
|
51
|
+
const publicPort = parseFunnelPort(opts.port ?? '443');
|
|
52
|
+
const localPort = Number.parseInt(opts.localPort, 10);
|
|
53
|
+
const command = buildFunnelUpCommand(publicPort, localPort);
|
|
54
|
+
await runOnHost(host, command);
|
|
55
|
+
console.log(chalk.green(`Funnel enabled on ${host}: public :${publicPort} → localhost:${localPort}`));
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
console.error(chalk.red(err.message));
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
}
|
package/dist/commands/hosts.js
CHANGED
|
@@ -18,6 +18,7 @@ import { resolveRemoteOsSync } from '../lib/hosts/remote-os.js';
|
|
|
18
18
|
import { listTasks, loadTask, findTaskByName, findTaskBySessionId } from '../lib/hosts/tasks.js';
|
|
19
19
|
import { reconcileRunningTasks } from '../lib/hosts/reconcile.js';
|
|
20
20
|
import { showHostTaskLog } from '../lib/hosts/logs.js';
|
|
21
|
+
import { stopDispatchedTask } from '../lib/hosts/dispatch.js';
|
|
21
22
|
/** Parse `user@host` or `host` into its pieces. */
|
|
22
23
|
function parseTarget(target) {
|
|
23
24
|
const at = target.indexOf('@');
|
|
@@ -201,6 +202,42 @@ async function doLogs(ref, follow, full) {
|
|
|
201
202
|
if (res.exitCode !== undefined)
|
|
202
203
|
process.exitCode = res.exitCode;
|
|
203
204
|
}
|
|
205
|
+
/** Resolve a host-task ref (id, --name handle, or session id) or null. */
|
|
206
|
+
function resolveTaskRef(ref) {
|
|
207
|
+
return loadTask(ref) ?? findTaskByName(ref) ?? findTaskBySessionId(ref);
|
|
208
|
+
}
|
|
209
|
+
async function doStop(ref) {
|
|
210
|
+
// Heal first so we don't try to kill a process that already exited.
|
|
211
|
+
const current = resolveTaskRef(ref);
|
|
212
|
+
if (!current) {
|
|
213
|
+
console.log(chalk.red(`Unknown task "${ref}".`));
|
|
214
|
+
process.exitCode = 1;
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const task = reconcileRunningTasks([current])[0] ?? current;
|
|
218
|
+
if (task.status !== 'running') {
|
|
219
|
+
console.log(chalk.gray(`Task ${task.id} is already ${task.status}` + (task.exitCode !== undefined ? ` (exit ${task.exitCode})` : '') + '.'));
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
try {
|
|
223
|
+
const stopped = stopDispatchedTask(task);
|
|
224
|
+
const statusColor = stopped.status === 'completed' ? chalk.green : chalk.yellow;
|
|
225
|
+
const exitNote = stopped.exitCode === 143
|
|
226
|
+
? 'exit 143 / SIGTERM'
|
|
227
|
+
: stopped.exitCode !== undefined
|
|
228
|
+
? `exit ${stopped.exitCode}`
|
|
229
|
+
: stopped.status;
|
|
230
|
+
console.log(chalk.green(`Stopped ${stopped.id}`) +
|
|
231
|
+
chalk.gray(` on ${stopped.host}`) +
|
|
232
|
+
' ' + statusColor(stopped.status) +
|
|
233
|
+
chalk.gray(` (${exitNote})`));
|
|
234
|
+
console.log(chalk.gray(`Logs: agents hosts logs ${stopped.id}`));
|
|
235
|
+
}
|
|
236
|
+
catch (err) {
|
|
237
|
+
console.error(chalk.red(err?.message ?? err));
|
|
238
|
+
process.exitCode = 1;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
204
241
|
/** Register the `agents hosts` command tree. */
|
|
205
242
|
export function registerHostsCommand(program) {
|
|
206
243
|
const hosts = program
|
|
@@ -239,4 +276,9 @@ export function registerHostsCommand(program) {
|
|
|
239
276
|
.option('-f, --follow', 'Follow live output')
|
|
240
277
|
.option('-m, --full', 'Show the full raw combined-stdout log instead of the concise summary')
|
|
241
278
|
.action((id, opts) => doLogs(id, !!opts.follow, !!opts.full));
|
|
279
|
+
hosts
|
|
280
|
+
.command('stop <id>')
|
|
281
|
+
.alias('kill')
|
|
282
|
+
.description('Terminate a running host task from this machine (SIGTERM process group; marks failed/143).')
|
|
283
|
+
.action((id) => doStop(id));
|
|
242
284
|
}
|