@diegonogueiradev_/mcp-graph 1.0.0 → 2.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 (219) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +240 -0
  3. package/dist/api/middleware/error-handler.d.ts +3 -0
  4. package/dist/api/middleware/error-handler.d.ts.map +1 -0
  5. package/dist/api/middleware/error-handler.js +35 -0
  6. package/dist/api/middleware/error-handler.js.map +1 -0
  7. package/dist/api/middleware/validate.d.ts +5 -0
  8. package/dist/api/middleware/validate.d.ts.map +1 -0
  9. package/dist/api/middleware/validate.js +23 -0
  10. package/dist/api/middleware/validate.js.map +1 -0
  11. package/dist/api/router.d.ts +11 -0
  12. package/dist/api/router.d.ts.map +1 -0
  13. package/dist/api/router.js +41 -0
  14. package/dist/api/router.js.map +1 -0
  15. package/dist/api/routes/capture.d.ts +3 -0
  16. package/dist/api/routes/capture.d.ts.map +1 -0
  17. package/dist/api/routes/capture.js +31 -0
  18. package/dist/api/routes/capture.js.map +1 -0
  19. package/dist/api/routes/context.d.ts +4 -0
  20. package/dist/api/routes/context.d.ts.map +1 -0
  21. package/dist/api/routes/context.js +25 -0
  22. package/dist/api/routes/context.js.map +1 -0
  23. package/dist/api/routes/docs-cache.d.ts +4 -0
  24. package/dist/api/routes/docs-cache.d.ts.map +1 -0
  25. package/dist/api/routes/docs-cache.js +79 -0
  26. package/dist/api/routes/docs-cache.js.map +1 -0
  27. package/dist/api/routes/edges.d.ts +4 -0
  28. package/dist/api/routes/edges.d.ts.map +1 -0
  29. package/dist/api/routes/edges.js +50 -0
  30. package/dist/api/routes/edges.js.map +1 -0
  31. package/dist/api/routes/events.d.ts +4 -0
  32. package/dist/api/routes/events.d.ts.map +1 -0
  33. package/dist/api/routes/events.js +37 -0
  34. package/dist/api/routes/events.js.map +1 -0
  35. package/dist/api/routes/graph.d.ts +4 -0
  36. package/dist/api/routes/graph.d.ts.map +1 -0
  37. package/dist/api/routes/graph.js +39 -0
  38. package/dist/api/routes/graph.js.map +1 -0
  39. package/dist/api/routes/import.d.ts +4 -0
  40. package/dist/api/routes/import.d.ts.map +1 -0
  41. package/dist/api/routes/import.js +92 -0
  42. package/dist/api/routes/import.js.map +1 -0
  43. package/dist/api/routes/insights.d.ts +4 -0
  44. package/dist/api/routes/insights.d.ts.map +1 -0
  45. package/dist/api/routes/insights.js +40 -0
  46. package/dist/api/routes/insights.js.map +1 -0
  47. package/dist/api/routes/integrations.d.ts +4 -0
  48. package/dist/api/routes/integrations.d.ts.map +1 -0
  49. package/dist/api/routes/integrations.js +56 -0
  50. package/dist/api/routes/integrations.js.map +1 -0
  51. package/dist/api/routes/nodes.d.ts +4 -0
  52. package/dist/api/routes/nodes.d.ts.map +1 -0
  53. package/dist/api/routes/nodes.js +123 -0
  54. package/dist/api/routes/nodes.js.map +1 -0
  55. package/dist/api/routes/project.d.ts +4 -0
  56. package/dist/api/routes/project.d.ts.map +1 -0
  57. package/dist/api/routes/project.js +33 -0
  58. package/dist/api/routes/project.js.map +1 -0
  59. package/dist/api/routes/search.d.ts +4 -0
  60. package/dist/api/routes/search.d.ts.map +1 -0
  61. package/dist/api/routes/search.js +25 -0
  62. package/dist/api/routes/search.js.map +1 -0
  63. package/dist/api/routes/skills.d.ts +3 -0
  64. package/dist/api/routes/skills.d.ts.map +1 -0
  65. package/dist/api/routes/skills.js +16 -0
  66. package/dist/api/routes/skills.js.map +1 -0
  67. package/dist/api/routes/stats.d.ts +4 -0
  68. package/dist/api/routes/stats.d.ts.map +1 -0
  69. package/dist/api/routes/stats.js +14 -0
  70. package/dist/api/routes/stats.js.map +1 -0
  71. package/dist/cli/commands/import-cmd.d.ts +3 -0
  72. package/dist/cli/commands/import-cmd.d.ts.map +1 -0
  73. package/dist/cli/commands/import-cmd.js +38 -0
  74. package/dist/cli/commands/import-cmd.js.map +1 -0
  75. package/dist/cli/commands/init.d.ts +3 -0
  76. package/dist/cli/commands/init.d.ts.map +1 -0
  77. package/dist/cli/commands/init.js +55 -0
  78. package/dist/cli/commands/init.js.map +1 -0
  79. package/dist/cli/commands/serve.d.ts +3 -0
  80. package/dist/cli/commands/serve.d.ts.map +1 -0
  81. package/dist/cli/commands/serve.js +18 -0
  82. package/dist/cli/commands/serve.js.map +1 -0
  83. package/dist/cli/commands/stats.d.ts +3 -0
  84. package/dist/cli/commands/stats.d.ts.map +1 -0
  85. package/dist/cli/commands/stats.js +39 -0
  86. package/dist/cli/commands/stats.js.map +1 -0
  87. package/dist/cli/index.d.ts +3 -0
  88. package/dist/cli/index.d.ts.map +1 -0
  89. package/dist/cli/index.js +17 -0
  90. package/dist/cli/index.js.map +1 -0
  91. package/dist/core/capture/content-extractor.d.ts +21 -0
  92. package/dist/core/capture/content-extractor.d.ts.map +1 -0
  93. package/dist/core/capture/content-extractor.js +74 -0
  94. package/dist/core/capture/content-extractor.js.map +1 -0
  95. package/dist/core/capture/web-capture.d.ts +20 -0
  96. package/dist/core/capture/web-capture.d.ts.map +1 -0
  97. package/dist/core/capture/web-capture.js +51 -0
  98. package/dist/core/capture/web-capture.js.map +1 -0
  99. package/dist/core/config/config-loader.d.ts +3 -0
  100. package/dist/core/config/config-loader.d.ts.map +1 -0
  101. package/dist/core/config/config-loader.js +43 -0
  102. package/dist/core/config/config-loader.js.map +1 -0
  103. package/dist/core/config/config-schema.d.ts +11 -0
  104. package/dist/core/config/config-schema.d.ts.map +1 -0
  105. package/dist/core/config/config-schema.js +12 -0
  106. package/dist/core/config/config-schema.js.map +1 -0
  107. package/dist/core/docs/docs-cache-store.d.ts +24 -0
  108. package/dist/core/docs/docs-cache-store.d.ts.map +1 -0
  109. package/dist/core/docs/docs-cache-store.js +61 -0
  110. package/dist/core/docs/docs-cache-store.js.map +1 -0
  111. package/dist/core/docs/docs-syncer.d.ts +13 -0
  112. package/dist/core/docs/docs-syncer.d.ts.map +1 -0
  113. package/dist/core/docs/docs-syncer.js +38 -0
  114. package/dist/core/docs/docs-syncer.js.map +1 -0
  115. package/dist/core/events/event-bus.d.ts +26 -0
  116. package/dist/core/events/event-bus.d.ts.map +1 -0
  117. package/dist/core/events/event-bus.js +47 -0
  118. package/dist/core/events/event-bus.js.map +1 -0
  119. package/dist/core/events/event-types.d.ts +57 -0
  120. package/dist/core/events/event-types.d.ts.map +1 -0
  121. package/dist/core/events/event-types.js +2 -0
  122. package/dist/core/events/event-types.js.map +1 -0
  123. package/dist/core/graph/mermaid-export.d.ts +9 -0
  124. package/dist/core/graph/mermaid-export.d.ts.map +1 -0
  125. package/dist/core/graph/mermaid-export.js +80 -0
  126. package/dist/core/graph/mermaid-export.js.map +1 -0
  127. package/dist/core/importer/prd-to-graph.d.ts.map +1 -1
  128. package/dist/core/importer/prd-to-graph.js +7 -0
  129. package/dist/core/importer/prd-to-graph.js.map +1 -1
  130. package/dist/core/insights/bottleneck-detector.d.ts +31 -0
  131. package/dist/core/insights/bottleneck-detector.d.ts.map +1 -0
  132. package/dist/core/insights/bottleneck-detector.js +69 -0
  133. package/dist/core/insights/bottleneck-detector.js.map +1 -0
  134. package/dist/core/insights/metrics-calculator.d.ts +31 -0
  135. package/dist/core/insights/metrics-calculator.d.ts.map +1 -0
  136. package/dist/core/insights/metrics-calculator.js +78 -0
  137. package/dist/core/insights/metrics-calculator.js.map +1 -0
  138. package/dist/core/insights/skill-recommender.d.ts +21 -0
  139. package/dist/core/insights/skill-recommender.d.ts.map +1 -0
  140. package/dist/core/insights/skill-recommender.js +129 -0
  141. package/dist/core/insights/skill-recommender.js.map +1 -0
  142. package/dist/core/integrations/serena-reader.d.ts +18 -0
  143. package/dist/core/integrations/serena-reader.d.ts.map +1 -0
  144. package/dist/core/integrations/serena-reader.js +50 -0
  145. package/dist/core/integrations/serena-reader.js.map +1 -0
  146. package/dist/core/integrations/tool-status.d.ts +18 -0
  147. package/dist/core/integrations/tool-status.d.ts.map +1 -0
  148. package/dist/core/integrations/tool-status.js +92 -0
  149. package/dist/core/integrations/tool-status.js.map +1 -0
  150. package/dist/core/parser/file-reader.d.ts +13 -0
  151. package/dist/core/parser/file-reader.d.ts.map +1 -0
  152. package/dist/core/parser/file-reader.js +52 -0
  153. package/dist/core/parser/file-reader.js.map +1 -0
  154. package/dist/core/parser/read-html.d.ts +7 -0
  155. package/dist/core/parser/read-html.d.ts.map +1 -0
  156. package/dist/core/parser/read-html.js +51 -0
  157. package/dist/core/parser/read-html.js.map +1 -0
  158. package/dist/core/parser/read-pdf.d.ts +10 -0
  159. package/dist/core/parser/read-pdf.d.ts.map +1 -0
  160. package/dist/core/parser/read-pdf.js +16 -0
  161. package/dist/core/parser/read-pdf.js.map +1 -0
  162. package/dist/core/planner/next-task.d.ts.map +1 -1
  163. package/dist/core/planner/next-task.js +4 -1
  164. package/dist/core/planner/next-task.js.map +1 -1
  165. package/dist/core/search/fts-search.d.ts.map +1 -1
  166. package/dist/core/search/fts-search.js +6 -1
  167. package/dist/core/search/fts-search.js.map +1 -1
  168. package/dist/core/store/migrations.d.ts.map +1 -1
  169. package/dist/core/store/migrations.js +38 -0
  170. package/dist/core/store/migrations.js.map +1 -1
  171. package/dist/core/store/sqlite-store.d.ts +7 -0
  172. package/dist/core/store/sqlite-store.d.ts.map +1 -1
  173. package/dist/core/store/sqlite-store.js +28 -3
  174. package/dist/core/store/sqlite-store.js.map +1 -1
  175. package/dist/core/utils/logger.d.ts +1 -0
  176. package/dist/core/utils/logger.d.ts.map +1 -1
  177. package/dist/core/utils/logger.js +5 -0
  178. package/dist/core/utils/logger.js.map +1 -1
  179. package/dist/mcp/init-project.d.ts.map +1 -1
  180. package/dist/mcp/init-project.js +12 -16
  181. package/dist/mcp/init-project.js.map +1 -1
  182. package/dist/mcp/server.js +16 -2
  183. package/dist/mcp/server.js.map +1 -1
  184. package/dist/mcp/stdio.js +0 -0
  185. package/dist/mcp/tools/export-mermaid.d.ts +4 -0
  186. package/dist/mcp/tools/export-mermaid.d.ts.map +1 -0
  187. package/dist/mcp/tools/export-mermaid.js +27 -0
  188. package/dist/mcp/tools/export-mermaid.js.map +1 -0
  189. package/dist/mcp/tools/index.d.ts.map +1 -1
  190. package/dist/mcp/tools/index.js +2 -0
  191. package/dist/mcp/tools/index.js.map +1 -1
  192. package/dist/web/dashboard/dist/assets/code-graph-tab-jvBo8Q9t.js +1 -0
  193. package/dist/web/dashboard/dist/assets/constants-CLJl-f3f.js +1 -0
  194. package/dist/web/dashboard/dist/assets/graph-tab-BoKfDlvO.js +1 -0
  195. package/dist/web/dashboard/dist/assets/graph-utils-BZV40eAE.css +1 -0
  196. package/dist/web/dashboard/dist/assets/graph-utils-uGOH4eMw.js +23 -0
  197. package/dist/web/dashboard/dist/assets/index-DM_LGeRr.css +1 -0
  198. package/dist/web/dashboard/dist/assets/index-DrHbgcp5.js +53 -0
  199. package/dist/web/dashboard/dist/assets/insights-tab-D7sHV2xV.js +1 -0
  200. package/dist/web/dashboard/dist/assets/prd-backlog-tab-C_Uq1Qte.js +1 -0
  201. package/dist/web/dashboard/dist/index.html +13 -0
  202. package/dist/web/public/css/styles.css +646 -0
  203. package/dist/web/public/index.html +209 -0
  204. package/dist/web/public/js/api-client.js +85 -0
  205. package/dist/web/public/js/app.js +112 -0
  206. package/dist/web/public/js/capture-form.js +196 -0
  207. package/dist/web/public/js/filters.js +94 -0
  208. package/dist/web/public/js/force-graph-renderer.js +498 -0
  209. package/dist/web/public/js/graph-renderer.js +62 -0
  210. package/dist/web/public/js/import-form.js +105 -0
  211. package/dist/web/public/js/node-detail.js +106 -0
  212. package/dist/web/public/js/tabs/code-graph-tab.js +66 -0
  213. package/dist/web/public/js/tabs/graph-tab.js +238 -0
  214. package/dist/web/public/js/tabs/insights-tab.js +236 -0
  215. package/dist/web/public/js/tabs/knowledge-tab.js +201 -0
  216. package/dist/web/public/js/tabs/prd-backlog-tab.js +167 -0
  217. package/dist/web/public/vendor/force-graph.min.js +5 -0
  218. package/dist/web/public/vendor/mermaid.min.js +2843 -0
  219. package/package.json +22 -3
