@phnx-labs/agents-cli 1.20.41 → 1.20.43
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 +14 -0
- package/dist/commands/computer-actions.js +1 -1
- package/dist/commands/computer.d.ts +2 -2
- package/dist/commands/computer.js +4 -4
- package/dist/commands/exec.js +18 -6
- package/dist/commands/go.js +6 -1
- package/dist/commands/hosts.js +10 -6
- package/dist/commands/secrets.js +26 -0
- package/dist/commands/sessions.js +30 -12
- package/dist/lib/browser/chrome.d.ts +22 -0
- package/dist/lib/browser/chrome.js +53 -13
- package/dist/lib/browser/service.js +13 -0
- package/dist/lib/computer-rpc.js +3 -3
- package/dist/lib/exec.d.ts +59 -0
- package/dist/lib/exec.js +163 -0
- package/dist/lib/hosts/dispatch.d.ts +5 -0
- package/dist/lib/hosts/dispatch.js +4 -0
- package/dist/lib/hosts/session-index.js +1 -0
- package/dist/lib/hosts/tasks.d.ts +15 -0
- package/dist/lib/hosts/tasks.js +16 -0
- package/dist/lib/menubar/install-menubar.js +2 -2
- package/dist/lib/rotate.d.ts +11 -6
- package/dist/lib/rotate.js +25 -11
- package/dist/lib/secrets/bundles.js +5 -3
- package/dist/lib/secrets/remote.js +14 -0
- package/dist/lib/secrets/sync.js +13 -0
- package/dist/lib/session/active.d.ts +49 -3
- package/dist/lib/session/active.js +139 -10
- package/dist/lib/session/db.d.ts +11 -0
- package/dist/lib/session/db.js +129 -50
- package/dist/lib/session/discover.d.ts +5 -0
- package/dist/lib/session/discover.js +14 -3
- package/dist/lib/session/remote.d.ts +4 -6
- package/dist/lib/session/remote.js +5 -12
- package/dist/lib/session/run-names.d.ts +32 -0
- package/dist/lib/session/run-names.js +63 -0
- package/dist/lib/session/types.d.ts +8 -0
- package/dist/lib/session/viewing-in.d.ts +54 -0
- package/dist/lib/session/viewing-in.js +155 -0
- package/dist/lib/ssh-tunnel.d.ts +1 -1
- package/dist/lib/ssh-tunnel.js +3 -3
- package/dist/lib/tmux/session.d.ts +46 -0
- package/dist/lib/tmux/session.js +84 -2
- package/dist/lib/usage.d.ts +5 -3
- package/dist/lib/usage.js +5 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 1.20.43
|
|
6
|
+
|
|
7
|
+
- **NEW: `agents run --name <slug>` — a durable, human/agent-friendly handle for any run.** An agent that dispatches another agent had no cheap status handle: the host-task id was never even printed (the `--no-follow` tip showed a literal `<id>` placeholder), and only Claude's session id is known up front (pre-minted `--session-id`) — every other agent's id is discovered later by scanning transcripts, so callers fell back to `agents logs`, which dumps the raw, token-heavy transcript. `--name` is chosen at launch, agent-agnostic, and stored on the structures that already back these views: a first-class `name` column on `sessions.db` (schema v9, additive, no rescan) parallel to `label` — `agents sessions <ref>` resolves against **both** name and label; the HostTask sidecar (forwarded to the remote run, so `agents hosts ps` gains a NAME column and `agents hosts logs <name>` resolves by name); and a run-name sidecar (`~/.agents/.cache/run-names/`) that joins a local run's name onto the index by id every scan via `syncNames` — the same idempotent pattern as `/rename` label sync. The `name` column is deliberately left out of the upsert `ON CONFLICT … SET` clause, so a discovery rescan can never null an existing name (regression-tested in `db.names.test.ts`). Omitting `--name` is a strict no-op: `name` stays unset and every id-based path is unchanged. The `--no-follow` dispatch tip now prints the real handle and steers to the compact `agents sessions` digest over the raw log. Source: `apps/cli/src/commands/exec.ts`, `apps/cli/src/lib/session/{db,run-names,discover}.ts`, `apps/cli/src/lib/hosts/{dispatch,tasks}.ts`.
|
|
8
|
+
- **New terminals (and teammates) no longer launch into a rate-limited account; `balanced` is now the default run strategy.** Two coupled fixes. (1) A bare `agents run <agent>` — every new agent terminal the extension spawns, and every non-version-pinned `agents teams add`/`start` teammate, since both route through bare `agents run` — used to default to the `available` strategy, which *prefers the pinned default version when it looks healthy*. But "healthy" was judged by the router's `getRoutingUsedPercent`, which **excluded the 5-hour session window** and looked at weekly usage only. So a session-maxed account with weekly headroom (e.g. session 100% / week 60%) was deemed eligible and kept getting launched — while `agents view` showed it "rate-limited" (its badge, `deriveUsageStatusFromSnapshot`, *counts* the session window). The router and the badge disagreed. Now `hasUsageAvailable` shares the badge's exact signal: an account maxed on **any** blocking window (session or weekly) is ineligible and skipped by both `available` and `balanced` — you never spin up an agent on an account that can't serve the next request. Capacity *weighting* still ranks eligible accounts by weekly headroom, so a brief session spike doesn't distort long-run routing. (2) The default strategy is now `balanced` (was `available`): a bare run spreads load across all healthy accounts by remaining headroom instead of sticking to the pinned default. Override per-workspace with `run.<agent>.strategy` in `agents.yaml`, or per-invocation with `--strategy` / `-b`. Source: `apps/cli/src/lib/rotate.ts`, `apps/cli/src/lib/usage.ts`, `apps/cli/src/commands/exec.ts`.
|
|
9
|
+
- **[browser] Logins survive browser restarts: sandboxed profiles keep memory-only session cookies, without restoring tabs.** Sites that issue login cookies with `expires=-1` (idealista, many banking/classifieds sites) logged the profile out on every browser restart, because Chromium purges memory-only session cookies at startup unless the session-restore preference is set — a constraint that had already leaked into agent designs as "sessions can't survive restarts". Every launch now pins `session.restore_on_startup: 1` ("continue where you left off") in the profile's `Default/Preferences`, which is the switch Chromium's cookie purge actually keys off — and pairs it with `--no-startup-window` so the *visible* side of restore never happens: no window exists at startup for restore to fill, no ghost tabs from the last task reopen, and the task flow creates its own tab over CDP exactly as before. Verified live on Windows/Comet: a memory-only cookie planted pre-restart was still present after a full stop/start, with OS-level window enumeration confirming a single window and zero restored tabs. The Preferences patch runs pre-spawn (browser down, so Chromium can't overwrite it on exit), stamps the profile name only on first launch, skips malformed files untouched, and is a no-op when already set. Electron profiles keep the old name-only seeding — they manage their own storage and need their startup window (the CDP driver binds to it). Bare `agents browser start` (no `--url`) recreates the old startup-window affordance by opening a blank page target when none exists, unregistered on the task like the startup window always was. Server-side session TTLs still apply — this removes the restart logout, not the site's own expiry. Source: `apps/cli/src/lib/browser/chrome.ts` (`ensureProfilePreferences`, launch args), `apps/cli/src/lib/browser/service.ts`.
|
|
10
|
+
- **Security fix: `agents sessions --host <target>` no longer accepts a leading-dash target (SSH argv-flag smuggling).** `session/remote.ts` carried its own copy of `assertValidSshTarget` that omitted the `host.startsWith('-')` guard every other SSH path enforces, so a bare flag like `-l` or `-F/path` — which passes the character allowlist — was handed straight to `ssh` as an argument (`-oProxyCommand=…`-class injection) before any connection. The duplicate validator (and its `SSH_TARGET_RE`) is deleted; `runRemoteSessions` now routes through the canonical `assertValidSshTarget` in `ssh-exec.ts`, whose dash guard is already regression-tested (`ssh-exec.test.ts`). Source: `apps/cli/src/lib/session/remote.ts`.
|
|
11
|
+
|
|
12
|
+
## 1.20.42
|
|
13
|
+
|
|
14
|
+
- **Fix: exiting a split pane inside an interactive `ag run` session kicked you out of tmux entirely.** When you split the window of an interactive agent session (`ag run claude`) with Ctrl-b `"`/`%` and then `exit`ed *your* split, the whole tmux client detached and dumped you back to the parent shell — even though the agent was still running in the other pane. Cause: `runInTmux` installed a session-wide `pane-died` hook (`detach-client`) meant to fire only when the AGENT pane exits (so the attach returns and the exit status is read), but with no `#{hook_pane}` guard it fired for *any* pane's death. The hook is now scoped to the agent pane; a user split that exits is closed in place (`kill-pane`, no lingering dead husk) and the agent keeps running full-window. Source: `apps/cli/src/lib/exec.ts`, `apps/cli/src/lib/tmux/session.test.ts`.
|
|
15
|
+
- **Every secret-value read is now audited, not just the ones that flowed through the resolver.** `agents events --module secrets` (or `--event secrets.get`) is meant to show "every secret accessed or revealed", but several paths read plaintext values without going through `readAndResolveBundleEnv` (the only place that emitted `secrets.get`), so they were invisible: `secrets push` (which reads the whole bundle to upload it — the most sensitive silent read), `secrets view --reveal`, the raw `secrets get <item>`, `secrets set <item>` (a raw write, no `secrets.set`), and the *initiating* side of `secrets exec --host` / `run --secrets bundle@host` (only the remote host logged it). Each now emits with a `source` telling you HOW it was read — `keychain`, `agent` (served from the unlocked broker), `reveal`, `raw-item`, `sync-push`, or `remote` (with the target `host`) — alongside the bundle, caller, keyCount, and OS-user/host/transport. The resolved **value is never written to the log**, only names and counts. All `secrets.*` events are now tagged `module: 'secrets'` so `--module secrets` actually surfaces the value reads (previously it matched only the coarse command events). Note: the event log has a 7-day retention, so export what you need for long-term records. Source: `src/lib/secrets/bundles.ts`, `src/lib/secrets/sync.ts`, `src/lib/secrets/remote.ts`, `src/commands/secrets.ts`, `docs/06-observability.md`.
|
|
16
|
+
- **Fix: `sessions --active` showed the SAME preview + topic for every co-located session.** Multiple Claude sessions in one cwd (e.g. several editor tabs, or two worktree siblings) all rendered identical activity — they looked like duplicate cards. `findClaudeSessionFile` fell back to the newest `.jsonl` in the cwd whenever a session's `<id>.jsonl` wasn't found, so every distinct session collapsed onto ONE file's preview/topic. The stale-id trigger: an editor caches the launch uuid in `live-terminals.json`, but Claude rotates its transcript uuid on resume/compact, so the cached id no longer matches any file. Now the terminal path resolves each tab's EXACT id from the pid registry (mirroring the headless path), the newest-file fallback is gated to the no-id case (`pickSessionFile`), and an unresolvable file reads as `idle` rather than `running`. Source: `apps/cli/src/lib/session/active.ts`.
|
|
17
|
+
- **Fix: one malformed Kimi session blanked the WHOLE `agents sessions` listing.** A Kimi `state.json` with neither `createdAt` nor `updatedAt` made `readKimiMeta` return an `undefined` timestamp, which binds `NULL` into the `timestamp TEXT NOT NULL` column and aborts the entire batch index — so a single bad session took down the listing for every session, not just itself. Two layers: `readKimiMeta` now coerces the timestamp to never-null, falling back to the `state.json` mtime (matching how the listing already ranks Kimi via `last_activity`, like every other parser); and `upsertSessionsBatch` wraps each row in a per-row guard so a future constraint-violating row skips itself (ledger deliberately not stamped, so the next scan re-tries it) instead of rolling back the whole batch. Source: `apps/cli/src/lib/session/discover.ts`, `apps/cli/src/lib/session/db.ts`.
|
|
18
|
+
|
|
5
19
|
## 1.20.41
|
|
6
20
|
|
|
7
21
|
- **NEW: `agents sessions focus [id]`** — one command to get back to a session, however it's reachable. It **attaches** a live session in place (tmux `switch-client`/`attach-session`, a remote tmux over `ssh -tt`, or a Ghostty tab — joining the live process without forking); where there's **no live terminal to attach**, it **opens a new tab and resumes** the session — locally, or on the remote peer over SSH (`runOnPeer`, so the peer resolves the version-pinned binary). No id opens the rich live-session picker (this-machine first). Reuses the live-session detection and the terminal launch engine (`openSurfaces`), and folds `go`'s attach paths in. Source: `src/commands/focus.ts`, `src/commands/go.ts`.
|
|
@@ -200,7 +200,7 @@ function warnIfNotFrontmost(res) {
|
|
|
200
200
|
}
|
|
201
201
|
}
|
|
202
202
|
function reportMissingHelper() {
|
|
203
|
-
console.error('helper not built. Run: ./
|
|
203
|
+
console.error('helper not built. Run: ./native/computer-mac/scripts/build.sh debug');
|
|
204
204
|
process.exit(1);
|
|
205
205
|
}
|
|
206
206
|
// Open a client, run fn, always close. Fails fast if no helper is present.
|
|
@@ -22,9 +22,9 @@ export declare function detectImageFormat(buf: Buffer): '.png' | '.jpg' | null;
|
|
|
22
22
|
* Make the screenshot filename honest about its bytes. The two helper backends
|
|
23
23
|
* encode DIFFERENT formats and neither re-encodes to match the requested name:
|
|
24
24
|
* the macOS helper (ScreenCaptureKit) returns JPEG
|
|
25
|
-
* (
|
|
25
|
+
* (native/computer-mac/Sources/ComputerHelper/Screenshot.swift:207,212),
|
|
26
26
|
* the Windows helper returns PNG
|
|
27
|
-
* (
|
|
27
|
+
* (native/computer-win/Screenshot.cs:33). So a fixed default extension
|
|
28
28
|
* cannot be correct for both — the only honest path is to sniff the real format
|
|
29
29
|
* and swap the extension to match. Pure so it's unit-testable.
|
|
30
30
|
*
|
|
@@ -53,9 +53,9 @@ export function detectImageFormat(buf) {
|
|
|
53
53
|
* Make the screenshot filename honest about its bytes. The two helper backends
|
|
54
54
|
* encode DIFFERENT formats and neither re-encodes to match the requested name:
|
|
55
55
|
* the macOS helper (ScreenCaptureKit) returns JPEG
|
|
56
|
-
* (
|
|
56
|
+
* (native/computer-mac/Sources/ComputerHelper/Screenshot.swift:207,212),
|
|
57
57
|
* the Windows helper returns PNG
|
|
58
|
-
* (
|
|
58
|
+
* (native/computer-win/Screenshot.cs:33). So a fixed default extension
|
|
59
59
|
* cannot be correct for both — the only honest path is to sniff the real format
|
|
60
60
|
* and swap the extension to match. Pure so it's unit-testable.
|
|
61
61
|
*
|
|
@@ -354,7 +354,7 @@ function registerSetupCommand(program) {
|
|
|
354
354
|
}
|
|
355
355
|
const srcApp = resolveHelperApp();
|
|
356
356
|
if (!srcApp || !fs.existsSync(srcApp)) {
|
|
357
|
-
console.error('helper not built. Run: ./
|
|
357
|
+
console.error('helper not built. Run: ./native/computer-mac/scripts/build.sh debug');
|
|
358
358
|
process.exit(1);
|
|
359
359
|
}
|
|
360
360
|
const home = os.homedir();
|
|
@@ -392,7 +392,7 @@ function registerSetupCommand(program) {
|
|
|
392
392
|
}
|
|
393
393
|
catch {
|
|
394
394
|
console.error('codesign verify FAILED. The destination .app is unsigned or its signature was stripped.');
|
|
395
|
-
console.error('rebuild the helper with a Developer ID cert: ./
|
|
395
|
+
console.error('rebuild the helper with a Developer ID cert: ./native/computer-mac/scripts/build.sh release');
|
|
396
396
|
process.exit(1);
|
|
397
397
|
}
|
|
398
398
|
// 3. Ensure socket + log parent dirs exist.
|
package/dist/commands/exec.js
CHANGED
|
@@ -218,11 +218,13 @@ 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>', 'Give the run a durable name — a stable handle you can check on later with `agents sessions <name>` (and `agents hosts logs <name>` for --host runs), instead of an opaque id. Optional; omitting it keeps today\'s id-only behavior.')
|
|
221
222
|
.option('--verbose', 'Show detailed execution logs')
|
|
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.')
|
|
222
224
|
.option('--timeout <duration>', 'Kill the agent after this duration (e.g., 30m, 1h, 2h30m)')
|
|
223
225
|
.option('--fallback <agents>', 'Comma-separated agents to try on rate-limit failure. Each entry accepts an optional @version pin (e.g., codex@0.116.0,gemini). The primary runs first; if it exits with a rate-limit error, the next agent picks up via /continue handoff.')
|
|
224
226
|
.option('-b, --balanced', 'Shortcut for --strategy balanced. Ignored when @version is pinned.')
|
|
225
|
-
.option('--strategy <strategy>', 'Version/account selection strategy: pinned | available | balanced. Defaults to run.<agent>.strategy, then
|
|
227
|
+
.option('--strategy <strategy>', 'Version/account selection strategy: pinned | available | balanced. Defaults to run.<agent>.strategy, then balanced (spreads load across healthy accounts and skips any that are rate-limited). (Legacy `rotate` accepted as alias for `balanced`.)')
|
|
226
228
|
.option('--acp', 'Route through the Agent Client Protocol instead of direct exec. Supported for gemini, claude (via @zed-industries/claude-code-acp adapter). Unified event stream; emits ndjson when --json.')
|
|
227
229
|
.option('-y, --yes', 'Skip the interactive budget-confirm prompt (require_confirm_over). Never skips a hard budget block.', false)
|
|
228
230
|
.option('--loop', 'Re-inject the prompt/entrypoint each iteration until a stop condition (issue #332). Guards (--max-iterations, --budget, --until) are enforced outside the agent. Writes a checkpoint after every iteration for --resume-checkpoint.')
|
|
@@ -274,9 +276,10 @@ export function registerRunCommand(program) {
|
|
|
274
276
|
Legacy 'full' is silently rewritten to 'skip'.
|
|
275
277
|
|
|
276
278
|
Run strategy (set via --strategy or run.<agent>.strategy in agents.yaml):
|
|
277
|
-
pinned use the workspace/global pinned version
|
|
278
|
-
available use pinned if
|
|
279
|
-
balanced distribute load across healthy accounts by remaining capacity
|
|
279
|
+
pinned use the workspace/global pinned version
|
|
280
|
+
available use pinned if it can run right now; otherwise switch to another signed-in version
|
|
281
|
+
balanced distribute load across healthy accounts by remaining capacity (default)
|
|
282
|
+
A version/account is skipped when it is rate-limited right now — any usage window (incl. the 5-hour session window) at 100%, matching the 'agents view' badge.
|
|
280
283
|
--balanced is shorthand for --strategy balanced. Ignored when @version is pinned, when a profile is used, or with --fallback.
|
|
281
284
|
|
|
282
285
|
Fallback: --fallback codex,gemini retries on rate-limit failure via /continue handoff. Each entry accepts @version.
|
|
@@ -403,15 +406,22 @@ export function registerRunCommand(program) {
|
|
|
403
406
|
model: options.model,
|
|
404
407
|
remoteCwd: options.remoteCwd,
|
|
405
408
|
sessionId: hostSessionId,
|
|
409
|
+
name: options.name,
|
|
406
410
|
resume: resumeId,
|
|
407
411
|
follow: options.follow !== false,
|
|
408
412
|
});
|
|
409
413
|
// Register the dispatched run in the LOCAL session index so it shows
|
|
410
|
-
// up in `agents sessions` and resolves by id, even though its
|
|
414
|
+
// up in `agents sessions` and resolves by id/name, even though its
|
|
411
415
|
// transcript lives on the host. No-op when no session id was captured.
|
|
412
416
|
registerHostSession(task, { cwd: process.cwd(), prompt });
|
|
413
417
|
if (options.follow === false) {
|
|
414
|
-
|
|
418
|
+
// The handle the caller uses to check on the run: the name if given,
|
|
419
|
+
// else the real host-task id (never the old literal `<id>`). Steer
|
|
420
|
+
// to the compact `agents sessions` digest over the raw log first.
|
|
421
|
+
const handle = task.name ?? task.id;
|
|
422
|
+
console.log(chalk.green(`Dispatched to ${host.name}${task.name ? ` as "${task.name}"` : ''}.`) + '\n' +
|
|
423
|
+
chalk.gray(` Status: agents sessions ${handle}`) + chalk.gray(' (compact digest — use this)') + '\n' +
|
|
424
|
+
chalk.gray(` Raw log: agents hosts logs ${handle} -f`) + chalk.gray(' (heavy, only if needed)'));
|
|
415
425
|
process.exit(0);
|
|
416
426
|
}
|
|
417
427
|
// -1 = the follow window closed but the run continues on the host (the
|
|
@@ -1061,8 +1071,10 @@ export function registerRunCommand(program) {
|
|
|
1061
1071
|
json: options.json,
|
|
1062
1072
|
headless: options.headless,
|
|
1063
1073
|
sessionId: resumeSessionId ?? options.sessionId,
|
|
1074
|
+
name: options.name,
|
|
1064
1075
|
resume: resumeNative,
|
|
1065
1076
|
verbose: options.verbose,
|
|
1077
|
+
raw: options.raw,
|
|
1066
1078
|
timeout: options.timeout,
|
|
1067
1079
|
env,
|
|
1068
1080
|
toolsRestrict: workflowToolsRestrict,
|
package/dist/commands/go.js
CHANGED
|
@@ -122,9 +122,14 @@ export function describeWhere(s, self) {
|
|
|
122
122
|
const remote = s.machine && s.machine !== self ? s.machine : undefined;
|
|
123
123
|
const mux = s.provenance?.mux;
|
|
124
124
|
if (mux?.kind === 'tmux' && mux.pane) {
|
|
125
|
+
// When the renderer has resolved the current viewer, fold it into the label
|
|
126
|
+
// so `focus` reports "tmux %3 (viewing in codium tab 2)" / "(detached)".
|
|
127
|
+
const view = s.viewingIn
|
|
128
|
+
? ` (viewing in ${s.viewingIn.app}${s.viewingIn.tab != null ? ` tab ${s.viewingIn.tab}` : ''})`
|
|
129
|
+
: '';
|
|
125
130
|
return remote
|
|
126
131
|
? { label: `tmux ${mux.pane} on ${remote}`, action: `ssh + attach on ${remote}` }
|
|
127
|
-
: { label: `tmux ${mux.pane}`, action: 'attach its tmux' };
|
|
132
|
+
: { label: `tmux ${mux.pane}${view}`, action: 'attach its tmux' };
|
|
128
133
|
}
|
|
129
134
|
if (!remote && s.host === 'ghostty')
|
|
130
135
|
return { label: 'Ghostty', action: 'focus its Ghostty tab' };
|
package/dist/commands/hosts.js
CHANGED
|
@@ -15,7 +15,7 @@ import { sshTargetFor } from '../lib/hosts/types.js';
|
|
|
15
15
|
import { listSshConfigHosts, listKnownHosts, isSshConfigHost } from '../lib/hosts/ssh-config.js';
|
|
16
16
|
import { probeHost, remoteAgentsVersion, bootstrapAgentsCli, localCliVersion, } from '../lib/hosts/ready.js';
|
|
17
17
|
import { resolveRemoteOsSync } from '../lib/hosts/remote-os.js';
|
|
18
|
-
import { listTasks } from '../lib/hosts/tasks.js';
|
|
18
|
+
import { listTasks, loadTask, findTaskByName, findTaskBySessionId } from '../lib/hosts/tasks.js';
|
|
19
19
|
import { reconcileRunningTasks } from '../lib/hosts/reconcile.js';
|
|
20
20
|
import { showHostTaskLog } from '../lib/hosts/logs.js';
|
|
21
21
|
/** Parse `user@host` or `host` into its pieces. */
|
|
@@ -179,18 +179,22 @@ async function doPs(json) {
|
|
|
179
179
|
return;
|
|
180
180
|
}
|
|
181
181
|
const cols = terminalWidth();
|
|
182
|
-
console.log(chalk.bold('ID').padEnd(11) + chalk.bold('HOST').padEnd(16) + chalk.bold('AGENT').padEnd(10) + chalk.bold('STATUS').padEnd(11) + chalk.bold('PROMPT'));
|
|
182
|
+
console.log(chalk.bold('ID').padEnd(11) + chalk.bold('NAME').padEnd(16) + chalk.bold('HOST').padEnd(16) + chalk.bold('AGENT').padEnd(10) + chalk.bold('STATUS').padEnd(11) + chalk.bold('PROMPT'));
|
|
183
183
|
for (const t of tasks) {
|
|
184
184
|
const status = t.status === 'completed' ? chalk.green(t.status) : t.status === 'failed' ? chalk.red(t.status) : chalk.yellow(t.status);
|
|
185
|
+
const nameCol = truncateToWidth(t.name ?? chalk.gray('-'), 15).padEnd(16);
|
|
185
186
|
// Prompt fills the remaining width instead of a fixed 50-char byte slice (98-char rows).
|
|
186
|
-
const promptCol = truncateToWidth(t.prompt, Math.max(12, cols - (11 + 16 + 10 + 11)));
|
|
187
|
-
console.log(t.id.padEnd(11) + t.host.padEnd(16) + t.agent.padEnd(10) + status.padEnd(11) + promptCol);
|
|
187
|
+
const promptCol = truncateToWidth(t.prompt, Math.max(12, cols - (11 + 16 + 16 + 10 + 11)));
|
|
188
|
+
console.log(t.id.padEnd(11) + nameCol + t.host.padEnd(16) + t.agent.padEnd(10) + status.padEnd(11) + promptCol);
|
|
188
189
|
}
|
|
189
190
|
}
|
|
190
|
-
async function doLogs(
|
|
191
|
+
async function doLogs(ref, follow) {
|
|
192
|
+
// Resolve the ref as a task id first, then fall back to a `--name` handle so
|
|
193
|
+
// `agents hosts logs <name>` works, not just the opaque id.
|
|
194
|
+
const id = loadTask(ref) ? ref : (findTaskByName(ref)?.id ?? findTaskBySessionId(ref)?.id ?? ref);
|
|
191
195
|
const res = await showHostTaskLog(id, follow);
|
|
192
196
|
if (!res.found) {
|
|
193
|
-
console.log(chalk.red(`Unknown task "${
|
|
197
|
+
console.log(chalk.red(`Unknown task "${ref}".`));
|
|
194
198
|
process.exitCode = 1;
|
|
195
199
|
return;
|
|
196
200
|
}
|
package/dist/commands/secrets.js
CHANGED
|
@@ -18,6 +18,7 @@ import { getKeychainToken, getKeychainTokens, hasKeychainToken, secretsKeychainI
|
|
|
18
18
|
import { assertOpAvailable, createPasswordItem, deleteItemByTitle, extractSecrets, itemExistsByTitle, listItems, listVaults, } from '../lib/onepassword.js';
|
|
19
19
|
import { DEFAULT_TTL_MS, agentLoad, agentLock, agentStatus, ensureAgentRunning, installSecretsAgentService, runAgentLoadFromStdin, runSecretsAgent, secretsAgentServiceInstalled, uninstallSecretsAgentService, } from '../lib/secrets/agent.js';
|
|
20
20
|
import { parseDuration } from '../lib/hooks/cache.js';
|
|
21
|
+
import { emit } from '../lib/events.js';
|
|
21
22
|
import { registerCommandGroups, setHelpSections } from '../lib/help.js';
|
|
22
23
|
import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
23
24
|
import { registerSecretsSyncCommands } from './secrets-sync.js';
|
|
@@ -658,6 +659,26 @@ export function registerSecretsCommands(program) {
|
|
|
658
659
|
catch {
|
|
659
660
|
// Fall through to masked output on cancellation / batch failure.
|
|
660
661
|
}
|
|
662
|
+
// Revealing plaintext bypasses readAndResolveBundleEnv (the usual
|
|
663
|
+
// audit chokepoint), so emit here — a `--reveal` exposes real values
|
|
664
|
+
// and must show up in `agents events --module secrets`. Count both the
|
|
665
|
+
// keychain values actually decrypted AND the inline literals (which
|
|
666
|
+
// `--reveal` always prints, even for a literal-only bundle with no
|
|
667
|
+
// keychain refs — see the entries loop below). Values are never
|
|
668
|
+
// included, only how many keys were exposed. Emit only when something
|
|
669
|
+
// was actually shown (a cancelled Touch ID + no literals reveals none).
|
|
670
|
+
const literalCount = entries.filter((e) => e.kind === 'literal').length;
|
|
671
|
+
const exposedCount = revealedValues.size + literalCount;
|
|
672
|
+
if (exposedCount > 0) {
|
|
673
|
+
emit('secrets.get', {
|
|
674
|
+
module: 'secrets',
|
|
675
|
+
bundle: bundle.name,
|
|
676
|
+
caller: 'view --reveal',
|
|
677
|
+
source: 'reveal',
|
|
678
|
+
status: 'success',
|
|
679
|
+
keyCount: exposedCount,
|
|
680
|
+
});
|
|
681
|
+
}
|
|
661
682
|
}
|
|
662
683
|
for (const e of entries) {
|
|
663
684
|
if (e.kind === 'keychain') {
|
|
@@ -703,6 +724,9 @@ export function registerSecretsCommands(program) {
|
|
|
703
724
|
// so `$(agents secrets get NAME)` captures it cleanly); diagnostics go
|
|
704
725
|
// to stderr so they never pollute the captured value.
|
|
705
726
|
const value = getKeychainToken(item);
|
|
727
|
+
// Raw item reads bypass readAndResolveBundleEnv, so audit here too.
|
|
728
|
+
// `item` is the keychain service name, never the value.
|
|
729
|
+
emit('secrets.get', { module: 'secrets', item, source: 'raw-item', status: 'success' });
|
|
706
730
|
process.stdout.write(value.endsWith('\n') ? value : `${value}\n`);
|
|
707
731
|
}
|
|
708
732
|
catch {
|
|
@@ -734,6 +758,8 @@ export function registerSecretsCommands(program) {
|
|
|
734
758
|
// so `agents secrets get` can read them back without a password sheet;
|
|
735
759
|
// on Linux it goes through secret-tool / encrypted-file fallback.
|
|
736
760
|
setKeychainToken(item, value);
|
|
761
|
+
// Raw item writes bypass writeBundle (the usual secrets.set chokepoint).
|
|
762
|
+
emit('secrets.set', { module: 'secrets', item, source: 'raw-item' });
|
|
737
763
|
console.error(chalk.green(`Stored keychain item '${item}'.`));
|
|
738
764
|
}
|
|
739
765
|
catch (err) {
|
|
@@ -18,7 +18,8 @@ import { discoverArtifacts, readArtifact, resolveArtifact } from '../lib/session
|
|
|
18
18
|
import { looksLikePath, toComparablePath, homeDir, needsWindowsShell, findExecutable } from '../lib/platform/index.js';
|
|
19
19
|
import { getActiveSessions } from '../lib/session/active.js';
|
|
20
20
|
import { enumerateGhosttyTabs, assignGhosttyTabs } from '../lib/session/ghostty-tabs.js';
|
|
21
|
-
import { mapPanesToTargets } from '../lib/tmux/session.js';
|
|
21
|
+
import { mapPanesToTargets, listClients } from '../lib/tmux/session.js';
|
|
22
|
+
import { resolveViewingIn } from '../lib/session/viewing-in.js';
|
|
22
23
|
import { machineId, normalizeHost } from '../lib/session/sync/config.js';
|
|
23
24
|
import { gatherRemoteActive, NO_FANOUT_ENV } from '../lib/session/remote-active.js';
|
|
24
25
|
import { gatherRemoteList, runOnPeer } from '../lib/session/remote-list.js';
|
|
@@ -314,6 +315,16 @@ function locatorBadge(s) {
|
|
|
314
315
|
parts.push(chalk.red('ssh'));
|
|
315
316
|
if (p?.mux?.kind === 'tmux' && (s.tmuxTarget || p.mux.pane)) {
|
|
316
317
|
parts.push(chalk.green(s.tmuxTarget ?? p.mux.pane));
|
|
318
|
+
// For a tmux-hosted session, say which app+tab is looking at it right now
|
|
319
|
+
// (or that it's running detached). Only meaningful for tmux (the pane is the
|
|
320
|
+
// durable handle; the viewer is transient).
|
|
321
|
+
if (s.viewingIn) {
|
|
322
|
+
const tab = s.viewingIn.tab != null ? ` tab ${s.viewingIn.tab}` : '';
|
|
323
|
+
parts.push(chalk.gray(`viewing in ${s.viewingIn.app}${tab}`));
|
|
324
|
+
}
|
|
325
|
+
else {
|
|
326
|
+
parts.push(chalk.gray('detached'));
|
|
327
|
+
}
|
|
317
328
|
}
|
|
318
329
|
else if (p?.mux?.kind === 'screen') {
|
|
319
330
|
parts.push(chalk.green('screen'));
|
|
@@ -617,20 +628,27 @@ async function enrichLocalLocators(local) {
|
|
|
617
628
|
}
|
|
618
629
|
}
|
|
619
630
|
catch { /* non-fatal */ }
|
|
620
|
-
// tmux attach targets, one batched query per
|
|
631
|
+
// tmux attach targets + "viewing in <app> tab N", one batched query per socket.
|
|
621
632
|
try {
|
|
622
633
|
const tmux = local.filter(s => s.provenance?.mux?.kind === 'tmux' && s.provenance.mux.pane);
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
for (const
|
|
629
|
-
|
|
634
|
+
if (tmux.length > 0) {
|
|
635
|
+
// One Ghostty enumeration shared across every socket's viewing-in resolve
|
|
636
|
+
// (a tmux client can be attached from a Ghostty tab).
|
|
637
|
+
const surfaces = await enumerateGhosttyTabs();
|
|
638
|
+
const sockets = new Set(tmux.map(s => s.provenance.mux.socket));
|
|
639
|
+
for (const socket of sockets) {
|
|
640
|
+
const paneMap = await mapPanesToTargets(socket);
|
|
641
|
+
if (paneMap.size === 0)
|
|
630
642
|
continue;
|
|
631
|
-
const
|
|
632
|
-
|
|
633
|
-
s.
|
|
643
|
+
const clients = await listClients(socket);
|
|
644
|
+
for (const s of tmux) {
|
|
645
|
+
if (s.provenance.mux.socket !== socket)
|
|
646
|
+
continue;
|
|
647
|
+
const target = paneMap.get(s.provenance.mux.pane);
|
|
648
|
+
if (target)
|
|
649
|
+
s.tmuxTarget = target;
|
|
650
|
+
s.viewingIn = await resolveViewingIn(s, clients, { paneToTarget: paneMap, ghosttySurfaces: surfaces });
|
|
651
|
+
}
|
|
634
652
|
}
|
|
635
653
|
}
|
|
636
654
|
}
|
|
@@ -36,6 +36,28 @@ export declare function getRunningChromeInfo(profileName: string): {
|
|
|
36
36
|
pid: number;
|
|
37
37
|
port: number;
|
|
38
38
|
} | null;
|
|
39
|
+
/**
|
|
40
|
+
* Prepare `<userDataDir>/Default/Preferences` before launch.
|
|
41
|
+
*
|
|
42
|
+
* Two concerns, one write:
|
|
43
|
+
* - First launch (file absent): stamp the agents-cli profile name so
|
|
44
|
+
* Chromium's UI shows "<profile>" instead of its default "Person 1".
|
|
45
|
+
* Cosmetic; existing files keep whatever Chrome wrote in the meantime.
|
|
46
|
+
* - Every launch (when `persistSessionCookies`): pin
|
|
47
|
+
* `session.restore_on_startup: 1` ("continue where you left off").
|
|
48
|
+
* Chromium purges memory-only session cookies at startup UNLESS this
|
|
49
|
+
* preference says the session will be restored — it keys the purge off
|
|
50
|
+
* the pref, not off tabs actually reopening. Sites like idealista issue
|
|
51
|
+
* login cookies with `expires=-1`, so without this every browser restart
|
|
52
|
+
* silently logs the profile out. The visible tab-restore side effect is
|
|
53
|
+
* suppressed separately via `--no-startup-window` (see launchBrowser).
|
|
54
|
+
*
|
|
55
|
+
* Runs only while the browser is down (called before spawn), so Chromium
|
|
56
|
+
* can't overwrite the patch on exit. Best-effort: a malformed existing file
|
|
57
|
+
* is left untouched (Chromium recovers its own state better than we can),
|
|
58
|
+
* and any I/O hiccup is silently ignored.
|
|
59
|
+
*/
|
|
60
|
+
export declare function ensureProfilePreferences(userDataDir: string, profileName: string, persistSessionCookies: boolean): void;
|
|
39
61
|
/**
|
|
40
62
|
* Is a TCP port currently bound? `lsof` on POSIX, `netstat -ano` on Windows
|
|
41
63
|
* (lsof doesn't exist there). Returns false on any tooling error so port
|
|
@@ -212,11 +212,11 @@ isElectron = false) {
|
|
|
212
212
|
const runtimeDir = getProfileRuntimeDir(profileName);
|
|
213
213
|
const userDataDir = path.join(runtimeDir, 'chrome-data');
|
|
214
214
|
fs.mkdirSync(userDataDir, { recursive: true });
|
|
215
|
-
//
|
|
216
|
-
//
|
|
217
|
-
//
|
|
218
|
-
//
|
|
219
|
-
|
|
215
|
+
// Pre-launch Preferences pass: first-launch profile-name stamp, plus (for
|
|
216
|
+
// real browsers, not Electron apps) the session-cookie persistence pin.
|
|
217
|
+
// Electron apps manage their own storage and don't read Chromium's
|
|
218
|
+
// `session.*` prefs, so they get the name stamp only.
|
|
219
|
+
ensureProfilePreferences(userDataDir, profileName, !isElectron);
|
|
220
220
|
// Chromium on macOS coordinates instances via the SingletonLock file
|
|
221
221
|
// *inside* each user-data-dir. Direct binary spawn with a fresh
|
|
222
222
|
// --user-data-dir creates a fully independent process — the user's
|
|
@@ -242,6 +242,14 @@ isElectron = false) {
|
|
|
242
242
|
// remote-debugging transport is active. That property is the loudest
|
|
243
243
|
// signal Cloudflare Turnstile, hCaptcha, and similar checks read.
|
|
244
244
|
'--disable-blink-features=AutomationControlled',
|
|
245
|
+
// Companion to `session.restore_on_startup: 1` (see
|
|
246
|
+
// ensureProfilePreferences): the pref keeps session cookies alive across
|
|
247
|
+
// restarts, but on its own it would also reopen last session's tabs at
|
|
248
|
+
// startup. Suppressing the startup window leaves restore nothing to fill —
|
|
249
|
+
// cookies survive, no ghost tabs — and the task flow creates its own tab
|
|
250
|
+
// over CDP anyway. Electron apps need their window to appear (the CDP
|
|
251
|
+
// driver binds to it), so they skip the flag.
|
|
252
|
+
...(isElectron ? [] : ['--no-startup-window']),
|
|
245
253
|
...(options.headless ? ['--headless=new'] : []),
|
|
246
254
|
`--window-size=${viewport.width},${viewport.height}`,
|
|
247
255
|
...(viewport.x !== undefined && viewport.y !== undefined
|
|
@@ -325,20 +333,52 @@ export function getRunningChromeInfo(profileName) {
|
|
|
325
333
|
return { pid: rt.pid, port: rt.port };
|
|
326
334
|
}
|
|
327
335
|
/**
|
|
328
|
-
*
|
|
329
|
-
*
|
|
330
|
-
*
|
|
331
|
-
*
|
|
332
|
-
*
|
|
336
|
+
* Prepare `<userDataDir>/Default/Preferences` before launch.
|
|
337
|
+
*
|
|
338
|
+
* Two concerns, one write:
|
|
339
|
+
* - First launch (file absent): stamp the agents-cli profile name so
|
|
340
|
+
* Chromium's UI shows "<profile>" instead of its default "Person 1".
|
|
341
|
+
* Cosmetic; existing files keep whatever Chrome wrote in the meantime.
|
|
342
|
+
* - Every launch (when `persistSessionCookies`): pin
|
|
343
|
+
* `session.restore_on_startup: 1` ("continue where you left off").
|
|
344
|
+
* Chromium purges memory-only session cookies at startup UNLESS this
|
|
345
|
+
* preference says the session will be restored — it keys the purge off
|
|
346
|
+
* the pref, not off tabs actually reopening. Sites like idealista issue
|
|
347
|
+
* login cookies with `expires=-1`, so without this every browser restart
|
|
348
|
+
* silently logs the profile out. The visible tab-restore side effect is
|
|
349
|
+
* suppressed separately via `--no-startup-window` (see launchBrowser).
|
|
350
|
+
*
|
|
351
|
+
* Runs only while the browser is down (called before spawn), so Chromium
|
|
352
|
+
* can't overwrite the patch on exit. Best-effort: a malformed existing file
|
|
353
|
+
* is left untouched (Chromium recovers its own state better than we can),
|
|
354
|
+
* and any I/O hiccup is silently ignored.
|
|
333
355
|
*/
|
|
334
|
-
function
|
|
356
|
+
export function ensureProfilePreferences(userDataDir, profileName, persistSessionCookies) {
|
|
335
357
|
const defaultDir = path.join(userDataDir, 'Default');
|
|
336
358
|
const prefsPath = path.join(defaultDir, 'Preferences');
|
|
337
|
-
|
|
359
|
+
let prefs;
|
|
360
|
+
try {
|
|
361
|
+
prefs = JSON.parse(fs.readFileSync(prefsPath, 'utf8'));
|
|
362
|
+
if (typeof prefs !== 'object' || prefs === null)
|
|
363
|
+
return; // not ours to fix
|
|
364
|
+
}
|
|
365
|
+
catch (err) {
|
|
366
|
+
if (err?.code !== 'ENOENT')
|
|
367
|
+
return; // unreadable/malformed: leave alone
|
|
368
|
+
}
|
|
369
|
+
const firstLaunch = prefs === undefined;
|
|
370
|
+
if (firstLaunch)
|
|
371
|
+
prefs = { profile: { name: profileName } };
|
|
372
|
+
let dirty = firstLaunch;
|
|
373
|
+
if (persistSessionCookies && prefs.session?.restore_on_startup !== 1) {
|
|
374
|
+
prefs.session = { ...prefs.session, restore_on_startup: 1 };
|
|
375
|
+
dirty = true;
|
|
376
|
+
}
|
|
377
|
+
if (!dirty)
|
|
338
378
|
return;
|
|
339
379
|
try {
|
|
340
380
|
fs.mkdirSync(defaultDir, { recursive: true });
|
|
341
|
-
fs.writeFileSync(prefsPath, JSON.stringify(
|
|
381
|
+
fs.writeFileSync(prefsPath, JSON.stringify(prefs));
|
|
342
382
|
}
|
|
343
383
|
catch { /* not critical */ }
|
|
344
384
|
}
|
|
@@ -310,6 +310,19 @@ export class BrowserService {
|
|
|
310
310
|
conn = await this.connectProfile(effectiveProfile, resolved.target);
|
|
311
311
|
this.connections.set(composite, conn);
|
|
312
312
|
}
|
|
313
|
+
// Browsers launch with --no-startup-window (session-cookie persistence,
|
|
314
|
+
// see launchBrowser), so a bare `start` with no --url would otherwise
|
|
315
|
+
// leave the user staring at a process with zero windows. Recreate the
|
|
316
|
+
// old startup-window affordance: if no page target exists, open a blank
|
|
317
|
+
// one. Deliberately NOT registered on the task — the startup window
|
|
318
|
+
// never was either, and tasks track only tabs they created.
|
|
319
|
+
if (!opts.url && !conn.electron) {
|
|
320
|
+
const { targetInfos } = (await conn.cdp.send('Target.getTargets'));
|
|
321
|
+
if (!targetInfos.some((t) => t.type === 'page')) {
|
|
322
|
+
await conn.cdp.send('Target.createTarget', { url: 'about:blank' });
|
|
323
|
+
this.invalidateTargetCache(conn);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
313
326
|
const task = {
|
|
314
327
|
id: taskId,
|
|
315
328
|
name: taskName,
|
package/dist/lib/computer-rpc.js
CHANGED
|
@@ -168,8 +168,8 @@ export function writeComputerPeers(allowedExecPaths) {
|
|
|
168
168
|
export function resolveHelperExec() {
|
|
169
169
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
170
170
|
const candidates = [
|
|
171
|
-
// Local build (running from the agents-cli checkout).
|
|
172
|
-
path.resolve(here, '..', '..', '
|
|
171
|
+
// Local build (running from the agents-cli checkout). apps/cli/dist/lib -> repo root (4 up) -> native/computer-mac.
|
|
172
|
+
path.resolve(here, '..', '..', '..', '..', 'native', 'computer-mac', 'dist', 'ComputerHelper.app', 'Contents', 'MacOS', 'ComputerHelper'),
|
|
173
173
|
// Bundled with the npm package (later: CDN download lands here).
|
|
174
174
|
path.resolve(here, '..', 'computer-helper', 'ComputerHelper.app', 'Contents', 'MacOS', 'ComputerHelper'),
|
|
175
175
|
];
|
|
@@ -218,7 +218,7 @@ export function openComputerClient() {
|
|
|
218
218
|
}
|
|
219
219
|
const helperExec = resolveHelperExec();
|
|
220
220
|
if (!helperExec) {
|
|
221
|
-
throw new Error('helper not built. Run: ./
|
|
221
|
+
throw new Error('helper not built. Run: ./native/computer-mac/scripts/build.sh debug');
|
|
222
222
|
}
|
|
223
223
|
return new StdioClient(helperExec);
|
|
224
224
|
}
|
package/dist/lib/exec.d.ts
CHANGED
|
@@ -80,6 +80,13 @@ export interface ExecOptions {
|
|
|
80
80
|
addDirs?: string[];
|
|
81
81
|
timeout?: string;
|
|
82
82
|
sessionId?: string;
|
|
83
|
+
/**
|
|
84
|
+
* Durable `agents run --name <slug>` handle. Exported to the agent's env as
|
|
85
|
+
* `AGENT_SESSION_NAME` (companion to `AGENT_SESSION_ID`) and, when a session
|
|
86
|
+
* id is known at launch, recorded in the run-name index so `agents sessions
|
|
87
|
+
* <name>` resolves the run. Absent for unnamed runs — no behavior change.
|
|
88
|
+
*/
|
|
89
|
+
name?: string;
|
|
83
90
|
/**
|
|
84
91
|
* Resume the conversation named by `sessionId` using the agent's NATIVE resume
|
|
85
92
|
* form (claude `--resume`, codex `resume`) instead of the default `--session-id`
|
|
@@ -109,6 +116,12 @@ export interface ExecOptions {
|
|
|
109
116
|
mcpConfigPath?: string;
|
|
110
117
|
/** Raw args captured after `--` on the command line, forwarded verbatim to the underlying agent CLI. */
|
|
111
118
|
passthroughArgs?: string[];
|
|
119
|
+
/**
|
|
120
|
+
* Escape hatch for the interactive tmux spawn-wrap (see shouldWrapInTmux):
|
|
121
|
+
* when true, spawn the agent directly instead of inside a shared-socket tmux
|
|
122
|
+
* session. Also forced off by AGENTS_NO_TMUX=1. No effect on headless runs.
|
|
123
|
+
*/
|
|
124
|
+
raw?: boolean;
|
|
112
125
|
}
|
|
113
126
|
/**
|
|
114
127
|
* Resolve interactive vs headless. Explicit flags are definitive and win over
|
|
@@ -213,6 +226,52 @@ export declare function resolveShimSpawn(platform: NodeJS.Platform, binary: stri
|
|
|
213
226
|
* keeping version resolution in one place instead of reimplementing it in batch.
|
|
214
227
|
*/
|
|
215
228
|
export declare function execShimPassthrough(agent: AgentId, rawArgs: string[], cwd: string, pinnedVersion?: string): Promise<number>;
|
|
229
|
+
/** Inputs that decide whether an interactive spawn is wrapped in a shared-socket tmux session. */
|
|
230
|
+
export interface TmuxWrapContext {
|
|
231
|
+
/** resolveInteractive() result — only interactive REPL launches are wrapped. */
|
|
232
|
+
interactive: boolean;
|
|
233
|
+
/** process.platform — Windows has no tmux path, always spawns bare. */
|
|
234
|
+
platform: NodeJS.Platform;
|
|
235
|
+
/** True when the launcher itself already runs inside tmux ($TMUX set) — never double-wrap. */
|
|
236
|
+
inTmux: boolean;
|
|
237
|
+
/** The `--raw` escape hatch. */
|
|
238
|
+
raw: boolean;
|
|
239
|
+
/** The AGENTS_NO_TMUX=1 escape hatch. */
|
|
240
|
+
noTmuxEnv: boolean;
|
|
241
|
+
/** Whether a tmux binary is on PATH. */
|
|
242
|
+
tmuxAvailable: boolean;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Decide whether to run an interactive agent INSIDE a detached tmux session on
|
|
246
|
+
* the shared socket (then attach the current TTY) instead of a bare spawn.
|
|
247
|
+
*
|
|
248
|
+
* tmux-wrapping gives every interactive agent an exact, unique `%pane` handle so
|
|
249
|
+
* `agents sessions --active` can tell co-located agents apart, and lets `agents
|
|
250
|
+
* focus` re-attach a live session without forking it. Pure so the gate is unit-
|
|
251
|
+
* tested independently of the (side-effecting) spawn.
|
|
252
|
+
*
|
|
253
|
+
* All five guards must pass:
|
|
254
|
+
* - interactive — a headless `-p` run has no TTY to attach; keep bare spawn.
|
|
255
|
+
* - not Windows — no tmux path on win32.
|
|
256
|
+
* - not already in tmux — nesting tmux-in-tmux is pointless and confusing.
|
|
257
|
+
* - not --raw — explicit opt-out.
|
|
258
|
+
* - not AGENTS_NO_TMUX=1 — env opt-out (CI, scripts, the shim passthrough path).
|
|
259
|
+
* - tmux installed — otherwise there is nothing to wrap with.
|
|
260
|
+
*/
|
|
261
|
+
export declare function shouldWrapInTmux(ctx: TmuxWrapContext): boolean;
|
|
262
|
+
/**
|
|
263
|
+
* Build the shell command that runs an agent inside a tmux pane with the exact
|
|
264
|
+
* env the bare spawn would use. tmux runs it via `sh -c <cmd>`; we `exec env
|
|
265
|
+
* K=V … <agent> <args…>` so:
|
|
266
|
+
* - `env` materializes the full agent env INTO the pane, independent of the
|
|
267
|
+
* (possibly stale, shared) tmux server environment — additive, so tmux's own
|
|
268
|
+
* $TMUX / $TMUX_PANE still reach the agent for provenance detection;
|
|
269
|
+
* - `exec` replaces the shell so the agent is the pane's leaf process (clean
|
|
270
|
+
* `#{pane_pid}`, clean signal delivery on detach/kill).
|
|
271
|
+
* Keys are filtered to valid identifiers so exported shell functions
|
|
272
|
+
* (`BASH_FUNC_*%%`) can't make `env` choke.
|
|
273
|
+
*/
|
|
274
|
+
export declare function buildTmuxAgentCommand(executable: string, args: string[], env: NodeJS.ProcessEnv): string;
|
|
216
275
|
/** Exit code spawnAgent resolves with when a run is killed for crossing a budget cap. */
|
|
217
276
|
export declare const BUDGET_KILL_EXIT_CODE = 7;
|
|
218
277
|
/**
|