@rpcajr/smart-graph-indexer 1.0.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 (60) hide show
  1. package/.agents/rules/antigravity-rtk-rules.md +32 -0
  2. package/README.md +116 -0
  3. package/dist/graph.d.ts +32 -0
  4. package/dist/graph.js +86 -0
  5. package/dist/index.d.ts +2 -0
  6. package/dist/index.js +227 -0
  7. package/dist/mcp_server.d.ts +48 -0
  8. package/dist/mcp_server.js +433 -0
  9. package/dist/parser.d.ts +42 -0
  10. package/dist/parser.js +529 -0
  11. package/dist/stats.d.ts +43 -0
  12. package/dist/stats.js +146 -0
  13. package/dist/ui.d.ts +2 -0
  14. package/dist/ui.js +1003 -0
  15. package/dist/vector.d.ts +66 -0
  16. package/dist/vector.js +144 -0
  17. package/dist/wasm/tree-sitter-c.wasm +0 -0
  18. package/dist/wasm/tree-sitter-c_sharp.wasm +0 -0
  19. package/dist/wasm/tree-sitter-cpp.wasm +0 -0
  20. package/dist/wasm/tree-sitter-go.wasm +0 -0
  21. package/dist/wasm/tree-sitter-java.wasm +0 -0
  22. package/dist/wasm/tree-sitter-python.wasm +0 -0
  23. package/dist/wasm/tree-sitter-ruby.wasm +0 -0
  24. package/dist/wasm/tree-sitter-rust.wasm +0 -0
  25. package/dist/wasm/tree-sitter-typescript.wasm +0 -0
  26. package/dist/wasm/tree-sitter.wasm +0 -0
  27. package/dist/watcher.d.ts +36 -0
  28. package/dist/watcher.js +166 -0
  29. package/mcp-config-example.json +12 -0
  30. package/mcp_smart_indexer_implementation_spec.md +366 -0
  31. package/package.json +35 -0
  32. package/src/graph.ts +93 -0
  33. package/src/index.ts +216 -0
  34. package/src/mcp_server.ts +454 -0
  35. package/src/parser.ts +484 -0
  36. package/src/stats.ts +156 -0
  37. package/src/ui.ts +956 -0
  38. package/src/vector.ts +166 -0
  39. package/src/watcher.ts +144 -0
  40. package/test_project/App.java +16 -0
  41. package/test_project/BillingService.cs +31 -0
  42. package/test_project/auth.ts +18 -0
  43. package/test_project/config.ts +11 -0
  44. package/test_project/database.ts +21 -0
  45. package/test_project/index.ts +13 -0
  46. package/test_project/main.go +11 -0
  47. package/test_project/main.py +21 -0
  48. package/test_project/processor.py +12 -0
  49. package/test_project/utils.py +13 -0
  50. package/tsconfig.json +16 -0
  51. package/wasm/tree-sitter-c.wasm +0 -0
  52. package/wasm/tree-sitter-c_sharp.wasm +0 -0
  53. package/wasm/tree-sitter-cpp.wasm +0 -0
  54. package/wasm/tree-sitter-go.wasm +0 -0
  55. package/wasm/tree-sitter-java.wasm +0 -0
  56. package/wasm/tree-sitter-python.wasm +0 -0
  57. package/wasm/tree-sitter-ruby.wasm +0 -0
  58. package/wasm/tree-sitter-rust.wasm +0 -0
  59. package/wasm/tree-sitter-typescript.wasm +0 -0
  60. package/wasm/tree-sitter.wasm +0 -0
