@veewo/gitnexus 1.3.11 → 1.4.6-rc

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 (181) hide show
  1. package/README.md +37 -80
  2. package/dist/benchmark/agent-context/tool-runner.js +2 -2
  3. package/dist/benchmark/neonspark-candidates.js +3 -3
  4. package/dist/benchmark/tool-runner.js +2 -2
  5. package/dist/cli/ai-context.d.ts +2 -1
  6. package/dist/cli/ai-context.js +16 -12
  7. package/dist/cli/analyze.d.ts +2 -0
  8. package/dist/cli/analyze.js +68 -48
  9. package/dist/cli/augment.js +1 -1
  10. package/dist/cli/eval-server.d.ts +8 -1
  11. package/dist/cli/eval-server.js +30 -13
  12. package/dist/cli/index.js +28 -82
  13. package/dist/cli/lazy-action.d.ts +6 -0
  14. package/dist/cli/lazy-action.js +18 -0
  15. package/dist/cli/mcp.js +3 -1
  16. package/dist/cli/setup.js +87 -48
  17. package/dist/cli/setup.test.js +18 -13
  18. package/dist/cli/skill-gen.d.ts +26 -0
  19. package/dist/cli/skill-gen.js +549 -0
  20. package/dist/cli/status.js +13 -4
  21. package/dist/cli/tool.d.ts +3 -2
  22. package/dist/cli/tool.js +50 -16
  23. package/dist/cli/wiki.js +8 -4
  24. package/dist/config/ignore-service.d.ts +25 -0
  25. package/dist/config/ignore-service.js +76 -0
  26. package/dist/config/supported-languages.d.ts +4 -1
  27. package/dist/config/supported-languages.js +3 -2
  28. package/dist/core/augmentation/engine.js +94 -67
  29. package/dist/core/embeddings/embedder.d.ts +1 -1
  30. package/dist/core/embeddings/embedder.js +1 -1
  31. package/dist/core/embeddings/embedding-pipeline.d.ts +3 -3
  32. package/dist/core/embeddings/embedding-pipeline.js +52 -25
  33. package/dist/core/embeddings/types.d.ts +1 -1
  34. package/dist/core/graph/types.d.ts +7 -2
  35. package/dist/core/ingestion/ast-cache.js +3 -2
  36. package/dist/core/ingestion/call-processor.d.ts +8 -6
  37. package/dist/core/ingestion/call-processor.js +468 -206
  38. package/dist/core/ingestion/call-routing.d.ts +53 -0
  39. package/dist/core/ingestion/call-routing.js +108 -0
  40. package/dist/core/ingestion/constants.d.ts +16 -0
  41. package/dist/core/ingestion/constants.js +16 -0
  42. package/dist/core/ingestion/entry-point-scoring.d.ts +2 -1
  43. package/dist/core/ingestion/entry-point-scoring.js +116 -23
  44. package/dist/core/ingestion/export-detection.d.ts +18 -0
  45. package/dist/core/ingestion/export-detection.js +231 -0
  46. package/dist/core/ingestion/filesystem-walker.js +4 -3
  47. package/dist/core/ingestion/framework-detection.d.ts +19 -4
  48. package/dist/core/ingestion/framework-detection.js +182 -6
  49. package/dist/core/ingestion/heritage-processor.d.ts +13 -5
  50. package/dist/core/ingestion/heritage-processor.js +109 -55
  51. package/dist/core/ingestion/import-processor.d.ts +16 -20
  52. package/dist/core/ingestion/import-processor.js +199 -579
  53. package/dist/core/ingestion/language-config.d.ts +46 -0
  54. package/dist/core/ingestion/language-config.js +167 -0
  55. package/dist/core/ingestion/mro-processor.d.ts +45 -0
  56. package/dist/core/ingestion/mro-processor.js +369 -0
  57. package/dist/core/ingestion/named-binding-extraction.d.ts +61 -0
  58. package/dist/core/ingestion/named-binding-extraction.js +363 -0
  59. package/dist/core/ingestion/parsing-processor.d.ts +4 -1
  60. package/dist/core/ingestion/parsing-processor.js +107 -109
  61. package/dist/core/ingestion/pipeline.d.ts +6 -3
  62. package/dist/core/ingestion/pipeline.js +208 -114
  63. package/dist/core/ingestion/process-processor.js +8 -2
  64. package/dist/core/ingestion/resolution-context.d.ts +53 -0
  65. package/dist/core/ingestion/resolution-context.js +132 -0
  66. package/dist/core/ingestion/resolvers/csharp.d.ts +22 -0
  67. package/dist/core/ingestion/resolvers/csharp.js +109 -0
  68. package/dist/core/ingestion/resolvers/go.d.ts +19 -0
  69. package/dist/core/ingestion/resolvers/go.js +42 -0
  70. package/dist/core/ingestion/resolvers/index.d.ts +18 -0
  71. package/dist/core/ingestion/resolvers/index.js +13 -0
  72. package/dist/core/ingestion/resolvers/jvm.d.ts +23 -0
  73. package/dist/core/ingestion/resolvers/jvm.js +87 -0
  74. package/dist/core/ingestion/resolvers/php.d.ts +15 -0
  75. package/dist/core/ingestion/resolvers/php.js +35 -0
  76. package/dist/core/ingestion/resolvers/python.d.ts +19 -0
  77. package/dist/core/ingestion/resolvers/python.js +52 -0
  78. package/dist/core/ingestion/resolvers/ruby.d.ts +12 -0
  79. package/dist/core/ingestion/resolvers/ruby.js +15 -0
  80. package/dist/core/ingestion/resolvers/rust.d.ts +15 -0
  81. package/dist/core/ingestion/resolvers/rust.js +73 -0
  82. package/dist/core/ingestion/resolvers/standard.d.ts +28 -0
  83. package/dist/core/ingestion/resolvers/standard.js +123 -0
  84. package/dist/core/ingestion/resolvers/utils.d.ts +33 -0
  85. package/dist/core/ingestion/resolvers/utils.js +122 -0
  86. package/dist/core/ingestion/symbol-table.d.ts +21 -1
  87. package/dist/core/ingestion/symbol-table.js +40 -12
  88. package/dist/core/ingestion/tree-sitter-queries.d.ts +13 -10
  89. package/dist/core/ingestion/tree-sitter-queries.js +297 -7
  90. package/dist/core/ingestion/type-env.d.ts +49 -0
  91. package/dist/core/ingestion/type-env.js +611 -0
  92. package/dist/core/ingestion/type-extractors/c-cpp.d.ts +2 -0
  93. package/dist/core/ingestion/type-extractors/c-cpp.js +385 -0
  94. package/dist/core/ingestion/type-extractors/csharp.d.ts +2 -0
  95. package/dist/core/ingestion/type-extractors/csharp.js +383 -0
  96. package/dist/core/ingestion/type-extractors/go.d.ts +2 -0
  97. package/dist/core/ingestion/type-extractors/go.js +467 -0
  98. package/dist/core/ingestion/type-extractors/index.d.ts +22 -0
  99. package/dist/core/ingestion/type-extractors/index.js +31 -0
  100. package/dist/core/ingestion/type-extractors/jvm.d.ts +3 -0
  101. package/dist/core/ingestion/type-extractors/jvm.js +681 -0
  102. package/dist/core/ingestion/type-extractors/php.d.ts +2 -0
  103. package/dist/core/ingestion/type-extractors/php.js +549 -0
  104. package/dist/core/ingestion/type-extractors/python.d.ts +2 -0
  105. package/dist/core/ingestion/type-extractors/python.js +406 -0
  106. package/dist/core/ingestion/type-extractors/ruby.d.ts +2 -0
  107. package/dist/core/ingestion/type-extractors/ruby.js +389 -0
  108. package/dist/core/ingestion/type-extractors/rust.d.ts +2 -0
  109. package/dist/core/ingestion/type-extractors/rust.js +449 -0
  110. package/dist/core/ingestion/type-extractors/shared.d.ts +133 -0
  111. package/dist/core/ingestion/type-extractors/shared.js +703 -0
  112. package/dist/core/ingestion/type-extractors/swift.d.ts +2 -0
  113. package/dist/core/ingestion/type-extractors/swift.js +137 -0
  114. package/dist/core/ingestion/type-extractors/types.d.ts +127 -0
  115. package/dist/core/ingestion/type-extractors/typescript.d.ts +2 -0
  116. package/dist/core/ingestion/type-extractors/typescript.js +494 -0
  117. package/dist/core/ingestion/utils.d.ts +103 -0
  118. package/dist/core/ingestion/utils.js +1085 -4
  119. package/dist/core/ingestion/workers/parse-worker.d.ts +51 -4
  120. package/dist/core/ingestion/workers/parse-worker.js +634 -222
  121. package/dist/core/ingestion/workers/worker-pool.js +8 -0
  122. package/dist/core/{kuzu → lbug}/csv-generator.d.ts +12 -10
  123. package/dist/core/{kuzu → lbug}/csv-generator.js +82 -101
  124. package/dist/core/{kuzu/kuzu-adapter.d.ts → lbug/lbug-adapter.d.ts} +20 -25
  125. package/dist/core/{kuzu/kuzu-adapter.js → lbug/lbug-adapter.js} +150 -122
  126. package/dist/core/{kuzu → lbug}/schema.d.ts +4 -4
  127. package/dist/core/{kuzu → lbug}/schema.js +23 -22
  128. package/dist/core/lbug/schema.test.d.ts +1 -0
  129. package/dist/core/search/bm25-index.d.ts +4 -4
  130. package/dist/core/search/bm25-index.js +12 -11
  131. package/dist/core/search/hybrid-search.d.ts +2 -2
  132. package/dist/core/search/hybrid-search.js +6 -6
  133. package/dist/core/tree-sitter/parser-loader.d.ts +1 -0
  134. package/dist/core/tree-sitter/parser-loader.js +19 -0
  135. package/dist/core/wiki/generator.d.ts +2 -2
  136. package/dist/core/wiki/generator.js +6 -6
  137. package/dist/core/wiki/graph-queries.d.ts +4 -4
  138. package/dist/core/wiki/graph-queries.js +7 -7
  139. package/dist/mcp/compatible-stdio-transport.d.ts +25 -0
  140. package/dist/mcp/compatible-stdio-transport.js +200 -0
  141. package/dist/mcp/core/{kuzu-adapter.d.ts → lbug-adapter.d.ts} +11 -10
  142. package/dist/mcp/core/lbug-adapter.js +327 -0
  143. package/dist/mcp/local/local-backend.d.ts +21 -16
  144. package/dist/mcp/local/local-backend.js +306 -706
  145. package/dist/mcp/local/unity-parity-seed-loader.d.ts +6 -1
  146. package/dist/mcp/local/unity-parity-seed-loader.js +119 -9
  147. package/dist/mcp/local/unity-parity-seed-loader.test.js +95 -7
  148. package/dist/mcp/resources.js +2 -2
  149. package/dist/mcp/server.js +28 -13
  150. package/dist/mcp/staleness.js +2 -2
  151. package/dist/mcp/tools.js +12 -3
  152. package/dist/server/api.js +12 -12
  153. package/dist/server/mcp-http.d.ts +1 -1
  154. package/dist/server/mcp-http.js +1 -1
  155. package/dist/storage/git.js +4 -1
  156. package/dist/storage/repo-manager.d.ts +20 -2
  157. package/dist/storage/repo-manager.js +74 -4
  158. package/dist/types/pipeline.d.ts +1 -1
  159. package/hooks/claude/gitnexus-hook.cjs +149 -46
  160. package/hooks/claude/pre-tool-use.sh +2 -1
  161. package/hooks/claude/session-start.sh +0 -0
  162. package/package.json +20 -4
  163. package/scripts/patch-tree-sitter-swift.cjs +74 -0
  164. package/skills/gitnexus-cli.md +8 -8
  165. package/skills/gitnexus-debugging.md +1 -1
  166. package/skills/gitnexus-exploring.md +1 -1
  167. package/skills/gitnexus-guide.md +1 -1
  168. package/skills/gitnexus-impact-analysis.md +1 -1
  169. package/skills/gitnexus-pr-review.md +163 -0
  170. package/skills/gitnexus-refactoring.md +1 -1
  171. package/dist/cli/claude-hooks.d.ts +0 -22
  172. package/dist/cli/claude-hooks.js +0 -97
  173. package/dist/mcp/core/kuzu-adapter.js +0 -231
  174. /package/dist/core/{kuzu/csv-generator.test.d.ts → ingestion/type-extractors/types.js} +0 -0
  175. /package/dist/core/{kuzu/relationship-pair-buckets.test.d.ts → lbug/csv-generator.test.d.ts} +0 -0
  176. /package/dist/core/{kuzu → lbug}/csv-generator.test.js +0 -0
  177. /package/dist/core/{kuzu → lbug}/relationship-pair-buckets.d.ts +0 -0
  178. /package/dist/core/{kuzu → lbug}/relationship-pair-buckets.js +0 -0
  179. /package/dist/core/{kuzu/schema.test.d.ts → lbug/relationship-pair-buckets.test.d.ts} +0 -0
  180. /package/dist/core/{kuzu → lbug}/relationship-pair-buckets.test.js +0 -0
  181. /package/dist/core/{kuzu → lbug}/schema.test.js +0 -0
