@ian2018cs/agenthub 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/LICENSE +675 -0
  2. package/README.md +330 -0
  3. package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  4. package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  5. package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  6. package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  7. package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  8. package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  9. package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  10. package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  11. package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  12. package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  13. package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  14. package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  15. package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  16. package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  17. package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  18. package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  19. package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  20. package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  21. package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  22. package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  23. package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  24. package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  25. package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  26. package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  27. package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  28. package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  29. package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  30. package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  31. package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  32. package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  33. package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  34. package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  35. package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  36. package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  37. package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  38. package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  39. package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  40. package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  41. package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  42. package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  43. package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  44. package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  45. package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  46. package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  47. package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  48. package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  49. package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  50. package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  51. package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  52. package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  53. package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  54. package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  55. package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  56. package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  57. package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  58. package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  59. package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  60. package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  61. package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  62. package/dist/assets/index-B4ru3EJb.css +32 -0
  63. package/dist/assets/index-DDFuyrpY.js +154 -0
  64. package/dist/assets/vendor-codemirror-C_VWDoZS.js +39 -0
  65. package/dist/assets/vendor-icons-CJV4dnDL.js +326 -0
  66. package/dist/assets/vendor-katex-DK8hFnhL.js +261 -0
  67. package/dist/assets/vendor-markdown-VwNYkg_0.js +35 -0
  68. package/dist/assets/vendor-react-BeVl62c0.js +59 -0
  69. package/dist/assets/vendor-syntax-CdGaPJRS.js +16 -0
  70. package/dist/assets/vendor-utils-00TdZexr.js +1 -0
  71. package/dist/assets/vendor-xterm-CvdiG4-n.js +66 -0
  72. package/dist/clear-cache.html +85 -0
  73. package/dist/convert-icons.md +53 -0
  74. package/dist/favicon.png +0 -0
  75. package/dist/favicon.svg +9 -0
  76. package/dist/generate-icons.js +49 -0
  77. package/dist/icons/claude-ai-icon.svg +1 -0
  78. package/dist/icons/codex-white.svg +3 -0
  79. package/dist/icons/codex.svg +3 -0
  80. package/dist/icons/cursor-white.svg +12 -0
  81. package/dist/icons/cursor.svg +1 -0
  82. package/dist/icons/generate-icons.md +19 -0
  83. package/dist/icons/icon-128x128.png +0 -0
  84. package/dist/icons/icon-128x128.svg +12 -0
  85. package/dist/icons/icon-144x144.png +0 -0
  86. package/dist/icons/icon-144x144.svg +12 -0
  87. package/dist/icons/icon-152x152.png +0 -0
  88. package/dist/icons/icon-152x152.svg +12 -0
  89. package/dist/icons/icon-192x192.png +0 -0
  90. package/dist/icons/icon-192x192.svg +12 -0
  91. package/dist/icons/icon-384x384.png +0 -0
  92. package/dist/icons/icon-384x384.svg +12 -0
  93. package/dist/icons/icon-512x512.png +0 -0
  94. package/dist/icons/icon-512x512.svg +12 -0
  95. package/dist/icons/icon-72x72.png +0 -0
  96. package/dist/icons/icon-72x72.svg +12 -0
  97. package/dist/icons/icon-96x96.png +0 -0
  98. package/dist/icons/icon-96x96.svg +12 -0
  99. package/dist/icons/icon-template.svg +12 -0
  100. package/dist/index.html +57 -0
  101. package/dist/logo-128.png +0 -0
  102. package/dist/logo-256.png +0 -0
  103. package/dist/logo-32.png +0 -0
  104. package/dist/logo-512.png +0 -0
  105. package/dist/logo-64.png +0 -0
  106. package/dist/logo.svg +17 -0
  107. package/dist/manifest.json +61 -0
  108. package/dist/screenshots/cli-selection.png +0 -0
  109. package/dist/screenshots/desktop-main.png +0 -0
  110. package/dist/screenshots/mobile-chat.png +0 -0
  111. package/dist/screenshots/tools-modal.png +0 -0
  112. package/dist/sw.js +49 -0
  113. package/package.json +113 -0
  114. package/server/claude-sdk.js +791 -0
  115. package/server/cli.js +330 -0
  116. package/server/database/auth.db +0 -0
  117. package/server/database/db.js +523 -0
  118. package/server/database/init.sql +23 -0
  119. package/server/index.js +1678 -0
  120. package/server/load-env.js +27 -0
  121. package/server/middleware/auth.js +118 -0
  122. package/server/projects.js +899 -0
  123. package/server/routes/admin.js +89 -0
  124. package/server/routes/auth.js +144 -0
  125. package/server/routes/commands.js +570 -0
  126. package/server/routes/mcp-utils.js +37 -0
  127. package/server/routes/mcp.js +593 -0
  128. package/server/routes/projects.js +216 -0
  129. package/server/routes/skills.js +891 -0
  130. package/server/routes/usage.js +206 -0
  131. package/server/services/pricing.js +196 -0
  132. package/server/services/usage-scanner.js +283 -0
  133. package/server/services/user-directories.js +123 -0
  134. package/server/utils/commandParser.js +303 -0
  135. package/server/utils/mcp-detector.js +73 -0
  136. package/shared/modelConstants.js +23 -0
