@adversity/coding-tool-x 2.6.0 → 3.0.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/dist/web/assets/icons-BlzwYoRU.js +1 -0
- package/dist/web/assets/{index-Ej0MPDUI.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 +2 -1
- package/src/plugins/constants.js +14 -0
- package/src/plugins/event-bus.js +54 -0
- package/src/plugins/manifest-validator.js +129 -0
- package/src/plugins/plugin-api.js +128 -0
- package/src/plugins/plugin-installer.js +601 -0
- package/src/plugins/plugin-loader.js +229 -0
- package/src/plugins/plugin-manager.js +170 -0
- package/src/plugins/registry.js +152 -0
- package/src/plugins/schema/plugin-manifest.json +115 -0
- package/src/server/api/mcp.js +1 -1
- package/src/server/api/plugins.js +150 -10
- package/src/server/api/sessions.js +4 -4
- package/src/server/services/plugins-service.js +537 -37
- package/src/server/services/sessions.js +72 -16
- package/dist/web/assets/icons-CNM9_Fh0.js +0 -1
- package/dist/web/assets/index-BcmuQT-z.css +0 -41
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { getPlugin } = require('./registry');
|
|
4
|
+
const { INSTALLED_DIR, CONFIG_DIR } = require('./constants');
|
|
5
|
+
const { createPluginContext } = require('./plugin-api');
|
|
6
|
+
|
|
7
|
+
// Track loaded plugins
|
|
8
|
+
const loadedPlugins = new Map();
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Load and activate a plugin
|
|
12
|
+
* @param {string} name - Plugin name
|
|
13
|
+
* @returns {Object} Result object with success status and optional error
|
|
14
|
+
*/
|
|
15
|
+
function loadPlugin(name) {
|
|
16
|
+
// ERROR ISOLATION: Wrap entire function to prevent CTX crash
|
|
17
|
+
try {
|
|
18
|
+
// Check if already loaded
|
|
19
|
+
if (loadedPlugins.has(name)) {
|
|
20
|
+
return {
|
|
21
|
+
success: true,
|
|
22
|
+
message: `Plugin '${name}' already loaded`,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Get plugin info from registry
|
|
27
|
+
const pluginInfo = getPlugin(name);
|
|
28
|
+
if (!pluginInfo) {
|
|
29
|
+
return {
|
|
30
|
+
success: false,
|
|
31
|
+
error: `Plugin '${name}' not found in registry`,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Check if enabled
|
|
36
|
+
if (!pluginInfo.enabled) {
|
|
37
|
+
return {
|
|
38
|
+
success: false,
|
|
39
|
+
error: `Plugin '${name}' is disabled`,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Construct path to installed plugin
|
|
44
|
+
const pluginDir = path.join(INSTALLED_DIR, name);
|
|
45
|
+
if (!fs.existsSync(pluginDir)) {
|
|
46
|
+
return {
|
|
47
|
+
success: false,
|
|
48
|
+
error: `Plugin directory not found: ${pluginDir}`,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Read and validate plugin.json
|
|
53
|
+
const pluginJsonPath = path.join(pluginDir, 'plugin.json');
|
|
54
|
+
if (!fs.existsSync(pluginJsonPath)) {
|
|
55
|
+
return {
|
|
56
|
+
success: false,
|
|
57
|
+
error: `plugin.json not found in ${pluginDir}`,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let pluginJson;
|
|
62
|
+
try {
|
|
63
|
+
const content = fs.readFileSync(pluginJsonPath, 'utf8');
|
|
64
|
+
pluginJson = JSON.parse(content);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
return {
|
|
67
|
+
success: false,
|
|
68
|
+
error: `Failed to parse plugin.json: ${error.message}`,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Validate plugin.json structure
|
|
73
|
+
if (!pluginJson.name || !pluginJson.version || !pluginJson.main) {
|
|
74
|
+
return {
|
|
75
|
+
success: false,
|
|
76
|
+
error: 'Invalid plugin.json: missing name, version, or main',
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Require plugin's main file
|
|
81
|
+
const mainPath = path.join(pluginDir, pluginJson.main);
|
|
82
|
+
|
|
83
|
+
// SECURITY: Verify resolved path is within pluginDir (prevent path traversal)
|
|
84
|
+
const resolvedMainPath = path.resolve(mainPath);
|
|
85
|
+
const resolvedPluginDir = path.resolve(pluginDir);
|
|
86
|
+
if (!resolvedMainPath.startsWith(resolvedPluginDir + path.sep)) {
|
|
87
|
+
return {
|
|
88
|
+
success: false,
|
|
89
|
+
error: `Security violation: Main file path traversal detected: ${pluginJson.main}`,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!fs.existsSync(mainPath)) {
|
|
94
|
+
return {
|
|
95
|
+
success: false,
|
|
96
|
+
error: `Main file not found: ${mainPath}`,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
let pluginModule;
|
|
101
|
+
try {
|
|
102
|
+
pluginModule = require(mainPath);
|
|
103
|
+
} catch (error) {
|
|
104
|
+
return {
|
|
105
|
+
success: false,
|
|
106
|
+
error: `Failed to require plugin: ${error.message}`,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Verify exports: { activate, deactivate }
|
|
111
|
+
if (typeof pluginModule.activate !== 'function') {
|
|
112
|
+
return {
|
|
113
|
+
success: false,
|
|
114
|
+
error: 'Plugin must export an activate() function',
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Load plugin-specific configuration
|
|
119
|
+
const pluginConfigPath = path.join(CONFIG_DIR, `${name}.json`);
|
|
120
|
+
let pluginConfig = {};
|
|
121
|
+
if (fs.existsSync(pluginConfigPath)) {
|
|
122
|
+
try {
|
|
123
|
+
const configContent = fs.readFileSync(pluginConfigPath, 'utf8');
|
|
124
|
+
pluginConfig = JSON.parse(configContent);
|
|
125
|
+
} catch (error) {
|
|
126
|
+
// Non-fatal: log but continue with empty config
|
|
127
|
+
console.warn(`[plugin-loader] Failed to load config for ${name}:`, error.message);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Create plugin context
|
|
132
|
+
const ctx = createPluginContext(name, pluginConfig, pluginDir);
|
|
133
|
+
|
|
134
|
+
// Call activate() wrapped in try/catch
|
|
135
|
+
try {
|
|
136
|
+
pluginModule.activate(ctx);
|
|
137
|
+
} catch (error) {
|
|
138
|
+
return {
|
|
139
|
+
success: false,
|
|
140
|
+
error: `Plugin activation failed: ${error.message}`,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Track loaded plugin
|
|
145
|
+
loadedPlugins.set(name, {
|
|
146
|
+
module: pluginModule,
|
|
147
|
+
context: ctx,
|
|
148
|
+
info: pluginJson,
|
|
149
|
+
loadedAt: new Date().toISOString(),
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
success: true,
|
|
154
|
+
message: `Plugin '${name}' loaded successfully`,
|
|
155
|
+
};
|
|
156
|
+
} catch (error) {
|
|
157
|
+
// ERROR ISOLATION: Catch any unexpected errors
|
|
158
|
+
return {
|
|
159
|
+
success: false,
|
|
160
|
+
error: `Unexpected error loading plugin '${name}': ${error.message}`,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Unload and deactivate a plugin
|
|
167
|
+
* @param {string} name - Plugin name
|
|
168
|
+
* @returns {Object} Result object with success status
|
|
169
|
+
*/
|
|
170
|
+
function unloadPlugin(name) {
|
|
171
|
+
try {
|
|
172
|
+
// Check if loaded
|
|
173
|
+
if (!loadedPlugins.has(name)) {
|
|
174
|
+
return {
|
|
175
|
+
success: false,
|
|
176
|
+
error: `Plugin '${name}' is not loaded`,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const plugin = loadedPlugins.get(name);
|
|
181
|
+
|
|
182
|
+
// Call deactivate() if exists
|
|
183
|
+
if (typeof plugin.module.deactivate === 'function') {
|
|
184
|
+
try {
|
|
185
|
+
plugin.module.deactivate();
|
|
186
|
+
} catch (error) {
|
|
187
|
+
// Log but don't fail unload
|
|
188
|
+
console.error(`[plugin-loader] Error during deactivation of '${name}':`, error.message);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Remove from tracking
|
|
193
|
+
loadedPlugins.delete(name);
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
success: true,
|
|
197
|
+
message: `Plugin '${name}' unloaded successfully`,
|
|
198
|
+
};
|
|
199
|
+
} catch (error) {
|
|
200
|
+
return {
|
|
201
|
+
success: false,
|
|
202
|
+
error: `Unexpected error unloading plugin '${name}': ${error.message}`,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Get list of currently loaded plugin names
|
|
209
|
+
* @returns {Array<string>} Array of loaded plugin names
|
|
210
|
+
*/
|
|
211
|
+
function getLoadedPlugins() {
|
|
212
|
+
return Array.from(loadedPlugins.keys());
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Get loaded plugin details (internal use)
|
|
217
|
+
* @param {string} name - Plugin name
|
|
218
|
+
* @returns {Object|undefined} Plugin details or undefined
|
|
219
|
+
*/
|
|
220
|
+
function getLoadedPluginDetails(name) {
|
|
221
|
+
return loadedPlugins.get(name);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
module.exports = {
|
|
225
|
+
loadPlugin,
|
|
226
|
+
unloadPlugin,
|
|
227
|
+
getLoadedPlugins,
|
|
228
|
+
getLoadedPluginDetails,
|
|
229
|
+
};
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
const { loadRegistry, listPlugins } = require('./registry');
|
|
2
|
+
const { loadPlugin, unloadPlugin, getLoadedPlugins, getLoadedPluginDetails } = require('./plugin-loader');
|
|
3
|
+
const eventBus = require('./event-bus');
|
|
4
|
+
|
|
5
|
+
// Module state
|
|
6
|
+
let initialized = false;
|
|
7
|
+
const registeredCommands = new Map(); // { commandName: { pluginName, handler } }
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Initialize all enabled plugins
|
|
11
|
+
* @param {Object} [options] - Initialization options
|
|
12
|
+
* @param {Object} [options.config] - Application configuration
|
|
13
|
+
* @param {Array} [options.args] - CLI arguments
|
|
14
|
+
* @returns {Object} Result with loaded count and failed plugins
|
|
15
|
+
*/
|
|
16
|
+
function initializePlugins(options = {}) {
|
|
17
|
+
if (initialized) {
|
|
18
|
+
return {
|
|
19
|
+
loaded: getLoadedPlugins().length,
|
|
20
|
+
failed: [],
|
|
21
|
+
message: 'Plugins already initialized',
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const { config = {}, args = [] } = options;
|
|
26
|
+
const failed = [];
|
|
27
|
+
let loadedCount = 0;
|
|
28
|
+
|
|
29
|
+
// Load registry
|
|
30
|
+
const registry = loadRegistry();
|
|
31
|
+
const plugins = Object.entries(registry.plugins || {});
|
|
32
|
+
|
|
33
|
+
// Sort plugins by loadOrder (lower = earlier)
|
|
34
|
+
const sortedPlugins = plugins
|
|
35
|
+
.filter(([, info]) => info.enabled)
|
|
36
|
+
.sort((a, b) => (a[1].loadOrder || 10) - (b[1].loadOrder || 10));
|
|
37
|
+
|
|
38
|
+
// Load each enabled plugin
|
|
39
|
+
for (const [name] of sortedPlugins) {
|
|
40
|
+
const result = loadPlugin(name);
|
|
41
|
+
|
|
42
|
+
if (result.success) {
|
|
43
|
+
loadedCount++;
|
|
44
|
+
|
|
45
|
+
// Collect registered commands from this plugin
|
|
46
|
+
const pluginDetails = getLoadedPluginDetails(name);
|
|
47
|
+
if (pluginDetails && pluginDetails.context) {
|
|
48
|
+
const commands = pluginDetails.context._getCommands();
|
|
49
|
+
commands.forEach((handler, cmdName) => {
|
|
50
|
+
registeredCommands.set(cmdName, { pluginName: name, handler });
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
} else {
|
|
54
|
+
failed.push({ name, error: result.error });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Emit cli:init event
|
|
59
|
+
eventBus.emitSync('cli:init', { config, args });
|
|
60
|
+
|
|
61
|
+
initialized = true;
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
loaded: loadedCount,
|
|
65
|
+
failed,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Shutdown all loaded plugins
|
|
71
|
+
* @returns {Object} Confirmation of shutdown
|
|
72
|
+
*/
|
|
73
|
+
function shutdownPlugins() {
|
|
74
|
+
if (!initialized) {
|
|
75
|
+
return {
|
|
76
|
+
success: true,
|
|
77
|
+
message: 'Plugins not initialized, nothing to shutdown',
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Emit cli:shutdown event
|
|
82
|
+
eventBus.emitSync('cli:shutdown', {});
|
|
83
|
+
|
|
84
|
+
// Unload each loaded plugin in reverse order
|
|
85
|
+
const loadedNames = getLoadedPlugins();
|
|
86
|
+
const errors = [];
|
|
87
|
+
|
|
88
|
+
for (const name of loadedNames.reverse()) {
|
|
89
|
+
const result = unloadPlugin(name);
|
|
90
|
+
if (!result.success) {
|
|
91
|
+
errors.push({ name, error: result.error });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Clear command registry
|
|
96
|
+
registeredCommands.clear();
|
|
97
|
+
initialized = false;
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
success: errors.length === 0,
|
|
101
|
+
unloaded: loadedNames.length - errors.length,
|
|
102
|
+
errors,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Get all registered commands from plugins
|
|
108
|
+
* @returns {Map} Map of { commandName: { pluginName, handler } }
|
|
109
|
+
*/
|
|
110
|
+
function getRegisteredCommands() {
|
|
111
|
+
return new Map(registeredCommands);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Check if a command is registered by a plugin
|
|
116
|
+
* @param {string} name - Command name
|
|
117
|
+
* @returns {boolean} True if command exists
|
|
118
|
+
*/
|
|
119
|
+
function isPluginCommand(name) {
|
|
120
|
+
return registeredCommands.has(name);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Execute a plugin-registered command
|
|
125
|
+
* @param {string} name - Command name
|
|
126
|
+
* @param {Array} args - Command arguments
|
|
127
|
+
* @returns {Object} Result with success status and result/error
|
|
128
|
+
*/
|
|
129
|
+
async function executePluginCommand(name, args = []) {
|
|
130
|
+
if (!registeredCommands.has(name)) {
|
|
131
|
+
return {
|
|
132
|
+
success: false,
|
|
133
|
+
error: `Command '${name}' not found`,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const { pluginName, handler } = registeredCommands.get(name);
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
const result = await handler(args);
|
|
141
|
+
return {
|
|
142
|
+
success: true,
|
|
143
|
+
result,
|
|
144
|
+
pluginName,
|
|
145
|
+
};
|
|
146
|
+
} catch (error) {
|
|
147
|
+
return {
|
|
148
|
+
success: false,
|
|
149
|
+
error: `Error executing command '${name}' from plugin '${pluginName}': ${error.message}`,
|
|
150
|
+
pluginName,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Check if plugins are initialized
|
|
157
|
+
* @returns {boolean} Initialization status
|
|
158
|
+
*/
|
|
159
|
+
function isInitialized() {
|
|
160
|
+
return initialized;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
module.exports = {
|
|
164
|
+
initializePlugins,
|
|
165
|
+
shutdownPlugins,
|
|
166
|
+
getRegisteredCommands,
|
|
167
|
+
isPluginCommand,
|
|
168
|
+
executePluginCommand,
|
|
169
|
+
isInitialized,
|
|
170
|
+
};
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { PLUGINS_DIR, REGISTRY_FILE } = require('./constants');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Ensure plugins directory exists
|
|
7
|
+
*/
|
|
8
|
+
function ensurePluginsDir() {
|
|
9
|
+
if (!fs.existsSync(PLUGINS_DIR)) {
|
|
10
|
+
fs.mkdirSync(PLUGINS_DIR, { recursive: true });
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Load registry.json, create with empty structure if missing
|
|
16
|
+
* @returns {Object} Registry data
|
|
17
|
+
*/
|
|
18
|
+
function loadRegistry() {
|
|
19
|
+
ensurePluginsDir();
|
|
20
|
+
|
|
21
|
+
if (!fs.existsSync(REGISTRY_FILE)) {
|
|
22
|
+
const emptyRegistry = { plugins: {} };
|
|
23
|
+
fs.writeFileSync(REGISTRY_FILE, JSON.stringify(emptyRegistry, null, 2), 'utf8');
|
|
24
|
+
return emptyRegistry;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const content = fs.readFileSync(REGISTRY_FILE, 'utf8');
|
|
29
|
+
const data = JSON.parse(content);
|
|
30
|
+
|
|
31
|
+
// Ensure plugins key exists
|
|
32
|
+
if (!data.plugins) {
|
|
33
|
+
data.plugins = {};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return data;
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.error('Failed to load registry:', error.message);
|
|
39
|
+
return { plugins: {} };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Save registry data to registry.json
|
|
45
|
+
* @param {Object} data - Registry data
|
|
46
|
+
*/
|
|
47
|
+
function saveRegistry(data) {
|
|
48
|
+
ensurePluginsDir();
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
fs.writeFileSync(REGISTRY_FILE, JSON.stringify(data, null, 2), 'utf8');
|
|
52
|
+
} catch (error) {
|
|
53
|
+
throw new Error(`Failed to save registry: ${error.message}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Add plugin to registry
|
|
59
|
+
* @param {string} name - Plugin name
|
|
60
|
+
* @param {Object} info - Plugin information
|
|
61
|
+
* @param {string} info.version - Plugin version
|
|
62
|
+
* @param {boolean} [info.enabled=true] - Whether plugin is enabled
|
|
63
|
+
* @param {string} [info.installedAt] - Installation timestamp
|
|
64
|
+
* @param {string} [info.source] - Git URL or source
|
|
65
|
+
* @param {number} [info.loadOrder=10] - Load order priority
|
|
66
|
+
* @returns {Object} Updated registry
|
|
67
|
+
*/
|
|
68
|
+
function addPlugin(name, info) {
|
|
69
|
+
const registry = loadRegistry();
|
|
70
|
+
|
|
71
|
+
registry.plugins[name] = {
|
|
72
|
+
version: info.version,
|
|
73
|
+
enabled: info.enabled !== undefined ? info.enabled : true,
|
|
74
|
+
installedAt: info.installedAt || new Date().toISOString(),
|
|
75
|
+
source: info.source || '',
|
|
76
|
+
loadOrder: info.loadOrder !== undefined ? info.loadOrder : 10,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
saveRegistry(registry);
|
|
80
|
+
return registry;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Remove plugin from registry
|
|
85
|
+
* @param {string} name - Plugin name
|
|
86
|
+
* @returns {Object} Updated registry
|
|
87
|
+
*/
|
|
88
|
+
function removePlugin(name) {
|
|
89
|
+
const registry = loadRegistry();
|
|
90
|
+
|
|
91
|
+
if (registry.plugins[name]) {
|
|
92
|
+
delete registry.plugins[name];
|
|
93
|
+
saveRegistry(registry);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return registry;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Get plugin info
|
|
101
|
+
* @param {string} name - Plugin name
|
|
102
|
+
* @returns {Object|null} Plugin info or null if not found
|
|
103
|
+
*/
|
|
104
|
+
function getPlugin(name) {
|
|
105
|
+
const registry = loadRegistry();
|
|
106
|
+
return registry.plugins[name] || null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* List all plugins
|
|
111
|
+
* @returns {Array} Array of plugin objects with name included
|
|
112
|
+
*/
|
|
113
|
+
function listPlugins() {
|
|
114
|
+
const registry = loadRegistry();
|
|
115
|
+
|
|
116
|
+
return Object.entries(registry.plugins).map(([name, info]) => ({
|
|
117
|
+
name,
|
|
118
|
+
...info,
|
|
119
|
+
}));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Update plugin info (partial update)
|
|
124
|
+
* @param {string} name - Plugin name
|
|
125
|
+
* @param {Object} updates - Partial updates to apply
|
|
126
|
+
* @returns {Object} Updated plugin info
|
|
127
|
+
*/
|
|
128
|
+
function updatePlugin(name, updates) {
|
|
129
|
+
const registry = loadRegistry();
|
|
130
|
+
|
|
131
|
+
if (!registry.plugins[name]) {
|
|
132
|
+
throw new Error(`Plugin '${name}' not found in registry`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
registry.plugins[name] = {
|
|
136
|
+
...registry.plugins[name],
|
|
137
|
+
...updates,
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
saveRegistry(registry);
|
|
141
|
+
return registry.plugins[name];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
module.exports = {
|
|
145
|
+
loadRegistry,
|
|
146
|
+
saveRegistry,
|
|
147
|
+
addPlugin,
|
|
148
|
+
removePlugin,
|
|
149
|
+
getPlugin,
|
|
150
|
+
listPlugins,
|
|
151
|
+
updatePlugin,
|
|
152
|
+
};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://github.com/zhangzeao/coding-tool/blob/main/src/plugins/schema/plugin-manifest.json",
|
|
4
|
+
"title": "CTX Plugin Manifest",
|
|
5
|
+
"description": "Schema for Coding-Tool-X (ctx) plugin manifest file",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["name", "version", "description", "main", "ctx"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"name": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"description": "Plugin name in kebab-case, must start with 'ctx-plugin-'",
|
|
12
|
+
"pattern": "^ctx-plugin-[a-z0-9-]+$"
|
|
13
|
+
},
|
|
14
|
+
"version": {
|
|
15
|
+
"type": "string",
|
|
16
|
+
"description": "Semantic version string",
|
|
17
|
+
"pattern": "^\\d+\\.\\d+\\.\\d+(-[a-zA-Z0-9.-]+)?(\\+[a-zA-Z0-9.-]+)?$"
|
|
18
|
+
},
|
|
19
|
+
"description": {
|
|
20
|
+
"type": "string",
|
|
21
|
+
"description": "Brief description of what the plugin does",
|
|
22
|
+
"minLength": 1,
|
|
23
|
+
"maxLength": 200
|
|
24
|
+
},
|
|
25
|
+
"main": {
|
|
26
|
+
"type": "string",
|
|
27
|
+
"description": "Entry point JavaScript file (relative to plugin root)",
|
|
28
|
+
"pattern": "^(?!.*\\.\\.)(?!.*\\.\\/)[a-zA-Z0-9_-]+(?:\\/[a-zA-Z0-9_-]+)*\\.js$"
|
|
29
|
+
},
|
|
30
|
+
"ctx": {
|
|
31
|
+
"type": "object",
|
|
32
|
+
"description": "CTX-specific metadata",
|
|
33
|
+
"required": ["minVersion", "hooks"],
|
|
34
|
+
"properties": {
|
|
35
|
+
"minVersion": {
|
|
36
|
+
"type": "string",
|
|
37
|
+
"description": "Minimum CTX version required",
|
|
38
|
+
"pattern": "^\\d+\\.\\d+\\.\\d+$"
|
|
39
|
+
},
|
|
40
|
+
"hooks": {
|
|
41
|
+
"type": "array",
|
|
42
|
+
"description": "Lifecycle hooks this plugin subscribes to",
|
|
43
|
+
"items": {
|
|
44
|
+
"type": "string",
|
|
45
|
+
"enum": [
|
|
46
|
+
"cli:init",
|
|
47
|
+
"cli:shutdown",
|
|
48
|
+
"cli:command:before",
|
|
49
|
+
"cli:command:after",
|
|
50
|
+
"config:loaded",
|
|
51
|
+
"config:saved",
|
|
52
|
+
"proxy:start",
|
|
53
|
+
"proxy:stop"
|
|
54
|
+
]
|
|
55
|
+
},
|
|
56
|
+
"uniqueItems": true,
|
|
57
|
+
"minItems": 1
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"additionalProperties": false
|
|
61
|
+
},
|
|
62
|
+
"author": {
|
|
63
|
+
"type": "string",
|
|
64
|
+
"description": "Author name and optional email"
|
|
65
|
+
},
|
|
66
|
+
"license": {
|
|
67
|
+
"type": "string",
|
|
68
|
+
"description": "SPDX license identifier"
|
|
69
|
+
},
|
|
70
|
+
"repository": {
|
|
71
|
+
"type": "string",
|
|
72
|
+
"description": "Git repository URL",
|
|
73
|
+
"format": "uri"
|
|
74
|
+
},
|
|
75
|
+
"dependencies": {
|
|
76
|
+
"type": "object",
|
|
77
|
+
"description": "npm dependencies (will be installed automatically)",
|
|
78
|
+
"patternProperties": {
|
|
79
|
+
"^[a-z0-9-_@/]+$": {
|
|
80
|
+
"type": "string"
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
"additionalProperties": false
|
|
84
|
+
},
|
|
85
|
+
"config": {
|
|
86
|
+
"type": "object",
|
|
87
|
+
"description": "Default configuration values for this plugin"
|
|
88
|
+
},
|
|
89
|
+
"commands": {
|
|
90
|
+
"type": "array",
|
|
91
|
+
"description": "CLI commands registered by this plugin",
|
|
92
|
+
"items": {
|
|
93
|
+
"type": "object",
|
|
94
|
+
"required": ["name", "description"],
|
|
95
|
+
"properties": {
|
|
96
|
+
"name": {
|
|
97
|
+
"type": "string",
|
|
98
|
+
"pattern": "^[a-z0-9-]+$",
|
|
99
|
+
"description": "Command name (kebab-case)"
|
|
100
|
+
},
|
|
101
|
+
"description": {
|
|
102
|
+
"type": "string",
|
|
103
|
+
"description": "Command description shown in help"
|
|
104
|
+
},
|
|
105
|
+
"usage": {
|
|
106
|
+
"type": "string",
|
|
107
|
+
"description": "Usage example"
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
"additionalProperties": false
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
"additionalProperties": false
|
|
115
|
+
}
|
package/src/server/api/mcp.js
CHANGED
|
@@ -341,7 +341,7 @@ router.get('/servers/:id/tools', async (req, res) => {
|
|
|
341
341
|
try {
|
|
342
342
|
const { id } = req.params;
|
|
343
343
|
const result = await mcpService.getServerTools(id);
|
|
344
|
-
res.json(result);
|
|
344
|
+
res.json({ success: true, ...result });
|
|
345
345
|
} catch (err) {
|
|
346
346
|
res.status(404).json({ success: false, error: err.message });
|
|
347
347
|
}
|