@phnx-labs/agents-cli 1.20.12 → 1.20.13

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 (61) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +3 -0
  3. package/dist/commands/computer-actions.d.ts +3 -0
  4. package/dist/commands/computer-actions.js +16 -0
  5. package/dist/commands/exec.js +25 -4
  6. package/dist/commands/import.js +17 -6
  7. package/dist/commands/inspect.d.ts +11 -1
  8. package/dist/commands/inspect.js +53 -19
  9. package/dist/commands/mcp.js +3 -3
  10. package/dist/commands/plugins.d.ts +2 -0
  11. package/dist/commands/plugins.js +69 -26
  12. package/dist/commands/sync.js +1 -1
  13. package/dist/commands/teams.js +1 -0
  14. package/dist/commands/trash.d.ts +11 -0
  15. package/dist/commands/trash.js +57 -41
  16. package/dist/commands/versions.js +68 -20
  17. package/dist/commands/view.js +1 -12
  18. package/dist/commands/wallet.d.ts +14 -0
  19. package/dist/commands/wallet.js +199 -0
  20. package/dist/index.js +4 -1
  21. package/dist/lib/agents.js +70 -22
  22. package/dist/lib/browser/ipc.d.ts +7 -0
  23. package/dist/lib/browser/ipc.js +43 -27
  24. package/dist/lib/capabilities.js +7 -1
  25. package/dist/lib/command-skills.d.ts +1 -0
  26. package/dist/lib/command-skills.js +23 -7
  27. package/dist/lib/exec.d.ts +32 -1
  28. package/dist/lib/exec.js +79 -7
  29. package/dist/lib/hooks.js +37 -5
  30. package/dist/lib/mcp.js +33 -0
  31. package/dist/lib/models.js +5 -0
  32. package/dist/lib/picker.d.ts +2 -0
  33. package/dist/lib/picker.js +96 -6
  34. package/dist/lib/platform/index.d.ts +1 -0
  35. package/dist/lib/platform/index.js +1 -0
  36. package/dist/lib/platform/winpath.d.ts +35 -0
  37. package/dist/lib/platform/winpath.js +86 -0
  38. package/dist/lib/plugins.d.ts +14 -0
  39. package/dist/lib/plugins.js +23 -0
  40. package/dist/lib/project-launch.js +110 -5
  41. package/dist/lib/registry.js +15 -2
  42. package/dist/lib/runner.js +14 -0
  43. package/dist/lib/sandbox.js +5 -2
  44. package/dist/lib/settings-manifest.d.ts +39 -0
  45. package/dist/lib/settings-manifest.js +163 -0
  46. package/dist/lib/shims.d.ts +1 -1
  47. package/dist/lib/shims.js +16 -31
  48. package/dist/lib/staleness/detectors/subagents.js +16 -0
  49. package/dist/lib/staleness/writers/subagents.js +11 -3
  50. package/dist/lib/subagents.d.ts +9 -0
  51. package/dist/lib/subagents.js +33 -0
  52. package/dist/lib/teams/agents.js +1 -1
  53. package/dist/lib/teams/parsers.d.ts +1 -1
  54. package/dist/lib/teams/parsers.js +6 -0
  55. package/dist/lib/types.d.ts +1 -1
  56. package/dist/lib/versions.d.ts +15 -3
  57. package/dist/lib/versions.js +88 -19
  58. package/dist/lib/wallet/index.d.ts +78 -0
  59. package/dist/lib/wallet/index.js +253 -0
  60. package/package.json +3 -3
  61. package/scripts/postinstall.js +35 -7
