gitnexus 1.4.8 → 1.4.9

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 (211) hide show
  1. package/README.md +7 -0
  2. package/dist/cli/index-repo.d.ts +15 -0
  3. package/dist/cli/index-repo.js +115 -0
  4. package/dist/cli/index.js +11 -2
  5. package/dist/cli/setup.js +12 -9
  6. package/dist/cli/wiki.d.ts +4 -0
  7. package/dist/cli/wiki.js +174 -53
  8. package/dist/config/supported-languages.d.ts +7 -5
  9. package/dist/config/supported-languages.js +6 -4
  10. package/dist/core/graph/graph.js +9 -1
  11. package/dist/core/graph/types.d.ts +10 -2
  12. package/dist/core/ingestion/call-processor.d.ts +18 -1
  13. package/dist/core/ingestion/call-processor.js +297 -38
  14. package/dist/core/ingestion/call-routing.d.ts +3 -18
  15. package/dist/core/ingestion/call-routing.js +0 -19
  16. package/dist/core/ingestion/cobol/cobol-copy-expander.d.ts +57 -0
  17. package/dist/core/ingestion/cobol/cobol-copy-expander.js +385 -0
  18. package/dist/core/ingestion/cobol/cobol-preprocessor.d.ts +210 -0
  19. package/dist/core/ingestion/cobol/cobol-preprocessor.js +1509 -0
  20. package/dist/core/ingestion/cobol/jcl-parser.d.ts +68 -0
  21. package/dist/core/ingestion/cobol/jcl-parser.js +217 -0
  22. package/dist/core/ingestion/cobol/jcl-processor.d.ts +33 -0
  23. package/dist/core/ingestion/cobol/jcl-processor.js +229 -0
  24. package/dist/core/ingestion/cobol-processor.d.ts +54 -0
  25. package/dist/core/ingestion/cobol-processor.js +1186 -0
  26. package/dist/core/ingestion/entry-point-scoring.d.ts +17 -0
  27. package/dist/core/ingestion/entry-point-scoring.js +18 -4
  28. package/dist/core/ingestion/export-detection.d.ts +47 -8
  29. package/dist/core/ingestion/export-detection.js +29 -50
  30. package/dist/core/ingestion/field-extractor.d.ts +29 -0
  31. package/dist/core/ingestion/field-extractor.js +25 -0
  32. package/dist/core/ingestion/field-extractors/configs/c-cpp.d.ts +3 -0
  33. package/dist/core/ingestion/field-extractors/configs/c-cpp.js +108 -0
  34. package/dist/core/ingestion/field-extractors/configs/csharp.d.ts +8 -0
  35. package/dist/core/ingestion/field-extractors/configs/csharp.js +73 -0
  36. package/dist/core/ingestion/field-extractors/configs/dart.d.ts +8 -0
  37. package/dist/core/ingestion/field-extractors/configs/dart.js +76 -0
  38. package/dist/core/ingestion/field-extractors/configs/go.d.ts +11 -0
  39. package/dist/core/ingestion/field-extractors/configs/go.js +64 -0
  40. package/dist/core/ingestion/field-extractors/configs/helpers.d.ts +44 -0
  41. package/dist/core/ingestion/field-extractors/configs/helpers.js +134 -0
  42. package/dist/core/ingestion/field-extractors/configs/jvm.d.ts +3 -0
  43. package/dist/core/ingestion/field-extractors/configs/jvm.js +118 -0
  44. package/dist/core/ingestion/field-extractors/configs/php.d.ts +8 -0
  45. package/dist/core/ingestion/field-extractors/configs/php.js +67 -0
  46. package/dist/core/ingestion/field-extractors/configs/python.d.ts +12 -0
  47. package/dist/core/ingestion/field-extractors/configs/python.js +91 -0
  48. package/dist/core/ingestion/field-extractors/configs/ruby.d.ts +16 -0
  49. package/dist/core/ingestion/field-extractors/configs/ruby.js +75 -0
  50. package/dist/core/ingestion/field-extractors/configs/rust.d.ts +9 -0
  51. package/dist/core/ingestion/field-extractors/configs/rust.js +55 -0
  52. package/dist/core/ingestion/field-extractors/configs/swift.d.ts +8 -0
  53. package/dist/core/ingestion/field-extractors/configs/swift.js +63 -0
  54. package/dist/core/ingestion/field-extractors/configs/typescript-javascript.d.ts +3 -0
  55. package/dist/core/ingestion/field-extractors/configs/typescript-javascript.js +60 -0
  56. package/dist/core/ingestion/field-extractors/generic.d.ts +46 -0
  57. package/dist/core/ingestion/field-extractors/generic.js +111 -0
  58. package/dist/core/ingestion/field-extractors/typescript.d.ts +77 -0
  59. package/dist/core/ingestion/field-extractors/typescript.js +291 -0
  60. package/dist/core/ingestion/field-types.d.ts +59 -0
  61. package/dist/core/ingestion/field-types.js +2 -0
  62. package/dist/core/ingestion/framework-detection.d.ts +87 -0
  63. package/dist/core/ingestion/framework-detection.js +65 -2
  64. package/dist/core/ingestion/heritage-processor.js +15 -17
  65. package/dist/core/ingestion/import-processor.d.ts +9 -10
  66. package/dist/core/ingestion/import-processor.js +59 -14
  67. package/dist/core/ingestion/{resolvers → import-resolvers}/csharp.d.ts +6 -9
  68. package/dist/core/ingestion/{resolvers → import-resolvers}/csharp.js +20 -2
  69. package/dist/core/ingestion/import-resolvers/dart.d.ts +7 -0
  70. package/dist/core/ingestion/import-resolvers/dart.js +44 -0
  71. package/dist/core/ingestion/{resolvers → import-resolvers}/go.d.ts +4 -5
  72. package/dist/core/ingestion/{resolvers → import-resolvers}/go.js +17 -0
  73. package/dist/core/ingestion/{resolvers → import-resolvers}/jvm.d.ts +9 -1
  74. package/dist/core/ingestion/{resolvers → import-resolvers}/jvm.js +56 -0
  75. package/dist/core/ingestion/{resolvers → import-resolvers}/php.d.ts +6 -10
  76. package/dist/core/ingestion/{resolvers → import-resolvers}/php.js +7 -2
  77. package/dist/core/ingestion/{resolvers → import-resolvers}/python.d.ts +9 -3
  78. package/dist/core/ingestion/{resolvers → import-resolvers}/python.js +35 -3
  79. package/dist/core/ingestion/{resolvers → import-resolvers}/ruby.d.ts +5 -2
  80. package/dist/core/ingestion/{resolvers → import-resolvers}/ruby.js +7 -2
  81. package/dist/core/ingestion/{resolvers → import-resolvers}/rust.d.ts +5 -2
  82. package/dist/core/ingestion/{resolvers → import-resolvers}/rust.js +41 -2
  83. package/dist/core/ingestion/{resolvers → import-resolvers}/standard.d.ts +15 -7
  84. package/dist/core/ingestion/{resolvers → import-resolvers}/standard.js +22 -3
  85. package/dist/core/ingestion/import-resolvers/swift.d.ts +7 -0
  86. package/dist/core/ingestion/import-resolvers/swift.js +23 -0
  87. package/dist/core/ingestion/import-resolvers/types.d.ts +44 -0
  88. package/dist/core/ingestion/import-resolvers/types.js +6 -0
  89. package/dist/core/ingestion/{resolvers → import-resolvers}/utils.d.ts +0 -3
  90. package/dist/core/ingestion/{resolvers → import-resolvers}/utils.js +0 -9
  91. package/dist/core/ingestion/language-config.d.ts +4 -1
  92. package/dist/core/ingestion/language-provider.d.ts +121 -0
  93. package/dist/core/ingestion/language-provider.js +24 -0
  94. package/dist/core/ingestion/languages/c-cpp.d.ts +12 -0
  95. package/dist/core/ingestion/languages/c-cpp.js +71 -0
  96. package/dist/core/ingestion/languages/cobol.d.ts +1 -0
  97. package/dist/core/ingestion/languages/cobol.js +26 -0
  98. package/dist/core/ingestion/languages/csharp.d.ts +8 -0
  99. package/dist/core/ingestion/languages/csharp.js +49 -0
  100. package/dist/core/ingestion/languages/dart.d.ts +12 -0
  101. package/dist/core/ingestion/languages/dart.js +58 -0
  102. package/dist/core/ingestion/languages/go.d.ts +11 -0
  103. package/dist/core/ingestion/languages/go.js +28 -0
  104. package/dist/core/ingestion/languages/index.d.ts +38 -0
  105. package/dist/core/ingestion/languages/index.js +63 -0
  106. package/dist/core/ingestion/languages/java.d.ts +9 -0
  107. package/dist/core/ingestion/languages/java.js +29 -0
  108. package/dist/core/ingestion/languages/kotlin.d.ts +9 -0
  109. package/dist/core/ingestion/languages/kotlin.js +53 -0
  110. package/dist/core/ingestion/languages/php.d.ts +8 -0
  111. package/dist/core/ingestion/languages/php.js +145 -0
  112. package/dist/core/ingestion/languages/python.d.ts +12 -0
  113. package/dist/core/ingestion/languages/python.js +39 -0
  114. package/dist/core/ingestion/languages/ruby.d.ts +9 -0
  115. package/dist/core/ingestion/languages/ruby.js +44 -0
  116. package/dist/core/ingestion/languages/rust.d.ts +12 -0
  117. package/dist/core/ingestion/languages/rust.js +44 -0
  118. package/dist/core/ingestion/languages/swift.d.ts +12 -0
  119. package/dist/core/ingestion/languages/swift.js +133 -0
  120. package/dist/core/ingestion/languages/typescript.d.ts +10 -0
  121. package/dist/core/ingestion/languages/typescript.js +60 -0
  122. package/dist/core/ingestion/mro-processor.js +14 -15
  123. package/dist/core/ingestion/{named-binding-extraction.d.ts → named-binding-processor.d.ts} +0 -9
  124. package/dist/core/ingestion/named-binding-processor.js +42 -0
  125. package/dist/core/ingestion/named-bindings/csharp.d.ts +3 -0
  126. package/dist/core/ingestion/named-bindings/csharp.js +37 -0
  127. package/dist/core/ingestion/named-bindings/java.d.ts +3 -0
  128. package/dist/core/ingestion/named-bindings/java.js +29 -0
  129. package/dist/core/ingestion/named-bindings/kotlin.d.ts +3 -0
  130. package/dist/core/ingestion/named-bindings/kotlin.js +36 -0
  131. package/dist/core/ingestion/named-bindings/php.d.ts +3 -0
  132. package/dist/core/ingestion/named-bindings/php.js +61 -0
  133. package/dist/core/ingestion/named-bindings/python.d.ts +3 -0
  134. package/dist/core/ingestion/named-bindings/python.js +49 -0
  135. package/dist/core/ingestion/named-bindings/rust.d.ts +3 -0
  136. package/dist/core/ingestion/named-bindings/rust.js +64 -0
  137. package/dist/core/ingestion/named-bindings/types.d.ts +16 -0
  138. package/dist/core/ingestion/named-bindings/types.js +6 -0
  139. package/dist/core/ingestion/named-bindings/typescript.d.ts +3 -0
  140. package/dist/core/ingestion/named-bindings/typescript.js +58 -0
  141. package/dist/core/ingestion/parsing-processor.d.ts +5 -1
  142. package/dist/core/ingestion/parsing-processor.js +115 -16
  143. package/dist/core/ingestion/pipeline.js +925 -424
  144. package/dist/core/ingestion/resolution-context.js +1 -1
  145. package/dist/core/ingestion/route-extractors/expo.d.ts +1 -0
  146. package/dist/core/ingestion/route-extractors/expo.js +36 -0
  147. package/dist/core/ingestion/route-extractors/middleware.d.ts +47 -0
  148. package/dist/core/ingestion/route-extractors/middleware.js +143 -0
  149. package/dist/core/ingestion/route-extractors/nextjs.d.ts +3 -0
  150. package/dist/core/ingestion/route-extractors/nextjs.js +76 -0
  151. package/dist/core/ingestion/route-extractors/php.d.ts +7 -0
  152. package/dist/core/ingestion/route-extractors/php.js +21 -0
  153. package/dist/core/ingestion/route-extractors/response-shapes.d.ts +20 -0
  154. package/dist/core/ingestion/route-extractors/response-shapes.js +290 -0
  155. package/dist/core/ingestion/tree-sitter-queries.d.ts +8 -7
  156. package/dist/core/ingestion/tree-sitter-queries.js +231 -9
  157. package/dist/core/ingestion/type-env.d.ts +14 -17
  158. package/dist/core/ingestion/type-env.js +66 -14
  159. package/dist/core/ingestion/type-extractors/c-cpp.d.ts +1 -1
  160. package/dist/core/ingestion/type-extractors/csharp.js +1 -1
  161. package/dist/core/ingestion/type-extractors/dart.d.ts +15 -0
  162. package/dist/core/ingestion/type-extractors/dart.js +371 -0
  163. package/dist/core/ingestion/type-extractors/jvm.js +1 -1
  164. package/dist/core/ingestion/type-extractors/shared.d.ts +1 -13
  165. package/dist/core/ingestion/type-extractors/shared.js +9 -102
  166. package/dist/core/ingestion/type-extractors/swift.js +334 -4
  167. package/dist/core/ingestion/type-extractors/types.d.ts +3 -1
  168. package/dist/core/ingestion/{ast-helpers.d.ts → utils/ast-helpers.d.ts} +16 -13
  169. package/dist/core/ingestion/{ast-helpers.js → utils/ast-helpers.js} +111 -32
  170. package/dist/core/ingestion/{call-analysis.js → utils/call-analysis.js} +37 -0
  171. package/dist/core/ingestion/utils/event-loop.d.ts +5 -0
  172. package/dist/core/ingestion/utils/event-loop.js +5 -0
  173. package/dist/core/ingestion/utils/language-detection.d.ts +9 -0
  174. package/dist/core/ingestion/utils/language-detection.js +70 -0
  175. package/dist/core/ingestion/utils/verbose.d.ts +1 -0
  176. package/dist/core/ingestion/utils/verbose.js +7 -0
  177. package/dist/core/ingestion/workers/parse-worker.d.ts +43 -2
  178. package/dist/core/ingestion/workers/parse-worker.js +361 -150
  179. package/dist/core/lbug/csv-generator.js +34 -1
  180. package/dist/core/lbug/lbug-adapter.js +6 -0
  181. package/dist/core/lbug/schema.d.ts +5 -3
  182. package/dist/core/lbug/schema.js +39 -2
  183. package/dist/core/tree-sitter/parser-loader.js +7 -1
  184. package/dist/core/wiki/cursor-client.d.ts +31 -0
  185. package/dist/core/wiki/cursor-client.js +127 -0
  186. package/dist/core/wiki/generator.d.ts +28 -9
  187. package/dist/core/wiki/generator.js +115 -18
  188. package/dist/core/wiki/graph-queries.d.ts +4 -0
  189. package/dist/core/wiki/graph-queries.js +7 -1
  190. package/dist/core/wiki/llm-client.d.ts +2 -0
  191. package/dist/core/wiki/llm-client.js +8 -4
  192. package/dist/core/wiki/prompts.d.ts +3 -3
  193. package/dist/core/wiki/prompts.js +6 -0
  194. package/dist/mcp/core/lbug-adapter.d.ts +5 -0
  195. package/dist/mcp/core/lbug-adapter.js +11 -1
  196. package/dist/mcp/local/local-backend.d.ts +16 -5
  197. package/dist/mcp/local/local-backend.js +711 -74
  198. package/dist/mcp/tools.js +71 -2
  199. package/dist/storage/repo-manager.d.ts +3 -0
  200. package/package.json +17 -16
  201. package/dist/core/ingestion/import-resolution.d.ts +0 -101
  202. package/dist/core/ingestion/import-resolution.js +0 -251
  203. package/dist/core/ingestion/named-binding-extraction.js +0 -373
  204. package/dist/core/ingestion/resolvers/index.d.ts +0 -18
  205. package/dist/core/ingestion/resolvers/index.js +0 -13
  206. package/dist/core/ingestion/type-extractors/index.d.ts +0 -22
  207. package/dist/core/ingestion/type-extractors/index.js +0 -31
  208. package/dist/core/ingestion/utils.d.ts +0 -20
  209. package/dist/core/ingestion/utils.js +0 -242
  210. package/scripts/patch-tree-sitter-swift.cjs +0 -74
  211. /package/dist/core/ingestion/{call-analysis.d.ts → utils/call-analysis.d.ts} +0 -0
