@phnx-labs/agents-cli 1.22.32 → 1.22.34
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 +55 -0
- package/README.md +2 -1
- package/dist/bin/agents +0 -0
- package/dist/commands/accounts.js +3 -3
- package/dist/commands/browser-sessions-picker.d.ts +16 -0
- package/dist/commands/browser-sessions-picker.js +179 -0
- package/dist/commands/browser.js +9 -4
- package/dist/commands/hosts.js +1 -5
- package/dist/commands/inspect.js +174 -41
- package/dist/commands/message.d.ts +6 -1
- package/dist/commands/message.js +60 -3
- package/dist/commands/sessions-browser.js +2 -2
- package/dist/commands/sessions.d.ts +54 -4
- package/dist/commands/sessions.js +274 -58
- package/dist/commands/ssh.js +17 -5
- package/dist/commands/teams.d.ts +28 -0
- package/dist/commands/teams.js +148 -13
- package/dist/commands/upgrade.d.ts +7 -0
- package/dist/commands/upgrade.js +10 -0
- package/dist/commands/watchdog.d.ts +2 -0
- package/dist/commands/watchdog.js +112 -27
- package/dist/index.js +51 -59
- package/dist/lib/agents.js +6 -0
- package/dist/lib/browser/sessions-list.d.ts +81 -0
- package/dist/lib/browser/sessions-list.js +179 -4
- package/dist/lib/daemon.js +45 -5
- package/dist/lib/devices/connect.d.ts +33 -0
- package/dist/lib/devices/connect.js +61 -3
- package/dist/lib/devices/doctor-findings.d.ts +4 -2
- package/dist/lib/devices/doctor-findings.js +4 -2
- package/dist/lib/exec.js +63 -6
- package/dist/lib/help.d.ts +3 -2
- package/dist/lib/help.js +4 -0
- package/dist/lib/hosts/dispatch.d.ts +2 -32
- package/dist/lib/hosts/dispatch.js +6 -61
- package/dist/lib/hosts/tasks.d.ts +7 -0
- package/dist/lib/hosts/tasks.js +9 -0
- package/dist/lib/mailbox-target.d.ts +27 -0
- package/dist/lib/mailbox-target.js +21 -0
- package/dist/lib/mcp.d.ts +10 -0
- package/dist/lib/mcp.js +21 -2
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/migrate.d.ts +11 -0
- package/dist/lib/migrate.js +40 -0
- package/dist/lib/project-root.d.ts +47 -0
- package/dist/lib/project-root.js +68 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/agent.d.ts +9 -2
- package/dist/lib/secrets/agent.js +52 -7
- package/dist/lib/secrets/reaper.d.ts +24 -3
- package/dist/lib/secrets/reaper.js +55 -6
- package/dist/lib/session/discover.js +12 -0
- package/dist/lib/session/render.d.ts +2 -0
- package/dist/lib/session/render.js +1 -1
- package/dist/lib/session/team-filter.d.ts +11 -0
- package/dist/lib/session/team-filter.js +10 -0
- package/dist/lib/shims.js +9 -2
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +8 -3
- package/dist/lib/startup/root-command.d.ts +3 -0
- package/dist/lib/startup/root-command.js +10 -0
- package/dist/lib/teams/agents.d.ts +136 -6
- package/dist/lib/teams/agents.js +324 -58
- package/dist/lib/teams/worktree.d.ts +39 -2
- package/dist/lib/teams/worktree.js +60 -4
- package/dist/lib/types.d.ts +11 -0
- package/dist/lib/versions.js +2 -2
- package/dist/lib/watchdog/history.d.ts +20 -0
- package/dist/lib/watchdog/history.js +46 -0
- package/dist/lib/watchdog/log.d.ts +16 -1
- package/dist/lib/watchdog/log.js +82 -2
- package/dist/lib/watchdog/runner.d.ts +12 -0
- package/dist/lib/watchdog/runner.js +20 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,60 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.22.34
|
|
4
|
+
|
|
5
|
+
- **A failed `agents teams add` no longer strands an `agents/<name>` branch that breaks every retry (RUSH-2356).** The worktree was created before the teammate record was persisted and nothing removed it when the add failed, so the next `teams add` with the same `--worktree` name died on `fatal: a branch named 'agents/<name>' already exists` — observed 2026-08-07, forcing three renames before a teammate could be created. Two guarantees now hold. Name uniqueness and the `--after` dependency graph are validated **before** `createWorktree` runs (`AgentManager.validateAddPreconditions`), so a duplicate name, unknown dependency, or cycle never creates a branch at all. A failure **after** the worktree exists — a missing harness CLI, a launch error, a cloud dispatch failure — removes the worktree and its branch before the command exits non-zero, and prints the exact `git worktree remove` / `git branch -D` pair if that teardown itself fails. Teardown is scoped twice over, because deleting a live teammate's worktree would destroy real work: only a worktree that same add created is a candidate, and only when no live teammate claims it (`AgentManager.isWorktreeClaimed`, a raw meta.json scan across every team, counting only non-terminal records). That second check matters because the add can fail *after* the record is durably saved — `spawn()` saves a staged teammate and only then runs the retention pass, which refreshes every sibling and can throw on a distributed one — so a teammate already recorded and waiting on an `--after` dependency keeps its worktree. If we can't prove a worktree is an orphan, it is left in place with the manual removal printed — the claim check **fails closed**, so an unreadable or half-written record answers "claimed" rather than "free": only a genuinely absent record (ENOENT) proves nothing claims the worktree, because the alternative error deletes a running teammate's uncommitted work. The pr-watch fixer path (`reactWithTeammate`) and a partially-failed `createWorktree` (branch ref created, checkout not) run the same guarded teardown — the fixer path matters because it stages its teammate with `--after` when it follows a source teammate, so it can reach the failure branch with a live, durably-recorded, merely-pending teammate already owning the worktree.
|
|
6
|
+
|
|
7
|
+
One user-visible surface change: a duplicate name, unknown `--after` dependency, or dependency cycle is now rejected before the worktree step, so it surfaces as the raw validation message under the friction id `teams/add-precondition-failed` rather than wrapped in `Could not add <agent> to <team>:` under `teams/add-failed`. Source: `apps/cli/src/commands/teams.ts`, `apps/cli/src/lib/teams/agents.ts`.
|
|
8
|
+
|
|
9
|
+
- **`teams add` no longer silently discards pending teammates past the retention cap (RUSH-2356).** `listCompleted()` classified every non-RUNNING status — including `pending` — as "completed", so `cleanupOldAgents()` reaped staged `--after` teammates once the team's history crossed the 50-record cap: `teams add --after` printed a full success block for a teammate whose record then vanished. Retention now filters on a real terminal-status set (`completed`/`failed`/`stopped`); a pending or running teammate is never a reap candidate. `spawn()` also asserts its record is durably on disk right after writing it, so a failed write can no longer report success for a teammate that doesn't exist. Source: `apps/cli/src/lib/teams/agents.ts`.
|
|
10
|
+
|
|
11
|
+
- **A session recorded on Windows no longer has its `cwd` corrupted when read on macOS or Linux (RUSH-2358).** `normalizeCwd()` is the shared cwd-normalisation path for every harness's session filtering and display, and its fallback used `path.isAbsolute(cwd)` — which is platform-relative. A Windows-rooted path (`C:\Users\dev\repo\...`) is not absolute *on POSIX*, so it fell through to `path.resolve()`, which silently prefixed the reading process's own working directory onto it. Transcripts sync across the fleet, so any session captured on a Windows box and indexed on a POSIX box has carried a corrupted `cwd` since the fallback was introduced — visible as wrong directory attribution in `agents sessions` filtering and display, and as a lost `worktree_slug`. The normalisation now recognises a Windows-rooted path (drive letter or UNC) on POSIX and treats it as already absolute, mirroring the existing branch that recognises a POSIX-rooted path on Windows. Source: `apps/cli/src/lib/session/discover.ts`.
|
|
12
|
+
|
|
13
|
+
- **A dead `--device` teammate no longer reports RUNNING forever, and `resume` relaunches it (RUSH-2366).** The remote liveness probe only distinguished "`.exit` sentinel present" from "absent" — a process killed before it could write its exit code (SIGKILL, OOM, box lost) fell into the same "absent" bucket as a still-running teammate and stayed RUNNING indefinitely, so `agents teams resume` kept routing it to steer/mailbox instead of relaunching. The probe now resolves three states (ALIVE / EXITED / GONE) in one round-trip; GONE (process confirmed dead, no sentinel) latches FAILED. A separate stale-manager race is also fixed: a long-lived `teams start --watch` supervisor's cached teammate could re-persist a stale in-memory RUNNING over a terminal status a different CLI invocation (`agents teams stop`) had just written to disk — `updateStatusFromProcess()` and `rescanFromDisk()` now adopt a newer on-disk terminal status instead of overwriting it. Source: `apps/cli/src/lib/teams/agents.ts`.
|
|
14
|
+
- **`agents message` can now reach a detached `agents run --device <host> --no-follow` dispatch (RUSH-2366 follow-up).** `getActiveSessions()` has no visibility into a detached dispatch's live remote process — only `~/.agents/.cache/hosts/<id>.json` (the record `agents hosts ps` reads) does — so `agents message <name>` reported "No running agent... matches" for a dispatch `agents hosts ps` showed running with a live pid, and the only recovery was killing and re-dispatching it (losing all context). `agents message` now resolves the same host-task records and reroutes the message over the existing `--host` passthrough to the box that actually owns it, or fails with an actionable "already `<status>`" message when the dispatch has already finished. It heals the dispatch record with `reconcileRunningTasks` before deciding, the same step `agents hosts stop`/`ps` already take — a detached record never self-updates, so a finished run stays stamped `running` on disk and would otherwise be routed through an SSH reroute that could only fail instead of being reported finished locally. Source: `apps/cli/src/commands/message.ts`, `apps/cli/src/lib/mailbox-target.ts`, `apps/cli/src/lib/hosts/tasks.ts`.
|
|
15
|
+
- **`teams add --worktree` no longer nests a new worktree inside another teammate's worktree (RUSH-2366 follow-up).** When the caller's ambient cwd was already inside a linked worktree (e.g. an orchestrator agent dispatching `teams add` from within its own worktree), `createWorktree` resolved the placement root via `git rev-parse --show-toplevel`, which returns that worktree's OWN root rather than the main checkout's — landing the new worktree at `.../worktrees/A/.agents/worktrees/B`. Cleaning up A then destroyed B along with it, even though B's commit history survived on its branch ref. `createWorktree`/`removeWorktree` now resolve via `getMainRepoRoot()` (the existing `git-common-dir`-based helper), which always points at the main checkout regardless of which worktree the caller is standing in. Source: `apps/cli/src/lib/teams/worktree.ts`.
|
|
16
|
+
|
|
17
|
+
- **An interactive Claude run authenticates from your own login again, not the headless setup-token (RUSH-2395).** `buildExecEnv` injected the `auth` bundle's per-account setup-token into `CLAUDE_CODE_OAUTH_TOKEN` for *every* resolved Claude launch, so a hand-driven session on a personal machine reported `Auth token: CLAUDE_CODE_OAUTH_TOKEN` in `/status` and ran on the probe credential instead of the login the user had established. That token exists for runs with no human present (usage probes, routines, dispatched runs), and it is `user:inference`-scoped, so it also cannot read usage (RUSH-2392). Injection is now gated on the run resolving **headless**; an interactive run is left on its per-version login, and an inherited copy of that same token is dropped so a nested interactive launch from inside a headless agent's shell does not keep authenticating as it. A token the caller exported themselves is matched by value and left alone. Source: `apps/cli/src/lib/exec.ts`.
|
|
18
|
+
|
|
19
|
+
- **Multiple Cursor accounts are now real — each run authenticates as the account you pick (RUSH-2400).** Previously `agents view cursor` showed several Cursor accounts and balanced rotation picked one, but every run silently authenticated as the single live `~/.cursor` login: `buildExecEnv` gave Cursor no per-account isolation, so the version/account you selected was discarded at exec. Cursor has no config-dir env var, but its OAuth token — the login gate — lives at `$XDG_CONFIG_HOME/cursor/auth.json` (verified empirically; `~/.cursor/cli-config.json` is only metadata). `buildExecEnv` and the versioned-alias shim now pin `XDG_CONFIG_HOME` at the version home for Cursor (the same XDG mechanism muse uses), so `agents run cursor@<account>`, the account picker, and balanced rotation each launch `cursor-agent` authenticated as that account's own token — isolated per child process, so two accounts run concurrently without clobbering one another. `agents view cursor` now verifies each account's signed-in state against its own token (`CREDENTIAL_FILE_SEGMENTS.cursor`), and a migration seeds the current global login into the active account's home so no one is logged out on upgrade. The routine overlay path is unchanged (seeds from the active login by design). Source: `apps/cli/src/lib/exec.ts`, `apps/cli/src/lib/shims.ts`, `apps/cli/src/lib/agents.ts`, `apps/cli/src/lib/migrate.ts`.
|
|
20
|
+
|
|
21
|
+
- **`agents browser sessions` / `agents sessions --browser` is task-first, with agent outcome previews (RUSH-2407).** On a TTY, the picker now groups every screenshot, PDF, and recording by browser task instead of printing one row per file, newest task first. A task whose `launchId` still resolves to an indexed agent session shows the canonical session digest (prompt, changes, tests, last response) before its captures; a task whose owning run already stopped is labeled unlinked rather than silently dropped. Downloads get their own group. Search matches task name, profile, the linked session's agent/topic, or an artifact filename; `enter` opens the highlighted capture (or drills into a capture list first for a multi-capture task). New `--no-interactive` flag prints the previous flat table; `--json` output is unchanged. Source: `apps/cli/src/lib/browser/sessions-list.ts`, `apps/cli/src/commands/browser-sessions-picker.ts`.
|
|
22
|
+
|
|
23
|
+
- **`agents sessions --routine` is outcome-aware: run history, then linked sessions (RUSH-2409).** Selecting a routine now renders its canonical run history first — every run's id, trigger, status (completed/failed/blocked/skipped/missed), start/duration, exit/error, execution type (agent/command/workflow), placement (local/host/cloud), and log/report paths — and links each run to the indexed agent session(s) it produced (agent/version/account/model/token/cost/duration/tool metadata). A command-only routine (e.g. `auto-dispatch`, which produces no transcript) no longer dead-ends into the generic "No sessions found" copy: it shows its runs and states plainly that no agent session is produced, and command/blocked/skipped/missed attempts appear with no fabricated session row. Counts distinguish run records from linked sessions, and routine selection stays global across every working directory. The drilldown is the default routine view; an explicit `--flat`/`--tree` or a session id/query keeps the scoped session listing/picker. The canonical source is the run history under `~/.agents/.history/runs/<routine>/` (`listRuns`), not the session index — no divergent state added. Source: `apps/cli/src/commands/sessions.ts`.
|
|
24
|
+
|
|
25
|
+
- **Inspect Watchdog decisions and actions from the CLI (RUSH-2410).** `agents watchdog history [sessionId]` renders every session inspection plus persisted decisions, nudges, rotates, and errors newest-first, with duration/limit filters, optional heartbeat rows, and safe JSON that excludes transcript content. The audit retains 5,000 events, and persisted transcript context is capped at 4,096 characters per event. `agents watchdog status` points to the history command instead of leaving the audit log undiscoverable. Source: `apps/cli/src/commands/watchdog.ts`, `apps/cli/src/lib/watchdog/{runner,log,history}.ts`.
|
|
26
|
+
|
|
27
|
+
- **`agents ssh <device>` mirrors your project directory on interactive login (RUSH-2412).**
|
|
28
|
+
An interactive `agents ssh <device>` with no command now starts the remote login
|
|
29
|
+
shell in the same home-relative directory you launched from — `agents ssh
|
|
30
|
+
yosemite-s0` from `~/src/app` lands in `~/src/app` on the target when it exists,
|
|
31
|
+
else the remote home — matching `agents run --host`. It reuses the one canonical
|
|
32
|
+
portable-cwd resolver (`deriveMirroredCwd` / `remoteCdPrefix`), is best-effort (a
|
|
33
|
+
missing checkout never fails the login), injection-safe for paths with spaces or
|
|
34
|
+
metacharacters, and covers POSIX and Windows PowerShell. An explicit `agents ssh
|
|
35
|
+
<device> <cmd…>` is unchanged. Source: `apps/cli/src/lib/devices/connect.ts`,
|
|
36
|
+
`apps/cli/src/lib/project-root.ts`, `apps/cli/src/commands/ssh.ts`.
|
|
37
|
+
|
|
38
|
+
- **Make live Watchdog ticks explain each session (RUSH-2416).** `agents watchdog` now prints the tick date/time and attention-worthy sessions with their stable id, label/topic, agent, host app, machine, project, activity, start/activity ages, cwd, latest preview, and exact decision reason. Healthy/non-actionable inspections collapse into one count; `--verbose` restores every row and `--json` remains the complete machine-readable result. Source: `apps/cli/src/commands/watchdog.ts`, `apps/cli/src/lib/watchdog/runner.ts`.
|
|
39
|
+
|
|
40
|
+
- **`agents daemon start` no longer defeats its own launchd/systemd launch (RUSH-2417).** `startDaemon()` and the daemon child's `claimDaemonInstance()` resolve the same `<daemonDir>/daemon.lock`, and the launchd/systemd branches busy-waited on `waitForPid(3000)` while still holding it — the release only happened in `startDaemon`'s outer `finally`. The freshly-launched daemon therefore hit `EEXIST` against a holder that was alive (the parent CLI process) and exited with the false "another daemon is mid-takeover" warning, so the service-manager fast-start path failed deterministically on every fresh install and fell back to a detached spawn. The start lock is now released the moment the launch has been issued — after `launchctl load` / `systemctl start` / the detached spawn — instead of after the pid wait. The concurrent-start guarantee is unchanged: two `startDaemon()` calls still cannot both launch, because launchd (one plist label) and systemd (one unit) no-op a second start and the detached path is covered by `claimDaemonInstance`'s last-wins takeover (SING-11). Source: `apps/cli/src/lib/daemon.ts`.
|
|
41
|
+
|
|
42
|
+
- **Reap orphaned keychain `watch-lock` helpers after ungraceful daemon death (RUSH-2419); await secrets broker socket release on close (RUSH-2421).** The auto-lock-on-sleep watcher is still never killed while its owning daemon is alive, but a separate reaper path now cleans it up once the parent is provably gone (OOM, SIGKILL, killTree) — closing the leak that only `close()` used to handle. Hosted and standalone broker `close()` now wait for `net.Server`'s `close` event with a 2s bound so a successor cannot race a half-released socket. Source: `apps/cli/src/lib/secrets/reaper.ts`, `apps/cli/src/lib/secrets/agent.ts`.
|
|
43
|
+
|
|
44
|
+
* **Searchable command API reference.** `apps/cli/docs/command-reference.html` is generated from the complete registered Commander tree and covers every visible command and nested subcommand, argument, flag form, choice, default, alias, example, and note. `npm run verify:index` now fails when the HTML or canonical JSON drifts from the CLI. Source: `apps/cli/scripts/gen-command-index.ts`.
|
|
45
|
+
|
|
46
|
+
- **`agents inspect` drill-downs render readable previews and return the resources that actually exist.** The detail view wrapped a description at a hardcoded 100 characters regardless of terminal width, so a long plugin or skill description lost its sentence at 80 columns and wasted the space at 200; it now wraps to the real width, and detail rows are width-aware too. `--rules` listed the composed `AGENTS.md` output and its symlinks while hiding every fragment under `rules/subrules/`, so `agents inspect . --rule foundations` exited 1 — rules now resolve from `subrules/`. `--hooks` did a flat `readdir` and reported the event directories plus doc files, disagreeing with the count the summary view printed for the same repo (12 vs 39); both now read through `listHookEntriesFromDir`. Plugin detail gains the bundle's real recursive size instead of `(bundle)`, plus its `author`, `dependencies`, and which execution surfaces it ships (`hooks/`, `bin/`, `.mcp.json`, …) — `author` was present in every `plugin.json` on disk but missing from the `PluginManifest` type, so nothing read it. Source: `apps/cli/src/commands/inspect.ts`, `apps/cli/src/lib/types.ts`.
|
|
47
|
+
|
|
48
|
+
- **`agents inspect` no longer crashes when a `plugin.json` or `agents.yaml` field's JSON/YAML type contradicts the interface it is cast to.** `loadPluginManifest` and the `agents.yaml` hook reader both cast parsed input straight to a typed shape and validate almost nothing, so any field can be a string where an array was declared, or a number where a string was. Four crashes came from that: `"dependencies": "some-plugin"` reached `.join()` (truthy `.length`, no `.join`) *while building the plugin list*, so one bad manifest took down `agents inspect .`, `--plugins`, `--json`, and even a query for a different, valid plugin; a numeric `"version"` and a non-string `"description"` threw later at render, breaking the detail view and (for `description`) the plugin list; and in `agents.yaml`, a scalar `events: PreToolUse` or a numeric `matches.prompt_contains` threw in `summarizeHook`, breaking bare `agents inspect <repo>` and — through the user-level manifest — `agents inspect <agent>` on every machine. Every uncontrolled field now routes through one of two coercion helpers, so a scalar renders where a list was expected and an object drops its row instead of printing `[object Object]`. Source: `apps/cli/src/commands/inspect.ts`.
|
|
49
|
+
|
|
50
|
+
- **One malformed `mcp/*.yaml` no longer takes down `agents inspect <repo>` with an unhandled stack trace.** `validateMcpYamlConfig` returns `null` for some bad shapes but *throws* for others — `args` that is not a string array, a non-string `command`, a non-map `env` — and the three directory scans that call it (`discoverMcpConfigsFromRepo`, `listMcpServerConfigs`, and the layered project/user MCP resource scan in `versions.ts`) let that throw escape, so a single bad file in a cloned repo crashed the whole command and hid the valid configs beside it. Scans now skip the offending file and name it on stderr, exactly as they already skip a `null`; explicit single-file operations still throw loudly. Source: `apps/cli/src/lib/mcp.ts`, `apps/cli/src/lib/versions.ts`.
|
|
51
|
+
|
|
52
|
+
## 1.22.33
|
|
53
|
+
|
|
54
|
+
- **Release tags are annotated with the folded changelog notes.** `scripts/release.sh` now creates `v<version>` as an annotated tag whose message is `Release <version>` plus the body of `.changelog/<version>.md` (the same notes that already become the release PR body). Agents keep writing one fragment under `.changelog/next/`; there is no separate tag-description channel. The already-published missing-tag recovery path uses the same helper with `--force`. Source: `apps/cli/scripts/release.sh`.
|
|
55
|
+
|
|
56
|
+
`agents sessions --routines` once again opens the routine picker across every working directory instead of an empty current-repository session browser.
|
|
57
|
+
|
|
3
58
|
## 1.22.32
|
|
4
59
|
|
|
5
60
|
- **Routines validate execution context before activation and fire once per schedule slot (RUSH-2290).** A routine selects one execution `project` plus a portable `cwd`; add/edit save proven blockers paused, durable slot and active-run claims prevent duplicate or overlapping launches, and every blocked/skipped/pre-spawn attempt remains visible without requiring a session transcript. Source: `apps/cli/src/lib/routine-context.ts`, `apps/cli/src/lib/routine-readiness.ts`, `apps/cli/src/lib/runner.ts`.
|
package/README.md
CHANGED
|
@@ -428,11 +428,12 @@ Top-level questions and waiting notifications publish one atomic open-block reco
|
|
|
428
428
|
|
|
429
429
|
```bash
|
|
430
430
|
agents watchdog # one tick, dry run — reports what it WOULD nudge and why
|
|
431
|
+
agents watchdog --verbose # include healthy/non-actionable session inspections
|
|
431
432
|
agents watchdog --nudge # actually inject "Continue." into the stalled split
|
|
432
433
|
agents watchdog --watch # daemon loop: a tick every --interval
|
|
433
434
|
```
|
|
434
435
|
|
|
435
|
-
`agents watchdog` detects a stalled session, resolves the *exact* terminal split it lives in (tmux, iTerm, VSCodium, or a raw pty), and injects a nudge -- `Continue.` by default, or set `--text`. It's dry by default; `--nudge` acts on a single tick. `agents watchdog on|off` controls the device-local daemon pass, which runs once every three minutes. Steer a single run with `agents watchdog policy <id> off | keep | handsoff`.
|
|
436
|
+
`agents watchdog` detects a stalled session, resolves the *exact* terminal split it lives in (tmux, iTerm, VSCodium, or a raw pty), and injects a nudge -- `Continue.` by default, or set `--text`. Its timestamped default output shows attention-worthy sessions with their agent, host app, machine, project, activity, age, path, latest preview, and decision reason; `--verbose` restores healthy/non-actionable inspections. It's dry by default; `--nudge` acts on a single tick. `agents watchdog on|off` controls the device-local daemon pass, which runs once every three minutes. Steer a single run with `agents watchdog policy <id> off | keep | handsoff`.
|
|
436
437
|
|
|
437
438
|
A stalled session whose tail shows a hard account limit ("You've hit your weekly limit · resets …") is **rotated in place** instead of nudged: the watchdog gates on the same healthy-account selection `agents run auto` makes (zero healthy → one skip event per cooldown window, terminal untouched), injects the harness's exit sequence, relaunches `agents run auto --interactive --session-id <uuid>` in the *same* tab, then replays the old session's resume once the new TUI is live. Default on; `agents watchdog rotate off` disables it (nudging stays on).
|
|
438
439
|
|
package/dist/bin/agents
CHANGED
|
Binary file
|
|
@@ -24,9 +24,9 @@ async function chooseAccount() { const accounts = await discoverAccounts(); if (
|
|
|
24
24
|
throw new Error('No signed-in accounts found. Run an installed agent and complete its normal login first.'); return select({ message: 'Which signed-in account do you want to name?', choices: accounts.map(account => ({ name: `${account.agent} ${account.display} (${account.versions.length} version${account.versions.length === 1 ? '' : 's'})${account.label ? ` currently “${account.label}”` : ''}`, value: account })) }); }
|
|
25
25
|
export function registerAccountsCommand(program) {
|
|
26
26
|
const accounts = program.command('accounts').description('Browse and name signed-in harness accounts').option('--json', 'Machine-readable discovered accounts').action(async (o) => printAccounts(!!o.json));
|
|
27
|
-
accounts.command('list').description('Alias for accounts').option('--json').action((o) => printAccounts(!!o.json));
|
|
27
|
+
accounts.command('list').description('Alias for accounts').option('--json', 'Machine-readable discovered accounts').action((o) => printAccounts(!!o.json));
|
|
28
28
|
accounts.command('name <label>').description('Name one signed-in account; matching installed versions are found automatically').option('--from <agent@version>', 'Non-interactive identity source').action(async (label, o) => { const picked = o.from ? await fingerprintFromSource(o.from) : await chooseAccount(); nameAccount(label, picked.agent, picked.fingerprint); console.log(chalk.green(`Named the ${picked.agent} account '${label}'.`)); console.log(chalk.gray(`Found it in ${picked.versions.length} installed version${picked.versions.length === 1 ? '' : 's'}: ${picked.versions.join(', ')}`)); });
|
|
29
|
-
accounts.command('rename <old> <new>').action((oldLabel, newLabel) => renameAccountLabel(oldLabel, newLabel));
|
|
30
|
-
accounts.command('remove <label>').action((label) => removeAccountLabel(label));
|
|
29
|
+
accounts.command('rename <old> <new>').description('Rename a saved account label').action((oldLabel, newLabel) => renameAccountLabel(oldLabel, newLabel));
|
|
30
|
+
accounts.command('remove <label>').description('Remove a saved account label').action((label) => removeAccountLabel(label));
|
|
31
31
|
setHelpSections(accounts, { examples: `agents accounts\nagents accounts name work\nagents accounts name work --from claude@2.1.220\nagents run claude --account work`, notes: 'First run the harness and complete its normal login. A label names one provider account; every matching installed version is discovered automatically. OAuth credentials are never copied or shared.' });
|
|
32
32
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface BrowserSessionsCommandOpts {
|
|
2
|
+
profile?: string;
|
|
3
|
+
open?: string | boolean;
|
|
4
|
+
json?: boolean;
|
|
5
|
+
/** Commander's `--no-interactive` convention: `false` opts out. */
|
|
6
|
+
interactive?: boolean;
|
|
7
|
+
}
|
|
8
|
+
/** True when the interactive picker should open instead of the printed table:
|
|
9
|
+
* a real TTY, no `--json`/`--open`, and `--no-interactive` not set. */
|
|
10
|
+
export declare function shouldOpenInteractiveBrowserSessions(opts: BrowserSessionsCommandOpts, isTTY: boolean): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Shared entry point for `agents browser sessions` and `agents sessions
|
|
13
|
+
* --browser`. Replaces direct calls to `runBrowserSessions` at both call
|
|
14
|
+
* sites so the interactive routing decision lives in one place.
|
|
15
|
+
*/
|
|
16
|
+
export declare function runBrowserSessionsCommand(opts: BrowserSessionsCommandOpts): Promise<void>;
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive, task-first `agents browser sessions` / `agents sessions --browser`
|
|
3
|
+
* view (RUSH-2407). Backs the TTY path only — non-TTY, `--json`, `--open`, and
|
|
4
|
+
* `--no-interactive` all fall straight through to the existing flat printer in
|
|
5
|
+
* `lib/browser/sessions-list.ts` (unchanged, so `--json` stays a stable surface).
|
|
6
|
+
*
|
|
7
|
+
* Reuses the same `itemPicker` + `buildPreview` primitives as the ordinary
|
|
8
|
+
* session picker (`sessions-picker.ts`) — same search/filter/quit help, and the
|
|
9
|
+
* preview pane for a linked task IS the canonical session digest, not a
|
|
10
|
+
* second renderer.
|
|
11
|
+
*/
|
|
12
|
+
import chalk from 'chalk';
|
|
13
|
+
import { itemPicker } from '../lib/picker.js';
|
|
14
|
+
import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
15
|
+
import { buildPreview } from './sessions-picker.js';
|
|
16
|
+
import { runBrowserSessions, buildBrowserSessionRows, matchesBrowserSessionRow, openArtifact, formatBytes, } from '../lib/browser/sessions-list.js';
|
|
17
|
+
import { formatRelativeTime } from '../lib/session/relative-time.js';
|
|
18
|
+
/** True when the interactive picker should open instead of the printed table:
|
|
19
|
+
* a real TTY, no `--json`/`--open`, and `--no-interactive` not set. */
|
|
20
|
+
export function shouldOpenInteractiveBrowserSessions(opts, isTTY) {
|
|
21
|
+
return opts.interactive !== false && !opts.json && opts.open === undefined && isTTY;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Shared entry point for `agents browser sessions` and `agents sessions
|
|
25
|
+
* --browser`. Replaces direct calls to `runBrowserSessions` at both call
|
|
26
|
+
* sites so the interactive routing decision lives in one place.
|
|
27
|
+
*/
|
|
28
|
+
export async function runBrowserSessionsCommand(opts) {
|
|
29
|
+
if (!shouldOpenInteractiveBrowserSessions(opts, isInteractiveTerminal())) {
|
|
30
|
+
runBrowserSessions({ profile: opts.profile, open: opts.open, json: opts.json });
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const rows = buildBrowserSessionRows(opts.profile);
|
|
34
|
+
if (rows.length === 0) {
|
|
35
|
+
console.log(`No browser captures found${opts.profile ? ` for profile "${opts.profile}"` : ''}.`);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
await browseAndOpen(rows);
|
|
39
|
+
}
|
|
40
|
+
const KIND_LABEL = {
|
|
41
|
+
screenshot: 'shots',
|
|
42
|
+
pdf: 'pdf',
|
|
43
|
+
recording: 'rec',
|
|
44
|
+
download: 'dl',
|
|
45
|
+
};
|
|
46
|
+
function formatCounts(counts) {
|
|
47
|
+
const parts = [];
|
|
48
|
+
for (const kind of Object.keys(KIND_LABEL)) {
|
|
49
|
+
if (counts[kind] > 0)
|
|
50
|
+
parts.push(`${KIND_LABEL[kind]} ${counts[kind]}`);
|
|
51
|
+
}
|
|
52
|
+
return parts.join(' · ') || '-';
|
|
53
|
+
}
|
|
54
|
+
function rowLinkSummary(row) {
|
|
55
|
+
if (row.kind === 'downloads')
|
|
56
|
+
return '';
|
|
57
|
+
if (row.linkStatus === 'linked' && row.linkedSession) {
|
|
58
|
+
const s = row.linkedSession;
|
|
59
|
+
return chalk.cyan(s.agent) + ' — ' + (s.label || s.topic || s.shortId);
|
|
60
|
+
}
|
|
61
|
+
if (row.linkStatus === 'unresolved') {
|
|
62
|
+
return chalk.yellow(`owner ${row.owner ?? 'unknown'} (session not indexed here)`);
|
|
63
|
+
}
|
|
64
|
+
return chalk.gray('unlinked');
|
|
65
|
+
}
|
|
66
|
+
function formatRowLabel(row) {
|
|
67
|
+
// Pad the raw text first, THEN colorize — padEnd on an already-chalked
|
|
68
|
+
// string counts the ANSI escape bytes as width and misaligns the column.
|
|
69
|
+
const name = (row.kind === 'downloads' ? '[downloads]' : (row.task ?? '')).padEnd(30);
|
|
70
|
+
const coloredName = row.kind === 'downloads' ? chalk.gray(name) : name;
|
|
71
|
+
const age = formatRelativeTime(new Date(row.latestMtimeMs).toISOString());
|
|
72
|
+
const counts = formatCounts(row.counts);
|
|
73
|
+
return [
|
|
74
|
+
coloredName,
|
|
75
|
+
row.profile.padEnd(26),
|
|
76
|
+
age.padEnd(11),
|
|
77
|
+
counts.padEnd(18),
|
|
78
|
+
rowLinkSummary(row),
|
|
79
|
+
].join(' ');
|
|
80
|
+
}
|
|
81
|
+
const PREVIEW_ARTIFACT_LIMIT = 12;
|
|
82
|
+
function buildRowPreview(row) {
|
|
83
|
+
const parts = [];
|
|
84
|
+
if (row.kind === 'downloads') {
|
|
85
|
+
parts.push(chalk.bold(`${row.profile} — downloads`));
|
|
86
|
+
}
|
|
87
|
+
else if (row.linkStatus === 'linked' && row.linkedSession) {
|
|
88
|
+
parts.push(buildPreview(row.linkedSession));
|
|
89
|
+
}
|
|
90
|
+
else if (row.linkStatus === 'unresolved') {
|
|
91
|
+
parts.push(chalk.yellow(`owner ${row.owner ?? 'unknown'} — launch ${row.launchId} has no indexed session on this machine.`));
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
parts.push(chalk.gray(`Unlinked — no owning agent session is known for this task`
|
|
95
|
+
+ (row.owner ? ` (last known owner: ${row.owner}).` : '.')));
|
|
96
|
+
}
|
|
97
|
+
parts.push('');
|
|
98
|
+
parts.push(chalk.bold(`Captures (${row.artifacts.length})`));
|
|
99
|
+
for (const a of row.artifacts.slice(0, PREVIEW_ARTIFACT_LIMIT)) {
|
|
100
|
+
const age = formatRelativeTime(new Date(a.mtimeMs).toISOString());
|
|
101
|
+
parts.push(` ${age.padEnd(11)} ${a.name.padEnd(30)} ${formatBytes(a.bytes).padStart(8)}`);
|
|
102
|
+
}
|
|
103
|
+
const more = row.artifacts.length - PREVIEW_ARTIFACT_LIMIT;
|
|
104
|
+
if (more > 0)
|
|
105
|
+
parts.push(chalk.gray(` … (${more} more)`));
|
|
106
|
+
return parts.join('\n');
|
|
107
|
+
}
|
|
108
|
+
function formatArtifactLabel(a) {
|
|
109
|
+
const age = formatRelativeTime(new Date(a.mtimeMs).toISOString());
|
|
110
|
+
return `${age.padEnd(11)} ${a.name.padEnd(34)} ${formatBytes(a.bytes).padStart(8)}`;
|
|
111
|
+
}
|
|
112
|
+
/** Open one artifact in the OS default app, printing its path (matches the
|
|
113
|
+
* non-interactive `--open` behavior) and any open failure. */
|
|
114
|
+
function openAndReport(a) {
|
|
115
|
+
console.log(a.path);
|
|
116
|
+
if (!openArtifact(a.path))
|
|
117
|
+
console.error(`Could not open ${a.path}`);
|
|
118
|
+
}
|
|
119
|
+
/** Task-level picker with a captures preview, then an artifact drill-down when
|
|
120
|
+
* a task holds more than one capture. Loops until the user quits (esc) so a
|
|
121
|
+
* capture-heavy task can be browsed and opened repeatedly in one session. */
|
|
122
|
+
async function browseAndOpen(rows) {
|
|
123
|
+
for (;;) {
|
|
124
|
+
let picked;
|
|
125
|
+
try {
|
|
126
|
+
picked = await itemPicker({
|
|
127
|
+
message: 'Browser sessions:',
|
|
128
|
+
items: rows,
|
|
129
|
+
filter: (query) => (query.trim() ? rows.filter((r) => matchesBrowserSessionRow(r, query)) : rows),
|
|
130
|
+
labelFor: (row) => formatRowLabel(row),
|
|
131
|
+
buildPreview: (row) => buildRowPreview(row),
|
|
132
|
+
emptyMessage: 'No browser sessions match.',
|
|
133
|
+
enterHint: 'open capture',
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
if (isPromptCancelled(err))
|
|
138
|
+
return;
|
|
139
|
+
throw err;
|
|
140
|
+
}
|
|
141
|
+
if (!picked)
|
|
142
|
+
return;
|
|
143
|
+
const row = picked.item;
|
|
144
|
+
if (row.artifacts.length === 0) {
|
|
145
|
+
console.log('No captures in this task yet.');
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (row.artifacts.length === 1) {
|
|
149
|
+
openAndReport(row.artifacts[0]);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const artifact = await pickArtifact(row);
|
|
153
|
+
if (artifact)
|
|
154
|
+
openAndReport(artifact);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/** Second-level picker over one row's captures, newest first. Enter opens the
|
|
158
|
+
* highlighted capture; esc returns to the task list. */
|
|
159
|
+
async function pickArtifact(row) {
|
|
160
|
+
try {
|
|
161
|
+
const picked = await itemPicker({
|
|
162
|
+
message: `Captures — ${row.task ?? 'downloads'} (${row.profile}):`,
|
|
163
|
+
items: row.artifacts,
|
|
164
|
+
filter: (query) => {
|
|
165
|
+
const q = query.trim().toLowerCase();
|
|
166
|
+
return q ? row.artifacts.filter((a) => a.name.toLowerCase().includes(q)) : row.artifacts;
|
|
167
|
+
},
|
|
168
|
+
labelFor: (a) => formatArtifactLabel(a),
|
|
169
|
+
emptyMessage: 'No captures.',
|
|
170
|
+
enterHint: 'open',
|
|
171
|
+
});
|
|
172
|
+
return picked?.item ?? null;
|
|
173
|
+
}
|
|
174
|
+
catch (err) {
|
|
175
|
+
if (isPromptCancelled(err))
|
|
176
|
+
return null;
|
|
177
|
+
throw err;
|
|
178
|
+
}
|
|
179
|
+
}
|
package/dist/commands/browser.js
CHANGED
|
@@ -9,7 +9,7 @@ import { readAndResolveBundleEnv, bundleExists, readBundle, describeBundle } fro
|
|
|
9
9
|
import { findBrowserPath, getPortOccupant, isLauncherScript } from '../lib/browser/chrome.js';
|
|
10
10
|
import { listProfileCacheDirs, removeProfileCache, listAllProfileSnapshots, } from '../lib/browser/runtime-state.js';
|
|
11
11
|
import { DEFAULT_VIEWPORT } from '../lib/browser/devices.js';
|
|
12
|
-
import {
|
|
12
|
+
import { runBrowserSessionsCommand } from './browser-sessions-picker.js';
|
|
13
13
|
import { discoverBrowserWsUrl, verifyBrowserIdentity } from '../lib/browser/cdp.js';
|
|
14
14
|
import { parseTargetFilter } from '../lib/browser/service.js';
|
|
15
15
|
import { BrowserDaemonNotRunningError, formatBrowserDaemonNotRunningError, sendIPCRequest, } from '../lib/browser/ipc.js';
|
|
@@ -58,6 +58,7 @@ const BROWSER_HELP_GROUPS = [
|
|
|
58
58
|
title: 'Capture evidence',
|
|
59
59
|
names: ['console', 'errors', 'requests', 'responsebody', 'record', 'pdf', 'logs'],
|
|
60
60
|
},
|
|
61
|
+
{ title: 'History and discovery', names: ['sessions', 'history', 'refs'] },
|
|
61
62
|
];
|
|
62
63
|
export function registerBrowserCommand(program) {
|
|
63
64
|
const browser = program
|
|
@@ -87,6 +88,9 @@ export function registerBrowserCommand(program) {
|
|
|
87
88
|
# Drive another machine's browser (needs its consent — see remote-control)
|
|
88
89
|
agents browser start --host zion
|
|
89
90
|
|
|
91
|
+
# Browse a task-heavy profile's captures: one row per task, not per file
|
|
92
|
+
agents browser sessions
|
|
93
|
+
|
|
90
94
|
# Allow / deny other fleet machines driving THIS machine's browser
|
|
91
95
|
agents browser remote-control on
|
|
92
96
|
|
|
@@ -1967,12 +1971,13 @@ function registerTaskCommands(browser) {
|
|
|
1967
1971
|
});
|
|
1968
1972
|
browser
|
|
1969
1973
|
.command('sessions')
|
|
1970
|
-
.description('
|
|
1974
|
+
.description('Browse a profile\'s captured screenshots, PDFs, recordings, and downloads, grouped by task')
|
|
1971
1975
|
.option('--profile <name>', 'Only this profile (default: all profiles with captures)')
|
|
1972
1976
|
.option('--open [selector]', "Open a capture in the OS default app: 'latest' or a filename")
|
|
1973
1977
|
.option('--json', 'Emit machine-readable JSON')
|
|
1974
|
-
.
|
|
1975
|
-
|
|
1978
|
+
.option('--no-interactive', 'Print the flat listing instead of opening the interactive task browser')
|
|
1979
|
+
.action(async (opts) => {
|
|
1980
|
+
await runBrowserSessionsCommand({ profile: opts.profile, open: opts.open, json: opts.json, interactive: opts.interactive });
|
|
1976
1981
|
});
|
|
1977
1982
|
// ─── Recording ─────────────────────────────────────────────────────────────
|
|
1978
1983
|
const record = browser.command('record').description('Record a video of the page');
|
package/dist/commands/hosts.js
CHANGED
|
@@ -17,7 +17,7 @@ import { hostIdentityArgs, sshTargetFor } from '../lib/hosts/types.js';
|
|
|
17
17
|
import { listSshConfigHosts, listKnownHosts, isSshConfigHost } from '../lib/hosts/ssh-config.js';
|
|
18
18
|
import { probeHost, remoteAgentsVersion, bootstrapAgentsCli, localCliVersion, } from '../lib/hosts/ready.js';
|
|
19
19
|
import { resolveRemoteOsSync } from '../lib/hosts/remote-os.js';
|
|
20
|
-
import { listTasks, loadTask, findTaskByName, findTaskBySessionId } from '../lib/hosts/tasks.js';
|
|
20
|
+
import { listTasks, loadTask, findTaskByName, findTaskBySessionId, resolveTaskRef } from '../lib/hosts/tasks.js';
|
|
21
21
|
import { reconcileRunningTasks } from '../lib/hosts/reconcile.js';
|
|
22
22
|
import { showHostTaskLog } from '../lib/hosts/logs.js';
|
|
23
23
|
import { stopDispatchedTask } from '../lib/hosts/dispatch.js';
|
|
@@ -252,10 +252,6 @@ async function doLogs(ref, follow, full) {
|
|
|
252
252
|
if (res.exitCode !== undefined)
|
|
253
253
|
process.exitCode = res.exitCode;
|
|
254
254
|
}
|
|
255
|
-
/** Resolve a host-task ref (id, --name handle, or session id) or null. */
|
|
256
|
-
function resolveTaskRef(ref) {
|
|
257
|
-
return loadTask(ref) ?? findTaskByName(ref) ?? findTaskBySessionId(ref);
|
|
258
|
-
}
|
|
259
255
|
async function doStop(ref) {
|
|
260
256
|
// Heal first so we don't try to kill a process that already exited.
|
|
261
257
|
const current = resolveTaskRef(ref);
|