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