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.
- package/CHANGELOG.md +40 -0
- package/README.md +220 -114
- package/dist/agents/Agent.d.ts +44 -0
- package/dist/agents/Agent.d.ts.map +1 -1
- package/dist/agents/Agent.js +111 -22
- package/dist/agents/Agent.js.map +1 -1
- package/dist/agents/AiderAgent.d.ts +9 -0
- package/dist/agents/AiderAgent.d.ts.map +1 -0
- package/dist/agents/AiderAgent.js +135 -0
- package/dist/agents/AiderAgent.js.map +1 -0
- package/dist/agents/ClaudeAgent.d.ts +4 -0
- package/dist/agents/ClaudeAgent.d.ts.map +1 -1
- package/dist/agents/ClaudeAgent.js +33 -18
- package/dist/agents/ClaudeAgent.js.map +1 -1
- package/dist/agents/ClaudeDesktopAgent.d.ts +5 -0
- package/dist/agents/ClaudeDesktopAgent.d.ts.map +1 -1
- package/dist/agents/ClaudeDesktopAgent.js +31 -1
- package/dist/agents/ClaudeDesktopAgent.js.map +1 -1
- package/dist/agents/ClineAgent.d.ts +8 -0
- package/dist/agents/ClineAgent.d.ts.map +1 -0
- package/dist/agents/ClineAgent.js +42 -0
- package/dist/agents/ClineAgent.js.map +1 -0
- package/dist/agents/CodexCliAgent.d.ts +4 -0
- package/dist/agents/CodexCliAgent.d.ts.map +1 -1
- package/dist/agents/CodexCliAgent.js +38 -8
- package/dist/agents/CodexCliAgent.js.map +1 -1
- package/dist/agents/CopilotAgent.d.ts +9 -0
- package/dist/agents/CopilotAgent.d.ts.map +1 -0
- package/dist/agents/CopilotAgent.js +131 -0
- package/dist/agents/CopilotAgent.js.map +1 -0
- package/dist/agents/CursorAgent.d.ts +4 -4
- package/dist/agents/CursorAgent.d.ts.map +1 -1
- package/dist/agents/CursorAgent.js +32 -43
- package/dist/agents/CursorAgent.js.map +1 -1
- package/dist/agents/DroidAgent.d.ts +5 -0
- package/dist/agents/DroidAgent.d.ts.map +1 -1
- package/dist/agents/DroidAgent.js +37 -18
- package/dist/agents/DroidAgent.js.map +1 -1
- package/dist/agents/GeminiCliAgent.d.ts +4 -0
- package/dist/agents/GeminiCliAgent.d.ts.map +1 -1
- package/dist/agents/GeminiCliAgent.js +37 -8
- package/dist/agents/GeminiCliAgent.js.map +1 -1
- package/dist/agents/MarkdownRulesAgent.d.ts +13 -0
- package/dist/agents/MarkdownRulesAgent.d.ts.map +1 -0
- package/dist/agents/MarkdownRulesAgent.js +53 -0
- package/dist/agents/MarkdownRulesAgent.js.map +1 -0
- package/dist/agents/RooCodeAgent.d.ts +9 -0
- package/dist/agents/RooCodeAgent.d.ts.map +1 -0
- package/dist/agents/RooCodeAgent.js +131 -0
- package/dist/agents/RooCodeAgent.js.map +1 -0
- package/dist/agents/WindsurfAgent.d.ts +9 -0
- package/dist/agents/WindsurfAgent.d.ts.map +1 -0
- package/dist/agents/WindsurfAgent.js +127 -0
- package/dist/agents/WindsurfAgent.js.map +1 -0
- package/dist/agents/ZedAgent.d.ts +9 -0
- package/dist/agents/ZedAgent.d.ts.map +1 -0
- package/dist/agents/ZedAgent.js +127 -0
- package/dist/agents/ZedAgent.js.map +1 -0
- package/dist/cli.js +29992 -11107
- package/dist/cli.js.map +1 -1
- package/dist/commands/apply.d.ts +10 -0
- package/dist/commands/apply.d.ts.map +1 -1
- package/dist/commands/apply.js +136 -5
- package/dist/commands/apply.js.map +1 -1
- package/dist/commands/detect.d.ts.map +1 -1
- package/dist/commands/detect.js +25 -0
- package/dist/commands/detect.js.map +1 -1
- package/dist/commands/init.js +1 -1
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/mcp.d.ts +2 -7
- package/dist/commands/mcp.d.ts.map +1 -1
- package/dist/commands/mcp.js +541 -110
- package/dist/commands/mcp.js.map +1 -1
- package/dist/commands/plugins.d.ts +3 -0
- package/dist/commands/plugins.d.ts.map +1 -0
- package/dist/commands/plugins.js +309 -0
- package/dist/commands/plugins.js.map +1 -0
- package/dist/commands/revert.d.ts +7 -0
- package/dist/commands/revert.d.ts.map +1 -0
- package/dist/commands/revert.js +48 -0
- package/dist/commands/revert.js.map +1 -0
- package/dist/commands/rules.d.ts +3 -0
- package/dist/commands/rules.d.ts.map +1 -0
- package/dist/commands/rules.js +354 -0
- package/dist/commands/rules.js.map +1 -0
- package/dist/commands/skills.d.ts +3 -0
- package/dist/commands/skills.d.ts.map +1 -0
- package/dist/commands/skills.js +179 -0
- package/dist/commands/skills.js.map +1 -0
- package/dist/commands/sync.d.ts +1 -0
- package/dist/commands/sync.d.ts.map +1 -1
- package/dist/commands/sync.js +18 -1
- package/dist/commands/sync.js.map +1 -1
- package/dist/commands/verifyMcp.d.ts.map +1 -1
- package/dist/commands/verifyMcp.js +27 -5
- package/dist/commands/verifyMcp.js.map +1 -1
- package/dist/constants/index.d.ts +1 -1
- package/dist/constants/index.d.ts.map +1 -1
- package/dist/constants/index.js +1 -1
- package/dist/constants/index.js.map +1 -1
- package/dist/constants/mcp.d.ts +1 -0
- package/dist/constants/mcp.d.ts.map +1 -1
- package/dist/constants/mcp.js +2 -0
- package/dist/constants/mcp.js.map +1 -1
- package/dist/core/agentDetector.d.ts.map +1 -1
- package/dist/core/agentDetector.js +8 -2
- package/dist/core/agentDetector.js.map +1 -1
- package/dist/core/agentManager.d.ts.map +1 -1
- package/dist/core/agentManager.js +12 -0
- package/dist/core/agentManager.js.map +1 -1
- package/dist/core/gitignoreManager.d.ts +8 -0
- package/dist/core/gitignoreManager.d.ts.map +1 -0
- package/dist/core/gitignoreManager.js +114 -0
- package/dist/core/gitignoreManager.js.map +1 -0
- package/dist/core/managedState.d.ts +42 -0
- package/dist/core/managedState.d.ts.map +1 -0
- package/dist/core/managedState.js +194 -0
- package/dist/core/managedState.js.map +1 -0
- package/dist/core/mcpClient.d.ts +124 -6
- package/dist/core/mcpClient.d.ts.map +1 -1
- package/dist/core/mcpClient.js +385 -39
- package/dist/core/mcpClient.js.map +1 -1
- package/dist/core/pluginManager.d.ts +134 -0
- package/dist/core/pluginManager.d.ts.map +1 -0
- package/dist/core/pluginManager.js +845 -0
- package/dist/core/pluginManager.js.map +1 -0
- package/dist/core/projectSkills.d.ts +19 -0
- package/dist/core/projectSkills.d.ts.map +1 -0
- package/dist/core/projectSkills.js +105 -0
- package/dist/core/projectSkills.js.map +1 -0
- package/dist/core/propagator.d.ts +8 -1
- package/dist/core/propagator.d.ts.map +1 -1
- package/dist/core/propagator.js +179 -36
- package/dist/core/propagator.js.map +1 -1
- package/dist/core/rulesApplicator.d.ts +0 -4
- package/dist/core/rulesApplicator.d.ts.map +1 -1
- package/dist/core/rulesApplicator.js +8 -39
- package/dist/core/rulesApplicator.js.map +1 -1
- package/dist/core/rulesTemplateLoader.js +2 -2
- package/dist/core/rulesTemplateLoader.js.map +1 -1
- package/dist/core/skillsManager.d.ts +61 -0
- package/dist/core/skillsManager.d.ts.map +1 -0
- package/dist/core/skillsManager.js +407 -0
- package/dist/core/skillsManager.js.map +1 -0
- package/dist/lib/utils/index.d.ts +3 -1
- package/dist/lib/utils/index.d.ts.map +1 -1
- package/dist/lib/utils/index.js +4 -1
- package/dist/lib/utils/index.js.map +1 -1
- package/dist/types/index.d.ts +24 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/dist/types/jsonSchema.d.ts +31 -0
- package/dist/types/jsonSchema.d.ts.map +1 -0
- package/dist/types/jsonSchema.js +6 -0
- package/dist/types/jsonSchema.js.map +1 -0
- package/dist/types/plugins.d.ts +161 -0
- package/dist/types/plugins.d.ts.map +1 -0
- package/dist/types/plugins.js +2 -0
- package/dist/types/plugins.js.map +1 -0
- package/dist/types/skills.d.ts +50 -0
- package/dist/types/skills.d.ts.map +1 -0
- package/dist/types/skills.js +2 -0
- package/dist/types/skills.js.map +1 -0
- package/dist/utils/packageVersion.d.ts +105 -0
- package/dist/utils/packageVersion.d.ts.map +1 -0
- package/dist/utils/packageVersion.js +219 -0
- package/dist/utils/packageVersion.js.map +1 -0
- package/package.json +7 -2
- package/dist/agentinit-1.6.0.tgz +0 -0
- package/dist/registry/mcpRegistry.d.ts +0 -12
- package/dist/registry/mcpRegistry.d.ts.map +0 -1
- package/dist/registry/mcpRegistry.js +0 -114
- 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
|