@veewo/gitnexus 1.3.11 → 1.4.7-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
@@ -1,2 +1,7 @@
1
1
  import type { UnityParitySeed } from '../../core/ingestion/unity-parity-seed.js';
2
- export declare function loadUnityParitySeed(storagePath: string): Promise<UnityParitySeed | null>;
2
+ interface LoadUnityParitySeedOptions {
3
+ indexedCommit?: string;
4
+ }
5
+ export declare function loadUnityParitySeed(storagePath: string, options?: LoadUnityParitySeedOptions): Promise<UnityParitySeed | null>;
6
+ export declare function __resetUnityParitySeedLoaderCacheForTest(): void;
7
+ export {};
@@ -1,18 +1,46 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  const SEED_FILENAME = 'unity-parity-seed.json';
4
- export async function loadUnityParitySeed(storagePath) {
4
+ const DEFAULT_IDLE_MS = 30_000;
5
+ const DEFAULT_MAX_ENTRIES = 2;
6
+ const seedCache = new Map();
7
+ const inFlightLoads = new Map();
8
+ export async function loadUnityParitySeed(storagePath, options) {
5
9
  const seedPath = path.join(storagePath, SEED_FILENAME);
6
- let raw = '';
7
- try {
8
- raw = await fs.readFile(seedPath, 'utf-8');
10
+ const cacheKey = await buildSeedCacheKey(seedPath, storagePath, options?.indexedCommit);
11
+ if (!cacheKey) {
12
+ return null;
9
13
  }
10
- catch (error) {
11
- if (error.code === 'ENOENT') {
12
- return null;
13
- }
14
- throw error;
14
+ const cached = seedCache.get(cacheKey);
15
+ if (cached) {
16
+ touchCacheEntry(cacheKey, cached);
17
+ return cached.value;
18
+ }
19
+ const pending = inFlightLoads.get(cacheKey);
20
+ if (pending) {
21
+ return pending;
15
22
  }
23
+ const loadPromise = (async () => {
24
+ let raw = '';
25
+ try {
26
+ raw = await fs.readFile(seedPath, 'utf-8');
27
+ }
28
+ catch (error) {
29
+ if (error.code === 'ENOENT') {
30
+ return null;
31
+ }
32
+ throw error;
33
+ }
34
+ const parsed = parseSeed(raw);
35
+ setCacheEntry(cacheKey, parsed);
36
+ return parsed;
37
+ })().finally(() => {
38
+ inFlightLoads.delete(cacheKey);
39
+ });
40
+ inFlightLoads.set(cacheKey, loadPromise);
41
+ return loadPromise;
42
+ }
43
+ function parseSeed(raw) {
16
44
  try {
17
45
  const parsed = JSON.parse(raw);
18
46
  if (!parsed
@@ -28,3 +56,85 @@ export async function loadUnityParitySeed(storagePath) {
28
56
  return null;
29
57
  }
30
58
  }
59
+ async function buildSeedCacheKey(seedPath, storagePath, indexedCommit) {
60
+ try {
61
+ const stat = await fs.stat(seedPath);
62
+ const commitKey = String(indexedCommit || '').trim() || 'no-commit';
63
+ return `${storagePath}::${commitKey}::${Math.trunc(stat.mtimeMs)}`;
64
+ }
65
+ catch (error) {
66
+ if (error.code === 'ENOENT') {
67
+ return null;
68
+ }
69
+ throw error;
70
+ }
71
+ }
72
+ function setCacheEntry(cacheKey, value) {
73
+ const now = Date.now();
74
+ const existing = seedCache.get(cacheKey);
75
+ if (existing?.idleTimer) {
76
+ clearTimeout(existing.idleTimer);
77
+ }
78
+ const entry = {
79
+ value,
80
+ lastAccessMs: now,
81
+ };
82
+ seedCache.set(cacheKey, entry);
83
+ scheduleEviction(cacheKey, entry);
84
+ pruneOldestEntries(resolveMaxEntries());
85
+ }
86
+ function touchCacheEntry(cacheKey, entry) {
87
+ entry.lastAccessMs = Date.now();
88
+ if (entry.idleTimer) {
89
+ clearTimeout(entry.idleTimer);
90
+ }
91
+ scheduleEviction(cacheKey, entry);
92
+ }
93
+ function scheduleEviction(cacheKey, entry) {
94
+ const idleMs = resolveIdleMs();
95
+ entry.idleTimer = setTimeout(() => {
96
+ const current = seedCache.get(cacheKey);
97
+ if (!current || current !== entry) {
98
+ return;
99
+ }
100
+ seedCache.delete(cacheKey);
101
+ }, idleMs);
102
+ entry.idleTimer.unref?.();
103
+ }
104
+ function pruneOldestEntries(maxEntries) {
105
+ if (seedCache.size <= maxEntries) {
106
+ return;
107
+ }
108
+ const rows = [...seedCache.entries()].sort((left, right) => left[1].lastAccessMs - right[1].lastAccessMs);
109
+ const removeCount = rows.length - maxEntries;
110
+ for (let index = 0; index < removeCount; index += 1) {
111
+ const [key, entry] = rows[index];
112
+ if (entry.idleTimer) {
113
+ clearTimeout(entry.idleTimer);
114
+ }
115
+ seedCache.delete(key);
116
+ }
117
+ }
118
+ function resolveIdleMs() {
119
+ const parsed = Number.parseInt(String(process.env.GITNEXUS_UNITY_PARITY_SEED_CACHE_IDLE_MS || '').trim(), 10);
120
+ if (Number.isFinite(parsed) && parsed > 0) {
121
+ return parsed;
122
+ }
123
+ return DEFAULT_IDLE_MS;
124
+ }
125
+ function resolveMaxEntries() {
126
+ const parsed = Number.parseInt(String(process.env.GITNEXUS_UNITY_PARITY_SEED_CACHE_MAX_ENTRIES || '').trim(), 10);
127
+ if (Number.isFinite(parsed) && parsed > 0) {
128
+ return parsed;
129
+ }
130
+ return DEFAULT_MAX_ENTRIES;
131
+ }
132
+ export function __resetUnityParitySeedLoaderCacheForTest() {
133
+ for (const entry of seedCache.values()) {
134
+ if (entry.idleTimer) {
135
+ clearTimeout(entry.idleTimer);
136
+ }
137
+ }
138
+ seedCache.clear();
139
+ inFlightLoads.clear();
140
+ }
@@ -3,23 +3,111 @@ import assert from 'node:assert/strict';
3
3
  import os from 'node:os';
4
4
  import path from 'node:path';
5
5
  import fs from 'node:fs/promises';
6
- import { loadUnityParitySeed } from './unity-parity-seed-loader.js';
6
+ import { __resetUnityParitySeedLoaderCacheForTest, loadUnityParitySeed } from './unity-parity-seed-loader.js';
7
+ const baseSeed = {
8
+ version: 1,
9
+ symbolToScriptPath: { DoorObj: 'Assets/Code/DoorObj.cs' },
10
+ scriptPathToGuid: { 'Assets/Code/DoorObj.cs': 'abc123abc123abc123abc123abc123ab' },
11
+ guidToResourcePaths: { abc123abc123abc123abc123abc123ab: ['Assets/Prefabs/Door.prefab'] },
12
+ };
13
+ async function writeSeed(storagePath, symbol = 'DoorObj') {
14
+ await fs.writeFile(path.join(storagePath, 'unity-parity-seed.json'), JSON.stringify({
15
+ ...baseSeed,
16
+ symbolToScriptPath: { [symbol]: `Assets/Code/${symbol}.cs` },
17
+ scriptPathToGuid: { [`Assets/Code/${symbol}.cs`]: 'abc123abc123abc123abc123abc123ab' },
18
+ }), 'utf-8');
19
+ }
7
20
  test('loadUnityParitySeed returns null on missing file and parsed object on valid file', async () => {
8
21
  const storagePath = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-seed-loader-'));
9
22
  try {
10
23
  const missing = await loadUnityParitySeed(storagePath);
11
24
  assert.equal(missing, null);
12
- await fs.writeFile(path.join(storagePath, 'unity-parity-seed.json'), JSON.stringify({
13
- version: 1,
14
- symbolToScriptPath: { DoorObj: 'Assets/Code/DoorObj.cs' },
15
- scriptPathToGuid: { 'Assets/Code/DoorObj.cs': 'abc123abc123abc123abc123abc123ab' },
16
- guidToResourcePaths: { abc123abc123abc123abc123abc123ab: ['Assets/Prefabs/Door.prefab'] },
17
- }), 'utf-8');
25
+ await writeSeed(storagePath, 'DoorObj');
18
26
  const loaded = await loadUnityParitySeed(storagePath);
19
27
  assert.equal(loaded?.version, 1);
20
28
  assert.equal(loaded?.symbolToScriptPath.DoorObj, 'Assets/Code/DoorObj.cs');
21
29
  }
22
30
  finally {
31
+ __resetUnityParitySeedLoaderCacheForTest();
32
+ await fs.rm(storagePath, { recursive: true, force: true });
33
+ }
34
+ });
35
+ test('loadUnityParitySeed deduplicates concurrent requests for same storage key', async (t) => {
36
+ const storagePath = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-seed-loader-'));
37
+ try {
38
+ await writeSeed(storagePath, 'ConcurrentSymbol');
39
+ const readFileOriginal = fs.readFile.bind(fs);
40
+ let readFileCalls = 0;
41
+ t.mock.method(fs, 'readFile', async (...args) => {
42
+ readFileCalls += 1;
43
+ await new Promise((resolve) => setTimeout(resolve, 20));
44
+ return readFileOriginal(...args);
45
+ });
46
+ const results = await Promise.all(Array.from({ length: 10 }, () => loadUnityParitySeed(storagePath)));
47
+ assert.equal(results.every((row) => row?.symbolToScriptPath.ConcurrentSymbol), true);
48
+ assert.equal(readFileCalls, 1);
49
+ }
50
+ finally {
51
+ __resetUnityParitySeedLoaderCacheForTest();
52
+ await fs.rm(storagePath, { recursive: true, force: true });
53
+ }
54
+ });
55
+ test('loadUnityParitySeed evicts idle cache entry after ttl', async (t) => {
56
+ const storagePath = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-seed-loader-'));
57
+ const idleEnvKey = 'GITNEXUS_UNITY_PARITY_SEED_CACHE_IDLE_MS';
58
+ const previousIdle = process.env[idleEnvKey];
59
+ process.env[idleEnvKey] = '15';
60
+ try {
61
+ await writeSeed(storagePath, 'IdleSymbol');
62
+ const readFileOriginal = fs.readFile.bind(fs);
63
+ let readFileCalls = 0;
64
+ t.mock.method(fs, 'readFile', async (...args) => {
65
+ readFileCalls += 1;
66
+ return readFileOriginal(...args);
67
+ });
68
+ await loadUnityParitySeed(storagePath);
69
+ await loadUnityParitySeed(storagePath);
70
+ assert.equal(readFileCalls, 1);
71
+ await new Promise((resolve) => setTimeout(resolve, 30));
72
+ await loadUnityParitySeed(storagePath);
73
+ assert.equal(readFileCalls, 2);
74
+ }
75
+ finally {
76
+ __resetUnityParitySeedLoaderCacheForTest();
77
+ if (previousIdle === undefined) {
78
+ delete process.env[idleEnvKey];
79
+ }
80
+ else {
81
+ process.env[idleEnvKey] = previousIdle;
82
+ }
83
+ await fs.rm(storagePath, { recursive: true, force: true });
84
+ }
85
+ });
86
+ test('loadUnityParitySeed invalidates cache when seed mtime changes', async (t) => {
87
+ const storagePath = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-seed-loader-'));
88
+ const seedPath = path.join(storagePath, 'unity-parity-seed.json');
89
+ try {
90
+ await writeSeed(storagePath, 'VersionA');
91
+ const readFileOriginal = fs.readFile.bind(fs);
92
+ let readFileCalls = 0;
93
+ t.mock.method(fs, 'readFile', async (...args) => {
94
+ readFileCalls += 1;
95
+ return readFileOriginal(...args);
96
+ });
97
+ const first = await loadUnityParitySeed(storagePath);
98
+ const second = await loadUnityParitySeed(storagePath);
99
+ assert.equal(first?.symbolToScriptPath.VersionA, 'Assets/Code/VersionA.cs');
100
+ assert.equal(second?.symbolToScriptPath.VersionA, 'Assets/Code/VersionA.cs');
101
+ assert.equal(readFileCalls, 1);
102
+ await new Promise((resolve) => setTimeout(resolve, 10));
103
+ await writeSeed(storagePath, 'VersionB');
104
+ await fs.utimes(seedPath, new Date(), new Date());
105
+ const third = await loadUnityParitySeed(storagePath);
106
+ assert.equal(third?.symbolToScriptPath.VersionB, 'Assets/Code/VersionB.cs');
107
+ assert.equal(readFileCalls, 2);
108
+ }
109
+ finally {
110
+ __resetUnityParitySeedLoaderCacheForTest();
23
111
  await fs.rm(storagePath, { recursive: true, force: true });
24
112
  }
25
113
  });
@@ -186,7 +186,7 @@ async function getContextResource(backend, repoName) {
186
186
  lines.push(' - cypher: Raw graph queries');
187
187
  lines.push(' - list_repos: Discover all indexed repositories');
188
188
  lines.push('');
189
- lines.push('re_index: If data is stale, ask user whether to run `npx -y gitnexus analyze` (reuses previous analyze scope/options unless `--no-reuse-options` is passed). If user declines, clearly state retrieval may not reflect current code. For build/analyze/test commands, use 10-30 minute timeout; on failure/timeout, report exact tool output and do not auto-retry or silently switch to glob/grep fallback.');
189
+ lines.push('re_index: If data is stale, ask user whether to run `npx -y @veewo/gitnexus@latest analyze` (reuses previous analyze scope/options unless `--no-reuse-options` is passed). If user declines, clearly state retrieval may not reflect current code. For build/analyze/test commands, use 10-30 minute timeout; on failure/timeout, report exact tool output and do not auto-retry or silently switch to glob/grep fallback.');
190
190
  lines.push('');
191
191
  lines.push('resources_available:');
192
192
  lines.push(' - gitnexus://repos: All indexed repositories');
@@ -372,7 +372,7 @@ async function getProcessDetailResource(name, backend, repoName) {
372
372
  async function getSetupResource(backend) {
373
373
  const repos = await backend.listRepos();
374
374
  if (repos.length === 0) {
375
- return '# GitNexus\n\nNo repositories indexed. Run: `npx gitnexus analyze` in a repository.';
375
+ return '# GitNexus\n\nNo repositories indexed. Run: `npx -y @veewo/gitnexus@latest analyze` in a repository.';
376
376
  }
377
377
  const sections = [];
378
378
  for (const repo of repos) {
@@ -10,8 +10,9 @@
10
10
  * Tools: list_repos, query, cypher, context, impact, detect_changes, rename
11
11
  * Resources: repos, repo/{name}/context, repo/{name}/clusters, ...
12
12
  */
13
+ import { createRequire } from 'module';
13
14
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
14
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
15
+ import { CompatibleStdioServerTransport } from './compatible-stdio-transport.js';
15
16
  import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, ListResourceTemplatesRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
16
17
  import { GITNEXUS_TOOLS } from './tools.js';
17
18
  import { getResourceDefinitions, getResourceTemplates, readResource } from './resources.js';
@@ -59,9 +60,11 @@ function getNextStepHint(toolName, args) {
59
60
  * Transport-agnostic — caller connects the desired transport.
60
61
  */
61
62
  export function createMCPServer(backend) {
63
+ const require = createRequire(import.meta.url);
64
+ const pkgVersion = require('../../package.json').version;
62
65
  const server = new Server({
63
66
  name: 'gitnexus',
64
- version: '1.1.9',
67
+ version: pkgVersion,
65
68
  }, {
66
69
  capabilities: {
67
70
  tools: {},
@@ -235,17 +238,29 @@ Follow these steps:
235
238
  export async function startMCPServer(backend) {
236
239
  const server = createMCPServer(backend);
237
240
  // Connect to stdio transport
238
- const transport = new StdioServerTransport();
241
+ const transport = new CompatibleStdioServerTransport();
239
242
  await server.connect(transport);
240
- // Handle graceful shutdown
241
- process.on('SIGINT', async () => {
242
- await backend.disconnect();
243
- await server.close();
244
- process.exit(0);
245
- });
246
- process.on('SIGTERM', async () => {
247
- await backend.disconnect();
248
- await server.close();
243
+ // Graceful shutdown helper
244
+ let shuttingDown = false;
245
+ const shutdown = async () => {
246
+ if (shuttingDown)
247
+ return;
248
+ shuttingDown = true;
249
+ try {
250
+ await backend.disconnect();
251
+ }
252
+ catch { }
253
+ try {
254
+ await server.close();
255
+ }
256
+ catch { }
249
257
  process.exit(0);
250
- });
258
+ };
259
+ // Handle graceful shutdown
260
+ process.on('SIGINT', shutdown);
261
+ process.on('SIGTERM', shutdown);
262
+ // Handle stdio errors — stdin close means the parent process is gone
263
+ process.stdin.on('end', shutdown);
264
+ process.stdin.on('error', () => shutdown());
265
+ process.stdout.on('error', () => shutdown());
251
266
  }
@@ -4,14 +4,14 @@
4
4
  * Checks if the GitNexus index is behind the current git HEAD.
5
5
  * Returns a hint for the LLM to call analyze if stale.
6
6
  */
7
- import { execSync } from 'child_process';
7
+ import { execFileSync } from 'child_process';
8
8
  /**
9
9
  * Check how many commits the index is behind HEAD
10
10
  */
11
11
  export function checkStaleness(repoPath, lastCommit) {
12
12
  try {
13
13
  // Get count of commits between lastCommit and HEAD
14
- const result = execSync(`git rev-list --count ${lastCommit}..HEAD`, { cwd: repoPath, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
14
+ const result = execFileSync('git', ['rev-list', '--count', `${lastCommit}..HEAD`], { cwd: repoPath, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
15
15
  const commitsBehind = parseInt(result, 10) || 0;
16
16
  if (commitsBehind > 0) {
17
17
  return {
package/dist/mcp/tools.js CHANGED
@@ -78,7 +78,7 @@ SCHEMA:
78
78
  - Nodes: File, Folder, Function, Class, Interface, Method, CodeElement, Community, Process
79
79
  - Multi-language nodes (use backticks): \`Struct\`, \`Enum\`, \`Trait\`, \`Impl\`, etc.
80
80
  - All edges via single CodeRelation table with 'type' property
81
- - Edge types: CONTAINS, DEFINES, CALLS, IMPORTS, EXTENDS, IMPLEMENTS, MEMBER_OF, STEP_IN_PROCESS
81
+ - Edge types: CONTAINS, DEFINES, CALLS, IMPORTS, EXTENDS, IMPLEMENTS, HAS_METHOD, OVERRIDES, MEMBER_OF, STEP_IN_PROCESS
82
82
  - Edge properties: type (STRING), confidence (DOUBLE), reason (STRING), step (INT32)
83
83
 
84
84
  EXAMPLES:
@@ -91,6 +91,15 @@ EXAMPLES:
91
91
  • Trace a process:
92
92
  MATCH (s)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process) WHERE p.heuristicLabel = "UserLogin" RETURN s.name, r.step ORDER BY r.step
93
93
 
94
+ • Find all methods of a class:
95
+ MATCH (c:Class {name: "UserService"})-[r:CodeRelation {type: 'HAS_METHOD'}]->(m:Method) RETURN m.name, m.parameterCount, m.returnType
96
+
97
+ • Find method overrides (MRO resolution):
98
+ MATCH (winner:Method)-[r:CodeRelation {type: 'OVERRIDES'}]->(loser:Method) RETURN winner.name, winner.filePath, loser.filePath, r.reason
99
+
100
+ • Detect diamond inheritance:
101
+ MATCH (d:Class)-[:CodeRelation {type: 'EXTENDS'}]->(b1), (d)-[:CodeRelation {type: 'EXTENDS'}]->(b2), (b1)-[:CodeRelation {type: 'EXTENDS'}]->(a), (b2)-[:CodeRelation {type: 'EXTENDS'}]->(a) WHERE b1 <> b2 RETURN d.name, b1.name, b2.name, a.name
102
+
94
103
  OUTPUT: Returns { markdown, row_count } — results formatted as a Markdown table for easy reading.
95
104
 
96
105
  TIPS:
@@ -208,7 +217,7 @@ Depth groups:
208
217
  - d=2: LIKELY AFFECTED (indirect)
209
218
  - d=3: MAY NEED TESTING (transitive)
210
219
 
211
- EdgeType: CALLS, IMPORTS, EXTENDS, IMPLEMENTS
220
+ EdgeType: CALLS, IMPORTS, EXTENDS, IMPLEMENTS, HAS_METHOD, OVERRIDES
212
221
  Confidence: 1.0 = certain, <0.8 = fuzzy match`,
213
222
  inputSchema: {
214
223
  type: 'object',
@@ -218,7 +227,7 @@ Confidence: 1.0 = certain, <0.8 = fuzzy match`,
218
227
  file_path: { type: 'string', description: 'Optional file path filter to disambiguate target name' },
219
228
  direction: { type: 'string', description: 'upstream (what depends on this) or downstream (what this depends on)' },
220
229
  maxDepth: { type: 'number', description: 'Max relationship depth (default: 3)', default: 3 },
221
- relationTypes: { type: 'array', items: { type: 'string' }, description: 'Filter: CALLS, IMPORTS, EXTENDS, IMPLEMENTS (default: usage-based)' },
230
+ relationTypes: { type: 'array', items: { type: 'string' }, description: 'Filter: CALLS, IMPORTS, EXTENDS, IMPLEMENTS, HAS_METHOD, OVERRIDES (default: usage-based)' },
222
231
  includeTests: { type: 'boolean', description: 'Include test files (default: false)' },
223
232
  minConfidence: { type: 'number', description: 'Minimum confidence 0-1 (default: 0.3)' },
224
233
  repo: { type: 'string', description: 'Repository name or path. Omit if only one repo is indexed.' },
@@ -12,9 +12,9 @@ import cors from 'cors';
12
12
  import path from 'path';
13
13
  import fs from 'fs/promises';
14
14
  import { loadMeta, listRegisteredRepos } from '../storage/repo-manager.js';
15
- import { executeQuery, closeKuzu, withKuzuDb } from '../core/kuzu/kuzu-adapter.js';
16
- import { NODE_TABLES } from '../core/kuzu/schema.js';
17
- import { searchFTSFromKuzu } from '../core/search/bm25-index.js';
15
+ import { executeQuery, closeLbug, withLbugDb } from '../core/lbug/lbug-adapter.js';
16
+ import { NODE_TABLES } from '../core/lbug/schema.js';
17
+ import { searchFTSFromLbug } from '../core/search/bm25-index.js';
18
18
  import { hybridSearch } from '../core/search/hybrid-search.js';
19
19
  // Embedding imports are lazy (dynamic import) to avoid loading onnxruntime-node
20
20
  // at server startup — crashes on unsupported Node ABI versions (#89)
@@ -171,8 +171,8 @@ export const createServer = async (port, host = '127.0.0.1') => {
171
171
  res.status(404).json({ error: 'Repository not found' });
172
172
  return;
173
173
  }
174
- const kuzuPath = path.join(entry.storagePath, 'kuzu');
175
- const graph = await withKuzuDb(kuzuPath, async () => buildGraph());
174
+ const lbugPath = path.join(entry.storagePath, 'lbug');
175
+ const graph = await withLbugDb(lbugPath, async () => buildGraph());
176
176
  res.json(graph);
177
177
  }
178
178
  catch (err) {
@@ -192,8 +192,8 @@ export const createServer = async (port, host = '127.0.0.1') => {
192
192
  res.status(404).json({ error: 'Repository not found' });
193
193
  return;
194
194
  }
195
- const kuzuPath = path.join(entry.storagePath, 'kuzu');
196
- const result = await withKuzuDb(kuzuPath, () => executeQuery(cypher));
195
+ const lbugPath = path.join(entry.storagePath, 'lbug');
196
+ const result = await withLbugDb(lbugPath, () => executeQuery(cypher));
197
197
  res.json({ result });
198
198
  }
199
199
  catch (err) {
@@ -213,19 +213,19 @@ export const createServer = async (port, host = '127.0.0.1') => {
213
213
  res.status(404).json({ error: 'Repository not found' });
214
214
  return;
215
215
  }
216
- const kuzuPath = path.join(entry.storagePath, 'kuzu');
216
+ const lbugPath = path.join(entry.storagePath, 'lbug');
217
217
  const parsedLimit = Number(req.body.limit ?? 10);
218
218
  const limit = Number.isFinite(parsedLimit)
219
219
  ? Math.max(1, Math.min(100, Math.trunc(parsedLimit)))
220
220
  : 10;
221
- const results = await withKuzuDb(kuzuPath, async () => {
221
+ const results = await withLbugDb(lbugPath, async () => {
222
222
  const { isEmbedderReady } = await import('../core/embeddings/embedder.js');
223
223
  if (isEmbedderReady()) {
224
224
  const { semanticSearch } = await import('../core/embeddings/embedding-pipeline.js');
225
225
  return hybridSearch(query, limit, executeQuery, semanticSearch);
226
226
  }
227
227
  // FTS-only fallback when embeddings aren't loaded
228
- return searchFTSFromKuzu(query, limit);
228
+ return searchFTSFromLbug(query, limit);
229
229
  });
230
230
  res.json({ results });
231
231
  }
@@ -331,11 +331,11 @@ export const createServer = async (port, host = '127.0.0.1') => {
331
331
  const server = app.listen(port, host, () => {
332
332
  console.log(`GitNexus server running on http://${host}:${port}`);
333
333
  });
334
- // Graceful shutdown — close Express + KuzuDB cleanly
334
+ // Graceful shutdown — close Express + LadybugDB cleanly
335
335
  const shutdown = async () => {
336
336
  server.close();
337
337
  await cleanupMcp();
338
- await closeKuzu();
338
+ await closeLbug();
339
339
  await backend.disconnect();
340
340
  process.exit(0);
341
341
  };
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Mounts the GitNexus MCP server on Express using StreamableHTTP transport.
5
5
  * Each connecting client gets its own stateful session; the LocalBackend
6
- * is shared across all sessions (thread-safe — lazy KuzuDB per repo).
6
+ * is shared across all sessions (thread-safe — lazy LadybugDB per repo).
7
7
  *
8
8
  * Sessions are cleaned up on explicit close or after SESSION_TTL_MS of inactivity
9
9
  * (guards against network drops that never trigger onclose).
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Mounts the GitNexus MCP server on Express using StreamableHTTP transport.
5
5
  * Each connecting client gets its own stateful session; the LocalBackend
6
- * is shared across all sessions (thread-safe — lazy KuzuDB per repo).
6
+ * is shared across all sessions (thread-safe — lazy LadybugDB per repo).
7
7
  *
8
8
  * Sessions are cleaned up on explicit close or after SESSION_TTL_MS of inactivity
9
9
  * (guards against network drops that never trigger onclose).
@@ -1,4 +1,5 @@
1
1
  import { execSync } from 'child_process';
2
+ import path from 'path';
2
3
  // Git utilities for repository detection, commit tracking, and diff analysis
3
4
  export const isGitRepo = (repoPath) => {
4
5
  try {
@@ -22,9 +23,11 @@ export const getCurrentCommit = (repoPath) => {
22
23
  */
23
24
  export const getGitRoot = (fromPath) => {
24
25
  try {
25
- return execSync('git rev-parse --show-toplevel', { cwd: fromPath })
26
+ const raw = execSync('git rev-parse --show-toplevel', { cwd: fromPath })
26
27
  .toString()
27
28
  .trim();
29
+ // On Windows, git returns /d/Projects/Foo — path.resolve normalizes to D:\Projects\Foo
30
+ return path.resolve(raw);
28
31
  }
29
32
  catch {
30
33
  return null;
@@ -27,7 +27,7 @@ export interface RepoMeta {
27
27
  export interface IndexedRepo {
28
28
  repoPath: string;
29
29
  storagePath: string;
30
- kuzuPath: string;
30
+ lbugPath: string;
31
31
  metaPath: string;
32
32
  meta: RepoMeta;
33
33
  }
@@ -56,9 +56,27 @@ export declare const getStoragePath: (repoPath: string) => string;
56
56
  */
57
57
  export declare const getStoragePaths: (repoPath: string) => {
58
58
  storagePath: string;
59
- kuzuPath: string;
59
+ lbugPath: string;
60
60
  metaPath: string;
61
61
  };
62
+ /**
63
+ * Check whether a KuzuDB index exists in the given storage path.
64
+ * Non-destructive — safe to call from status commands.
65
+ */
66
+ export declare const hasKuzuIndex: (storagePath: string) => Promise<boolean>;
67
+ /**
68
+ * Clean up stale KuzuDB files after migration to LadybugDB.
69
+ *
70
+ * Returns:
71
+ * found — true if .gitnexus/kuzu existed and was deleted
72
+ * needsReindex — true if kuzu existed but lbug does not (re-analyze required)
73
+ *
74
+ * Callers own the user-facing messaging; this function only deletes files.
75
+ */
76
+ export declare const cleanupOldKuzuFiles: (storagePath: string) => Promise<{
77
+ found: boolean;
78
+ needsReindex: boolean;
79
+ }>;
62
80
  /**
63
81
  * Load metadata from an indexed repo
64
82
  */