package/README.md CHANGED
@@ -53,7 +53,11 @@ If you prefer to configure manually instead of using `gitnexus setup`:
53
53
  ### Claude Code (full support — MCP + skills + hooks)
54
54
 
55
55
  ```bash
56
+ # macOS / Linux
56
57
  claude mcp add gitnexus -- npx -y gitnexus@latest mcp
58
+
59
+ # Windows
60
+ claude mcp add gitnexus -- cmd /c npx -y gitnexus@latest mcp
57
61
  ```
58
62
 
59
63
  ### Codex (full support — MCP + skills)
@@ -99,6 +103,8 @@ GitNexus builds a complete knowledge graph of your codebase through a multi-phas
99
103
  1. **Structure** — Walks the file tree and maps folder/file relationships
100
104
  2. **Parsing** — Extracts functions, classes, methods, and interfaces using Tree-sitter ASTs
101
105
  3. **Resolution** — Resolves imports and function calls across files with language-aware logic
106
+ - **Field & Property Type Resolution** — Tracks field types across classes and interfaces for deep chain resolution (e.g., `user.address.city.getName()`)
107
+ - **Return-Type-Aware Variable Binding** — Infers variable types from function return types, enabling accurate call-result binding
102
108
  4. **Clustering** — Groups related symbols into functional communities