package/CHANGELOG.md CHANGED
@@ -2,6 +2,19 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ **Windows: `agents` is discoverable right after `npm i -g`**
6
+
7
+ - On a global Windows install, postinstall now prepends npm's global-bin dir (where `agents.cmd`/`agents.ps1` live) to the **User PATH** via the .NET environment API. Node's installer normally adds it, but winget / portable / nvm-windows setups often don't — and then `npm i -g @phnx-labs/agents-cli` succeeds yet `agents` is "not recognized". The shims dir (claude/codex/…) is still left to `agents setup`, which the user can now run because `agents` resolves.
8
+ - Postinstall also detects a `Restricted`/`AllSigned` PowerShell execution policy (which blocks the generated `.ps1` launchers, so even an on-PATH `agents` fails in PowerShell) and prints the one-line fix (`Set-ExecutionPolicy -Scope CurrentUser RemoteSigned`). The policy is a security setting, so it is never changed silently — only surfaced.
9
+ - Refactor: the Windows User-PATH prepend logic moved from `shims.ts` into a new `src/lib/platform/winpath.ts` leaf module (`prependToWindowsUserPath`, `getEffectiveExecutionPolicy`, `blocksLocalScripts`, `npmGlobalBinFromEntry`); `addShimsToWindowsUserPath` now delegates to it. Pure helpers are unit-tested.
10
+
11
+ **Factory AI Droid (first-class support)**
12
+
13
+ - Add `droid` as a first-class supported agent (AgentId + full registry entry for Factory AI's `droid` CLI, config in `~/.factory/`). Installs via the official script (`curl -fsSL https://app.factory.ai/cli | sh`); the binary is resolved through the standard install-script path and isolated per version via the `~/.factory` config symlink (Droid has no `*_HOME` override).
14
+ - Resource sync wired for the four resource types Droid supports natively: **MCP** (`~/.factory/mcp.json`), **rules** (native `AGENTS.md`), **subagents** (custom droids flattened to `~/.factory/droids/*.md`, with the unsupported `color` frontmatter key stripped), and **commands** (`~/.factory/commands/`). Skills/plugins/workflows have no Droid equivalent and are disabled; hooks/permissions are deferred.
15
+ - `agents run droid` and `agents teams add … droid` work end-to-end: headless `droid exec` with mode mapping (plan → read-only, edit → `--auto low`, auto → `--auto high`, skip → `--skip-permissions-unsafe`), `-o stream-json` output, `-m` model selection, and `-r` reasoning effort. Routine/daemon jobs (`buildJobCommand`) support Droid too.
16
+ - Known limitation: `agents teams` renders Droid events through the generic normalizer pending a verified `droid exec -o stream-json` event schema; structured tool/file categorization will follow. Session reading and Factory cloud dispatch remain follow-ups.
17
+
5
18
  **`agents upgrade` now refreshes the macOS Keychain helper**
6
19
 
7
20
  - Upgrading runs `npm install -g … --ignore-scripts`, so the postinstall that installs the signed Keychain helper never fired — a user upgrading away from a broken build (e.g. the entitlement-less 1.20.4 helper that failed `SecItemAdd` with `errSecMissingEntitlement -34018`) kept the broken helper until the lazy staleness check in `getKeychainHelperPath()` happened to repair it on their next secret operation. `installResolvedPackage` now force-refreshes the helper (`ensureKeychainHelperInstalled({ forceReinstall: true })`) on darwin after the install, so both the explicit `agents upgrade` and the auto-update prompt land the fixed helper immediately. Best-effort and non-fatal: an upgrade never fails because the helper could not be reinstalled, and `agents helper install --force` remains the manual path.
package/README.md CHANGED
@@ -29,6 +29,8 @@
29
29
  <a href="https://github.com/NousResearch/hermes-agent" title="Hermes Agent"><img src="assets/harnesses/hermes.png" height="32" alt="Hermes Agent" /></a>
30
30
  &nbsp;&nbsp;&nbsp;&nbsp;
31
31
  <a href="https://x.ai" title="Grok Build (xAI)"><strong>Grok</strong></a>
32
+ &nbsp;&nbsp;&nbsp;&nbsp;
33
+ <a href="https://factory.ai" title="Factory AI Droid"><strong>Droid</strong></a>
32
34
  </p>
33
35
 
34
36
  https://agents-cli.sh/demo.mp4
@@ -86,6 +88,7 @@ Think `requirements.txt` for CLI coding agents, on steroids. A shim reads `agent
86
88
  ```bash
87
89
  agents add claude@2.0.65 # Install a specific version
88
90
  agents add codex@latest # Install latest
91
+ agents add codex@oldest # Install the oldest published version
89
92
  agents view # See everything installed
90
93
  ```
91
94
 
@@ -35,6 +35,9 @@ export declare function buildRaiseParams(opts: {
35
35
  windowId?: number;
36
36
  title?: string;
37
37
  }): Record<string, unknown>;
38
+ export declare const CHAR_DELAY_MIN_MS = 1;
39
+ export declare const CHAR_DELAY_MAX_MS = 250;
40
+ export declare function clampCharDelay(ms: number | undefined): number | undefined;
38
41
  export declare function buildWaitParams(opts: {
39
42
  duration?: number;
40
43
  id?: string;
@@ -70,6 +70,18 @@ export function buildRaiseParams(opts) {
70
70
  params.title = opts.title;
71
71
  return params;
72
72
  }
73
+ // Inter-character typing delay for type-text. Default 4ms matches the daemon's
74
+ // historical fixed rate; lossy keyboard relays (Parallels/VM guests) drop chars
75
+ // at that rate, so callers can raise it. Clamp to [1, 250]ms CLI-side (the
76
+ // daemon clamps too — defense in depth). Returns undefined when unset so the
77
+ // daemon applies its own default. Pure + tested.
78
+ export const CHAR_DELAY_MIN_MS = 1;
79
+ export const CHAR_DELAY_MAX_MS = 250;
80
+ export function clampCharDelay(ms) {
81
+ if (ms === undefined || !Number.isFinite(ms))
82
+ return undefined;
83
+ return Math.min(CHAR_DELAY_MAX_MS, Math.max(CHAR_DELAY_MIN_MS, Math.trunc(ms)));
84
+ }
73
85
  // Build the wait RPC params. Pure + tested. Three modes, mirroring the
74
86
  // daemon's Wait.run: --duration (unconditional sleep), --id + --until
75
87
  // (cached-element poll), or --role/--label/--identifier (live locator poll).
@@ -276,6 +288,7 @@ export function registerActionCommands(program) {
276
288
  .option('--commit', 'Press Return after typing')
277
289
  .option('--raise', 'Bring the target app to the front first')
278
290
  .option('--require-frontmost', 'Fail (not warn) if the target is not the frontmost app')
291
+ .option('--char-delay <ms>', 'Inter-character delay in ms (default 4; raise for lossy keyboard relays like VM guests, e.g. 25). Clamped to [1, 250].', (v) => parseInt(v, 10))
279
292
  .option('--json', 'Emit JSON')).action(async (opts) => {
280
293
  await withClient(async (client) => {
281
294
  const pid = await resolveTargetPid(client, opts);
@@ -285,6 +298,9 @@ export function registerActionCommands(program) {
285
298
  params.commit = true;
286
299
  if (opts.requireFrontmost)
287
300
  params.require_frontmost = true;
301
+ const charDelay = clampCharDelay(opts.charDelay);
302
+ if (charDelay !== undefined)
303
+ params.char_delay_ms = charDelay;
288
304
  const res = unwrap(await client.call('type_text', params));
289
305
  warnIfNotFrontmost(res);
290
306
  emit(res, Boolean(opts.json), () => `typed ${res.chars ?? opts.text.length} char(s)`);
@@ -36,8 +36,8 @@ export function registerRunCommand(program) {
36
36
  .option('--add-dir <dir>', 'Grant access to an additional directory outside the project (Claude only, repeatable)', (val, prev) => [...prev, val], [])
37
37
  .option('--json', 'Stream events as JSON lines (for parsing by other tools)')
38
38
  .option('--quiet', 'Suppress preamble (rotation banner, "Running:" line). Useful when piping JSON events to a parser.', false)
39
- .option('--headless', 'Non-interactive mode (auto-enabled when prompt provided)', false)
40
- .option('-i, --interactive', 'Force interactive mode even when a prompt is provided')
39
+ .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)
40
+ .option('-i, --interactive', 'Force interactive mode even when a prompt is provided. Mutually exclusive with --headless.')
41
41
  .option('--session-id <id>', 'Resume a previous conversation (Claude only)')
42
42
  .option('--verbose', 'Show detailed execution logs')
43
43
  .option('--timeout <duration>', 'Kill the agent after this duration (e.g., 30m, 1h, 2h30m)')
@@ -85,7 +85,7 @@ export function registerRunCommand(program) {
85
85
  `,
86
86
  });
