@phnx-labs/agents-cli 1.20.50 → 1.20.52

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/dist/commands/browser-picker.js +1 -18
  3. package/dist/commands/browser.js +215 -7
  4. package/dist/commands/cloud.js +7 -25
  5. package/dist/commands/computer.d.ts +1 -0
  6. package/dist/commands/computer.js +129 -8
  7. package/dist/commands/events.d.ts +1 -1
  8. package/dist/commands/events.js +2 -3
  9. package/dist/commands/exec.js +66 -8
  10. package/dist/commands/factory.js +9 -4
  11. package/dist/commands/feed.d.ts +9 -0
  12. package/dist/commands/feed.js +69 -0
  13. package/dist/commands/inspect.js +1 -11
  14. package/dist/commands/logs.d.ts +5 -1
  15. package/dist/commands/logs.js +248 -3
  16. package/dist/commands/mcp.js +9 -6
  17. package/dist/commands/message.js +1 -4
  18. package/dist/commands/profiles.js +1 -18
  19. package/dist/commands/repo.js +33 -14
  20. package/dist/commands/resource-view.d.ts +1 -0
  21. package/dist/commands/resource-view.js +5 -17
  22. package/dist/commands/secrets.d.ts +23 -0
  23. package/dist/commands/secrets.js +174 -70
  24. package/dist/commands/sessions-picker.js +1 -18
  25. package/dist/commands/sessions.js +6 -8
  26. package/dist/commands/teams-picker.js +1 -32
  27. package/dist/commands/teams.js +5 -27
  28. package/dist/commands/tmux.js +1 -3
  29. package/dist/commands/view.js +1 -9
  30. package/dist/commands/worktree.js +1 -4
  31. package/dist/index.js +6 -2
  32. package/dist/lib/agents.d.ts +0 -4
  33. package/dist/lib/agents.js +20 -33
  34. package/dist/lib/auto-dispatch-linear.d.ts +18 -0
  35. package/dist/lib/auto-dispatch-linear.js +107 -0
  36. package/dist/lib/auto-dispatch-provider.d.ts +10 -0
  37. package/dist/lib/auto-dispatch-provider.js +25 -0
  38. package/dist/lib/auto-dispatch.d.ts +87 -0
  39. package/dist/lib/auto-dispatch.js +142 -0
  40. package/dist/lib/browser/cdp.js +11 -2
  41. package/dist/lib/browser/drivers/ssh.d.ts +28 -10
  42. package/dist/lib/browser/drivers/ssh.js +57 -18
  43. package/dist/lib/browser/login-detection.d.ts +94 -0
  44. package/dist/lib/browser/login-detection.js +274 -0
  45. package/dist/lib/browser/profiles.d.ts +17 -8
  46. package/dist/lib/browser/profiles.js +27 -8
  47. package/dist/lib/browser/refs.js +1 -5
  48. package/dist/lib/browser/secret-ref.d.ts +10 -0
  49. package/dist/lib/browser/secret-ref.js +14 -0
  50. package/dist/lib/browser/service.js +14 -12
  51. package/dist/lib/cli-resources.d.ts +0 -2
  52. package/dist/lib/cli-resources.js +30 -13
  53. package/dist/lib/cloud/rush.d.ts +15 -24
  54. package/dist/lib/cloud/rush.js +7 -32
  55. package/dist/lib/crabbox/cli.js +4 -1
  56. package/dist/lib/crabbox/lease.d.ts +6 -0
  57. package/dist/lib/crabbox/lease.js +40 -10
  58. package/dist/lib/crabbox/runtimes.d.ts +38 -1
  59. package/dist/lib/crabbox/runtimes.js +98 -5
  60. package/dist/lib/daemon.d.ts +12 -9
  61. package/dist/lib/daemon.js +73 -17
  62. package/dist/lib/events.d.ts +31 -5
  63. package/dist/lib/events.js +288 -101
  64. package/dist/lib/exec.js +32 -14
  65. package/dist/lib/feed.d.ts +56 -0
  66. package/dist/lib/feed.js +251 -0
  67. package/dist/lib/format.d.ts +38 -0
  68. package/dist/lib/format.js +108 -0
  69. package/dist/lib/git.d.ts +21 -0
  70. package/dist/lib/git.js +92 -0
  71. package/dist/lib/hooks/cache.d.ts +9 -2
  72. package/dist/lib/hooks/cache.js +220 -8
  73. package/dist/lib/hooks.js +24 -10
  74. package/dist/lib/hosts/passthrough.js +1 -0
  75. package/dist/lib/platform/exec.d.ts +4 -1
  76. package/dist/lib/platform/exec.js +8 -2
  77. package/dist/lib/resources.d.ts +0 -8
  78. package/dist/lib/resources.js +0 -10
  79. package/dist/lib/rotate.js +2 -0
  80. package/dist/lib/runner.js +10 -2
  81. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  82. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  83. package/dist/lib/secrets/agent.d.ts +21 -0
  84. package/dist/lib/secrets/agent.js +63 -1
  85. package/dist/lib/secrets/bundles.d.ts +33 -1
  86. package/dist/lib/secrets/bundles.js +38 -8
  87. package/dist/lib/secrets/icloud-import.d.ts +70 -0
  88. package/dist/lib/secrets/icloud-import.js +173 -0
  89. package/dist/lib/secrets/index.d.ts +36 -0
  90. package/dist/lib/secrets/index.js +99 -9
  91. package/dist/lib/secrets/remote.js +1 -1
  92. package/dist/lib/secrets/sync.js +1 -1
  93. package/dist/lib/session/active.d.ts +11 -1
  94. package/dist/lib/session/active.js +3 -0
  95. package/dist/lib/session/db.d.ts +1 -4
  96. package/dist/lib/session/db.js +20 -25
  97. package/dist/lib/session/discover.d.ts +2 -2
  98. package/dist/lib/session/discover.js +60 -48
  99. package/dist/lib/session/parse.js +35 -34
  100. package/dist/lib/session/render.d.ts +7 -3
  101. package/dist/lib/session/render.js +15 -9
  102. package/dist/lib/session/state.d.ts +55 -0
  103. package/dist/lib/session/state.js +100 -11
  104. package/dist/lib/session/types.d.ts +9 -0
  105. package/dist/lib/shims.d.ts +9 -3
  106. package/dist/lib/shims.js +44 -8
  107. package/dist/lib/ssh-tunnel.d.ts +33 -2
  108. package/dist/lib/ssh-tunnel.js +94 -7
  109. package/dist/lib/staleness/types.d.ts +0 -1
  110. package/dist/lib/startup/command-registry.d.ts +1 -0
  111. package/dist/lib/startup/command-registry.js +2 -0
  112. package/dist/lib/state.d.ts +2 -0
  113. package/dist/lib/state.js +25 -8
  114. package/dist/lib/teams/agents.js +6 -3
  115. package/dist/lib/types.d.ts +24 -1
  116. package/dist/lib/versions.d.ts +0 -26
  117. package/dist/lib/versions.js +2 -145
  118. package/dist/lib/warn-unpushed.d.ts +40 -0
  119. package/dist/lib/warn-unpushed.js +128 -0
  120. package/dist/lib/whats-new.d.ts +5 -3
  121. package/dist/lib/whats-new.js +25 -5
  122. package/package.json +3 -1
  123. package/dist/lib/resources/index.d.ts +0 -53
  124. package/dist/lib/resources/index.js +0 -76
