@hileeon/mcc 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 (138) hide show
  1. package/.claude/CLAUDE.md +204 -0
  2. package/.claude/agents/.gitkeep +0 -0
  3. package/.claude/settings.json +9 -0
  4. package/.claude/skills/.gitkeep +0 -0
  5. package/README.md +127 -0
  6. package/dist/accounts/instance-manager.d.ts +11 -0
  7. package/dist/accounts/instance-manager.d.ts.map +1 -0
  8. package/dist/accounts/instance-manager.js +89 -0
  9. package/dist/accounts/instance-manager.js.map +1 -0
  10. package/dist/accounts/shared-manager.d.ts +25 -0
  11. package/dist/accounts/shared-manager.d.ts.map +1 -0
  12. package/dist/accounts/shared-manager.js +186 -0
  13. package/dist/accounts/shared-manager.js.map +1 -0
  14. package/dist/accounts/store.d.ts +30 -0
  15. package/dist/accounts/store.d.ts.map +1 -0
  16. package/dist/accounts/store.js +128 -0
  17. package/dist/accounts/store.js.map +1 -0
  18. package/dist/core/model-router.d.ts +30 -0
  19. package/dist/core/model-router.d.ts.map +1 -0
  20. package/dist/core/model-router.js +64 -0
  21. package/dist/core/model-router.js.map +1 -0
  22. package/dist/dashboard-server.d.ts +5 -0
  23. package/dist/dashboard-server.d.ts.map +1 -0
  24. package/dist/dashboard-server.js +387 -0
  25. package/dist/dashboard-server.js.map +1 -0
  26. package/dist/mcc.d.ts +8 -0
  27. package/dist/mcc.d.ts.map +1 -0
  28. package/dist/mcc.js +474 -0
  29. package/dist/mcc.js.map +1 -0
  30. package/dist/mcp/external-registry.d.ts +24 -0
  31. package/dist/mcp/external-registry.d.ts.map +1 -0
  32. package/dist/mcp/external-registry.js +99 -0
  33. package/dist/mcp/external-registry.js.map +1 -0
  34. package/dist/mcp/installer.d.ts +31 -0
  35. package/dist/mcp/installer.d.ts.map +1 -0
  36. package/dist/mcp/installer.js +273 -0
  37. package/dist/mcp/installer.js.map +1 -0
  38. package/dist/mcp/mcp-config.d.ts +86 -0
  39. package/dist/mcp/mcp-config.d.ts.map +1 -0
  40. package/dist/mcp/mcp-config.js +178 -0
  41. package/dist/mcp/mcp-config.js.map +1 -0
  42. package/dist/mcp/registry.d.ts +23 -0
  43. package/dist/mcp/registry.d.ts.map +1 -0
  44. package/dist/mcp/registry.js +100 -0
  45. package/dist/mcp/registry.js.map +1 -0
  46. package/dist/proxy/proxy-daemon.d.ts +27 -0
  47. package/dist/proxy/proxy-daemon.d.ts.map +1 -0
  48. package/dist/proxy/proxy-daemon.js +192 -0
  49. package/dist/proxy/proxy-daemon.js.map +1 -0
  50. package/dist/proxy/proxy-entry.d.ts +11 -0
  51. package/dist/proxy/proxy-entry.d.ts.map +1 -0
  52. package/dist/proxy/proxy-entry.js +74 -0
  53. package/dist/proxy/proxy-entry.js.map +1 -0
  54. package/dist/proxy/proxy-paths.d.ts +27 -0
  55. package/dist/proxy/proxy-paths.d.ts.map +1 -0
  56. package/dist/proxy/proxy-paths.js +125 -0
  57. package/dist/proxy/proxy-paths.js.map +1 -0
  58. package/dist/proxy/proxy-server.d.ts +20 -0
  59. package/dist/proxy/proxy-server.d.ts.map +1 -0
  60. package/dist/proxy/proxy-server.js +280 -0
  61. package/dist/proxy/proxy-server.js.map +1 -0
  62. package/dist/proxy/upstream-url.d.ts +7 -0
  63. package/dist/proxy/upstream-url.d.ts.map +1 -0
  64. package/dist/proxy/upstream-url.js +38 -0
  65. package/dist/proxy/upstream-url.js.map +1 -0
  66. package/dist/shared/logger.d.ts +23 -0
  67. package/dist/shared/logger.d.ts.map +1 -0
  68. package/dist/shared/logger.js +184 -0
  69. package/dist/shared/logger.js.map +1 -0
  70. package/dist/shared/provider-preset-catalog.d.ts +41 -0
  71. package/dist/shared/provider-preset-catalog.d.ts.map +1 -0
  72. package/dist/shared/provider-preset-catalog.js +299 -0
  73. package/dist/shared/provider-preset-catalog.js.map +1 -0
  74. package/docs/decisions.md +33 -0
  75. package/docs/lessons.md +8 -0
  76. package/docs/product.md +37 -0
  77. package/lib/mcp/mcc-image-analysis-server.cjs +454 -0
  78. package/lib/mcp/mcc-websearch-server.cjs +339 -0
  79. package/lib/mcp-hooks/image-analysis-runtime.cjs +510 -0
  80. package/lib/mcp-hooks/image-analyzer-transformer.cjs +526 -0
  81. package/lib/mcp-hooks/websearch-transformer.cjs +1421 -0
  82. package/lib/proxy/config/config-loader-facade.js +24 -0
  83. package/lib/proxy/glmt/delta-accumulator.js +363 -0
  84. package/lib/proxy/glmt/glmt-transformer.js +204 -0
  85. package/lib/proxy/glmt/index.js +41 -0
  86. package/lib/proxy/glmt/locale-enforcer.js +69 -0
  87. package/lib/proxy/glmt/pipeline/content-transformer.js +162 -0
  88. package/lib/proxy/glmt/pipeline/index.js +20 -0
  89. package/lib/proxy/glmt/pipeline/request-transformer.js +116 -0
  90. package/lib/proxy/glmt/pipeline/response-builder.js +205 -0
  91. package/lib/proxy/glmt/pipeline/stream-parser.js +234 -0
  92. package/lib/proxy/glmt/pipeline/tool-call-handler.js +78 -0
  93. package/lib/proxy/glmt/pipeline/types.js +6 -0
  94. package/lib/proxy/glmt/reasoning-enforcer.js +151 -0
  95. package/lib/proxy/glmt/sse-parser.js +102 -0
  96. package/lib/proxy/services/logging.js +13 -0
  97. package/lib/proxy/transformers/request-transformer.js +452 -0
  98. package/lib/proxy/transformers/sse-stream-transformer.js +199 -0
  99. package/lib/shared/logger.cjs +138 -0
  100. package/package.json +35 -0
  101. package/src/accounts/instance-manager.ts +58 -0
  102. package/src/accounts/shared-manager.ts +154 -0
  103. package/src/accounts/store.ts +111 -0
  104. package/src/core/model-router.ts +82 -0
  105. package/src/dashboard-server.ts +407 -0
  106. package/src/mcc.ts +474 -0
  107. package/src/mcp/external-registry.ts +73 -0
  108. package/src/mcp/installer.ts +258 -0
  109. package/src/mcp/mcp-config.ts +168 -0
  110. package/src/mcp/registry.ts +89 -0
  111. package/src/proxy/proxy-daemon.ts +184 -0
  112. package/src/proxy/proxy-entry.ts +63 -0
  113. package/src/proxy/proxy-paths.ts +97 -0
  114. package/src/proxy/proxy-server.ts +278 -0
  115. package/src/proxy/upstream-url.ts +38 -0
  116. package/src/shared/logger.ts +140 -0
  117. package/src/shared/provider-preset-catalog.ts +340 -0
  118. package/tsconfig.json +33 -0
  119. package/ui/.prettierrc +9 -0
  120. package/ui/index.html +12 -0
  121. package/ui/package.json +33 -0
  122. package/ui/postcss.config.js +6 -0
  123. package/ui/src/App.tsx +753 -0
  124. package/ui/src/components/ui/button.tsx +48 -0
  125. package/ui/src/components/ui/card.tsx +50 -0
  126. package/ui/src/components/ui/input.tsx +21 -0
  127. package/ui/src/components/ui/label.tsx +20 -0
  128. package/ui/src/components/ui/select.tsx +80 -0
  129. package/ui/src/components/ui/switch.tsx +26 -0
  130. package/ui/src/components/ui/tabs.tsx +52 -0
  131. package/ui/src/index.css +33 -0
  132. package/ui/src/lib/api.ts +185 -0
  133. package/ui/src/lib/utils.ts +6 -0
  134. package/ui/src/main.tsx +10 -0
  135. package/ui/src/vite-env.d.ts +1 -0
  136. package/ui/tailwind.config.js +49 -0
  137. package/ui/tsconfig.json +25 -0
  138. package/ui/vite.config.ts +20 -0
