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.
- package/README.md +194 -214
- package/dist/cli/ai-context.d.ts +1 -2
- package/dist/cli/ai-context.js +90 -117
- package/dist/cli/analyze.d.ts +0 -2
- package/dist/cli/analyze.js +2 -20
- package/dist/cli/index.js +25 -17
- package/dist/cli/setup.js +19 -17
- package/dist/core/augmentation/engine.js +20 -20
- package/dist/core/embeddings/embedding-pipeline.js +26 -26
- package/dist/core/graph/types.d.ts +2 -5
- package/dist/core/ingestion/ast-cache.js +2 -3
- package/dist/core/ingestion/call-processor.d.ts +5 -5
- package/dist/core/ingestion/call-processor.js +258 -173
- package/dist/core/ingestion/cluster-enricher.js +16 -16
- package/dist/core/ingestion/entry-point-scoring.d.ts +1 -2
- package/dist/core/ingestion/entry-point-scoring.js +22 -81
- package/dist/core/ingestion/framework-detection.d.ts +1 -5
- package/dist/core/ingestion/framework-detection.js +8 -39
- package/dist/core/ingestion/heritage-processor.d.ts +4 -13
- package/dist/core/ingestion/heritage-processor.js +28 -92
- package/dist/core/ingestion/import-processor.d.ts +19 -17
- package/dist/core/ingestion/import-processor.js +695 -170
- package/dist/core/ingestion/parsing-processor.d.ts +10 -1
- package/dist/core/ingestion/parsing-processor.js +177 -41
- package/dist/core/ingestion/pipeline.js +26 -49
- package/dist/core/ingestion/process-processor.js +1 -2
- package/dist/core/ingestion/symbol-table.d.ts +1 -12
- package/dist/core/ingestion/symbol-table.js +12 -19
- package/dist/core/ingestion/tree-sitter-queries.d.ts +11 -11
- package/dist/core/ingestion/tree-sitter-queries.js +485 -590
- package/dist/core/ingestion/utils.d.ts +0 -67
- package/dist/core/ingestion/utils.js +9 -692
- package/dist/core/ingestion/workers/parse-worker.d.ts +3 -20
- package/dist/core/ingestion/workers/parse-worker.js +345 -84
- package/dist/core/ingestion/workers/worker-pool.js +0 -8
- package/dist/core/kuzu/csv-generator.js +3 -19
- package/dist/core/kuzu/kuzu-adapter.js +19 -14
- package/dist/core/kuzu/schema.d.ts +3 -3
- package/dist/core/kuzu/schema.js +288 -303
- package/dist/core/search/bm25-index.js +6 -7
- package/dist/core/search/hybrid-search.js +3 -3
- package/dist/core/wiki/diagrams.d.ts +27 -0
- package/dist/core/wiki/diagrams.js +163 -0
- package/dist/core/wiki/generator.d.ts +50 -2
- package/dist/core/wiki/generator.js +548 -49
- package/dist/core/wiki/graph-queries.d.ts +42 -0
- package/dist/core/wiki/graph-queries.js +276 -97
- package/dist/core/wiki/html-viewer.js +192 -192
- package/dist/core/wiki/llm-client.js +73 -11
- package/dist/core/wiki/prompts.d.ts +52 -8
- package/dist/core/wiki/prompts.js +200 -86
- package/dist/mcp/core/kuzu-adapter.d.ts +3 -1
- package/dist/mcp/core/kuzu-adapter.js +44 -13
- package/dist/mcp/local/local-backend.js +128 -128
- package/dist/mcp/resources.js +42 -42
- package/dist/mcp/server.js +19 -18
- package/dist/mcp/tools.js +104 -103
- package/hooks/claude/gitnexus-hook.cjs +155 -238
- package/hooks/claude/pre-tool-use.sh +79 -79
- package/hooks/claude/session-start.sh +42 -42
- package/package.json +96 -96
- package/scripts/patch-tree-sitter-swift.cjs +74 -74
- package/skills/gitnexus-cli.md +82 -82
- package/skills/gitnexus-debugging.md +89 -89
- package/skills/gitnexus-exploring.md +78 -78
- package/skills/gitnexus-guide.md +64 -64
- package/skills/gitnexus-impact-analysis.md +97 -97
- package/skills/gitnexus-pr-review.md +163 -163
- package/skills/gitnexus-refactoring.md +121 -121
- package/vendor/leiden/index.cjs +355 -355
- package/vendor/leiden/utils.cjs +392 -392
- package/dist/cli/lazy-action.d.ts +0 -6
- package/dist/cli/lazy-action.js +0 -18
- package/dist/cli/skill-gen.d.ts +0 -26
- package/dist/cli/skill-gen.js +0 -549
- package/dist/core/ingestion/constants.d.ts +0 -16
- package/dist/core/ingestion/constants.js +0 -16
- package/dist/core/ingestion/export-detection.d.ts +0 -18
- package/dist/core/ingestion/export-detection.js +0 -230
- package/dist/core/ingestion/language-config.d.ts +0 -46
- package/dist/core/ingestion/language-config.js +0 -167
- package/dist/core/ingestion/mro-processor.d.ts +0 -45
- package/dist/core/ingestion/mro-processor.js +0 -369
- package/dist/core/ingestion/named-binding-extraction.d.ts +0 -61
- package/dist/core/ingestion/named-binding-extraction.js +0 -363
- package/dist/core/ingestion/resolvers/csharp.d.ts +0 -22
- package/dist/core/ingestion/resolvers/csharp.js +0 -109
- package/dist/core/ingestion/resolvers/go.d.ts +0 -19
- package/dist/core/ingestion/resolvers/go.js +0 -42
- package/dist/core/ingestion/resolvers/index.d.ts +0 -16
- package/dist/core/ingestion/resolvers/index.js +0 -11
- package/dist/core/ingestion/resolvers/jvm.d.ts +0 -23
- package/dist/core/ingestion/resolvers/jvm.js +0 -87
- package/dist/core/ingestion/resolvers/php.d.ts +0 -15
- package/dist/core/ingestion/resolvers/php.js +0 -35
- package/dist/core/ingestion/resolvers/rust.d.ts +0 -15
- package/dist/core/ingestion/resolvers/rust.js +0 -73
- package/dist/core/ingestion/resolvers/standard.d.ts +0 -28
- package/dist/core/ingestion/resolvers/standard.js +0 -145
- package/dist/core/ingestion/resolvers/utils.d.ts +0 -33
- package/dist/core/ingestion/resolvers/utils.js +0 -120
- package/dist/core/ingestion/symbol-resolver.d.ts +0 -32
- package/dist/core/ingestion/symbol-resolver.js +0 -83
- package/dist/core/ingestion/type-env.d.ts +0 -27
- package/dist/core/ingestion/type-env.js +0 -86
- package/dist/core/ingestion/type-extractors/c-cpp.d.ts +0 -2
- package/dist/core/ingestion/type-extractors/c-cpp.js +0 -60
- package/dist/core/ingestion/type-extractors/csharp.d.ts +0 -2
- package/dist/core/ingestion/type-extractors/csharp.js +0 -89
- package/dist/core/ingestion/type-extractors/go.d.ts +0 -2
- package/dist/core/ingestion/type-extractors/go.js +0 -105
- package/dist/core/ingestion/type-extractors/index.d.ts +0 -21
- package/dist/core/ingestion/type-extractors/index.js +0 -29
- package/dist/core/ingestion/type-extractors/jvm.d.ts +0 -3
- package/dist/core/ingestion/type-extractors/jvm.js +0 -121
- package/dist/core/ingestion/type-extractors/php.d.ts +0 -2
- package/dist/core/ingestion/type-extractors/php.js +0 -31
- package/dist/core/ingestion/type-extractors/python.d.ts +0 -2
- package/dist/core/ingestion/type-extractors/python.js +0 -41
- package/dist/core/ingestion/type-extractors/rust.d.ts +0 -2
- package/dist/core/ingestion/type-extractors/rust.js +0 -39
- package/dist/core/ingestion/type-extractors/shared.d.ts +0 -17
- package/dist/core/ingestion/type-extractors/shared.js +0 -97
- package/dist/core/ingestion/type-extractors/swift.d.ts +0 -2
- package/dist/core/ingestion/type-extractors/swift.js +0 -43
- package/dist/core/ingestion/type-extractors/types.d.ts +0 -14
- package/dist/core/ingestion/type-extractors/types.js +0 -1
- package/dist/core/ingestion/type-extractors/typescript.d.ts +0 -2
- package/dist/core/ingestion/type-extractors/typescript.js +0 -46
- package/dist/mcp/compatible-stdio-transport.d.ts +0 -25
- 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 —
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
130
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
+
}
|