gitnexus 1.4.10 → 1.5.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 (186) hide show
  1. package/README.md +6 -5
  2. package/dist/cli/ai-context.d.ts +4 -1
  3. package/dist/cli/ai-context.js +19 -11
  4. package/dist/cli/analyze.d.ts +6 -0
  5. package/dist/cli/analyze.js +105 -251
  6. package/dist/cli/eval-server.js +20 -11
  7. package/dist/cli/index-repo.js +20 -22
  8. package/dist/cli/index.js +8 -7
  9. package/dist/cli/mcp.js +1 -1
  10. package/dist/cli/serve.js +29 -1
  11. package/dist/cli/setup.js +9 -9
  12. package/dist/cli/skill-gen.js +15 -9
  13. package/dist/cli/wiki.d.ts +2 -0
  14. package/dist/cli/wiki.js +141 -26
  15. package/dist/config/ignore-service.js +102 -22
  16. package/dist/config/supported-languages.d.ts +8 -42
  17. package/dist/config/supported-languages.js +8 -43
  18. package/dist/core/augmentation/engine.js +19 -7
  19. package/dist/core/embeddings/embedder.js +19 -15
  20. package/dist/core/embeddings/embedding-pipeline.js +6 -6
  21. package/dist/core/embeddings/http-client.js +3 -3
  22. package/dist/core/embeddings/text-generator.js +9 -24
  23. package/dist/core/embeddings/types.d.ts +1 -1
  24. package/dist/core/embeddings/types.js +1 -7
  25. package/dist/core/graph/graph.js +6 -2
  26. package/dist/core/graph/types.d.ts +9 -59
  27. package/dist/core/ingestion/ast-cache.js +3 -3
  28. package/dist/core/ingestion/call-processor.d.ts +20 -2
  29. package/dist/core/ingestion/call-processor.js +347 -144
  30. package/dist/core/ingestion/call-routing.js +10 -4
  31. package/dist/core/ingestion/call-sites/extract-language-call-site.d.ts +10 -0
  32. package/dist/core/ingestion/call-sites/extract-language-call-site.js +22 -0
  33. package/dist/core/ingestion/call-sites/java.d.ts +9 -0
  34. package/dist/core/ingestion/call-sites/java.js +30 -0
  35. package/dist/core/ingestion/cluster-enricher.js +6 -8
  36. package/dist/core/ingestion/cobol/cobol-copy-expander.js +10 -3
  37. package/dist/core/ingestion/cobol/cobol-preprocessor.js +287 -81
  38. package/dist/core/ingestion/cobol/jcl-parser.js +1 -1
  39. package/dist/core/ingestion/cobol/jcl-processor.js +1 -1
  40. package/dist/core/ingestion/cobol-processor.js +102 -56
  41. package/dist/core/ingestion/community-processor.js +21 -15
  42. package/dist/core/ingestion/entry-point-scoring.d.ts +1 -1
  43. package/dist/core/ingestion/entry-point-scoring.js +5 -6
  44. package/dist/core/ingestion/export-detection.js +32 -9
  45. package/dist/core/ingestion/field-extractor.d.ts +1 -1
  46. package/dist/core/ingestion/field-extractors/configs/c-cpp.js +8 -12
  47. package/dist/core/ingestion/field-extractors/configs/csharp.js +45 -2
  48. package/dist/core/ingestion/field-extractors/configs/dart.js +5 -3
  49. package/dist/core/ingestion/field-extractors/configs/go.js +3 -7
  50. package/dist/core/ingestion/field-extractors/configs/helpers.d.ts +5 -0
  51. package/dist/core/ingestion/field-extractors/configs/helpers.js +14 -0
  52. package/dist/core/ingestion/field-extractors/configs/jvm.js +7 -7
  53. package/dist/core/ingestion/field-extractors/configs/php.js +9 -11
  54. package/dist/core/ingestion/field-extractors/configs/python.js +1 -1
  55. package/dist/core/ingestion/field-extractors/configs/ruby.js +4 -3
  56. package/dist/core/ingestion/field-extractors/configs/rust.js +2 -5
  57. package/dist/core/ingestion/field-extractors/configs/swift.js +9 -7
  58. package/dist/core/ingestion/field-extractors/configs/typescript-javascript.js +2 -6
  59. package/dist/core/ingestion/field-extractors/generic.d.ts +5 -2
  60. package/dist/core/ingestion/field-extractors/generic.js +6 -0
  61. package/dist/core/ingestion/field-extractors/typescript.d.ts +1 -1
  62. package/dist/core/ingestion/field-extractors/typescript.js +1 -1
  63. package/dist/core/ingestion/field-types.d.ts +4 -2
  64. package/dist/core/ingestion/filesystem-walker.js +3 -3
  65. package/dist/core/ingestion/framework-detection.d.ts +1 -1
  66. package/dist/core/ingestion/framework-detection.js +355 -85
  67. package/dist/core/ingestion/heritage-processor.d.ts +24 -0
  68. package/dist/core/ingestion/heritage-processor.js +99 -8
  69. package/dist/core/ingestion/import-processor.js +44 -15
  70. package/dist/core/ingestion/import-resolvers/csharp.js +7 -3
  71. package/dist/core/ingestion/import-resolvers/dart.js +1 -1
  72. package/dist/core/ingestion/import-resolvers/go.js +4 -2
  73. package/dist/core/ingestion/import-resolvers/jvm.js +4 -4
  74. package/dist/core/ingestion/import-resolvers/php.js +4 -4
  75. package/dist/core/ingestion/import-resolvers/python.js +1 -1
  76. package/dist/core/ingestion/import-resolvers/rust.js +9 -3
  77. package/dist/core/ingestion/import-resolvers/standard.d.ts +1 -1
  78. package/dist/core/ingestion/import-resolvers/standard.js +6 -5
  79. package/dist/core/ingestion/import-resolvers/swift.js +2 -1
  80. package/dist/core/ingestion/import-resolvers/utils.js +26 -7
  81. package/dist/core/ingestion/language-config.js +5 -4
  82. package/dist/core/ingestion/language-provider.d.ts +7 -2
  83. package/dist/core/ingestion/languages/c-cpp.js +106 -21
  84. package/dist/core/ingestion/languages/cobol.js +1 -1
  85. package/dist/core/ingestion/languages/csharp.js +96 -19
  86. package/dist/core/ingestion/languages/dart.js +23 -7
  87. package/dist/core/ingestion/languages/go.js +1 -1
  88. package/dist/core/ingestion/languages/index.d.ts +1 -1
  89. package/dist/core/ingestion/languages/index.js +2 -3
  90. package/dist/core/ingestion/languages/java.js +4 -1
  91. package/dist/core/ingestion/languages/kotlin.js +60 -13
  92. package/dist/core/ingestion/languages/php.js +102 -25
  93. package/dist/core/ingestion/languages/python.js +28 -5
  94. package/dist/core/ingestion/languages/ruby.js +56 -14
  95. package/dist/core/ingestion/languages/rust.js +55 -11
  96. package/dist/core/ingestion/languages/swift.js +112 -27
  97. package/dist/core/ingestion/languages/typescript.js +95 -19
  98. package/dist/core/ingestion/markdown-processor.js +5 -5
  99. package/dist/core/ingestion/method-extractors/configs/csharp.d.ts +2 -0
  100. package/dist/core/ingestion/method-extractors/configs/csharp.js +283 -0
  101. package/dist/core/ingestion/method-extractors/configs/jvm.d.ts +3 -0
  102. package/dist/core/ingestion/method-extractors/configs/jvm.js +326 -0
  103. package/dist/core/ingestion/method-extractors/generic.d.ts +5 -0
  104. package/dist/core/ingestion/method-extractors/generic.js +137 -0
  105. package/dist/core/ingestion/method-types.d.ts +61 -0
  106. package/dist/core/ingestion/method-types.js +2 -0
  107. package/dist/core/ingestion/mro-processor.d.ts +1 -1
  108. package/dist/core/ingestion/mro-processor.js +12 -8
  109. package/dist/core/ingestion/named-binding-processor.js +2 -2
  110. package/dist/core/ingestion/named-bindings/rust.js +3 -1
  111. package/dist/core/ingestion/parsing-processor.js +74 -24
  112. package/dist/core/ingestion/pipeline.d.ts +2 -1
  113. package/dist/core/ingestion/pipeline.js +208 -102
  114. package/dist/core/ingestion/process-processor.js +12 -10
  115. package/dist/core/ingestion/resolution-context.js +3 -3
  116. package/dist/core/ingestion/route-extractors/middleware.js +31 -7
  117. package/dist/core/ingestion/route-extractors/php.js +2 -1
  118. package/dist/core/ingestion/route-extractors/response-shapes.js +8 -4
  119. package/dist/core/ingestion/structure-processor.d.ts +1 -1
  120. package/dist/core/ingestion/structure-processor.js +4 -4
  121. package/dist/core/ingestion/symbol-table.d.ts +1 -1
  122. package/dist/core/ingestion/symbol-table.js +22 -6
  123. package/dist/core/ingestion/tree-sitter-queries.d.ts +1 -1
  124. package/dist/core/ingestion/tree-sitter-queries.js +1 -1
  125. package/dist/core/ingestion/type-env.d.ts +2 -2
  126. package/dist/core/ingestion/type-env.js +75 -50
  127. package/dist/core/ingestion/type-extractors/c-cpp.js +33 -30
  128. package/dist/core/ingestion/type-extractors/csharp.js +24 -14
  129. package/dist/core/ingestion/type-extractors/dart.js +6 -8
  130. package/dist/core/ingestion/type-extractors/go.js +7 -6
  131. package/dist/core/ingestion/type-extractors/jvm.js +10 -21
  132. package/dist/core/ingestion/type-extractors/php.js +26 -13
  133. package/dist/core/ingestion/type-extractors/python.js +11 -15
  134. package/dist/core/ingestion/type-extractors/ruby.js +8 -3
  135. package/dist/core/ingestion/type-extractors/rust.js +6 -8
  136. package/dist/core/ingestion/type-extractors/shared.js +134 -50
  137. package/dist/core/ingestion/type-extractors/swift.js +16 -13
  138. package/dist/core/ingestion/type-extractors/typescript.js +23 -15
  139. package/dist/core/ingestion/utils/ast-helpers.d.ts +8 -8
  140. package/dist/core/ingestion/utils/ast-helpers.js +72 -35
  141. package/dist/core/ingestion/utils/call-analysis.d.ts +2 -0
  142. package/dist/core/ingestion/utils/call-analysis.js +96 -49
  143. package/dist/core/ingestion/utils/event-loop.js +1 -1
  144. package/dist/core/ingestion/workers/parse-worker.d.ts +7 -2
  145. package/dist/core/ingestion/workers/parse-worker.js +364 -84
  146. package/dist/core/ingestion/workers/worker-pool.js +5 -10
  147. package/dist/core/lbug/csv-generator.js +54 -15
  148. package/dist/core/lbug/lbug-adapter.d.ts +5 -0
  149. package/dist/core/lbug/lbug-adapter.js +86 -23
  150. package/dist/core/lbug/schema.d.ts +3 -6
  151. package/dist/core/lbug/schema.js +6 -30
  152. package/dist/core/run-analyze.d.ts +49 -0
  153. package/dist/core/run-analyze.js +257 -0
  154. package/dist/core/tree-sitter/parser-loader.d.ts +1 -1
  155. package/dist/core/tree-sitter/parser-loader.js +1 -1
  156. package/dist/core/wiki/cursor-client.js +2 -7
  157. package/dist/core/wiki/generator.js +38 -23
  158. package/dist/core/wiki/graph-queries.js +10 -10
  159. package/dist/core/wiki/html-viewer.js +7 -3
  160. package/dist/core/wiki/llm-client.d.ts +23 -2
  161. package/dist/core/wiki/llm-client.js +96 -26
  162. package/dist/core/wiki/prompts.js +7 -6
  163. package/dist/mcp/core/embedder.js +1 -1
  164. package/dist/mcp/core/lbug-adapter.d.ts +4 -1
  165. package/dist/mcp/core/lbug-adapter.js +17 -7
  166. package/dist/mcp/local/local-backend.js +247 -95
  167. package/dist/mcp/resources.js +14 -6
  168. package/dist/mcp/server.js +13 -5
  169. package/dist/mcp/staleness.js +5 -1
  170. package/dist/mcp/tools.js +100 -23
  171. package/dist/server/analyze-job.d.ts +53 -0
  172. package/dist/server/analyze-job.js +146 -0
  173. package/dist/server/analyze-worker.d.ts +13 -0
  174. package/dist/server/analyze-worker.js +59 -0
  175. package/dist/server/api.js +795 -44
  176. package/dist/server/git-clone.d.ts +25 -0
  177. package/dist/server/git-clone.js +91 -0
  178. package/dist/storage/git.js +1 -3
  179. package/dist/storage/repo-manager.d.ts +5 -2
  180. package/dist/storage/repo-manager.js +4 -4
  181. package/dist/types/pipeline.d.ts +1 -21
  182. package/dist/types/pipeline.js +1 -18
  183. package/hooks/claude/gitnexus-hook.cjs +52 -22
  184. package/package.json +3 -2
  185. package/dist/core/ingestion/utils/language-detection.d.ts +0 -9
  186. package/dist/core/ingestion/utils/language-detection.js +0 -70