package/CHANGELOG.md CHANGED
@@ -2,6 +2,46 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 1.20.52
6
+
7
+ - **Re-pinned the keychain helper to a freshly notarized build carrying the new iCloud verbs.** The #904 session changed `keychain-helper.swift` (legacy iCloud/synchronizable item verbs) and re-pinned the sha, but the notarized binary it pinned lived only in that session's since-removed worktree — the release prepack gate (`verify-keychain-helper.sh`) then failed on every machine (`SHA256 mismatch`) because no reachable `bin/Agents CLI.app` matched the pin. Rebuilt the helper from current source on the release machine (universal, signed, notarized: Gatekeeper `Notarized Developer ID`, submission accepted) and pinned that binary. Same class of fix as #835.
8
+ - **Fix: version-skewed CLI invocations no longer wipe the secrets-agent's hot cache — the recurring Touch ID storm on version-churning machines is closed.** `ensureAgentRunning` tore down a reachable broker whenever the broker's running version differed from the caller's on-disk version — unconditionally, via `launchctl kickstart -k`, wiping every unlocked bundle. On a machine where installed versions churn (dev builds stamp a fresh `0.0.0-dev.<sha>` per install; an npm copy and a dev copy invoke in turn), that meant constant wipes, and the next read of *each* held bundle popped a fresh Touch ID prompt — the exact storm #435 fixed on the server side, reintroduced client-side. The client now accepts a protocol-compatible, version-skewed broker while it holds real unlocks (`shouldTeardownVersionSkewedBroker`); the broker's own sweep still adopts new code at the next quiet moment (store empty), so upgrades land without ever costing a re-prompt. Source: `apps/cli/src/lib/secrets/agent.ts` (`ensureAgentRunning`, `shouldTeardownVersionSkewedBroker`).
9
+ - **Fix: mutating a bundle now evicts the broker-held copy — `rotate` no longer serves the old secret for up to 7 days.** Only `secrets policy` invalidated the secrets-agent after a write; `add`, `rotate`, `remove`, `rename`, `delete`, and `import` updated the keychain while a broker-held snapshot kept serving the pre-write values for the rest of the ~7d hold — a rotated credential silently kept injecting the OLD value into every run. `writeBundle` and `deleteBundle` now evict the bundle from the broker after every mutating write (`agentEvictSync`, a synchronous socket client mirroring the read fast-path), so the next read re-resolves fresh from the keychain (one prompt) and re-caches. The usage-telemetry stamp (`stampLastUsed`, fired on every broker HIT) opts out — evicting there would make the cache destroy itself on first use — and the eviction honors `AGENTS_SECRETS_NO_AGENT` plus the test-backend override so suites never evict a user's real unlocks (`shouldEvictAfterBundleWrite`). The `policy` command's bespoke eviction is superseded by the chokepoint. Source: `apps/cli/src/lib/secrets/bundles.ts` (`writeBundle`, `deleteBundle`, `shouldEvictAfterBundleWrite`), `apps/cli/src/lib/secrets/agent.ts` (`agentEvictSync`), `apps/cli/src/commands/secrets.ts`.
10
+ - **`agents secrets import` gains a unified `--from <source>` and recovers bundles stranded in the iCloud Keychain.** One axis for every source: a .env path (`-` reads stdin), `1password:<vault>` (the boolean `--from-1password --vault <name>` pair still works as a hidden deprecated alias), and the new `icloud`. Bundles created in the pre-biometry era were synced via iCloud Keychain; the device-local cutover pinned every query to `kSecAttrSynchronizable: false`, which orphaned those items — visible in Keychain Access under iCloud, invisible to `secrets list` and `migrate-acl`. `agents secrets import --from icloud` discovers them (bundle metadata *and* bare per-key secret items whose metadata never synced), offers an interactive multi-select (or takes an explicit bundle name for non-interactive use), re-imports them as normal device-local biometry-gated bundles, and with `--purge` deletes the successfully-read iCloud copies (never an unreadable one). `secrets view <name>` on a missing bundle now points at the recovery command when an iCloud copy of that name exists. Three new keychain-helper verbs (`list-synced`, `get-batch-synced`, `delete-synced`) match synchronizable items exclusively, so the live device-local store is untouchable from the recovery path. Source: `apps/cli/src/lib/secrets/icloud-import.ts`, `apps/cli/src/lib/secrets/keychain-helper.swift`, `apps/cli/src/commands/secrets.ts` (`parseImportSource`).
11
+ - **`agents feed` surfaces every top-level agent question that is waiting on the user.** `AskUserQuestion` hooks publish atomic open-block records with the full question and option set, mailbox/session identity, host, and runtime; Task subagents are gated out so their internal questions do not flood the operator view. The command renders those blocks locally or as JSON, forwards across configured hosts, and registers its runtime-managed hook from the CLI-writable user layer without dirtying the auto-pulled system repository. Source: `apps/cli/src/commands/feed.ts`, `apps/cli/src/lib/feed.ts`, runtime labelling in `apps/cli/src/lib/exec.ts` and `apps/cli/src/lib/teams/agents.ts`. (RUSH-1473)
12
+ - **Browser-profile credentials: account identity, `secrets get <bundle> <KEY>`, and `browser type --secret` for leak-free login.** `agents browser profiles logins` now shows, per profile, the account signed into each live service (plaintext username from Chromium `Login Data` — never decrypts the encrypted password) and whether login creds are declared in the profile's secrets bundle (columns `SERVICE | ACCOUNT | CREDS`); `profiles show` gains a `Logins:` block. `agents secrets get <bundle> <KEY>` prints one resolved value from a bundle (arg-count overload of the existing raw `get <item>`; ungated like it, and the `secrets.get` audit event fires inside the resolver). `agents browser type <ref> --secret <bundle>/<KEY>` resolves a credential in-process and types it into the page — the value never crosses stdout or the agent transcript — so an agent can drive a login by composing `profiles logins` → `browser start <loginUrl>` → `refs` → `type --secret` → `screenshot`, handling 2FA/selectors itself (no fragile CLI auto-login engine; Google/X block automation anyway). A profile's `--secrets` bundle is the credential store, keyed by the `<PREFIX>_USERNAME`/`<PREFIX>_PASSWORD` convention (per-service prefixes in `AUTH_SIGNATURES`); `profiles create --secrets` now warns if the bundle doesn't exist yet. Cookie-persistence-first remains the headline (the `browser` skill's credential guidance was corrected — the bundle only injected env vars into the browser process before, inert for web login). Source: `apps/cli/src/lib/browser/login-detection.ts`, `apps/cli/src/lib/browser/secret-ref.ts`, `apps/cli/src/commands/browser.ts`, `apps/cli/src/commands/secrets.ts`.
13
+ - **`agents logs audit` / `agents logs stats` / `agents logs rotate` — user-facing audit trail viewer.** The append-only local event log (`~/.agents/events.jsonl`) is now a first-class audit surface. `agents logs audit` queries events with filters (`--module`, `--command`, `--event`, `--agent`, `--caller`, `--level`, `--since`, `--limit`, `--json`) and `--follow` for live tailing; `agents logs stats` shows aggregate breakdowns by level, event type, module, and user; `agents logs rotate` prunes old numbered archives (`--days`, default 7). Events carry `level` (audit/warn/info/debug) and an environment-derived `caller` (`claude-code`, Factory agent kind, terminal, or script). Sensitive flag values, secret-shaped payload fields, token-like strings, and raw prompts are redacted before append. Security-relevant operations (secrets, teams lifecycle, cloud dispatch) auto-classify as `audit`. At 10 MB the active file rotates losslessly through `events.1.jsonl.gz`, `events.2.jsonl.gz`, and so on; `query()` reads every archive transparently. New instrumentation in `cloud.ts`, `factory.ts`, `teams.ts`, `secrets.ts`, `mcp.ts`, and `rotate.ts`. Source: `apps/cli/src/lib/events.ts`, `apps/cli/src/commands/logs.ts`, instrumentation call sites. (RUSH-460)
14
+ - **`agents run claude --lease` now runs the box logged-in — the Claude OAuth token ships alongside the config.** The lease copied `~/.claude.json` (config/account-metadata) but never the OAuth token, so Claude booted "Not logged in" on every leased box. The token lives in the macOS Keychain (hash-suffixed service for an agents-cli managed home, bare for a default install) and on Linux at `~/.claude/.credentials.json`. `resolveClaudeCredentialsBlob()` now reads the raw wrapped Keychain payload **silently** (`/usr/bin/security … -w` — Claude's item trusts it, no Touch ID): bare service first, then enumerate installed version homes, preferring the account whose email matches the copied config; off-darwin it reuses the existing `.credentials.json` file branch. `buildCredentialScript` writes that blob to `~/.claude/.credentials.json` (0600) via the same quoted-heredoc that carries every other cred (the box's `~/.claude` is a symlink into the versioned home, so it lands exactly where the shim's `CLAUDE_CONFIG_DIR` reads it); it is shredded after the run **regardless of `--keep-box`**. Resolved in the command layer after the existing per-run consent prompt, whose text now names the token explicitly. Scope is Claude-only — Codex/Grok already ship their token in the copied auth file. Verified live on Hetzner: a leased box ran `agents run claude` and returned a real model reply (`LOGIN_OK`, exit 0), and the token file was absent (shredded) afterward. Source: `apps/cli/src/lib/crabbox/runtimes.ts` (`resolveClaudeCredentialsBlob`, `buildCredentialScript`), `apps/cli/src/lib/crabbox/lease.ts`, `apps/cli/src/commands/exec.ts`.
15
+ - **`agents browser profiles set-default <name>` picks the profile a bare `agents browser start` uses — so agents stop opening a logged-out Chrome.** With no `--profile`, `start` used to auto-detect the first installed Chromium-family browser (Chrome first on macOS) and save it as `default`, ignoring a profile you'd actually logged into. Now `start` resolves in order: (1) your configured default, (2) an existing `default` profile, (3) auto-detect. The configured default ALSO re-points an explicit `--profile default`, so an agent that hardcodes `default` still lands on your chosen profile. The setting is **device-local** — stored in `~/.agents/devices/<machine>/agents.yaml`, never synced to other machines (the target profile may hold machine-local logins). `profiles list`/`show` mark it; `set-default --unset` reverts to auto-detect; a missing target warns and falls back rather than hard-failing. Source: `apps/cli/src/lib/browser/profiles.ts` (`ensureDefaultBrowserProfile`, `getConfiguredDefaultProfileName`), `apps/cli/src/lib/state.ts` (`writeMetaUnlocked`, `overlayMachineLocal`), `apps/cli/src/lib/types.ts` (`Meta.defaultBrowserProfile`), `apps/cli/src/commands/browser.ts`.
16
+ - **`agents browser` now warns when a task opens a login-gated site on a logged-out profile — grounded in real session state.** New `apps/cli/src/lib/browser/login-detection.ts` reads a profile's Chromium cookie store (presence only — never decrypts the Keychain-encrypted values, and filters expiry in SQL so Chromium's >2^53 microsecond timestamps never trip `node:sqlite`'s integer range) to tell which login-gated services (LinkedIn, Google, X, GitHub, Reddit) have a live session. `agents browser start --url <login-gated>` prints a stderr hint like `profile "default" has no linkedin.com session. logged in elsewhere: comet-local. try: --profile comet-local` when the chosen profile is logged out; it never blocks or slows start. `agents browser profiles logins` shows a profile-by-service table. Source: `apps/cli/src/lib/browser/login-detection.ts`, `apps/cli/src/commands/browser.ts`.
17
+ - **Fix: a finished session that signed off with a trailing "?" no longer reads as `input_required` forever (RUSH-1522).** The session state engine's prose-question heuristic (last assistant message ends with a question) now decays after 30 minutes without a session write: an unanswered prose question older than that classifies as `idle`, not `waiting_input` — so `agents sessions --active` and the Factory Floor's NEEDS YOU lane stop surfacing long-finished sessions as needing input. The structural signals are exempt and never decay: a genuinely pending `ExitPlanMode` (plan review) or `AskUserQuestion` still classifies as `waiting_input` at any age. Source: `apps/cli/src/lib/session/state.ts` (`inferActivity`, `PROSE_QUESTION_FRESH_MS`).
18
+ - **The post-upgrade "What's new" summary shows the release notes again.** The summary parser only recognized the old changelog format (standalone `**Heading**` lines with sub-bullets); every release since the changelog moved to single-line `- **Title.** prose…` entries rendered as a bare version header with zero bullets, so upgrades looked like they shipped nothing. The parser now extracts the bold heading from both formats (prose still dropped — full notes stay in the changelog). Verified against the real changelog: the 1.20.49 → 1.20.50 range renders all four 1.20.50 entry titles. Source: `apps/cli/src/lib/whats-new.ts`.
19
+ - **Fix: daemon no longer crash-loops when started from the bare `browser` or `computer` shim.** Daemon launch resolution now maps installed sibling shims to the `agents` launcher and compiled shims to `index.js` before generating launchd/systemd commands, and fails clearly if that invariant is broken. Headless auto-start reads the long-lived Claude token only from an already-unlocked secrets-agent snapshot, so it cannot hang on a biometric prompt nobody can answer; an interactive start can still prompt normally. Source: `apps/cli/src/lib/daemon.ts` (`getAgentsBinPath`, `readDaemonClaudeOAuthToken`), `apps/cli/src/lib/secrets/bundles.ts` (`agentOnly`). (RUSH-1527)
20
+
21
+ ## 1.20.51
22
+
23
+ - **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.
24
+ - **`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`.
25
+ - **`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)
26
+ - **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`).
27
+ - **`--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`.
28
+ - **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`.
29
+
30
+ - **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`).
31
+ - **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)
32
+ - **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)
33
+ - **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)
34
+ - **`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)
35
+ - **`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`.
36
+ - **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)
37
+ - **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)
38
+ - **`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`).
39
+ - **`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)
40
+ - **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)
41
+ - **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)
42
+ - **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)
43
+ - **`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)
44
+
5
45
  ## 1.20.50
6
46
 
7
47
  - **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`.
