coder-config 0.40.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.
Files changed (68) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +553 -0
  3. package/cli.js +431 -0
  4. package/config-loader.js +294 -0
  5. package/hooks/activity-track.sh +56 -0
  6. package/hooks/codex-workstream.sh +44 -0
  7. package/hooks/gemini-workstream.sh +44 -0
  8. package/hooks/workstream-inject.sh +20 -0
  9. package/lib/activity.js +283 -0
  10. package/lib/apply.js +344 -0
  11. package/lib/cli.js +267 -0
  12. package/lib/config.js +171 -0
  13. package/lib/constants.js +55 -0
  14. package/lib/env.js +114 -0
  15. package/lib/index.js +47 -0
  16. package/lib/init.js +122 -0
  17. package/lib/mcps.js +139 -0
  18. package/lib/memory.js +201 -0
  19. package/lib/projects.js +138 -0
  20. package/lib/registry.js +83 -0
  21. package/lib/utils.js +129 -0
  22. package/lib/workstreams.js +652 -0
  23. package/package.json +80 -0
  24. package/scripts/capture-screenshots.js +142 -0
  25. package/scripts/postinstall.js +122 -0
  26. package/scripts/release.sh +71 -0
  27. package/scripts/sync-version.js +77 -0
  28. package/scripts/tauri-prepare.js +328 -0
  29. package/shared/mcp-registry.json +76 -0
  30. package/ui/dist/assets/index-DbZ3_HBD.js +3204 -0
  31. package/ui/dist/assets/index-DjLdm3Mr.css +32 -0
  32. package/ui/dist/icons/icon-192.svg +16 -0
  33. package/ui/dist/icons/icon-512.svg +16 -0
  34. package/ui/dist/index.html +39 -0
  35. package/ui/dist/manifest.json +25 -0
  36. package/ui/dist/sw.js +24 -0
  37. package/ui/dist/tutorial/claude-settings.png +0 -0
  38. package/ui/dist/tutorial/header.png +0 -0
  39. package/ui/dist/tutorial/mcp-registry.png +0 -0
  40. package/ui/dist/tutorial/memory-view.png +0 -0
  41. package/ui/dist/tutorial/permissions.png +0 -0
  42. package/ui/dist/tutorial/plugins-view.png +0 -0
  43. package/ui/dist/tutorial/project-explorer.png +0 -0
  44. package/ui/dist/tutorial/projects-view.png +0 -0
  45. package/ui/dist/tutorial/sidebar.png +0 -0
  46. package/ui/dist/tutorial/tutorial-view.png +0 -0
  47. package/ui/dist/tutorial/workstreams-view.png +0 -0
  48. package/ui/routes/activity.js +58 -0
  49. package/ui/routes/commands.js +74 -0
  50. package/ui/routes/configs.js +329 -0
  51. package/ui/routes/env.js +40 -0
  52. package/ui/routes/file-explorer.js +668 -0
  53. package/ui/routes/index.js +41 -0
  54. package/ui/routes/mcp-discovery.js +235 -0
  55. package/ui/routes/memory.js +385 -0
  56. package/ui/routes/package.json +3 -0
  57. package/ui/routes/plugins.js +466 -0
  58. package/ui/routes/projects.js +198 -0
  59. package/ui/routes/registry.js +30 -0
  60. package/ui/routes/rules.js +74 -0
  61. package/ui/routes/search.js +125 -0
  62. package/ui/routes/settings.js +381 -0
  63. package/ui/routes/subprojects.js +208 -0
  64. package/ui/routes/tool-sync.js +127 -0
  65. package/ui/routes/updates.js +339 -0
  66. package/ui/routes/workstreams.js +224 -0
  67. package/ui/server.cjs +773 -0
  68. package/ui/terminal-server.cjs +160 -0
