gitnexus 1.4.10 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (211) hide show
  1. package/README.md +6 -5
  2. package/dist/_shared/graph/types.d.ts +65 -0
  3. package/dist/_shared/graph/types.d.ts.map +1 -0
  4. package/dist/_shared/graph/types.js +8 -0
  5. package/dist/_shared/graph/types.js.map +1 -0
  6. package/dist/_shared/index.d.ts +7 -0
  7. package/dist/_shared/index.d.ts.map +1 -0
  8. package/dist/_shared/index.js +6 -0
  9. package/dist/_shared/index.js.map +1 -0
  10. package/dist/_shared/language-detection.d.ts +23 -0
  11. package/dist/_shared/language-detection.d.ts.map +1 -0
  12. package/dist/_shared/language-detection.js +137 -0
  13. package/dist/_shared/language-detection.js.map +1 -0
  14. package/dist/_shared/languages.d.ts +25 -0
  15. package/dist/_shared/languages.d.ts.map +1 -0
  16. package/dist/_shared/languages.js +26 -0
  17. package/dist/_shared/languages.js.map +1 -0
  18. package/dist/_shared/lbug/schema-constants.d.ts +16 -0
  19. package/dist/_shared/lbug/schema-constants.d.ts.map +1 -0
  20. package/dist/_shared/lbug/schema-constants.js +64 -0
  21. package/dist/_shared/lbug/schema-constants.js.map +1 -0
  22. package/dist/_shared/pipeline.d.ts +16 -0
  23. package/dist/_shared/pipeline.d.ts.map +1 -0
  24. package/dist/_shared/pipeline.js +5 -0
  25. package/dist/_shared/pipeline.js.map +1 -0
  26. package/dist/cli/ai-context.d.ts +4 -1
  27. package/dist/cli/ai-context.js +19 -11
  28. package/dist/cli/analyze.d.ts +6 -0
  29. package/dist/cli/analyze.js +105 -251
  30. package/dist/cli/eval-server.js +20 -11
  31. package/dist/cli/index-repo.js +20 -22
  32. package/dist/cli/index.js +8 -7
  33. package/dist/cli/mcp.js +1 -1
  34. package/dist/cli/serve.js +29 -1
  35. package/dist/cli/setup.js +9 -9
  36. package/dist/cli/skill-gen.js +15 -9
  37. package/dist/cli/wiki.d.ts +2 -0
  38. package/dist/cli/wiki.js +141 -26
  39. package/dist/config/ignore-service.js +102 -22
  40. package/dist/config/supported-languages.d.ts +8 -42
  41. package/dist/config/supported-languages.js +8 -43
  42. package/dist/core/augmentation/engine.js +19 -7
  43. package/dist/core/embeddings/embedder.js +19 -15
  44. package/dist/core/embeddings/embedding-pipeline.js +6 -6
  45. package/dist/core/embeddings/http-client.js +3 -3
  46. package/dist/core/embeddings/text-generator.js +9 -24
  47. package/dist/core/embeddings/types.d.ts +1 -1
  48. package/dist/core/embeddings/types.js +1 -7
  49. package/dist/core/graph/graph.js +6 -2
  50. package/dist/core/graph/types.d.ts +9 -59
  51. package/dist/core/ingestion/ast-cache.js +3 -3
  52. package/dist/core/ingestion/call-processor.d.ts +20 -2
  53. package/dist/core/ingestion/call-processor.js +347 -144
  54. package/dist/core/ingestion/call-routing.js +10 -4
  55. package/dist/core/ingestion/call-sites/extract-language-call-site.d.ts +10 -0
  56. package/dist/core/ingestion/call-sites/extract-language-call-site.js +22 -0
  57. package/dist/core/ingestion/call-sites/java.d.ts +9 -0
  58. package/dist/core/ingestion/call-sites/java.js +30 -0
  59. package/dist/core/ingestion/cluster-enricher.js +6 -8
  60. package/dist/core/ingestion/cobol/cobol-copy-expander.js +10 -3
  61. package/dist/core/ingestion/cobol/cobol-preprocessor.js +287 -81
  62. package/dist/core/ingestion/cobol/jcl-parser.js +1 -1
  63. package/dist/core/ingestion/cobol/jcl-processor.js +1 -1
  64. package/dist/core/ingestion/cobol-processor.js +102 -56
  65. package/dist/core/ingestion/community-processor.js +21 -15
  66. package/dist/core/ingestion/entry-point-scoring.d.ts +1 -1
  67. package/dist/core/ingestion/entry-point-scoring.js +5 -6
  68. package/dist/core/ingestion/export-detection.js +32 -9
  69. package/dist/core/ingestion/field-extractor.d.ts +1 -1
  70. package/dist/core/ingestion/field-extractors/configs/c-cpp.js +8 -12
  71. package/dist/core/ingestion/field-extractors/configs/csharp.js +45 -2
  72. package/dist/core/ingestion/field-extractors/configs/dart.js +5 -3
  73. package/dist/core/ingestion/field-extractors/configs/go.js +3 -7
  74. package/dist/core/ingestion/field-extractors/configs/helpers.d.ts +5 -0
  75. package/dist/core/ingestion/field-extractors/configs/helpers.js +14 -0
  76. package/dist/core/ingestion/field-extractors/configs/jvm.js +7 -7
  77. package/dist/core/ingestion/field-extractors/configs/php.js +9 -11
  78. package/dist/core/ingestion/field-extractors/configs/python.js +1 -1
  79. package/dist/core/ingestion/field-extractors/configs/ruby.js +4 -3
  80. package/dist/core/ingestion/field-extractors/configs/rust.js +2 -5
  81. package/dist/core/ingestion/field-extractors/configs/swift.js +9 -7
  82. package/dist/core/ingestion/field-extractors/configs/typescript-javascript.js +2 -6
  83. package/dist/core/ingestion/field-extractors/generic.d.ts +5 -2
  84. package/dist/core/ingestion/field-extractors/generic.js +6 -0
  85. package/dist/core/ingestion/field-extractors/typescript.d.ts +1 -1
  86. package/dist/core/ingestion/field-extractors/typescript.js +1 -1
  87. package/dist/core/ingestion/field-types.d.ts +4 -2
  88. package/dist/core/ingestion/filesystem-walker.js +3 -3
  89. package/dist/core/ingestion/framework-detection.d.ts +1 -1
  90. package/dist/core/ingestion/framework-detection.js +355 -85
  91. package/dist/core/ingestion/heritage-processor.d.ts +24 -0
  92. package/dist/core/ingestion/heritage-processor.js +99 -8
  93. package/dist/core/ingestion/import-processor.js +44 -15
  94. package/dist/core/ingestion/import-resolvers/csharp.js +7 -3
  95. package/dist/core/ingestion/import-resolvers/dart.js +1 -1
  96. package/dist/core/ingestion/import-resolvers/go.js +4 -2
  97. package/dist/core/ingestion/import-resolvers/jvm.js +4 -4
  98. package/dist/core/ingestion/import-resolvers/php.js +4 -4
  99. package/dist/core/ingestion/import-resolvers/python.js +1 -1
  100. package/dist/core/ingestion/import-resolvers/rust.js +9 -3
  101. package/dist/core/ingestion/import-resolvers/standard.d.ts +1 -1
  102. package/dist/core/ingestion/import-resolvers/standard.js +6 -5
  103. package/dist/core/ingestion/import-resolvers/swift.js +2 -1
  104. package/dist/core/ingestion/import-resolvers/utils.js +26 -7
  105. package/dist/core/ingestion/language-config.js +5 -4
  106. package/dist/core/ingestion/language-provider.d.ts +7 -2
  107. package/dist/core/ingestion/languages/c-cpp.js +106 -21
  108. package/dist/core/ingestion/languages/cobol.js +1 -1
  109. package/dist/core/ingestion/languages/csharp.js +96 -19
  110. package/dist/core/ingestion/languages/dart.js +23 -7
  111. package/dist/core/ingestion/languages/go.js +1 -1
  112. package/dist/core/ingestion/languages/index.d.ts +1 -1
  113. package/dist/core/ingestion/languages/index.js +2 -3
  114. package/dist/core/ingestion/languages/java.js +4 -1
  115. package/dist/core/ingestion/languages/kotlin.js +60 -13
  116. package/dist/core/ingestion/languages/php.js +102 -25
  117. package/dist/core/ingestion/languages/python.js +28 -5
  118. package/dist/core/ingestion/languages/ruby.js +56 -14
  119. package/dist/core/ingestion/languages/rust.js +55 -11
  120. package/dist/core/ingestion/languages/swift.js +112 -27
  121. package/dist/core/ingestion/languages/typescript.js +95 -19
  122. package/dist/core/ingestion/markdown-processor.js +5 -5
  123. package/dist/core/ingestion/method-extractors/configs/csharp.d.ts +2 -0
  124. package/dist/core/ingestion/method-extractors/configs/csharp.js +283 -0
  125. package/dist/core/ingestion/method-extractors/configs/jvm.d.ts +3 -0
  126. package/dist/core/ingestion/method-extractors/configs/jvm.js +326 -0
  127. package/dist/core/ingestion/method-extractors/generic.d.ts +5 -0
  128. package/dist/core/ingestion/method-extractors/generic.js +137 -0
  129. package/dist/core/ingestion/method-types.d.ts +61 -0
  130. package/dist/core/ingestion/method-types.js +2 -0
  131. package/dist/core/ingestion/mro-processor.d.ts +1 -1
  132. package/dist/core/ingestion/mro-processor.js +12 -8
  133. package/dist/core/ingestion/named-binding-processor.js +2 -2
  134. package/dist/core/ingestion/named-bindings/rust.js +3 -1
  135. package/dist/core/ingestion/parsing-processor.js +74 -24
  136. package/dist/core/ingestion/pipeline.d.ts +2 -1
  137. package/dist/core/ingestion/pipeline.js +208 -102
  138. package/dist/core/ingestion/process-processor.js +12 -10
  139. package/dist/core/ingestion/resolution-context.js +3 -3
  140. package/dist/core/ingestion/route-extractors/middleware.js +31 -7
  141. package/dist/core/ingestion/route-extractors/php.js +2 -1
  142. package/dist/core/ingestion/route-extractors/response-shapes.js +8 -4
  143. package/dist/core/ingestion/structure-processor.d.ts +1 -1
  144. package/dist/core/ingestion/structure-processor.js +4 -4
  145. package/dist/core/ingestion/symbol-table.d.ts +1 -1
  146. package/dist/core/ingestion/symbol-table.js +22 -6
  147. package/dist/core/ingestion/tree-sitter-queries.d.ts +1 -1
  148. package/dist/core/ingestion/tree-sitter-queries.js +1 -1
  149. package/dist/core/ingestion/type-env.d.ts +2 -2
  150. package/dist/core/ingestion/type-env.js +75 -50
  151. package/dist/core/ingestion/type-extractors/c-cpp.js +33 -30
  152. package/dist/core/ingestion/type-extractors/csharp.js +24 -14
  153. package/dist/core/ingestion/type-extractors/dart.js +6 -8
  154. package/dist/core/ingestion/type-extractors/go.js +7 -6
  155. package/dist/core/ingestion/type-extractors/jvm.js +10 -21
  156. package/dist/core/ingestion/type-extractors/php.js +26 -13
  157. package/dist/core/ingestion/type-extractors/python.js +11 -15
  158. package/dist/core/ingestion/type-extractors/ruby.js +8 -3
  159. package/dist/core/ingestion/type-extractors/rust.js +6 -8
  160. package/dist/core/ingestion/type-extractors/shared.js +134 -50
  161. package/dist/core/ingestion/type-extractors/swift.js +16 -13
  162. package/dist/core/ingestion/type-extractors/typescript.js +23 -15
  163. package/dist/core/ingestion/utils/ast-helpers.d.ts +8 -8
  164. package/dist/core/ingestion/utils/ast-helpers.js +72 -35
  165. package/dist/core/ingestion/utils/call-analysis.d.ts +2 -0
  166. package/dist/core/ingestion/utils/call-analysis.js +96 -49
  167. package/dist/core/ingestion/utils/event-loop.js +1 -1
  168. package/dist/core/ingestion/workers/parse-worker.d.ts +7 -2
  169. package/dist/core/ingestion/workers/parse-worker.js +364 -84
  170. package/dist/core/ingestion/workers/worker-pool.js +5 -10
  171. package/dist/core/lbug/csv-generator.js +54 -15
  172. package/dist/core/lbug/lbug-adapter.d.ts +5 -0
  173. package/dist/core/lbug/lbug-adapter.js +86 -23
  174. package/dist/core/lbug/schema.d.ts +3 -6
  175. package/dist/core/lbug/schema.js +6 -30
  176. package/dist/core/run-analyze.d.ts +49 -0
  177. package/dist/core/run-analyze.js +257 -0
  178. package/dist/core/tree-sitter/parser-loader.d.ts +1 -1
  179. package/dist/core/tree-sitter/parser-loader.js +1 -1
  180. package/dist/core/wiki/cursor-client.js +2 -7
  181. package/dist/core/wiki/generator.js +38 -23
  182. package/dist/core/wiki/graph-queries.js +10 -10
  183. package/dist/core/wiki/html-viewer.js +7 -3
  184. package/dist/core/wiki/llm-client.d.ts +23 -2
  185. package/dist/core/wiki/llm-client.js +96 -26
  186. package/dist/core/wiki/prompts.js +7 -6
  187. package/dist/mcp/core/embedder.js +1 -1
  188. package/dist/mcp/core/lbug-adapter.d.ts +4 -1
  189. package/dist/mcp/core/lbug-adapter.js +17 -7
  190. package/dist/mcp/local/local-backend.js +247 -95
  191. package/dist/mcp/resources.js +14 -6
  192. package/dist/mcp/server.js +13 -5
  193. package/dist/mcp/staleness.js +5 -1
  194. package/dist/mcp/tools.js +100 -23
  195. package/dist/server/analyze-job.d.ts +53 -0
  196. package/dist/server/analyze-job.js +146 -0
  197. package/dist/server/analyze-worker.d.ts +13 -0
  198. package/dist/server/analyze-worker.js +59 -0
  199. package/dist/server/api.js +795 -44
  200. package/dist/server/git-clone.d.ts +25 -0
  201. package/dist/server/git-clone.js +91 -0
  202. package/dist/storage/git.js +1 -3
  203. package/dist/storage/repo-manager.d.ts +5 -2
  204. package/dist/storage/repo-manager.js +4 -4
  205. package/dist/types/pipeline.d.ts +1 -21
  206. package/dist/types/pipeline.js +1 -18
  207. package/hooks/claude/gitnexus-hook.cjs +52 -22
  208. package/package.json +5 -4
  209. package/scripts/build.js +69 -0
  210. package/dist/core/ingestion/utils/language-detection.d.ts +0 -9
  211. package/dist/core/ingestion/utils/language-detection.js +0 -70