103
109
  5. **Processes** — Traces execution flows from entry points through call chains
104
110
  6. **Search** — Builds hybrid search indexes for fast retrieval
@@ -150,6 +156,7 @@ gitnexus analyze --embeddings # Enable embedding generation (slower, better
150
156
  gitnexus analyze --verbose # Log skipped files when parsers are unavailable
151
157
  gitnexus mcp # Start MCP server (stdio) — serves all indexed repos
152
158
  gitnexus serve # Start local HTTP server (multi-repo) for web UI
159
+ gitnexus index # Register an existing .gitnexus/ folder into the global registry
153
160
  gitnexus list # List all indexed repositories
154
161
  gitnexus status # Show index status for current repo
155
162
  gitnexus clean # Delete index for current repo
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Index Command
3
+ *
4
+ * Registers an existing .gitnexus/ folder into the global registry so the
5
+ * MCP server can discover the repo without running a full `gitnexus analyze`.
6
+ *
7
+ * Useful when a pre-built .gitnexus/ directory is already present (e.g. after
8
+ * cloning a repo that ships its index, restoring from backup, or using a
9
+ * shared team index).
10
+ */
11
+ export interface IndexOptions {
12
+ force?: boolean;
13
+ allowNonGit?: boolean;
14
+ }
15
+ export declare const indexCommand: (inputPathParts?: string[], options?: IndexOptions) => Promise<void>;
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Index Command
3
+ *
4
+ * Registers an existing .gitnexus/ folder into the global registry so the
5
+ * MCP server can discover the repo without running a full `gitnexus analyze`.
6
+ *
7
+ * Useful when a pre-built .gitnexus/ directory is already present (e.g. after
8
+ * cloning a repo that ships its index, restoring from backup, or using a
9
+ * shared team index).
10
+ */
11
+ import path from "path";
12
+ import fs from "fs/promises";
13
+ import { getStoragePaths, loadMeta, addToGitignore, registerRepo, } from "../storage/repo-manager.js";
14
+ import { getGitRoot, isGitRepo } from "../storage/git.js";
15
+ export const indexCommand = async (inputPathParts, options) => {
16
+ console.log("\n GitNexus Index\n");
17
+ const inputPath = inputPathParts?.length
18
+ ? inputPathParts.join(" ")
19
+ : undefined;
20
+ if (inputPathParts && inputPathParts.length > 1) {
21
+ const resolvedCombinedPath = path.resolve(inputPath);
22
+ try {
23
+ await fs.access(resolvedCombinedPath);
24
+ }
25
+ catch {
26
+ console.log(" The `index` command accepts a single path only.");
27
+ console.log(" If your path contains spaces, wrap it in quotes.");
28
+ console.log(` Received multiple path parts: ${inputPathParts.join(", ")}`);
29
+ console.log("");
30
+ process.exitCode = 1;
31
+ return;
32
+ }
33
+ }
34
+ let repoPath;
35
+ if (inputPath) {
36
+ repoPath = path.resolve(inputPath);
37
+ }
38
+ else {
39
+ const gitRoot = getGitRoot(process.cwd());
40
+ if (!gitRoot) {
41
+ console.log(" Not inside a git repository, try to run git init\n");
42
+ process.exitCode = 1;
43
+ return;
44
+ }
45
+ repoPath = gitRoot;
46
+ }
47
+ if (!options?.allowNonGit && !isGitRepo(repoPath)) {
48
+ console.log(` Not a git repository: ${repoPath}`);
49
+ console.log(" Initialize one with `git init` or choose a valid repo path.\n");
50
+ console.log(" Or use --allow-non-git to register an existing .gitnexus index anyway.\n");
51
+ process.exitCode = 1;
52
+ return;
53
+ }
54
+ const { storagePath, lbugPath } = getStoragePaths(repoPath);
55
+ // ── Verify .gitnexus/ exists ──────────────────────────────────────
56
+ try {
57
+ await fs.access(storagePath);
58
+ }
59
+ catch {
60
+ console.log(` No .gitnexus/ folder found at: ${storagePath}`);
61
+ console.log(" Run `gitnexus analyze` to build the index first.\n");
62
+ process.exitCode = 1;
63
+ return;
64
+ }
65
+ // ── Verify lbug database exists ───────────────────────────────────
66
+ try {
67
+ await fs.access(lbugPath);
68
+ }
69
+ catch {
70
+ console.log(` .gitnexus/ folder exists but contains no LadybugDB index.`);
71
+ console.log(" Run `gitnexus analyze` to build the index.\n");
72
+ process.exitCode = 1;
73
+ return;
74
+ }
75
+ // ── Load or reconstruct meta ──────────────────────────────────────
76
+ let meta = await loadMeta(storagePath);
77
+ if (!meta) {
78
+ if (!options?.force) {
79
+ console.log(` .gitnexus/ exists but meta.json is missing.`);
80
+ console.log(" Use --force to register anyway (stats will be empty),");
81
+ console.log(" or run `gitnexus analyze` to rebuild properly.\n");
82
+ process.exitCode = 1;
83
+ return;
84
+ }
85
+ // --force: build a minimal meta so the repo can be registered
86
+ meta = {
87
+ repoPath,
88
+ lastCommit: "",
89
+ indexedAt: new Date().toISOString(),
90
+ };
91
+ }
92
+ // ── Register in global registry ───────────────────────────────────
93
+ await registerRepo(repoPath, meta);
94
+ await addToGitignore(repoPath);
95
+ const projectName = path.basename(repoPath);
96
+ const { stats } = meta;
97
+ console.log(` Repository registered: ${projectName}`);
98
+ if (stats) {
99
+ const parts = [];
100
+ if (stats.nodes != null) {
101
+ parts.push(`${stats.nodes.toLocaleString()} nodes`);
102
+ }
103
+ if (stats.edges != null) {
104
+ parts.push(`${stats.edges.toLocaleString()} edges`);
105
+ }
106
+ if (stats.communities != null)
107
+ parts.push(`${stats.communities} clusters`);
108
+ if (stats.processes != null)
109
+ parts.push(`${stats.processes} flows`);
110
+ if (parts.length)
111
+ console.log(` ${parts.join(" | ")}`);
112
+ }
113
+ console.log(` ${repoPath}`);
114
+ console.log("");
115
+ };
package/dist/cli/index.js CHANGED
@@ -25,6 +25,12 @@ program
25
25
  .option('-v, --verbose', 'Enable verbose ingestion warnings (default: false)')
