agentinit 1.12.1 → 1.13.1

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,13 +34,390 @@ 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;
43
+ preparedInstallContexts = new Map();
22
44
  constructor(agentManager) {
23
45
  this.agentManager = agentManager || new AgentManager();
24
46
  this.skillsManager = new SkillsManager(this.agentManager);
25
47
  }
48
+ getPreparedInstallKey(source, from) {
49
+ return `${from || ''}\n${source}`;
50
+ }
51
+ async cleanupLoadedPluginContext(context) {
52
+ if (context?.tempDir) {
53
+ await fs.rm(context.tempDir, { recursive: true, force: true }).catch(() => { });
54
+ }
55
+ }
56
+ async storePreparedInstallContext(source, from, context) {
57
+ const key = this.getPreparedInstallKey(source, from);
58
+ const existing = this.preparedInstallContexts.get(key);
59
+ this.preparedInstallContexts.set(key, context);
60
+ if (existing && existing !== context) {
61
+ await this.cleanupLoadedPluginContext(existing);
62
+ }
63
+ }
64
+ takePreparedInstallContext(source, from) {
65
+ const key = this.getPreparedInstallKey(source, from);
66
+ const existing = this.preparedInstallContexts.get(key) || null;
67
+ if (existing) {
68
+ this.preparedInstallContexts.delete(key);
69
+ }
70
+ return existing;
71
+ }
72
+ async discardPreparedPlugin(source, options = {}) {
73
+ const context = this.takePreparedInstallContext(source, options.from);
74
+ await this.cleanupLoadedPluginContext(context);
75
+ }
76
+ parseGitHubRepo(url) {
77
+ const normalized = url.replace(/\.git$/, '');
78
+ const match = normalized.match(/github\.com[:/]([^/]+)\/([^/]+)/i);
79
+ if (!match) {
80
+ return null;
81
+ }
82
+ const [, owner, repo] = match;
83
+ if (!owner || !repo) {
84
+ return null;
85
+ }
86
+ return {
87
+ owner,
88
+ repo,
89
+ };
90
+ }
91
+ deriveGitHubFallbackSource(source, marketplaceId) {
92
+ const repoShorthandMatch = source.match(/^([a-zA-Z0-9._-]+)\/([a-zA-Z0-9._-]+)$/);
93
+ if (repoShorthandMatch) {
94
+ const [, owner, repo] = repoShorthandMatch;
95
+ return {
96
+ type: 'github',
97
+ url: `https://github.com/${owner}/${repo}.git`,
98
+ owner,
99
+ repo,
100
+ };
101
+ }
102
+ if (!marketplaceId || !/^[a-zA-Z0-9._-]+$/.test(source)) {
103
+ return null;
104
+ }
105
+ const registry = this.getMarketplace(marketplaceId);
106
+ const registryRepo = registry ? this.parseGitHubRepo(registry.repoUrl) : null;
107
+ if (!registryRepo) {
108
+ return null;
109
+ }
110
+ return {
111
+ type: 'github',
112
+ url: `https://github.com/${registryRepo.owner}/${source}.git`,
113
+ owner: registryRepo.owner,
114
+ repo: source,
115
+ };
116
+ }
117
+ formatGitHubRepoUrl(source) {
118
+ if (!source.owner || !source.repo) {
119
+ return null;
120
+ }
121
+ return `https://github.com/${source.owner}/${source.repo}`;
122
+ }
123
+ async resolvePreparedPluginDir(pluginDir, source) {
124
+ const claudeMarketplaceManifestPath = join(pluginDir, '.claude-plugin', 'marketplace.json');
125
+ if (!(await fileExists(claudeMarketplaceManifestPath))) {
126
+ return { pluginDir, warnings: [] };
127
+ }
128
+ const manifestContent = await readFileIfExists(claudeMarketplaceManifestPath);
129
+ if (!manifestContent) {
130
+ return { pluginDir, warnings: [] };
131
+ }
132
+ let manifest;
133
+ try {
134
+ manifest = JSON.parse(manifestContent);
135
+ }
136
+ catch {
137
+ throw new Error(`Invalid .claude-plugin/marketplace.json in ${pluginDir}`);
138
+ }
139
+ const entries = Array.isArray(manifest.plugins)
140
+ ? manifest.plugins.filter((entry) => typeof entry?.name === 'string' && typeof entry?.source === 'string')
141
+ : [];
142
+ if (entries.length === 0) {
143
+ throw new Error(`No plugins declared in .claude-plugin/marketplace.json for ${pluginDir}`);
144
+ }
145
+ const [firstEntry] = entries;
146
+ if (!firstEntry) {
147
+ throw new Error(`No plugins declared in .claude-plugin/marketplace.json for ${pluginDir}`);
148
+ }
149
+ let selectedEntry = firstEntry;
150
+ if (entries.length > 1) {
151
+ const candidates = new Set([source.pluginName, source.repo]
152
+ .filter((value) => !!value)
153
+ .flatMap(value => [value, basename(value)]));
154
+ const matched = entries.find(entry => candidates.has(entry.name) || candidates.has(basename(entry.source)));
155
+ if (!matched) {
156
+ throw new Error(`Repository "${pluginDir}" is a Claude marketplace bundle with multiple plugins. Select one of: ${entries.map(entry => entry.name).join(', ')}.`);
157
+ }
158
+ selectedEntry = matched;
159
+ }
160
+ const selectedPluginDir = resolve(pluginDir, selectedEntry.source);
161
+ const relativePath = relative(resolve(pluginDir), selectedPluginDir);
162
+ if (relativePath.startsWith('..') ||
163
+ relativePath.includes('/../') ||
164
+ relativePath.includes('\\..\\')) {
165
+ throw new Error(`Invalid bundled plugin source path "${selectedEntry.source}" in ${claudeMarketplaceManifestPath}`);
166
+ }
167
+ if (!(await isDirectory(selectedPluginDir))) {
168
+ throw new Error(`Bundled plugin path not found: ${selectedPluginDir}`);
169
+ }
170
+ const sourceLabel = this.formatGitHubRepoUrl(source) || pluginDir;
171
+ return {
172
+ pluginDir: selectedPluginDir,
173
+ warnings: [
174
+ `Source "${sourceLabel}" is a Claude Code marketplace bundle; using bundled plugin "${selectedEntry.name}".`,
175
+ ],
176
+ claudeBundle: {
177
+ bundleName: manifest.name || selectedEntry.name,
178
+ pluginName: selectedEntry.name,
179
+ },
180
+ };
181
+ }
182
+ async loadPluginFromDirectory(pluginDir, source, warnings = []) {
183
+ const preparedPlugin = await this.resolvePreparedPluginDir(pluginDir, source);
184
+ const parsedPlugin = await this.parsePlugin(preparedPlugin.pluginDir, source);
185
+ return {
186
+ ...parsedPlugin,
187
+ warnings: [...warnings, ...preparedPlugin.warnings, ...parsedPlugin.warnings],
188
+ resolvedPluginDir: preparedPlugin.pluginDir,
189
+ ...(preparedPlugin.claudeBundle ? { nativeClaudeBundle: preparedPlugin.claudeBundle } : {}),
190
+ };
191
+ }
192
+ async loadPluginContext(source, options = {}) {
193
+ const resolved = this.resolveSource(source, { from: options.from });
194
+ let effectiveSource = resolved;
195
+ let pluginDir;
196
+ let tempDir = null;
197
+ const resolutionWarnings = [];
198
+ if (resolved.type === 'marketplace') {
199
+ try {
200
+ pluginDir = await this.resolveMarketplacePlugin(resolved.pluginName, resolved.marketplace || 'claude');
201
+ }
202
+ catch (error) {
203
+ if (!(error instanceof MarketplacePluginNotFoundError)) {
204
+ throw error;
205
+ }
206
+ const fallbackSource = this.deriveGitHubFallbackSource(source, resolved.marketplace);
207
+ if (!fallbackSource || !fallbackSource.url) {
208
+ throw error;
209
+ }
210
+ const fallbackUrl = this.formatGitHubRepoUrl(fallbackSource) || fallbackSource.url.replace(/\.git$/, '');
211
+ resolutionWarnings.push(error.message);
212
+ resolutionWarnings.push(`Marketplace lookup failed; trying unverified GitHub repository ${fallbackUrl} instead.`);
213
+ try {
214
+ tempDir = await this.skillsManager.cloneRepo(fallbackSource.url);
215
+ }
216
+ catch (fallbackError) {
217
+ throw new Error(`${error.message} Tried unverified GitHub repository ${fallbackUrl} but failed: ${fallbackError instanceof Error ? fallbackError.message : 'Unknown error'}`);
218
+ }
219
+ effectiveSource = {
220
+ ...fallbackSource,
221
+ ...(resolved.pluginName ? { pluginName: resolved.pluginName } : {}),
222
+ };
223
+ pluginDir = tempDir;
224
+ }
225
+ }
226
+ else if (resolved.type === 'github') {
227
+ if (!resolved.url)
228
+ throw new Error(`Invalid source: ${source}`);
229
+ tempDir = await this.skillsManager.cloneRepo(resolved.url);
230
+ pluginDir = tempDir;
231
+ }
232
+ else {
233
+ pluginDir = resolve(resolved.path || source);
234
+ if (!(await fileExists(pluginDir))) {
235
+ throw new Error(`Local path not found: ${pluginDir}`);
236
+ }
237
+ }
238
+ return {
239
+ plugin: await this.loadPluginFromDirectory(pluginDir, effectiveSource, resolutionWarnings),
240
+ effectiveSource,
241
+ tempDir,
242
+ };
243
+ }
244
+ async buildPluginInspection(context) {
245
+ const nativeTarget = await this.getClaudeNativeInstallTarget(context.plugin, context.plugin.resolvedPluginDir);
246
+ return {
247
+ plugin: context.plugin,
248
+ nativePreview: nativeTarget ? this.toNativePluginPreview(nativeTarget) : null,
249
+ };
250
+ }
251
+ async preparePluginInstall(source, options = {}) {
252
+ const context = await this.loadPluginContext(source, options);
253
+ try {
254
+ const inspection = await this.buildPluginInspection(context);
255
+ await this.storePreparedInstallContext(source, options.from, context);
256
+ return inspection;
257
+ }
258
+ catch (error) {
259
+ await this.cleanupLoadedPluginContext(context);
260
+ throw error;
261
+ }
262
+ }
263
+ async inspectPlugin(source, options = {}) {
264
+ const context = await this.loadPluginContext(source, options);
265
+ try {
266
+ return await this.buildPluginInspection(context);
267
+ }
268
+ finally {
269
+ await this.cleanupLoadedPluginContext(context);
270
+ }
271
+ }
272
+ slugifyPluginNamespace(value) {
273
+ return value
274
+ .trim()
275
+ .toLowerCase()
276
+ .replace(/[^a-z0-9._-]+/g, '-')
277
+ .replace(/^-+|-+$/g, '') || 'plugin';
278
+ }
279
+ async getClaudeNativeFeatureKinds(pluginDir, manifest) {
280
+ const featureChecks = await Promise.all([
281
+ (async () => !!manifest.commands || await isDirectory(join(pluginDir, 'commands')))(),
282
+ (async () => !!manifest.hooks || await isDirectory(join(pluginDir, 'hooks')))(),
283
+ (async () => !!manifest.agents || await isDirectory(join(pluginDir, 'agents')))(),
284
+ isDirectory(join(pluginDir, 'prompts')),
285
+ isDirectory(join(pluginDir, 'schemas')),
286
+ isDirectory(join(pluginDir, 'scripts')),
287
+ isDirectory(join(pluginDir, 'templates')),
288
+ ]);
289
+ return [
290
+ ...(featureChecks[0] ? ['commands'] : []),
291
+ ...(featureChecks[1] ? ['hooks'] : []),
292
+ ...(featureChecks[2] ? ['agents'] : []),
293
+ ...(featureChecks[3] ? ['prompts'] : []),
294
+ ...(featureChecks[4] ? ['schemas'] : []),
295
+ ...(featureChecks[5] ? ['scripts'] : []),
296
+ ...(featureChecks[6] ? ['templates'] : []),
297
+ ];
298
+ }
299
+ async getClaudeNativeInstallTarget(plugin, pluginDir) {
300
+ if (plugin.format !== 'claude') {
301
+ return null;
302
+ }
303
+ const manifestContent = await readFileIfExists(join(pluginDir, '.claude-plugin', 'plugin.json'));
304
+ if (!manifestContent) {
305
+ return null;
306
+ }
307
+ let manifest;
308
+ try {
309
+ manifest = JSON.parse(manifestContent);
310
+ }
311
+ catch {
312
+ return null;
313
+ }
314
+ const features = await this.getClaudeNativeFeatureKinds(pluginDir, manifest);
315
+ if (features.length === 0) {
316
+ return null;
317
+ }
318
+ const baseNamespace = plugin.nativeClaudeBundle?.bundleName
319
+ || plugin.source.marketplace
320
+ || (plugin.source.owner && plugin.source.repo ? `${plugin.source.owner}-${plugin.source.repo}` : plugin.name);
321
+ const namespace = `agentinit-${this.slugifyPluginNamespace(baseNamespace)}`;
322
+ const versionDir = this.slugifyPluginNamespace(plugin.version || '0.0.0');
323
+ return {
324
+ namespace,
325
+ pluginKey: `${plugin.name}@${namespace}`,
326
+ installPath: join(homedir(), '.claude', 'plugins', 'cache', namespace, plugin.name, versionDir),
327
+ features,
328
+ };
329
+ }
330
+ async readClaudeInstalledPlugins() {
331
+ const path = getClaudeInstalledPluginsPath();
332
+ const content = await readFileIfExists(path);
333
+ if (!content) {
334
+ return { version: 2, plugins: {} };
335
+ }
336
+ try {
337
+ const parsed = JSON.parse(content);
338
+ if (!parsed || parsed.version !== 2 || typeof parsed.plugins !== 'object' || parsed.plugins === null) {
339
+ return { version: 2, plugins: {} };
340
+ }
341
+ return parsed;
342
+ }
343
+ catch {
344
+ return { version: 2, plugins: {} };
345
+ }
346
+ }
347
+ async saveClaudeInstalledPlugins(state) {
348
+ await writeFile(getClaudeInstalledPluginsPath(), JSON.stringify(state, null, 2));
349
+ }
350
+ async installNativeClaudePlugin(plugin, pluginDir, agents) {
351
+ const installed = [];
352
+ const skipped = [];
353
+ const warnings = [];
354
+ const nativeTarget = await this.getClaudeNativeInstallTarget(plugin, pluginDir);
355
+ if (!nativeTarget) {
356
+ return { installed, skipped, warnings };
357
+ }
358
+ const hasClaudeTarget = agents.some(agent => agent.id === 'claude');
359
+ const featureLabel = nativeTarget.features.join(', ');
360
+ if (!hasClaudeTarget) {
361
+ skipped.push({
362
+ agent: 'claude',
363
+ reason: `Claude Code was not selected; skipped native plugin components (${featureLabel}).`,
364
+ });
365
+ warnings.push(`Claude Code-native plugin components detected (${featureLabel}), but no Claude Code target was selected; skipped native install.`);
366
+ return { installed, skipped, warnings };
367
+ }
368
+ warnings.push(`Claude Code-native plugin components detected (${featureLabel}); they will only work in Claude Code and install into ~/.claude/plugins.`);
369
+ const claudeInstalled = await this.readClaudeInstalledPlugins();
370
+ const conflictingKey = Object.keys(claudeInstalled.plugins).find(key => key !== nativeTarget.pluginKey && key.startsWith(`${plugin.name}@`));
371
+ if (conflictingKey) {
372
+ skipped.push({
373
+ agent: 'claude',
374
+ reason: `Claude plugin "${plugin.name}" is already installed as ${conflictingKey}; skipped native install to avoid duplicates.`,
375
+ });
376
+ warnings.push(`Skipped native Claude plugin install because Claude already has "${plugin.name}" installed as ${conflictingKey}.`);
377
+ return { installed, skipped, warnings };
378
+ }
379
+ await fs.rm(nativeTarget.installPath, { recursive: true, force: true }).catch(() => { });
380
+ await fs.mkdir(dirname(nativeTarget.installPath), { recursive: true });
381
+ await fs.cp(pluginDir, nativeTarget.installPath, { recursive: true, dereference: true });
382
+ const now = new Date().toISOString();
383
+ claudeInstalled.plugins[nativeTarget.pluginKey] = [{
384
+ scope: 'user',
385
+ installPath: nativeTarget.installPath,
386
+ version: plugin.version,
387
+ installedAt: now,
388
+ lastUpdated: now,
389
+ }];
390
+ await this.saveClaudeInstalledPlugins(claudeInstalled);
391
+ installed.push({
392
+ agent: 'claude',
393
+ pluginKey: nativeTarget.pluginKey,
394
+ installPath: nativeTarget.installPath,
395
+ });
396
+ warnings.push('Reload plugins in Claude Code with /reload-plugins to activate native plugin components.');
397
+ return { installed, skipped, warnings };
398
+ }
399
+ toNativePluginPreview(target) {
400
+ return {
401
+ agent: 'claude',
402
+ pluginKey: target.pluginKey,
403
+ installPath: target.installPath,
404
+ features: target.features,
405
+ };
406
+ }
407
+ async removeNativeClaudePlugin(component) {
408
+ const claudeInstalled = await this.readClaudeInstalledPlugins();
409
+ const entries = claudeInstalled.plugins[component.pluginKey] || [];
410
+ const remainingEntries = entries.filter(entry => entry.installPath !== component.installPath);
411
+ if (remainingEntries.length > 0) {
412
+ claudeInstalled.plugins[component.pluginKey] = remainingEntries;
413
+ }
414
+ else {
415
+ delete claudeInstalled.plugins[component.pluginKey];
416
+ }
417
+ await this.saveClaudeInstalledPlugins(claudeInstalled);
418
+ await fs.rm(component.installPath, { recursive: true, force: true }).catch(() => { });
419
+ return true;
420
+ }
26
421
  // ── Source Resolution ──────────────────────────────────────────────