@@ -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;
@@ -1,6 +1,10 @@
1
1
  import * as fs from 'fs';
2
2
  import * as path from 'path';
3
- import { listProfiles, getProfile, createProfile, deleteProfile, ensureDefaultBrowserProfile, getProfileRuntimeDir, extractConfiguredPort, findFreeProfilePort, getEndpointPresets, } from '../lib/browser/profiles.js';
3
+ import { listProfiles, getProfile, createProfile, deleteProfile, ensureDefaultBrowserProfile, getConfiguredDefaultProfileName, DEFAULT_BROWSER_PROFILE_NAME, getProfileRuntimeDir, extractConfiguredPort, findFreeProfilePort, getEndpointPresets, } from '../lib/browser/profiles.js';
4
+ import { updateMeta } from '../lib/state.js';
5
+ import { loginsForProfile, profilesLoggedInto, serviceForUrl, loginsWithAccountsForProfile, accountsForProfile, credKeysForService, AUTH_SIGNATURES, } from '../lib/browser/login-detection.js';
6
+ import { parseSecretRef } from '../lib/browser/secret-ref.js';
7
+ import { readAndResolveBundleEnv, bundleExists, readBundle, describeBundle } from '../lib/secrets/bundles.js';
4
8
  import { findBrowserPath, getPortOccupant, isLauncherScript } from '../lib/browser/chrome.js';