@@ -0,0 +1,105 @@
1
+ /**
2
+ * PRD import form with drag-and-drop
3
+ */
4
+ const ImportForm = (() => {
5
+ let selectedFile = null;
6
+
7
+ function init() {
8
+ const modal = document.getElementById('import-modal');
9
+ const dropZone = document.getElementById('drop-zone');
10
+ const fileInput = document.getElementById('file-input');
11
+ const submitBtn = document.getElementById('import-submit');
12
+ const cancelBtn = document.getElementById('import-cancel');
13
+ const closeBtn = document.getElementById('modal-close');
14
+ const openBtn = document.getElementById('btn-import-prd');
15
+
16
+ if (!modal || !dropZone || !fileInput) return;
17
+
18
+ openBtn?.addEventListener('click', () => open());
19
+ closeBtn?.addEventListener('click', () => close());
20
+ cancelBtn?.addEventListener('click', () => close());
21
+
22
+ // Drop zone events
23
+ dropZone.addEventListener('click', () => fileInput.click());
24
+ dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('drag-over'); });
25
+ dropZone.addEventListener('dragleave', () => dropZone.classList.remove('drag-over'));
26
+ dropZone.addEventListener('drop', (e) => {
27
+ e.preventDefault();
28
+ dropZone.classList.remove('drag-over');
29
+ if (e.dataTransfer.files.length) selectFile(e.dataTransfer.files[0]);
30
+ });
31
+ fileInput.addEventListener('change', () => {
32
+ if (fileInput.files.length) selectFile(fileInput.files[0]);
33
+ });
34
+
35
+ submitBtn?.addEventListener('click', () => submit());
36
+
37
+ // Close on backdrop click
38
+ modal.addEventListener('click', (e) => {
39
+ if (e.target === modal) close();
40
+ });
41
+ }
42
+
43
+ function open() {
44
+ selectedFile = null;
45
+ const modal = document.getElementById('import-modal');
46
+ const status = document.getElementById('import-status');
47
+ const submitBtn = document.getElementById('import-submit');
48
+ const dropZone = document.getElementById('drop-zone');
49
+
50
+ if (status) status.innerHTML = '';
51
+ if (submitBtn) submitBtn.disabled = true;
52
+ if (dropZone) dropZone.querySelector('p').textContent = 'Drag & drop a file here\nor click to select';
53
+
54
+ modal?.showModal();
55
+ }
56
+
57
+ function close() {
58
+ document.getElementById('import-modal')?.close();
59
+ selectedFile = null;
60
+ }
61
+
62
+ function selectFile(file) {
63
+ selectedFile = file;
64
+ const dropZone = document.getElementById('drop-zone');
65
+ const submitBtn = document.getElementById('import-submit');
66
+
67
+ if (dropZone) dropZone.querySelector('p').textContent = `Selected: ${file.name} (${formatSize(file.size)})`;
68
+ if (submitBtn) submitBtn.disabled = false;
69
+ }
70
+
71
+ async function submit() {
72
+ if (!selectedFile) return;
73
+
74
+ const submitBtn = document.getElementById('import-submit');
75
+ const status = document.getElementById('import-status');
76
+ const force = document.getElementById('import-force')?.checked || false;
77
+
78
+ if (submitBtn) { submitBtn.disabled = true; submitBtn.textContent = 'Importing...'; }
79
+ if (status) status.innerHTML = '<p class="info">Uploading and processing...</p>';
80
+
81
+ try {
82
+ const result = await ApiClient.importFile(selectedFile, force);
83
+ if (status) {
84
+ status.innerHTML = `<p class="success">Imported ${result.nodesCreated} nodes and ${result.edgesCreated} edges.</p>`;
85
+ }
86
+ // Refresh dashboard
87
+ setTimeout(() => {
88
+ close();
89
+ document.dispatchEvent(new CustomEvent('import-completed'));
90
+ }, 1500);
91
+ } catch (err) {
92
+ if (status) status.innerHTML = `<p class="error">${err.message}</p>`;
93
+ } finally {
94
+ if (submitBtn) { submitBtn.disabled = false; submitBtn.textContent = 'Import'; }
95
+ }
96
+ }
97
+
98
+ function formatSize(bytes) {
99
+ if (bytes < 1024) return bytes + ' B';
100
+ if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
101
+ return (bytes / 1048576).toFixed(1) + ' MB';
102
+ }
103
+
104
+ return { init, open, close };
105
+ })();
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Node detail sidebar panel
3
+ */
4
+ const NodeDetail = (() => {
5
+ const STATUS_COLORS = {
6
+ done: '#4caf50', in_progress: '#2196f3', blocked: '#f44336',
7
+ backlog: '#9e9e9e', ready: '#ff9800',
8
+ };
9
+
10
+ function show(node) {
11
+ const panel = document.getElementById('detail-panel');
12
+ const title = document.getElementById('detail-title');
13
+ const body = document.getElementById('detail-body');
14
+ if (!panel || !body || !title) return;
15
+
16
+ title.textContent = node.title;
17
+ body.innerHTML = buildDetail(node);
18
+ panel.classList.remove('hidden');
19
+ }
20
+
21
+ function hide() {
22
+ const panel = document.getElementById('detail-panel');
23
+ if (panel) panel.classList.add('hidden');
24
+ }
25
+
26
+ function buildDetail(node) {
27
+ const color = STATUS_COLORS[node.status] || '#9e9e9e';
28
+ const sections = [];
29
+
30
+ sections.push(`
31
+ <div class="detail-row">
32
+ <span class="detail-label">ID</span>
33
+ <code class="detail-value">${esc(node.id)}</code>
34
+ </div>
35
+ <div class="detail-row">
36
+ <span class="detail-label">Type</span>
37
+ <span class="badge badge-type">${esc(node.type)}</span>
38
+ </div>
39
+ <div class="detail-row">
40
+ <span class="detail-label">Status</span>
41
+ <span class="badge" style="background:${color};color:#fff">${esc(node.status)}</span>
42
+ </div>
43
+ <div class="detail-row">
44
+ <span class="detail-label">Priority</span>
45
+ <span class="detail-value">${node.priority}</span>
46
+ </div>
47
+ `);
48
+
49
+ if (node.xpSize) {
50
+ sections.push(`<div class="detail-row"><span class="detail-label">Size</span><span class="detail-value">${esc(node.xpSize)}</span></div>`);
51
+ }
52
+ if (node.estimateMinutes) {
53
+ sections.push(`<div class="detail-row"><span class="detail-label">Estimate</span><span class="detail-value">${node.estimateMinutes}min</span></div>`);
54
+ }
55
+ if (node.sprint) {
56
+ sections.push(`<div class="detail-row"><span class="detail-label">Sprint</span><span class="detail-value">${esc(node.sprint)}</span></div>`);
57
+ }
58
+ if (node.description) {
59
+ sections.push(`<div class="detail-section"><h4>Description</h4><p>${esc(node.description)}</p></div>`);
60
+ }
61
+ if (node.acceptanceCriteria?.length) {
62
+ sections.push(`<div class="detail-section"><h4>Acceptance Criteria</h4><ul>${node.acceptanceCriteria.map(ac => `<li>${esc(ac)}</li>`).join('')}</ul></div>`);
63
+ }
64
+ if (node.tags?.length) {
65
+ sections.push(`<div class="detail-row"><span class="detail-label">Tags</span><span class="detail-value">${node.tags.map(t => `<span class="tag">${esc(t)}</span>`).join(' ')}</span></div>`);
66
+ }
67
+ if (node.sourceRef) {
68
+ sections.push(`<div class="detail-row"><span class="detail-label">Source</span><span class="detail-value">${esc(node.sourceRef.file)}${node.sourceRef.startLine ? ':' + node.sourceRef.startLine : ''}</span></div>`);
69
+ }
70
+
71
+ sections.push(`<div class="detail-row"><span class="detail-label">Created</span><span class="detail-value">${formatDate(node.createdAt)}</span></div>`);
72
+ sections.push(`<div class="detail-row"><span class="detail-label">Updated</span><span class="detail-value">${formatDate(node.updatedAt)}</span></div>`);
73
+
74
+ return sections.join('');
75
+ }
76
+
77
+ function esc(text) {
78
+ if (!text) return '';
79
+ const div = document.createElement('div');
80
+ div.textContent = String(text);
81
+ return div.innerHTML;
82
+ }
83
+
84
+ function formatDate(iso) {
85
+ if (!iso) return '-';
86
+ try {
87
+ return new Date(iso).toLocaleString();
88
+ } catch {
89
+ return iso;
90
+ }
91
+ }
92
+
93
+ function init() {
94
+ document.getElementById('detail-close')?.addEventListener('click', hide);
95
+ document.addEventListener('node-clicked', async (e) => {
96
+ try {
97
+ const node = await ApiClient.getNode(e.detail.nodeId);
98
+ if (node) show(node);
99
+ } catch {
100
+ // Node may not match id format from mermaid
101
+ }
102
+ });
103
+ }
104
+
105
+ return { init, show, hide };
106
+ })();
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Tab 3: Code Graph — GitNexus iframe + Serena fallback
3
+ */
4
+ const CodeGraphTab = (() => {
5
+ async function load() {
6
+ const container = document.getElementById('tab-code-graph');
7
+ if (!container) return;
8
+
9
+ try {
10
+ const status = await ApiClient.request('/integrations/status');
11
+
12
+ if (status.gitnexus?.running) {
13
+ container.innerHTML = `
14
+ <div class="iframe-container">
15
+ <div class="iframe-header">
16
+ <span class="badge status-done">GitNexus Running</span>
17
+ <a href="${esc(status.gitnexus.url)}" target="_blank" class="btn btn-sm">Open in new tab</a>
18
+ </div>
19
+ <iframe src="${esc(status.gitnexus.url)}" class="gitnexus-iframe" title="GitNexus Code Graph"></iframe>
20
+ </div>
21
+ `;
22
+ return;
23
+ }
24
+
25
+ // Fallback: show Serena memories as static overview
26
+ let html = `
27
+ <div class="fallback-panel">
28
+ <div class="fallback-notice">
29
+ <h3>Code Graph (GitNexus not running)</h3>
30
+ <p>Start GitNexus for interactive code graph visualization:</p>
31
+ <pre><code>gitnexus analyze && gitnexus serve</code></pre>
32
+ </div>
33
+ `;
34
+
35
+ if (status.serena?.memories?.length > 0) {
36
+ html += '<h3 class="section-title">Serena Memories (Codebase Overview)</h3>';
37
+ const memories = await ApiClient.request('/integrations/serena/memories');
38
+ for (const mem of memories) {
39
+ html += `
40
+ <div class="memory-card">
41
+ <h4>${esc(mem.name)}</h4>
42
+ <pre class="memory-content">${esc(mem.content)}</pre>
43
+ </div>
44
+ `;
45
+ }
46
+ } else {
47
+ html += '<p class="text-muted">No Serena memories found. Configure Serena for codebase intelligence.</p>';
48
+ }
49
+
50
+ html += '</div>';
51
+ container.innerHTML = html;
52
+ } catch (err) {
53
+ container.innerHTML = `<div class="error-state"><p>Failed to load: ${err.message}</p></div>`;
54
+ }
55
+ }
56
+
57
+ function esc(text) {
58
+ if (!text) return '';
59
+ const div = document.createElement('div');
60
+ div.textContent = String(text);
61
+ return div.innerHTML;
62
+ }
63
+
64
+ function init() {}
65
+ return { init, load };
66
+ })();
@@ -0,0 +1,238 @@
1
+ /**
2
+ * Tab 1: Graph visualization — Force Graph (default) + Mermaid toggle
3
+ */
4
+ const GraphTab = (() => {
5
+ const VIEW_KEY = 'mcp-graph-view-mode';
6
+ let nodes = [];
7
+ let currentSort = { key: 'priority', dir: 'asc' };
8
+ let viewMode = localStorage.getItem(VIEW_KEY) || 'force';
9
+
10
+ async function load() {
11
+ try {
12
+ nodes = await ApiClient.getNodes();
13
+ if (viewMode === 'force') {
14
+ await renderForceGraph();
15
+ } else {
16
+ await renderMermaid();
17
+ }
18
+ renderTable(nodes);
19
+ } catch (err) {
20
+ console.error('Failed to load graph tab:', err);
21
+ }
22
+ }
23
+
24
+ async function renderForceGraph() {
25
+ try {
26
+ const graphData = await ApiClient.getGraph();
27
+ if (!graphData || (!graphData.nodes?.length && !graphData.edges?.length)) {
28
+ const container = document.getElementById('force-graph-container');
29
+ if (container) container.innerHTML = '<div class="empty-state"><p>No graph data. Import a PRD to get started.</p></div>';
30
+ return;
31
+ }
32
+
33
+ // Transform edges → links format for force-graph
34
+ const forceNodes = (graphData.nodes || []).map(n => ({
35
+ id: n.id,
36
+ title: n.title,
37
+ type: n.type,
38
+ status: n.status,
39
+ priority: n.priority,
40
+ description: n.description,
41
+ xpSize: n.xpSize,
42
+ sprint: n.sprint,
43
+ }));
44
+
45
+ const forceLinks = (graphData.edges || []).map(e => ({
46
+ source: e.source,
47
+ target: e.target,
48
+ relationType: e.relationType || e.type,
49
+ }));
50
+
51
+ // Apply status/type filters from the filter panel
52
+ const filterVals = Filters.getValues();
53
+ let filteredNodes = forceNodes;
54
+ if (filterVals.status.length) {
55
+ filteredNodes = filteredNodes.filter(n => filterVals.status.includes(n.status));
56
+ }
57
+ if (filterVals.type.length) {
58
+ filteredNodes = filteredNodes.filter(n => filterVals.type.includes(n.type));
59
+ }
60
+ const nodeIds = new Set(filteredNodes.map(n => n.id));
61
+ const filteredLinks = forceLinks.filter(l => nodeIds.has(l.source) && nodeIds.has(l.target));
62
+
63
+ ForceGraphRenderer.render(filteredNodes, filteredLinks);
64
+
65
+ // Apply edge type + depth filters
66
+ const edgeTypes = filterVals.edgeTypes;
67
+ const focusDepth = filterVals.focusDepth;
68
+ ForceGraphRenderer.setFilters({ edgeTypes, depth: focusDepth });
69
+ } catch (err) {
70
+ console.error('Failed to render force graph:', err);
71
+ const container = document.getElementById('force-graph-container');
72
+ if (container) container.innerHTML = '<div class="error-state"><p>Error loading graph: ' + (err.message || err) + '</p></div>';
73
+ }
74
+ }
75
+
76
+ async function renderMermaid() {
77
+ const container = document.getElementById('mermaid-output');
78
+ const empty = document.getElementById('graph-empty');
79
+ if (!container) return;
80
+
81
+ const params = Filters.toQueryParams();
82
+ const mermaidCode = await ApiClient.getMermaid(params);
83
+
84
+ if (!mermaidCode || mermaidCode.trim() === 'graph TD\n' || mermaidCode.trim() === 'graph TD') {
85
+ if (empty) empty.classList.remove('hidden');
86
+ container.innerHTML = '';
87
+ return;
88
+ }
89
+
90
+ if (empty) empty.classList.add('hidden');
91
+ await GraphRenderer.render(container, mermaidCode);
92
+ }
93
+
94
+ async function renderGraph() {
95
+ if (viewMode === 'force') {
96
+ await renderForceGraph();
97
+ } else {
98
+ await renderMermaid();
99
+ }
100
+ }
101
+
102
+ function setViewMode(mode) {
103
+ viewMode = mode;
104
+ localStorage.setItem(VIEW_KEY, mode);
105
+
106
+ // Toggle active button
107
+ document.querySelectorAll('.view-btn').forEach(btn => {
108
+ btn.classList.toggle('active', btn.dataset.view === mode);
109
+ });
110
+
111
+ // Toggle containers
112
+ const forceContainer = document.getElementById('force-graph-container');
113
+ const mermaidContainer = document.getElementById('mermaid-container');
114
+ const zoomControls = document.getElementById('zoom-controls');
115
+
116
+ if (forceContainer) forceContainer.classList.toggle('hidden', mode !== 'force');
117
+ if (mermaidContainer) mermaidContainer.classList.toggle('hidden', mode !== 'mermaid');
118
+ if (zoomControls) zoomControls.style.display = mode === 'force' ? 'flex' : 'none';
119
+
120
+ // Toggle filter visibility
121
+ Filters.setViewMode(mode);
122
+
123
+ // Render the active view
124
+ renderGraph();
125
+ }
126
+
127
+ function renderTable(data) {
128
+ const tbody = document.getElementById('node-table-body');
129
+ if (!tbody) return;
130
+
131
+ if (data.length === 0) {
132
+ tbody.innerHTML = '<tr><td colspan="6" class="empty-cell">No nodes found</td></tr>';
133
+ return;
134
+ }
135
+
136
+ tbody.innerHTML = data.map(node => `
137
+ <tr class="node-row" data-id="${esc(node.id)}">
138
+ <td class="cell-title">${esc(node.title)}</td>
139
+ <td><span class="badge badge-type">${esc(node.type)}</span></td>
140
+ <td><span class="badge status-${node.status}">${esc(node.status)}</span></td>
141
+ <td class="cell-center">${node.priority}</td>
142
+ <td class="cell-center">${node.xpSize || '-'}</td>
143
+ <td>${esc(node.sprint || '-')}</td>
144
+ </tr>
145
+ `).join('');
146
+
147
+ // Row click → detail
148
+ tbody.querySelectorAll('.node-row').forEach(row => {
149
+ row.addEventListener('click', () => {
150
+ const nodeId = row.dataset.id;
151
+ document.dispatchEvent(new CustomEvent('node-clicked', { detail: { nodeId } }));
152
+ // Also select in force graph
153
+ if (viewMode === 'force') {
154
+ ForceGraphRenderer.selectNode(nodeId);
155
+ }
156
+ });
157
+ });
158
+ }
159
+
160
+ function sortTable(key) {
161
+ if (currentSort.key === key) {
162
+ currentSort.dir = currentSort.dir === 'asc' ? 'desc' : 'asc';
163
+ } else {
164
+ currentSort = { key, dir: 'asc' };
165
+ }
166
+
167
+ const sorted = [...nodes].sort((a, b) => {
168
+ const va = a[key] ?? '';
169
+ const vb = b[key] ?? '';
170
+ const cmp = typeof va === 'number' ? va - vb : String(va).localeCompare(String(vb));
171
+ return currentSort.dir === 'asc' ? cmp : -cmp;
172
+ });
173
+
174
+ renderTable(sorted);
175
+ }
176
+
177
+ function filterTable(query) {
178
+ if (!query) { renderTable(nodes); return; }
179
+ const q = query.toLowerCase();
180
+ const filtered = nodes.filter(n =>
181
+ n.title.toLowerCase().includes(q) ||
182
+ n.type.toLowerCase().includes(q) ||
183
+ n.status.toLowerCase().includes(q) ||
184
+ (n.sprint || '').toLowerCase().includes(q)
185
+ );
186
+ renderTable(filtered);
187
+ }
188
+
189
+ function init() {
190
+ // Initialize force graph renderer
191
+ ForceGraphRenderer.init('force-graph-container');
192
+
193
+ // View toggle buttons
194
+ document.querySelectorAll('.view-btn').forEach(btn => {
195
+ btn.addEventListener('click', () => setViewMode(btn.dataset.view));
196
+ });
197
+
198
+ // Zoom controls
199
+ document.getElementById('zoom-in')?.addEventListener('click', () => ForceGraphRenderer.zoomIn());
200
+ document.getElementById('zoom-out')?.addEventListener('click', () => ForceGraphRenderer.zoomOut());
201
+ document.getElementById('zoom-fit')?.addEventListener('click', () => ForceGraphRenderer.zoomFit());
202
+
203
+ // Filter buttons
204
+ document.getElementById('btn-apply-filters')?.addEventListener('click', renderGraph);
205
+ document.getElementById('btn-clear-filters')?.addEventListener('click', () => {
206
+ Filters.clear();
207
+ renderGraph();
208
+ });
209
+
210
+ // Table sort
211
+ document.querySelectorAll('#node-table th[data-sort]').forEach(th => {
212
+ th.style.cursor = 'pointer';
213
+ th.addEventListener('click', () => sortTable(th.dataset.sort));
214
+ });
215
+
216
+ // Table search
217
+ document.getElementById('table-search')?.addEventListener('input', (e) => {
218
+ filterTable(e.target.value);
219
+ });
220
+
221
+ // Resize handler
222
+ window.addEventListener('resize', () => {
223
+ if (viewMode === 'force') ForceGraphRenderer.resize();
224
+ });
225
+
226
+ // Apply initial view mode
227
+ setViewMode(viewMode);
228
+ }
229
+
230
+ function esc(text) {
231
+ if (!text) return '';
232
+ const div = document.createElement('div');
233
+ div.textContent = String(text);
234
+ return div.innerHTML;
235
+ }
236
+
237
+ return { init, load, renderGraph };
238
+ })();