@@ -0,0 +1,257 @@
1
+ /**
2
+ * Shared Analysis Orchestrator
3
+ *
4
+ * Extracts the core analysis pipeline from the CLI analyze command into a
5
+ * reusable function that can be called from both the CLI and a server-side
6
+ * worker process.
7
+ *
8
+ * IMPORTANT: This module must NEVER call process.exit(). The caller (CLI
9
+ * wrapper or server worker) is responsible for process lifecycle.
10
+ */
11
+ import path from 'path';
12
+ import fs from 'fs/promises';
13
+ import { runPipelineFromRepo } from './ingestion/pipeline.js';
14
+ import { initLbug, loadGraphToLbug, getLbugStats, executeQuery, executeWithReusedStatement, closeLbug, createFTSIndex, loadCachedEmbeddings, } from './lbug/lbug-adapter.js';
15
+ import { getStoragePaths, saveMeta, loadMeta, addToGitignore, registerRepo, cleanupOldKuzuFiles, } from '../storage/repo-manager.js';
16
+ import { getCurrentCommit, hasGitDir } from '../storage/git.js';
17
+ import { generateAIContextFiles } from '../cli/ai-context.js';
18
+ /** Threshold: auto-skip embeddings for repos with more nodes than this */
19
+ const EMBEDDING_NODE_LIMIT = 50_000;
20
+ export const PHASE_LABELS = {
21
+ extracting: 'Scanning files',
22
+ structure: 'Building structure',
23
+ parsing: 'Parsing code',
24
+ imports: 'Resolving imports',
25
+ calls: 'Tracing calls',
26
+ heritage: 'Extracting inheritance',
27
+ communities: 'Detecting communities',
28
+ processes: 'Detecting processes',
29
+ complete: 'Pipeline complete',
30
+ lbug: 'Loading into LadybugDB',
31
+ fts: 'Creating search indexes',
32
+ embeddings: 'Generating embeddings',
33
+ done: 'Done',
34
+ };
35
+ // ---------------------------------------------------------------------------
36
+ // Main orchestrator
37
+ // ---------------------------------------------------------------------------
38
+ /**
39
+ * Run the full GitNexus analysis pipeline.
40
+ *
41
+ * This is the shared core extracted from the CLI `analyze` command. It
42
+ * handles: pipeline execution, LadybugDB loading, FTS indexing, embedding
43
+ * generation, metadata persistence, and AI context file generation.
44
+ *
45
+ * The function communicates progress and log messages exclusively through
46
+ * the {@link AnalyzeCallbacks} interface — it never writes to stdout/stderr
47
+ * directly and never calls `process.exit()`.
48
+ */
49
+ export async function runFullAnalysis(repoPath, options, callbacks) {
50
+ const log = (msg) => callbacks.onLog?.(msg);
51
+ const progress = (phase, percent, message) => callbacks.onProgress(phase, percent, message);
52
+ const { storagePath, lbugPath } = getStoragePaths(repoPath);
53
+ // Clean up stale KuzuDB files from before the LadybugDB migration.
54
+ const kuzuResult = await cleanupOldKuzuFiles(storagePath);
55
+ if (kuzuResult.found && kuzuResult.needsReindex) {
56
+ log('Migrating from KuzuDB to LadybugDB — rebuilding index...');
57
+ }
58
+ const repoHasGit = hasGitDir(repoPath);
59
+ const currentCommit = repoHasGit ? getCurrentCommit(repoPath) : '';
60
+ const existingMeta = await loadMeta(storagePath);
61
+ // ── Early-return: already up to date ──────────────────────────────
62
+ if (existingMeta && !options.force && existingMeta.lastCommit === currentCommit) {
63
+ // Non-git folders have currentCommit = '' — always rebuild since we can't detect changes
64
+ if (currentCommit !== '') {
65
+ return {
66
+ repoName: path.basename(repoPath),
67
+ repoPath,
68
+ stats: existingMeta.stats ?? {},
69
+ alreadyUpToDate: true,
70
+ };
71
+ }
72
+ }
73
+ // ── Cache embeddings from existing index before rebuild ────────────
74
+ let cachedEmbeddingNodeIds = new Set();
75
+ let cachedEmbeddings = [];
76
+ if (options.embeddings && existingMeta && !options.force) {
77
+ try {
78
+ progress('embeddings', 0, 'Caching embeddings...');
79
+ await initLbug(lbugPath);
80
+ const cached = await loadCachedEmbeddings();
81
+ cachedEmbeddingNodeIds = cached.embeddingNodeIds;
82
+ cachedEmbeddings = cached.embeddings;
83
+ await closeLbug();
84
+ }
85
+ catch {
86
+ try {
87
+ await closeLbug();
88
+ }
89
+ catch {
90
+ /* swallow */
91
+ }
92
+ }
93
+ }
94
+ // ── Phase 1: Full Pipeline (0–60%) ────────────────────────────────
95
+ const pipelineResult = await runPipelineFromRepo(repoPath, (p) => {
96
+ const phaseLabel = PHASE_LABELS[p.phase] || p.phase;
97
+ const scaled = Math.round(p.percent * 0.6);
98
+ progress(p.phase, scaled, phaseLabel);
99
+ });
100
+ // ── Phase 2: LadybugDB (60–85%) ──────────────────────────────────
101
+ progress('lbug', 60, 'Loading into LadybugDB...');
102
+ await closeLbug();
103
+ const lbugFiles = [lbugPath, `${lbugPath}.wal`, `${lbugPath}.lock`];
104
+ for (const f of lbugFiles) {
105
+ try {
106
+ await fs.rm(f, { recursive: true, force: true });
107
+ }
108
+ catch {
109
+ /* swallow */
110
+ }
111
+ }
112
+ await initLbug(lbugPath);
113
+ try {
114
+ // All work after initLbug is wrapped in try/finally to ensure closeLbug()
115
+ // is called even if an error occurs — the module-level singleton DB handle
116
+ // must be released to avoid blocking subsequent invocations.
117
+ let lbugMsgCount = 0;
118
+ await loadGraphToLbug(pipelineResult.graph, pipelineResult.repoPath, storagePath, (msg) => {
119
+ lbugMsgCount++;
120
+ const pct = Math.min(84, 60 + Math.round((lbugMsgCount / (lbugMsgCount + 10)) * 24));
121
+ progress('lbug', pct, msg);
122
+ });
123
+ // ── Phase 3: FTS (85–90%) ─────────────────────────────────────────
124
+ progress('fts', 85, 'Creating search indexes...');
125
+ try {
126
+ await createFTSIndex('File', 'file_fts', ['name', 'content']);
127
+ await createFTSIndex('Function', 'function_fts', ['name', 'content']);
128
+ await createFTSIndex('Class', 'class_fts', ['name', 'content']);
129
+ await createFTSIndex('Method', 'method_fts', ['name', 'content']);
130
+ await createFTSIndex('Interface', 'interface_fts', ['name', 'content']);
131
+ }
132
+ catch {
133
+ // Non-fatal — FTS is best-effort
134
+ }
135
+ // ── Phase 3.5: Re-insert cached embeddings ────────────────────────
136
+ if (cachedEmbeddings.length > 0) {
137
+ const cachedDims = cachedEmbeddings[0].embedding.length;
138
+ const { EMBEDDING_DIMS } = await import('./lbug/schema.js');
139
+ if (cachedDims !== EMBEDDING_DIMS) {
140
+ // Dimensions changed (e.g. switched embedding model) — discard cache and re-embed all
141
+ log(`Embedding dimensions changed (${cachedDims}d -> ${EMBEDDING_DIMS}d), discarding cache`);
142
+ cachedEmbeddings = [];
143
+ cachedEmbeddingNodeIds = new Set();
144
+ }
145
+ else {
146
+ progress('embeddings', 88, `Restoring ${cachedEmbeddings.length} cached embeddings...`);
147
+ const EMBED_BATCH = 200;
148
+ for (let i = 0; i < cachedEmbeddings.length; i += EMBED_BATCH) {
149
+ const batch = cachedEmbeddings.slice(i, i + EMBED_BATCH);
150
+ const paramsList = batch.map((e) => ({ nodeId: e.nodeId, embedding: e.embedding }));
151
+ try {
152
+ await executeWithReusedStatement(`CREATE (e:CodeEmbedding {nodeId: $nodeId, embedding: $embedding})`, paramsList);
153
+ }
154
+ catch {
155
+ /* some may fail if node was removed, that's fine */
156
+ }
157
+ }
158
+ }
159
+ }
160
+ // ── Phase 4: Embeddings (90–98%) ──────────────────────────────────
161
+ const stats = await getLbugStats();
162
+ let embeddingSkipped = true;
163
+ if (options.embeddings) {
164
+ if (stats.nodes <= EMBEDDING_NODE_LIMIT) {
165
+ embeddingSkipped = false;
166
+ }
167
+ }
168
+ if (!embeddingSkipped) {
169
+ const { isHttpMode } = await import('./embeddings/http-client.js');
170
+ const httpMode = isHttpMode();
171
+ progress('embeddings', 90, httpMode ? 'Connecting to embedding endpoint...' : 'Loading embedding model...');
172
+ const { runEmbeddingPipeline } = await import('./embeddings/embedding-pipeline.js');
173
+ await runEmbeddingPipeline(executeQuery, executeWithReusedStatement, (p) => {
174
+ const scaled = 90 + Math.round((p.percent / 100) * 8);
175
+ const label = p.phase === 'loading-model'
176
+ ? httpMode
177
+ ? 'Connecting to embedding endpoint...'
178
+ : 'Loading embedding model...'
179
+ : `Embedding ${p.nodesProcessed || 0}/${p.totalNodes || '?'}`;
180
+ progress('embeddings', scaled, label);
181
+ }, {}, cachedEmbeddingNodeIds.size > 0 ? cachedEmbeddingNodeIds : undefined);
182
+ }
183
+ // ── Phase 5: Finalize (98–100%) ───────────────────────────────────
184
+ progress('done', 98, 'Saving metadata...');
185
+ // Count embeddings in the index (cached + newly generated)
186
+ let embeddingCount = 0;
187
+ try {
188
+ const embResult = await executeQuery(`MATCH (e:CodeEmbedding) RETURN count(e) AS cnt`);
189
+ embeddingCount = embResult?.[0]?.cnt ?? 0;
190
+ }
191
+ catch {
192
+ /* table may not exist if embeddings never ran */
193
+ }
194
+ const meta = {
195
+ repoPath,
196
+ lastCommit: currentCommit,
197
+ indexedAt: new Date().toISOString(),
198
+ stats: {
199
+ files: pipelineResult.totalFileCount,
200
+ nodes: stats.nodes,
201
+ edges: stats.edges,
202
+ communities: pipelineResult.communityResult?.stats.totalCommunities,
203
+ processes: pipelineResult.processResult?.stats.totalProcesses,
204
+ embeddings: embeddingCount,
205
+ },
206
+ };
207
+ await saveMeta(storagePath, meta);
208
+ await registerRepo(repoPath, meta);
209
+ // Only attempt to update .gitignore when a .git directory is present.
210
+ if (hasGitDir(repoPath)) {
211
+ await addToGitignore(repoPath);
212
+ }
213
+ const projectName = path.basename(repoPath);
214
+ // ── Generate AI context files (best-effort) ───────────────────────
215
+ let aggregatedClusterCount = 0;
216
+ if (pipelineResult.communityResult?.communities) {
217
+ const groups = new Map();
218
+ for (const c of pipelineResult.communityResult.communities) {
219
+ const label = c.heuristicLabel || c.label || 'Unknown';
220
+ groups.set(label, (groups.get(label) || 0) + c.symbolCount);
221
+ }
222
+ aggregatedClusterCount = Array.from(groups.values()).filter((count) => count >= 5).length;
223
+ }
224
+ try {
225
+ await generateAIContextFiles(repoPath, storagePath, projectName, {
226
+ files: pipelineResult.totalFileCount,
227
+ nodes: stats.nodes,
228
+ edges: stats.edges,
229
+ communities: pipelineResult.communityResult?.stats.totalCommunities,
230
+ clusters: aggregatedClusterCount,
231
+ processes: pipelineResult.processResult?.stats.totalProcesses,
232
+ }, undefined, { skipAgentsMd: options.skipAgentsMd });
233
+ }
234
+ catch {
235
+ // Best-effort — don't fail the entire analysis for context file issues
236
+ }
237
+ // ── Close LadybugDB ──────────────────────────────────────────────
238
+ await closeLbug();
239
+ progress('done', 100, 'Done');
240
+ return {
241
+ repoName: projectName,
242
+ repoPath,
243
+ stats: meta.stats,
244
+ pipelineResult,
245
+ };
246
+ }
247
+ catch (err) {
248
+ // Ensure LadybugDB is closed even on error
249
+ try {
250
+ await closeLbug();
251
+ }
252
+ catch {
253
+ /* swallow */
254
+ }
255
+ throw err;
256
+ }
257
+ }
@@ -1,5 +1,5 @@
1
1
  import Parser from 'tree-sitter';
