agentinit 1.6.0 → 1.8.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.
Files changed (173) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +220 -114
  3. package/dist/agents/Agent.d.ts +44 -0
  4. package/dist/agents/Agent.d.ts.map +1 -1
  5. package/dist/agents/Agent.js +111 -22
  6. package/dist/agents/Agent.js.map +1 -1
  7. package/dist/agents/AiderAgent.d.ts +9 -0
  8. package/dist/agents/AiderAgent.d.ts.map +1 -0
  9. package/dist/agents/AiderAgent.js +135 -0
  10. package/dist/agents/AiderAgent.js.map +1 -0
  11. package/dist/agents/ClaudeAgent.d.ts +4 -0
  12. package/dist/agents/ClaudeAgent.d.ts.map +1 -1
  13. package/dist/agents/ClaudeAgent.js +33 -18
  14. package/dist/agents/ClaudeAgent.js.map +1 -1
  15. package/dist/agents/ClaudeDesktopAgent.d.ts +5 -0
  16. package/dist/agents/ClaudeDesktopAgent.d.ts.map +1 -1
  17. package/dist/agents/ClaudeDesktopAgent.js +31 -1
  18. package/dist/agents/ClaudeDesktopAgent.js.map +1 -1
  19. package/dist/agents/ClineAgent.d.ts +8 -0
  20. package/dist/agents/ClineAgent.d.ts.map +1 -0
  21. package/dist/agents/ClineAgent.js +42 -0
  22. package/dist/agents/ClineAgent.js.map +1 -0
  23. package/dist/agents/CodexCliAgent.d.ts +4 -0
  24. package/dist/agents/CodexCliAgent.d.ts.map +1 -1
  25. package/dist/agents/CodexCliAgent.js +38 -8
  26. package/dist/agents/CodexCliAgent.js.map +1 -1
  27. package/dist/agents/CopilotAgent.d.ts +9 -0
  28. package/dist/agents/CopilotAgent.d.ts.map +1 -0
  29. package/dist/agents/CopilotAgent.js +131 -0
  30. package/dist/agents/CopilotAgent.js.map +1 -0
  31. package/dist/agents/CursorAgent.d.ts +4 -4
  32. package/dist/agents/CursorAgent.d.ts.map +1 -1
  33. package/dist/agents/CursorAgent.js +32 -43
  34. package/dist/agents/CursorAgent.js.map +1 -1
  35. package/dist/agents/DroidAgent.d.ts +5 -0
  36. package/dist/agents/DroidAgent.d.ts.map +1 -1
  37. package/dist/agents/DroidAgent.js +37 -18
  38. package/dist/agents/DroidAgent.js.map +1 -1
  39. package/dist/agents/GeminiCliAgent.d.ts +4 -0
  40. package/dist/agents/GeminiCliAgent.d.ts.map +1 -1
  41. package/dist/agents/GeminiCliAgent.js +37 -8
  42. package/dist/agents/GeminiCliAgent.js.map +1 -1
  43. package/dist/agents/MarkdownRulesAgent.d.ts +13 -0
  44. package/dist/agents/MarkdownRulesAgent.d.ts.map +1 -0
  45. package/dist/agents/MarkdownRulesAgent.js +53 -0
  46. package/dist/agents/MarkdownRulesAgent.js.map +1 -0
  47. package/dist/agents/RooCodeAgent.d.ts +9 -0
  48. package/dist/agents/RooCodeAgent.d.ts.map +1 -0
  49. package/dist/agents/RooCodeAgent.js +131 -0
  50. package/dist/agents/RooCodeAgent.js.map +1 -0
  51. package/dist/agents/WindsurfAgent.d.ts +9 -0
  52. package/dist/agents/WindsurfAgent.d.ts.map +1 -0
  53. package/dist/agents/WindsurfAgent.js +127 -0
  54. package/dist/agents/WindsurfAgent.js.map +1 -0
  55. package/dist/agents/ZedAgent.d.ts +9 -0
  56. package/dist/agents/ZedAgent.d.ts.map +1 -0
  57. package/dist/agents/ZedAgent.js +127 -0
  58. package/dist/agents/ZedAgent.js.map +1 -0
  59. package/dist/cli.js +29992 -11107
  60. package/dist/cli.js.map +1 -1
  61. package/dist/commands/apply.d.ts +10 -0
  62. package/dist/commands/apply.d.ts.map +1 -1
  63. package/dist/commands/apply.js +136 -5
  64. package/dist/commands/apply.js.map +1 -1
  65. package/dist/commands/detect.d.ts.map +1 -1
  66. package/dist/commands/detect.js +25 -0
  67. package/dist/commands/detect.js.map +1 -1
  68. package/dist/commands/init.js +1 -1
  69. package/dist/commands/init.js.map +1 -1
  70. package/dist/commands/mcp.d.ts +2 -7
  71. package/dist/commands/mcp.d.ts.map +1 -1
  72. package/dist/commands/mcp.js +541 -110
  73. package/dist/commands/mcp.js.map +1 -1
  74. package/dist/commands/plugins.d.ts +3 -0
  75. package/dist/commands/plugins.d.ts.map +1 -0
  76. package/dist/commands/plugins.js +309 -0
  77. package/dist/commands/plugins.js.map +1 -0
  78. package/dist/commands/revert.d.ts +7 -0
  79. package/dist/commands/revert.d.ts.map +1 -0
  80. package/dist/commands/revert.js +48 -0
  81. package/dist/commands/revert.js.map +1 -0
  82. package/dist/commands/rules.d.ts +3 -0
  83. package/dist/commands/rules.d.ts.map +1 -0
  84. package/dist/commands/rules.js +354 -0
  85. package/dist/commands/rules.js.map +1 -0
  86. package/dist/commands/skills.d.ts +3 -0
  87. package/dist/commands/skills.d.ts.map +1 -0
  88. package/dist/commands/skills.js +179 -0
  89. package/dist/commands/skills.js.map +1 -0
  90. package/dist/commands/sync.d.ts +1 -0
  91. package/dist/commands/sync.d.ts.map +1 -1
  92. package/dist/commands/sync.js +18 -1
  93. package/dist/commands/sync.js.map +1 -1
  94. package/dist/commands/verifyMcp.d.ts.map +1 -1
  95. package/dist/commands/verifyMcp.js +27 -5
  96. package/dist/commands/verifyMcp.js.map +1 -1
  97. package/dist/constants/index.d.ts +1 -1
  98. package/dist/constants/index.d.ts.map +1 -1
  99. package/dist/constants/index.js +1 -1
  100. package/dist/constants/index.js.map +1 -1
  101. package/dist/constants/mcp.d.ts +1 -0
  102. package/dist/constants/mcp.d.ts.map +1 -1
  103. package/dist/constants/mcp.js +2 -0
  104. package/dist/constants/mcp.js.map +1 -1
  105. package/dist/core/agentDetector.d.ts.map +1 -1
  106. package/dist/core/agentDetector.js +8 -2
  107. package/dist/core/agentDetector.js.map +1 -1
  108. package/dist/core/agentManager.d.ts.map +1 -1
  109. package/dist/core/agentManager.js +12 -0
  110. package/dist/core/agentManager.js.map +1 -1
  111. package/dist/core/gitignoreManager.d.ts +8 -0
  112. package/dist/core/gitignoreManager.d.ts.map +1 -0
  113. package/dist/core/gitignoreManager.js +114 -0
  114. package/dist/core/gitignoreManager.js.map +1 -0
  115. package/dist/core/managedState.d.ts +42 -0
  116. package/dist/core/managedState.d.ts.map +1 -0
  117. package/dist/core/managedState.js +194 -0
  118. package/dist/core/managedState.js.map +1 -0
  119. package/dist/core/mcpClient.d.ts +124 -6
  120. package/dist/core/mcpClient.d.ts.map +1 -1
  121. package/dist/core/mcpClient.js +385 -39
  122. package/dist/core/mcpClient.js.map +1 -1
  123. package/dist/core/pluginManager.d.ts +134 -0
  124. package/dist/core/pluginManager.d.ts.map +1 -0
  125. package/dist/core/pluginManager.js +845 -0
  126. package/dist/core/pluginManager.js.map +1 -0
  127. package/dist/core/projectSkills.d.ts +19 -0
  128. package/dist/core/projectSkills.d.ts.map +1 -0
  129. package/dist/core/projectSkills.js +105 -0
  130. package/dist/core/projectSkills.js.map +1 -0
  131. package/dist/core/propagator.d.ts +8 -1
  132. package/dist/core/propagator.d.ts.map +1 -1
  133. package/dist/core/propagator.js +179 -36
  134. package/dist/core/propagator.js.map +1 -1
  135. package/dist/core/rulesApplicator.d.ts +0 -4
  136. package/dist/core/rulesApplicator.d.ts.map +1 -1
  137. package/dist/core/rulesApplicator.js +8 -39
  138. package/dist/core/rulesApplicator.js.map +1 -1
  139. package/dist/core/rulesTemplateLoader.js +2 -2
  140. package/dist/core/rulesTemplateLoader.js.map +1 -1
  141. package/dist/core/skillsManager.d.ts +61 -0
  142. package/dist/core/skillsManager.d.ts.map +1 -0
  143. package/dist/core/skillsManager.js +407 -0
  144. package/dist/core/skillsManager.js.map +1 -0
  145. package/dist/lib/utils/index.d.ts +3 -1
  146. package/dist/lib/utils/index.d.ts.map +1 -1
  147. package/dist/lib/utils/index.js +4 -1
  148. package/dist/lib/utils/index.js.map +1 -1
  149. package/dist/types/index.d.ts +24 -1
  150. package/dist/types/index.d.ts.map +1 -1
  151. package/dist/types/index.js.map +1 -1
  152. package/dist/types/jsonSchema.d.ts +31 -0
  153. package/dist/types/jsonSchema.d.ts.map +1 -0
  154. package/dist/types/jsonSchema.js +6 -0
  155. package/dist/types/jsonSchema.js.map +1 -0
  156. package/dist/types/plugins.d.ts +161 -0
  157. package/dist/types/plugins.d.ts.map +1 -0
  158. package/dist/types/plugins.js +2 -0
  159. package/dist/types/plugins.js.map +1 -0
  160. package/dist/types/skills.d.ts +50 -0
  161. package/dist/types/skills.d.ts.map +1 -0
  162. package/dist/types/skills.js +2 -0
  163. package/dist/types/skills.js.map +1 -0
  164. package/dist/utils/packageVersion.d.ts +105 -0
  165. package/dist/utils/packageVersion.d.ts.map +1 -0
  166. package/dist/utils/packageVersion.js +219 -0
  167. package/dist/utils/packageVersion.js.map +1 -0
  168. package/package.json +7 -2
  169. package/dist/agentinit-1.6.0.tgz +0 -0
  170. package/dist/registry/mcpRegistry.d.ts +0 -12
  171. package/dist/registry/mcpRegistry.d.ts.map +0 -1
  172. package/dist/registry/mcpRegistry.js +0 -114
  173. package/dist/registry/mcpRegistry.js.map +0 -1