5
9
  import { listProfileCacheDirs, removeProfileCache, listAllProfileSnapshots, } from '../lib/browser/runtime-state.js';
6
10
  import { DEFAULT_VIEWPORT } from '../lib/browser/devices.js';
@@ -94,6 +98,29 @@ export function registerBrowserSubcommands(program) {
94
98
  registerTaskCommands(program);
95
99
  registerCommandGroups(program, BROWSER_HELP_GROUPS);
96
100
  }
101
+ /**
102
+ * Whether a profile's secrets bundle holds login creds for `service`
103
+ * (`<PREFIX>_USERNAME` + `<PREFIX>_PASSWORD`). Reads bundle METADATA only (key
104
+ * names, never values) so it doesn't decrypt. Returns the bundle name when both
105
+ * keys exist, '-' when not, or '(unknown)' if the metadata read would prompt or
106
+ * fails — the view must never block on Touch ID.
107
+ */
108
+ function credAvailability(bundleName, service) {
109
+ if (!bundleName)
110
+ return '-';
111
+ const keys = credKeysForService(service);
112
+ if (!keys)
113
+ return '-';
114
+ try {
115
+ if (!bundleExists(bundleName))
116
+ return '-';
117
+ const present = new Set(describeBundle(readBundle(bundleName)).map((e) => e.key));
118
+ return present.has(keys.user) && present.has(keys.pass) ? bundleName : '-';
119
+ }
120
+ catch {
121
+ return '(unknown)';
122
+ }
123
+ }
97
124
  function registerProfilesCommands(browser) {
98
125
  const profiles = browser
99
126
  .command('profiles')
@@ -109,6 +136,10 @@ function registerProfilesCommands(browser) {
109
136
  console.log('Create one with: agents browser profiles create <name> --endpoint <url>');
110
137
  return;
111
138
  }
139
+ // The configured default is what a bare `agents browser start` (and an
140
+ // explicit `--profile default`) resolves to on THIS machine — mark it.
141
+ const configuredDefault = getConfiguredDefaultProfileName();
142
+ const marker = (name) => (name === configuredDefault ? ' (default)' : '');
112
143
  const hasDescriptions = allProfiles.some(p => p.description);
113
144
  if (hasDescriptions) {
114
145
  console.log('NAME'.padEnd(20) + 'BROWSER'.padEnd(12) + 'DESCRIPTION'.padEnd(38) + 'ENDPOINTS');
@@ -119,7 +150,7 @@ function registerProfilesCommands(browser) {
119
150
  .map(([name, ep]) => (name.startsWith('endpoint-') ? ep.target : `${name}=${ep.target}`))
120
151
  .join(', ');
121
152
  const desc = (p.description ?? '').slice(0, 36).padEnd(38);
122
- console.log(p.name.padEnd(20) + (p.browser || '-').padEnd(12) + desc + endpoints);
153
+ console.log(p.name.padEnd(20) + (p.browser || '-').padEnd(12) + desc + endpoints + marker(p.name));
123
154
  }
124
155
  }
125
156
  else {
@@ -130,7 +161,78 @@ function registerProfilesCommands(browser) {
130
161
  const endpoints = Object.entries(presets)
131
162
  .map(([name, ep]) => (name.startsWith('endpoint-') ? ep.target : `${name}=${ep.target}`))
132
163
  .join(', ');
133
- console.log(p.name.padEnd(20) + (p.browser || '-').padEnd(12) + endpoints);
164
+ console.log(p.name.padEnd(20) + (p.browser || '-').padEnd(12) + endpoints + marker(p.name));
165
+ }
166
+ }
167
+ if (configuredDefault) {
168
+ console.log('');
169
+ console.log(`Default profile (this machine): ${configuredDefault}`);
170
+ }
171
+ });
172
+ profiles
173
+ .command('set-default [name]')
174
+ .description('Set the profile `agents browser start` uses when no --profile is passed (also re-points an explicit `--profile default`). Device-local — each machine has its own. No name prints the current value.')
175
+ .option('--unset', 'Clear the configured default (revert to auto-detecting an installed browser)')
176
+ .action(async (name, opts) => {
177
+ if (opts.unset) {
178
+ updateMeta((m) => {
179
+ const { defaultBrowserProfile, ...rest } = m;
180
+ void defaultBrowserProfile;
181
+ return rest;
182
+ });
183
+ console.log('Default browser profile cleared. `agents browser start` will auto-detect an installed Chromium-family browser.');
184
+ return;
185
+ }
186
+ if (!name) {
187
+ const current = getConfiguredDefaultProfileName();
188
+ if (current) {
189
+ console.log(`Default browser profile (this machine): ${current}`);
190
+ }
191
+ else {
192
+ console.log('No default browser profile set. `agents browser start` auto-detects an installed Chromium-family browser.');
193
+ console.log('Set one with: agents browser profiles set-default <name>');
194
+ }
195
+ return;
196
+ }
197
+ const target = await getProfile(name);
198
+ if (!target) {
199
+ console.error(`Profile "${name}" not found.`);
200
+ const all = await listProfiles();
201
+ if (all.length > 0)
202
+ console.error(`Available profiles: ${all.map((p) => p.name).join(', ')}`);
203
+ process.exit(1);
204
+ }
205
+ updateMeta((m) => ({ ...m, defaultBrowserProfile: name }));
206
+ const eps = getEndpointPresets(target);
207
+ const epStr = Object.values(eps)[0]?.target ?? '';
208
+ console.log(`Default browser profile (this machine) is now "${name}" (${target.browser}${epStr ? `, ${epStr}` : ''}).`);
209
+ console.log('Bare `agents browser start` and `--profile default` will use it.');
210
+ });
211
+ profiles
212
+ .command('logins')
213
+ .description('Show which login-gated services each profile has a live session for, the account signed in, and whether login creds are available in the profile\'s secrets bundle (reads cookie/username presence only, never decrypts).')
214
+ .action(async () => {
215
+ const allProfiles = await listProfiles();
216
+ if (allProfiles.length === 0) {
217
+ console.log('No browser profiles configured.');
218
+ return;
219
+ }
220
+ console.log('PROFILE'.padEnd(20) + 'SERVICE'.padEnd(12) + 'ACCOUNT'.padEnd(32) + 'CREDS');
221
+ console.log('-'.repeat(80));
222
+ for (const p of allProfiles) {
223
+ const rows = await loginsWithAccountsForProfile(p.name);
224
+ if (rows.length === 0) {
225
+ console.log(p.name.padEnd(20) + '(none detected)');
226
+ continue;
227
+ }
228
+ let first = true;
229
+ for (const r of rows) {
230
+ const name = first ? p.name : '';
231
+ console.log(name.padEnd(20) +
232
+ r.service.padEnd(12) +
233
+ (r.username ?? '(unknown)').padEnd(32) +
234
+ credAvailability(p.secrets, r.service));
235
+ first = false;
134
236
  }
135
237
  }
136
238
  });