2
- import { SupportedLanguages } from '../../config/supported-languages.js';
2
+ import { SupportedLanguages } from 'gitnexus-shared';
3
3
  export declare const isLanguageAvailable: (language: SupportedLanguages) => boolean;
4
4
  export declare const loadParser: () => Promise<Parser>;
5
5
  export declare const loadLanguage: (language: SupportedLanguages, filePath?: string) => Promise<void>;
@@ -11,7 +11,7 @@ import Rust from 'tree-sitter-rust';
11
11
  import PHP from 'tree-sitter-php';
12
12
  import Ruby from 'tree-sitter-ruby';
13
13
  import { createRequire } from 'node:module';
14
- import { SupportedLanguages } from '../../config/supported-languages.js';
14
+ import { SupportedLanguages } from 'gitnexus-shared';
15
15
  // tree-sitter-swift and tree-sitter-dart are optionalDependencies — may not be installed
16
16
  const _require = createRequire(import.meta.url);
17
17
  let Swift = null;
@@ -57,13 +57,8 @@ export async function callCursorLLM(prompt, config, systemPrompt, options) {
57
57
  // Always use text format to get clean output without agent narration/thinking.
58
58
  // stream-json captures assistant messages which include "Let me explore..." narration
59
59
  // that pollutes the actual content when using thinking models.
60
- const fullPrompt = systemPrompt
61
- ? `${systemPrompt}\n\n---\n\n${prompt}`
62
- : prompt;
63
- const args = [
64
- '-p',
65
- '--output-format', 'text',
66
- ];
60
+ const fullPrompt = systemPrompt ? `${systemPrompt}\n\n---\n\n${prompt}` : prompt;
61
+ const args = ['-p', '--output-format', 'text'];
67
62
  if (config.model) {
68
63
  args.push('--model', config.model);
69
64
  }
@@ -15,7 +15,7 @@ import { execSync, execFileSync } from 'child_process';
15
15
  import { initWikiDb, closeWikiDb, touchWikiDb, getFilesWithExports, getAllFiles, getIntraModuleCallEdges, getInterModuleCallEdges, getProcessesForFiles, getAllProcesses, getInterModuleEdgesForOverview, } from './graph-queries.js';
16
16
  import { generateHTMLViewer } from './html-viewer.js';
17
17
  import { callLLM, estimateTokens, } from './llm-client.js';
18
- import { callCursorLLM, resolveCursorConfig, } from './cursor-client.js';
18
+ import { callCursorLLM, resolveCursorConfig } from './cursor-client.js';
19
19
  import { GROUPING_SYSTEM_PROMPT, GROUPING_USER_PROMPT, MODULE_SYSTEM_PROMPT, MODULE_USER_PROMPT, PARENT_SYSTEM_PROMPT, PARENT_USER_PROMPT, OVERVIEW_SYSTEM_PROMPT, OVERVIEW_USER_PROMPT, fillTemplate, formatFileListForGrouping, formatDirectoryTree, formatCallEdges, formatProcesses, } from './prompts.js';
20
20
  import { shouldIgnorePath } from '../../config/ignore-service.js';
21
21
  // ─── Constants ────────────────────────────────────────────────────────
@@ -72,7 +72,7 @@ export class WikiGenerator {
72
72
  if (hasFixedStart) {
73
73
  // For fixed phases (like grouping), show incremental progress
74
74
  const progress = Math.min(1, tokens / expectedTokens);
75
- const pct = Math.round(startPercent + (progress * percentRange));
75
+ const pct = Math.round(startPercent + progress * percentRange);
76
76
  this.onProgress('stream', pct, `${label} (${tokens} tok)`);
77
77
  }
78
78
  else {
@@ -155,7 +155,7 @@ export class WikiGenerator {
155
155
  async ensureHTMLViewer() {
156
156
  // Only generate if there are markdown pages to bundle
157
157
  const dirEntries = await fs.readdir(this.wikiDir).catch(() => []);
158
- const hasMd = dirEntries.some(f => f.endsWith('.md'));
158
+ const hasMd = dirEntries.some((f) => f.endsWith('.md'));
159
159
  if (!hasMd)
160
160
  return;
161
161
  this.onProgress('html', 98, 'Building HTML viewer...');
@@ -170,13 +170,13 @@ export class WikiGenerator {
170
170
  const filesWithExports = await getFilesWithExports();
171
171
  const allFiles = await getAllFiles();
172
172
  // Filter to source files only
173
- const sourceFiles = allFiles.filter(f => !shouldIgnorePath(f));
173
+ const sourceFiles = allFiles.filter((f) => !shouldIgnorePath(f));
174
174
  if (sourceFiles.length === 0) {
175
175
  throw new Error('No source files found in the knowledge graph. Nothing to document.');
176
176
  }
177
177
  // Build enriched file list (merge exports into all source files)
178
- const exportMap = new Map(filesWithExports.map(f => [f.filePath, f]));
179
- const enrichedFiles = sourceFiles.map(fp => {
178
+ const exportMap = new Map(filesWithExports.map((f) => [f.filePath, f]));
179
+ const enrichedFiles = sourceFiles.map((fp) => {
180
180
  return exportMap.get(fp) || { filePath: fp, symbols: [] };
181
181
  });
182
182
  this.onProgress('gather', 10, `Found ${sourceFiles.length} source files`);
@@ -187,7 +187,12 @@ export class WikiGenerator {
187
187
  if (this.options.reviewOnly) {
188
188
  await this.saveModuleTree(moduleTree);
189
189
  this.onProgress('review', 30, 'Module tree ready for review');
190
- const reviewResult = { pagesGenerated: 0, mode: 'full', failedModules: [], moduleTree };
190
+ const reviewResult = {
191
+ pagesGenerated: 0,
192
+ mode: 'full',
193
+ failedModules: [],
194
+ moduleTree,
195
+ };
191
196
  return reviewResult;
192
197
  }
193
198
  // Phase 2: Generate module pages (parallel with concurrency limit)
@@ -287,7 +292,7 @@ export class WikiGenerator {
287
292
  }
288
293
  this.onProgress('grouping', 15, 'Grouping files into modules (LLM)...');
289
294
  const fileList = formatFileListForGrouping(files);
290
- const dirTree = formatDirectoryTree(files.map(f => f.filePath));
295
+ const dirTree = formatDirectoryTree(files.map((f) => f.filePath));
291
296
  const prompt = fillTemplate(GROUPING_USER_PROMPT, {
292
297
  FILE_LIST: fileList,
293
298
  DIRECTORY_TREE: dirTree,
@@ -340,13 +345,13 @@ export class WikiGenerator {
340
345
  return this.fallbackGrouping(files);
341
346
  }
342
347
  // Validate — ensure all files are assigned
343
- const allFilePaths = new Set(files.map(f => f.filePath));
348
+ const allFilePaths = new Set(files.map((f) => f.filePath));
344
349
  const assignedFiles = new Set();
345
350
  const validGrouping = {};
346
351
  for (const [mod, paths] of Object.entries(parsed)) {
347
352
  if (!Array.isArray(paths))
348
353
  continue;
349
- const validPaths = paths.filter(p => {
354
+ const validPaths = paths.filter((p) => {
350
355
  if (allFilePaths.has(p) && !assignedFiles.has(p)) {
351
356
  assignedFiles.add(p);
352
357
  return true;
@@ -358,15 +363,11 @@ export class WikiGenerator {
358
363
  }
359
364
  }
360
365
  // Assign unassigned files to a "Miscellaneous" module
361
- const unassigned = files
362
- .map(f => f.filePath)
363
- .filter(fp => !assignedFiles.has(fp));
366
+ const unassigned = files.map((f) => f.filePath).filter((fp) => !assignedFiles.has(fp));
364
367
  if (unassigned.length > 0) {
365
368
  validGrouping['Other'] = unassigned;
366
369
  }
367
- return Object.keys(validGrouping).length > 0
368
- ? validGrouping
369
- : this.fallbackGrouping(files);
370
+ return Object.keys(validGrouping).length > 0 ? validGrouping : this.fallbackGrouping(files);
370
371
  }
371
372
  /**
372
373
  * Fallback grouping by top-level directory when LLM parsing fails.
@@ -403,7 +404,7 @@ export class WikiGenerator {
403
404
  group.push(fp);
404
405
  }
405
406
  // Check if basenames are unique; if not, use the full subDir path
406
- const basenames = Array.from(subGroups.keys()).map(s => path.basename(s));
407
+ const basenames = Array.from(subGroups.keys()).map((s) => path.basename(s));
407
408
  const hasCollisions = new Set(basenames).size < basenames.length;
408
409
  return Array.from(subGroups.entries()).map(([subDir, subFiles]) => {
409
410
  const label = hasCollisions ? subDir.replace(/\//g, '-') : path.basename(subDir);
@@ -469,7 +470,7 @@ export class WikiGenerator {
469
470
  }
470
471
  }
471
472
  // Get cross-child call edges
472
- const allChildFiles = node.children.flatMap(c => c.files);
473
+ const allChildFiles = node.children.flatMap((c) => c.files);
473
474
  const crossCalls = await getIntraModuleCallEdges(allChildFiles);
474
475
  const processes = await getProcessesForFiles(allChildFiles, 3);
475
476
  const prompt = fillTemplate(PARENT_USER_PROMPT, {
@@ -506,7 +507,7 @@ export class WikiGenerator {
506
507
  // Read project config
507
508
  const projectInfo = await this.readProjectInfo();
508
509
  const edgesText = moduleEdges.length > 0
509
- ? moduleEdges.map(e => `${e.from} → ${e.to} (${e.count} calls)`).join('\n')
510
+ ? moduleEdges.map((e) => `${e.from} → ${e.to} (${e.count} calls)`).join('\n')
510
511
  : 'No inter-module call edges detected';
511
512
  const prompt = fillTemplate(OVERVIEW_USER_PROMPT, {
512
513
  PROJECT_INFO: projectInfo,
@@ -644,7 +645,10 @@ export class WikiGenerator {
644
645
  */
645
646
  isCommitReachable(fromCommit, toCommit) {
646
647
  try {
647
- execFileSync('git', ['merge-base', '--is-ancestor', fromCommit, toCommit], { cwd: this.repoPath, stdio: 'ignore' });
648
+ execFileSync('git', ['merge-base', '--is-ancestor', fromCommit, toCommit], {
649
+ cwd: this.repoPath,
650
+ stdio: 'ignore',
651
+ });
648
652
  return true;
649
653
  }
650
654
  catch {
@@ -658,7 +662,11 @@ export class WikiGenerator {
658
662
  return null; // Signal that we can't compute diff (divergent branches)
659
663
  }
660
664
  try {
661
- const output = execFileSync('git', ['diff', `${fromCommit}..${toCommit}`, '--name-only'], { cwd: this.repoPath }).toString().trim();
665
+ const output = execFileSync('git', ['diff', `${fromCommit}..${toCommit}`, '--name-only'], {
666
+ cwd: this.repoPath,
667
+ })
668
+ .toString()
669
+ .trim();
662
670
  return output ? output.split('\n').filter(Boolean) : [];
663
671
  }
664
672
  catch {
@@ -700,7 +708,14 @@ export class WikiGenerator {
700
708
  return total;
701
709
  }
702
710
  async readProjectInfo() {
703
- const candidates = ['package.json', 'Cargo.toml', 'pyproject.toml', 'go.mod', 'pom.xml', 'build.gradle'];
711
+ const candidates = [
712
+ 'package.json',
713
+ 'Cargo.toml',
714
+ 'pyproject.toml',
715
+ 'go.mod',
716
+ 'pom.xml',
717
+ 'build.gradle',
718
+ ];
704
719
  const lines = [`Project: ${path.basename(this.repoPath)}`];
705
720
  for (const file of candidates) {
706
721
  const fullPath = path.join(this.repoPath, file);
@@ -742,7 +757,7 @@ export class WikiGenerator {
742
757
  const result = {};
743
758
  for (const node of tree) {
744
759
  if (node.children && node.children.length > 0) {
745
- result[node.name] = node.children.flatMap(c => c.files);
760
+ result[node.name] = node.children.flatMap((c) => c.files);
746
761
  for (const child of node.children) {
747
762
  result[child.name] = child.files;
748
763
  }
@@ -57,7 +57,7 @@ export async function getAllFiles() {
57
57
  RETURN f.filePath AS filePath
58
58
  ORDER BY f.filePath
59
59
  `);
60
- return rows.map(r => r.filePath || r[0]);
60
+ return rows.map((r) => r.filePath || r[0]);
61
61
  }
62
62
  /**
63
63
  * Get inter-file call edges (calls between different files).
@@ -69,7 +69,7 @@ export async function getInterFileCallEdges() {
69
69
  RETURN DISTINCT a.filePath AS fromFile, a.name AS fromName,
70
70
  b.filePath AS toFile, b.name AS toName
71
71
  `);
72
- return rows.map(r => ({
72
+ return rows.map((r) => ({
73
73
  fromFile: r.fromFile || r[0],
74
74
  fromName: r.fromName || r[1],
75
75
  toFile: r.toFile || r[2],
@@ -82,14 +82,14 @@ export async function getInterFileCallEdges() {
82
82
  export async function getIntraModuleCallEdges(filePaths) {
83
83
  if (filePaths.length === 0)
84
84
  return [];
85
- const fileList = filePaths.map(f => `'${f.replace(/'/g, "''")}'`).join(', ');
85
+ const fileList = filePaths.map((f) => `'${f.replace(/'/g, "''")}'`).join(', ');
86
86
  const rows = await executeQuery(REPO_ID, `
87
87
  MATCH (a)-[:CodeRelation {type: 'CALLS'}]->(b)
88
88
  WHERE a.filePath IN [${fileList}] AND b.filePath IN [${fileList}]
89
89
  RETURN DISTINCT a.filePath AS fromFile, a.name AS fromName,
90
90
  b.filePath AS toFile, b.name AS toName
91
91
  `);
92
- return rows.map(r => ({
92
+ return rows.map((r) => ({
93
93
  fromFile: r.fromFile || r[0],
94
94
  fromName: r.fromName || r[1],
95
95
  toFile: r.toFile || r[2],
@@ -102,7 +102,7 @@ export async function getIntraModuleCallEdges(filePaths) {
102
102
  export async function getInterModuleCallEdges(filePaths) {
103
103
  if (filePaths.length === 0)
104
104
  return { outgoing: [], incoming: [] };
105
- const fileList = filePaths.map(f => `'${f.replace(/'/g, "''")}'`).join(', ');
105
+ const fileList = filePaths.map((f) => `'${f.replace(/'/g, "''")}'`).join(', ');
106
106
  const outRows = await executeQuery(REPO_ID, `
107
107
  MATCH (a)-[:CodeRelation {type: 'CALLS'}]->(b)
108
108
  WHERE a.filePath IN [${fileList}] AND NOT b.filePath IN [${fileList}]
@@ -118,13 +118,13 @@ export async function getInterModuleCallEdges(filePaths) {
118
118
  LIMIT 30
119
119
  `);
120
120
  return {
121
- outgoing: outRows.map(r => ({
121
+ outgoing: outRows.map((r) => ({
122
122
  fromFile: r.fromFile || r[0],
123
123
  fromName: r.fromName || r[1],
124
124
  toFile: r.toFile || r[2],
125
125
  toName: r.toName || r[3],
126
126
  })),
127
- incoming: inRows.map(r => ({
127
+ incoming: inRows.map((r) => ({
128
128
  fromFile: r.fromFile || r[0],
129
129
  fromName: r.fromName || r[1],
130
130
  toFile: r.toFile || r[2],
@@ -139,7 +139,7 @@ export async function getInterModuleCallEdges(filePaths) {
139
139
  export async function getProcessesForFiles(filePaths, limit = 5) {
140
140
  if (filePaths.length === 0)
141
141
  return [];
142
- const fileList = filePaths.map(f => `'${f.replace(/'/g, "''")}'`).join(', ');
142
+ const fileList = filePaths.map((f) => `'${f.replace(/'/g, "''")}'`).join(', ');
143
143
  // Find processes that have steps in the given files
144
144
  const procRows = await executeQuery(REPO_ID, `
145
145
  MATCH (s)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
@@ -166,7 +166,7 @@ export async function getProcessesForFiles(filePaths, limit = 5) {
166
166
  label,
167
167
  type,
168
168
  stepCount,
169
- steps: stepRows.map(s => ({
169
+ steps: stepRows.map((s) => ({
170
170
  step: s.step || s[3] || 0,
171
171
  name: s.name || s[0],
172
172
  filePath: s.filePath || s[1],
@@ -203,7 +203,7 @@ export async function getAllProcesses(limit = 20) {
203
203
  label,
204
204
  type,
205
205
  stepCount,
206
- steps: stepRows.map(s => ({
206
+ steps: stepRows.map((s) => ({
207
207
  step: s.step || s[3] || 0,
208
208
  name: s.name || s[0],
209
209
  filePath: s.filePath || s[1],
@@ -16,18 +16,22 @@ export async function generateHTMLViewer(wikiDir, projectName) {
16
16
  const raw = await fs.readFile(path.join(wikiDir, 'module_tree.json'), 'utf-8');
17
17
  moduleTree = JSON.parse(raw);
18
18
  }
19
- catch { /* will show empty nav */ }
19
+ catch {
20
+ /* will show empty nav */
21
+ }
20
22
  // Load meta
21
23
  let meta = null;
22
24
  try {
23
25
  const raw = await fs.readFile(path.join(wikiDir, 'meta.json'), 'utf-8');
24
26
  meta = JSON.parse(raw);
25
27
  }
26
- catch { /* no meta */ }
28
+ catch {
29
+ /* no meta */
30
+ }
27
31
  // Read all markdown files into a { slug: content } map
28
32
  const pages = {};
29
33
  const dirEntries = await fs.readdir(wikiDir);
30
- for (const f of dirEntries.filter(f => f.endsWith('.md'))) {
34
+ for (const f of dirEntries.filter((f) => f.endsWith('.md'))) {
31
35
  const content = await fs.readFile(path.join(wikiDir, f), 'utf-8');
32
36
  pages[f.replace(/\.md$/, '')] = content;
33
37
  }
@@ -6,14 +6,19 @@
6
6
  *
7
7
  * Config priority: CLI flags > env vars > defaults
8
8
  */
9
- export type LLMProvider = 'openai' | 'cursor';
9
+ export type LLMProvider = 'openai' | 'openrouter' | 'azure' | 'custom' | 'cursor';
10
10
  export interface LLMConfig {
11
11
  apiKey: string;
12
12
  baseUrl: string;
13
13
  model: string;
14
14
  maxTokens: number;
15
15
  temperature: number;
16
- provider?: LLMProvider;
16
+ /** Provider type — controls auth header behaviour */
17
+ provider?: 'openai' | 'openrouter' | 'azure' | 'custom' | 'cursor';
18
+ /** Azure api-version query param (e.g. '2024-10-21'). Appended to URL when set. */
19
+ apiVersion?: string;
20
+ /** When true, strips sampling params and uses max_completion_tokens instead of max_tokens */
21
+ isReasoningModel?: boolean;
17
22
  }
18
23
  export interface LLMResponse {
19
24
  content: string;
@@ -31,6 +36,22 @@ export declare function resolveLLMConfig(overrides?: Partial<LLMConfig>): Promis
31
36
  * Estimate token count from text (rough heuristic: ~4 chars per token).
32
37
  */
33
38
  export declare function estimateTokens(text: string): number;
39
+ /**
40
+ * Returns true if the given base URL is an Azure OpenAI endpoint.
41
+ * Uses proper hostname matching to avoid spoofed URLs like
42
+ * "https://myresource.openai.azure.com.evil.com/v1".
43
+ */
44
+ export declare function isAzureProvider(baseUrl: string): boolean;
45
+ /**
46
+ * Returns true if the model name matches a known reasoning model pattern,
47
+ * or if the explicit override is true.
48
+ * Pass override=false to force non-reasoning even for o-series names.
49
+ */
50
+ export declare function isReasoningModel(model: string, override?: boolean): boolean;
51
+ /**
52
+ * Build the full chat completions URL, appending ?api-version when provided.
53
+ */
54
+ export declare function buildRequestUrl(baseUrl: string, apiVersion: string | undefined): string;
34
55
  export interface CallLLMOptions {
35
56
  onChunk?: (charsReceived: number) => void;
36
57
  }