aidex-mcp 1.4.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 (76) hide show
  1. package/CHANGELOG.md +128 -0
  2. package/LICENSE +21 -0
  3. package/MCP-API-REFERENCE.md +690 -0
  4. package/README.md +314 -0
  5. package/build/commands/files.d.ts +28 -0
  6. package/build/commands/files.js +124 -0
  7. package/build/commands/index.d.ts +14 -0
  8. package/build/commands/index.js +14 -0
  9. package/build/commands/init.d.ts +24 -0
  10. package/build/commands/init.js +396 -0
  11. package/build/commands/link.d.ts +45 -0
  12. package/build/commands/link.js +167 -0
  13. package/build/commands/note.d.ts +29 -0
  14. package/build/commands/note.js +105 -0
  15. package/build/commands/query.d.ts +36 -0
  16. package/build/commands/query.js +176 -0
  17. package/build/commands/scan.d.ts +25 -0
  18. package/build/commands/scan.js +104 -0
  19. package/build/commands/session.d.ts +52 -0
  20. package/build/commands/session.js +216 -0
  21. package/build/commands/signature.d.ts +52 -0
  22. package/build/commands/signature.js +171 -0
  23. package/build/commands/summary.d.ts +56 -0
  24. package/build/commands/summary.js +324 -0
  25. package/build/commands/update.d.ts +36 -0
  26. package/build/commands/update.js +273 -0
  27. package/build/constants.d.ts +10 -0
  28. package/build/constants.js +10 -0
  29. package/build/db/database.d.ts +69 -0
  30. package/build/db/database.js +126 -0
  31. package/build/db/index.d.ts +7 -0
  32. package/build/db/index.js +6 -0
  33. package/build/db/queries.d.ts +163 -0
  34. package/build/db/queries.js +273 -0
  35. package/build/db/schema.sql +136 -0
  36. package/build/index.d.ts +13 -0
  37. package/build/index.js +74 -0
  38. package/build/parser/extractor.d.ts +41 -0
  39. package/build/parser/extractor.js +249 -0
  40. package/build/parser/index.d.ts +7 -0
  41. package/build/parser/index.js +7 -0
  42. package/build/parser/languages/c.d.ts +28 -0
  43. package/build/parser/languages/c.js +70 -0
  44. package/build/parser/languages/cpp.d.ts +28 -0
  45. package/build/parser/languages/cpp.js +91 -0
  46. package/build/parser/languages/csharp.d.ts +32 -0
  47. package/build/parser/languages/csharp.js +97 -0
  48. package/build/parser/languages/go.d.ts +28 -0
  49. package/build/parser/languages/go.js +83 -0
  50. package/build/parser/languages/index.d.ts +21 -0
  51. package/build/parser/languages/index.js +107 -0
  52. package/build/parser/languages/java.d.ts +28 -0
  53. package/build/parser/languages/java.js +58 -0
  54. package/build/parser/languages/php.d.ts +28 -0
  55. package/build/parser/languages/php.js +75 -0
  56. package/build/parser/languages/python.d.ts +28 -0
  57. package/build/parser/languages/python.js +67 -0
  58. package/build/parser/languages/ruby.d.ts +28 -0
  59. package/build/parser/languages/ruby.js +68 -0
  60. package/build/parser/languages/rust.d.ts +28 -0
  61. package/build/parser/languages/rust.js +73 -0
  62. package/build/parser/languages/typescript.d.ts +28 -0
  63. package/build/parser/languages/typescript.js +82 -0
  64. package/build/parser/tree-sitter.d.ts +30 -0
  65. package/build/parser/tree-sitter.js +132 -0
  66. package/build/server/mcp-server.d.ts +7 -0
  67. package/build/server/mcp-server.js +36 -0
  68. package/build/server/tools.d.ts +18 -0
  69. package/build/server/tools.js +1245 -0
  70. package/build/viewer/git-status.d.ts +25 -0
  71. package/build/viewer/git-status.js +163 -0
  72. package/build/viewer/index.d.ts +5 -0
  73. package/build/viewer/index.js +5 -0
  74. package/build/viewer/server.d.ts +12 -0
  75. package/build/viewer/server.js +1122 -0
  76. package/package.json +66 -0
