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
package/README.md CHANGED
@@ -149,11 +149,12 @@ Your AI agent gets these tools automatically:
149
149
  ## CLI Commands
150
150
 
151
151
  ```bash
152
- gitnexus setup # Configure MCP for your editors (one-time)
153
- gitnexus analyze [path] # Index a repository (or update stale index)
154
- gitnexus analyze --force # Force full re-index
155
- gitnexus analyze --embeddings # Enable embedding generation (slower, better search)
156
- gitnexus analyze --verbose # Log skipped files when parsers are unavailable
152
+ gitnexus setup # Configure MCP for your editors (one-time)
153
+ gitnexus analyze [path] # Index a repository (or update stale index)
154
+ gitnexus analyze --force # Force full re-index
155
+ gitnexus analyze --embeddings # Enable embedding generation (slower, better search)
156
+ gitnexus analyze --skip-agents-md # Preserve custom AGENTS.md/CLAUDE.md gitnexus section edits
157
+ gitnexus analyze --verbose # Log skipped files when parsers are unavailable
157
158
  gitnexus mcp # Start MCP server (stdio) — serves all indexed repos
158
159
  gitnexus serve # Start local HTTP server (multi-repo) for web UI
159
160
  gitnexus index # Register an existing .gitnexus/ folder into the global registry
@@ -14,10 +14,13 @@ interface RepoStats {
14
14
  clusters?: number;
15
15
  processes?: number;
16
16
  }
17
+ export interface AIContextOptions {
18
+ skipAgentsMd?: boolean;
19
+ }
17
20
  /**
18
21
  * Generate AI context files after indexing
19
22
  */
20
- export declare function generateAIContextFiles(repoPath: string, _storagePath: string, projectName: string, stats: RepoStats, generatedSkills?: GeneratedSkillInfo[]): Promise<{
23
+ export declare function generateAIContextFiles(repoPath: string, _storagePath: string, projectName: string, stats: RepoStats, generatedSkills?: GeneratedSkillInfo[], options?: AIContextOptions): Promise<{
21
24
  files: string[];
22
25
  }>;
23
26
  export {};
@@ -25,8 +25,10 @@ const GITNEXUS_END_MARKER = '<!-- gitnexus:end -->';
25
25
  * - Self-review checklist — forces model to verify its own work
26
26
  */
27
27
  function generateGitNexusContent(projectName, stats, generatedSkills) {
28
- const generatedRows = (generatedSkills && generatedSkills.length > 0)
29
- ? generatedSkills.map(s => `| Work in the ${s.label} area (${s.symbolCount} symbols) | \`.claude/skills/generated/${s.name}/SKILL.md\` |`).join('\n')
28
+ const generatedRows = generatedSkills && generatedSkills.length > 0
29
+ ? generatedSkills
30
+ .map((s) => `| Work in the ${s.label} area (${s.symbolCount} symbols) | \`.claude/skills/generated/${s.name}/SKILL.md\` |`)
31
+ .join('\n')
30
32
  : '';
