@adversity/coding-tool-x 2.5.1 → 2.6.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.
- package/CHANGELOG.md +14 -0
- package/dist/web/assets/icons-BlzwYoRU.js +1 -0
- package/dist/web/assets/{index-DZjidyED.js → index-AtwYwBZD.js} +2 -2
- package/dist/web/assets/index-BNHWEpD4.css +41 -0
- package/dist/web/assets/{naive-ui-sh0u_0bf.js → naive-ui-BcSq2wzw.js} +1 -1
- package/dist/web/assets/{vendors-CzcvkTIS.js → vendors-D2HHw_aW.js} +1 -1
- package/dist/web/assets/{vue-vendor-CEeI-Azr.js → vue-vendor-6JaYHOiI.js} +1 -1
- package/dist/web/index.html +6 -6
- package/package.json +3 -1
- package/src/commands/plugin.js +585 -0
- package/src/config/default.js +22 -3
- package/src/config/loader.js +6 -1
- package/src/index.js +229 -1
- package/src/server/api/dashboard.js +4 -3
- package/src/server/api/mcp.js +63 -0
- package/src/server/api/plugins.js +416 -0
- package/src/server/api/sessions.js +4 -4
- package/src/server/index.js +1 -0
- package/src/server/proxy-server.js +6 -3
- package/src/server/services/mcp-client.js +775 -0
- package/src/server/services/mcp-service.js +203 -0
- package/src/server/services/model-detector.js +350 -0
- package/src/server/services/plugins-service.js +677 -0
- package/src/server/services/pty-manager.js +65 -2
- package/src/server/services/sessions.js +72 -16
- package/src/server/services/speed-test.js +68 -37
- package/src/server/utils/pricing.js +32 -1
- package/src/ui/menu.js +1 -0
- package/dist/web/assets/icons-BALJo7bE.js +0 -1
- package/dist/web/assets/index-CvHZsWbE.css +0 -41
|
@@ -0,0 +1,677 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugins Service
|
|
3
|
+
*
|
|
4
|
+
* Wraps the plugin system for API access
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const os = require('os');
|
|
10
|
+
const { listPlugins, getPlugin, updatePlugin: updatePluginRegistry } = require('../../plugins/registry');
|
|
11
|
+
const { installPlugin: installPluginCore, uninstallPlugin: uninstallPluginCore } = require('../../plugins/plugin-installer');
|
|
12
|
+
const { initializePlugins, shutdownPlugins } = require('../../plugins/plugin-manager');
|
|
13
|
+
const { INSTALLED_DIR, CONFIG_DIR } = require('../../plugins/constants');
|
|
14
|
+
|
|
15
|
+
const CLAUDE_PLUGINS_DIR = path.join(os.homedir(), '.claude', 'plugins');
|
|
16
|
+
const CLAUDE_INSTALLED_FILE = path.join(CLAUDE_PLUGINS_DIR, 'installed_plugins.json');
|
|
17
|
+
const CLAUDE_MARKETPLACES_FILE = path.join(CLAUDE_PLUGINS_DIR, 'known_marketplaces.json');
|
|
18
|
+
|
|
19
|
+
class PluginsService {
|
|
20
|
+
/**
|
|
21
|
+
* List all installed plugins with their status
|
|
22
|
+
* Reads from Claude Code's native installed_plugins.json
|
|
23
|
+
* @returns {Object} { plugins: Array }
|
|
24
|
+
*/
|
|
25
|
+
listPlugins() {
|
|
26
|
+
const plugins = [];
|
|
27
|
+
|
|
28
|
+
// Read Claude Code's installed_plugins.json
|
|
29
|
+
if (fs.existsSync(CLAUDE_INSTALLED_FILE)) {
|
|
30
|
+
try {
|
|
31
|
+
const data = JSON.parse(fs.readFileSync(CLAUDE_INSTALLED_FILE, 'utf8'));
|
|
32
|
+
if (data.plugins) {
|
|
33
|
+
for (const [key, installations] of Object.entries(data.plugins)) {
|
|
34
|
+
if (installations && installations.length > 0) {
|
|
35
|
+
const install = installations[0]; // Get first installation
|
|
36
|
+
const [name, marketplace] = key.split('@');
|
|
37
|
+
|
|
38
|
+
// Read plugin.json from installPath for description
|
|
39
|
+
let description = '';
|
|
40
|
+
let source = install.source || '';
|
|
41
|
+
let repoUrl = '';
|
|
42
|
+
|
|
43
|
+
if (install.installPath && fs.existsSync(install.installPath)) {
|
|
44
|
+
const manifestPath = path.join(install.installPath, 'plugin.json');
|
|
45
|
+
if (fs.existsSync(manifestPath)) {
|
|
46
|
+
try {
|
|
47
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
48
|
+
description = manifest.description || '';
|
|
49
|
+
} catch (err) {
|
|
50
|
+
// Ignore parse errors
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Parse repoUrl from source if available
|
|
56
|
+
if (source) {
|
|
57
|
+
const match = source.match(/github\.com\/([^\/]+)\/([^\/\.]+)/);
|
|
58
|
+
if (match) {
|
|
59
|
+
repoUrl = `https://github.com/${match[1]}/${match[2]}`;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
plugins.push({
|
|
64
|
+
name,
|
|
65
|
+
marketplace,
|
|
66
|
+
version: install.version || '1.0.0',
|
|
67
|
+
installPath: install.installPath,
|
|
68
|
+
installedAt: install.installedAt,
|
|
69
|
+
scope: install.scope,
|
|
70
|
+
enabled: true,
|
|
71
|
+
description,
|
|
72
|
+
source,
|
|
73
|
+
repoUrl
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
} catch (err) {
|
|
79
|
+
console.error('[PluginsService] Failed to read installed_plugins.json:', err.message);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Also check legacy registry
|
|
84
|
+
try {
|
|
85
|
+
const legacyPlugins = listPlugins();
|
|
86
|
+
for (const plugin of legacyPlugins) {
|
|
87
|
+
if (!plugins.find(p => p.name === plugin.name)) {
|
|
88
|
+
plugins.push(plugin);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
} catch (err) {
|
|
92
|
+
// Ignore legacy registry errors
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return { plugins };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Get single plugin details
|
|
100
|
+
* @param {string} name - Plugin name
|
|
101
|
+
* @returns {Object|null} Plugin details or null
|
|
102
|
+
*/
|
|
103
|
+
getPlugin(name) {
|
|
104
|
+
const plugin = getPlugin(name);
|
|
105
|
+
if (!plugin) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const pluginDir = path.join(INSTALLED_DIR, name);
|
|
110
|
+
const manifestPath = path.join(pluginDir, 'plugin.json');
|
|
111
|
+
|
|
112
|
+
let manifest = null;
|
|
113
|
+
if (fs.existsSync(manifestPath)) {
|
|
114
|
+
try {
|
|
115
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
116
|
+
} catch (err) {
|
|
117
|
+
// Ignore parse errors
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
name,
|
|
123
|
+
...plugin,
|
|
124
|
+
description: manifest?.description || '',
|
|
125
|
+
author: manifest?.author || '',
|
|
126
|
+
commands: manifest?.commands || [],
|
|
127
|
+
hooks: manifest?.hooks || [],
|
|
128
|
+
manifest
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Install plugin from Git URL or repo directory
|
|
134
|
+
* @param {string} source - Git repository URL or tree URL
|
|
135
|
+
* @param {Object} repoInfo - Optional repo info { owner, name, branch, directory }
|
|
136
|
+
* @returns {Promise<Object>} Installation result
|
|
137
|
+
*/
|
|
138
|
+
async installPlugin(source, repoInfo = null) {
|
|
139
|
+
// If repoInfo is provided, download from GitHub directly
|
|
140
|
+
if (repoInfo && repoInfo.owner && repoInfo.name && repoInfo.directory) {
|
|
141
|
+
return await this._installFromGitHubDirectory(repoInfo);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Parse tree URL format: https://github.com/owner/repo/tree/branch/path
|
|
145
|
+
const treeMatch = source.match(/github\.com\/([^\/]+)\/([^\/]+)\/tree\/([^\/]+)\/(.+)/);
|
|
146
|
+
if (treeMatch) {
|
|
147
|
+
const [, owner, name, branch, directory] = treeMatch;
|
|
148
|
+
return await this._installFromGitHubDirectory({ owner, name, branch, directory });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Fallback to original git clone method
|
|
152
|
+
return await installPluginCore(source);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Install plugin from GitHub directory
|
|
157
|
+
* @private
|
|
158
|
+
*/
|
|
159
|
+
async _installFromGitHubDirectory(repoInfo) {
|
|
160
|
+
const { owner, name, branch, directory } = repoInfo;
|
|
161
|
+
const https = require('https');
|
|
162
|
+
const pluginName = directory.split('/').pop();
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
// Fetch plugin.json from the directory
|
|
166
|
+
const manifestUrl = `https://raw.githubusercontent.com/${owner}/${name}/${branch}/${directory}/plugin.json`;
|
|
167
|
+
let manifest;
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
manifest = await this._fetchJson(manifestUrl);
|
|
171
|
+
} catch (e) {
|
|
172
|
+
// No plugin.json, create a basic manifest
|
|
173
|
+
manifest = { name: pluginName, version: '1.0.0' };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Create plugin directory
|
|
177
|
+
const pluginDir = path.join(INSTALLED_DIR, manifest.name || pluginName);
|
|
178
|
+
if (!fs.existsSync(pluginDir)) {
|
|
179
|
+
fs.mkdirSync(pluginDir, { recursive: true });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Download all files from the directory
|
|
183
|
+
const contentsUrl = `https://api.github.com/repos/${owner}/${name}/contents/${directory}?ref=${branch}`;
|
|
184
|
+
const contents = await this._fetchJson(contentsUrl);
|
|
185
|
+
|
|
186
|
+
for (const item of contents) {
|
|
187
|
+
if (item.type === 'file') {
|
|
188
|
+
const fileContent = await this._fetchRawFile(item.download_url);
|
|
189
|
+
fs.writeFileSync(path.join(pluginDir, item.name), fileContent);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Write plugin.json if not exists
|
|
194
|
+
const manifestPath = path.join(pluginDir, 'plugin.json');
|
|
195
|
+
if (!fs.existsSync(manifestPath)) {
|
|
196
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Register plugin
|
|
200
|
+
const { addPlugin } = require('../../plugins/registry');
|
|
201
|
+
addPlugin(manifest.name || pluginName, {
|
|
202
|
+
version: manifest.version || '1.0.0',
|
|
203
|
+
enabled: true,
|
|
204
|
+
installedAt: new Date().toISOString(),
|
|
205
|
+
source: `https://github.com/${owner}/${name}/tree/${branch}/${directory}`
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
success: true,
|
|
210
|
+
plugin: {
|
|
211
|
+
name: manifest.name || pluginName,
|
|
212
|
+
version: manifest.version || '1.0.0',
|
|
213
|
+
description: manifest.description || ''
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
} catch (err) {
|
|
217
|
+
return {
|
|
218
|
+
success: false,
|
|
219
|
+
error: `Failed to install plugin: ${err.message}`
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Fetch raw file content
|
|
226
|
+
* @private
|
|
227
|
+
*/
|
|
228
|
+
async _fetchRawFile(url) {
|
|
229
|
+
const https = require('https');
|
|
230
|
+
return new Promise((resolve, reject) => {
|
|
231
|
+
https.get(url, {
|
|
232
|
+
headers: { 'User-Agent': 'coding-tool-x' }
|
|
233
|
+
}, (res) => {
|
|
234
|
+
let data = '';
|
|
235
|
+
res.on('data', chunk => data += chunk);
|
|
236
|
+
res.on('end', () => {
|
|
237
|
+
if (res.statusCode === 200) {
|
|
238
|
+
resolve(data);
|
|
239
|
+
} else {
|
|
240
|
+
reject(new Error(`HTTP ${res.statusCode}`));
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
}).on('error', reject);
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Uninstall plugin
|
|
249
|
+
* @param {string} name - Plugin name
|
|
250
|
+
* @returns {Object} Uninstallation result
|
|
251
|
+
*/
|
|
252
|
+
uninstallPlugin(name) {
|
|
253
|
+
return uninstallPluginCore(name);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Toggle plugin enabled/disabled
|
|
258
|
+
* @param {string} name - Plugin name
|
|
259
|
+
* @param {boolean} enabled - Enable or disable
|
|
260
|
+
* @returns {Object} Updated plugin info
|
|
261
|
+
*/
|
|
262
|
+
togglePlugin(name, enabled) {
|
|
263
|
+
const plugin = getPlugin(name);
|
|
264
|
+
if (!plugin) {
|
|
265
|
+
throw new Error(`Plugin "${name}" not found`);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
updatePluginRegistry(name, { enabled });
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
name,
|
|
272
|
+
...getPlugin(name)
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Update plugin config
|
|
278
|
+
* @param {string} name - Plugin name
|
|
279
|
+
* @param {Object} config - Configuration object
|
|
280
|
+
* @returns {Object} Result
|
|
281
|
+
*/
|
|
282
|
+
updatePluginConfig(name, config) {
|
|
283
|
+
const plugin = getPlugin(name);
|
|
284
|
+
if (!plugin) {
|
|
285
|
+
throw new Error(`Plugin "${name}" not found`);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const configFile = path.join(CONFIG_DIR, `${name}.json`);
|
|
289
|
+
|
|
290
|
+
// Ensure config directory exists
|
|
291
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
292
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
fs.writeFileSync(configFile, JSON.stringify(config, null, 2), 'utf8');
|
|
296
|
+
|
|
297
|
+
return {
|
|
298
|
+
success: true,
|
|
299
|
+
message: `Configuration updated for plugin "${name}"`
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Get plugin repositories config file path
|
|
305
|
+
* @returns {string} Config file path
|
|
306
|
+
*/
|
|
307
|
+
getReposConfigPath() {
|
|
308
|
+
const os = require('os');
|
|
309
|
+
const configDir = path.join(os.homedir(), '.claude', 'cc-tool');
|
|
310
|
+
if (!fs.existsSync(configDir)) {
|
|
311
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
312
|
+
}
|
|
313
|
+
return path.join(configDir, 'plugin-repos.json');
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Load repos from config file
|
|
318
|
+
* @returns {Object} Config object with repos array
|
|
319
|
+
*/
|
|
320
|
+
loadReposConfig() {
|
|
321
|
+
const configPath = this.getReposConfigPath();
|
|
322
|
+
if (!fs.existsSync(configPath)) {
|
|
323
|
+
return { repos: [] };
|
|
324
|
+
}
|
|
325
|
+
try {
|
|
326
|
+
return JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
327
|
+
} catch (err) {
|
|
328
|
+
console.error('Failed to load repos config:', err);
|
|
329
|
+
return { repos: [] };
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Save repos to config file
|
|
335
|
+
* @param {Object} config - Config object with repos array
|
|
336
|
+
*/
|
|
337
|
+
saveReposConfig(config) {
|
|
338
|
+
const configPath = this.getReposConfigPath();
|
|
339
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Get plugin repositories
|
|
344
|
+
* Reads from both our config and Claude Code's native marketplace config
|
|
345
|
+
* @returns {Array} Repos list
|
|
346
|
+
*/
|
|
347
|
+
getRepos() {
|
|
348
|
+
const repos = [];
|
|
349
|
+
const seenRepos = new Set();
|
|
350
|
+
|
|
351
|
+
// 1. Load our own config
|
|
352
|
+
const config = this.loadReposConfig();
|
|
353
|
+
for (const repo of config.repos || []) {
|
|
354
|
+
const key = `${repo.owner}/${repo.name}`;
|
|
355
|
+
if (!seenRepos.has(key)) {
|
|
356
|
+
repos.push(repo);
|
|
357
|
+
seenRepos.add(key);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// 2. Load Claude Code's native marketplace config
|
|
362
|
+
if (fs.existsSync(CLAUDE_MARKETPLACES_FILE)) {
|
|
363
|
+
try {
|
|
364
|
+
const marketplaces = JSON.parse(fs.readFileSync(CLAUDE_MARKETPLACES_FILE, 'utf8'));
|
|
365
|
+
|
|
366
|
+
for (const [marketplaceName, marketplaceData] of Object.entries(marketplaces)) {
|
|
367
|
+
if (marketplaceData.source && marketplaceData.source.url) {
|
|
368
|
+
const url = marketplaceData.source.url;
|
|
369
|
+
const match = url.match(/github\.com\/([^\/]+)\/([^\/\.]+)/);
|
|
370
|
+
|
|
371
|
+
if (match) {
|
|
372
|
+
const [, owner, name] = match;
|
|
373
|
+
const key = `${owner}/${name}`;
|
|
374
|
+
|
|
375
|
+
if (!seenRepos.has(key)) {
|
|
376
|
+
repos.push({
|
|
377
|
+
owner,
|
|
378
|
+
name,
|
|
379
|
+
url,
|
|
380
|
+
branch: 'main', // Default branch
|
|
381
|
+
enabled: true,
|
|
382
|
+
source: 'claude-native',
|
|
383
|
+
lastUpdated: marketplaceData.lastUpdated
|
|
384
|
+
});
|
|
385
|
+
seenRepos.add(key);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
} catch (err) {
|
|
391
|
+
console.error('[PluginsService] Failed to read known_marketplaces.json:', err.message);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return repos;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Add repository
|
|
400
|
+
* @param {Object} repo - Repository info { url, owner, name, branch, enabled }
|
|
401
|
+
* @returns {Array} Updated repos list
|
|
402
|
+
*/
|
|
403
|
+
addRepo(repo) {
|
|
404
|
+
const config = this.loadReposConfig();
|
|
405
|
+
|
|
406
|
+
// Parse URL if provided
|
|
407
|
+
let owner = repo.owner;
|
|
408
|
+
let name = repo.name;
|
|
409
|
+
let url = repo.url;
|
|
410
|
+
|
|
411
|
+
if (url && !owner && !name) {
|
|
412
|
+
// Extract owner/name from URL
|
|
413
|
+
const match = url.match(/github\.com\/([^\/]+)\/([^\/\.]+)/);
|
|
414
|
+
if (match) {
|
|
415
|
+
owner = match[1];
|
|
416
|
+
name = match[2];
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (!owner || !name) {
|
|
421
|
+
throw new Error('Repository owner and name are required');
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Construct URL if not provided
|
|
425
|
+
if (!url) {
|
|
426
|
+
url = `https://github.com/${owner}/${name}`;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Check if repo already exists
|
|
430
|
+
const exists = config.repos.some(r => r.owner === owner && r.name === name);
|
|
431
|
+
if (exists) {
|
|
432
|
+
throw new Error(`Repository ${owner}/${name} already exists`);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Add new repo
|
|
436
|
+
const newRepo = {
|
|
437
|
+
owner,
|
|
438
|
+
name,
|
|
439
|
+
url,
|
|
440
|
+
branch: repo.branch || 'main',
|
|
441
|
+
enabled: repo.enabled !== false,
|
|
442
|
+
addedAt: new Date().toISOString()
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
config.repos.push(newRepo);
|
|
446
|
+
this.saveReposConfig(config);
|
|
447
|
+
|
|
448
|
+
return config.repos;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Remove repository
|
|
453
|
+
* @param {string} owner - Repository owner
|
|
454
|
+
* @param {string} name - Repository name
|
|
455
|
+
* @returns {Array} Updated repos list
|
|
456
|
+
*/
|
|
457
|
+
removeRepo(owner, name) {
|
|
458
|
+
const config = this.loadReposConfig();
|
|
459
|
+
config.repos = config.repos.filter(r => !(r.owner === owner && r.name === name));
|
|
460
|
+
this.saveReposConfig(config);
|
|
461
|
+
return config.repos;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Toggle repository enabled status
|
|
466
|
+
* @param {string} owner - Repository owner
|
|
467
|
+
* @param {string} name - Repository name
|
|
468
|
+
* @param {boolean} enabled - Enable or disable
|
|
469
|
+
* @returns {Array} Updated repos list
|
|
470
|
+
*/
|
|
471
|
+
toggleRepo(owner, name, enabled) {
|
|
472
|
+
const config = this.loadReposConfig();
|
|
473
|
+
const repo = config.repos.find(r => r.owner === owner && r.name === name);
|
|
474
|
+
if (!repo) {
|
|
475
|
+
throw new Error(`Repository ${owner}/${name} not found`);
|
|
476
|
+
}
|
|
477
|
+
repo.enabled = enabled;
|
|
478
|
+
this.saveReposConfig(config);
|
|
479
|
+
return config.repos;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Sync repositories to Claude Code marketplace
|
|
484
|
+
* @returns {Promise<Object>} Sync results
|
|
485
|
+
*/
|
|
486
|
+
async syncRepos() {
|
|
487
|
+
const repos = this.getRepos();
|
|
488
|
+
const results = [];
|
|
489
|
+
const { execSync } = require('child_process');
|
|
490
|
+
|
|
491
|
+
for (const repo of repos.filter(r => r.enabled)) {
|
|
492
|
+
try {
|
|
493
|
+
execSync(`claude plugin marketplace add ${repo.url}`, {
|
|
494
|
+
encoding: 'utf8',
|
|
495
|
+
timeout: 30000,
|
|
496
|
+
stdio: 'pipe'
|
|
497
|
+
});
|
|
498
|
+
results.push({ repo: repo.url, success: true });
|
|
499
|
+
} catch (err) {
|
|
500
|
+
results.push({ repo: repo.url, success: false, error: err.message });
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
return { success: true, results };
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Sync plugins from Claude Code
|
|
509
|
+
* @returns {Promise<Object>} Updated plugins list
|
|
510
|
+
*/
|
|
511
|
+
async syncPlugins() {
|
|
512
|
+
return this.listPlugins();
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Fetch JSON from URL
|
|
517
|
+
* @private
|
|
518
|
+
*/
|
|
519
|
+
async _fetchJson(url) {
|
|
520
|
+
const https = require('https');
|
|
521
|
+
return new Promise((resolve, reject) => {
|
|
522
|
+
https.get(url, {
|
|
523
|
+
headers: {
|
|
524
|
+
'User-Agent': 'coding-tool-x',
|
|
525
|
+
'Accept': 'application/vnd.github.v3+json'
|
|
526
|
+
}
|
|
527
|
+
}, (res) => {
|
|
528
|
+
let data = '';
|
|
529
|
+
res.on('data', chunk => data += chunk);
|
|
530
|
+
res.on('end', () => {
|
|
531
|
+
if (res.statusCode === 200) {
|
|
532
|
+
resolve(JSON.parse(data));
|
|
533
|
+
} else {
|
|
534
|
+
reject(new Error(`HTTP ${res.statusCode}: ${data.slice(0, 200)}`));
|
|
535
|
+
}
|
|
536
|
+
});
|
|
537
|
+
}).on('error', reject);
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Get plugin README content
|
|
543
|
+
* @param {Object} plugin - Plugin object with name, repoUrl, source, or repoInfo
|
|
544
|
+
* @returns {Promise<string>} README content or empty string
|
|
545
|
+
*/
|
|
546
|
+
async getPluginReadme(plugin) {
|
|
547
|
+
try {
|
|
548
|
+
let readmeUrl = null;
|
|
549
|
+
|
|
550
|
+
// Case 1: Market plugin with repoInfo
|
|
551
|
+
if (plugin.repoOwner && plugin.repoName && plugin.directory) {
|
|
552
|
+
const branch = plugin.repoBranch || 'main';
|
|
553
|
+
readmeUrl = `https://raw.githubusercontent.com/${plugin.repoOwner}/${plugin.repoName}/${branch}/${plugin.directory}/README.md`;
|
|
554
|
+
}
|
|
555
|
+
// Case 2: Installed plugin with source URL
|
|
556
|
+
else if (plugin.source) {
|
|
557
|
+
const treeMatch = plugin.source.match(/github\.com\/([^\/]+)\/([^\/]+)\/tree\/([^\/]+)\/(.+)/);
|
|
558
|
+
if (treeMatch) {
|
|
559
|
+
const [, owner, name, branch, directory] = treeMatch;
|
|
560
|
+
readmeUrl = `https://raw.githubusercontent.com/${owner}/${name}/${branch}/${directory}/README.md`;
|
|
561
|
+
} else {
|
|
562
|
+
// Try to parse as regular repo URL
|
|
563
|
+
const repoMatch = plugin.source.match(/github\.com\/([^\/]+)\/([^\/\.]+)/);
|
|
564
|
+
if (repoMatch) {
|
|
565
|
+
const [, owner, name] = repoMatch;
|
|
566
|
+
readmeUrl = `https://raw.githubusercontent.com/${owner}/${name}/main/README.md`;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
// Case 3: Plugin with repoUrl
|
|
571
|
+
else if (plugin.repoUrl) {
|
|
572
|
+
const match = plugin.repoUrl.match(/github\.com\/([^\/]+)\/([^\/\.]+)/);
|
|
573
|
+
if (match) {
|
|
574
|
+
const [, owner, name] = match;
|
|
575
|
+
readmeUrl = `https://raw.githubusercontent.com/${owner}/${name}/main/README.md`;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
if (!readmeUrl) {
|
|
580
|
+
return '';
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// Fetch README content
|
|
584
|
+
const content = await this._fetchRawFile(readmeUrl);
|
|
585
|
+
return content;
|
|
586
|
+
} catch (err) {
|
|
587
|
+
console.error('[PluginsService] Failed to fetch README:', err.message);
|
|
588
|
+
return '';
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Get market plugins from configured repositories
|
|
594
|
+
* @returns {Promise<Array>} List of available market plugins
|
|
595
|
+
*/
|
|
596
|
+
async getMarketPlugins() {
|
|
597
|
+
const repos = this.getRepos().filter(r => r.enabled);
|
|
598
|
+
const marketPlugins = [];
|
|
599
|
+
|
|
600
|
+
for (const repo of repos) {
|
|
601
|
+
try {
|
|
602
|
+
const branch = repo.branch || 'main';
|
|
603
|
+
|
|
604
|
+
// Try to fetch marketplace.json first (official format)
|
|
605
|
+
try {
|
|
606
|
+
const marketplaceUrl = `https://raw.githubusercontent.com/${repo.owner}/${repo.name}/${branch}/.claude-plugin/marketplace.json`;
|
|
607
|
+
const marketplace = await this._fetchJson(marketplaceUrl);
|
|
608
|
+
|
|
609
|
+
if (marketplace && marketplace.plugins) {
|
|
610
|
+
for (const plugin of marketplace.plugins) {
|
|
611
|
+
marketPlugins.push({
|
|
612
|
+
name: plugin.name,
|
|
613
|
+
description: plugin.description || '',
|
|
614
|
+
author: plugin.author?.name || marketplace.owner?.name || repo.owner,
|
|
615
|
+
version: plugin.version || '1.0.0',
|
|
616
|
+
category: plugin.category || 'general',
|
|
617
|
+
repoUrl: `https://github.com/${repo.owner}/${repo.name}`,
|
|
618
|
+
repoOwner: repo.owner,
|
|
619
|
+
repoName: repo.name,
|
|
620
|
+
repoBranch: branch,
|
|
621
|
+
directory: plugin.source?.replace(/^\.\//, '') || plugin.name,
|
|
622
|
+
lspServers: plugin.lspServers || null,
|
|
623
|
+
isInstalled: false
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
continue; // Skip legacy format check
|
|
627
|
+
}
|
|
628
|
+
} catch (e) {
|
|
629
|
+
// marketplace.json not found, try legacy format
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// Legacy format: each directory is a plugin with plugin.json
|
|
633
|
+
const apiUrl = `https://api.github.com/repos/${repo.owner}/${repo.name}/contents?ref=${branch}`;
|
|
634
|
+
const contents = await this._fetchJson(apiUrl);
|
|
635
|
+
const pluginDirs = contents.filter(item => item.type === 'dir' && !item.name.startsWith('.'));
|
|
636
|
+
|
|
637
|
+
for (const dir of pluginDirs) {
|
|
638
|
+
try {
|
|
639
|
+
const manifestUrl = `https://raw.githubusercontent.com/${repo.owner}/${repo.name}/${branch}/${dir.name}/plugin.json`;
|
|
640
|
+
const manifest = await this._fetchJson(manifestUrl);
|
|
641
|
+
|
|
642
|
+
marketPlugins.push({
|
|
643
|
+
name: manifest.name || dir.name,
|
|
644
|
+
description: manifest.description || '',
|
|
645
|
+
author: manifest.author || repo.owner,
|
|
646
|
+
version: manifest.version || '1.0.0',
|
|
647
|
+
repoUrl: `https://github.com/${repo.owner}/${repo.name}`,
|
|
648
|
+
repoOwner: repo.owner,
|
|
649
|
+
repoName: repo.name,
|
|
650
|
+
repoBranch: branch,
|
|
651
|
+
directory: dir.name,
|
|
652
|
+
commands: manifest.commands || [],
|
|
653
|
+
hooks: manifest.hooks || [],
|
|
654
|
+
isInstalled: false
|
|
655
|
+
});
|
|
656
|
+
} catch (e) {
|
|
657
|
+
// No plugin.json in this directory, skip
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
} catch (err) {
|
|
661
|
+
console.error(`[PluginsService] Failed to fetch plugins from ${repo.owner}/${repo.name}:`, err.message);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// Mark installed plugins
|
|
666
|
+
const installedPlugins = this.listPlugins().plugins;
|
|
667
|
+
const installedNames = new Set(installedPlugins.map(p => p.name));
|
|
668
|
+
|
|
669
|
+
marketPlugins.forEach(plugin => {
|
|
670
|
+
plugin.isInstalled = installedNames.has(plugin.name);
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
return marketPlugins;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
module.exports = { PluginsService };
|