gitnexus 1.4.0 → 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 (131) hide show
  1. package/README.md +194 -214
  2. package/dist/cli/ai-context.d.ts +1 -2
  3. package/dist/cli/ai-context.js +90 -117
  4. package/dist/cli/analyze.d.ts +0 -2
  5. package/dist/cli/analyze.js +2 -20
  6. package/dist/cli/index.js +25 -17
  7. package/dist/cli/setup.js +19 -17
  8. package/dist/core/augmentation/engine.js +20 -20
  9. package/dist/core/embeddings/embedding-pipeline.js +26 -26
  10. package/dist/core/graph/types.d.ts +2 -5
  11. package/dist/core/ingestion/ast-cache.js +2 -3
  12. package/dist/core/ingestion/call-processor.d.ts +5 -5
  13. package/dist/core/ingestion/call-processor.js +258 -173
  14. package/dist/core/ingestion/cluster-enricher.js +16 -16
  15. package/dist/core/ingestion/entry-point-scoring.d.ts +1 -2
  16. package/dist/core/ingestion/entry-point-scoring.js +22 -81
  17. package/dist/core/ingestion/framework-detection.d.ts +1 -5
  18. package/dist/core/ingestion/framework-detection.js +8 -39
  19. package/dist/core/ingestion/heritage-processor.d.ts +4 -13
  20. package/dist/core/ingestion/heritage-processor.js +28 -92
  21. package/dist/core/ingestion/import-processor.d.ts +19 -17
  22. package/dist/core/ingestion/import-processor.js +695 -170
  23. package/dist/core/ingestion/parsing-processor.d.ts +10 -1
  24. package/dist/core/ingestion/parsing-processor.js +177 -41
  25. package/dist/core/ingestion/pipeline.js +26 -49
  26. package/dist/core/ingestion/process-processor.js +1 -2
  27. package/dist/core/ingestion/symbol-table.d.ts +1 -12
  28. package/dist/core/ingestion/symbol-table.js +12 -19
  29. package/dist/core/ingestion/tree-sitter-queries.d.ts +11 -11
  30. package/dist/core/ingestion/tree-sitter-queries.js +485 -590
  31. package/dist/core/ingestion/utils.d.ts +0 -67
  32. package/dist/core/ingestion/utils.js +9 -692
  33. package/dist/core/ingestion/workers/parse-worker.d.ts +3 -20
  34. package/dist/core/ingestion/workers/parse-worker.js +345 -84
  35. package/dist/core/ingestion/workers/worker-pool.js +0 -8
  36. package/dist/core/kuzu/csv-generator.js +3 -19
  37. package/dist/core/kuzu/kuzu-adapter.js +19 -14
  38. package/dist/core/kuzu/schema.d.ts +3 -3
  39. package/dist/core/kuzu/schema.js +288 -303
  40. package/dist/core/search/bm25-index.js +6 -7
  41. package/dist/core/search/hybrid-search.js +3 -3
  42. package/dist/core/wiki/diagrams.d.ts +27 -0
  43. package/dist/core/wiki/diagrams.js +163 -0
  44. package/dist/core/wiki/generator.d.ts +50 -2
  45. package/dist/core/wiki/generator.js +548 -49
  46. package/dist/core/wiki/graph-queries.d.ts +42 -0
  47. package/dist/core/wiki/graph-queries.js +276 -97
  48. package/dist/core/wiki/html-viewer.js +192 -192
  49. package/dist/core/wiki/llm-client.js +73 -11
  50. package/dist/core/wiki/prompts.d.ts +52 -8
  51. package/dist/core/wiki/prompts.js +200 -86
  52. package/dist/mcp/core/kuzu-adapter.d.ts +3 -1
  53. package/dist/mcp/core/kuzu-adapter.js +44 -13
  54. package/dist/mcp/local/local-backend.js +128 -128
  55. package/dist/mcp/resources.js +42 -42
  56. package/dist/mcp/server.js +19 -18
  57. package/dist/mcp/tools.js +104 -103
  58. package/hooks/claude/gitnexus-hook.cjs +155 -238
  59. package/hooks/claude/pre-tool-use.sh +79 -79
  60. package/hooks/claude/session-start.sh +42 -42
  61. package/package.json +96 -96
  62. package/scripts/patch-tree-sitter-swift.cjs +74 -74
  63. package/skills/gitnexus-cli.md +82 -82
  64. package/skills/gitnexus-debugging.md +89 -89
  65. package/skills/gitnexus-exploring.md +78 -78
  66. package/skills/gitnexus-guide.md +64 -64
  67. package/skills/gitnexus-impact-analysis.md +97 -97
  68. package/skills/gitnexus-pr-review.md +163 -163
  69. package/skills/gitnexus-refactoring.md +121 -121
  70. package/vendor/leiden/index.cjs +355 -355
  71. package/vendor/leiden/utils.cjs +392 -392
  72. package/dist/cli/lazy-action.d.ts +0 -6
  73. package/dist/cli/lazy-action.js +0 -18
  74. package/dist/cli/skill-gen.d.ts +0 -26
  75. package/dist/cli/skill-gen.js +0 -549
  76. package/dist/core/ingestion/constants.d.ts +0 -16
  77. package/dist/core/ingestion/constants.js +0 -16
  78. package/dist/core/ingestion/export-detection.d.ts +0 -18
  79. package/dist/core/ingestion/export-detection.js +0 -230
  80. package/dist/core/ingestion/language-config.d.ts +0 -46
  81. package/dist/core/ingestion/language-config.js +0 -167
  82. package/dist/core/ingestion/mro-processor.d.ts +0 -45
  83. package/dist/core/ingestion/mro-processor.js +0 -369
  84. package/dist/core/ingestion/named-binding-extraction.d.ts +0 -61
  85. package/dist/core/ingestion/named-binding-extraction.js +0 -363
  86. package/dist/core/ingestion/resolvers/csharp.d.ts +0 -22
  87. package/dist/core/ingestion/resolvers/csharp.js +0 -109
  88. package/dist/core/ingestion/resolvers/go.d.ts +0 -19
  89. package/dist/core/ingestion/resolvers/go.js +0 -42
  90. package/dist/core/ingestion/resolvers/index.d.ts +0 -16
  91. package/dist/core/ingestion/resolvers/index.js +0 -11
  92. package/dist/core/ingestion/resolvers/jvm.d.ts +0 -23
  93. package/dist/core/ingestion/resolvers/jvm.js +0 -87
  94. package/dist/core/ingestion/resolvers/php.d.ts +0 -15
  95. package/dist/core/ingestion/resolvers/php.js +0 -35
  96. package/dist/core/ingestion/resolvers/rust.d.ts +0 -15
  97. package/dist/core/ingestion/resolvers/rust.js +0 -73
  98. package/dist/core/ingestion/resolvers/standard.d.ts +0 -28
  99. package/dist/core/ingestion/resolvers/standard.js +0 -145
  100. package/dist/core/ingestion/resolvers/utils.d.ts +0 -33
  101. package/dist/core/ingestion/resolvers/utils.js +0 -120
  102. package/dist/core/ingestion/symbol-resolver.d.ts +0 -32
  103. package/dist/core/ingestion/symbol-resolver.js +0 -83
  104. package/dist/core/ingestion/type-env.d.ts +0 -27
  105. package/dist/core/ingestion/type-env.js +0 -86
  106. package/dist/core/ingestion/type-extractors/c-cpp.d.ts +0 -2
  107. package/dist/core/ingestion/type-extractors/c-cpp.js +0 -60
  108. package/dist/core/ingestion/type-extractors/csharp.d.ts +0 -2
  109. package/dist/core/ingestion/type-extractors/csharp.js +0 -89
  110. package/dist/core/ingestion/type-extractors/go.d.ts +0 -2
  111. package/dist/core/ingestion/type-extractors/go.js +0 -105
  112. package/dist/core/ingestion/type-extractors/index.d.ts +0 -21
  113. package/dist/core/ingestion/type-extractors/index.js +0 -29
  114. package/dist/core/ingestion/type-extractors/jvm.d.ts +0 -3
  115. package/dist/core/ingestion/type-extractors/jvm.js +0 -121
  116. package/dist/core/ingestion/type-extractors/php.d.ts +0 -2
  117. package/dist/core/ingestion/type-extractors/php.js +0 -31
  118. package/dist/core/ingestion/type-extractors/python.d.ts +0 -2
  119. package/dist/core/ingestion/type-extractors/python.js +0 -41
  120. package/dist/core/ingestion/type-extractors/rust.d.ts +0 -2
  121. package/dist/core/ingestion/type-extractors/rust.js +0 -39
  122. package/dist/core/ingestion/type-extractors/shared.d.ts +0 -17
  123. package/dist/core/ingestion/type-extractors/shared.js +0 -97
  124. package/dist/core/ingestion/type-extractors/swift.d.ts +0 -2
  125. package/dist/core/ingestion/type-extractors/swift.js +0 -43
  126. package/dist/core/ingestion/type-extractors/types.d.ts +0 -14
  127. package/dist/core/ingestion/type-extractors/types.js +0 -1
  128. package/dist/core/ingestion/type-extractors/typescript.d.ts +0 -2
  129. package/dist/core/ingestion/type-extractors/typescript.js +0 -46
  130. package/dist/mcp/compatible-stdio-transport.d.ts +0 -25
  131. package/dist/mcp/compatible-stdio-transport.js +0 -200
