@phnx-labs/agents-cli 1.18.2 → 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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
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
+
3
13
  ## 1.18.2
4
14
 
5
15
  **Teams**
@@ -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
  }
@@ -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 {};
@@ -0,0 +1,239 @@
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 * as fs from 'fs';
20
+ import * as path from 'path';
21
+ export const MARKETPLACE_NAME = 'agents-cli';
22
+ function pluginsRootForVersion(agent, versionHome) {
23
+ return path.join(versionHome, `.${agent}`, 'plugins');
24
+ }
25
+ export function marketplaceRoot(agent, versionHome) {
26
+ return path.join(pluginsRootForVersion(agent, versionHome), 'marketplaces', MARKETPLACE_NAME);
27
+ }
28
+ export function marketplaceManifestPath(agent, versionHome) {
29
+ return path.join(marketplaceRoot(agent, versionHome), '.claude-plugin', 'marketplace.json');
30
+ }
31
+ export function pluginInstallDir(plugin, agent, versionHome) {
32
+ return path.join(marketplaceRoot(agent, versionHome), 'plugins', plugin.name);
33
+ }
34
+ export function knownMarketplacesPath(agent, versionHome) {
35
+ return path.join(pluginsRootForVersion(agent, versionHome), 'known_marketplaces.json');
36
+ }
37
+ function settingsPath(agent, versionHome) {
38
+ return path.join(versionHome, `.${agent}`, 'settings.json');
39
+ }
40
+ /**
41
+ * Copy plugin source into marketplace install dir.
42
+ * Source of truth remains ~/.agents/plugins/<name>/ — this is a per-version snapshot.
43
+ */
44
+ export function copyPluginToMarketplace(plugin, agent, versionHome) {
45
+ const dest = pluginInstallDir(plugin, agent, versionHome);
46
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
47
+ if (fs.existsSync(dest)) {
48
+ fs.rmSync(dest, { recursive: true, force: true });
49
+ }
50
+ fs.cpSync(plugin.root, dest, { recursive: true, dereference: false });
51
+ return dest;
52
+ }
53
+ /**
54
+ * Re-synthesize <marketplace>/.claude-plugin/marketplace.json from the list of
55
+ * plugins installed under <marketplace>/plugins/. Always run after add or remove
56
+ * so the manifest stays in lockstep with on-disk contents.
57
+ */
58
+ export function syncMarketplaceManifest(agent, versionHome) {
59
+ const root = marketplaceRoot(agent, versionHome);
60
+ const pluginsDir = path.join(root, 'plugins');
61
+ if (!fs.existsSync(pluginsDir))
62
+ return null;
63
+ const entries = [];
64
+ for (const entry of fs.readdirSync(pluginsDir, { withFileTypes: true })) {
65
+ if (!entry.isDirectory() || entry.name.startsWith('.'))
66
+ continue;
67
+ const manifestFile = path.join(pluginsDir, entry.name, '.claude-plugin', 'plugin.json');
68
+ if (!fs.existsSync(manifestFile))
69
+ continue;
70
+ let manifest;
71
+ try {
72
+ manifest = JSON.parse(fs.readFileSync(manifestFile, 'utf-8'));
73
+ }
74
+ catch {
75
+ continue;
76
+ }
77
+ entries.push({
78
+ name: manifest.name,
79
+ source: `./plugins/${manifest.name}`,
80
+ description: manifest.description,
81
+ version: manifest.version,
82
+ ...(manifest.author ? { author: manifest.author } : {}),
83
+ });
84
+ }
85
+ const manifest = {
86
+ $schema: 'https://anthropic.com/claude-code/marketplace.schema.json',
87
+ name: MARKETPLACE_NAME,
88
+ description: 'Plugins managed by agents-cli',
89
+ owner: { name: 'agents-cli' },
90
+ plugins: entries.sort((a, b) => a.name.localeCompare(b.name)),
91
+ };
92
+ const manifestPath = marketplaceManifestPath(agent, versionHome);
93
+ fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
94
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n', 'utf-8');
95
+ return manifest;
96
+ }
97
+ /**
98
+ * Register the agents-cli marketplace in known_marketplaces.json so Claude Code
99
+ * discovers it on startup. Idempotent: re-running just refreshes lastUpdated.
100
+ */
101
+ export function registerMarketplace(agent, versionHome) {
102
+ const root = marketplaceRoot(agent, versionHome);
103
+ const knownPath = knownMarketplacesPath(agent, versionHome);
104
+ let known = {};
105
+ if (fs.existsSync(knownPath)) {
106
+ try {
107
+ known = JSON.parse(fs.readFileSync(knownPath, 'utf-8'));
108
+ }
109
+ catch {
110
+ known = {};
111
+ }
112
+ }
113
+ known[MARKETPLACE_NAME] = {
114
+ source: { source: 'local', path: root },
115
+ installLocation: root,
116
+ lastUpdated: new Date().toISOString(),
117
+ };
118
+ fs.mkdirSync(path.dirname(knownPath), { recursive: true });
119
+ fs.writeFileSync(knownPath, JSON.stringify(known, null, 2) + '\n', 'utf-8');
120
+ }
121
+ /**
122
+ * Drop the agents-cli marketplace entry from known_marketplaces.json.
123
+ * Called when the last plugin under it is removed.
124
+ */
125
+ export function unregisterMarketplace(agent, versionHome) {
126
+ const knownPath = knownMarketplacesPath(agent, versionHome);
127
+ if (!fs.existsSync(knownPath))
128
+ return;
129
+ let known;
130
+ try {
131
+ known = JSON.parse(fs.readFileSync(knownPath, 'utf-8'));
132
+ }
133
+ catch {
134
+ return;
135
+ }
136
+ if (!(MARKETPLACE_NAME in known))
137
+ return;
138
+ delete known[MARKETPLACE_NAME];
139
+ if (Object.keys(known).length === 0) {
140
+ try {
141
+ fs.unlinkSync(knownPath);
142
+ }
143
+ catch { /* ignore */ }
144
+ }
145
+ else {
146
+ fs.writeFileSync(knownPath, JSON.stringify(known, null, 2) + '\n', 'utf-8');
147
+ }
148
+ }
149
+ /**
150
+ * Mark a plugin as enabled in <versionHome>/.{agent}/settings.json under
151
+ * enabledPlugins["<plugin>@agents-cli"]: true. Reads, mutates, writes —
152
+ * preserving every other key.
153
+ */
154
+ export function enablePluginInSettings(pluginName, agent, versionHome) {
155
+ const sPath = settingsPath(agent, versionHome);
156
+ let settings = {};
157
+ if (fs.existsSync(sPath)) {
158
+ try {
159
+ settings = JSON.parse(fs.readFileSync(sPath, 'utf-8'));
160
+ }
161
+ catch {
162
+ settings = {};
163
+ }
164
+ }
165
+ if (!settings.enabledPlugins || typeof settings.enabledPlugins !== 'object') {
166
+ settings.enabledPlugins = {};
167
+ }
168
+ const enabled = settings.enabledPlugins;
169
+ const key = `${pluginName}@${MARKETPLACE_NAME}`;
170
+ if (enabled[key] === true)
171
+ return;
172
+ enabled[key] = true;
173
+ fs.mkdirSync(path.dirname(sPath), { recursive: true });
174
+ fs.writeFileSync(sPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
175
+ }
176
+ /**
177
+ * Remove the enabledPlugins key for this plugin. Inverse of enablePluginInSettings.
178
+ */
179
+ export function disablePluginInSettings(pluginName, agent, versionHome) {
180
+ const sPath = settingsPath(agent, versionHome);
181
+ if (!fs.existsSync(sPath))
182
+ return;
183
+ let settings;
184
+ try {
185
+ settings = JSON.parse(fs.readFileSync(sPath, 'utf-8'));
186
+ }
187
+ catch {
188
+ return;
189
+ }
190
+ const enabled = settings.enabledPlugins;
191
+ if (!enabled)
192
+ return;
193
+ const key = `${pluginName}@${MARKETPLACE_NAME}`;
194
+ if (!(key in enabled))
195
+ return;
196
+ delete enabled[key];
197
+ if (Object.keys(enabled).length === 0) {
198
+ delete settings.enabledPlugins;
199
+ }
200
+ fs.writeFileSync(sPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
201
+ }
202
+ /**
203
+ * Remove a plugin's installed marketplace directory. Returns true if the dir
204
+ * existed and was removed.
205
+ */
206
+ export function removePluginFromMarketplace(pluginName, agent, versionHome) {
207
+ const installed = path.join(marketplaceRoot(agent, versionHome), 'plugins', pluginName);
208
+ if (!fs.existsSync(installed))
209
+ return false;
210
+ fs.rmSync(installed, { recursive: true, force: true });
211
+ return true;
212
+ }
213
+ /**
214
+ * Return true if the marketplace has no plugins left under it.
215
+ */
216
+ export function marketplaceIsEmpty(agent, versionHome) {
217
+ const pluginsDir = path.join(marketplaceRoot(agent, versionHome), 'plugins');
218
+ if (!fs.existsSync(pluginsDir))
219
+ return true;
220
+ const remaining = fs.readdirSync(pluginsDir, { withFileTypes: true })
221
+ .filter(d => d.isDirectory() && !d.name.startsWith('.'));
222
+ return remaining.length === 0;
223
+ }
224
+ /**
225
+ * Drop the entire marketplace directory. Called after the last plugin removal.
226
+ */
227
+ export function removeEmptyMarketplaceDir(agent, versionHome) {
228
+ const root = marketplaceRoot(agent, versionHome);
229
+ if (!fs.existsSync(root))
230
+ return;
231
+ fs.rmSync(root, { recursive: true, force: true });
232
+ }
233
+ /**
234
+ * Detect whether a plugin is installed via the native marketplace path.
235
+ */
236
+ export function isInstalledInMarketplace(pluginName, agent, versionHome) {
237
+ const installed = path.join(marketplaceRoot(agent, versionHome), 'plugins', pluginName);
238
+ return fs.existsSync(path.join(installed, '.claude-plugin', 'plugin.json'));
239
+ }
@@ -35,6 +35,12 @@ export declare function discoverPluginCommands(pluginRoot: string): string[];
35
35
  export declare function discoverPluginAgentDefs(pluginRoot: string): string[];
36
36
  /** Discover executable files in a plugin's bin/ directory. */
37
37
  export declare function discoverPluginBin(pluginRoot: string): string[];
38
+ /** Discover MCP server names from .mcp.json at the plugin root. */
39
+ export declare function discoverPluginMcpServers(pluginRoot: string): string[];
40
+ /** Discover LSP server keys from .lsp.json at the plugin root. */
41
+ export declare function discoverPluginLspServers(pluginRoot: string): string[];
42
+ /** Discover monitor names from monitors/monitors.json. */
43
+ export declare function discoverPluginMonitors(pluginRoot: string): string[];
38
44
  /**
39
45
  * Expand plugin variables in a string.
40
46
  *
@@ -60,15 +66,18 @@ export declare function checkPluginDependencies(manifest: PluginManifest): strin
60
66
  /**
61
67
  * Sync a plugin to a specific agent version's home directory.
62
68
  *
63
- * For Claude:
64
- * 1. Copy plugin skills into version's skills dir (prefixed: pluginName--skillName)
65
- * 2. Copy plugin commands into version's commands dir (prefixed: pluginName--cmdName.md)
66
- * 3. Copy plugin agent defs into version's agents dir (prefixed: pluginName--agentName.md)
67
- * 4. Copy plugin bin/ into version home plugin-bin/<pluginName>/, note path in settings
68
- * 5. Read hooks/hooks.json, expand vars, merge into settings.json hooks
69
- * 6. Read .mcp.json, expand vars, merge mcpServers into settings.json
70
- * 7. Read settings.json, merge non-permission keys non-destructively into settings.json
71
- * 8. Read settings.json permissions, expand vars, merge into settings.json
69
+ * For plugins-capable agents (claude, openclaw):
70
+ * 1. Copy plugin source into <versionHome>/.<agent>/plugins/marketplaces/agents-cli/plugins/<name>/
71
+ * 2. Pre-expand ${user_config.*} variables in copied text files (Claude doesn't know this var).
72
+ * 3. (Re-)synthesize the marketplace.json catalog from the installed plugins.
73
+ * 4. Register the synthetic marketplace in known_marketplaces.json.
74
+ * 5. Mark <plugin>@agents-cli enabled in settings.json#enabledPlugins.
75
+ * 6. Migrate (remove) legacy dual-dash skills/commands/agents/bin/hooks/mcp entries.
76
+ *
77
+ * Claude/OpenClaw natively handle the plugin's skills, commands, agents, hooks,
78
+ * MCP servers, bin/, settings.json, and permissions once the plugin lives at the
79
+ * native install path and is marked enabled — see
80
+ * https://code.claude.com/docs/en/plugins.
72
81
  */
73
82
  export declare function syncPluginToVersion(plugin: DiscoveredPlugin, agent: AgentId, versionHome: string): {
74
83
  success: boolean;
@@ -82,8 +91,9 @@ export declare function syncPluginToVersion(plugin: DiscoveredPlugin, agent: Age
82
91
  settings: boolean;
83
92
  };
84
93
  /**
85
- * Check if a plugin is synced to a version by inspecting the version home.
86
- * Checks skills, commands, agent defs, bin, hook commands, and permissions.
94
+ * Check if a plugin is synced to a version. True when the plugin lives at the
95
+ * native marketplace install path. Legacy dual-dash entries are not counted —
96
+ * they're treated as stale and migrated away on the next sync.
87
97
  */
88
98
  export declare function isPluginSynced(plugin: DiscoveredPlugin, agent: AgentId, versionHome: string): boolean;
89
99
  /**
@@ -102,8 +112,10 @@ export declare function removePluginFromVersion(pluginName: string, pluginRoot:
102
112
  mcp: number;
103
113
  };
104
114
  /**
105
- * Remove orphaned plugin skill directories from a version home.
106
- * Soft-deletes to ~/.agents/.trash/plugins/.
115
+ * Remove orphaned plugin entries from a version home. An entry is "orphan" if
116
+ * its plugin name is not in the active plugin set. Soft-deletes the affected
117
+ * marketplace plugin dir to ~/.agents/.trash/plugins/. Also cleans up any
118
+ * legacy dual-dash skills/ directories from older agents-cli versions.
107
119
  */
108
120
  export declare function cleanOrphanedPluginSkills(agent: AgentId, versionHome: string, activePluginNames: Set<string>, version?: string): string[];
109
121
  export interface VersionPluginDiff {