@@ -220,6 +322,11 @@ function registerProfilesCommands(browser) {
220
322
  };
221
323
  await createProfile(profile);
222
324
  console.log(`Created profile: ${name}`);
325
+ // Warn (don't fail) if the declared secrets bundle doesn't exist yet — it
326
+ // may be created later, but a typo should surface now.
327
+ if (opts.secrets && !bundleExists(opts.secrets)) {
328
+ console.error(`warning: secrets bundle "${opts.secrets}" does not exist yet. Create it with: agents secrets create ${opts.secrets}`);
329
+ }
223
330
  });
224
331
  profiles
225
332
  .command('show <name>')
@@ -242,6 +349,9 @@ function registerProfilesCommands(browser) {
242
349
  }
243
350
  console.log(`Name: ${profile.name}`);
244
351
  console.log(`Browser: ${profile.browser}`);
352
+ if (getConfiguredDefaultProfileName() === profile.name) {
353
+ console.log('Default: yes (this machine — used by `agents browser start`)');
354
+ }
245
355
  if (profile.binary)
246
356
  console.log(`Binary: ${profile.binary}`);
247
357
  if (profile.electron)
@@ -273,6 +383,27 @@ function registerProfilesCommands(browser) {
273
383
  console.log(`Secrets: ${profile.secrets}`);
274
384
  if (profile.chrome?.headless)
275
385
  console.log(`Headless: true`);
386
+ // Login state per known service: live session + account identity + whether
387
+ // login creds are declared in the profile's secrets bundle.
388
+ const active = await loginsForProfile(profile.name);
389
+ const accounts = await accountsForProfile(profile.name);
390
+ const lines = [];
391
+ for (const service of Object.keys(AUTH_SIGNATURES)) {
392
+ const isActive = active.includes(service);
393
+ const creds = credAvailability(profile.secrets, service);
394
+ if (!isActive && creds === '-')
395
+ continue; // nothing interesting to show
396
+ const status = isActive
397
+ ? `logged in${accounts[service] ? ` as ${accounts[service]}` : ''}`
398
+ : 'logged out';
399
+ const credNote = creds !== '-' ? ` creds: ${creds}` : '';
400
+ lines.push(` ${service.padEnd(10)} ${status}${credNote}`);
401
+ }
402
+ if (lines.length > 0) {
403
+ console.log('Logins:');
404
+ for (const l of lines)
405
+ console.log(l);
406
+ }
276
407
  });
