agentinit 1.12.1 → 1.13.0

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.
@@ -1,4 +1,4 @@
1
- import { resolve, join, basename } from 'path';
1
+ import { resolve, join, basename, relative, dirname } from 'path';
2
2
  import { promises as fs } from 'fs';
3
3
  import { homedir } from 'os';
4
4
  import matter from 'gray-matter';
@@ -7,6 +7,24 @@ import { AgentManager } from './agentManager.js';
7
7
  import { MCPFilter } from './mcpFilter.js';
8
8
  import { MARKETPLACES, getMarketplace as lookupMarketplace, getMarketplaceIds as lookupMarketplaceIds } from './marketplaceRegistry.js';
9
9
  import { SkillsManager } from './skillsManager.js';
10
+ export class MarketplacePluginNotFoundError extends Error {
11
+ pluginName;
12
+ marketplaceId;
13
+ marketplaceName;
14
+ suggestions;
15
+ constructor(pluginName, marketplaceId, marketplaceName, suggestions) {
16
+ let msg = `Plugin "${pluginName}" not found in ${marketplaceName} marketplace.`;
17
+ if (suggestions.length > 0) {
18
+ msg += ` Did you mean: ${suggestions.join(', ')}?`;
19
+ }
20
+ super(msg);
21
+ this.name = 'MarketplacePluginNotFoundError';
22
+ this.pluginName = pluginName;
23
+ this.marketplaceId = marketplaceId;
24
+ this.marketplaceName = marketplaceName;
25
+ this.suggestions = suggestions;
26
+ }
27
+ }
10
28
  function getMarketplaceCacheDir(registryId) {
11
29
  return join(homedir(), '.agentinit', 'marketplace-cache', registryId);
12
30
  }
@@ -16,6 +34,9 @@ function getRegistryPath(projectPath, global) {
16
34
  }
17
35
  return join(projectPath, '.agentinit', 'plugins.json');
18
36
  }
