@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.
Files changed (47) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/dist/commands/doctor.d.ts +5 -2
  3. package/dist/commands/doctor.js +126 -27
  4. package/dist/commands/inspect.d.ts +2 -1
  5. package/dist/commands/inspect.js +1 -1
  6. package/dist/commands/menubar.js +6 -1
  7. package/dist/commands/repo.js +40 -0
  8. package/dist/commands/secrets.js +16 -12
  9. package/dist/commands/sessions.js +20 -1
  10. package/dist/index.js +2 -12
  11. package/dist/lib/agent-spec.d.ts +36 -0
  12. package/dist/lib/agent-spec.js +157 -0
  13. package/dist/lib/agents.js +1 -0
  14. package/dist/lib/daemon.js +32 -0
  15. package/dist/lib/doctor-diff.d.ts +7 -0
  16. package/dist/lib/doctor-diff.js +18 -13
  17. package/dist/lib/fs-atomic.d.ts +3 -2
  18. package/dist/lib/fs-atomic.js +22 -7
  19. package/dist/lib/heal.d.ts +107 -0
  20. package/dist/lib/heal.js +279 -0
  21. package/dist/lib/hooks.js +36 -1
  22. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  23. package/dist/lib/menubar/install-menubar.d.ts +27 -3
  24. package/dist/lib/menubar/install-menubar.js +74 -9
  25. package/dist/lib/plugin-marketplace.d.ts +18 -0
  26. package/dist/lib/plugin-marketplace.js +67 -1
  27. package/dist/lib/plugins.d.ts +23 -1
  28. package/dist/lib/plugins.js +55 -10
  29. package/dist/lib/resources/rules.d.ts +5 -2
  30. package/dist/lib/resources/rules.js +39 -10
  31. package/dist/lib/rules/compose.d.ts +22 -0
  32. package/dist/lib/rules/compose.js +114 -9
  33. package/dist/lib/secrets/agent.d.ts +4 -2
  34. package/dist/lib/secrets/agent.js +6 -4
  35. package/dist/lib/secrets/bundles.d.ts +20 -14
  36. package/dist/lib/secrets/bundles.js +31 -10
  37. package/dist/lib/session/remote.d.ts +33 -0
  38. package/dist/lib/session/remote.js +114 -0
  39. package/dist/lib/staleness/checkers/rules.js +13 -1
  40. package/dist/lib/staleness/detectors/commands.js +7 -6
  41. package/dist/lib/staleness/writers/commands.js +7 -12
  42. package/dist/lib/startup/dev-build.d.ts +22 -0
  43. package/dist/lib/startup/dev-build.js +41 -0
  44. package/dist/lib/types.d.ts +13 -2
  45. package/dist/lib/versions.d.ts +3 -1
  46. package/dist/lib/versions.js +35 -3
  47. 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 never mutates state. Run `agents prune cleanup` to act on orphan
19
- * readouts, or just launch the agent to apply pending sync.
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;
@@ -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 version = getGlobalDefault(agent);
20
- if (!version)
21
- continue;
22
- const manifest = loadManifest(agent, version);
23
- if (!manifest) {
24
- rows.push({ agent, version, status: 'never-synced' });
25
- continue;
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
- if (Object.keys(clis).length === 0) {
73
- console.log(chalk.gray(' (no agents reported)'));
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 Object.entries(clis)) {
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.padEnd(10)} ${chalk.gray(entry.path || '')}`);
84
+ console.log(` ${chalk.green('ready')} ${pretty} ${chalk.gray(entry.path || '')}`);
80
85
  }
81
86
  else {
82
- console.log(` ${chalk.red('no ')} ${pretty.padEnd(10)} ${chalk.gray(entry.error || 'not installed')}`);
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 (default versions)'));
95
+ console.log(chalk.bold('Sync status (installed versions)'));
88
96
  if (syncRows.length === 0) {
89
- console.log(chalk.gray(' (no default versions set; pin one with `agents use <agent>@<version>`)'));
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 label = `${AGENT_NAMES[row.agent] || row.agent}@${row.version}`;
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 (default versions)'));
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 sync ${report.agent}@${report.version}\` to reconcile, or \`agents prune cleanup\` to drop extras.`));
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. */
@@ -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) {
@@ -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 (!s.serviceInstalled && !s.disabledByUser) {
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
  });
@@ -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')
@@ -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, so concurrent
428
- agents each re-prompt. Each bundle has a prompt policy, shown in the POLICY
429
- column of 'agents secrets list':
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
- Set it with 'agents secrets policy <bundle> daily'. 'agents secrets unlock
434
- <bundle>' holds any bundle after one prompt regardless of policy. Nothing on disk.
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: always (default, ask every time) or daily (ask once a day)')
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
- const policy = parsePolicyOpt(opts.policy ?? opts.tier);
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
- policy === 'daily' ? 'policy: daily' : 'policy: always ask',
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: always (default, ask every time) or daily (ask once a day).")
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: run `agents secrets unlock`, or enable auto-cache with `secrets.agent.auto: true` in agents.yaml.'));
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
+ };