26
26
  .addHelpText('after', '\nEnvironment variables:\n GITNEXUS_NO_GITIGNORE=1 Skip .gitignore parsing (still reads .gitnexusignore)')
27
27
  .action(createLazyAction(() => import('./analyze.js'), 'analyzeCommand'));
28
+ program
29
+ .command('index [path...]')
30
+ .description('Register an existing .gitnexus/ folder into the global registry (no re-analysis needed)')
31
+ .option('-f, --force', 'Register even if meta.json is missing (stats will be empty)')
32
+ .option('--allow-non-git', 'Allow registering folders that are not Git repositories')
33
+ .action(createLazyAction(() => import('./index-repo.js'), 'indexCommand'));
28
34
  program
29
35
  .command('serve')
30
36
  .description('Start local HTTP server for web UI connection')
@@ -53,11 +59,14 @@ program
53
59
  .command('wiki [path]')
54
60
  .description('Generate repository wiki from knowledge graph')
55
61
  .option('-f, --force', 'Force full regeneration even if up to date')
56
- .option('--model <model>', 'LLM model name (default: minimax/minimax-m2.5)')
57
- .option('--base-url <url>', 'LLM API base URL (default: OpenAI)')
62
+ .option('--provider <provider>', 'LLM provider: openai or cursor (default: openai)')
63
+ .option('--model <model>', 'LLM model name (default depends on provider)')
64
+ .option('--base-url <url>', 'LLM API base URL (for openai provider)')
58
65
  .option('--api-key <key>', 'LLM API key (saved to ~/.gitnexus/config.json)')