@@ -0,0 +1,570 @@
1
+ import express from 'express';
2
+ import { promises as fs } from 'fs';
3
+ import path from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ import matter from 'gray-matter';
6
+ import { CLAUDE_MODELS } from '../../shared/modelConstants.js';
7
+ import { getUserPaths } from '../services/user-directories.js';
8
+
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = path.dirname(__filename);
11
+
12
+ const router = express.Router();
13
+
14
+ /**
15
+ * Recursively scan directory for command files (.md)
16
+ * @param {string} dir - Directory to scan
17
+ * @param {string} baseDir - Base directory for relative paths
18
+ * @param {string} namespace - Namespace for commands (e.g., 'project', 'user')
19
+ * @returns {Promise<Array>} Array of command objects
20
+ */
21
+ async function scanCommandsDirectory(dir, baseDir, namespace) {
22
+ const commands = [];
23
+
24
+ try {
25
+ // Check if directory exists
26
+ await fs.access(dir);
27
+
28
+ const entries = await fs.readdir(dir, { withFileTypes: true });
29
+
30
+ for (const entry of entries) {
31
+ const fullPath = path.join(dir, entry.name);
32
+
33
+ if (entry.isDirectory()) {
34
+ // Recursively scan subdirectories
35
+ const subCommands = await scanCommandsDirectory(fullPath, baseDir, namespace);
36
+ commands.push(...subCommands);
37
+ } else if (entry.isFile() && entry.name.endsWith('.md')) {
38
+ // Parse markdown file for metadata
39
+ try {
40
+ const content = await fs.readFile(fullPath, 'utf8');
41
+ const { data: frontmatter, content: commandContent } = matter(content);
42
+
43
+ // Calculate relative path from baseDir for command name
44
+ const relativePath = path.relative(baseDir, fullPath);
45
+ // Remove .md extension and convert to command name
46
+ const commandName = '/' + relativePath.replace(/\.md$/, '').replace(/\\/g, '/');
47
+
48
+ // Extract description from frontmatter or first line of content
49
+ let description = frontmatter.description || '';
50
+ if (!description) {
51
+ const firstLine = commandContent.trim().split('\n')[0];
52
+ description = firstLine.replace(/^#+\s*/, '').trim();
53
+ }
54
+
55
+ commands.push({
56
+ name: commandName,
57
+ path: fullPath,
58
+ relativePath,
59
+ description,
60
+ namespace,
61
+ metadata: frontmatter
62
+ });
63
+ } catch (err) {
64
+ console.error(`Error parsing command file ${fullPath}:`, err.message);
65
+ }
66
+ }
67
+ }
68
+ } catch (err) {
69
+ // Directory doesn't exist or can't be accessed - this is okay
70
+ if (err.code !== 'ENOENT' && err.code !== 'EACCES') {
71
+ console.error(`Error scanning directory ${dir}:`, err.message);
72
+ }
73
+ }
74
+
75
+ return commands;
76
+ }
77
+
78
+ /**
79
+ * Built-in commands that are always available
80
+ */
81
+ const builtInCommands = [
82
+ {
83
+ name: '/help',
84
+ description: 'Show help documentation for Claude Code',
85
+ namespace: 'builtin',
86
+ metadata: { type: 'builtin' }
87
+ },
88
+ {
89
+ name: '/clear',
90
+ description: 'Clear the conversation history',
91
+ namespace: 'builtin',
92
+ metadata: { type: 'builtin' }
93
+ },
94
+ {
95
+ name: '/model',
96
+ description: 'Switch or view the current AI model',
97
+ namespace: 'builtin',
98
+ metadata: { type: 'builtin' }
99
+ },
100
+ {
101
+ name: '/cost',
102
+ description: 'Display token usage and cost information',
103
+ namespace: 'builtin',
104
+ metadata: { type: 'builtin' }
105
+ },
106
+ {
107
+ name: '/memory',
108
+ description: 'Open CLAUDE.md memory file for editing',
109
+ namespace: 'builtin',
110
+ metadata: { type: 'builtin' }
111
+ },
112
+ {
113
+ name: '/config',
114
+ description: 'Open settings and configuration',
115
+ namespace: 'builtin',
116
+ metadata: { type: 'builtin' }
117
+ },
118
+ {
119
+ name: '/status',
120
+ description: 'Show system status and version information',
121
+ namespace: 'builtin',
122
+ metadata: { type: 'builtin' }
123
+ },
124
+ {
125
+ name: '/rewind',
126
+ description: 'Rewind the conversation to a previous state',
127
+ namespace: 'builtin',
128
+ metadata: { type: 'builtin' }
129
+ }
130
+ ];
131
+
132
+ /**
133
+ * Built-in command handlers
134
+ * Each handler returns { type: 'builtin', action: string, data: any }
135
+ */
136
+ const builtInHandlers = {
137
+ '/help': async (args, context) => {
138
+ const helpText = `# Claude Code Commands
139
+
140
+ ## Built-in Commands
141
+
142
+ ${builtInCommands.map(cmd => `### ${cmd.name}
143
+ ${cmd.description}
144
+ `).join('\n')}
145
+
146
+ ## Custom Commands
147
+
148
+ Custom commands can be created in:
149
+ - Project: \`.claude/commands/\` (project-specific)
150
+ - User: \`~/.claude/commands/\` (available in all projects)
151
+
152
+ ### Command Syntax
153
+
154
+ - **Arguments**: Use \`$ARGUMENTS\` for all args or \`$1\`, \`$2\`, etc. for positional
155
+ - **File Includes**: Use \`@filename\` to include file contents
156
+ - **Bash Commands**: Use \`!command\` to execute bash commands
157
+
158
+ ### Examples
159
+
160
+ \`\`\`markdown
161
+ /mycommand arg1 arg2
162
+ \`\`\`
163
+ `;
164
+
165
+ return {
166
+ type: 'builtin',
167
+ action: 'help',
168
+ data: {
169
+ content: helpText,
170
+ format: 'markdown'
171
+ }
172
+ };
173
+ },
174
+
175
+ '/clear': async (args, context) => {
176
+ return {
177
+ type: 'builtin',
178
+ action: 'clear',
179
+ data: {
180
+ message: 'Conversation history cleared'
181
+ }
182
+ };
183
+ },
184
+
185
+ '/model': async (args, context) => {
186
+ // Read available models from centralized constants
187
+ const availableModels = {
188
+ claude: CLAUDE_MODELS.OPTIONS.map(o => o.value)
189
+ };
190
+
191
+ const currentProvider = context?.provider || 'claude';
192
+ const currentModel = context?.model || CLAUDE_MODELS.DEFAULT;
193
+
194
+ return {
195
+ type: 'builtin',
196
+ action: 'model',
197
+ data: {
198
+ current: {
199
+ provider: currentProvider,
200
+ model: currentModel
201
+ },
202
+ available: availableModels,
203
+ message: args.length > 0
204
+ ? `Switching to model: ${args[0]}`
205
+ : `Current model: ${currentModel}`
206
+ }
207
+ };
208
+ },
209
+
210
+ '/status': async (args, context) => {
211
+ // Read version from package.json
212
+ const packageJsonPath = path.join(path.dirname(__dirname), '..', 'package.json');
213
+ let version = 'unknown';
214
+ let packageName = 'claude-code-ui';
215
+
216
+ try {
217
+ const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
218
+ version = packageJson.version;
219
+ packageName = packageJson.name;
220
+ } catch (err) {
221
+ console.error('Error reading package.json:', err);
222
+ }
223
+
224
+ const uptime = process.uptime();
225
+ const uptimeMinutes = Math.floor(uptime / 60);
226
+ const uptimeHours = Math.floor(uptimeMinutes / 60);
227
+ const uptimeFormatted = uptimeHours > 0
228
+ ? `${uptimeHours}h ${uptimeMinutes % 60}m`
229
+ : `${uptimeMinutes}m`;
230
+
231
+ return {
232
+ type: 'builtin',
233
+ action: 'status',
234
+ data: {
235
+ version,
236
+ packageName,
237
+ uptime: uptimeFormatted,
238
+ uptimeSeconds: Math.floor(uptime),
239
+ model: context?.model || 'claude-sonnet-4.5',
240
+ provider: context?.provider || 'claude',
241
+ nodeVersion: process.version,
242
+ platform: process.platform
243
+ }
244
+ };
245
+ },
246
+
247
+ '/memory': async (args, context) => {
248
+ const projectPath = context?.projectPath;
249
+
250
+ if (!projectPath) {
251
+ return {
252
+ type: 'builtin',
253
+ action: 'memory',
254
+ data: {
255
+ error: 'No project selected',
256
+ message: 'Please select a project to access its CLAUDE.md file'
257
+ }
258
+ };
259
+ }
260
+
261
+ const claudeMdPath = path.join(projectPath, 'CLAUDE.md');
262
+
263
+ // Check if CLAUDE.md exists
264
+ let exists = false;
265
+ try {
266
+ await fs.access(claudeMdPath);
267
+ exists = true;
268
+ } catch (err) {
269
+ // File doesn't exist
270
+ }
271
+
272
+ return {
273
+ type: 'builtin',
274
+ action: 'memory',
275
+ data: {
276
+ path: claudeMdPath,
277
+ exists,
278
+ message: exists
279
+ ? `Opening CLAUDE.md at ${claudeMdPath}`
280
+ : `CLAUDE.md not found at ${claudeMdPath}. Create it to store project-specific instructions.`
281
+ }
282
+ };
283
+ },
284
+
285
+ '/config': async (args, context) => {
286
+ return {
287
+ type: 'builtin',
288
+ action: 'config',
289
+ data: {
290
+ message: 'Opening settings...'
291
+ }
292
+ };
293
+ },
294
+
295
+ '/rewind': async (args, context) => {
296
+ const steps = args[0] ? parseInt(args[0]) : 1;
297
+
298
+ if (isNaN(steps) || steps < 1) {
299
+ return {
300
+ type: 'builtin',
301
+ action: 'rewind',
302
+ data: {
303
+ error: 'Invalid steps parameter',
304
+ message: 'Usage: /rewind [number] - Rewind conversation by N steps (default: 1)'
305
+ }
306
+ };
307
+ }
308
+
309
+ return {
310
+ type: 'builtin',
311
+ action: 'rewind',
312
+ data: {
313
+ steps,
314
+ message: `Rewinding conversation by ${steps} step${steps > 1 ? 's' : ''}...`
315
+ }
316
+ };
317
+ },
318
+
319
+ '/cost': async (args, context) => {
320
+ const tokenUsage = context?.tokenUsage;
321
+
322
+ if (!tokenUsage) {
323
+ return {
324
+ type: 'builtin',
325
+ action: 'cost',
326
+ data: {
327
+ message: 'No token usage data available for this session.',
328
+ usage: null
329
+ }
330
+ };
331
+ }
332
+
333
+ const { used = 0, total = 160000 } = tokenUsage;
334
+ const percentage = total > 0 ? ((used / total) * 100).toFixed(1) : 0;
335
+
336
+ return {
337
+ type: 'builtin',
338
+ action: 'cost',
339
+ data: {
340
+ usage: {
341
+ used,
342
+ total,
343
+ percentage: parseFloat(percentage),
344
+ remaining: total - used
345
+ },
346
+ message: `Token usage: ${used.toLocaleString()} / ${total.toLocaleString()} (${percentage}%)`
347
+ }
348
+ };
349
+ }
350
+ };
351
+
352
+ /**
353
+ * POST /api/commands/list
354
+ * List all available commands from project and user directories
355
+ */
356
+ router.post('/list', async (req, res) => {
357
+ try {
358
+ const { projectPath } = req.body;
359
+ const userUuid = req.user?.uuid;
360
+ const allCommands = [...builtInCommands];
361
+
362
+ // Scan project-level commands (.claude/commands/)
363
+ if (projectPath) {
364
+ const projectCommandsDir = path.join(projectPath, '.claude', 'commands');
365
+ const projectCommands = await scanCommandsDirectory(
366
+ projectCommandsDir,
367
+ projectCommandsDir,
368
+ 'project'
369
+ );
370
+ allCommands.push(...projectCommands);
371
+ }
372
+
373
+ // Scan user-level commands from user's .claude/commands/
374
+ if (userUuid) {
375
+ const userPaths = getUserPaths(userUuid);
376
+ const userCommandsDir = path.join(userPaths.claudeDir, 'commands');
377
+ const userCommands = await scanCommandsDirectory(
378
+ userCommandsDir,
379
+ userCommandsDir,
380
+ 'user'
381
+ );
382
+ allCommands.push(...userCommands);
383
+ }
384
+
385
+ // Separate built-in and custom commands
386
+ const customCommands = allCommands.filter(cmd => cmd.namespace !== 'builtin');
387
+
388
+ // Sort commands alphabetically by name
389
+ customCommands.sort((a, b) => a.name.localeCompare(b.name));
390
+
391
+ res.json({
392
+ builtIn: builtInCommands,
393
+ custom: customCommands,
394
+ count: allCommands.length
395
+ });
396
+ } catch (error) {
397
+ console.error('Error listing commands:', error);
398
+ res.status(500).json({
399
+ error: 'Failed to list commands',
400
+ message: error.message
401
+ });
402
+ }
403
+ });
404
+
405
+ /**
406
+ * POST /api/commands/load
407
+ * Load a specific command file and return its content and metadata
408
+ */
409
+ router.post('/load', async (req, res) => {
410
+ try {
411
+ const { commandPath } = req.body;
412
+ const userUuid = req.user?.uuid;
413
+
414
+ if (!commandPath) {
415
+ return res.status(400).json({
416
+ error: 'Command path is required'
417
+ });
418
+ }
419
+
420
+ // Security: Prevent path traversal - validate path is in allowed directories
421
+ const resolvedPath = path.resolve(commandPath);
422
+ let isAllowed = resolvedPath.includes('.claude/commands');
423
+
424
+ // Also allow if path is under user's claudeDir
425
+ if (userUuid) {
426
+ const userPaths = getUserPaths(userUuid);
427
+ const userBase = path.resolve(userPaths.claudeDir);
428
+ if (resolvedPath.startsWith(userBase)) {
429
+ isAllowed = true;
430
+ }
431
+ }
432
+
433
+ if (!isAllowed) {
434
+ return res.status(403).json({
435
+ error: 'Access denied',
436
+ message: 'Command must be in .claude/commands directory'
437
+ });
438
+ }
439
+
440
+ // Read and parse the command file
441
+ const content = await fs.readFile(commandPath, 'utf8');
442
+ const { data: metadata, content: commandContent } = matter(content);
443
+
444
+ res.json({
445
+ path: commandPath,
446
+ metadata,
447
+ content: commandContent
448
+ });
449
+ } catch (error) {
450
+ if (error.code === 'ENOENT') {
451
+ return res.status(404).json({
452
+ error: 'Command not found',
453
+ message: `Command file not found: ${req.body.commandPath}`
454
+ });
455
+ }
456
+
457
+ console.error('Error loading command:', error);
458
+ res.status(500).json({
459
+ error: 'Failed to load command',
460
+ message: error.message
461
+ });
462
+ }
463
+ });
464
+
465
+ /**
466
+ * POST /api/commands/execute
467
+ * Execute a command with argument replacement
468
+ * This endpoint prepares the command content but doesn't execute bash commands yet
469
+ * (that will be handled in the command parser utility)
470
+ */
471
+ router.post('/execute', async (req, res) => {
472
+ try {
473
+ const { commandName, commandPath, args = [], context = {} } = req.body;
474
+ const userUuid = req.user?.uuid;
475
+
476
+ if (!commandName) {
477
+ return res.status(400).json({
478
+ error: 'Command name is required'
479
+ });
480
+ }
481
+
482
+ // Handle built-in commands
483
+ const handler = builtInHandlers[commandName];
484
+ if (handler) {
485
+ try {
486
+ const result = await handler(args, context);
487
+ return res.json({
488
+ ...result,
489
+ command: commandName
490
+ });
491
+ } catch (error) {
492
+ console.error(`Error executing built-in command ${commandName}:`, error);
493
+ return res.status(500).json({
494
+ error: 'Command execution failed',
495
+ message: error.message,
496
+ command: commandName
497
+ });
498
+ }
499
+ }
500
+
501
+ // Handle custom commands
502
+ if (!commandPath) {
503
+ return res.status(400).json({
504
+ error: 'Command path is required for custom commands'
505
+ });
506
+ }
507
+
508
+ // Load command content
509
+ // Security: validate commandPath is within allowed directories
510
+ {
511
+ const resolvedPath = path.resolve(commandPath);
512
+ // Build user-specific base path
513
+ const userBase = userUuid
514
+ ? path.resolve(path.join(getUserPaths(userUuid).claudeDir, 'commands'))
515
+ : null;
516
+ const projectBase = context?.projectPath
517
+ ? path.resolve(path.join(context.projectPath, '.claude', 'commands'))
518
+ : null;
519
+ const isUnder = (base) => {
520
+ if (!base) return false;
521
+ const rel = path.relative(base, resolvedPath);
522
+ return rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel);
523
+ };
524
+ if (!((userBase && isUnder(userBase)) || (projectBase && isUnder(projectBase)))) {
525
+ return res.status(403).json({
526
+ error: 'Access denied',
527
+ message: 'Command must be in .claude/commands directory'
528
+ });
529
+ }
530
+ }
531
+ const content = await fs.readFile(commandPath, 'utf8');
532
+ const { data: metadata, content: commandContent } = matter(content);
533
+ // Basic argument replacement (will be enhanced in command parser utility)
534
+ let processedContent = commandContent;
535
+
536
+ // Replace $ARGUMENTS with all arguments joined
537
+ const argsString = args.join(' ');
538
+ processedContent = processedContent.replace(/\$ARGUMENTS/g, argsString);
539
+
540
+ // Replace $1, $2, etc. with positional arguments
541
+ args.forEach((arg, index) => {
542
+ const placeholder = `$${index + 1}`;
543
+ processedContent = processedContent.replace(new RegExp(`\\${placeholder}\\b`, 'g'), arg);
544
+ });
545
+
546
+ res.json({
547
+ type: 'custom',
548
+ command: commandName,
549
+ content: processedContent,
550
+ metadata,
551
+ hasFileIncludes: processedContent.includes('@'),
552
+ hasBashCommands: processedContent.includes('!')
553
+ });
554
+ } catch (error) {
555
+ if (error.code === 'ENOENT') {
556
+ return res.status(404).json({
557
+ error: 'Command not found',
558
+ message: `Command file not found: ${req.body.commandPath}`
559
+ });
560
+ }
561
+
562
+ console.error('Error executing command:', error);
563
+ res.status(500).json({
564
+ error: 'Failed to execute command',
565
+ message: error.message
566
+ });
567
+ }
568
+ });
569
+
570
+ export default router;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * MCP UTILITIES API ROUTES
3
+ * ========================
4
+ *
5
+ * API endpoints for MCP server detection and configuration utilities.
6
+ * These endpoints expose centralized MCP detection functionality.
7
+ */
8
+
9
+ import express from 'express';
10
+ import { getAllMCPServers } from '../utils/mcp-detector.js';
11
+
12
+ const router = express.Router();
13
+
14
+ /**
15
+ * GET /api/mcp-utils/all-servers
16
+ * Get all configured MCP servers for the current user
17
+ */
18
+ router.get('/all-servers', async (req, res) => {
19
+ try {
20
+ const userUuid = req.user?.uuid;
21
+ if (!userUuid) {
22
+ return res.status(401).json({
23
+ error: 'User authentication required'
24
+ });
25
+ }
26
+ const result = await getAllMCPServers(userUuid);
27
+ res.json(result);
28
+ } catch (error) {
29
+ console.error('MCP servers detection error:', error);
30
+ res.status(500).json({
31
+ error: 'Failed to get MCP servers',
32
+ message: error.message
33
+ });
34
+ }
35
+ });
36
+
37
+ export default router;