27
422
  /**
28
423
  * Resolve a source string into a PluginSource.
@@ -182,11 +577,7 @@ export class PluginManager {
182
577
  .filter(p => p.name.includes(name) || name.includes(p.name))
183
578
  .map(p => p.name)
184
579
  .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);
580
+ throw new MarketplacePluginNotFoundError(name, registryId, registry.name, suggestions);
190
581
  }
191
582
  /**
192
583
  * List all plugins in a marketplace, optionally filtered
@@ -201,7 +592,13 @@ export class PluginManager {
201
592
  const fullDir = join(cacheDir, dir);
202
593
  if (!(await isDirectory(fullDir)))
203
594
  continue;
204
- const cat = dir === 'plugins' ? 'official' : dir === 'external_plugins' ? 'community' : dir;
595
+ const cat = dir === 'plugins'
596
+ ? 'official'
597
+ : dir === 'external_plugins'
598
+ ? 'community'
599
+ : dir.startsWith('skills/.')
600
+ ? dir.slice('skills/.'.length)
601
+ : dir;
205
602
  if (category && cat !== category)
206
603
  continue;
207
604
  const entries = await listFiles(fullDir);
@@ -312,10 +709,10 @@ export class PluginManager {
312
709
  const mcpServers = await this.parseMcpJson(pluginDir);
313
710
  // Warn about agent-specific features
314
711
  if (await isDirectory(join(pluginDir, 'hooks')) || manifest.hooks) {
315
- warnings.push('Hooks (hooks/) are Claude Code-specific and were not installed');
712
+ warnings.push('Hooks (hooks/) are Claude Code-specific');
316
713
  }
317
714
  if (await isDirectory(join(pluginDir, 'agents')) || manifest.agents) {
318
- warnings.push('Agent definitions (agents/) are Claude Code-specific and were not installed');
715
+ warnings.push('Agent definitions (agents/) are Claude Code-specific');
319
716
  }
320
717
  return {
321
718
  name: manifest.name,
@@ -499,34 +896,18 @@ ${body.trim()}
499
896
  * This is the main one-liner entry point.
500
897
  */
