@phnx-labs/agents-cli 1.20.25 → 1.20.27
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 +37 -0
- package/dist/commands/doctor.d.ts +5 -2
- package/dist/commands/doctor.js +126 -27
- package/dist/commands/inspect.d.ts +2 -1
- package/dist/commands/inspect.js +1 -1
- package/dist/commands/menubar.js +6 -1
- package/dist/commands/repo.js +40 -0
- package/dist/commands/secrets.js +16 -12
- package/dist/commands/sessions.js +20 -1
- package/dist/index.js +2 -12
- package/dist/lib/agent-spec.d.ts +36 -0
- package/dist/lib/agent-spec.js +157 -0
- package/dist/lib/agents.js +1 -0
- package/dist/lib/daemon.js +32 -0
- package/dist/lib/doctor-diff.d.ts +7 -0
- package/dist/lib/doctor-diff.js +18 -13
- package/dist/lib/fs-atomic.d.ts +3 -2
- package/dist/lib/fs-atomic.js +22 -7
- package/dist/lib/heal.d.ts +107 -0
- package/dist/lib/heal.js +279 -0
- package/dist/lib/hooks.js +36 -1
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/install-menubar.d.ts +27 -3
- package/dist/lib/menubar/install-menubar.js +74 -9
- package/dist/lib/plugin-marketplace.d.ts +18 -0
- package/dist/lib/plugin-marketplace.js +67 -1
- package/dist/lib/plugins.d.ts +23 -1
- package/dist/lib/plugins.js +55 -10
- package/dist/lib/resources/rules.d.ts +5 -2
- package/dist/lib/resources/rules.js +39 -10
- package/dist/lib/rules/compose.d.ts +22 -0
- package/dist/lib/rules/compose.js +114 -9
- package/dist/lib/secrets/agent.d.ts +4 -2
- package/dist/lib/secrets/agent.js +6 -4
- package/dist/lib/secrets/bundles.d.ts +20 -14
- package/dist/lib/secrets/bundles.js +31 -10
- package/dist/lib/session/remote.d.ts +33 -0
- package/dist/lib/session/remote.js +114 -0
- package/dist/lib/staleness/checkers/rules.js +13 -1
- package/dist/lib/staleness/detectors/commands.js +7 -6
- package/dist/lib/staleness/writers/commands.js +7 -12
- 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 +13 -2
- package/dist/lib/versions.d.ts +3 -1
- package/dist/lib/versions.js +35 -3
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,43 @@
|
|
|
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
|
+
|
|
14
|
+
**Secrets default policy is now `daily` (one Touch ID per ~24h), not `always`**
|
|
15
|
+
|
|
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.
|
|
17
|
+
- **Auto-cache is on by default.** The secrets-agent is the mechanism that delivers the daily policy, so it self-caches a `daily` bundle on first read with no `secrets.agent.auto: true` needed. Opt out with `secrets.agent.auto: false`.
|
|
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.
|
|
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.
|
|
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
|
+
|
|
29
|
+
**`agents repos view [name]`: inspect one repo's contents without opening it**
|
|
30
|
+
|
|
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`.
|
|
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
|
+
|
|
5
42
|
**Secrets prompt policy: human-readable `always` / `daily`, and `secrets list` now shows it**
|
|
6
43
|
|
|
7
44
|
- 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,25 @@ 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) ────────────────────────────────────────────────
|
|
16
17
|
function checkSyncStatus(cwd) {
|
|
17
18
|
const rows = [];
|
|
19
|
+
// Every installed version, not just the default — a stale NON-default version
|
|
20
|
+
// (e.g. one you launched from yesterday) is exactly the rot that silently
|
|
21
|
+
// serves outdated/invalid resources and that `--fix` now heals. Hiding it here
|
|
22
|
+
// is why that class of bug went unnoticed.
|
|
18
23
|
for (const agent of ALL_AGENT_IDS) {
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
const def = getGlobalDefault(agent);
|
|
25
|
+
for (const version of listInstalledVersions(agent)) {
|
|
26
|
+
const manifest = loadManifest(agent, version);
|
|
27
|
+
const status = !manifest
|
|
28
|
+
? 'never-synced'
|
|
29
|
+
: isStale(manifest, agent, version, cwd) ? 'stale' : 'fresh';
|
|
30
|
+
rows.push({ agent, version, status, isDefault: version === def });
|
|
26
31
|
}
|
|
27
|
-
const stale = isStale(manifest, agent, version, cwd);
|
|
28
|
-
rows.push({ agent, version, status: stale ? 'stale' : 'fresh' });
|
|
29
32
|
}
|
|
30
33
|
return rows;
|
|
31
34
|
}
|
|
@@ -41,15 +44,11 @@ function countOrphans() {
|
|
|
41
44
|
return row;
|
|
42
45
|
};
|
|
43
46
|
for (const { agent, version } of iterCommandsCapableVersions()) {
|
|
44
|
-
if (version !== getGlobalDefault(agent))
|
|
45
|
-
continue;
|
|
46
47
|
const diff = diffVersionCommands(agent, version);
|
|
47
48
|
if (diff.orphans.length > 0)
|
|
48
49
|
ensure(agent, version).commands = diff.orphans.length;
|
|
49
50
|
}
|
|
50
51
|
for (const { agent, version } of iterSkillsCapableVersions()) {
|
|
51
|
-
if (version !== getGlobalDefault(agent))
|
|
52
|
-
continue;
|
|
53
52
|
const diff = diffVersionSkills(agent, version);
|
|
54
53
|
if (diff.orphans.length > 0)
|
|
55
54
|
ensure(agent, version).skills = diff.orphans.length;
|
|
@@ -59,8 +58,6 @@ function countOrphans() {
|
|
|
59
58
|
// never fire. (Distinct from the source-diff `diffVersionHooks().orphans`,
|
|
60
59
|
// which false-flags valid system-sourced registered hooks.)
|
|
61
60
|
for (const { agent, version } of iterHooksCapableVersions()) {
|
|
62
|
-
if (version !== getGlobalDefault(agent))
|
|
63
|
-
continue;
|
|
64
61
|
const dead = listUnmanagedHooksInVersionHome(agent, version);
|
|
65
62
|
if (dead.length > 0)
|
|
66
63
|
ensure(agent, version).hooks = dead.length;
|
|
@@ -69,28 +66,40 @@ function countOrphans() {
|
|
|
69
66
|
}
|
|
70
67
|
function renderOverviewText(clis, syncRows, orphanRows, hostClis) {
|
|
71
68
|
console.log(chalk.bold('Agent CLIs'));
|
|
72
|
-
|
|
73
|
-
|
|
69
|
+
// Show the fleet you actually run — agents that are ready in PATH, plus any
|
|
70
|
+
// you MANAGE (have installed versions) whose binary isn't resolving (a real
|
|
71
|
+
// problem). The other supported-but-unadopted agents collapse to one hint line
|
|
72
|
+
// instead of a column of red "not installed" nags for tools you never wanted.
|
|
73
|
+
const managed = new Set(ALL_AGENT_IDS.filter((a) => listInstalledVersions(a).length > 0));
|
|
74
|
+
const entries = Object.entries(clis);
|
|
75
|
+
const shown = entries.filter(([name, e]) => e.installed || managed.has(name));
|
|
76
|
+
const hidden = entries.filter(([name, e]) => !e.installed && !managed.has(name)).map(([name]) => name);
|
|
77
|
+
if (shown.length === 0) {
|
|
78
|
+
console.log(chalk.gray(' (none installed — `agents add <name>` to start)'));
|
|
74
79
|
}
|
|
75
80
|
else {
|
|
76
|
-
for (const [name, entry] of
|
|
77
|
-
const pretty = AGENT_NAMES[name] || name;
|
|
81
|
+
for (const [name, entry] of shown) {
|
|
82
|
+
const pretty = (AGENT_NAMES[name] || name).padEnd(11);
|
|
78
83
|
if (entry.installed) {
|
|
79
|
-
console.log(` ${chalk.green('ready')} ${pretty
|
|
84
|
+
console.log(` ${chalk.green('ready')} ${pretty} ${chalk.gray(entry.path || '')}`);
|
|
80
85
|
}
|
|
81
86
|
else {
|
|
82
|
-
console.log(` ${chalk.red('no ')} ${pretty
|
|
87
|
+
console.log(` ${chalk.red('no ')} ${pretty} ${chalk.gray(entry.error || 'not installed')}`);
|
|
83
88
|
}
|
|
84
89
|
}
|
|
85
90
|
}
|
|
91
|
+
if (hidden.length > 0) {
|
|
92
|
+
console.log(chalk.gray(` +${hidden.length} more supported (${hidden.join(', ')}) — \`agents add <name>\` to manage`));
|
|
93
|
+
}
|
|
86
94
|
console.log();
|
|
87
|
-
console.log(chalk.bold('Sync status (
|
|
95
|
+
console.log(chalk.bold('Sync status (installed versions)'));
|
|
88
96
|
if (syncRows.length === 0) {
|
|
89
|
-
console.log(chalk.gray(' (no
|
|
97
|
+
console.log(chalk.gray(' (no versions installed; add one with `agents add <agent>@<version>`)'));
|
|
90
98
|
}
|
|
91
99
|
else {
|
|
92
100
|
for (const row of syncRows) {
|
|
93
|
-
const
|
|
101
|
+
const tag = row.isDefault ? chalk.gray(' (default)') : '';
|
|
102
|
+
const label = `${AGENT_NAMES[row.agent] || row.agent}@${row.version}${tag}`;
|
|
94
103
|
if (row.status === 'fresh') {
|
|
95
104
|
console.log(` ${chalk.green('fresh')} ${label}`);
|
|
96
105
|
}
|
|
@@ -103,7 +112,7 @@ function renderOverviewText(clis, syncRows, orphanRows, hostClis) {
|
|
|
103
112
|
}
|
|
104
113
|
}
|
|
105
114
|
console.log();
|
|
106
|
-
console.log(chalk.bold('Orphans (
|
|
115
|
+
console.log(chalk.bold('Orphans (installed versions)'));
|
|
107
116
|
if (orphanRows.length === 0) {
|
|
108
117
|
console.log(chalk.gray(' (none — version homes match central sources)'));
|
|
109
118
|
}
|
|
@@ -340,9 +349,77 @@ function renderTargetText(report, options) {
|
|
|
340
349
|
}
|
|
341
350
|
else {
|
|
342
351
|
console.log(` Verdict: ${verdictParts.join(', ')}.`);
|
|
343
|
-
console.log(chalk.gray(` Run \`agents
|
|
352
|
+
console.log(chalk.gray(` Run \`agents doctor ${report.agent}@${report.version} --fix\` to heal, or \`agents prune cleanup\` to drop extras.`));
|
|
344
353
|
}
|
|
345
354
|
}
|
|
355
|
+
// ─── fix / heal mode ───────────────────────────────────────────────────────────
|
|
356
|
+
function renderHealText(result) {
|
|
357
|
+
for (const r of result.repairedManifests) {
|
|
358
|
+
console.log(` ${chalk.green('repair')} plugin ${chalk.bold(r.plugin)} ${chalk.gray(`— dropped invalid ${r.droppedFields.join(', ')} field`)}`);
|
|
359
|
+
}
|
|
360
|
+
for (const r of result.refreshedPlugins) {
|
|
361
|
+
console.log(` ${chalk.green('refresh')} plugin ${chalk.bold(r.plugin)} ${chalk.gray(`${r.from} → ${r.to}`)}`);
|
|
362
|
+
}
|
|
363
|
+
for (const s of result.skippedPlugins) {
|
|
364
|
+
const why = s.reason === 'modified'
|
|
365
|
+
? `locally modified — left as-is (run \`agents plugins update ${s.plugin}\` to force)`
|
|
366
|
+
: `no baseline recorded — left as-is (run \`agents plugins update ${s.plugin}\` to adopt)`;
|
|
367
|
+
console.log(` ${chalk.yellow('hold ')} plugin ${chalk.bold(s.plugin)} ${chalk.gray(`${s.from} → ${s.upstream} available; ${why}`)}`);
|
|
368
|
+
}
|
|
369
|
+
for (const v of result.versions) {
|
|
370
|
+
const label = `${AGENT_NAMES[v.agent] || v.agent}@${v.version}`;
|
|
371
|
+
if (v.healed.length === 0 && v.skipped.length === 0)
|
|
372
|
+
continue;
|
|
373
|
+
const byKind = new Map();
|
|
374
|
+
for (const h of v.healed)
|
|
375
|
+
byKind.set(h.kind, (byKind.get(h.kind) ?? 0) + 1);
|
|
376
|
+
const parts = Array.from(byKind, ([k, n]) => `${n} ${k}`);
|
|
377
|
+
if (v.healed.length > 0) {
|
|
378
|
+
console.log(` ${chalk.green('fixed ')} ${label} ${chalk.gray(parts.join(', '))}`);
|
|
379
|
+
}
|
|
380
|
+
const drift = v.skipped.filter((s) => s.reason === 'drift');
|
|
381
|
+
const unres = v.skipped.filter((s) => s.reason === 'unreconcilable');
|
|
382
|
+
if (drift.length > 0) {
|
|
383
|
+
console.log(` ${chalk.yellow('drift ')} ${label} ${chalk.gray(`${drift.length} hand-edited — left as-is (use \`--diff\` to inspect)`)}`);
|
|
384
|
+
}
|
|
385
|
+
if (unres.length > 0) {
|
|
386
|
+
const names = unres.map((s) => `${s.kind}/${s.name}`).join(', ');
|
|
387
|
+
console.log(` ${chalk.yellow('hold ')} ${label} ${chalk.gray(`${unres.length} couldn't reconcile (${names}) — source/home mismatch the writer can't satisfy`)}`);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
console.log();
|
|
391
|
+
const healed = result.versions.reduce((n, v) => n + v.healed.length, 0);
|
|
392
|
+
const touchedVersions = result.versions.filter((v) => v.healed.length > 0).length;
|
|
393
|
+
if (!healChangedAnything(result)) {
|
|
394
|
+
console.log(chalk.green('✓ Everything in sync — nothing to heal.'));
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
397
|
+
const bits = [];
|
|
398
|
+
if (healed > 0)
|
|
399
|
+
bits.push(`${healed} resource${healed === 1 ? '' : 's'} across ${touchedVersions} version${touchedVersions === 1 ? '' : 's'}`);
|
|
400
|
+
if (result.repairedManifests.length > 0)
|
|
401
|
+
bits.push(`${result.repairedManifests.length} manifest${result.repairedManifests.length === 1 ? '' : 's'} repaired`);
|
|
402
|
+
if (result.refreshedPlugins.length > 0)
|
|
403
|
+
bits.push(`${result.refreshedPlugins.length} plugin${result.refreshedPlugins.length === 1 ? '' : 's'} refreshed`);
|
|
404
|
+
console.log(chalk.green(`✓ Healed ${bits.join(', ')}.`));
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
async function runFix(parsed, opts) {
|
|
408
|
+
// Heal targets the global install — project layer is irrelevant, so cwd is
|
|
409
|
+
// left to heal's neutral default rather than process.cwd().
|
|
410
|
+
if (!opts.json)
|
|
411
|
+
console.log(chalk.bold('Healing…'));
|
|
412
|
+
const result = await heal({
|
|
413
|
+
mode: 'full',
|
|
414
|
+
agent: parsed?.agent,
|
|
415
|
+
versions: parsed?.versions,
|
|
416
|
+
});
|
|
417
|
+
if (opts.json) {
|
|
418
|
+
console.log(JSON.stringify(result, null, 2));
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
renderHealText(result);
|
|
422
|
+
}
|
|
346
423
|
// ─── command registration ────────────────────────────────────────────────────
|
|
347
424
|
export function registerDoctorCommand(program) {
|
|
348
425
|
const doctorCmd = program
|
|
@@ -350,6 +427,7 @@ export function registerDoctorCommand(program) {
|
|
|
350
427
|
.description('Diagnose CLI availability, sync status, and resource divergence (optionally for a specific agent[@version]).')
|
|
351
428
|
.option('--json', 'Output machine-readable JSON')
|
|
352
429
|
.option('--diff', 'In target mode, include unified diffs for divergent files')
|
|
430
|
+
.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
431
|
.option('--kind <kinds>', 'Restrict to comma-separated resource kinds (commands,skills,hooks,rules,mcp,permissions,subagents,plugins,promptcuts)')
|
|
354
432
|
.option('--cwd <path>', 'Resolution cwd for project layer detection (default: process.cwd())');
|
|
355
433
|
setHelpSections(doctorCmd, {
|
|
@@ -368,10 +446,31 @@ export function registerDoctorCommand(program) {
|
|
|
368
446
|
|
|
369
447
|
# Inspect only rules and hooks, with full diffs
|
|
370
448
|
agents doctor claude@default --kind rules,hooks --diff
|
|
449
|
+
|
|
450
|
+
# Heal every gap across all installed versions
|
|
451
|
+
agents doctor --fix
|
|
452
|
+
|
|
453
|
+
# Heal just one agent (all its installed versions)
|
|
454
|
+
agents doctor claude --fix
|
|
371
455
|
`,
|
|
372
456
|
});
|
|
373
|
-
doctorCmd.action((target, opts) => {
|
|
457
|
+
doctorCmd.action(async (target, opts) => {
|
|
374
458
|
const cwd = opts.cwd ? opts.cwd : process.cwd();
|
|
459
|
+
// --fix turns the read-only diagnosis into a heal. With no target it heals
|
|
460
|
+
// every installed version; with a target it scopes to that agent.
|
|
461
|
+
if (opts.fix) {
|
|
462
|
+
let scope = null;
|
|
463
|
+
if (target) {
|
|
464
|
+
const parsed = parseTargetArg(target);
|
|
465
|
+
if ('error' in parsed) {
|
|
466
|
+
console.error(chalk.red(parsed.error));
|
|
467
|
+
process.exit(1);
|
|
468
|
+
}
|
|
469
|
+
scope = parsed;
|
|
470
|
+
}
|
|
471
|
+
await runFix(scope, opts);
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
375
474
|
if (!target) {
|
|
376
475
|
const clis = checkAllClis();
|
|
377
476
|
const syncRows = checkSyncStatus(cwd);
|
|
@@ -32,7 +32,7 @@ interface ResourceItem {
|
|
|
32
32
|
/** For plugins: the resource categories (skills, commands, …) the bundle packages. */
|
|
33
33
|
groups?: PluginResourceGroup[];
|
|
34
34
|
}
|
|
35
|
-
interface InspectOptions {
|
|
35
|
+
export interface InspectOptions {
|
|
36
36
|
brief?: boolean;
|
|
37
37
|
json?: boolean;
|
|
38
38
|
commands?: boolean | string;
|
|
@@ -66,6 +66,7 @@ export interface RepoTarget {
|
|
|
66
66
|
* Returns null when the target is none of these.
|
|
67
67
|
*/
|
|
68
68
|
export declare function resolveRepoTarget(target: string, cwd?: string): RepoTarget | null;
|
|
69
|
+
export declare function inspectRepo(repo: RepoTarget, options: InspectOptions): Promise<void>;
|
|
69
70
|
/** List one resource kind from a single repo root — no layering, no overrides. */
|
|
70
71
|
export declare function collectRepoKind(repo: RepoTarget, kind: DrillableKind): ResourceItem[];
|
|
71
72
|
/** Recursive size + file count of a path; symlinks are not followed. */
|
package/dist/commands/inspect.js
CHANGED
|
@@ -227,7 +227,7 @@ function isDotAgentsRoot(dir) {
|
|
|
227
227
|
}
|
|
228
228
|
return false;
|
|
229
229
|
}
|
|
230
|
-
async function inspectRepo(repo, options) {
|
|
230
|
+
export async function inspectRepo(repo, options) {
|
|
231
231
|
const drill = pickDrillKind(options);
|
|
232
232
|
const jsonHead = { repo: repo.label, root: repo.root };
|
|
233
233
|
if (drill) {
|
package/dist/commands/menubar.js
CHANGED
|
@@ -61,9 +61,14 @@ export function registerMenubarCommands(program) {
|
|
|
61
61
|
console.log(` running ${yn(s.running)}`);
|
|
62
62
|
console.log(` service installed ${yn(s.serviceInstalled)}`);
|
|
63
63
|
console.log(` app installed ${s.installedApp ? chalk.gray(s.installedApp) : chalk.gray('no')}`);
|
|
64
|
+
console.log(` installed version ${s.installedVersion ? chalk.gray(s.installedVersion) : chalk.gray('unknown')}`);
|
|
65
|
+
console.log(` current version ${chalk.gray(s.currentVersion)}`);
|
|
64
66
|
console.log(` bundle source ${s.source ? chalk.gray(s.source) : chalk.red('missing (cannot enable)')}`);
|
|
65
67
|
console.log(` disabled by user ${yn(s.disabledByUser)}`);
|
|
66
|
-
if (
|
|
68
|
+
if (s.stale) {
|
|
69
|
+
console.log(chalk.yellow('\n Installed helper is stale — runs on next `agents` startup, or `agents menubar enable` now.'));
|
|
70
|
+
}
|
|
71
|
+
else if (!s.serviceInstalled && !s.disabledByUser) {
|
|
67
72
|
console.log(chalk.gray('\n Enable it with `agents menubar enable`.'));
|
|
68
73
|
}
|
|
69
74
|
});
|
package/dist/commands/repo.js
CHANGED
|
@@ -7,6 +7,8 @@ import simpleGit from 'simple-git';
|
|
|
7
7
|
import { confirm, input } from '@inquirer/prompts';
|
|
8
8
|
import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
9
9
|
import { setHelpSections } from '../lib/help.js';
|
|
10
|
+
import { itemPicker } from '../lib/picker.js';
|
|
11
|
+
import { inspectRepo, resolveRepoTarget, } from './inspect.js';
|
|
10
12
|
const HOME = os.homedir();
|
|
11
13
|
/**
|
|
12
14
|
* Resolve a target argument to an absolute path.
|
|
@@ -162,6 +164,7 @@ async function listRepos(alias) {
|
|
|
162
164
|
export function registerRepoCommands(program) {
|
|
163
165
|
const repoCmd = program
|
|
164
166
|
.command('repo')
|
|
167
|
+
.alias('repos')
|
|
165
168
|
.description('Manage extra DotAgent repos alongside ~/.agents/ (for private or team skills).');
|
|
166
169
|
setHelpSections(repoCmd, {
|
|
167
170
|
examples: `
|
|
@@ -180,6 +183,9 @@ export function registerRepoCommands(program) {
|
|
|
180
183
|
# See what's registered
|
|
181
184
|
agents repo list
|
|
182
185
|
|
|
186
|
+
# View one repo's contents (git state + resource counts); omit the name for a picker
|
|
187
|
+
agents repos view system
|
|
188
|
+
|
|
183
189
|
# Temporarily disable without deleting
|
|
184
190
|
agents repo disable acme
|
|
185
191
|
`,
|
|
@@ -356,6 +362,40 @@ export function registerRepoCommands(program) {
|
|
|
356
362
|
.action(async (alias) => {
|
|
357
363
|
await listRepos(alias);
|
|
358
364
|
});
|
|
365
|
+
repoCmd
|
|
366
|
+
.command('view [name]')
|
|
367
|
+
.description("Show one repo's contents: git state and per-kind resource counts. Omit the name for an interactive picker.")
|
|
368
|
+
.option('--brief', 'header + git only; skip resource counts')
|
|
369
|
+
.option('--json', 'machine-readable JSON output')
|
|
370
|
+
.action(async (name, options) => {
|
|
371
|
+
if (name) {
|
|
372
|
+
const repo = resolveRepoTarget(name);
|
|
373
|
+
if (!repo) {
|
|
374
|
+
console.log(chalk.red(`Unknown repo "${name}". Use "system", "user", "project", or a registered extra alias.`));
|
|
375
|
+
process.exitCode = 1;
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
await inspectRepo(repo, options);
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
const targets = collectRepoTargets(undefined) || [];
|
|
382
|
+
if (!isInteractiveTerminal()) {
|
|
383
|
+
console.log(chalk.red('No repo name given and not an interactive terminal.'));
|
|
384
|
+
console.log(chalk.gray('Pass a name (e.g. `agents repos view system`) or run in a TTY for the picker.'));
|
|
385
|
+
process.exitCode = 1;
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
const picked = await itemPicker({
|
|
389
|
+
message: 'Select a repo to view',
|
|
390
|
+
items: targets,
|
|
391
|
+
filter: (q) => targets.filter((t) => t.alias.toLowerCase().includes(q.toLowerCase())),
|
|
392
|
+
labelFor: (t) => `${chalk.cyan(t.alias.padEnd(10))} ${chalk.gray(t.dir)}`,
|
|
393
|
+
});
|
|
394
|
+
if (!picked)
|
|
395
|
+
return;
|
|
396
|
+
const repo = { label: picked.item.alias, root: picked.item.dir };
|
|
397
|
+
await inspectRepo(repo, options);
|
|
398
|
+
});
|
|
359
399
|
repoCmd
|
|
360
400
|
.command('remove <alias>')
|
|
361
401
|
.alias('rm')
|
package/dist/commands/secrets.js
CHANGED
|
@@ -424,14 +424,15 @@ export function registerSecretsCommands(program) {
|
|
|
424
424
|
never touch disk in plaintext. Every item is device-local and gated by Touch ID
|
|
425
425
|
or device passcode; cross-machine sync is handled by 'agents secrets push/pull'.
|
|
426
426
|
|
|
427
|
-
Touch ID noise: macOS pops a prompt per bundle per process
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
always (default) ask for Touch ID every time — never auto-held.
|
|
431
|
-
daily ask once, then hold it silently in the local agent up
|
|
427
|
+
Touch ID noise: macOS pops a prompt per bundle per process. Each bundle has
|
|
428
|
+
a prompt policy, shown in the POLICY column of 'agents secrets list':
|
|
429
|
+
daily (default) ask once, then hold it silently in the local agent up
|
|
432
430
|
to ~24h, until screen-lock / sleep / logout or 'lock'.
|
|
433
|
-
|
|
434
|
-
|
|
431
|
+
always ask for Touch ID every time — never auto-held.
|
|
432
|
+
The default is 'daily' (one Touch ID per ~24h); change it globally with
|
|
433
|
+
'secrets.policy' in agents.yaml, or per bundle with 'agents secrets policy
|
|
434
|
+
<bundle> always'. 'agents secrets unlock <bundle>' holds any bundle after one
|
|
435
|
+
prompt regardless of policy. Nothing on disk.
|
|
435
436
|
|
|
436
437
|
See also:
|
|
437
438
|
agents secrets policy <bundle> daily ask once a day, not every run
|
|
@@ -623,7 +624,7 @@ export function registerSecretsCommands(program) {
|
|
|
623
624
|
.description('Create an empty bundle')
|
|
624
625
|
.option('--description <text>', 'Free-form description')
|
|
625
626
|
.option('--allow-exec', 'Allow exec: refs in this bundle (off by default)')
|
|
626
|
-
.option('--policy <policy>', 'prompt policy:
|
|
627
|
+
.option('--policy <policy>', 'prompt policy: daily (default, ask once a day) or always (ask every time)')
|
|
627
628
|
.addOption(new Option('--tier <policy>', 'deprecated alias for --policy').hideHelp())
|
|
628
629
|
.option('--backend <backend>', 'storage backend: keychain (default) or file (passphrase-encrypted, headless-readable)', 'keychain')
|
|
629
630
|
.option('--force', 'Overwrite an existing bundle')
|
|
@@ -631,7 +632,10 @@ export function registerSecretsCommands(program) {
|
|
|
631
632
|
try {
|
|
632
633
|
const resolvedName = name ?? (await promptBundleName());
|
|
633
634
|
validateBundleName(resolvedName);
|
|
634
|
-
|
|
635
|
+
// Leave policy unset unless the user explicitly chose one, so the bundle
|
|
636
|
+
// inherits the configured default (`daily`) instead of being pinned.
|
|
637
|
+
const policyOpt = opts.policy ?? opts.tier;
|
|
638
|
+
const policy = policyOpt ? parsePolicyOpt(policyOpt) : undefined;
|
|
635
639
|
const backend = parseBackendOpt(opts.backend);
|
|
636
640
|
if (bundleExists(resolvedName) && !opts.force) {
|
|
637
641
|
console.error(chalk.red(`Bundle '${resolvedName}' already exists. Use --force to overwrite.`));
|
|
@@ -647,7 +651,7 @@ export function registerSecretsCommands(program) {
|
|
|
647
651
|
};
|
|
648
652
|
writeBundle(bundle);
|
|
649
653
|
const tags = [
|
|
650
|
-
|
|
654
|
+
bundlePolicy(bundle) === 'daily' ? 'policy: daily' : 'policy: always ask',
|
|
651
655
|
backend === 'file' ? 'backend: file' : null,
|
|
652
656
|
].filter(Boolean);
|
|
653
657
|
console.log(chalk.green(`Bundle '${resolvedName}' created (${tags.join(', ')}).`));
|
|
@@ -1430,7 +1434,7 @@ Examples:
|
|
|
1430
1434
|
cmd
|
|
1431
1435
|
.command('policy <bundle> [policy]')
|
|
1432
1436
|
.alias('tier')
|
|
1433
|
-
.description("Show or set a bundle's prompt policy:
|
|
1437
|
+
.description("Show or set a bundle's prompt policy: daily (default, ask once a day) or always (ask every time).")
|
|
1434
1438
|
.action((bundleName, policyArg) => {
|
|
1435
1439
|
try {
|
|
1436
1440
|
const bundle = readBundle(bundleName);
|
|
@@ -1443,7 +1447,7 @@ Examples:
|
|
|
1443
1447
|
writeBundle(bundle);
|
|
1444
1448
|
console.log(chalk.green(`${bundle.name} policy set to ${next}.`));
|
|
1445
1449
|
if (next === 'daily') {
|
|
1446
|
-
console.log(chalk.gray('Held by the secrets-agent after one unlock
|
|
1450
|
+
console.log(chalk.gray('Held by the secrets-agent for ~24h after one unlock (auto-cache is on by default; disable with `secrets.agent.auto: false` in agents.yaml).'));
|
|
1447
1451
|
}
|
|
1448
1452
|
else {
|
|
1449
1453
|
console.log(chalk.gray('Asks for Touch ID every time — never auto-held.'));
|
|
@@ -20,6 +20,7 @@ import { getActiveSessions } from '../lib/session/active.js';
|
|
|
20
20
|
import { discoverSessions, countSessionsInScope, resolveSessionById, searchContentIndex } from '../lib/session/discover.js';
|
|
21
21
|
import { filterTeamSessions } from '../lib/session/team-filter.js';
|
|
22
22
|
import { parseSession } from '../lib/session/parse.js';
|
|
23
|
+
import { runRemoteSessions } from '../lib/session/remote.js';
|
|
23
24
|
import { renderConversationMarkdown, renderSummary, renderSummaryHeader, computeSummaryStats, renderJson, filterEvents, parseRoleList } from '../lib/session/render.js';
|
|
24
25
|
import { renderMarkdown } from '../lib/markdown.js';
|
|
25
26
|
import { colorAgent, resolveAgentName } from '../lib/agents.js';
|
|
@@ -313,6 +314,16 @@ async function renderActiveSessions(asJson) {
|
|
|
313
314
|
}
|
|
314
315
|
/** Main action handler for `agents sessions`. Routes to picker, table, or single-session render. */
|
|
315
316
|
async function sessionsAction(query, options) {
|
|
317
|
+
if (options.host && options.host.length > 0) {
|
|
318
|
+
try {
|
|
319
|
+
runRemoteSessions(options.host);
|
|
320
|
+
}
|
|
321
|
+
catch (err) {
|
|
322
|
+
console.error(chalk.red(err.message));
|
|
323
|
+
process.exit(1);
|
|
324
|
+
}
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
316
327
|
if (options.active) {
|
|
317
328
|
await renderActiveSessions(options.json === true);
|
|
318
329
|
return;
|
|
@@ -1116,7 +1127,8 @@ export function registerSessionsCommands(program) {
|
|
|
1116
1127
|
.option('--artifacts', 'List all files written or edited during a session')
|
|
1117
1128
|
.option('--artifact <name>', 'Read a specific artifact by filename or path (outputs to stdout)')
|
|
1118
1129
|
.option('--active', 'Show only sessions running right now across terminals, teams, cloud, and headless agents')
|
|
1119
|
-
.option('--cloud', 'Source sessions from Rush Cloud (captured runs) instead of local disk')
|
|
1130
|
+
.option('--cloud', 'Source sessions from Rush Cloud (captured runs) instead of local disk')
|
|
1131
|
+
.option('-H, --host <target...>', 'Run this query on remote machine(s) over SSH (host alias or user@host; repeatable)');
|
|
1120
1132
|
setHelpSections(sessionsCmd, {
|
|
1121
1133
|
examples: `
|
|
1122
1134
|
# Search prior sessions in this project by topic, file path, or command
|
|
@@ -1136,8 +1148,15 @@ export function registerSessionsCommands(program) {
|
|
|
1136
1148
|
|
|
1137
1149
|
# Export for analysis
|
|
1138
1150
|
agents sessions --since 30d --limit 200 --json > sessions.json
|
|
1151
|
+
|
|
1152
|
+
# Search another machine's sessions live over SSH (no sync needed)
|
|
1153
|
+
agents sessions "auth bug" --last 3 --host yosemite-s1
|
|
1154
|
+
|
|
1155
|
+
# Fan the same query out across several machines
|
|
1156
|
+
agents sessions --all "deploy script" --host box-a --host box-b
|
|
1139
1157
|
`,
|
|
1140
1158
|
notes: `
|
|
1159
|
+
- --host runs the query on the remote's own index over SSH (host alias or user@host); repeat or pass several to fan out. SSH access is the only auth.
|
|
1141
1160
|
- --include and --exclude are mutually exclusive.
|
|
1142
1161
|
- --first and --last are mutually exclusive.
|
|
1143
1162
|
- A filter flag (--include/--exclude/--first/--last) without --markdown/--json defaults to --markdown output.
|
package/dist/index.js
CHANGED
|
@@ -11,6 +11,7 @@ import * as fs from 'fs';
|
|
|
11
11
|
import * as os from 'os';
|
|
12
12
|
import * as path from 'path';
|
|
13
13
|
import { fileURLToPath } from 'url';
|
|
14
|
+
import { detectDevBuild } from './lib/startup/dev-build.js';
|
|
14
15
|
// `ora`, `@inquirer/prompts`, `./commands/utils.js`, and the agents/versions/shims
|
|
15
16
|
// modules are imported dynamically at their use sites: they are needed only on
|
|
16
17
|
// interactive / update / shim-repair paths, never for fast commands like
|
|
@@ -37,18 +38,7 @@ import { NPM_PACKAGE_NAME, deriveGlobalPrefix, detectPackageManager, installPack
|
|
|
37
38
|
// must not scribble on the user's real ~/.agents/), and skip the update prompt
|
|
38
39
|
// (the "0.0.0-dev -> 1.x.y" message is misleading). Each individual env var
|
|
39
40
|
// can still be set explicitly to override (set to '0' to re-enable).
|
|
40
|
-
const IS_DEV_BUILD = (
|
|
41
|
-
if (VERSION.startsWith('0.0.0-dev'))
|
|
42
|
-
return true;
|
|
43
|
-
try {
|
|
44
|
-
const cliPath = process.argv[1] || '';
|
|
45
|
-
const repoRoot = path.dirname(path.dirname(cliPath));
|
|
46
|
-
return fs.existsSync(path.join(repoRoot, '.git'));
|
|
47
|
-
}
|
|
48
|
-
catch {
|
|
49
|
-
return false;
|
|
50
|
-
}
|
|
51
|
-
})();
|
|
41
|
+
const IS_DEV_BUILD = detectDevBuild(process.argv[1] || '', VERSION);
|
|
52
42
|
if (IS_DEV_BUILD) {
|
|
53
43
|
if (process.env.AGENTS_NO_AUTOPULL === undefined)
|
|
54
44
|
process.env.AGENTS_NO_AUTOPULL = '1';
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { AgentId } from './types.js';
|
|
2
|
+
export interface AgentTarget {
|
|
3
|
+
agent: AgentId;
|
|
4
|
+
/** Resolved exact version, or null when the agent has no installed versions yet. */
|
|
5
|
+
version: string | null;
|
|
6
|
+
}
|
|
7
|
+
/** Canonical qualifier set, in help/display order. `pinned` ≡ `default`. */
|
|
8
|
+
export declare const AGENT_QUALIFIERS: readonly ["latest", "oldest", "pinned", "default", "all"];
|
|
9
|
+
export type AgentQualifier = (typeof AGENT_QUALIFIERS)[number];
|
|
10
|
+
/** Shared `--help` epilog so every agent-spec command documents the same grammar. */
|
|
11
|
+
export declare const AGENT_SPEC_HELP: string;
|
|
12
|
+
export declare class AgentSpecError extends Error {
|
|
13
|
+
constructor(message: string);
|
|
14
|
+
}
|
|
15
|
+
export interface ResolveAgentTargetsOptions {
|
|
16
|
+
/** Project dir for resolving a bare spec's project pin. Defaults to process.cwd(). */
|
|
17
|
+
cwd?: string;
|
|
18
|
+
/** Restrict the agents a spec may name (e.g. only mcp-capable). Defaults to all. */
|
|
19
|
+
availableAgents?: readonly AgentId[];
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Resolve an agent spec (single or comma-list) into concrete installed targets.
|
|
23
|
+
* Domain = installed: `@latest`/`@oldest`/`@all` range over installed versions
|
|
24
|
+
* (`add`/`install` use a separate available-version path). Throws AgentSpecError
|
|
25
|
+
* on bad input — never calls process.exit, so it is safe on the hot path and in
|
|
26
|
+
* library contexts.
|
|
27
|
+
*/
|
|
28
|
+
export declare function resolveAgentTargets(spec: string, opts?: ResolveAgentTargetsOptions): AgentTarget[];
|
|
29
|
+
/**
|
|
30
|
+
* Convenience for single-target commands (`use`, `run`): resolve a spec that
|
|
31
|
+
* must name exactly one installed version. Rejects `@all` / multi-target specs.
|
|
32
|
+
*/
|
|
33
|
+
export declare function resolveSingleAgentTarget(spec: string, opts?: ResolveAgentTargetsOptions): {
|
|
34
|
+
agent: AgentId;
|
|
35
|
+
version: string;
|
|
36
|
+
};
|