31
33
  const skillsTable = `| Task | Read this skill file |
32
34
  |------|---------------------|
@@ -245,17 +247,23 @@ Use GitNexus tools to accomplish this task.
245
247
  /**
246
248
  * Generate AI context files after indexing
247
249
  */
248
- export async function generateAIContextFiles(repoPath, _storagePath, projectName, stats, generatedSkills) {
250
+ export async function generateAIContextFiles(repoPath, _storagePath, projectName, stats, generatedSkills, options) {
249
251
  const content = generateGitNexusContent(projectName, stats, generatedSkills);
250
252
  const createdFiles = [];
251
- // Create AGENTS.md (standard for Cursor, Windsurf, OpenCode, Cline, etc.)
252
- const agentsPath = path.join(repoPath, 'AGENTS.md');
253
- const agentsResult = await upsertGitNexusSection(agentsPath, content);
254
- createdFiles.push(`AGENTS.md (${agentsResult})`);
255
- // Create CLAUDE.md (for Claude Code)
256
- const claudePath = path.join(repoPath, 'CLAUDE.md');
257
- const claudeResult = await upsertGitNexusSection(claudePath, content);
258
- createdFiles.push(`CLAUDE.md (${claudeResult})`);
253
+ if (!options?.skipAgentsMd) {
254
+ // Create AGENTS.md (standard for Cursor, Windsurf, OpenCode, Cline, etc.)
255
+ const agentsPath = path.join(repoPath, 'AGENTS.md');
256
+ const agentsResult = await upsertGitNexusSection(agentsPath, content);
257
+ createdFiles.push(`AGENTS.md (${agentsResult})`);
258
+ // Create CLAUDE.md (for Claude Code)
259
+ const claudePath = path.join(repoPath, 'CLAUDE.md');
260
+ const claudeResult = await upsertGitNexusSection(claudePath, content);
261
+ createdFiles.push(`CLAUDE.md (${claudeResult})`);
262
+ }
263
+ else {
264
+ createdFiles.push('AGENTS.md (skipped via --skip-agents-md)');
265
+ createdFiles.push('CLAUDE.md (skipped via --skip-agents-md)');
266
+ }
259
267
  // Install skills to .claude/skills/gitnexus/
260
268
  const installedSkills = await installSkills(repoPath);
261
269
  if (installedSkills.length > 0) {
@@ -2,12 +2,18 @@
2
2
  * Analyze Command
3
3
  *
4
4
  * Indexes a repository and stores the knowledge graph in .gitnexus/
5
+ *
6
+ * Delegates core analysis to the shared runFullAnalysis orchestrator.
7
+ * This CLI wrapper handles: heap management, progress bar, SIGINT,
8
+ * skill generation (--skills), summary output, and process.exit().
5
9
  */
6
10
  export interface AnalyzeOptions {
7
11
  force?: boolean;
8
12
  embeddings?: boolean;
9
13
  skills?: boolean;
10
14
  verbose?: boolean;
15
+ /** Skip AGENTS.md and CLAUDE.md gitnexus block updates. */
16
+ skipAgentsMd?: boolean;
11
17
  /** Index the folder even when no .git directory is present. */
12
18
  skipGit?: boolean;
13
19
  }
@@ -2,21 +2,19 @@
2
2
  * Analyze Command
3
3
  *
4
4
  * Indexes a repository and stores the knowledge graph in .gitnexus/
5
+ *
6
+ * Delegates core analysis to the shared runFullAnalysis orchestrator.
7
+ * This CLI wrapper handles: heap management, progress bar, SIGINT,
8
+ * skill generation (--skills), summary output, and process.exit().
5
9
  */
6
10
  import path from 'path';
7
11
  import { execFileSync } from 'child_process';
8
12
  import v8 from 'v8';
9
13
  import cliProgress from 'cli-progress';
10
- import { runPipelineFromRepo } from '../core/ingestion/pipeline.js';
11
- import { initLbug, loadGraphToLbug, getLbugStats, executeQuery, executeWithReusedStatement, closeLbug, createFTSIndex, loadCachedEmbeddings } from '../core/lbug/lbug-adapter.js';
12
- // Embedding imports are lazy (dynamic import) so onnxruntime-node is never
13
- // loaded when embeddings are not requested. This avoids crashes on Node
14
- // versions whose ABI is not yet supported by the native binary (#89).
15
- // disposeEmbedder intentionally not called — ONNX Runtime segfaults on cleanup (see #38)
16
- import { getStoragePaths, saveMeta, loadMeta, addToGitignore, registerRepo, getGlobalRegistryPath, cleanupOldKuzuFiles } from '../storage/repo-manager.js';
17
- import { getCurrentCommit, getGitRoot, hasGitDir } from '../storage/git.js';
18
- import { generateAIContextFiles } from './ai-context.js';
19
- import { generateSkillFiles } from './skill-gen.js';
14
+ import { closeLbug } from '../core/lbug/lbug-adapter.js';
15
+ import { getStoragePaths, getGlobalRegistryPath } from '../storage/repo-manager.js';
16
+ import { getGitRoot, hasGitDir } from '../storage/git.js';
17
+ import { runFullAnalysis } from '../core/run-analyze.js';
20
18
  import fs from 'fs/promises';
21
19
  const HEAP_MB = 8192;
22
20
  const HEAP_FLAG = `--max-old-space-size=${HEAP_MB}`;
@@ -39,23 +37,6 @@ function ensureHeap() {
39
37
  }
40
38
  return true;
41
39
  }
42
- /** Threshold: auto-skip embeddings for repos with more nodes than this */
43
- const EMBEDDING_NODE_LIMIT = 50_000;
44
- const PHASE_LABELS = {
45
- extracting: 'Scanning files',
46
- structure: 'Building structure',
47
- parsing: 'Parsing code',
48
- imports: 'Resolving imports',
49
- calls: 'Tracing calls',
50
- heritage: 'Extracting inheritance',
51
- communities: 'Detecting communities',
52
- processes: 'Detecting processes',
53
- complete: 'Pipeline complete',
54
- lbug: 'Loading into LadybugDB',
55
- fts: 'Creating search indexes',
56
- embeddings: 'Generating embeddings',
57
- done: 'Done',
58
- };
59
40
  export const analyzeCommand = async (inputPath, options) => {
60
41
  if (ensureHeap())
61
42
  return;
@@ -91,26 +72,12 @@ export const analyzeCommand = async (inputPath, options) => {
91
72
  if (!repoHasGit) {
92
73
  console.log(' Warning: no .git directory found \u2014 commit-tracking and incremental updates disabled.\n');
93
74
  }
94
- const { storagePath, lbugPath } = getStoragePaths(repoPath);
95
- // Clean up stale KuzuDB files from before the LadybugDB migration.
96
- // If kuzu existed but lbug doesn't, we're doing a migration re-index — say so.
97
- const kuzuResult = await cleanupOldKuzuFiles(storagePath);
98
- if (kuzuResult.found && kuzuResult.needsReindex) {
99
- console.log(' Migrating from KuzuDB to LadybugDB — rebuilding index...\n');
100
- }
101
- const currentCommit = repoHasGit ? getCurrentCommit(repoPath) : '';
102
- const existingMeta = await loadMeta(storagePath);
103
- if (existingMeta && !options?.force && !options?.skills && existingMeta.lastCommit === currentCommit) {
104
- // Non-git folders have currentCommit = '' — always rebuild since we can't detect changes
105
- if (currentCommit !== '') {
106
- console.log(' Already up to date\n');
107
- return;
108
- }
109
- }
75
+ // KuzuDB migration cleanup is handled by runFullAnalysis internally.
76
+ // Note: --skills is handled after runFullAnalysis using the returned pipelineResult.
110
77
  if (process.env.GITNEXUS_NO_GITIGNORE) {
111
78
  console.log(' GITNEXUS_NO_GITIGNORE is set — skipping .gitignore (still reading .gitnexusignore)\n');
112
79
  }
113
- // Single progress bar for entire pipeline
80
+ // ── CLI progress bar setup ─────────────────────────────────────────
114
81
  const bar = new cliProgress.SingleBar({
115
82
  format: ' {bar} {percentage}% | {phase}',
116
83
  barCompleteChar: '\u2588',
@@ -122,35 +89,33 @@ export const analyzeCommand = async (inputPath, options) => {
122
89
  stopOnComplete: false,
123
90
  }, cliProgress.Presets.shades_grey);
124
91
  bar.start(100, 0, { phase: 'Initializing...' });
125
- // Graceful SIGINT handling — clean up resources and exit
92
+ // Graceful SIGINT handling
126
93
  let aborted = false;
127
94
  const sigintHandler = () => {
128
95
  if (aborted)
129
- process.exit(1); // Second Ctrl-C: force exit
96
+ process.exit(1);
130
97
  aborted = true;
131
98
  bar.stop();
132
99
  console.log('\n Interrupted — cleaning up...');
133
- closeLbug().catch(() => { }).finally(() => process.exit(130));
100
+ closeLbug()
101
+ .catch(() => { })
102
+ .finally(() => process.exit(130));
134
103
  };
135
104
  process.on('SIGINT', sigintHandler);
136
- // Route all console output through bar.log() so the bar doesn't stamp itself
137
- // multiple times when other code writes to stdout/stderr mid-render.
105
+ // Route console output through bar.log() to prevent progress bar corruption
138
106
  const origLog = console.log.bind(console);
139
107
  const origWarn = console.warn.bind(console);
140
108
  const origError = console.error.bind(console);
141
109
  const barLog = (...args) => {
142
- // Clear the bar line, print the message, then let the next bar.update redraw
143
110
  process.stdout.write('\x1b[2K\r');
144
- origLog(args.map(a => (typeof a === 'string' ? a : String(a))).join(' '));
111
+ origLog(args.map((a) => (typeof a === 'string' ? a : String(a))).join(' '));
145
112
  };
146
113
  console.log = barLog;
147
114
  console.warn = barLog;
148
115
  console.error = barLog;
149
- // Track elapsed time per phase — both updateBar and the interval use the
150
- // same format so they don't flicker against each other.
116
+ // Track elapsed time per phase
151
117
  let lastPhaseLabel = 'Initializing...';
152
118
  let phaseStart = Date.now();
153
- /** Update bar with phase label + elapsed seconds (shown after 3s). */
154
119
  const updateBar = (value, phaseLabel) => {
155
120
  if (phaseLabel !== lastPhaseLabel) {
156
121
  lastPhaseLabel = phaseLabel;
@@ -160,217 +125,106 @@ export const analyzeCommand = async (inputPath, options) => {
160
125
  const display = elapsed >= 3 ? `${phaseLabel} (${elapsed}s)` : phaseLabel;
161
126
  bar.update(value, { phase: display });
162
127
  };
163
- // Tick elapsed seconds for phases with infrequent progress callbacks
164
- // (e.g. CSV streaming, FTS indexing). Uses the same display format as
165
- // updateBar so there's no flickering.
166
128
  const elapsedTimer = setInterval(() => {
167
129
  const elapsed = Math.round((Date.now() - phaseStart) / 1000);
168
130
  if (elapsed >= 3) {
169
131
  bar.update({ phase: `${lastPhaseLabel} (${elapsed}s)` });
170
132
  }
171
133
  }, 1000);
172
- const t0Global = Date.now();
173
- // ── Cache embeddings from existing index before rebuild ────────────
174
- let cachedEmbeddingNodeIds = new Set();
175
- let cachedEmbeddings = [];
176
- if (options?.embeddings && existingMeta && !options?.force) {
177
- try {
178
- updateBar(0, 'Caching embeddings...');
179
- await initLbug(lbugPath);
180
- const cached = await loadCachedEmbeddings();
181
- cachedEmbeddingNodeIds = cached.embeddingNodeIds;
182
- cachedEmbeddings = cached.embeddings;
183
- await closeLbug();
184
- }
185
- catch {
186
- try {
187
- await closeLbug();
188
- }
189
- catch { }
190
- }
191
- }
192
- // ── Phase 1: Full Pipeline (0–60%) ─────────────────────────────────
193
- const pipelineResult = await runPipelineFromRepo(repoPath, (progress) => {
194
- const phaseLabel = PHASE_LABELS[progress.phase] || progress.phase;
195
- const scaled = Math.round(progress.percent * 0.6);
196
- updateBar(scaled, phaseLabel);
197
- });
198
- // ── Phase 2: LadybugDB (60–85%) ──────────────────────────────────────
199
- updateBar(60, 'Loading into LadybugDB...');
200
- await closeLbug();
201
- const lbugFiles = [lbugPath, `${lbugPath}.wal`, `${lbugPath}.lock`];
202
- for (const f of lbugFiles) {
203
- try {
204
- await fs.rm(f, { recursive: true, force: true });
205
- }
206
- catch { }
207
- }
208
- const t0Lbug = Date.now();
209
- await initLbug(lbugPath);
210
- let lbugMsgCount = 0;
211
- const lbugResult = await loadGraphToLbug(pipelineResult.graph, pipelineResult.repoPath, storagePath, (msg) => {
212
- lbugMsgCount++;
213
- const progress = Math.min(84, 60 + Math.round((lbugMsgCount / (lbugMsgCount + 10)) * 24));
214
- updateBar(progress, msg);
215
- });
216
- const lbugTime = ((Date.now() - t0Lbug) / 1000).toFixed(1);
217
- const lbugWarnings = lbugResult.warnings;
218
- // ── Phase 3: FTS (85–90%) ─────────────────────────────────────────
219
- updateBar(85, 'Creating search indexes...');
220
- const t0Fts = Date.now();
134
+ const t0 = Date.now();
135
+ // ── Run shared analysis orchestrator ───────────────────────────────
221
136
  try {
222
- await createFTSIndex('File', 'file_fts', ['name', 'content']);
223
- await createFTSIndex('Function', 'function_fts', ['name', 'content']);
224
- await createFTSIndex('Class', 'class_fts', ['name', 'content']);
225
- await createFTSIndex('Method', 'method_fts', ['name', 'content']);
226
- await createFTSIndex('Interface', 'interface_fts', ['name', 'content']);
227
- }
228
- catch (e) {
229
- // Non-fatal — FTS is best-effort
230
- }
231
- const ftsTime = ((Date.now() - t0Fts) / 1000).toFixed(1);
232
- // ── Phase 3.5: Re-insert cached embeddings ────────────────────────
233
- if (cachedEmbeddings.length > 0) {
234
- // Check if cached embedding dimensions match current schema
235
- const cachedDims = cachedEmbeddings[0].embedding.length;
236
- const { EMBEDDING_DIMS } = await import('../core/lbug/schema.js');
237
- if (cachedDims !== EMBEDDING_DIMS) {
238
- // Dimensions changed (e.g. switched embedding model) — discard cache and re-embed all
239
- console.error(`⚠️ Embedding dimensions changed (${cachedDims}d → ${EMBEDDING_DIMS}d), discarding cache`);
240
- cachedEmbeddings = [];
241
- cachedEmbeddingNodeIds = new Set();
137
+ const result = await runFullAnalysis(repoPath, {
138
+ force: options?.force || options?.skills,
139
+ embeddings: options?.embeddings,
140
+ skipGit: options?.skipGit,
141
+ skipAgentsMd: options?.skipAgentsMd,
142
+ }, {
143
+ onProgress: (_phase, percent, message) => {
144
+ updateBar(percent, message);
145
+ },
146
+ onLog: barLog,
147
+ });
148
+ if (result.alreadyUpToDate) {
149
+ clearInterval(elapsedTimer);
150
+ process.removeListener('SIGINT', sigintHandler);
151
+ console.log = origLog;
152
+ console.warn = origWarn;
153
+ console.error = origError;
154
+ bar.stop();
155
+ console.log(' Already up to date\n');
156
+ // Safe to return without process.exit(0) — the early-return path in
157
+ // runFullAnalysis never opens LadybugDB, so no native handles prevent exit.
158
+ return;
242
159
  }
243
- else {
244
- updateBar(88, `Restoring ${cachedEmbeddings.length} cached embeddings...`);
245
- const EMBED_BATCH = 200;
246
- for (let i = 0; i < cachedEmbeddings.length; i += EMBED_BATCH) {
247
- const batch = cachedEmbeddings.slice(i, i + EMBED_BATCH);
248
- const paramsList = batch.map(e => ({ nodeId: e.nodeId, embedding: e.embedding }));
249
- try {
250
- await executeWithReusedStatement(`CREATE (e:CodeEmbedding {nodeId: $nodeId, embedding: $embedding})`, paramsList);
160
+ // Skill generation (CLI-only, uses pipeline result from analysis)
161
+ if (options?.skills && result.pipelineResult) {
162
+ updateBar(99, 'Generating skill files...');
163
+ try {
164
+ const { generateSkillFiles } = await import('./skill-gen.js');
165
+ const { generateAIContextFiles } = await import('./ai-context.js');
166
+ const skillResult = await generateSkillFiles(repoPath, result.repoName, result.pipelineResult);
167
+ if (skillResult.skills.length > 0) {
168
+ barLog(` Generated ${skillResult.skills.length} skill files`);
169
+ // Re-generate AI context files now that we have skill info
170
+ const s = result.stats;
171
+ const communityResult = result.pipelineResult?.communityResult;
172
+ let aggregatedClusterCount = 0;
173
+ if (communityResult?.communities) {
174
+ const groups = new Map();
175
+ for (const c of communityResult.communities) {
176
+ const label = c.heuristicLabel || c.label || 'Unknown';
177
+ groups.set(label, (groups.get(label) || 0) + c.symbolCount);
178
+ }
179
+ aggregatedClusterCount = Array.from(groups.values()).filter((count) => count >= 5).length;
180
+ }
181
+ const { storagePath: sp } = getStoragePaths(repoPath);
182
+ await generateAIContextFiles(repoPath, sp, result.repoName, {
183
+ files: s.files ?? 0,
184
+ nodes: s.nodes ?? 0,
185
+ edges: s.edges ?? 0,
186
+ communities: s.communities,
187
+ clusters: aggregatedClusterCount,
188
+ processes: s.processes,
189
+ }, skillResult.skills, { skipAgentsMd: options?.skipAgentsMd });
251
190
  }
252
- catch { /* some may fail if node was removed, that's fine */ }
191
+ }
192
+ catch {
193
+ /* best-effort */
253
194
  }
254
195
  }
255
- }
256
- // ── Phase 4: Embeddings (90–98%) ──────────────────────────────────
257
- const stats = await getLbugStats();
258
- let embeddingTime = '0.0';
259
- let embeddingSkipped = true;
260
- let embeddingSkipReason = 'off (use --embeddings to enable)';
261
- if (options?.embeddings) {
262
- if (stats.nodes > EMBEDDING_NODE_LIMIT) {
263
- embeddingSkipReason = `skipped (${stats.nodes.toLocaleString()} nodes > ${EMBEDDING_NODE_LIMIT.toLocaleString()} limit)`;
264
- }
265
- else {
266
- embeddingSkipped = false;
196
+ const totalTime = ((Date.now() - t0) / 1000).toFixed(1);
197
+ clearInterval(elapsedTimer);
198
+ process.removeListener('SIGINT', sigintHandler);
199
+ console.log = origLog;
200
+ console.warn = origWarn;
201
+ console.error = origError;
202
+ bar.update(100, { phase: 'Done' });
203
+ bar.stop();
204
+ // ── Summary ────────────────────────────────────────────────────
205
+ const s = result.stats;
206
+ console.log(`\n Repository indexed successfully (${totalTime}s)\n`);
207
+ console.log(` ${(s.nodes ?? 0).toLocaleString()} nodes | ${(s.edges ?? 0).toLocaleString()} edges | ${s.communities ?? 0} clusters | ${s.processes ?? 0} flows`);
208
+ console.log(` ${repoPath}`);
209
+ try {
210
+ await fs.access(getGlobalRegistryPath());
267
211
  }
268
- }
269
- if (!embeddingSkipped) {
270
- const { isHttpMode } = await import('../core/embeddings/http-client.js');
271
- const httpMode = isHttpMode();
272
- updateBar(90, httpMode ? 'Connecting to embedding endpoint...' : 'Loading embedding model...');
273
- const t0Emb = Date.now();
274
- const { runEmbeddingPipeline } = await import('../core/embeddings/embedding-pipeline.js');
275
- await runEmbeddingPipeline(executeQuery, executeWithReusedStatement, (progress) => {
276
- const scaled = 90 + Math.round((progress.percent / 100) * 8);
277
- const label = progress.phase === 'loading-model'
278
- ? (httpMode ? 'Connecting to embedding endpoint...' : 'Loading embedding model...')
279
- : `Embedding ${progress.nodesProcessed || 0}/${progress.totalNodes || '?'}`;
280
- updateBar(scaled, label);
281
- }, {}, cachedEmbeddingNodeIds.size > 0 ? cachedEmbeddingNodeIds : undefined);
282
- embeddingTime = ((Date.now() - t0Emb) / 1000).toFixed(1);
283
- }
284
- // ── Phase 5: Finalize (98–100%) ───────────────────────────────────
285
- updateBar(98, 'Saving metadata...');
286
- // Count embeddings in the index (cached + newly generated)
287
- let embeddingCount = 0;
288
- try {
289
- const embResult = await executeQuery(`MATCH (e:CodeEmbedding) RETURN count(e) AS cnt`);
290
- embeddingCount = embResult?.[0]?.cnt ?? 0;
291
- }
292
- catch { /* table may not exist if embeddings never ran */ }
293
- const meta = {
294
- repoPath,
295
- lastCommit: currentCommit,
296
- indexedAt: new Date().toISOString(),
297
- stats: {
298
- files: pipelineResult.totalFileCount,
299
- nodes: stats.nodes,
300
- edges: stats.edges,
301
- communities: pipelineResult.communityResult?.stats.totalCommunities,
302
- processes: pipelineResult.processResult?.stats.totalProcesses,
303
- embeddings: embeddingCount,
304
- },
305
- };
306
- await saveMeta(storagePath, meta);
307
- await registerRepo(repoPath, meta);
308
- // Only attempt to update .gitignore when a .git directory is present.
309
- // Use hasGitDir (filesystem check) rather than git CLI subprocess
310
- // so we skip correctly for --skip-git folders even if git CLI is available.
311
- if (hasGitDir(repoPath)) {
312
- await addToGitignore(repoPath);
313
- }
314
- const projectName = path.basename(repoPath);
315
- let aggregatedClusterCount = 0;
316
- if (pipelineResult.communityResult?.communities) {
317
- const groups = new Map();
318
- for (const c of pipelineResult.communityResult.communities) {
319
- const label = c.heuristicLabel || c.label || 'Unknown';
320
- groups.set(label, (groups.get(label) || 0) + c.symbolCount);
212
+ catch {
213
+ console.log('\n Tip: Run `gitnexus setup` to configure MCP for your editor.');
321
214
  }
322
- aggregatedClusterCount = Array.from(groups.values()).filter(count => count >= 5).length;
323
- }
324
- let generatedSkills = [];
325
- if (options?.skills && pipelineResult.communityResult) {
326
- updateBar(99, 'Generating skill files...');
327
- const skillResult = await generateSkillFiles(repoPath, projectName, pipelineResult);
328
- generatedSkills = skillResult.skills;
329
- }
330
- const aiContext = await generateAIContextFiles(repoPath, storagePath, projectName, {
331
- files: pipelineResult.totalFileCount,
332
- nodes: stats.nodes,
333
- edges: stats.edges,
334
- communities: pipelineResult.communityResult?.stats.totalCommunities,
335
- clusters: aggregatedClusterCount,
336
- processes: pipelineResult.processResult?.stats.totalProcesses,
337
- }, generatedSkills);
338
- await closeLbug();
339
- // Note: we intentionally do NOT call disposeEmbedder() here.
340
- // ONNX Runtime's native cleanup segfaults on macOS and some Linux configs.
341
- // Since the process exits immediately after, Node.js reclaims everything.
342
- const totalTime = ((Date.now() - t0Global) / 1000).toFixed(1);
343
- clearInterval(elapsedTimer);
344
- process.removeListener('SIGINT', sigintHandler);
345
- console.log = origLog;
346
- console.warn = origWarn;
347
- console.error = origError;
348
- bar.update(100, { phase: 'Done' });
349
- bar.stop();
350
- // ── Summary ───────────────────────────────────────────────────────
351
- const embeddingsCached = cachedEmbeddings.length > 0;
352
- console.log(`\n Repository indexed successfully (${totalTime}s)${embeddingsCached ? ` [${cachedEmbeddings.length} embeddings cached]` : ''}\n`);
353
- console.log(` ${stats.nodes.toLocaleString()} nodes | ${stats.edges.toLocaleString()} edges | ${pipelineResult.communityResult?.stats.totalCommunities || 0} clusters | ${pipelineResult.processResult?.stats.totalProcesses || 0} flows`);
354
- console.log(` LadybugDB ${lbugTime}s | FTS ${ftsTime}s | Embeddings ${embeddingSkipped ? embeddingSkipReason : embeddingTime + 's'}`);
355
- console.log(` ${repoPath}`);
356
- if (aiContext.files.length > 0) {
357
- console.log(` Context: ${aiContext.files.join(', ')}`);
358
- }
359
- // Show a quiet summary if some edge types needed fallback insertion
360
- if (lbugWarnings.length > 0) {
361
- const totalFallback = lbugWarnings.reduce((sum, w) => {
362
- const m = w.match(/\((\d+) edges\)/);
363
- return sum + (m ? parseInt(m[1]) : 0);
364
- }, 0);
365
- console.log(` Note: ${totalFallback} edges across ${lbugWarnings.length} types inserted via fallback (schema will be updated in next release)`);
366
- }
367
- try {
368
- await fs.access(getGlobalRegistryPath());
369
- }
370
- catch {
371
- console.log('\n Tip: Run `gitnexus setup` to configure MCP for your editor.');
215
+ console.log('');
216
+ }
217
+ catch (err) {
218
+ clearInterval(elapsedTimer);
219
+ process.removeListener('SIGINT', sigintHandler);
220
+ console.log = origLog;
221
+ console.warn = origWarn;
222
+ console.error = origError;
223
+ bar.stop();
224
+ console.error(`\n Analysis failed: ${err.message}\n`);
225
+ process.exitCode = 1;
226
+ return;
372
227
  }
373
- console.log('');
374
228
  // LadybugDB's native module holds open handles that prevent Node from exiting.
375
229
  // ONNX Runtime also registers native atexit hooks that segfault on some
376
230
  // platforms (#38, #40). Force-exit to ensure clean termination.
@@ -68,7 +68,9 @@ export function formatContextResult(result) {
68
68
  if (result.error)
69
69
  return `Error: ${result.error}`;
70
70
  if (result.status === 'ambiguous') {
71
- const lines = [`Multiple symbols named '${result.candidates?.[0]?.name || '?'}'. Disambiguate with file path:\n`];
71
+ const lines = [
72
+ `Multiple symbols named '${result.candidates?.[0]?.name || '?'}'. Disambiguate with file path:\n`,
73
+ ];
72
74
  for (const c of result.candidates || []) {
73
75
  lines.push(` ${c.kind} ${c.name} → ${c.filePath}:${c.line || '?'} (uid: ${c.uid})`);
74
76
  }
@@ -171,7 +173,7 @@ export function formatCypherResult(result) {
171
173
  const keys = Object.keys(result[0]);
172
174
  const lines = [`${result.length} row(s):\n`];
173
175
  for (const row of result.slice(0, 30)) {
174
- const parts = keys.map(k => `${k}: ${row[k]}`);
176
+ const parts = keys.map((k) => `${k}: ${row[k]}`);
175
177
  lines.push(` ${parts.join(' | ')}`);
176
178
  }
177
179
  if (result.length > 30) {
@@ -230,13 +232,20 @@ export function formatListReposResult(result) {
230
232
  */
231
233
  function formatToolResult(toolName, result) {
232
234
  switch (toolName) {
233
- case 'query': return formatQueryResult(result);
234
- case 'context': return formatContextResult(result);
235
- case 'impact': return formatImpactResult(result);
236
- case 'cypher': return formatCypherResult(result);
237
- case 'detect_changes': return formatDetectChangesResult(result);
238
- case 'list_repos': return formatListReposResult(result);
239
- default: return typeof result === 'string' ? result : JSON.stringify(result, null, 2);
235
+ case 'query':
236
+ return formatQueryResult(result);
237
+ case 'context':
238
+ return formatContextResult(result);
239
+ case 'impact':
240
+ return formatImpactResult(result);
241
+ case 'cypher':
242
+ return formatCypherResult(result);
243
+ case 'detect_changes':
244
+ return formatDetectChangesResult(result);
245
+ case 'list_repos':
246
+ return formatListReposResult(result);
247
+ default:
248
+ return typeof result === 'string' ? result : JSON.stringify(result, null, 2);
240
249
  }
241
250
  }
242
251
  // ─── Next-Step Hints ──────────────────────────────────────────────────
@@ -269,7 +278,7 @@ export async function evalServerCommand(options) {
269
278
  process.exit(1);
270
279
  }
271
280
  const repos = await backend.listRepos();
272
- console.error(`GitNexus eval-server: ${repos.length} repo(s) loaded: ${repos.map(r => r.name).join(', ')}`);
281
+ console.error(`GitNexus eval-server: ${repos.length} repo(s) loaded: ${repos.map((r) => r.name).join(', ')}`);
273
282
  let idleTimer = null;
274
283
  function resetIdleTimer() {
275
284
  if (idleTimeoutSec <= 0)
@@ -289,7 +298,7 @@ export async function evalServerCommand(options) {
289
298
  if (req.method === 'GET' && req.url === '/health') {
290
299
  res.setHeader('Content-Type', 'application/json');
291
300
  res.writeHead(200);
292
- res.end(JSON.stringify({ status: 'ok', repos: repos.map(r => r.name) }));
301
+ res.end(JSON.stringify({ status: 'ok', repos: repos.map((r) => r.name) }));
293
302
  return;
294
303
  }
295
304
  // Shutdown