package/dist/cli/tool.js CHANGED
@@ -10,9 +10,11 @@
10
10
  * gitnexus impact --target "AuthService" --direction upstream
11
11
  * gitnexus cypher "MATCH (n:Function) RETURN n.name LIMIT 10"
12
12
  *
13
- * Note: Output goes to stderr because KuzuDB's native module captures stdout
14
- * at the OS level during init. This is consistent with augment.ts.
13
+ * Note: Output goes to stdout via fs.writeSync(fd 1), bypassing LadybugDB's
14
+ * native module which captures the Node.js process.stdout stream during init.
15
+ * See the output() function for details (#324).
15
16
  */
17
+ import { writeSync } from 'node:fs';
16
18
  import { LocalBackend } from '../mcp/local/local-backend.js';
17
19
  let _backend = null;
18
20
  async function getBackend() {
@@ -26,10 +28,30 @@ async function getBackend() {
26
28
  }
27
29
  return _backend;
28
30
  }
31
+ /**
32
+ * Write tool output to stdout using low-level fd write.
33
+ *
34
+ * LadybugDB's native module captures Node.js process.stdout during init,
35
+ * but the underlying OS file descriptor 1 (stdout) remains intact.
36
+ * By using fs.writeSync(1, ...) we bypass the Node.js stream layer
37
+ * and write directly to the real stdout fd (#324).
38
+ *
39
+ * Falls back to stderr if the fd write fails (e.g., broken pipe).
40
+ */
29
41
  function output(data) {
30
42
  const text = typeof data === 'string' ? data : JSON.stringify(data, null, 2);
31
- // stderr because KuzuDB captures stdout at OS level
32
- process.stderr.write(text + '\n');
43
+ try {
44
+ writeSync(1, text + '\n');
45
+ }
46
+ catch (err) {
47
+ if (err?.code === 'EPIPE') {
48
+ // Consumer closed the pipe (e.g., `gitnexus cypher ... | head -1`)
49
+ // Exit cleanly per Unix convention
50
+ process.exit(0);
51
+ }
52
+ // Fallback: stderr (previous behavior, works on all platforms)
53
+ process.stderr.write(text + '\n');
54
+ }
33
55
  }
34
56
  export async function queryCommand(queryText, options) {
35
57
  if (!queryText?.trim()) {
@@ -71,18 +93,30 @@ export async function impactCommand(target, options) {
71
93
  console.error('Usage: gitnexus impact <symbol_name> [--direction upstream|downstream]');
72
94
  process.exit(1);
73
95
  }
74
- const backend = await getBackend();
75
- const result = await backend.callTool('impact', {
76
- target,
77
- target_uid: options?.uid,
78
- file_path: options?.file,
79
- direction: options?.direction || 'upstream',
80
- maxDepth: options?.depth ? parseInt(options.depth) : undefined,
81
- minConfidence: options?.minConfidence ? parseFloat(options.minConfidence) : undefined,
82
- includeTests: options?.includeTests ?? false,
83
- repo: options?.repo,
84
- });
85
- output(result);
96
+ try {
97
+ const backend = await getBackend();
98
+ const result = await backend.callTool('impact', {
99
+ target,
100
+ target_uid: options?.uid,
101
+ file_path: options?.file,
102
+ direction: options?.direction || 'upstream',
103
+ maxDepth: options?.depth ? parseInt(options.depth, 10) : undefined,
104
+ minConfidence: options?.minConfidence ? parseFloat(options.minConfidence) : undefined,
105
+ includeTests: options?.includeTests ?? false,
106
+ repo: options?.repo,
107
+ });
108
+ output(result);
109
+ }
110
+ catch (err) {
111
+ // Belt-and-suspenders: catch infrastructure failures (getBackend, callTool transport)
112
+ // The backend's impact() already returns structured errors for graph query failures
113
+ output({
114
+ error: (err instanceof Error ? err.message : String(err)) || 'Impact analysis failed unexpectedly',
115
+ target: { name: target },
116
+ direction: options?.direction || 'upstream',
117
+ suggestion: 'Try reducing --depth or using gitnexus context <symbol> as a fallback',
118
+ });
119
+ }
86
120
  }
87
121
  export async function cypherCommand(query, options) {
88
122
  if (!query?.trim()) {
package/dist/cli/wiki.js CHANGED
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import path from 'path';
8
8
  import readline from 'readline';
9
- import { execSync } from 'child_process';
9
+ import { execSync, execFileSync } from 'child_process';
10
10
  import cliProgress from 'cli-progress';
11
11
  import { getGitRoot, isGitRepo } from '../storage/git.js';
12
12
  import { getStoragePaths, loadMeta, loadCLIConfig, saveCLIConfig } from '../storage/repo-manager.js';
@@ -86,7 +86,7 @@ export const wikiCommand = async (inputPath, options) => {
86
86
  return;
87
87
  }
88
88
  // ── Check for existing index ────────────────────────────────────────
89
- const { storagePath, kuzuPath } = getStoragePaths(repoPath);
89
+ const { storagePath, lbugPath } = getStoragePaths(repoPath);
90
90
  const meta = await loadMeta(storagePath);
91
91
  if (!meta) {
92
92
  console.log(' Error: No GitNexus index found.');
@@ -217,7 +217,7 @@ export const wikiCommand = async (inputPath, options) => {
217
217
  baseUrl: options?.baseUrl,
218
218
  concurrency: options?.concurrency ? parseInt(options.concurrency, 10) : undefined,
219
219
  };
220
- const generator = new WikiGenerator(repoPath, storagePath, kuzuPath, llmConfig, wikiOptions, (phase, percent, detail) => {
220
+ const generator = new WikiGenerator(repoPath, storagePath, lbugPath, llmConfig, wikiOptions, (phase, percent, detail) => {
221
221
  const label = detail || phase;
222
222
  if (label !== lastPhase) {
223
223
  lastPhase = label;
@@ -299,7 +299,11 @@ function hasGhCLI() {
299
299
  }
300
300
  function publishGist(htmlPath) {
301
301
  try {
302
- const output = execSync(`gh gist create "${htmlPath}" --desc "Repository Wiki — generated by GitNexus" --public`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
302
+ const output = execFileSync('gh', [
303
+ 'gist', 'create', htmlPath,
304
+ '--desc', 'Repository Wiki — generated by GitNexus',
305
+ '--public',
306
+ ], { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
303
307
  // gh gist create prints the gist URL as the last line
304
308
  const lines = output.split('\n');
305
309
  const gistUrl = lines.find(l => l.includes('gist.github.com')) || lines[lines.length - 1];
@@ -1 +1,26 @@
1
+ import { type Ignore } from 'ignore';
2
+ import type { Path } from 'path-scurry';
1
3
  export declare const shouldIgnorePath: (filePath: string) => boolean;
4
+ /** Check if a directory name is in the hardcoded ignore list */
5
+ export declare const isHardcodedIgnoredDirectory: (name: string) => boolean;
6
+ /**
7
+ * Load .gitignore and .gitnexusignore rules from the repo root.
8
+ * Returns an `ignore` instance with all patterns, or null if no files found.
9
+ */
10
+ export interface IgnoreOptions {
11
+ /** Skip .gitignore parsing, only read .gitnexusignore. Defaults to GITNEXUS_NO_GITIGNORE env var. */
12
+ noGitignore?: boolean;
13
+ }
14
+ export declare const loadIgnoreRules: (repoPath: string, options?: IgnoreOptions) => Promise<Ignore | null>;
15
+ /**
16
+ * Create a glob-compatible ignore filter combining:
17
+ * - .gitignore / .gitnexusignore patterns (via `ignore` package)
18
+ * - Hardcoded DEFAULT_IGNORE_LIST, IGNORED_EXTENSIONS, IGNORED_FILES
19
+ *
20
+ * Returns an IgnoreLike object for glob's `ignore` option,
21
+ * enabling directory-level pruning during traversal.
22
+ */
23
+ export declare const createIgnoreFilter: (repoPath: string, options?: IgnoreOptions) => Promise<{
24
+ ignored(p: Path): boolean;
25
+ childrenIgnored(p: Path): boolean;
26
+ }>;
@@ -1,3 +1,6 @@
1
+ import ignore from 'ignore';
2
+ import fs from 'fs/promises';
3
+ import nodePath from 'path';
1
4
  const DEFAULT_IGNORE_LIST = new Set([
2
5
  // Version Control
3
6
  '.git',
@@ -163,6 +166,10 @@ const IGNORED_FILES = new Set([
163
166
  '.env.test',
164
167
  '.env.example',
165
168
  ]);
169
+ // NOTE: Negation patterns in .gitnexusignore (e.g. `!vendor/`) cannot override
170
+ // entries in DEFAULT_IGNORE_LIST — this is intentional. The hardcoded list protects
171
+ // against indexing directories that are almost never source code (node_modules, .git, etc.).
172
+ // Users who need to include such directories should remove them from the hardcoded list.
166
173
  export const shouldIgnorePath = (filePath) => {
167
174
  const normalizedPath = filePath.replace(/\\/g, '/');
168
175
  const parts = normalizedPath.split('/');
@@ -208,3 +215,72 @@ export const shouldIgnorePath = (filePath) => {
208
215
  }
209
216
  return false;
210
217
  };
218
+ /** Check if a directory name is in the hardcoded ignore list */
219
+ export const isHardcodedIgnoredDirectory = (name) => {
220
+ return DEFAULT_IGNORE_LIST.has(name);
221
+ };
222
+ export const loadIgnoreRules = async (repoPath, options) => {
223
+ const ig = ignore();
224
+ let hasRules = false;
225
+ // Allow users to bypass .gitignore parsing (e.g. when .gitignore accidentally excludes source files)
226
+ const skipGitignore = options?.noGitignore ?? !!process.env.GITNEXUS_NO_GITIGNORE;
227
+ const filenames = skipGitignore
228
+ ? ['.gitnexusignore']
229
+ : ['.gitignore', '.gitnexusignore'];
230
+ for (const filename of filenames) {
231
+ try {
232
+ const content = await fs.readFile(nodePath.join(repoPath, filename), 'utf-8');
233
+ ig.add(content);
234
+ hasRules = true;
235
+ }
236
+ catch (err) {
237
+ const code = err.code;
238
+ if (code !== 'ENOENT') {
239
+ console.warn(` Warning: could not read ${filename}: ${err.message}`);
240
+ }
241
+ }
242
+ }
243
+ return hasRules ? ig : null;
244
+ };
245
+ /**
246
+ * Create a glob-compatible ignore filter combining:
247
+ * - .gitignore / .gitnexusignore patterns (via `ignore` package)
248
+ * - Hardcoded DEFAULT_IGNORE_LIST, IGNORED_EXTENSIONS, IGNORED_FILES
249
+ *
250
+ * Returns an IgnoreLike object for glob's `ignore` option,
251
+ * enabling directory-level pruning during traversal.
252
+ */
253
+ export const createIgnoreFilter = async (repoPath, options) => {
254
+ const ig = await loadIgnoreRules(repoPath, options);
255
+ return {
256
+ ignored(p) {
257
+ // path-scurry's Path.relative() returns POSIX paths on all platforms,
258
+ // which is what the `ignore` package expects. No explicit normalization needed.
259
+ const rel = p.relative();
260
+ if (!rel)
261
+ return false;
262
+ // Check .gitignore / .gitnexusignore patterns
263
+ if (ig && ig.ignores(rel))
264
+ return true;
265
+ // Fall back to hardcoded rules
266
+ return shouldIgnorePath(rel);
267
+ },
268
+ childrenIgnored(p) {
269
+ // Fast path: check directory name against hardcoded list.
270
+ // Note: dot-directories (.git, .vscode, etc.) are primarily excluded by
271
+ // glob's `dot: false` option in filesystem-walker.ts. This check is
272
+ // defense-in-depth — do not remove `dot: false` assuming this covers it.
273
+ if (DEFAULT_IGNORE_LIST.has(p.name))
274
+ return true;
275
+ // Check against .gitignore / .gitnexusignore patterns.
276
+ // Test both bare path and path with trailing slash to handle
277
+ // bare-name patterns (e.g. `local`) and dir-only patterns (e.g. `local/`).
278
+ if (ig) {
279
+ const rel = p.relative();
280
+ if (rel && (ig.ignores(rel) || ig.ignores(rel + '/')))
281
+ return true;
282
+ }
283
+ return false;
284
+ },
285
+ };
286
+ };
@@ -7,6 +7,9 @@ export declare enum SupportedLanguages {
7
7
  CPlusPlus = "cpp",
8
8
  CSharp = "csharp",
9
9
  Go = "go",
10
+ Ruby = "ruby",
10
11
  Rust = "rust",
11
- PHP = "php"
12
+ PHP = "php",
13
+ Kotlin = "kotlin",
14
+ Swift = "swift"
12
15
  }
@@ -8,8 +8,9 @@ export var SupportedLanguages;
8
8
  SupportedLanguages["CPlusPlus"] = "cpp";
9
9
  SupportedLanguages["CSharp"] = "csharp";
10
10
  SupportedLanguages["Go"] = "go";
11
+ SupportedLanguages["Ruby"] = "ruby";
11
12
  SupportedLanguages["Rust"] = "rust";
12
13
  SupportedLanguages["PHP"] = "php";
13
- // Ruby = 'ruby',
14
- // Swift = 'swift',
14
+ SupportedLanguages["Kotlin"] = "kotlin";
15
+ SupportedLanguages["Swift"] = "swift";
15
16
  })(SupportedLanguages || (SupportedLanguages = {}));
@@ -56,7 +56,7 @@ async function findRepoForCwd(cwd) {
56
56
  return {
57
57
  name: bestMatch.name,
58
58
  storagePath: bestMatch.storagePath,
59
- kuzuPath: path.join(bestMatch.storagePath, 'kuzu'),
59
+ lbugPath: path.join(bestMatch.storagePath, 'lbug'),
60
60
  };
61
61
  }
62
62
  catch {
@@ -81,16 +81,16 @@ export async function augment(pattern, cwd) {
81
81
  const repo = await findRepoForCwd(workDir);
82
82
  if (!repo)
83
83
  return '';
84
- // Lazy-load kuzu adapter (skip unnecessary init)
85
- const { initKuzu, executeQuery, isKuzuReady } = await import('../../mcp/core/kuzu-adapter.js');
86
- const { searchFTSFromKuzu } = await import('../search/bm25-index.js');
84
+ // Lazy-load lbug adapter (skip unnecessary init)
85
+ const { initLbug, executeQuery, isLbugReady } = await import('../../mcp/core/lbug-adapter.js');
86
+ const { searchFTSFromLbug } = await import('../search/bm25-index.js');
87
87
  const repoId = repo.name.toLowerCase();
88
- // Init KuzuDB if not already
89
- if (!isKuzuReady(repoId)) {
90
- await initKuzu(repoId, repo.kuzuPath);
88
+ // Init LadybugDB if not already
89
+ if (!isLbugReady(repoId)) {
90
+ await initLbug(repoId, repo.lbugPath);
91
91
  }
92
92
  // Step 1: BM25 search (fast, no embeddings)
93
- const bm25Results = await searchFTSFromKuzu(pattern, 10, repoId);
93
+ const bm25Results = await searchFTSFromLbug(pattern, 10, repoId);
94
94
  if (bm25Results.length === 0)
95
95
  return '';
96
96
  // Step 2: Map BM25 file results to symbols
@@ -118,72 +118,99 @@ export async function augment(pattern, cwd) {
118
118
  }
119
119
  if (symbolMatches.length === 0)
120
120
  return '';
121
- // Step 3: For top matches, fetch callers/callees/processes
122
- // Also get cluster cohesion internally for ranking
123
- const enriched = [];
124
- const seen = new Set();
125
- for (const sym of symbolMatches.slice(0, 5)) {
126
- if (seen.has(sym.nodeId))
127
- continue;
128
- seen.add(sym.nodeId);
129
- const escaped = sym.nodeId.replace(/'/g, "''");
130
- // Callers
131
- let callers = [];
132
- try {
133
- const rows = await executeQuery(repoId, `
134
- MATCH (caller)-[:CodeRelation {type: 'CALLS'}]->(n {id: '${escaped}'})
135
- RETURN caller.name AS name
136
- LIMIT 3
137
- `);
138
- callers = rows.map((r) => r.name || r[0]).filter(Boolean);
139
- }
140
- catch { /* skip */ }
141
- // Callees
142
- let callees = [];
143
- try {
144
- const rows = await executeQuery(repoId, `
145
- MATCH (n {id: '${escaped}'})-[:CodeRelation {type: 'CALLS'}]->(callee)
146
- RETURN callee.name AS name
147
- LIMIT 3
148
- `);
149
- callees = rows.map((r) => r.name || r[0]).filter(Boolean);
121
+ // Step 3: Batch-fetch callers/callees/processes/cohesion for top matches
122
+ // Uses batched WHERE n.id IN [...] queries instead of per-symbol queries
123
+ const uniqueSymbols = symbolMatches.slice(0, 5).filter((sym, i, arr) => arr.findIndex(s => s.nodeId === sym.nodeId) === i);
124
+ if (uniqueSymbols.length === 0)
125
+ return '';
126
+ const idList = uniqueSymbols.map(s => `'${s.nodeId.replace(/'/g, "''")}'`).join(', ');
127
+ // Batch fetch callers
128
+ const callersMap = new Map();
129
+ try {
130
+ const rows = await executeQuery(repoId, `
131
+ MATCH (caller)-[:CodeRelation {type: 'CALLS'}]->(n)
132
+ WHERE n.id IN [${idList}]
133
+ RETURN n.id AS targetId, caller.name AS name
134
+ LIMIT 15
135
+ `);
136
+ for (const r of rows) {
137
+ const tid = r.targetId || r[0];
138
+ const name = r.name || r[1];
139
+ if (tid && name) {
140
+ if (!callersMap.has(tid))
141
+ callersMap.set(tid, []);
142
+ callersMap.get(tid).push(name);
143
+ }
150
144
  }
151
- catch { /* skip */ }
152
- // Processes
153
- let processes = [];
154
- try {
155
- const rows = await executeQuery(repoId, `
156
- MATCH (n {id: '${escaped}'})-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
157
- RETURN p.heuristicLabel AS label, r.step AS step, p.stepCount AS stepCount
158
- `);
159
- processes = rows.map((r) => {
160
- const label = r.label || r[0];
161
- const step = r.step || r[1];
162
- const stepCount = r.stepCount || r[2];
163
- return `${label} (step ${step}/${stepCount})`;
164
- }).filter(Boolean);
145
+ }
146
+ catch { /* skip */ }
147
+ // Batch fetch callees
148
+ const calleesMap = new Map();
149
+ try {
150
+ const rows = await executeQuery(repoId, `
151
+ MATCH (n)-[:CodeRelation {type: 'CALLS'}]->(callee)
152
+ WHERE n.id IN [${idList}]
153
+ RETURN n.id AS sourceId, callee.name AS name
154
+ LIMIT 15
155
+ `);
156
+ for (const r of rows) {
157
+ const sid = r.sourceId || r[0];
158
+ const name = r.name || r[1];
159
+ if (sid && name) {
160
+ if (!calleesMap.has(sid))
161
+ calleesMap.set(sid, []);
162
+ calleesMap.get(sid).push(name);
163
+ }
165
164
  }
166
- catch { /* skip */ }
167
- // Cluster cohesion (internal ranking signal)
168
- let cohesion = 0;
169
- try {
170
- const rows = await executeQuery(repoId, `
171
- MATCH (n {id: '${escaped}'})-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
172
- RETURN c.cohesion AS cohesion
173
- LIMIT 1
174
- `);
175
- if (rows.length > 0) {
176
- cohesion = (rows[0].cohesion ?? rows[0][0]) || 0;
165
+ }
166
+ catch { /* skip */ }
167
+ // Batch fetch processes
168
+ const processesMap = new Map();
169
+ try {
170
+ const rows = await executeQuery(repoId, `
171
+ MATCH (n)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
172
+ WHERE n.id IN [${idList}]
173
+ RETURN n.id AS nodeId, p.heuristicLabel AS label, r.step AS step, p.stepCount AS stepCount
174
+ `);
175
+ for (const r of rows) {
176
+ const nid = r.nodeId || r[0];
177
+ const label = r.label || r[1];
178
+ const step = r.step || r[2];
179
+ const stepCount = r.stepCount || r[3];
180
+ if (nid && label) {
181
+ if (!processesMap.has(nid))
182
+ processesMap.set(nid, []);
183
+ processesMap.get(nid).push(`${label} (step ${step}/${stepCount})`);
177
184
  }
178
185
  }
179
- catch { /* skip */ }
186
+ }
187
+ catch { /* skip */ }
188
+ // Batch fetch cohesion
189
+ const cohesionMap = new Map();
190
+ try {
191
+ const rows = await executeQuery(repoId, `
192
+ MATCH (n)-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
193
+ WHERE n.id IN [${idList}]
194
+ RETURN n.id AS nodeId, c.cohesion AS cohesion
195
+ `);
196
+ for (const r of rows) {
197
+ const nid = r.nodeId || r[0];
198
+ const coh = r.cohesion ?? r[1] ?? 0;
199
+ if (nid)
200
+ cohesionMap.set(nid, coh);
201
+ }
202
+ }
203
+ catch { /* skip */ }
204
+ // Assemble enriched results
205
+ const enriched = [];
206
+ for (const sym of uniqueSymbols) {
180
207
  enriched.push({
181
208
  name: sym.name,
182
209
  filePath: sym.filePath,
183
- callers,
184
- callees,
185
- processes,
186
- cohesion,
210
+ callers: (callersMap.get(sym.nodeId) || []).slice(0, 3),
211
+ callees: (calleesMap.get(sym.nodeId) || []).slice(0, 3),
212
+ processes: processesMap.get(sym.nodeId) || [],
213
+ cohesion: cohesionMap.get(sym.nodeId) || 0,
187
214
  });
188
215
  }
189
216
  if (enriched.length === 0)
@@ -50,7 +50,7 @@ export declare const embedText: (text: string) => Promise<Float32Array>;
50
50
  */
51
51
  export declare const embedBatch: (texts: string[]) => Promise<Float32Array[]>;
52
52
  /**
53
- * Convert Float32Array to regular number array (for KuzuDB storage)
53
+ * Convert Float32Array to regular number array (for LadybugDB storage)
54
54
  */
55
55
  export declare const embeddingToArray: (embedding: Float32Array) => number[];
56
56
  /**
@@ -225,7 +225,7 @@ export const embedBatch = async (texts) => {
225
225
  return embeddings;
226
226
  };
227
227
  /**
228
- * Convert Float32Array to regular number array (for KuzuDB storage)
228
+ * Convert Float32Array to regular number array (for LadybugDB storage)
229
229
  */
230
230
  export const embeddingToArray = (embedding) => {
231
231
  return Array.from(embedding);
@@ -2,10 +2,10 @@
2
2
  * Embedding Pipeline Module
3
3
  *
4
4
  * Orchestrates the background embedding process:
5
- * 1. Query embeddable nodes from KuzuDB
5
+ * 1. Query embeddable nodes from LadybugDB
6
6
  * 2. Generate text representations
7
7
  * 3. Batch embed using transformers.js
8
- * 4. Update KuzuDB with embeddings
8
+ * 4. Update LadybugDB with embeddings
9
9
  * 5. Create vector index for semantic search
10
10
  */
11
11
  import { type EmbeddingProgress, type EmbeddingConfig, type SemanticSearchResult } from './types.js';
@@ -16,7 +16,7 @@ export type EmbeddingProgressCallback = (progress: EmbeddingProgress) => void;
16
16
  /**
17
17
  * Run the embedding pipeline
18
18
  *
19
- * @param executeQuery - Function to execute Cypher queries against KuzuDB
19
+ * @param executeQuery - Function to execute Cypher queries against LadybugDB
20
20
  * @param executeWithReusedStatement - Function to execute with reused prepared statement
21
21
  * @param onProgress - Callback for progress updates
22
22
  * @param config - Optional configuration override
@@ -2,10 +2,10 @@
2
2
  * Embedding Pipeline Module
3
3
  *
4
4
  * Orchestrates the background embedding process:
5
- * 1. Query embeddable nodes from KuzuDB
5
+ * 1. Query embeddable nodes from LadybugDB
6
6
  * 2. Generate text representations
7
7
  * 3. Batch embed using transformers.js
8
- * 4. Update KuzuDB with embeddings
8
+ * 4. Update LadybugDB with embeddings
9
9
  * 5. Create vector index for semantic search
10
10
  */
11
11
  import { initEmbedder, embedBatch, embedText, embeddingToArray, isEmbedderReady } from './embedder.js';
@@ -13,7 +13,7 @@ import { generateBatchEmbeddingTexts } from './text-generator.js';
13
13
  import { DEFAULT_EMBEDDING_CONFIG, EMBEDDABLE_LABELS, } from './types.js';
14
14
  const isDev = process.env.NODE_ENV === 'development';
15
15
  /**
16
- * Query all embeddable nodes from KuzuDB
16
+ * Query all embeddable nodes from LadybugDB
17
17
  * Uses table-specific queries (File has different schema than code elements)
18
18
  */
19
19
  const queryEmbeddableNodes = async (executeQuery) => {
@@ -76,7 +76,20 @@ const batchInsertEmbeddings = async (executeWithReusedStatement, updates) => {
76
76
  * Create the vector index for semantic search
77
77
  * Now indexes the separate CodeEmbedding table
78
78
  */
79
+ let vectorExtensionLoaded = false;
79
80
  const createVectorIndex = async (executeQuery) => {
81
+ // LadybugDB v0.15+ requires explicit VECTOR extension loading (once per session)
82
+ if (!vectorExtensionLoaded) {
83
+ try {
84
+ await executeQuery('INSTALL VECTOR');
85
+ await executeQuery('LOAD EXTENSION VECTOR');
86
+ vectorExtensionLoaded = true;
87
+ }
88
+ catch {
89
+ // Extension may already be loaded — CREATE_VECTOR_INDEX will fail clearly if not
90
+ vectorExtensionLoaded = true;
91
+ }
92
+ }
80
93
  const cypher = `
81
94
  CALL CREATE_VECTOR_INDEX('CodeEmbedding', 'code_embedding_idx', 'embedding', metric := 'cosine')
82
95
  `;
@@ -93,7 +106,7 @@ const createVectorIndex = async (executeQuery) => {
93
106
  /**
94
107
  * Run the embedding pipeline
95
108
  *
96
- * @param executeQuery - Function to execute Cypher queries against KuzuDB
109
+ * @param executeQuery - Function to execute Cypher queries against LadybugDB
97
110
  * @param executeWithReusedStatement - Function to execute with reused prepared statement
98
111
  * @param onProgress - Callback for progress updates
99
112
  * @param config - Optional configuration override
@@ -167,7 +180,7 @@ export const runEmbeddingPipeline = async (executeQuery, executeWithReusedStatem
167
180
  const texts = generateBatchEmbeddingTexts(batch, finalConfig);
168
181
  // Embed the batch
169
182
  const embeddings = await embedBatch(texts);
170
- // Update KuzuDB with embeddings
183
+ // Update LadybugDB with embeddings
171
184
  const updates = batch.map((node, i) => ({
172
185
  id: node.id,
173
186
  embedding: embeddingToArray(embeddings[i]),
@@ -253,49 +266,63 @@ export const semanticSearch = async (executeQuery, query, k = 10, maxDistance =
253
266
  if (embResults.length === 0) {
254
267
  return [];
255
268
  }
256
- // Get metadata for each result by querying each node table
257
- const results = [];
269
+ // Group results by label for batched metadata queries
270
+ const byLabel = new Map();
258
271
  for (const embRow of embResults) {
259
272
  const nodeId = embRow.nodeId ?? embRow[0];
260
273
  const distance = embRow.distance ?? embRow[1];
261
- // Extract label from node ID (format: Label:path:name)
262
274
  const labelEndIdx = nodeId.indexOf(':');
263
275
  const label = labelEndIdx > 0 ? nodeId.substring(0, labelEndIdx) : 'Unknown';
264
- // Query the specific table for this node
265
- // File nodes don't have startLine/endLine
276
+ if (!byLabel.has(label))
277
+ byLabel.set(label, []);
278
+ byLabel.get(label).push({ nodeId, distance });
279
+ }
280
+ // Batch-fetch metadata per label
281
+ const results = [];
282
+ for (const [label, items] of byLabel) {
283
+ const idList = items.map(i => `'${i.nodeId.replace(/'/g, "''")}'`).join(', ');
266
284
  try {
267
285
  let nodeQuery;
268
286
  if (label === 'File') {
269
287
  nodeQuery = `
270
- MATCH (n:File {id: '${nodeId.replace(/'/g, "''")}'})
271
- RETURN n.name AS name, n.filePath AS filePath
288
+ MATCH (n:File) WHERE n.id IN [${idList}]
289
+ RETURN n.id AS id, n.name AS name, n.filePath AS filePath
272
290
  `;
273
291
  }
274
292
  else {
275
293
  nodeQuery = `
276
- MATCH (n:${label} {id: '${nodeId.replace(/'/g, "''")}'})
277
- RETURN n.name AS name, n.filePath AS filePath,
294
+ MATCH (n:${label}) WHERE n.id IN [${idList}]
295
+ RETURN n.id AS id, n.name AS name, n.filePath AS filePath,
278
296
  n.startLine AS startLine, n.endLine AS endLine
279
297
  `;
280
298
  }
281
299
  const nodeRows = await executeQuery(nodeQuery);
282
- if (nodeRows.length > 0) {
283
- const nodeRow = nodeRows[0];
284
- results.push({
285
- nodeId,
286
- name: nodeRow.name ?? nodeRow[0] ?? '',
287
- label,
288
- filePath: nodeRow.filePath ?? nodeRow[1] ?? '',
289
- distance,
290
- startLine: label !== 'File' ? (nodeRow.startLine ?? nodeRow[2]) : undefined,
291
- endLine: label !== 'File' ? (nodeRow.endLine ?? nodeRow[3]) : undefined,
292
- });
300
+ const rowMap = new Map();
301
+ for (const row of nodeRows) {
302
+ const id = row.id ?? row[0];
303
+ rowMap.set(id, row);
304
+ }
305
+ for (const item of items) {
306
+ const nodeRow = rowMap.get(item.nodeId);
307
+ if (nodeRow) {
308
+ results.push({
309
+ nodeId: item.nodeId,
310
+ name: nodeRow.name ?? nodeRow[1] ?? '',
311
+ label,
312
+ filePath: nodeRow.filePath ?? nodeRow[2] ?? '',
313
+ distance: item.distance,
314
+ startLine: label !== 'File' ? (nodeRow.startLine ?? nodeRow[3]) : undefined,
315
+ endLine: label !== 'File' ? (nodeRow.endLine ?? nodeRow[4]) : undefined,
316
+ });
317
+ }
293
318
  }
294
319
  }
295
320
  catch {
296
321
  // Table might not exist, skip
297
322
  }
298
323
  }
324
+ // Re-sort by distance since batch queries may have mixed order
325
+ results.sort((a, b) => a.distance - b.distance);
299
326
  return results;
300
327
  };
301
328
  /**