package/dist/cli/serve.js CHANGED
@@ -1,6 +1,34 @@
1
1
  import { createServer } from '../server/api.js';
2
+ // Catch anything that would cause a silent exit
3
+ process.on('uncaughtException', (err) => {
4
+ console.error('\n[gitnexus serve] Uncaught exception:', err.message);
5
+ if (process.env.DEBUG)
6
+ console.error(err.stack);
7
+ process.exit(1);
8
+ });
9
+ process.on('unhandledRejection', (reason) => {
10
+ console.error('\n[gitnexus serve] Unhandled rejection:', reason?.message || reason);
11
+ if (process.env.DEBUG)
12
+ console.error(reason?.stack);
13
+ process.exit(1);
14
+ });
2
15
  export const serveCommand = async (options) => {
3
16
  const port = Number(options?.port ?? 4747);
4
17
  const host = options?.host ?? '127.0.0.1';
5
- await createServer(port, host);
18
+ try {
19
+ await createServer(port, host);
20
+ }
21
+ catch (err) {
22
+ console.error(`\nFailed to start GitNexus server:\n`);
23
+ console.error(` ${err.message || err}\n`);
24
+ if (err.code === 'EADDRINUSE') {
25
+ console.error(` Port ${port} is already in use. Either:`);
26
+ console.error(` 1. Stop the other process using port ${port}`);
27
+ console.error(` 2. Use a different port: gitnexus serve --port 4748\n`);
28
+ }
29
+ if (err.stack && process.env.DEBUG) {
30
+ console.error(err.stack);
31
+ }
32
+ process.exit(1);
33
+ }
6
34
  };