59
66
  .option('--concurrency <n>', 'Parallel LLM calls (default: 3)', '3')
60
67
  .option('--gist', 'Publish wiki as a public GitHub Gist after generation')
68
+ .option('-v, --verbose', 'Enable verbose output (show LLM commands and responses)')
69
+ .option('--review', 'Stop after grouping to review module structure before generating pages')
61
70
  .action(createLazyAction(() => import('./wiki.js'), 'wikiCommand'));
62
71
  program
63
72
  .command('augment <pattern>')
package/dist/cli/setup.js CHANGED
@@ -97,18 +97,21 @@ async function setupCursor(result) {
97
97
  }
98
98
  async function setupClaudeCode(result) {
99
99
  const claudeDir = path.join(os.homedir(), '.claude');
100
- const hasClaude = await dirExists(claudeDir);
101
- if (!hasClaude) {
100
+ if (!(await dirExists(claudeDir))) {
102
101
  result.skipped.push('Claude Code (not installed)');
103
102
  return;
104
103
  }
105
- // Claude Code uses a JSON settings file at ~/.claude.json or claude mcp add
106
- console.log('');
107
- console.log(' Claude Code detected. Run this command to add GitNexus MCP:');
108
- console.log('');
109
- console.log(' claude mcp add gitnexus -- npx -y gitnexus mcp');
110
- console.log('');
111
- result.configured.push('Claude Code (MCP manual step printed)');
104
+ // Claude Code stores MCP config in ~/.claude.json
105
+ const mcpPath = path.join(os.homedir(), '.claude.json');
106
+ try {
107
+ const existing = await readJsonFile(mcpPath);
108
+ const updated = mergeMcpConfig(existing);
109
+ await writeJsonFile(mcpPath, updated);
110
+ result.configured.push('Claude Code');
111
+ }
112
+ catch (err) {
113
+ result.errors.push(`Claude Code: ${err.message}`);
114
+ }
112
115
  }
113
116
  /**
114
117
  * Install GitNexus skills to ~/.claude/skills/ for Claude Code.
@@ -4,6 +4,7 @@
4
4
  * Generates repository documentation from the knowledge graph.
5
5
  * Usage: gitnexus wiki [path] [options]
6
6
  */
7
+ import { type LLMProvider } from '../core/wiki/llm-client.js';
7
8
  export interface WikiCommandOptions {
8
9
  force?: boolean;
9
10
  model?: string;
@@ -11,5 +12,8 @@ export interface WikiCommandOptions {
11
12
  apiKey?: string;
12
13
  concurrency?: string;
13
14
  gist?: boolean;
15
+ provider?: LLMProvider;
16
+ verbose?: boolean;
17
+ review?: boolean;
14
18
  }
15
19
  export declare const wikiCommand: (inputPath?: string, options?: WikiCommandOptions) => Promise<void>;
package/dist/cli/wiki.js CHANGED
@@ -12,6 +12,7 @@ import { getGitRoot, isGitRepo } from '../storage/git.js';
12
12
  import { getStoragePaths, loadMeta, loadCLIConfig, saveCLIConfig } from '../storage/repo-manager.js';
13
13
  import { WikiGenerator } from '../core/wiki/generator.js';
14
14
  import { resolveLLMConfig } from '../core/wiki/llm-client.js';
15
+ import { detectCursorCLI } from '../core/wiki/cursor-client.js';
15
16
  /**
16
17
  * Prompt the user for input via stdin.
17
18
  */
@@ -65,6 +66,10 @@ function prompt(question, hide = false) {
65
66
  });
66
67
  }