501
898
  async installPlugin(source, projectPath, options = {}) {
502
- const resolved = this.resolveSource(source, { from: options.from });
503
- let pluginDir;
504
- let tempDir = null;
505
- // 1. Resolve source to a local directory
506
- if (resolved.type === 'marketplace') {
507
- pluginDir = await this.resolveMarketplacePlugin(resolved.pluginName, resolved.marketplace || 'claude');
508
- }
509
- else if (resolved.type === 'github') {
510
- if (!resolved.url)
511
- throw new Error(`Invalid source: ${source}`);
512
- tempDir = await this.skillsManager.cloneRepo(resolved.url);
513
- pluginDir = tempDir;
514
- }
515
- else {
516
- pluginDir = resolve(resolved.path || source);
517
- if (!(await fileExists(pluginDir))) {
518
- throw new Error(`Local path not found: ${pluginDir}`);
519
- }
520
- }
899
+ const context = this.takePreparedInstallContext(source, options.from)
900
+ || await this.loadPluginContext(source, { from: options.from });
521
901
  try {
522
902
  // 2. Parse plugin
523
- const plugin = await this.parsePlugin(pluginDir, resolved);
903
+ const plugin = context.plugin;
524
904
  // 3. If --list, return early with contents
525
905
  if (options.list) {
526
906
  return {
527
907
  plugin,
528
908
  skills: { installed: [], skipped: [] },
529
909
  mcpServers: { applied: [], skipped: [] },
910
+ nativePlugins: { installed: [], skipped: [] },
530
911
  warnings: plugin.warnings,
531
912
  };
532
913
  }
@@ -537,6 +918,7 @@ ${body.trim()}
537
918
  plugin,
538
919
  skills: { installed: [], skipped: plugin.skills.map(s => ({ name: s.name, reason: 'No target agents found' })) },
539
920
  mcpServers: { applied: [], skipped: plugin.mcpServers.map(s => ({ name: s.name, reason: 'No target agents found' })) },
921
+ nativePlugins: { installed: [], skipped: [] },
540
922
  warnings: plugin.warnings,
541
923
  };
542
924
  }