87
87
  runCmd.action(async (agentSpec, prompt, options) => {
88
- const [{ buildExecCommand, parseExecEnv, execAgent, runWithFallback, normalizeMode, resolveMode, defaultModeFor }, { ALL_AGENT_IDS }, { profileExists, resolveProfileForRun }, { readAndResolveBundleEnv, describeBundle }, { getConfiguredRunStrategy, normalizeRunStrategy, resolveRunVersion, RUN_STRATEGIES }, { getGlobalDefault, getVersionHomePath, resolveVersion, resolveVersionAlias }, { buildDiscoveredPlugin, loadPluginManifest, syncPluginToVersion }, { parseWorkflowFrontmatter, resolveWorkflowRef }, { resolveRunDefaults },] = await Promise.all([
88
+ 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 }, { resolveRunDefaults },] = await Promise.all([
89
89
  import('../lib/exec.js'),
90
90
  import('../lib/agents.js'),
91
91
  import('../lib/profiles.js'),
@@ -306,6 +306,23 @@ export function registerRunCommand(program) {
306
306
  process.exit(1);
307
307
  }
308
308
  }
309
+ // Fail fast on the headless-plan stall footgun: a slash command run
310
+ // headless under the implicit default 'plan' mode hangs forever at
311
+ // ExitPlanMode (no TTY to approve the plan). Tell the user how to fix it
312
+ // instead of leaving them staring at a frozen process. Explicit
313
+ // `--mode plan` is respected for genuine read-only command runs.
314
+ const stallCmd = headlessPlanStallCommand({
315
+ prompt,
316
+ interactive: options.interactive,
317
+ mode,
318
+ modeIsDefault,
319
+ });
320
+ if (stallCmd) {
321
+ console.error(chalk.red(`Refusing to run ${stallCmd} headless in read-only 'plan' mode — it would hang at ExitPlanMode (no TTY to approve the plan).`));
322
+ console.error(chalk.yellow(`Re-run with an explicit mode: --mode auto (recommended — auto-approves safe ops, blocks risky ones), --mode edit, or --mode full.`));
323
+ console.error(chalk.gray(`Pass --mode plan explicitly if you really want a read-only run.`));
324
+ process.exit(1);
325
+ }
309
326
  const effort = options.effort;
310
327
  if (!['low', 'medium', 'high', 'xhigh', 'max', 'auto'].includes(effort)) {
311
328
  console.error(chalk.red(`Invalid effort: ${effort}. Use 'low', 'medium', 'high', 'xhigh', 'max', or 'auto'`));
@@ -363,12 +380,16 @@ export function registerRunCommand(program) {
363
380
  model,
364
381
  addDirs: options.addDir,
365
382
  json: options.json,
366
- headless: options.headless ?? true,
383
+ headless: options.headless,
367
384
  sessionId: options.sessionId,
368
385
  verbose: options.verbose,
369
386
  timeout: options.timeout,
370
387
  env,
371
388
  };
389
+ if (options.interactive && options.headless) {
390
+ console.error(chalk.red('--interactive and --headless are mutually exclusive. Pass one, or neither (mode is inferred from prompt presence).'));
391
+ process.exit(1);
392
+ }
372
393
  if (options.interactive) {
373
394
  if (options.fallback) {
374
395
  console.error(chalk.red('--interactive is not compatible with --fallback. Fallback only works for headless prompt runs.'));
@@ -44,6 +44,11 @@ async function runImport(agentArg, opts) {
44
44
  // installer dropped it. We adopt by symlinking that PATH binary directly
45
45
  // into the version's `node_modules/.bin/`. No package.json walk.
46
46
  const isInstallScriptAgent = !agent.npmPackage;
47
+ // Whether to adopt the binary by a direct symlink (installScript style) vs.
48
+ // the npm package.json walk. Starts equal to isInstallScriptAgent, but an
49
+ // npm-capable agent that turns out to be installed as a standalone binary
50
+ // (see the resolvePackageDirFromBinary fallback below) flips this to true.
51
+ let useDirectBinaryImport = isInstallScriptAgent;
47
52
  let globalPath = null;
48
53
  let installScriptBinary = null;
49
54
  if (opts.fromPath) {
@@ -84,9 +89,15 @@ async function runImport(agentArg, opts) {
84
89
  else {
85
90
  globalPath = resolvePackageDirFromBinary(binary);
86
91
  if (!globalPath) {
87
- console.error(chalk.red(`Could not resolve npm package for binary: ${binary}`));
88
- console.error(chalk.gray('Pass --from-path <dir> with the package directory explicitly.'));
89
- process.exit(1);
92
+ // npmPackage is declared, but the binary on PATH doesn't live inside an
93
+ // npm package layout — e.g. Kimi installed via its curl install.sh,
94
+ // which drops a standalone bundled binary at ~/.kimi-code/bin/kimi
95
+ // rather than into node_modules/<pkg>/. The binary is valid and
96
+ // self-contained, so adopt it the same way as an installScript agent:
97
+ // a direct symlink. Key the decision on the on-disk layout, not on
98
+ // whether an npmPackage label happens to exist.
99
+ installScriptBinary = binary;
100
+ useDirectBinaryImport = true;
90
101
  }
91
102
  }
92
103
  }
@@ -112,7 +123,7 @@ async function runImport(agentArg, opts) {
112
123
  }
113
124
  let version = opts.version;
114
125
  if (!version) {
115
- if (!isInstallScriptAgent && globalPath) {
126
+ if (!useDirectBinaryImport && globalPath) {
116
127
  try {
117
128
  const pkg = JSON.parse(fs.readFileSync(path.join(globalPath, 'package.json'), 'utf8'));
118
129
  version = typeof pkg.version === 'string' ? pkg.version : undefined;
@@ -142,7 +153,7 @@ async function runImport(agentArg, opts) {
142
153
  process.exit(1);
143
154
  }
144
155
  const versionDir = getVersionDir(agentId, version);
145
- const fromLabel = isInstallScriptAgent ? installScriptBinary : globalPath;
156
+ const fromLabel = useDirectBinaryImport ? installScriptBinary : globalPath;
146
157
  console.log(chalk.bold(`\nImport ${agentLabel(agentId)} v${version}`));
147
158
  console.log(` from: ${chalk.gray(fromLabel)}`);
148
159
  console.log(` into: ${chalk.gray(versionDir)}`);
@@ -197,7 +208,7 @@ async function runImport(agentArg, opts) {
197
208
  }
198
209
  }
199
210
  const binSpinner = ora(`Registering ${agentLabel(agentId)} v${version} binary...`).start();
200
- const binResult = isInstallScriptAgent
211
+ const binResult = useDirectBinaryImport
201
212
  ? importInstallScriptBinary({ agentId, npmPackage: agent.npmPackage, cliCommand: agent.cliCommand }, version, installScriptBinary, versionDir)
202
213
  : importAgentBinary({ agentId, npmPackage: agent.npmPackage, cliCommand: agent.cliCommand }, version, globalPath, versionDir);
203
214
  if (binResult.success) {
@@ -12,6 +12,7 @@
12
12
  * AGENT.md / the file itself) so users can click straight to the source.
13
13
  */
14
14
  import { Command } from 'commander';
15
+ import { type PluginResourceGroup } from '../lib/plugins.js';
15
16
  /** Resource kinds the inspect command can drill into. */
16
17
  declare const DRILLABLE_KINDS: readonly ["commands", "skills", "hooks", "mcp", "rules", "plugins", "workflows", "subagents"];
17
18
  type DrillableKind = typeof DRILLABLE_KINDS[number];
@@ -24,8 +25,10 @@ interface ResourceItem {
24
25
  linkTarget: string;
25
26
  /** One-line description (frontmatter `description:` or first non-frontmatter line). */
26
27
  description: string;
27
- /** Extra detail rows surfaced in detail mode (e.g. a plugin's bundled skills/commands). */
28
+ /** Scalar detail rows surfaced in detail mode (e.g. a plugin's version). */
28
29
  extra?: Array<[string, string]>;
30
+ /** For plugins: the resource categories (skills, commands, …) the bundle packages. */
31
+ groups?: PluginResourceGroup[];
29
32
  }
30
33
  interface InspectOptions {
31
34
  brief?: boolean;
@@ -38,6 +41,13 @@ interface InspectOptions {
38
41
  plugins?: boolean | string;
39
42
  workflows?: boolean | string;
40
43
  subagents?: boolean | string;
44
+ command?: string;
45
+ skill?: string;
46
+ hook?: string;
47
+ rule?: string;
48
+ plugin?: string;
49
+ workflow?: string;
50
+ subagent?: string;
41
51
  }
42
52
  export declare function registerInspectCommand(program: Command): void;
43
53
  export declare function inspectAction(target: string, options: InspectOptions): Promise<void>;
@@ -23,7 +23,8 @@ import { readMeta, getUserAgentsDir, getSystemAgentsDir, getProjectAgentsDir, ge
23
23
  import { getVersionHomePath } from '../lib/versions.js';
24
24
  import { getShimsDir, getVersionedAliasPath } from '../lib/shims.js';
25
25
  import { getAgentResources, listResources, } from '../lib/resources.js';
26
- import { discoverPlugins, discoverPluginsInDir } from '../lib/plugins.js';
26
+ import { discoverPlugins, discoverPluginsInDir, pluginResourceGroups } from '../lib/plugins.js';
27
+ import { PLUGIN_GROUP_COLORS } from './plugins.js';
27
28
  import { countSessionsInScope } from '../lib/session/discover.js';
28
29
  import { damerauLevenshtein } from '../lib/fuzzy.js';
29
30
  /** Resource kinds the inspect command can drill into. */
@@ -37,6 +38,21 @@ const DRILLABLE_KINDS = [
37
38
  'workflows',
38
39
  'subagents',
39
40
  ];
41
+ /**
42
+ * Singular aliases for the plural drill-down flags. `--plugin code` reads as
43
+ * "show the one plugin named code" — a required-value flag that always lands in
44
+ * detail mode, the natural counterpart to `--plugins` (list). `mcp` has no
45
+ * distinct singular, so it is intentionally absent.
46
+ */
47
+ const SINGULAR_DRILL_ALIASES = {
48
+ command: 'commands',
49
+ skill: 'skills',
50
+ hook: 'hooks',
51
+ rule: 'rules',
52
+ plugin: 'plugins',
53
+ workflow: 'workflows',
54
+ subagent: 'subagents',
55
+ };
40
56
  const CAPABILITY_NAMES = [
41
57
  'hooks', 'mcp', 'skills', 'commands', 'subagents', 'plugins', 'workflows', 'rules', 'allowlist',
42
58
  ];
@@ -50,6 +66,9 @@ export function registerInspectCommand(program) {
50
66
  for (const kind of DRILLABLE_KINDS) {
51
67
  cmd.option(`--${kind} [query]`, `list ${kind}; pass a name (fuzzy) to show detail`);
52
68
  }
69
+ for (const singular of Object.keys(SINGULAR_DRILL_ALIASES)) {
70
+ cmd.option(`--${singular} <query>`, `show detail for one ${singular} by name (fuzzy)`);
71
+ }
53
72
  cmd.action(async (target, options) => {
54
73
  await inspectAction(target, options);
55
74
  });
@@ -121,15 +140,21 @@ function pickDrillKind(options) {
121
140
  for (const kind of DRILLABLE_KINDS) {
122
141
  const value = options[kind];
123
142
  if (value !== undefined)
124
- active.push({ kind, query: value });
143
+ active.push({ flag: `--${kind}`, kind, query: value });
144
+ }
145
+ // Singular aliases (`--plugin code`) always carry a name → detail mode.
146
+ for (const [singular, plural] of Object.entries(SINGULAR_DRILL_ALIASES)) {
147
+ const value = options[singular];
148
+ if (typeof value === 'string')
149
+ active.push({ flag: `--${singular}`, kind: plural, query: value });
125
150
  }
126
151
  if (active.length === 0)
127
152
  return null;
128
153
  if (active.length > 1) {
129
- console.error(chalk.red(`Pick at most one drill-down flag. Got: ${active.map(a => '--' + a.kind).join(', ')}`));
154
+ console.error(chalk.red(`Pick at most one drill-down flag. Got: ${active.map(a => a.flag).join(', ')}`));
130
155
  process.exit(1);
131
156
  }
132
- return active[0];
157
+ return { kind: active[0].kind, query: active[0].query };
133
158
  }
134
159
  /** Files at a DotAgents root that mark it as one, beyond the per-kind dirs. */
135
160
  const REPO_MARKER_FILES = ['agents.yaml', 'hooks.yaml'];
@@ -526,7 +551,7 @@ function renderItemList(header, jsonHead, kind, items, options) {
526
551
  ...jsonHead,
527
552
  kind,
528
553
  count: items.length,
529
- items: items.map(i => ({ name: i.name, source: i.source, path: i.path, description: i.description })),
554
+ items: items.map(i => ({ name: i.name, source: i.source, path: i.path, description: i.description, ...(i.groups ? { groups: i.groups } : {}) })),
530
555
  }, null, 2));
531
556
  return;
532
557
  }
@@ -542,9 +567,23 @@ function renderItemList(header, jsonHead, kind, items, options) {
542
567
  if (item.description) {
543
568
  console.log(` ${chalk.gray(truncate(item.description, 90))}`);
544
569
  }
570
+ if (item.groups)
571
+ printGroupRows(item.groups);
545
572
  }
546
573
  console.log('');
547
574
  }
575
+ /** Print a plugin's resource breakdown as aligned `label items` rows under a list entry. */
576
+ function printGroupRows(groups) {
577
+ if (groups.length === 0)
578
+ return;
579
+ const width = Math.max(...groups.map(g => g.label.length));
580
+ for (const g of groups) {
581
+ const colorFn = PLUGIN_GROUP_COLORS[g.label] ?? chalk.white;
582
+ const label = chalk.gray(g.label.padEnd(width));
583
+ const value = g.items.map((s) => colorFn(s)).join(chalk.gray(', '));
584
+ console.log(` ${label} ${value}`);
585
+ }
586
+ }
548
587
  // ─── Detail mode (fuzzy) ─────────────────────────────────────────────────────
549
588
  async function renderDetail(agent, version, versionHome, kind, query, options) {
550
589
  const items = collectKind(agent, versionHome, kind);
@@ -653,17 +692,6 @@ function pluginItems() {
653
692
  */
654
693
  function pluginToItem(plugin, source) {
655
694
  const extra = [];
656
- const list = (names) => names.length <= 8 ? names.join(', ') : `${names.slice(0, 8).join(', ')}, +${names.length - 8} more`;
657
- if (plugin.skills.length)
658
- extra.push(['skills', `${plugin.skills.length} (${list(plugin.skills)})`]);
659
- if (plugin.commands.length)
660
- extra.push(['commands', `${plugin.commands.length} (${list(plugin.commands)})`]);
661
- if (plugin.agentDefs.length)
662
- extra.push(['subagents', `${plugin.agentDefs.length} (${list(plugin.agentDefs)})`]);
663
- if (plugin.hooks.length)
664
- extra.push(['hooks', String(plugin.hooks.length)]);
665
- if (plugin.mcpServers.length)
666
- extra.push(['mcp', list(plugin.mcpServers)]);
667
695
  if (plugin.manifest.version)
668
696
  extra.push(['version', plugin.manifest.version]);
669
697
  return {
@@ -673,6 +701,7 @@ function pluginToItem(plugin, source) {
673
701
  linkTarget: linkTarget(plugin.root),
674
702
  description: plugin.manifest.description ?? '',
675
703
  extra,
704
+ groups: pluginResourceGroups(plugin),
676
705
  };
677
706
  }
678
707
  function entriesFromAgentResources(agent, versionHome, kind) {
@@ -742,9 +771,14 @@ function buildDetailRows(item, kind) {
742
771
  rows.push(['tools', fm.tools.join(', ')]);
743
772
  }
744
773
  }
745
- // Plugin bundles carry their nested resources as pre-built rows.
746
- if (kind === 'plugins' && item.extra) {
747
- rows.push(...item.extra);
774
+ // Plugin bundles surface their nested resources (skills, commands, …) plus
775
+ // scalar rows (version).
776
+ if (kind === 'plugins') {
777
+ if (item.groups)
778
+ for (const g of item.groups)
779
+ rows.push([g.label, g.items.join(', ')]);
780
+ if (item.extra)
781
+ rows.push(...item.extra);
748
782
  }
749
783
  return rows;
750
784
  }
@@ -92,9 +92,9 @@ function parseMcpAgentTargets(value) {
92
92
  }
93
93
  continue;
94
94
  }
95
- const resolvedVersion = versionToken === 'latest'
96
- ? installedVersions[installedVersions.length - 1]
97
- : versionToken;
95
+ const resolvedVersion = versionToken === 'latest' ? installedVersions[installedVersions.length - 1]
96
+ : versionToken === 'oldest' ? installedVersions[0]
97
+ : versionToken;
98
98
  if (!installedVersions.includes(resolvedVersion)) {
99
99
  throw new VersionNotInstalledError(agentId, resolvedVersion, installedVersions);
100
100
  }
@@ -22,4 +22,6 @@ interface MarketplaceRow {
22
22
  * default Claude version's settings.json#enabledPlugins keyed on @<marketplace>.
23
23
  */
24
24
  export declare function collectMarketplaceRows(): MarketplaceRow[];
25
+ /** Per-category color for a plugin resource breakdown (shared with `agents inspect`). */
26
+ export declare const PLUGIN_GROUP_COLORS: Record<string, (s: string) => string>;
25
27
  export {};
@@ -12,7 +12,7 @@ import { homeDir } from '../lib/platform/index.js';
12
12
  import { input } from '@inquirer/prompts';
13
13
  import { agentLabel } from '../lib/agents.js';
14
14
  import { capableAgents, isCapable } from '../lib/capabilities.js';
15
- import { discoverPlugins, getPlugin, pluginSupportsAgent, removePluginFromVersion, isPluginSynced, installPlugin, updatePlugin, loadUserConfig, saveUserConfig, checkPluginDependencies, hasPluginExecSurfaces, inspectPluginCapabilities, pluginCapabilityLabels, parseInstallSpec, syncPluginToVersion, } from '../lib/plugins.js';
15
+ import { discoverPlugins, getPlugin, pluginSupportsAgent, removePluginFromVersion, isPluginSynced, installPlugin, updatePlugin, loadUserConfig, saveUserConfig, checkPluginDependencies, hasPluginExecSurfaces, inspectPluginCapabilities, pluginCapabilityLabels, parseInstallSpec, syncPluginToVersion, pluginResourceGroups, } from '../lib/plugins.js';
16
16
  import { listInstalledVersions, syncResourcesToVersion, getGlobalDefault, getVersionHomePath, } from '../lib/versions.js';
17
17
  import { isPromptCancelled, isInteractiveTerminal, requireDestructiveArg, requireInteractiveSelection, promptRemovalTargets, } from './utils.js';
18
18
  import { itemPicker } from '../lib/picker.js';
@@ -276,14 +276,17 @@ Examples:
276
276
  // agents plugins sync <name> [agent]
277
277
  pluginsCmd
278
278
  .command('sync <name> [agent]')
279
- .description('Apply a plugin to the default version of an agent (or all supported agents if none specified)')
279
+ .description('Apply a plugin to an agent. Syncs every installed version (pass agent@version to target one).')
280
280
  .option('--allow-exec-surfaces', 'Enable the plugin even when it ships hooks/, .mcp.json, bin/, scripts/, settings.json, or permissions/')
281
281
  .addHelpText('after', `
282
282
  Examples:
283
- # Sync a plugin to a specific agent (default version)
283
+ # Sync a plugin to every installed version of an agent
284
284
  agents plugins sync rush-toolkit claude
285
285
 
286
- # Sync to all supported agents
286
+ # Sync to one specific version (parity with 'agents sync')
287
+ agents plugins sync rush-toolkit claude@2.1.142
288
+
289
+ # Sync to all supported agents (every installed version of each)
287
290
  agents plugins sync rush-toolkit
288
291
 
289
292
  # Re-affirm consent for a hooks-bearing plugin
@@ -295,12 +298,22 @@ Examples:
295
298
  console.log(chalk.red(`Plugin '${name}' not found`));
296
299
  process.exit(1);
297
300
  }
301
+ // Accept the same "agent@version" form as `agents sync`. Splitting here
302
+ // also means an unknown spec is reported cleanly rather than crashing
303
+ // isCapable() with a bare "claude@2.1.168".
304
+ let versionArg;
305
+ let agentName = agentArg;
306
+ if (agentArg && agentArg.includes('@')) {
307
+ const at = agentArg.lastIndexOf('@');
308
+ agentName = agentArg.slice(0, at);
309
+ versionArg = agentArg.slice(at + 1);
310
+ }
298
311
  // Determine target agents
299
312
  let targetAgents;
300
- if (agentArg) {
301
- const agentId = agentArg;
313
+ if (agentName) {
314
+ const agentId = agentName;
302
315
  if (!isCapable(agentId, 'plugins')) {
303
- console.log(chalk.red(`Agent '${agentArg}' does not support plugins`));
316
+ console.log(chalk.red(`Agent '${agentName}' does not support plugins`));
304
317
  process.exit(1);
305
318
  }
306
319
  if (!pluginSupportsAgent(plugin, agentId)) {
@@ -310,6 +323,10 @@ Examples:
310
323
  targetAgents = [agentId];
311
324
  }
312
325
  else {
326
+ if (versionArg) {
327
+ console.log(chalk.red(`A version (@${versionArg}) requires naming the agent, e.g. claude@${versionArg}`));
328
+ process.exit(1);
329
+ }
313
330
  targetAgents = capableAgents('plugins').filter(a => pluginSupportsAgent(plugin, a));
314
331
  }
315
332
  const allowExec = options.allowExecSurfaces === true;
@@ -317,8 +334,21 @@ Examples:
317
334
  const versions = listInstalledVersions(agentId);
318
335
  if (versions.length === 0)
319
336
  continue;
320
- const defaultVer = getGlobalDefault(agentId);
321
- const targetVersions = defaultVer ? [defaultVer] : [versions[versions.length - 1]];
337
+ // Default to EVERY installed version. The previous behaviour synced only
338
+ // the global default, which silently skipped non-default versions used
339
+ // by balanced rotation -- so a rotated version would lack the plugin's
340
+ // slash commands. An explicit agent@version narrows back to one.
341
+ let targetVersions;
342
+ if (versionArg) {
343
+ if (!versions.includes(versionArg)) {
344
+ console.log(chalk.red(`${agentLabel(agentId)} has no installed version ${versionArg} (installed: ${versions.join(', ')})`));
345
+ process.exit(1);
346
+ }
347
+ targetVersions = [versionArg];
348
+ }
349
+ else {
350
+ targetVersions = versions;
351
+ }
322
352
  for (const version of targetVersions) {
323
353
  const didSync = allowExec
324
354
  ? syncPluginToVersion(plugin, agentId, getVersionHomePath(agentId, version), { allowExecSurfaces: true, version }).success
@@ -711,6 +741,32 @@ function buildPluginRows(plugins) {
711
741
  });
712
742
  return rows;
713
743
  }
744
+ /** Per-category color for a plugin resource breakdown (shared with `agents inspect`). */
745
+ export const PLUGIN_GROUP_COLORS = {
746
+ skills: chalk.cyan,
747
+ commands: chalk.cyan,
748
+ subagents: chalk.magenta,
749
+ hooks: chalk.yellow,
750
+ mcp: chalk.green,
751
+ lsp: chalk.green,
752
+ monitors: chalk.blue,
753
+ bin: chalk.white,
754
+ scripts: chalk.white,
755
+ settings: chalk.gray,
756
+ };
757
+ /** Human-readable section header per category, used by the picker detail pane. */
758
+ const PLUGIN_GROUP_TITLES = {
759
+ skills: 'Skills',
760
+ commands: 'Commands',
761
+ subagents: 'Subagents',
762
+ hooks: 'Hooks',
763
+ mcp: 'MCP Servers',
764
+ lsp: 'LSP Servers',
765
+ monitors: 'Monitors',
766
+ bin: 'Bin',
767
+ scripts: 'Scripts',
768
+ settings: 'Settings',
769
+ };
714
770
  /** Build the multi-line detail pane shown when a plugin is selected in the picker. */
715
771
  function formatPluginDetail(plugin, targets) {
716
772
  const lines = [];
@@ -728,24 +784,11 @@ function formatPluginDetail(plugin, targets) {
728
784
  lines.push(' ' + chalk.gray('Supports: ') + supported.join(chalk.gray(' · ')));
729
785
  }
730
786
  lines.push(' ' + chalk.gray(formatPath(plugin.root)));
731
- const section = (label, items, colorFn) => {
732
- if (items.length === 0)
733
- return;
787
+ for (const group of pluginResourceGroups(plugin)) {
788
+ const colorFn = PLUGIN_GROUP_COLORS[group.label] ?? chalk.white;
734
789
  lines.push('');
735
- lines.push(chalk.bold(` ${label}`));
736
- lines.push(' ' + items.map(colorFn).join(chalk.gray(', ')));
737
- };
738
- section('Skills', plugin.skills.map((s) => `/${plugin.name}:${s}`), chalk.cyan);
739
- section('Commands', plugin.commands.map((c) => `/${plugin.name}:${c}`), chalk.cyan);
740
- section('Subagents', plugin.agentDefs, chalk.magenta);
741
- section('Hooks', plugin.hooks, chalk.yellow);
742
- section('MCP Servers', plugin.mcpServers, chalk.green);
743
- section('LSP Servers', plugin.lspServers, chalk.green);
744
- section('Monitors', plugin.monitors, chalk.blue);
745
- section('Bin', plugin.bin, chalk.white);
746
- section('Scripts', plugin.scripts, chalk.white);
747
- if (plugin.hasSettings) {
748
- section('Settings', ['settings.json'], chalk.gray);
790
+ lines.push(chalk.bold(` ${PLUGIN_GROUP_TITLES[group.label] ?? group.label}`));
791
+ lines.push(' ' + group.items.map((s) => colorFn(s)).join(chalk.gray(', ')));
749
792
  }
750
793
  if (targets.length > 0) {
751
794
  lines.push('');
@@ -61,7 +61,7 @@ async function runSync(agentSpec, opts) {
61
61
  return;
62
62
  }
63
63
  agentId = parsed.agent;
64
- if (parsed.version !== 'latest')
64
+ if (parsed.version !== 'latest' && parsed.version !== 'oldest')
65
65
  version = parsed.version;
66
66
  }
67
67
  if (opts.agent) {
@@ -25,6 +25,7 @@ const AGENT_NAMES = {
25
25
  grok: 'Grok',
26
26
  antigravity: 'Antigravity',
27
27
  kimi: 'Kimi',
28
+ droid: 'Droid',
28
29
  };
29
30
  const VALID_AGENTS = Object.keys(AGENT_NAMES);
30
31
  // 'full' kept as historical alias for 'skip'; normalized to 'skip' downstream.
@@ -7,4 +7,15 @@
7
7
  * `rm -rf ~/.agents/.history/trash/` removes bytes from disk.
8
8
  */
9
9
  import type { Command } from 'commander';
10
+ /**
11
+ * Restore a soft-deleted version back into ~/.agents/.history/versions/.
12
+ * Shared by `agents trash restore` and the top-level `agents restore` alias.
13
+ * Exits the process with a non-zero code on any failure.
14
+ */
15
+ export declare function restoreVersion(target: string): void;
16
+ /**
17
+ * Register the top-level `agents restore` command — a shorthand for
18
+ * `agents trash restore` so users can undo a `remove`/`prune` directly.
19
+ */
20
+ export declare function registerRestoreCommand(program: Command): void;
10
21
  export declare function registerTrashCommands(program: Command): void;