67
68
  export const wikiCommand = async (inputPath, options) => {
69
+ // Set verbose mode globally for cursor-client to pick up
70
+ if (options?.verbose) {
71
+ process.env.GITNEXUS_VERBOSE = '1';
72
+ }
68
73
  console.log('\n GitNexus Wiki Generator\n');
69
74
  // ── Resolve repo path ───────────────────────────────────────────────
70
75
  let repoPath;
@@ -96,94 +101,130 @@ export const wikiCommand = async (inputPath, options) => {
96
101
  }
97
102
  // ── Resolve LLM config (with interactive fallback) ─────────────────
98
103
  // Save any CLI overrides immediately
99
- if (options?.apiKey || options?.model || options?.baseUrl) {
104
+ if (options?.apiKey || options?.model || options?.baseUrl || options?.provider) {
100
105
  const existing = await loadCLIConfig();
101
106
  const updates = {};
102
107
  if (options.apiKey)
103
108
  updates.apiKey = options.apiKey;
104
- if (options.model)
105
- updates.model = options.model;
106
109
  if (options.baseUrl)
107
110
  updates.baseUrl = options.baseUrl;
111
+ if (options.provider)
112
+ updates.provider = options.provider;
113
+ // Save model to appropriate field based on provider
114
+ if (options.model) {
115
+ if (options.provider === 'cursor') {
116
+ updates.cursorModel = options.model;
117
+ }
118
+ else {
119
+ updates.model = options.model;
120
+ }
121
+ }
108
122
  await saveCLIConfig({ ...existing, ...updates });
109
123
  console.log(' Config saved to ~/.gitnexus/config.json\n');
110
124
  }
111
125
  const savedConfig = await loadCLIConfig();
112
- const hasSavedConfig = !!(savedConfig.apiKey && savedConfig.baseUrl);
113
- const hasCLIOverrides = !!(options?.apiKey || options?.model || options?.baseUrl);
126
+ const hasSavedConfig = !!(savedConfig.provider === 'cursor' || (savedConfig.apiKey && savedConfig.baseUrl));
127
+ const hasCLIOverrides = !!(options?.apiKey || options?.model || options?.baseUrl || options?.provider);
114
128
  let llmConfig = await resolveLLMConfig({
115
129
  model: options?.model,
116
130
  baseUrl: options?.baseUrl,
117
131
  apiKey: options?.apiKey,
132
+ provider: options?.provider,
118
133
  });
119
134
  // Run interactive setup if no saved config and no CLI flags provided
120
135
  // (even if env vars exist — let user explicitly choose their provider)
121
136
  if (!hasSavedConfig && !hasCLIOverrides) {
122
137
  if (!process.stdin.isTTY) {
123
- if (!llmConfig.apiKey) {
138
+ // Non-interactive mode — need either API key or Cursor CLI
139
+ if (!llmConfig.apiKey && llmConfig.provider !== 'cursor') {
124
140
  console.log(' Error: No LLM API key found.');
125
141
  console.log(' Set OPENAI_API_KEY or GITNEXUS_API_KEY environment variable,');
126
- console.log(' or pass --api-key <key>.\n');
142
+ console.log(' or pass --api-key <key>, or use --provider cursor.\n');
127
143
  process.exitCode = 1;
128
144
  return;
129
145
  }
130
- // Non-interactive with env var — just use it
146
+ // Non-interactive with env var or cursor — just use it
131
147
  }
132
148
  else {
133
149
  console.log(' No LLM configured. Let\'s set it up.\n');
134
- console.log(' Supports OpenAI, OpenRouter, or any OpenAI-compatible API.\n');
150
+ console.log(' Supports OpenAI, OpenRouter, any OpenAI-compatible API, or Cursor CLI.\n');
151
+ // Check if Cursor CLI is available
152
+ const hasCursor = detectCursorCLI();
135
153
  // Provider selection
136
154
  console.log(' [1] OpenAI (api.openai.com)');
137
155
  console.log(' [2] OpenRouter (openrouter.ai)');
138
- console.log(' [3] Custom endpoint\n');
139
- const choice = await prompt(' Select provider (1/2/3): ');
156
+ console.log(' [3] Custom endpoint');
157
+ if (hasCursor) {
158
+ console.log(' [4] Cursor CLI (local, uses your Cursor subscription)');
159
+ }
160
+ console.log('');
161
+ const maxChoice = hasCursor ? '4' : '3';
162
+ const choice = await prompt(` Select provider (1/${maxChoice}): `);
140
163
  let baseUrl;
141
164
  let defaultModel;
142
- if (choice === '2') {
143
- baseUrl = 'https://openrouter.ai/api/v1';
144
- defaultModel = 'minimax/minimax-m2.5';
145
- }
146
- else if (choice === '3') {
147
- baseUrl = await prompt(' Base URL (e.g. http://localhost:11434/v1): ');
148
- if (!baseUrl) {
149
- console.log('\n No URL provided. Aborting.\n');
150
- process.exitCode = 1;
151
- return;
152
- }
153
- defaultModel = 'gpt-4o-mini';
165
+ let provider = 'openai';
166
+ let key = '';
167
+ if (choice === '4' && hasCursor) {
168
+ // Cursor CLI selected - model defaults to 'auto' (Cursor's default)
169
+ provider = 'cursor';
170
+ baseUrl = '';
171
+ const modelInput = await prompt(' Model (leave empty for auto): ');
172
+ const model = modelInput || '';
173
+ // Save config for Cursor
174
+ const cursorConfig = { provider: 'cursor' };
175
+ if (model)
176
+ cursorConfig.cursorModel = model;
177
+ await saveCLIConfig(cursorConfig);
178
+ console.log(' Config saved to ~/.gitnexus/config.json\n');
179
+ llmConfig = { ...llmConfig, provider: 'cursor', model, apiKey: '', baseUrl: '' };
154
180
  }
155
181
  else {
156
- baseUrl = 'https://api.openai.com/v1';
157
- defaultModel = 'gpt-4o-mini';
158
- }
159
- // Model
160
- const modelInput = await prompt(` Model (default: ${defaultModel}): `);
161
- const model = modelInput || defaultModel;
162
- // API key pre-fill hint if env var exists
163
- const envKey = process.env.GITNEXUS_API_KEY || process.env.OPENAI_API_KEY || '';
164
- let key;
165
- if (envKey) {
166
- const masked = envKey.slice(0, 6) + '...' + envKey.slice(-4);
167
- const useEnv = await prompt(` Use existing env key (${masked})? (Y/n): `);
168
- if (!useEnv || useEnv.toLowerCase() === 'y' || useEnv.toLowerCase() === 'yes') {
169
- key = envKey;
182
+ // OpenAI-compatible provider
183
+ if (choice === '2') {
184
+ baseUrl = 'https://openrouter.ai/api/v1';
185
+ defaultModel = 'minimax/minimax-m2.5';
186
+ }
187
+ else if (choice === '3') {
188
+ baseUrl = await prompt(' Base URL (e.g. http://localhost:11434/v1): ');
189
+ if (!baseUrl) {
190
+ console.log('\n No URL provided. Aborting.\n');
191
+ process.exitCode = 1;
192
+ return;
193
+ }
194
+ defaultModel = 'gpt-4o-mini';
195
+ }
196
+ else {
197
+ baseUrl = 'https://api.openai.com/v1';
198
+ defaultModel = 'gpt-4o-mini';
199
+ }
200
+ // Model
201
+ const modelInput = await prompt(` Model (default: ${defaultModel}): `);
202
+ const model = modelInput || defaultModel;
203
+ // API key — pre-fill hint if env var exists
204
+ const envKey = process.env.GITNEXUS_API_KEY || process.env.OPENAI_API_KEY || '';
205
+ if (envKey) {
206
+ const masked = envKey.slice(0, 6) + '...' + envKey.slice(-4);
207
+ const useEnv = await prompt(` Use existing env key (${masked})? (Y/n): `);
208
+ if (!useEnv || useEnv.toLowerCase() === 'y' || useEnv.toLowerCase() === 'yes') {
209
+ key = envKey;
210
+ }
211
+ else {
212
+ key = await prompt(' API key: ', true);
213
+ }
170
214
  }
171
215
  else {
172
216
  key = await prompt(' API key: ', true);
173
217
  }
218
+ if (!key) {
219
+ console.log('\n No key provided. Aborting.\n');
220
+ process.exitCode = 1;
221
+ return;
222
+ }
223
+ // Save
224
+ await saveCLIConfig({ apiKey: key, baseUrl, model, provider: 'openai' });
225
+ console.log(' Config saved to ~/.gitnexus/config.json\n');
226
+ llmConfig = { ...llmConfig, apiKey: key, baseUrl, model, provider: 'openai' };
174
227
  }
175
- else {
176
- key = await prompt(' API key: ', true);
177
- }
178
- if (!key) {
179
- console.log('\n No key provided. Aborting.\n');
180
- process.exitCode = 1;
181
- return;
182
- }
183
- // Save
184
- await saveCLIConfig({ apiKey: key, baseUrl, model });
185
- console.log(' Config saved to ~/.gitnexus/config.json\n');
186
- llmConfig = { ...llmConfig, apiKey: key, baseUrl, model };
187
228
  }
188
229
  }
189
230
  // ── Setup progress bar with elapsed timer ──────────────────────────
@@ -213,9 +254,8 @@ export const wikiCommand = async (inputPath, options) => {
213
254
  // ── Run generator ───────────────────────────────────────────────────
214
255
  const wikiOptions = {
215
256
  force: options?.force,
216
- model: options?.model,
217
- baseUrl: options?.baseUrl,
218
257
  concurrency: options?.concurrency ? parseInt(options.concurrency, 10) : undefined,
258
+ reviewOnly: options?.review,
219
259
  };
220
260
  const generator = new WikiGenerator(repoPath, storagePath, lbugPath, llmConfig, wikiOptions, (phase, percent, detail) => {
221
261
  const label = detail || phase;
@@ -228,11 +268,92 @@ export const wikiCommand = async (inputPath, options) => {
228
268
  try {
229
269
  const result = await generator.run();
230
270
  clearInterval(elapsedTimer);
231
- bar.update(100, { phase: 'Done' });
232
271
  bar.stop();
233
272
  const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
234
273
  const wikiDir = path.join(storagePath, 'wiki');
235
274
  const viewerPath = path.join(wikiDir, 'index.html');
275
+ const treeFile = path.join(wikiDir, 'module_tree.json');
276
+ // Review mode: show module tree and ask for confirmation
277
+ if (options?.review && result.moduleTree) {
278
+ console.log(`\n Module structure ready for review (${elapsed}s)\n`);
279
+ console.log(' Modules to generate:\n');
280
+ const printTree = (nodes, indent = 0) => {
281
+ for (const node of nodes) {
282
+ const prefix = ' '.repeat(indent + 2);
283
+ const fileCount = node.files?.length || 0;
284
+ const childCount = node.children?.length || 0;
285
+ const suffix = fileCount > 0 ? ` (${fileCount} files)` : childCount > 0 ? ` (${childCount} children)` : '';
286
+ console.log(`${prefix}- ${node.name}${suffix}`);
287
+ if (node.children && node.children.length > 0) {
288
+ printTree(node.children, indent + 1);
289
+ }
290
+ }
291
+ };
292
+ printTree(result.moduleTree);
293
+ console.log(`\n Tree saved to: ${treeFile}`);
294
+ console.log(' You can edit this file to remove/rename modules.\n');
295
+ // Ask for confirmation (auto-continue in non-interactive environments)
296
+ if (!process.stdin.isTTY) {
297
+ console.log(' Non-interactive mode — auto-continuing with generation.\n');
298
+ }
299
+ const answer = process.stdin.isTTY
300
+ ? await prompt(' Continue with generation? (Y/n/edit): ')
301
+ : 'y';
302
+ const choice = answer.trim().toLowerCase();
303
+ if (choice === 'n' || choice === 'no') {
304
+ console.log('\n Generation cancelled. Run `gitnexus wiki` later to generate.\n');
305
+ return;
306
+ }
307
+ if (choice === 'edit' || choice === 'e') {
308
+ // Open editor for the user
309
+ const editor = process.env.EDITOR || process.env.VISUAL || 'vi';
310
+ console.log(`\n Opening ${treeFile} in ${editor}...`);
311
+ console.log(' Save and close the editor when done.\n');
312
+ try {
313
+ execSync(`${editor} "${treeFile}"`, { stdio: 'inherit' });
314
+ }
315
+ catch {
316
+ console.log(` Could not open editor. Please edit manually:\n ${treeFile}\n`);
317
+ console.log(' Then run `gitnexus wiki` to continue.\n');
318
+ return;
319
+ }
320
+ }
321
+ // Continue with generation using the (possibly edited) tree
322
+ console.log('\n Continuing with wiki generation...\n');
323
+ bar.start(100, 30, { phase: 'Generating pages...' });
324
+ // Re-run generator without reviewOnly flag
325
+ const continueOptions = {
326
+ ...wikiOptions,
327
+ reviewOnly: false,
328
+ };
329
+ const continueGenerator = new WikiGenerator(repoPath, storagePath, lbugPath, llmConfig, continueOptions, (phase, percent, detail) => {
330
+ const label = detail || phase;
331
+ if (label !== lastPhase) {
332
+ lastPhase = label;
333
+ phaseStart = Date.now();
334
+ }
335
+ bar.update(percent, { phase: label });
336
+ });
337
+ const continueResult = await continueGenerator.run();
338
+ bar.update(100, { phase: 'Done' });
339
+ bar.stop();
340
+ const totalElapsed = ((Date.now() - t0) / 1000).toFixed(1);
341
+ console.log(`\n Wiki generated successfully (${totalElapsed}s)\n`);
342
+ console.log(` Mode: ${continueResult.mode}`);
343
+ console.log(` Pages: ${continueResult.pagesGenerated}`);
344
+ console.log(` Output: ${wikiDir}`);
345
+ console.log(` Viewer: ${viewerPath}`);
346
+ if (continueResult.failedModules && continueResult.failedModules.length > 0) {
347
+ console.log(`\n Failed modules (${continueResult.failedModules.length}):`);
348
+ for (const mod of continueResult.failedModules) {
349
+ console.log(` - ${mod}`);
350
+ }
351
+ }
352
+ console.log('');
353
+ await maybePublishGist(viewerPath, options?.gist);
354
+ return;
355
+ }
356
+ bar.update(100, { phase: 'Done' });
236
357
  if (result.mode === 'up-to-date' && !options?.force) {
237
358
  console.log('\n Wiki is already up to date.');
238
359
  console.log(` Viewer: ${viewerPath}\n`);
@@ -280,7 +401,7 @@ export const wikiCommand = async (inputPath, options) => {
280
401
  }
281
402
  else {
282
403
  console.log(`\n Error: ${err.message}\n`);
283
- if (process.env.DEBUG) {
404
+ if (process.env.GITNEXUS_VERBOSE) {
284
405
  console.error(err);
285
406
  }
286
407
  }
@@ -9,14 +9,13 @@
9
9
  * ----------------------------------|------------------------------------------|---------------------------
10
10
  * tree-sitter-queries.ts | Query string + LANGUAGE_QUERIES entry | (required)
11
11
  * export-detection.ts | ExportChecker function + table entry | (required)
12
- * import-resolution.ts | Resolver in importResolvers | resolveStandard(...)
13
- * import-resolution.ts | namedBindingExtractors entry | undefined
14
- * call-routing.ts | callRouters entry | noRouting
12
+ * import-resolvers/<lang>.ts | Exported resolve<Lang>Import function | resolveStandard(...)
13
+ * call-routing.ts | CallRouter function (or noRouting) | noRouting
15
14
  * entry-point-scoring.ts | ENTRY_POINT_PATTERNS entry | []
16
15
  * framework-detection.ts | AST_FRAMEWORK_PATTERNS entry | []
17
16
  * type-extractors/<lang>.ts | New file + index.ts import | (required)
18
17
  * resolvers/<lang>.ts | Resolver file (if non-standard) | (only if resolveStandard insufficient)
19
- * named-binding-extraction.ts | Extractor (if named imports) | (only if language has named imports)
18
+ * named-bindings/<lang>.ts | Extractor (if named imports) | (only if language has named imports)
20
19
  *
21
20
  * 4. Also check these files for language-specific if-checks (no compile-time guard):
22
21
  * - mro-processor.ts (MRO strategy selection)
@@ -41,5 +40,8 @@ export declare enum SupportedLanguages {
41
40
  Rust = "rust",
42
41
  PHP = "php",
43
42
  Kotlin = "kotlin",
44
- Swift = "swift"
43
+ Swift = "swift",
44
+ Dart = "dart",
45
+ /** Standalone regex processor — no tree-sitter, no LanguageProvider. */
46
+ Cobol = "cobol"
45
47
  }
@@ -9,14 +9,13 @@
9
9
  * ----------------------------------|------------------------------------------|---------------------------
10
10
  * tree-sitter-queries.ts | Query string + LANGUAGE_QUERIES entry | (required)
11
11
  * export-detection.ts | ExportChecker function + table entry | (required)
12
- * import-resolution.ts | Resolver in importResolvers | resolveStandard(...)
13
- * import-resolution.ts | namedBindingExtractors entry | undefined
14
- * call-routing.ts | callRouters entry | noRouting
12
+ * import-resolvers/<lang>.ts | Exported resolve<Lang>Import function | resolveStandard(...)
13
+ * call-routing.ts | CallRouter function (or noRouting) | noRouting
15
14
  * entry-point-scoring.ts | ENTRY_POINT_PATTERNS entry | []
16
15
  * framework-detection.ts | AST_FRAMEWORK_PATTERNS entry | []
17
16
  * type-extractors/<lang>.ts | New file + index.ts import | (required)
18
17
  * resolvers/<lang>.ts | Resolver file (if non-standard) | (only if resolveStandard insufficient)
19
- * named-binding-extraction.ts | Extractor (if named imports) | (only if language has named imports)
18
+ * named-bindings/<lang>.ts | Extractor (if named imports) | (only if language has named imports)
20
19
  *
21
20
  * 4. Also check these files for language-specific if-checks (no compile-time guard):
22
21
  * - mro-processor.ts (MRO strategy selection)
@@ -43,4 +42,7 @@ export var SupportedLanguages;
43
42
  SupportedLanguages["PHP"] = "php";
44
43
  SupportedLanguages["Kotlin"] = "kotlin";
45
44
  SupportedLanguages["Swift"] = "swift";
45
+ SupportedLanguages["Dart"] = "dart";
46
+ /** Standalone regex processor — no tree-sitter, no LanguageProvider. */
47
+ SupportedLanguages["Cobol"] = "cobol";
46
48
  })(SupportedLanguages || (SupportedLanguages = {}));
@@ -27,7 +27,14 @@ export const createKnowledgeGraph = () => {
27
27
  return true;
28
28
  };
29
29
  /**
30
- * Remove all nodes (and their relationships) belonging to a file
30
+ * Remove a single relationship by id.
31
+ * Returns true if the relationship existed and was removed, false otherwise.
32
+ */
33
+ const removeRelationship = (relationshipId) => {
34
+ return relationshipMap.delete(relationshipId);
35
+ };
36
+ /**
37
+ * Remove all nodes (and their relationships) belonging to a file.
31
38
  */
32
39
  const removeNodesByFile = (filePath) => {
33
40
  let removed = 0;
@@ -62,5 +69,6 @@ export const createKnowledgeGraph = () => {
62
69
  addRelationship,
63
70
  removeNode,
64
71
  removeNodesByFile,
72
+ removeRelationship,
65
73
  };
66
74
  };