package/src/ui.ts ADDED
@@ -0,0 +1,956 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { IndexerEngine } from './mcp_server.js';
4
+
5
+ export function generateGraphHtml(engine: IndexerEngine): string {
6
+ const report = engine.getReport();
7
+ const projectName = path.basename((engine as any).projectPath) || 'Project';
8
+
9
+ // Format data for Vis.js
10
+ const nodes: any[] = [];
11
+ const edges: any[] = [];
12
+ const nodeMap = new Map<string, number>();
13
+ let idCounter = 1;
14
+
15
+ // Add all files as nodes
16
+ for (const file of report.files) {
17
+ const id = idCounter++;
18
+ nodeMap.set(file, id);
19
+
20
+ // Determine language based on extension for icon/color
21
+ const ext = path.extname(file).toLowerCase();
22
+ let group = 'other';
23
+ let color = '#7f8c8d'; // gray
24
+
25
+ if (['.ts', '.tsx', '.js', '.jsx'].includes(ext)) { group = 'typescript'; color = '#3178c6'; }
26
+ else if (ext === '.py') { group = 'python'; color = '#3776ab'; }
27
+ else if (ext === '.cs') { group = 'csharp'; color = '#178600'; }
28
+ else if (ext === '.java') { group = 'java'; color = '#b07219'; }
29
+ else if (ext === '.go') { group = 'go'; color = '#00add8'; }
30
+ else if (ext === '.rs') { group = 'rust'; color = '#dee5e7'; }
31
+ else if (ext === '.rb') { group = 'ruby'; color = '#701516'; }
32
+ else if (['.cpp', '.hpp', '.cc', '.h', '.c'].includes(ext)) { group = 'cpp'; color = '#f34b7d'; }
33
+
34
+ const skeleton = engine.getFileSkeleton(file);
35
+ const symbolsCount = skeleton?.symbols.length ?? 0;
36
+
37
+ const tooltipText = `${file}\nSymbols: ${symbolsCount}\nImports: ${skeleton?.dependencies.length ?? 0}`;
38
+
39
+ nodes.push({
40
+ id,
41
+ label: path.basename(file),
42
+ title: tooltipText,
43
+ value: Math.max(5, symbolsCount), // size based on symbol count
44
+ color: {
45
+ background: '#1e1e2e',
46
+ border: color,
47
+ highlight: {
48
+ background: color,
49
+ border: '#ffffff'
50
+ }
51
+ },
52
+ font: { color: '#cdd6f4' },
53
+ borderWidth: 2,
54
+ shape: 'dot',
55
+ filePath: file,
56
+ symbols: skeleton?.symbols || [],
57
+ dependencies: skeleton?.dependencies || []
58
+ });
59
+ }
60
+
61
+ // Add edges
62
+ for (const [sourceFile, data] of Object.entries(report.dependencyGraph)) {
63
+ const sourceId = nodeMap.get(sourceFile);
64
+ if (!sourceId) continue;
65
+
66
+ for (const targetName of data.imports) {
67
+ // Find if target matches a file in our project
68
+ const targetFile = report.files.find(f => {
69
+ const baseF = path.basename(f, path.extname(f));
70
+ const baseT = path.basename(targetName, path.extname(targetName));
71
+ return f === targetName || baseF === baseT;
72
+ });
73
+
74
+ if (targetFile) {
75
+ const targetId = nodeMap.get(targetFile);
76
+ if (targetId && sourceId !== targetId) {
77
+ edges.push({
78
+ from: sourceId,
79
+ to: targetId,
80
+ arrows: 'to',
81
+ color: { color: '#a6adc8', highlight: '#f5c2e7' },
82
+ width: 1
83
+ });
84
+ }
85
+ }
86
+ }
87
+ }
88
+
89
+ return `
90
+ <!DOCTYPE html>
91
+ <html lang="en">
92
+ <head>
93
+ <meta charset="UTF-8">
94
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
95
+ <title>SmartGraphIndexer - ${projectName}</title>
96
+ <!-- Vis.js for Network Graph -->
97
+ <script type="text/javascript" src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"></script>
98
+ <!-- Google Fonts Outfit -->
99
+ <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;800&display=swap" rel="stylesheet">
100
+ <style>
101
+ :root {
102
+ --bg-base: #0f0f16;
103
+ --bg-surface: #1e1e2e;
104
+ --bg-panel: rgba(30, 30, 46, 0.7);
105
+ --text-main: #cdd6f4;
106
+ --text-muted: #a6adc8;
107
+ --accent: #cba6f7;
108
+ --accent-glow: rgba(203, 166, 247, 0.4);
109
+ --border: #313244;
110
+ }
111
+
112
+ * {
113
+ box-sizing: border-box;
114
+ margin: 0;
115
+ padding: 0;
116
+ font-family: 'Outfit', sans-serif;
117
+ }
118
+
119
+ body {
120
+ background-color: var(--bg-base);
121
+ color: var(--text-main);
122
+ height: 100vh;
123
+ overflow: hidden;
124
+ display: flex;
125
+ }
126
+
127
+ /* Glassmorphism sidebar */
128
+ .sidebar {
129
+ width: 400px;
130
+ background: var(--bg-panel);
131
+ backdrop-filter: blur(16px);
132
+ -webkit-backdrop-filter: blur(16px);
133
+ border-right: 1px solid var(--border);
134
+ display: flex;
135
+ flex-direction: column;
136
+ height: 100%;
137
+ z-index: 10;
138
+ transition: transform 0.3s ease;
139
+ position: relative;
140
+ }
141
+
142
+ .header {
143
+ padding: 20px 24px;
144
+ border-bottom: 1px solid var(--border);
145
+ }
146
+
147
+ .header h1 {
148
+ font-size: 22px;
149
+ font-weight: 800;
150
+ background: linear-gradient(45deg, #cba6f7, #f5c2e7);
151
+ -webkit-background-clip: text;
152
+ -webkit-text-fill-color: transparent;
153
+ margin-bottom: 4px;
154
+ word-break: break-all;
155
+ }
156
+
157
+ .header p {
158
+ font-size: 12px;
159
+ color: var(--text-muted);
160
+ }
161
+
162
+ /* Token Stats Panel */
163
+ .stats-panel {
164
+ padding: 16px 24px;
165
+ border-bottom: 1px solid var(--border);
166
+ background: rgba(203, 166, 247, 0.02);
167
+ }
168
+
169
+ .stats-title {
170
+ font-size: 10px;
171
+ text-transform: uppercase;
172
+ letter-spacing: 1px;
173
+ color: var(--accent);
174
+ margin-bottom: 10px;
175
+ font-weight: 600;
176
+ }
177
+
178
+ .stats-grid {
179
+ display: flex;
180
+ gap: 12px;
181
+ }
182
+
183
+ .stat-card {
184
+ flex: 1;
185
+ background: rgba(15, 15, 22, 0.4);
186
+ border: 1px solid var(--border);
187
+ border-radius: 6px;
188
+ padding: 8px;
189
+ text-align: center;
190
+ }
191
+
192
+ .stat-value {
193
+ font-size: 15px;
194
+ font-weight: 800;
195
+ color: #a6e3a1; /* green */
196
+ }
197
+
198
+ .stat-label {
199
+ font-size: 9.5px;
200
+ color: var(--text-muted);
201
+ margin-top: 2px;
202
+ }
203
+
204
+ /* Premium Tabs styling */
205
+ .tabs-header {
206
+ display: flex;
207
+ background: rgba(15, 15, 22, 0.3);
208
+ border-bottom: 1px solid var(--border);
209
+ }
210
+
211
+ .tab-btn {
212
+ flex: 1;
213
+ background: none;
214
+ border: none;
215
+ color: var(--text-muted);
216
+ padding: 12px;
217
+ font-size: 13px;
218
+ font-weight: 600;
219
+ cursor: pointer;
220
+ transition: all 0.3s ease;
221
+ position: relative;
222
+ outline: none;
223
+ }
224
+
225
+ .tab-btn.active {
226
+ color: var(--accent);
227
+ background: rgba(203, 166, 247, 0.03);
228
+ }
229
+
230
+ .tab-btn.active::after {
231
+ content: '';
232
+ position: absolute;
233
+ bottom: -1px;
234
+ left: 0;
235
+ width: 100%;
236
+ height: 2px;
237
+ background: var(--accent);
238
+ box-shadow: 0 0 8px var(--accent);
239
+ }
240
+
241
+ .search-box {
242
+ padding: 14px 24px;
243
+ border-bottom: 1px solid var(--border);
244
+ }
245
+
246
+ .search-input {
247
+ width: 100%;
248
+ padding: 10px 14px;
249
+ background: rgba(15, 15, 22, 0.6);
250
+ border: 1px solid var(--border);
251
+ border-radius: 8px;
252
+ color: var(--text-main);
253
+ font-size: 13px;
254
+ outline: none;
255
+ transition: all 0.3s ease;
256
+ }
257
+
258
+ .search-input:focus {
259
+ border-color: var(--accent);
260
+ box-shadow: 0 0 10px var(--accent-glow);
261
+ }
262
+
263
+ .content-panel {
264
+ flex: 1;
265
+ overflow-y: auto;
266
+ padding: 20px 24px 60px 24px; /* leaves space for footer */
267
+ }
268
+
269
+ .tab-panel {
270
+ display: none;
271
+ animation: fadeIn 0.3s ease forwards;
272
+ }
273
+
274
+ .tab-panel.active-panel {
275
+ display: block;
276
+ }
277
+
278
+ .empty-state {
279
+ text-align: center;
280
+ color: var(--text-muted);
281
+ margin-top: 40px;
282
+ font-weight: 300;
283
+ font-size: 13px;
284
+ }
285
+
286
+ /* Node Details Panel */
287
+ .detail-card {
288
+ display: none;
289
+ }
290
+
291
+ .detail-title {
292
+ font-size: 18px;
293
+ font-weight: 600;
294
+ color: #ffffff;
295
+ margin-bottom: 6px;
296
+ word-break: break-all;
297
+ }
298
+
299
+ .detail-path {
300
+ font-size: 11px;
301
+ color: var(--text-muted);
302
+ background: rgba(0, 0, 0, 0.2);
303
+ padding: 4px 8px;
304
+ border-radius: 4px;
305
+ font-family: monospace;
306
+ margin-bottom: 16px;
307
+ display: inline-block;
308
+ }
309
+
310
+ .section-title {
311
+ font-size: 12.5px;
312
+ text-transform: uppercase;
313
+ letter-spacing: 1px;
314
+ color: var(--accent);
315
+ margin-bottom: 10px;
316
+ margin-top: 20px;
317
+ font-weight: 600;
318
+ }
319
+
320
+ .symbol-list {
321
+ list-style: none;
322
+ }
323
+
324
+ .symbol-item {
325
+ background: rgba(255, 255, 255, 0.03);
326
+ border: 1px solid var(--border);
327
+ border-radius: 6px;
328
+ padding: 8px 10px;
329
+ margin-bottom: 8px;
330
+ font-size: 12.5px;
331
+ }
332
+
333
+ .symbol-header {
334
+ display: flex;
335
+ justify-content: space-between;
336
+ margin-bottom: 4px;
337
+ }
338
+
339
+ .symbol-name {
340
+ font-weight: 600;
341
+ color: #f5e0dc;
342
+ }
343
+
344
+ .symbol-kind {
345
+ font-size: 10px;
346
+ padding: 1px 5px;
347
+ border-radius: 4px;
348
+ background: rgba(203, 166, 247, 0.15);
349
+ color: var(--accent);
350
+ }
351
+
352
+ .symbol-sig {
353
+ font-family: monospace;
354
+ font-size: 10.5px;
355
+ color: var(--text-muted);
356
+ overflow-x: auto;
357
+ white-space: nowrap;
358
+ padding-top: 2px;
359
+ }
360
+
361
+ .symbol-doc {
362
+ font-size: 11px;
363
+ color: #a6e3a1;
364
+ font-style: italic;
365
+ margin-top: 4px;
366
+ border-left: 2px solid #a6e3a1;
367
+ padding-left: 6px;
368
+ }
369
+
370
+ .dep-badge {
371
+ display: inline-block;
372
+ padding: 5px 8px;
373
+ border-radius: 20px;
374
+ background: rgba(245, 194, 231, 0.08);
375
+ border: 1px solid rgba(245, 194, 231, 0.2);
376
+ color: #f5c2e7;
377
+ font-size: 11.5px;
378
+ margin-right: 6px;
379
+ margin-bottom: 6px;
380
+ }
381
+
382
+ /* Detailed UI History Layout */
383
+ .session-card {
384
+ background: rgba(255, 255, 255, 0.02);
385
+ border: 1px solid var(--border);
386
+ border-radius: 8px;
387
+ margin-bottom: 16px;
388
+ overflow: hidden;
389
+ }
390
+
391
+ .session-header {
392
+ background: rgba(255, 255, 255, 0.03);
393
+ padding: 12px 16px;
394
+ cursor: pointer;
395
+ display: flex;
396
+ justify-content: space-between;
397
+ align-items: center;
398
+ border-bottom: 1px solid var(--border);
399
+ transition: background 0.3s ease;
400
+ }
401
+
402
+ .session-header:hover {
403
+ background: rgba(255, 255, 255, 0.06);
404
+ }
405
+
406
+ .session-meta {
407
+ display: flex;
408
+ flex-direction: column;
409
+ }
410
+
411
+ .session-date {
412
+ font-size: 13px;
413
+ font-weight: 600;
414
+ color: #ffffff;
415
+ }
416
+
417
+ .session-summary {
418
+ font-size: 11px;
419
+ color: var(--text-muted);
420
+ margin-top: 2px;
421
+ }
422
+
423
+ .session-savings {
424
+ font-size: 14px;
425
+ font-weight: 800;
426
+ color: #a6e3a1;
427
+ }
428
+
429
+ .session-body {
430
+ padding: 14px;
431
+ }
432
+
433
+ .history-item {
434
+ padding: 12px 0;
435
+ border-bottom: 1px solid rgba(49, 50, 68, 0.2);
436
+ display: flex;
437
+ flex-direction: column;
438
+ gap: 6px;
439
+ }
440
+
441
+ .history-item:last-child {
442
+ border-bottom: none;
443
+ padding-bottom: 0;
444
+ }
445
+
446
+ .history-row-top {
447
+ display: flex;
448
+ justify-content: space-between;
449
+ align-items: center;
450
+ }
451
+
452
+ .history-tool-badge {
453
+ font-size: 9.5px;
454
+ font-weight: 700;
455
+ color: var(--accent);
456
+ background: rgba(203, 166, 247, 0.12);
457
+ padding: 2px 6px;
458
+ border-radius: 4px;
459
+ text-transform: uppercase;
460
+ letter-spacing: 0.5px;
461
+ }
462
+
463
+ .history-time {
464
+ font-size: 10px;
465
+ color: var(--text-muted);
466
+ }
467
+
468
+ .history-target {
469
+ font-family: monospace;
470
+ font-size: 10.5px;
471
+ color: #b4befe;
472
+ background: rgba(0, 0, 0, 0.2);
473
+ padding: 4px 8px;
474
+ border-radius: 4px;
475
+ word-break: break-all;
476
+ }
477
+
478
+ .history-metrics {
479
+ display: flex;
480
+ justify-content: space-between;
481
+ align-items: center;
482
+ font-size: 11px;
483
+ color: var(--text-muted);
484
+ margin-top: 2px;
485
+ }
486
+
487
+ .history-saved {
488
+ font-weight: 700;
489
+ color: #a6e3a1; /* green */
490
+ }
491
+
492
+ .progress-bar-container {
493
+ width: 100%;
494
+ height: 4px;
495
+ background: rgba(49, 50, 68, 0.4);
496
+ border-radius: 2px;
497
+ overflow: hidden;
498
+ margin-top: 2px;
499
+ }
500
+
501
+ .progress-bar-fill {
502
+ height: 100%;
503
+ background: linear-gradient(90deg, #cba6f7, #a6e3a1);
504
+ border-radius: 2px;
505
+ transition: width 0.4s ease;
506
+ }
507
+
508
+ /* Main view area */
509
+ .view-area {
510
+ flex: 1;
511
+ position: relative;
512
+ height: 100%;
513
+ }
514
+
515
+ #network-container {
516
+ width: 100%;
517
+ height: 100%;
518
+ background-color: var(--bg-base);
519
+ }
520
+
521
+ /* Float controls */
522
+ .controls {
523
+ position: absolute;
524
+ bottom: 24px;
525
+ right: 24px;
526
+ display: flex;
527
+ gap: 12px;
528
+ z-index: 5;
529
+ }
530
+
531
+ .btn {
532
+ background: var(--bg-surface);
533
+ border: 1px solid var(--border);
534
+ color: var(--text-main);
535
+ padding: 10px 16px;
536
+ border-radius: 8px;
537
+ cursor: pointer;
538
+ font-weight: 600;
539
+ font-size: 13px;
540
+ display: flex;
541
+ align-items: center;
542
+ gap: 8px;
543
+ transition: all 0.3s ease;
544
+ box-shadow: 0 4px 12px rgba(0,0,0,0.3);
545
+ }
546
+
547
+ .btn:hover {
548
+ border-color: var(--accent);
549
+ color: #ffffff;
550
+ box-shadow: 0 4px 16px var(--accent-glow);
551
+ }
552
+
553
+ /* Sidebar Footer Logo */
554
+ .sidebar-footer {
555
+ position: absolute;
556
+ bottom: 0;
557
+ left: 0;
558
+ width: 100%;
559
+ padding: 16px 24px;
560
+ background: linear-gradient(180deg, rgba(30, 30, 46, 0) 0%, rgba(30, 30, 46, 0.95) 100%);
561
+ border-top: 1px solid rgba(49, 50, 68, 0.4);
562
+ display: flex;
563
+ justify-content: space-between;
564
+ align-items: center;
565
+ font-size: 11px;
566
+ color: var(--text-muted);
567
+ z-index: 12;
568
+ }
569
+
570
+ .sidebar-footer span {
571
+ font-weight: 600;
572
+ color: var(--accent);
573
+ }
574
+ </style>
575
+ </head>
576
+ <body>
577
+
578
+ <!-- Sidebar details panel -->
579
+ <div class="sidebar">
580
+ <div class="header">
581
+ <h1>${projectName}</h1>
582
+ <p>Project Dependencies Map</p>
583
+ </div>
584
+
585
+ <!-- Token Stats persistent widget -->
586
+ <div class="stats-panel">
587
+ <div class="stats-title">Token Savings (RTK)</div>
588
+ <div class="stats-grid">
589
+ <div class="stat-card">
590
+ <div class="stat-value" id="stat-saved-tokens">0</div>
591
+ <div class="stat-label">Tokens Saved</div>
592
+ </div>
593
+ <div class="stat-card">
594
+ <div class="stat-value" id="stat-efficiency">0%</div>
595
+ <div class="stat-label">Efficiency</div>
596
+ </div>
597
+ </div>
598
+ </div>
599
+
600
+ <!-- Tab selectors -->
601
+ <div class="tabs-header">
602
+ <button class="tab-btn active" id="tab-graph" onclick="switchTab('graph')">Graph</button>
603
+ <button class="tab-btn" id="tab-history" onclick="switchTab('history')">History (RTK)</button>
604
+ </div>
605
+
606
+ <!-- Tab panels wrapper -->
607
+ <div class="content-panel">
608
+
609
+ <!-- Panel 1: Graph/AST Details -->
610
+ <div id="panel-graph" class="tab-panel active-panel">
611
+ <div class="search-box" style="padding: 0 0 16px 0; border-bottom: none;">
612
+ <input type="text" id="search" class="search-input" placeholder="Search file or symbol...">
613
+ </div>
614
+
615
+ <div id="empty-state" class="empty-state">
616
+ <p>Select a file node in the graph to view structural signatures and skeletons.</p>
617
+ </div>
618
+
619
+ <div id="detail-card" class="detail-card">
620
+ <div id="detail-title" class="detail-title">Parser.ts</div>
621
+ <div id="detail-path" class="detail-path">src/parser.ts</div>
622
+
623
+ <div class="section-title">Imports & Dependencies</div>
624
+ <div id="detail-deps"></div>
625
+
626
+ <div class="section-title">Symbols & Signatures (AST)</div>
627
+ <ul id="detail-symbols" class="symbol-list"></ul>
628
+ </div>
629
+ </div>
630
+
631
+ <!-- Panel 2: Persistent History Log -->
632
+ <div id="panel-history" class="tab-panel">
633
+ <div id="history-container">
634
+ <!-- Populated dynamically via JS -->
635
+ </div>
636
+ </div>
637
+
638
+ </div>
639
+
640
+ <!-- Graphical Footer Branding -->
641
+ <div class="sidebar-footer">
642
+ <div>Mapped by <span>SmartGraphIndexer</span></div>
643
+ <div>v1.0.0</div>
644
+ </div>
645
+ </div>
646
+
647
+ <!-- Graph interactive area -->
648
+ <div class="view-area">
649
+ <div id="network-container"></div>
650
+ <div class="controls">
651
+ <button class="btn" onclick="fitGraph()">Fit Graph</button>
652
+ <button class="btn" onclick="togglePhysics()">Pause Physics</button>
653
+ </div>
654
+ </div>
655
+
656
+ <script type="text/javascript">
657
+ const rawNodes = ${JSON.stringify(nodes)};
658
+ const rawEdges = ${JSON.stringify(edges)};
659
+ const tokenStats = ${JSON.stringify(report.tokenStats || {})};
660
+
661
+ // Populate token stats in UI
662
+ function formatNumber(num) {
663
+ if (num === undefined || isNaN(num)) return '0';
664
+ if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M';
665
+ if (num >= 1000) return (num / 1000).toFixed(1) + 'K';
666
+ return num.toString();
667
+ }
668
+
669
+ if (tokenStats && tokenStats.totalTokensSaved !== undefined) {
670
+ document.getElementById('stat-saved-tokens').innerText = formatNumber(tokenStats.totalTokensSaved);
671
+ document.getElementById('stat-efficiency').innerText = (tokenStats.efficiencyPercentage || 0) + '%';
672
+ }
673
+
674
+ // Switch Tabs
675
+ function switchTab(tabName) {
676
+ document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active'));
677
+ document.querySelectorAll('.tab-panel').forEach(panel => panel.classList.remove('active-panel'));
678
+
679
+ if (tabName === 'graph') {
680
+ document.getElementById('tab-graph').classList.add('active');
681
+ document.getElementById('panel-graph').classList.add('active-panel');
682
+ } else {
683
+ document.getElementById('tab-history').classList.add('active');
684
+ document.getElementById('panel-history').classList.add('active-panel');
685
+ renderHistory();
686
+ }
687
+ }
688
+
689
+ // Render history grouped by session
690
+ function renderHistory() {
691
+ const container = document.getElementById('history-container');
692
+ container.innerHTML = '';
693
+
694
+ if (!tokenStats || !tokenStats.history || tokenStats.history.length === 0) {
695
+ container.innerHTML = '<div class="empty-state">No stats records found. Start using the MCP to log savings.</div>';
696
+ return;
697
+ }
698
+
699
+ // Group by sessionId
700
+ const sessions = {};
701
+ tokenStats.history.forEach(item => {
702
+ if (!sessions[item.sessionId]) {
703
+ sessions[item.sessionId] = [];
704
+ }
705
+ sessions[item.sessionId].push(item);
706
+ });
707
+
708
+ // Sort sessions descending by date
709
+ const sortedSessionIds = Object.keys(sessions).sort((a, b) => new Date(b).getTime() - new Date(a).getTime());
710
+
711
+ sortedSessionIds.forEach((sessId, index) => {
712
+ const items = sessions[sessId];
713
+ const date = new Date(sessId);
714
+ const dateStr = date.toLocaleDateString('en-US') + ' ' + date.toLocaleTimeString('en-US', {hour: '2-digit', minute:'2-digit'});
715
+
716
+ let sessionSaved = 0;
717
+ let sessionUsed = 0;
718
+ let sessionRaw = 0;
719
+ let sessionTime = 0;
720
+ let sessionCalls = items.length;
721
+
722
+ items.forEach(it => {
723
+ sessionSaved += it.saved;
724
+ sessionUsed += it.actual;
725
+ sessionRaw += it.alternative;
726
+ sessionTime += (it.execTimeMs || 0);
727
+ });
728
+
729
+ const sessionEff = sessionRaw > 0 ? Math.round((sessionSaved / sessionRaw) * 100) : 0;
730
+ const avgTime = sessionCalls > 0 ? Math.round(sessionTime / sessionCalls) : 0;
731
+
732
+ const card = document.createElement('div');
733
+ card.className = 'session-card';
734
+
735
+ const header = document.createElement('div');
736
+ header.className = 'session-header';
737
+ header.onclick = () => {
738
+ const body = card.querySelector('.session-body');
739
+ body.style.display = body.style.display === 'none' ? 'block' : 'none';
740
+ };
741
+
742
+ header.innerHTML = \`
743
+ <div class="session-meta">
744
+ <span class="session-date">\${dateStr}</span>
745
+ <span class="session-summary">\${sessionCalls} optimized requests</span>
746
+ </div>
747
+ <div class="session-savings">+\${formatNumber(sessionSaved)}</div>
748
+ \`;
749
+
750
+ const body = document.createElement('div');
751
+ body.className = 'session-body';
752
+ body.style.display = index === 0 ? 'block' : 'none';
753
+
754
+ // Add RTK Console Output Mock
755
+ const consoleMock = document.createElement('div');
756
+ consoleMock.style.cssText = 'background: rgba(0, 0, 0, 0.35); border: 1px solid var(--border); padding: 12px; border-radius: 6px; font-family: monospace; font-size: 11.5px; margin-bottom: 12px; color: #a6e3a1; border-left: 3px solid var(--accent); line-height: 1.4;';
757
+
758
+ // Build meter block
759
+ const filledBlocks = Math.round(sessionEff / 10);
760
+ const emptyBlocks = 10 - filledBlocks;
761
+ const meterStr = '='.repeat(filledBlocks) + ' '.repeat(emptyBlocks);
762
+
763
+ consoleMock.innerHTML = \`
764
+ <div style="font-weight: 800; color: var(--accent); margin-bottom: 6px;">RTK Token Savings (Session Scope)</div>
765
+ <div style="border-bottom: 1px dashed rgba(49, 50, 68, 0.6); margin-bottom: 6px; padding-bottom: 6px;">
766
+ <div>Total requests: \${sessionCalls}</div>
767
+ <div>Input tokens (Used): \${sessionUsed}</div>
768
+ <div>Raw cost (No MCP): \${sessionRaw}</div>
769
+ <div>Tokens saved: \${sessionSaved} (\${sessionEff}.0%)</div>
770
+ <div>Total exec time: \${sessionTime}ms (avg \${avgTime}ms)</div>
771
+ </div>
772
+ <div style="display: flex; align-items: center; gap: 8px;">
773
+ <div>Efficiency meter:</div>
774
+ <div style="color: #a6e3a1; font-weight: bold;">[\${meterStr}] \${sessionEff}%</div>
775
+ </div>
776
+ \`;
777
+ body.appendChild(consoleMock);
778
+
779
+ // Add Divider Header
780
+ const requestsHeader = document.createElement('div');
781
+ requestsHeader.className = 'section-title';
782
+ requestsHeader.style.cssText = 'margin-top: 12px; margin-bottom: 8px; font-size: 11px;';
783
+ requestsHeader.innerText = 'By Request';
784
+ body.appendChild(requestsHeader);
785
+
786
+ items.forEach(it => {
787
+ const itDate = new Date(it.timestamp);
788
+ const timeStr = itDate.toLocaleTimeString('en-US', {hour: '2-digit', minute:'2-digit', second:'2-digit'});
789
+ const eff = it.alternative > 0 ? Math.round((it.saved / it.alternative) * 100) : 0;
790
+
791
+ const itemDiv = document.createElement('div');
792
+ itemDiv.className = 'history-item';
793
+
794
+ let targetHtml = it.target ? \`<div class="history-target">\${it.target}</div>\` : '';
795
+
796
+ itemDiv.innerHTML = \`
797
+ <div class="history-row-top">
798
+ <span class="history-tool-badge">\${it.toolName}</span>
799
+ <span class="history-time">\${timeStr}</span>
800
+ </div>
801
+ \${targetHtml}
802
+ <div class="history-metrics">
803
+ <span>Used: <strong>\${it.actual}</strong> | Raw: <strong>\${it.alternative}</strong> | Time: <strong>\${it.execTimeMs || 0}ms</strong></span>
804
+ <span class="history-saved">Saved: <strong>+\&nbsp;\${formatNumber(it.saved)}</strong> (\${eff}%)</span>
805
+ </div>
806
+ <div class="progress-bar-container">
807
+ <div class="progress-bar-fill" style="width: \${eff}%"></div>
808
+ </div>
809
+ \`;
810
+ body.appendChild(itemDiv);
811
+ });
812
+
813
+ card.appendChild(header);
814
+ card.appendChild(body);
815
+ container.appendChild(card);
816
+ });
817
+ }
818
+
819
+ const containerNet = document.getElementById('network-container');
820
+ const nodesDataSet = new vis.DataSet(rawNodes);
821
+ const edgesDataSet = new vis.DataSet(rawEdges);
822
+
823
+ const data = {
824
+ nodes: nodesDataSet,
825
+ edges: edgesDataSet
826
+ };
827
+
828
+ const options = {
829
+ nodes: {
830
+ scaling: {
831
+ min: 10,
832
+ max: 30
833
+ }
834
+ },
835
+ edges: {
836
+ smooth: {
837
+ type: 'continuous',
838
+ roundness: 0.5
839
+ }
840
+ },
841
+ physics: {
842
+ stabilization: true,
843
+ barnesHut: {
844
+ gravitationalConstant: -3000,
845
+ centralGravity: 0.3,
846
+ springLength: 95,
847
+ springConstant: 0.04
848
+ }
849
+ },
850
+ interaction: {
851
+ hover: true,
852
+ tooltipDelay: 100
853
+ }
854
+ };
855
+
856
+ const network = new vis.Network(containerNet, data, options);
857
+
858
+ // Event listener for click
859
+ network.on("click", function (params) {
860
+ if (params.nodes.length > 0) {
861
+ const nodeId = params.nodes[0];
862
+ const node = nodesDataSet.get(nodeId);
863
+ showDetails(node);
864
+ } else {
865
+ hideDetails();
866
+ }
867
+ });
868
+
869
+ function showDetails(node) {
870
+ document.getElementById('empty-state').style.display = 'none';
871
+ const card = document.getElementById('detail-card');
872
+ card.style.display = 'block';
873
+
874
+ document.getElementById('detail-title').innerText = node.label;
875
+ document.getElementById('detail-path').innerText = node.filePath;
876
+
877
+ // Render deps
878
+ const depsContainer = document.getElementById('detail-deps');
879
+ depsContainer.innerHTML = '';
880
+ if (node.dependencies.length === 0) {
881
+ depsContainer.innerHTML = '<span style="color: var(--text-muted); font-size:13px;">No external dependencies</span>';
882
+ } else {
883
+ node.dependencies.forEach(dep => {
884
+ const span = document.createElement('span');
885
+ span.className = 'dep-badge';
886
+ span.innerText = dep;
887
+ depsContainer.appendChild(span);
888
+ });
889
+ }
890
+
891
+ // Render symbols
892
+ const symbolsContainer = document.getElementById('detail-symbols');
893
+ symbolsContainer.innerHTML = '';
894
+ if (node.symbols.length === 0) {
895
+ symbolsContainer.innerHTML = '<li style="color: var(--text-muted); font-size:13px;">No public symbols extracted</li>';
896
+ } else {
897
+ node.symbols.forEach(sym => {
898
+ const li = document.createElement('li');
899
+ li.className = 'symbol-item';
900
+
901
+ let docHtml = sym.docstring ? \`<div class="symbol-doc">\${sym.docstring}</div>\` : '';
902
+
903
+ li.innerHTML = \`
904
+ <div class="symbol-header">
905
+ <span class="symbol-name">\${sym.name}</span>
906
+ <span class="symbol-kind">\${sym.kind}</span>
907
+ </div>
908
+ <div class="symbol-sig">\${sym.signature}</div>
909
+ \${docHtml}
910
+ \`;
911
+ symbolsContainer.appendChild(li);
912
+ });
913
+ }
914
+ }
915
+
916
+ function hideDetails() {
917
+ document.getElementById('empty-state').style.display = 'block';
918
+ document.getElementById('detail-card').style.display = 'none';
919
+ }
920
+
921
+ // Search bar filtering
922
+ document.getElementById('search').addEventListener('input', function(e) {
923
+ const val = e.target.value.toLowerCase();
924
+ if (!val) {
925
+ nodesDataSet.forEach(node => {
926
+ nodesDataSet.update({ id: node.id, hidden: false });
927
+ });
928
+ return;
929
+ }
930
+
931
+ nodesDataSet.forEach(node => {
932
+ const fileMatch = node.filePath.toLowerCase().includes(val);
933
+ const symbolMatch = node.symbols.some(s => s.name.toLowerCase().includes(val));
934
+
935
+ nodesDataSet.update({
936
+ id: node.id,
937
+ hidden: !(fileMatch || symbolMatch)
938
+ });
939
+ });
940
+ });
941
+
942
+ function fitGraph() {
943
+ network.fit({ animation: true });
944
+ }
945
+
946
+ let physicsEnabled = true;
947
+ function togglePhysics() {
948
+ physicsEnabled = !physicsEnabled;
949
+ network.setOptions({ physics: physicsEnabled });
950
+ event.target.innerText = physicsEnabled ? 'Pause Physics' : 'Resume Physics';
951
+ }
952
+ </script>
953
+ </body>
954
+ </html>
955
+ `;
956
+ }