@@ -544,21 +926,25 @@ ${body.trim()}
544
926
  const skillResult = await this.installPluginSkills(plugin, projectPath, agents, options);
545
927
  // 6. Apply MCP servers per agent
546
928
  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) {
929
+ // 7. Install agent-native plugin payloads when supported.
930
+ const nativePluginResult = await this.installNativeClaudePlugin(plugin, plugin.resolvedPluginDir, agents);
931
+ const installWarnings = [...plugin.warnings, ...nativePluginResult.warnings];
932
+ // 8. Save to registry only when the install actually applied components.
933
+ if (skillResult.installed.length > 0 || mcpResult.applied.length > 0 || nativePluginResult.installed.length > 0) {
549
934
  const installed = {
550
935
  name: plugin.name,
551
936
  version: plugin.version,
552
937
  description: plugin.description,
553
- source: resolved,
938
+ source: context.effectiveSource,
554
939
  format: plugin.format,
555
940
  installedAt: new Date().toISOString(),
556
941
  scope: options.global ? 'global' : 'project',
557
942
  components: {
558
943
  skills: skillResult.installed,
559
944
  mcpServers: mcpResult.applied,
945
+ nativePlugins: nativePluginResult.installed,
560
946
  },
561
- warnings: plugin.warnings,
947
+ warnings: installWarnings,
562
948
  };
563
949
  await this.addToRegistry(installed, projectPath, options.global);
564
950
  }
@@ -566,13 +952,12 @@ ${body.trim()}
566
952
  plugin,
567
953
  skills: skillResult,
568
954
  mcpServers: mcpResult,
569
- warnings: plugin.warnings,
955
+ nativePlugins: nativePluginResult,
956
+ warnings: installWarnings,
570
957
  };