@@ -0,0 +1,845 @@
1
+ import { resolve, join, basename } from 'path';
2
+ import { promises as fs } from 'fs';
3
+ import { homedir } from 'os';
4
+ import matter from 'gray-matter';
5
+ import { readFileIfExists, fileExists, isDirectory, listFiles, writeFile } from '../utils/fs.js';
6
+ import { AgentManager } from './agentManager.js';
7
+ import { MCPFilter } from './mcpFilter.js';
8
+ import { SkillsManager } from './skillsManager.js';
9
+ /**
10
+ * Built-in marketplace registries
11
+ */
12
+ const MARKETPLACES = [
13
+ {
14
+ id: 'claude',
15
+ name: 'Claude Plugins Official',
16
+ repoUrl: 'https://github.com/anthropics/claude-plugins-official.git',
17
+ pluginDirs: ['plugins', 'external_plugins'],
18
+ cacheTtlMs: 3600000, // 1 hour
19
+ },
20
+ // Future: cursor, codex, gemini registries
21
+ ];
22
+ function getMarketplaceCacheDir(registryId) {
23
+ return join(homedir(), '.agentinit', 'marketplace-cache', registryId);
24
+ }
25
+ function getRegistryPath(projectPath, global) {
26
+ if (global) {
27
+ return join(homedir(), '.agentinit', 'plugins.json');
28
+ }
29
+ return join(projectPath, '.agentinit', 'plugins.json');
30
+ }
31
+ export class PluginManager {
32
+ agentManager;
33
+ skillsManager;
34
+ constructor(agentManager) {
35
+ this.agentManager = agentManager || new AgentManager();
36
+ this.skillsManager = new SkillsManager(this.agentManager);
37
+ }
38
+ // ── Source Resolution ──────────────────────────────────────────────
39
+ /**
40
+ * Resolve a source string into a PluginSource.
41
+ * Supported forms:
42
+ * - local path
43
+ * - full GitHub URL / git URL
44
+ * - marketplace prefix: <marketplace>/<plugin>
45
+ * - GitHub shorthand: owner/repo
46
+ * - marketplace override via --from <marketplace> <plugin>
47
+ */
48
+ resolveSource(source, options) {
49
+ // Local path
50
+ if (source.startsWith('.') || source.startsWith('/') || source.startsWith('~')) {
51
+ return { type: 'local', path: source };
52
+ }
53
+ // GitHub URL
54
+ if (source.startsWith('https://github.com/') || source.startsWith('http://github.com/')) {
55
+ const url = source.replace(/\.git$/, '');
56
+ const match = url.match(/github\.com\/([^/]+)\/([^/]+)/);
57
+ return {
58
+ type: 'github',
59
+ url: `https://github.com/${match?.[1]}/${match?.[2]}.git`,
60
+ owner: match?.[1],
61
+ repo: match?.[2],
62
+ };
63
+ }
64
+ // Git URL
65
+ if (source.startsWith('git@') || source.endsWith('.git')) {
66
+ return { type: 'github', url: source };
67
+ }
68
+ // Explicit marketplace override
69
+ if (options?.from) {
70
+ if (!this.getMarketplace(options.from)) {
71
+ throw new Error(`Unknown marketplace: ${options.from}. Available: ${this.getMarketplaceIds().join(', ')}`);
72
+ }
73
+ return {
74
+ type: 'marketplace',
75
+ marketplace: options.from,
76
+ pluginName: source,
77
+ };
78
+ }
79
+ const marketplacePrefixMatch = source.match(/^([a-zA-Z0-9._-]+)\/(.+)$/);
80
+ if (marketplacePrefixMatch) {
81
+ const [, marketplaceId, pluginName] = marketplacePrefixMatch;
82
+ if (marketplaceId && pluginName && this.getMarketplace(marketplaceId)) {
83
+ return {
84
+ type: 'marketplace',
85
+ marketplace: marketplaceId,
86
+ pluginName,
87
+ };
88
+ }
89
+ }
90
+ // GitHub shorthand: owner/repo
91
+ if (/^[a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+$/.test(source)) {
92
+ const [owner, repo] = source.split('/');
93
+ return {
94
+ type: 'github',
95
+ url: `https://github.com/${owner}/${repo}.git`,
96
+ owner,
97
+ repo,
98
+ };
99
+ }
100
+ throw new Error(`Ambiguous plugin source "${source}". Use <marketplace>/<plugin> (for example, claude/${source}), --from <marketplace>, a GitHub repo, or a local path.`);
101
+ }
102
+ // ── Marketplace ────────────────────────────────────────────────────
103
+ getMarketplaceIds() {
104
+ return MARKETPLACES.map(marketplace => marketplace.id);
105
+ }
106
+ /**
107
+ * Get a marketplace registry by ID
108
+ */
109
+ getMarketplace(id) {
110
+ return MARKETPLACES.find(m => m.id === id);
111
+ }
112
+ /**
113
+ * Ensure the marketplace cache is fresh. Clone if missing or stale.
114
+ */
115
+ async ensureMarketplaceCache(registryId) {
116
+ const registry = this.getMarketplace(registryId);
117
+ if (!registry) {
118
+ throw new Error(`Unknown marketplace: ${registryId}. Available: ${MARKETPLACES.map(m => m.id).join(', ')}`);
119
+ }
120
+ const cacheDir = getMarketplaceCacheDir(registryId);
121
+ const cacheMetaPath = join(cacheDir, '.agentinit-cache-meta.json');
122
+ // Check if cache exists and is fresh
123
+ if (await fileExists(cacheMetaPath)) {
124
+ try {
125
+ const meta = JSON.parse(await fs.readFile(cacheMetaPath, 'utf8'));
126
+ const age = Date.now() - (meta.fetchedAt || 0);
127
+ if (age < registry.cacheTtlMs) {
128
+ return cacheDir;
129
+ }
130
+ }
131
+ catch {
132
+ // Corrupt meta, re-fetch
133
+ }
134
+ }
135
+ // Clone or update
136
+ if (await fileExists(join(cacheDir, '.git'))) {
137
+ // Pull latest
138
+ const { execFile } = await import('child_process');
139
+ const { promisify } = await import('util');
140
+ const exec = promisify(execFile);
141
+ try {
142
+ await exec('git', ['pull', '--ff-only'], { cwd: cacheDir, timeout: 30000 });
143
+ }
144
+ catch {
145
+ // Pull failed, re-clone
146
+ await fs.rm(cacheDir, { recursive: true, force: true });
147
+ await this.cloneMarketplace(registry.repoUrl, cacheDir);
148
+ }
149
+ }
150
+ else {
151
+ await this.cloneMarketplace(registry.repoUrl, cacheDir);
152
+ }
153
+ // Write cache meta
154
+ await fs.mkdir(cacheDir, { recursive: true });
155
+ await fs.writeFile(cacheMetaPath, JSON.stringify({ fetchedAt: Date.now() }));
156
+ return cacheDir;
157
+ }
158
+ async cloneMarketplace(repoUrl, dest) {
159
+ await fs.mkdir(dest, { recursive: true });
160
+ const { execFile } = await import('child_process');
161
+ const { promisify } = await import('util');
162
+ const exec = promisify(execFile);
163
+ // Remove dest first if it exists (for re-clone)
164
+ await fs.rm(dest, { recursive: true, force: true }).catch(() => { });
165
+ await exec('git', ['clone', '--depth', '1', repoUrl, dest], { timeout: 60000 });
166
+ }
167
+ /**
168
+ * Find a plugin by name in a marketplace
169
+ */
170
+ async resolveMarketplacePlugin(name, registryId) {
171
+ const registry = this.getMarketplace(registryId);
172
+ if (!registry)
173
+ throw new Error(`Unknown marketplace: ${registryId}`);
174
+ const cacheDir = await this.ensureMarketplaceCache(registryId);
175
+ // Search in each plugin directory
176
+ for (const dir of registry.pluginDirs) {
177
+ const pluginPath = join(cacheDir, dir, name);
178
+ if (await isDirectory(pluginPath)) {
179
+ return pluginPath;
180
+ }
181
+ }
182
+ // Not found — suggest similar names
183
+ const available = await this.listMarketplacePlugins(registryId);
184
+ const suggestions = available
185
+ .filter(p => p.name.includes(name) || name.includes(p.name))
186
+ .map(p => p.name)
187
+ .slice(0, 5);
188
+ let msg = `Plugin "${name}" not found in ${registry.name} marketplace.`;
189
+ if (suggestions.length > 0) {
190
+ msg += ` Did you mean: ${suggestions.join(', ')}?`;
191
+ }
192
+ throw new Error(msg);
193
+ }
194
+ /**
195
+ * List all plugins in a marketplace, optionally filtered
196
+ */
197
+ async listMarketplacePlugins(registryId, query, category) {
198
+ const registry = this.getMarketplace(registryId);
199
+ if (!registry)
200
+ throw new Error(`Unknown marketplace: ${registryId}`);
201
+ const cacheDir = await this.ensureMarketplaceCache(registryId);
202
+ const results = [];
203
+ for (const dir of registry.pluginDirs) {
204
+ const fullDir = join(cacheDir, dir);
205
+ if (!(await isDirectory(fullDir)))
206
+ continue;
207
+ const cat = dir === 'plugins' ? 'official' : dir === 'external_plugins' ? 'community' : dir;
208
+ if (category && cat !== category)
209
+ continue;
210
+ const entries = await listFiles(fullDir);
211
+ for (const entry of entries) {
212
+ if (entry.startsWith('.'))
213
+ continue;
214
+ const entryPath = join(fullDir, entry);
215
+ if (!(await isDirectory(entryPath)))
216
+ continue;
217
+ // Try to read plugin manifest
218
+ const manifestPath = join(entryPath, '.claude-plugin', 'plugin.json');
219
+ let name = entry;
220
+ let description = '';
221
+ let version = '0.0.0';
222
+ if (await fileExists(manifestPath)) {
223
+ try {
224
+ const manifest = JSON.parse(await fs.readFile(manifestPath, 'utf8'));
225
+ name = manifest.name || entry;
226
+ description = manifest.description || '';
227
+ version = manifest.version || '0.0.0';
228
+ }
229
+ catch { /* use defaults */ }
230
+ }
231
+ if (query) {
232
+ const q = query.toLowerCase();
233
+ if (!name.toLowerCase().includes(q) && !description.toLowerCase().includes(q)) {
234
+ continue;
235
+ }
236
+ }
237
+ results.push({ name, description, version, path: `${dir}/${entry}`, category: cat, registry: registryId });
238
+ }
239
+ }
240
+ return results.sort((a, b) => a.name.localeCompare(b.name));
241
+ }
242
+ // ── Format Detection ───────────────────────────────────────────────
243
+ /**
244
+ * Auto-detect plugin format from directory contents
245
+ */
246
+ async detectFormat(pluginDir) {
247
+ if (await fileExists(join(pluginDir, '.claude-plugin', 'plugin.json'))) {
248
+ return 'claude';
249
+ }
250
+ if (await fileExists(join(pluginDir, '.cursor-plugin', 'plugin.json'))) {
251
+ return 'cursor';
252
+ }
253
+ return 'generic';
254
+ }
255
+ // ── Format Parsers ─────────────────────────────────────────────────
256
+ /**
257
+ * Parse a plugin directory into a NormalizedPlugin, auto-detecting format
258
+ */
259
+ async parsePlugin(pluginDir, source) {
260
+ const format = await this.detectFormat(pluginDir);
261
+ switch (format) {
262
+ case 'claude':
263
+ return this.parseClaudePlugin(pluginDir, source);
264
+ case 'cursor':
265
+ return this.parseCursorPlugin(pluginDir, source);
266
+ default:
267
+ return this.parseGenericPlugin(pluginDir, source);
268
+ }
269
+ }
270
+ /**
271
+ * Parse Claude plugin format
272
+ */
273
+ async parseClaudePlugin(pluginDir, source) {
274
+ const manifestPath = join(pluginDir, '.claude-plugin', 'plugin.json');
275
+ const manifestContent = await readFileIfExists(manifestPath);
276
+ if (!manifestContent) {
277
+ throw new Error(`Missing .claude-plugin/plugin.json in ${pluginDir}`);
278
+ }
279
+ const manifest = JSON.parse(manifestContent);
280
+ const warnings = [];
281
+ // Extract skills
282
+ const skills = await this.skillsManager.discoverSkills(pluginDir);
283
+ // Convert commands/ to skills
284
+ const convertedSkills = await this.convertCommandsToSkills(pluginDir, manifest);
285
+ skills.push(...convertedSkills);
286
+ // Extract MCP servers
287
+ const mcpServers = await this.parseMcpJson(pluginDir);
288
+ // Warn about agent-specific features
289
+ if (await isDirectory(join(pluginDir, 'hooks')) || manifest.hooks) {
290
+ warnings.push('Hooks (hooks/) are Claude Code-specific and were not installed');
291
+ }
292
+ if (await isDirectory(join(pluginDir, 'agents')) || manifest.agents) {
293
+ warnings.push('Agent definitions (agents/) are Claude Code-specific and were not installed');
294
+ }
295
+ return {
296
+ name: manifest.name,
297
+ version: manifest.version || '0.0.0',
298
+ description: manifest.description || '',
299
+ source,
300
+ format: 'claude',
301
+ skills,
302
+ mcpServers,
303
+ warnings,
304
+ };
305
+ }
306
+ /**
307
+ * Parse Cursor plugin format
308
+ */
309
+ async parseCursorPlugin(pluginDir, source) {
310
+ const manifestPath = join(pluginDir, '.cursor-plugin', 'plugin.json');
311
+ const manifestContent = await readFileIfExists(manifestPath);
312
+ if (!manifestContent) {
313
+ throw new Error(`Missing .cursor-plugin/plugin.json in ${pluginDir}`);
314
+ }
315
+ const manifest = JSON.parse(manifestContent);
316
+ const warnings = [];
317
+ // Extract skills
318
+ const skills = await this.skillsManager.discoverSkills(pluginDir);
319
+ // Extract MCP servers from .mcp.json (Cursor also uses this)
320
+ const mcpServers = await this.parseMcpJson(pluginDir);
321
+ // Warn about Cursor-specific features
322
+ if (manifest.rules) {
323
+ warnings.push('Rules (.mdc files) are Cursor-specific and were not installed');
324
+ }
325
+ return {
326
+ name: manifest.name,
327
+ version: manifest.version || '0.0.0',
328
+ description: manifest.description || '',
329
+ source,
330
+ format: 'cursor',
331
+ skills,
332
+ mcpServers,
333
+ warnings,
334
+ };
335
+ }
336
+ /**
337
+ * Parse generic plugin (just skills/ and .mcp.json, no manifest)
338
+ */
339
+ async parseGenericPlugin(pluginDir, source) {
340
+ const skills = await this.skillsManager.discoverSkills(pluginDir);
341
+ const mcpServers = await this.parseMcpJson(pluginDir);
342
+ const dirName = basename(pluginDir);
343
+ return {
344
+ name: dirName,
345
+ version: '0.0.0',
346
+ description: '',
347
+ source,
348
+ format: 'generic',
349
+ skills,
350
+ mcpServers,
351
+ warnings: [],
352
+ };
353
+ }
354
+ // ── MCP Parsing ────────────────────────────────────────────────────
355
+ /**
356
+ * Parse .mcp.json from a plugin directory into MCPServerConfig[]
357
+ * Handles the { mcpServers: { name: config } } format used by Claude and Cursor
358
+ */
359
+ async parseMcpJson(pluginDir) {
360
+ const mcpPath = join(pluginDir, '.mcp.json');
361
+ const content = await readFileIfExists(mcpPath);
362
+ if (!content)
363
+ return [];
364
+ try {
365
+ const config = JSON.parse(content);
366
+ return this.parseMcpJsonObject(config);
367
+ }
368
+ catch {
369
+ return [];
370
+ }
371
+ }
372
+ /**
373
+ * Parse a raw MCP JSON config object into MCPServerConfig[]
374
+ */
375
+ parseMcpJsonObject(config) {
376
+ const servers = [];
377
+ const mcpServers = config.mcpServers || config;
378
+ if (typeof mcpServers !== 'object' || mcpServers === null)
379
+ return [];
380
+ for (const [name, serverConfig] of Object.entries(mcpServers)) {
381
+ const sc = serverConfig;
382
+ if (!sc || typeof sc !== 'object')
383
+ continue;
384
+ const server = {
385
+ name,
386
+ type: (sc.type || (sc.command ? 'stdio' : sc.url ? 'http' : 'stdio')),
387
+ };
388
+ if (sc.command)
389
+ server.command = sc.command;
390
+ if (sc.args)
391
+ server.args = sc.args;
392
+ if (sc.env)
393
+ server.env = sc.env;
394
+ if (sc.url)
395
+ server.url = sc.url;
396
+ if (sc.headers)
397
+ server.headers = sc.headers;
398
+ servers.push(server);
399
+ }
400
+ return servers;
401
+ }
402
+ // ── Command → Skill Conversion ─────────────────────────────────────
403
+ /**
404
+ * Convert commands/*.md files to SKILL.md format
405
+ */
406
+ async convertCommandsToSkills(pluginDir, manifest) {
407
+ const skills = [];
408
+ // Determine commands directory
409
+ const commandsDirs = [];
410
+ if (manifest.commands) {
411
+ const cmds = Array.isArray(manifest.commands) ? manifest.commands : [manifest.commands];
412
+ for (const cmd of cmds) {
413
+ commandsDirs.push(resolve(pluginDir, cmd));
414
+ }
415
+ }
416
+ else {
417
+ commandsDirs.push(join(pluginDir, 'commands'));
418
+ }
419
+ for (const commandsDir of commandsDirs) {
420
+ if (!(await isDirectory(commandsDir)))
421
+ continue;
422
+ const entries = await listFiles(commandsDir);
423
+ for (const entry of entries) {
424
+ if (!entry.endsWith('.md'))
425
+ continue;
426
+ const cmdPath = join(commandsDir, entry);
427
+ const skill = await this.convertSingleCommandToSkill(cmdPath, manifest.name);
428
+ if (skill)
429
+ skills.push(skill);
430
+ }
431
+ }
432
+ return skills;
433
+ }
434
+ /**
435
+ * Convert a single command .md file to a skill
436
+ */
437
+ async convertSingleCommandToSkill(cmdPath, pluginName) {
438
+ const content = await readFileIfExists(cmdPath);
439
+ if (!content)
440
+ return null;
441
+ const fileName = basename(cmdPath, '.md');
442
+ let skillName;
443
+ let description;
444
+ let body;
445
+ try {
446
+ const parsed = matter(content);
447
+ skillName = parsed.data.name || fileName;
448
+ description = parsed.data.description || `Command from ${pluginName} plugin`;
449
+ body = parsed.content;
450
+ }
451
+ catch {
452
+ skillName = fileName;
453
+ description = `Command from ${pluginName} plugin`;
454
+ body = content;
455
+ }
456
+ const skillContent = `---
457
+ name: ${skillName}
458
+ description: ${description}
459
+ version: 1.0.0
460
+ ---
461
+
462
+ ${body.trim()}
463
+ `;
464
+ return {
465
+ name: skillName,
466
+ description,
467
+ path: cmdPath,
468
+ generatedContent: skillContent,
469
+ };
470
+ }
471
+ // ── Installation ───────────────────────────────────────────────────
472
+ /**
473
+ * Install a plugin from any source into target agents.
474
+ * This is the main one-liner entry point.
475
+ */
476
+ async installPlugin(source, projectPath, options = {}) {
477
+ const resolved = this.resolveSource(source, { from: options.from });
478
+ let pluginDir;
479
+ let tempDir = null;
480
+ // 1. Resolve source to a local directory
481
+ if (resolved.type === 'marketplace') {
482
+ pluginDir = await this.resolveMarketplacePlugin(resolved.pluginName, resolved.marketplace || 'claude');
483
+ }
484
+ else if (resolved.type === 'github') {
485
+ if (!resolved.url)
486
+ throw new Error(`Invalid source: ${source}`);
487
+ tempDir = await this.skillsManager.cloneRepo(resolved.url);
488
+ pluginDir = tempDir;
489
+ }
490
+ else {
491
+ pluginDir = resolve(resolved.path || source);
492
+ if (!(await fileExists(pluginDir))) {
493
+ throw new Error(`Local path not found: ${pluginDir}`);
494
+ }
495
+ }
496
+ try {
497
+ // 2. Parse plugin
498
+ const plugin = await this.parsePlugin(pluginDir, resolved);
499
+ // 3. If --list, return early with contents
500
+ if (options.list) {
501
+ return {
502
+ plugin,
503
+ skills: { installed: [], skipped: [] },
504
+ mcpServers: { applied: [], skipped: [] },
505
+ warnings: plugin.warnings,
506
+ };
507
+ }
508
+ // 4. Get target agents
509
+ const agents = await this.getTargetAgents(projectPath, options);
510
+ if (agents.length === 0) {
511
+ return {
512
+ plugin,
513
+ skills: { installed: [], skipped: plugin.skills.map(s => ({ name: s.name, reason: 'No target agents found' })) },
514
+ mcpServers: { applied: [], skipped: plugin.mcpServers.map(s => ({ name: s.name, reason: 'No target agents found' })) },
515
+ warnings: plugin.warnings,
516
+ };
517
+ }
518
+ // 5. Install skills (deduplicated by shared directory)
519
+ const skillResult = await this.installPluginSkills(plugin, projectPath, agents, options);
520
+ // 6. Apply MCP servers per agent
521
+ const mcpResult = await this.applyPluginMcpServers(plugin, projectPath, agents, options.global);
522
+ // 7. Save to registry only when the install actually applied portable components.
523
+ if (skillResult.installed.length > 0 || mcpResult.applied.length > 0) {
524
+ const installed = {
525
+ name: plugin.name,
526
+ version: plugin.version,
527
+ description: plugin.description,
528
+ source: resolved,
529
+ format: plugin.format,
530
+ installedAt: new Date().toISOString(),
531
+ scope: options.global ? 'global' : 'project',
532
+ components: {
533
+ skills: skillResult.installed,
534
+ mcpServers: mcpResult.applied,
535
+ },
536
+ warnings: plugin.warnings,
537
+ };
538
+ await this.addToRegistry(installed, projectPath, options.global);
539
+ }
540
+ return {
541
+ plugin,
542
+ skills: skillResult,
543
+ mcpServers: mcpResult,
544
+ warnings: plugin.warnings,
545
+ };
546
+ }
547
+ finally {
548
+ if (tempDir) {
549
+ await fs.rm(tempDir, { recursive: true, force: true }).catch(() => { });
550
+ }
551
+ }
552
+ }
553
+ /**
554
+ * Install skills from a plugin, deduplicating by shared directory
555
+ */
556
+ async installPluginSkills(plugin, projectPath, agents, options) {
557
+ const installed = [];
558
+ const skipped = [];
559
+ if (plugin.skills.length === 0)
560
+ return { installed, skipped };
561
+ // Group agents by their skills directory to avoid duplicate installs
562
+ const dirToAgents = new Map();
563
+ for (const agent of agents) {
564
+ if (!agent.supportsSkills()) {
565
+ for (const skill of plugin.skills) {
566
+ skipped.push({ name: skill.name, reason: `${agent.name} does not support skills` });
567
+ }
568
+ continue;
569
+ }
570
+ const skillsDir = agent.getSkillsDir(projectPath, options.global);
571
+ if (!skillsDir) {
572
+ for (const skill of plugin.skills) {
573
+ skipped.push({ name: skill.name, reason: `No skills directory for ${agent.name}` });
574
+ }
575
+ continue;
576
+ }
577
+ const existing = dirToAgents.get(skillsDir) || [];
578
+ existing.push(agent);
579
+ dirToAgents.set(skillsDir, existing);
580
+ }
581
+ // Install once per unique directory
582
+ for (const [skillsDir, dirAgents] of dirToAgents) {
583
+ for (const skill of plugin.skills) {
584
+ try {
585
+ const installedPath = skill.generatedContent
586
+ ? await this.skillsManager.installSkillFromContent(skill.name, skill.generatedContent, skillsDir)
587
+ : await this.skillsManager.installSkill(skill.path, skill.name, skillsDir, true // Plugins always copy to avoid temp/cache symlink issues.
588
+ );
589
+ // Record for all agents sharing this directory
590
+ for (const agent of dirAgents) {
591
+ installed.push({ name: skill.name, agent: agent.id, path: installedPath });
592
+ }
593
+ }
594
+ catch (error) {
595
+ skipped.push({ name: skill.name, reason: error.message });
596
+ }
597
+ }
598
+ }
599
+ return { installed, skipped };
600
+ }
601
+ /**
602
+ * Apply MCP servers from a plugin to each target agent
603
+ */
604
+ async applyPluginMcpServers(plugin, projectPath, agents, global) {
605
+ const applied = [];
606
+ const skipped = [];
607
+ if (plugin.mcpServers.length === 0)
608
+ return { applied, skipped };
609
+ for (const agent of agents) {
610
+ if (global && !agent.supportsGlobalConfig()) {
611
+ for (const server of plugin.mcpServers) {
612
+ skipped.push({ name: server.name, reason: `${agent.name} does not support global MCP configuration` });
613
+ }
614
+ continue;
615
+ }
616
+ // Filter and transform MCP servers for this agent
617
+ const filtered = MCPFilter.filterForAgent(agent, plugin.mcpServers);
618
+ if (filtered.servers.length === 0) {
619
+ for (const server of plugin.mcpServers) {
620
+ skipped.push({ name: server.name, reason: `Not compatible with ${agent.name}` });
621
+ }
622
+ continue;
623
+ }
624
+ try {
625
+ if (global) {
626
+ await agent.applyGlobalMCPConfig(filtered.servers);
627
+ }
628
+ else {
629
+ await agent.applyMCPConfig(projectPath, filtered.servers);
630
+ }
631
+ for (const server of filtered.servers) {
632
+ applied.push({ name: server.name, agent: agent.id });
633
+ }
634
+ }
635
+ catch (error) {
636
+ for (const server of filtered.servers) {
637
+ skipped.push({ name: server.name, reason: `Failed for ${agent.name}: ${error.message}` });
638
+ }
639
+ }
640
+ }
641
+ return { applied, skipped };
642
+ }
643
+ // ── Agent Selection ────────────────────────────────────────────────
644
+ /**
645
+ * Get target agents based on options
646
+ */
647
+ async getTargetAgents(projectPath, options = {}) {
648
+ if (options.agents && options.agents.length > 0) {
649
+ const agents = [];
650
+ for (const id of options.agents) {
651
+ const agent = this.agentManager.getAgentById(id);
652
+ if (agent)
653
+ agents.push(agent);
654
+ }
655
+ return agents;
656
+ }
657
+ // Auto-detect agents in the project
658
+ const detected = await this.agentManager.detectAgents(projectPath);
659
+ return detected.map(d => d.agent);
660
+ }
661
+ /**
662
+ * Group detected agents by their shared skills directory for interactive prompt.
663
+ * Returns entries like: { dir: '.agents/', agents: [cursor, codex, gemini] }
664
+ */
665
+ async groupAgentsBySkillsDir(projectPath, global) {
666
+ const detected = await this.agentManager.detectAgents(projectPath);
667
+ const dirToAgents = new Map();
668
+ for (const { agent } of detected) {
669
+ if (!agent.supportsSkills())
670
+ continue;
671
+ const skillsDir = agent.getSkillsDir(projectPath, global);
672
+ if (!skillsDir)
673
+ continue;
674
+ // Use a relative display path
675
+ const relDir = skillsDir.startsWith(projectPath)
676
+ ? skillsDir.slice(projectPath.length + 1).replace(/\/$/, '') + '/'
677
+ : skillsDir;
678
+ const existing = dirToAgents.get(relDir) || [];
679
+ existing.push(agent);
680
+ dirToAgents.set(relDir, existing);
681
+ }
682
+ return Array.from(dirToAgents.entries()).map(([dir, agents]) => ({
683
+ dir,
684
+ agents,
685
+ agentNames: agents.map(a => a.name),
686
+ }));
687
+ }
688
+ // ── Registry ───────────────────────────────────────────────────────
689
+ /**
690
+ * Read the plugin registry
691
+ */
692
+ async getRegistry(projectPath, global) {
693
+ const path = getRegistryPath(projectPath, global);
694
+ const content = await readFileIfExists(path);
695
+ if (!content)
696
+ return { version: 1, plugins: [] };
697
+ try {
698
+ return JSON.parse(content);
699
+ }
700
+ catch {
701
+ return { version: 1, plugins: [] };
702
+ }
703
+ }
704
+ /**
705
+ * Save the plugin registry
706
+ */
707
+ async saveRegistry(registry, projectPath, global) {
708
+ const path = getRegistryPath(projectPath, global);
709
+ await writeFile(path, JSON.stringify(registry, null, 2));
710
+ }
711
+ /**
712
+ * Add an installed plugin to the registry
713
+ */
714
+ async addToRegistry(plugin, projectPath, global) {
715
+ const registry = await this.getRegistry(projectPath, global);
716
+ // Replace existing entry with same name
717
+ registry.plugins = registry.plugins.filter(p => p.name !== plugin.name);
718
+ registry.plugins.push(plugin);
719
+ await this.saveRegistry(registry, projectPath, global);
720
+ }
721
+ /**
722
+ * List all installed plugins
723
+ */
724
+ async listPlugins(projectPath, options = {}) {
725
+ const registry = await this.getRegistry(projectPath, options.global);
726
+ let plugins = registry.plugins;
727
+ if (options.agents && options.agents.length > 0) {
728
+ const agentSet = new Set(options.agents);
729
+ plugins = plugins.filter(p => p.components.skills.some(s => agentSet.has(s.agent)) ||
730
+ p.components.mcpServers.some(m => agentSet.has(m.agent)));
731
+ }
732
+ return plugins;
733
+ }
734
+ /**
735
+ * Remove a plugin by name
736
+ */
737
+ async removePlugin(name, projectPath, options = {}) {
738
+ const registry = await this.getRegistry(projectPath, options.global);
739
+ const plugin = registry.plugins.find(p => p.name === name);
740
+ if (!plugin) {
741
+ return { removed: false, details: [`Plugin "${name}" not found in registry`] };
742
+ }
743
+ if (plugin.components.skills.length === 0 && plugin.components.mcpServers.length === 0) {
744
+ registry.plugins = registry.plugins.filter(p => p.name !== name);
745
+ await this.saveRegistry(registry, projectPath, options.global);
746
+ return {
747
+ removed: true,
748
+ details: ['Removed stale plugin registry entry'],
749
+ };
750
+ }
751
+ const details = [];
752
+ const agentFilter = options.agents?.length ? new Set(options.agents) : null;
753
+ const targetedSkills = agentFilter
754
+ ? plugin.components.skills.filter(skill => agentFilter.has(skill.agent))
755
+ : plugin.components.skills;
756
+ const retainedSkills = agentFilter
757
+ ? plugin.components.skills.filter(skill => !agentFilter.has(skill.agent))
758
+ : [];
759
+ const retainedSkillPaths = new Set(retainedSkills.map(skill => skill.path));
760
+ const removedSkillPaths = new Set();
761
+ const sharedSkillPaths = new Set();
762
+ for (const skill of targetedSkills) {
763
+ if (removedSkillPaths.has(skill.path) || sharedSkillPaths.has(skill.path)) {
764
+ continue;
765
+ }
766
+ if (retainedSkillPaths.has(skill.path)) {
767
+ sharedSkillPaths.add(skill.path);
768
+ details.push(`Skipped shared skill path: ${skill.path}`);
769
+ continue;
770
+ }
771
+ try {
772
+ await fs.rm(skill.path, { recursive: true, force: true });
773
+ removedSkillPaths.add(skill.path);
774
+ }
775
+ catch {
776
+ details.push(`Could not remove skill path: ${skill.path}`);
777
+ }
778
+ }
779
+ const removedSkills = targetedSkills.filter(skill => removedSkillPaths.has(skill.path));
780
+ for (const skill of removedSkills) {
781
+ details.push(`Removed skill: ${skill.name} (${skill.agent})`);
782
+ }
783
+ const remainingSkills = [
784
+ ...retainedSkills,
785
+ ...targetedSkills.filter(skill => !removedSkillPaths.has(skill.path)),
786
+ ];
787
+ const targetedMcpServers = agentFilter
788
+ ? plugin.components.mcpServers.filter(mcp => agentFilter.has(mcp.agent))
789
+ : plugin.components.mcpServers;
790
+ const retainedMcpServers = agentFilter
791
+ ? plugin.components.mcpServers.filter(mcp => !agentFilter.has(mcp.agent))
792
+ : [];
793
+ const removedMcpKeys = new Set();
794
+ for (const mcp of targetedMcpServers) {
795
+ const agent = this.agentManager.getAgentById(mcp.agent);
796
+ if (!agent) {
797
+ details.push(`Could not resolve agent for MCP server: ${mcp.name} (${mcp.agent})`);
798
+ continue;
799
+ }
800
+ try {
801
+ const removed = options.global
802
+ ? await agent.removeGlobalMCPServer(mcp.name)
803
+ : await agent.removeMCPServer(projectPath, mcp.name);
804
+ if (removed) {
805
+ removedMcpKeys.add(`${mcp.agent}:${mcp.name}`);
806
+ details.push(`Removed MCP server: ${mcp.name} (${mcp.agent})`);
807
+ }
808
+ else {
809
+ details.push(`MCP server not found: ${mcp.name} (${mcp.agent})`);
810
+ }
811
+ }
812
+ catch {
813
+ details.push(`Could not remove MCP server: ${mcp.name} from ${mcp.agent}`);
814
+ }
815
+ }
816
+ const remainingMcpServers = [
817
+ ...retainedMcpServers,
818
+ ...targetedMcpServers.filter(mcp => !removedMcpKeys.has(`${mcp.agent}:${mcp.name}`)),
819
+ ];
820
+ if (removedSkillPaths.size === 0 && removedMcpKeys.size === 0) {
821
+ if (agentFilter) {
822
+ details.push(`No removable plugin components matched the requested agents for "${name}"`);
823
+ }
824
+ return { removed: false, details };
825
+ }
826
+ const updatedPlugin = {
827
+ ...plugin,
828
+ components: {
829
+ skills: remainingSkills,
830
+ mcpServers: remainingMcpServers,
831
+ },
832
+ };
833
+ registry.plugins = registry.plugins.filter(p => p.name !== name);
834
+ if (updatedPlugin.components.skills.length > 0 || updatedPlugin.components.mcpServers.length > 0) {
835
+ registry.plugins.push(updatedPlugin);
836
+ details.push('Updated plugin registry');
837
+ }
838
+ else {
839
+ details.push('Removed from plugin registry');
840
+ }
841
+ await this.saveRegistry(registry, projectPath, options.global);
842
+ return { removed: true, details };
843
+ }
844
+ }
845
+ //# sourceMappingURL=pluginManager.js.map