@phnx-labs/agents-cli 1.20.49 → 1.20.51
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 +31 -0
- package/README.md +3 -0
- package/dist/commands/browser-picker.js +1 -18
- package/dist/commands/cloud.js +1 -25
- package/dist/commands/computer.d.ts +1 -0
- package/dist/commands/computer.js +129 -8
- package/dist/commands/doctor.js +133 -2
- package/dist/commands/exec.js +49 -6
- package/dist/commands/factory.js +1 -4
- package/dist/commands/inspect.js +1 -11
- package/dist/commands/mcp.js +2 -6
- package/dist/commands/message.js +1 -4
- package/dist/commands/profiles.js +1 -18
- package/dist/commands/repo.js +33 -14
- package/dist/commands/resource-view.d.ts +1 -0
- package/dist/commands/resource-view.js +5 -17
- package/dist/commands/secrets.d.ts +1 -0
- package/dist/commands/secrets.js +1 -28
- package/dist/commands/sessions-picker.js +1 -18
- package/dist/commands/sessions.js +6 -8
- package/dist/commands/teams-picker.js +1 -32
- package/dist/commands/teams.js +218 -97
- package/dist/commands/tmux.js +1 -3
- package/dist/commands/view.js +1 -9
- package/dist/commands/worktree.js +1 -4
- package/dist/lib/agents.d.ts +0 -4
- package/dist/lib/agents.js +20 -33
- package/dist/lib/auto-dispatch-linear.d.ts +18 -0
- package/dist/lib/auto-dispatch-linear.js +107 -0
- package/dist/lib/auto-dispatch-provider.d.ts +10 -0
- package/dist/lib/auto-dispatch-provider.js +25 -0
- package/dist/lib/auto-dispatch.d.ts +87 -0
- package/dist/lib/auto-dispatch.js +142 -0
- package/dist/lib/browser/cdp.js +11 -2
- package/dist/lib/browser/drivers/ssh.d.ts +28 -10
- package/dist/lib/browser/drivers/ssh.js +57 -18
- package/dist/lib/browser/refs.js +1 -5
- package/dist/lib/cli-resources.d.ts +0 -2
- package/dist/lib/cli-resources.js +30 -13
- package/dist/lib/cloud/rush.d.ts +0 -24
- package/dist/lib/cloud/rush.js +0 -31
- package/dist/lib/crabbox/cli.js +4 -1
- package/dist/lib/crabbox/lease.js +29 -1
- package/dist/lib/daemon.js +41 -0
- package/dist/lib/exec.js +43 -18
- package/dist/lib/format.d.ts +38 -0
- package/dist/lib/format.js +108 -0
- package/dist/lib/git.d.ts +21 -0
- package/dist/lib/git.js +92 -0
- package/dist/lib/hooks/cache.d.ts +9 -2
- package/dist/lib/hooks/cache.js +220 -8
- package/dist/lib/hooks.js +17 -8
- package/dist/lib/hosts/passthrough.js +30 -1
- package/dist/lib/hosts/progress.d.ts +31 -0
- package/dist/lib/hosts/progress.js +35 -0
- package/dist/lib/hosts/remote-cmd.d.ts +1 -1
- package/dist/lib/hosts/remote-cmd.js +13 -3
- package/dist/lib/platform/exec.d.ts +4 -1
- package/dist/lib/platform/exec.js +8 -2
- package/dist/lib/resources.d.ts +0 -8
- package/dist/lib/resources.js +0 -10
- package/dist/lib/runner.js +10 -2
- 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/session/active.d.ts +11 -1
- package/dist/lib/session/active.js +3 -0
- package/dist/lib/session/db.d.ts +1 -4
- package/dist/lib/session/db.js +20 -25
- package/dist/lib/session/discover.d.ts +2 -2
- package/dist/lib/session/discover.js +61 -48
- package/dist/lib/session/parse.js +35 -34
- package/dist/lib/session/render.d.ts +7 -3
- package/dist/lib/session/render.js +15 -9
- package/dist/lib/session/state.d.ts +55 -0
- package/dist/lib/session/state.js +87 -10
- package/dist/lib/session/types.d.ts +9 -0
- package/dist/lib/shims.d.ts +25 -2
- package/dist/lib/shims.js +73 -8
- package/dist/lib/ssh-tunnel.d.ts +33 -2
- package/dist/lib/ssh-tunnel.js +94 -7
- package/dist/lib/staleness/types.d.ts +0 -1
- package/dist/lib/teams/agents.d.ts +108 -1
- package/dist/lib/teams/agents.js +511 -11
- package/dist/lib/teams/api.d.ts +7 -1
- package/dist/lib/teams/api.js +5 -2
- package/dist/lib/teams/registry.d.ts +17 -0
- package/dist/lib/teams/registry.js +2 -0
- package/dist/lib/teams/remoteWorktree.d.ts +57 -0
- package/dist/lib/teams/remoteWorktree.js +213 -0
- package/dist/lib/teams/scheduler.d.ts +29 -0
- package/dist/lib/teams/scheduler.js +78 -0
- package/dist/lib/teams/supervisor.js +7 -0
- package/dist/lib/types.d.ts +14 -1
- package/dist/lib/versions.d.ts +7 -26
- package/dist/lib/versions.js +44 -146
- package/dist/lib/warn-unpushed.d.ts +40 -0
- package/dist/lib/warn-unpushed.js +128 -0
- package/package.json +3 -1
- package/dist/lib/resources/index.d.ts +0 -53
- package/dist/lib/resources/index.js +0 -76
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,37 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 1.20.51
|
|
6
|
+
|
|
7
|
+
- **Fix: `agents run --lease` bootstraps a fresh crabbox image and no longer leaks the box after the run.** Three failures compounded on a stock Hetzner lease (Ubuntu 24.04, no node preinstalled): (1) the bootstrap's `npm install -g @phnx-labs/agents-cli` ran with no node/npm on the box and swallowed the failure with `|| true`, so every run died deep in the script with `agents: command not found` (exit 127) and no hint why; (2) even with the CLI installed, a fresh install refuses `agents run` with "agents-cli is not set up" until `agents setup` has run; (3) teardown called `crabbox stop --id <slug>`, but crabbox's `stop` takes a positional target (unlike `status`/`run`/`ssh`) and died with `flag provided but not defined: -id` — silently, because `crabboxStop` is best-effort — so every one-shot lease box was **kept, billed, and left carrying the run's working data** until someone noticed (`Box … kept` instead of destroyed). The bootstrap now: exports `~/.local/bin` onto PATH, installs node user-level from the official `latest-v22.x` tarball when missing (arch-aware, satisfies `engines.node >=22.5.0`, no sudo needed), points the npm prefix at `~/.local`, fails loud with exit 96 and a diagnostic when the CLI still isn't runnable, and runs `agents setup` behind the same `[ ! -d ~/.agents/.system ]` first-run guard the hosts bootstrap uses; `crabboxStop` passes the slug positionally. Verified live on a fresh Hetzner cpx62 by the run's own progression across builds: the pre-fix lease exited 127 (`agents: command not found`) with `Box … kept`; after the node/npm fix it reached `agents-cli is not set up`; after the setup fix it reached the agent's login check (`Not logged in`); and every post-fix run ends with `Box <slug> destroyed.` instead of leaking. Source: `apps/cli/src/lib/crabbox/lease.ts` (`ENSURE_AGENTS_CLI`, `buildBootstrapScript`), `apps/cli/src/lib/crabbox/cli.ts` (`crabboxStop`). Known follow-up: leasing a **Claude** runtime from a Mac whose Claude Code credential lives in the login Keychain (the default install, and any agents-cli managed home — service name is hash-suffixed) still lands "Not logged in" on the box, because the picker copies `~/.claude.json` (config/state) but not the OAuth token, and extracting the token from the Keychain needs an interactive ACL approval; tracked separately.
|
|
8
|
+
- **`agents repo pull user <git-url>` now git-backs a plain `~/.agents` instead of silently skipping it — fixing config sync on Windows/fresh machines.** Setup only ever git-clones the *system* repo (`~/.agents/.system/`); the user repo is created as a bare directory (`state.ts ensureAgentsDir`), so `~/.agents` is git-backed only where it was cloned by hand as a dotfiles step. On a box where that never happened (a fresh install, or Windows), `agents repo pull` just printed `user: not a git repo, skipping` and the machine silently fell out of config sync — no `rules/`, no `agents sync` of shared resources. Now, passing your config remote once — `agents repo pull user git@github.com:you/.agents.git` — **adopts** the existing directory in place: it clones your remote and moves the `.git` in without deleting anything, materializes the tracked resources it was missing, and **backs up any locally-modified tracked file** (e.g. a machine-specific `agents.yaml`) to a sibling `~/.agents.pre-adopt-backup/` before overwriting it. Untracked runtime state (`.cache/`, `.history/`, `.system/` — all gitignored) is never touched. Every subsequent `agents repo pull` / `agents sync` is plain (the remote is now `origin`). No new command; the URL is only needed the first time. SSH transport is preserved (a `git@…` URL clones over SSH, not a rewritten https that would hang on a private-repo credential prompt), and git never prompts (`GIT_TERMINAL_PROMPT=0`). Source: `apps/cli/src/lib/git.ts` (`adoptRepo`), `apps/cli/src/commands/repo.ts`.
|
|
9
|
+
- **`agents run` now warns when a headless run leaves committed-but-unpushed work, instead of stranding it silently.** A headless `agents run` in a writable mode (`edit`/`skip`/`auto`) could end with the agent having committed on a branch but never pushed it — the run's exit path did no git work, so those commits sat invisible in a worktree until someone audited the box (exactly how a batch dispatch loop can quietly lose a verified fix). After a non-interactive, writable run the CLI now inspects the cwd for commits on the current branch that haven't reached any remote (`git log HEAD --not --remotes`, correct even when no upstream is set — work already on an `origin/*` ref is not flagged) and prints a loud stderr warning naming the branch, the unpushed commits, and the exact `git push` / `gh pr create` commands. Advisory only: it never pushes, never mutates the repo, and never throws (a 5s git timeout plus full error-swallowing guarantee it can't delay or break the run's exit). The check is wired into every headless exit path — single run, `--loop`, `--acp`, `--resume-checkpoint`, and the crash/catch path — and gated by `shouldWarnUnpushed(mode, interactive)` so it stays silent for interactive runs (the human sees their shell) and read-only `plan` mode. Source: `apps/cli/src/lib/warn-unpushed.ts`, `apps/cli/src/commands/exec.ts`. (#868)
|
|
10
|
+
- **Codex mode flags now match what the mode names promise — only `--mode skip` is yolo.** `--mode edit` used to append `--dangerously-bypass-approvals-and-sandbox` (Codex's `--yolo`) alongside `--sandbox workspace-write`, and the bypass flag wins — so "edit" silently ran Codex with **no sandbox and no approvals**, verified against codex 0.142.5's own session banner (`sandbox: danger-full-access`). And `--mode plan` mapped to `workspace-write` (writable!) because the template predated Codex's `read-only` sandbox. Now: `plan` → `--sandbox read-only`, `edit` → `--sandbox workspace-write -c sandbox_workspace_write.network_access=true` (sandboxed writes, network on so git/gh/installs keep working, no approval bypass), `skip` → `--dangerously-bypass-approvals-and-sandbox` (unchanged — skip IS the gnarly mode, equivalent to `codex --yolo`). Same fix in routine jobs (`runner.ts`) and in headless `codex exec resume`, which used to get the bypass for ANY non-plan resume — it now maps plan/edit through `-c sandbox_mode=…` and reserves the bypass for skip; interactive `codex resume` now carries the mode's sandbox flags instead of none. Verified live per mode against codex 0.142.5 session banners: skip = `approval: never / sandbox: danger-full-access`, edit = `sandbox: workspace-write (network access enabled)`, plan = `sandbox: read-only`. Source: `apps/cli/src/lib/exec.ts` (`AGENT_COMMANDS.codex`, resume block), `apps/cli/src/lib/runner.ts` (`buildJobCommand`).
|
|
11
|
+
- **`--add-dir` is now forwarded to Codex (it was silently dropped).** `agents teams` passes `--add-dir ~/.agents` so Codex teammates can run `agents teams add`, but `buildExecCommand` emitted `--add-dir` for Claude only — the grant never reached Codex, masked until now by edit mode's accidental sandbox bypass. Codex takes `--add-dir` natively (widens the workspace-write sandbox); it is now forwarded for fresh runs and skipped on resume (`codex exec resume` rejects it). Source: `apps/cli/src/lib/exec.ts`.
|
|
12
|
+
- **Fix: the documented `agents run <agent> [prompt] -- <native flags>` passthrough works again.** commander ≥13 rejects excess operands by default, so any post-`--` token (e.g. `agents run codex -- --yolo`) died with `too many arguments` before the run started. The run command now allows excess operands, re-derives the `--` boundary from argv (a post-`--` token can never be mis-parsed as the prompt — `agents run codex -- --yolo` launches the TUI with `--yolo`, it doesn't headless-run the "prompt" `--yolo`), and still errors, with a hint to quote the prompt, on excess operands NOT behind `--`. Verified live: `agents run codex "…" -- --yolo` forwards `--yolo` and codex reports `sandbox: danger-full-access`. Source: `apps/cli/src/commands/exec.ts`.
|
|
13
|
+
|
|
14
|
+
- **Fix: grok launch shims resolve the binary from the versioned home before the global `~/.grok/downloads`, so a pinned grok that installed into the versioned home no longer dies with "grok@<version> not installed."** Grok ships a native binary (not an npm package), and it lands in the versioned home's `.grok/downloads` whenever the installer runs with `GROK_HOME` set — via the shim, a correct `agents add grok`, or a grok self-update from within the shim. Both generated shims (the dispatcher in `generateShimScript` and the `grok@<version>` versioned alias in `generateVersionedAliasScript`) checked only `$HOME/.grok/downloads`, which was often empty, so they fell through to the "not installed" error even though the binary existed in the versioned home. `getBinaryPath` already checked the versioned home first, so `agents view` and the shims disagreed. Both shim blocks now check `$VERSION_DIR/home/.grok/downloads` first and fall back to the global `$HOME/.grok/downloads` for pre-fix installs, then the existing adopted-launcher/PATH last resort. Bumps `SHIM_SCHEMA_VERSION` 25→26 and `VERSIONED_ALIAS_SCHEMA_VERSION` 12→13 so existing on-disk grok shims regenerate. Supersedes the pre-monorepo #830. Source: `apps/cli/src/lib/shims.ts` (`generateShimScript` grok dispatcher block, `generateVersionedAliasScript` `binaryResolution`).
|
|
15
|
+
- **Fix: `browser stop --host <windows>` tree-kills the remote browser — relaunches never wedge on a stale `SingletonLock`.** The kill script used `Stop-Process` on the CDP port owner only; orphaned Chromium child processes survived, kept the profile's `SingletonLock` held, and the next `browser start --host` against the same profile exited immediately as a second instance. The script now uses `taskkill /PID <owner> /T /F` to take down the whole process tree. Source: `apps/cli/src/lib/browser/drivers/ssh.ts` (`buildWindowsKillScript`). (GitHub #561)
|
|
16
|
+
- **Fix: `agents browser start --host <windows>` actually serves CDP now — the remote browser launches in the user's interactive session instead of session 0.** The Windows launch used WMI `Win32_Process.Create` (chosen so the browser outlives the ssh session), but a WMI-created process lands in session 0, where Edge binds the debugging port yet its DevTools server never initializes — every `/json/version` probe hung forever and `DevToolsActivePort` was never written, so `browser start --host` failed with a connection error on every attempt. The launch is now a one-shot scheduled task registered and started by the logged-on user: it survives ssh disconnect the same way, runs in the interactive session where DevTools comes up normally, and is unregistered immediately after start. The launch args also gained the same automation-modal suppressors the local launcher has (`--no-first-run --no-default-browser-check --hide-crash-restore-bubble --disable-session-crashed-bubble`) — without them a relaunch against a previously hard-killed profile triggers session-restore churn that closes the CDP page target mid-command. Verified live against win-mini (Edg/150). Source: `apps/cli/src/lib/browser/drivers/ssh.ts` (`buildWindowsLaunchScript`). (GitHub #561)
|
|
17
|
+
- **Fix: remote CDP no longer dies on large payloads — screenshots of content-rich pages over `browser --host` work.** The CDP client rode the platform (undici) WebSocket, which enforces a non-configurable max decompressed message size; a `Page.captureScreenshot` response for a content-rich page blew past it and the socket closed with 1006 ("Max decompressed message size exceeded") while the command was pending, surfacing as "CDP connection closed". The websocket transport now uses the `ws` client (no permessage-deflate offer by default, explicit 256MB `maxPayload`); the local pipe transport is unchanged. Source: `apps/cli/src/lib/browser/cdp.ts`. (GitHub #561)
|
|
18
|
+
- **`agents sessions <id> --json` now exposes the ExitPlanMode plan markdown as a top-level field, and the shape changed from a bare event array to `{ session, events }`.** The session-state engine already detected plan-review (`awaitingReason: 'plan_review'`) off a trailing `ExitPlanMode` tool call, but the plan markdown itself was dropped on the floor — forcing every consumer that wanted it (the Factory NEEDS-YOU panel via `parsePlanFromClaudeJsonl`, external dashboards) to re-open the raw JSONL and scan for the same tool call. That "extension re-implements the session engine" gap now closes at the source: the state engine surfaces `state.plan` alongside `awaitingReason`, the Claude scanner captures the plan text at scan time and persists it to `sessions.db` (schema v11, additive, rescan-on-migrate), it's exposed as `plan` on `SessionMeta` in every `agents sessions --json` row, and `agents sessions <id> --json` now emits `{ session: SessionMeta, events: SessionEvent[] }` so the plan is one top-level `output.session.plan` read instead of a needle-in-haystack scan. Verified live against a real Claude session with an ExitPlanMode event: `agents sessions 74464df7 --json` prints the plan markdown at `.session.plan`. Source: `apps/cli/src/lib/session/{state,discover,db,render,types}.ts`, `apps/cli/src/lib/session/active.ts`, `apps/cli/src/commands/sessions.ts`. (issue #743 / RUSH-1505)
|
|
19
|
+
- **`agents computer` Windows parity: scoped screenshots, `get-text --max-chars`, `status`/`reload --host`, and honest `--background`/`--require-frontmost` handling (#548).** Four params the CLI already sent were silently ignored by the Windows daemon. (1) **Screenshots are now pid-scoped like macOS** — `screenshot --list` enumerates the target pid's top-level windows (`window_id` is the Win32 HWND, the same id `raise --window-id` takes), the default capture crops to the pid's largest on-screen window, `--window-id` shoots one window, and `--display` captures the display the app is on; previously every capture was the whole virtual desktop. Verified live on win-mini: window capture 2097x984/28KB vs full display 2560x1440/380KB. (2) **`get-text --max-chars` is honored** (default stays 20k, ceiling 200k like macOS) — `--max-chars 100` now returns exactly 100 chars. (3) **`status --host <device>` and `reload --host <device>`** — status reports the recorded tunnel plus a live daemon probe (previously it misreported macOS-local install state for a remote Windows daemon); reload restarts the daemon's scheduled task (the way to pick up a freshly pushed exe) and confirms it answers through the tunnel. (4) **`--require-frontmost` is enforced on Windows** — `SendInput` lands in the *focused* window, so `type-text`/`key` now report `frontmost` (feeding the existing CLI warning) and the flag hard-fails with `not_frontmost` when the target isn't foreground; **`--background` is rejected** with `action_unsupported` instead of silently no-oping (macOS postToPid delivery has no Win32 analogue — element-mode clicks via UIA patterns are the focus-safe path). Source: `native/computer-win/{Screenshot,Automation}.cs`, `apps/cli/src/commands/computer.ts`, `apps/cli/docs/computer.md`.
|
|
20
|
+
- **The `never` prompt-policy is now live — the signed keychain helper was rebuilt, re-notarized, and re-pinned.** `agents secrets create --policy never --i-understand` stores bundle values with no biometry ACL (`kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly`, device-local, non-synchronizable) so headless automation can read them with zero Touch ID prompts. The Swift `set-no-acl` path shipped in #682 but the pinned helper binary predated it, so the policy failed against the shipped helper; the helper is rebuilt from current source, notarized (Apple submission `a2373c91-7fc2-4894-a801-b37c111597aa`, status Accepted, stapled, Gatekeeper `Notarized Developer ID`), and `scripts/Agents CLI.app.sha256` re-pinned to the new binary. (GitHub #421)
|
|
21
|
+
- **Consolidated ~30 copy-pasted terminal-formatting helpers into one shared `apps/cli/src/lib/format.ts`, fixing three user-visible drifts at the source.** `die`, `truncate`, `relTime`, `humanDuration`, `visibleWidth`, `padRight`/`padVisible`, `isJsonMode`, `readStdinSync`, and `termLink` had drifted into per-command copies with different behavior; every consumer now imports the single canonical version. Three normalizations are user-visible: (1) the truncation ellipsis is now the single glyph `…` everywhere — `agents cloud` task lists, `agents sessions` overflow, and session prompt/tool summaries previously showed ASCII `...` or a bare `.`; (2) `agents cloud` relative timestamps switch from the long "5 minutes ago" form to the compact "5m ago" form already used by `agents teams`; (3) the `agents teams` picker duration cell gains a space ("2h5m" → "2h 5m") to match the sessions/browser pickers. It also fixes a latent bug: `agents repo`'s divergence-table column alignment used a `visibleWidth` regex missing its `\x1b` escape, so ANSI-colored cells were mis-measured and columns could misalign — the canonical `visibleWidth` strips the full SGR sequence. `lib/events.ts`'s `truncate` (a distinct nullable, exported helper that truncates persisted event payloads) and the domain-specific `statusColor` copies (different status vocabularies with conflicting color assignments) are deliberately left in place. Internal refactor plus the noted string normalizations; no other behavior change. Source: `apps/cli/src/lib/format.ts` and consumers across `apps/cli/src/commands/` and `apps/cli/src/lib/`. (GitHub #753 / RUSH-1515)
|
|
22
|
+
- **`agents computer setup --host` now works from a plain `npm i -g` install — the Windows helper exe downloads on demand from GitHub releases.** The ~157MB `computer-helper-win.exe` never shipped in the npm tarball, so setup died with "Windows helper exe not built. Run: bash scripts/build-win.sh" for anyone without a repo checkout. On `v*` tags the `computer-helper-win.yml` workflow now builds the self-contained exe, smoke-tests it, and uploads it plus a `.sha256` as GitHub release assets; `setup --host` resolves a local build first, then downloads the asset for the exact running CLI version, verifies its sha256 against the published checksum, and caches it under `~/.agents/.cache/computer/win-helper/v<version>/`. A tag with no asset is a hard error naming that tag — never a silent fallback to a different release. (GitHub #547) Source: `apps/cli/src/lib/ssh-tunnel.ts` (`ensureWinHelperExe`, `downloadWinHelperExe`), `.github/workflows/computer-helper-win.yml` (`release-exe`).
|
|
23
|
+
- **`registerMcp` HTTP transport now routes through the capability table instead of an inline agent-id allowlist.** MCP-over-HTTP support and MCP-header support were gated by hardcoded `agentId !== 'claude' && agentId !== 'codex' && agentId !== 'gemini'` / `agentId !== 'claude'` checks in `apps/cli/src/lib/agents.ts`, bypassing the `capabilities.ts`/`supports()` table that is the single source of truth for "which agent supports what." A newly-added agent would silently get the wrong HTTP-MCP behavior with no compile-time signal. Two new capabilities land on the `AgentConfig` matrix — `mcpHttp` (Claude/Codex/Gemini today) and `mcpHeaders` (Claude only) — and both inline allowlists are replaced with `supports(agentId, cap)` calls. Pure refactor: `capableAgents('mcpHttp')` is `['claude','codex','gemini']` and `capableAgents('mcpHeaders')` is `['claude']`, matching the pre-change behavior exactly. Source: `apps/cli/src/lib/{agents,capabilities,types}.ts`, `apps/cli/src/lib/{agents,capabilities}.test.ts`, `apps/cli/src/lib/__tests__/capabilities.test.ts`. (issue #742 / RUSH-1504)
|
|
24
|
+
- **Hook `matches:` predicates are now enforced at fire time — the documented gating was inert.** A hook manifest entry could declare `matches:` predicates (`prompt_contains` / `prompt_matches` / `tool_name` / `tool_args_match` / `cwd_includes` / `project_has` / `git_dirty`) to gate when it fires, and the docs described the gate ("all predicates AND together; an empty block always fires"), but `shouldFire()` (the evaluator in `src/lib/hooks/match.ts`) had **zero runtime callers**: the agent execs the registered command directly and nothing evaluated `matches:`, so any hook with a `matches:` block fired unconditionally. A hook that declares `matches:` (with or without `cache:`) is now registered as a generated wrapper shim that evaluates the predicates against the event JSON on stdin before running the script — a non-matching event exits 0 without running the hook body (logged as `cache:"skip"`), a matching event runs it. Matches-only hooks (no `cache:`) get a gate-only pass-through shim; cached hooks apply the gate before the cache. The shim gate is a faithful port of `shouldFire()` (same AND semantics, same ReDoS guard) and is pinned to it by a 20-case conformance test so the two can't drift. Gating is fail-open: a garbled predicate runs the hook rather than silently disabling a safety hook (e.g. `git-guard`). No installed or bundled hook currently declares `matches:`, so this changes no existing hook's behavior — it activates a documented feature for authors who add one. Verified end-to-end by generating a shim from a `matches: { tool_name: Bash, tool_args_match: "rm -rf" }` manifest and firing it: a `Read` event and a `Bash`+`ls` event were skipped, a `Bash`+`rm -rf` event ran the body. Source: `apps/cli/src/lib/hooks/cache.ts` (`renderShim` gate + pass-through tail), `apps/cli/src/lib/hooks.ts` (`resolveHookCommand`), `apps/cli/docs/hooks.md`. (RUSH-1506)
|
|
25
|
+
- **Browser-over-SSH no longer hangs on an unreachable remote host — it fails fast (~10s).** The raw-`ssh` spawns in the browser SSH driver (`ensureRemoteBrowser`, `runSSHCommand`) passed only `-o BatchMode=yes`, with no `ConnectTimeout`, so a dropped SYN to a down host stalled on the OS default TCP timeout (~127s) instead of erroring. Both call sites now compose the shared hardened baseline `SSH_OPTS` from `ssh-exec.ts` (`BatchMode` + `ConnectTimeout=10` + `ServerAlive` keepalive) rather than re-listing options — the same baseline `sshExec` and the `-L` tunnel already use. The options now also precede the target (matching `sshExec`); on macOS/BSD `getopt` an option placed after the target is swallowed into the remote command instead of applied. Verified against TEST-NET `203.0.113.1` (guaranteed unreachable): `-o BatchMode=yes` alone was still hanging at a 30s cap (en route to ~127s), while the `SSH_OPTS` set failed in `10.04s` with `connect to host 203.0.113.1 port 22: Connection timed out`. Source: `apps/cli/src/lib/browser/drivers/ssh.ts`. (RUSH-1508)
|
|
26
|
+
- **Fix: OpenCode sessions now load on Windows.** Reading OpenCode sessions shelled out to the `sqlite3` CLI at three call sites — `parseOpenCode` (transcript parse) plus the session scan and active-account lookup in discovery — and that binary is absent on Windows, so OpenCode sessions silently never appeared in `agents sessions` there. All three now read through the same runtime-aware node/bun `Database` wrapper the Antigravity parser already uses (`bun:sqlite`/`node:sqlite`, no native addon, no CLI), and the OpenCode transcript query binds the session id as a parameter instead of interpolating it. No behavior change on macOS/Linux. Source: `apps/cli/src/lib/session/parse.ts` (`parseOpenCode`), `apps/cli/src/lib/session/discover.ts` (`scanOpenCodeIncremental`, `getOpenCodeAccount`). (RUSH-1513)
|
|
27
|
+
- **`agents sessions --active --json` now carries the agent's actual decision, not a truncated status line.** A session waiting on you used to collapse everything to a one-line `preview` — an `AskUserQuestion` became the generic `"Asked you a question"` (throwing away the options that are already in the tool input), and a trailing thinking block masked the real turn as `"thinking…"`. The state engine now emits a structured `question` object (`{ text, reason, options: [{ label, description, key }] }`) for every waiting path — `AskUserQuestion` (with each option's 1-based select key), plan review, permission (Approve=`1` / Deny=`esc`), and a trailing prose question — plus a short assistant `tail` for context, and `preview` no longer degrades to `"thinking…"` when a real turn exists. Every consumer (the Factory NEEDS-YOU panel, teams, cloud) now gets the real "what does it want from me" instead of re-deriving it from prose. Verified live: the blocked session in the screenshot now reports `awaitingReason: question` with the real question text. Source: `apps/cli/src/lib/session/state.ts` (`structuredQuestionFromAsk`, `inferActivity`), `apps/cli/src/lib/session/active.ts`. (RUSH-453)
|
|
28
|
+
|
|
29
|
+
## 1.20.50
|
|
30
|
+
|
|
31
|
+
- **Distributed agent teams: teammates can now run on different machines across your fleet, not just the box running `teams start`.** A single team can place the backend teammate on a Linux box and the UI teammate on a Mac while one orchestrator still drives the DAG, polls status, and cleans up. One vocabulary, all optional (omit it and teams stay 100% local as before): `teams create --devices a,b,c` (alias `--hosts`) declares a pool the team may auto-schedule onto, `--repo <url|path>` (defaults to the local checkout's `origin`) says how each device gets the code, and `teams add --device X` (alias `--host`) pins one teammate to a host — which needs **no** pool, so "send just one teammate elsewhere" is zero-setup. Placement resolves top-down at launch: explicit `--device` pin → single-device pool (whole team there) → multi-device pool (least-loaded auto-schedule) → local. Remote teammates dispatch over SSH via the existing `agents devices`/host machinery (a third teammate backend beside local and cloud), are monitored by offset-tailing the remote log + `.exit` sentinel, and get the repo auto-provisioned per device (reuse an existing checkout, else clone into `~/.agents/repos/<team>`) with an optional per-teammate git worktree on the host. `teams status`/`teams logs` show each teammate's host and stream its output back with the local mirror capped (~512KB rolling tail) so a 10+-teammate fleet can't blow up the orchestrator. POSIX hosts only in v1 (Windows rejected with a clear message). Source: `apps/cli/src/lib/teams/{scheduler,remoteWorktree,agents,api,supervisor,registry}.ts`, `apps/cli/src/lib/hosts/{progress,passthrough}.ts`, `apps/cli/src/commands/teams.ts`, `apps/cli/docs/teams.md`.
|
|
32
|
+
- **NEW: `agents doctor --devices` shows a cross-device agent-readiness matrix.** `agents doctor` could already run on one remote machine via `--host`, but checking the whole fleet meant running the command once per box. `--devices` fans out `agents teams doctor --json` to every registered device (plus the local machine), renders a device × agent matrix, and emits a stable JSON contract with `--json`. `--device <name>` or `--host <name>` scopes the same matrix to a single machine. The remote probe now bootstraps `PATH` with the canonical shim directories before running, so login shells that haven't sourced interactive rc files no longer report false "not installed" negatives. Source: `apps/cli/src/commands/doctor.ts`, `apps/cli/src/lib/teams/agents.ts`, `apps/cli/src/lib/hosts/{passthrough,remote-cmd}.ts`.
|
|
33
|
+
- **`agents run codex` / `agents teams` now honor your configured Codex model instead of silently defaulting to `gpt-5.3-codex`.** Codex runs under a per-version `CODEX_HOME`, and your `model` preference (`~/.codex/config.toml`) lives only in the version-home that was active when you set it. A dispatch pinned to a different version read a home with no top-level `model`, so Codex fell back to its built-in default — which a ChatGPT-tier account isn't entitled to use, so the run died with `400: The 'gpt-5.3-codex' model is not supported when using Codex with a ChatGPT account` before doing any work, even though `ag view` reported Codex "signed in". When no explicit `--model` is passed, the model is now defaulted (for Codex) to the top-level `model` in your active `~/.codex/config.toml` and forwarded via `--model`; it's read-only (no file writes), so fanning out many parallel runs to one version-home can't race. Verified live on a box where Codex was 100% unusable: the request model changed `gpt-5.3-codex` → `gpt-5.5` and codex@0.142.0 returned successfully. Source: `apps/cli/src/lib/exec.ts`, `apps/cli/src/lib/shims.ts` (`readCodexConfiguredModel`).
|
|
34
|
+
- **Fix: `agents add claude@<version>` now produces a runnable install — it no longer ships a half-built binary that dies with "claude native binary not installed."** `installVersion` runs `npm install --ignore-scripts` (the right posture for the dependency *tree* — never run arbitrary transitive postinstalls), but that also skipped the agent package's OWN postinstall, which for `@anthropic-ai/claude-code` is a required step: the package ships a ~500-byte stub at `bin/claude.exe` plus per-arch native binaries as optional deps, and its `postinstall` (`node install.cjs`) is what copies the correct ~231 MB native binary over the stub. Skipped, every launch died with `Error: claude native binary not installed`. The existing launch-health self-heal (#764, and its Windows/daemon extension) couldn't save it on two counts: the stub reports its breakage *politely* rather than with a raw `ENOENT`, so the probe's missing-binary signature didn't match and the gutted install read as healthy; and the repair path (`ensureAgentRunnable` → clean reinstall) re-ran the same `--ignore-scripts` install, so it never copied the binary either. `installVersion` now runs the **first-party** package's declared `postinstall` after the npm install (scoped to that one package — never the dependency tree, never claude-code's `exit 1` `prepare` guard), best-effort, before the integrity gate. Because `installVersion` is the single choke point for `agents add`, config refresh, run-time heal, and the daemon's proactive heal, this also revives the repair path for the whole class. `isMissingBinarySignature` was additionally widened to recognize the stub's polite phrases (`native binary not installed`, `postinstall did not run`, `optional dependency was not downloaded`) so the self-heal catches this failure mode if a postinstall ever silently no-ops. Verified end-to-end on linux-arm64: `installVersion('claude','2.1.186')` into a clean HOME runs the postinstall automatically, lands the 231,782,112-byte binary (not the stub), and `claude.exe --version` returns `2.1.186 (Claude Code)` — with no manual `install.cjs` step. Source: `apps/cli/src/lib/versions.ts` (`installVersion`, `isMissingBinarySignature`).
|
|
35
|
+
|
|
5
36
|
## 1.20.49
|
|
6
37
|
|
|
7
38
|
- **`agents run --mode plan` no longer hard-fails on agents without a read-only mode (antigravity, cursor, kiro, …).** Those agents have no plan flag, so an explicit or default `--mode plan` used to abort with `does not support 'plan' mode` — breaking multi-agent scripts that pass a uniform plan flag, and diverging from `agents teams add` (default mode `edit`). `resolveMode` now degrades unsupported `plan` to the agent's safest native mode (`capabilities.modes[0]`, typically `edit`), matching the existing `auto` → `edit` degrade. The CLI prints a yellow warning when the user explicitly asked for plan (gray for the implicit default) so the elevation is never silent. `skip` still hard-fails when unsupported. Source: `apps/cli/src/lib/exec.ts`, `apps/cli/src/commands/exec.ts`.
|
package/README.md
CHANGED
|
@@ -307,6 +307,9 @@ agents logs <id> --full # the full raw transcript / stdout (toke
|
|
|
307
307
|
agents logs <id> -f # re-attach to a running one and follow
|
|
308
308
|
agents view claude --host gpu-box # inspect the remote install
|
|
309
309
|
agents sync --host gpu-box # make the remote machine current
|
|
310
|
+
agents doctor --devices # readiness matrix for every registered device
|
|
311
|
+
agents doctor --devices --json # machine-readable fleet readiness
|
|
312
|
+
agents doctor --device mac-mini # same matrix, scoped to one device
|
|
310
313
|
|
|
311
314
|
# Your Tailscale fleet, auto-discovered
|
|
312
315
|
agents devices sync # ingest `tailscale status`
|
|
@@ -5,29 +5,12 @@
|
|
|
5
5
|
* Shows task name, profile, domains, and tab count; preview pane shows full tab list.
|
|
6
6
|
*/
|
|
7
7
|
import chalk from 'chalk';
|
|
8
|
+
import { truncate, humanDuration } from '../lib/format.js';
|
|
8
9
|
import { itemPicker } from '../lib/picker.js';
|
|
9
10
|
const DOT = chalk.gray(' · ');
|
|
10
|
-
function humanDuration(ms) {
|
|
11
|
-
const s = Math.floor(ms / 1000);
|
|
12
|
-
if (s < 60)
|
|
13
|
-
return `${s}s`;
|
|
14
|
-
const m = Math.floor(s / 60);
|
|
15
|
-
if (m < 60)
|
|
16
|
-
return `${m}m`;
|
|
17
|
-
const h = Math.floor(m / 60);
|
|
18
|
-
const mm = m % 60;
|
|
19
|
-
if (h < 24)
|
|
20
|
-
return mm ? `${h}h ${mm}m` : `${h}h`;
|
|
21
|
-
const d = Math.floor(h / 24);
|
|
22
|
-
const hh = h % 24;
|
|
23
|
-
return hh ? `${d}d ${hh}h` : `${d}d`;
|
|
24
|
-
}
|
|
25
11
|
function formatAge(ms) {
|
|
26
12
|
return humanDuration(Date.now() - ms) + ' ago';
|
|
27
13
|
}
|
|
28
|
-
function truncate(s, max) {
|
|
29
|
-
return s.length > max ? s.slice(0, max - 1) + '…' : s;
|
|
30
|
-
}
|
|
31
14
|
/** Build the preview pane for a browser task. */
|
|
32
15
|
export function buildBrowserPreview(item) {
|
|
33
16
|
const { task, profile } = item;
|
package/dist/commands/cloud.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
+
import { die, relTime, truncate, isJsonMode } from '../lib/format.js';
|
|
2
3
|
import * as fs from 'fs';
|
|
3
4
|
import * as path from 'path';
|
|
4
5
|
import ora from 'ora';
|
|
@@ -35,28 +36,6 @@ function parseSkillRef(raw) {
|
|
|
35
36
|
return { id: raw };
|
|
36
37
|
}
|
|
37
38
|
/** Print an error message to stderr and exit. */
|
|
38
|
-
function die(msg, code = 1) {
|
|
39
|
-
console.error(chalk.red(msg));
|
|
40
|
-
process.exit(code);
|
|
41
|
-
}
|
|
42
|
-
/** Format an ISO timestamp as a human-readable relative time string. */
|
|
43
|
-
function relTime(iso) {
|
|
44
|
-
const secs = Math.floor((Date.now() - new Date(iso).getTime()) / 1000);
|
|
45
|
-
if (secs < 10)
|
|
46
|
-
return 'just now';
|
|
47
|
-
if (secs < 60)
|
|
48
|
-
return `${secs}s ago`;
|
|
49
|
-
if (secs < 3600)
|
|
50
|
-
return `${Math.floor(secs / 60)} minutes ago`;
|
|
51
|
-
if (secs < 86400)
|
|
52
|
-
return `${Math.floor(secs / 3600)} hours ago`;
|
|
53
|
-
return `${Math.floor(secs / 86400)} days ago`;
|
|
54
|
-
}
|
|
55
|
-
function truncate(s, n) {
|
|
56
|
-
if (s.length <= n)
|
|
57
|
-
return s;
|
|
58
|
-
return s.slice(0, n - 1) + '...';
|
|
59
|
-
}
|
|
60
39
|
/** Return a chalk color function appropriate for the given task status. */
|
|
61
40
|
function statusColor(status) {
|
|
62
41
|
switch (status) {
|
|
@@ -70,9 +49,6 @@ function statusColor(status) {
|
|
|
70
49
|
default: return chalk.white;
|
|
71
50
|
}
|
|
72
51
|
}
|
|
73
|
-
function isJsonMode(opts) {
|
|
74
|
-
return Boolean(opts.json) || !process.stdout.isTTY;
|
|
75
|
-
}
|
|
76
52
|
/**
|
|
77
53
|
* After a `MissingTargetError`, try to resolve the target interactively.
|
|
78
54
|
* Returns the chosen id, or undefined when no interactive resolution is
|
|
@@ -37,5 +37,6 @@ export declare function reconcileScreenshotExt(outPath: string, buf: Buffer): {
|
|
|
37
37
|
};
|
|
38
38
|
export declare function registerComputerCommand(program: Command): void;
|
|
39
39
|
export declare function registerComputerSubcommands(program: Command): void;
|
|
40
|
+
export declare function buildRestartTaskScript(taskName: string, exeName: string): string;
|
|
40
41
|
export { resolveHelperExec as resolveHelperPath };
|
|
41
42
|
export { resolveSocketPath };
|
|
@@ -4,7 +4,9 @@ import * as os from 'os';
|
|
|
4
4
|
import * as path from 'path';
|
|
5
5
|
import { registerCommandGroups } from '../lib/help.js';
|
|
6
6
|
import { openComputerClient, resolveHelperApp, resolveHelperExec, resolveSocketPath, resolveLogPath, resolvePolicyPath, resolvePeersPath, resolveTcpEndpoint, loadComputerAllowList, loadDefaultPeers, writeComputerPolicy, writeComputerPeers, } from '../lib/computer-rpc.js';
|
|
7
|
-
import { setupRemoteHelper, startRemoteTunnel, stopRemoteHelper, hydrateRemoteEnvFromState, } from '../lib/ssh-tunnel.js';
|
|
7
|
+
import { setupRemoteHelper, startRemoteTunnel, stopRemoteHelper, hydrateRemoteEnvFromState, readRemoteState, resolveRemoteDevice, REMOTE_TASK_NAME, WIN_HELPER_EXE, } from '../lib/ssh-tunnel.js';
|
|
8
|
+
import { sshExec } from '../lib/ssh-exec.js';
|
|
9
|
+
import { encodePowershell } from '../lib/hosts/remote-cmd.js';
|
|
8
10
|
import { registerActionCommands, withClient, unwrap, pickTarget } from './computer-actions.js';
|
|
9
11
|
import { runComputerLoop } from '../lib/computer/loop.js';
|
|
10
12
|
import { makeVerbDispatcher } from '../lib/computer/dispatch.js';
|
|
@@ -18,9 +20,10 @@ const COMPUTER_HELP_GROUPS = [
|
|
|
18
20
|
{ title: 'Interact', names: ['launch', 'raise', 'click', 'right-click', 'type', 'type-text', 'key', 'drag', 'scroll', 'ax-action', 'focus', 'wait'] },
|
|
19
21
|
];
|
|
20
22
|
// Subcommands that manage the `--host` remote path themselves (provisioning /
|
|
21
|
-
// tunnel lifecycle
|
|
22
|
-
//
|
|
23
|
-
|
|
23
|
+
// tunnel lifecycle, or daemon-state reporting that must degrade gracefully
|
|
24
|
+
// when no tunnel is recorded). Every other `--host`-bearing subcommand is a
|
|
25
|
+
// plain verb that just needs the TCP endpoint hydrated before it runs.
|
|
26
|
+
const REMOTE_LIFECYCLE = new Set(['setup', 'start', 'stop', 'status', 'reload']);
|
|
24
27
|
/**
|
|
25
28
|
* Pure platform gate. The computer subsystem is macOS-only for LOCAL driving
|
|
26
29
|
* (Accessibility / launchctl). It is NOT blocked off macOS when a remote daemon
|
|
@@ -111,8 +114,13 @@ export function registerComputerSubcommands(program) {
|
|
|
111
114
|
function registerStatusCommand(program) {
|
|
112
115
|
program
|
|
113
116
|
.command('status')
|
|
114
|
-
.description('Report install state, daemon state, and Accessibility trust')
|
|
115
|
-
.
|
|
117
|
+
.description('Report install state, daemon state, and Accessibility trust — or a remote Windows daemon with --host')
|
|
118
|
+
.option('--host <device>', 'Report the remote Windows daemon (tunnel + liveness) instead of the local helper')
|
|
119
|
+
.action(async (opts) => {
|
|
120
|
+
if (opts.host) {
|
|
121
|
+
await reportRemoteStatus(opts.host);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
116
124
|
const socketPath = resolveSocketPath();
|
|
117
125
|
const installed = fs.existsSync(HELPER_APP_DEST);
|
|
118
126
|
const socketUp = fs.existsSync(socketPath);
|
|
@@ -159,6 +167,108 @@ function registerStatusCommand(program) {
|
|
|
159
167
|
}
|
|
160
168
|
});
|
|
161
169
|
}
|
|
170
|
+
// status --host: the local checks (app install, launchd socket, policy files)
|
|
171
|
+
// are macOS concepts — a remote Windows daemon is reported from what actually
|
|
172
|
+
// exists for it: the recorded tunnel and a live trust_status probe through it.
|
|
173
|
+
async function reportRemoteStatus(host) {
|
|
174
|
+
console.log(`host: ${host}`);
|
|
175
|
+
const state = readRemoteState(host);
|
|
176
|
+
if (!state) {
|
|
177
|
+
console.log('tunnel: none');
|
|
178
|
+
console.log('daemon: unknown (no tunnel to probe through)');
|
|
179
|
+
console.log('');
|
|
180
|
+
console.log(`Run: agents computer start --host ${host}`);
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
183
|
+
console.log(`tunnel: 127.0.0.1:${state.localPort} -> ${state.target} (127.0.0.1:${state.remotePort})`);
|
|
184
|
+
hydrateRemoteEnvFromState(host);
|
|
185
|
+
try {
|
|
186
|
+
const client = openComputerClient();
|
|
187
|
+
try {
|
|
188
|
+
const r = await client.call('trust_status');
|
|
189
|
+
if (r.error) {
|
|
190
|
+
console.error(`error: ${r.error.code}: ${r.error.message}`);
|
|
191
|
+
process.exit(1);
|
|
192
|
+
}
|
|
193
|
+
console.log('daemon: running');
|
|
194
|
+
console.log(`trust: ${r.result?.trusted ? 'granted' : 'denied'} (Windows UIAutomation needs no per-app grant)`);
|
|
195
|
+
if (typeof r.result?.pid === 'number')
|
|
196
|
+
console.log(`pid: ${r.result.pid}`);
|
|
197
|
+
if (typeof r.result?.path === 'string' && r.result.path)
|
|
198
|
+
console.log(`exe: ${r.result.path}`);
|
|
199
|
+
}
|
|
200
|
+
finally {
|
|
201
|
+
await client.close();
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
catch (err) {
|
|
205
|
+
console.log('daemon: unreachable');
|
|
206
|
+
console.log(` ${err.message}`);
|
|
207
|
+
console.log('');
|
|
208
|
+
console.log(`Run: agents computer start --host ${host}`);
|
|
209
|
+
process.exit(1);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
// PowerShell to bounce the remote daemon: kill the running exe (tolerating
|
|
213
|
+
// "not running"), then start the LOGON scheduled task that owns its
|
|
214
|
+
// lifecycle. Pure so tests can assert the exact script (mirrors the
|
|
215
|
+
// ssh-tunnel script builders).
|
|
216
|
+
export function buildRestartTaskScript(taskName, exeName) {
|
|
217
|
+
const procName = exeName.replace(/\.exe$/i, '');
|
|
218
|
+
return [
|
|
219
|
+
`$ErrorActionPreference = 'Stop'`,
|
|
220
|
+
`Stop-Process -Name '${procName}' -Force -ErrorAction SilentlyContinue`,
|
|
221
|
+
`Start-ScheduledTask -TaskName '${taskName}'`,
|
|
222
|
+
`Write-Output 'restarted'`,
|
|
223
|
+
].join('; ');
|
|
224
|
+
}
|
|
225
|
+
// reload --host: the Windows daemon has no policy file to re-read (it
|
|
226
|
+
// enforces no allow-list — see TrustStatus in native/computer-win/Rpc.cs), so
|
|
227
|
+
// reload means bounce the daemon via its scheduled task — the way to pick up
|
|
228
|
+
// a freshly pushed exe — then prove it answers through the recorded tunnel.
|
|
229
|
+
async function reloadRemoteHelper(host) {
|
|
230
|
+
const { target } = await resolveRemoteDevice(host);
|
|
231
|
+
const script = buildRestartTaskScript(REMOTE_TASK_NAME, WIN_HELPER_EXE);
|
|
232
|
+
const res = sshExec(target, `powershell -NoProfile -NonInteractive -EncodedCommand ${encodePowershell(script)}`, { timeoutMs: 60_000 });
|
|
233
|
+
if (res.code !== 0) {
|
|
234
|
+
const msg = (res.stderr || res.stdout || '').trim();
|
|
235
|
+
console.error(`restart failed on ${target}${res.timedOut ? ' (timed out)' : ''}${msg ? `: ${msg}` : ''}`);
|
|
236
|
+
process.exit(1);
|
|
237
|
+
}
|
|
238
|
+
console.log(`task: restarted "${REMOTE_TASK_NAME}" on ${target}`);
|
|
239
|
+
const state = readRemoteState(host);
|
|
240
|
+
if (!state) {
|
|
241
|
+
console.log(`(no tunnel recorded — run \`agents computer start --host ${host}\` to drive it)`);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
hydrateRemoteEnvFromState(host);
|
|
245
|
+
// The relaunched daemon needs a beat to rebind its port; poll through the
|
|
246
|
+
// tunnel until it answers.
|
|
247
|
+
const deadline = Date.now() + 15_000;
|
|
248
|
+
let lastErr = '';
|
|
249
|
+
while (Date.now() < deadline) {
|
|
250
|
+
try {
|
|
251
|
+
const client = openComputerClient();
|
|
252
|
+
try {
|
|
253
|
+
const r = await client.call('trust_status');
|
|
254
|
+
if (!r.error && r.result) {
|
|
255
|
+
console.log(`reloaded: daemon answering (pid ${r.result.pid ?? '?'})`);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
lastErr = r.error ? `${r.error.code}: ${r.error.message}` : 'empty result';
|
|
259
|
+
}
|
|
260
|
+
finally {
|
|
261
|
+
await client.close();
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
catch (err) {
|
|
265
|
+
lastErr = err.message;
|
|
266
|
+
}
|
|
267
|
+
await sleep(500);
|
|
268
|
+
}
|
|
269
|
+
console.error(`daemon did not answer within 15s after restart${lastErr ? ` (${lastErr})` : ''}`);
|
|
270
|
+
process.exit(1);
|
|
271
|
+
}
|
|
162
272
|
// run — the embedded observe -> act -> verify agent loop. A reasoning model
|
|
163
273
|
// (Claude API by default, or any Anthropic-shaped endpoint via --base-url for
|
|
164
274
|
// Ollama / vLLM / LiteLLM) drives the EXISTING computer verbs as tools over the
|
|
@@ -560,8 +670,19 @@ function registerStartCommand(program) {
|
|
|
560
670
|
function registerReloadCommand(program) {
|
|
561
671
|
program
|
|
562
672
|
.command('reload')
|
|
563
|
-
.description('Reload the allow-list policy
|
|
564
|
-
.
|
|
673
|
+
.description('Reload the allow-list policy (SIGHUP the local daemon) — or restart a remote Windows daemon with --host')
|
|
674
|
+
.option('--host <device>', 'Restart the remote Windows daemon (its scheduled task) instead of SIGHUPing the local one')
|
|
675
|
+
.action(async (opts) => {
|
|
676
|
+
if (opts.host) {
|
|
677
|
+
try {
|
|
678
|
+
await reloadRemoteHelper(opts.host);
|
|
679
|
+
}
|
|
680
|
+
catch (err) {
|
|
681
|
+
console.error(`error: ${err.message}`);
|
|
682
|
+
process.exit(1);
|
|
683
|
+
}
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
565
686
|
const socketPath = resolveSocketPath();
|
|
566
687
|
if (!fs.existsSync(socketPath)) {
|
|
567
688
|
console.error(`daemon not running (no socket at ${socketPath})`);
|
package/dist/commands/doctor.js
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { addHostOption } from '../lib/hosts/option.js';
|
|
2
|
+
import { buildRemoteAgentsInvocation } from '../lib/hosts/remote-cmd.js';
|
|
3
|
+
import { loadDevices } from '../lib/devices/registry.js';
|
|
4
|
+
import { resolveHost } from '../lib/hosts/registry.js';
|
|
5
|
+
import { sshExec } from '../lib/ssh-exec.js';
|
|
6
|
+
import { sshTargetFor } from '../lib/hosts/types.js';
|
|
7
|
+
import { machineId } from '../lib/session/sync/config.js';
|
|
2
8
|
import chalk from 'chalk';
|
|
3
|
-
import { checkAllClis } from '../lib/teams/agents.js';
|
|
9
|
+
import { checkAllClis, collectTeamsDoctorData } from '../lib/teams/agents.js';
|
|
4
10
|
import { AGENTS, ALL_AGENT_IDS, resolveAgentName, formatAgentError } from '../lib/agents.js';
|
|
5
11
|
import { getGlobalDefault, getVersionHomePath, isVersionInstalled, listInstalledVersions, parseAgentSpec, } from '../lib/versions.js';
|
|
6
12
|
import { loadManifest, isStale } from '../lib/staleness/index.js';
|
|
@@ -203,6 +209,121 @@ function renderExecPolicyAdvisory() {
|
|
|
203
209
|
console.log(chalk.gray(` ${line}`));
|
|
204
210
|
}
|
|
205
211
|
}
|
|
212
|
+
const AGENT_ORDER = ['claude', 'codex', 'kimi', 'grok', 'antigravity', 'opencode', 'cursor', 'gemini', 'droid'];
|
|
213
|
+
function shortAgentHeader(name) {
|
|
214
|
+
return name.slice(0, 4).padEnd(4);
|
|
215
|
+
}
|
|
216
|
+
function agentCell(entry) {
|
|
217
|
+
if (!entry)
|
|
218
|
+
return chalk.gray('- ');
|
|
219
|
+
if (entry.installed) {
|
|
220
|
+
const signedInHint = entry.signedIn ? '*' : ' ';
|
|
221
|
+
return chalk.green(`rdy${signedInHint}`.padEnd(4));
|
|
222
|
+
}
|
|
223
|
+
if (entry.error)
|
|
224
|
+
return chalk.red('err '.padEnd(4));
|
|
225
|
+
return chalk.gray('no '.padEnd(4));
|
|
226
|
+
}
|
|
227
|
+
async function resolveFleetTargets(opts) {
|
|
228
|
+
const singleName = opts.host || opts.device;
|
|
229
|
+
if (singleName) {
|
|
230
|
+
// --device / --host as a single-device filter: resolve through the device
|
|
231
|
+
// registry first, then the general host registry, then ad-hoc user@host.
|
|
232
|
+
const registry = await loadDevices();
|
|
233
|
+
const deviceProfile = registry[singleName];
|
|
234
|
+
if (deviceProfile) {
|
|
235
|
+
return [{
|
|
236
|
+
name: deviceProfile.name,
|
|
237
|
+
sshTarget: deviceProfile.name,
|
|
238
|
+
os: deviceProfile.platform !== 'unknown' ? deviceProfile.platform : undefined,
|
|
239
|
+
}];
|
|
240
|
+
}
|
|
241
|
+
const host = await resolveHost(singleName);
|
|
242
|
+
if (host) {
|
|
243
|
+
return [{ name: singleName, sshTarget: sshTargetFor(host), os: host.os }];
|
|
244
|
+
}
|
|
245
|
+
console.error(chalk.red(`Unknown host or device '${singleName}'.`));
|
|
246
|
+
process.exit(1);
|
|
247
|
+
}
|
|
248
|
+
const registry = await loadDevices();
|
|
249
|
+
const localName = machineId();
|
|
250
|
+
return Object.values(registry)
|
|
251
|
+
.filter((d) => d.name.toLowerCase() !== localName)
|
|
252
|
+
.map((d) => ({
|
|
253
|
+
name: d.name,
|
|
254
|
+
sshTarget: d.name,
|
|
255
|
+
os: d.platform !== 'unknown' ? d.platform : undefined,
|
|
256
|
+
}));
|
|
257
|
+
}
|
|
258
|
+
async function probeFleetTarget(target) {
|
|
259
|
+
const forwarded = ['teams', 'doctor', '--json'];
|
|
260
|
+
const isWin = /^win/i.test((target.os ?? '').trim());
|
|
261
|
+
const remoteCmd = buildRemoteAgentsInvocation(forwarded, undefined, isWin ? 'windows' : undefined,
|
|
262
|
+
// POSIX login shells often lack the shims dir; Windows PowerShell usually
|
|
263
|
+
// has it via the install profile, and our single-quote escaping would
|
|
264
|
+
// prevent $HOME expansion there, so skip the bootstrap on Windows.
|
|
265
|
+
isWin ? undefined : { PATH: '$HOME/.agents/.cache/shims:$HOME/.local/bin:$PATH' });
|
|
266
|
+
const res = sshExec(target.sshTarget, remoteCmd, { timeoutMs: 30000, multiplex: true });
|
|
267
|
+
if (res.code !== 0) {
|
|
268
|
+
return {
|
|
269
|
+
name: target.name,
|
|
270
|
+
online: false,
|
|
271
|
+
error: res.timedOut ? 'timed out' : (res.stderr || `exit ${res.code ?? 'unknown'}`),
|
|
272
|
+
agents: {},
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
try {
|
|
276
|
+
const agents = JSON.parse(res.stdout);
|
|
277
|
+
return { name: target.name, online: true, agents };
|
|
278
|
+
}
|
|
279
|
+
catch (err) {
|
|
280
|
+
const stderrHint = res.stderr ? ` stderr: ${res.stderr.trim()}` : '';
|
|
281
|
+
return {
|
|
282
|
+
name: target.name,
|
|
283
|
+
online: true,
|
|
284
|
+
error: `invalid JSON (${err?.message ?? 'parse error'})${stderrHint}`,
|
|
285
|
+
agents: {},
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
async function runDevicesDoctor(opts) {
|
|
290
|
+
const singleName = opts.host || opts.device;
|
|
291
|
+
const targets = await resolveFleetTargets(opts);
|
|
292
|
+
const localName = machineId();
|
|
293
|
+
const results = [];
|
|
294
|
+
// Local machine first, directly.
|
|
295
|
+
if (!singleName) {
|
|
296
|
+
results.push({ name: localName, online: true, agents: await collectTeamsDoctorData() });
|
|
297
|
+
}
|
|
298
|
+
// Remote targets in parallel.
|
|
299
|
+
const remoteResults = await Promise.all(targets.map(probeFleetTarget));
|
|
300
|
+
results.push(...remoteResults);
|
|
301
|
+
if (opts.json) {
|
|
302
|
+
console.log(JSON.stringify({ devices: results }, null, 2));
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
if (results.length === 0) {
|
|
306
|
+
console.log(chalk.gray('No registered devices. Run `agents devices` to register some.'));
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
const agentsToShow = AGENT_ORDER.filter((a) => results.some((r) => r.agents[a] !== undefined));
|
|
310
|
+
console.log(chalk.bold('Agent readiness by device'));
|
|
311
|
+
if (agentsToShow.length === 0) {
|
|
312
|
+
console.log(chalk.gray(' (no agent data collected)'));
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
const nameWidth = Math.max(...results.map((r) => r.name.length));
|
|
316
|
+
const header = ` ${'Device'.padEnd(nameWidth)} ${agentsToShow.map(shortAgentHeader).join(' ')}`;
|
|
317
|
+
console.log(chalk.gray(header));
|
|
318
|
+
for (const row of results) {
|
|
319
|
+
const status = row.online ? chalk.green('online ') : chalk.red('offline');
|
|
320
|
+
const errorSuffix = row.error ? ` ${chalk.gray(row.error)}` : '';
|
|
321
|
+
const cells = agentsToShow.map((a) => agentCell(row.agents[a])).join(' ');
|
|
322
|
+
console.log(` ${row.name.padEnd(nameWidth)} ${status} ${cells}${errorSuffix}`);
|
|
323
|
+
}
|
|
324
|
+
console.log();
|
|
325
|
+
console.log(chalk.gray(' rdy* = installed and signed in · rdy = installed · no = not installed · err = probe failed · - = offline'));
|
|
326
|
+
}
|
|
206
327
|
function parseTargetArg(arg) {
|
|
207
328
|
const at = arg.indexOf('@');
|
|
208
329
|
const agentPart = at === -1 ? arg : arg.slice(0, at);
|
|
@@ -484,7 +605,9 @@ export function registerDoctorCommand(program) {
|
|
|
484
605
|
.option('--kind <kinds>', 'Restrict to comma-separated resource kinds (commands,skills,hooks,rules,mcp,permissions,subagents,plugins,promptcuts)')
|
|
485
606
|
.option('--cwd <path>', 'Resolution cwd for project layer detection (default: process.cwd())')
|
|
486
607
|
.option('--adopt <agent>', "Take over the agent's native launcher that shadows the shim (symlink it to the version-managed shim; reversible with --release)")
|
|
487
|
-
.option('--release <agent>', 'Undo --adopt: restore the native launcher agents-cli previously adopted')
|
|
608
|
+
.option('--release <agent>', 'Undo --adopt: restore the native launcher agents-cli previously adopted')
|
|
609
|
+
.option('--devices', 'Check agent readiness on every registered device (alias --hosts)')
|
|
610
|
+
.option('--hosts', 'Alias of --devices');
|
|
488
611
|
setHelpSections(doctorCmd, {
|
|
489
612
|
examples: `
|
|
490
613
|
# Overview: CLI availability + sync status + orphans across all defaults
|
|
@@ -511,6 +634,14 @@ export function registerDoctorCommand(program) {
|
|
|
511
634
|
});
|
|
512
635
|
doctorCmd.action(async (target, opts) => {
|
|
513
636
|
const cwd = opts.cwd ? opts.cwd : process.cwd();
|
|
637
|
+
if (opts.devices || opts.hosts) {
|
|
638
|
+
if (target) {
|
|
639
|
+
console.error(chalk.red('Cannot combine --devices with a target argument.'));
|
|
640
|
+
process.exit(1);
|
|
641
|
+
}
|
|
642
|
+
await runDevicesDoctor(opts);
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
514
645
|
// Launcher adoption escape hatch. `--adopt <agent>` forces the take-over
|
|
515
646
|
// even for a non-default agent; `--release <agent>` reverses it.
|
|
516
647
|
if (opts.adopt || opts.release) {
|