@phnx-labs/agents-cli 1.20.71 → 1.20.73
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 +325 -1
- package/README.md +40 -4
- package/dist/bin/agents +0 -0
- package/dist/commands/activity.d.ts +12 -0
- package/dist/commands/activity.js +68 -0
- package/dist/commands/apply.d.ts +13 -0
- package/dist/commands/apply.js +28 -3
- package/dist/commands/browser.js +1 -1
- package/dist/commands/cloud.js +21 -11
- package/dist/commands/commands.js +2 -0
- package/dist/commands/doctor.js +8 -4
- package/dist/commands/events.d.ts +11 -10
- package/dist/commands/events.js +30 -21
- package/dist/commands/exec.d.ts +34 -0
- package/dist/commands/exec.js +337 -39
- package/dist/commands/feed.d.ts +13 -0
- package/dist/commands/feed.js +130 -29
- package/dist/commands/hq.d.ts +2 -0
- package/dist/commands/hq.js +58 -0
- package/dist/commands/lease.d.ts +19 -0
- package/dist/commands/lease.js +138 -11
- package/dist/commands/login.d.ts +2 -0
- package/dist/commands/login.js +123 -0
- package/dist/commands/logs.js +39 -10
- package/dist/commands/mcp.js +2 -0
- package/dist/commands/mine.d.ts +27 -0
- package/dist/commands/mine.js +207 -0
- package/dist/commands/monitors.js +79 -49
- package/dist/commands/plugins.js +1 -0
- package/dist/commands/profiles.js +158 -0
- package/dist/commands/repo.js +2 -60
- package/dist/commands/resource-view.d.ts +2 -0
- package/dist/commands/resource-view.js +8 -0
- package/dist/commands/resources.d.ts +5 -0
- package/dist/commands/resources.js +95 -0
- package/dist/commands/routines.d.ts +2 -0
- package/dist/commands/routines.js +139 -37
- package/dist/commands/secrets.js +234 -47
- package/dist/commands/send.d.ts +15 -0
- package/dist/commands/send.js +62 -0
- package/dist/commands/sessions-tail.d.ts +1 -0
- package/dist/commands/sessions-tail.js +20 -9
- package/dist/commands/sessions.d.ts +6 -0
- package/dist/commands/sessions.js +26 -3
- package/dist/commands/setup-mine.d.ts +18 -0
- package/dist/commands/setup-mine.js +106 -0
- package/dist/commands/setup-share.js +2 -2
- package/dist/commands/setup.js +3 -1
- package/dist/commands/share.d.ts +8 -1
- package/dist/commands/share.js +122 -37
- package/dist/commands/skills.js +2 -0
- package/dist/commands/ssh.d.ts +9 -0
- package/dist/commands/ssh.js +196 -26
- package/dist/commands/status.js +8 -2
- package/dist/commands/subagents.js +3 -1
- package/dist/commands/sync.js +1 -1
- package/dist/commands/teams.js +11 -2
- package/dist/commands/uninstall.d.ts +11 -0
- package/dist/commands/uninstall.js +158 -0
- package/dist/commands/utils.d.ts +29 -0
- package/dist/commands/utils.js +24 -0
- package/dist/commands/versions.js +120 -11
- package/dist/index.js +83 -12
- package/dist/lib/activity.d.ts +124 -0
- package/dist/lib/activity.js +542 -0
- package/dist/lib/agents.js +16 -5
- package/dist/lib/artifact-actions.d.ts +1 -1
- package/dist/lib/artifact-actions.js +1 -1
- package/dist/lib/ask-classifier.d.ts +2 -1
- package/dist/lib/ask-classifier.js +3 -3
- package/dist/lib/auth-health.d.ts +32 -4
- package/dist/lib/auth-health.js +40 -1
- package/dist/lib/auto-pull.js +8 -1
- package/dist/lib/brand.d.ts +40 -0
- package/dist/lib/brand.js +122 -0
- package/dist/lib/browser/drivers/ssh.js +4 -7
- package/dist/lib/browser/service.js +6 -8
- package/dist/lib/channels/providers/index.d.ts +2 -0
- package/dist/lib/channels/providers/index.js +20 -0
- package/dist/lib/channels/providers/mailbox.d.ts +2 -0
- package/dist/lib/channels/providers/mailbox.js +26 -0
- package/dist/lib/channels/providers/openclaw-telegram.d.ts +2 -0
- package/dist/lib/channels/providers/openclaw-telegram.js +34 -0
- package/dist/lib/channels/providers/rush.d.ts +6 -0
- package/dist/lib/channels/providers/rush.js +55 -0
- package/dist/lib/channels/registry.d.ts +42 -0
- package/dist/lib/channels/registry.js +20 -0
- package/dist/lib/channels/resolve.d.ts +11 -0
- package/dist/lib/channels/resolve.js +12 -0
- package/dist/lib/cli-resources.d.ts +18 -0
- package/dist/lib/cli-resources.js +53 -4
- package/dist/lib/cloud/codex.js +6 -3
- package/dist/lib/cloud/factory.d.ts +1 -0
- package/dist/lib/cloud/factory.js +10 -4
- package/dist/lib/cloud/rush.d.ts +3 -1
- package/dist/lib/cloud/rush.js +6 -1
- package/dist/lib/cloud/types.d.ts +2 -0
- package/dist/lib/codex-home.d.ts +35 -0
- package/dist/lib/codex-home.js +136 -0
- package/dist/lib/crabbox/cli.d.ts +44 -0
- package/dist/lib/crabbox/cli.js +120 -20
- package/dist/lib/crabbox/lease.d.ts +52 -1
- package/dist/lib/crabbox/lease.js +117 -16
- package/dist/lib/crabbox/progress.d.ts +31 -0
- package/dist/lib/crabbox/progress.js +76 -0
- package/dist/lib/crabbox/runtimes.d.ts +4 -0
- package/dist/lib/crabbox/runtimes.js +33 -8
- package/dist/lib/crabbox/setup-copy.d.ts +87 -0
- package/dist/lib/crabbox/setup-copy.js +127 -0
- package/dist/lib/daemon.d.ts +14 -5
- package/dist/lib/daemon.js +52 -9
- package/dist/lib/drive-sync.js +6 -3
- package/dist/lib/event-stream.d.ts +36 -0
- package/dist/lib/event-stream.js +68 -0
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/exec.d.ts +11 -0
- package/dist/lib/exec.js +23 -2
- package/dist/lib/feed-policy.d.ts +1 -1
- package/dist/lib/feed-policy.js +14 -9
- package/dist/lib/feed-ranking.d.ts +32 -0
- package/dist/lib/feed-ranking.js +224 -0
- package/dist/lib/feed.d.ts +35 -2
- package/dist/lib/feed.js +66 -3
- package/dist/lib/fleet/auth-sync.d.ts +53 -0
- package/dist/lib/fleet/auth-sync.js +92 -0
- package/dist/lib/fleet/remote-login.d.ts +187 -0
- package/dist/lib/fleet/remote-login.js +556 -0
- package/dist/lib/format.d.ts +30 -3
- package/dist/lib/format.js +34 -5
- package/dist/lib/fs-atomic.d.ts +7 -2
- package/dist/lib/fs-atomic.js +8 -12
- package/dist/lib/git.d.ts +16 -0
- package/dist/lib/git.js +79 -2
- package/dist/lib/heal.js +11 -3
- package/dist/lib/hosts/logs.d.ts +12 -0
- package/dist/lib/hosts/logs.js +18 -0
- package/dist/lib/hosts/progress.d.ts +28 -10
- package/dist/lib/hosts/progress.js +79 -22
- package/dist/lib/hosts/remote-cmd.js +4 -0
- package/dist/lib/hq/floor.d.ts +87 -0
- package/dist/lib/hq/floor.js +226 -0
- package/dist/lib/menubar/install-menubar.js +14 -20
- package/dist/lib/notify.d.ts +1 -0
- package/dist/lib/notify.js +3 -3
- package/dist/lib/open-url.d.ts +2 -0
- package/dist/lib/open-url.js +19 -0
- package/dist/lib/openclaw-keychain.d.ts +56 -0
- package/dist/lib/openclaw-keychain.js +236 -0
- package/dist/lib/overdue.js +10 -0
- package/dist/lib/permissions.d.ts +44 -1
- package/dist/lib/permissions.js +284 -7
- package/dist/lib/project-launch.d.ts +6 -12
- package/dist/lib/project-launch.js +13 -228
- package/dist/lib/project-resources.d.ts +7 -0
- package/dist/lib/project-resources.js +291 -0
- package/dist/lib/pty-client.d.ts +27 -0
- package/dist/lib/pty-client.js +136 -10
- package/dist/lib/refresh.js +14 -10
- package/dist/lib/resource-profiles.d.ts +26 -0
- package/dist/lib/resource-profiles.js +157 -0
- package/dist/lib/resources/permissions.js +7 -1
- package/dist/lib/resources/types.d.ts +1 -1
- package/dist/lib/resources.d.ts +1 -1
- package/dist/lib/resources.js +32 -3
- package/dist/lib/routines.d.ts +11 -0
- package/dist/lib/routines.js +56 -15
- package/dist/lib/runner.d.ts +1 -0
- package/dist/lib/runner.js +66 -5
- package/dist/lib/secrets/bundles.d.ts +16 -3
- package/dist/lib/secrets/bundles.js +206 -37
- package/dist/lib/secrets/icloud-import.d.ts +2 -2
- package/dist/lib/secrets/icloud-import.js +9 -6
- package/dist/lib/secrets/mcp.js +1 -1
- package/dist/lib/secrets/vault-age-helper.d.ts +1 -0
- package/dist/lib/secrets/vault-age-helper.js +34 -0
- package/dist/lib/secrets/vault.d.ts +49 -0
- package/dist/lib/secrets/vault.js +397 -0
- package/dist/lib/self-heal/checks/path.js +3 -1
- package/dist/lib/self-heal/checks/shadowing.js +10 -2
- package/dist/lib/self-heal/checks/shims.js +19 -11
- package/dist/lib/session/cloud.d.ts +2 -2
- package/dist/lib/session/cloud.js +2 -2
- package/dist/lib/session/db.d.ts +4 -0
- package/dist/lib/session/db.js +44 -7
- package/dist/lib/session/discover.d.ts +2 -0
- package/dist/lib/session/discover.js +114 -3
- package/dist/lib/session/stream-render.d.ts +3 -0
- package/dist/lib/session/stream-render.js +130 -0
- package/dist/lib/session/types.d.ts +6 -0
- package/dist/lib/share/analytics.d.ts +13 -0
- package/dist/lib/share/analytics.js +45 -0
- package/dist/lib/share/config.d.ts +19 -5
- package/dist/lib/share/config.js +44 -8
- package/dist/lib/share/provision.d.ts +58 -6
- package/dist/lib/share/provision.js +97 -22
- package/dist/lib/share/publish.d.ts +36 -13
- package/dist/lib/share/publish.js +55 -8
- package/dist/lib/share/worker-template.js +83 -17
- package/dist/lib/shims.d.ts +36 -0
- package/dist/lib/shims.js +120 -16
- package/dist/lib/ssh-exec.d.ts +14 -0
- package/dist/lib/ssh-exec.js +57 -0
- package/dist/lib/staleness/detectors/hooks.js +25 -1
- package/dist/lib/staleness/detectors/permissions.js +66 -0
- package/dist/lib/staleness/detectors/workflows.js +20 -0
- package/dist/lib/staleness/writers/hooks.js +58 -4
- package/dist/lib/staleness/writers/permissions.js +2 -2
- package/dist/lib/staleness/writers/skills.js +1 -1
- package/dist/lib/staleness/writers/sources.d.ts +10 -1
- package/dist/lib/staleness/writers/sources.js +68 -1
- package/dist/lib/star-nudge.d.ts +45 -0
- package/dist/lib/star-nudge.js +91 -0
- package/dist/lib/startup/command-registry.d.ts +7 -1
- package/dist/lib/startup/command-registry.js +19 -3
- package/dist/lib/state.d.ts +2 -0
- package/dist/lib/state.js +3 -0
- package/dist/lib/subagents-registry.d.ts +2 -0
- package/dist/lib/subagents-registry.js +3 -0
- package/dist/lib/teams/parsers.js +214 -6
- package/dist/lib/teams/supervisor.d.ts +7 -0
- package/dist/lib/teams/supervisor.js +2 -1
- package/dist/lib/template.d.ts +1 -1
- package/dist/lib/template.js +1 -1
- package/dist/lib/triggers/webhook.js +36 -3
- package/dist/lib/types.d.ts +73 -0
- package/dist/lib/uninstall.d.ts +84 -0
- package/dist/lib/uninstall.js +347 -0
- package/dist/lib/usage.d.ts +13 -1
- package/dist/lib/usage.js +32 -14
- package/dist/lib/version.d.ts +40 -0
- package/dist/lib/version.js +94 -16
- package/dist/lib/versions.d.ts +27 -0
- package/dist/lib/versions.js +248 -79
- package/dist/lib/workflows.d.ts +2 -0
- package/dist/lib/workflows.js +88 -0
- package/package.json +2 -1
- package/dist/commands/daemon.d.ts +0 -10
- package/dist/commands/daemon.js +0 -121
package/dist/commands/exec.js
CHANGED
|
@@ -8,10 +8,12 @@
|
|
|
8
8
|
import { Option } from 'commander';
|
|
9
9
|
import chalk from 'chalk';
|
|
10
10
|
import { setHelpSections } from '../lib/help.js';
|
|
11
|
-
import { isInteractiveTerminal, requireInteractiveSelection } from './utils.js';
|
|
11
|
+
import { isInteractiveTerminal, isPromptCancelled, requireInteractiveSelection } from './utils.js';
|
|
12
|
+
import { getUserAgentsDir } from '../lib/state.js';
|
|
12
13
|
import { parseLoopInterval } from '../lib/loop.js';
|
|
13
14
|
import { AGENTS } from '../lib/agents.js';
|
|
14
15
|
import { recordDispatchedRun } from '../lib/audit/log.js';
|
|
16
|
+
import { maybeShowStarNudge } from '../lib/star-nudge.js';
|
|
15
17
|
import { warnUnpushedWork, shouldWarnUnpushed } from '../lib/warn-unpushed.js';
|
|
16
18
|
import { isHostPinned, pinHostKey, managedKnownHostsPath } from '../lib/devices/known-hosts.js';
|
|
17
19
|
import { sshResolve } from '../lib/hosts/ssh-config.js';
|
|
@@ -19,6 +21,7 @@ import * as fs from 'fs';
|
|
|
19
21
|
import * as path from 'path';
|
|
20
22
|
import * as os from 'os';
|
|
21
23
|
import { randomUUID } from 'crypto';
|
|
24
|
+
import { spawnSync } from 'child_process';
|
|
22
25
|
/** Distinguish a terminal account-picker marker from an explicit @version pin. */
|
|
23
26
|
export function parseRunAccountPickerRequest(agentSpec) {
|
|
24
27
|
const requested = agentSpec.endsWith('@');
|
|
@@ -40,6 +43,8 @@ export function runAccountPickerConflicts(options) {
|
|
|
40
43
|
conflicts.push('--balanced');
|
|
41
44
|
if (options.lease)
|
|
42
45
|
conflicts.push('--lease');
|
|
46
|
+
if (options.box)
|
|
47
|
+
conflicts.push('--box');
|
|
43
48
|
if (options.host || options.device || options.on || options.computer)
|
|
44
49
|
conflicts.push('--host/--device');
|
|
45
50
|
return conflicts;
|
|
@@ -55,6 +60,77 @@ function formatRotationBanner(result, verb = 'balanced') {
|
|
|
55
60
|
const ratio = `${healthy.length} of ${healthy.length + excluded.length} healthy`;
|
|
56
61
|
return `[agents] ${verb} picked ${label} (${ratio})`;
|
|
57
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* Whether `cwd` is inside a git work tree.
|
|
65
|
+
*
|
|
66
|
+
* `--lease` / `--box` sync the working directory to the box through crabbox,
|
|
67
|
+
* which enumerates the files to copy with `git ls-files`. Outside a git repo
|
|
68
|
+
* that exits 128 (`fatal: not a git repository`) and the whole run dies at
|
|
69
|
+
* "build sync file list: exit status 128" — AFTER the box is provisioned and
|
|
70
|
+
* billed. Checking this up front lets the caller fail fast, before provisioning.
|
|
71
|
+
*/
|
|
72
|
+
export function isInsideGitWorkTree(cwd) {
|
|
73
|
+
const r = spawnSync('git', ['-C', cwd, 'rev-parse', '--is-inside-work-tree'], {
|
|
74
|
+
encoding: 'utf-8',
|
|
75
|
+
});
|
|
76
|
+
return r.status === 0 && r.stdout.trim() === 'true';
|
|
77
|
+
}
|
|
78
|
+
/** Absolute git toplevel for `cwd`, or null when it is not a git repo. */
|
|
79
|
+
export function gitToplevel(cwd) {
|
|
80
|
+
const r = spawnSync('git', ['-C', cwd, 'rev-parse', '--show-toplevel'], { encoding: 'utf-8' });
|
|
81
|
+
return r.status === 0 ? r.stdout.trim() : null;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Network mode for a `--lease` run (F5, RUSH-1924). `--tailscale` forces the
|
|
85
|
+
* tailnet, `--no-tailscale` forces public, and neither (undefined) defaults to
|
|
86
|
+
* the tailnet ONLY in a reuse context (`--reuse`, `--box`, or a picked warm
|
|
87
|
+
* box) — a one-shot solo `--lease` stays public. Pure so it is unit-testable;
|
|
88
|
+
* the caller downgrades to `'public'` when no auth key is configured.
|
|
89
|
+
*/
|
|
90
|
+
export function computeNetMode(opts) {
|
|
91
|
+
if (opts.tailscale === false)
|
|
92
|
+
return 'public'; // --no-tailscale wins
|
|
93
|
+
if (opts.tailscale === true)
|
|
94
|
+
return 'tailscale'; // explicit --tailscale
|
|
95
|
+
return opts.reuseContext ? 'tailscale' : 'public';
|
|
96
|
+
}
|
|
97
|
+
// ── "Always provision fresh" per-repo memory (F3, RUSH-1922) ─────────────────
|
|
98
|
+
// The picker's "Always provision fresh (remember for this repo)" choice is
|
|
99
|
+
// persisted as a list of git-toplevel paths in a small state file, so a repo
|
|
100
|
+
// that opted out of the reuse picker is never prompted again.
|
|
101
|
+
/** True when `repoRoot` is in the remembered always-fresh set. Pure. */
|
|
102
|
+
export function isAlwaysFreshRepo(repos, repoRoot) {
|
|
103
|
+
return repos.includes(repoRoot);
|
|
104
|
+
}
|
|
105
|
+
/** Add `repoRoot` to the always-fresh set (idempotent). Pure. */
|
|
106
|
+
export function addAlwaysFreshRepo(repos, repoRoot) {
|
|
107
|
+
return repos.includes(repoRoot) ? repos : [...repos, repoRoot];
|
|
108
|
+
}
|
|
109
|
+
/** Path to the always-fresh state file under the USER agents dir (CLI-written per-user preference, never the maintainer-owned `.system` repo). */
|
|
110
|
+
export function leaseFreshReposPath() {
|
|
111
|
+
return path.join(getUserAgentsDir(), 'lease-fresh-repos.json');
|
|
112
|
+
}
|
|
113
|
+
/** Read the remembered always-fresh repo roots (empty on any read/parse error). */
|
|
114
|
+
export function readAlwaysFreshRepos() {
|
|
115
|
+
try {
|
|
116
|
+
const parsed = JSON.parse(fs.readFileSync(leaseFreshReposPath(), 'utf-8'));
|
|
117
|
+
return Array.isArray(parsed) ? parsed.filter((x) => typeof x === 'string') : [];
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/** Persist the always-fresh repo roots. Best-effort — never throws. */
|
|
124
|
+
export function writeAlwaysFreshRepos(repos) {
|
|
125
|
+
try {
|
|
126
|
+
const p = leaseFreshReposPath();
|
|
127
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
128
|
+
fs.writeFileSync(p, JSON.stringify(repos, null, 2));
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
/* best-effort — losing the preference just means the picker shows next time */
|
|
132
|
+
}
|
|
133
|
+
}
|
|
58
134
|
/**
|
|
59
135
|
* Decide whether `--copy-creds` may ship credentials (and the Claude OAuth
|
|
60
136
|
* token) to `host` (RUSH-1767). Credentials ship only to a host whose SSH host
|
|
@@ -313,7 +389,12 @@ export function registerRunCommand(program) {
|
|
|
313
389
|
.option('--any', 'With --host <cap> (a capability tag), pick any matching host instead of erroring when several match.')
|
|
314
390
|
.option('--copy-creds', 'With --host, copy the picked runtime credentials (and Claude OAuth token) to the host, then shred them after the run. Opt-in per run.')
|
|
315
391
|
.option('--lease [backend]', 'Invent a disposable cloud box for this run and tear it down after (via crabbox). Optional backend selects the cloud (hetzner/aws/do). Unlike --host, no machine is registered.')
|
|
316
|
-
.option('--
|
|
392
|
+
.option('--box <slug>', 'Reuse an existing warm crabbox box for this run instead of provisioning a disposable --lease box.')
|
|
393
|
+
.option('--keep-box', 'With --lease, keep the box after the run instead of stopping it.')
|
|
394
|
+
.option('--reuse', 'With --lease, reuse the most-recently-used warm box if one exists (else provision fresh). The scriptable form of the interactive reuse picker.')
|
|
395
|
+
.option('--bare', 'With --lease, skip copying your local ~/.agents setup (skills/hooks/commands/MCP) onto the box.')
|
|
396
|
+
.option('--tailscale', 'Lease the box onto your tailnet (reachable only over Tailscale) rather than a public IP.')
|
|
397
|
+
.option('--no-tailscale', 'Force a public-IP lease even when a reuse context would default to Tailscale.');
|
|
317
398
|
// `--on` and `--computer` are hidden aliases of `--host` — same behavior.
|
|
318
399
|
runCmd.addOption(new Option('--on <name>', 'Alias of --host.').hideHelp());
|
|
319
400
|
runCmd.addOption(new Option('--computer <name>', 'Alias of --host.').hideHelp());
|
|
@@ -422,17 +503,31 @@ export function registerRunCommand(program) {
|
|
|
422
503
|
}
|
|
423
504
|
}
|
|
424
505
|
// --lease: invent a disposable cloud box for this run (via crabbox), run
|
|
425
|
-
// the agent there, then tear it down.
|
|
426
|
-
|
|
506
|
+
// the agent there, then tear it down. --box: reuse a named warm crabbox
|
|
507
|
+
// box, run the same bootstrap there, and leave the box running.
|
|
508
|
+
if (options.lease || options.box) {
|
|
427
509
|
if (prompt === undefined) {
|
|
428
|
-
console.error(chalk.red(
|
|
510
|
+
console.error(chalk.red(`A prompt is required for crabbox runs: agents run <agent> "<task>" ${options.box ? '--box <slug>' : '--lease'}`));
|
|
511
|
+
process.exit(1);
|
|
512
|
+
}
|
|
513
|
+
if (options.lease && options.box) {
|
|
514
|
+
console.error(chalk.red('Pass either --lease to provision a disposable box, or --box <slug> to reuse a warm box — not both.'));
|
|
429
515
|
process.exit(1);
|
|
430
516
|
}
|
|
431
517
|
const backend = typeof options.lease === 'string' ? options.lease : undefined;
|
|
518
|
+
// crabbox syncs this directory to the box via `git ls-files`; outside a
|
|
519
|
+
// git repo that fails with "build sync file list: exit status 128" — but
|
|
520
|
+
// only AFTER the box is provisioned and billed. Fail fast here instead.
|
|
521
|
+
const leaseCwd = options.cwd ?? process.cwd();
|
|
522
|
+
if (!isInsideGitWorkTree(leaseCwd)) {
|
|
523
|
+
console.error(chalk.red(`${options.box ? '--box' : '--lease'} syncs the working directory to the box, but ${leaseCwd} is not a git repository.`));
|
|
524
|
+
console.error(chalk.yellow(`Run from inside a git repo, or initialize one: (cd ${leaseCwd} && git init)`));
|
|
525
|
+
process.exit(1);
|
|
526
|
+
}
|
|
432
527
|
// First-run: no provider credential resolves (no env var, no config, no
|
|
433
528
|
// detectable bundle) → guide the user through one-time setup, then continue.
|
|
434
529
|
const { resolveLeaseBundle } = await import('../lib/crabbox/cli.js');
|
|
435
|
-
if (!resolveLeaseBundle()) {
|
|
530
|
+
if (options.lease && !resolveLeaseBundle()) {
|
|
436
531
|
const { runLeaseSetup } = await import('./lease.js');
|
|
437
532
|
const ok = await runLeaseSetup({ provider: backend ?? 'hetzner' });
|
|
438
533
|
if (!ok) {
|
|
@@ -440,22 +535,140 @@ export function registerRunCommand(program) {
|
|
|
440
535
|
process.exit(1);
|
|
441
536
|
}
|
|
442
537
|
}
|
|
443
|
-
|
|
538
|
+
// ── F3 reuse (RUSH-1922) + F5 net-mode (RUSH-1924) ───────────────────
|
|
539
|
+
// Resolve which box this run targets and how it is networked BEFORE any
|
|
540
|
+
// provisioning. `--box` is an explicit reuse; otherwise, on an
|
|
541
|
+
// interactive tty, offer the warm boxes as a reuse picker (headless /
|
|
542
|
+
// --json never blocks — it provisions fresh unless --reuse/--box).
|
|
543
|
+
const leaseSecretsBundle = process.env.AGENTS_LEASE_SECRETS_BUNDLE;
|
|
544
|
+
const nowSecs = Math.floor(Date.now() / 1000);
|
|
545
|
+
let reuseSlug = options.box;
|
|
546
|
+
if (options.lease && !reuseSlug) {
|
|
547
|
+
const { crabboxList } = await import('../lib/crabbox/cli.js');
|
|
548
|
+
const { reusableBoxes, formatBoxRow } = await import('./lease.js');
|
|
549
|
+
let warm = [];
|
|
550
|
+
try {
|
|
551
|
+
warm = reusableBoxes(crabboxList({ secretsBundle: leaseSecretsBundle }), nowSecs);
|
|
552
|
+
}
|
|
553
|
+
catch {
|
|
554
|
+
warm = []; // crabbox unavailable / no creds → just provision fresh
|
|
555
|
+
}
|
|
556
|
+
const repoRoot = gitToplevel(leaseCwd);
|
|
557
|
+
const alwaysFresh = repoRoot ? isAlwaysFreshRepo(readAlwaysFreshRepos(), repoRoot) : false;
|
|
558
|
+
if (warm.length > 0 && !alwaysFresh) {
|
|
559
|
+
if (options.reuse) {
|
|
560
|
+
reuseSlug = warm[0].slug; // scriptable: most-recently-touched warm box
|
|
561
|
+
}
|
|
562
|
+
else if (isInteractiveTerminal() && options.json !== true) {
|
|
563
|
+
const { select } = await import('@inquirer/prompts');
|
|
564
|
+
try {
|
|
565
|
+
const choice = await select({
|
|
566
|
+
message: 'Reuse a warm box, or provision a fresh one?',
|
|
567
|
+
choices: [
|
|
568
|
+
...warm.map((b) => ({ name: formatBoxRow(b, nowSecs), value: b.slug })),
|
|
569
|
+
{ name: 'Provision a fresh box', value: '__fresh__' },
|
|
570
|
+
{ name: 'Always provision fresh (remember for this repo)', value: '__always_fresh__' },
|
|
571
|
+
],
|
|
572
|
+
});
|
|
573
|
+
if (choice === '__always_fresh__') {
|
|
574
|
+
if (repoRoot) {
|
|
575
|
+
writeAlwaysFreshRepos(addAlwaysFreshRepo(readAlwaysFreshRepos(), repoRoot));
|
|
576
|
+
console.error(chalk.dim(`Will always provision fresh for ${repoRoot} (edit ${leaseFreshReposPath()} to undo).`));
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
else if (choice !== '__fresh__') {
|
|
580
|
+
reuseSlug = choice;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
catch (e) {
|
|
584
|
+
if (!isPromptCancelled(e))
|
|
585
|
+
throw e;
|
|
586
|
+
console.error(chalk.yellow('Selection cancelled — provisioning a fresh box.'));
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
// Headless with no --reuse falls through here → provision fresh.
|
|
590
|
+
}
|
|
591
|
+
else if (options.reuse && warm.length > 0) {
|
|
592
|
+
// --reuse still honors a warm box even when the picker is suppressed.
|
|
593
|
+
reuseSlug = warm[0].slug;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
// reuseContext: an existing box (picked, --box, or --reuse) defaults the
|
|
597
|
+
// network to the tailnet; a solo one-shot --lease stays public.
|
|
598
|
+
const reuseContext = !!reuseSlug || !!options.reuse;
|
|
599
|
+
let netMode = computeNetMode({ tailscale: options.tailscale, reuseContext });
|
|
600
|
+
const copySetup = !options.bare;
|
|
601
|
+
// Tailscale requested but no auth key configured → downgrade to public
|
|
602
|
+
// with an actionable hint instead of hard-failing the run (F5).
|
|
603
|
+
if (netMode === 'tailscale') {
|
|
604
|
+
const { pickTailscaleBundleFromList } = await import('../lib/crabbox/cli.js');
|
|
605
|
+
const { listBundles } = await import('../lib/secrets/bundles.js');
|
|
606
|
+
let hasKey = !!process.env.CRABBOX_TAILSCALE_AUTH_KEY;
|
|
607
|
+
if (!hasKey) {
|
|
608
|
+
try {
|
|
609
|
+
hasKey = !!pickTailscaleBundleFromList(listBundles());
|
|
610
|
+
}
|
|
611
|
+
catch {
|
|
612
|
+
hasKey = false;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
if (!hasKey) {
|
|
616
|
+
console.error(chalk.yellow('Tailscale requested but no auth key is configured — falling back to a public-IP lease.'));
|
|
617
|
+
console.error(chalk.gray('Set one up with `agents lease setup` (mint an EPHEMERAL, pre-authorized, tag:crabbox key), or store CRABBOX_TAILSCALE_AUTH_KEY in a secrets bundle.'));
|
|
618
|
+
netMode = 'public';
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
const { detectSignedInRuntimes, resolveClaudeCredentialsBlob, inferLeaseRuntime, profileNeedsBaseRuntimeCredentials } = await import('../lib/crabbox/runtimes.js');
|
|
444
622
|
const { leaseAndRun } = await import('../lib/crabbox/lease.js');
|
|
623
|
+
const { boxAddress } = await import('./lease.js');
|
|
445
624
|
const { getConfiguredRunStrategy, resolveRunVersion } = await import('../lib/rotate.js');
|
|
625
|
+
const { profileExists, readProfile, resolveProfileEnv } = await import('../lib/profiles.js');
|
|
446
626
|
const detected = await detectSignedInRuntimes();
|
|
447
|
-
const agentName = normalizedAgentSpec.split('@')
|
|
448
|
-
|
|
627
|
+
const [agentName, rawLeaseVersion] = normalizedAgentSpec.split('@');
|
|
628
|
+
let runtime = null;
|
|
629
|
+
let credentialRuntimes = [];
|
|
630
|
+
let dispatchProfile;
|
|
449
631
|
// `--lease` requires a prompt (guarded above), so it is headless by
|
|
450
632
|
// contract — never block on an interactive picker. Provision exactly the
|
|
451
633
|
// one runtime this run needs, inferred from the agent, not every
|
|
452
634
|
// signed-in CLI (which would ship unrelated tokens to a throwaway box).
|
|
453
|
-
|
|
635
|
+
if (profileExists(agentName)) {
|
|
636
|
+
try {
|
|
637
|
+
const profile = readProfile(agentName);
|
|
638
|
+
const profileEnv = resolveProfileEnv(profile);
|
|
639
|
+
runtime = profile.host.agent;
|
|
640
|
+
const profileNeedsCredentials = profileNeedsBaseRuntimeCredentials(runtime, profileEnv, profile.auth?.envVar);
|
|
641
|
+
credentialRuntimes = profileNeedsCredentials ? [runtime] : [];
|
|
642
|
+
dispatchProfile = {
|
|
643
|
+
name: profile.name,
|
|
644
|
+
agent: profile.host.agent,
|
|
645
|
+
version: rawLeaseVersion || profile.host.version,
|
|
646
|
+
env: profileEnv,
|
|
647
|
+
description: profile.description,
|
|
648
|
+
preset: profile.preset,
|
|
649
|
+
provider: profile.provider,
|
|
650
|
+
fallbackModel: profile.fallback_model,
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
catch (err) {
|
|
654
|
+
console.error(chalk.red(err.message));
|
|
655
|
+
process.exit(1);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
else {
|
|
659
|
+
runtime = inferLeaseRuntime(agentName, detected);
|
|
660
|
+
if (runtime)
|
|
661
|
+
credentialRuntimes = [runtime];
|
|
662
|
+
}
|
|
454
663
|
if (!runtime) {
|
|
455
664
|
console.error(chalk.yellow('No signed-in runtime to provision on the box. Sign into one locally (e.g. run `claude` once) then retry.'));
|
|
456
665
|
process.exit(1);
|
|
457
666
|
}
|
|
458
667
|
const runtimes = [runtime];
|
|
668
|
+
if (credentialRuntimes.length > 0 && !detected.some((d) => d.id === runtime && d.signedIn && d.credPath)) {
|
|
669
|
+
console.error(chalk.yellow(`Profile '${agentName}' needs ${runtime} credentials, but ${runtime} is not signed in locally. Sign in locally, then retry.`));
|
|
670
|
+
process.exit(1);
|
|
671
|
+
}
|
|
459
672
|
// Copy the account the run's OWN strategy would pick (default `balanced`:
|
|
460
673
|
// a healthy, non-rate-limited account weighted by remaining headroom) —
|
|
461
674
|
// never the raw default signed-in one, which could be throttled or out of
|
|
@@ -468,7 +681,7 @@ export function registerRunCommand(program) {
|
|
|
468
681
|
// the notice below would name a balanced-picked account that is NOT the one
|
|
469
682
|
// actually shipped. Best-effort: fall back to the default account on error.
|
|
470
683
|
let leaseEmail = detected.find((d) => d.id === runtime)?.email ?? null;
|
|
471
|
-
if (
|
|
684
|
+
if (credentialRuntimes.includes('claude')) {
|
|
472
685
|
try {
|
|
473
686
|
const strategy = getConfiguredRunStrategy(runtime, leaseCwd);
|
|
474
687
|
const { rotation } = await resolveRunVersion(runtime, strategy, leaseCwd);
|
|
@@ -483,43 +696,82 @@ export function registerRunCommand(program) {
|
|
|
483
696
|
// where — copying an auth token to a cloud box is a credential transfer.
|
|
484
697
|
// The box is destroyed after the run, so the credential's lifetime is
|
|
485
698
|
// bounded by the run.
|
|
486
|
-
const whatShips =
|
|
487
|
-
|
|
699
|
+
const whatShips = credentialRuntimes.includes('claude')
|
|
700
|
+
? `${dispatchProfile ? `profile '${dispatchProfile.name}', ` : ''}${runtime} credentials + Claude OAuth token`
|
|
701
|
+
: credentialRuntimes.length > 0
|
|
702
|
+
? `${dispatchProfile ? `profile '${dispatchProfile.name}', ` : ''}${runtime} credentials`
|
|
703
|
+
: dispatchProfile
|
|
704
|
+
? `profile '${dispatchProfile.name}'`
|
|
705
|
+
: `${runtime} credentials`;
|
|
706
|
+
const boxLifecycle = reuseSlug
|
|
707
|
+
? `Reusing crabbox box ${reuseSlug}`
|
|
708
|
+
: `Leasing a ${backend ?? 'hetzner'} box${netMode === 'tailscale' ? ' on your tailnet' : ''}`;
|
|
709
|
+
const boxAfterRun = reuseSlug
|
|
710
|
+
? 'the box is kept after the run'
|
|
711
|
+
: options.keepBox
|
|
712
|
+
? 'the box is kept after the run'
|
|
713
|
+
: 'the box is destroyed after the run';
|
|
714
|
+
console.error(chalk.gray(`${boxLifecycle} · shipping ${whatShips}${credentialRuntimes.length > 0 && leaseEmail ? ` (${leaseEmail})` : ''}; ${boxAfterRun}.`));
|
|
488
715
|
// Read the Claude OAuth token from the local Keychain (silent) so it can be
|
|
489
716
|
// written to ~/.claude/.credentials.json on the box — otherwise Claude boots
|
|
490
717
|
// "Not logged in". `preferEmail` targets the strategy-picked account so the
|
|
491
718
|
// token matches the account this run resolved to.
|
|
492
719
|
let claudeCredentialsJson = null;
|
|
493
|
-
if (
|
|
720
|
+
if (credentialRuntimes.includes('claude')) {
|
|
494
721
|
claudeCredentialsJson = await resolveClaudeCredentialsBlob({ preferEmail: leaseEmail });
|
|
495
722
|
if (!claudeCredentialsJson) {
|
|
496
723
|
console.error(chalk.yellow('Warning: could not read the local Claude OAuth token — the box may come up "Not logged in".'));
|
|
497
724
|
}
|
|
498
725
|
}
|
|
499
|
-
// Progress UI. A self-throttled spinner (NOT ora — see
|
|
500
|
-
//
|
|
501
|
-
//
|
|
502
|
-
//
|
|
503
|
-
//
|
|
504
|
-
|
|
726
|
+
// Progress UI (F2, RUSH-1921). A self-throttled spinner (NOT ora — see
|
|
727
|
+
// progress.ts) covers provisioning; the box-side bootstrap then streams a
|
|
728
|
+
// structured step stream (sync → install → runtime → creds → …) via the
|
|
729
|
+
// router's `onStep`, and each step renders as a checklist line through the
|
|
730
|
+
// lib's `renderStepLine` (✔ <Step> — <detail> (<elapsed>)). The agent's own
|
|
731
|
+
// output prints verbatim after the box-side marker. Rule: only ONE spinner
|
|
732
|
+
// phase is active at a time, so it can never storm.
|
|
733
|
+
const { createLeaseOutputRouter, createSpinner, renderStepLine } = await import('../lib/crabbox/progress.js');
|
|
505
734
|
const spinner = createSpinner({ stream: process.stderr });
|
|
506
735
|
let warmupTimer;
|
|
507
736
|
const stopTimer = () => { if (warmupTimer) {
|
|
508
737
|
clearInterval(warmupTimer);
|
|
509
738
|
warmupTimer = undefined;
|
|
510
739
|
} };
|
|
511
|
-
const
|
|
512
|
-
|
|
513
|
-
|
|
740
|
+
const jsonMode = options.json === true;
|
|
741
|
+
const stepsTty = Boolean(process.stderr.isTTY) && !jsonMode;
|
|
742
|
+
// The step currently in flight (its label spins on a TTY). It is persisted
|
|
743
|
+
// as a ✔ line when the NEXT step arrives (whose elapsedMs measures how long
|
|
744
|
+
// THIS step's block took) — or, for the last step, when agent output or
|
|
745
|
+
// teardown begins.
|
|
746
|
+
let activeStep = null;
|
|
747
|
+
const flushStep = (elapsedMs) => {
|
|
748
|
+
if (!activeStep)
|
|
749
|
+
return;
|
|
750
|
+
const done = activeStep;
|
|
751
|
+
activeStep = null;
|
|
752
|
+
if (jsonMode) {
|
|
753
|
+
process.stdout.write(JSON.stringify({ phase: 'setup', name: done.name, elapsedMs: elapsedMs ?? null }) + '\n');
|
|
754
|
+
}
|
|
755
|
+
else {
|
|
756
|
+
spinner.stopAndPersist('✔', renderStepLine({ ...done, elapsedMs }));
|
|
757
|
+
}
|
|
514
758
|
};
|
|
515
759
|
const router = createLeaseOutputRouter({
|
|
516
|
-
|
|
517
|
-
//
|
|
518
|
-
|
|
760
|
+
now: () => Date.now(),
|
|
761
|
+
// Raw setup lines stay captured for a failure dump (router.setupLines());
|
|
762
|
+
// the structured step stream drives the visible checklist, so plain lines
|
|
763
|
+
// are not shown (they would fight the step spinner).
|
|
764
|
+
onSetupLine: () => { },
|
|
765
|
+
onStep: (step) => {
|
|
766
|
+
flushStep(step.elapsedMs); // persist the previous step, timed by this one
|
|
767
|
+
activeStep = step;
|
|
768
|
+
if (stepsTty)
|
|
769
|
+
spinner.start(renderStepLine(step));
|
|
770
|
+
},
|
|
519
771
|
onAgentChunk: (chunk) => {
|
|
520
|
-
//
|
|
772
|
+
flushStep(); // last setup step done (no following sentinel to time it)
|
|
521
773
|
if (spinner.active)
|
|
522
|
-
spinner.
|
|
774
|
+
spinner.stop();
|
|
523
775
|
process.stdout.write(chunk);
|
|
524
776
|
},
|
|
525
777
|
});
|
|
@@ -531,30 +783,41 @@ export function registerRunCommand(program) {
|
|
|
531
783
|
model: options.model,
|
|
532
784
|
backend,
|
|
533
785
|
runtimes,
|
|
786
|
+
credentialRuntimes,
|
|
534
787
|
detected,
|
|
788
|
+
dispatchProfile,
|
|
535
789
|
claudeCredentialsJson,
|
|
536
|
-
secretsBundle:
|
|
790
|
+
secretsBundle: leaseSecretsBundle,
|
|
537
791
|
keep: options.keepBox,
|
|
792
|
+
reuseBox: reuseSlug,
|
|
793
|
+
copySetup,
|
|
794
|
+
netMode,
|
|
538
795
|
onData: (chunk) => router.push(chunk),
|
|
539
796
|
onPhase: (phase) => {
|
|
540
797
|
if (phase.kind === 'warmup') {
|
|
541
|
-
const label = `Leasing a ${phase.backend ?? 'hetzner'} box`;
|
|
798
|
+
const label = `Leasing a ${phase.backend ?? 'hetzner'} box${netMode === 'tailscale' ? ' (tailnet)' : ''}`;
|
|
542
799
|
spinner.start(`${label}…`);
|
|
543
800
|
const t0 = Date.now();
|
|
544
801
|
warmupTimer = setInterval(() => spinner.update(`${label}… (${Math.round((Date.now() - t0) / 1000)}s)`), 1000);
|
|
545
802
|
}
|
|
803
|
+
else if (phase.kind === 'reuse') {
|
|
804
|
+
spinner.start(`Reusing crabbox box ${phase.slug}…`);
|
|
805
|
+
}
|
|
546
806
|
else if (phase.kind === 'ready') {
|
|
547
807
|
stopTimer();
|
|
548
|
-
|
|
549
|
-
spinner
|
|
808
|
+
const addr = boxAddress(phase.box);
|
|
809
|
+
// Steps drive the spinner from here — no generic "Setting up box…".
|
|
810
|
+
spinner.stopAndPersist('✔', `Box ${phase.box.slug} ready${addr ? ` (${addr})` : ''} · ${Math.round(phase.elapsedMs / 1000)}s`);
|
|
550
811
|
}
|
|
551
812
|
else if (phase.kind === 'teardown') {
|
|
813
|
+
flushStep();
|
|
552
814
|
if (spinner.active)
|
|
553
815
|
spinner.stop();
|
|
554
816
|
}
|
|
555
817
|
},
|
|
556
818
|
});
|
|
557
819
|
router.end();
|
|
820
|
+
flushStep();
|
|
558
821
|
stopTimer();
|
|
559
822
|
if (spinner.active)
|
|
560
823
|
spinner.stop();
|
|
@@ -566,11 +829,13 @@ export function registerRunCommand(program) {
|
|
|
566
829
|
if (log.length)
|
|
567
830
|
process.stderr.write(chalk.dim(log.join('\n')) + '\n');
|
|
568
831
|
}
|
|
569
|
-
|
|
832
|
+
const keptAddr = boxAddress(box);
|
|
833
|
+
console.error(chalk.gray(toreDown ? `Box ${box.slug} destroyed.` : `Box ${box.slug} kept${keptAddr ? ` (${keptAddr})` : ''}. Stop it: agents lease stop ${box.slug}`));
|
|
570
834
|
process.exit(exitCode === null ? 1 : exitCode);
|
|
571
835
|
}
|
|
572
836
|
catch (err) {
|
|
573
837
|
stopTimer();
|
|
838
|
+
flushStep();
|
|
574
839
|
if (spinner.active)
|
|
575
840
|
spinner.stopAndPersist('✖', chalk.red('Lease failed'));
|
|
576
841
|
const log = router.setupLines();
|
|
@@ -956,7 +1221,7 @@ export function registerRunCommand(program) {
|
|
|
956
1221
|
await warnUnpushedWork(resumeExec.cwd ?? process.cwd());
|
|
957
1222
|
process.exit(resumeExit);
|
|
958
1223
|
}
|
|
959
|
-
const [{ buildExecCommand, parseExecEnv, execAgent, runWithFallback, normalizeMode, resolveMode, headlessPlanStallCommand, nativeResume, resolveInteractive }, { ALL_AGENT_IDS, ACCOUNT_INSPECTION_AGENT_IDS, agentLabel, supportsAccountInspection }, { profileExists, resolveProfileForRun }, { readAndResolveBundleEnv, describeBundle, assertRemoteBundleFlagsUnsupported, isHeadlessSecretsContext }, { splitBundleRef, resolveSshTarget, remoteResolveEnv }, { getConfiguredRunStrategy, normalizeRunStrategy, resolveRunVersion, rotationFailoverChain, shouldArmRotationFailover, RUN_STRATEGIES }, { getGlobalDefault, getVersionHomePath, resolveVersion, resolveVersionAlias, ensureAgentRunnable }, { buildDiscoveredPlugin, loadPluginManifest, syncPluginToVersion }, { parseWorkflowFrontmatter, resolveWorkflowRef, resolveAllowedSubagents, pruneStaleWorkflowSubagents, ensureSubagentDispatchTool }, { resolveRunDefaults }, { getMcpServersByName, buildWorkflowMcpConfig }, { supports },] = await Promise.all([
|
|
1224
|
+
const [{ buildExecCommand, parseExecEnv, execAgent, runWithFallback, normalizeMode, resolveMode, headlessPlanStallCommand, nativeResume, resolveInteractive, inferredInteractiveWithoutTty }, { ALL_AGENT_IDS, ACCOUNT_INSPECTION_AGENT_IDS, agentLabel, supportsAccountInspection }, { profileExists, resolveProfileForRun }, { readAndResolveBundleEnv, describeBundle, assertRemoteBundleFlagsUnsupported, isHeadlessSecretsContext }, { splitBundleRef, resolveSshTarget, remoteResolveEnv }, { getConfiguredRunStrategy, normalizeRunStrategy, resolveRunVersion, rotationFailoverChain, shouldArmRotationFailover, RUN_STRATEGIES }, { getGlobalDefault, getVersionHomePath, resolveVersion, resolveVersionAlias, ensureAgentRunnable }, { buildDiscoveredPlugin, loadPluginManifest, syncPluginToVersion }, { parseWorkflowFrontmatter, resolveWorkflowRef, resolveAllowedSubagents, pruneStaleWorkflowSubagents, ensureSubagentDispatchTool }, { resolveRunDefaults }, { getMcpServersByName, buildWorkflowMcpConfig }, { supports }, { shareRuntimeEnv },] = await Promise.all([
|
|
960
1225
|
import('../lib/exec.js'),
|
|
961
1226
|
import('../lib/agents.js'),
|
|
962
1227
|
import('../lib/profiles.js'),
|
|
@@ -969,6 +1234,7 @@ export function registerRunCommand(program) {
|
|
|
969
1234
|
import('../lib/run-defaults.js'),
|
|
970
1235
|
import('../lib/mcp.js'),
|
|
971
1236
|
import('../lib/capabilities.js'),
|
|
1237
|
+
import('../lib/share/config.js'),
|
|
972
1238
|
]);
|
|
973
1239
|
const isValidAgent = (agent) => ALL_AGENT_IDS.includes(agent);
|
|
974
1240
|
// Parse agent@version
|
|
@@ -1433,7 +1699,14 @@ export function registerRunCommand(program) {
|
|
|
1433
1699
|
if (launchTarget) {
|
|
1434
1700
|
const healed = await ensureAgentRunnable(agent, launchTarget, options.quiet ? undefined : (m) => process.stderr.write(chalk.yellow(`[agents] ${m}\n`)));
|
|
1435
1701
|
if (healed === null) {
|
|
1436
|
-
|
|
1702
|
+
// An isolated copy is never repaired by adopting another version, so
|
|
1703
|
+
// `add <agent>@latest` would build an unrelated NORMAL install rather
|
|
1704
|
+
// than fix what the user asked to run. Point at the isolated re-add.
|
|
1705
|
+
const { isVersionIsolated } = await import('../lib/versions.js');
|
|
1706
|
+
const hint = isVersionIsolated(agent, launchTarget)
|
|
1707
|
+
? `agents add ${agent}@${launchTarget} --isolated`
|
|
1708
|
+
: `agents add ${agent}@latest`;
|
|
1709
|
+
console.error(chalk.red(`agents: ${agent}@${launchTarget} is not runnable and could not be repaired. Try: ${hint}`));
|
|
1437
1710
|
process.exit(1);
|
|
1438
1711
|
}
|
|
1439
1712
|
// Always adopt the healed version explicitly. In the version-undefined
|
|
@@ -1608,12 +1881,17 @@ export function registerRunCommand(program) {
|
|
|
1608
1881
|
process.exit(1);
|
|
1609
1882
|
}
|
|
1610
1883
|
}
|
|
1611
|
-
|
|
1884
|
+
const autoShareEnv = options.autoSecrets !== false
|
|
1885
|
+
? shareRuntimeEnv({ agentOnly: isHeadlessSecretsContext() })
|
|
1886
|
+
: undefined;
|
|
1887
|
+
// Merge order (later wins): profile env < auto share token < secrets bundles < --env K=V.
|
|
1612
1888
|
// Profile carries provider auth; secrets bundles carry user-defined
|
|
1613
|
-
// values; --env is the per-invocation override.
|
|
1614
|
-
|
|
1889
|
+
// values; --env is the per-invocation override. The share token is
|
|
1890
|
+
// best-effort: if it is not already in env or an unlocked bundle, unrelated
|
|
1891
|
+
// runs keep working, and `agents share` itself still fails loudly on use.
|
|
1892
|
+
const hasOverrides = profileEnv || autoShareEnv || options.secrets.length > 0 || userEnv;
|
|
1615
1893
|
const env = hasOverrides
|
|
1616
|
-
? { ...(profileEnv ?? {}), ...secretsEnv, ...(userEnv ?? {}) }
|
|
1894
|
+
? { ...(profileEnv ?? {}), ...(autoShareEnv ?? {}), ...secretsEnv, ...(userEnv ?? {}) }
|
|
1617
1895
|
: undefined;
|
|
1618
1896
|
const modelSource = runCmd.getOptionValueSource('model');
|
|
1619
1897
|
const model = options.model
|
|
@@ -1940,6 +2218,22 @@ export function registerRunCommand(program) {
|
|
|
1940
2218
|
process.exit(1);
|
|
1941
2219
|
}
|
|
1942
2220
|
}
|
|
2221
|
+
// Agent footgun (RUSH-1829): a run with no prompt and no explicit
|
|
2222
|
+
// --interactive resolves to interactive intent, but in a non-TTY shell
|
|
2223
|
+
// (a headless agent, a pipe, CI) there is no terminal to host the REPL —
|
|
2224
|
+
// the TUI attaches to dead stdin and hangs forever. Fail fast with the
|
|
2225
|
+
// headless alternatives instead of launching a doomed interactive session.
|
|
2226
|
+
if (inferredInteractiveWithoutTty(execOptions, isInteractiveTerminal())) {
|
|
2227
|
+
// Tear down the workflow MCP config + subagents staged above before we
|
|
2228
|
+
// exit — same as every sibling exit path; requireInteractiveSelection
|
|
2229
|
+
// process.exits, so cleanup must happen first or it leaks.
|
|
2230
|
+
cleanupWorkflowMcpConfig();
|
|
2231
|
+
cleanupWorkflowSubagents();
|
|
2232
|
+
requireInteractiveSelection(`Launching ${agent} interactively`, [
|
|
2233
|
+
`agents run ${agent} "<your task>" # headless: prints the agent's result`,
|
|
2234
|
+
`agents run ${agent} --headless # headless: reads the prompt from stdin`,
|
|
2235
|
+
]);
|
|
2236
|
+
}
|
|
1943
2237
|
try {
|
|
1944
2238
|
let exitCode;
|
|
1945
2239
|
let ranAgent = agent;
|
|
@@ -1968,6 +2262,10 @@ export function registerRunCommand(program) {
|
|
|
1968
2262
|
// Governance chokepoint (#347): every dispatched run finalizes here.
|
|
1969
2263
|
// ONE tamper-evident audit record per run — non-fatal by contract.
|
|
1970
2264
|
recordDispatchedRun({ agent: ranAgent, version: ranVersion ?? 'unknown', mode, cwd, exitCode });
|
|
2265
|
+
// First-successful-run star nudge (one-time, non-nagging). Only on a
|
|
2266
|
+
// clean run, and never when output is machine-readable/quiet.
|
|
2267
|
+
if (exitCode === 0)
|
|
2268
|
+
maybeShowStarNudge({ quiet: options.json || options.quiet });
|
|
1971
2269
|
process.exit(exitCode);
|
|
1972
2270
|
}
|
|
1973
2271
|
catch (err) {
|
package/dist/commands/feed.d.ts
CHANGED
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
import type { Command } from 'commander';
|
|
13
13
|
import { type OpenBlock } from '../lib/feed.js';
|
|
14
14
|
import { type OutcomeGroup, type SessionOutcomeHint } from '../lib/feed-outcome.js';
|
|
15
|
+
import { filterBlocksForFeed } from '../lib/ask-classifier.js';
|
|
16
|
+
import { type FeedSessionSignal } from '../lib/feed-ranking.js';
|
|
15
17
|
export declare const FEED_NO_FANOUT_ENV = "AGENTS_FEED_LOCAL";
|
|
16
18
|
/** Right-hand masthead summary: `N blocks · M agents`. */
|
|
17
19
|
export declare function formatFeedMastheadRight(blocks: OpenBlock[]): string;
|
|
@@ -20,8 +22,19 @@ export declare function formatFeedReplyHint(mailboxId: string): string;
|
|
|
20
22
|
export declare function parseRemoteFeed(stdout: string, machine: string): OpenBlock[];
|
|
21
23
|
/** Merge local and remote rows, keeping the first copy of a host/session block. */
|
|
22
24
|
export declare function mergeFeedBlocks(...groups: OpenBlock[][]): OpenBlock[];
|
|
25
|
+
export type FeedControlAction = 'pause' | 'kill';
|
|
26
|
+
export declare function controlFeedSession(action: FeedControlAction, target: string, signals: FeedSessionSignal[]): Promise<string>;
|
|
23
27
|
export declare function shouldIncludeLocalFeed(hosts: string[] | undefined, self: string): boolean;
|
|
24
28
|
export declare function remoteFeedHostsToDial(hosts: string[] | undefined, self: string): string[] | undefined;
|
|
29
|
+
export declare function prepareLocalFeedBlocks(localBlocks: OpenBlock[], opts: {
|
|
30
|
+
includeLocal: boolean;
|
|
31
|
+
all?: boolean;
|
|
32
|
+
dispatch?: boolean;
|
|
33
|
+
}): {
|
|
34
|
+
visible: OpenBlock[];
|
|
35
|
+
dispatch: OpenBlock[];
|
|
36
|
+
filter: ReturnType<typeof filterBlocksForFeed>;
|
|
37
|
+
};
|
|
25
38
|
/** Human summary line for one outcome rollup. */
|
|
26
39
|
export declare function formatOutcomeHeader(group: OutcomeGroup): string;
|
|
27
40
|
/** Map active sessions into the lightweight hints outcome enrichment needs. */
|