@@ -100,198 +100,198 @@ const BOOK_SVG = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" st
100
100
  '<path d="M2 3h6a4 4 0 014 4v14a3 3 0 00-3-3H2z"/>' +
101
101
  '<path d="M22 3h-6a4 4 0 00-4 4v14a3 3 0 013-3h7z"/>' +
102
102
  '</svg>';
103
- const CSS = `
104
- *{margin:0;padding:0;box-sizing:border-box}
105
- :root{
106
- --bg:#ffffff;--sidebar-bg:#f8f9fb;--border:#e5e7eb;
107
- --text:#1e293b;--text-muted:#64748b;--primary:#2563eb;
108
- --primary-soft:#eff6ff;--hover:#f1f5f9;--code-bg:#f1f5f9;
109
- --radius:8px;--shadow:0 1px 3px rgba(0,0,0,.08);
110
- }
111
- body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;
112
- line-height:1.65;color:var(--text);background:var(--bg)}
113
-
114
- .layout{display:flex;min-height:100vh}
115
- .sidebar{width:280px;background:var(--sidebar-bg);border-right:1px solid var(--border);
116
- position:fixed;top:0;left:0;bottom:0;overflow-y:auto;padding:24px 16px;
117
- display:flex;flex-direction:column;z-index:10}
118
- .content{margin-left:280px;flex:1;padding:48px 64px;max-width:960px}
119
-
120
- .sidebar-header{margin-bottom:20px;padding-bottom:16px;border-bottom:1px solid var(--border)}
121
- .sidebar-title{font-size:16px;font-weight:700;color:var(--text);display:flex;align-items:center;gap:8px}
122
- .sidebar-title svg{flex-shrink:0}
123
- .sidebar-meta{font-size:11px;color:var(--text-muted);margin-top:6px}
124
- .nav-section{margin-bottom:2px}
125
- .nav-item{display:block;padding:7px 12px;border-radius:var(--radius);cursor:pointer;
126
- font-size:13px;color:var(--text);text-decoration:none;transition:all .15s;
127
- white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
128
- .nav-item:hover{background:var(--hover)}
129
- .nav-item.active{background:var(--primary-soft);color:var(--primary);font-weight:600}
130
- .nav-item.overview{font-weight:600;margin-bottom:4px}
131
- .nav-children{padding-left:14px;border-left:1px solid var(--border);margin-left:12px}
132
- .nav-group-label{font-size:11px;font-weight:600;color:var(--text-muted);
133
- text-transform:uppercase;letter-spacing:.5px;padding:12px 12px 4px;user-select:none}
134
- .sidebar-footer{margin-top:auto;padding-top:16px;border-top:1px solid var(--border);
135
- font-size:11px;color:var(--text-muted);text-align:center}
136
-
137
- .content h1{font-size:28px;font-weight:700;margin-bottom:8px;line-height:1.3}
138
- .content h2{font-size:22px;font-weight:600;margin:32px 0 12px;padding-bottom:6px;border-bottom:1px solid var(--border)}
139
- .content h3{font-size:17px;font-weight:600;margin:24px 0 8px}
140
- .content h4{font-size:15px;font-weight:600;margin:20px 0 6px}
141
- .content p{margin:12px 0}
142
- .content ul,.content ol{margin:12px 0 12px 24px}
143
- .content li{margin:4px 0}
144
- .content a{color:var(--primary);text-decoration:none}
145
- .content a:hover{text-decoration:underline}
146
- .content blockquote{border-left:3px solid var(--primary);padding:8px 16px;margin:16px 0;
147
- background:var(--primary-soft);border-radius:0 var(--radius) var(--radius) 0;
148
- color:var(--text-muted);font-size:14px}
149
- .content code{font-family:'SF Mono',Consolas,'Courier New',monospace;font-size:13px;
150
- background:var(--code-bg);padding:2px 6px;border-radius:4px}
151
- .content pre{background:#1e293b;color:#e2e8f0;border-radius:var(--radius);padding:16px;
152
- overflow-x:auto;margin:16px 0}
153
- .content pre code{background:none;padding:0;font-size:13px;line-height:1.6;color:inherit}
154
- .content table{border-collapse:collapse;width:100%;margin:16px 0}
155
- .content th,.content td{border:1px solid var(--border);padding:8px 12px;text-align:left;font-size:14px}
156
- .content th{background:var(--sidebar-bg);font-weight:600}
157
- .content img{max-width:100%;border-radius:var(--radius)}
158
- .content hr{border:none;border-top:1px solid var(--border);margin:32px 0}
159
- .content .mermaid{margin:20px 0;text-align:center}
160
-
161
- .menu-toggle{display:none;position:fixed;top:12px;left:12px;z-index:20;
162
- background:var(--bg);border:1px solid var(--border);border-radius:var(--radius);
163
- padding:8px 12px;cursor:pointer;font-size:18px;box-shadow:var(--shadow)}
164
- @media(max-width:768px){
165
- .sidebar{transform:translateX(-100%);transition:transform .2s}
166
- .sidebar.open{transform:translateX(0);box-shadow:2px 0 12px rgba(0,0,0,.1)}
167
- .content{margin-left:0;padding:24px 20px;padding-top:56px}
168
- .menu-toggle{display:block}
169
- }
170
- .empty-state{text-align:center;padding:80px 20px;color:var(--text-muted)}
171
- .empty-state h2{font-size:20px;margin-bottom:8px;border:none}
103
+ const CSS = `
104
+ *{margin:0;padding:0;box-sizing:border-box}
105
+ :root{
106
+ --bg:#ffffff;--sidebar-bg:#f8f9fb;--border:#e5e7eb;
107
+ --text:#1e293b;--text-muted:#64748b;--primary:#2563eb;
108
+ --primary-soft:#eff6ff;--hover:#f1f5f9;--code-bg:#f1f5f9;
109
+ --radius:8px;--shadow:0 1px 3px rgba(0,0,0,.08);
110
+ }
111
+ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;
112
+ line-height:1.65;color:var(--text);background:var(--bg)}
113
+
114
+ .layout{display:flex;min-height:100vh}
115
+ .sidebar{width:280px;background:var(--sidebar-bg);border-right:1px solid var(--border);
116
+ position:fixed;top:0;left:0;bottom:0;overflow-y:auto;padding:24px 16px;
117
+ display:flex;flex-direction:column;z-index:10}
118
+ .content{margin-left:280px;flex:1;padding:48px 64px;max-width:960px}
119
+
120
+ .sidebar-header{margin-bottom:20px;padding-bottom:16px;border-bottom:1px solid var(--border)}
121
+ .sidebar-title{font-size:16px;font-weight:700;color:var(--text);display:flex;align-items:center;gap:8px}
122
+ .sidebar-title svg{flex-shrink:0}
123
+ .sidebar-meta{font-size:11px;color:var(--text-muted);margin-top:6px}
124
+ .nav-section{margin-bottom:2px}
125
+ .nav-item{display:block;padding:7px 12px;border-radius:var(--radius);cursor:pointer;
126
+ font-size:13px;color:var(--text);text-decoration:none;transition:all .15s;
127
+ white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
128
+ .nav-item:hover{background:var(--hover)}
129
+ .nav-item.active{background:var(--primary-soft);color:var(--primary);font-weight:600}
130
+ .nav-item.overview{font-weight:600;margin-bottom:4px}
131
+ .nav-children{padding-left:14px;border-left:1px solid var(--border);margin-left:12px}
132
+ .nav-group-label{font-size:11px;font-weight:600;color:var(--text-muted);
133
+ text-transform:uppercase;letter-spacing:.5px;padding:12px 12px 4px;user-select:none}
134
+ .sidebar-footer{margin-top:auto;padding-top:16px;border-top:1px solid var(--border);
135
+ font-size:11px;color:var(--text-muted);text-align:center}
136
+
137
+ .content h1{font-size:28px;font-weight:700;margin-bottom:8px;line-height:1.3}
138
+ .content h2{font-size:22px;font-weight:600;margin:32px 0 12px;padding-bottom:6px;border-bottom:1px solid var(--border)}
139
+ .content h3{font-size:17px;font-weight:600;margin:24px 0 8px}
140
+ .content h4{font-size:15px;font-weight:600;margin:20px 0 6px}
141
+ .content p{margin:12px 0}
142
+ .content ul,.content ol{margin:12px 0 12px 24px}
143
+ .content li{margin:4px 0}
144
+ .content a{color:var(--primary);text-decoration:none}
145
+ .content a:hover{text-decoration:underline}
146
+ .content blockquote{border-left:3px solid var(--primary);padding:8px 16px;margin:16px 0;
147
+ background:var(--primary-soft);border-radius:0 var(--radius) var(--radius) 0;
148
+ color:var(--text-muted);font-size:14px}
149
+ .content code{font-family:'SF Mono',Consolas,'Courier New',monospace;font-size:13px;
150
+ background:var(--code-bg);padding:2px 6px;border-radius:4px}
151
+ .content pre{background:#1e293b;color:#e2e8f0;border-radius:var(--radius);padding:16px;
152
+ overflow-x:auto;margin:16px 0}
153
+ .content pre code{background:none;padding:0;font-size:13px;line-height:1.6;color:inherit}
154
+ .content table{border-collapse:collapse;width:100%;margin:16px 0}
155
+ .content th,.content td{border:1px solid var(--border);padding:8px 12px;text-align:left;font-size:14px}
156
+ .content th{background:var(--sidebar-bg);font-weight:600}
157
+ .content img{max-width:100%;border-radius:var(--radius)}
158
+ .content hr{border:none;border-top:1px solid var(--border);margin:32px 0}
159
+ .content .mermaid{margin:20px 0;text-align:center}
160
+
161
+ .menu-toggle{display:none;position:fixed;top:12px;left:12px;z-index:20;
162
+ background:var(--bg);border:1px solid var(--border);border-radius:var(--radius);
163
+ padding:8px 12px;cursor:pointer;font-size:18px;box-shadow:var(--shadow)}
164
+ @media(max-width:768px){
165
+ .sidebar{transform:translateX(-100%);transition:transform .2s}
166
+ .sidebar.open{transform:translateX(0);box-shadow:2px 0 12px rgba(0,0,0,.1)}
167
+ .content{margin-left:0;padding:24px 20px;padding-top:56px}
168
+ .menu-toggle{display:block}
169
+ }
170
+ .empty-state{text-align:center;padding:80px 20px;color:var(--text-muted)}
171
+ .empty-state h2{font-size:20px;margin-bottom:8px;border:none}
172
172
  `;
173
173
  // The client-side JS is kept as a plain string to avoid template literal conflicts
174
- const JS_APP = `
175
- (function() {
176
- var activePage = 'overview';
177
-
178
- document.addEventListener('DOMContentLoaded', function() {
179
- mermaid.initialize({ startOnLoad: false, theme: 'neutral', securityLevel: 'loose' });
180
- renderMeta();
181
- renderNav();
182
- document.getElementById('menu-toggle').addEventListener('click', function() {
183
- document.getElementById('sidebar').classList.toggle('open');
184
- });
185
- if (location.hash && location.hash.length > 1) {
186
- activePage = decodeURIComponent(location.hash.slice(1));
187
- }
188
- navigateTo(activePage);
189
- });
190
-
191
- function renderMeta() {
192
- if (!META) return;
193
- var el = document.getElementById('meta-info');
194
- var parts = [];
195
- if (META.generatedAt) {
196
- parts.push(new Date(META.generatedAt).toLocaleDateString());
197
- }
198
- if (META.model) parts.push(META.model);
199
- if (META.fromCommit) parts.push(META.fromCommit.slice(0, 8));
200
- el.textContent = parts.join(' \\u00b7 ');
201
- }
202
-
203
- function renderNav() {
204
- var container = document.getElementById('nav-tree');
205
- var html = '<div class="nav-section">';
206
- html += '<a class="nav-item overview" data-page="overview" href="#overview">Overview</a>';
207
- html += '</div>';
208
- if (TREE.length > 0) {
209
- html += '<div class="nav-group-label">Modules</div>';
210
- html += buildNavTree(TREE);
211
- }
212
- container.innerHTML = html;
213
- container.addEventListener('click', function(e) {
214
- var target = e.target;
215
- while (target && !target.dataset.page) { target = target.parentElement; }
216
- if (target && target.dataset.page) {
217
- e.preventDefault();
218
- navigateTo(target.dataset.page);
219
- }
220
- });
221
- }
222
-
223
- function buildNavTree(nodes) {
224
- var html = '';
225
- for (var i = 0; i < nodes.length; i++) {
226
- var node = nodes[i];
227
- html += '<div class="nav-section">';
228
- html += '<a class="nav-item" data-page="' + escH(node.slug) + '" href="#' + encodeURIComponent(node.slug) + '">' + escH(node.name) + '</a>';
229
- if (node.children && node.children.length > 0) {
230
- html += '<div class="nav-children">' + buildNavTree(node.children) + '</div>';
231
- }
232
- html += '</div>';
233
- }
234
- return html;
235
- }
236
-
237
- function escH(s) {
238
- var d = document.createElement('div');
239
- d.textContent = s;
240
- return d.innerHTML;
241
- }
242
-
243
- function navigateTo(page) {
244
- activePage = page;
245
- location.hash = encodeURIComponent(page);
246
-
247
- var items = document.querySelectorAll('.nav-item');
248
- for (var i = 0; i < items.length; i++) {
249
- if (items[i].dataset.page === page) {
250
- items[i].classList.add('active');
251
- } else {
252
- items[i].classList.remove('active');
253
- }
254
- }
255
-
256
- var contentEl = document.getElementById('content');
257
- var md = PAGES[page];
258
-
259
- if (!md) {
260
- contentEl.innerHTML = '<div class="empty-state"><h2>Page not found</h2><p>' + escH(page) + '.md does not exist.</p></div>';
261
- return;
262
- }
263
-
264
- contentEl.innerHTML = marked.parse(md);
265
-
266
- // Rewrite .md links to hash navigation
267
- var links = contentEl.querySelectorAll('a[href]');
268
- for (var i = 0; i < links.length; i++) {
269
- var href = links[i].getAttribute('href');
270
- if (href && href.endsWith('.md') && href.indexOf('://') === -1) {
271
- var slug = href.replace(/\\.md$/, '');
272
- links[i].setAttribute('href', '#' + encodeURIComponent(slug));
273
- (function(s) {
274
- links[i].addEventListener('click', function(e) {
275
- e.preventDefault();
276
- navigateTo(s);
277
- });
278
- })(slug);
279
- }
280
- }
281
-
282
- // Convert mermaid code blocks into mermaid divs
283
- var mermaidBlocks = contentEl.querySelectorAll('pre code.language-mermaid');
284
- for (var i = 0; i < mermaidBlocks.length; i++) {
285
- var pre = mermaidBlocks[i].parentElement;
286
- var div = document.createElement('div');
287
- div.className = 'mermaid';
288
- div.textContent = mermaidBlocks[i].textContent;
289
- pre.parentNode.replaceChild(div, pre);
290
- }
291
- try { mermaid.run({ querySelector: '.mermaid' }); } catch(e) {}
292
-
293
- window.scrollTo(0, 0);
294
- document.getElementById('sidebar').classList.remove('open');
295
- }
296
- })();
174
+ const JS_APP = `
175
+ (function() {
176
+ var activePage = 'overview';
177
+
178
+ document.addEventListener('DOMContentLoaded', function() {
179
+ mermaid.initialize({ startOnLoad: false, theme: 'neutral', securityLevel: 'loose' });
180
+ renderMeta();
181
+ renderNav();
182
+ document.getElementById('menu-toggle').addEventListener('click', function() {
183
+ document.getElementById('sidebar').classList.toggle('open');
184
+ });
185
+ if (location.hash && location.hash.length > 1) {
186
+ activePage = decodeURIComponent(location.hash.slice(1));
187
+ }
188
+ navigateTo(activePage);
189
+ });
190
+
191
+ function renderMeta() {
192
+ if (!META) return;
193
+ var el = document.getElementById('meta-info');
194
+ var parts = [];
195
+ if (META.generatedAt) {
196
+ parts.push(new Date(META.generatedAt).toLocaleDateString());
197
+ }
198
+ if (META.model) parts.push(META.model);
199
+ if (META.fromCommit) parts.push(META.fromCommit.slice(0, 8));
200
+ el.textContent = parts.join(' \\u00b7 ');
201
+ }
202
+
203
+ function renderNav() {
204
+ var container = document.getElementById('nav-tree');
205
+ var html = '<div class="nav-section">';
206
+ html += '<a class="nav-item overview" data-page="overview" href="#overview">Overview</a>';
207
+ html += '</div>';
208
+ if (TREE.length > 0) {
209
+ html += '<div class="nav-group-label">Modules</div>';
210
+ html += buildNavTree(TREE);
211
+ }
212
+ container.innerHTML = html;
213
+ container.addEventListener('click', function(e) {
214
+ var target = e.target;
215
+ while (target && !target.dataset.page) { target = target.parentElement; }
216
+ if (target && target.dataset.page) {
217
+ e.preventDefault();
218
+ navigateTo(target.dataset.page);
219
+ }
220
+ });
221
+ }
222
+
223
+ function buildNavTree(nodes) {
224
+ var html = '';
225
+ for (var i = 0; i < nodes.length; i++) {
226
+ var node = nodes[i];
227
+ html += '<div class="nav-section">';
228
+ html += '<a class="nav-item" data-page="' + escH(node.slug) + '" href="#' + encodeURIComponent(node.slug) + '">' + escH(node.name) + '</a>';
229
+ if (node.children && node.children.length > 0) {
230
+ html += '<div class="nav-children">' + buildNavTree(node.children) + '</div>';
231
+ }
232
+ html += '</div>';
233
+ }
234
+ return html;
235
+ }
236
+
237
+ function escH(s) {
238
+ var d = document.createElement('div');
239
+ d.textContent = s;
240
+ return d.innerHTML;
241
+ }
242
+
243
+ function navigateTo(page) {
244
+ activePage = page;
245
+ location.hash = encodeURIComponent(page);
246
+
247
+ var items = document.querySelectorAll('.nav-item');
248
+ for (var i = 0; i < items.length; i++) {
249
+ if (items[i].dataset.page === page) {
250
+ items[i].classList.add('active');
251
+ } else {
252
+ items[i].classList.remove('active');
253
+ }
254
+ }
255
+
256
+ var contentEl = document.getElementById('content');
257
+ var md = PAGES[page];
258
+
259
+ if (!md) {
260
+ contentEl.innerHTML = '<div class="empty-state"><h2>Page not found</h2><p>' + escH(page) + '.md does not exist.</p></div>';
261
+ return;
262
+ }
263
+
264
+ contentEl.innerHTML = marked.parse(md);
265
+
266
+ // Rewrite .md links to hash navigation
267
+ var links = contentEl.querySelectorAll('a[href]');
268
+ for (var i = 0; i < links.length; i++) {
269
+ var href = links[i].getAttribute('href');
270
+ if (href && href.endsWith('.md') && href.indexOf('://') === -1) {
271
+ var slug = href.replace(/\\.md$/, '');
272
+ links[i].setAttribute('href', '#' + encodeURIComponent(slug));
273
+ (function(s) {
274
+ links[i].addEventListener('click', function(e) {
275
+ e.preventDefault();
276
+ navigateTo(s);
277
+ });
278
+ })(slug);
279
+ }
280
+ }
281
+
282
+ // Convert mermaid code blocks into mermaid divs
283
+ var mermaidBlocks = contentEl.querySelectorAll('pre code.language-mermaid');
284
+ for (var i = 0; i < mermaidBlocks.length; i++) {
285
+ var pre = mermaidBlocks[i].parentElement;
286
+ var div = document.createElement('div');
287
+ div.className = 'mermaid';
288
+ div.textContent = mermaidBlocks[i].textContent;
289
+ pre.parentNode.replaceChild(div, pre);
290
+ }
291
+ try { mermaid.run({ querySelector: '.mermaid' }); } catch(e) {}
292
+
293
+ window.scrollTo(0, 0);
294
+ document.getElementById('sidebar').classList.remove('open');
295
+ }
296
+ })();
297
297
  `;
@@ -62,8 +62,11 @@ export async function callLLM(prompt, config, systemPrompt, options) {
62
62
  if (useStream)
63
63
  body.stream = true;
64
64
  const MAX_RETRIES = 3;
65
+ const FETCH_TIMEOUT_MS = 60_000; // 60s max to get response headers
65
66
  let lastError = null;
66
67
  for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
68
+ const controller = new AbortController();
69
+ const fetchTimer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
67
70
  try {
68
71
  const response = await fetch(url, {
69
72
  method: 'POST',
@@ -72,17 +75,20 @@ export async function callLLM(prompt, config, systemPrompt, options) {
72
75
  'Authorization': `Bearer ${config.apiKey}`,
73
76
  },
74
77
  body: JSON.stringify(body),
78
+ signal: controller.signal,
75
79
  });
80
+ clearTimeout(fetchTimer);
76
81
  if (!response.ok) {
77
82
  const errorText = await response.text().catch(() => 'unknown error');
78
- // Rate limit — wait with exponential backoff and retry
83
+ // Rate limit — use retry-after if available, otherwise short fixed delay
84
+ // (the caller's runParallel handles concurrency reduction + re-queuing)
79
85
  if (response.status === 429 && attempt < MAX_RETRIES - 1) {
80
86
  const retryAfter = parseInt(response.headers.get('retry-after') || '0', 10);
81
- const delay = retryAfter > 0 ? retryAfter * 1000 : (2 ** attempt) * 3000;
87
+ const delay = retryAfter > 0 ? Math.min(retryAfter * 1000, 30_000) : 3_000;
82
88
  await sleep(delay);
83
89
  continue;
84
90
  }
85
- // Server error — retry with backoff
91
+ // Server error — retry with short backoff
86
92
  if (response.status >= 500 && attempt < MAX_RETRIES - 1) {
87
93
  await sleep((attempt + 1) * 2000);
88
94
  continue;
@@ -106,9 +112,16 @@ export async function callLLM(prompt, config, systemPrompt, options) {
106
112
  };
107
113
  }
108
114
  catch (err) {
115
+ clearTimeout(fetchTimer);
109
116
  lastError = err;
110
- // Network error — retry with backoff
111
- if (attempt < MAX_RETRIES - 1 && (err.code === 'ECONNREFUSED' || err.code === 'ETIMEDOUT' || err.message?.includes('fetch'))) {
117
+ // Network/timeout error — retry with backoff
118
+ const isRetryable = err.code === 'ECONNREFUSED'
119
+ || err.code === 'ETIMEDOUT'
120
+ || err.name === 'AbortError'
121
+ || err.message?.includes('abort')
122
+ || err.message?.includes('fetch');
123
+ if (attempt < MAX_RETRIES - 1 && isRetryable) {
124
+ console.warn(`[llm] Attempt ${attempt + 1} failed (${err.message?.slice(0, 100)}), retrying...`);
112
125
  await sleep((attempt + 1) * 3000);
113
126
  continue;
114
127
  }
@@ -119,38 +132,79 @@ export async function callLLM(prompt, config, systemPrompt, options) {
119
132
  }
120
133
  /**
121
134
  * Read an SSE stream from an OpenAI-compatible streaming response.
135
+ * Aborts if no chunk is received within STREAM_CHUNK_TIMEOUT_MS.
122
136
  */
137
+ const STREAM_READ_TIMEOUT_MS = 90_000; // 90s max wait for any bytes from connection
138
+ const CONTENT_STALL_TIMEOUT_MS = 120_000; // 120s max without new content tokens
123
139
  async function readSSEStream(body, onChunk) {
124
140
  const decoder = new TextDecoder();
125
141
  const reader = body.getReader();
126
142
  let content = '';
127
143
  let buffer = '';
144
+ let lastContentTime = Date.now();
128
145
  while (true) {
129
- const { done, value } = await reader.read();
130
- if (done)
146
+ let result;
147
+ try {
148
+ result = await withTimeout(reader.read(), STREAM_READ_TIMEOUT_MS, `LLM stream stalled — no data received for ${STREAM_READ_TIMEOUT_MS / 1000}s ` +
149
+ `(received ${content.length} chars so far). ` +
150
+ 'This may indicate the model is overloaded or the connection was dropped.');
151
+ }
152
+ catch (err) {
153
+ reader.cancel().catch(() => { });
154
+ throw err;
155
+ }
156
+ const { done, value } = result;
157
+ if (done) {
158
+ const elapsed = ((Date.now() - lastContentTime) / 1000).toFixed(1);
159
+ console.log(`[sse] stream done — ${content.length} chars total, ${elapsed}s since last content`);
131
160
  break;
132
- buffer += decoder.decode(value, { stream: true });
161
+ }
162
+ const rawChunk = decoder.decode(value, { stream: true });
163
+ buffer += rawChunk;
133
164
  const lines = buffer.split('\n');
134
165
  buffer = lines.pop() || '';
166
+ let gotContent = false;
135
167
  for (const line of lines) {
136
168
  const trimmed = line.trim();
137
- if (!trimmed || !trimmed.startsWith('data: '))
169
+ if (!trimmed)
170
+ continue;
171
+ // Log non-data lines (keep-alives, comments, errors)
172
+ if (!trimmed.startsWith('data: ')) {
173
+ console.log(`[sse] non-data line: ${trimmed.slice(0, 200)}`);
138
174
  continue;
175
+ }
139
176
  const data = trimmed.slice(6);
140
- if (data === '[DONE]')
177
+ if (data === '[DONE]') {
178
+ console.log(`[sse] received [DONE] — ${content.length} chars total`);
141
179
  continue;
180
+ }
142
181
  try {
143
182
  const parsed = JSON.parse(data);
144
183
  const delta = parsed.choices?.[0]?.delta?.content;
184
+ const finishReason = parsed.choices?.[0]?.finish_reason;
185
+ if (finishReason) {
186
+ console.log(`[sse] finish_reason: ${finishReason} — ${content.length} chars`);
187
+ }
145
188
  if (delta) {
146
189
  content += delta;
190
+ gotContent = true;
147
191
  onChunk(content.length);
148
192
  }
149
193
  }
150
194
  catch {
151
- // Skip malformed SSE chunks
195
+ console.log(`[sse] malformed chunk: ${data.slice(0, 200)}`);
152
196
  }
153
197
  }
198
+ if (gotContent) {
199
+ lastContentTime = Date.now();
200
+ }
201
+ else if (Date.now() - lastContentTime > CONTENT_STALL_TIMEOUT_MS) {
202
+ // Connection is alive (keep-alives arriving) but no actual tokens for too long
203
+ reader.cancel().catch(() => { });
204
+ throw new Error(`LLM stream content stalled — receiving keep-alives but no new tokens for ` +
205
+ `${CONTENT_STALL_TIMEOUT_MS / 1000}s (received ${content.length} chars total). ` +
206
+ 'The model may have hit an internal limit or the provider dropped the generation.');
207
+ }
154
208
  }
155
209
  if (!content) {
156
210
  throw new Error('LLM returned empty streaming response');
@@ -160,3 +214,11 @@ async function readSSEStream(body, onChunk) {
160
214
  function sleep(ms) {
161
215
  return new Promise(resolve => setTimeout(resolve, ms));
162
216
  }
217
+ /** Race a promise against a timeout. Cleans up the timer on resolve to avoid leaks. */
218
+ function withTimeout(promise, ms, message) {
219
+ let timer;
220
+ const timeout = new Promise((_, reject) => {
221
+ timer = setTimeout(() => reject(new Error(message)), ms);
222
+ });
223
+ return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
224
+ }