571
958
  }
572
959
  finally {
573
- if (tempDir) {
574
- await fs.rm(tempDir, { recursive: true, force: true }).catch(() => { });
575
- }
960
+ await this.cleanupLoadedPluginContext(context);
576
961
  }
577
962
  }
578
963
  /**
@@ -765,7 +1150,8 @@ ${body.trim()}
765
1150
  if (options.agents && options.agents.length > 0) {
766
1151
  const agentSet = new Set(options.agents);
767
1152
  plugins = plugins.filter(p => p.components.skills.some(s => agentSet.has(s.agent)) ||
768
- p.components.mcpServers.some(m => agentSet.has(m.agent)));
1153
+ p.components.mcpServers.some(m => agentSet.has(m.agent)) ||
1154
+ (p.components.nativePlugins || []).some(nativePlugin => agentSet.has(nativePlugin.agent)));
769
1155
  }
770
1156
  return plugins;
771
1157
  }
@@ -778,7 +1164,8 @@ ${body.trim()}
778
1164
  if (!plugin) {
779
1165
  return { removed: false, details: [`Plugin "${name}" not found in registry`] };
780
1166
  }
781
- if (plugin.components.skills.length === 0 && plugin.components.mcpServers.length === 0) {
1167
+ const pluginNativeComponents = plugin.components.nativePlugins || [];
1168
+ if (plugin.components.skills.length === 0 && plugin.components.mcpServers.length === 0 && pluginNativeComponents.length === 0) {
782
1169
  registry.plugins = registry.plugins.filter(p => p.name !== name);
783
1170
  await this.saveRegistry(registry, projectPath, options.global);
784
1171
  return {
@@ -877,7 +1264,48 @@ ${body.trim()}
877
1264
  ...retainedMcpServers,
878
1265
  ...targetedMcpServers.filter(mcp => !removedMcpKeys.has(`${mcp.agent}:${mcp.name}`)),
879
1266
  ];
880
- if (removedSkillPaths.size === 0 && removedMcpKeys.size === 0) {
1267
+ const targetedNativePlugins = agentFilter
1268
+ ? pluginNativeComponents.filter(nativePlugin => agentFilter.has(nativePlugin.agent))
1269
+ : pluginNativeComponents;
1270
+ const retainedNativePlugins = agentFilter
1271
+ ? pluginNativeComponents.filter(nativePlugin => !agentFilter.has(nativePlugin.agent))
1272
+ : [];
1273
+ const otherScopeRegistry = await this.getRegistry(projectPath, !options.global);
1274
+ const otherRegistryNativePlugins = otherScopeRegistry.plugins
1275
+ .flatMap(entry => entry.components.nativePlugins || []);
1276
+ const otherPluginNativePlugins = registry.plugins
1277
+ .filter(entry => entry.name !== plugin.name)
1278
+ .flatMap(entry => entry.components.nativePlugins || []);
1279
+ const remainingNativeRefs = [
1280
+ ...retainedNativePlugins,
1281
+ ...otherPluginNativePlugins,
1282
+ ...otherRegistryNativePlugins,
1283
+ ];
1284
+ const removedNativeKeys = new Set();
1285
+ for (const nativePlugin of targetedNativePlugins) {
1286
+ const nativeKey = `${nativePlugin.agent}:${nativePlugin.pluginKey}:${nativePlugin.installPath}`;
1287
+ if (removedNativeKeys.has(nativeKey)) {
1288
+ continue;
1289
+ }
1290
+ const sharedNativeInstall = remainingNativeRefs.some(other => other.installPath === nativePlugin.installPath || other.pluginKey === nativePlugin.pluginKey);
1291
+ if (sharedNativeInstall) {
1292
+ details.push(`Skipped shared native plugin payload: ${nativePlugin.pluginKey} (${nativePlugin.agent})`);
1293
+ continue;
1294
+ }
1295
+ try {
1296
+ await this.removeNativeClaudePlugin(nativePlugin);
1297
+ removedNativeKeys.add(nativeKey);
1298
+ details.push(`Removed native plugin payload: ${nativePlugin.pluginKey} (${nativePlugin.agent})`);
1299
+ }
1300
+ catch {
1301
+ details.push(`Could not remove native plugin payload: ${nativePlugin.pluginKey} (${nativePlugin.agent})`);
1302
+ }
1303
+ }
1304
+ const remainingNativePlugins = [
1305
+ ...retainedNativePlugins,
1306
+ ...targetedNativePlugins.filter(nativePlugin => !removedNativeKeys.has(`${nativePlugin.agent}:${nativePlugin.pluginKey}:${nativePlugin.installPath}`)),
1307
+ ];
1308
+ if (removedSkillPaths.size === 0 && removedMcpKeys.size === 0 && removedNativeKeys.size === 0) {
881
1309
  if (agentFilter) {
882
1310
  details.push(`No removable plugin components matched the requested agents for "${name}"`);
883
1311
  }
@@ -888,10 +1316,13 @@ ${body.trim()}
888
1316
  components: {
889
1317
  skills: remainingSkills,
890
1318
  mcpServers: remainingMcpServers,
1319
+ nativePlugins: remainingNativePlugins,
891
1320
  },
892
1321
  };
893
1322
  registry.plugins = registry.plugins.filter(p => p.name !== name);
894
- if (updatedPlugin.components.skills.length > 0 || updatedPlugin.components.mcpServers.length > 0) {
1323
+ if (updatedPlugin.components.skills.length > 0 ||
1324
+ updatedPlugin.components.mcpServers.length > 0 ||
1325
+ (updatedPlugin.components.nativePlugins || []).length > 0) {
895
1326
  registry.plugins.push(updatedPlugin);
896
1327
  details.push('Updated plugin registry');
897
1328
  }