37
+ function getClaudeInstalledPluginsPath() {
38
+ return join(homedir(), '.claude', 'plugins', 'installed_plugins.json');
39
+ }
19
40
  export class PluginManager {
20
41
  agentManager;
21
42
  skillsManager;
@@ -23,6 +44,259 @@ export class PluginManager {
23
44
  this.agentManager = agentManager || new AgentManager();
24
45
  this.skillsManager = new SkillsManager(this.agentManager);
25
46
  }
47
+ parseGitHubRepo(url) {
48
+ const normalized = url.replace(/\.git$/, '');
49
+ const match = normalized.match(/github\.com[:/]([^/]+)\/([^/]+)/i);
50
+ if (!match) {
51
+ return null;
52
+ }
53
+ const [, owner, repo] = match;
54
+ if (!owner || !repo) {
55
+ return null;
56
+ }
57
+ return {
58
+ owner,
59
+ repo,
60
+ };
61
+ }
62
+ deriveGitHubFallbackSource(source, marketplaceId) {
63
+ const repoShorthandMatch = source.match(/^([a-zA-Z0-9._-]+)\/([a-zA-Z0-9._-]+)$/);
64
+ if (repoShorthandMatch) {
65
+ const [, owner, repo] = repoShorthandMatch;
66
+ return {
67
+ type: 'github',
68
+ url: `https://github.com/${owner}/${repo}.git`,
69
+ owner,
70
+ repo,
71
+ };
72
+ }
73
+ if (!marketplaceId || !/^[a-zA-Z0-9._-]+$/.test(source)) {
74
+ return null;
75
+ }
76
+ const registry = this.getMarketplace(marketplaceId);
77
+ const registryRepo = registry ? this.parseGitHubRepo(registry.repoUrl) : null;
78
+ if (!registryRepo) {
79
+ return null;
80
+ }
81
+ return {
82
+ type: 'github',
83
+ url: `https://github.com/${registryRepo.owner}/${source}.git`,
84
+ owner: registryRepo.owner,
85
+ repo: source,
86
+ };
87
+ }
88
+ formatGitHubRepoUrl(source) {
89
+ if (!source.owner || !source.repo) {
90
+ return null;
91
+ }
92
+ return `https://github.com/${source.owner}/${source.repo}`;
93
+ }
94
+ async resolvePreparedPluginDir(pluginDir, source) {
95
+ const claudeMarketplaceManifestPath = join(pluginDir, '.claude-plugin', 'marketplace.json');
96
+ if (!(await fileExists(claudeMarketplaceManifestPath))) {
97
+ return { pluginDir, warnings: [] };
98
+ }
99
+ const manifestContent = await readFileIfExists(claudeMarketplaceManifestPath);
100
+ if (!manifestContent) {
101
+ return { pluginDir, warnings: [] };
102
+ }
103
+ let manifest;
104
+ try {
105
+ manifest = JSON.parse(manifestContent);
106
+ }
107
+ catch {
108
+ throw new Error(`Invalid .claude-plugin/marketplace.json in ${pluginDir}`);
109
+ }
110
+ const entries = Array.isArray(manifest.plugins)
111
+ ? manifest.plugins.filter((entry) => typeof entry?.name === 'string' && typeof entry?.source === 'string')
112
+ : [];
113
+ if (entries.length === 0) {
114
+ throw new Error(`No plugins declared in .claude-plugin/marketplace.json for ${pluginDir}`);
115
+ }
116
+ const [firstEntry] = entries;
117
+ if (!firstEntry) {
118
+ throw new Error(`No plugins declared in .claude-plugin/marketplace.json for ${pluginDir}`);
119
+ }
120
+ let selectedEntry = firstEntry;
121
+ if (entries.length > 1) {
122
+ const candidates = new Set([source.pluginName, source.repo]
123
+ .filter((value) => !!value)
124
+ .flatMap(value => [value, basename(value)]));
125
+ const matched = entries.find(entry => candidates.has(entry.name) || candidates.has(basename(entry.source)));
126
+ if (!matched) {
127
+ throw new Error(`Repository "${pluginDir}" is a Claude marketplace bundle with multiple plugins. Select one of: ${entries.map(entry => entry.name).join(', ')}.`);
128
+ }
129
+ selectedEntry = matched;
130
+ }
131
+ const selectedPluginDir = resolve(pluginDir, selectedEntry.source);
132
+ const relativePath = relative(resolve(pluginDir), selectedPluginDir);
133
+ if (relativePath.startsWith('..') ||
134
+ relativePath.includes('/../') ||
135
+ relativePath.includes('\\..\\')) {
136
+ throw new Error(`Invalid bundled plugin source path "${selectedEntry.source}" in ${claudeMarketplaceManifestPath}`);
137
+ }
138
+ if (!(await isDirectory(selectedPluginDir))) {
139
+ throw new Error(`Bundled plugin path not found: ${selectedPluginDir}`);
140
+ }
141
+ const sourceLabel = this.formatGitHubRepoUrl(source) || pluginDir;
142
+ return {
143
+ pluginDir: selectedPluginDir,
144
+ warnings: [
145
+ `Source "${sourceLabel}" is a Claude Code marketplace bundle; using bundled plugin "${selectedEntry.name}".`,
146
+ ],
147
+ claudeBundle: {
148
+ bundleName: manifest.name || selectedEntry.name,
149
+ pluginName: selectedEntry.name,
150
+ },
151
+ };
152
+ }
153
+ async loadPluginFromDirectory(pluginDir, source, warnings = []) {
154
+ const preparedPlugin = await this.resolvePreparedPluginDir(pluginDir, source);
155
+ const parsedPlugin = await this.parsePlugin(preparedPlugin.pluginDir, source);
156
+ return {
157
+ ...parsedPlugin,
158
+ warnings: [...warnings, ...preparedPlugin.warnings, ...parsedPlugin.warnings],
159
+ resolvedPluginDir: preparedPlugin.pluginDir,
160
+ ...(preparedPlugin.claudeBundle ? { nativeClaudeBundle: preparedPlugin.claudeBundle } : {}),
161
+ };
162
+ }
163
+ slugifyPluginNamespace(value) {
164
+ return value
165
+ .trim()
166
+ .toLowerCase()
167
+ .replace(/[^a-z0-9._-]+/g, '-')
168
+ .replace(/^-+|-+$/g, '') || 'plugin';
169
+ }
170
+ async getClaudeNativeFeatureKinds(pluginDir, manifest) {
171
+ const featureChecks = await Promise.all([
172
+ (async () => !!manifest.commands || await isDirectory(join(pluginDir, 'commands')))(),
173
+ (async () => !!manifest.hooks || await isDirectory(join(pluginDir, 'hooks')))(),
174
+ (async () => !!manifest.agents || await isDirectory(join(pluginDir, 'agents')))(),
175
+ isDirectory(join(pluginDir, 'prompts')),
176
+ isDirectory(join(pluginDir, 'schemas')),
177
+ isDirectory(join(pluginDir, 'scripts')),
178
+ isDirectory(join(pluginDir, 'templates')),
179
+ ]);
180
+ return [
181
+ ...(featureChecks[0] ? ['commands'] : []),
182
+ ...(featureChecks[1] ? ['hooks'] : []),
183
+ ...(featureChecks[2] ? ['agents'] : []),
184
+ ...(featureChecks[3] ? ['prompts'] : []),
185
+ ...(featureChecks[4] ? ['schemas'] : []),
186
+ ...(featureChecks[5] ? ['scripts'] : []),
187
+ ...(featureChecks[6] ? ['templates'] : []),
188
+ ];
189
+ }
190
+ async getClaudeNativeInstallTarget(plugin, pluginDir) {
191
+ if (plugin.format !== 'claude') {
192
+ return null;
193
+ }
194
+ const manifestContent = await readFileIfExists(join(pluginDir, '.claude-plugin', 'plugin.json'));
195
+ if (!manifestContent) {
196
+ return null;
197
+ }
198
+ let manifest;
199
+ try {
200
+ manifest = JSON.parse(manifestContent);
201
+ }
202
+ catch {
203
+ return null;
204
+ }
205
+ const features = await this.getClaudeNativeFeatureKinds(pluginDir, manifest);
206
+ if (features.length === 0) {
207
+ return null;
208
+ }
209
+ const baseNamespace = plugin.nativeClaudeBundle?.bundleName
210
+ || plugin.source.marketplace
211
+ || (plugin.source.owner && plugin.source.repo ? `${plugin.source.owner}-${plugin.source.repo}` : plugin.name);
212
+ const namespace = `agentinit-${this.slugifyPluginNamespace(baseNamespace)}`;
213
+ const versionDir = this.slugifyPluginNamespace(plugin.version || '0.0.0');
214
+ return {
215
+ namespace,
216
+ pluginKey: `${plugin.name}@${namespace}`,
217
+ installPath: join(homedir(), '.claude', 'plugins', 'cache', namespace, plugin.name, versionDir),
218
+ features,
219
+ };
220
+ }
221
+ async readClaudeInstalledPlugins() {
222
+ const path = getClaudeInstalledPluginsPath();
223
+ const content = await readFileIfExists(path);
224
+ if (!content) {
225
+ return { version: 2, plugins: {} };
226
+ }
227
+ try {
228
+ const parsed = JSON.parse(content);
229
+ if (!parsed || parsed.version !== 2 || typeof parsed.plugins !== 'object' || parsed.plugins === null) {
230
+ return { version: 2, plugins: {} };
231
+ }
232
+ return parsed;
233
+ }
234
+ catch {
235
+ return { version: 2, plugins: {} };
236
+ }
237
+ }
238
+ async saveClaudeInstalledPlugins(state) {
239
+ await writeFile(getClaudeInstalledPluginsPath(), JSON.stringify(state, null, 2));
240
+ }
241
+ async installNativeClaudePlugin(plugin, pluginDir, agents) {
242
+ const installed = [];
243
+ const skipped = [];
244
+ const warnings = [];
245
+ const nativeTarget = await this.getClaudeNativeInstallTarget(plugin, pluginDir);
246
+ if (!nativeTarget) {
247
+ return { installed, skipped, warnings };
248
+ }
249
+ const hasClaudeTarget = agents.some(agent => agent.id === 'claude');
250
+ const featureLabel = nativeTarget.features.join(', ');
251
+ if (!hasClaudeTarget) {
252
+ warnings.push(`Claude Code-native plugin components detected (${featureLabel}), but no Claude Code target was selected; skipped native install.`);
253
+ return { installed, skipped, warnings };
254
+ }
255
+ warnings.push(`Claude Code-native plugin components detected (${featureLabel}); they will only work in Claude Code and install into ~/.claude/plugins.`);
256
+ const claudeInstalled = await this.readClaudeInstalledPlugins();
257
+ const conflictingKey = Object.keys(claudeInstalled.plugins).find(key => key !== nativeTarget.pluginKey && key.startsWith(`${plugin.name}@`));
258
+ if (conflictingKey) {
259
+ skipped.push({
260
+ agent: 'claude',
261
+ reason: `Claude plugin "${plugin.name}" is already installed as ${conflictingKey}; skipped native install to avoid duplicates.`,
262
+ });
263
+ warnings.push(`Skipped native Claude plugin install because Claude already has "${plugin.name}" installed as ${conflictingKey}.`);
264
+ return { installed, skipped, warnings };
265
+ }
266
+ await fs.rm(nativeTarget.installPath, { recursive: true, force: true }).catch(() => { });
267
+ await fs.mkdir(dirname(nativeTarget.installPath), { recursive: true });
268
+ await fs.cp(pluginDir, nativeTarget.installPath, { recursive: true, dereference: true });
269
+ const now = new Date().toISOString();
270
+ claudeInstalled.plugins[nativeTarget.pluginKey] = [{
271
+ scope: 'user',
272
+ installPath: nativeTarget.installPath,
273
+ version: plugin.version,
274
+ installedAt: now,
275
+ lastUpdated: now,
276
+ }];
277
+ await this.saveClaudeInstalledPlugins(claudeInstalled);
278
+ installed.push({
279
+ agent: 'claude',
280
+ pluginKey: nativeTarget.pluginKey,
281
+ installPath: nativeTarget.installPath,
282
+ });
283
+ warnings.push('Reload plugins in Claude Code with /reload-plugins to activate native plugin components.');
284
+ return { installed, skipped, warnings };
285
+ }
286
+ async removeNativeClaudePlugin(component) {
287
+ const claudeInstalled = await this.readClaudeInstalledPlugins();
288
+ const entries = claudeInstalled.plugins[component.pluginKey] || [];
289
+ const remainingEntries = entries.filter(entry => entry.installPath !== component.installPath);
290
+ if (remainingEntries.length > 0) {
291
+ claudeInstalled.plugins[component.pluginKey] = remainingEntries;
292
+ }
293
+ else {
294
+ delete claudeInstalled.plugins[component.pluginKey];
295
+ }
296
+ await this.saveClaudeInstalledPlugins(claudeInstalled);
297
+ await fs.rm(component.installPath, { recursive: true, force: true }).catch(() => { });
298
+ return true;
299
+ }
26
300
  // ── Source Resolution ──────────────────────────────────────────────
27
301
  /**
28
302
  * Resolve a source string into a PluginSource.
@@ -182,11 +456,7 @@ export class PluginManager {
182
456
  .filter(p => p.name.includes(name) || name.includes(p.name))
183
457
  .map(p => p.name)
184
458
  .slice(0, 5);
185
- let msg = `Plugin "${name}" not found in ${registry.name} marketplace.`;
186
- if (suggestions.length > 0) {
187
- msg += ` Did you mean: ${suggestions.join(', ')}?`;
188
- }
189
- throw new Error(msg);
459
+ throw new MarketplacePluginNotFoundError(name, registryId, registry.name, suggestions);
190
460
  }
191
461
  /**
192
462
  * List all plugins in a marketplace, optionally filtered
@@ -201,7 +471,13 @@ export class PluginManager {
201
471
  const fullDir = join(cacheDir, dir);
202
472
  if (!(await isDirectory(fullDir)))
203
473
  continue;
204
- const cat = dir === 'plugins' ? 'official' : dir === 'external_plugins' ? 'community' : dir;
474
+ const cat = dir === 'plugins'
475
+ ? 'official'
476
+ : dir === 'external_plugins'
477
+ ? 'community'
478
+ : dir.startsWith('skills/.')
479
+ ? dir.slice('skills/.'.length)
480
+ : dir;
205
481
  if (category && cat !== category)
206
482
  continue;
207
483
  const entries = await listFiles(fullDir);
@@ -312,10 +588,10 @@ export class PluginManager {
312
588
  const mcpServers = await this.parseMcpJson(pluginDir);
313
589
  // Warn about agent-specific features
314
590
  if (await isDirectory(join(pluginDir, 'hooks')) || manifest.hooks) {
315
- warnings.push('Hooks (hooks/) are Claude Code-specific and were not installed');
591
+ warnings.push('Hooks (hooks/) are Claude Code-specific');
316
592
  }
317
593
  if (await isDirectory(join(pluginDir, 'agents')) || manifest.agents) {
318
- warnings.push('Agent definitions (agents/) are Claude Code-specific and were not installed');
594
+ warnings.push('Agent definitions (agents/) are Claude Code-specific');
319
595
  }
320
596
  return {
321
597
  name: manifest.name,
@@ -500,11 +776,38 @@ ${body.trim()}
500
776
  */
501
777
  async installPlugin(source, projectPath, options = {}) {
502
778
  const resolved = this.resolveSource(source, { from: options.from });
779
+ let effectiveSource = resolved;
503
780
  let pluginDir;
504
781
  let tempDir = null;
782
+ const resolutionWarnings = [];
505
783
  // 1. Resolve source to a local directory
506
784
  if (resolved.type === 'marketplace') {
507
- pluginDir = await this.resolveMarketplacePlugin(resolved.pluginName, resolved.marketplace || 'claude');
785
+ try {
786
+ pluginDir = await this.resolveMarketplacePlugin(resolved.pluginName, resolved.marketplace || 'claude');
787
+ }
788
+ catch (error) {
789
+ if (!(error instanceof MarketplacePluginNotFoundError)) {
790
+ throw error;
791
+ }
792
+ const fallbackSource = this.deriveGitHubFallbackSource(source, resolved.marketplace);
793
+ if (!fallbackSource || !fallbackSource.url) {
794
+ throw error;
795
+ }
796
+ const fallbackUrl = this.formatGitHubRepoUrl(fallbackSource) || fallbackSource.url.replace(/\.git$/, '');
797
+ resolutionWarnings.push(error.message);
798
+ resolutionWarnings.push(`Marketplace lookup failed; trying unverified GitHub repository ${fallbackUrl} instead.`);
799
+ try {
800
+ tempDir = await this.skillsManager.cloneRepo(fallbackSource.url);
801
+ }
802
+ catch (fallbackError) {
803
+ throw new Error(`${error.message} Tried unverified GitHub repository ${fallbackUrl} but failed: ${fallbackError instanceof Error ? fallbackError.message : 'Unknown error'}`);
804
+ }
805
+ effectiveSource = {
806
+ ...fallbackSource,
807
+ ...(resolved.pluginName ? { pluginName: resolved.pluginName } : {}),
808
+ };
809
+ pluginDir = tempDir;
810
+ }
508
811
  }
509
812
  else if (resolved.type === 'github') {
510
813
  if (!resolved.url)
@@ -520,13 +823,14 @@ ${body.trim()}
520
823
  }
521
824
  try {
522
825
  // 2. Parse plugin
523
- const plugin = await this.parsePlugin(pluginDir, resolved);
826
+ const plugin = await this.loadPluginFromDirectory(pluginDir, effectiveSource, resolutionWarnings);
524
827
  // 3. If --list, return early with contents
525
828
  if (options.list) {
526
829
  return {
527
830
  plugin,
528
831
  skills: { installed: [], skipped: [] },
529
832
  mcpServers: { applied: [], skipped: [] },
833
+ nativePlugins: { installed: [], skipped: [] },
530
834
  warnings: plugin.warnings,
531
835
  };
532
836
  }
@@ -537,6 +841,7 @@ ${body.trim()}
537
841
  plugin,
538
842
  skills: { installed: [], skipped: plugin.skills.map(s => ({ name: s.name, reason: 'No target agents found' })) },
539
843
  mcpServers: { applied: [], skipped: plugin.mcpServers.map(s => ({ name: s.name, reason: 'No target agents found' })) },
844
+ nativePlugins: { installed: [], skipped: [] },
540
845
  warnings: plugin.warnings,
541
846
  };
542
847
  }
@@ -544,21 +849,25 @@ ${body.trim()}
544
849
  const skillResult = await this.installPluginSkills(plugin, projectPath, agents, options);
545
850
  // 6. Apply MCP servers per agent
546
851
  const mcpResult = await this.applyPluginMcpServers(plugin, projectPath, agents, options.global);
547
- // 7. Save to registry only when the install actually applied portable components.
548
- if (skillResult.installed.length > 0 || mcpResult.applied.length > 0) {
852
+ // 7. Install agent-native plugin payloads when supported.
853
+ const nativePluginResult = await this.installNativeClaudePlugin(plugin, plugin.resolvedPluginDir, agents);
854
+ const installWarnings = [...plugin.warnings, ...nativePluginResult.warnings];
855
+ // 8. Save to registry only when the install actually applied components.
856
+ if (skillResult.installed.length > 0 || mcpResult.applied.length > 0 || nativePluginResult.installed.length > 0) {
549
857
  const installed = {
550
858
  name: plugin.name,
551
859
  version: plugin.version,
552
860
  description: plugin.description,
553
- source: resolved,
861
+ source: effectiveSource,
554
862
  format: plugin.format,
555
863
  installedAt: new Date().toISOString(),
556
864
  scope: options.global ? 'global' : 'project',
557
865
  components: {
558
866
  skills: skillResult.installed,
559
867
  mcpServers: mcpResult.applied,
868
+ nativePlugins: nativePluginResult.installed,
560
869
  },
561
- warnings: plugin.warnings,
870
+ warnings: installWarnings,
562
871
  };
563
872
  await this.addToRegistry(installed, projectPath, options.global);
564
873
  }
@@ -566,7 +875,8 @@ ${body.trim()}
566
875
  plugin,
567
876
  skills: skillResult,
568
877
  mcpServers: mcpResult,
569
- warnings: plugin.warnings,
878
+ nativePlugins: nativePluginResult,
879
+ warnings: installWarnings,
570
880
  };
571
881
  }
