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
package/ui/server.cjs ADDED
@@ -0,0 +1,773 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Claude Config Web UI Server
5
+ * Thin wrapper that delegates to route modules
6
+ */
7
+
8
+ const http = require('http');
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const url = require('url');
12
+ const os = require('os');
13
+ const { spawn } = require('child_process');
14
+ const TerminalServer = require('./terminal-server.cjs');
15
+
16
+ // Route modules
17
+ const routes = require('./routes');
18
+
19
+ class ConfigUIServer {
20
+ constructor(port = 3333, projectDir = null, manager = null) {
21
+ this.port = port;
22
+ this.manager = manager;
23
+ this.distDir = path.join(__dirname, 'dist');
24
+ this.terminalServer = new TerminalServer();
25
+ this.configPath = path.join(os.homedir(), '.claude-config', 'config.json');
26
+ this.config = this.loadConfig();
27
+ this.serverVersion = this.getPackageVersion();
28
+ this.serverStartTime = Date.now();
29
+
30
+ // Determine project directory
31
+ if (projectDir) {
32
+ this.projectDir = path.resolve(projectDir);
33
+ } else {
34
+ const activeProject = this.getActiveProjectFromRegistry();
35
+ this.projectDir = activeProject?.path || process.cwd();
36
+ }
37
+ this.projectDir = path.resolve(this.projectDir);
38
+ }
39
+
40
+ // ==================== Core Methods ====================
41
+
42
+ getActiveProjectFromRegistry() {
43
+ if (!this.manager) return null;
44
+ try {
45
+ const registry = this.manager.loadProjectsRegistry();
46
+ if (registry.activeProjectId && registry.projects) {
47
+ return registry.projects.find(p => p.id === registry.activeProjectId);
48
+ }
49
+ } catch (e) {}
50
+ return null;
51
+ }
52
+
53
+ loadConfig() {
54
+ const defaults = {
55
+ toolsDir: path.join(os.homedir(), 'mcp-tools'),
56
+ registryPath: path.join(os.homedir(), '.claude', 'registry.json'),
57
+ ui: { port: 3333, openBrowser: true },
58
+ enabledTools: ['claude']
59
+ };
60
+
61
+ try {
62
+ const oldConfigPath = path.join(os.homedir(), '.claude', 'config.json');
63
+ if (!fs.existsSync(this.configPath) && fs.existsSync(oldConfigPath)) {
64
+ const dir = path.dirname(this.configPath);
65
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
66
+ fs.copyFileSync(oldConfigPath, this.configPath);
67
+ }
68
+
69
+ if (fs.existsSync(this.configPath)) {
70
+ const userConfig = JSON.parse(fs.readFileSync(this.configPath, 'utf8'));
71
+ return { ...defaults, ...userConfig };
72
+ }
73
+ } catch (e) {
74
+ console.error('Error loading config:', e.message);
75
+ }
76
+ return defaults;
77
+ }
78
+
79
+ saveConfig(config) {
80
+ try {
81
+ const dir = path.dirname(this.configPath);
82
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
83
+ fs.writeFileSync(this.configPath, JSON.stringify(config, null, 2) + '\n');
84
+ this.config = config;
85
+ return { success: true };
86
+ } catch (e) {
87
+ return { error: e.message };
88
+ }
89
+ }
90
+
91
+ getPackageVersion() {
92
+ try {
93
+ const pkgPath = path.join(__dirname, '..', 'package.json');
94
+ return JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version;
95
+ } catch (e) {
96
+ return 'unknown';
97
+ }
98
+ }
99
+
100
+ getChangelog() {
101
+ try {
102
+ const changelogPath = path.join(__dirname, '..', 'CHANGELOG.md');
103
+ if (fs.existsSync(changelogPath)) {
104
+ return { success: true, content: fs.readFileSync(changelogPath, 'utf8') };
105
+ }
106
+ return { success: false, error: 'Changelog not found' };
107
+ } catch (e) {
108
+ return { success: false, error: e.message };
109
+ }
110
+ }
111
+
112
+ browseDirectory(dirPath, type = 'directory') {
113
+ try {
114
+ const expandedPath = dirPath.replace(/^~/, os.homedir());
115
+ const resolvedPath = path.resolve(expandedPath);
116
+
117
+ if (!fs.existsSync(resolvedPath)) {
118
+ return { error: 'Directory not found', path: resolvedPath };
119
+ }
120
+
121
+ const stat = fs.statSync(resolvedPath);
122
+ if (!stat.isDirectory()) {
123
+ return this.browseDirectory(path.dirname(resolvedPath), type);
124
+ }
125
+
126
+ const entries = fs.readdirSync(resolvedPath, { withFileTypes: true });
127
+ const items = [];
128
+
129
+ const parentDir = path.dirname(resolvedPath);
130
+ if (parentDir !== resolvedPath) {
131
+ items.push({ name: '..', path: parentDir, type: 'directory', isParent: true });
132
+ }
133
+
134
+ for (const entry of entries) {
135
+ if (entry.name.startsWith('.') && entry.name !== '.claude') continue;
136
+
137
+ const fullPath = path.join(resolvedPath, entry.name);
138
+ const isDir = entry.isDirectory();
139
+
140
+ if (type === 'directory' && !isDir) continue;
141
+ if (type === 'file' && !isDir && !entry.name.endsWith('.json')) continue;
142
+
143
+ items.push({ name: entry.name, path: fullPath, type: isDir ? 'directory' : 'file' });
144
+ }
145
+
146
+ items.sort((a, b) => {
147
+ if (a.isParent) return -1;
148
+ if (b.isParent) return 1;
149
+ if (a.type !== b.type) return a.type === 'directory' ? -1 : 1;
150
+ return a.name.localeCompare(b.name);
151
+ });
152
+
153
+ return { path: resolvedPath, items, home: os.homedir() };
154
+ } catch (e) {
155
+ return { error: e.message };
156
+ }
157
+ }
158
+
159
+ getHierarchy() {
160
+ const configs = this.manager.findAllConfigs(this.projectDir);
161
+ return configs.map(c => ({
162
+ dir: c.dir,
163
+ label: c.dir === process.env.HOME ? '~' : path.relative(this.projectDir, c.dir) || '.',
164
+ configPath: c.configPath
165
+ }));
166
+ }
167
+
168
+ getToolsInfo() {
169
+ const toolPaths = this.manager.getToolPaths();
170
+ const detected = this.manager.detectInstalledTools();
171
+ const enabledTools = this.config.enabledTools || ['claude'];
172
+
173
+ return {
174
+ tools: Object.entries(toolPaths).map(([id, config]) => ({
175
+ id,
176
+ name: config.name,
177
+ icon: config.icon,
178
+ color: config.color,
179
+ globalConfig: config.globalConfig,
180
+ projectFolder: config.projectFolder,
181
+ projectRules: config.projectRules,
182
+ projectInstructions: config.projectInstructions,
183
+ supportsEnvInterpolation: config.supportsEnvInterpolation,
184
+ detected: detected[id] || { installed: false },
185
+ enabled: enabledTools.includes(id)
186
+ })),
187
+ enabledTools
188
+ };
189
+ }
190
+
191
+ switchProject(newDir) {
192
+ if (!fs.existsSync(newDir)) {
193
+ return { success: false, error: 'Directory not found' };
194
+ }
195
+ this.projectDir = path.resolve(newDir);
196
+ return {
197
+ success: true,
198
+ dir: this.projectDir,
199
+ hierarchy: this.getHierarchy(),
200
+ subprojects: routes.subprojects.getSubprojectsForDir(this.manager, this.config, this.projectDir)
201
+ };
202
+ }
203
+
204
+ applyConfig(dir) {
205
+ return routes.configs.applyConfig(dir, this.projectDir, this.config, this.manager);
206
+ }
207
+
208
+ getClaudeFolders() {
209
+ const configs = this.manager.findAllConfigs(this.projectDir);
210
+ const home = os.homedir();
211
+ const folders = [];
212
+
213
+ for (const c of configs) {
214
+ const folder = routes.fileExplorer.scanFolderForExplorer(c.dir, this.manager);
215
+ if (folder) folders.push(folder);
216
+ }
217
+
218
+ // Add subprojects
219
+ const addSubprojectsRecursive = (parentDir, depth = 0) => {
220
+ if (depth > 3) return;
221
+ const subprojects = routes.subprojects.getSubprojectsForDir(this.manager, this.config, parentDir);
222
+ for (const sub of subprojects) {
223
+ if (folders.some(f => f.dir === sub.dir)) continue;
224
+ let subFolder = routes.fileExplorer.scanFolderForExplorer(sub.dir, this.manager, sub.name);
225
+ if (!subFolder) {
226
+ subFolder = {
227
+ dir: sub.dir, label: sub.name,
228
+ claudePath: path.join(sub.dir, '.claude'),
229
+ agentPath: path.join(sub.dir, '.agent'),
230
+ geminiPath: path.join(sub.dir, '.gemini'),
231
+ exists: false, agentExists: false, geminiExists: false,
232
+ files: [], agentFiles: [], geminiFiles: [],
233
+ appliedTemplate: null
234
+ };
235
+ }
236
+ subFolder.appliedTemplate = null;
237
+ subFolder.isSubproject = true;
238
+ subFolder.hasConfig = sub.hasConfig;
239
+ subFolder.mcpCount = sub.mcpCount || 0;
240
+ subFolder.isManual = sub.isManual || false;
241
+ subFolder.parentDir = parentDir;
242
+ subFolder.depth = depth + 1;
243
+ folders.push(subFolder);
244
+ addSubprojectsRecursive(sub.dir, depth + 1);
245
+ }
246
+ };
247
+
248
+ addSubprojectsRecursive(this.projectDir);
249
+ return folders;
250
+ }
251
+
252
+ // ==================== HTTP Server ====================
253
+
254
+ start() {
255
+ const server = http.createServer((req, res) => this.handleRequest(req, res));
256
+ this.terminalServer.attach(server);
257
+
258
+ server.listen(this.port, () => {
259
+ console.log(`\n🚀 Claude Config UI running at http://localhost:${this.port}`);
260
+ console.log(`📁 Project: ${this.projectDir}`);
261
+ console.log(`💻 Terminal WebSocket: ws://localhost:${this.port}/ws/terminal\n`);
262
+ });
263
+ }
264
+
265
+ async handleRequest(req, res) {
266
+ const parsedUrl = url.parse(req.url, true);
267
+ const pathname = parsedUrl.pathname;
268
+
269
+ res.setHeader('Access-Control-Allow-Origin', '*');
270
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
271
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
272
+
273
+ if (req.method === 'OPTIONS') {
274
+ res.writeHead(200);
275
+ res.end();
276
+ return;
277
+ }
278
+
279
+ try {
280
+ if (pathname.startsWith('/api/')) {
281
+ return this.handleAPI(req, res, pathname, parsedUrl.query);
282
+ }
283
+ return this.serveStatic(req, res, pathname);
284
+ } catch (error) {
285
+ console.error('Server error:', error);
286
+ res.writeHead(500);
287
+ res.end(JSON.stringify({ error: error.message }));
288
+ }
289
+ }
290
+
291
+ serveStatic(req, res, pathname) {
292
+ let filePath = pathname === '/' || pathname === '/index.html'
293
+ ? path.join(this.distDir, 'index.html')
294
+ : path.join(this.distDir, pathname);
295
+
296
+ if (!filePath.startsWith(this.distDir)) {
297
+ res.writeHead(403);
298
+ res.end('Forbidden');
299
+ return;
300
+ }
301
+
302
+ if (!fs.existsSync(filePath)) {
303
+ filePath = path.join(this.distDir, 'index.html');
304
+ if (!fs.existsSync(filePath)) {
305
+ res.writeHead(404);
306
+ res.end('Not Found - Run "npm run build" in the ui/ directory first');
307
+ return;
308
+ }
309
+ }
310
+
311
+ const ext = path.extname(filePath).toLowerCase();
312
+ const contentTypes = {
313
+ '.html': 'text/html', '.js': 'application/javascript', '.css': 'text/css',
314
+ '.json': 'application/json', '.png': 'image/png', '.jpg': 'image/jpeg',
315
+ '.svg': 'image/svg+xml', '.ico': 'image/x-icon', '.woff2': 'font/woff2'
316
+ };
317
+
318
+ try {
319
+ const content = fs.readFileSync(filePath);
320
+ res.writeHead(200, { 'Content-Type': contentTypes[ext] || 'application/octet-stream' });
321
+ res.end(content);
322
+ } catch (error) {
323
+ res.writeHead(500);
324
+ res.end('Error reading file');
325
+ }
326
+ }
327
+
328
+ async handleAPI(req, res, pathname, query) {
329
+ res.setHeader('Content-Type', 'application/json');
330
+
331
+ let body = {};
332
+ if (req.method === 'POST' || req.method === 'PUT') {
333
+ body = await this.parseBody(req);
334
+ }
335
+
336
+ // Route dispatch
337
+ switch (pathname) {
338
+ // Project info
339
+ case '/api/project':
340
+ return this.json(res, {
341
+ dir: this.projectDir,
342
+ hierarchy: this.getHierarchy(),
343
+ subprojects: routes.subprojects.getSubprojectsForDir(this.manager, this.config, this.projectDir)
344
+ });
345
+
346
+ case '/api/subprojects':
347
+ const subDir = query.dir ? path.resolve(query.dir.replace(/^~/, os.homedir())) : this.projectDir;
348
+ return this.json(res, { subprojects: routes.subprojects.getSubprojectsForDir(this.manager, this.config, subDir) });
349
+
350
+ case '/api/subprojects/add':
351
+ if (req.method === 'POST') {
352
+ return this.json(res, routes.subprojects.addManualSubproject(this.config, c => this.saveConfig(c), body.projectDir, body.subprojectDir));
353
+ }
354
+ break;
355
+
356
+ case '/api/subprojects/remove':
357
+ if (req.method === 'POST') {
358
+ return this.json(res, routes.subprojects.removeManualSubproject(this.config, c => this.saveConfig(c), body.projectDir, body.subprojectDir));
359
+ }
360
+ break;
361
+
362
+ case '/api/subprojects/hide':
363
+ if (req.method === 'POST') {
364
+ return this.json(res, routes.subprojects.hideSubproject(this.config, c => this.saveConfig(c), body.projectDir, body.subprojectDir));
365
+ }
366
+ break;
367
+
368
+ case '/api/subprojects/unhide':
369
+ if (req.method === 'POST') {
370
+ return this.json(res, routes.subprojects.unhideSubproject(this.config, c => this.saveConfig(c), body.projectDir, body.subprojectDir));
371
+ }
372
+ break;
373
+
374
+ case '/api/subprojects/hidden':
375
+ const hiddenDir = query.dir ? path.resolve(query.dir.replace(/^~/, os.homedir())) : this.projectDir;
376
+ return this.json(res, { hidden: routes.subprojects.getHiddenSubprojects(this.config, hiddenDir) });
377
+
378
+ case '/api/switch-project':
379
+ if (req.method === 'POST') return this.json(res, this.switchProject(body.dir));
380
+ break;
381
+
382
+ case '/api/configs':
383
+ return this.json(res, routes.configs.getConfigs(this.manager, this.projectDir));
384
+
385
+ case '/api/config':
386
+ if (req.method === 'PUT') {
387
+ return this.json(res, routes.configs.updateConfig(body, this.manager, dir => this.applyConfig(dir)));
388
+ }
389
+ break;
390
+
391
+ case '/api/version':
392
+ return this.json(res, {
393
+ version: this.serverVersion,
394
+ currentVersion: this.getPackageVersion(),
395
+ startTime: this.serverStartTime,
396
+ needsRestart: this.serverVersion !== this.getPackageVersion()
397
+ });
398
+
399
+ case '/api/changelog':
400
+ return this.json(res, this.getChangelog());
401
+
402
+ case '/api/restart':
403
+ if (req.method === 'POST') {
404
+ this.json(res, { success: true, message: 'Server restarting...' });
405
+ setTimeout(() => {
406
+ const child = spawn(process.argv[0], process.argv.slice(1), {
407
+ detached: true, stdio: 'ignore', cwd: process.cwd(), env: process.env
408
+ });
409
+ child.unref();
410
+ process.exit(0);
411
+ }, 500);
412
+ return;
413
+ }
414
+ break;
415
+
416
+ case '/api/registry':
417
+ if (req.method === 'GET') return this.json(res, routes.registry.getRegistry(this.manager));
418
+ if (req.method === 'PUT') return this.json(res, routes.registry.updateRegistry(this.manager, body));
419
+ break;
420
+
421
+ case '/api/plugins':
422
+ if (req.method === 'GET') {
423
+ // Ensure default marketplace is installed on first access
424
+ await routes.plugins.ensureDefaultMarketplace();
425
+ return this.json(res, routes.plugins.getPluginsWithEnabledState(this.manager, this.projectDir));
426
+ }
427
+ break;
428
+
429
+ case '/api/plugins/install':
430
+ if (req.method === 'POST') return this.json(res, await routes.plugins.installPlugin(body.pluginId, body.marketplace, body.scope, body.projectDir));
431
+ break;
432
+
433
+ case '/api/plugins/uninstall':
434
+ if (req.method === 'POST') return this.json(res, await routes.plugins.uninstallPlugin(body.pluginId));
435
+ break;
436
+
437
+ case '/api/plugins/enabled':
438
+ if (req.method === 'GET') {
439
+ const targetDir = query.dir || this.projectDir;
440
+ return this.json(res, routes.plugins.getEnabledPluginsForDir(this.manager, targetDir));
441
+ }
442
+ if (req.method === 'POST') {
443
+ return this.json(res, routes.plugins.setPluginEnabled(this.manager, body.dir, body.pluginId, body.enabled));
444
+ }
445
+ break;
446
+
447
+ case '/api/plugins/marketplaces':
448
+ if (req.method === 'GET') return this.json(res, routes.plugins.getMarketplaces());
449
+ if (req.method === 'POST') return this.json(res, await routes.plugins.addMarketplace(body.name, body.repo));
450
+ break;
451
+
452
+ case '/api/plugins/marketplaces/refresh':
453
+ if (req.method === 'POST') return this.json(res, await routes.plugins.refreshMarketplace(body.name));
454
+ break;
455
+
456
+ case '/api/rules':
457
+ return this.json(res, routes.rules.getRules(this.manager, this.projectDir));
458
+
459
+ case '/api/rule':
460
+ if (req.method === 'GET') return this.json(res, routes.rules.getRule(query.path));
461
+ if (req.method === 'PUT') return this.json(res, routes.rules.saveRule(body));
462
+ if (req.method === 'DELETE') return this.json(res, routes.rules.deleteRule(query.path));
463
+ if (req.method === 'POST') return this.json(res, routes.rules.createRule(body, this.projectDir));
464
+ break;
465
+
466
+ case '/api/commands':
467
+ return this.json(res, routes.commands.getCommands(this.manager, this.projectDir));
468
+
469
+ case '/api/command':
470
+ if (req.method === 'GET') return this.json(res, routes.commands.getCommand(query.path));
471
+ if (req.method === 'PUT') return this.json(res, routes.commands.saveCommand(body));
472
+ if (req.method === 'DELETE') return this.json(res, routes.commands.deleteCommand(query.path));
473
+ if (req.method === 'POST') return this.json(res, routes.commands.createCommand(body, this.projectDir));
474
+ break;
475
+
476
+ case '/api/apply':
477
+ if (req.method === 'POST') return this.json(res, this.applyConfig(body.dir));
478
+ break;
479
+
480
+ case '/api/env':
481
+ if (req.method === 'GET') return this.json(res, routes.env.getEnv(query.dir, this.projectDir));
482
+ if (req.method === 'PUT') return this.json(res, routes.env.saveEnv(body));
483
+ break;
484
+
485
+ case '/api/file-hashes':
486
+ return this.json(res, routes.fileExplorer.getFileHashes(this.manager, this.projectDir));
487
+
488
+ case '/api/version-check':
489
+ return this.json(res, await routes.updates.checkForUpdates(this.manager, __dirname));
490
+
491
+ case '/api/update':
492
+ if (req.method === 'POST') return this.json(res, await routes.updates.performUpdate(body, this.manager));
493
+ break;
494
+
495
+ case '/api/reload':
496
+ if (req.method === 'POST') return this.json(res, { success: true, message: 'Reload triggered' });
497
+ break;
498
+
499
+ case '/api/search/github':
500
+ if (req.method === 'GET') return this.json(res, await routes.search.searchGithub(query.q));
501
+ break;
502
+
503
+ case '/api/search/npm':
504
+ if (req.method === 'GET') return this.json(res, await routes.search.searchNpm(query.q));
505
+ break;
506
+
507
+ case '/api/mcp-tools':
508
+ if (req.method === 'GET') {
509
+ const toolsDir = query.dir || this.config.toolsDir;
510
+ return this.json(res, { dir: toolsDir, tools: await routes.fileExplorer.scanMcpTools(toolsDir) });
511
+ }
512
+ break;
513
+
514
+ case '/api/mcp-server-tools':
515
+ if (req.method === 'GET') {
516
+ if (query.server) {
517
+ return this.json(res, await routes.mcpDiscovery.getServerTools(this.manager, query.server));
518
+ } else {
519
+ return this.json(res, await routes.mcpDiscovery.getAllServerTools(this.manager));
520
+ }
521
+ }
522
+ if (req.method === 'DELETE') {
523
+ return this.json(res, routes.mcpDiscovery.clearCache(query.server));
524
+ }
525
+ break;
526
+
527
+ case '/api/claude-folders':
528
+ return this.json(res, this.getClaudeFolders());
529
+
530
+ case '/api/intermediate-paths':
531
+ return this.json(res, routes.fileExplorer.getIntermediatePaths(this.projectDir));
532
+
533
+ case '/api/claude-file':
534
+ if (req.method === 'GET') return this.json(res, routes.fileExplorer.getClaudeFile(query.path));
535
+ if (req.method === 'PUT') return this.json(res, routes.fileExplorer.saveClaudeFile(body));
536
+ if (req.method === 'DELETE') return this.json(res, routes.fileExplorer.deleteClaudeFile(query.path));
537
+ if (req.method === 'POST') return this.json(res, routes.fileExplorer.createClaudeFile(body));
538
+ break;
539
+
540
+ case '/api/claude-move':
541
+ if (req.method === 'POST') return this.json(res, routes.fileExplorer.moveClaudeItem(body, this.manager));
542
+ break;
543
+
544
+ case '/api/claude-rename':
545
+ if (req.method === 'POST') return this.json(res, routes.fileExplorer.renameClaudeFile(body));
546
+ break;
547
+
548
+ case '/api/init-claude-folder':
549
+ if (req.method === 'POST') return this.json(res, routes.fileExplorer.initClaudeFolder(body.dir));
550
+ break;
551
+
552
+ case '/api/delete-claude-folder':
553
+ if (req.method === 'POST') return this.json(res, routes.fileExplorer.deleteClaudeFolder(body.dir));
554
+ break;
555
+
556
+ case '/api/init-claude-folder-batch':
557
+ if (req.method === 'POST') return this.json(res, routes.fileExplorer.initClaudeFolderBatch(body.dirs));
558
+ break;
559
+
560
+ case '/api/sync/preview':
561
+ if (req.method === 'POST') return this.json(res, routes.toolSync.getSyncPreview(body.dir || this.projectDir, body.source, body.target));
562
+ break;
563
+
564
+ case '/api/sync/rules':
565
+ if (req.method === 'POST') return this.json(res, routes.toolSync.syncRules(body.dir || this.projectDir, body.source, body.target, body.files));
566
+ break;
567
+
568
+ case '/api/memory':
569
+ return this.json(res, routes.memory.getMemory(this.projectDir));
570
+
571
+ case '/api/memory/file':
572
+ if (req.method === 'GET') return this.json(res, routes.memory.getMemoryFile(query.path, this.projectDir));
573
+ if (req.method === 'PUT') return this.json(res, routes.memory.saveMemoryFile(body, this.projectDir));
574
+ break;
575
+
576
+ case '/api/memory/entry':
577
+ if (req.method === 'POST') return this.json(res, routes.memory.addMemoryEntry(body, this.projectDir));
578
+ break;
579
+
580
+ case '/api/memory/init':
581
+ if (req.method === 'POST') return this.json(res, routes.memory.initProjectMemory(body.dir, this.projectDir));
582
+ break;
583
+
584
+ case '/api/memory/search':
585
+ if (req.method === 'GET') return this.json(res, routes.memory.searchMemory(query.q, this.projectDir));
586
+ break;
587
+
588
+ case '/api/memory/sync':
589
+ return this.json(res, routes.memory.getSyncState());
590
+
591
+ case '/api/claude-settings':
592
+ if (req.method === 'GET') return this.json(res, routes.settings.getClaudeSettings());
593
+ if (req.method === 'PUT') return this.json(res, routes.settings.saveClaudeSettings(body));
594
+ break;
595
+
596
+ case '/api/gemini-settings':
597
+ if (req.method === 'GET') return this.json(res, routes.settings.getGeminiSettings());
598
+ if (req.method === 'PUT') return this.json(res, routes.settings.saveGeminiSettings(body));
599
+ break;
600
+
601
+ case '/api/antigravity-settings':
602
+ if (req.method === 'GET') return this.json(res, routes.settings.getAntigravitySettings());
603
+ if (req.method === 'PUT') return this.json(res, routes.settings.saveAntigravitySettings(body));
604
+ break;
605
+
606
+ case '/api/codex-settings':
607
+ if (req.method === 'GET') return this.json(res, routes.settings.getCodexSettings());
608
+ if (req.method === 'PUT') return this.json(res, routes.settings.saveCodexSettings(body));
609
+ break;
610
+
611
+ case '/api/preferences':
612
+ if (req.method === 'GET') return this.json(res, { config: this.config, path: this.configPath });
613
+ if (req.method === 'PUT') return this.json(res, this.saveConfig(body));
614
+ break;
615
+
616
+ case '/api/tools':
617
+ if (req.method === 'GET') return this.json(res, this.getToolsInfo());
618
+ break;
619
+
620
+ case '/api/browse':
621
+ if (req.method === 'POST') return this.json(res, this.browseDirectory(body.path, body.type));
622
+ break;
623
+
624
+ case '/api/projects':
625
+ if (req.method === 'GET') return this.json(res, routes.projects.getProjects(this.manager, this.projectDir));
626
+ if (req.method === 'POST') return this.json(res, routes.projects.addProject(this.manager, body.path, body.name, (p) => { this.projectDir = p; }, body.runClaudeInit));
627
+ break;
628
+
629
+ case '/api/projects/active':
630
+ if (req.method === 'GET') return this.json(res, routes.projects.getActiveProject(this.manager, this.projectDir, () => this.getHierarchy(), () => routes.subprojects.getSubprojectsForDir(this.manager, this.config, this.projectDir)));
631
+ if (req.method === 'PUT') {
632
+ const result = routes.projects.setActiveProject(
633
+ this.manager,
634
+ body.id,
635
+ (newDir) => { this.projectDir = newDir; },
636
+ () => this.getHierarchy(),
637
+ () => routes.subprojects.getSubprojectsForDir(this.manager, this.config, this.projectDir)
638
+ );
639
+ return this.json(res, result);
640
+ }
641
+ break;
642
+
643
+ case '/api/workstreams':
644
+ if (req.method === 'GET') return this.json(res, routes.workstreams.getWorkstreams(this.manager));
645
+ if (req.method === 'POST') return this.json(res, routes.workstreams.createWorkstream(this.manager, body));
646
+ break;
647
+
648
+ case '/api/workstreams/active':
649
+ if (req.method === 'GET') return this.json(res, routes.workstreams.getActiveWorkstream(this.manager));
650
+ if (req.method === 'PUT') return this.json(res, routes.workstreams.setActiveWorkstream(this.manager, body.id));
651
+ break;
652
+
653
+ case '/api/workstreams/inject':
654
+ if (req.method === 'GET') return this.json(res, routes.workstreams.injectWorkstream(this.manager));
655
+ break;
656
+
657
+ case '/api/workstreams/detect':
658
+ if (req.method === 'POST') return this.json(res, routes.workstreams.detectWorkstream(this.manager, body.dir || this.projectDir));
659
+ break;
660
+
661
+ case '/api/workstreams/hook-status':
662
+ if (req.method === 'GET') return this.json(res, routes.workstreams.getWorkstreamHookStatus());
663
+ break;
664
+
665
+ case '/api/workstreams/install-hook':
666
+ if (req.method === 'POST') return this.json(res, routes.workstreams.installWorkstreamHook());
667
+ break;
668
+
669
+ case '/api/activity':
670
+ if (req.method === 'GET') return this.json(res, routes.activity.getActivitySummary(this.manager));
671
+ if (req.method === 'DELETE') return this.json(res, routes.activity.clearActivity(this.manager, body.olderThanDays || 30));
672
+ break;
673
+
674
+ case '/api/activity/log':
675
+ if (req.method === 'POST') return this.json(res, routes.activity.logActivity(this.manager, body.files, body.sessionId));
676
+ break;
677
+
678
+ case '/api/activity/suggestions':
679
+ if (req.method === 'GET') return this.json(res, routes.activity.getWorkstreamSuggestions(this.manager));
680
+ break;
681
+
682
+ case '/api/smart-sync/status':
683
+ if (req.method === 'GET') return this.json(res, routes.smartSync.getSmartSyncStatus(this.manager));
684
+ break;
685
+
686
+ case '/api/smart-sync/detect':
687
+ if (req.method === 'POST') return this.json(res, routes.smartSync.smartSyncDetect(this.manager, body.projects));
688
+ break;
689
+
690
+ case '/api/smart-sync/nudge':
691
+ if (req.method === 'POST') return this.json(res, routes.smartSync.smartSyncCheckNudge(this.manager, body.projects));
692
+ break;
693
+
694
+ case '/api/smart-sync/action':
695
+ if (req.method === 'POST') return this.json(res, routes.smartSync.smartSyncHandleAction(this.manager, body.nudgeKey, body.action, body.context));
696
+ break;
697
+
698
+ case '/api/smart-sync/settings':
699
+ if (req.method === 'PUT') return this.json(res, routes.smartSync.smartSyncUpdateSettings(this.manager, body));
700
+ break;
701
+
702
+ case '/api/smart-sync/remember':
703
+ if (req.method === 'POST') return this.json(res, routes.smartSync.smartSyncRememberChoice(this.manager, body.projectPath, body.workstreamId, body.choice));
704
+ break;
705
+ }
706
+
707
+ // Dynamic routes
708
+ if (pathname.startsWith('/api/workstreams/') && !pathname.includes('/active') && !pathname.includes('/inject') && !pathname.includes('/detect')) {
709
+ const parts = pathname.split('/');
710
+ const workstreamId = parts[3];
711
+ const action = parts[4];
712
+
713
+ if (workstreamId) {
714
+ if (req.method === 'PUT' && !action) return this.json(res, routes.workstreams.updateWorkstream(this.manager, workstreamId, body));
715
+ if (req.method === 'DELETE' && !action) return this.json(res, routes.workstreams.deleteWorkstream(this.manager, workstreamId));
716
+ if (req.method === 'POST' && action === 'add-project') return this.json(res, routes.workstreams.addProjectToWorkstream(this.manager, workstreamId, body.projectPath));
717
+ if (req.method === 'POST' && action === 'remove-project') return this.json(res, routes.workstreams.removeProjectFromWorkstream(this.manager, workstreamId, body.projectPath));
718
+ }
719
+ }
720
+
721
+ if (pathname.startsWith('/api/projects/') && req.method === 'DELETE') {
722
+ const projectId = pathname.split('/').pop();
723
+ if (projectId && projectId !== 'active') {
724
+ return this.json(res, routes.projects.removeProject(this.manager, projectId));
725
+ }
726
+ }
727
+
728
+ res.writeHead(404);
729
+ res.end(JSON.stringify({ error: 'Not found' }));
730
+ }
731
+
732
+ json(res, data) {
733
+ res.writeHead(200);
734
+ res.end(JSON.stringify(data, null, 2));
735
+ }
736
+
737
+ parseBody(req) {
738
+ return new Promise((resolve, reject) => {
739
+ let body = '';
740
+ req.on('data', chunk => body += chunk);
741
+ req.on('end', () => {
742
+ try { resolve(JSON.parse(body || '{}')); }
743
+ catch (e) { resolve({}); }
744
+ });
745
+ req.on('error', reject);
746
+ });
747
+ }
748
+ }
749
+
750
+ // CLI
751
+ if (require.main === module) {
752
+ const args = process.argv.slice(2);
753
+ let port = 3333;
754
+ let dir = null;
755
+
756
+ for (let i = 0; i < args.length; i++) {
757
+ const arg = args[i];
758
+ if (arg.startsWith('--port=')) port = parseInt(arg.split('=')[1]) || 3333;
759
+ else if (arg === '--port' || arg === '-p') port = parseInt(args[++i]) || 3333;
760
+ else if (arg.startsWith('--dir=')) dir = arg.split('=')[1] || null;
761
+ else if (arg === '--dir' || arg === '-d') dir = args[++i] || null;
762
+ else if (!arg.startsWith('-') && fs.existsSync(arg) && fs.statSync(arg).isDirectory()) dir = arg;
763
+ }
764
+
765
+ dir = dir || process.cwd();
766
+
767
+ const ClaudeConfigManager = require('../config-loader.js');
768
+ const manager = new ClaudeConfigManager();
769
+ const server = new ConfigUIServer(port, dir, manager);
770
+ server.start();
771
+ }
772
+
773
+ module.exports = ConfigUIServer;