@phnx-labs/agents-cli 1.18.1 → 1.18.3

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 (56) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/commands/doctor.js +19 -5
  3. package/dist/commands/exec.js +9 -4
  4. package/dist/commands/plugins.js +58 -14
  5. package/dist/commands/view.js +16 -7
  6. package/dist/index.js +30 -0
  7. package/dist/lib/hooks.js +21 -3
  8. package/dist/lib/migrate.js +35 -12
  9. package/dist/lib/plugin-marketplace.d.ts +93 -0
  10. package/dist/lib/plugin-marketplace.js +239 -0
  11. package/dist/lib/plugins.d.ts +25 -13
  12. package/dist/lib/plugins.js +350 -566
  13. package/dist/lib/shims.d.ts +3 -1
  14. package/dist/lib/shims.js +81 -7
  15. package/dist/lib/staleness/checkers/commands.d.ts +7 -0
  16. package/dist/lib/staleness/checkers/commands.js +27 -0
  17. package/dist/lib/staleness/checkers/hooks.d.ts +13 -0
  18. package/dist/lib/staleness/checkers/hooks.js +63 -0
  19. package/dist/lib/staleness/checkers/mcp.d.ts +12 -0
  20. package/dist/lib/staleness/checkers/mcp.js +38 -0
  21. package/dist/lib/staleness/checkers/permissions.d.ts +17 -0
  22. package/dist/lib/staleness/checkers/permissions.js +73 -0
  23. package/dist/lib/staleness/checkers/plugins.d.ts +11 -0
  24. package/dist/lib/staleness/checkers/plugins.js +39 -0
  25. package/dist/lib/staleness/checkers/rules.d.ts +19 -0
  26. package/dist/lib/staleness/checkers/rules.js +86 -0
  27. package/dist/lib/staleness/checkers/skills.d.ts +7 -0
  28. package/dist/lib/staleness/checkers/skills.js +34 -0
  29. package/dist/lib/staleness/checkers/subagents.d.ts +12 -0
  30. package/dist/lib/staleness/checkers/subagents.js +39 -0
  31. package/dist/lib/staleness/checkers/types.d.ts +44 -0
  32. package/dist/lib/staleness/checkers/types.js +20 -0
  33. package/dist/lib/staleness/checkers/workflows.d.ts +10 -0
  34. package/dist/lib/staleness/checkers/workflows.js +37 -0
  35. package/dist/lib/staleness/fingerprint.d.ts +38 -0
  36. package/dist/lib/staleness/fingerprint.js +154 -0
  37. package/dist/lib/staleness/index.d.ts +26 -0
  38. package/dist/lib/staleness/index.js +122 -0
  39. package/dist/lib/staleness/layers.d.ts +37 -0
  40. package/dist/lib/staleness/layers.js +100 -0
  41. package/dist/lib/staleness/types.d.ts +56 -0
  42. package/dist/lib/staleness/types.js +6 -0
  43. package/dist/lib/state.d.ts +2 -0
  44. package/dist/lib/state.js +2 -0
  45. package/dist/lib/teams/agents.d.ts +11 -20
  46. package/dist/lib/teams/agents.js +55 -202
  47. package/dist/lib/teams/index.d.ts +3 -2
  48. package/dist/lib/teams/index.js +2 -2
  49. package/dist/lib/teams/persistence.d.ts +0 -38
  50. package/dist/lib/teams/persistence.js +7 -329
  51. package/dist/lib/teams/registry.js +7 -5
  52. package/dist/lib/types.d.ts +6 -0
  53. package/dist/lib/versions.js +34 -12
  54. package/package.json +1 -1
  55. package/dist/lib/sync-manifest.d.ts +0 -81
  56. package/dist/lib/sync-manifest.js +0 -450
