@ian2018cs/agenthub 0.1.69 → 0.1.70
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/assets/index-DURCpZD_.css +32 -0
- package/dist/assets/index-DxBc5bLY.js +186 -0
- package/dist/assets/{vendor-icons-DxBNDMja.js → vendor-icons-BWqhkbta.js} +1 -1
- package/dist/index.html +3 -3
- package/package.json +1 -1
- package/server/claude-sdk.js +75 -13
- package/server/index.js +6 -1
- package/server/routes/agents.js +336 -5
- package/server/routes/mcp.js +122 -147
- package/server/routes/skills.js +83 -0
- package/server/services/system-mcp-repo.js +1 -1
- package/server/services/system-repo.js +1 -1
- package/dist/assets/index-HOTjBpXH.css +0 -32
- package/dist/assets/index-u5cEXvaS.js +0 -184
package/server/routes/mcp.js
CHANGED
|
@@ -70,67 +70,67 @@ router.post('/cli/add', async (req, res) => {
|
|
|
70
70
|
return res.status(401).json({ error: 'User authentication required' });
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
console.log(`➕ Adding MCP server
|
|
73
|
+
console.log(`➕ Adding MCP server (${scope} scope):`, name);
|
|
74
74
|
|
|
75
75
|
const userPaths = getUserPaths(userUuid);
|
|
76
|
+
|
|
77
|
+
// For local scope, bypass Claude CLI and directly write to .claude.json.
|
|
78
|
+
// Claude CLI resolves the project key by walking up to the git root, which in
|
|
79
|
+
// our multi-user deployment always resolves to the app root — not the user's
|
|
80
|
+
// actual project directory. We write directly to avoid this mis-keying.
|
|
81
|
+
if (scope === 'local' && projectPath) {
|
|
82
|
+
const claudeJsonPath = path.join(userPaths.claudeDir, '.claude.json');
|
|
83
|
+
let claudeConfig = {};
|
|
84
|
+
try {
|
|
85
|
+
claudeConfig = JSON.parse(await fs.readFile(claudeJsonPath, 'utf-8'));
|
|
86
|
+
} catch { /* start fresh if missing */ }
|
|
87
|
+
|
|
88
|
+
// Build the server entry in the same shape Claude CLI would write
|
|
89
|
+
let serverEntry = {};
|
|
90
|
+
if (type === 'http' || type === 'sse') {
|
|
91
|
+
serverEntry = { transport: type, url, ...(Object.keys(headers).length ? { headers } : {}) };
|
|
92
|
+
} else {
|
|
93
|
+
serverEntry = { command, ...(args?.length ? { args } : {}), ...(Object.keys(env).length ? { env } : {}) };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!claudeConfig.projects) claudeConfig.projects = {};
|
|
97
|
+
if (!claudeConfig.projects[projectPath]) claudeConfig.projects[projectPath] = {};
|
|
98
|
+
if (!claudeConfig.projects[projectPath].mcpServers) claudeConfig.projects[projectPath].mcpServers = {};
|
|
99
|
+
claudeConfig.projects[projectPath].mcpServers[name] = serverEntry;
|
|
100
|
+
|
|
101
|
+
await fs.writeFile(claudeJsonPath, JSON.stringify(claudeConfig, null, 2), 'utf-8');
|
|
102
|
+
console.log(`[MCP] Wrote local-scoped MCP "${name}" for project ${projectPath}`);
|
|
103
|
+
return res.json({ success: true, message: `MCP server "${name}" added successfully` });
|
|
104
|
+
}
|
|
105
|
+
|
|
76
106
|
const { spawn } = await import('child_process');
|
|
77
|
-
|
|
78
|
-
let cliArgs = ['mcp', 'add'];
|
|
79
|
-
|
|
80
|
-
// Add scope flag
|
|
81
|
-
cliArgs.push('--scope', scope);
|
|
82
|
-
|
|
107
|
+
|
|
108
|
+
let cliArgs = ['mcp', 'add', '--scope', scope];
|
|
109
|
+
|
|
83
110
|
if (type === 'http') {
|
|
84
111
|
cliArgs.push('--transport', 'http', name, url);
|
|
85
|
-
|
|
86
|
-
Object.entries(headers).forEach(([key, value]) => {
|
|
87
|
-
cliArgs.push('--header', `${key}: ${value}`);
|
|
88
|
-
});
|
|
112
|
+
Object.entries(headers).forEach(([key, value]) => { cliArgs.push('--header', `${key}: ${value}`); });
|
|
89
113
|
} else if (type === 'sse') {
|
|
90
114
|
cliArgs.push('--transport', 'sse', name, url);
|
|
91
|
-
|
|
92
|
-
Object.entries(headers).forEach(([key, value]) => {
|
|
93
|
-
cliArgs.push('--header', `${key}: ${value}`);
|
|
94
|
-
});
|
|
115
|
+
Object.entries(headers).forEach(([key, value]) => { cliArgs.push('--header', `${key}: ${value}`); });
|
|
95
116
|
} else {
|
|
96
|
-
// stdio (default): claude mcp add --scope user <name> <command> [args...]
|
|
97
117
|
cliArgs.push(name);
|
|
98
|
-
|
|
99
|
-
Object.entries(env).forEach(([key, value]) => {
|
|
100
|
-
cliArgs.push('-e', `${key}=${value}`);
|
|
101
|
-
});
|
|
118
|
+
Object.entries(env).forEach(([key, value]) => { cliArgs.push('-e', `${key}=${value}`); });
|
|
102
119
|
cliArgs.push(command);
|
|
103
|
-
if (args
|
|
104
|
-
cliArgs.push(...args);
|
|
105
|
-
}
|
|
120
|
+
if (args?.length) cliArgs.push(...args);
|
|
106
121
|
}
|
|
107
|
-
|
|
122
|
+
|
|
108
123
|
console.log('🔧 Running Claude CLI command:', 'claude', cliArgs.join(' '));
|
|
109
124
|
|
|
110
|
-
|
|
111
|
-
const spawnOptions = {
|
|
125
|
+
const cliProcess = spawn('claude', cliArgs, {
|
|
112
126
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
113
127
|
env: { ...process.env, CLAUDE_CONFIG_DIR: userPaths.claudeDir }
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
if (scope === 'local' && projectPath) {
|
|
117
|
-
spawnOptions.cwd = projectPath;
|
|
118
|
-
console.log('📁 Running in project directory:', projectPath);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const cliProcess = spawn('claude', cliArgs, spawnOptions);
|
|
128
|
+
});
|
|
122
129
|
|
|
123
130
|
let stdout = '';
|
|
124
131
|
let stderr = '';
|
|
125
|
-
|
|
126
|
-
cliProcess.
|
|
127
|
-
stdout += data.toString();
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
cliProcess.stderr.on('data', (data) => {
|
|
131
|
-
stderr += data.toString();
|
|
132
|
-
});
|
|
133
|
-
|
|
132
|
+
cliProcess.stdout.on('data', (data) => { stdout += data.toString(); });
|
|
133
|
+
cliProcess.stderr.on('data', (data) => { stderr += data.toString(); });
|
|
134
134
|
cliProcess.on('close', (code) => {
|
|
135
135
|
if (code === 0) {
|
|
136
136
|
res.json({ success: true, output: stdout, message: `MCP server "${name}" added successfully` });
|
|
@@ -139,9 +139,7 @@ router.post('/cli/add', async (req, res) => {
|
|
|
139
139
|
res.status(400).json({ error: 'Claude CLI command failed', details: stderr });
|
|
140
140
|
}
|
|
141
141
|
});
|
|
142
|
-
|
|
143
142
|
cliProcess.on('error', (error) => {
|
|
144
|
-
console.error('Error running Claude CLI:', error);
|
|
145
143
|
res.status(500).json({ error: 'Failed to run Claude CLI', details: error.message });
|
|
146
144
|
});
|
|
147
145
|
} catch (error) {
|
|
@@ -150,6 +148,7 @@ router.post('/cli/add', async (req, res) => {
|
|
|
150
148
|
}
|
|
151
149
|
});
|
|
152
150
|
|
|
151
|
+
// POST /api/mcp/cli/add-json - Add MCP server using JSON format
|
|
153
152
|
// POST /api/mcp/cli/add-json - Add MCP server using JSON format
|
|
154
153
|
router.post('/cli/add-json', async (req, res) => {
|
|
155
154
|
try {
|
|
@@ -167,74 +166,62 @@ router.post('/cli/add-json', async (req, res) => {
|
|
|
167
166
|
try {
|
|
168
167
|
parsedConfig = typeof jsonConfig === 'string' ? JSON.parse(jsonConfig) : jsonConfig;
|
|
169
168
|
} catch (parseError) {
|
|
170
|
-
return res.status(400).json({
|
|
171
|
-
error: 'Invalid JSON configuration',
|
|
172
|
-
details: parseError.message
|
|
173
|
-
});
|
|
169
|
+
return res.status(400).json({ error: 'Invalid JSON configuration', details: parseError.message });
|
|
174
170
|
}
|
|
175
171
|
|
|
176
|
-
// Validate required fields
|
|
177
172
|
if (!parsedConfig.type) {
|
|
178
|
-
return res.status(400).json({
|
|
179
|
-
error: 'Invalid configuration',
|
|
180
|
-
details: 'Missing required field: type'
|
|
181
|
-
});
|
|
173
|
+
return res.status(400).json({ error: 'Invalid configuration', details: 'Missing required field: type' });
|
|
182
174
|
}
|
|
183
|
-
|
|
184
175
|
if (parsedConfig.type === 'stdio' && !parsedConfig.command) {
|
|
185
|
-
return res.status(400).json({
|
|
186
|
-
error: 'Invalid configuration',
|
|
187
|
-
details: 'stdio type requires a command field'
|
|
188
|
-
});
|
|
176
|
+
return res.status(400).json({ error: 'Invalid configuration', details: 'stdio type requires a command field' });
|
|
189
177
|
}
|
|
190
|
-
|
|
191
178
|
if ((parsedConfig.type === 'http' || parsedConfig.type === 'sse') && !parsedConfig.url) {
|
|
192
|
-
return res.status(400).json({
|
|
193
|
-
error: 'Invalid configuration',
|
|
194
|
-
details: `${parsedConfig.type} type requires a url field`
|
|
195
|
-
});
|
|
179
|
+
return res.status(400).json({ error: 'Invalid configuration', details: `${parsedConfig.type} type requires a url field` });
|
|
196
180
|
}
|
|
197
181
|
|
|
198
182
|
const userPaths = getUserPaths(userUuid);
|
|
199
|
-
const { spawn } = await import('child_process');
|
|
200
183
|
|
|
201
|
-
//
|
|
202
|
-
|
|
184
|
+
// For local scope, bypass Claude CLI (same reason as cli/add — git root mis-keying).
|
|
185
|
+
if (scope === 'local' && projectPath) {
|
|
186
|
+
const claudeJsonPath = path.join(userPaths.claudeDir, '.claude.json');
|
|
187
|
+
let claudeConfig = {};
|
|
188
|
+
try {
|
|
189
|
+
claudeConfig = JSON.parse(await fs.readFile(claudeJsonPath, 'utf-8'));
|
|
190
|
+
} catch { /* start fresh if missing */ }
|
|
191
|
+
|
|
192
|
+
if (!claudeConfig.projects) claudeConfig.projects = {};
|
|
193
|
+
if (!claudeConfig.projects[projectPath]) claudeConfig.projects[projectPath] = {};
|
|
194
|
+
if (!claudeConfig.projects[projectPath].mcpServers) claudeConfig.projects[projectPath].mcpServers = {};
|
|
195
|
+
claudeConfig.projects[projectPath].mcpServers[name] = parsedConfig;
|
|
196
|
+
|
|
197
|
+
await fs.writeFile(claudeJsonPath, JSON.stringify(claudeConfig, null, 2), 'utf-8');
|
|
198
|
+
console.log(`[MCP] Wrote local-scoped MCP "${name}" (JSON) for project ${projectPath}`);
|
|
199
|
+
|
|
200
|
+
// Sync to Codex config.toml (fire-and-forget)
|
|
201
|
+
const codexConfigPath = path.join(userPaths.codexHomeDir, '.codex', 'config.toml');
|
|
202
|
+
addToCodexConfig(codexConfigPath, { [name]: parsedConfig }).catch(err =>
|
|
203
|
+
console.error('[MCP] Failed to sync to Codex config.toml:', err.message)
|
|
204
|
+
);
|
|
205
|
+
return res.json({ success: true, message: `MCP server "${name}" added successfully via JSON` });
|
|
206
|
+
}
|
|
203
207
|
|
|
204
|
-
|
|
205
|
-
const jsonString = JSON.stringify(parsedConfig);
|
|
206
|
-
cliArgs.push(jsonString);
|
|
208
|
+
const { spawn } = await import('child_process');
|
|
207
209
|
|
|
208
|
-
|
|
210
|
+
const cliArgs = ['mcp', 'add-json', '--scope', scope, name, JSON.stringify(parsedConfig)];
|
|
211
|
+
console.log('🔧 Running Claude CLI command:', 'claude', cliArgs[0], cliArgs[1], cliArgs[2], cliArgs[3], cliArgs[4]);
|
|
209
212
|
|
|
210
|
-
|
|
211
|
-
const spawnOptions = {
|
|
213
|
+
const cliProcess = spawn('claude', cliArgs, {
|
|
212
214
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
213
215
|
env: { ...process.env, CLAUDE_CONFIG_DIR: userPaths.claudeDir }
|
|
214
|
-
};
|
|
215
|
-
|
|
216
|
-
if (scope === 'local' && projectPath) {
|
|
217
|
-
spawnOptions.cwd = projectPath;
|
|
218
|
-
console.log('📁 Running in project directory:', projectPath);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
const cliProcess = spawn('claude', cliArgs, spawnOptions);
|
|
216
|
+
});
|
|
222
217
|
|
|
223
218
|
let stdout = '';
|
|
224
219
|
let stderr = '';
|
|
225
|
-
|
|
226
|
-
cliProcess.
|
|
227
|
-
stdout += data.toString();
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
cliProcess.stderr.on('data', (data) => {
|
|
231
|
-
stderr += data.toString();
|
|
232
|
-
});
|
|
233
|
-
|
|
220
|
+
cliProcess.stdout.on('data', (data) => { stdout += data.toString(); });
|
|
221
|
+
cliProcess.stderr.on('data', (data) => { stderr += data.toString(); });
|
|
234
222
|
cliProcess.on('close', (code) => {
|
|
235
223
|
if (code === 0) {
|
|
236
224
|
res.json({ success: true, output: stdout, message: `MCP server "${name}" added successfully via JSON` });
|
|
237
|
-
// Sync to Codex config.toml (fire-and-forget)
|
|
238
225
|
const codexConfigPath = path.join(userPaths.codexHomeDir, '.codex', 'config.toml');
|
|
239
226
|
addToCodexConfig(codexConfigPath, { [name]: parsedConfig }).catch(err =>
|
|
240
227
|
console.error('[MCP] Failed to sync to Codex config.toml:', err.message)
|
|
@@ -244,9 +231,7 @@ router.post('/cli/add-json', async (req, res) => {
|
|
|
244
231
|
res.status(400).json({ error: 'Claude CLI command failed', details: stderr });
|
|
245
232
|
}
|
|
246
233
|
});
|
|
247
|
-
|
|
248
234
|
cliProcess.on('error', (error) => {
|
|
249
|
-
console.error('Error running Claude CLI:', error);
|
|
250
235
|
res.status(500).json({ error: 'Failed to run Claude CLI', details: error.message });
|
|
251
236
|
});
|
|
252
237
|
} catch (error) {
|
|
@@ -259,42 +244,44 @@ router.post('/cli/add-json', async (req, res) => {
|
|
|
259
244
|
router.delete('/cli/remove/:name', async (req, res) => {
|
|
260
245
|
try {
|
|
261
246
|
const { name } = req.params;
|
|
262
|
-
const { scope } = req.query;
|
|
247
|
+
const { scope, projectPath } = req.query;
|
|
263
248
|
|
|
264
249
|
const userUuid = req.user?.uuid;
|
|
265
250
|
if (!userUuid) {
|
|
266
251
|
return res.status(401).json({ error: 'User authentication required' });
|
|
267
252
|
}
|
|
268
253
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
let actualScope = scope;
|
|
272
|
-
|
|
273
|
-
// If the name includes a scope prefix like "local:test", extract it
|
|
274
|
-
if (name.includes(':')) {
|
|
275
|
-
const [prefix, serverName] = name.split(':');
|
|
276
|
-
actualName = serverName;
|
|
277
|
-
actualScope = actualScope || prefix; // Use prefix as scope if not provided in query
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
console.log('🗑️ Removing MCP server using Claude CLI:', actualName, 'scope:', actualScope);
|
|
254
|
+
const actualScope = scope || 'user';
|
|
255
|
+
console.log('🗑️ Removing MCP server:', name, 'scope:', actualScope);
|
|
281
256
|
|
|
282
257
|
const userPaths = getUserPaths(userUuid);
|
|
283
|
-
const { spawn } = await import('child_process');
|
|
284
|
-
|
|
285
|
-
// Build command args based on scope
|
|
286
|
-
let cliArgs = ['mcp', 'remove'];
|
|
287
258
|
|
|
288
|
-
//
|
|
259
|
+
// For local scope, bypass Claude CLI and directly edit .claude.json.
|
|
289
260
|
if (actualScope === 'local') {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
261
|
+
if (!projectPath) {
|
|
262
|
+
return res.status(400).json({ error: 'projectPath is required for local-scoped MCP removal' });
|
|
263
|
+
}
|
|
264
|
+
const claudeJsonPath = path.join(userPaths.claudeDir, '.claude.json');
|
|
265
|
+
try {
|
|
266
|
+
const claudeConfig = JSON.parse(await fs.readFile(claudeJsonPath, 'utf-8'));
|
|
267
|
+
const projectMcps = claudeConfig.projects?.[projectPath]?.mcpServers;
|
|
268
|
+
if (projectMcps && name in projectMcps) {
|
|
269
|
+
delete projectMcps[name];
|
|
270
|
+
await fs.writeFile(claudeJsonPath, JSON.stringify(claudeConfig, null, 2), 'utf-8');
|
|
271
|
+
}
|
|
272
|
+
} catch (e) {
|
|
273
|
+
return res.status(500).json({ error: 'Failed to update .claude.json', details: e.message });
|
|
274
|
+
}
|
|
275
|
+
// Sync removal to Codex (fire-and-forget)
|
|
276
|
+
const codexConfigPath = path.join(userPaths.codexHomeDir, '.codex', 'config.toml');
|
|
277
|
+
removeFromCodexConfig(codexConfigPath, name).catch(err =>
|
|
278
|
+
console.error('[MCP] Failed to sync removal to Codex config.toml:', err.message)
|
|
279
|
+
);
|
|
280
|
+
return res.json({ success: true, message: `MCP server "${name}" removed successfully` });
|
|
294
281
|
}
|
|
295
282
|
|
|
296
|
-
|
|
297
|
-
|
|
283
|
+
const { spawn } = await import('child_process');
|
|
284
|
+
const cliArgs = ['mcp', 'remove', '--scope', actualScope, name];
|
|
298
285
|
console.log('🔧 Running Claude CLI command:', 'claude', cliArgs.join(' '));
|
|
299
286
|
|
|
300
287
|
const cliProcess = spawn('claude', cliArgs, {
|
|
@@ -304,21 +291,13 @@ router.delete('/cli/remove/:name', async (req, res) => {
|
|
|
304
291
|
|
|
305
292
|
let stdout = '';
|
|
306
293
|
let stderr = '';
|
|
307
|
-
|
|
308
|
-
cliProcess.
|
|
309
|
-
stdout += data.toString();
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
cliProcess.stderr.on('data', (data) => {
|
|
313
|
-
stderr += data.toString();
|
|
314
|
-
});
|
|
315
|
-
|
|
294
|
+
cliProcess.stdout.on('data', (data) => { stdout += data.toString(); });
|
|
295
|
+
cliProcess.stderr.on('data', (data) => { stderr += data.toString(); });
|
|
316
296
|
cliProcess.on('close', (code) => {
|
|
317
297
|
if (code === 0) {
|
|
318
298
|
res.json({ success: true, output: stdout, message: `MCP server "${name}" removed successfully` });
|
|
319
|
-
// Sync removal to Codex config.toml (fire-and-forget)
|
|
320
299
|
const codexConfigPath = path.join(userPaths.codexHomeDir, '.codex', 'config.toml');
|
|
321
|
-
removeFromCodexConfig(codexConfigPath,
|
|
300
|
+
removeFromCodexConfig(codexConfigPath, name).catch(err =>
|
|
322
301
|
console.error('[MCP] Failed to sync removal to Codex config.toml:', err.message)
|
|
323
302
|
);
|
|
324
303
|
} else {
|
|
@@ -326,9 +305,7 @@ router.delete('/cli/remove/:name', async (req, res) => {
|
|
|
326
305
|
res.status(400).json({ error: 'Claude CLI command failed', details: stderr });
|
|
327
306
|
}
|
|
328
307
|
});
|
|
329
|
-
|
|
330
308
|
cliProcess.on('error', (error) => {
|
|
331
|
-
console.error('Error running Claude CLI:', error);
|
|
332
309
|
res.status(500).json({ error: 'Failed to run Claude CLI', details: error.message });
|
|
333
310
|
});
|
|
334
311
|
} catch (error) {
|
|
@@ -464,26 +441,24 @@ router.get('/config/read', async (req, res) => {
|
|
|
464
441
|
}
|
|
465
442
|
}
|
|
466
443
|
|
|
467
|
-
// Check for local-scoped MCP servers (project-specific)
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
console.log(`🔍 Found local-scoped MCP servers for ${currentProjectPath}:`, Object.keys(projectConfig.mcpServers));
|
|
444
|
+
// Check for local-scoped MCP servers (project-specific) — iterate all projects
|
|
445
|
+
if (configData.projects && typeof configData.projects === 'object') {
|
|
446
|
+
for (const [projectPath, projectConfig] of Object.entries(configData.projects)) {
|
|
447
|
+
if (!projectConfig?.mcpServers || typeof projectConfig.mcpServers !== 'object') continue;
|
|
448
|
+
const mcpNames = Object.keys(projectConfig.mcpServers);
|
|
449
|
+
if (mcpNames.length === 0) continue;
|
|
450
|
+
console.log(`🔍 Found local-scoped MCP servers for ${projectPath}:`, mcpNames);
|
|
475
451
|
for (const [name, config] of Object.entries(projectConfig.mcpServers)) {
|
|
476
452
|
const server = {
|
|
477
|
-
id:
|
|
478
|
-
name: name,
|
|
479
|
-
type: 'stdio',
|
|
480
|
-
scope: 'local',
|
|
481
|
-
projectPath:
|
|
453
|
+
id: name,
|
|
454
|
+
name: name,
|
|
455
|
+
type: 'stdio',
|
|
456
|
+
scope: 'local',
|
|
457
|
+
projectPath: projectPath,
|
|
482
458
|
config: {},
|
|
483
|
-
raw: config
|
|
459
|
+
raw: config
|
|
484
460
|
};
|
|
485
|
-
|
|
486
|
-
// Determine transport type and extract config
|
|
461
|
+
|
|
487
462
|
if (config.command) {
|
|
488
463
|
server.type = 'stdio';
|
|
489
464
|
server.config.command = config.command;
|
|
@@ -494,7 +469,7 @@ router.get('/config/read', async (req, res) => {
|
|
|
494
469
|
server.config.url = config.url;
|
|
495
470
|
server.config.headers = config.headers || {};
|
|
496
471
|
}
|
|
497
|
-
|
|
472
|
+
|
|
498
473
|
servers.push(server);
|
|
499
474
|
}
|
|
500
475
|
}
|
package/server/routes/skills.js
CHANGED
|
@@ -423,6 +423,89 @@ router.post('/enable/:name', async (req, res) => {
|
|
|
423
423
|
}
|
|
424
424
|
});
|
|
425
425
|
|
|
426
|
+
/**
|
|
427
|
+
* Recursively add directory contents to a zip archive.
|
|
428
|
+
* Uses fs.stat (follows symlinks) so symlinked files are included transparently.
|
|
429
|
+
*/
|
|
430
|
+
async function addDirToZip(zip, dirPath, zipPrefix) {
|
|
431
|
+
let names;
|
|
432
|
+
try {
|
|
433
|
+
names = await fs.readdir(dirPath);
|
|
434
|
+
} catch {
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
for (const name of names) {
|
|
439
|
+
if (name.startsWith('.')) continue;
|
|
440
|
+
|
|
441
|
+
const fullPath = path.join(dirPath, name);
|
|
442
|
+
const zipPath = zipPrefix + name;
|
|
443
|
+
|
|
444
|
+
let stat;
|
|
445
|
+
try {
|
|
446
|
+
stat = await fs.stat(fullPath); // follows symlinks
|
|
447
|
+
} catch {
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (stat.isDirectory()) {
|
|
452
|
+
await addDirToZip(zip, fullPath, zipPath + '/');
|
|
453
|
+
} else if (stat.isFile()) {
|
|
454
|
+
const data = await fs.readFile(fullPath);
|
|
455
|
+
zip.addFile(zipPath, data);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* GET /api/skills/:name/download
|
|
462
|
+
* Download a skill as a zip file (symlinks are resolved transparently)
|
|
463
|
+
*/
|
|
464
|
+
router.get('/:name/download', async (req, res) => {
|
|
465
|
+
try {
|
|
466
|
+
const userUuid = req.user?.uuid;
|
|
467
|
+
if (!userUuid) {
|
|
468
|
+
return res.status(401).json({ error: 'User authentication required' });
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const { name } = req.params;
|
|
472
|
+
|
|
473
|
+
if (!SKILL_NAME_REGEX.test(name)) {
|
|
474
|
+
return res.status(400).json({ error: 'Invalid skill name' });
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const userPaths = getUserPaths(userUuid);
|
|
478
|
+
const skillPath = path.join(userPaths.skillsDir, name);
|
|
479
|
+
|
|
480
|
+
// Resolve symlink to real directory
|
|
481
|
+
let realPath = skillPath;
|
|
482
|
+
try {
|
|
483
|
+
const stat = await fs.lstat(skillPath);
|
|
484
|
+
if (stat.isSymbolicLink()) {
|
|
485
|
+
realPath = await fs.realpath(skillPath);
|
|
486
|
+
}
|
|
487
|
+
} catch {
|
|
488
|
+
return res.status(404).json({ error: 'Skill not found' });
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if (!await isValidSkill(realPath)) {
|
|
492
|
+
return res.status(404).json({ error: 'Skill not found or invalid' });
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const zip = new AdmZip();
|
|
496
|
+
await addDirToZip(zip, realPath, `${name}/`);
|
|
497
|
+
const zipBuffer = zip.toBuffer();
|
|
498
|
+
|
|
499
|
+
res.setHeader('Content-Type', 'application/zip');
|
|
500
|
+
res.setHeader('Content-Disposition', `attachment; filename="${name}.zip"`);
|
|
501
|
+
res.setHeader('Content-Length', zipBuffer.length);
|
|
502
|
+
res.send(zipBuffer);
|
|
503
|
+
} catch (error) {
|
|
504
|
+
console.error('Error downloading skill:', error);
|
|
505
|
+
res.status(500).json({ error: error.message });
|
|
506
|
+
}
|
|
507
|
+
});
|
|
508
|
+
|
|
426
509
|
/**
|
|
427
510
|
* DELETE /api/skills/disable/:name
|
|
428
511
|
* Disable a skill by removing symlink
|
|
@@ -42,7 +42,7 @@ function updateRepository(repoPath) {
|
|
|
42
42
|
});
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
async function ensureSystemMcpRepo() {
|
|
45
|
+
export async function ensureSystemMcpRepo() {
|
|
46
46
|
const publicPaths = getPublicPaths();
|
|
47
47
|
const publicRepoPath = path.join(publicPaths.mcpRepoDir, SYSTEM_MCP_REPO_OWNER, SYSTEM_MCP_REPO_NAME);
|
|
48
48
|
|
|
@@ -51,7 +51,7 @@ function updateRepository(repoPath) {
|
|
|
51
51
|
* If already cloned, attempts to pull latest changes.
|
|
52
52
|
* Returns the path to the public clone.
|
|
53
53
|
*/
|
|
54
|
-
async function ensureSystemRepo() {
|
|
54
|
+
export async function ensureSystemRepo() {
|
|
55
55
|
const publicPaths = getPublicPaths();
|
|
56
56
|
const publicRepoPath = path.join(publicPaths.skillsRepoDir, SYSTEM_REPO_OWNER, SYSTEM_REPO_NAME);
|
|
57
57
|
|