@phnx-labs/agents-cli 1.20.46 → 1.20.48
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 +15 -1
- package/README.md +135 -12
- package/dist/commands/secrets-import.d.ts +18 -0
- package/dist/commands/secrets-import.js +74 -0
- package/dist/commands/secrets.js +2 -0
- package/dist/lib/daemon.js +31 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/install-menubar.d.ts +16 -0
- package/dist/lib/menubar/install-menubar.js +43 -1
- package/dist/lib/secrets/fallback.d.ts +48 -0
- package/dist/lib/secrets/fallback.js +48 -0
- package/dist/lib/secrets/index.d.ts +11 -0
- package/dist/lib/secrets/index.js +20 -2
- package/dist/lib/secrets/linux.d.ts +7 -0
- package/dist/lib/secrets/linux.js +113 -5
- package/dist/lib/secrets/windows.d.ts +7 -0
- package/dist/lib/secrets/windows.js +110 -5
- package/dist/lib/self-heal/checks/shims.js +19 -1
- package/dist/lib/session/discover.js +41 -0
- package/dist/lib/shims.d.ts +22 -0
- package/dist/lib/shims.js +79 -0
- package/dist/lib/versions.d.ts +16 -0
- package/dist/lib/versions.js +83 -12
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,10 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 1.20.48
|
|
6
|
+
|
|
7
|
+
- **Menu-bar helper: a RECENT TICKETS section shows the issues you filed via the quick-issue bar, each clickable to open in Linear.** The completion notification is transient, so the tickets the `Cmd-Shift-O` bar creates now also persist to a small local ledger (`~/.agents/.history/menubar/recent-tickets.json`, newest-first, deduped by id, capped at 10) that the menu-bar dropdown surfaces below RECENT sessions — click a row to open the ticket. The dispatch records the id + note + Linear URL on a successful create; the section renders nothing when the ledger is empty. Source: `apps/cli/menubar/Sources/MenubarHelper/{RecentTickets,StatusItemController,AgentsCLI,IssueSelfTest}.swift`.
|
|
8
|
+
- **Menu-bar helper: the quick-issue completion notification now deep-links to the created ticket, and the helper self-heals onto the install you actually run.** Two fixes from dogfooding the `Cmd-Shift-O` bar. (1) **Clickable notification** — the "Created RUSH-####" banner carried no click target, so there was no way to open the ticket. The ticket agent now also prints the issue's `URL:` line, the helper parses it, and clicking the notification (or its **Open** button) opens the ticket in Linear (via an `NSUserNotificationCenterDelegate`; the banner is also force-presented so it can't be silently swallowed when the accessory app is frontmost). (2) **Dual-install self-heal** — the helper bakes the node interpreter + CLI entry into its launchd plist so a GUI process can find `agents` without a login PATH, but the staleness check only re-baked on a *version* change. With two installs present (e.g. an nvm copy and a bun copy), the plist kept pointing at whichever copy first wrote it, so the menu data **and** the quick-issue dispatch ran on a stale install even after `agents upgrade`. The startup self-heal now also re-points when the plist's baked `AGENTS_ENTRY`/`AGENTS_NODE` no longer match the install currently running `agents` (a null active entry — a dev/tsx run — never churns the plist). Source: `apps/cli/menubar/Sources/MenubarHelper/{PromptPanel,AgentsCLI,IssueSelfTest}.swift`, `apps/cli/src/lib/menubar/install-menubar.ts`.
|
|
9
|
+
- **The npm release can now be driven from a Linux box** by offloading the Mac-only helper signing to a remote sign host. The tarball bundles two signed macOS `.app` helpers a Linux runner can't build — `bin/Agents CLI.app` (the keychain helper: `swiftc` universal → codesign with entitlements + embedded provisioning profile → `notarytool` → staple) and `bin/MenubarHelper.app` (the menu-bar status item: `swift build` → codesign, no notarization) — which is the only reason publishing was macOS-pinned. New `scripts/remote-sign-mac.sh` (invoked automatically by `release.sh` when it runs on a non-macOS host and the signed apps are absent, or on any host with `FORCE_REMOTE_SIGN=1`) rsyncs the build inputs to `${SIGN_HOST:-mac-mini}`, runs both Mac build scripts there under the appliance's headless signing creds (unlocks `rush-signing.keychain-db`, injects Apple notary creds via the `apple.com` secrets bundle), then pulls the signed `bin/*.app` back and re-verifies the keychain sha locally. The `build` script now copies the helpers into `dist/` on a **presence** gate (`[ -d 'bin/…' ]`) instead of `[ "$(uname)" = 'Darwin' ]`, so a Linux box that pulled the pre-signed bundles packages them, and `prepack`'s sha gate uses `shasum` or `sha256sum` (whichever is present) so it works on Linux too. Override the sign host with `SIGN_HOST` and its checkout with `SIGN_HOST_REPO`. Source: `apps/cli/scripts/remote-sign-mac.sh`, `apps/cli/scripts/release.sh`, `apps/cli/scripts/verify-keychain-helper.sh`, `apps/cli/package.json`.
|
|
10
|
+
|
|
11
|
+
- **The shim self-heal now repairs shims that point at a *removed* install and prunes orphaned command shims.** A dispatch shim bakes its `AGENTS_BIN` (the agents-cli entrypoint it execs) at generation time, so when that install moves or is deleted — a dev build under `~/.local/agents-cli-dev`, an old npm-global under `/opt/homebrew`, a rotated version dir — the shim keeps pointing at the dead path. Agent shims survive it via their runtime self-recovery block, but the previous self-heal only compared the *schema marker*, so a schema-current shim aimed at a removed install read as healthy and was never repaired. Two additions to the `shims` self-heal check (daemon + interactive startup): (1) **drift repair** — an agent shim whose baked `AGENTS_BIN` names a *different, now-missing* install is force-regenerated to the current install (`shimPointsAtLiveInstall`); a shim pointing at another install that still exists is left alone, so two live installs sharing the shims dir can't ping-pong. (2) **orphan prune** — legacy standalone command shims (`browser`/`secrets`/`sessions`/`teams`/`pty`) that a removed install left in the shims dir, which the current source never regenerates and which either die with `exit 127` or shadow the real package bin on PATH, are removed when their baked install is gone (`pruneOrphanedCommandShim`); user `agents alias` shims and any shim whose install still exists are spared. Verified end-to-end against a real machine carrying a deleted dev build + a removed Homebrew install: the agent shims repoint to the live install and five dead command shims are pruned. Source: `apps/cli/src/lib/shims.ts` (`shimPointsAtLiveInstall`, `pruneOrphanedCommandShim`, `listShimFileNames`), `apps/cli/src/lib/self-heal/checks/shims.ts`.
|
|
12
|
+
## 1.20.47
|
|
13
|
+
|
|
14
|
+
- **Quick-issue bar (`Cmd-Shift-O`): `Cmd-V` now pastes into the note field, and double-clicking a screenshot thumbnail opens it in Preview.** Two fixes from dogfooding the new bar. (1) The panel is a borderless `.accessory` window with **no main menu**, so the standard clipboard key-equivalents (`Cmd-V`/`C`/`X`/`A`) were never dispatched to the field editor — paste silently did nothing. `PromptPanel.performKeyEquivalent` now routes them through the responder chain so the text field handles them. (2) Thumbnails are small, so there was no way to confirm which screenshot you were attaching: **single click still toggles selection, double click opens the full image in the default viewer (Preview)**. The single-click toggle is deferred by the double-click interval so a double-click previews without also flipping the selection, and the bar suppresses its own click-outside dismissal while Preview takes focus (so summoning Preview never closes the bar or drops your typed note; it re-arms when the bar regains focus). Source: `apps/cli/menubar/Sources/MenubarHelper/PromptPanel.swift`.
|
|
15
|
+
- **Fix: the headless file-store fallback no longer silently shadows the OS keyring; NEW `agents secrets import-keyring` migrates stranded secrets into it.** On headless Linux/Windows the encrypted-file store is *sticky* — once any item is on disk, `preflight()` routed **every** op to the file store and never consulted GNOME Keyring / Windows Credential Manager again, so a secret written earlier into the native store (e.g. while a desktop keyring was unlocked) read back **empty** with no hint. This stranded real Linear CLI credentials in a locked keyring while other bundles lived in the file store, silently breaking the SessionStart hook. Two fixes: (1) `get`/`has` now **read through** to the native store on a file-store *miss* (the fast path and the non-fallback keychain-first path are untouched — the file store is still checked first), emitting a one-time stderr notice pointing at `import-keyring`; once a locked/`1312` error is seen the store is marked unreachable so it stops re-probing a known-dead store. (2) NEW **`agents secrets import-keyring`** — the Linux/Windows analogue of the macOS `migrate-acl`/orphan sweep — enumerates `agents-cli` items in the native store and copies them into the encrypted file store (the durable, passwordless headless backend). Dry-run by default; `--commit` writes; existing file-store items are never overwritten; Windows enumeration is floored to the `agents-cli.` namespace since Credential Manager targets have no service scoping. macOS is unaffected (it has no file fallback and keeps `migrate-acl`). Source: `apps/cli/src/lib/secrets/{fallback,linux,windows,index}.ts`, `apps/cli/src/commands/{secrets-import,secrets}.ts`, `apps/cli/docs/secrets.md`.
|
|
16
|
+
- **Launch-health self-heal now covers Windows, and the daemon repairs a gutted install proactively — before your next `agents run`.** #764 gave `agents run` an install/run-time self-heal (probe `<binary> --version`; clean-reinstall in place, else fall back to another installed version that launches), but it **skipped the probe on Windows** — `verifyInstalledBinaryLaunches` returned healthy on `win32` unconditionally, because probing the extensionless `.bin/<cli>` wrapper would ENOENT even on a *healthy* install. So the exact Windows failure the self-heal was built for went unhealed: a vendor auto-update renames the native `claude.exe` to `claude.exe.old.<epochMs>` and never lands the replacement, leaving the shim chain intact but pointing at a missing file, and every launch dies with `'…claude.exe' is not recognized`. The probe now runs on Windows against the **real launch target** — the npm `.cmd` wrapper `agents run` actually execs (`getBinaryPath + '.cmd'`, resolved via `cmd.exe`), which chains to the native `.exe` — so a gutted install trips the existing missing-binary signature (`is not recognized`) and is repaired by the same `ensureAgentRunnable` machinery; a missing `.cmd` (a non-npm/global agent like `droid.exe`) is still treated as healthy so a good install is never destroyed. Separately, the **daemon** now runs a proactive launch-health pass (`healBrokenDefaultLaunches`) ~90s after startup and every ~6h: it probes each agent's default version and, if it won't launch, repairs it in the background — so a gutted install is fixed *before* the next `agents run` hits the ENOENT, not at spawn time (the run-time `ensureAgentRunnable` only fires once a run is already starting). Verified end-to-end on a real Windows host: renaming `claude.exe` to `.old` makes the `.cmd` probe emit `is not recognized`; restoring it returns `2.1.191 (Claude Code)`. Source: `apps/cli/src/lib/versions.ts` (`verifyInstalledBinaryLaunches`, `healBrokenDefaultLaunches`), `apps/cli/src/lib/daemon.ts`.
|
|
17
|
+
|
|
18
|
+
## 1.20.45
|
|
19
|
+
|
|
5
20
|
## 1.20.46
|
|
6
21
|
|
|
7
22
|
- **NEW: `Cmd-Shift-O` opens a Spotlight-style quick-issue bar in the menu-bar helper — type a sentence, attach recent screenshots, and an agent files the Linear ticket for you.** The menu-bar helper already turned a screenshot into a `<host>:<path>` token with `Cmd-Shift-V` (clip capture), but there was no path from "I see a bug" to "a triaged ticket exists." The new chord summons a borderless panel (a thin capture surface, not another form): you type a one-line note, optionally toggle one or more recent screenshots (from the system screencapture folder, CleanShot's export path, or the clip history) as a thumbnail strip (the newest is pre-selected when it's fresh), and hit Return. It then **dispatches a headless agent** (`agents run claude --mode auto`, isolated behind one `AgentsCLI.dispatchTicketAgent` call so a cloud pod is a later swap) that reads the screenshots, runs `agents sessions` to identify which repo/project this concerns, does a brief investigation for real context, and files the ticket via `~/.agents/skills/linear/scripts/linear create` with an honest priority + a `repo:<name>` label — no preview step, the panel closes immediately and a notification reports the created `RUSH-####`. Focus is handled for a no-Dock `.accessory` app (`NSApp.activate` → `makeKeyAndOrderFront` → `makeFirstResponder`, with a borderless `NSPanel` overriding `canBecomeKey`; click-outside dismissal is armed only after the summon settles so the activation race can't self-dismiss the panel). The `Cmd-Shift-V` clip hotkey is unchanged — the Carbon hotkey manager now demultiplexes both chords by `EventHotKeyID.id` through one installed handler. Self-test: `MENUBAR_ISSUE_TEST=1 MenubarHelper` exercises screenshot selection, ticket-id parsing, and the meta-prompt contract; `MENUBAR_PROMPT_PREVIEW=1` renders the panel without the global hotkey for QA. Source: `apps/cli/menubar/Sources/MenubarHelper/{PromptPanel,Hotkey,AgentsCLI,main,IssueSelfTest,Clip}.swift`.
|
|
8
|
-
|
|
9
23
|
- **NEW: a unified self-heal subsystem — the shim/PATH "repair" notice no longer nags on every terminal, and the daemon now heals shim drift in the background.** agents-cli had accumulated ~37 separate repair routines scattered across the daemon, every CLI startup, and a handful of commands, each hand-rolling its own detect+fix on its own trigger. The most visible symptom: the interactive shim bootstrap (`maybeBootstrapShimIntegration`) regenerated shims, adopted shadowing launchers, and offered to add the shims dir to PATH **in the foreground on every invocation**, suppressed only by a `process.ppid`-keyed temp sentinel — so a new terminal re-ran the whole detect-and-nag, and the underlying condition was never permanently fixed. This lands a single `HealCheck` registry (`lib/self-heal/`) with one runner (`runSelfHeal`) driven by two front doors — the daemon (on its existing ~30s-after-start + ~6h `safe`-mode cycle) and the interactive startup — sharing the same checks: `shims` (regenerate stale shims/aliases), `shadowing` (adopt symlink launchers; report real-binary shadows), `path` (add the shims dir to PATH once), and `resources` (the existing `heal()` engine, wrapped unchanged). The daemon's heal cycle now runs all four in `safe` mode (low-risk fixes silently; risky ones reported), replacing the resource-only `heal()` call — and drops the desktop toast for background heals (the log is the record). The interactive startup now heals **silently** and prints at most a **persistent, once-per-condition** notice (`lib/shim-heal.ts`, keyed to a signature of the actionable state under `~/.agents/.cache/state/shim-notice.json`) for what a machine genuinely can't fix for you — a real native binary shadowing the shim — instead of re-nagging every shell. What changes is *where* the repairs run (background/silent) and *how often* you hear about them (once, not every terminal). Source: `apps/cli/src/lib/self-heal/` (new), `apps/cli/src/lib/shim-heal.ts` (new), `apps/cli/src/lib/daemon.ts`, `apps/cli/src/index.ts`, `apps/cli/src/lib/shims.ts` (`isShimCurrent` exported).
|
|
10
24
|
## 1.20.45
|
|
11
25
|
- **NEW: `agents run <agent> --host <name>` without a prompt forwards your TTY over SSH and runs the agent interactively on the remote host.** Previously `--host` runs required a prompt and were always headless (`agents run <agent> "<task>" --host <name>`). Now, omitting the prompt takes the interactive path: when local stdin is a TTY, the local CLI SSHes with `-tt`, runs `agents run <agent>` on the host, and lets the remote machine's `agents` start its normal tmux wrapper. The tmux session lives on the remote box, so detaching (`Ctrl-b d`) ends the SSH connection but keeps the agent running; you can reattach from the host or resume by session id. Session ids for Claude are still minted up front so `agents sessions` can surface and resolve the remote run. `--no-follow` is rejected for interactive host runs (it is meaningless for an attached TTY), and `--mode`, `--model`, `--name`, passthrough args after `--`, and `--raw`/`--no-tmux` are forwarded to the remote invocation. Source: `apps/cli/src/commands/exec.ts`, `apps/cli/src/lib/hosts/dispatch.ts`, `apps/cli/src/lib/hosts/session-index.ts`, `apps/cli/docs/hosts.md`.
|
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
<a href="https://github.com/phnx-labs/agents-cli"><img src="https://img.shields.io/badge/github-phnx--labs%2Fagents--cli-blue?style=flat-square" alt="github" /></a>
|
|
12
12
|
</p>
|
|
13
13
|
|
|
14
|
-
**The missing toolchain for CLI coding agents.** Run any agent on your existing subscription. Spawn parallel teams in isolated terminals. Schedule routines, drive browsers and Electron apps,
|
|
14
|
+
**The missing toolchain for CLI coding agents.** Run any agent on your existing subscription. Spawn parallel teams in isolated terminals or dispatch to the cloud for a PR. Watch live state across the fleet, nudge stalled runs, and message agents mid-flight. Schedule routines, drive browsers and Electron apps, store secrets behind Touch ID, and file tickets from a menu-bar bar — all from one CLI.
|
|
15
15
|
|
|
16
16
|
<p align="center">
|
|
17
17
|
<a href="https://github.com/anthropics/claude-code" title="Claude Code"><img src="assets/harnesses/anthropic.svg" height="32" alt="Claude Code" /></a>
|
|
@@ -51,14 +51,17 @@ Also available as `ag` -- all commands work with both `agents` and `ag`.
|
|
|
51
51
|
- [One config, every agent](#one-config-every-agent)
|
|
52
52
|
- [Run any agent](#run-any-agent)
|
|
53
53
|
- [Sessions across agents](#sessions-across-agents)
|
|
54
|
+
- [Control the fleet](#control-the-fleet)
|
|
54
55
|
- [Run open models through Claude Code](#run-open-models-through-claude-code)
|
|
55
56
|
- [Teams](#teams)
|
|
57
|
+
- [Cloud](#cloud)
|
|
56
58
|
- [Workflows](#workflows)
|
|
57
59
|
- [Browser](#browser)
|
|
58
60
|
- [Secrets](#secrets)
|
|
59
61
|
- [Routines](#routines)
|
|
60
62
|
- [PTY](#pty)
|
|
61
63
|
- [Portable setup](#portable-setup)
|
|
64
|
+
- [Menu bar](#menu-bar)
|
|
62
65
|
- [Private skills](#private-skills)
|
|
63
66
|
- [Security & Privacy](#security--privacy)
|
|
64
67
|
- [Compatibility](#compatibility)
|
|
@@ -188,7 +191,62 @@ agents sessions a1b2c3d4 --last 3 --include user
|
|
|
188
191
|
|
|
189
192
|
Interactive picker when you're in a terminal. Structured output (`--json`, `--markdown`, filtered by role or turn count) when piped.
|
|
190
193
|
|
|
191
|
-
Backed by a SQLite + FTS5 index at `~/.agents/.history/sessions/sessions.db` with incremental scanning -- warm reads in ~100ms. External tools can consume `--json` output as a programmatic observability layer; see [docs/05-sessions.md](docs/05-sessions.md) for the schema and [docs/06-observability.md](docs/06-observability.md) for the consumption patterns.
|
|
194
|
+
Backed by a SQLite + FTS5 index at `~/.agents/.history/sessions/sessions.db` with incremental scanning -- warm reads in ~100ms. External tools can consume `--json` output as a programmatic observability layer; see [docs/05-sessions.md](apps/cli/docs/05-sessions.md) for the schema and [docs/06-observability.md](apps/cli/docs/06-observability.md) for the consumption patterns.
|
|
195
|
+
|
|
196
|
+
### Live state, and catching up fast
|
|
197
|
+
|
|
198
|
+
Search is the past tense. `--active` is the present -- it infers what each running session is *doing right now* from the tail of its transcript.
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
agents sessions --active # every live run across the fleet, with state
|
|
202
|
+
agents sessions focus a1b2c3d4 # jump back into one — attach in place, or resume
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Each live session resolves to `working`, `waiting_input` (with why -- a question, a plan review, or a permission prompt), or `idle`, alongside badges for the PR it opened, the worktree it sits in, and the ticket it's working. `agents sessions focus [id]` attaches the live pane in place -- the tmux split locally or over SSH, or its Ghostty tab -- and falls back to a fresh tab + resume when the terminal is gone.
|
|
206
|
+
|
|
207
|
+
Landing on a session cold? `agents sessions <id>` prints a catch-up digest: an inferred title, files changed grouped by directory (created / modified / deleted), a histogram of which tools did the work, and the last test verdict -- the signals to reload a task in seconds.
|
|
208
|
+
|
|
209
|
+
### Resume anywhere — and stay resumed
|
|
210
|
+
|
|
211
|
+
Pick up any past conversation and drop it back into a terminal:
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
agents sessions resume # multi-select; packs two sessions per tab
|
|
215
|
+
agents sessions resume "auth middleware" # pre-filter the pool, then choose
|
|
216
|
+
agents sessions resume --tmux # into persistent tmux — survives editor restarts
|
|
217
|
+
agents sessions resume --host zion --tmux # resume on another machine over SSH
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
`agents sessions resume` reopens sessions in whatever terminal you're in -- auto-detected across iTerm, Ghostty, tmux, and the VSCodium agent-terminal, or forced with `--iterm` / `--ghostty` / `--tmux` / `--vscodium`. Back them with **tmux** and the runs turn durable: detach, close your editor, reboot the GUI -- the session is still alive to `agents tmux attach`. The whole `agents tmux` subsystem (persistent multiplexer sessions that survive editor restarts and can be shared with other tools) sits underneath.
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
## Control the fleet
|
|
225
|
+
|
|
226
|
+
Running agents aren't fire-and-forget. Steer them mid-run without opening their terminals.
|
|
227
|
+
|
|
228
|
+
<p align="center">
|
|
229
|
+
<img src="assets/fleet-control.svg" alt="agents sessions infers live state (working, waiting, idle); watchdog injects Continue into the exact stalled split; message reaches a running agent at its next tool call" width="100%" />
|
|
230
|
+
</p>
|
|
231
|
+
|
|
232
|
+
### Message a running agent
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
# Delivered at the agent's next tool call — no restart, no lost context.
|
|
236
|
+
agents message tester "also cover the null case"
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
`agents message <target> <text>` reaches any running agent by name or id -- a live local run, a teammate, a loop agent, or a cloud task -- and the text lands at its next tool call. Tag the sender with `--from <who>`.
|
|
240
|
+
|
|
241
|
+
### Auto-nudge stalls
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
agents watchdog # one tick, dry run — reports what it WOULD nudge and why
|
|
245
|
+
agents watchdog --nudge # actually inject "Continue." into the stalled split
|
|
246
|
+
agents watchdog --watch # daemon loop: a tick every --interval
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
`agents watchdog` detects a stalled session, resolves the *exact* terminal split it lives in (tmux, iTerm, VSCodium, or a raw pty), and injects a nudge -- `Continue.` by default, or set `--text`. It's dry by default; `--nudge` acts on a single tick, and `agents watchdog enable` flips global auto-nudge on so `--watch` injects on its own. Steer a single run with `agents watchdog policy <id> off | keep | handsoff`.
|
|
192
250
|
|
|
193
251
|
---
|
|
194
252
|
|
|
@@ -242,7 +300,8 @@ agents hosts add gpu-box
|
|
|
242
300
|
agents hosts check gpu-box # reachable? which agents-cli version?
|
|
243
301
|
|
|
244
302
|
# Run there instead of locally
|
|
245
|
-
agents run claude --host gpu-box "profile this build" # follows live by default
|
|
303
|
+
agents run claude --host gpu-box "profile this build" # headless: follows live by default
|
|
304
|
+
agents run claude --host gpu-box # no prompt → interactive TTY over SSH (tmux-backed)
|
|
246
305
|
agents logs --host gpu-box # pick a dispatched run — concise summary by default
|
|
247
306
|
agents logs <id> --full # the full raw transcript / stdout (token-heavy)
|
|
248
307
|
agents logs <id> -f # re-attach to a running one and follow
|
|
@@ -255,9 +314,9 @@ agents ssh mac-mini # hardened SSH: fails fast if offline,
|
|
|
255
314
|
# PowerShell on Windows, password-from-Keychain
|
|
256
315
|
```
|
|
257
316
|
|
|
258
|
-
**Hosts** (`agents hosts`) are git-synced dispatch targets in `agents.yaml`; **devices** (`agents devices`) are your Tailscale machines in a local registry. Both ride SSH. See [docs/00-concepts.md](docs/00-concepts.md#devices--hosts).
|
|
317
|
+
**Hosts** (`agents hosts`) are git-synced dispatch targets in `agents.yaml`; **devices** (`agents devices`) are your Tailscale machines in a local registry. Both ride SSH. See [docs/00-concepts.md](apps/cli/docs/00-concepts.md#devices--hosts).
|
|
259
318
|
|
|
260
|
-
Every `--host` command rides one multiplexed SSH engine, tuned for driving a fleet from a small laptop: the first call to a machine opens a control socket and every later call reuses it (no repeat TCP+auth handshake), connections carry keepalive so a dropped link dies in ~45 s instead of zombying, and following a remote run polls in a single round-trip per cycle. Measured against a Tailscale-relayed host: repeated calls **~6–7× faster**, dispatch readiness **~2×**, and the follow loop **~21× faster with 50% fewer local ssh spawns**. Design: [docs/09-ssh-transport.md](docs/09-ssh-transport.md) · reproduce: `node scripts/bench-ssh.mjs <host>`.
|
|
319
|
+
Every `--host` command rides one multiplexed SSH engine, tuned for driving a fleet from a small laptop: the first call to a machine opens a control socket and every later call reuses it (no repeat TCP+auth handshake), connections carry keepalive so a dropped link dies in ~45 s instead of zombying, and following a remote run polls in a single round-trip per cycle. Measured against a Tailscale-relayed host: repeated calls **~6–7× faster**, dispatch readiness **~2×**, and the follow loop **~21× faster with 50% fewer local ssh spawns**. Design: [docs/09-ssh-transport.md](apps/cli/docs/09-ssh-transport.md) · reproduce: `node scripts/bench-ssh.mjs <host>`.
|
|
261
320
|
|
|
262
321
|
---
|
|
263
322
|
|
|
@@ -277,7 +336,39 @@ agents teams status auth-feature # Who's working, what they changed, what the
|
|
|
277
336
|
|
|
278
337
|
Teammates run detached -- close your terminal, they keep working. Check in with `teams status`, glance at a teammate's summary with `teams logs <name>` (add `--full` for the raw output), clean up with `teams disband`.
|
|
279
338
|
|
|
280
|
-
Team state is observable via `agents teams list --json` / `agents teams status --json` (compact by default; add `--verbose` for the full per-teammate shape). External tools join it with `sessions --json` (teammates get `isTeamOrigin: true`) and `cloud list --json` (for `--cloud` teammates) to build a unified fleet view. See [docs/06-observability.md](docs/06-observability.md).
|
|
339
|
+
Team state is observable via `agents teams list --json` / `agents teams status --json` (compact by default; add `--verbose` for the full per-teammate shape). External tools join it with `sessions --json` (teammates get `isTeamOrigin: true`) and `cloud list --json` (for `--cloud` teammates) to build a unified fleet view. See [docs/06-observability.md](apps/cli/docs/06-observability.md).
|
|
340
|
+
|
|
341
|
+
---
|
|
342
|
+
|
|
343
|
+
## Cloud
|
|
344
|
+
|
|
345
|
+
Some work shouldn't tie up your laptop. `agents cloud run` hands a task to a managed provider that clones the repo, plans, implements, tests, and opens a PR -- while your terminal stays free.
|
|
346
|
+
|
|
347
|
+
<p align="center">
|
|
348
|
+
<img src="assets/cloud.svg" alt="agents cloud run dispatches one prompt to a managed provider (Rush, Codex, Factory, or Antigravity) that clones, plans, tests, and opens a pull request while you keep working" width="100%" />
|
|
349
|
+
</p>
|
|
350
|
+
|
|
351
|
+
```bash
|
|
352
|
+
# Dispatch and detach — streams to the cloud, not your terminal.
|
|
353
|
+
agents cloud run "fix the flaky test in the payments suite" \
|
|
354
|
+
--provider rush --repo acme/api --branch main
|
|
355
|
+
|
|
356
|
+
agents cloud list # what's running, queued, or needs review
|
|
357
|
+
agents cloud logs <id> # re-attach and stream
|
|
358
|
+
agents cloud message <id> "also update the changelog" # steer it mid-run
|
|
359
|
+
agents cloud cancel <id>
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
Four managed backends behind one interface (`agents cloud providers`):
|
|
363
|
+
|
|
364
|
+
| Provider | What runs | Notes |
|
|
365
|
+
|---|---|---|
|
|
366
|
+
| `rush` | Claude against a GitHub repo + branch | Opens a PR. Multi-repo via repeatable `--repo`; attach screenshots with `--image` for vision dispatch. |
|
|
367
|
+
| `codex` | A pre-built Codex Cloud environment | Target it with `--env`. |
|
|
368
|
+
| `factory` | `droid exec` on a cloud VM | Computer-use; pick the box with `--computer`. |
|
|
369
|
+
| `antigravity` | Gemini managed agents | Antigravity harness in a remote sandbox. |
|
|
370
|
+
|
|
371
|
+
Auto-routes each `--agent` to its native cloud, or pin the backend with `--provider`. Instead of dispatching now, register a run as an **event trigger** with `--on pull_request` (also `push`, `issue_comment`, `workflow_run`) -- it persists as a trigger-bound routine that fires on the event. `--json` on every subcommand for scripting.
|
|
281
372
|
|
|
282
373
|
---
|
|
283
374
|
|
|
@@ -582,9 +673,30 @@ Two repos with the same shape, different roles:
|
|
|
582
673
|
|
|
583
674
|
**Resource resolution:** When syncing resources (commands, skills, rules, hooks, MCP, permissions), the order is **project > user > system**. A `.agents/` directory at project root wins, then `~/.agents/`, then `~/.agents-system/`. Same-named resources higher in the chain override lower ones; everything else unions in.
|
|
584
675
|
|
|
585
|
-
See [docs/00-concepts.md](docs/00-concepts.md) for the full mental model: DotAgents repos, resource kinds, and how resolution works end-to-end.
|
|
676
|
+
See [docs/00-concepts.md](apps/cli/docs/00-concepts.md) for the full mental model: DotAgents repos, resource kinds, and how resolution works end-to-end.
|
|
677
|
+
|
|
678
|
+
Other useful commands: `agents doctor` checks CLI availability and resource sync drift, `agents usage` shows available quota/rate-limit data for installed agents, `agents budget` shows cross-vendor spend caps and current spend-to-cap (and enforces pre-flight estimates + a hard-cap kill-switch on every run — see [docs/06-observability.md](apps/cli/docs/06-observability.md#budget-guardrails-agents-budget)), `agents import` adopts an existing unmanaged install, `agents trash` lists and restores soft-deleted version directories, and `agents subagents` installs reusable subagent definitions for parent-agent workflows.
|
|
586
679
|
|
|
587
|
-
|
|
680
|
+
---
|
|
681
|
+
|
|
682
|
+
## Menu bar
|
|
683
|
+
|
|
684
|
+
On macOS, `agents-cli` puts a status item in your menu bar -- a live glance at what your agents are doing, plus a Spotlight-style bar for filing work without breaking focus.
|
|
685
|
+
|
|
686
|
+
```bash
|
|
687
|
+
agents menubar enable # install + launch at login
|
|
688
|
+
agents menubar status # is it installed and running?
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
The dropdown surfaces a **NEEDS YOU** queue (agents waiting on a question, a plan review, or a permission prompt), the running roster, and a routines summary -- the same live state as `agents sessions --active`, one click away.
|
|
692
|
+
|
|
693
|
+
### Quick-issue bar (⌘⇧O)
|
|
694
|
+
|
|
695
|
+
Press `Cmd-Shift-O` anywhere for a thin capture surface: type a one-line note, `Cmd-V` to paste, and attach one or more recent screenshots (double-click a thumbnail to preview it in full). Submit, and a headless agent picks the right project from your recent sessions, investigates, and files the Linear ticket itself -- you never leave what you were doing.
|
|
696
|
+
|
|
697
|
+
<p align="center">
|
|
698
|
+
<img src="assets/menubar-quickissue.svg" alt="The Cmd-Shift-O quick-issue bar: a one-line note with attached screenshot thumbnails that a headless agent turns into a filed Linear ticket" width="100%" />
|
|
699
|
+
</p>
|
|
588
700
|
|
|
589
701
|
---
|
|
590
702
|
|
|
@@ -675,7 +787,7 @@ By default, secrets sync via iCloud Keychain to your other Macs. With `--no-iclo
|
|
|
675
787
|
|
|
676
788
|
## Compatibility
|
|
677
789
|
|
|
678
|
-
Which DotAgents resources each agent CLI can load. Source of truth: [src/lib/agents.ts](src/lib/agents.ts) (`capabilities`); gates use `supports(agent, cap, version)` from [src/lib/capabilities.ts](src/lib/capabilities.ts). Full matrix also in [docs/00-concepts.md](docs/00-concepts.md).
|
|
790
|
+
Which DotAgents resources each agent CLI can load. Source of truth: [src/lib/agents.ts](apps/cli/src/lib/agents.ts) (`capabilities`); gates use `supports(agent, cap, version)` from [src/lib/capabilities.ts](apps/cli/src/lib/capabilities.ts). Full matrix also in [docs/00-concepts.md](apps/cli/docs/00-concepts.md).
|
|
679
791
|
|
|
680
792
|
> **† Gemini CLI is deprecated.** Google retired it for free, Pro, and Ultra tiers on **June 18, 2026** (announced at Google I/O 2026); the `gemini` command no longer serves requests on those tiers. agents-cli still manages existing installs, but warns on `agents add gemini` and `agents teams add … gemini`. New setups should use **Antigravity CLI** (`antigravity`), Google's official successor — see [the transition notice](https://developers.googleblog.com/an-important-update-transitioning-gemini-cli-to-antigravity-cli/).
|
|
681
793
|
|
|
@@ -794,21 +906,32 @@ Profiles (experimental — available by default). Works with LiteLLM Proxy, Olla
|
|
|
794
906
|
|
|
795
907
|
### Can I add support for a new agent?
|
|
796
908
|
|
|
797
|
-
Agents are defined in [src/lib/agents.ts](src/lib/agents.ts) -- each is a config object declaring commands dir, rules file, and capabilities. PRs welcome.
|
|
909
|
+
Agents are defined in [src/lib/agents.ts](apps/cli/src/lib/agents.ts) -- each is a config object declaring commands dir, rules file, and capabilities. PRs welcome.
|
|
798
910
|
|
|
799
911
|
### What's the relationship to Phoenix Labs / Rush?
|
|
800
912
|
|
|
801
913
|
`agents-cli` is an open client maintained by Phoenix Labs. Rush is a separate product. No Rush account required, no upsell.
|
|
802
914
|
|
|
915
|
+
## This monorepo also contains
|
|
916
|
+
|
|
917
|
+
`@phnx-labs/agents-cli` is the published package and this README is its front page — but the repo houses more. No JS workspaces: each package installs and builds independently (`bun install` inside it).
|
|
918
|
+
|
|
919
|
+
| Path | What |
|
|
920
|
+
|---|---|
|
|
921
|
+
| [`apps/cli`](apps/cli) | **The CLI** (this README) — version management, config sync, sessions, teams, cloud, browser, computer, secrets. |
|
|
922
|
+
| [`apps/factory`](apps/factory) | **Factory** — a VS Code extension that spawns agent terminals as tabs and adds the Factory Floor dashboard. A separate product with its own publish identity. |
|
|
923
|
+
| [`native/computer-mac`](native/computer-mac) · [`native/computer-win`](native/computer-win) | Native backends behind `agents computer` — Swift (macOS Accessibility + screen capture) and C#/.NET (Windows UI Automation). |
|
|
924
|
+
| [`packages/session-tracker`](packages/session-tracker) | The `SessionStart` hook that writes live-session state the CLI reads back. |
|
|
925
|
+
|
|
803
926
|
## Contributing
|
|
804
927
|
|
|
805
928
|
```bash
|
|
806
929
|
git clone https://github.com/phnx-labs/agents-cli
|
|
807
|
-
cd agents-cli
|
|
930
|
+
cd agents-cli/apps/cli
|
|
808
931
|
bun install && bun run build && bun test
|
|
809
932
|
```
|
|
810
933
|
|
|
811
|
-
Commands in [src/commands
|
|
934
|
+
Commands in [`apps/cli/src/commands/`](apps/cli/src/commands/), libraries in [`apps/cli/src/lib/`](apps/cli/src/lib/), tests as `*.test.ts` under vitest. [CLAUDE.md](CLAUDE.md) has the full style guide. [docs/04-landscape.md](apps/cli/docs/04-landscape.md) covers the competitive landscape.
|
|
812
935
|
|
|
813
936
|
## License
|
|
814
937
|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agents secrets import-keyring` — migrate agents-cli secrets out of the native
|
|
3
|
+
* credential store (GNOME Keyring / Windows Credential Manager) and into the
|
|
4
|
+
* encrypted file store.
|
|
5
|
+
*
|
|
6
|
+
* Why: on headless Linux/Windows the file store is the durable, passwordless
|
|
7
|
+
* backend, but secrets written earlier (e.g. while a desktop keyring was
|
|
8
|
+
* unlocked) can linger in the native store where a headless session can't reach
|
|
9
|
+
* them. This is the Linux/Windows analogue of the macOS `migrate-acl` /
|
|
10
|
+
* orphan sweep. Dry-run by default; `--commit` performs the copy.
|
|
11
|
+
*
|
|
12
|
+
* Requires the native store to be reachable/unlocked — a locked keyring can't be
|
|
13
|
+
* read, so unlock it first (or the values are already only in the file store and
|
|
14
|
+
* there is nothing to do).
|
|
15
|
+
*/
|
|
16
|
+
import type { Command } from 'commander';
|
|
17
|
+
/** Register `agents secrets import-keyring` on the parent secrets Command. */
|
|
18
|
+
export declare function registerSecretsImportKeyringCommand(secrets: Command): void;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agents secrets import-keyring` — migrate agents-cli secrets out of the native
|
|
3
|
+
* credential store (GNOME Keyring / Windows Credential Manager) and into the
|
|
4
|
+
* encrypted file store.
|
|
5
|
+
*
|
|
6
|
+
* Why: on headless Linux/Windows the file store is the durable, passwordless
|
|
7
|
+
* backend, but secrets written earlier (e.g. while a desktop keyring was
|
|
8
|
+
* unlocked) can linger in the native store where a headless session can't reach
|
|
9
|
+
* them. This is the Linux/Windows analogue of the macOS `migrate-acl` /
|
|
10
|
+
* orphan sweep. Dry-run by default; `--commit` performs the copy.
|
|
11
|
+
*
|
|
12
|
+
* Requires the native store to be reachable/unlocked — a locked keyring can't be
|
|
13
|
+
* read, so unlock it first (or the values are already only in the file store and
|
|
14
|
+
* there is nothing to do).
|
|
15
|
+
*/
|
|
16
|
+
import chalk from 'chalk';
|
|
17
|
+
import { importNativeItems } from '../lib/secrets/index.js';
|
|
18
|
+
/** Register `agents secrets import-keyring` on the parent secrets Command. */
|
|
19
|
+
export function registerSecretsImportKeyringCommand(secrets) {
|
|
20
|
+
secrets
|
|
21
|
+
.command('import-keyring')
|
|
22
|
+
.description('Migrate agents-cli secrets from the OS keyring / Credential Manager into the encrypted file store (headless-safe). Dry-run by default.')
|
|
23
|
+
.option('--commit', 'Perform the import (default is dry-run reporting only)')
|
|
24
|
+
.option('--prefix <p>', 'Only import items beginning with PREFIX (default: all agents-cli items)')
|
|
25
|
+
.action((opts) => {
|
|
26
|
+
try {
|
|
27
|
+
if (process.platform === 'darwin') {
|
|
28
|
+
throw new Error('import-keyring is for the Linux/Windows file-store fallback. On macOS use `agents secrets migrate-acl`.');
|
|
29
|
+
}
|
|
30
|
+
const commit = !!opts.commit;
|
|
31
|
+
const report = importNativeItems(opts.prefix ?? '', commit);
|
|
32
|
+
if (!report.available) {
|
|
33
|
+
console.log(chalk.gray('No native credential tooling found (secret-tool / PowerShell) — nothing to import.'));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (report.locked) {
|
|
37
|
+
console.error(chalk.yellow('The native credential store is locked/unreachable, so its secrets can\'t be read. ' +
|
|
38
|
+
'Unlock it and retry (a locked store can\'t be migrated).'));
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
if (report.results.length === 0) {
|
|
42
|
+
console.log(chalk.green('Nothing to import — no native secrets outside the file store.'));
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const imported = report.results.filter((r) => r.status === 'imported' || r.status === 'would-import');
|
|
46
|
+
const existing = report.results.filter((r) => r.status === 'exists');
|
|
47
|
+
const failed = report.results.filter((r) => r.status === 'failed');
|
|
48
|
+
for (const r of report.results) {
|
|
49
|
+
if (r.status === 'imported')
|
|
50
|
+
console.log(` ${chalk.green('imported')} ${r.item}`);
|
|
51
|
+
else if (r.status === 'would-import')
|
|
52
|
+
console.log(` ${chalk.cyan('would import')} ${r.item}`);
|
|
53
|
+
else if (r.status === 'exists')
|
|
54
|
+
console.log(` ${chalk.gray('exists')} ${r.item} ${chalk.gray('(already in file store)')}`);
|
|
55
|
+
else
|
|
56
|
+
console.log(` ${chalk.red('failed')} ${r.item} ${chalk.gray(r.detail ?? '')}`);
|
|
57
|
+
}
|
|
58
|
+
console.log();
|
|
59
|
+
if (!commit) {
|
|
60
|
+
console.log(chalk.gray(`Dry-run: ${imported.length} would be imported, ${existing.length} already present, ${failed.length} unreadable. Pass --commit to write.`));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (failed.length > 0) {
|
|
64
|
+
console.error(chalk.yellow(`Imported ${imported.length}; ${existing.length} already present; ${failed.length} failed.`));
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
console.log(chalk.green(`Imported ${imported.length} secret(s) into the file store (${existing.length} already present).`));
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
console.error(chalk.red(err.message));
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
}
|
package/dist/commands/secrets.js
CHANGED
|
@@ -25,6 +25,7 @@ import { registerCommandGroups, setHelpSections } from '../lib/help.js';
|
|
|
25
25
|
import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
26
26
|
import { registerSecretsSyncCommands } from './secrets-sync.js';
|
|
27
27
|
import { registerSecretsMigrateAclCommand } from './secrets-migrate.js';
|
|
28
|
+
import { registerSecretsImportKeyringCommand } from './secrets-import.js';
|
|
28
29
|
/** Prompt the user for a secret value with masked input. Requires an interactive TTY. */
|
|
29
30
|
async function promptForSecret(message) {
|
|
30
31
|
if (!isInteractiveTerminal()) {
|
|
@@ -1817,6 +1818,7 @@ Examples:
|
|
|
1817
1818
|
});
|
|
1818
1819
|
registerSecretsSyncCommands(cmd);
|
|
1819
1820
|
registerSecretsMigrateAclCommand(cmd);
|
|
1821
|
+
registerSecretsImportKeyringCommand(cmd);
|
|
1820
1822
|
}
|
|
1821
1823
|
/** Validate a prompt-policy value, throwing a clear message on a bad one (the
|
|
1822
1824
|
* caller's try/catch renders it and exits). Accepts the legacy `biometry` /
|
package/dist/lib/daemon.js
CHANGED
|
@@ -454,6 +454,35 @@ export async function runDaemon() {
|
|
|
454
454
|
};
|
|
455
455
|
const tmuxReconcileInterval = setInterval(() => { void runTmuxReconcile(); }, 5 * 60_000);
|
|
456
456
|
const tmuxReconcileKickoff = setTimeout(() => { void runTmuxReconcile(); }, 20_000);
|
|
457
|
+
// Launch-health self-heal: probe that each agent's DEFAULT version actually
|
|
458
|
+
// LAUNCHES (not just that its files exist), and repair a gutted install — the
|
|
459
|
+
// JS wrapper present but its native binary renamed/missing (a vendor
|
|
460
|
+
// auto-update that never landed its replacement, or a partially-extracted
|
|
461
|
+
// tarball) — BEFORE the user's next `agents run` dies with a raw ENOENT. This
|
|
462
|
+
// is the proactive companion to the run-time heal (ensureAgentRunnable), which
|
|
463
|
+
// only fires once a run is already starting. Cheap steady-state: one
|
|
464
|
+
// `--version` probe per default version; a clean reinstall runs only on a real
|
|
465
|
+
// launch failure. ~every 6h, plus once ~90s after startup (staggered off launch).
|
|
466
|
+
let checkingLaunchHealth = false;
|
|
467
|
+
const runLaunchHealthCheck = async () => {
|
|
468
|
+
if (checkingLaunchHealth)
|
|
469
|
+
return;
|
|
470
|
+
checkingLaunchHealth = true;
|
|
471
|
+
try {
|
|
472
|
+
const { healBrokenDefaultLaunches } = await import('./versions.js');
|
|
473
|
+
const repaired = await healBrokenDefaultLaunches((m) => log('INFO', `launch-health: ${m}`));
|
|
474
|
+
if (repaired.length)
|
|
475
|
+
log('INFO', `launch-health: repaired ${repaired.join(', ')}`);
|
|
476
|
+
}
|
|
477
|
+
catch (err) {
|
|
478
|
+
log('ERROR', `launch-health check failed: ${err.message}`);
|
|
479
|
+
}
|
|
480
|
+
finally {
|
|
481
|
+
checkingLaunchHealth = false;
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
const launchHealthInterval = setInterval(() => { void runLaunchHealthCheck(); }, 6 * 60 * 60_000);
|
|
485
|
+
const launchHealthKickoff = setTimeout(() => { void runLaunchHealthCheck(); }, 90_000);
|
|
457
486
|
const handleReload = () => {
|
|
458
487
|
log('INFO', 'Reloading jobs (SIGHUP)');
|
|
459
488
|
scheduler.reloadAll();
|
|
@@ -475,6 +504,8 @@ export async function runDaemon() {
|
|
|
475
504
|
clearTimeout(deviceProbeKickoff);
|
|
476
505
|
clearInterval(tmuxReconcileInterval);
|
|
477
506
|
clearTimeout(tmuxReconcileKickoff);
|
|
507
|
+
clearInterval(launchHealthInterval);
|
|
508
|
+
clearTimeout(launchHealthKickoff);
|
|
478
509
|
removeDaemonPid();
|
|
479
510
|
process.exit(0);
|
|
480
511
|
};
|
|
Binary file
|
|
@@ -48,6 +48,22 @@ export declare function isMenubarStale(opts: {
|
|
|
48
48
|
currentVersion: string;
|
|
49
49
|
execExists: boolean;
|
|
50
50
|
}): boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Pure re-point decision (no I/O): the plist's baked interpreter/entry no longer
|
|
53
|
+
* match the install that is now running `agents`. This catches DUAL-INSTALL skew
|
|
54
|
+
* that a version bump can't — e.g. the plist was baked by an nvm copy but the
|
|
55
|
+
* user's `agents` now resolves to a bun copy (same or different version), so the
|
|
56
|
+
* helper keeps shelling the stale install for its menu data AND the quick-issue
|
|
57
|
+
* dispatch. A null active entry (a dev/tsx run where the compiled entry can't be
|
|
58
|
+
* resolved) never triggers a re-point, so it can't churn the plist onto a
|
|
59
|
+
* transient path.
|
|
60
|
+
*/
|
|
61
|
+
export declare function menubarPlistNeedsRepoint(opts: {
|
|
62
|
+
plistEntry: string | null;
|
|
63
|
+
plistNode: string | null;
|
|
64
|
+
activeEntry: string | null;
|
|
65
|
+
activeNode: string | null;
|
|
66
|
+
}): boolean;
|
|
51
67
|
/**
|
|
52
68
|
* Stop + remove the menu-bar service and write the sticky opt-out so the
|
|
53
69
|
* upgrade migration won't re-enable it.
|
|
@@ -273,6 +273,45 @@ function menubarSetupStale() {
|
|
|
273
273
|
execExists: fs.existsSync(installedExecutablePath()),
|
|
274
274
|
});
|
|
275
275
|
}
|
|
276
|
+
/**
|
|
277
|
+
* Pure re-point decision (no I/O): the plist's baked interpreter/entry no longer
|
|
278
|
+
* match the install that is now running `agents`. This catches DUAL-INSTALL skew
|
|
279
|
+
* that a version bump can't — e.g. the plist was baked by an nvm copy but the
|
|
280
|
+
* user's `agents` now resolves to a bun copy (same or different version), so the
|
|
281
|
+
* helper keeps shelling the stale install for its menu data AND the quick-issue
|
|
282
|
+
* dispatch. A null active entry (a dev/tsx run where the compiled entry can't be
|
|
283
|
+
* resolved) never triggers a re-point, so it can't churn the plist onto a
|
|
284
|
+
* transient path.
|
|
285
|
+
*/
|
|
286
|
+
export function menubarPlistNeedsRepoint(opts) {
|
|
287
|
+
if (!opts.activeEntry)
|
|
288
|
+
return false; // can't resolve the running install — don't churn
|
|
289
|
+
if (opts.plistEntry !== opts.activeEntry)
|
|
290
|
+
return true;
|
|
291
|
+
if (opts.activeNode && opts.plistNode !== opts.activeNode)
|
|
292
|
+
return true;
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
/** Read one EnvironmentVariables value from the installed service plist. */
|
|
296
|
+
function readPlistEnvValue(key) {
|
|
297
|
+
try {
|
|
298
|
+
const xml = fs.readFileSync(servicePlistPath(), 'utf-8');
|
|
299
|
+
const m = xml.match(new RegExp(`<key>${key}</key>\\s*<string>([^<]*)</string>`));
|
|
300
|
+
return m ? m[1] : null;
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
/** True when the installed plist points at a different install than the active one. */
|
|
307
|
+
function menubarSetupNeedsRepoint() {
|
|
308
|
+
return menubarPlistNeedsRepoint({
|
|
309
|
+
plistEntry: readPlistEnvValue('AGENTS_ENTRY'),
|
|
310
|
+
plistNode: readPlistEnvValue('AGENTS_NODE'),
|
|
311
|
+
activeEntry: resolveCliEntry(),
|
|
312
|
+
activeNode: process.execPath,
|
|
313
|
+
});
|
|
314
|
+
}
|
|
276
315
|
/**
|
|
277
316
|
* Stop + remove the menu-bar service and write the sticky opt-out so the
|
|
278
317
|
* upgrade migration won't re-enable it.
|
|
@@ -326,7 +365,10 @@ export function installMenubarLaunchAgentOnUpgrade() {
|
|
|
326
365
|
enableMenubarService({ clearOptOut: false });
|
|
327
366
|
return;
|
|
328
367
|
}
|
|
329
|
-
|
|
368
|
+
// Re-enable (recopy helper + rewrite plist) when the version drifted OR the
|
|
369
|
+
// plist's baked interpreter/entry no longer point at the install now running
|
|
370
|
+
// `agents` — the dual-install skew a version bump alone can't catch.
|
|
371
|
+
if (menubarSetupStale() || menubarSetupNeedsRepoint()) {
|
|
330
372
|
enableMenubarService({ clearOptOut: false });
|
|
331
373
|
}
|
|
332
374
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for the Linux/Windows encrypted-file fallback.
|
|
3
|
+
*
|
|
4
|
+
* When the native credential store (GNOME Keyring / Windows Credential Manager)
|
|
5
|
+
* is unreachable, both backends route to the AES-256-GCM file store. The routing
|
|
6
|
+
* is "sticky": once any item is on disk, every op stays on the file store. That
|
|
7
|
+
* is correct for perf, but on its own it would silently *shadow* secrets that
|
|
8
|
+
* still live in the native store — reads for them would return empty with no
|
|
9
|
+
* hint. This module centralizes the two pieces that keep that from being silent:
|
|
10
|
+
*
|
|
11
|
+
* 1. `noteNativeShadow()` — a one-time stderr notice, emitted when a read
|
|
12
|
+
* falls through to the native store (found something shadowed, or hit a
|
|
13
|
+
* locked/unreachable store), pointing at `agents secrets import-keyring`.
|
|
14
|
+
* 2. The result types for that import command.
|
|
15
|
+
*
|
|
16
|
+
* macOS has no file fallback (see ./index.ts) and uses `migrate-acl` /
|
|
17
|
+
* `migrate-orphans` for its own invisible-item classes, so none of this runs
|
|
18
|
+
* there.
|
|
19
|
+
*/
|
|
20
|
+
export type NativeImportStatus = 'imported' | 'would-import' | 'exists' | 'failed';
|
|
21
|
+
export interface NativeImportResult {
|
|
22
|
+
item: string;
|
|
23
|
+
status: NativeImportStatus;
|
|
24
|
+
detail?: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Outcome of an `import-keyring` run. `available` is false when no native
|
|
28
|
+
* tooling exists (no `secret-tool` / no `powershell.exe`); `locked` is true when
|
|
29
|
+
* the native store exists but is locked/unreachable, so nothing could be read.
|
|
30
|
+
*/
|
|
31
|
+
export interface NativeImportReport {
|
|
32
|
+
available: boolean;
|
|
33
|
+
locked: boolean;
|
|
34
|
+
results: NativeImportResult[];
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Emit a one-time stderr notice that the file fallback is masking the native
|
|
38
|
+
* credential store. Both backends share the copy so the guidance is identical.
|
|
39
|
+
*
|
|
40
|
+
* 'shadowed' — a secret was just read from the native store that isn't in the
|
|
41
|
+
* file store. It works, but each read pays a native lookup and it
|
|
42
|
+
* won't survive the store locking; suggest migrating it.
|
|
43
|
+
* 'locked' — the native store is locked/unreachable, so its secrets can't be
|
|
44
|
+
* read in this session at all; the user must unlock, then migrate.
|
|
45
|
+
*/
|
|
46
|
+
export declare function noteNativeShadow(kind: 'shadowed' | 'locked', fileDir: string): void;
|
|
47
|
+
/** Test-only: clear the one-time notice guard between cases. */
|
|
48
|
+
export declare function _resetFallbackNoticeForTest(): void;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for the Linux/Windows encrypted-file fallback.
|
|
3
|
+
*
|
|
4
|
+
* When the native credential store (GNOME Keyring / Windows Credential Manager)
|
|
5
|
+
* is unreachable, both backends route to the AES-256-GCM file store. The routing
|
|
6
|
+
* is "sticky": once any item is on disk, every op stays on the file store. That
|
|
7
|
+
* is correct for perf, but on its own it would silently *shadow* secrets that
|
|
8
|
+
* still live in the native store — reads for them would return empty with no
|
|
9
|
+
* hint. This module centralizes the two pieces that keep that from being silent:
|
|
10
|
+
*
|
|
11
|
+
* 1. `noteNativeShadow()` — a one-time stderr notice, emitted when a read
|
|
12
|
+
* falls through to the native store (found something shadowed, or hit a
|
|
13
|
+
* locked/unreachable store), pointing at `agents secrets import-keyring`.
|
|
14
|
+
* 2. The result types for that import command.
|
|
15
|
+
*
|
|
16
|
+
* macOS has no file fallback (see ./index.ts) and uses `migrate-acl` /
|
|
17
|
+
* `migrate-orphans` for its own invisible-item classes, so none of this runs
|
|
18
|
+
* there.
|
|
19
|
+
*/
|
|
20
|
+
let noticeEmitted = false;
|
|
21
|
+
/**
|
|
22
|
+
* Emit a one-time stderr notice that the file fallback is masking the native
|
|
23
|
+
* credential store. Both backends share the copy so the guidance is identical.
|
|
24
|
+
*
|
|
25
|
+
* 'shadowed' — a secret was just read from the native store that isn't in the
|
|
26
|
+
* file store. It works, but each read pays a native lookup and it
|
|
27
|
+
* won't survive the store locking; suggest migrating it.
|
|
28
|
+
* 'locked' — the native store is locked/unreachable, so its secrets can't be
|
|
29
|
+
* read in this session at all; the user must unlock, then migrate.
|
|
30
|
+
*/
|
|
31
|
+
export function noteNativeShadow(kind, fileDir) {
|
|
32
|
+
if (noticeEmitted)
|
|
33
|
+
return;
|
|
34
|
+
noticeEmitted = true;
|
|
35
|
+
if (kind === 'locked') {
|
|
36
|
+
process.stderr.write(`[agents] the native credential store is locked/unreachable — secrets stored there are not ` +
|
|
37
|
+
`readable in this session. Unlock it, then run \`agents secrets import-keyring\` to migrate ` +
|
|
38
|
+
`them into the encrypted file store at ${fileDir}.\n`);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
process.stderr.write(`[agents] read a secret from the native credential store that is not in the file store at ` +
|
|
42
|
+
`${fileDir}. Run \`agents secrets import-keyring\` to migrate it so it stays readable headless.\n`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/** Test-only: clear the one-time notice guard between cases. */
|
|
46
|
+
export function _resetFallbackNoticeForTest() {
|
|
47
|
+
noticeEmitted = false;
|
|
48
|
+
}
|