@@ -0,0 +1,1122 @@
1
+ /**
2
+ * AiDex Viewer - Local HTTP Server with WebSocket
3
+ * Opens an interactive project tree in the browser
4
+ *
5
+ * Features:
6
+ * - Tab-based navigation (Code/All files, Overview/Code view)
7
+ * - Session change indicators (modified/new files)
8
+ * - Syntax highlighting with highlight.js
9
+ */
10
+ import express from 'express';
11
+ import { createServer } from 'http';
12
+ import { WebSocketServer, WebSocket } from 'ws';
13
+ import { exec } from 'child_process';
14
+ import path from 'path';
15
+ import { existsSync, readFileSync } from 'fs';
16
+ import chokidar from 'chokidar';
17
+ import { openDatabase, createQueries } from '../db/index.js';
18
+ import { update as updateIndex } from '../commands/update.js';
19
+ import { getGitStatus } from './git-status.js';
20
+ import { PRODUCT_NAME, INDEX_DIR } from '../constants.js';
21
+ const PORT = 3333;
22
+ let server = null;
23
+ let wss = null;
24
+ let fileWatcher = null;
25
+ export async function startViewer(projectPath) {
26
+ // Check if already running
27
+ if (server) {
28
+ return `Viewer already running at http://localhost:${PORT}`;
29
+ }
30
+ const dbPath = path.join(projectPath, INDEX_DIR, 'index.db');
31
+ const db = openDatabase(dbPath, true); // readonly for queries
32
+ const sqlite = db.getDb();
33
+ const queries = createQueries(db);
34
+ const projectRoot = path.resolve(projectPath);
35
+ const absoluteProjectPath = path.resolve(projectPath); // For updateIndex
36
+ // Track files changed - initialize with DB session changes, then add live changes
37
+ const dbSessionChanges = detectSessionChanges(sqlite);
38
+ const viewerSessionChanges = {
39
+ modified: new Set(dbSessionChanges.modified),
40
+ new: new Set(dbSessionChanges.new)
41
+ };
42
+ console.error('[Viewer] Session changes from DB:', viewerSessionChanges.modified.size, 'modified,', viewerSessionChanges.new.size, 'new');
43
+ // Git status - fetch once at startup, refresh on file changes
44
+ let cachedGitInfo;
45
+ const refreshGitStatus = async () => {
46
+ cachedGitInfo = await getGitStatus(projectPath);
47
+ console.error('[Viewer] Git status:', cachedGitInfo.isGitRepo ? 'repo' : 'no-repo', cachedGitInfo.hasRemote ? 'with-remote' : 'no-remote', cachedGitInfo.fileStatuses.size, 'files with status');
48
+ };
49
+ await refreshGitStatus();
50
+ const app = express();
51
+ server = createServer(app);
52
+ wss = new WebSocketServer({ server });
53
+ // File watcher for live reload
54
+ let debounceTimer = null;
55
+ const pendingChanges = new Set(); // Files changed since last broadcast
56
+ const broadcastTreeUpdate = async () => {
57
+ if (!wss)
58
+ return;
59
+ // Re-index changed files before refreshing the tree
60
+ if (pendingChanges.size > 0) {
61
+ console.error('[Viewer] Re-indexing', pendingChanges.size, 'changed file(s)');
62
+ for (const changedFile of pendingChanges) {
63
+ // Convert absolute path to relative path
64
+ const relativePath = path.relative(projectRoot, changedFile).replace(/\\/g, '/');
65
+ try {
66
+ // updateIndex opens its own DB connection with write access
67
+ const result = updateIndex({ path: absoluteProjectPath, file: relativePath });
68
+ console.error('[Viewer] Re-indexed:', relativePath, result.success ? '✓' : '✗');
69
+ // Track as modified in viewer session
70
+ viewerSessionChanges.modified.add(relativePath);
71
+ }
72
+ catch (err) {
73
+ console.error('[Viewer] Failed to re-index:', relativePath, err);
74
+ }
75
+ }
76
+ pendingChanges.clear();
77
+ }
78
+ // Refresh git status on file changes
79
+ await refreshGitStatus();
80
+ // Build fresh trees for both modes using viewer session tracking
81
+ const freshDb = openDatabase(dbPath, true);
82
+ const codeTree = await buildTree(freshDb.getDb(), projectPath, 'code', viewerSessionChanges, cachedGitInfo);
83
+ const allTree = await buildTree(freshDb.getDb(), projectPath, 'all', viewerSessionChanges, cachedGitInfo);
84
+ freshDb.close();
85
+ // Broadcast to all connected clients
86
+ wss.clients.forEach((client) => {
87
+ if (client.readyState === WebSocket.OPEN) {
88
+ client.send(JSON.stringify({ type: 'refresh', codeTree, allTree }));
89
+ }
90
+ });
91
+ console.error('[Viewer] Broadcast tree update to', wss.clients.size, 'clients');
92
+ };
93
+ // Use chokidar for reliable cross-platform file watching
94
+ fileWatcher = chokidar.watch(projectRoot, {
95
+ ignored: [
96
+ '**/node_modules/**',
97
+ '**/.git/**',
98
+ `**/${INDEX_DIR}/**`,
99
+ '**/build/**',
100
+ '**/dist/**'
101
+ ],
102
+ ignoreInitial: true,
103
+ persistent: true
104
+ });
105
+ fileWatcher.on('ready', () => {
106
+ console.error('[Viewer] Chokidar ready, watching for changes');
107
+ });
108
+ fileWatcher.on('error', (error) => {
109
+ console.error('[Viewer] Chokidar error:', error);
110
+ });
111
+ fileWatcher.on('all', (event, filePath) => {
112
+ console.error('[Viewer] Chokidar event:', event, filePath);
113
+ // Track changed files for re-indexing (only for change/add events on code files)
114
+ if ((event === 'change' || event === 'add') && /\.(ts|tsx|js|jsx|cs|rs|py|c|cpp|h|hpp|java|go|php|rb)$/i.test(filePath)) {
115
+ pendingChanges.add(filePath);
116
+ }
117
+ // Debounce: wait 500ms after last change before broadcasting
118
+ if (debounceTimer) {
119
+ clearTimeout(debounceTimer);
120
+ }
121
+ debounceTimer = setTimeout(() => {
122
+ console.error('[Viewer] Broadcasting after debounce');
123
+ broadcastTreeUpdate();
124
+ }, 500);
125
+ });
126
+ console.error('[Viewer] Initializing chokidar for', projectRoot);
127
+ // Serve static HTML
128
+ app.get('/', (req, res) => {
129
+ res.send(getViewerHTML(projectPath));
130
+ });
131
+ // Debug endpoint to manually trigger refresh
132
+ app.get('/refresh', async (req, res) => {
133
+ await broadcastTreeUpdate();
134
+ res.send('Refresh triggered');
135
+ });
136
+ // WebSocket handling
137
+ wss.on('connection', (ws) => {
138
+ console.error('[Viewer] Client connected');
139
+ ws.on('message', async (data) => {
140
+ try {
141
+ const msg = JSON.parse(data.toString());
142
+ if (msg.type === 'getTree') {
143
+ const mode = msg.mode || 'code';
144
+ const tree = await buildTree(sqlite, projectPath, mode, viewerSessionChanges, cachedGitInfo);
145
+ ws.send(JSON.stringify({ type: 'tree', mode, data: tree }));
146
+ }
147
+ else if (msg.type === 'getSignature' && msg.file) {
148
+ const signature = await getFileSignature(sqlite, msg.file);
149
+ ws.send(JSON.stringify({ type: 'signature', file: msg.file, data: signature }));
150
+ }
151
+ else if (msg.type === 'getFileContent' && msg.file) {
152
+ const content = getFileContent(projectRoot, msg.file);
153
+ ws.send(JSON.stringify({ type: 'fileContent', file: msg.file, data: content }));
154
+ }
155
+ }
156
+ catch (err) {
157
+ console.error('[Viewer] Error:', err);
158
+ ws.send(JSON.stringify({ type: 'error', message: String(err) }));
159
+ }
160
+ });
161
+ ws.on('close', () => {
162
+ console.error('[Viewer] Client disconnected');
163
+ });
164
+ // Send initial tree (code files only)
165
+ buildTree(sqlite, projectPath, 'code', viewerSessionChanges, cachedGitInfo).then(tree => {
166
+ ws.send(JSON.stringify({ type: 'tree', mode: 'code', data: tree }));
167
+ });
168
+ });
169
+ return new Promise((resolve, reject) => {
170
+ server.listen(PORT, () => {
171
+ const url = `http://localhost:${PORT}`;
172
+ console.error(`[Viewer] Server running at ${url}`);
173
+ // Open browser
174
+ openBrowser(url);
175
+ resolve(`Viewer opened at ${url}`);
176
+ });
177
+ server.on('error', (err) => {
178
+ if (err.code === 'EADDRINUSE') {
179
+ resolve(`Port ${PORT} already in use - viewer may already be running at http://localhost:${PORT}`);
180
+ }
181
+ else {
182
+ reject(err);
183
+ }
184
+ });
185
+ });
186
+ }
187
+ export function stopViewer() {
188
+ if (server) {
189
+ fileWatcher?.close();
190
+ fileWatcher = null;
191
+ wss?.close();
192
+ server.close();
193
+ server = null;
194
+ wss = null;
195
+ return 'Viewer stopped';
196
+ }
197
+ return 'Viewer was not running';
198
+ }
199
+ function openBrowser(url) {
200
+ const platform = process.platform;
201
+ let cmd;
202
+ if (platform === 'win32') {
203
+ cmd = `start "" "${url}"`;
204
+ }
205
+ else if (platform === 'darwin') {
206
+ cmd = `open "${url}"`;
207
+ }
208
+ else {
209
+ cmd = `xdg-open "${url}"`;
210
+ }
211
+ exec(cmd, (err) => {
212
+ if (err)
213
+ console.error('[Viewer] Failed to open browser:', err);
214
+ });
215
+ }
216
+ /**
217
+ * Detect files changed in the current session
218
+ * Uses last_indexed timestamps vs session start time
219
+ */
220
+ function detectSessionChanges(db) {
221
+ const changes = {
222
+ modified: new Set(),
223
+ new: new Set()
224
+ };
225
+ try {
226
+ // Get session start time from metadata
227
+ const sessionStartRow = db.prepare(`SELECT value FROM metadata WHERE key = 'current_session_start'`).get();
228
+ if (!sessionStartRow) {
229
+ // No session tracking yet - all files are "unchanged"
230
+ return changes;
231
+ }
232
+ const sessionStart = parseInt(sessionStartRow.value, 10);
233
+ // Find files indexed AFTER session start (not AT session start)
234
+ // This ensures a fresh re-index doesn't mark everything as modified
235
+ const recentlyIndexed = db.prepare(`
236
+ SELECT path, last_indexed,
237
+ (SELECT COUNT(*) FROM lines l WHERE l.file_id = f.id) as line_count
238
+ FROM files f
239
+ WHERE last_indexed > ?
240
+ `).all(sessionStart);
241
+ for (const file of recentlyIndexed) {
242
+ // Heuristic: if file has very few lines, it might be new
243
+ // But we can't really distinguish new vs modified without more metadata
244
+ // For now, mark all recently indexed files as "modified"
245
+ changes.modified.add(file.path);
246
+ }
247
+ }
248
+ catch {
249
+ // Silently fail
250
+ }
251
+ return changes;
252
+ }
253
+ async function buildTree(db, projectPath, mode, sessionChanges, gitInfo) {
254
+ let files;
255
+ if (mode === 'code') {
256
+ // Only indexed code files (original behavior)
257
+ files = db.prepare(`
258
+ SELECT f.path,
259
+ COUNT(DISTINCT o.item_id) as items,
260
+ (SELECT COUNT(*) FROM methods m WHERE m.file_id = f.id) as methods,
261
+ (SELECT COUNT(*) FROM types t WHERE t.file_id = f.id) as types
262
+ FROM files f
263
+ LEFT JOIN lines l ON l.file_id = f.id
264
+ LEFT JOIN occurrences o ON o.line_id = l.id
265
+ GROUP BY f.id
266
+ ORDER BY f.path
267
+ `).all();
268
+ }
269
+ else {
270
+ // All project files from project_files table
271
+ const projectFiles = db.prepare(`
272
+ SELECT path, type as fileType FROM project_files WHERE type != 'dir' ORDER BY path
273
+ `).all();
274
+ // Get stats for indexed files
275
+ const statsMap = new Map();
276
+ const indexedStats = db.prepare(`
277
+ SELECT f.path,
278
+ COUNT(DISTINCT o.item_id) as items,
279
+ (SELECT COUNT(*) FROM methods m WHERE m.file_id = f.id) as methods,
280
+ (SELECT COUNT(*) FROM types t WHERE t.file_id = f.id) as types
281
+ FROM files f
282
+ LEFT JOIN lines l ON l.file_id = f.id
283
+ LEFT JOIN occurrences o ON o.line_id = l.id
284
+ GROUP BY f.id
285
+ `).all();
286
+ for (const stat of indexedStats) {
287
+ statsMap.set(stat.path, { items: stat.items, methods: stat.methods, types: stat.types });
288
+ }
289
+ files = projectFiles.map(f => ({
290
+ path: f.path,
291
+ fileType: f.fileType,
292
+ items: statsMap.get(f.path)?.items || 0,
293
+ methods: statsMap.get(f.path)?.methods || 0,
294
+ types: statsMap.get(f.path)?.types || 0
295
+ }));
296
+ }
297
+ const root = {
298
+ name: path.basename(projectPath),
299
+ path: '',
300
+ type: 'dir',
301
+ children: []
302
+ };
303
+ for (const file of files) {
304
+ const parts = file.path.split('/');
305
+ let current = root;
306
+ for (let i = 0; i < parts.length; i++) {
307
+ const part = parts[i];
308
+ const isFile = i === parts.length - 1;
309
+ const currentPath = parts.slice(0, i + 1).join('/');
310
+ let child = current.children?.find(c => c.name === part);
311
+ if (!child) {
312
+ child = {
313
+ name: part,
314
+ path: currentPath,
315
+ type: isFile ? 'file' : 'dir',
316
+ fileType: isFile ? file.fileType : undefined,
317
+ children: isFile ? undefined : [],
318
+ stats: isFile ? { items: file.items, methods: file.methods, types: file.types } : undefined,
319
+ status: isFile ? getFileStatus(file.path, sessionChanges) : undefined,
320
+ gitStatus: isFile && gitInfo?.isGitRepo ? getGitFileStatus(file.path, gitInfo) : undefined
321
+ };
322
+ current.children?.push(child);
323
+ }
324
+ current = child;
325
+ }
326
+ }
327
+ // Sort: directories first, then alphabetically
328
+ sortTree(root);
329
+ return root;
330
+ }
331
+ function getFileStatus(filePath, changes) {
332
+ if (changes.modified.has(filePath))
333
+ return 'modified';
334
+ if (changes.new.has(filePath))
335
+ return 'new';
336
+ return 'unchanged';
337
+ }
338
+ function getGitFileStatus(filePath, gitInfo) {
339
+ const status = gitInfo.fileStatuses.get(filePath);
340
+ if (status)
341
+ return status;
342
+ // File is tracked and clean - show as pushed (green) if remote exists, otherwise committed (blue)
343
+ return gitInfo.hasRemote ? 'pushed' : 'committed';
344
+ }
345
+ function sortTree(node) {
346
+ if (node.children) {
347
+ node.children.sort((a, b) => {
348
+ if (a.type !== b.type)
349
+ return a.type === 'dir' ? -1 : 1;
350
+ return a.name.localeCompare(b.name);
351
+ });
352
+ node.children.forEach(sortTree);
353
+ }
354
+ }
355
+ async function getFileSignature(db, filePath) {
356
+ const file = db.prepare(`SELECT id FROM files WHERE path = ?`).get(filePath);
357
+ if (!file) {
358
+ return { error: 'File not found in index' };
359
+ }
360
+ const signature = db.prepare(`SELECT header_comments FROM signatures WHERE file_id = ?`).get(file.id);
361
+ const methods = db.prepare(`
362
+ SELECT prototype, line_number, visibility, is_static, is_async
363
+ FROM methods WHERE file_id = ? ORDER BY line_number
364
+ `).all(file.id);
365
+ const types = db.prepare(`
366
+ SELECT name, kind, line_number
367
+ FROM types WHERE file_id = ? ORDER BY line_number
368
+ `).all(file.id);
369
+ return {
370
+ header: signature?.header_comments || null,
371
+ methods: methods.map(m => ({
372
+ prototype: m.prototype,
373
+ line: m.line_number,
374
+ visibility: m.visibility,
375
+ static: !!m.is_static,
376
+ async: !!m.is_async
377
+ })),
378
+ types: types.map(t => ({
379
+ name: t.name,
380
+ kind: t.kind,
381
+ line: t.line_number
382
+ }))
383
+ };
384
+ }
385
+ /**
386
+ * Get file content for the Code tab
387
+ */
388
+ function getFileContent(projectRoot, filePath) {
389
+ const fullPath = path.join(projectRoot, filePath);
390
+ if (!existsSync(fullPath)) {
391
+ return { error: 'File not found' };
392
+ }
393
+ try {
394
+ const content = readFileSync(fullPath, 'utf-8');
395
+ const language = getLanguageFromExtension(filePath);
396
+ return { content, language };
397
+ }
398
+ catch (err) {
399
+ return { error: `Failed to read file: ${err}` };
400
+ }
401
+ }
402
+ /**
403
+ * Map file extension to highlight.js language identifier
404
+ */
405
+ function getLanguageFromExtension(filePath) {
406
+ const ext = path.extname(filePath).toLowerCase();
407
+ const langMap = {
408
+ '.ts': 'typescript',
409
+ '.tsx': 'typescript',
410
+ '.js': 'javascript',
411
+ '.jsx': 'javascript',
412
+ '.mjs': 'javascript',
413
+ '.cjs': 'javascript',
414
+ '.cs': 'csharp',
415
+ '.rs': 'rust',
416
+ '.py': 'python',
417
+ '.pyw': 'python',
418
+ '.c': 'c',
419
+ '.h': 'c',
420
+ '.cpp': 'cpp',
421
+ '.cc': 'cpp',
422
+ '.cxx': 'cpp',
423
+ '.hpp': 'cpp',
424
+ '.hxx': 'cpp',
425
+ '.java': 'java',
426
+ '.go': 'go',
427
+ '.php': 'php',
428
+ '.rb': 'ruby',
429
+ '.rake': 'ruby',
430
+ '.json': 'json',
431
+ '.xml': 'xml',
432
+ '.html': 'html',
433
+ '.htm': 'html',
434
+ '.css': 'css',
435
+ '.scss': 'scss',
436
+ '.less': 'less',
437
+ '.yaml': 'yaml',
438
+ '.yml': 'yaml',
439
+ '.md': 'markdown',
440
+ '.sql': 'sql',
441
+ '.sh': 'bash',
442
+ '.bash': 'bash',
443
+ '.bat': 'batch',
444
+ '.ps1': 'powershell',
445
+ '.toml': 'toml',
446
+ '.ini': 'ini',
447
+ '.cfg': 'ini'
448
+ };
449
+ return langMap[ext] || 'plaintext';
450
+ }
451
+ function getViewerHTML(projectPath) {
452
+ const projectName = path.basename(projectPath);
453
+ return `<!DOCTYPE html>
454
+ <html lang="en">
455
+ <head>
456
+ <meta charset="UTF-8">
457
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
458
+ <title>${PRODUCT_NAME} Viewer - ${projectName}</title>
459
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.0/styles/tokyo-night-dark.min.css">
460
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.0/highlight.min.js"></script>
461
+ <style>
462
+ :root {
463
+ --bg-primary: #1a1b26;
464
+ --bg-secondary: #24283b;
465
+ --bg-tertiary: #414868;
466
+ --text-primary: #c0caf5;
467
+ --text-secondary: #a9b1d6;
468
+ --text-muted: #565f89;
469
+ --accent: #7aa2f7;
470
+ --accent-green: #9ece6a;
471
+ --accent-orange: #ff9e64;
472
+ --accent-purple: #bb9af7;
473
+ --accent-cyan: #7dcfff;
474
+ --accent-yellow: #e0af68;
475
+ --accent-red: #f7768e;
476
+ --border: #3b4261;
477
+ }
478
+
479
+ * { box-sizing: border-box; margin: 0; padding: 0; }
480
+
481
+ body {
482
+ font-family: 'Segoe UI', system-ui, sans-serif;
483
+ background: var(--bg-primary);
484
+ color: var(--text-primary);
485
+ height: 100vh;
486
+ display: flex;
487
+ flex-direction: column;
488
+ }
489
+
490
+ header {
491
+ background: var(--bg-secondary);
492
+ padding: 12px 20px;
493
+ border-bottom: 1px solid var(--border);
494
+ display: flex;
495
+ align-items: center;
496
+ gap: 15px;
497
+ }
498
+
499
+ header h1 {
500
+ font-size: 1.3em;
501
+ color: var(--accent);
502
+ font-weight: 500;
503
+ }
504
+
505
+ header .project-name {
506
+ color: var(--accent-purple);
507
+ font-weight: 600;
508
+ }
509
+
510
+ .container {
511
+ display: flex;
512
+ flex: 1;
513
+ overflow: hidden;
514
+ }
515
+
516
+ /* Splitter */
517
+ .splitter {
518
+ width: 6px;
519
+ background: var(--bg-tertiary);
520
+ cursor: col-resize;
521
+ transition: background 0.2s;
522
+ flex-shrink: 0;
523
+ }
524
+
525
+ .splitter:hover, .splitter.dragging {
526
+ background: var(--accent);
527
+ }
528
+
529
+ /* Panel styles */
530
+ .panel {
531
+ display: flex;
532
+ flex-direction: column;
533
+ overflow: hidden;
534
+ }
535
+
536
+ .tree-panel {
537
+ width: 350px;
538
+ background: var(--bg-secondary);
539
+ border-right: 1px solid var(--border);
540
+ }
541
+
542
+ .detail-panel {
543
+ flex: 1;
544
+ }
545
+
546
+ /* Tab bar styles */
547
+ .tab-bar {
548
+ display: flex;
549
+ background: var(--bg-tertiary);
550
+ border-bottom: 1px solid var(--border);
551
+ }
552
+
553
+ .tab {
554
+ padding: 10px 20px;
555
+ cursor: pointer;
556
+ color: var(--text-muted);
557
+ border-bottom: 2px solid transparent;
558
+ transition: all 0.2s;
559
+ }
560
+
561
+ .tab:hover {
562
+ color: var(--text-secondary);
563
+ background: rgba(122, 162, 247, 0.1);
564
+ }
565
+
566
+ .tab.active {
567
+ color: var(--accent);
568
+ border-bottom-color: var(--accent);
569
+ }
570
+
571
+ .panel-content {
572
+ flex: 1;
573
+ overflow-y: auto;
574
+ padding: 10px 0;
575
+ }
576
+
577
+ .detail-panel .panel-content {
578
+ padding: 20px;
579
+ }
580
+
581
+ /* Tree styles */
582
+ .tree-node {
583
+ padding: 6px 10px 6px 0;
584
+ cursor: pointer;
585
+ display: flex;
586
+ align-items: center;
587
+ gap: 6px;
588
+ white-space: nowrap;
589
+ }
590
+
591
+ .tree-node:hover {
592
+ background: rgba(122, 162, 247, 0.1);
593
+ }
594
+
595
+ .tree-node.selected {
596
+ background: rgba(122, 162, 247, 0.2);
597
+ }
598
+
599
+ .tree-node .status-icon {
600
+ width: 16px;
601
+ font-size: 11px;
602
+ text-align: center;
603
+ flex-shrink: 0;
604
+ }
605
+
606
+ .tree-node .status-icon.modified {
607
+ color: var(--accent-orange);
608
+ }
609
+
610
+ .tree-node .status-icon.new {
611
+ color: var(--accent-green);
612
+ }
613
+
614
+ .tree-node .status-icon.unchanged {
615
+ color: var(--accent-green);
616
+ opacity: 0.7;
617
+ }
618
+
619
+ /* Git status cat icon */
620
+ .tree-node .git-cat {
621
+ width: 16px;
622
+ height: 16px;
623
+ flex-shrink: 0;
624
+ display: flex;
625
+ align-items: center;
626
+ justify-content: center;
627
+ }
628
+
629
+ .tree-node .git-cat svg {
630
+ width: 14px;
631
+ height: 14px;
632
+ }
633
+
634
+ .tree-node .git-cat.untracked svg { fill: #6b7280; }
635
+ .tree-node .git-cat.modified svg { fill: #f59e0b; }
636
+ .tree-node .git-cat.committed svg { fill: #3b82f6; }
637
+ .tree-node .git-cat.pushed svg { fill: #22c55e; }
638
+
639
+ .tree-node .icon {
640
+ width: 18px;
641
+ text-align: center;
642
+ flex-shrink: 0;
643
+ }
644
+
645
+ .tree-node .name {
646
+ flex: 1;
647
+ overflow: hidden;
648
+ text-overflow: ellipsis;
649
+ }
650
+
651
+ .tree-node .stats {
652
+ font-size: 0.75em;
653
+ color: var(--text-muted);
654
+ margin-left: auto;
655
+ padding-right: 10px;
656
+ }
657
+
658
+ .tree-node.dir .icon { color: var(--accent-yellow); }
659
+ .tree-node.file .icon { color: var(--accent-cyan); }
660
+ .tree-node.file.config .icon { color: var(--accent-purple); }
661
+ .tree-node.file.doc .icon { color: var(--accent-green); }
662
+ .tree-node.file.test .icon { color: var(--accent-orange); }
663
+
664
+ .tree-children {
665
+ margin-left: 20px;
666
+ }
667
+
668
+ .tree-children.collapsed {
669
+ display: none;
670
+ }
671
+
672
+ /* Detail panel styles */
673
+ .detail-panel h2 {
674
+ color: var(--accent-purple);
675
+ font-size: 1.2em;
676
+ margin-bottom: 15px;
677
+ padding-bottom: 10px;
678
+ border-bottom: 1px solid var(--border);
679
+ }
680
+
681
+ .detail-panel .file-path {
682
+ color: var(--text-muted);
683
+ font-size: 0.9em;
684
+ margin-bottom: 20px;
685
+ }
686
+
687
+ .section {
688
+ margin-bottom: 25px;
689
+ }
690
+
691
+ .section h3 {
692
+ color: var(--accent-cyan);
693
+ font-size: 1em;
694
+ margin-bottom: 10px;
695
+ }
696
+
697
+ .header-comment {
698
+ background: var(--bg-secondary);
699
+ padding: 15px;
700
+ border-radius: 6px;
701
+ font-family: 'Consolas', 'Fira Code', monospace;
702
+ font-size: 0.9em;
703
+ white-space: pre-wrap;
704
+ color: var(--accent-green);
705
+ border-left: 3px solid var(--accent-green);
706
+ }
707
+
708
+ .method-list, .type-list {
709
+ list-style: none;
710
+ }
711
+
712
+ .method-list li, .type-list li {
713
+ padding: 8px 12px;
714
+ background: var(--bg-secondary);
715
+ margin-bottom: 6px;
716
+ border-radius: 4px;
717
+ font-family: 'Consolas', monospace;
718
+ font-size: 0.85em;
719
+ display: flex;
720
+ align-items: center;
721
+ gap: 10px;
722
+ }
723
+
724
+ .method-list .line-num, .type-list .line-num {
725
+ color: var(--text-muted);
726
+ font-size: 0.8em;
727
+ min-width: 40px;
728
+ }
729
+
730
+ .method-list .visibility {
731
+ color: var(--accent-purple);
732
+ font-size: 0.75em;
733
+ padding: 2px 6px;
734
+ background: rgba(187, 154, 247, 0.15);
735
+ border-radius: 3px;
736
+ }
737
+
738
+ .method-list .modifier {
739
+ color: var(--accent-orange);
740
+ font-size: 0.75em;
741
+ }
742
+
743
+ .type-list .kind {
744
+ color: var(--accent-yellow);
745
+ font-size: 0.75em;
746
+ padding: 2px 6px;
747
+ background: rgba(224, 175, 104, 0.15);
748
+ border-radius: 3px;
749
+ }
750
+
751
+ .empty-state {
752
+ color: var(--text-muted);
753
+ text-align: center;
754
+ padding: 40px;
755
+ }
756
+
757
+ .loading {
758
+ color: var(--text-muted);
759
+ padding: 20px;
760
+ text-align: center;
761
+ }
762
+
763
+ /* Code view styles */
764
+ .code-view {
765
+ background: var(--bg-secondary);
766
+ border-radius: 6px;
767
+ overflow: hidden;
768
+ }
769
+
770
+ .code-view pre {
771
+ margin: 0;
772
+ padding: 15px;
773
+ overflow-x: auto;
774
+ font-size: 0.85em;
775
+ line-height: 1.5;
776
+ }
777
+
778
+ .code-view code {
779
+ font-family: 'Consolas', 'Fira Code', monospace;
780
+ }
781
+
782
+ /* Override highlight.js background to match our theme */
783
+ .hljs {
784
+ background: var(--bg-secondary) !important;
785
+ }
786
+ </style>
787
+ </head>
788
+ <body>
789
+ <header>
790
+ <h1>${PRODUCT_NAME} Viewer</h1>
791
+ <span class="project-name">${projectName}</span>
792
+ </header>
793
+
794
+ <div class="container">
795
+ <div class="panel tree-panel" id="treePanel">
796
+ <div class="tab-bar">
797
+ <div class="tab active" data-tab="code">Code</div>
798
+ <div class="tab" data-tab="all">All</div>
799
+ </div>
800
+ <div class="panel-content" id="tree">
801
+ <div class="loading">Loading project tree...</div>
802
+ </div>
803
+ </div>
804
+ <div class="splitter" id="splitter"></div>
805
+ <div class="panel detail-panel">
806
+ <div class="tab-bar">
807
+ <div class="tab active" data-tab="overview">Overview</div>
808
+ <div class="tab" data-tab="source">Code</div>
809
+ </div>
810
+ <div class="panel-content" id="detail">
811
+ <div class="empty-state">
812
+ <p>Click on a file to view its signature</p>
813
+ </div>
814
+ </div>
815
+ </div>
816
+ </div>
817
+
818
+ <script>
819
+ const ws = new WebSocket('ws://localhost:${PORT}');
820
+ let selectedNode = null;
821
+ let currentFile = null;
822
+ let currentTreeMode = 'code';
823
+ let currentDetailTab = 'overview';
824
+ let cachedSignature = null;
825
+ let cachedContent = null;
826
+
827
+ ws.onopen = () => {
828
+ console.log('Connected to AiDex Viewer');
829
+ };
830
+
831
+ let cachedCodeTree = null;
832
+ let cachedAllTree = null;
833
+
834
+ ws.onmessage = (event) => {
835
+ const msg = JSON.parse(event.data);
836
+ console.log('📨 Received:', msg.type, msg);
837
+
838
+ if (msg.type === 'tree') {
839
+ // Cache the tree for the mode
840
+ if (msg.mode === 'code') cachedCodeTree = msg.data;
841
+ else cachedAllTree = msg.data;
842
+ renderTree(msg.data);
843
+ } else if (msg.type === 'refresh') {
844
+ // Live reload: update cached trees and re-render current mode
845
+ console.log('🔄 Live reload triggered');
846
+ cachedCodeTree = msg.codeTree;
847
+ cachedAllTree = msg.allTree;
848
+ const treeToRender = currentTreeMode === 'code' ? cachedCodeTree : cachedAllTree;
849
+ if (treeToRender) renderTree(treeToRender);
850
+ } else if (msg.type === 'signature') {
851
+ cachedSignature = { file: msg.file, data: msg.data };
852
+ if (currentDetailTab === 'overview') {
853
+ renderSignature(msg.file, msg.data);
854
+ }
855
+ } else if (msg.type === 'fileContent') {
856
+ cachedContent = { file: msg.file, data: msg.data };
857
+ if (currentDetailTab === 'source') {
858
+ renderFileContent(msg.file, msg.data);
859
+ }
860
+ }
861
+ };
862
+
863
+ // Tab switching - Tree panel
864
+ document.querySelectorAll('.tree-panel .tab').forEach(tab => {
865
+ tab.addEventListener('click', () => {
866
+ document.querySelectorAll('.tree-panel .tab').forEach(t => t.classList.remove('active'));
867
+ tab.classList.add('active');
868
+ currentTreeMode = tab.dataset.tab;
869
+ document.getElementById('tree').innerHTML = '<div class="loading">Loading...</div>';
870
+ ws.send(JSON.stringify({ type: 'getTree', mode: currentTreeMode }));
871
+ });
872
+ });
873
+
874
+ // Tab switching - Detail panel
875
+ document.querySelectorAll('.detail-panel .tab').forEach(tab => {
876
+ tab.addEventListener('click', () => {
877
+ if (!currentFile) return;
878
+
879
+ document.querySelectorAll('.detail-panel .tab').forEach(t => t.classList.remove('active'));
880
+ tab.classList.add('active');
881
+ currentDetailTab = tab.dataset.tab;
882
+
883
+ if (currentDetailTab === 'overview') {
884
+ if (cachedSignature && cachedSignature.file === currentFile) {
885
+ renderSignature(cachedSignature.file, cachedSignature.data);
886
+ } else {
887
+ ws.send(JSON.stringify({ type: 'getSignature', file: currentFile }));
888
+ }
889
+ } else if (currentDetailTab === 'source') {
890
+ if (cachedContent && cachedContent.file === currentFile) {
891
+ renderFileContent(cachedContent.file, cachedContent.data);
892
+ } else {
893
+ document.getElementById('detail').innerHTML = '<div class="loading">Loading source...</div>';
894
+ ws.send(JSON.stringify({ type: 'getFileContent', file: currentFile }));
895
+ }
896
+ }
897
+ });
898
+ });
899
+
900
+ function renderTree(node, container = document.getElementById('tree'), depth = 0) {
901
+ if (depth === 0) {
902
+ container.innerHTML = '';
903
+ }
904
+
905
+ const div = document.createElement('div');
906
+ div.className = 'tree-node ' + node.type + (node.fileType ? ' ' + node.fileType : '');
907
+ div.style.paddingLeft = (depth * 20 + 10) + 'px';
908
+ div.dataset.path = node.path;
909
+ div.dataset.type = node.type;
910
+
911
+ // Status icon (modified/new/unchanged)
912
+ const statusIcon = document.createElement('span');
913
+ statusIcon.className = 'status-icon';
914
+ if (node.status === 'modified') {
915
+ statusIcon.className += ' modified';
916
+ statusIcon.textContent = '✏️';
917
+ statusIcon.title = 'Modified in this session';
918
+ } else if (node.status === 'new') {
919
+ statusIcon.className += ' new';
920
+ statusIcon.textContent = '➕';
921
+ statusIcon.title = 'New in this session';
922
+ } else if (node.status === 'unchanged') {
923
+ statusIcon.className += ' unchanged';
924
+ statusIcon.textContent = '✓';
925
+ statusIcon.title = 'Unchanged';
926
+ }
927
+ div.appendChild(statusIcon);
928
+
929
+ // Git status cat icon (only for files in git repos)
930
+ if (node.gitStatus) {
931
+ const gitCat = document.createElement('span');
932
+ gitCat.className = 'git-cat ' + node.gitStatus;
933
+ // Cat silhouette SVG - simple sitting cat with raised paw
934
+ gitCat.innerHTML = '<svg viewBox="0 0 24 24"><path d="M12,8L10.67,8.09C9.81,7.07 7.4,4.5 5,4.5C5,4.5 3.03,7.46 4.96,11.41C4.41,12.24 4.07,12.67 4,13.66L2.07,13.95L2.28,14.93L4.04,14.67L4.18,15.38L2.61,16.32L3.08,17.21L4.53,16.32C5.68,18.76 8.59,20 12,20C15.41,20 18.32,18.76 19.47,16.32L20.92,17.21L21.39,16.32L19.82,15.38L19.96,14.67L21.72,14.93L21.93,13.95L20,13.66C19.93,12.67 19.59,12.24 19.04,11.41C20.97,7.46 19,4.5 19,4.5C16.6,4.5 14.19,7.07 13.33,8.09L12,8M9,11A1,1 0 0,1 10,12A1,1 0 0,1 9,13A1,1 0 0,1 8,12A1,1 0 0,1 9,11M15,11A1,1 0 0,1 16,12A1,1 0 0,1 15,13A1,1 0 0,1 14,12A1,1 0 0,1 15,11M11,14H13V16H11V14Z"/></svg>';
935
+ const gitTitles = {
936
+ 'untracked': 'Untracked - not in git',
937
+ 'modified': 'Modified - not committed',
938
+ 'committed': 'Committed - not pushed',
939
+ 'pushed': 'Pushed - in sync with remote'
940
+ };
941
+ gitCat.title = gitTitles[node.gitStatus] || node.gitStatus;
942
+ div.appendChild(gitCat);
943
+ }
944
+
945
+ // File/folder icon
946
+ const icon = document.createElement('span');
947
+ icon.className = 'icon';
948
+ if (node.type === 'dir') {
949
+ icon.textContent = '📁';
950
+ } else {
951
+ // Different icons for different file types
952
+ const iconMap = {
953
+ 'code': '📄',
954
+ 'config': '⚙️',
955
+ 'doc': '📝',
956
+ 'test': '🧪',
957
+ 'asset': '🖼️',
958
+ 'other': '📄'
959
+ };
960
+ icon.textContent = iconMap[node.fileType] || '📄';
961
+ }
962
+
963
+ const name = document.createElement('span');
964
+ name.className = 'name';
965
+ name.textContent = node.name;
966
+
967
+ div.appendChild(icon);
968
+ div.appendChild(name);
969
+
970
+ if (node.stats && (node.stats.methods > 0 || node.stats.types > 0)) {
971
+ const stats = document.createElement('span');
972
+ stats.className = 'stats';
973
+ stats.textContent = node.stats.methods + 'm ' + node.stats.types + 't';
974
+ div.appendChild(stats);
975
+ }
976
+
977
+ div.onclick = (e) => {
978
+ e.stopPropagation();
979
+
980
+ if (node.type === 'dir') {
981
+ const children = div.nextElementSibling;
982
+ if (children && children.classList.contains('tree-children')) {
983
+ children.classList.toggle('collapsed');
984
+ icon.textContent = children.classList.contains('collapsed') ? '📁' : '📂';
985
+ }
986
+ } else {
987
+ if (selectedNode) selectedNode.classList.remove('selected');
988
+ div.classList.add('selected');
989
+ selectedNode = div;
990
+ currentFile = node.path;
991
+ cachedSignature = null;
992
+ cachedContent = null;
993
+
994
+ // Reset to overview tab
995
+ currentDetailTab = 'overview';
996
+ document.querySelectorAll('.detail-panel .tab').forEach(t => t.classList.remove('active'));
997
+ document.querySelector('.detail-panel .tab[data-tab="overview"]').classList.add('active');
998
+
999
+ ws.send(JSON.stringify({ type: 'getSignature', file: node.path }));
1000
+ }
1001
+ };
1002
+
1003
+ container.appendChild(div);
1004
+
1005
+ if (node.children && node.children.length > 0) {
1006
+ const childContainer = document.createElement('div');
1007
+ childContainer.className = 'tree-children';
1008
+ container.appendChild(childContainer);
1009
+
1010
+ for (const child of node.children) {
1011
+ renderTree(child, childContainer, depth + 1);
1012
+ }
1013
+ }
1014
+ }
1015
+
1016
+ function renderSignature(filePath, data) {
1017
+ const detail = document.getElementById('detail');
1018
+
1019
+ if (data.error) {
1020
+ detail.innerHTML = '<div class="empty-state">' + data.error + '</div>';
1021
+ return;
1022
+ }
1023
+
1024
+ let html = '<h2>' + filePath.split('/').pop() + '</h2>';
1025
+ html += '<div class="file-path">' + filePath + '</div>';
1026
+
1027
+ if (data.header) {
1028
+ html += '<div class="section"><h3>Header Comments</h3>';
1029
+ html += '<div class="header-comment">' + escapeHtml(data.header) + '</div></div>';
1030
+ }
1031
+
1032
+ if (data.types && data.types.length > 0) {
1033
+ html += '<div class="section"><h3>Types (' + data.types.length + ')</h3>';
1034
+ html += '<ul class="type-list">';
1035
+ for (const t of data.types) {
1036
+ html += '<li><span class="line-num">:' + t.line + '</span>';
1037
+ html += '<span class="kind">' + t.kind + '</span>';
1038
+ html += '<span>' + escapeHtml(t.name) + '</span></li>';
1039
+ }
1040
+ html += '</ul></div>';
1041
+ }
1042
+
1043
+ if (data.methods && data.methods.length > 0) {
1044
+ html += '<div class="section"><h3>Methods (' + data.methods.length + ')</h3>';
1045
+ html += '<ul class="method-list">';
1046
+ for (const m of data.methods) {
1047
+ html += '<li><span class="line-num">:' + m.line + '</span>';
1048
+ if (m.visibility) html += '<span class="visibility">' + m.visibility + '</span>';
1049
+ if (m.static) html += '<span class="modifier">static</span>';
1050
+ if (m.async) html += '<span class="modifier">async</span>';
1051
+ html += '<span>' + escapeHtml(m.prototype) + '</span></li>';
1052
+ }
1053
+ html += '</ul></div>';
1054
+ }
1055
+
1056
+ if (!data.header && (!data.types || data.types.length === 0) && (!data.methods || data.methods.length === 0)) {
1057
+ html += '<div class="empty-state">No signature data for this file</div>';
1058
+ }
1059
+
1060
+ detail.innerHTML = html;
1061
+ }
1062
+
1063
+ function renderFileContent(filePath, data) {
1064
+ const detail = document.getElementById('detail');
1065
+
1066
+ if (data.error) {
1067
+ detail.innerHTML = '<div class="empty-state">' + data.error + '</div>';
1068
+ return;
1069
+ }
1070
+
1071
+ let html = '<h2>' + filePath.split('/').pop() + '</h2>';
1072
+ html += '<div class="file-path">' + filePath + '</div>';
1073
+ html += '<div class="code-view"><pre><code class="language-' + data.language + '">' + escapeHtml(data.content) + '</code></pre></div>';
1074
+
1075
+ detail.innerHTML = html;
1076
+
1077
+ // Apply syntax highlighting
1078
+ detail.querySelectorAll('pre code').forEach((block) => {
1079
+ hljs.highlightElement(block);
1080
+ });
1081
+ }
1082
+
1083
+ function escapeHtml(text) {
1084
+ const div = document.createElement('div');
1085
+ div.textContent = text;
1086
+ return div.innerHTML;
1087
+ }
1088
+
1089
+ // Splitter functionality
1090
+ const splitter = document.getElementById('splitter');
1091
+ const treePanel = document.getElementById('treePanel');
1092
+ let isDragging = false;
1093
+
1094
+ splitter.addEventListener('mousedown', (e) => {
1095
+ isDragging = true;
1096
+ splitter.classList.add('dragging');
1097
+ document.body.style.cursor = 'col-resize';
1098
+ document.body.style.userSelect = 'none';
1099
+ });
1100
+
1101
+ document.addEventListener('mousemove', (e) => {
1102
+ if (!isDragging) return;
1103
+ const containerRect = document.querySelector('.container').getBoundingClientRect();
1104
+ const newWidth = e.clientX - containerRect.left;
1105
+ if (newWidth >= 200 && newWidth <= 800) {
1106
+ treePanel.style.width = newWidth + 'px';
1107
+ }
1108
+ });
1109
+
1110
+ document.addEventListener('mouseup', () => {
1111
+ if (isDragging) {
1112
+ isDragging = false;
1113
+ splitter.classList.remove('dragging');
1114
+ document.body.style.cursor = '';
1115
+ document.body.style.userSelect = '';
1116
+ }
1117
+ });
1118
+ </script>
1119
+ </body>
1120
+ </html>`;
1121
+ }
1122
+ //# sourceMappingURL=server.js.map