@@ -0,0 +1,407 @@
1
+ /**
2
+ * Dashboard Server - Express API + static file server
3
+ */
4
+
5
+ import express from 'express';
6
+ import cors from 'cors';
7
+ import * as path from 'path';
8
+ import * as fs from 'fs';
9
+ import { spawn } from 'child_process';
10
+ import type { Profile } from './accounts/store';
11
+ import { BUILTIN_MCP_SERVERS, getAllServers, type McpRegistryEntry } from './mcp/registry';
12
+ import {
13
+ readMcpConfig,
14
+ writeMcpConfig,
15
+ getProviderPresets,
16
+ type McpConfig,
17
+ } from './mcp/mcp-config';
18
+ import {
19
+ readExternalMcpRegistry,
20
+ addExternalMcpServer,
21
+ removeExternalMcpServer,
22
+ type ExternalMcpServer,
23
+ } from './mcp/external-registry';
24
+ import {
25
+ enableInstanceExternalMcp,
26
+ disableInstanceExternalMcp,
27
+ readInstanceExternalEnabled,
28
+ } from './mcp/installer';
29
+ import { MCCInstanceManager } from './accounts/instance-manager';
30
+
31
+ const PORT = 3000;
32
+ const DIST_DIR = path.join(__dirname, '..', 'ui', 'dist');
33
+
34
+ async function importModule<T>(modulePath: string, fn: string): Promise<T> {
35
+ const mod = await import(modulePath);
36
+ return (mod as Record<string, T>)[fn] as T;
37
+ }
38
+
39
+ async function listProfiles() {
40
+ const fn = await importModule<() => Profile[]>('./accounts/store', 'listProfiles');
41
+ return fn();
42
+ }
43
+
44
+ async function saveProfile(profile: Profile, apiKey: string) {
45
+ const fn = await importModule<(profile: Profile, apiKey: string) => void>(
46
+ './accounts/store',
47
+ 'saveProfile'
48
+ );
49
+ return fn(profile, apiKey);
50
+ }
51
+
52
+ async function deleteProfile(name: string) {
53
+ const fn = await importModule<(name: string) => void>('./accounts/store', 'deleteProfile');
54
+ return fn(name);
55
+ }
56
+
57
+ async function setDefaultProfile(name: string) {
58
+ const fn = await importModule<(name: string) => void>('./accounts/store', 'setDefaultProfile');
59
+ return fn(name);
60
+ }
61
+
62
+ async function getDefaultProfile() {
63
+ const fn = await importModule<() => string | undefined>('./accounts/store', 'getDefaultProfile');
64
+ return fn();
65
+ }
66
+
67
+ function openBrowser(url: string) {
68
+ const isWindows = process.platform === 'win32';
69
+ if (isWindows) {
70
+ spawn('cmd', ['/c', 'start', '""', url], { detached: true, stdio: 'ignore' }).unref();
71
+ } else {
72
+ spawn('open', [url], { detached: true, stdio: 'ignore' }).unref();
73
+ }
74
+ }
75
+
76
+ async function main() {
77
+ const app = express();
78
+
79
+ app.use(cors());
80
+ app.use(express.json());
81
+
82
+ if (fs.existsSync(DIST_DIR)) {
83
+ app.use(express.static(DIST_DIR));
84
+ }
85
+
86
+ // GET /api/profiles
87
+ app.get('/api/profiles', async (_req, res) => {
88
+ try {
89
+ res.json(await listProfiles());
90
+ } catch (e) {
91
+ res.status(500).json({ error: (e as Error).message });
92
+ }
93
+ });
94
+
95
+ // POST /api/profiles
96
+ app.post('/api/profiles', async (req, res) => {
97
+ try {
98
+ const { name, baseUrl, apiKey, model, opusModel, sonnetModel, haikuModel, protocol } = req.body as {
99
+ name: string;
100
+ baseUrl: string;
101
+ apiKey: string;
102
+ model: string;
103
+ opusModel?: string;
104
+ sonnetModel?: string;
105
+ haikuModel?: string;
106
+ protocol?: 'anthropic' | 'openai';
107
+ };
108
+ if (!name || !baseUrl || !apiKey || !model) {
109
+ res.status(400).json({ error: 'Missing required fields' });
110
+ return;
111
+ }
112
+ const profile: Profile = { name, baseUrl, model, opusModel, sonnetModel, haikuModel, protocol: protocol || 'anthropic', createdAt: new Date().toISOString() };
113
+ await saveProfile(profile, apiKey);
114
+ console.log(`[i] Profile created: ${name} (model: ${model}, protocol: ${protocol || 'anthropic'})`);
115
+ res.json({ ok: true });
116
+ } catch (e) {
117
+ res.status(500).json({ error: (e as Error).message });
118
+ }
119
+ });
120
+
121
+ // PUT /api/profiles/:name — update profile
122
+ app.put('/api/profiles/:name', async (req, res) => {
123
+ try {
124
+ const { baseUrl, apiKey, model, opusModel, sonnetModel, haikuModel, protocol } = req.body as {
125
+ baseUrl?: string;
126
+ apiKey?: string;
127
+ model?: string;
128
+ opusModel?: string;
129
+ sonnetModel?: string;
130
+ haikuModel?: string;
131
+ protocol?: 'anthropic' | 'openai';
132
+ };
133
+ const profileName = req.params.name;
134
+ const getProfileApiKey = await importModule<(name: string) => string | undefined>(
135
+ './accounts/store',
136
+ 'getProfileApiKey'
137
+ );
138
+ const existingKey = getProfileApiKey(profileName);
139
+ const profiles = await listProfiles();
140
+ const existing = profiles.find((p) => p.name === profileName);
141
+ if (!existing) {
142
+ res.status(404).json({ error: 'Profile not found' });
143
+ return;
144
+ }
145
+ const updated: Profile = {
146
+ ...existing,
147
+ baseUrl: baseUrl ?? existing.baseUrl,
148
+ model: model ?? existing.model,
149
+ opusModel: opusModel !== undefined ? (opusModel || undefined) : existing.opusModel,
150
+ sonnetModel: sonnetModel !== undefined ? (sonnetModel || undefined) : existing.sonnetModel,
151
+ haikuModel: haikuModel !== undefined ? (haikuModel || undefined) : existing.haikuModel,
152
+ protocol: protocol ?? existing.protocol,
153
+ };
154
+ // Only update API key if a new one is provided
155
+ await saveProfile(updated, apiKey ?? existingKey ?? '');
156
+ console.log(`[i] Profile updated: ${profileName} (model: ${updated.model}, protocol: ${updated.protocol || 'anthropic'})`);
157
+ res.json({ ok: true });
158
+ } catch (e) {
159
+ res.status(500).json({ error: (e as Error).message });
160
+ }
161
+ });
162
+
163
+ // DELETE /api/profiles/:name
164
+ app.delete('/api/profiles/:name', async (req, res) => {
165
+ try {
166
+ const name = req.params.name;
167
+ await deleteProfile(name);
168
+ console.log(`[i] Profile deleted: ${name}`);
169
+ res.json({ ok: true });
170
+ } catch (e) {
171
+ res.status(500).json({ error: (e as Error).message });
172
+ }
173
+ });
174
+
175
+ // PUT /api/profiles/:name/default
176
+ app.put('/api/profiles/:name/default', async (req, res) => {
177
+ try {
178
+ await setDefaultProfile(req.params.name);
179
+ res.json({ ok: true });
180
+ } catch (e) {
181
+ res.status(500).json({ error: (e as Error).message });
182
+ }
183
+ });
184
+
185
+ // GET /api/ping - connection health check
186
+ app.get('/api/ping', (_req, res) => {
187
+ res.json({ ok: true });
188
+ });
189
+
190
+ // GET /api/status
191
+ app.get('/api/status', async (_req, res) => {
192
+ try {
193
+ const defaultProfile = await getDefaultProfile();
194
+ let currentProfile = defaultProfile;
195
+ res.json({ currentProfile });
196
+ } catch (e) {
197
+ res.status(500).json({ error: (e as Error).message });
198
+ }
199
+ });
200
+
201
+ // GET /api/mcp
202
+ app.get('/api/mcp', (_req, res) => {
203
+ const servers = BUILTIN_MCP_SERVERS.map((s) => ({
204
+ name: s.name,
205
+ displayName: s.displayName,
206
+ description: s.description,
207
+ enabled: s.enabledByDefault,
208
+ }));
209
+ res.json(servers);
210
+ });
211
+
212
+ // PUT /api/mcp/:name/:action
213
+ app.put('/api/mcp/:name/:action', (req, res) => {
214
+ const { name, action } = req.params;
215
+ if (action !== 'enable' && action !== 'disable') {
216
+ res.status(400).json({ error: `Invalid action: ${action}` });
217
+ return;
218
+ }
219
+ // Built-in servers: no per-instance tracking yet
220
+ const builtin = BUILTIN_MCP_SERVERS.find((s) => s.name === name);
221
+ if (builtin) {
222
+ res.json({ ok: true });
223
+ return;
224
+ }
225
+ // External servers: require instance param
226
+ const instanceName = req.query.instance as string | undefined;
227
+ if (!instanceName) {
228
+ res.status(400).json({ error: 'instance query param required for external MCPs' });
229
+ return;
230
+ }
231
+ const instanceMgr = new MCCInstanceManager();
232
+ const instancePath = instanceMgr.getInstancePath(instanceName);
233
+ if (action === 'enable') {
234
+ enableInstanceExternalMcp(instancePath, name);
235
+ } else {
236
+ disableInstanceExternalMcp(instancePath, name);
237
+ }
238
+ res.json({ ok: true });
239
+ });
240
+
241
+ // GET /api/mcp/all - all servers (built-in + external) with enabled state
242
+ app.get('/api/mcp/all', (req, res) => {
243
+ try {
244
+ const instanceName = req.query.instance as string | undefined;
245
+ const instanceMgr = new MCCInstanceManager();
246
+ let instanceExternalEnabled: string[] = [];
247
+ if (instanceName) {
248
+ const instancePath = instanceMgr.getInstancePath(instanceName);
249
+ instanceExternalEnabled = readInstanceExternalEnabled(instancePath);
250
+ }
251
+ const servers = getAllServers();
252
+ const result = servers.map((s: McpRegistryEntry | ExternalMcpServer) => {
253
+ const isBuiltin = 'config' in s;
254
+ const isEnabled = isBuiltin
255
+ ? s.enabledByDefault
256
+ : instanceExternalEnabled.includes(s.name);
257
+ return {
258
+ name: s.name,
259
+ displayName: s.displayName,
260
+ description: s.description,
261
+ builtin: isBuiltin,
262
+ enabledByDefault: isBuiltin ? s.enabledByDefault : (s as ExternalMcpServer).enabledByDefault,
263
+ enabled: isEnabled,
264
+ };
265
+ });
266
+ res.json(result);
267
+ } catch (e) {
268
+ res.status(500).json({ error: (e as Error).message });
269
+ }
270
+ });
271
+
272
+ // GET /api/mcp/external - list external MCP servers
273
+ app.get('/api/mcp/external', (_req, res) => {
274
+ try {
275
+ res.json(readExternalMcpRegistry());
276
+ } catch (e) {
277
+ res.status(500).json({ error: (e as Error).message });
278
+ }
279
+ });
280
+
281
+ // POST /api/mcp/external - add external MCP server
282
+ app.post('/api/mcp/external', (req, res) => {
283
+ try {
284
+ const server = req.body as ExternalMcpServer;
285
+ if (!server.name || !server.command || !server.args) {
286
+ res.status(400).json({ error: 'name, command, and args are required' });
287
+ return;
288
+ }
289
+ addExternalMcpServer(server);
290
+ res.json({ ok: true });
291
+ } catch (e) {
292
+ res.status(500).json({ error: (e as Error).message });
293
+ }
294
+ });
295
+
296
+ // DELETE /api/mcp/external/:name - remove external MCP server
297
+ app.delete('/api/mcp/external/:name', (req, res) => {
298
+ try {
299
+ removeExternalMcpServer(req.params.name);
300
+ res.json({ ok: true });
301
+ } catch (e) {
302
+ res.status(500).json({ error: (e as Error).message });
303
+ }
304
+ });
305
+
306
+ // GET /api/mcp-config
307
+ app.get('/api/mcp-config', (_req, res) => {
308
+ try {
309
+ res.json(readMcpConfig());
310
+ } catch (e) {
311
+ res.status(500).json({ error: (e as Error).message });
312
+ }
313
+ });
314
+
315
+ // PUT /api/mcp-config
316
+ app.put('/api/mcp-config', (req, res) => {
317
+ try {
318
+ const newConfig = req.body as McpConfig;
319
+ if (!newConfig || !newConfig.websearch || !newConfig.imageAnalysis) {
320
+ res.status(400).json({ error: 'Invalid MCP config' });
321
+ return;
322
+ }
323
+
324
+ const oldConfig = readMcpConfig();
325
+ const changes: string[] = [];
326
+
327
+ // Section-level toggles
328
+ if (oldConfig.websearch.enabled !== newConfig.websearch.enabled) {
329
+ changes.push(`websearch ${newConfig.websearch.enabled ? 'enabled' : 'disabled'}`);
330
+ }
331
+ if (oldConfig.imageAnalysis.enabled !== newConfig.imageAnalysis.enabled) {
332
+ changes.push(`imageAnalysis ${newConfig.imageAnalysis.enabled ? 'enabled' : 'disabled'}`);
333
+ }
334
+
335
+ // WebSearch provider changes
336
+ for (const [id, np] of Object.entries(newConfig.websearch.providers)) {
337
+ const op = oldConfig.websearch.providers[id];
338
+ if (!op) continue;
339
+ if (op.enabled !== np.enabled) {
340
+ changes.push(`websearch.${id} ${np.enabled ? 'on' : 'off'}`);
341
+ }
342
+ if (op.apiKey !== np.apiKey) {
343
+ changes.push(np.apiKey ? `websearch.${id} apiKey updated` : `websearch.${id} apiKey cleared`);
344
+ }
345
+ }
346
+
347
+ // ImageAnalysis provider changes
348
+ for (const [id, np] of Object.entries(newConfig.imageAnalysis.providers)) {
349
+ const op = oldConfig.imageAnalysis.providers[id];
350
+ if (!op) continue;
351
+ if (op.enabled !== np.enabled) {
352
+ changes.push(`imageAnalysis.${id} ${np.enabled ? 'on' : 'off'}`);
353
+ }
354
+ if (op.apiKey !== np.apiKey) {
355
+ changes.push(np.apiKey ? `imageAnalysis.${id} apiKey updated` : `imageAnalysis.${id} apiKey cleared`);
356
+ }
357
+ if (op.model !== np.model) {
358
+ changes.push(`imageAnalysis.${id} model=${np.model}`);
359
+ }
360
+ if (op.baseUrl !== np.baseUrl) {
361
+ changes.push(`imageAnalysis.${id} endpoint updated`);
362
+ }
363
+ }
364
+
365
+ writeMcpConfig(newConfig);
366
+
367
+ if (changes.length > 0) {
368
+ console.log(`[i] MCP config updated: ${changes.join('; ')}`);
369
+ } else {
370
+ console.log('[i] MCP config saved (no changes detected)');
371
+ }
372
+
373
+ res.json({ ok: true });
374
+ } catch (e) {
375
+ res.status(500).json({ error: (e as Error).message });
376
+ }
377
+ });
378
+
379
+ // GET /api/mcp-config/presets
380
+ app.get('/api/mcp-config/presets', (_req, res) => {
381
+ res.json(getProviderPresets());
382
+ });
383
+
384
+ app.get('*', (_req, res) => {
385
+ const indexPath = path.join(DIST_DIR, 'index.html');
386
+ if (fs.existsSync(indexPath)) {
387
+ res.sendFile(indexPath);
388
+ } else {
389
+ res.status(404).send('Dashboard not built. Run: npm run build:ui');
390
+ }
391
+ });
392
+
393
+ app.listen(PORT, () => {
394
+ console.log(`[OK] MCC Dashboard: http://localhost:${PORT}`);
395
+ openBrowser(`http://localhost:${PORT}`);
396
+ });
397
+ }
398
+
399
+ process.on('SIGINT', () => {
400
+ console.log('\n[i] Dashboard shutting down...');
401
+ process.exit(0);
402
+ });
403
+
404
+ main().catch((err) => {
405
+ console.error(`[!] Dashboard server error: ${err.message}`);
406
+ process.exit(1);
407
+ });