package/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.18.3
4
+
5
+ **Plugins** ([#22](https://github.com/phnx-labs/agents-cli/issues/22))
6
+
7
+ - `agents plugins sync` now installs plugins via Claude Code's native marketplace path — `<versionHome>/.{claude,openclaw}/plugins/marketplaces/agents-cli/plugins/<name>/` — instead of flattening contents into `~/.claude/skills/<plugin>--<skill>/`. Skills resolve as `/plugin:skill` (the documented form) instead of `/plugin--skill`. Plugins appear in Claude's `/plugins` UI under Installed and respond to `/plugin enable`, `/plugin disable`.
8
+ - A synthetic `agents-cli` marketplace is materialized per version: `.claude-plugin/marketplace.json` is synthesized from discovered plugins, an entry is added to `<versionHome>/.claude/plugins/known_marketplaces.json`, and `settings.json#enabledPlugins["<plugin>@agents-cli"]` is flipped to `true`. Removal is symmetric — last plugin out drops the marketplace dir and the known_marketplaces entry.
9
+ - The sync now copies the whole plugin tree verbatim (single `fs.cpSync`) instead of re-implementing per-feature merges into `settings.json`. Every Claude plugin feature — skills, commands, subagents, hooks, `.mcp.json`, `.lsp.json`, `monitors/monitors.json`, `bin/`, `settings.json` — is preserved end-to-end. `${CLAUDE_PLUGIN_ROOT}` and `${CLAUDE_PLUGIN_DATA}` are left intact so Claude can expand them at runtime; only `${user_config.*}` (agents-cli-specific) is pre-expanded in copied text files.
10
+ - Legacy dual-dash layout from prior versions is auto-migrated at sync time — `~/.claude/skills/<plugin>--*`, `~/.claude/commands/<plugin>--*.md`, `~/.claude/agents/<plugin>--*.md`, `plugin-bin/<plugin>/`, and namespaced `mcpServers["<plugin>--*"]` entries are removed after the marketplace install succeeds.
11
+ - `agents plugins view <name>` surfaces every feature the plugin ships: Skills, Commands, Subagents, Hooks, MCP Servers, LSP Servers, Monitors, Bin, Scripts, Settings. The `agents view <agent>@<version>` Plugins section gains MCP/LSP/Monitor/Bin/Settings counts. New `discoverPluginMcpServers`, `discoverPluginLspServers`, `discoverPluginMonitors` helpers parse `.mcp.json`, `.lsp.json`, and `monitors/monitors.json`.
12
+
13
+ ## 1.18.2
14
+
15
+ **Teams**
16
+
17
+ - Dropped `~/.agents/teams/config.json` entirely. It duplicated information agents-cli already has — agent commands, enabled flags, model defaults, provider endpoints — none of which the team runner was actually reading. Teams now discover agents via `listInstalledVersions()` (the same source `agents view` uses) and invoke them via the canonical `agents run` subcommand. One spawn path, one canonical exec module (`src/lib/exec.ts`). The deprecated `AGENT_COMMANDS`, `applyEditMode`, `applyFullMode`, `readConfig`, `writeConfig`, `setAgentEnabled`, `AgentConfig`, `SwarmConfig`, `ProviderConfig`, `ModelOverrides`, `ReadConfigResult`, and `EffortLevel` (the persistence-module copy) exports are removed from `@phnx-labs/agents-cli/teams`. Migration deletes both `~/.agents/teams/config.json` and the legacy `~/.agents/config.json`.
18
+ - `~/.agents/teams/registry.json` moves to `~/.agents/.history/teams/registry.json` — it's per-machine runtime state (timestamps + absolute worktree paths) and shouldn't be synced across machines via `agents repo push`.
19
+ - New `agents run --quiet` flag suppresses the rotation banner and `Running: …` preamble lines. Used by the team runner so stream-json events reach the parser without non-JSON preamble.
20
+
21
+ **Dev builds**
22
+
23
+ - The CLI auto-detects dev builds (version stamped `0.0.0-dev.<sha>` by `scripts/install.sh`, or invoked from a working tree where `<cli-dir>/../.git/` exists) and defaults `AGENTS_NO_AUTOPULL=1`, `AGENTS_SKIP_MIGRATION=1`, and `AGENTS_CLI_DISABLE_AUTO_UPDATE=1`. No more typing those three env vars on every iteration. Production installs (registry global, no `.git/` at package root) are unaffected.
24
+
3
25
  ## 1.18.1
4
26
 
5
27
  **Fixes**
@@ -1,8 +1,8 @@
1
1
  import chalk from 'chalk';
2
2
  import { checkAllClis } from '../lib/teams/agents.js';
3
3
  import { AGENTS, ALL_AGENT_IDS, resolveAgentName, formatAgentError } from '../lib/agents.js';
4
- import { getAvailableResources, getGlobalDefault, getVersionHomePath, isVersionInstalled, listInstalledVersions, parseAgentSpec, } from '../lib/versions.js';
5
- import { loadSyncManifest, isSyncStale } from '../lib/sync-manifest.js';
4
+ import { getGlobalDefault, getVersionHomePath, isVersionInstalled, listInstalledVersions, parseAgentSpec, } from '../lib/versions.js';
5
+ import { loadManifest, isStale } from '../lib/staleness/index.js';
6
6
  import { diffVersionCommands, iterCommandsCapableVersions } from '../lib/commands.js';
7
7
  import { diffVersionSkills, iterSkillsCapableVersions } from '../lib/skills.js';
8
8
  import { diffVersionHooks, iterHooksCapableVersions } from '../lib/hooks.js';
@@ -17,13 +17,12 @@ function checkSyncStatus(cwd) {
17
17
  const version = getGlobalDefault(agent);
18
18
  if (!version)
19
19
  continue;
20
- const manifest = loadSyncManifest(agent, version);
20
+ const manifest = loadManifest(agent, version);
21
21
  if (!manifest) {
22
22
  rows.push({ agent, version, status: 'never-synced' });
23
23
  continue;
24
24
  }
25
- const available = getAvailableResources(cwd);
26
- const stale = isSyncStale(manifest, available, agent, version, cwd);
25
+ const stale = isStale(manifest, agent, version, cwd);
27
26
  rows.push({ agent, version, status: stale ? 'stale' : 'fresh' });
28
27
  }
29
28
  return rows;
@@ -270,6 +269,21 @@ function renderTargetText(report, options) {
270
269
  : null,
271
270
  ].filter(Boolean).join(' ');
272
271
  console.log(chalk.gray(` layers: ${layerStr}`));
272
+ // Staleness manifest verdict — single-line summary from the staleness
273
+ // library, sitting alongside the detailed per-resource diff below.
274
+ const manifest = loadManifest(report.agent, report.version);
275
+ if (!manifest) {
276
+ console.log(chalk.gray(` manifest: ${chalk.gray('cold')} (never synced)`));
277
+ }
278
+ else {
279
+ const stale = isStale(manifest, report.agent, report.version, report.cwd);
280
+ if (stale) {
281
+ console.log(chalk.gray(' manifest: ') + chalk.yellow('stale') + chalk.gray(' (sources changed since last sync)'));
282
+ }
283
+ else {
284
+ console.log(chalk.gray(' manifest: ') + chalk.green('fresh'));
285
+ }
286
+ }
273
287
  console.log();
274
288
  for (const kind of DOCTOR_ALL_KINDS) {
275
289
  const rows = report.kinds[kind];
@@ -51,6 +51,7 @@ export function registerRunCommand(program) {
51
51
  .option('--cwd <dir>', 'Working directory for the agent (defaults to current directory)')
52
52
  .option('--add-dir <dir>', 'Grant access to an additional directory outside the project (Claude only, repeatable)', (val, prev) => [...prev, val], [])
53
53
  .option('--json', 'Stream events as JSON lines (for parsing by other tools)')
54
+ .option('--quiet', 'Suppress preamble (rotation banner, "Running:" line). Useful when piping JSON events to a parser.', false)
54
55
  .option('--headless', 'Non-interactive mode (auto-enabled when prompt provided)', false)
55
56
  .option('-i, --interactive', 'Force interactive mode even when a prompt is provided')
56
57
  .option('--session-id <id>', 'Resume a previous conversation (Claude only)')
@@ -249,17 +250,19 @@ Examples:
249
250
  const resolved = await resolveRunVersion(agent, strategy, cwd);
250
251
  if (resolved.version) {
251
252
  version = resolved.version;
252
- if (resolved.rotation) {
253
+ if (resolved.rotation && !options.quiet) {
253
254
  const banner = formatRotationBanner(resolved.rotation, strategy);
254
255
  process.stderr.write(chalk.gray(banner + '\n'));
255
256
  }
256
257
  }
257
- else {
258
+ else if (!options.quiet) {
258
259
  process.stderr.write(chalk.yellow(`[agents] strategy ${strategy} found no usable ${agent} version; falling back to defaults\n`));
259
260
  }
260
261
  }
261
262
  catch (err) {
262
- process.stderr.write(chalk.yellow(`[agents] strategy ${strategy} skipped: ${err.message}\n`));
263
+ if (!options.quiet) {
264
+ process.stderr.write(chalk.yellow(`[agents] strategy ${strategy} skipped: ${err.message}\n`));
265
+ }
263
266
  }
264
267
  }
265
268
  }
@@ -388,7 +391,9 @@ Examples:
388
391
  }
389
392
  }
390
393
  const cmd = buildExecCommand(execOptions);
391
- process.stderr.write(chalk.gray(`Running: ${cmd.join(' ')}\n\n`));
394
+ if (!options.quiet) {
395
+ process.stderr.write(chalk.gray(`Running: ${cmd.join(' ')}\n\n`));
396
+ }
392
397
  try {
393
398
  let exitCode;
394
399
  if (fallback.length > 0) {
@@ -149,7 +149,19 @@ Examples:
149
149
  if (plugin.skills.length > 0) {
150
150
  console.log(chalk.bold('\n Skills'));
151
151
  for (const skill of plugin.skills) {
152
- console.log(` ${chalk.cyan(`${plugin.name}:${skill}`)}`);
152
+ console.log(` ${chalk.cyan(`/${plugin.name}:${skill}`)}`);
153
+ }
154
+ }
155
+ if (plugin.commands.length > 0) {
156
+ console.log(chalk.bold('\n Commands'));
157
+ for (const cmd of plugin.commands) {
158
+ console.log(` ${chalk.cyan(`/${plugin.name}:${cmd}`)}`);
159
+ }
160
+ }
161
+ if (plugin.agentDefs.length > 0) {
162
+ console.log(chalk.bold('\n Subagents'));
163
+ for (const a of plugin.agentDefs) {
164
+ console.log(` ${chalk.magenta(a)}`);
153
165
  }
154
166
  }
155
167
  if (plugin.hooks.length > 0) {
@@ -158,12 +170,40 @@ Examples:
158
170
  console.log(` ${chalk.yellow(hook)}`);
159
171
  }
160
172
  }
173
+ if (plugin.mcpServers.length > 0) {
174
+ console.log(chalk.bold('\n MCP Servers'));
175
+ for (const s of plugin.mcpServers) {
176
+ console.log(` ${chalk.green(s)}`);
177
+ }
178
+ }
179
+ if (plugin.lspServers.length > 0) {
180
+ console.log(chalk.bold('\n LSP Servers'));
181
+ for (const s of plugin.lspServers) {
182
+ console.log(` ${chalk.green(s)}`);
183
+ }
184
+ }
185
+ if (plugin.monitors.length > 0) {
186
+ console.log(chalk.bold('\n Monitors'));
187
+ for (const m of plugin.monitors) {
188
+ console.log(` ${chalk.blue(m)}`);
189
+ }
190
+ }
191
+ if (plugin.bin.length > 0) {
192
+ console.log(chalk.bold('\n Bin'));
193
+ for (const b of plugin.bin) {
194
+ console.log(` ${chalk.white(b)}`);
195
+ }
196
+ }
161
197
  if (plugin.scripts.length > 0) {
162
198
  console.log(chalk.bold('\n Scripts'));
163
199
  for (const script of plugin.scripts) {
164
200
  console.log(` ${chalk.gray(script)}`);
165
201
  }
166
202
  }
203
+ if (plugin.hasSettings) {
204
+ console.log(chalk.bold('\n Settings'));
205
+ console.log(` ${chalk.gray('settings.json')}`);
206
+ }
167
207
  // Show installation status per agent version
168
208
  console.log(chalk.bold('\n Installation Status'));
169
209
  let anyInstalled = false;
@@ -572,20 +612,24 @@ function formatPluginDetail(plugin, targets) {
572
612
  lines.push(' ' + chalk.gray('Supports: ') + supported.join(chalk.gray(' · ')));
573
613
  }
574
614
  lines.push(' ' + chalk.gray(formatPath(plugin.root)));
575
- if (plugin.skills.length > 0) {
576
- lines.push('');
577
- lines.push(chalk.bold(' Skills'));
578
- lines.push(' ' + plugin.skills.map((s) => chalk.cyan(s)).join(chalk.gray(', ')));
579
- }
580
- if (plugin.hooks.length > 0) {
581
- lines.push('');
582
- lines.push(chalk.bold(' Hooks'));
583
- lines.push(' ' + plugin.hooks.map((h) => chalk.yellow(h)).join(chalk.gray(', ')));
584
- }
585
- if (plugin.scripts.length > 0) {
615
+ const section = (label, items, colorFn) => {
616
+ if (items.length === 0)
617
+ return;
586
618
  lines.push('');
587
- lines.push(chalk.bold(' Scripts'));
588
- lines.push(' ' + plugin.scripts.map((s) => chalk.white(s)).join(chalk.gray(', ')));
619
+ lines.push(chalk.bold(` ${label}`));
620
+ lines.push(' ' + items.map(colorFn).join(chalk.gray(', ')));
621
+ };
622
+ section('Skills', plugin.skills.map((s) => `/${plugin.name}:${s}`), chalk.cyan);
623
+ section('Commands', plugin.commands.map((c) => `/${plugin.name}:${c}`), chalk.cyan);
624
+ section('Subagents', plugin.agentDefs, chalk.magenta);
625
+ section('Hooks', plugin.hooks, chalk.yellow);
626
+ section('MCP Servers', plugin.mcpServers, chalk.green);
627
+ section('LSP Servers', plugin.lspServers, chalk.green);
628
+ section('Monitors', plugin.monitors, chalk.blue);
629
+ section('Bin', plugin.bin, chalk.white);
630
+ section('Scripts', plugin.scripts, chalk.white);
631
+ if (plugin.hasSettings) {
632
+ section('Settings', ['settings.json'], chalk.gray);
589
633
  }
590
634
  if (targets.length > 0) {
591
635
  lines.push('');
@@ -604,19 +604,28 @@ async function showAgentResources(agentId, requestedVersion) {
604
604
  const versionStr = agentData.version ? ` (${agentData.version})` : '';
605
605
  const agentHeader = home ? termLink(agentData.agentName, home) : agentData.agentName;
606
606
  console.log(` ${chalk.bold(agentHeader)}${chalk.gray(versionStr)}:`);
607
+ const pluralize = (n, singular) => `${n} ${singular}${n === 1 ? '' : 's'}`;
607
608
  for (const p of plugins) {
608
609
  const linkedName = termLink(p.name, linkTarget(p.root));
609
610
  const parts = [];
610
611
  if (p.skills.length > 0)
611
- parts.push(`${p.skills.length} skill${p.skills.length === 1 ? '' : 's'}`);
612
+ parts.push(pluralize(p.skills.length, 'skill'));
612
613
  if (p.commands.length > 0)
613
- parts.push(`${p.commands.length} command${p.commands.length === 1 ? '' : 's'}`);
614
- if (p.hooks.length > 0)
615
- parts.push(`${p.hooks.length} hook${p.hooks.length === 1 ? '' : 's'}`);
614
+ parts.push(pluralize(p.commands.length, 'command'));
616
615
  if (p.agentDefs.length > 0)
617
- parts.push(`${p.agentDefs.length} subagent${p.agentDefs.length === 1 ? '' : 's'}`);
618
- if (p.hasMcp)
619
- parts.push('mcp');
616
+ parts.push(pluralize(p.agentDefs.length, 'subagent'));
617
+ if (p.hooks.length > 0)
618
+ parts.push(pluralize(p.hooks.length, 'hook'));
619
+ if (p.mcpServers.length > 0)
620
+ parts.push(`${p.mcpServers.length} MCP`);
621
+ if (p.lspServers.length > 0)
622
+ parts.push(`${p.lspServers.length} LSP`);
623
+ if (p.monitors.length > 0)
624
+ parts.push(pluralize(p.monitors.length, 'monitor'));
625
+ if (p.bin.length > 0)
626
+ parts.push(pluralize(p.bin.length, 'bin'));
627
+ if (p.hasSettings)
628
+ parts.push('settings');
620
629
  const contents = parts.length > 0 ? chalk.gray(` (${parts.join(', ')})`) : '';
621
630
  console.log(` ${chalk.cyan(linkedName)}${contents} ${chalk.cyan('[user]')}`);
622
631
  }
package/dist/index.js CHANGED
@@ -23,6 +23,36 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
23
23
  const packageJsonPath = path.join(__dirname, '..', 'package.json');
24
24
  const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
25
25
  const VERSION = packageJson.version;
26
+ // Detect dev/working-tree builds and default the noisy startup steps off.
27
+ // Three cases trip this:
28
+ // 1. Dev install (scripts/install.sh) — package.json version stamped 0.0.0-dev.<sha>
29
+ // 2. Running `node dist/index.js` from a working tree — repo root has .git/
30
+ // 3. Running tsx/ts-node from src/ — also has .git/ at the repo root
31
+ // For all three: skip auto-pull (no network noise + no surprise FF on the
32
+ // system repo while iterating), skip migration (a buggy in-progress migration
33
+ // must not scribble on the user's real ~/.agents/), and skip the update prompt
34
+ // (the "0.0.0-dev -> 1.x.y" message is misleading). Each individual env var
35
+ // can still be set explicitly to override (set to '0' to re-enable).
36
+ const IS_DEV_BUILD = (() => {
37
+ if (VERSION.startsWith('0.0.0-dev'))
38
+ return true;
39
+ try {
40
+ const cliPath = process.argv[1] || '';
41
+ const repoRoot = path.dirname(path.dirname(cliPath));
42
+ return fs.existsSync(path.join(repoRoot, '.git'));
43
+ }
44
+ catch {
45
+ return false;
46
+ }
47
+ })();
48
+ if (IS_DEV_BUILD) {
49
+ if (process.env.AGENTS_NO_AUTOPULL === undefined)
50
+ process.env.AGENTS_NO_AUTOPULL = '1';
51
+ if (process.env.AGENTS_SKIP_MIGRATION === undefined)
52
+ process.env.AGENTS_SKIP_MIGRATION = '1';
53
+ if (process.env.AGENTS_CLI_DISABLE_AUTO_UPDATE === undefined)
54
+ process.env.AGENTS_CLI_DISABLE_AUTO_UPDATE = '1';
55
+ }
26
56
  // Import command registrations
27
57
  import { registerPullCommand } from './commands/pull.js';
28
58
  import { registerRepoCommands } from './commands/repo.js';
package/dist/lib/hooks.js CHANGED
@@ -51,6 +51,18 @@ function isManagedHookCommand(command, prefixes) {
51
51
  return false;
52
52
  }
53
53
  import { getEffectiveHome, getVersionHomePath, listInstalledVersions } from './versions.js';
54
+ /**
55
+ * Extensions that are NEVER hooks — docs, configuration, plain data. A file
56
+ * in hooks/ with one of these extensions is auxiliary content (e.g., the
57
+ * `promptcuts.yaml` data file read directly by the expand-promptcuts
58
+ * script, or the `README.md` that documents the hooks directory). They
59
+ * sometimes carry an exec bit by accident (older sync runs chmod 0o755'd
60
+ * everything) but they are not scripts.
61
+ */
62
+ const NON_SCRIPT_EXTENSIONS = new Set([
63
+ '.md', '.markdown', '.rst', '.txt',
64
+ '.yaml', '.yml', '.json', '.toml', '.ini', '.conf',
65
+ ]);
54
66
  const SCRIPT_EXTENSIONS = new Set([
55
67
  '.sh',
56
68
  '.bash',
@@ -140,9 +152,15 @@ export function listHookEntriesFromDir(dir) {
140
152
  const entries = [];
141
153
  for (const [base, group] of grouped) {
142
154
  group.sort((a, b) => a.name.localeCompare(b.name));
143
- const script = group.find((f) => f.isExec) ||
144
- group.find((f) => SCRIPT_EXTENSIONS.has(f.ext.toLowerCase())) ||
145
- group[0];
155
+ // A group is a hook only if it has an actual script: a script extension,
156
+ // OR an executable bit on a file whose extension is not a known data /
157
+ // docs type. Files like `README.md` (docs) or `promptcuts.yaml` (data
158
+ // the expand-promptcuts hook reads directly) sit alongside hooks but
159
+ // are NOT hooks themselves and must not surface in the hooks list
160
+ // anywhere — doctor, sync, view, or otherwise. Older sync runs may have
161
+ // chmod 0o755'd these files; an exec bit alone is not enough.
162
+ const script = group.find((f) => SCRIPT_EXTENSIONS.has(f.ext.toLowerCase())) ||
163
+ group.find((f) => f.isExec && !NON_SCRIPT_EXTENSIONS.has(f.ext.toLowerCase()));
146
164
  if (!script)
147
165
  continue;
148
166
  const data = group.find((f) => f !== script);
@@ -58,14 +58,14 @@ function deleteSystemPromptsJson() {
58
58
  * The teams persistence layer already reads the legacy path as a fallback;
59
59
  * moving it here keeps the canonical location consistent.
60
60
  */
61
+ // Delete the legacy ~/.agents-system/config.json. This was the teams agent
62
+ // registry, which no longer exists — `agents teams` discovers agents through
63
+ // `listInstalledVersions` and invokes them through `agents run`.
61
64
  function migrateSystemConfigJson() {
62
65
  const src = path.join(SYSTEM_DIR, 'config.json');
63
- const dest = path.join(USER_DIR, 'teams', 'config.json');
64
- if (!fs.existsSync(src) || fs.existsSync(dest))
66
+ if (!fs.existsSync(src))
65
67
  return;
66
68
  try {
67
- fs.mkdirSync(path.dirname(dest), { recursive: true, mode: 0o700 });
68
- fs.copyFileSync(src, dest);
69
69
  fs.unlinkSync(src);
70
70
  }
71
71
  catch { /* best-effort */ }
@@ -375,20 +375,41 @@ function deleteUserPromptsJson() {
375
375
  catch { /* best-effort */ }
376
376
  }
377
377
  /**
378
- * Delete ~/.agents/config.json. The canonical teams config is at
379
- * ~/.agents/teams/config.json (teams/persistence.ts). If the canonical
380
- * file exists we just unlink the legacy copy; otherwise migrate first.
378
+ * Delete ~/.agents/teams/config.json. The teams subsystem no longer carries
379
+ * its own agent registry — agent discovery flows through `listInstalledVersions`
380
+ * (the same source `agents view` uses) and invocation flows through
381
+ * `agents run`. The on-disk file is pure dead state on existing installs.
382
+ */
383
+ function deleteTeamsConfigJson() {
384
+ const f = path.join(USER_DIR, 'teams', 'config.json');
385
+ if (!fs.existsSync(f))
386
+ return;
387
+ try {
388
+ fs.unlinkSync(f);
389
+ }
390
+ catch { /* best-effort */ }
391
+ }
392
+ /**
393
+ * Move ~/.agents/teams/registry.json → ~/.agents/.history/teams/registry.json.
394
+ * The registry is per-machine runtime state (timestamps + absolute worktree
395
+ * paths) and belongs in the durable-runtime bucket, not at the user-root
396
+ * where `agents repo push` would sync it across machines.
397
+ */
398
+ function moveTeamsRegistryToHistory() {
399
+ const src = path.join(USER_DIR, 'teams', 'registry.json');
400
+ const dest = path.join(HISTORY_DIR, 'teams', 'registry.json');
401
+ moveFileOnce(src, dest);
402
+ }
403
+ /**
404
+ * Delete ~/.agents/config.json. This was the legacy teams config location;
405
+ * the teams subsystem no longer carries a config file at all, so the legacy
406
+ * copy is simply removed.
381
407
  */
382
408
  function cleanupUserConfigJson() {
383
409
  const legacy = path.join(USER_DIR, 'config.json');
384
410
  if (!fs.existsSync(legacy))
385
411
  return;
386
- const canonical = path.join(USER_DIR, 'teams', 'config.json');
387
412
  try {
388
- if (!fs.existsSync(canonical)) {
389
- fs.mkdirSync(path.dirname(canonical), { recursive: true, mode: 0o700 });
390
- fs.copyFileSync(legacy, canonical);
391
- }
392
413
  fs.unlinkSync(legacy);
393
414
  }
394
415
  catch { /* best-effort */ }
@@ -1455,6 +1476,8 @@ export async function runMigration() {
1455
1476
  migratePermissionSetsToPresets();
1456
1477
  deleteUserLinearJson();
1457
1478
  deleteUserPromptsJson();
1479
+ deleteTeamsConfigJson();
1480
+ moveTeamsRegistryToHistory();
1458
1481
  cleanupUserConfigJson();
1459
1482
  cleanupEmptyTopLevelRuns();
1460
1483
  foldUserHooksYamlIntoAgentsYaml();
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Native plugin marketplace install path for Claude / OpenClaw.
3
+ *
4
+ * Plugins managed by agents-cli are exposed as a synthetic local marketplace
5
+ * named "agents-cli" under each version's plugin directory:
6
+ *
7
+ * <versionHome>/.{claude,openclaw}/plugins/
8
+ * known_marketplaces.json # registers the "agents-cli" marketplace
9
+ * marketplaces/agents-cli/
10
+ * .claude-plugin/marketplace.json # synthesized catalog
11
+ * plugins/<plugin>/ # copied plugin source
12
+ *
13
+ * Plus the version's settings.json gets `enabledPlugins["<plugin>@agents-cli"] = true`.
14
+ *
15
+ * This produces native `/plugin:skill` slash namespacing, visibility in `/plugins`,
16
+ * and `/plugin enable|disable` support — matching the Claude Code spec at
17
+ * https://code.claude.com/docs/en/plugins and /plugin-marketplaces.
18
+ */
19
+ import type { AgentId, DiscoveredPlugin } from './types.js';
20
+ export declare const MARKETPLACE_NAME = "agents-cli";
21
+ interface MarketplacePluginEntry {
22
+ name: string;
23
+ source: string;
24
+ description?: string;
25
+ version?: string;
26
+ author?: {
27
+ name: string;
28
+ email?: string;
29
+ };
30
+ }
31
+ interface MarketplaceManifest {
32
+ $schema?: string;
33
+ name: string;
34
+ description?: string;
35
+ owner: {
36
+ name: string;
37
+ email?: string;
38
+ };
39
+ plugins: MarketplacePluginEntry[];
40
+ }
41
+ export declare function marketplaceRoot(agent: AgentId, versionHome: string): string;
42
+ export declare function marketplaceManifestPath(agent: AgentId, versionHome: string): string;
43
+ export declare function pluginInstallDir(plugin: DiscoveredPlugin, agent: AgentId, versionHome: string): string;
44
+ export declare function knownMarketplacesPath(agent: AgentId, versionHome: string): string;
45
+ /**
46
+ * Copy plugin source into marketplace install dir.
47
+ * Source of truth remains ~/.agents/plugins/<name>/ — this is a per-version snapshot.
48
+ */
49
+ export declare function copyPluginToMarketplace(plugin: DiscoveredPlugin, agent: AgentId, versionHome: string): string;
50
+ /**
51
+ * Re-synthesize <marketplace>/.claude-plugin/marketplace.json from the list of
52
+ * plugins installed under <marketplace>/plugins/. Always run after add or remove
53
+ * so the manifest stays in lockstep with on-disk contents.
54
+ */
55
+ export declare function syncMarketplaceManifest(agent: AgentId, versionHome: string): MarketplaceManifest | null;
56
+ /**
57
+ * Register the agents-cli marketplace in known_marketplaces.json so Claude Code
58
+ * discovers it on startup. Idempotent: re-running just refreshes lastUpdated.
59
+ */
60
+ export declare function registerMarketplace(agent: AgentId, versionHome: string): void;
61
+ /**
62
+ * Drop the agents-cli marketplace entry from known_marketplaces.json.
63
+ * Called when the last plugin under it is removed.
64
+ */
65
+ export declare function unregisterMarketplace(agent: AgentId, versionHome: string): void;
66
+ /**
67
+ * Mark a plugin as enabled in <versionHome>/.{agent}/settings.json under
68
+ * enabledPlugins["<plugin>@agents-cli"]: true. Reads, mutates, writes —
69
+ * preserving every other key.
70
+ */
71
+ export declare function enablePluginInSettings(pluginName: string, agent: AgentId, versionHome: string): void;
72
+ /**
73
+ * Remove the enabledPlugins key for this plugin. Inverse of enablePluginInSettings.
74
+ */
75
+ export declare function disablePluginInSettings(pluginName: string, agent: AgentId, versionHome: string): void;
76
+ /**
77
+ * Remove a plugin's installed marketplace directory. Returns true if the dir
78
+ * existed and was removed.
79
+ */
80
+ export declare function removePluginFromMarketplace(pluginName: string, agent: AgentId, versionHome: string): boolean;
81
+ /**
82
+ * Return true if the marketplace has no plugins left under it.
83
+ */
84
+ export declare function marketplaceIsEmpty(agent: AgentId, versionHome: string): boolean;
85
+ /**
86
+ * Drop the entire marketplace directory. Called after the last plugin removal.
87
+ */
88
+ export declare function removeEmptyMarketplaceDir(agent: AgentId, versionHome: string): void;
89
+ /**
90
+ * Detect whether a plugin is installed via the native marketplace path.
91
+ */
92
+ export declare function isInstalledInMarketplace(pluginName: string, agent: AgentId, versionHome: string): boolean;
93
+ export {};