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,466 @@
1
+ /**
2
+ * Plugins Routes
3
+ */
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const os = require('os');
8
+ const { spawn } = require('child_process');
9
+
10
+ /**
11
+ * Default marketplace to auto-install
12
+ */
13
+ const DEFAULT_MARKETPLACE = 'regression-io/claude-config-plugins';
14
+
15
+ /**
16
+ * Get plugins directory path
17
+ */
18
+ function getPluginsDir() {
19
+ return path.join(os.homedir(), '.claude', 'plugins');
20
+ }
21
+
22
+ /**
23
+ * Ensure the default marketplace is installed
24
+ * Called automatically on first plugin view load
25
+ */
26
+ let defaultMarketplaceChecked = false;
27
+ async function ensureDefaultMarketplace() {
28
+ if (defaultMarketplaceChecked) return;
29
+ defaultMarketplaceChecked = true;
30
+
31
+ const pluginsDir = getPluginsDir();
32
+ const marketplacesPath = path.join(pluginsDir, 'known_marketplaces.json');
33
+
34
+ // Check if any marketplaces exist
35
+ let hasDefaultMarketplace = false;
36
+ if (fs.existsSync(marketplacesPath)) {
37
+ try {
38
+ const known = JSON.parse(fs.readFileSync(marketplacesPath, 'utf8'));
39
+ // Check if our default marketplace is already installed
40
+ hasDefaultMarketplace = Object.values(known).some(
41
+ m => m.source && m.source.includes('claude-config-plugins')
42
+ );
43
+ } catch (e) {}
44
+ }
45
+
46
+ // Auto-install if not present
47
+ if (!hasDefaultMarketplace) {
48
+ console.log(`Auto-installing default marketplace: ${DEFAULT_MARKETPLACE}`);
49
+ try {
50
+ await addMarketplaceInternal(DEFAULT_MARKETPLACE);
51
+ } catch (e) {
52
+ console.warn('Failed to auto-install default marketplace:', e.message);
53
+ }
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Internal marketplace add (no repo parameter needed)
59
+ */
60
+ function addMarketplaceInternal(repo) {
61
+ return new Promise((resolve, reject) => {
62
+ const proc = spawn('claude', ['plugin', 'marketplace', 'add', repo], {
63
+ cwd: os.homedir(),
64
+ env: process.env,
65
+ stdio: ['ignore', 'pipe', 'pipe']
66
+ });
67
+
68
+ let stderr = '';
69
+ proc.stderr?.on('data', (data) => { stderr += data.toString(); });
70
+
71
+ proc.on('close', (code) => {
72
+ if (code === 0) {
73
+ resolve();
74
+ } else {
75
+ reject(new Error(stderr || 'Failed to add marketplace'));
76
+ }
77
+ });
78
+
79
+ proc.on('error', reject);
80
+ });
81
+ }
82
+
83
+ /**
84
+ * Get all plugins
85
+ */
86
+ function getPlugins(manager) {
87
+ const pluginsDir = getPluginsDir();
88
+ const installedPath = path.join(pluginsDir, 'installed_plugins.json');
89
+ const marketplacesPath = path.join(pluginsDir, 'known_marketplaces.json');
90
+
91
+ // Load installed plugins
92
+ let installed = {};
93
+ if (fs.existsSync(installedPath)) {
94
+ try {
95
+ const data = JSON.parse(fs.readFileSync(installedPath, 'utf8'));
96
+ installed = data.plugins || {};
97
+ } catch (e) {}
98
+ }
99
+
100
+ // Load marketplaces and their plugins
101
+ const marketplaces = [];
102
+ const allPlugins = [];
103
+ const categories = new Set();
104
+
105
+ if (fs.existsSync(marketplacesPath)) {
106
+ try {
107
+ const known = JSON.parse(fs.readFileSync(marketplacesPath, 'utf8'));
108
+ for (const [name, info] of Object.entries(known)) {
109
+ const marketplace = {
110
+ name,
111
+ source: info.source,
112
+ installLocation: info.installLocation,
113
+ lastUpdated: info.lastUpdated,
114
+ plugins: [],
115
+ externalPlugins: []
116
+ };
117
+
118
+ // Track plugin names from manifest to avoid duplicates
119
+ const manifestPluginNames = new Set();
120
+
121
+ // Load marketplace manifest for plugins
122
+ const manifestPath = path.join(info.installLocation, '.claude-plugin', 'marketplace.json');
123
+ if (fs.existsSync(manifestPath)) {
124
+ try {
125
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
126
+ marketplace.description = manifest.description;
127
+ marketplace.owner = manifest.owner;
128
+ marketplace.plugins = (manifest.plugins || []).map(p => {
129
+ if (p.category) categories.add(p.category);
130
+ manifestPluginNames.add(p.name);
131
+ const isExternal = p.source?.includes('external_plugins');
132
+ const plugin = {
133
+ ...p,
134
+ marketplace: name,
135
+ sourceType: isExternal ? 'external' : 'internal',
136
+ installed: !!installed[`${p.name}@${name}`],
137
+ installedInfo: installed[`${p.name}@${name}`]?.[0] || null
138
+ };
139
+ allPlugins.push(plugin);
140
+ return plugin;
141
+ });
142
+ } catch (e) {}
143
+ }
144
+
145
+ // Load external plugins by scanning external_plugins directory
146
+ const externalDir = path.join(info.installLocation, 'external_plugins');
147
+ if (fs.existsSync(externalDir)) {
148
+ try {
149
+ const externals = fs.readdirSync(externalDir, { withFileTypes: true })
150
+ .filter(d => d.isDirectory())
151
+ .map(d => d.name);
152
+
153
+ for (const pluginName of externals) {
154
+ if (manifestPluginNames.has(pluginName)) continue;
155
+
156
+ const pluginManifestPath = path.join(externalDir, pluginName, '.claude-plugin', 'plugin.json');
157
+ if (fs.existsSync(pluginManifestPath)) {
158
+ try {
159
+ const pluginManifest = JSON.parse(fs.readFileSync(pluginManifestPath, 'utf8'));
160
+ if (manifestPluginNames.has(pluginManifest.name)) continue;
161
+
162
+ if (pluginManifest.category) categories.add(pluginManifest.category);
163
+ const plugin = {
164
+ name: pluginManifest.name || pluginName,
165
+ description: pluginManifest.description || '',
166
+ version: pluginManifest.version || '1.0.0',
167
+ author: pluginManifest.author,
168
+ category: pluginManifest.category || 'external',
169
+ homepage: pluginManifest.homepage,
170
+ mcpServers: pluginManifest.mcpServers,
171
+ lspServers: pluginManifest.lspServers,
172
+ commands: pluginManifest.commands,
173
+ marketplace: name,
174
+ sourceType: 'external',
175
+ installed: !!installed[`${pluginManifest.name || pluginName}@${name}`],
176
+ installedInfo: installed[`${pluginManifest.name || pluginName}@${name}`]?.[0] || null
177
+ };
178
+ marketplace.externalPlugins.push(plugin);
179
+ allPlugins.push(plugin);
180
+ } catch (e) {}
181
+ }
182
+ }
183
+ } catch (e) {}
184
+ }
185
+
186
+ marketplaces.push(marketplace);
187
+ }
188
+ } catch (e) {}
189
+ }
190
+
191
+ return {
192
+ installed,
193
+ marketplaces,
194
+ allPlugins,
195
+ categories: Array.from(categories).sort(),
196
+ pluginsDir
197
+ };
198
+ }
199
+
200
+ /**
201
+ * Get marketplaces
202
+ */
203
+ function getMarketplaces() {
204
+ const pluginsDir = getPluginsDir();
205
+ const marketplacesPath = path.join(pluginsDir, 'known_marketplaces.json');
206
+
207
+ if (fs.existsSync(marketplacesPath)) {
208
+ try {
209
+ return JSON.parse(fs.readFileSync(marketplacesPath, 'utf8'));
210
+ } catch (e) {}
211
+ }
212
+ return {};
213
+ }
214
+
215
+ /**
216
+ * Install a plugin
217
+ */
218
+ async function installPlugin(pluginId, marketplace, scope = 'user', projectDir = null) {
219
+ const args = ['plugin', 'install', `${pluginId}@${marketplace}`];
220
+ if (scope && scope !== 'user') {
221
+ args.push('--scope', scope);
222
+ }
223
+ return new Promise((resolve) => {
224
+ const proc = spawn('claude', args, {
225
+ cwd: projectDir || os.homedir(),
226
+ env: process.env,
227
+ stdio: ['ignore', 'pipe', 'pipe']
228
+ });
229
+
230
+ let stdout = '';
231
+ let stderr = '';
232
+
233
+ proc.stdout?.on('data', (data) => { stdout += data.toString(); });
234
+ proc.stderr?.on('data', (data) => { stderr += data.toString(); });
235
+
236
+ proc.on('close', (code) => {
237
+ if (code === 0) {
238
+ resolve({ success: true, message: stdout || 'Plugin installed' });
239
+ } else {
240
+ resolve({ success: false, error: stderr || stdout || 'Installation failed' });
241
+ }
242
+ });
243
+
244
+ proc.on('error', (err) => {
245
+ resolve({ success: false, error: err.message });
246
+ });
247
+ });
248
+ }
249
+
250
+ /**
251
+ * Uninstall a plugin
252
+ */
253
+ async function uninstallPlugin(pluginId) {
254
+ return new Promise((resolve) => {
255
+ const proc = spawn('claude', ['plugin', 'uninstall', pluginId], {
256
+ cwd: os.homedir(),
257
+ env: process.env,
258
+ stdio: ['ignore', 'pipe', 'pipe']
259
+ });
260
+
261
+ let stdout = '';
262
+ let stderr = '';
263
+
264
+ proc.stdout?.on('data', (data) => { stdout += data.toString(); });
265
+ proc.stderr?.on('data', (data) => { stderr += data.toString(); });
266
+
267
+ proc.on('close', (code) => {
268
+ if (code === 0) {
269
+ resolve({ success: true, message: stdout || 'Plugin uninstalled' });
270
+ } else {
271
+ resolve({ success: false, error: stderr || stdout || 'Uninstallation failed' });
272
+ }
273
+ });
274
+
275
+ proc.on('error', (err) => {
276
+ resolve({ success: false, error: err.message });
277
+ });
278
+ });
279
+ }
280
+
281
+ /**
282
+ * Add a marketplace
283
+ */
284
+ async function addMarketplace(name, repo) {
285
+ return new Promise((resolve) => {
286
+ const proc = spawn('claude', ['plugin', 'marketplace', 'add', repo], {
287
+ cwd: os.homedir(),
288
+ env: process.env,
289
+ stdio: ['ignore', 'pipe', 'pipe']
290
+ });
291
+
292
+ let stdout = '';
293
+ let stderr = '';
294
+
295
+ proc.stdout?.on('data', (data) => { stdout += data.toString(); });
296
+ proc.stderr?.on('data', (data) => { stderr += data.toString(); });
297
+
298
+ proc.on('close', (code) => {
299
+ if (code === 0) {
300
+ resolve({ success: true, message: stdout || 'Marketplace added' });
301
+ } else {
302
+ resolve({ success: false, error: stderr || stdout || 'Failed to add marketplace' });
303
+ }
304
+ });
305
+
306
+ proc.on('error', (err) => {
307
+ resolve({ success: false, error: err.message });
308
+ });
309
+ });
310
+ }
311
+
312
+ /**
313
+ * Refresh a marketplace
314
+ */
315
+ async function refreshMarketplace(name) {
316
+ return new Promise((resolve) => {
317
+ const proc = spawn('claude', ['plugin', 'marketplace', 'update', name], {
318
+ cwd: os.homedir(),
319
+ env: process.env,
320
+ stdio: ['ignore', 'pipe', 'pipe']
321
+ });
322
+
323
+ let stdout = '';
324
+ let stderr = '';
325
+
326
+ proc.stdout?.on('data', (data) => { stdout += data.toString(); });
327
+ proc.stderr?.on('data', (data) => { stderr += data.toString(); });
328
+
329
+ proc.on('close', (code) => {
330
+ if (code === 0) {
331
+ resolve({ success: true, message: stdout || 'Marketplace refreshed' });
332
+ } else {
333
+ resolve({ success: false, error: stderr || stdout || 'Failed to refresh marketplace' });
334
+ }
335
+ });
336
+
337
+ proc.on('error', (err) => {
338
+ resolve({ success: false, error: err.message });
339
+ });
340
+ });
341
+ }
342
+
343
+ /**
344
+ * Get enabled plugins for a directory (with hierarchy merging)
345
+ */
346
+ function getEnabledPluginsForDir(manager, dir) {
347
+ const homeDir = os.homedir();
348
+ const configs = manager.findAllConfigs(dir);
349
+
350
+ // Merge enabledPlugins from all configs (child overrides parent)
351
+ const merged = {};
352
+ for (const { configPath } of configs) {
353
+ try {
354
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
355
+ if (config.enabledPlugins) {
356
+ Object.assign(merged, config.enabledPlugins);
357
+ }
358
+ } catch (e) {}
359
+ }
360
+
361
+ // Build per-directory breakdown
362
+ const perDir = configs.map(({ dir: d, configPath }) => {
363
+ let enabledPlugins = {};
364
+ try {
365
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
366
+ enabledPlugins = config.enabledPlugins || {};
367
+ } catch (e) {}
368
+
369
+ return {
370
+ dir: d,
371
+ label: d === homeDir ? '~' : path.relative(dir, d) || '.',
372
+ enabledPlugins
373
+ };
374
+ });
375
+
376
+ return {
377
+ merged,
378
+ perDir
379
+ };
380
+ }
381
+
382
+ /**
383
+ * Set plugin enabled/disabled for a specific directory
384
+ */
385
+ function setPluginEnabled(manager, dir, pluginId, enabled) {
386
+ const configPath = path.join(dir, '.claude', 'mcps.json');
387
+ const claudeDir = path.join(dir, '.claude');
388
+
389
+ // Ensure .claude directory exists
390
+ if (!fs.existsSync(claudeDir)) {
391
+ fs.mkdirSync(claudeDir, { recursive: true });
392
+ }
393
+
394
+ // Load existing config or create new
395
+ let config = { include: [], mcpServers: {}, enabledPlugins: {} };
396
+ if (fs.existsSync(configPath)) {
397
+ try {
398
+ config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
399
+ if (!config.enabledPlugins) {
400
+ config.enabledPlugins = {};
401
+ }
402
+ } catch (e) {}
403
+ }
404
+
405
+ // Set the plugin state
406
+ if (enabled === null || enabled === undefined) {
407
+ // Remove the override (inherit from parent)
408
+ delete config.enabledPlugins[pluginId];
409
+ } else {
410
+ config.enabledPlugins[pluginId] = enabled;
411
+ }
412
+
413
+ // Clean up empty enabledPlugins
414
+ if (Object.keys(config.enabledPlugins).length === 0) {
415
+ delete config.enabledPlugins;
416
+ }
417
+
418
+ // Save config
419
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
420
+
421
+ return { success: true, dir, pluginId, enabled };
422
+ }
423
+
424
+ /**
425
+ * Get all plugins with their enabled state per directory
426
+ */
427
+ function getPluginsWithEnabledState(manager, projectDir) {
428
+ const pluginsData = getPlugins(manager);
429
+ const enabledData = getEnabledPluginsForDir(manager, projectDir);
430
+
431
+ // Add enabled state to each plugin
432
+ const pluginsWithState = pluginsData.allPlugins.map(plugin => {
433
+ const pluginId = `${plugin.name}@${plugin.marketplace}`;
434
+ return {
435
+ ...plugin,
436
+ enabledState: {
437
+ merged: enabledData.merged[pluginId] ?? null, // null means no explicit setting
438
+ perDir: enabledData.perDir.map(d => ({
439
+ dir: d.dir,
440
+ label: d.label,
441
+ enabled: d.enabledPlugins[pluginId] ?? null
442
+ }))
443
+ }
444
+ };
445
+ });
446
+
447
+ return {
448
+ ...pluginsData,
449
+ allPlugins: pluginsWithState,
450
+ enabledPlugins: enabledData
451
+ };
452
+ }
453
+
454
+ module.exports = {
455
+ getPluginsDir,
456
+ getPlugins,
457
+ getMarketplaces,
458
+ installPlugin,
459
+ uninstallPlugin,
460
+ addMarketplace,
461
+ refreshMarketplace,
462
+ getEnabledPluginsForDir,
463
+ setPluginEnabled,
464
+ getPluginsWithEnabledState,
465
+ ensureDefaultMarketplace,
466
+ };
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Projects Registry Routes
3
+ */
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const os = require('os');
8
+ const { execFileSync } = require('child_process');
9
+
10
+ /**
11
+ * Get all registered projects with status info
12
+ */
13
+ function getProjects(manager, projectDir) {
14
+ if (!manager) {
15
+ return { projects: [], activeProjectId: null, error: 'Manager not available' };
16
+ }
17
+
18
+ const registry = manager.loadProjectsRegistry();
19
+
20
+ const projects = registry.projects.map(p => ({
21
+ ...p,
22
+ exists: fs.existsSync(p.path),
23
+ hasClaudeConfig: fs.existsSync(path.join(p.path, '.claude')),
24
+ isActive: p.id === registry.activeProjectId
25
+ }));
26
+
27
+ projects.sort((a, b) => {
28
+ if (a.isActive) return -1;
29
+ if (b.isActive) return 1;
30
+ if (a.lastOpened && b.lastOpened) {
31
+ return new Date(b.lastOpened) - new Date(a.lastOpened);
32
+ }
33
+ return 0;
34
+ });
35
+
36
+ return {
37
+ projects,
38
+ activeProjectId: registry.activeProjectId,
39
+ currentDir: projectDir
40
+ };
41
+ }
42
+
43
+ /**
44
+ * Get active project details
45
+ */
46
+ function getActiveProject(manager, projectDir, getHierarchy, getSubprojects) {
47
+ if (!manager) return { error: 'Manager not available' };
48
+
49
+ const registry = manager.loadProjectsRegistry();
50
+ const activeProject = registry.projects.find(p => p.id === registry.activeProjectId);
51
+
52
+ return {
53
+ project: activeProject || null,
54
+ dir: projectDir,
55
+ hierarchy: getHierarchy(),
56
+ subprojects: getSubprojects()
57
+ };
58
+ }
59
+
60
+ /**
61
+ * Add a project to the registry
62
+ * @param {boolean} runClaudeInit - If true, run `claude /init` to create CLAUDE.md
63
+ */
64
+ function addProject(manager, projectPath, name, setProjectDir, runClaudeInit = false) {
65
+ if (!manager) return { error: 'Manager not available' };
66
+
67
+ const absPath = path.resolve(projectPath.replace(/^~/, os.homedir()));
68
+
69
+ if (!fs.existsSync(absPath)) {
70
+ return { error: 'Path not found', path: absPath };
71
+ }
72
+
73
+ const registry = manager.loadProjectsRegistry();
74
+
75
+ if (registry.projects.some(p => p.path === absPath)) {
76
+ return { error: 'Project already registered', path: absPath };
77
+ }
78
+
79
+ const claudeDir = path.join(absPath, '.claude');
80
+ const claudeMd = path.join(absPath, 'CLAUDE.md');
81
+ const mcpsFile = path.join(claudeDir, 'mcps.json');
82
+ let claudeInitRan = false;
83
+ let claudeInitError = null;
84
+
85
+ // Run claude /init if requested and CLAUDE.md doesn't exist
86
+ if (runClaudeInit && !fs.existsSync(claudeMd)) {
87
+ try {
88
+ execFileSync('claude', ['/init'], {
89
+ cwd: absPath,
90
+ stdio: 'pipe',
91
+ timeout: 30000
92
+ });
93
+ claudeInitRan = true;
94
+ } catch (err) {
95
+ // Claude Code not installed or init failed
96
+ claudeInitError = err.message;
97
+ }
98
+ }
99
+
100
+ // Ensure .claude/mcps.json exists (for claude-config to work)
101
+ if (!fs.existsSync(claudeDir)) {
102
+ fs.mkdirSync(claudeDir, { recursive: true });
103
+ }
104
+ if (!fs.existsSync(mcpsFile)) {
105
+ fs.writeFileSync(mcpsFile, JSON.stringify({ mcpServers: {} }, null, 2));
106
+ }
107
+
108
+ const project = {
109
+ id: Date.now().toString(36) + Math.random().toString(36).substr(2, 5),
110
+ name: name || path.basename(absPath),
111
+ path: absPath,
112
+ addedAt: new Date().toISOString(),
113
+ lastOpened: null
114
+ };
115
+
116
+ registry.projects.push(project);
117
+
118
+ if (!registry.activeProjectId) {
119
+ registry.activeProjectId = project.id;
120
+ setProjectDir(absPath);
121
+ }
122
+
123
+ manager.saveProjectsRegistry(registry);
124
+
125
+ return {
126
+ success: true,
127
+ project,
128
+ claudeInitRan,
129
+ claudeInitError
130
+ };
131
+ }
132
+
133
+ /**
134
+ * Remove a project from the registry
135
+ */
136
+ function removeProject(manager, projectId, setProjectDir) {
137
+ if (!manager) return { error: 'Manager not available' };
138
+
139
+ const registry = manager.loadProjectsRegistry();
140
+ const idx = registry.projects.findIndex(p => p.id === projectId);
141
+
142
+ if (idx === -1) {
143
+ return { error: 'Project not found' };
144
+ }
145
+
146
+ const removed = registry.projects.splice(idx, 1)[0];
147
+
148
+ if (registry.activeProjectId === projectId) {
149
+ registry.activeProjectId = registry.projects[0]?.id || null;
150
+ if (registry.projects[0]) {
151
+ setProjectDir(registry.projects[0].path);
152
+ }
153
+ }
154
+
155
+ manager.saveProjectsRegistry(registry);
156
+
157
+ return { success: true, removed };
158
+ }
159
+
160
+ /**
161
+ * Set active project and switch server context
162
+ */
163
+ function setActiveProject(manager, projectId, setProjectDir, getHierarchy, getSubprojects) {
164
+ if (!manager) return { error: 'Manager not available' };
165
+
166
+ const registry = manager.loadProjectsRegistry();
167
+ const project = registry.projects.find(p => p.id === projectId);
168
+
169
+ if (!project) {
170
+ return { error: 'Project not found' };
171
+ }
172
+
173
+ if (!fs.existsSync(project.path)) {
174
+ return { error: 'Project path no longer exists', path: project.path };
175
+ }
176
+
177
+ registry.activeProjectId = projectId;
178
+ project.lastOpened = new Date().toISOString();
179
+ manager.saveProjectsRegistry(registry);
180
+
181
+ setProjectDir(project.path);
182
+
183
+ return {
184
+ success: true,
185
+ project,
186
+ dir: project.path,
187
+ hierarchy: getHierarchy(),
188
+ subprojects: getSubprojects()
189
+ };
190
+ }
191
+
192
+ module.exports = {
193
+ getProjects,
194
+ getActiveProject,
195
+ addProject,
196
+ removeProject,
197
+ setActiveProject,
198
+ };
@@ -0,0 +1,30 @@
1
+ /**
2
+ * MCP Registry Routes
3
+ */
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+
8
+ /**
9
+ * Get MCP registry
10
+ */
11
+ function getRegistry(manager) {
12
+ return manager.loadJson(manager.registryPath) || { mcpServers: {} };
13
+ }
14
+
15
+ /**
16
+ * Update MCP registry
17
+ */
18
+ function updateRegistry(manager, body) {
19
+ try {
20
+ manager.saveJson(manager.registryPath, body);
21
+ return { success: true };
22
+ } catch (e) {
23
+ return { error: e.message };
24
+ }
25
+ }
26
+
27
+ module.exports = {
28
+ getRegistry,
29
+ updateRegistry,
30
+ };