gitnexus 1.4.7 → 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 (242) hide show
  1. package/README.md +29 -1
  2. package/dist/cli/ai-context.d.ts +1 -1
  3. package/dist/cli/ai-context.js +1 -1
  4. package/dist/cli/analyze.d.ts +2 -0
  5. package/dist/cli/analyze.js +54 -21
  6. package/dist/cli/index-repo.d.ts +15 -0
  7. package/dist/cli/index-repo.js +115 -0
  8. package/dist/cli/index.js +13 -3
  9. package/dist/cli/setup.js +90 -10
  10. package/dist/cli/wiki.d.ts +4 -0
  11. package/dist/cli/wiki.js +174 -53
  12. package/dist/config/supported-languages.d.ts +33 -1
  13. package/dist/config/supported-languages.js +32 -0
  14. package/dist/core/embeddings/embedder.d.ts +6 -1
  15. package/dist/core/embeddings/embedder.js +65 -5
  16. package/dist/core/embeddings/embedding-pipeline.js +11 -9
  17. package/dist/core/embeddings/http-client.d.ts +31 -0
  18. package/dist/core/embeddings/http-client.js +179 -0
  19. package/dist/core/embeddings/index.d.ts +1 -0
  20. package/dist/core/embeddings/index.js +1 -0
  21. package/dist/core/embeddings/types.d.ts +1 -1
  22. package/dist/core/graph/graph.js +9 -1
  23. package/dist/core/graph/types.d.ts +11 -2
  24. package/dist/core/ingestion/call-processor.d.ts +66 -2
  25. package/dist/core/ingestion/call-processor.js +650 -30
  26. package/dist/core/ingestion/call-routing.d.ts +9 -18
  27. package/dist/core/ingestion/call-routing.js +0 -19
  28. package/dist/core/ingestion/cobol/cobol-copy-expander.d.ts +57 -0
  29. package/dist/core/ingestion/cobol/cobol-copy-expander.js +385 -0
  30. package/dist/core/ingestion/cobol/cobol-preprocessor.d.ts +210 -0
  31. package/dist/core/ingestion/cobol/cobol-preprocessor.js +1509 -0
  32. package/dist/core/ingestion/cobol/jcl-parser.d.ts +68 -0
  33. package/dist/core/ingestion/cobol/jcl-parser.js +217 -0
  34. package/dist/core/ingestion/cobol/jcl-processor.d.ts +33 -0
  35. package/dist/core/ingestion/cobol/jcl-processor.js +229 -0
  36. package/dist/core/ingestion/cobol-processor.d.ts +54 -0
  37. package/dist/core/ingestion/cobol-processor.js +1186 -0
  38. package/dist/core/ingestion/entry-point-scoring.d.ts +17 -0
  39. package/dist/core/ingestion/entry-point-scoring.js +52 -28
  40. package/dist/core/ingestion/export-detection.d.ts +47 -8
  41. package/dist/core/ingestion/export-detection.js +29 -50
  42. package/dist/core/ingestion/field-extractor.d.ts +29 -0
  43. package/dist/core/ingestion/field-extractor.js +25 -0
  44. package/dist/core/ingestion/field-extractors/configs/c-cpp.d.ts +3 -0
  45. package/dist/core/ingestion/field-extractors/configs/c-cpp.js +108 -0
  46. package/dist/core/ingestion/field-extractors/configs/csharp.d.ts +8 -0
  47. package/dist/core/ingestion/field-extractors/configs/csharp.js +73 -0
  48. package/dist/core/ingestion/field-extractors/configs/dart.d.ts +8 -0
  49. package/dist/core/ingestion/field-extractors/configs/dart.js +76 -0
  50. package/dist/core/ingestion/field-extractors/configs/go.d.ts +11 -0
  51. package/dist/core/ingestion/field-extractors/configs/go.js +64 -0
  52. package/dist/core/ingestion/field-extractors/configs/helpers.d.ts +44 -0
  53. package/dist/core/ingestion/field-extractors/configs/helpers.js +134 -0
  54. package/dist/core/ingestion/field-extractors/configs/jvm.d.ts +3 -0
  55. package/dist/core/ingestion/field-extractors/configs/jvm.js +118 -0
  56. package/dist/core/ingestion/field-extractors/configs/php.d.ts +8 -0
  57. package/dist/core/ingestion/field-extractors/configs/php.js +67 -0
  58. package/dist/core/ingestion/field-extractors/configs/python.d.ts +12 -0
  59. package/dist/core/ingestion/field-extractors/configs/python.js +91 -0
  60. package/dist/core/ingestion/field-extractors/configs/ruby.d.ts +16 -0
  61. package/dist/core/ingestion/field-extractors/configs/ruby.js +75 -0
  62. package/dist/core/ingestion/field-extractors/configs/rust.d.ts +9 -0
  63. package/dist/core/ingestion/field-extractors/configs/rust.js +55 -0
  64. package/dist/core/ingestion/field-extractors/configs/swift.d.ts +8 -0
  65. package/dist/core/ingestion/field-extractors/configs/swift.js +63 -0
  66. package/dist/core/ingestion/field-extractors/configs/typescript-javascript.d.ts +3 -0
  67. package/dist/core/ingestion/field-extractors/configs/typescript-javascript.js +60 -0
  68. package/dist/core/ingestion/field-extractors/generic.d.ts +46 -0
  69. package/dist/core/ingestion/field-extractors/generic.js +111 -0
  70. package/dist/core/ingestion/field-extractors/typescript.d.ts +77 -0
  71. package/dist/core/ingestion/field-extractors/typescript.js +291 -0
  72. package/dist/core/ingestion/field-types.d.ts +59 -0
  73. package/dist/core/ingestion/field-types.js +2 -0
  74. package/dist/core/ingestion/framework-detection.d.ts +97 -2
  75. package/dist/core/ingestion/framework-detection.js +114 -14
  76. package/dist/core/ingestion/heritage-processor.js +62 -66
  77. package/dist/core/ingestion/import-processor.d.ts +9 -10
  78. package/dist/core/ingestion/import-processor.js +150 -196
  79. package/dist/core/ingestion/{resolvers → import-resolvers}/csharp.d.ts +6 -9
  80. package/dist/core/ingestion/{resolvers → import-resolvers}/csharp.js +20 -2
  81. package/dist/core/ingestion/import-resolvers/dart.d.ts +7 -0
  82. package/dist/core/ingestion/import-resolvers/dart.js +44 -0
  83. package/dist/core/ingestion/{resolvers → import-resolvers}/go.d.ts +4 -5
  84. package/dist/core/ingestion/{resolvers → import-resolvers}/go.js +17 -0
  85. package/dist/core/ingestion/{resolvers → import-resolvers}/jvm.d.ts +10 -1
  86. package/dist/core/ingestion/import-resolvers/jvm.js +159 -0
  87. package/dist/core/ingestion/import-resolvers/php.d.ts +25 -0
  88. package/dist/core/ingestion/import-resolvers/php.js +80 -0
  89. package/dist/core/ingestion/{resolvers → import-resolvers}/python.d.ts +9 -3
  90. package/dist/core/ingestion/{resolvers → import-resolvers}/python.js +35 -3
  91. package/dist/core/ingestion/{resolvers → import-resolvers}/ruby.d.ts +5 -2
  92. package/dist/core/ingestion/{resolvers → import-resolvers}/ruby.js +7 -2
  93. package/dist/core/ingestion/{resolvers → import-resolvers}/rust.d.ts +5 -2
  94. package/dist/core/ingestion/{resolvers → import-resolvers}/rust.js +41 -2
  95. package/dist/core/ingestion/{resolvers → import-resolvers}/standard.d.ts +15 -7
  96. package/dist/core/ingestion/{resolvers → import-resolvers}/standard.js +22 -3
  97. package/dist/core/ingestion/import-resolvers/swift.d.ts +7 -0
  98. package/dist/core/ingestion/import-resolvers/swift.js +23 -0
  99. package/dist/core/ingestion/import-resolvers/types.d.ts +44 -0
  100. package/dist/core/ingestion/import-resolvers/types.js +6 -0
  101. package/dist/core/ingestion/{resolvers → import-resolvers}/utils.d.ts +2 -0
  102. package/dist/core/ingestion/{resolvers → import-resolvers}/utils.js +7 -0
  103. package/dist/core/ingestion/language-config.d.ts +6 -0
  104. package/dist/core/ingestion/language-config.js +13 -0
  105. package/dist/core/ingestion/language-provider.d.ts +121 -0
  106. package/dist/core/ingestion/language-provider.js +24 -0
  107. package/dist/core/ingestion/languages/c-cpp.d.ts +12 -0
  108. package/dist/core/ingestion/languages/c-cpp.js +71 -0
  109. package/dist/core/ingestion/languages/cobol.d.ts +1 -0
  110. package/dist/core/ingestion/languages/cobol.js +26 -0
  111. package/dist/core/ingestion/languages/csharp.d.ts +8 -0
  112. package/dist/core/ingestion/languages/csharp.js +49 -0
  113. package/dist/core/ingestion/languages/dart.d.ts +12 -0
  114. package/dist/core/ingestion/languages/dart.js +58 -0
  115. package/dist/core/ingestion/languages/go.d.ts +11 -0
  116. package/dist/core/ingestion/languages/go.js +28 -0
  117. package/dist/core/ingestion/languages/index.d.ts +38 -0
  118. package/dist/core/ingestion/languages/index.js +63 -0
  119. package/dist/core/ingestion/languages/java.d.ts +9 -0
  120. package/dist/core/ingestion/languages/java.js +29 -0
  121. package/dist/core/ingestion/languages/kotlin.d.ts +9 -0
  122. package/dist/core/ingestion/languages/kotlin.js +53 -0
  123. package/dist/core/ingestion/languages/php.d.ts +8 -0
  124. package/dist/core/ingestion/languages/php.js +145 -0
  125. package/dist/core/ingestion/languages/python.d.ts +12 -0
  126. package/dist/core/ingestion/languages/python.js +39 -0
  127. package/dist/core/ingestion/languages/ruby.d.ts +9 -0
  128. package/dist/core/ingestion/languages/ruby.js +44 -0
  129. package/dist/core/ingestion/languages/rust.d.ts +12 -0
  130. package/dist/core/ingestion/languages/rust.js +44 -0
  131. package/dist/core/ingestion/languages/swift.d.ts +12 -0
  132. package/dist/core/ingestion/languages/swift.js +133 -0
  133. package/dist/core/ingestion/languages/typescript.d.ts +10 -0
  134. package/dist/core/ingestion/languages/typescript.js +60 -0
  135. package/dist/core/ingestion/markdown-processor.d.ts +17 -0
  136. package/dist/core/ingestion/markdown-processor.js +124 -0
  137. package/dist/core/ingestion/mro-processor.js +22 -18
  138. package/dist/core/ingestion/named-binding-processor.d.ts +18 -0
  139. package/dist/core/ingestion/named-binding-processor.js +42 -0
  140. package/dist/core/ingestion/named-bindings/csharp.d.ts +3 -0
  141. package/dist/core/ingestion/named-bindings/csharp.js +37 -0
  142. package/dist/core/ingestion/named-bindings/java.d.ts +3 -0
  143. package/dist/core/ingestion/named-bindings/java.js +29 -0
  144. package/dist/core/ingestion/named-bindings/kotlin.d.ts +3 -0
  145. package/dist/core/ingestion/named-bindings/kotlin.js +36 -0
  146. package/dist/core/ingestion/named-bindings/php.d.ts +3 -0
  147. package/dist/core/ingestion/named-bindings/php.js +61 -0
  148. package/dist/core/ingestion/named-bindings/python.d.ts +3 -0
  149. package/dist/core/ingestion/named-bindings/python.js +49 -0
  150. package/dist/core/ingestion/named-bindings/rust.d.ts +3 -0
  151. package/dist/core/ingestion/named-bindings/rust.js +64 -0
  152. package/dist/core/ingestion/named-bindings/types.d.ts +16 -0
  153. package/dist/core/ingestion/named-bindings/types.js +6 -0
  154. package/dist/core/ingestion/named-bindings/typescript.d.ts +3 -0
  155. package/dist/core/ingestion/named-bindings/typescript.js +58 -0
  156. package/dist/core/ingestion/parsing-processor.d.ts +6 -2
  157. package/dist/core/ingestion/parsing-processor.js +125 -85
  158. package/dist/core/ingestion/pipeline.d.ts +10 -0
  159. package/dist/core/ingestion/pipeline.js +1235 -317
  160. package/dist/core/ingestion/resolution-context.d.ts +5 -0
  161. package/dist/core/ingestion/resolution-context.js +8 -5
  162. package/dist/core/ingestion/route-extractors/expo.d.ts +1 -0
  163. package/dist/core/ingestion/route-extractors/expo.js +36 -0
  164. package/dist/core/ingestion/route-extractors/middleware.d.ts +47 -0
  165. package/dist/core/ingestion/route-extractors/middleware.js +143 -0
  166. package/dist/core/ingestion/route-extractors/nextjs.d.ts +3 -0
  167. package/dist/core/ingestion/route-extractors/nextjs.js +76 -0
  168. package/dist/core/ingestion/route-extractors/php.d.ts +7 -0
  169. package/dist/core/ingestion/route-extractors/php.js +21 -0
  170. package/dist/core/ingestion/route-extractors/response-shapes.d.ts +20 -0
  171. package/dist/core/ingestion/route-extractors/response-shapes.js +290 -0
  172. package/dist/core/ingestion/symbol-table.d.ts +16 -0
  173. package/dist/core/ingestion/symbol-table.js +20 -6
  174. package/dist/core/ingestion/tree-sitter-queries.d.ts +10 -9
  175. package/dist/core/ingestion/tree-sitter-queries.js +274 -11
  176. package/dist/core/ingestion/type-env.d.ts +42 -18
  177. package/dist/core/ingestion/type-env.js +481 -106
  178. package/dist/core/ingestion/type-extractors/c-cpp.d.ts +5 -0
  179. package/dist/core/ingestion/type-extractors/c-cpp.js +119 -0
  180. package/dist/core/ingestion/type-extractors/csharp.js +149 -16
  181. package/dist/core/ingestion/type-extractors/dart.d.ts +15 -0
  182. package/dist/core/ingestion/type-extractors/dart.js +371 -0
  183. package/dist/core/ingestion/type-extractors/jvm.js +169 -66
  184. package/dist/core/ingestion/type-extractors/rust.js +35 -1
  185. package/dist/core/ingestion/type-extractors/shared.d.ts +1 -15
  186. package/dist/core/ingestion/type-extractors/shared.js +14 -112
  187. package/dist/core/ingestion/type-extractors/swift.js +338 -7
  188. package/dist/core/ingestion/type-extractors/types.d.ts +40 -8
  189. package/dist/core/ingestion/type-extractors/typescript.js +141 -9
  190. package/dist/core/ingestion/utils/ast-helpers.d.ts +83 -0
  191. package/dist/core/ingestion/utils/ast-helpers.js +817 -0
  192. package/dist/core/ingestion/utils/call-analysis.d.ts +73 -0
  193. package/dist/core/ingestion/utils/call-analysis.js +527 -0
  194. package/dist/core/ingestion/utils/event-loop.d.ts +5 -0
  195. package/dist/core/ingestion/utils/event-loop.js +5 -0
  196. package/dist/core/ingestion/utils/language-detection.d.ts +9 -0
  197. package/dist/core/ingestion/utils/language-detection.js +70 -0
  198. package/dist/core/ingestion/utils/verbose.d.ts +1 -0
  199. package/dist/core/ingestion/utils/verbose.js +7 -0
  200. package/dist/core/ingestion/workers/parse-worker.d.ts +55 -5
  201. package/dist/core/ingestion/workers/parse-worker.js +415 -225
  202. package/dist/core/lbug/csv-generator.js +51 -1
  203. package/dist/core/lbug/lbug-adapter.d.ts +10 -0
  204. package/dist/core/lbug/lbug-adapter.js +75 -4
  205. package/dist/core/lbug/schema.d.ts +8 -4
  206. package/dist/core/lbug/schema.js +65 -4
  207. package/dist/core/tree-sitter/parser-loader.js +7 -1
  208. package/dist/core/wiki/cursor-client.d.ts +31 -0
  209. package/dist/core/wiki/cursor-client.js +127 -0
  210. package/dist/core/wiki/generator.d.ts +28 -9
  211. package/dist/core/wiki/generator.js +115 -18
  212. package/dist/core/wiki/graph-queries.d.ts +4 -0
  213. package/dist/core/wiki/graph-queries.js +7 -1
  214. package/dist/core/wiki/llm-client.d.ts +2 -0
  215. package/dist/core/wiki/llm-client.js +8 -4
  216. package/dist/core/wiki/prompts.d.ts +3 -3
  217. package/dist/core/wiki/prompts.js +6 -0
  218. package/dist/mcp/core/embedder.js +11 -3
  219. package/dist/mcp/core/lbug-adapter.d.ts +5 -0
  220. package/dist/mcp/core/lbug-adapter.js +23 -2
  221. package/dist/mcp/local/local-backend.d.ts +38 -5
  222. package/dist/mcp/local/local-backend.js +804 -63
  223. package/dist/mcp/resources.js +2 -0
  224. package/dist/mcp/tools.js +73 -4
  225. package/dist/server/api.d.ts +19 -1
  226. package/dist/server/api.js +66 -6
  227. package/dist/storage/git.d.ts +12 -0
  228. package/dist/storage/git.js +21 -0
  229. package/dist/storage/repo-manager.d.ts +3 -0
  230. package/package.json +25 -16
  231. package/dist/core/ingestion/named-binding-extraction.d.ts +0 -61
  232. package/dist/core/ingestion/named-binding-extraction.js +0 -363
  233. package/dist/core/ingestion/resolvers/index.d.ts +0 -18
  234. package/dist/core/ingestion/resolvers/index.js +0 -13
  235. package/dist/core/ingestion/resolvers/jvm.js +0 -87
  236. package/dist/core/ingestion/resolvers/php.d.ts +0 -15
  237. package/dist/core/ingestion/resolvers/php.js +0 -35
  238. package/dist/core/ingestion/type-extractors/index.d.ts +0 -22
  239. package/dist/core/ingestion/type-extractors/index.js +0 -31
  240. package/dist/core/ingestion/utils.d.ts +0 -138
  241. package/dist/core/ingestion/utils.js +0 -1290
  242. package/scripts/patch-tree-sitter-swift.cjs +0 -74