277
408
  profiles
278
409
  .command('delete <name>')
@@ -302,6 +433,10 @@ function registerProfilesCommands(browser) {
302
433
  }
303
434
  console.log(`Deleted profile: ${name}` +
304
435
  (removed > 0 ? ` (and ${removed} cache dir${removed === 1 ? '' : 's'})` : ''));
436
+ if (getConfiguredDefaultProfileName() === name) {
437
+ console.error(`warning: "${name}" was this machine's default browser profile; ` +
438
+ `\`agents browser start\` will auto-detect until you run: agents browser profiles set-default <name>`);
439
+ }
305
440
  });
306
441
  profiles
307
442
  .command('doctor <name>')
@@ -475,8 +610,8 @@ function registerProfilesCommands(browser) {
475
610
  function registerTaskCommands(browser) {
476
611
  browser
477
612
  .command('start')
478
- .description('Start a browser task. Pass --profile <name>, or omit to auto-pick a Chromium-family browser already installed on this machine.')
479
- .option('-p, --profile <name>', 'Browser profile to use (auto-picks from installed Chromium-family browsers if omitted)')
613
+ .description('Start a browser task. Pass --profile <name>; omit to use your configured default (`agents browser profiles set-default`), else auto-pick an installed Chromium-family browser.')
614
+ .option('-p, --profile <name>', 'Browser profile to use (omit to use the configured default, else auto-pick an installed Chromium-family browser)')
480
615
  .option(TASK_OPTION_FLAG, 'Task name (auto-generated if omitted)')
481
616
  .option('-e, --endpoint <name>', 'Endpoint preset (defaults to the profile\'s default)')
482
617
  .option('-u, --url <url>', 'Open URL in first tab')
@@ -497,6 +632,23 @@ function registerTaskCommands(browser) {
497
632
  process.exit(1);
498
633
  }
499
634
  }
635
+ else if (profileName === DEFAULT_BROWSER_PROFILE_NAME) {
636
+ // Explicit `--profile default` honors the configured default too, so an
637
+ // agent that hardcodes the reserved `default` name still lands on the
638
+ // user's chosen profile (e.g. their logged-in Comet) rather than a
639
+ // literal `default` (which auto-detects Chrome). Narrowly scoped: only
640
+ // the reserved name, only here in `start`.
641
+ const configured = getConfiguredDefaultProfileName();
642
+ if (configured && configured !== DEFAULT_BROWSER_PROFILE_NAME) {
643
+ const target = await getProfile(configured);
644
+ if (target) {
645
+ profileName = target.name;
646
+ }
647
+ else {
648
+ console.error(`warning: configured default profile "${configured}" not found; using "${DEFAULT_BROWSER_PROFILE_NAME}".`);
649
+ }
650
+ }
651
+ }
500
652
  // Pre-check the profile locally so we fail fast with a helpful error
501
653
  // instead of round-tripping a generic "Profile not found" through the daemon.
502
654
  const profile = await getProfile(profileName);
@@ -518,6 +670,26 @@ function registerTaskCommands(browser) {
518
670
  process.exit(1);
519
671
  }
520
672
  }