572
882
  finally {
@@ -765,7 +1075,8 @@ ${body.trim()}
765
1075
  if (options.agents && options.agents.length > 0) {
766
1076
  const agentSet = new Set(options.agents);
767
1077
  plugins = plugins.filter(p => p.components.skills.some(s => agentSet.has(s.agent)) ||
768
- p.components.mcpServers.some(m => agentSet.has(m.agent)));
1078
+ p.components.mcpServers.some(m => agentSet.has(m.agent)) ||
1079
+ (p.components.nativePlugins || []).some(nativePlugin => agentSet.has(nativePlugin.agent)));
769
1080
  }
770
1081
  return plugins;
771
1082
  }
@@ -778,7 +1089,8 @@ ${body.trim()}
778
1089
  if (!plugin) {
779
1090
  return { removed: false, details: [`Plugin "${name}" not found in registry`] };
780
1091
  }
781
- if (plugin.components.skills.length === 0 && plugin.components.mcpServers.length === 0) {
1092
+ const pluginNativeComponents = plugin.components.nativePlugins || [];
1093
+ if (plugin.components.skills.length === 0 && plugin.components.mcpServers.length === 0 && pluginNativeComponents.length === 0) {
782
1094
  registry.plugins = registry.plugins.filter(p => p.name !== name);
783
1095
  await this.saveRegistry(registry, projectPath, options.global);
784
1096
  return {
@@ -877,7 +1189,48 @@ ${body.trim()}
877
1189
  ...retainedMcpServers,
878
1190
  ...targetedMcpServers.filter(mcp => !removedMcpKeys.has(`${mcp.agent}:${mcp.name}`)),
879
1191
  ];
880
- if (removedSkillPaths.size === 0 && removedMcpKeys.size === 0) {
1192
+ const targetedNativePlugins = agentFilter
1193
+ ? pluginNativeComponents.filter(nativePlugin => agentFilter.has(nativePlugin.agent))
1194
+ : pluginNativeComponents;
1195
+ const retainedNativePlugins = agentFilter
1196
+ ? pluginNativeComponents.filter(nativePlugin => !agentFilter.has(nativePlugin.agent))
1197
+ : [];
1198
+ const otherScopeRegistry = await this.getRegistry(projectPath, !options.global);
1199
+ const otherRegistryNativePlugins = otherScopeRegistry.plugins
1200
+ .flatMap(entry => entry.components.nativePlugins || []);
1201
+ const otherPluginNativePlugins = registry.plugins
1202
+ .filter(entry => entry.name !== plugin.name)
1203
+ .flatMap(entry => entry.components.nativePlugins || []);
1204
+ const remainingNativeRefs = [
1205
+ ...retainedNativePlugins,
1206
+ ...otherPluginNativePlugins,
1207
+ ...otherRegistryNativePlugins,
1208
+ ];
1209
+ const removedNativeKeys = new Set();
1210
+ for (const nativePlugin of targetedNativePlugins) {
1211
+ const nativeKey = `${nativePlugin.agent}:${nativePlugin.pluginKey}:${nativePlugin.installPath}`;
1212
+ if (removedNativeKeys.has(nativeKey)) {
1213
+ continue;
1214
+ }
1215
+ const sharedNativeInstall = remainingNativeRefs.some(other => other.installPath === nativePlugin.installPath || other.pluginKey === nativePlugin.pluginKey);
1216
+ if (sharedNativeInstall) {
1217
+ details.push(`Skipped shared native plugin payload: ${nativePlugin.pluginKey} (${nativePlugin.agent})`);
1218
+ continue;
1219
+ }
1220
+ try {
1221
+ await this.removeNativeClaudePlugin(nativePlugin);
1222
+ removedNativeKeys.add(nativeKey);
1223
+ details.push(`Removed native plugin payload: ${nativePlugin.pluginKey} (${nativePlugin.agent})`);
1224
+ }
1225
+ catch {
1226
+ details.push(`Could not remove native plugin payload: ${nativePlugin.pluginKey} (${nativePlugin.agent})`);
1227
+ }
1228
+ }
1229
+ const remainingNativePlugins = [
1230
+ ...retainedNativePlugins,
1231
+ ...targetedNativePlugins.filter(nativePlugin => !removedNativeKeys.has(`${nativePlugin.agent}:${nativePlugin.pluginKey}:${nativePlugin.installPath}`)),
1232
+ ];
1233
+ if (removedSkillPaths.size === 0 && removedMcpKeys.size === 0 && removedNativeKeys.size === 0) {
881
1234
  if (agentFilter) {
882
1235
  details.push(`No removable plugin components matched the requested agents for "${name}"`);
883
1236
  }
@@ -888,10 +1241,13 @@ ${body.trim()}
888
1241
  components: {
889
1242
  skills: remainingSkills,
890
1243
  mcpServers: remainingMcpServers,
1244
+ nativePlugins: remainingNativePlugins,
891
1245
  },
892
1246
  };
893
1247
  registry.plugins = registry.plugins.filter(p => p.name !== name);
894
- if (updatedPlugin.components.skills.length > 0 || updatedPlugin.components.mcpServers.length > 0) {
1248
+ if (updatedPlugin.components.skills.length > 0 ||
1249
+ updatedPlugin.components.mcpServers.length > 0 ||
1250
+ (updatedPlugin.components.nativePlugins || []).length > 0) {
895
1251
  registry.plugins.push(updatedPlugin);
896
1252
  details.push('Updated plugin registry');
897
1253
  }