@phnx-labs/agents-cli 1.20.26 → 1.20.28
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 +29 -0
- package/dist/commands/doctor.d.ts +5 -2
- package/dist/commands/doctor.js +182 -30
- package/dist/commands/exec.d.ts +1 -1
- package/dist/commands/exec.js +177 -6
- package/dist/commands/hosts.d.ts +11 -0
- package/dist/commands/hosts.js +229 -0
- package/dist/commands/repo.d.ts +29 -0
- package/dist/commands/repo.js +174 -38
- package/dist/commands/secrets.d.ts +2 -7
- package/dist/commands/secrets.js +15 -23
- package/dist/commands/sessions.d.ts +2 -0
- package/dist/commands/sessions.js +27 -25
- package/dist/commands/sync.d.ts +2 -0
- package/dist/commands/sync.js +22 -5
- package/dist/commands/view.js +27 -11
- package/dist/index.js +4 -13
- package/dist/lib/agent-spec.d.ts +36 -0
- package/dist/lib/agent-spec.js +157 -0
- package/dist/lib/agents.d.ts +1 -0
- package/dist/lib/agents.js +45 -4
- package/dist/lib/browser/drivers/ssh.d.ts +47 -2
- package/dist/lib/browser/drivers/ssh.js +113 -24
- package/dist/lib/browser/profiles.js +28 -1
- package/dist/lib/browser/runtime-state.js +28 -8
- package/dist/lib/browser/types.d.ts +10 -1
- package/dist/lib/cli-resources.js +10 -1
- package/dist/lib/daemon.js +32 -0
- package/dist/lib/doctor-diff.d.ts +19 -0
- package/dist/lib/doctor-diff.js +107 -15
- package/dist/lib/exec.d.ts +27 -0
- package/dist/lib/exec.js +62 -19
- package/dist/lib/heal.d.ts +107 -0
- package/dist/lib/heal.js +279 -0
- package/dist/lib/hooks.d.ts +17 -0
- package/dist/lib/hooks.js +127 -3
- package/dist/lib/hosts/dispatch.d.ts +26 -0
- package/dist/lib/hosts/dispatch.js +71 -0
- package/dist/lib/hosts/progress.d.ts +21 -0
- package/dist/lib/hosts/progress.js +49 -0
- package/dist/lib/hosts/providers/local.d.ts +17 -0
- package/dist/lib/hosts/providers/local.js +81 -0
- package/dist/lib/hosts/ready.d.ts +37 -0
- package/dist/lib/hosts/ready.js +88 -0
- package/dist/lib/hosts/registry.d.ts +22 -0
- package/dist/lib/hosts/registry.js +65 -0
- package/dist/lib/hosts/ssh-config.d.ts +37 -0
- package/dist/lib/hosts/ssh-config.js +157 -0
- package/dist/lib/hosts/tasks.d.ts +32 -0
- package/dist/lib/hosts/tasks.js +58 -0
- package/dist/lib/hosts/types.d.ts +51 -0
- package/dist/lib/hosts/types.js +21 -0
- package/dist/lib/loop.d.ts +9 -0
- package/dist/lib/loop.js +13 -1
- package/dist/lib/mcp.js +12 -3
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/migrate.js +9 -5
- package/dist/lib/platform/exec.d.ts +10 -0
- package/dist/lib/platform/exec.js +17 -0
- package/dist/lib/platform/index.d.ts +1 -0
- package/dist/lib/platform/index.js +1 -0
- package/dist/lib/platform/links.d.ts +15 -0
- package/dist/lib/platform/links.js +42 -0
- package/dist/lib/platform/paths.d.ts +18 -0
- package/dist/lib/platform/paths.js +22 -0
- package/dist/lib/platform/posixpath.d.ts +28 -0
- package/dist/lib/platform/posixpath.js +153 -0
- package/dist/lib/plugin-marketplace.d.ts +18 -0
- package/dist/lib/plugin-marketplace.js +67 -1
- package/dist/lib/plugins.d.ts +33 -1
- package/dist/lib/plugins.js +56 -11
- package/dist/lib/project-launch.js +6 -3
- package/dist/lib/sandbox.js +5 -2
- package/dist/lib/self-update.js +7 -2
- package/dist/lib/session/db.d.ts +23 -0
- package/dist/lib/session/db.js +76 -1
- package/dist/lib/session/discover.d.ts +26 -0
- package/dist/lib/session/discover.js +75 -4
- package/dist/lib/session/relative-time.d.ts +7 -0
- package/dist/lib/session/relative-time.js +28 -0
- package/dist/lib/session/remote.d.ts +61 -0
- package/dist/lib/session/remote.js +221 -0
- package/dist/lib/ssh-exec.d.ts +45 -0
- package/dist/lib/ssh-exec.js +61 -0
- package/dist/lib/staleness/detectors/commands.js +7 -6
- package/dist/lib/staleness/writers/commands.js +7 -12
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/startup/dev-build.d.ts +22 -0
- package/dist/lib/startup/dev-build.js +41 -0
- package/dist/lib/types.d.ts +28 -0
- package/dist/lib/versions.d.ts +9 -3
- package/dist/lib/versions.js +43 -7
- package/package.json +3 -3
- package/scripts/postinstall.js +62 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
**`agents sessions --host <machine>`: query a remote machine's sessions live over SSH**
|
|
6
|
+
|
|
7
|
+
- `agents sessions "<query>" --host <alias|user@host>` runs the same session query on a remote machine's own index over SSH and streams the result back — repeat `--host` (or pass several) to fan out across machines. SSH access is the only auth; there's no daemon or shared store. Targets are validated against a strict allowlist (`SSH_TARGET_RE`) to block flag-smuggling, and the forwarded invocation is double-quoted (`shellQuote`) so a query like `$(whoami)` survives as a literal string on both shell layers. Source: `src/lib/session/remote.ts`, `src/commands/sessions.ts`, `docs/05-sessions.md`.
|
|
8
|
+
|
|
9
|
+
**Fix: migrations + menu-bar self-heal were silently disabled on Homebrew-node installs**
|
|
10
|
+
|
|
11
|
+
- The "is this a dev build?" check walked `dirname(dirname(argv[1]))` looking for a `.git`, without resolving the bin symlink. On a Homebrew-node setup `agents` is `/opt/homebrew/bin/agents`, so it walked up to `/opt/homebrew` — **which is itself a git repo** — and false-positived as a dev build. Dev builds auto-set `AGENTS_SKIP_MIGRATION=1`, which gates **both** one-shot migrations **and** the menu-bar upgrade self-heal. Net effect: every Homebrew-node user ran with migrations and the menu-bar refresh permanently off.
|
|
12
|
+
- Detection now `realpath`s the entrypoint (so a symlinked bin resolves into the real package dir) and requires the `.git`'s repo root to actually be the `@phnx-labs/agents-cli` package — an unrelated ancestor repo no longer counts. Extracted to `src/lib/startup/dev-build.ts` with tests covering the Homebrew symlink layout, a real checkout, and unrelated-ancestor cases.
|
|
13
|
+
|
|
5
14
|
**Secrets default policy is now `daily` (one Touch ID per ~24h), not `always`**
|
|
6
15
|
|
|
7
16
|
- The default prompt policy for bundles without an explicit one flipped from `always` (Touch ID on *every* read) to **`daily`** (one prompt, then held ~24h until screen-lock / sleep / logout). This is the fix for the prompt storm: a background reader like sessions-sync hammering a bundle now costs one Touch ID per ~24h instead of one per read.
|
|
@@ -9,10 +18,30 @@
|
|
|
9
18
|
- **Configurable, still flexible.** Set the global default in `agents.yaml` (`secrets.policy: always` to restore prompt-every-time), or override per bundle with `agents secrets policy <bundle> always` for high-value keys (signing, SSH) you want to confirm on every read.
|
|
10
19
|
- **Explicit `always` now persists** under the legacy `tier: biometry` token (older CLIs read it as their own always default). Bundles with no stored policy inherit the configured default — so an existing always-by-default bundle quietly becomes `daily` on first read by the new CLI, which is the intended migration.
|
|
11
20
|
|
|
21
|
+
**Menu bar: a macOS status item for agent activity (`agents menubar`)**
|
|
22
|
+
|
|
23
|
+
- New no-Dock menu bar app showing live agent activity on the machine: a **NEEDS YOU** section (sessions awaiting input + failed/overdue routines), a per-agent **roster** (running / idle counts across installed agents), a **+ New session** launcher, and a one-line routines summary. The icon badges red `!` when something needs you, green with a count when sessions are running.
|
|
24
|
+
- Reads state **directly from disk** — `live-terminals.json`, teams `meta.json`, and the cloud `tasks.db` — so opening the menu never triggers the costly sessions transcript re-index. The CLI is shelled only for actions (start a session, run a routine).
|
|
25
|
+
- **Auto-enabled on macOS** for every user as a launchd login service (`com.phnx-labs.agents-menubar`); a fresh install brings the icon up with no manual step. Manage with `agents menubar enable | disable | status`. Opt out with `agents menubar disable` — sticky across upgrades.
|
|
26
|
+
- **Upgrade self-heal:** the installed bundle is version-stamped, and the startup self-heal now re-installs the helper when a newer release ships a newer build (or the installed copy goes missing), instead of skipping whenever a service already existed. So `npm update` actually moves users onto the new helper binary + plist rather than leaving the old one running (#442). `agents menubar status` shows installed vs current version and staleness.
|
|
27
|
+
- Docs: [Menu bar](docs/menubar.md). macOS only.
|
|
28
|
+
|
|
12
29
|
**`agents repos view [name]`: inspect one repo's contents without opening it**
|
|
13
30
|
|
|
14
31
|
- New `agents repo view <name>` (also reachable as `agents repos view`, now a first-class alias of the `repo` command) prints a single repo's git state and per-kind resource counts — `system`, `user`, `project`, or an extra-repo alias. Omit the name for an interactive picker over the registered repos. It reuses the `inspect` repo renderer, so output matches `agents inspect <repo>`; supports `--brief` and `--json`. Source: `src/commands/repo.ts`, `src/commands/inspect.ts`.
|
|
15
32
|
|
|
33
|
+
**`agents doctor --fix` + a daemon safety check: heal the gap between defined and installed**
|
|
34
|
+
|
|
35
|
+
- Root cause behind "a plugin/command silently vanished": a DotAgents repo can DEFINE a resource that never makes it into an agent home, and nothing closes the gap. Two concrete failure modes — (1) `agents plugins update`/`sync` only reconcile each agent's **default** version, so a non-default installed version keeps serving stale/invalid resources; (2) a plugin.json with a bare-name `skills`/`commands` field makes Claude Code **silently reject the entire plugin**, and the sync path only *warned*. The detection (`agents doctor`'s live-home diff) and the healing (`syncResourcesToVersion`) existed but were never wired together — and the sync fast-guard keyed off the staleness manifest, which is blind to home-side rot.
|
|
36
|
+
- **`agents doctor --fix`** turns the read-only diagnosis into a heal: installs missing resources, repairs Claude-invalid plugin manifests (strips the bare `skills`/`commands` field — Claude auto-discovers from the dirs), fast-forwards stale plugins from their `.source`, and reconciles drift — across **every installed version**, not just defaults. With no target it heals the whole install; `agents doctor <agent> --fix` scopes to one.
|
|
37
|
+
- **Daemon safety check:** the routines daemon now runs the same heal in conservative `safe` mode (~every 6h + ~30s after start) — it fixes only unambiguous gaps (missing resources, invalid manifests, *provably-unmodified* stale plugins) and **notifies rather than clobbers** on hand-edited content or a plugin it can't prove is pristine.
|
|
38
|
+
- Built on the **live-home diff**, not the staleness manifest, so it catches drift the sync fast-guard can't. Heal **fills and fixes, never deletes** (orphans stay `agents prune cleanup`'s job), excludes the project layer (the global home isn't reconciled against per-cwd project resources), and **verifies after writing** — it only claims resources that actually reconciled, so repeated runs converge instead of "fixing" the same item forever.
|
|
39
|
+
- `.source` now records the plugin version at pull time, a baseline that lets the safe path tell an untouched mirror (fast-forward) from a user edit (leave alone).
|
|
40
|
+
- **`agents doctor` overview now covers every installed version, not just defaults.** Sync status and orphans previously reported only each agent's default version — so a stale NON-default version (the exact rot `--fix` heals) was invisible in the readout. Each version is now listed with its default marked. The **Agent CLIs** list also stops nagging: it shows the agents you actually run (ready, or managed-but-broken) and collapses the rest of the supported catalog to a single `+N more supported …` hint instead of a column of red "not installed" lines for tools you never adopted.
|
|
41
|
+
- **Says exactly WHAT is out of sync — plugins first.** A stale version in the overview now lists the specifics under it, prioritizing plugins and their bundled content: `plugin code — 0.6.1→0.7.0, missing skills: ship, learn`. The plugin diff went from presence-only ("installed: yes") to **content-aware** — it compares the version's marketplace mirror against the central source and surfaces a stale mirror version, a Claude-invalid manifest, and the plugin's own skills/commands that never reached the mirror (the system-repo content that matters most). `agents doctor <agent>@<version>` shows the same detail per plugin row.
|
|
42
|
+
- **Fixed a false "drift" that could never be reconciled:** a hook's `.md`/`.rst` doc sibling (e.g. `git-guard.md` next to `git-guard.sh`) was wrongly treated as the hook's runtime *data file*, so the installer's correct omission of docs showed as perpetual drift in `doctor` (and as an un-healable item under `--fix`). Docs are no longer counted as hook data; structured siblings (`.yaml`/`.json`/...) still are.
|
|
43
|
+
- **Corrected a false promise in the sync-status readout.** Stale/cold versions used to say "will sync on next launch" / "first launch will populate" — but version homes are NOT reconciled on launch (the shim hot path only resolves a version and compiles project-scoped resources; v15/v16 moved version-home reconciliation to management commands). The readout now states the fact ("sources changed since last sync" / "never synced") and points at the real fix: `agents doctor <agent>@<version> --fix` or `agents sync <agent>@<version>`.
|
|
44
|
+
|
|
16
45
|
**Secrets prompt policy: human-readable `always` / `daily`, and `secrets list` now shows it**
|
|
17
46
|
|
|
18
47
|
- Renamed the secrets-agent `tier` to a **prompt policy** with plain-language names: `biometry` → **`always`** (ask every time), `session` → **`daily`** (ask once, then held ~24h until screen-lock / sleep / logout). The old name `session` was misleading — it never meant "once per login session" — and collided with the half-dozen other "session" concepts in the CLI (`agents sessions`, sessions-sync, pty/browser sessions). Set it with `agents secrets policy <bundle> [always|daily]`.
|
|
@@ -15,8 +15,11 @@
|
|
|
15
15
|
* unified diff body for each divergent file. Mirrors the resolution that
|
|
16
16
|
* the shim drives at runtime: project > user > system > extras.
|
|
17
17
|
*
|
|
18
|
-
* Read-only: doctor
|
|
19
|
-
*
|
|
18
|
+
* Read-only by default: doctor diagnoses, it doesn't mutate. Pass `--fix` to
|
|
19
|
+
* heal the gaps it finds (install missing resources, repair Claude-invalid
|
|
20
|
+
* plugin manifests, refresh stale plugins, reconcile drift). Run
|
|
21
|
+
* `agents prune cleanup` to act on orphan readouts, or just launch the agent to
|
|
22
|
+
* apply pending sync.
|
|
20
23
|
*/
|
|
21
24
|
import type { Command } from 'commander';
|
|
22
25
|
export declare function registerDoctorCommand(program: Command): void;
|
package/dist/commands/doctor.js
CHANGED
|
@@ -10,22 +10,64 @@ import { diffVersionResources, DOCTOR_ALL_KINDS, } from '../lib/doctor-diff.js';
|
|
|
10
10
|
import { unifiedDiff, colorizeUnifiedDiff } from '../lib/diff-text.js';
|
|
11
11
|
import { listCliStatus } from '../lib/cli-resources.js';
|
|
12
12
|
import { setHelpSections } from '../lib/help.js';
|
|
13
|
+
import { heal, healChangedAnything } from '../lib/heal.js';
|
|
13
14
|
import * as fs from 'fs';
|
|
14
15
|
const AGENT_NAMES = Object.fromEntries(ALL_AGENT_IDS.map((id) => [id, AGENTS[id].name]));
|
|
15
16
|
// ─── overview mode (no target) ────────────────────────────────────────────────
|
|
17
|
+
// Lines naming exactly what's out of sync for a version, plugins prioritized:
|
|
18
|
+
// each divergent plugin gets its own line with specifics (stale mirror version,
|
|
19
|
+
// invalid manifest, or the bundled skills/commands missing from the mirror —
|
|
20
|
+
// the system-repo plugin content that matters most). Other kinds collapse to
|
|
21
|
+
// compact counts so the readout stays scannable.
|
|
22
|
+
function divergenceLines(report) {
|
|
23
|
+
const lines = [];
|
|
24
|
+
for (const p of report.kinds.plugins) {
|
|
25
|
+
if (p.status === 'missing')
|
|
26
|
+
lines.push(`plugin ${p.name} — not installed`);
|
|
27
|
+
else if (p.status === 'diff')
|
|
28
|
+
lines.push(`plugin ${p.name} — ${p.detail ?? 'mirror drifted'}`);
|
|
29
|
+
}
|
|
30
|
+
const counts = [];
|
|
31
|
+
for (const kind of ['commands', 'skills', 'hooks', 'rules', 'mcp', 'permissions', 'subagents']) {
|
|
32
|
+
const rows = report.kinds[kind];
|
|
33
|
+
const miss = rows.filter((r) => r.status === 'missing').length;
|
|
34
|
+
const dif = rows.filter((r) => r.status === 'diff').length;
|
|
35
|
+
const bits = [];
|
|
36
|
+
if (miss)
|
|
37
|
+
bits.push(`${miss} missing`);
|
|
38
|
+
if (dif)
|
|
39
|
+
bits.push(`${dif} drifted`);
|
|
40
|
+
if (bits.length)
|
|
41
|
+
counts.push(`${kind} ${bits.join('/')}`);
|
|
42
|
+
}
|
|
43
|
+
if (counts.length)
|
|
44
|
+
lines.push(counts.join(' · '));
|
|
45
|
+
return lines;
|
|
46
|
+
}
|
|
16
47
|
function checkSyncStatus(cwd) {
|
|
17
48
|
const rows = [];
|
|
49
|
+
// Every installed version, not just the default — a stale NON-default version
|
|
50
|
+
// (e.g. one you launched from yesterday) is exactly the rot that silently
|
|
51
|
+
// serves outdated/invalid resources and that `--fix` now heals. Hiding it here
|
|
52
|
+
// is why that class of bug went unnoticed.
|
|
18
53
|
for (const agent of ALL_AGENT_IDS) {
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
54
|
+
const def = getGlobalDefault(agent);
|
|
55
|
+
for (const version of listInstalledVersions(agent)) {
|
|
56
|
+
const manifest = loadManifest(agent, version);
|
|
57
|
+
const status = !manifest
|
|
58
|
+
? 'never-synced'
|
|
59
|
+
: isStale(manifest, agent, version, cwd) ? 'stale' : 'fresh';
|
|
60
|
+
const row = { agent, version, status, isDefault: version === def };
|
|
61
|
+
if (status === 'stale') {
|
|
62
|
+
// Resolve the specifics against non-project layers (the global home is
|
|
63
|
+
// never reconciled against per-cwd project resources).
|
|
64
|
+
const report = diffVersionResources(agent, version, { cwd, excludeProject: true });
|
|
65
|
+
const lines = divergenceLines(report);
|
|
66
|
+
if (lines.length)
|
|
67
|
+
row.divergence = lines;
|
|
68
|
+
}
|
|
69
|
+
rows.push(row);
|
|
26
70
|
}
|
|
27
|
-
const stale = isStale(manifest, agent, version, cwd);
|
|
28
|
-
rows.push({ agent, version, status: stale ? 'stale' : 'fresh' });
|
|
29
71
|
}
|
|
30
72
|
return rows;
|
|
31
73
|
}
|
|
@@ -41,15 +83,11 @@ function countOrphans() {
|
|
|
41
83
|
return row;
|
|
42
84
|
};
|
|
43
85
|
for (const { agent, version } of iterCommandsCapableVersions()) {
|
|
44
|
-
if (version !== getGlobalDefault(agent))
|
|
45
|
-
continue;
|
|
46
86
|
const diff = diffVersionCommands(agent, version);
|
|
47
87
|
if (diff.orphans.length > 0)
|
|
48
88
|
ensure(agent, version).commands = diff.orphans.length;
|
|
49
89
|
}
|
|
50
90
|
for (const { agent, version } of iterSkillsCapableVersions()) {
|
|
51
|
-
if (version !== getGlobalDefault(agent))
|
|
52
|
-
continue;
|
|
53
91
|
const diff = diffVersionSkills(agent, version);
|
|
54
92
|
if (diff.orphans.length > 0)
|
|
55
93
|
ensure(agent, version).skills = diff.orphans.length;
|
|
@@ -59,8 +97,6 @@ function countOrphans() {
|
|
|
59
97
|
// never fire. (Distinct from the source-diff `diffVersionHooks().orphans`,
|
|
60
98
|
// which false-flags valid system-sourced registered hooks.)
|
|
61
99
|
for (const { agent, version } of iterHooksCapableVersions()) {
|
|
62
|
-
if (version !== getGlobalDefault(agent))
|
|
63
|
-
continue;
|
|
64
100
|
const dead = listUnmanagedHooksInVersionHome(agent, version);
|
|
65
101
|
if (dead.length > 0)
|
|
66
102
|
ensure(agent, version).hooks = dead.length;
|
|
@@ -69,41 +105,66 @@ function countOrphans() {
|
|
|
69
105
|
}
|
|
70
106
|
function renderOverviewText(clis, syncRows, orphanRows, hostClis) {
|
|
71
107
|
console.log(chalk.bold('Agent CLIs'));
|
|
72
|
-
|
|
73
|
-
|
|
108
|
+
// Show the fleet you actually run — agents that are ready in PATH, plus any
|
|
109
|
+
// you MANAGE (have installed versions) whose binary isn't resolving (a real
|
|
110
|
+
// problem). The other supported-but-unadopted agents collapse to one hint line
|
|
111
|
+
// instead of a column of red "not installed" nags for tools you never wanted.
|
|
112
|
+
const managed = new Set(ALL_AGENT_IDS.filter((a) => listInstalledVersions(a).length > 0));
|
|
113
|
+
const entries = Object.entries(clis);
|
|
114
|
+
const shown = entries.filter(([name, e]) => e.installed || managed.has(name));
|
|
115
|
+
const hidden = entries.filter(([name, e]) => !e.installed && !managed.has(name)).map(([name]) => name);
|
|
116
|
+
if (shown.length === 0) {
|
|
117
|
+
console.log(chalk.gray(' (none installed — `agents add <name>` to start)'));
|
|
74
118
|
}
|
|
75
119
|
else {
|
|
76
|
-
for (const [name, entry] of
|
|
77
|
-
const pretty = AGENT_NAMES[name] || name;
|
|
120
|
+
for (const [name, entry] of shown) {
|
|
121
|
+
const pretty = (AGENT_NAMES[name] || name).padEnd(11);
|
|
78
122
|
if (entry.installed) {
|
|
79
|
-
console.log(` ${chalk.green('ready')} ${pretty
|
|
123
|
+
console.log(` ${chalk.green('ready')} ${pretty} ${chalk.gray(entry.path || '')}`);
|
|
80
124
|
}
|
|
81
125
|
else {
|
|
82
|
-
console.log(` ${chalk.red('no ')} ${pretty
|
|
126
|
+
console.log(` ${chalk.red('no ')} ${pretty} ${chalk.gray(entry.error || 'not installed')}`);
|
|
83
127
|
}
|
|
84
128
|
}
|
|
85
129
|
}
|
|
130
|
+
if (hidden.length > 0) {
|
|
131
|
+
console.log(chalk.gray(` +${hidden.length} more supported (${hidden.join(', ')}) — \`agents add <name>\` to manage`));
|
|
132
|
+
}
|
|
86
133
|
console.log();
|
|
87
|
-
console.log(chalk.bold('Sync status (
|
|
134
|
+
console.log(chalk.bold('Sync status (installed versions)'));
|
|
88
135
|
if (syncRows.length === 0) {
|
|
89
|
-
console.log(chalk.gray(' (no
|
|
136
|
+
console.log(chalk.gray(' (no versions installed; add one with `agents add <agent>@<version>`)'));
|
|
90
137
|
}
|
|
91
138
|
else {
|
|
139
|
+
let anyOutOfSync = false;
|
|
92
140
|
for (const row of syncRows) {
|
|
93
|
-
const
|
|
141
|
+
const tag = row.isDefault ? chalk.gray(' (default)') : '';
|
|
142
|
+
const label = `${AGENT_NAMES[row.agent] || row.agent}@${row.version}${tag}`;
|
|
94
143
|
if (row.status === 'fresh') {
|
|
95
144
|
console.log(` ${chalk.green('fresh')} ${label}`);
|
|
96
145
|
}
|
|
97
146
|
else if (row.status === 'stale') {
|
|
98
|
-
|
|
147
|
+
anyOutOfSync = true;
|
|
148
|
+
console.log(` ${chalk.yellow('stale')} ${label} ${chalk.gray('— sources changed since last sync')}`);
|
|
149
|
+
for (const line of row.divergence ?? []) {
|
|
150
|
+
console.log(chalk.gray(` ${line}`));
|
|
151
|
+
}
|
|
99
152
|
}
|
|
100
153
|
else {
|
|
101
|
-
|
|
154
|
+
anyOutOfSync = true;
|
|
155
|
+
console.log(` ${chalk.gray('cold ')} ${label} ${chalk.gray('— never synced')}`);
|
|
102
156
|
}
|
|
103
157
|
}
|
|
158
|
+
// Launching does NOT reconcile a version home — the shim hot path only
|
|
159
|
+
// resolves a version and compiles project-scoped resources (shims.ts v15/v16).
|
|
160
|
+
// Version homes are reconciled only by management commands, so point at one
|
|
161
|
+
// rather than promising an auto-sync that never happens.
|
|
162
|
+
if (anyOutOfSync) {
|
|
163
|
+
console.log(chalk.gray(' Reconcile with `agents doctor <agent>@<version> --fix` or `agents sync <agent>@<version>` (not applied on launch).'));
|
|
164
|
+
}
|
|
104
165
|
}
|
|
105
166
|
console.log();
|
|
106
|
-
console.log(chalk.bold('Orphans (
|
|
167
|
+
console.log(chalk.bold('Orphans (installed versions)'));
|
|
107
168
|
if (orphanRows.length === 0) {
|
|
108
169
|
console.log(chalk.gray(' (none — version homes match central sources)'));
|
|
109
170
|
}
|
|
@@ -252,7 +313,8 @@ function renderKindSection(kind, rows, layers, options) {
|
|
|
252
313
|
}
|
|
253
314
|
for (const r of visible) {
|
|
254
315
|
const src = sourceLabel(r, layers);
|
|
255
|
-
|
|
316
|
+
const detail = r.detail ? chalk.gray(` ${r.detail}`) : '';
|
|
317
|
+
console.log(` ${statusLabel(r.status)} ${r.name.padEnd(28)} ${src}${detail}`);
|
|
256
318
|
if (options.showDiff && r.status === 'diff' && r.sourcePath && r.homePath) {
|
|
257
319
|
const expected = readExpectedForDiff(kind, r);
|
|
258
320
|
const actual = safeRead(r.homePath);
|
|
@@ -340,9 +402,77 @@ function renderTargetText(report, options) {
|
|
|
340
402
|
}
|
|
341
403
|
else {
|
|
342
404
|
console.log(` Verdict: ${verdictParts.join(', ')}.`);
|
|
343
|
-
console.log(chalk.gray(` Run \`agents
|
|
405
|
+
console.log(chalk.gray(` Run \`agents doctor ${report.agent}@${report.version} --fix\` to heal, or \`agents prune cleanup\` to drop extras.`));
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
// ─── fix / heal mode ───────────────────────────────────────────────────────────
|
|
409
|
+
function renderHealText(result) {
|
|
410
|
+
for (const r of result.repairedManifests) {
|
|
411
|
+
console.log(` ${chalk.green('repair')} plugin ${chalk.bold(r.plugin)} ${chalk.gray(`— dropped invalid ${r.droppedFields.join(', ')} field`)}`);
|
|
412
|
+
}
|
|
413
|
+
for (const r of result.refreshedPlugins) {
|
|
414
|
+
console.log(` ${chalk.green('refresh')} plugin ${chalk.bold(r.plugin)} ${chalk.gray(`${r.from} → ${r.to}`)}`);
|
|
415
|
+
}
|
|
416
|
+
for (const s of result.skippedPlugins) {
|
|
417
|
+
const why = s.reason === 'modified'
|
|
418
|
+
? `locally modified — left as-is (run \`agents plugins update ${s.plugin}\` to force)`
|
|
419
|
+
: `no baseline recorded — left as-is (run \`agents plugins update ${s.plugin}\` to adopt)`;
|
|
420
|
+
console.log(` ${chalk.yellow('hold ')} plugin ${chalk.bold(s.plugin)} ${chalk.gray(`${s.from} → ${s.upstream} available; ${why}`)}`);
|
|
421
|
+
}
|
|
422
|
+
for (const v of result.versions) {
|
|
423
|
+
const label = `${AGENT_NAMES[v.agent] || v.agent}@${v.version}`;
|
|
424
|
+
if (v.healed.length === 0 && v.skipped.length === 0)
|
|
425
|
+
continue;
|
|
426
|
+
const byKind = new Map();
|
|
427
|
+
for (const h of v.healed)
|
|
428
|
+
byKind.set(h.kind, (byKind.get(h.kind) ?? 0) + 1);
|
|
429
|
+
const parts = Array.from(byKind, ([k, n]) => `${n} ${k}`);
|
|
430
|
+
if (v.healed.length > 0) {
|
|
431
|
+
console.log(` ${chalk.green('fixed ')} ${label} ${chalk.gray(parts.join(', '))}`);
|
|
432
|
+
}
|
|
433
|
+
const drift = v.skipped.filter((s) => s.reason === 'drift');
|
|
434
|
+
const unres = v.skipped.filter((s) => s.reason === 'unreconcilable');
|
|
435
|
+
if (drift.length > 0) {
|
|
436
|
+
console.log(` ${chalk.yellow('drift ')} ${label} ${chalk.gray(`${drift.length} hand-edited — left as-is (use \`--diff\` to inspect)`)}`);
|
|
437
|
+
}
|
|
438
|
+
if (unres.length > 0) {
|
|
439
|
+
const names = unres.map((s) => `${s.kind}/${s.name}`).join(', ');
|
|
440
|
+
console.log(` ${chalk.yellow('hold ')} ${label} ${chalk.gray(`${unres.length} couldn't reconcile (${names}) — source/home mismatch the writer can't satisfy`)}`);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
console.log();
|
|
444
|
+
const healed = result.versions.reduce((n, v) => n + v.healed.length, 0);
|
|
445
|
+
const touchedVersions = result.versions.filter((v) => v.healed.length > 0).length;
|
|
446
|
+
if (!healChangedAnything(result)) {
|
|
447
|
+
console.log(chalk.green('✓ Everything in sync — nothing to heal.'));
|
|
448
|
+
}
|
|
449
|
+
else {
|
|
450
|
+
const bits = [];
|
|
451
|
+
if (healed > 0)
|
|
452
|
+
bits.push(`${healed} resource${healed === 1 ? '' : 's'} across ${touchedVersions} version${touchedVersions === 1 ? '' : 's'}`);
|
|
453
|
+
if (result.repairedManifests.length > 0)
|
|
454
|
+
bits.push(`${result.repairedManifests.length} manifest${result.repairedManifests.length === 1 ? '' : 's'} repaired`);
|
|
455
|
+
if (result.refreshedPlugins.length > 0)
|
|
456
|
+
bits.push(`${result.refreshedPlugins.length} plugin${result.refreshedPlugins.length === 1 ? '' : 's'} refreshed`);
|
|
457
|
+
console.log(chalk.green(`✓ Healed ${bits.join(', ')}.`));
|
|
344
458
|
}
|
|
345
459
|
}
|
|
460
|
+
async function runFix(parsed, opts) {
|
|
461
|
+
// Heal targets the global install — project layer is irrelevant, so cwd is
|
|
462
|
+
// left to heal's neutral default rather than process.cwd().
|
|
463
|
+
if (!opts.json)
|
|
464
|
+
console.log(chalk.bold('Healing…'));
|
|
465
|
+
const result = await heal({
|
|
466
|
+
mode: 'full',
|
|
467
|
+
agent: parsed?.agent,
|
|
468
|
+
versions: parsed?.versions,
|
|
469
|
+
});
|
|
470
|
+
if (opts.json) {
|
|
471
|
+
console.log(JSON.stringify(result, null, 2));
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
renderHealText(result);
|
|
475
|
+
}
|
|
346
476
|
// ─── command registration ────────────────────────────────────────────────────
|
|
347
477
|
export function registerDoctorCommand(program) {
|
|
348
478
|
const doctorCmd = program
|
|
@@ -350,6 +480,7 @@ export function registerDoctorCommand(program) {
|
|
|
350
480
|
.description('Diagnose CLI availability, sync status, and resource divergence (optionally for a specific agent[@version]).')
|
|
351
481
|
.option('--json', 'Output machine-readable JSON')
|
|
352
482
|
.option('--diff', 'In target mode, include unified diffs for divergent files')
|
|
483
|
+
.option('--fix', 'Heal gaps: install missing resources, repair invalid plugin manifests, refresh stale plugins, and reconcile drift (all installed versions, or just the target)')
|
|
353
484
|
.option('--kind <kinds>', 'Restrict to comma-separated resource kinds (commands,skills,hooks,rules,mcp,permissions,subagents,plugins,promptcuts)')
|
|
354
485
|
.option('--cwd <path>', 'Resolution cwd for project layer detection (default: process.cwd())');
|
|
355
486
|
setHelpSections(doctorCmd, {
|
|
@@ -368,10 +499,31 @@ export function registerDoctorCommand(program) {
|
|
|
368
499
|
|
|
369
500
|
# Inspect only rules and hooks, with full diffs
|
|
370
501
|
agents doctor claude@default --kind rules,hooks --diff
|
|
502
|
+
|
|
503
|
+
# Heal every gap across all installed versions
|
|
504
|
+
agents doctor --fix
|
|
505
|
+
|
|
506
|
+
# Heal just one agent (all its installed versions)
|
|
507
|
+
agents doctor claude --fix
|
|
371
508
|
`,
|
|
372
509
|
});
|
|
373
|
-
doctorCmd.action((target, opts) => {
|
|
510
|
+
doctorCmd.action(async (target, opts) => {
|
|
374
511
|
const cwd = opts.cwd ? opts.cwd : process.cwd();
|
|
512
|
+
// --fix turns the read-only diagnosis into a heal. With no target it heals
|
|
513
|
+
// every installed version; with a target it scopes to that agent.
|
|
514
|
+
if (opts.fix) {
|
|
515
|
+
let scope = null;
|
|
516
|
+
if (target) {
|
|
517
|
+
const parsed = parseTargetArg(target);
|
|
518
|
+
if ('error' in parsed) {
|
|
519
|
+
console.error(chalk.red(parsed.error));
|
|
520
|
+
process.exit(1);
|
|
521
|
+
}
|
|
522
|
+
scope = parsed;
|
|
523
|
+
}
|
|
524
|
+
await runFix(scope, opts);
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
375
527
|
if (!target) {
|
|
376
528
|
const clis = checkAllClis();
|
|
377
529
|
const syncRows = checkSyncStatus(cwd);
|
package/dist/commands/exec.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* or headlessly. Supports profile resolution, version rotation, secrets
|
|
6
6
|
* injection, and multi-agent fallback chains for rate-limit resilience.
|
|
7
7
|
*/
|
|
8
|
-
import type
|
|
8
|
+
import { type Command } from 'commander';
|
|
9
9
|
/**
|
|
10
10
|
* Build the LoopConfig the driver consumes from CLI flags and/or a workflow's
|
|
11
11
|
* `loop:` frontmatter block (issue #332). Returns undefined when neither source
|
package/dist/commands/exec.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* or headlessly. Supports profile resolution, version rotation, secrets
|
|
6
6
|
* injection, and multi-agent fallback chains for rate-limit resilience.
|
|
7
7
|
*/
|
|
8
|
+
import { Option } from 'commander';
|
|
8
9
|
import chalk from 'chalk';
|
|
9
10
|
import { setHelpSections } from '../lib/help.js';
|
|
10
11
|
import { parseLoopInterval } from '../lib/loop.js';
|
|
@@ -117,7 +118,8 @@ export function registerRunCommand(program) {
|
|
|
117
118
|
.option('--quiet', 'Suppress preamble (rotation banner, "Running:" line). Useful when piping JSON events to a parser.', false)
|
|
118
119
|
.option('--headless', 'Force headless mode. Auto-enabled when a prompt is provided; pass explicitly to stay headless with no prompt (reads the prompt from stdin).', false)
|
|
119
120
|
.option('-i, --interactive', 'Force interactive mode even when a prompt is provided. Mutually exclusive with --headless.')
|
|
120
|
-
.option('--
|
|
121
|
+
.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.')
|
|
122
|
+
.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.')
|
|
121
123
|
.option('--verbose', 'Show detailed execution logs')
|
|
122
124
|
.option('--timeout <duration>', 'Kill the agent after this duration (e.g., 30m, 1h, 2h30m)')
|
|
123
125
|
.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.')
|
|
@@ -130,7 +132,14 @@ export function registerRunCommand(program) {
|
|
|
130
132
|
.option('--max-iterations <n>', 'Loop hard cap: stop after N iterations (stoppedBy: max). Loop only.')
|
|
131
133
|
.option('--budget <tokens>', 'Loop token hard-cap: stop once cumulative tokens reach this (stoppedBy: budget), enforced outside the agent. Loop only.')
|
|
132
134
|
.option('--until <signal>', 'Loop stop condition. `signal` reads <runDir>/loop-signal.json {continue,reason} each iteration; absent or continue:false stops (fail-closed). Loop only.')
|
|
133
|
-
.option('--interval <dur>', 'Loop delay between iterations ("0" back-to-back, "30m" paces). Loop only.')
|
|
135
|
+
.option('--interval <dur>', 'Loop delay between iterations ("0" back-to-back, "30m" paces). Loop only.')
|
|
136
|
+
.option('--host <name>', 'Offload this run onto a registered agent host over SSH instead of running locally. See `agents hosts`.')
|
|
137
|
+
.option('--remote-cwd <dir>', 'Working directory on the host for --host runs.')
|
|
138
|
+
.option('--no-follow', 'With --host, dispatch detached and return immediately (track via `agents hosts ps/logs`).')
|
|
139
|
+
.option('--any', 'With --host <cap> (a capability tag), pick any matching host instead of erroring when several match.');
|
|
140
|
+
// `--on` and `--computer` are hidden aliases of `--host` — same behavior.
|
|
141
|
+
runCmd.addOption(new Option('--on <name>', 'Alias of --host.').hideHelp());
|
|
142
|
+
runCmd.addOption(new Option('--computer <name>', 'Alias of --host.').hideHelp());
|
|
134
143
|
setHelpSections(runCmd, {
|
|
135
144
|
examples: `
|
|
136
145
|
# Headless, read-only: investigate or summarize without writing files
|
|
@@ -171,7 +180,7 @@ export function registerRunCommand(program) {
|
|
|
171
180
|
|
|
172
181
|
Fallback: --fallback codex,gemini retries on rate-limit failure via /continue handoff. Each entry accepts @version.
|
|
173
182
|
|
|
174
|
-
Resume: --
|
|
183
|
+
Resume: --resume <id> continues a prior conversation (full or partial id; omit to pick interactively). claude/codex resume natively; others replay via a /continue first message. Add a prompt to continue headlessly.
|
|
175
184
|
|
|
176
185
|
Passthrough: everything after -- is forwarded verbatim to the underlying agent CLI.
|
|
177
186
|
agents run kimi -- --plan --some-native-flag value
|
|
@@ -182,6 +191,61 @@ export function registerRunCommand(program) {
|
|
|
182
191
|
// Use command.args (all positional strings) and strip the declared positional args from the front.
|
|
183
192
|
const declaredArgCount = prompt !== undefined ? 2 : 1;
|
|
184
193
|
const passthroughArgs = command.args.slice(declaredArgCount);
|
|
194
|
+
// --host/--on/--computer: offload this run onto a registered agent host
|
|
195
|
+
// over SSH instead of running locally. The three flags are aliases.
|
|
196
|
+
const hostGiven = [options.host, options.on, options.computer].filter((v) => !!v);
|
|
197
|
+
if (hostGiven.length > 0) {
|
|
198
|
+
if (new Set(hostGiven).size > 1) {
|
|
199
|
+
console.error(chalk.red('Conflicting --host/--on/--computer values — pass just one.'));
|
|
200
|
+
process.exit(1);
|
|
201
|
+
}
|
|
202
|
+
const hostName = hostGiven[0];
|
|
203
|
+
if (prompt === undefined) {
|
|
204
|
+
console.error(chalk.red('A prompt is required for host runs: agents run <agent> "<task>" --host <name>'));
|
|
205
|
+
process.exit(1);
|
|
206
|
+
}
|
|
207
|
+
const { resolveHost, resolveHostByCap } = await import('../lib/hosts/registry.js');
|
|
208
|
+
const { dispatchToHost } = await import('../lib/hosts/dispatch.js');
|
|
209
|
+
let host = await resolveHost(hostName);
|
|
210
|
+
if (!host) {
|
|
211
|
+
// Not a host name — try capability routing (e.g. --host gpu). A
|
|
212
|
+
// "Multiple hosts tagged…" error is actionable and must surface;
|
|
213
|
+
// only "no host tagged" falls through to the generic unknown-host msg.
|
|
214
|
+
try {
|
|
215
|
+
host = await resolveHostByCap(hostName, options.any);
|
|
216
|
+
}
|
|
217
|
+
catch (e) {
|
|
218
|
+
const msg = e.message ?? '';
|
|
219
|
+
if (msg.startsWith('Multiple hosts')) {
|
|
220
|
+
console.error(chalk.red(msg));
|
|
221
|
+
process.exit(1);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (!host) {
|
|
226
|
+
console.error(chalk.red(`Unknown host "${hostName}". List hosts: agents hosts list`));
|
|
227
|
+
process.exit(1);
|
|
228
|
+
}
|
|
229
|
+
try {
|
|
230
|
+
const { exitCode } = await dispatchToHost(host, {
|
|
231
|
+
agent: agentSpec.split('@')[0],
|
|
232
|
+
prompt,
|
|
233
|
+
mode: options.mode,
|
|
234
|
+
model: options.model,
|
|
235
|
+
remoteCwd: options.remoteCwd,
|
|
236
|
+
follow: options.follow !== false,
|
|
237
|
+
});
|
|
238
|
+
if (options.follow === false) {
|
|
239
|
+
console.log(chalk.green(`Dispatched to ${host.name}.`) + chalk.gray(' Track: agents hosts ps · Follow: agents hosts logs <id> -f'));
|
|
240
|
+
process.exit(0);
|
|
241
|
+
}
|
|
242
|
+
process.exit(exitCode === undefined || exitCode === -1 ? 1 : exitCode);
|
|
243
|
+
}
|
|
244
|
+
catch (err) {
|
|
245
|
+
console.error(chalk.red(err.message));
|
|
246
|
+
process.exit(1);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
185
249
|
// --resume-checkpoint short-circuits normal dispatch entirely: the
|
|
186
250
|
// checkpoint already carries the agent, version, prompt, session id,
|
|
187
251
|
// iteration, and loop config of the killed run. Reconstruct ExecOptions
|
|
@@ -260,7 +324,7 @@ export function registerRunCommand(program) {
|
|
|
260
324
|
process.stderr.write(chalk.gray(`[loop] stopped: ${result.stoppedBy} after ${result.iterations} iteration(s), ${result.tokens} tokens\n`));
|
|
261
325
|
process.exit(loopExitCode(result.stoppedBy));
|
|
262
326
|
}
|
|
263
|
-
const [{ buildExecCommand, parseExecEnv, execAgent, runWithFallback, normalizeMode, resolveMode, defaultModeFor, headlessPlanStallCommand }, { ALL_AGENT_IDS }, { profileExists, resolveProfileForRun }, { readAndResolveBundleEnv, describeBundle }, { getConfiguredRunStrategy, normalizeRunStrategy, resolveRunVersion, RUN_STRATEGIES }, { getGlobalDefault, getVersionHomePath, resolveVersion, resolveVersionAlias }, { buildDiscoveredPlugin, loadPluginManifest, syncPluginToVersion }, { parseWorkflowFrontmatter, resolveWorkflowRef, resolveAllowedSubagents }, { resolveRunDefaults }, { getMcpServersByName, buildWorkflowMcpConfig }, { supports },] = await Promise.all([
|
|
327
|
+
const [{ buildExecCommand, parseExecEnv, execAgent, runWithFallback, normalizeMode, resolveMode, defaultModeFor, headlessPlanStallCommand, nativeResume, resolveInteractive }, { ALL_AGENT_IDS }, { profileExists, resolveProfileForRun }, { readAndResolveBundleEnv, describeBundle }, { getConfiguredRunStrategy, normalizeRunStrategy, resolveRunVersion, RUN_STRATEGIES }, { getGlobalDefault, getVersionHomePath, resolveVersion, resolveVersionAlias }, { buildDiscoveredPlugin, loadPluginManifest, syncPluginToVersion }, { parseWorkflowFrontmatter, resolveWorkflowRef, resolveAllowedSubagents }, { resolveRunDefaults }, { getMcpServersByName, buildWorkflowMcpConfig }, { supports },] = await Promise.all([
|
|
264
328
|
import('../lib/exec.js'),
|
|
265
329
|
import('../lib/agents.js'),
|
|
266
330
|
import('../lib/profiles.js'),
|
|
@@ -489,6 +553,112 @@ export function registerRunCommand(program) {
|
|
|
489
553
|
}
|
|
490
554
|
}
|
|
491
555
|
version = resolveVersionAlias(agent, version);
|
|
556
|
+
// --resume: resolve a prior conversation and rewrite the run target to
|
|
557
|
+
// continue it. `version` here is already the alias-resolved candidate-version
|
|
558
|
+
// FILTER (undefined for default/any, concrete for @latest/@oldest/@x.y.z);
|
|
559
|
+
// it is replaced below by the chosen session's OWN version (isolation).
|
|
560
|
+
let resumeNative = false;
|
|
561
|
+
let resumeSessionId;
|
|
562
|
+
let forceInteractive = false;
|
|
563
|
+
if (options.resume !== undefined) {
|
|
564
|
+
if (options.sessionId) {
|
|
565
|
+
console.error(chalk.red('--resume and --session-id are mutually exclusive. --session-id CREATES a session with a fixed id; --resume continues an existing one.'));
|
|
566
|
+
process.exit(1);
|
|
567
|
+
}
|
|
568
|
+
if (options.loop || options.fallback || options.resumeCheckpoint) {
|
|
569
|
+
console.error(chalk.red('--resume cannot be combined with --loop, --fallback, or --resume-checkpoint (those are separate continuation mechanisms).'));
|
|
570
|
+
process.exit(1);
|
|
571
|
+
}
|
|
572
|
+
const { findSessionsById } = await import('../lib/session/db.js');
|
|
573
|
+
const { discoverSessions } = await import('../lib/session/discover.js');
|
|
574
|
+
const { pickSessionInteractive } = await import('./sessions.js');
|
|
575
|
+
const { buildContinuePrompt } = await import('../lib/loop.js');
|
|
576
|
+
// Freshen the index for this agent before any lookup (incremental, cached).
|
|
577
|
+
// AgentId is wider than SessionAgentId (cursor/amp/… keep no transcripts);
|
|
578
|
+
// those simply yield no matches and fall through to the not-found error.
|
|
579
|
+
const sessionAgent = agent;
|
|
580
|
+
await discoverSessions({ agent: sessionAgent, version });
|
|
581
|
+
// Resume is interactive unless a follow-on prompt makes it headless.
|
|
582
|
+
const wantsInteractive = resolveInteractive({ interactive: options.interactive, headless: options.headless, prompt });
|
|
583
|
+
const idArg = typeof options.resume === 'string' ? options.resume.trim() : '';
|
|
584
|
+
let scopeCwd;
|
|
585
|
+
try {
|
|
586
|
+
scopeCwd = fs.realpathSync(cwd);
|
|
587
|
+
}
|
|
588
|
+
catch {
|
|
589
|
+
scopeCwd = cwd;
|
|
590
|
+
}
|
|
591
|
+
let session;
|
|
592
|
+
if (idArg) {
|
|
593
|
+
let matches = findSessionsById(idArg, { agent: sessionAgent, version, cwd: scopeCwd });
|
|
594
|
+
if (matches.length === 0) {
|
|
595
|
+
const wide = findSessionsById(idArg, { agent: sessionAgent, version });
|
|
596
|
+
if (wide.length > 0) {
|
|
597
|
+
if (!options.quiet)
|
|
598
|
+
process.stderr.write(chalk.gray(`No match for "${idArg}" in this project; widened to all projects.\n`));
|
|
599
|
+
matches = wide;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
if (matches.length === 0) {
|
|
603
|
+
console.error(chalk.red(`No ${agent} session matching "${idArg}".`));
|
|
604
|
+
console.error(chalk.gray(`Browse sessions: agents sessions ${idArg}`));
|
|
605
|
+
process.exit(1);
|
|
606
|
+
}
|
|
607
|
+
else if (matches.length === 1) {
|
|
608
|
+
session = matches[0];
|
|
609
|
+
}
|
|
610
|
+
else if (wantsInteractive) {
|
|
611
|
+
const picked = await pickSessionInteractive(matches, `Multiple sessions match "${idArg}":`);
|
|
612
|
+
if (!picked)
|
|
613
|
+
process.exit(0);
|
|
614
|
+
session = picked.session;
|
|
615
|
+
}
|
|
616
|
+
else {
|
|
617
|
+
console.error(chalk.red(`"${idArg}" is ambiguous — ${matches.length} sessions match:`));
|
|
618
|
+
for (const m of matches.slice(0, 10)) {
|
|
619
|
+
console.error(chalk.gray(` ${m.shortId} ${m.timestamp.slice(0, 16).replace('T', ' ')} ${m.topic ?? m.label ?? ''}`));
|
|
620
|
+
}
|
|
621
|
+
console.error(chalk.gray('Pass more of the id, or resume interactively (drop the prompt).'));
|
|
622
|
+
process.exit(1);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
else {
|
|
626
|
+
// Bare --resume: pick from recent sessions in scope. Needs a TTY.
|
|
627
|
+
if (!wantsInteractive) {
|
|
628
|
+
console.error(chalk.red('--resume with no id needs an interactive terminal. Pass a session id (full or prefix), or run without --headless.'));
|
|
629
|
+
process.exit(1);
|
|
630
|
+
}
|
|
631
|
+
const recent = await discoverSessions({ agent: sessionAgent, version, limit: 200 });
|
|
632
|
+
if (recent.length === 0) {
|
|
633
|
+
console.error(chalk.red(`No ${agent} sessions found to resume in this project.`));
|
|
634
|
+
console.error(chalk.gray('Browse all: agents sessions'));
|
|
635
|
+
process.exit(1);
|
|
636
|
+
}
|
|
637
|
+
const picked = await pickSessionInteractive(recent, `Resume which ${agent} session?`);
|
|
638
|
+
if (!picked)
|
|
639
|
+
process.exit(0);
|
|
640
|
+
session = picked.session;
|
|
641
|
+
forceInteractive = true; // bare resume always lands in the agent's TUI
|
|
642
|
+
}
|
|
643
|
+
// Pin to the chosen session's own version (the isolated HOME the transcript
|
|
644
|
+
// lives in) and route by tier.
|
|
645
|
+
version = session.version;
|
|
646
|
+
if (nativeResume(agent)) {
|
|
647
|
+
resumeNative = true;
|
|
648
|
+
resumeSessionId = session.id;
|
|
649
|
+
if (!options.quiet)
|
|
650
|
+
process.stderr.write(chalk.gray(`Resuming ${agent} ${session.shortId} (native)${version ? ` @${version}` : ''}\n`));
|
|
651
|
+
}
|
|
652
|
+
else {
|
|
653
|
+
// Tier-2: launch fresh with a /continue <id> first message; the agent
|
|
654
|
+
// loads the transcript via `agents sessions <id>` and picks up.
|
|
655
|
+
prompt = buildContinuePrompt(session.id, prompt);
|
|
656
|
+
if (prompt.trim() === `/continue ${session.id}`)
|
|
657
|
+
forceInteractive = true;
|
|
658
|
+
if (!options.quiet)
|
|
659
|
+
process.stderr.write(chalk.gray(`Resuming ${agent} ${session.shortId} (/continue replay)${version ? ` @${version}` : ''}\n`));
|
|
660
|
+
}
|
|
661
|
+
}
|
|
492
662
|
const configuredStrategy = getConfiguredRunStrategy(agent, cwd);
|
|
493
663
|
const explicitStrategy = options.strategy ? normalizeRunStrategy(options.strategy) : null;
|
|
494
664
|
if (options.strategy && !explicitStrategy) {
|
|
@@ -639,7 +809,7 @@ export function registerRunCommand(program) {
|
|
|
639
809
|
agent,
|
|
640
810
|
version,
|
|
641
811
|
prompt,
|
|
642
|
-
interactive: options.interactive,
|
|
812
|
+
interactive: options.interactive || forceInteractive,
|
|
643
813
|
mode,
|
|
644
814
|
effort,
|
|
645
815
|
cwd: options.cwd,
|
|
@@ -647,7 +817,8 @@ export function registerRunCommand(program) {
|
|
|
647
817
|
addDirs: options.addDir,
|
|
648
818
|
json: options.json,
|
|
649
819
|
headless: options.headless,
|
|
650
|
-
sessionId: options.sessionId,
|
|
820
|
+
sessionId: resumeSessionId ?? options.sessionId,
|
|
821
|
+
resume: resumeNative,
|
|
651
822
|
verbose: options.verbose,
|
|
652
823
|
timeout: options.timeout,
|
|
653
824
|
env,
|