package/dist/cli/setup.js CHANGED
@@ -164,13 +164,13 @@ async function installClaudeCodeHooks(result) {
164
164
  const hookPath = path.join(destHooksDir, 'gitnexus-hook.cjs').replace(/\\/g, '/');
165
165
  const hookCmd = `node "${hookPath.replace(/"/g, '\\"')}"`;
166
166
  // Merge hook config into ~/.claude/settings.json
167
- const existing = await readJsonFile(settingsPath) || {};
167
+ const existing = (await readJsonFile(settingsPath)) || {};
168
168
  if (!existing.hooks)
169
169
  existing.hooks = {};
170
170
  function ensureHookEntry(eventName, matcher, timeout, statusMessage) {
171
171
  if (!existing.hooks[eventName])
172
172
  existing.hooks[eventName] = [];
173
- const hasHook = existing.hooks[eventName].some((h) => h.hooks?.some(hh => hh.command?.includes('gitnexus-hook')));
173
+ const hasHook = existing.hooks[eventName].some((h) => h.hooks?.some((hh) => hh.command?.includes('gitnexus-hook')));
174
174
  if (!hasHook) {
175
175
  existing.hooks[eventName].push({
176
176
  matcher,
@@ -213,7 +213,7 @@ async function setupOpenCode(result) {
213
213
  function getCodexMcpTomlSection() {
214
214
  const entry = getMcpEntry();
215
215
  const command = JSON.stringify(entry.command);
216
- const args = `[${entry.args.map(arg => JSON.stringify(arg)).join(', ')}]`;
216
+ const args = `[${entry.args.map((arg) => JSON.stringify(arg)).join(', ')}]`;
217
217
  return `[mcp_servers.gitnexus]\ncommand = ${command}\nargs = ${args}\n`;
218
218
  }
219
219
  /**
@@ -231,9 +231,7 @@ async function upsertCodexConfigToml(configPath) {
231
231
  return;
232
232
  }
233
233
  const section = getCodexMcpTomlSection();
234
- const nextContent = existing.trim().length > 0
235
- ? `${existing.trimEnd()}\n\n${section}`
236
- : section;
234
+ const nextContent = existing.trim().length > 0 ? `${existing.trimEnd()}\n\n${section}` : section;
237
235
  await fs.mkdir(path.dirname(configPath), { recursive: true });
238
236
  await fs.writeFile(configPath, `${nextContent.trimEnd()}\n`, 'utf-8');
239
237
  }
@@ -245,7 +243,9 @@ async function setupCodex(result) {
245
243
  }
246
244
  try {
247
245
  const entry = getMcpEntry();
248
- await execFileAsync('codex', ['mcp', 'add', 'gitnexus', '--', entry.command, ...entry.args], { shell: process.platform === 'win32' });
246
+ await execFileAsync('codex', ['mcp', 'add', 'gitnexus', '--', entry.command, ...entry.args], {
247
+ shell: process.platform === 'win32',
248
+ });
249
249
  result.configured.push('Codex');
250
250
  return;
251
251
  }
@@ -436,8 +436,8 @@ export const setupCommand = async () => {
436
436
  }
437
437
  console.log('');
438
438
  console.log(' Summary:');
439
- console.log(` MCP configured for: ${result.configured.filter(c => !c.includes('skills')).join(', ') || 'none'}`);
440
- console.log(` Skills installed to: ${result.configured.filter(c => c.includes('skills')).length > 0 ? result.configured.filter(c => c.includes('skills')).join(', ') : 'none'}`);
439
+ console.log(` MCP configured for: ${result.configured.filter((c) => !c.includes('skills')).join(', ') || 'none'}`);
440
+ console.log(` Skills installed to: ${result.configured.filter((c) => c.includes('skills')).length > 0 ? result.configured.filter((c) => c.includes('skills')).join(', ') : 'none'}`);
441
441
  console.log('');
442
442
  console.log(' Next steps:');
443
443
  console.log(' 1. cd into any git repo');
@@ -37,7 +37,7 @@ export const generateSkillFiles = async (repoPath, projectName, pipelineResult)
37
37
  // Step 2: Filter to significant communities
38
38
  // Keep communities with >= 3 symbols after aggregation.
39
39
  const significant = aggregated
40
- .filter(c => c.symbolCount >= 3)
40
+ .filter((c) => c.symbolCount >= 3)
41
41
  .sort((a, b) => b.symbolCount - a.symbolCount)
42
42
  .slice(0, 20);
43
43
  if (significant.length === 0) {
@@ -51,7 +51,9 @@ export const generateSkillFiles = async (repoPath, projectName, pipelineResult)
51
51
  try {
52
52
  await fs.rm(outputDir, { recursive: true, force: true });
53
53
  }
54
- catch { /* may not exist */ }
54
+ catch {
55
+ /* may not exist */
56
+ }
55
57
  await fs.mkdir(outputDir, { recursive: true });
56
58
  // Step 5: Generate skill files
57
59
  const skills = [];
@@ -146,7 +148,7 @@ const buildCommunitiesFromMemberships = (memberships, graph, repoPath) => {
146
148
  const nodeSet = new Set(nodeIds);
147
149
  let internalEdges = 0;
148
150
  let totalEdges = 0;
149
- graph.forEachRelationship(rel => {
151
+ graph.forEachRelationship((rel) => {
150
152
  if (nodeSet.has(rel.sourceId)) {
151
153
  totalEdges++;
152
154
  if (nodeSet.has(rel.targetId))
@@ -311,7 +313,7 @@ const gatherEntryPoints = (members) => {
311
313
  Interface: 3,
312
314
  };
313
315
  return members
314
- .filter(m => m.isExported)
316
+ .filter((m) => m.isExported)
315
317
  .sort((a, b) => {
316
318
  const pa = typePriority[a.label] ?? 99;
317
319
  const pb = typePriority[b.label] ?? 99;
@@ -327,7 +329,7 @@ const gatherEntryPoints = (members) => {
327
329
  const gatherFlows = (rawIds, processes) => {
328
330
  const rawIdSet = new Set(rawIds);
329
331
  return processes
330
- .filter(proc => proc.communities.some(cid => rawIdSet.has(cid)))
332
+ .filter((proc) => proc.communities.some((cid) => rawIdSet.has(cid)))
331
333
  .sort((a, b) => b.stepCount - a.stepCount);
332
334
  };
333
335
  /**
@@ -350,7 +352,7 @@ const gatherCrossConnections = (rawIds, ownLabel, membershipsByComm, nodeIdToCom
350
352
  }
351
353
  // Count outgoing CALLS to nodes in different communities
352
354
  const targetCounts = new Map();
353
- graph.forEachRelationship(rel => {
355
+ graph.forEachRelationship((rel) => {
354
356
  if (rel.type !== 'CALLS')
355
357
  return;
356
358
  if (!ownNodeIds.has(rel.sourceId))
@@ -386,10 +388,10 @@ const renderSkillMarkdown = (community, projectName, members, files, entryPoints
386
388
  // Dominant directory: most common top-level directory
387
389
  const dominantDir = getDominantDirectory(files);
388
390
  // Top symbol names for "When to Use"
389
- const topNames = entryPoints.slice(0, 3).map(e => e.name);
391
+ const topNames = entryPoints.slice(0, 3).map((e) => e.name);
390
392
  if (topNames.length === 0) {
391
393
  // Fallback to any members
392
- topNames.push(...members.slice(0, 3).map(m => m.name));
394
+ topNames.push(...members.slice(0, 3).map((m) => m.name));
393
395
  }
394
396
  const lines = [];
395
397
  // Frontmatter
@@ -473,7 +475,11 @@ const renderSkillMarkdown = (community, projectName, members, files, entryPoints
473
475
  lines.push('');
474
476
  }
475
477
  // How to Explore
476
- const firstEntry = entryPoints.length > 0 ? entryPoints[0].name : (members.length > 0 ? members[0].name : community.label);
478
+ const firstEntry = entryPoints.length > 0
479
+ ? entryPoints[0].name
480
+ : members.length > 0
481
+ ? members[0].name
482
+ : community.label;
477
483
  lines.push('## How to Explore');
478
484
  lines.push('');
479
485
  lines.push(`1. \`gitnexus_context({name: "${firstEntry}"})\` \u2014 see callers and callees`);
@@ -10,6 +10,8 @@ export interface WikiCommandOptions {
10
10
  model?: string;
11
11
  baseUrl?: string;
12
12
  apiKey?: string;
13
+ apiVersion?: string;
14
+ reasoningModel?: boolean;
13
15
  concurrency?: string;
14
16
  gist?: boolean;
15
17
  provider?: LLMProvider;
package/dist/cli/wiki.js CHANGED
@@ -9,7 +9,7 @@ import readline from 'readline';
9
9
  import { execSync, execFileSync } from 'child_process';
10
10
  import cliProgress from 'cli-progress';
11
11
  import { getGitRoot, isGitRepo } from '../storage/git.js';
12
- import { getStoragePaths, loadMeta, loadCLIConfig, saveCLIConfig } from '../storage/repo-manager.js';
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
15
  import { detectCursorCLI } from '../core/wiki/cursor-client.js';
@@ -101,7 +101,12 @@ export const wikiCommand = async (inputPath, options) => {
101
101
  }
102
102
  // ── Resolve LLM config (with interactive fallback) ─────────────────
103
103
  // Save any CLI overrides immediately
104
- if (options?.apiKey || options?.model || options?.baseUrl || options?.provider) {
104
+ if (options?.apiKey ||
105
+ options?.model ||
106
+ options?.baseUrl ||
107
+ options?.provider ||
108
+ options?.apiVersion ||
109
+ options?.reasoningModel !== undefined) {
105
110
  const existing = await loadCLIConfig();
106
111
  const updates = {};
107
112
  if (options.apiKey)
@@ -110,6 +115,10 @@ export const wikiCommand = async (inputPath, options) => {
110
115
  updates.baseUrl = options.baseUrl;
111
116
  if (options.provider)
112
117
  updates.provider = options.provider;
118
+ if (options.apiVersion)
119
+ updates.apiVersion = options.apiVersion;
120
+ if (options.reasoningModel !== undefined)
121
+ updates.isReasoningModel = options.reasoningModel;
113
122
  // Save model to appropriate field based on provider
114
123
  if (options.model) {
115
124
  if (options.provider === 'cursor') {
@@ -123,13 +132,21 @@ export const wikiCommand = async (inputPath, options) => {
123
132
  console.log(' Config saved to ~/.gitnexus/config.json\n');
124
133
  }
125
134
  const savedConfig = await loadCLIConfig();
126
- const hasSavedConfig = !!(savedConfig.provider === 'cursor' || (savedConfig.apiKey && savedConfig.baseUrl));
127
- const hasCLIOverrides = !!(options?.apiKey || options?.model || options?.baseUrl || options?.provider);
135
+ const hasSavedConfig = !!(savedConfig.provider === 'cursor' ||
136
+ (savedConfig.apiKey && savedConfig.baseUrl));
137
+ const hasCLIOverrides = !!(options?.apiKey ||
138
+ options?.model ||
139
+ options?.baseUrl ||
140
+ options?.provider ||
141
+ options?.apiVersion ||
142
+ options?.reasoningModel !== undefined);
128
143
  let llmConfig = await resolveLLMConfig({
129
144
  model: options?.model,
130
145
  baseUrl: options?.baseUrl,
131
146
  apiKey: options?.apiKey,
132
147
  provider: options?.provider,
148
+ apiVersion: options?.apiVersion,
149
+ isReasoningModel: options?.reasoningModel,
133
150
  });
134
151
  // Run interactive setup if no saved config and no CLI flags provided
135
152
  // (even if env vars exist — let user explicitly choose their provider)
@@ -146,25 +163,26 @@ export const wikiCommand = async (inputPath, options) => {
146
163
  // Non-interactive with env var or cursor — just use it
147
164
  }
148
165
  else {
149
- console.log(' No LLM configured. Let\'s set it up.\n');
150
- console.log(' Supports OpenAI, OpenRouter, any OpenAI-compatible API, or Cursor CLI.\n');
166
+ console.log(" No LLM configured. Let's set it up.\n");
167
+ console.log(' Supports OpenAI, OpenRouter, Azure, any OpenAI-compatible API, or Cursor CLI.\n');
151
168
  // Check if Cursor CLI is available
152
169
  const hasCursor = detectCursorCLI();
153
170
  // Provider selection
154
171
  console.log(' [1] OpenAI (api.openai.com)');
155
172
  console.log(' [2] OpenRouter (openrouter.ai)');
156
- console.log(' [3] Custom endpoint');
173
+ console.log(' [3] Azure OpenAI');
174
+ console.log(' [4] Custom endpoint');
157
175
  if (hasCursor) {
158
- console.log(' [4] Cursor CLI (local, uses your Cursor subscription)');
176
+ console.log(' [5] Cursor CLI (local, uses your Cursor subscription)');
159
177
  }
160
178
  console.log('');
161
- const maxChoice = hasCursor ? '4' : '3';
179
+ const maxChoice = hasCursor ? '5' : '4';
162
180
  const choice = await prompt(` Select provider (1/${maxChoice}): `);
163
181
  let baseUrl;
164
182
  let defaultModel;
165
183
  let provider = 'openai';
166
184
  let key = '';
167
- if (choice === '4' && hasCursor) {
185
+ if (choice === '5' && hasCursor) {
168
186
  // Cursor CLI selected - model defaults to 'auto' (Cursor's default)
169
187
  provider = 'cursor';
170
188
  baseUrl = '';
@@ -178,13 +196,98 @@ export const wikiCommand = async (inputPath, options) => {
178
196
  console.log(' Config saved to ~/.gitnexus/config.json\n');
179
197
  llmConfig = { ...llmConfig, provider: 'cursor', model, apiKey: '', baseUrl: '' };
180
198
  }
199
+ else if (choice === '3') {
200
+ // Azure OpenAI guided setup
201
+ console.log('\n Azure OpenAI setup.');
202
+ console.log(' You need: your resource name, deployment name, and API key from the Azure portal.\n');
203
+ const resourceName = (await prompt(' Azure resource name (e.g. my-openai-resource): ')).trim();
204
+ if (!resourceName) {
205
+ console.log('\n No resource name provided. Aborting.\n');
206
+ process.exitCode = 1;
207
+ return;
208
+ }
209
+ const deploymentName = (await prompt(' Deployment name (the name you gave your model deployment): ')).trim();
210
+ if (!deploymentName) {
211
+ console.log('\n No deployment name provided. Aborting.\n');
212
+ process.exitCode = 1;
213
+ return;
214
+ }
215
+ // Offer v1 or legacy URL
216
+ console.log('\n API format:');
217
+ console.log(' [1] v1 API — recommended (no api-version needed)');
218
+ console.log(' [2] Legacy — uses api-version query param\n');
219
+ const apiFormat = await prompt(' Select format (1/2, default: 1): ');
220
+ let azureApiVersion;
221
+ let azureBaseUrl;
222
+ if (apiFormat === '2') {
223
+ const versionInput = await prompt(' api-version (default: 2024-10-21): ');
224
+ azureApiVersion = versionInput || '2024-10-21';
225
+ azureBaseUrl = `https://${resourceName}.openai.azure.com/openai/deployments/${deploymentName}`;
226
+ }
227
+ else {
228
+ azureBaseUrl = `https://${resourceName}.openai.azure.com/openai/v1`;
229
+ azureApiVersion = undefined;
230
+ }
231
+ defaultModel = deploymentName;
232
+ // Ask if this is a reasoning model deployment
233
+ const reasoningAnswer = await prompt(' Is this a reasoning model (o1, o3, o4-mini)? (y/N): ');
234
+ const isReasoningModelDeployment = ['y', 'yes'].includes(reasoningAnswer.toLowerCase());
235
+ if (isReasoningModelDeployment) {
236
+ console.log(' Note: temperature and max_tokens will be omitted for this deployment (Azure reasoning model requirement).\n');
237
+ }
238
+ const modelInput = await prompt(` Model / deployment name (default: ${defaultModel}): `);
239
+ const model = modelInput || defaultModel;
240
+ // API key
241
+ const envKey = process.env.GITNEXUS_API_KEY || process.env.OPENAI_API_KEY || '';
242
+ let azureKey;
243
+ if (envKey) {
244
+ const masked = envKey.slice(0, 6) + '...' + envKey.slice(-4);
245
+ const useEnv = await prompt(` Use existing env key (${masked})? (Y/n): `);
246
+ if (!useEnv || useEnv.toLowerCase() === 'y' || useEnv.toLowerCase() === 'yes') {
247
+ azureKey = envKey;
248
+ }
249
+ else {
250
+ azureKey = await prompt(' API key: ', true);
251
+ }
252
+ }
253
+ else {
254
+ azureKey = await prompt(' API key: ', true);
255
+ }
256
+ if (!azureKey) {
257
+ console.log('\n No key provided. Aborting.\n');
258
+ process.exitCode = 1;
259
+ return;
260
+ }
261
+ // Save Azure config including optional apiVersion and isReasoningModel
262
+ const azureConfig = {
263
+ apiKey: azureKey,
264
+ baseUrl: azureBaseUrl,
265
+ model,
266
+ provider: 'azure',
267
+ isReasoningModel: isReasoningModelDeployment,
268
+ };
269
+ if (azureApiVersion)
270
+ azureConfig.apiVersion = azureApiVersion;
271
+ await saveCLIConfig(azureConfig);
272
+ console.log(' Config saved to ~/.gitnexus/config.json\n');
273
+ llmConfig = {
274
+ ...llmConfig,
275
+ apiKey: azureKey,
276
+ baseUrl: azureBaseUrl,
277
+ model,
278
+ provider: 'azure',
279
+ apiVersion: azureApiVersion,
280
+ isReasoningModel: isReasoningModelDeployment,
281
+ };
282
+ }
181
283
  else {
182
- // OpenAI-compatible provider
284
+ // OpenAI-compatible provider (OpenAI, OpenRouter, Custom)
183
285
  if (choice === '2') {
184
286
  baseUrl = 'https://openrouter.ai/api/v1';
185
287
  defaultModel = 'minimax/minimax-m2.5';
288
+ provider = 'openrouter';
186
289
  }
187
- else if (choice === '3') {
290
+ else if (choice === '4') {
188
291
  baseUrl = await prompt(' Base URL (e.g. http://localhost:11434/v1): ');
189
292
  if (!baseUrl) {
190
293
  console.log('\n No URL provided. Aborting.\n');
@@ -192,10 +295,12 @@ export const wikiCommand = async (inputPath, options) => {
192
295
  return;
193
296
  }
194
297
  defaultModel = 'gpt-4o-mini';
298
+ provider = 'custom';
195
299
  }
196
300
  else {
197
301
  baseUrl = 'https://api.openai.com/v1';
198
302
  defaultModel = 'gpt-4o-mini';
303
+ provider = 'openai';
199
304
  }
200
305
  // Model
201
306
  const modelInput = await prompt(` Model (default: ${defaultModel}): `);
@@ -221,9 +326,9 @@ export const wikiCommand = async (inputPath, options) => {
221
326
  return;
222
327
  }
223
328
  // Save
224
- await saveCLIConfig({ apiKey: key, baseUrl, model, provider: 'openai' });
329
+ await saveCLIConfig({ apiKey: key, baseUrl, model, provider });
225
330
  console.log(' Config saved to ~/.gitnexus/config.json\n');
226
- llmConfig = { ...llmConfig, apiKey: key, baseUrl, model, provider: 'openai' };
331
+ llmConfig = { ...llmConfig, apiKey: key, baseUrl, model, provider };
227
332
  }
228
333
  }
229
334
  }
@@ -282,7 +387,11 @@ export const wikiCommand = async (inputPath, options) => {
282
387
  const prefix = ' '.repeat(indent + 2);
283
388
  const fileCount = node.files?.length || 0;
284
389
  const childCount = node.children?.length || 0;
285
- const suffix = fileCount > 0 ? ` (${fileCount} files)` : childCount > 0 ? ` (${childCount} children)` : '';
390
+ const suffix = fileCount > 0
391
+ ? ` (${fileCount} files)`
392
+ : childCount > 0
393
+ ? ` (${childCount} children)`
394
+ : '';
286
395
  console.log(`${prefix}- ${node.name}${suffix}`);
287
396
  if (node.children && node.children.length > 0) {
288
397
  printTree(node.children, indent + 1);
@@ -310,7 +419,7 @@ export const wikiCommand = async (inputPath, options) => {
310
419
  console.log(`\n Opening ${treeFile} in ${editor}...`);
311
420
  console.log(' Save and close the editor when done.\n');
312
421
  try {
313
- execSync(`${editor} "${treeFile}"`, { stdio: 'inherit' });
422
+ execFileSync(editor, [treeFile], { stdio: 'inherit' });
314
423
  }
315
424
  catch {
316
425
  console.log(` Could not open editor. Please edit manually:\n ${treeFile}\n`);
@@ -381,16 +490,26 @@ export const wikiCommand = async (inputPath, options) => {
381
490
  if (err.message?.includes('No source files')) {
382
491
  console.log(`\n ${err.message}\n`);
383
492
  }
493
+ else if (err.message?.includes('content filter')) {
494
+ // Content filter block — actionable message
495
+ console.log(`\n Content Filter: ${err.message}\n`);
496
+ console.log(' To resolve: rephrase your prompt or adjust the content filter policy for your deployment.\n');
497
+ }
384
498
  else if (err.message?.includes('API key') || err.message?.includes('API error')) {
385
499
  console.log(`\n LLM Error: ${err.message}\n`);
386
500
  // Offer to reconfigure on auth-related failures
387
- const isAuthError = err.message?.includes('401') || err.message?.includes('403')
388
- || err.message?.includes('502') || err.message?.includes('authenticate')
389
- || err.message?.includes('Unauthorized');
501
+ const isAuthError = err.message?.includes('401') ||
502
+ err.message?.includes('403') ||
503
+ err.message?.includes('502') ||
504
+ err.message?.includes('authenticate') ||
505
+ err.message?.includes('Unauthorized');
390
506
  if (isAuthError && process.stdin.isTTY) {
391
507
  const answer = await new Promise((resolve) => {
392
508
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
393
- rl.question(' Reconfigure LLM settings? (Y/n): ', (ans) => { rl.close(); resolve(ans.trim().toLowerCase()); });
509
+ rl.question(' Reconfigure LLM settings? (Y/n): ', (ans) => {
510
+ rl.close();
511
+ resolve(ans.trim().toLowerCase());
512
+ });
394
513
  });
395
514
  if (!answer || answer === 'y' || answer === 'yes') {
396
515
  // Clear saved config so next run triggers interactive setup
@@ -420,14 +539,10 @@ function hasGhCLI() {
420
539
  }
421
540
  function publishGist(htmlPath) {
422
541
  try {
423
- const output = execFileSync('gh', [
424
- 'gist', 'create', htmlPath,
425
- '--desc', 'Repository Wiki — generated by GitNexus',
426
- '--public',
427
- ], { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
542
+ const output = execFileSync('gh', ['gist', 'create', htmlPath, '--desc', 'Repository Wiki — generated by GitNexus', '--public'], { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
428
543
  // gh gist create prints the gist URL as the last line
429
544
  const lines = output.split('\n');
430
- const gistUrl = lines.find(l => l.includes('gist.github.com')) || lines[lines.length - 1];
545
+ const gistUrl = lines.find((l) => l.includes('gist.github.com')) || lines[lines.length - 1];
431
546
  if (!gistUrl || !gistUrl.includes('gist.github.com'))
432
547
  return null;
433
548
  // Build a raw viewer URL via gist.githack.com
@@ -92,41 +92,122 @@ const DEFAULT_IGNORE_LIST = new Set([
92
92
  ]);
93
93
  const IGNORED_EXTENSIONS = new Set([
94
94
  // Images
95
- '.png', '.jpg', '.jpeg', '.gif', '.svg', '.ico', '.webp', '.bmp', '.tiff', '.tif',
96
- '.psd', '.ai', '.sketch', '.fig', '.xd',
95
+ '.png',
96
+ '.jpg',
97
+ '.jpeg',
98
+ '.gif',
99
+ '.svg',
100
+ '.ico',
101
+ '.webp',
102
+ '.bmp',
103
+ '.tiff',
104
+ '.tif',
105
+ '.psd',
106
+ '.ai',
107
+ '.sketch',
108
+ '.fig',
109
+ '.xd',
97
110
  // Archives
98
- '.zip', '.tar', '.gz', '.rar', '.7z', '.bz2', '.xz', '.tgz',
111
+ '.zip',
112
+ '.tar',
113
+ '.gz',
114
+ '.rar',
115
+ '.7z',
116
+ '.bz2',
117
+ '.xz',
118
+ '.tgz',
99
119
  // Binary/Compiled
100
- '.exe', '.dll', '.so', '.dylib', '.a', '.lib', '.o', '.obj',
101
- '.class', '.jar', '.war', '.ear',
102
- '.pyc', '.pyo', '.pyd',
120
+ '.exe',
121
+ '.dll',
122
+ '.so',
123
+ '.dylib',
124
+ '.a',
125
+ '.lib',
126
+ '.o',
127
+ '.obj',
128
+ '.class',
129
+ '.jar',
130
+ '.war',
131
+ '.ear',
132
+ '.pyc',
133
+ '.pyo',
134
+ '.pyd',
103
135
  '.beam', // Erlang
104
136
  '.wasm', // WebAssembly - important!
105
137
  '.node', // Native Node addons
106
138
  // Documents
107
- '.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx',
108
- '.odt', '.ods', '.odp',
139
+ '.pdf',
140
+ '.doc',
141
+ '.docx',
142
+ '.xls',
143
+ '.xlsx',
144
+ '.ppt',
145
+ '.pptx',
146
+ '.odt',
147
+ '.ods',
148
+ '.odp',
109
149
  // Media
110
- '.mp4', '.mp3', '.wav', '.mov', '.avi', '.mkv', '.flv', '.wmv',
111
- '.ogg', '.webm', '.flac', '.aac', '.m4a',
150
+ '.mp4',
151
+ '.mp3',
152
+ '.wav',
153
+ '.mov',
154
+ '.avi',
155
+ '.mkv',
156
+ '.flv',
157
+ '.wmv',
158
+ '.ogg',
159
+ '.webm',
160
+ '.flac',
161
+ '.aac',
162
+ '.m4a',
112
163
  // Fonts
113
- '.woff', '.woff2', '.ttf', '.eot', '.otf',
164
+ '.woff',
165
+ '.woff2',
166
+ '.ttf',
167
+ '.eot',
168
+ '.otf',
114
169
  // Databases
115
- '.db', '.sqlite', '.sqlite3', '.mdb', '.accdb',
170
+ '.db',
171
+ '.sqlite',
172
+ '.sqlite3',
173
+ '.mdb',
174
+ '.accdb',
116
175
  // Minified/Bundled files
117
- '.min.js', '.min.css', '.bundle.js', '.chunk.js',
176
+ '.min.js',
177
+ '.min.css',
178
+ '.bundle.js',
179
+ '.chunk.js',
118
180
  // Source maps (debug files, not source)
119
181
  '.map',
120
182
  // Lock files (handled separately, but also here)
121
183
  '.lock',
122
184
  // Certificates & Keys (security - don't index!)
123
- '.pem', '.key', '.crt', '.cer', '.p12', '.pfx',
185
+ '.pem',
186
+ '.key',
187
+ '.crt',
188
+ '.cer',
189
+ '.p12',
190
+ '.pfx',
124
191
  // Data files (often large/binary)
125
- '.csv', '.tsv', '.parquet', '.avro', '.feather',
126
- '.npy', '.npz', '.pkl', '.pickle', '.h5', '.hdf5',
192
+ '.csv',
193
+ '.tsv',
194
+ '.parquet',
195
+ '.avro',
196
+ '.feather',
197
+ '.npy',
198
+ '.npz',
199
+ '.pkl',
200
+ '.pickle',
201
+ '.h5',
202
+ '.hdf5',
127
203
  // Misc binary
128
- '.bin', '.dat', '.data', '.raw',
129
- '.iso', '.img', '.dmg',
204
+ '.bin',
205
+ '.dat',
206
+ '.data',
207
+ '.raw',
208
+ '.iso',
209
+ '.img',
210
+ '.dmg',
130
211
  ]);
131
212
  // Files to ignore by exact name
132
213
  const IGNORED_FILES = new Set([
@@ -208,7 +289,8 @@ export const shouldIgnorePath = (filePath) => {
208
289
  if (fileNameLower.includes('.bundle.') ||
209
290
  fileNameLower.includes('.chunk.') ||
210
291
  fileNameLower.includes('.generated.') ||
211
- fileNameLower.endsWith('.d.ts')) { // TypeScript declaration files
292
+ fileNameLower.endsWith('.d.ts')) {
293
+ // TypeScript declaration files
212
294
  return true;
213
295
  }
214
296
  return false;
@@ -222,9 +304,7 @@ export const loadIgnoreRules = async (repoPath, options) => {
222
304
  let hasRules = false;
223
305
  // Allow users to bypass .gitignore parsing (e.g. when .gitignore accidentally excludes source files)
224
306
  const skipGitignore = options?.noGitignore ?? !!process.env.GITNEXUS_NO_GITIGNORE;
225
- const filenames = skipGitignore
226
- ? ['.gitnexusignore']
227
- : ['.gitignore', '.gitnexusignore'];
307
+ const filenames = skipGitignore ? ['.gitnexusignore'] : ['.gitignore', '.gitnexusignore'];
228
308
  for (const filename of filenames) {
229
309
  try {
230
310
  const content = await fs.readFile(nodePath.join(repoPath, filename), 'utf-8');