@phnx-labs/agents-cli 1.20.44 → 1.20.46
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 +12 -1
- package/dist/commands/exec.js +54 -11
- package/dist/commands/secrets.d.ts +18 -0
- package/dist/commands/secrets.js +105 -30
- package/dist/commands/teams.js +61 -3
- package/dist/index.js +14 -119
- package/dist/lib/daemon.js +9 -6
- package/dist/lib/hosts/dispatch.d.ts +29 -0
- package/dist/lib/hosts/dispatch.js +46 -1
- package/dist/lib/hosts/remote-cmd.d.ts +17 -0
- package/dist/lib/hosts/remote-cmd.js +27 -0
- package/dist/lib/hosts/session-index.d.ts +15 -0
- package/dist/lib/hosts/session-index.js +28 -2
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/rotate.d.ts +33 -0
- package/dist/lib/rotate.js +37 -0
- package/dist/lib/secrets/remote.d.ts +14 -0
- package/dist/lib/secrets/remote.js +18 -1
- package/dist/lib/self-heal/checks/path.d.ts +2 -0
- package/dist/lib/self-heal/checks/path.js +30 -0
- package/dist/lib/self-heal/checks/resources.d.ts +2 -0
- package/dist/lib/self-heal/checks/resources.js +36 -0
- package/dist/lib/self-heal/checks/shadowing.d.ts +2 -0
- package/dist/lib/self-heal/checks/shadowing.js +48 -0
- package/dist/lib/self-heal/checks/shims.d.ts +2 -0
- package/dist/lib/self-heal/checks/shims.js +35 -0
- package/dist/lib/self-heal/registry.d.ts +22 -0
- package/dist/lib/self-heal/registry.js +66 -0
- package/dist/lib/self-heal/types.d.ts +41 -0
- package/dist/lib/self-heal/types.js +21 -0
- package/dist/lib/session/active.d.ts +4 -0
- package/dist/lib/session/active.js +2 -0
- package/dist/lib/session/db.d.ts +16 -9
- package/dist/lib/session/db.js +66 -44
- package/dist/lib/session/discover.d.ts +4 -0
- package/dist/lib/session/discover.js +84 -13
- package/dist/lib/session/run-names.d.ts +9 -7
- package/dist/lib/session/run-names.js +9 -7
- package/dist/lib/session/state.d.ts +29 -3
- package/dist/lib/session/state.js +84 -5
- package/dist/lib/session/types.d.ts +19 -8
- package/dist/lib/shim-heal.d.ts +23 -0
- package/dist/lib/shim-heal.js +109 -0
- package/dist/lib/shims.d.ts +6 -0
- package/dist/lib/shims.js +1 -1
- package/dist/lib/teams/agents.js +9 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,8 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
-
## 1.20.
|
|
5
|
+
## 1.20.46
|
|
6
|
+
|
|
7
|
+
- **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`.
|
|
6
8
|
|
|
9
|
+
- **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
|
+
## 1.20.45
|
|
11
|
+
- **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`.
|
|
12
|
+
- **`agents secrets export --host` now works against Windows targets, and a new `agents secrets unlock --host` unlocks a bundle on a remote machine.** The export push was POSIX-only (`bash -lc`, `--from /dev/stdin`, `create … || true`, `IFS= read`), so a Windows remote died with `'true' is not recognized … cannot find the path specified`. Two changes fix it: `agents secrets import` now accepts **`--from -`** (read the `.env` from stdin, replacing the POSIX-only `/dev/stdin`), and the push is **platform-aware** — `bash -lc` on POSIX, `powershell -EncodedCommand` on Windows, with the target's OS taken from the device registry. Because the npm `agents.ps1` shim does **not** forward ssh-piped stdin to the underlying node process (a raw `--from -` read hangs), the Windows keychain push bridges the piped `.env` through PowerShell into a temp file and imports `--from <file>` (deleted afterwards). File-backend export to a Windows target is refused cleanly rather than emitting broken PowerShell. Verified end-to-end: `agents secrets export linear.app --host win-mini` imported all 13 keys. Separately, **`agents secrets unlock --host <machine> <bundle>`** runs the unlock ON the remote over `ssh -tt`, so a **file-backed** bundle's passphrase prompt surfaces on your terminal — the "unlock the Mac from the road with its password" path; keychain/biometry bundles are GUI-only (a local Touch-ID/passcode sheet can't cross SSH) and can't be remote-unlocked. `unlock`'s `--host` is single-valued so it never swallows the positional bundle name. Source: `apps/cli/src/commands/secrets.ts`, `apps/cli/src/lib/hosts/remote-cmd.ts`.
|
|
13
|
+
- **A session now has ONE name, not two. `--name` seeds the session label instead of a parallel column.** Shipping `agents run --name` (1.20.43) as a separate immutable `name` column created two look-alike fields — an unshown, frozen `name` and the shown, searchable `label` — that both resolved `agents sessions <ref>` and forced tie-break bookkeeping nobody could keep straight. They unify into one field. `--name` is now the universal way to *seed* the `label` at launch — the same field an agent-generated title (Claude's `/rename`) later refines and `agents sessions` displays and searches — and it works consistently across interactive, headless, `--host`, and teams teammate runs (a teammate's friendly name now seeds its session label; before, teammate sessions had no name at all). Priority is a plain fallback chain resolved at scan time, no stored winner: an agent-generated title wins, else the `--name` seed, else the listing falls back to `topic`. So a Claude run's `--name` shows until Claude titles it (your seed, then refined); a non-Claude run keeps its `--name` as the label (it has no auto-title). The seeded name is now fuzzy-searchable in FTS (the old `name` column was not). `agents hosts logs <name>` is unchanged — it resolves against the host-task sidecar, not the session column. Schema v10 folds any existing `name` into `label` (where the label was empty), mirrors it into the FTS row, then drops the `name` column; the run-name sidecars re-seed every scan (`seedLabelsFromNames`), so no rescan is needed. Reworks the 1.20.43 `--name` design (partly reverts its separate-column approach). Source: `apps/cli/src/lib/session/{db,discover,run-names,types}.ts`, `apps/cli/src/lib/hosts/session-index.ts`, `apps/cli/src/lib/teams/agents.ts`, `apps/cli/src/commands/exec.ts`, `apps/cli/docs/{05-sessions,hosts}.md`.
|
|
14
|
+
- **NEW: `agents teams add`/`start` warns when a *version-pinned* teammate is on a throttled or signed-out account.** The 1.20.43 `balanced`-default fix keeps *bare* teammates off rate-limited accounts (they route through bare `agents run`, which rotates), but a **version-pinned** (`claude@2.1.112`) or **profile** teammate spawns `agents run <agent>@<version>` / `agents run <profile>`, and a pin/profile deliberately *bypasses* rotation — so it would launch straight onto a maxed account and 429 on the first request, with no mid-run failover either (that only arms when a non-pinned strategy actually rotated). `agents teams add` (at add time) and `agents teams start` (per staged teammate, deduped by `agent@version`) now pre-check a **version-pinned** teammate's account and print an advisory when it's rate-limited, out of credits, or not signed in — reusing the router's *exact* eligibility gate (`checkRunAccountReadiness` → `hasUsageAvailable`, the same session-inclusive signal the `agents view` badge uses), so the warning can never disagree with what the spawn would actually do. It **warns, never blocks** (mirroring the existing "may not be signed in" advisory); `--force` silences it. Scoped to version-pinned teammates on purpose: bare teammates are already handled by rotation, and a profile injects its own auth (a different account than the version home carries) that isn't locally checkable — so no unreliable profile warning is emitted. Source: `apps/cli/src/lib/rotate.ts` (`readinessFromCandidate`, `checkRunAccountReadiness`, `rotate.test.ts`), `apps/cli/src/commands/teams.ts`.
|
|
15
|
+
|
|
16
|
+
## 1.20.44
|
|
7
17
|
- **Every `logs` command is concise by default; the token-heavy raw dump is now opt-in behind `--full`.** Agents that spin up agents on other machines or add teammates were pulling whole transcripts just to glance at status — `agents logs <session>` printed the full markdown transcript, and `agents hosts logs` / `agents teams logs` / `agents routines logs` each `cat`'d their entire captured stdout, because each subsystem had hand-rolled its own "cat the log" verb over its own storage. All four now default to a bounded, concise view, with `-m/--full` for the raw log: `agents logs <session>` renders the same summary digest as `agents sessions <id>` (a real session shrank 92% — 29.9 KB → 2.6 KB); `agents routines logs <name>` shows a status header + the extracted report (a real run shrank 99.5% — 386 KB → 1.8 KB), falling back to a bounded stdout tail when no report was extracted; `agents teams logs <teammate>` renders the teammate's session summary (its agentId **is** the session id), with `-n <lines>` / `--full` for raw stdout; `agents hosts logs <id>` shows a bounded tail of the captured stdout (`tailLines`, with a "… N earlier lines hidden — pass --full" note) instead of the whole log. `renderSessionLog` now takes a mode and defaults to `'summary'`; `agents sessions <id>` was already summary-by-default and is unchanged. Regression-tested: `tailLines` truncation/elision math (`hosts/logs.test.ts`) and `formatRunDuration` human-time formatting (`routines-logs.test.ts`). Source: `apps/cli/src/commands/{logs,sessions,hosts,teams,routines}.ts`, `apps/cli/src/lib/hosts/logs.ts`. Scoped follow-up (not in this PR): host-task and sandboxed-routine runs write their real transcript on the remote / in an overlay HOME, so `logs` can't yet resolve them to the full `renderSummary` — making those runs discoverable is a separate change; until then the bounded tail / extracted report is the safe concise default.
|
|
8
18
|
- **The daemon now self-heals the `pane-died` hook on already-running `agents run` sessions.** The v1.20.42 fix that stops exiting a split from kicking you out of tmux is installed once, at session creation — so sessions already alive under the long-lived shared tmux server keep the old, unconditional `detach-client` hook until they exit or the server is recycled. On a machine that's never "between sessions," that meant hand-repairing live sessions. The daemon now runs `reconcileSessionHooks()` ~20s after startup and every ~5 min: it walks the managed `ag-` sessions on the shared socket and retrofits the `#{hook_pane}`-guarded hook onto any whose hook predates the current schema. It is strictly **non-destructive — `set-hook` only, never a `kill-pane` or `detach-client`** — so it is safe to run against sessions you're attached to; a per-session `@ag_hook_schema` marker makes steady-state a no-op. The hook string is now built in one place (`agentPaneDiedHook`) shared by the spawn-wrap and the reconcile so they can't drift. Source: `apps/cli/src/lib/tmux/session.ts`, `apps/cli/src/lib/daemon.ts`, `apps/cli/src/lib/exec.ts`.
|
|
9
19
|
- **NEW: `agents run` self-heals a gutted install instead of crashing with `ENOENT`.** The recurring failure: an npm agent whose native binary ships as an optional per-arch dependency (codex → `@openai/codex-<platform>`) can have that tarball extract **partially** — the platform package's `package.json` lands, its `vendor/<triple>/…/codex` binary does not (an interrupted or concurrently-raced `agents add` into the same version dir). The CLI's wrapper `require.resolve`s the platform package, finds the `package.json`, and sails straight past its own "missing optional dependency" guard into a `spawn(binaryPath)` that dies with a raw `ENOENT`. `agents run` now probes the version it's about to launch and, if the binary can't run, **repairs it in place** (a *clean* reinstall — the partial `node_modules` is wiped first, because npm treats the present-but-gutted platform package as already installed and would otherwise skip re-fetching it), then falls back to another installed version that launches (re-pinning it as the default so the shim path heals too), then to installing `latest` — only erroring if nothing can be made runnable. `installVersion` gained a `{ clean }` option for the wipe-then-reinstall. Source: `apps/cli/src/lib/versions.ts` (`ensureAgentRunnable`), `apps/cli/src/commands/exec.ts`.
|
|
@@ -11,6 +21,7 @@
|
|
|
11
21
|
- **NEW: `--no-tmux` / `--disable-tmux` on `agents run`.** The interactive tmux wrapper (which gives `%pane` addressing + re-attach) already had an opt-out, but it was hidden behind the opaquely-named `--raw`. `--no-tmux` (and its alias `--disable-tmux`) spawn the agent directly with full stdio inherited — the fastest way to see an agent's real startup output when a launch is failing. Same effect as `--raw` and `AGENTS_NO_TMUX=1`. Source: `apps/cli/src/commands/exec.ts`.
|
|
12
22
|
- **Fix: `agents add <agent>@<version>` no longer records a gutted install as healthy (root cause of the ENOENT crash + a broken default pin).** npm packages that ship their native binary via an optional per-arch dependency (e.g. codex → `@openai/codex-<platform>`) can land the JS wrapper at `node_modules/.bin/<cli>` while the real platform binary is missing (interrupted install, omitted optional dep, `--ignore-scripts`). `getBinaryPath()` only checked the wrapper, so the broken version read as installed, got pinned as the default, and got picked to run — then died with ENOENT. `installVersion` now probes `<binary> --version` (under the version's isolated HOME) after install and **fails the install** if the binary can't launch, so a broken version is never silently pinned. The check is deliberately narrow — only the missing-binary signature (`ENOENT`/"no such file"/"command not found") fails it; a plain nonzero exit or a timeout is treated as healthy, so a well-behaved agent that dislikes `--version` is never false-failed. Source: `apps/cli/src/lib/versions.ts`, `apps/cli/src/lib/versions-integrity.test.ts`.
|
|
13
23
|
- **Security fix: the routines daemon log no longer leaks GitHub / AWS / npm tokens.** `daemon.ts` carried its own private `redactSecrets` (used by every `log()` write to `logs.jsonl`) that predated and diverged from the canonical `redact.ts` — it caught `sk-`, `eyJ…`, `Bearer …`, and a narrow `NAME=value` list, but **not** `ghp_` (GitHub PAT), `AKIA…` (AWS access key), or `npm_` (npm token), so any of those appearing in a daemon message (a git push URL, a bundle-env dump, an error string) was written to the log in the clear. The private copy is deleted; `log()` now routes through the canonical `redactSecrets` in `redact.ts`, which covers all of those classes with a stronger quote-aware `NAME=value` pattern. The one pattern the daemon copy had and the canonical lacked — `Bearer <token>` — is added to `redact.ts`, so the shared redactor (also used by session-transcript export in `session/render.ts`) is now a strict superset. New `redact.test.ts` pins every token class as a regression guard. Source: `apps/cli/src/lib/daemon.ts`, `apps/cli/src/lib/redact.ts`, `apps/cli/src/lib/redact.test.ts`.
|
|
24
|
+
- **Fix: `agents teams doctor` tells the truth, a version fallback never spawns an unspawnable literal, and shims survive a vanished dispatcher (completing this release's self-heal series).** Three gaps remained after the `agents run` self-heal above. (1) **`agents teams doctor` lied** — it reported `installed: true` whenever a *shim file* existed, never checking the real binary, so a stub or gutted-native install (the exact codex/kimi failure) showed "ready" and then `ENOENT`'d at spawn. `checkCliAvailable` now verifies the resolved default version is actually installed, and doctor additionally **launch-probes** each installed agent (`verifyInstalledBinaryLaunches`) and flips a gutted-native one to not-installed with a repair hint. (2) **A version fallback spawned an unspawnable literal** — when a specific version was requested (`agents run kimi@0.19.2`, the path every version-pinned teammate takes) and no versioned shim existed on disk, the launch left the bare `<agent>@<version>` name as `argv[0]`, which is not on PATH, so it died with `spawn kimi@0.19.2 ENOENT`; it now resolves the version's real binary (`getBinaryPath`) instead, falling back to the literal only when no binary exists at all. (3) **A shim couldn't survive its dispatcher vanishing** — when the baked `AGENTS_BIN` (often a dev build under `~/.local/agents-cli-dev`) was removed, moved, or went stale, the shim exited 127 and bricked *every* managed launch; it now **self-recovers** to whatever `agents` resolves to on PATH before erroring (`SHIM_SCHEMA_VERSION` → 25). Also drops a stale, npm-unrecoverable `codex 0.116.0` pin from the repo's own `agents.yaml` so codex resolves to the machine default instead of self-healing on every run. Source: `apps/cli/src/lib/{exec,shims}.ts`, `apps/cli/src/lib/teams/agents.ts`, `apps/cli/src/commands/teams.ts`, `agents.yaml`.
|
|
14
25
|
|
|
15
26
|
## 1.20.43
|
|
16
27
|
|
package/dist/commands/exec.js
CHANGED
|
@@ -218,7 +218,7 @@ export function registerRunCommand(program) {
|
|
|
218
218
|
.option('-i, --interactive', 'Force interactive mode even when a prompt is provided. Mutually exclusive with --headless.')
|
|
219
219
|
.option('--resume [id]', 'Resume a previous conversation. Accepts a full or partial session id (prefix-matched against the index); omit the id to pick from recent sessions interactively. Resumes under the version that started the session. claude/codex resume natively; other agents replay via a /continue first message. Pair with a prompt to continue headlessly.')
|
|
220
220
|
.option('--session-id <id>', 'Force a NEW conversation to use this exact session UUID (Claude only). This CREATES a session — to resume an existing one, use --resume.')
|
|
221
|
-
.option('--name <slug>', '
|
|
221
|
+
.option('--name <slug>', 'Name the run — seeds the session label so it shows up as `<name>` in `agents sessions` and resolves by it (and `agents hosts logs <name>` for --host runs) instead of an opaque id. An agent-generated title later refines the label; your name shows until then. Optional.')
|
|
222
222
|
.option('--verbose', 'Show detailed execution logs')
|
|
223
223
|
.option('--raw', 'Interactive runs on macOS/Linux launch inside a shared tmux session (for %pane addressing + re-attach). Pass --raw to spawn the agent directly instead. Also disabled by AGENTS_NO_TMUX=1.')
|
|
224
224
|
.option('--no-tmux', 'Spawn the agent directly instead of wrapping it in the shared tmux session. Same effect as --raw / AGENTS_NO_TMUX=1. Use this to see the agent\'s full startup output when a launch is failing.')
|
|
@@ -359,13 +359,9 @@ export function registerRunCommand(program) {
|
|
|
359
359
|
process.exit(1);
|
|
360
360
|
}
|
|
361
361
|
const hostName = hostGiven[0];
|
|
362
|
-
if (prompt === undefined) {
|
|
363
|
-
console.error(chalk.red('A prompt is required for host runs: agents run <agent> "<task>" --host <name>'));
|
|
364
|
-
process.exit(1);
|
|
365
|
-
}
|
|
366
362
|
const { resolveHost, resolveHostByCap } = await import('../lib/hosts/registry.js');
|
|
367
|
-
const { dispatchToHost } = await import('../lib/hosts/dispatch.js');
|
|
368
|
-
const { registerHostSession } = await import('../lib/hosts/session-index.js');
|
|
363
|
+
const { dispatchToHost, runInteractiveOnHost } = await import('../lib/hosts/dispatch.js');
|
|
364
|
+
const { registerHostSession, registerInteractiveHostSession } = await import('../lib/hosts/session-index.js');
|
|
369
365
|
// A password-auth device throws DeviceOffloadUnsupportedError here; it's
|
|
370
366
|
// printed cleanly by the top-level catch in index.ts (covers every
|
|
371
367
|
// resolveHost caller), so it never falls through to capability routing.
|
|
@@ -396,10 +392,57 @@ export function registerRunCommand(program) {
|
|
|
396
392
|
// which can't run over a detached remote dispatch — only forward a
|
|
397
393
|
// concrete id.
|
|
398
394
|
const resumeId = typeof options.resume === 'string' ? options.resume : undefined;
|
|
399
|
-
//
|
|
400
|
-
//
|
|
401
|
-
//
|
|
402
|
-
//
|
|
395
|
+
// Decide whether this host run is interactive. No prompt always means
|
|
396
|
+
// interactive (matching local resolveInteractive); --interactive forces
|
|
397
|
+
// interactive even when a prompt is provided; --headless forces headless
|
|
398
|
+
// and therefore requires a prompt.
|
|
399
|
+
if (options.interactive && options.headless) {
|
|
400
|
+
console.error(chalk.red('--interactive and --headless are mutually exclusive. Pass one, or neither (mode is inferred from prompt presence).'));
|
|
401
|
+
process.exit(1);
|
|
402
|
+
}
|
|
403
|
+
const interactiveHost = options.interactive === true || (prompt === undefined && options.headless !== true);
|
|
404
|
+
if (interactiveHost) {
|
|
405
|
+
// Interactive host run: forward the local TTY over SSH and let the
|
|
406
|
+
// remote agent start its normal interactive UI (tmux on the host).
|
|
407
|
+
if (options.follow === false) {
|
|
408
|
+
console.error(chalk.red('--no-follow is not compatible with interactive host runs. Interactive runs are attached by definition.'));
|
|
409
|
+
process.exit(1);
|
|
410
|
+
}
|
|
411
|
+
// Mirror the local path (lib/exec.ts): only Claude accepts a forced
|
|
412
|
+
// `--session-id`. Generating it here lets us register the run in the
|
|
413
|
+
// local index and makes it resumable by id. On resume the remote
|
|
414
|
+
// session keeps its existing id — don't mint a new one.
|
|
415
|
+
const hostSessionId = runAgent === 'claude' && !resumeId ? randomUUID() : undefined;
|
|
416
|
+
if (hostSessionId) {
|
|
417
|
+
registerInteractiveHostSession({
|
|
418
|
+
cwd: process.cwd(),
|
|
419
|
+
host: host.name,
|
|
420
|
+
agent: runAgent,
|
|
421
|
+
sessionId: hostSessionId,
|
|
422
|
+
name: options.name,
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
const exitCode = await runInteractiveOnHost(host, {
|
|
426
|
+
agent: runAgent,
|
|
427
|
+
prompt,
|
|
428
|
+
mode: options.mode,
|
|
429
|
+
model: options.model,
|
|
430
|
+
remoteCwd: options.remoteCwd,
|
|
431
|
+
sessionId: hostSessionId,
|
|
432
|
+
name: options.name,
|
|
433
|
+
resume: resumeId,
|
|
434
|
+
passthroughArgs,
|
|
435
|
+
raw: options.raw || options.tmux === false || options.disableTmux === true,
|
|
436
|
+
forceInteractive: options.interactive,
|
|
437
|
+
});
|
|
438
|
+
process.exit(exitCode);
|
|
439
|
+
}
|
|
440
|
+
// Headless host run: launch detached, tail the remote log, and follow
|
|
441
|
+
// until the remote process exits.
|
|
442
|
+
if (prompt === undefined) {
|
|
443
|
+
console.error(chalk.red('A prompt is required for headless host runs: agents run <agent> "<task>" --host <name>'));
|
|
444
|
+
process.exit(1);
|
|
445
|
+
}
|
|
403
446
|
const hostSessionId = runAgent === 'claude' && !resumeId ? randomUUID() : undefined;
|
|
404
447
|
const { task, exitCode } = await dispatchToHost(host, {
|
|
405
448
|
agent: runAgent,
|
|
@@ -9,6 +9,24 @@ import { type Command } from 'commander';
|
|
|
9
9
|
import { SSH_TARGET_RE, assertValidSshTarget } from '../lib/ssh-exec.js';
|
|
10
10
|
import { quoteWin32ExecArg } from '../lib/platform/index.js';
|
|
11
11
|
import { type SecretsBundle, type SecretsPolicy } from '../lib/secrets/bundles.js';
|
|
12
|
+
/**
|
|
13
|
+
* Read the raw `.env` text for `import --from <path|->`. A `-` reads the .env
|
|
14
|
+
* from stdin (the SSH push path: `export --host` pipes the resolved dotenv over
|
|
15
|
+
* ssh stdin, which has no `/dev/stdin` on a Windows remote); any other value is
|
|
16
|
+
* a filesystem path.
|
|
17
|
+
*/
|
|
18
|
+
export declare function readImportDotenv(from: string): string;
|
|
19
|
+
/**
|
|
20
|
+
* Build the remote `agents secrets unlock` argv for `unlock --host`. `--all`
|
|
21
|
+
* forwards verbatim; otherwise the explicit bundle names. A `--ttl` is passed
|
|
22
|
+
* through as-is so the REMOTE parses its own duration (its platform rules, its
|
|
23
|
+
* defaults). Shared with the command action so the wiring is unit-testable
|
|
24
|
+
* without a live SSH session.
|
|
25
|
+
*/
|
|
26
|
+
export declare function buildRemoteUnlockArgs(names: string[], opts: {
|
|
27
|
+
all?: boolean;
|
|
28
|
+
ttl?: string;
|
|
29
|
+
}): string[];
|
|
12
30
|
export { SSH_TARGET_RE, assertValidSshTarget };
|
|
13
31
|
/**
|
|
14
32
|
* Build the child environment for `agents secrets exec`. Strips
|
package/dist/commands/secrets.js
CHANGED
|
@@ -12,7 +12,9 @@ import * as fs from 'fs';
|
|
|
12
12
|
import { SSH_TARGET_RE, assertValidSshTarget, sshExec } from '../lib/ssh-exec.js';
|
|
13
13
|
import { quoteWin32ExecArg, composeWin32CommandLine } from '../lib/platform/index.js';
|
|
14
14
|
import { ensureDaemonStarted } from '../lib/daemon.js';
|
|
15
|
-
import { parseHostsOption, remoteResolveEnv, remoteSecretsRaw, resolveSshTarget, } from '../lib/secrets/remote.js';
|
|
15
|
+
import { parseHostsOption, remoteResolveEnv, remoteSecretsRaw, remoteSecretsStream, resolveSshTarget, } from '../lib/secrets/remote.js';
|
|
16
|
+
import { remoteShellFor, buildWindowsStdinImportCommand } from '../lib/hosts/remote-cmd.js';
|
|
17
|
+
import { resolveRemoteOsSync } from '../lib/hosts/remote-os.js';
|
|
16
18
|
import { bundleExists, bundleItemStore, bundlePolicy, deleteBundle, describeBundle, keychainItemsForBundle, keychainRef, listBundles, migrateLegacyBundles, parseDotenv, readAndResolveBundleEnv, readBundle, renameBundle, rotateBundleSecret, sanitizeProcessEnv, validateBundleName, validateEnvKey, validateExpiresFutureDated, validateSecretType, writeBundle, } from '../lib/secrets/bundles.js';
|
|
17
19
|
import { getKeychainToken, getKeychainTokens, hasKeychainToken, secretsKeychainItem, setKeychainToken, } from '../lib/secrets/index.js';
|
|
18
20
|
import { assertOpAvailable, createPasswordItem, deleteItemByTitle, extractSecrets, itemExistsByTitle, listItems, listVaults, } from '../lib/onepassword.js';
|
|
@@ -139,6 +141,29 @@ function readStdinSync() {
|
|
|
139
141
|
}
|
|
140
142
|
return Buffer.concat(chunks).toString('utf-8').trim();
|
|
141
143
|
}
|
|
144
|
+
/**
|
|
145
|
+
* Read the raw `.env` text for `import --from <path|->`. A `-` reads the .env
|
|
146
|
+
* from stdin (the SSH push path: `export --host` pipes the resolved dotenv over
|
|
147
|
+
* ssh stdin, which has no `/dev/stdin` on a Windows remote); any other value is
|
|
148
|
+
* a filesystem path.
|
|
149
|
+
*/
|
|
150
|
+
export function readImportDotenv(from) {
|
|
151
|
+
return from === '-' ? readStdinSync() : fs.readFileSync(from, 'utf-8');
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Build the remote `agents secrets unlock` argv for `unlock --host`. `--all`
|
|
155
|
+
* forwards verbatim; otherwise the explicit bundle names. A `--ttl` is passed
|
|
156
|
+
* through as-is so the REMOTE parses its own duration (its platform rules, its
|
|
157
|
+
* defaults). Shared with the command action so the wiring is unit-testable
|
|
158
|
+
* without a live SSH session.
|
|
159
|
+
*/
|
|
160
|
+
export function buildRemoteUnlockArgs(names, opts) {
|
|
161
|
+
return [
|
|
162
|
+
'unlock',
|
|
163
|
+
...(opts.all ? ['--all'] : names),
|
|
164
|
+
...(opts.ttl ? ['--ttl', opts.ttl] : []),
|
|
165
|
+
];
|
|
166
|
+
}
|
|
142
167
|
// SSH target validation is defined canonically in src/lib/ssh-exec.ts and
|
|
143
168
|
// re-exported here for back-compat with existing importers of these symbols.
|
|
144
169
|
export { SSH_TARGET_RE, assertValidSshTarget };
|
|
@@ -1162,7 +1187,7 @@ Examples:
|
|
|
1162
1187
|
cmd
|
|
1163
1188
|
.command('import [bundle]')
|
|
1164
1189
|
.description('Import keys from a .env file or a 1Password vault into a bundle. The bundle is created if it does not exist. Values are stored in the bundle\'s backend (keychain by default).')
|
|
1165
|
-
.option('--from <path>', 'Path to a .env file')
|
|
1190
|
+
.option('--from <path>', 'Path to a .env file (use - to read the .env from stdin)')
|
|
1166
1191
|
.option('--from-1password', 'Import secrets from a 1Password vault (requires the op CLI)')
|
|
1167
1192
|
.option('--vault <name>', '1Password vault name (used with --from-1password)')
|
|
1168
1193
|
.option('--all-plaintext', 'Store every imported value as a literal in the bundle metadata (skip keychain item creation)')
|
|
@@ -1227,7 +1252,7 @@ Examples:
|
|
|
1227
1252
|
console.log(chalk.green(`Imported ${added} key(s) from 1Password vault '${vault}'${skipped ? `, skipped ${skipped} (already set, pass --force)` : ''}.`));
|
|
1228
1253
|
}
|
|
1229
1254
|
else {
|
|
1230
|
-
const raw =
|
|
1255
|
+
const raw = readImportDotenv(opts.from);
|
|
1231
1256
|
const pairs = parseDotenv(raw);
|
|
1232
1257
|
for (const [key, value] of Object.entries(pairs)) {
|
|
1233
1258
|
if (!opts.force && key in bundle.vars) {
|
|
@@ -1294,34 +1319,47 @@ Examples:
|
|
|
1294
1319
|
const { env } = readAndResolveBundleEnv(resolvedBundleName, { caller: `ssh export` });
|
|
1295
1320
|
const dotenv = bundleEnvToDotenv(env);
|
|
1296
1321
|
const keyCount = Object.keys(env).length;
|
|
1297
|
-
// Drive the remote's own `agents secrets
|
|
1298
|
-
// chosen backend
|
|
1299
|
-
//
|
|
1300
|
-
//
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
if (remoteBackend === 'file') {
|
|
1306
|
-
// import --backend file auto-creates the file-backed bundle; no
|
|
1307
|
-
// separate `create` needed.
|
|
1308
|
-
remoteAgents =
|
|
1309
|
-
`IFS= read -r AGENTS_SECRETS_PASSPHRASE; export AGENTS_SECRETS_PASSPHRASE; ` +
|
|
1310
|
-
`agents secrets import ${shellQuote(resolvedBundleName)} --from /dev/stdin${backendFlag}${force}`;
|
|
1311
|
-
input = `${remotePassphrase}\n${dotenv}`;
|
|
1312
|
-
}
|
|
1313
|
-
else {
|
|
1314
|
-
remoteAgents =
|
|
1315
|
-
`agents secrets create ${shellQuote(resolvedBundleName)} >/dev/null 2>&1 || true; ` +
|
|
1316
|
-
`agents secrets import ${shellQuote(resolvedBundleName)} --from /dev/stdin${force}`;
|
|
1317
|
-
input = dotenv;
|
|
1318
|
-
}
|
|
1319
|
-
const remoteCmd = `bash -lc ${shellQuote(remoteAgents)}`;
|
|
1322
|
+
// Drive the remote's own `agents secrets import --from -` so the values
|
|
1323
|
+
// land in its chosen backend, reading the .env off ssh stdin (never
|
|
1324
|
+
// parsed by a remote shell — `--from -` replaces the POSIX-only
|
|
1325
|
+
// `/dev/stdin`). The keychain path is built OS-aware via
|
|
1326
|
+
// `remoteSecretsRaw` (bash -lc on POSIX, PowerShell on Windows), so it
|
|
1327
|
+
// works on macOS, Linux AND Windows targets. `import` auto-creates the
|
|
1328
|
+
// bundle, so no separate `create` (the old `|| true` was a POSIXism
|
|
1329
|
+
// that broke on PowerShell: `'true' is not recognized`).
|
|
1320
1330
|
let failures = 0;
|
|
1321
1331
|
for (const host of hosts) {
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1332
|
+
let res;
|
|
1333
|
+
if (remoteBackend === 'file') {
|
|
1334
|
+
// File backend forwards AGENTS_SECRETS_PASSPHRASE as the FIRST stdin
|
|
1335
|
+
// line (consumed by `read`, so it never lands in argv / `ps` /
|
|
1336
|
+
// remote history), then the .env. That `read`/`export` prologue is
|
|
1337
|
+
// POSIX shell — refuse a Windows target cleanly rather than emit
|
|
1338
|
+
// broken PowerShell.
|
|
1339
|
+
if (remoteShellFor(resolveRemoteOsSync(host.split('@').pop() ?? host)) === 'powershell') {
|
|
1340
|
+
failures++;
|
|
1341
|
+
console.error(chalk.red(`${host}: file backend export to a Windows target is not yet supported.`));
|
|
1342
|
+
continue;
|
|
1343
|
+
}
|
|
1344
|
+
const remoteAgents = `IFS= read -r AGENTS_SECRETS_PASSPHRASE; export AGENTS_SECRETS_PASSPHRASE; ` +
|
|
1345
|
+
`agents secrets import ${shellQuote(resolvedBundleName)} --from - --backend file${opts.force ? ' --force' : ''}`;
|
|
1346
|
+
res = sshExec(host, `bash -lc ${shellQuote(remoteAgents)}`, { input: `${remotePassphrase}\n${dotenv}` });
|
|
1347
|
+
}
|
|
1348
|
+
else if (remoteShellFor(resolveRemoteOsSync(host.split('@').pop() ?? host)) === 'powershell') {
|
|
1349
|
+
// Keychain on a Windows target: the `agents.ps1` shim doesn't
|
|
1350
|
+
// forward ssh-piped stdin to node, so `--from -` would hang.
|
|
1351
|
+
// Bridge the piped .env through PowerShell into a temp file and
|
|
1352
|
+
// import `--from <file>` (deleted afterwards). Same hardened ssh
|
|
1353
|
+
// engine, .env still only ever crosses the wire over ssh stdin.
|
|
1354
|
+
res = sshExec(host, buildWindowsStdinImportCommand(resolvedBundleName, { force: opts.force }), { input: dotenv });
|
|
1355
|
+
}
|
|
1356
|
+
else {
|
|
1357
|
+
// Keychain on a POSIX target: OS-aware wrapping + hardened ssh
|
|
1358
|
+
// engine (BatchMode, ConnectTimeout, keepalive, control-socket
|
|
1359
|
+
// reuse) via the same path the READ inverse (`remoteResolveEnv`)
|
|
1360
|
+
// uses. `--from -` reads the .env off ssh stdin.
|
|
1361
|
+
res = remoteSecretsRaw(host, ['import', resolvedBundleName, '--from', '-', ...(opts.force ? ['--force'] : [])], { input: dotenv });
|
|
1362
|
+
}
|
|
1325
1363
|
if (res.code === null) {
|
|
1326
1364
|
failures++;
|
|
1327
1365
|
console.error(chalk.red(`${host}: ${res.stderr.trim() || (res.timedOut ? 'ssh timed out' : 'ssh failed')}`));
|
|
@@ -1554,10 +1592,47 @@ Examples:
|
|
|
1554
1592
|
});
|
|
1555
1593
|
cmd
|
|
1556
1594
|
.command('unlock [names...]')
|
|
1557
|
-
.description('Hold a bundle in the secrets-agent after one Touch ID, so concurrent runs read it without re-prompting (macOS).')
|
|
1595
|
+
.description('Hold a bundle in the secrets-agent after one Touch ID, so concurrent runs read it without re-prompting (macOS). With --host, unlock FILE-backed bundle(s) on a remote (the passphrase prompt surfaces over the SSH TTY); keychain/biometry bundles are GUI-only and can\'t be remote-unlocked.')
|
|
1558
1596
|
.option('--ttl <duration>', 'How long to hold it (e.g. 30m, 8h, 3d). Default 7d.')
|
|
1559
1597
|
.option('--all', 'Unlock every configured bundle')
|
|
1598
|
+
.option('--host <target>', 'Unlock the bundle(s) on this remote machine over SSH instead of locally (file-backed bundles only — the remote\'s passphrase prompt surfaces on your terminal over a -tt session). Single-valued (NOT variadic) so it never swallows the bundle name: `unlock <name> --host <machine>`.')
|
|
1560
1599
|
.action(async (names, opts) => {
|
|
1600
|
+
// Single-valued (not variadic): a variadic --host greedily consumes the
|
|
1601
|
+
// positional bundle name (`unlock --host mac wztest` -> host=[mac,wztest],
|
|
1602
|
+
// names=[]). Unlock targets one remote at a time anyway.
|
|
1603
|
+
const hosts = opts.host ? [opts.host] : [];
|
|
1604
|
+
if (hosts.length > 0) {
|
|
1605
|
+
// Remote unlock: the REMOTE enforces its own platform rules, so the
|
|
1606
|
+
// local darwin-only guard below does NOT apply. Only file-backed
|
|
1607
|
+
// bundles are remote-unlockable — their passphrase prompt surfaces over
|
|
1608
|
+
// the -tt SSH TTY; a keychain/biometry bundle would trigger a local GUI
|
|
1609
|
+
// Touch-ID sheet that can't cross SSH.
|
|
1610
|
+
if (!opts.all && (!names || names.length === 0)) {
|
|
1611
|
+
console.error(chalk.red('Specify one or more bundle names, or --all.'));
|
|
1612
|
+
process.exit(1);
|
|
1613
|
+
}
|
|
1614
|
+
const unlockArgs = buildRemoteUnlockArgs(names, opts);
|
|
1615
|
+
let failures = 0;
|
|
1616
|
+
for (const h of hosts) {
|
|
1617
|
+
const target = await resolveSshTarget(h);
|
|
1618
|
+
// FOREGROUND stream (stdio inherited), NOT the piped remoteSecretsRaw:
|
|
1619
|
+
// the remote's passphrase prompt only surfaces if the remote process
|
|
1620
|
+
// sees a real TTY, which requires our local terminal to pass straight
|
|
1621
|
+
// through. The remote's prompt + output stream to this terminal; we get
|
|
1622
|
+
// back only the exit code.
|
|
1623
|
+
const code = remoteSecretsStream(target, unlockArgs);
|
|
1624
|
+
if (code === 0) {
|
|
1625
|
+
console.log(chalk.green(`${h}: unlocked`));
|
|
1626
|
+
}
|
|
1627
|
+
else {
|
|
1628
|
+
failures++;
|
|
1629
|
+
console.error(chalk.red(`${h}: unlock failed (exit ${code})`));
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
if (failures > 0)
|
|
1633
|
+
process.exit(1);
|
|
1634
|
+
return;
|
|
1635
|
+
}
|
|
1561
1636
|
if (process.platform !== 'darwin') {
|
|
1562
1637
|
console.error(chalk.red('secrets-agent is macOS-only (no biometry prompt to deduplicate elsewhere).'));
|
|
1563
1638
|
process.exit(1);
|
package/dist/commands/teams.js
CHANGED
|
@@ -17,6 +17,7 @@ import { discoverSessions, parseTimeFilter, resolveSessionById } from '../lib/se
|
|
|
17
17
|
import { renderSessionLog } from './sessions.js';
|
|
18
18
|
import { buildPreview as buildSessionPreview } from './sessions-picker.js';
|
|
19
19
|
import { parseExecEnv } from '../lib/exec.js';
|
|
20
|
+
import { checkRunAccountReadiness } from '../lib/rotate.js';
|
|
20
21
|
import { teamPicker, printTeamTable } from './teams-picker.js';
|
|
21
22
|
import { itemPicker } from '../lib/picker.js';
|
|
22
23
|
import { profileExists, readProfile } from '../lib/profiles.js';
|
|
@@ -192,6 +193,51 @@ export function wireCloudDispatcher(mgr) {
|
|
|
192
193
|
* teammate whose CLI may not be signed in. Warn-only — never blocks `start`.
|
|
193
194
|
* Local teammates only; cloud teammates authenticate through their provider.
|
|
194
195
|
*/
|
|
196
|
+
/**
|
|
197
|
+
* Advisory line for a version-pinned teammate whose account can't serve a run
|
|
198
|
+
* right now. A pinned target (`agents run <agent>@<version>`) bypasses account
|
|
199
|
+
* rotation — the pin IS the target — so unlike a bare teammate it can't route
|
|
200
|
+
* around a throttled/expired account; it will launch and likely 429 at once.
|
|
201
|
+
*/
|
|
202
|
+
function throttleWarningLine(agent, version, r) {
|
|
203
|
+
const who = `${AGENT_NAMES[agent]} ${version}`;
|
|
204
|
+
const acct = r.email ? ` (${r.email})` : '';
|
|
205
|
+
const reason = r.reason === 'out_of_credits' ? 'is out of credits'
|
|
206
|
+
: r.reason === 'signed_out' ? 'is not signed in'
|
|
207
|
+
: 'is rate-limited right now';
|
|
208
|
+
return (chalk.yellow(`⚠ ${who}${acct} ${reason}.`) +
|
|
209
|
+
chalk.gray(`\n A pinned version skips account rotation, so it will launch on this account and may immediately hit its limit.` +
|
|
210
|
+
`\n Use a bare \`${agent}\` teammate to let the team pick a healthy account, or pass --force to silence this.`));
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Advisory: for each staged VERSION-PINNED teammate, warn if its account is
|
|
214
|
+
* rate-limited / out of credits / signed out right now — reusing the router's
|
|
215
|
+
* own eligibility signal (`checkRunAccountReadiness`) so the warning matches
|
|
216
|
+
* what the spawn would actually do. Bare teammates (rotation handles them) and
|
|
217
|
+
* profile/cloud teammates (account not locally checkable) are skipped. Warns,
|
|
218
|
+
* never blocks. Deduped by agent@version so N teammates on one account warn once.
|
|
219
|
+
*/
|
|
220
|
+
async function warnThrottledTeammates(mgr, team) {
|
|
221
|
+
let pending;
|
|
222
|
+
try {
|
|
223
|
+
pending = (await mgr.listByTask(team)).filter((a) => a.status === 'pending' && !a.cloudProvider && !a.profileName && a.version);
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
return; // team not loadable yet — nothing to warn about
|
|
227
|
+
}
|
|
228
|
+
const seen = new Set();
|
|
229
|
+
for (const a of pending) {
|
|
230
|
+
const agent = a.agentType;
|
|
231
|
+
const version = a.version;
|
|
232
|
+
const key = `${agent}@${version}`;
|
|
233
|
+
if (seen.has(key) || !AGENT_NAMES[agent])
|
|
234
|
+
continue;
|
|
235
|
+
seen.add(key);
|
|
236
|
+
const readiness = await checkRunAccountReadiness(agent, version);
|
|
237
|
+
if (!readiness.ready)
|
|
238
|
+
console.error(throttleWarningLine(agent, version, readiness));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
195
241
|
async function warnUnsignedTeammates(mgr, team) {
|
|
196
242
|
let pending;
|
|
197
243
|
try {
|
|
@@ -967,7 +1013,7 @@ export function registerTeamsCommands(program) {
|
|
|
967
1013
|
.option('--cloud <provider>', `Dispatch to cloud backend instead of local CLI: ${VALID_CLOUD_PROVIDERS.join('|')}`)
|
|
968
1014
|
.option('--repo <owner/repo>', 'GitHub repository (required for --cloud rush)')
|
|
969
1015
|
.option('--branch <name>', 'Target git branch for cloud dispatch')
|
|
970
|
-
.option('--force', "Skip the advisory 'may not be signed in'
|
|
1016
|
+
.option('--force', "Skip the advisory 'may not be signed in' / 'account throttled' warnings")
|
|
971
1017
|
.option('--json', 'Output machine-readable JSON')
|
|
972
1018
|
.action(async (team, teammate, task, opts) => {
|
|
973
1019
|
if (!VALID_MODES.includes(opts.mode)) {
|
|
@@ -1007,6 +1053,16 @@ export function registerTeamsCommands(program) {
|
|
|
1007
1053
|
console.error(chalk.yellow(`⚠ ${AGENT_NAMES[agent]} may not be signed in (detection is unreliable). Adding anyway.`) +
|
|
1008
1054
|
chalk.gray(`\n If it fails to start, run \`${AGENTS[agent].cliCommand}\` to log in, or pass --force to silence this.`));
|
|
1009
1055
|
}
|
|
1056
|
+
// Advisory throttle check — only for a version-pinned teammate, which
|
|
1057
|
+
// bypasses account rotation and so can't route around a rate-limited /
|
|
1058
|
+
// out-of-credits / signed-out account (see throttleWarningLine). Skip bare
|
|
1059
|
+
// targets (rotation handles them), profiles (auth-injected account isn't
|
|
1060
|
+
// the version-home one we can read), and cloud dispatch. Warn, never block.
|
|
1061
|
+
if (!opts.force && !cloudProviderId && !profileName && version) {
|
|
1062
|
+
const readiness = await checkRunAccountReadiness(agent, version);
|
|
1063
|
+
if (!readiness.ready)
|
|
1064
|
+
console.error(throttleWarningLine(agent, version, readiness));
|
|
1065
|
+
}
|
|
1010
1066
|
if (opts.name !== undefined) {
|
|
1011
1067
|
if (!opts.name || !/^[A-Za-z0-9_-]+$/.test(opts.name)) {
|
|
1012
1068
|
die(`Invalid teammate name '${opts.name}'. Use letters, numbers, '-', or '_'.`);
|
|
@@ -1276,7 +1332,7 @@ export function registerTeamsCommands(program) {
|
|
|
1276
1332
|
.option('--watch', 'Keep running: poll every --interval seconds, fire new waves, exit when the DAG drains.')
|
|
1277
1333
|
.option('--interval <seconds>', 'Seconds between waves in --watch mode (default 8)', '8')
|
|
1278
1334
|
.option('--max-waves <n>', 'Safety cap on waves in --watch mode (default 1000)', '1000')
|
|
1279
|
-
.option('--force', "Skip the advisory 'may not be signed in'
|
|
1335
|
+
.option('--force', "Skip the advisory 'may not be signed in' / 'account throttled' warnings for staged teammates")
|
|
1280
1336
|
.action(async (team, opts) => {
|
|
1281
1337
|
const mgr = mkManager();
|
|
1282
1338
|
wireCloudDispatcher(mgr);
|
|
@@ -1286,8 +1342,10 @@ export function registerTeamsCommands(program) {
|
|
|
1286
1342
|
return;
|
|
1287
1343
|
team = picked;
|
|
1288
1344
|
}
|
|
1289
|
-
if (!opts.force && !isJsonMode(opts))
|
|
1345
|
+
if (!opts.force && !isJsonMode(opts)) {
|
|
1290
1346
|
await warnUnsignedTeammates(mgr, team);
|
|
1347
|
+
await warnThrottledTeammates(mgr, team);
|
|
1348
|
+
}
|
|
1291
1349
|
if (!opts.watch) {
|
|
1292
1350
|
await runOneWave(mgr, team, Boolean(opts.json));
|
|
1293
1351
|
return;
|