@@ -0,0 +1,668 @@
1
+ /**
2
+ * File Explorer Routes
3
+ */
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const os = require('os');
8
+
9
+ /**
10
+ * Scan a directory for .claude, .agent, .gemini folders
11
+ * Returns folder object for FileExplorer
12
+ */
13
+ function scanFolderForExplorer(dir, manager, label = null) {
14
+ const home = os.homedir();
15
+ const claudeDir = path.join(dir, '.claude');
16
+ const agentDir = path.join(dir, '.agent');
17
+ const geminiDir = path.join(dir, '.gemini');
18
+
19
+ // Use label or generate from path
20
+ if (!label) {
21
+ if (dir === home) {
22
+ label = '~';
23
+ } else if (dir.startsWith(home + '/')) {
24
+ label = '~' + dir.slice(home.length);
25
+ } else {
26
+ label = dir;
27
+ }
28
+ }
29
+
30
+ const folder = {
31
+ dir: dir,
32
+ label,
33
+ claudePath: claudeDir,
34
+ agentPath: agentDir,
35
+ geminiPath: geminiDir,
36
+ exists: fs.existsSync(claudeDir),
37
+ agentExists: fs.existsSync(agentDir),
38
+ geminiExists: fs.existsSync(geminiDir),
39
+ files: [],
40
+ agentFiles: [],
41
+ geminiFiles: [],
42
+ appliedTemplate: null
43
+ };
44
+
45
+ // If none of the config folders exist, don't include
46
+ if (!folder.exists && !folder.agentExists && !folder.geminiExists) {
47
+ return null;
48
+ }
49
+
50
+ // Scan .claude folder
51
+ if (folder.exists) {
52
+ scanClaudeFolder(folder, claudeDir, manager);
53
+ }
54
+
55
+ // Scan .agent folder
56
+ if (folder.agentExists) {
57
+ scanAgentFolder(folder, agentDir);
58
+ }
59
+
60
+ // Scan .gemini folder
61
+ if (folder.geminiExists) {
62
+ scanGeminiFolder(folder, geminiDir, manager);
63
+ }
64
+
65
+ // Root CLAUDE.md
66
+ const rootClaudeMd = path.join(dir, 'CLAUDE.md');
67
+ if (fs.existsSync(rootClaudeMd)) {
68
+ folder.files.push({
69
+ name: 'CLAUDE.md (root)',
70
+ path: rootClaudeMd,
71
+ type: 'claudemd',
72
+ size: fs.statSync(rootClaudeMd).size,
73
+ isRoot: true
74
+ });
75
+ }
76
+
77
+ // Root GEMINI.md
78
+ const rootGeminiMd = path.join(dir, 'GEMINI.md');
79
+ if (fs.existsSync(rootGeminiMd)) {
80
+ folder.agentFiles.push({
81
+ name: 'GEMINI.md (root)',
82
+ path: rootGeminiMd,
83
+ type: 'geminimd',
84
+ size: fs.statSync(rootGeminiMd).size,
85
+ isRoot: true
86
+ });
87
+ }
88
+
89
+ return folder;
90
+ }
91
+
92
+ /**
93
+ * Scan .claude folder contents
94
+ */
95
+ function scanClaudeFolder(folder, claudeDir, manager) {
96
+ // mcps.json
97
+ const mcpsPath = path.join(claudeDir, 'mcps.json');
98
+ if (fs.existsSync(mcpsPath)) {
99
+ const content = manager.loadJson(mcpsPath) || {};
100
+ folder.files.push({
101
+ name: 'mcps.json',
102
+ path: mcpsPath,
103
+ type: 'mcps',
104
+ size: fs.statSync(mcpsPath).size,
105
+ mcpCount: (content.include?.length || 0) + Object.keys(content.mcpServers || {}).length
106
+ });
107
+ }
108
+
109
+ // settings.json
110
+ const settingsPath = path.join(claudeDir, 'settings.json');
111
+ if (fs.existsSync(settingsPath)) {
112
+ folder.files.push({
113
+ name: 'settings.json',
114
+ path: settingsPath,
115
+ type: 'settings',
116
+ size: fs.statSync(settingsPath).size
117
+ });
118
+ }
119
+
120
+ // commands folder
121
+ addSubfolder(folder.files, claudeDir, 'commands', '.md', 'command');
122
+
123
+ // rules folder
124
+ addSubfolder(folder.files, claudeDir, 'rules', '.md', 'rule');
125
+
126
+ // workflows folder
127
+ addSubfolder(folder.files, claudeDir, 'workflows', '.md', 'workflow');
128
+
129
+ // memory folder
130
+ addSubfolder(folder.files, claudeDir, 'memory', '.md', 'memory');
131
+
132
+ // CLAUDE.md inside .claude
133
+ const claudeMdPath = path.join(claudeDir, 'CLAUDE.md');
134
+ if (fs.existsSync(claudeMdPath)) {
135
+ folder.files.push({
136
+ name: 'CLAUDE.md',
137
+ path: claudeMdPath,
138
+ type: 'claudemd',
139
+ size: fs.statSync(claudeMdPath).size
140
+ });
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Scan .agent folder contents
146
+ */
147
+ function scanAgentFolder(folder, agentDir) {
148
+ // rules folder
149
+ addSubfolder(folder.agentFiles, agentDir, 'rules', '.md', 'rule');
150
+ }
151
+
152
+ /**
153
+ * Scan .gemini folder contents
154
+ */
155
+ function scanGeminiFolder(folder, geminiDir, manager) {
156
+ // settings.json
157
+ const geminiSettingsPath = path.join(geminiDir, 'settings.json');
158
+ if (fs.existsSync(geminiSettingsPath)) {
159
+ const content = manager.loadJson(geminiSettingsPath) || {};
160
+ folder.geminiFiles.push({
161
+ name: 'settings.json',
162
+ path: geminiSettingsPath,
163
+ type: 'settings',
164
+ size: fs.statSync(geminiSettingsPath).size,
165
+ mcpCount: Object.keys(content.mcpServers || {}).length
166
+ });
167
+ }
168
+
169
+ // GEMINI.md
170
+ const geminiMdPath = path.join(geminiDir, 'GEMINI.md');
171
+ if (fs.existsSync(geminiMdPath)) {
172
+ folder.geminiFiles.push({
173
+ name: 'GEMINI.md',
174
+ path: geminiMdPath,
175
+ type: 'geminimd',
176
+ size: fs.statSync(geminiMdPath).size
177
+ });
178
+ }
179
+
180
+ // commands folder (Gemini uses TOML)
181
+ const commandsDir = path.join(geminiDir, 'commands');
182
+ if (fs.existsSync(commandsDir)) {
183
+ const commands = fs.readdirSync(commandsDir)
184
+ .filter(f => f.endsWith('.toml') || f.endsWith('.md'))
185
+ .map(f => ({
186
+ name: f,
187
+ path: path.join(commandsDir, f),
188
+ type: 'command',
189
+ size: fs.statSync(path.join(commandsDir, f)).size
190
+ }));
191
+ if (commands.length > 0) {
192
+ folder.geminiFiles.push({
193
+ name: 'commands',
194
+ path: commandsDir,
195
+ type: 'folder',
196
+ children: commands
197
+ });
198
+ }
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Helper to add subfolder contents
204
+ */
205
+ function addSubfolder(filesArray, parentDir, folderName, extension, fileType) {
206
+ const dir = path.join(parentDir, folderName);
207
+ if (fs.existsSync(dir)) {
208
+ const files = fs.readdirSync(dir)
209
+ .filter(f => f.endsWith(extension))
210
+ .map(f => ({
211
+ name: f,
212
+ path: path.join(dir, f),
213
+ type: fileType,
214
+ size: fs.statSync(path.join(dir, f)).size
215
+ }));
216
+ if (files.length > 0) {
217
+ filesArray.push({
218
+ name: folderName,
219
+ path: dir,
220
+ type: 'folder',
221
+ children: files
222
+ });
223
+ }
224
+ }
225
+ }
226
+
227
+ /**
228
+ * Get all intermediate paths between home and project
229
+ */
230
+ function getIntermediatePaths(projectDir) {
231
+ const home = os.homedir();
232
+ const paths = [];
233
+ let current = projectDir;
234
+
235
+ while (current && current !== path.dirname(current)) {
236
+ const claudeDir = path.join(current, '.claude');
237
+ let label = current;
238
+ if (current === home) {
239
+ label = '~';
240
+ } else if (current.startsWith(home + '/')) {
241
+ label = '~' + current.slice(home.length);
242
+ }
243
+ paths.unshift({
244
+ dir: current,
245
+ label,
246
+ hasClaudeFolder: fs.existsSync(claudeDir),
247
+ isHome: current === home,
248
+ isProject: current === projectDir
249
+ });
250
+
251
+ if (current === home) break;
252
+ current = path.dirname(current);
253
+ }
254
+
255
+ return paths;
256
+ }
257
+
258
+ /**
259
+ * Get contents of a specific .claude file
260
+ */
261
+ function getClaudeFile(filePath) {
262
+ if (!filePath || !fs.existsSync(filePath)) {
263
+ return { error: 'File not found', path: filePath };
264
+ }
265
+
266
+ const stat = fs.statSync(filePath);
267
+ if (stat.isDirectory()) {
268
+ return { error: 'Path is a directory', path: filePath };
269
+ }
270
+
271
+ const content = fs.readFileSync(filePath, 'utf8');
272
+ const ext = path.extname(filePath);
273
+
274
+ if (ext === '.json') {
275
+ try {
276
+ return { path: filePath, content, parsed: JSON.parse(content) };
277
+ } catch (e) {
278
+ return { path: filePath, content, parseError: e.message };
279
+ }
280
+ }
281
+
282
+ return { path: filePath, content };
283
+ }
284
+
285
+ /**
286
+ * Save content to a .claude file
287
+ */
288
+ function saveClaudeFile(body) {
289
+ const { path: filePath, content } = body;
290
+ if (!filePath) {
291
+ return { error: 'Path is required' };
292
+ }
293
+
294
+ const dir = path.dirname(filePath);
295
+ if (!fs.existsSync(dir)) {
296
+ fs.mkdirSync(dir, { recursive: true });
297
+ }
298
+
299
+ fs.writeFileSync(filePath, content, 'utf8');
300
+ return { success: true, path: filePath };
301
+ }
302
+
303
+ /**
304
+ * Delete a .claude file or folder
305
+ */
306
+ function deleteClaudeFile(filePath) {
307
+ if (!filePath || !fs.existsSync(filePath)) {
308
+ return { error: 'File not found', path: filePath };
309
+ }
310
+
311
+ const stat = fs.statSync(filePath);
312
+ if (stat.isDirectory()) {
313
+ fs.rmSync(filePath, { recursive: true });
314
+ } else {
315
+ fs.unlinkSync(filePath);
316
+ }
317
+
318
+ return { success: true, path: filePath };
319
+ }
320
+
321
+ /**
322
+ * Create a new .claude file
323
+ */
324
+ function createClaudeFile(body) {
325
+ const { dir, name, type, content = '' } = body;
326
+ if (!dir || !name) {
327
+ return { error: 'Dir and name are required' };
328
+ }
329
+
330
+ let filePath;
331
+ let initialContent = content;
332
+
333
+ switch (type) {
334
+ case 'mcps':
335
+ filePath = path.join(dir, '.claude', 'mcps.json');
336
+ initialContent = content || JSON.stringify({ include: [], mcpServers: {} }, null, 2);
337
+ break;
338
+ case 'settings':
339
+ filePath = path.join(dir, '.claude', 'settings.json');
340
+ initialContent = content || JSON.stringify({}, null, 2);
341
+ break;
342
+ case 'command':
343
+ filePath = path.join(dir, '.claude', 'commands', name);
344
+ break;
345
+ case 'rule':
346
+ filePath = path.join(dir, '.claude', 'rules', name);
347
+ break;
348
+ case 'workflow':
349
+ filePath = path.join(dir, '.claude', 'workflows', name);
350
+ break;
351
+ case 'memory':
352
+ filePath = path.join(dir, '.claude', 'memory', name);
353
+ break;
354
+ case 'claudemd':
355
+ filePath = path.join(dir, '.claude', 'CLAUDE.md');
356
+ break;
357
+ default:
358
+ filePath = path.join(dir, '.claude', name);
359
+ }
360
+
361
+ const parentDir = path.dirname(filePath);
362
+ if (!fs.existsSync(parentDir)) {
363
+ fs.mkdirSync(parentDir, { recursive: true });
364
+ }
365
+
366
+ if (fs.existsSync(filePath)) {
367
+ return { error: 'File already exists', path: filePath };
368
+ }
369
+
370
+ fs.writeFileSync(filePath, initialContent, 'utf8');
371
+ return { success: true, path: filePath, content: initialContent };
372
+ }
373
+
374
+ /**
375
+ * Rename a .claude file
376
+ */
377
+ function renameClaudeFile(body) {
378
+ const { oldPath, newName } = body;
379
+ if (!oldPath || !newName) {
380
+ return { error: 'oldPath and newName are required' };
381
+ }
382
+
383
+ if (!fs.existsSync(oldPath)) {
384
+ return { error: 'File not found', path: oldPath };
385
+ }
386
+
387
+ const dir = path.dirname(oldPath);
388
+ const newPath = path.join(dir, newName.endsWith('.md') ? newName : `${newName}.md`);
389
+
390
+ if (fs.existsSync(newPath)) {
391
+ return { error: 'A file with that name already exists', path: newPath };
392
+ }
393
+
394
+ fs.renameSync(oldPath, newPath);
395
+ return { success: true, oldPath, newPath };
396
+ }
397
+
398
+ /**
399
+ * Initialize a .claude folder in a directory
400
+ */
401
+ function initClaudeFolder(dir) {
402
+ if (!dir) {
403
+ return { error: 'dir is required' };
404
+ }
405
+
406
+ const absDir = path.resolve(dir.replace(/^~/, os.homedir()));
407
+ if (!fs.existsSync(absDir)) {
408
+ return { error: 'Directory not found', dir: absDir };
409
+ }
410
+
411
+ const claudeDir = path.join(absDir, '.claude');
412
+ if (fs.existsSync(claudeDir)) {
413
+ return { error: '.claude folder already exists', dir: claudeDir };
414
+ }
415
+
416
+ fs.mkdirSync(claudeDir, { recursive: true });
417
+ fs.writeFileSync(
418
+ path.join(claudeDir, 'mcps.json'),
419
+ JSON.stringify({ mcpServers: {} }, null, 2)
420
+ );
421
+
422
+ return { success: true, dir: claudeDir };
423
+ }
424
+
425
+ /**
426
+ * Delete a .claude folder
427
+ */
428
+ function deleteClaudeFolder(dir) {
429
+ if (!dir) {
430
+ return { error: 'dir is required' };
431
+ }
432
+
433
+ const absDir = path.resolve(dir.replace(/^~/, os.homedir()));
434
+ const claudeDir = path.join(absDir, '.claude');
435
+
436
+ if (!fs.existsSync(claudeDir)) {
437
+ return { error: '.claude folder not found', dir: claudeDir };
438
+ }
439
+
440
+ fs.rmSync(claudeDir, { recursive: true, force: true });
441
+ return { success: true, dir: claudeDir };
442
+ }
443
+
444
+ /**
445
+ * Initialize .claude folders in batch
446
+ */
447
+ function initClaudeFolderBatch(dirs) {
448
+ if (!dirs || !Array.isArray(dirs) || dirs.length === 0) {
449
+ return { error: 'dirs array is required' };
450
+ }
451
+
452
+ const results = [];
453
+ let successCount = 0;
454
+
455
+ for (const dir of dirs) {
456
+ const result = initClaudeFolder(dir);
457
+ results.push({ dir, ...result });
458
+ if (result.success) {
459
+ successCount++;
460
+ }
461
+ }
462
+
463
+ return {
464
+ success: true,
465
+ count: successCount,
466
+ results
467
+ };
468
+ }
469
+
470
+ /**
471
+ * Move or copy a .claude file/folder
472
+ */
473
+ function moveClaudeItem(body, manager) {
474
+ const { sourcePath, targetDir, mode = 'copy', merge = false } = body;
475
+ if (!sourcePath || !targetDir) {
476
+ return { error: 'sourcePath and targetDir are required' };
477
+ }
478
+
479
+ if (!fs.existsSync(sourcePath)) {
480
+ return { error: 'Source not found', path: sourcePath };
481
+ }
482
+
483
+ const sourceName = path.basename(sourcePath);
484
+ const targetClaudeDir = path.join(targetDir, '.claude');
485
+ let targetPath;
486
+
487
+ // Determine target path based on source type
488
+ if (sourceName === 'mcps.json' || sourceName === 'settings.json' || sourceName === 'CLAUDE.md') {
489
+ targetPath = path.join(targetClaudeDir, sourceName);
490
+ } else if (sourcePath.includes('/commands/')) {
491
+ targetPath = path.join(targetClaudeDir, 'commands', sourceName);
492
+ } else if (sourcePath.includes('/rules/')) {
493
+ targetPath = path.join(targetClaudeDir, 'rules', sourceName);
494
+ } else if (sourcePath.includes('/workflows/')) {
495
+ targetPath = path.join(targetClaudeDir, 'workflows', sourceName);
496
+ } else {
497
+ targetPath = path.join(targetClaudeDir, sourceName);
498
+ }
499
+
500
+ // Ensure target directory exists
501
+ const targetParent = path.dirname(targetPath);
502
+ if (!fs.existsSync(targetParent)) {
503
+ fs.mkdirSync(targetParent, { recursive: true });
504
+ }
505
+
506
+ // Handle existing target
507
+ if (fs.existsSync(targetPath)) {
508
+ if (!merge) {
509
+ return { error: 'Target already exists', targetPath, needsMerge: true };
510
+ }
511
+
512
+ if (targetPath.endsWith('.json')) {
513
+ const sourceContent = manager.loadJson(sourcePath) || {};
514
+ const targetContent = manager.loadJson(targetPath) || {};
515
+
516
+ if (sourceName === 'mcps.json') {
517
+ const merged = {
518
+ ...targetContent,
519
+ ...sourceContent,
520
+ include: [...new Set([...(targetContent.include || []), ...(sourceContent.include || [])])],
521
+ mcpServers: { ...(targetContent.mcpServers || {}), ...(sourceContent.mcpServers || {}) }
522
+ };
523
+ fs.writeFileSync(targetPath, JSON.stringify(merged, null, 2));
524
+ } else {
525
+ const merged = { ...targetContent, ...sourceContent };
526
+ fs.writeFileSync(targetPath, JSON.stringify(merged, null, 2));
527
+ }
528
+ } else {
529
+ fs.copyFileSync(sourcePath, targetPath);
530
+ }
531
+ } else {
532
+ fs.copyFileSync(sourcePath, targetPath);
533
+ }
534
+
535
+ if (mode === 'move') {
536
+ fs.unlinkSync(sourcePath);
537
+ }
538
+
539
+ return { success: true, sourcePath, targetPath, mode };
540
+ }
541
+
542
+ /**
543
+ * Scan directory for MCP tool projects
544
+ */
545
+ async function scanMcpTools(toolsDir) {
546
+ const tools = [];
547
+
548
+ try {
549
+ if (!fs.existsSync(toolsDir)) {
550
+ return tools;
551
+ }
552
+
553
+ const entries = fs.readdirSync(toolsDir, { withFileTypes: true });
554
+
555
+ for (const entry of entries) {
556
+ if (!entry.isDirectory()) continue;
557
+
558
+ const projectPath = path.join(toolsDir, entry.name);
559
+ const tool = { name: entry.name, path: projectPath, type: null };
560
+
561
+ // Check for Python MCP
562
+ const pyprojectPath = path.join(projectPath, 'pyproject.toml');
563
+ if (fs.existsSync(pyprojectPath)) {
564
+ tool.type = 'python';
565
+ try {
566
+ const content = fs.readFileSync(pyprojectPath, 'utf8');
567
+ const descMatch = content.match(/description\s*=\s*"([^"]+)"/);
568
+ if (descMatch) tool.description = descMatch[1];
569
+ if (content.includes('mcp') || content.includes('fastmcp')) {
570
+ tool.framework = 'fastmcp';
571
+ }
572
+ } catch (e) {}
573
+ }
574
+
575
+ // Check for Node MCP
576
+ const packagePath = path.join(projectPath, 'package.json');
577
+ if (fs.existsSync(packagePath)) {
578
+ tool.type = tool.type || 'node';
579
+ try {
580
+ const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
581
+ tool.description = tool.description || pkg.description;
582
+ if (pkg.dependencies?.['@modelcontextprotocol/sdk'] || pkg.name?.includes('mcp')) {
583
+ tool.framework = 'mcp-sdk';
584
+ }
585
+ } catch (e) {}
586
+ }
587
+
588
+ // Check for mcp_server.py
589
+ const mcpServerPath = path.join(projectPath, 'mcp_server.py');
590
+ if (fs.existsSync(mcpServerPath)) {
591
+ tool.type = 'python';
592
+ tool.framework = tool.framework || 'fastmcp';
593
+ tool.entryPoint = 'mcp_server.py';
594
+ }
595
+
596
+ if (tool.type) {
597
+ tools.push(tool);
598
+ }
599
+ }
600
+ } catch (e) {
601
+ console.error('Error scanning MCP tools:', e.message);
602
+ }
603
+
604
+ return tools;
605
+ }
606
+
607
+ /**
608
+ * Get file hashes for change detection
609
+ */
610
+ function getFileHashes(manager, projectDir) {
611
+ const hashes = {};
612
+ const crypto = require('crypto');
613
+
614
+ const hashFile = (filePath) => {
615
+ try {
616
+ if (!fs.existsSync(filePath)) return null;
617
+ const content = fs.readFileSync(filePath);
618
+ return crypto.createHash('md5').update(content).digest('hex');
619
+ } catch (e) {
620
+ return null;
621
+ }
622
+ };
623
+
624
+ // Hash all config files in hierarchy
625
+ const configs = manager.findAllConfigs(projectDir);
626
+ for (const { configPath } of configs) {
627
+ const hash = hashFile(configPath);
628
+ if (hash) hashes[configPath] = hash;
629
+ }
630
+
631
+ // Hash registry
632
+ const registryHash = hashFile(manager.registryPath);
633
+ if (registryHash) hashes[manager.registryPath] = registryHash;
634
+
635
+ // Hash all rules and commands
636
+ const rules = manager.collectFilesFromHierarchy(configs, 'rules');
637
+ const commands = manager.collectFilesFromHierarchy(configs, 'commands');
638
+
639
+ for (const { fullPath } of [...rules, ...commands]) {
640
+ const hash = hashFile(fullPath);
641
+ if (hash) hashes[fullPath] = hash;
642
+ }
643
+
644
+ // Hash env files
645
+ for (const { dir } of configs) {
646
+ const envPath = path.join(dir, '.claude', '.env');
647
+ const hash = hashFile(envPath);
648
+ if (hash) hashes[envPath] = hash;
649
+ }
650
+
651
+ return { hashes };
652
+ }
653
+
654
+ module.exports = {
655
+ scanFolderForExplorer,
656
+ getIntermediatePaths,
657
+ getClaudeFile,
658
+ saveClaudeFile,
659
+ deleteClaudeFile,
660
+ createClaudeFile,
661
+ renameClaudeFile,
662
+ initClaudeFolder,
663
+ deleteClaudeFolder,
664
+ initClaudeFolderBatch,
665
+ moveClaudeItem,
666
+ scanMcpTools,
667
+ getFileHashes,
668
+ };
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Routes Index - Aggregates all route modules
3
+ */
4
+
5
+ const projects = require('./projects');
6
+ const workstreams = require('./workstreams');
7
+ const activity = require('./activity');
8
+ const subprojects = require('./subprojects');
9
+ const registry = require('./registry');
10
+ const rules = require('./rules');
11
+ const commands = require('./commands');
12
+ const plugins = require('./plugins');
13
+ const updates = require('./updates');
14
+ const search = require('./search');
15
+ const toolSync = require('./tool-sync');
16
+ const fileExplorer = require('./file-explorer');
17
+ const memory = require('./memory');
18
+ const settings = require('./settings');
19
+ const env = require('./env');
20
+ const configs = require('./configs');
21
+ const mcpDiscovery = require('./mcp-discovery');
22
+
23
+ module.exports = {
24
+ projects,
25
+ workstreams,
26
+ activity,
27
+ subprojects,
28
+ registry,
29
+ rules,
30
+ commands,
31
+ plugins,
32
+ updates,
33
+ search,
34
+ toolSync,
35
+ fileExplorer,
36
+ memory,
37
+ settings,
38
+ env,
39
+ configs,
40
+ mcpDiscovery,
41
+ };