@@ -12,11 +12,10 @@
12
12
  import { type LLMConfig } from './llm-client.js';
13
13
  export interface WikiOptions {
14
14
  force?: boolean;
15
- model?: string;
16
- baseUrl?: string;
17
- apiKey?: string;
18
15
  maxTokensPerModule?: number;
19
16
  concurrency?: number;
17
+ /** If true, stop after building module tree for user review */
18
+ reviewOnly?: boolean;
20
19
  }
21
20
  export interface WikiMeta {
22
21
  fromCommit: string;
@@ -32,6 +31,12 @@ export interface ModuleTreeNode {
32
31
  children?: ModuleTreeNode[];
33
32
  }
34
33
  export type ProgressCallback = (phase: string, percent: number, detail?: string) => void;
34
+ export interface WikiRunResult {
35
+ pagesGenerated: number;
36
+ mode: 'full' | 'incremental' | 'up-to-date';
37
+ failedModules: string[];
38
+ moduleTree?: ModuleTreeNode[];
39
+ }
35
40
  export declare class WikiGenerator {
36
41
  private repoPath;
37
42
  private storagePath;
@@ -47,17 +52,24 @@ export declare class WikiGenerator {
47
52
  private lastPercent;
48
53
  /**
49
54
  * Create streaming options that report LLM progress to the progress bar.
50
- * Uses the last known percent so streaming doesn't reset the bar backwards.
55
+ *
56
+ * Progress calculation:
57
+ * - If fixedPercent is provided, we show incremental progress within that phase
58
+ * based on token generation (e.g., grouping at 15% → 15-28%)
59
+ * - If fixedPercent is NOT provided, we only update the label with token count
60
+ * but keep the current percentage (avoids fluctuation during module generation)
61
+ *
62
+ * Also touches the DB connection periodically to prevent idle timeout.
51
63
  */
52
64
  private streamOpts;
65
+ /**
66
+ * Route LLM call to the appropriate provider (OpenAI-compatible or Cursor CLI).
67
+ */
68
+ private invokeLLM;
53
69
  /**
54
70
  * Main entry point. Runs the full pipeline or incremental update.
55
71
  */
56
- run(): Promise<{
57
- pagesGenerated: number;
58
- mode: 'full' | 'incremental' | 'up-to-date';
59
- failedModules: string[];
60
- }>;
72
+ run(): Promise<WikiRunResult>;
61
73
  private ensureHTMLViewer;
62
74
  private fullGeneration;
63
75
  private buildModuleTree;
@@ -71,6 +83,8 @@ export declare class WikiGenerator {
71
83
  private fallbackGrouping;
72
84
  /**
73
85
  * Split a large module into sub-modules by subdirectory.
86
+ * Uses the full subDir path for naming to avoid slug collisions
87
+ * (e.g., "synapse-screen/src" vs "synapse-core/src").
74
88
  */
75
89
  private splitBySubdirectory;
76
90
  /**
@@ -84,6 +98,11 @@ export declare class WikiGenerator {
84
98
  private generateOverview;
85
99
  private incrementalUpdate;
86
100
  private getCurrentCommit;
101
+ /**
102
+ * Check if fromCommit is an ancestor of toCommit (reachable in git history).
103
+ * Returns false if commits are on divergent branches or fromCommit doesn't exist.
104
+ */
105
+ private isCommitReachable;
87
106
  private getChangedFiles;
88
107
  private readSourceFiles;
89
108
  private truncateSource;
@@ -12,9 +12,10 @@
12
12
  import fs from 'fs/promises';
13
13
  import path from 'path';
14
14
  import { execSync, execFileSync } from 'child_process';
15
- import { initWikiDb, closeWikiDb, getFilesWithExports, getAllFiles, getIntraModuleCallEdges, getInterModuleCallEdges, getProcessesForFiles, getAllProcesses, getInterModuleEdgesForOverview, } from './graph-queries.js';
15
+ import { initWikiDb, closeWikiDb, touchWikiDb, getFilesWithExports, getAllFiles, getIntraModuleCallEdges, getInterModuleCallEdges, getProcessesForFiles, getAllProcesses, getInterModuleEdgesForOverview, } from './graph-queries.js';
16
16
  import { generateHTMLViewer } from './html-viewer.js';
17
17
  import { callLLM, estimateTokens, } from './llm-client.js';
18
+ import { callCursorLLM, resolveCursorConfig, } from './cursor-client.js';
18
19
  import { GROUPING_SYSTEM_PROMPT, GROUPING_USER_PROMPT, MODULE_SYSTEM_PROMPT, MODULE_USER_PROMPT, PARENT_SYSTEM_PROMPT, PARENT_USER_PROMPT, OVERVIEW_SYSTEM_PROMPT, OVERVIEW_USER_PROMPT, fillTemplate, formatFileListForGrouping, formatDirectoryTree, formatCallEdges, formatProcesses, } from './prompts.js';
19
20
  import { shouldIgnorePath } from '../../config/ignore-service.js';
20
21
  // ─── Constants ────────────────────────────────────────────────────────
@@ -51,17 +52,55 @@ export class WikiGenerator {
51
52
  lastPercent = 0;
52
53
  /**
53
54
  * Create streaming options that report LLM progress to the progress bar.
54
- * Uses the last known percent so streaming doesn't reset the bar backwards.
55
+ *
56
+ * Progress calculation:
57
+ * - If fixedPercent is provided, we show incremental progress within that phase
58
+ * based on token generation (e.g., grouping at 15% → 15-28%)
59
+ * - If fixedPercent is NOT provided, we only update the label with token count
60
+ * but keep the current percentage (avoids fluctuation during module generation)
61
+ *
62
+ * Also touches the DB connection periodically to prevent idle timeout.
55
63
  */
56
- streamOpts(label, fixedPercent) {
64
+ streamOpts(label, fixedPercent, percentRange = 10) {
65
+ const hasFixedStart = fixedPercent !== undefined;
66
+ const startPercent = fixedPercent ?? this.lastPercent;
67
+ const expectedTokens = 2000;
68
+ let lastTouch = Date.now();
57
69
  return {
58
70
  onChunk: (chars) => {
59
71
  const tokens = Math.round(chars / 4);
60
- const pct = fixedPercent ?? this.lastPercent;
61
- this.onProgress('stream', pct, `${label} (${tokens} tok)`);
72
+ if (hasFixedStart) {
73
+ // For fixed phases (like grouping), show incremental progress
74
+ const progress = Math.min(1, tokens / expectedTokens);
75
+ const pct = Math.round(startPercent + (progress * percentRange));
76
+ this.onProgress('stream', pct, `${label} (${tokens} tok)`);
77
+ }
78
+ else {
79
+ // For module generation, only update the label, keep current percent
80
+ this.onProgress('stream', this.lastPercent, `${label} (${tokens} tok)`);
81
+ }
82
+ // Touch DB every 60s to prevent idle timeout during long LLM calls
83
+ const now = Date.now();
84
+ if (now - lastTouch > 60_000) {
85
+ touchWikiDb();
86
+ lastTouch = now;
87
+ }
62
88
  },
63
89
  };
64
90
  }
91
+ /**
92
+ * Route LLM call to the appropriate provider (OpenAI-compatible or Cursor CLI).
93
+ */
94
+ async invokeLLM(prompt, systemPrompt, options) {
95
+ if (this.llmConfig.provider === 'cursor') {
96
+ const cursorConfig = resolveCursorConfig({
97
+ model: this.llmConfig.model,
98
+ workingDirectory: this.repoPath,
99
+ });
100
+ return callCursorLLM(prompt, cursorConfig, systemPrompt, options);
101
+ }
102
+ return callLLM(prompt, this.llmConfig, systemPrompt, options);
103
+ }
65
104
  /**
66
105
  * Main entry point. Runs the full pipeline or incremental update.
67
106
  */
@@ -144,6 +183,13 @@ export class WikiGenerator {
144
183
  // Phase 1: Build module tree
145
184
  const moduleTree = await this.buildModuleTree(enrichedFiles);
146
185
  pagesGenerated = 0;
186
+ // If reviewOnly mode, save tree and stop for user to review/edit
187
+ if (this.options.reviewOnly) {
188
+ await this.saveModuleTree(moduleTree);
189
+ this.onProgress('review', 30, 'Module tree ready for review');
190
+ const reviewResult = { pagesGenerated: 0, mode: 'full', failedModules: [], moduleTree };
191
+ return reviewResult;
192
+ }
147
193
  // Phase 2: Generate module pages (parallel with concurrency limit)
148
194
  const totalModules = this.countModules(moduleTree);
149
195
  let modulesProcessed = 0;
@@ -213,6 +259,19 @@ export class WikiGenerator {
213
259
  }
214
260
  // ─── Phase 1: Build Module Tree ────────────────────────────────────
215
261
  async buildModuleTree(files) {
262
+ // First, check for user-edited module_tree.json (from --review workflow)
263
+ const editablePath = path.join(this.wikiDir, 'module_tree.json');
264
+ try {
265
+ const edited = await fs.readFile(editablePath, 'utf-8');
266
+ const parsed = JSON.parse(edited);
267
+ if (Array.isArray(parsed) && parsed.length > 0) {
268
+ this.onProgress('grouping', 25, 'Using edited module tree');
269
+ return parsed;
270
+ }
271
+ }
272
+ catch {
273
+ // No edited tree, check for original snapshot
274
+ }
216
275
  // Check for existing immutable snapshot (resumability)
217
276
  const snapshotPath = path.join(this.wikiDir, 'first_module_tree.json');
218
277
  try {
@@ -233,7 +292,7 @@ export class WikiGenerator {
233
292
  FILE_LIST: fileList,
234
293
  DIRECTORY_TREE: dirTree,
235
294
  });
236
- const response = await callLLM(prompt, this.llmConfig, GROUPING_SYSTEM_PROMPT, this.streamOpts('Grouping files', 15));
295
+ const response = await this.invokeLLM(prompt, GROUPING_SYSTEM_PROMPT, this.streamOpts('Grouping files', 15, 13));
237
296
  const grouping = this.parseGroupingResponse(response.content, files);
238
297
  // Convert to tree nodes
239
298
  const tree = [];
@@ -243,8 +302,14 @@ export class WikiGenerator {
243
302
  // Token budget check — split if too large
244
303
  const totalTokens = await this.estimateModuleTokens(modulePaths);
245
304
  if (totalTokens > this.maxTokensPerModule && modulePaths.length > 3) {
246
- node.children = this.splitBySubdirectory(moduleName, modulePaths);
247
- node.files = []; // Parent doesn't own files directly when split
305
+ const children = this.splitBySubdirectory(moduleName, modulePaths);
306
+ // Only create hierarchy if we actually got multiple children
307
+ // If splitting results in 1 child, keep files flat (avoid redundant nesting)
308
+ if (children.length > 1) {
309
+ node.children = children;
310
+ node.files = []; // Parent doesn't own files directly when split
311
+ }
312
+ // If only 1 child, keep original flat structure (files stay in node.files)
248
313
  }
249
314
  tree.push(node);
250
315
  }
@@ -322,12 +387,13 @@ export class WikiGenerator {
322
387
  }
323
388
  /**
324
389
  * Split a large module into sub-modules by subdirectory.
390
+ * Uses the full subDir path for naming to avoid slug collisions
391
+ * (e.g., "synapse-screen/src" vs "synapse-core/src").
325
392
  */
326
393
  splitBySubdirectory(moduleName, files) {
327
394
  const subGroups = new Map();
328
395
  for (const fp of files) {
329
396
  const parts = fp.replace(/\\/g, '/').split('/');
330
- // Use the deepest common-ish directory
331
397
  const subDir = parts.length > 2 ? parts.slice(0, 2).join('/') : parts[0];
332
398
  let group = subGroups.get(subDir);
333
399
  if (!group) {
@@ -336,11 +402,17 @@ export class WikiGenerator {
336
402
  }
337
403
  group.push(fp);
338
404
  }
339
- return Array.from(subGroups.entries()).map(([subDir, subFiles]) => ({
340
- name: `${moduleName} ${path.basename(subDir)}`,
341
- slug: this.slugify(`${moduleName}-${path.basename(subDir)}`),
342
- files: subFiles,
343
- }));
405
+ // Check if basenames are unique; if not, use the full subDir path
406
+ const basenames = Array.from(subGroups.keys()).map(s => path.basename(s));
407
+ const hasCollisions = new Set(basenames).size < basenames.length;
408
+ return Array.from(subGroups.entries()).map(([subDir, subFiles]) => {
409
+ const label = hasCollisions ? subDir.replace(/\//g, '-') : path.basename(subDir);
410
+ return {
411
+ name: `${moduleName} — ${label}`,
412
+ slug: this.slugify(`${moduleName}-${label}`),
413
+ files: subFiles,
414
+ };
415
+ });
344
416
  }
345
417
  // ─── Phase 2: Generate Module Pages ─────────────────────────────────
346
418
  /**
@@ -370,7 +442,7 @@ export class WikiGenerator {
370
442
  INCOMING_CALLS: formatCallEdges(interCalls.incoming),
371
443
  PROCESSES: formatProcesses(processes),
372
444
  });
373
- const response = await callLLM(prompt, this.llmConfig, MODULE_SYSTEM_PROMPT, this.streamOpts(node.name));
445
+ const response = await this.invokeLLM(prompt, MODULE_SYSTEM_PROMPT, this.streamOpts(node.name));
374
446
  // Write page with front matter
375
447
  const pageContent = `# ${node.name}\n\n${response.content}`;
376
448
  await fs.writeFile(path.join(this.wikiDir, `${node.slug}.md`), pageContent, 'utf-8');
@@ -406,7 +478,7 @@ export class WikiGenerator {
406
478
  CROSS_MODULE_CALLS: formatCallEdges(crossCalls),
407
479
  CROSS_PROCESSES: formatProcesses(processes),
408
480
  });
409
- const response = await callLLM(prompt, this.llmConfig, PARENT_SYSTEM_PROMPT, this.streamOpts(node.name));
481
+ const response = await this.invokeLLM(prompt, PARENT_SYSTEM_PROMPT, this.streamOpts(node.name));
410
482
  const pageContent = `# ${node.name}\n\n${response.content}`;
411
483
  await fs.writeFile(path.join(this.wikiDir, `${node.slug}.md`), pageContent, 'utf-8');
412
484
  }
@@ -442,7 +514,7 @@ export class WikiGenerator {
442
514
  MODULE_EDGES: edgesText,
443
515
  TOP_PROCESSES: formatProcesses(topProcesses),
444
516
  });
445
- const response = await callLLM(prompt, this.llmConfig, OVERVIEW_SYSTEM_PROMPT, this.streamOpts('Generating overview', 88));
517
+ const response = await this.invokeLLM(prompt, OVERVIEW_SYSTEM_PROMPT, this.streamOpts('Generating overview', 88));
446
518
  const pageContent = `# ${path.basename(this.repoPath)} — Wiki\n\n${response.content}`;
447
519
  await fs.writeFile(path.join(this.wikiDir, 'overview.md'), pageContent, 'utf-8');
448
520
  }
@@ -451,6 +523,13 @@ export class WikiGenerator {
451
523
  this.onProgress('incremental', 5, 'Detecting changes...');
452
524
  // Get changed files since last generation
453
525
  const changedFiles = this.getChangedFiles(existingMeta.fromCommit, currentCommit);
526
+ // If null, commits are on divergent branches (e.g., wiki generated on feature branch,
527
+ // now running on main). Fall back to full generation.
528
+ if (changedFiles === null) {
529
+ this.onProgress('incremental', 10, 'Branch diverged, running full generation...');
530
+ const fullResult = await this.fullGeneration(currentCommit);
531
+ return { ...fullResult, mode: 'incremental' };
532
+ }
454
533
  if (changedFiles.length === 0) {
455
534
  // No file changes but commit differs (e.g. merge commit)
456
535
  await this.saveWikiMeta({
@@ -559,13 +638,31 @@ export class WikiGenerator {
559
638
  return '';
560
639
  }
561
640
  }
641
+ /**
642
+ * Check if fromCommit is an ancestor of toCommit (reachable in git history).
643
+ * Returns false if commits are on divergent branches or fromCommit doesn't exist.
644
+ */
645
+ isCommitReachable(fromCommit, toCommit) {
646
+ try {
647
+ execFileSync('git', ['merge-base', '--is-ancestor', fromCommit, toCommit], { cwd: this.repoPath, stdio: 'ignore' });
648
+ return true;
649
+ }
650
+ catch {
651
+ return false;
652
+ }
653
+ }
562
654
  getChangedFiles(fromCommit, toCommit) {
655
+ // First check if fromCommit is reachable from toCommit
656
+ // This handles the case where wiki was generated on a different branch
657
+ if (!this.isCommitReachable(fromCommit, toCommit)) {
658
+ return null; // Signal that we can't compute diff (divergent branches)
659
+ }
563
660
  try {
564
661
  const output = execFileSync('git', ['diff', `${fromCommit}..${toCommit}`, '--name-only'], { cwd: this.repoPath }).toString().trim();
565
662
  return output ? output.split('\n').filter(Boolean) : [];
566
663
  }
567
664
  catch {
568
- return [];
665
+ return null; // Treat git errors as needing full regen
569
666
  }
570
667
  }
571
668
  async readSourceFiles(filePaths) {
@@ -4,6 +4,10 @@
4
4
  * Encapsulated Cypher queries against the GitNexus knowledge graph.
5
5
  * Uses the MCP-style pooled lbug-adapter for connection management.
6
6
  */
7
+ /**
8
+ * Touch the wiki DB connection to prevent idle timeout during long LLM calls.
9
+ */
10
+ export declare function touchWikiDb(): void;
7
11
  export interface FileWithExports {
8
12
  filePath: string;
9
13
  symbols: Array<{
@@ -4,8 +4,14 @@
4
4
  * Encapsulated Cypher queries against the GitNexus knowledge graph.
5
5
  * Uses the MCP-style pooled lbug-adapter for connection management.
6
6
  */
7
- import { initLbug, executeQuery, closeLbug } from '../../mcp/core/lbug-adapter.js';
7
+ import { initLbug, executeQuery, closeLbug, touchRepo } from '../../mcp/core/lbug-adapter.js';
8
8
  const REPO_ID = '__wiki__';
9
+ /**
10
+ * Touch the wiki DB connection to prevent idle timeout during long LLM calls.
11
+ */
12
+ export function touchWikiDb() {
13
+ touchRepo(REPO_ID);
14
+ }
9
15
  /**
10
16
  * Initialize the LadybugDB connection for wiki generation.
11
17
  */
@@ -6,12 +6,14 @@
6
6
  *
7
7
  * Config priority: CLI flags > env vars > defaults
8
8
  */
9
+ export type LLMProvider = 'openai' | 'cursor';
9
10
  export interface LLMConfig {
10
11
  apiKey: string;
11
12
  baseUrl: string;
12
13
  model: string;
13
14
  maxTokens: number;
14
15
  temperature: number;
16
+ provider?: LLMProvider;
15
17
  }
16
18
  export interface LLMResponse {
17
19
  content: string;
@@ -15,23 +15,27 @@
15
15
  export async function resolveLLMConfig(overrides) {
16
16
  const { loadCLIConfig } = await import('../../storage/repo-manager.js');
17
17
  const savedConfig = await loadCLIConfig();
18
+ const provider = overrides?.provider || savedConfig.provider || 'openai';
18
19
  const apiKey = overrides?.apiKey
19
20
  || process.env.GITNEXUS_API_KEY
20
21
  || process.env.OPENAI_API_KEY
21
22
  || savedConfig.apiKey
22
23
  || '';
24
+ // For cursor provider, only use model if explicitly provided (default is 'auto' handled by CLI)
25
+ // For openai provider, use model with fallback to default
26
+ const model = provider === 'cursor'
27
+ ? (overrides?.model || savedConfig.cursorModel || '')
28
+ : (overrides?.model || process.env.GITNEXUS_MODEL || savedConfig.model || 'minimax/minimax-m2.5');
23
29
  return {
24
30
  apiKey,
25
31
  baseUrl: overrides?.baseUrl
26
32
  || process.env.GITNEXUS_LLM_BASE_URL
27
33
  || savedConfig.baseUrl
28
34
  || 'https://openrouter.ai/api/v1',
29
- model: overrides?.model
30
- || process.env.GITNEXUS_MODEL
31
- || savedConfig.model
32
- || 'minimax/minimax-m2.5',
35
+ model,
33
36
  maxTokens: overrides?.maxTokens ?? 16_384,
34
37
  temperature: overrides?.temperature ?? 0,
38
+ provider,
35
39
  };
36
40
  }
37
41
  /**
@@ -6,11 +6,11 @@
6
6
  */
7
7
  export declare const GROUPING_SYSTEM_PROMPT = "You are a documentation architect. Given a list of source files with their exported symbols, group them into logical documentation modules.\n\nRules:\n- Each module should represent a cohesive feature, layer, or domain\n- Every file must appear in exactly one module\n- Module names should be human-readable (e.g. \"Authentication\", \"Database Layer\", \"API Routes\")\n- Aim for 5-15 modules for a typical project. Fewer for small projects, more for large ones\n- Group by functionality, not by file type or directory structure alone\n- Do NOT create modules for tests, configs, or non-source files";
8
8
  export declare const GROUPING_USER_PROMPT = "Group these source files into documentation modules.\n\n**Files and their exports:**\n{{FILE_LIST}}\n\n**Directory structure:**\n{{DIRECTORY_TREE}}\n\nRespond with ONLY a JSON object mapping module names to file path arrays. No markdown, no explanation.\nExample format:\n{\n \"Authentication\": [\"src/auth/login.ts\", \"src/auth/session.ts\"],\n \"Database\": [\"src/db/connection.ts\", \"src/db/models.ts\"]\n}";
9
- export declare const MODULE_SYSTEM_PROMPT = "You are a technical documentation writer. Write clear, developer-focused documentation for a code module.\n\nRules:\n- Reference actual function names, class names, and code patterns \u2014 do NOT invent APIs\n- Use the call graph and execution flow data for accuracy, but do NOT mechanically list every edge\n- Include Mermaid diagrams only when they genuinely help understanding. Keep them small (5-10 nodes max)\n- Structure the document however makes sense for this module \u2014 there is no mandatory format\n- Write for a developer who needs to understand and contribute to this code";
9
+ export declare const MODULE_SYSTEM_PROMPT = "You are a technical documentation writer. Write clear, developer-focused documentation for a code module.\n\nRules:\n- Output ONLY the documentation content \u2014 no meta-commentary like \"I've written...\", \"Here's the documentation...\", \"The documentation covers...\", or similar\n- Start directly with the module heading and content\n- Reference actual function names, class names, and code patterns \u2014 do NOT invent APIs\n- Use the call graph and execution flow data for accuracy, but do NOT mechanically list every edge\n- Include Mermaid diagrams only when they genuinely help understanding. Keep them small (5-10 nodes max)\n- Structure the document however makes sense for this module \u2014 there is no mandatory format\n- Write for a developer who needs to understand and contribute to this code";
10
10
  export declare const MODULE_USER_PROMPT = "Write documentation for the **{{MODULE_NAME}}** module.\n\n## Source Code\n\n{{SOURCE_CODE}}\n\n## Call Graph & Execution Flows (reference for accuracy)\n\nInternal calls: {{INTRA_CALLS}}\nOutgoing calls: {{OUTGOING_CALLS}}\nIncoming calls: {{INCOMING_CALLS}}\nExecution flows: {{PROCESSES}}\n\n---\n\nWrite comprehensive documentation for this module. Cover its purpose, how it works, its key components, and how it connects to the rest of the codebase. Use whatever structure best fits this module \u2014 you decide the sections and headings. Include a Mermaid diagram only if it genuinely clarifies the architecture.";
11
- export declare const PARENT_SYSTEM_PROMPT = "You are a technical documentation writer. Write a summary page for a module that contains sub-modules. Synthesize the children's documentation \u2014 do not re-read source code.\n\nRules:\n- Reference actual components from the child modules\n- Focus on how the sub-modules work together, not repeating their individual docs\n- Keep it concise \u2014 the reader can click through to child pages for detail\n- Include a Mermaid diagram only if it genuinely clarifies how the sub-modules relate";
11
+ export declare const PARENT_SYSTEM_PROMPT = "You are a technical documentation writer. Write a summary page for a module that contains sub-modules. Synthesize the children's documentation \u2014 do not re-read source code.\n\nRules:\n- Output ONLY the documentation content \u2014 no meta-commentary like \"I've written...\", \"Here's the documentation...\", \"The documentation covers...\", or similar\n- Start directly with the module heading and content\n- Reference actual components from the child modules\n- Focus on how the sub-modules work together, not repeating their individual docs\n- Keep it concise \u2014 the reader can click through to child pages for detail\n- Include a Mermaid diagram only if it genuinely clarifies how the sub-modules relate";
12
12
  export declare const PARENT_USER_PROMPT = "Write documentation for the **{{MODULE_NAME}}** module, which contains these sub-modules:\n\n{{CHILDREN_DOCS}}\n\nCross-module calls: {{CROSS_MODULE_CALLS}}\nShared execution flows: {{CROSS_PROCESSES}}\n\n---\n\nWrite a concise overview of this module group. Explain its purpose, how the sub-modules fit together, and the key workflows that span them. Link to sub-module pages (e.g. `[Sub-module Name](sub-module-slug.md)`) rather than repeating their content. Use whatever structure fits best.";
13
- export declare const OVERVIEW_SYSTEM_PROMPT = "You are a technical documentation writer. Write the top-level overview page for a repository wiki. This is the first page a new developer sees.\n\nRules:\n- Be clear and welcoming \u2014 this is the entry point to the entire codebase\n- Reference actual module names so readers can navigate to their docs\n- Include a high-level Mermaid architecture diagram showing only the most important modules and their relationships (max 10 nodes). A new dev should grasp it in 10 seconds\n- Do NOT create module index tables or list every module with descriptions \u2014 just link to module pages naturally within the text\n- Use the inter-module edges and execution flow data for accuracy, but do NOT dump them raw";
13
+ export declare const OVERVIEW_SYSTEM_PROMPT = "You are a technical documentation writer. Write the top-level overview page for a repository wiki. This is the first page a new developer sees.\n\nRules:\n- Output ONLY the documentation content \u2014 no meta-commentary like \"I've written...\", \"Here's the documentation...\", \"The page has been rewritten...\", or similar\n- Start directly with the project heading and content\n- Be clear and welcoming \u2014 this is the entry point to the entire codebase\n- Reference actual module names so readers can navigate to their docs\n- Include a high-level Mermaid architecture diagram showing only the most important modules and their relationships (max 10 nodes). A new dev should grasp it in 10 seconds\n- Do NOT create module index tables or list every module with descriptions \u2014 just link to module pages naturally within the text\n- Use the inter-module edges and execution flow data for accuracy, but do NOT dump them raw";
14
14
  export declare const OVERVIEW_USER_PROMPT = "Write the overview page for this repository's wiki.\n\n## Project Info\n\n{{PROJECT_INFO}}\n\n## Module Summaries\n\n{{MODULE_SUMMARIES}}\n\n## Reference Data (for accuracy \u2014 do not reproduce verbatim)\n\nInter-module call edges: {{MODULE_EDGES}}\nKey system flows: {{TOP_PROCESSES}}\n\n---\n\nWrite a clear overview of this project: what it does, how it's architected, and the key end-to-end flows. Include a simple Mermaid architecture diagram (max 10 nodes, big-picture only). Link to module pages (e.g. `[Module Name](module-slug.md)`) naturally in the text rather than listing them in a table. If project config was provided, include brief setup instructions. Structure the page however reads best.";
15
15
  /**
16
16
  * Replace {{PLACEHOLDER}} tokens in a template string.
@@ -32,6 +32,8 @@ Example format:
32
32
  export const MODULE_SYSTEM_PROMPT = `You are a technical documentation writer. Write clear, developer-focused documentation for a code module.
33
33
 
34
34
  Rules:
35
+ - Output ONLY the documentation content — no meta-commentary like "I've written...", "Here's the documentation...", "The documentation covers...", or similar
36
+ - Start directly with the module heading and content
35
37
  - Reference actual function names, class names, and code patterns — do NOT invent APIs
36
38
  - Use the call graph and execution flow data for accuracy, but do NOT mechanically list every edge
37
39
  - Include Mermaid diagrams only when they genuinely help understanding. Keep them small (5-10 nodes max)
@@ -57,6 +59,8 @@ Write comprehensive documentation for this module. Cover its purpose, how it wor
57
59
  export const PARENT_SYSTEM_PROMPT = `You are a technical documentation writer. Write a summary page for a module that contains sub-modules. Synthesize the children's documentation — do not re-read source code.
58
60
 
59
61
  Rules:
62
+ - Output ONLY the documentation content — no meta-commentary like "I've written...", "Here's the documentation...", "The documentation covers...", or similar
63
+ - Start directly with the module heading and content
60
64
  - Reference actual components from the child modules
61
65
  - Focus on how the sub-modules work together, not repeating their individual docs
62
66
  - Keep it concise — the reader can click through to child pages for detail
@@ -75,6 +79,8 @@ Write a concise overview of this module group. Explain its purpose, how the sub-
75
79
  export const OVERVIEW_SYSTEM_PROMPT = `You are a technical documentation writer. Write the top-level overview page for a repository wiki. This is the first page a new developer sees.
76
80
 
77
81
  Rules:
82
+ - Output ONLY the documentation content — no meta-commentary like "I've written...", "Here's the documentation...", "The page has been rewritten...", or similar
83
+ - Start directly with the project heading and content
78
84
  - Be clear and welcoming — this is the entry point to the entire codebase
79
85
  - Reference actual module names so readers can navigate to their docs
80
86
  - Include a high-level Mermaid architecture diagram showing only the most important modules and their relationships (max 10 nodes). A new dev should grasp it in 10 seconds
@@ -5,9 +5,9 @@
5
5
  * For MCP, we only need to compute query embeddings, not batch embed.
6
6
  */
7
7
  import { pipeline, env } from '@huggingface/transformers';
8
+ import { isHttpMode, getHttpDimensions, httpEmbedQuery } from '../../core/embeddings/http-client.js';
8
9
  // Model config
9
10
  const MODEL_ID = 'Snowflake/snowflake-arctic-embed-xs';
10
- const EMBEDDING_DIMS = 384;
11
11
  // Module-level state for singleton pattern
12
12
  let embedderInstance = null;
13
13
  let isInitializing = false;
@@ -16,6 +16,9 @@ let initPromise = null;
16
16
  * Initialize the embedding model (lazy, on first search)
17
17
  */
18
18
  export const initEmbedder = async () => {
19
+ if (isHttpMode()) {
20
+ throw new Error('initEmbedder() should not be called in HTTP mode.');
21
+ }
19
22
  if (embedderInstance) {
20
23
  return embedderInstance;
21
24
  }
@@ -75,11 +78,14 @@ export const initEmbedder = async () => {
75
78
  /**
76
79
  * Check if embedder is ready
77
80
  */
78
- export const isEmbedderReady = () => embedderInstance !== null;
81
+ export const isEmbedderReady = () => isHttpMode() || embedderInstance !== null;
79
82
  /**
80
83
  * Embed a query text for semantic search
81
84
  */
82
85
  export const embedQuery = async (query) => {
86
+ if (isHttpMode()) {
87
+ return httpEmbedQuery(query);
88
+ }
83
89
  const embedder = await initEmbedder();
84
90
  const result = await embedder(query, {
85
91
  pooling: 'mean',
@@ -90,7 +96,9 @@ export const embedQuery = async (query) => {
90
96
  /**
91
97
  * Get embedding dimensions
92
98
  */
93
- export const getEmbeddingDims = () => EMBEDDING_DIMS;
99
+ export const getEmbeddingDims = () => {
100
+ return getHttpDimensions() ?? 384;
101
+ };
94
102
  /**
95
103
  * Cleanup embedder
96
104
  */
@@ -15,6 +15,11 @@
15
15
  import lbug from '@ladybugdb/core';
16
16
  /** Saved real stdout.write — used to silence LadybugDB native output without race conditions */
17
17
  export declare const realStdoutWrite: any;
18
+ /**
19
+ * Touch a repo to reset its idle timeout.
20
+ * Call this during long-running operations to prevent the connection from being closed.
21
+ */
22
+ export declare const touchRepo: (repoId: string) => void;
18
23
  /**
19
24
  * Initialize (or reuse) a Database + connection pool for a specific repo.
20
25
  * Retries on lock errors (e.g., when `gitnexus analyze` is running).
@@ -46,6 +46,16 @@ function ensureIdleTimer() {
46
46
  idleTimer.unref();
47
47
  }
48
48
  }
49
+ /**
50
+ * Touch a repo to reset its idle timeout.
51
+ * Call this during long-running operations to prevent the connection from being closed.
52
+ */
53
+ export const touchRepo = (repoId) => {
54
+ const entry = pool.get(repoId);
55
+ if (entry) {
56
+ entry.lastUsed = Date.now();
57
+ }
58
+ };
49
59
  /**
50
60
  * Evict the least-recently-used repo if pool is at capacity
51
61
  */
@@ -109,6 +119,7 @@ function closeOne(repoId) {
109
119
  * Create a new Connection from a repo's Database.
110
120
  * Silences stdout to prevent native module output from corrupting MCP stdio.
111
121
  */
122
+ let activeQueryCount = 0;
112
123
  function silenceStdout() {
113
124
  if (stdoutSilenceCount++ === 0) {
114
125
  process.stdout.write = (() => true);
@@ -122,8 +133,10 @@ function restoreStdout() {
122
133
  }
123
134
  // Safety watchdog: restore stdout if it gets stuck silenced (e.g. native crash
124
135
  // inside createConnection before restoreStdout runs).
136
+ // Exempts active queries and pre-warm — these legitimately hold silence for
137
+ // longer than 1 second (queries can take up to QUERY_TIMEOUT_MS = 30s).
125
138
  setInterval(() => {
126
- if (stdoutSilenceCount > 0 && !preWarmActive) {
139
+ if (stdoutSilenceCount > 0 && !preWarmActive && activeQueryCount === 0) {
127
140
  stdoutSilenceCount = 0;
128
141
  process.stdout.write = realStdoutWrite;
129
142
  }
@@ -389,6 +402,8 @@ export const executeQuery = async (repoId, cypher) => {
389
402
  }
390
403
  entry.lastUsed = Date.now();
391
404
  const conn = await checkout(entry);
405
+ silenceStdout();
406
+ activeQueryCount++;
392
407
  try {
393
408
  const queryResult = await withTimeout(conn.query(cypher), QUERY_TIMEOUT_MS, 'Query');
394
409
  const result = Array.isArray(queryResult) ? queryResult[0] : queryResult;
@@ -396,6 +411,8 @@ export const executeQuery = async (repoId, cypher) => {
396
411
  return rows;
397
412
  }
398
413
  finally {
414
+ activeQueryCount--;
415
+ restoreStdout();
399
416
  checkin(entry, conn);
400
417
  }
401
418
  };
@@ -410,6 +427,8 @@ export const executeParameterized = async (repoId, cypher, params) => {
410
427
  }
411
428
  entry.lastUsed = Date.now();
412
429
  const conn = await checkout(entry);
430
+ silenceStdout();
431
+ activeQueryCount++;
413
432
  try {
414
433
  const stmt = await withTimeout(conn.prepare(cypher), QUERY_TIMEOUT_MS, 'Prepare');
415
434
  if (!stmt.isSuccess()) {
@@ -422,6 +441,8 @@ export const executeParameterized = async (repoId, cypher, params) => {
422
441
  return rows;
423
442
  }
424
443
  finally {
444
+ activeQueryCount--;
445
+ restoreStdout();
425
446
  checkin(entry, conn);
426
447
  }
427
448
  };
@@ -448,7 +469,7 @@ export const closeLbug = async (repoId) => {
448
469
  */
449
470
  export const isLbugReady = (repoId) => pool.has(repoId);
450
471
  /** Regex to detect write operations in user-supplied Cypher queries */
451
- export const CYPHER_WRITE_RE = /\b(CREATE|DELETE|SET|MERGE|REMOVE|DROP|ALTER|COPY|DETACH)\b/i;
472
+ export const CYPHER_WRITE_RE = /(?<!:)\b(CREATE|DELETE|SET|MERGE|REMOVE|DROP|ALTER|COPY|DETACH|FOREACH)\b/i;
452
473
  /** Check if a Cypher query contains write operations */
453
474
  export function isWriteQuery(query) {
454
475
  return CYPHER_WRITE_RE.test(query);