673
+ // Grounded login guardrail: if the opening URL is a known login-gated
674
+ // service and the resolved profile has no session for it, say so on stderr
675
+ // and name a profile that IS logged in. Reads the profile's real cookie DB
676
+ // (presence only) — never blocks or slows start; any failure is silent.
677
+ if (opts.url) {
678
+ try {
679
+ const service = serviceForUrl(opts.url);
680
+ if (service) {
681
+ const here = await loginsForProfile(profileName);
682
+ if (!here.includes(service)) {
683
+ const elsewhere = (await profilesLoggedInto(service)).filter((p) => p !== profileName);
684
+ const hint = elsewhere.length
685
+ ? `logged in elsewhere: ${elsewhere.join(', ')}. try: --profile ${elsewhere[0]}`
686
+ : `no profile on this machine has a ${service} session.`;
687
+ console.error(`warning: profile "${profileName}" (${profile.browser}) has no ${service} session. ${hint}`);
688
+ }
689
+ }
690
+ }
691
+ catch { /* login detection is best-effort; never block start */ }
692
+ }
521
693
  const response = await sendIPCRequest({
522
694
  action: 'start',
523
695
  profile: profileName,
@@ -1219,7 +1391,8 @@ function registerTaskCommands(browser) {
1219
1391
  .description('Type text into an element by ref')
1220
1392
  .option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
1221
1393
  .option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
1222
- .requiredOption('--text <text>', 'Text to type (use quotes for spaces/special chars)')
1394
+ .option('--text <text>', 'Text to type (use quotes for spaces/special chars)')
1395
+ .option('--secret <ref>', 'Resolve <bundle>/<KEY> from `agents secrets` and type the value. The secret is resolved in-process and never printed to stdout or the transcript. Alternative to --text.')
1223
1396
  .option('--clear', 'Clear editor content before typing')
1224
1397
  .action(async (ref, opts) => {
1225
1398
  const task = resolveTaskName(opts);
@@ -1228,18 +1401,53 @@ function registerTaskCommands(browser) {
1228
1401
  console.error(`<ref> must be an integer, got: ${ref}`);
1229
1402
  process.exit(1);
1230
1403
  }
1404
+ // Resolve the text to type: literal --text, or a --secret <bundle>/<KEY>
1405
+ // reference resolved in-process so the value never crosses stdout/transcript.
1406
+ let text = opts.text;
1407
+ if (opts.secret) {
1408
+ if (opts.text !== undefined) {
1409
+ console.error('Pass either --text or --secret, not both.');
1410
+ process.exit(1);
1411
+ }
1412
+ const parsed = parseSecretRef(opts.secret);
1413
+ if (!parsed) {
1414
+ console.error(`--secret must be <bundle>/<KEY>, got: ${opts.secret}`);
1415
+ process.exit(1);
1416
+ }
1417
+ if (!bundleExists(parsed.bundle)) {
1418
+ console.error(`Secrets bundle "${parsed.bundle}" not found.`);
1419
+ process.exit(1);
1420
+ }
1421
+ try {
1422
+ const { env } = readAndResolveBundleEnv(parsed.bundle, { caller: 'browser type', keys: [parsed.key] });
1423
+ if (!(parsed.key in env)) {
1424
+ console.error(`Key "${parsed.key}" not in bundle "${parsed.bundle}".`);
1425
+ process.exit(1);
1426
+ }
1427
+ text = env[parsed.key];
1428
+ }
1429
+ catch (err) {
1430
+ console.error(err.message);
1431
+ process.exit(1);
1432
+ }
1433
+ }
1434
+ if (text === undefined) {
1435
+ console.error('Provide --text <text> or --secret <bundle>/<KEY>.');
1436
+ process.exit(1);
1437
+ }
1231
1438
  const response = await sendIPCRequest({
1232
1439
  action: 'type',
1233
1440
  task,
1234
1441
  tabId: opts.tab,
1235
1442
  ref: refNum,
1236
- text: opts.text,
1443
+ text,
1237
1444
  clear: opts.clear,
1238
1445
  });
1239
1446
  if (!response.ok) {
1240
1447
  console.error(response.error);
1241
1448
  process.exit(1);
1242
1449
  }
1450
+ // Never echo the typed text — with --secret it would leak the credential.
1243
1451
  console.log('Typed');
1244
1452
  });
1245
1453
  browser
@@ -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';
@@ -7,6 +8,7 @@ import { insertTask, updateTaskStatus, getTaskById, listTasks as listStoredTasks
7
8
  import { renderStream } from '../lib/cloud/stream.js';
8
9
  import { MissingTargetError, MAX_IMAGES_PER_DISPATCH } from '../lib/cloud/types.js';
9
10
  import { normalizeTriggerEvent, validateTrigger, writeJob, jobExists, GITHUB_TRIGGER_EVENTS } from '../lib/routines.js';
11
+ import { emit } from '../lib/events.js';
10
12
  /** Map a supported image file extension to its wire mimeType. Rejects anything else. */
11
13
  function imageMimeFromPath(file) {
12
14
  const ext = path.extname(file).toLowerCase();
@@ -35,28 +37,6 @@ function parseSkillRef(raw) {
35
37
  return { id: raw };
36
38
  }
37
39
  /** 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
40
  /** Return a chalk color function appropriate for the given task status. */
61
41
  function statusColor(status) {
62
42
  switch (status) {
@@ -70,9 +50,6 @@ function statusColor(status) {
70
50
  default: return chalk.white;
71
51
  }
72
52
  }
73
- function isJsonMode(opts) {
74
- return Boolean(opts.json) || !process.stdout.isTTY;
75
- }
76
53
  /**
77
54
  * After a `MissingTargetError`, try to resolve the target interactively.
78
55
  * Returns the chosen id, or undefined when no interactive resolution is
@@ -370,6 +347,7 @@ Examples:
370
347
  }
371
348
  // Persist locally
372
349
  insertTask(task);
350
+ emit('cloud.dispatch', { module: 'cloud', taskId: task.id, agent: task.agent, provider: task.provider, status: task.status });
373
351
  if (json) {
374
352
  process.stdout.write(JSON.stringify(task) + '\n');
375
353
  }
@@ -395,6 +373,7 @@ Examples:
395
373
  summary: result.summary,
396
374
  prUrl: result.prUrl,
397
375
  });
376
+ emit('cloud.complete', { module: 'cloud', taskId: task.id, status: result.status, prUrl: result.prUrl });
398
377
  if (gated?.gate.breached()) {
399
378
  const b = gated.gate.breach();
400
379
  process.stderr.write(`[budget] cap ${b?.cap} exceeded — cancelled cloud task ${task.id}\n`);
@@ -557,6 +536,7 @@ Examples:
557
536
  summary: result.summary,
558
537
  prUrl: result.prUrl,
559
538
  });
539
+ emit('cloud.complete', { module: 'cloud', taskId: id, status: result.status, prUrl: result.prUrl });
560
540
  if (gated?.gate.breached()) {
561
541
  process.exitCode = 7;
562
542
  }
@@ -577,6 +557,7 @@ Examples:
577
557
  try {
578
558
  await provider.cancel(id);
579
559
  updateTaskStatus(id, 'cancelled');
560
+ emit('cloud.cancel', { module: 'cloud', taskId: id, provider: task.provider });
580
561
  console.log(chalk.green(`Task ${id} cancelled.`));
581
562
  }
582
563
  catch (err) {
@@ -595,6 +576,7 @@ Examples:
595
576
  try {
596
577
  await provider.message(id, text);
597
578
  updateTaskStatus(id, 'running');
579
+ emit('cloud.message', { module: 'cloud', taskId: id });
598
580
  console.log(chalk.green(`Message sent to task ${id}. Agent is continuing.`));
599
581
  }
600
582
  catch (err) {
@@ -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 };