gitnexus 1.4.6 → 1.4.8

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 (99) hide show
  1. package/README.md +22 -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.js +2 -1
  7. package/dist/cli/setup.js +78 -1
  8. package/dist/config/supported-languages.d.ts +30 -0
  9. package/dist/config/supported-languages.js +30 -0
  10. package/dist/core/embeddings/embedder.d.ts +6 -1
  11. package/dist/core/embeddings/embedder.js +65 -5
  12. package/dist/core/embeddings/embedding-pipeline.js +11 -9
  13. package/dist/core/embeddings/http-client.d.ts +31 -0
  14. package/dist/core/embeddings/http-client.js +179 -0
  15. package/dist/core/embeddings/index.d.ts +1 -0
  16. package/dist/core/embeddings/index.js +1 -0
  17. package/dist/core/embeddings/types.d.ts +1 -1
  18. package/dist/core/graph/types.d.ts +4 -3
  19. package/dist/core/ingestion/ast-helpers.d.ts +80 -0
  20. package/dist/core/ingestion/ast-helpers.js +738 -0
  21. package/dist/core/ingestion/call-analysis.d.ts +73 -0
  22. package/dist/core/ingestion/call-analysis.js +490 -0
  23. package/dist/core/ingestion/call-processor.d.ts +55 -2
  24. package/dist/core/ingestion/call-processor.js +673 -108
  25. package/dist/core/ingestion/call-routing.d.ts +23 -2
  26. package/dist/core/ingestion/call-routing.js +21 -0
  27. package/dist/core/ingestion/entry-point-scoring.js +36 -26
  28. package/dist/core/ingestion/framework-detection.d.ts +10 -2
  29. package/dist/core/ingestion/framework-detection.js +49 -12
  30. package/dist/core/ingestion/heritage-processor.js +47 -49
  31. package/dist/core/ingestion/import-processor.d.ts +1 -1
  32. package/dist/core/ingestion/import-processor.js +103 -194
  33. package/dist/core/ingestion/import-resolution.d.ts +101 -0
  34. package/dist/core/ingestion/import-resolution.js +251 -0
  35. package/dist/core/ingestion/language-config.d.ts +3 -0
  36. package/dist/core/ingestion/language-config.js +13 -0
  37. package/dist/core/ingestion/markdown-processor.d.ts +17 -0
  38. package/dist/core/ingestion/markdown-processor.js +124 -0
  39. package/dist/core/ingestion/mro-processor.js +8 -3
  40. package/dist/core/ingestion/named-binding-extraction.d.ts +9 -43
  41. package/dist/core/ingestion/named-binding-extraction.js +89 -79
  42. package/dist/core/ingestion/parsing-processor.d.ts +3 -2
  43. package/dist/core/ingestion/parsing-processor.js +27 -60
  44. package/dist/core/ingestion/pipeline.d.ts +10 -0
  45. package/dist/core/ingestion/pipeline.js +425 -4
  46. package/dist/core/ingestion/resolution-context.d.ts +5 -0
  47. package/dist/core/ingestion/resolution-context.js +7 -4
  48. package/dist/core/ingestion/resolvers/index.d.ts +1 -1
  49. package/dist/core/ingestion/resolvers/index.js +1 -1
  50. package/dist/core/ingestion/resolvers/jvm.d.ts +2 -1
  51. package/dist/core/ingestion/resolvers/jvm.js +25 -9
  52. package/dist/core/ingestion/resolvers/php.d.ts +14 -0
  53. package/dist/core/ingestion/resolvers/php.js +43 -3
  54. package/dist/core/ingestion/resolvers/utils.d.ts +5 -0
  55. package/dist/core/ingestion/resolvers/utils.js +16 -0
  56. package/dist/core/ingestion/symbol-table.d.ts +29 -3
  57. package/dist/core/ingestion/symbol-table.js +42 -9
  58. package/dist/core/ingestion/tree-sitter-queries.d.ts +12 -12
  59. package/dist/core/ingestion/tree-sitter-queries.js +243 -2
  60. package/dist/core/ingestion/type-env.d.ts +28 -1
  61. package/dist/core/ingestion/type-env.js +451 -72
  62. package/dist/core/ingestion/type-extractors/c-cpp.d.ts +5 -0
  63. package/dist/core/ingestion/type-extractors/c-cpp.js +146 -2
  64. package/dist/core/ingestion/type-extractors/csharp.js +189 -16
  65. package/dist/core/ingestion/type-extractors/go.js +45 -0
  66. package/dist/core/ingestion/type-extractors/index.d.ts +1 -1
  67. package/dist/core/ingestion/type-extractors/index.js +1 -1
  68. package/dist/core/ingestion/type-extractors/jvm.js +244 -69
  69. package/dist/core/ingestion/type-extractors/php.js +31 -4
  70. package/dist/core/ingestion/type-extractors/python.js +89 -17
  71. package/dist/core/ingestion/type-extractors/ruby.js +17 -2
  72. package/dist/core/ingestion/type-extractors/rust.js +72 -4
  73. package/dist/core/ingestion/type-extractors/shared.d.ts +12 -2
  74. package/dist/core/ingestion/type-extractors/shared.js +115 -13
  75. package/dist/core/ingestion/type-extractors/swift.js +7 -6
  76. package/dist/core/ingestion/type-extractors/types.d.ts +54 -11
  77. package/dist/core/ingestion/type-extractors/typescript.js +171 -9
  78. package/dist/core/ingestion/utils.d.ts +2 -95
  79. package/dist/core/ingestion/utils.js +3 -892
  80. package/dist/core/ingestion/workers/parse-worker.d.ts +36 -11
  81. package/dist/core/ingestion/workers/parse-worker.js +116 -95
  82. package/dist/core/lbug/csv-generator.js +18 -1
  83. package/dist/core/lbug/lbug-adapter.d.ts +12 -0
  84. package/dist/core/lbug/lbug-adapter.js +71 -4
  85. package/dist/core/lbug/schema.d.ts +6 -4
  86. package/dist/core/lbug/schema.js +27 -3
  87. package/dist/mcp/core/embedder.js +11 -3
  88. package/dist/mcp/core/lbug-adapter.d.ts +22 -0
  89. package/dist/mcp/core/lbug-adapter.js +178 -23
  90. package/dist/mcp/local/local-backend.d.ts +22 -0
  91. package/dist/mcp/local/local-backend.js +136 -32
  92. package/dist/mcp/resources.js +13 -0
  93. package/dist/mcp/server.js +26 -4
  94. package/dist/mcp/tools.js +17 -7
  95. package/dist/server/api.d.ts +19 -1
  96. package/dist/server/api.js +66 -6
  97. package/dist/storage/git.d.ts +12 -0
  98. package/dist/storage/git.js +21 -0
  99. package/package.json +12 -4
@@ -15,6 +15,26 @@ export declare function isTestFilePath(filePath: string): boolean;
15
15
  export declare const VALID_NODE_LABELS: Set<string>;
16
16
  /** Valid relation types for impact analysis filtering */
17
17
  export declare const VALID_RELATION_TYPES: Set<string>;
18
+ /**
19
+ * Per-relation-type confidence floor for impact analysis.
20
+ *
21
+ * When the graph stores a relation with a confidence value, that stored
22
+ * value is used as-is (it reflects resolution-tier accuracy from analysis
23
+ * time). This map provides the floor for each edge type when no stored
24
+ * confidence is available, and is also used for display / tooltip hints.
25
+ *
26
+ * Rationale:
27
+ * CALLS / IMPORTS – direct, strongly-typed references → 0.9
28
+ * EXTENDS – class hierarchy, statically verifiable → 0.85
29
+ * IMPLEMENTS – interface contract, statically verifiable → 0.85
30
+ * OVERRIDES – method override, statically verifiable → 0.85
31
+ * HAS_METHOD – structural containment → 0.95
32
+ * HAS_PROPERTY – structural containment → 0.95
33
+ * ACCESSES – field read/write, may be indirect → 0.8
34
+ * CONTAINS – folder/file containment → 0.95
35
+ * (unknown type) – conservative fallback → 0.5
36
+ */
37
+ export declare const IMPACT_RELATION_CONFIDENCE: Readonly<Record<string, number>>;
18
38
  /** Regex to detect write operations in user-supplied Cypher queries */
19
39
  export declare const CYPHER_WRITE_RE: RegExp;
20
40
  /** Check if a Cypher query contains write operations */
@@ -42,6 +62,8 @@ export declare class LocalBackend {
42
62
  private repos;
43
63
  private contextCache;
44
64
  private initializedRepos;
65
+ private reinitPromises;
66
+ private lastStalenessCheck;
45
67
  /**
46
68
  * Initialize from the global registry.
47
69
  * Returns true if at least one repo is available.
@@ -37,7 +37,42 @@ export const VALID_NODE_LABELS = new Set([
37
37
  'Record', 'Delegate', 'Annotation', 'Constructor', 'Template', 'Module',
38
38
  ]);
39
39
  /** Valid relation types for impact analysis filtering */
40
- export const VALID_RELATION_TYPES = new Set(['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS', 'HAS_METHOD', 'OVERRIDES']);
40
+ export const VALID_RELATION_TYPES = new Set(['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS', 'HAS_METHOD', 'HAS_PROPERTY', 'OVERRIDES', 'ACCESSES']);
41
+ /**
42
+ * Per-relation-type confidence floor for impact analysis.
43
+ *
44
+ * When the graph stores a relation with a confidence value, that stored
45
+ * value is used as-is (it reflects resolution-tier accuracy from analysis
46
+ * time). This map provides the floor for each edge type when no stored
47
+ * confidence is available, and is also used for display / tooltip hints.
48
+ *
49
+ * Rationale:
50
+ * CALLS / IMPORTS – direct, strongly-typed references → 0.9
51
+ * EXTENDS – class hierarchy, statically verifiable → 0.85
52
+ * IMPLEMENTS – interface contract, statically verifiable → 0.85
53
+ * OVERRIDES – method override, statically verifiable → 0.85
54
+ * HAS_METHOD – structural containment → 0.95
55
+ * HAS_PROPERTY – structural containment → 0.95
56
+ * ACCESSES – field read/write, may be indirect → 0.8
57
+ * CONTAINS – folder/file containment → 0.95
58
+ * (unknown type) – conservative fallback → 0.5
59
+ */
60
+ export const IMPACT_RELATION_CONFIDENCE = {
61
+ CALLS: 0.9,
62
+ IMPORTS: 0.9,
63
+ EXTENDS: 0.85,
64
+ IMPLEMENTS: 0.85,
65
+ OVERRIDES: 0.85,
66
+ HAS_METHOD: 0.95,
67
+ HAS_PROPERTY: 0.95,
68
+ ACCESSES: 0.8,
69
+ CONTAINS: 0.95,
70
+ };
71
+ /**
72
+ * Return the confidence floor for a given relation type.
73
+ * Falls back to 0.5 for unknown types so they are not silently elevated.
74
+ */
75
+ const confidenceForRelType = (relType) => IMPACT_RELATION_CONFIDENCE[relType ?? ''] ?? 0.5;
41
76
  /** Regex to detect write operations in user-supplied Cypher queries */
42
77
  export const CYPHER_WRITE_RE = /\b(CREATE|DELETE|SET|MERGE|REMOVE|DROP|ALTER|COPY|DETACH)\b/i;
43
78
  /** Check if a Cypher query contains write operations */
@@ -53,6 +88,8 @@ export class LocalBackend {
53
88
  repos = new Map();
54
89
  contextCache = new Map();
55
90
  initializedRepos = new Set();
91
+ reinitPromises = new Map();
92
+ lastStalenessCheck = new Map();
56
93
  // ─── Initialization ──────────────────────────────────────────────
57
94
  /**
58
95
  * Initialize from the global registry.
@@ -195,12 +232,53 @@ export class LocalBackend {
195
232
  }
196
233
  // ─── Lazy LadybugDB Init ────────────────────────────────────────────
197
234
  async ensureInitialized(repoId) {
198
- // Always check the actual pool the idle timer may have evicted the connection
199
- if (this.initializedRepos.has(repoId) && isLbugReady(repoId))
200
- return;
235
+ // If a reinit is already in progress for this repo, wait for it
236
+ const pending = this.reinitPromises.get(repoId);
237
+ if (pending)
238
+ return pending;
201
239
  const handle = this.repos.get(repoId);
202
240
  if (!handle)
203
241
  throw new Error(`Unknown repo: ${repoId}`);
242
+ // Check if the index was rebuilt since we opened the connection (#297).
243
+ // Throttle staleness checks to at most once per 5 seconds per repo to
244
+ // avoid an fs.readFile round-trip on every tool invocation.
245
+ if (this.initializedRepos.has(repoId) && isLbugReady(repoId)) {
246
+ const now = Date.now();
247
+ const lastCheck = this.lastStalenessCheck.get(repoId) ?? 0;
248
+ if (now - lastCheck < 5000)
249
+ return; // Checked recently — skip
250
+ this.lastStalenessCheck.set(repoId, now);
251
+ try {
252
+ const metaPath = path.join(handle.storagePath, 'meta.json');
253
+ const metaRaw = await fs.readFile(metaPath, 'utf-8');
254
+ const meta = JSON.parse(metaRaw);
255
+ if (meta.indexedAt && meta.indexedAt !== handle.indexedAt) {
256
+ // Index was rebuilt — close stale connection and re-init.
257
+ // Wrap in reinitPromises to prevent TOCTOU race where concurrent
258
+ // callers both detect staleness and double-close the pool.
259
+ const reinit = (async () => {
260
+ try {
261
+ await closeLbug(repoId);
262
+ this.initializedRepos.delete(repoId);
263
+ handle.indexedAt = meta.indexedAt;
264
+ await initLbug(repoId, handle.lbugPath);
265
+ this.initializedRepos.add(repoId);
266
+ }
267
+ finally {
268
+ this.reinitPromises.delete(repoId);
269
+ }
270
+ })();
271
+ this.reinitPromises.set(repoId, reinit);
272
+ return reinit;
273
+ }
274
+ else {
275
+ return; // Pool is current
276
+ }
277
+ }
278
+ catch {
279
+ return; // Can't read meta — assume pool is fine
280
+ }
281
+ }
204
282
  try {
205
283
  await initLbug(repoId, handle.lbugPath);
206
284
  this.initializedRepos.add(repoId);
@@ -788,14 +866,14 @@ export class LocalBackend {
788
866
  // Categorized incoming refs
789
867
  const incomingRows = await executeParameterized(repo.id, `
790
868
  MATCH (caller)-[r:CodeRelation]->(n {id: $symId})
791
- WHERE r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS']
869
+ WHERE r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS', 'HAS_METHOD', 'HAS_PROPERTY', 'OVERRIDES', 'ACCESSES']
792
870
  RETURN r.type AS relType, caller.id AS uid, caller.name AS name, caller.filePath AS filePath, labels(caller)[0] AS kind
793
871
  LIMIT 30
794
872
  `, { symId });
795
873
  // Categorized outgoing refs
796
874
  const outgoingRows = await executeParameterized(repo.id, `
797
875
  MATCH (n {id: $symId})-[r:CodeRelation]->(target)
798
- WHERE r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS']
876
+ WHERE r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS', 'HAS_METHOD', 'HAS_PROPERTY', 'OVERRIDES', 'ACCESSES']
799
877
  RETURN r.type AS relType, target.id AS uid, target.name AS name, target.filePath AS filePath, labels(target)[0] AS kind
800
878
  LIMIT 30
801
879
  `, { symId });
@@ -1247,14 +1325,21 @@ export class LocalBackend {
1247
1325
  if (!visited.has(relId)) {
1248
1326
  visited.add(relId);
1249
1327
  nextFrontier.push(relId);
1328
+ const storedConfidence = rel.confidence ?? rel[6];
1329
+ const relationType = rel.relType || rel[5];
1330
+ // Prefer the stored confidence from the graph (set at analysis time);
1331
+ // fall back to the per-type floor for edges without a stored value.
1332
+ const effectiveConfidence = typeof storedConfidence === 'number' && storedConfidence > 0
1333
+ ? storedConfidence
1334
+ : confidenceForRelType(relationType);
1250
1335
  impacted.push({
1251
1336
  depth,
1252
1337
  id: relId,
1253
1338
  name: rel.name || rel[2],
1254
1339
  type: rel.type || rel[3],
1255
1340
  filePath,
1256
- relationType: rel.relType || rel[5],
1257
- confidence: rel.confidence || rel[6] || 1.0,
1341
+ relationType,
1342
+ confidence: effectiveConfidence,
1258
1343
  });
1259
1344
  }
1260
1345
  }
@@ -1279,30 +1364,49 @@ export class LocalBackend {
1279
1364
  let affectedProcesses = [];
1280
1365
  let affectedModules = [];
1281
1366
  if (impacted.length > 0) {
1282
- const allIds = impacted.map(i => `'${i.id.replace(/'/g, "''")}'`).join(', ');
1283
- const d1Ids = (grouped[1] || []).map((i) => `'${i.id.replace(/'/g, "''")}'`).join(', ');
1284
- // Affected processes: which execution flows are broken and at which step
1285
- const [processRows, moduleRows, directModuleRows] = await Promise.all([
1286
- executeQuery(repo.id, `
1287
- MATCH (s)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
1288
- WHERE s.id IN [${allIds}]
1289
- RETURN p.heuristicLabel AS name, COUNT(DISTINCT s.id) AS hits, MIN(r.step) AS minStep, p.stepCount AS stepCount
1290
- ORDER BY hits DESC
1291
- LIMIT 20
1292
- `).catch(() => []),
1293
- executeQuery(repo.id, `
1294
- MATCH (s)-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
1295
- WHERE s.id IN [${allIds}]
1296
- RETURN c.heuristicLabel AS name, COUNT(DISTINCT s.id) AS hits
1297
- ORDER BY hits DESC
1298
- LIMIT 20
1299
- `).catch(() => []),
1300
- d1Ids ? executeQuery(repo.id, `
1301
- MATCH (s)-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
1302
- WHERE s.id IN [${d1Ids}]
1303
- RETURN DISTINCT c.heuristicLabel AS name
1304
- `).catch(() => []) : Promise.resolve([]),
1305
- ]);
1367
+ // Cap IN-clause to 100 IDs to prevent oversized queries that crash
1368
+ // the native DB engine on arm64 macOS (#292)
1369
+ const cappedImpacted = impacted.slice(0, 100);
1370
+ const allIds = cappedImpacted.map(i => `'${String(i.id ?? '').replace(/'/g, "''")}'`).join(', ');
1371
+ const d1Items = (grouped[1] || []).slice(0, 100);
1372
+ const d1Ids = d1Items.map((i) => `'${String(i.id ?? '').replace(/'/g, "''")}'`).join(', ');
1373
+ // Enrichment queries: sequential on arm64 macOS to avoid SIGSEGV from
1374
+ // concurrent native DB access (#285, #290, #292); parallel elsewhere
1375
+ // to preserve performance on unaffected platforms.
1376
+ const isArm64Mac = process.platform === 'darwin' && process.arch === 'arm64';
1377
+ const processQuery = executeQuery(repo.id, `
1378
+ MATCH (s)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
1379
+ WHERE s.id IN [${allIds}]
1380
+ RETURN p.heuristicLabel AS name, COUNT(DISTINCT s.id) AS hits, MIN(r.step) AS minStep, p.stepCount AS stepCount
1381
+ ORDER BY hits DESC
1382
+ LIMIT 20
1383
+ `).catch(() => []);
1384
+ const moduleQuery = () => executeQuery(repo.id, `
1385
+ MATCH (s)-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
1386
+ WHERE s.id IN [${allIds}]
1387
+ RETURN c.heuristicLabel AS name, COUNT(DISTINCT s.id) AS hits
1388
+ ORDER BY hits DESC
1389
+ LIMIT 20
1390
+ `).catch(() => []);
1391
+ const directModuleQuery = () => d1Ids
1392
+ ? executeQuery(repo.id, `
1393
+ MATCH (s)-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
1394
+ WHERE s.id IN [${d1Ids}]
1395
+ RETURN DISTINCT c.heuristicLabel AS name
1396
+ `).catch(() => [])
1397
+ : Promise.resolve([]);
1398
+ let processRows, moduleRows, directModuleRows;
1399
+ if (isArm64Mac) {
1400
+ // Sequential: avoid concurrent native DB access
1401
+ processRows = await processQuery;
1402
+ moduleRows = await moduleQuery();
1403
+ directModuleRows = await directModuleQuery();
1404
+ }
1405
+ else {
1406
+ // Parallel: safe on non-arm64 platforms
1407
+ processRows = await processQuery;
1408
+ [moduleRows, directModuleRows] = await Promise.all([moduleQuery(), directModuleQuery()]);
1409
+ }
1306
1410
  affectedProcesses = processRows.map((r) => ({
1307
1411
  name: r.name || r[0],
1308
1412
  hits: r.hits || r[1],
@@ -271,6 +271,15 @@ nodes:
271
271
 
272
272
  additional_node_types: "Multi-language: Struct, Enum, Macro, Typedef, Union, Namespace, Trait, Impl, TypeAlias, Const, Static, Property, Record, Delegate, Annotation, Constructor, Template, Module (use backticks in queries: \`Struct\`, \`Enum\`, etc.)"
273
273
 
274
+ node_properties:
275
+ common: "name (STRING), filePath (STRING), startLine (INT32), endLine (INT32)"
276
+ Method: "parameterCount (INT32), returnType (STRING), isVariadic (BOOL)"
277
+ Function: "parameterCount (INT32), returnType (STRING), isVariadic (BOOL)"
278
+ Property: "declaredType (STRING) — the field's type annotation (e.g., 'Address', 'City'). Used for field-access chain resolution."
279
+ Constructor: "parameterCount (INT32)"
280
+ Community: "heuristicLabel (STRING), cohesion (DOUBLE), symbolCount (INT32), keywords (STRING[]), description (STRING), enrichedBy (STRING)"
281
+ Process: "heuristicLabel (STRING), processType (STRING — 'intra_community' or 'cross_community'), stepCount (INT32), communities (STRING[]), entryPointId (STRING), terminalId (STRING)"
282
+
274
283
  relationships:
275
284
  - CONTAINS: File/Folder contains child
276
285
  - DEFINES: File defines a symbol
@@ -278,6 +287,10 @@ relationships:
278
287
  - IMPORTS: Module imports
279
288
  - EXTENDS: Class inheritance
280
289
  - IMPLEMENTS: Interface implementation
290
+ - HAS_METHOD: Class/Struct/Interface owns a Method
291
+ - HAS_PROPERTY: Class/Struct/Interface owns a Property (field)
292
+ - ACCESSES: Function/Method reads or writes a Property (reason: 'read' or 'write')
293
+ - OVERRIDES: Method overrides another Method (MRO)
281
294
  - MEMBER_OF: Symbol belongs to community
282
295
  - STEP_IN_PROCESS: Symbol is step N in process
283
296
 
@@ -15,6 +15,7 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
15
15
  import { CompatibleStdioServerTransport } from './compatible-stdio-transport.js';
16
16
  import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, ListResourceTemplatesRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
17
17
  import { GITNEXUS_TOOLS } from './tools.js';
18
+ import { realStdoutWrite } from './core/lbug-adapter.js';
18
19
  import { getResourceDefinitions, getResourceTemplates, readResource } from './resources.js';
19
20
  /**
20
21
  * Next-step hints appended to tool responses.
@@ -237,12 +238,22 @@ Follow these steps:
237
238
  */
238
239
  export async function startMCPServer(backend) {
239
240
  const server = createMCPServer(backend);
240
- // Connect to stdio transport
241
- const transport = new CompatibleStdioServerTransport();
241
+ // Use the shared stdout reference captured at module-load time by the
242
+ // lbug-adapter. Avoids divergence if anything patches stdout between
243
+ // module load and server start.
244
+ const _safeStdout = new Proxy(process.stdout, {
245
+ get(target, prop, receiver) {
246
+ if (prop === 'write')
247
+ return realStdoutWrite;
248
+ const val = Reflect.get(target, prop, receiver);
249
+ return typeof val === 'function' ? val.bind(target) : val;
250
+ }
251
+ });
252
+ const transport = new CompatibleStdioServerTransport(process.stdin, _safeStdout);
242
253
  await server.connect(transport);
243
254
  // Graceful shutdown helper
244
255
  let shuttingDown = false;
245
- const shutdown = async () => {
256
+ const shutdown = async (exitCode = 0) => {
246
257
  if (shuttingDown)
247
258
  return;
248
259
  shuttingDown = true;
@@ -254,11 +265,22 @@ export async function startMCPServer(backend) {
254
265
  await server.close();
255
266
  }
256
267
  catch { }
257
- process.exit(0);
268
+ process.exit(exitCode);
258
269
  };
259
270
  // Handle graceful shutdown
260
271
  process.on('SIGINT', shutdown);
261
272
  process.on('SIGTERM', shutdown);
273
+ // Log crashes to stderr so they aren't silently lost.
274
+ // uncaughtException is fatal — shut down.
275
+ // unhandledRejection is logged but kept non-fatal (availability-first):
276
+ // killing the server for one missed catch would be worse than logging it.
277
+ process.on('uncaughtException', (err) => {
278
+ process.stderr.write(`GitNexus MCP uncaughtException: ${err?.stack || err}\n`);
279
+ shutdown(1);
280
+ });
281
+ process.on('unhandledRejection', (reason) => {
282
+ process.stderr.write(`GitNexus MCP unhandledRejection: ${reason?.stack || reason}\n`);
283
+ });
262
284
  // Handle stdio errors — stdin close means the parent process is gone
263
285
  process.stdin.on('end', shutdown);
264
286
  process.stdin.on('error', () => shutdown());
package/dist/mcp/tools.js CHANGED
@@ -61,7 +61,7 @@ SCHEMA:
61
61
  - Nodes: File, Folder, Function, Class, Interface, Method, CodeElement, Community, Process
62
62
  - Multi-language nodes (use backticks): \`Struct\`, \`Enum\`, \`Trait\`, \`Impl\`, etc.
63
63
  - All edges via single CodeRelation table with 'type' property
64
- - Edge types: CONTAINS, DEFINES, CALLS, IMPORTS, EXTENDS, IMPLEMENTS, HAS_METHOD, OVERRIDES, MEMBER_OF, STEP_IN_PROCESS
64
+ - Edge types: CONTAINS, DEFINES, CALLS, IMPORTS, EXTENDS, IMPLEMENTS, HAS_METHOD, HAS_PROPERTY, ACCESSES, OVERRIDES, MEMBER_OF, STEP_IN_PROCESS
65
65
  - Edge properties: type (STRING), confidence (DOUBLE), reason (STRING), step (INT32)
66
66
 
67
67
  EXAMPLES:
@@ -77,6 +77,12 @@ EXAMPLES:
77
77
  • Find all methods of a class:
78
78
  MATCH (c:Class {name: "UserService"})-[r:CodeRelation {type: 'HAS_METHOD'}]->(m:Method) RETURN m.name, m.parameterCount, m.returnType
79
79
 
80
+ • Find all properties of a class:
81
+ MATCH (c:Class {name: "User"})-[r:CodeRelation {type: 'HAS_PROPERTY'}]->(p:Property) RETURN p.name, p.declaredType
82
+
83
+ • Find all writers of a field:
84
+ MATCH (f:Function)-[r:CodeRelation {type: 'ACCESSES', reason: 'write'}]->(p:Property) WHERE p.name = "address" RETURN f.name, f.filePath
85
+
80
86
  • Find method overrides (MRO resolution):
81
87
  MATCH (winner:Method)-[r:CodeRelation {type: 'OVERRIDES'}]->(loser:Method) RETURN winner.name, winner.filePath, loser.filePath, r.reason
82
88
 
@@ -87,8 +93,8 @@ OUTPUT: Returns { markdown, row_count } — results formatted as a Markdown tabl
87
93
 
88
94
  TIPS:
89
95
  - All relationships use single CodeRelation table — filter with {type: 'CALLS'} etc.
90
- - Community = auto-detected functional area (Leiden algorithm)
91
- - Process = execution flow trace from entry point to terminal
96
+ - Community = auto-detected functional area (Leiden algorithm). Properties: heuristicLabel, cohesion, symbolCount, keywords, description, enrichedBy
97
+ - Process = execution flow trace from entry point to terminal. Properties: heuristicLabel, processType, stepCount, communities, entryPointId, terminalId
92
98
  - Use heuristicLabel (not label) for human-readable community/process names`,
93
99
  inputSchema: {
94
100
  type: 'object',
@@ -102,12 +108,14 @@ TIPS:
102
108
  {
103
109
  name: 'context',
104
110
  description: `360-degree view of a single code symbol.
105
- Shows categorized incoming/outgoing references (calls, imports, extends, implements), process participation, and file location.
111
+ Shows categorized incoming/outgoing references (calls, imports, extends, implements, methods, properties, overrides), process participation, and file location.
106
112
 
107
113
  WHEN TO USE: After query() to understand a specific symbol in depth. When you need to know all callers, callees, and what execution flows a symbol participates in.
108
114
  AFTER THIS: Use impact() if planning changes, or READ gitnexus://repo/{name}/process/{processName} for full execution trace.
109
115
 
110
- Handles disambiguation: if multiple symbols share the same name, returns candidates for you to pick from. Use uid param for zero-ambiguity lookup from prior results.`,
116
+ Handles disambiguation: if multiple symbols share the same name, returns candidates for you to pick from. Use uid param for zero-ambiguity lookup from prior results.
117
+
118
+ NOTE: ACCESSES edges (field read/write tracking) are included in context results with reason 'read' or 'write'. CALLS edges resolve through field access chains and method-call chains (e.g., user.address.getCity().save() produces CALLS edges at each step).`,
111
119
  inputSchema: {
112
120
  type: 'object',
113
121
  properties: {
@@ -183,7 +191,9 @@ Depth groups:
183
191
  - d=2: LIKELY AFFECTED (indirect)
184
192
  - d=3: MAY NEED TESTING (transitive)
185
193
 
186
- EdgeType: CALLS, IMPORTS, EXTENDS, IMPLEMENTS, HAS_METHOD, OVERRIDES
194
+ TIP: Default traversal uses CALLS/IMPORTS/EXTENDS/IMPLEMENTS. For class members, include HAS_METHOD and HAS_PROPERTY in relationTypes. For field access analysis, include ACCESSES in relationTypes.
195
+
196
+ EdgeType: CALLS, IMPORTS, EXTENDS, IMPLEMENTS, HAS_METHOD, HAS_PROPERTY, OVERRIDES, ACCESSES
187
197
  Confidence: 1.0 = certain, <0.8 = fuzzy match`,
188
198
  inputSchema: {
189
199
  type: 'object',
@@ -191,7 +201,7 @@ Confidence: 1.0 = certain, <0.8 = fuzzy match`,
191
201
  target: { type: 'string', description: 'Name of function, class, or file to analyze' },
192
202
  direction: { type: 'string', description: 'upstream (what depends on this) or downstream (what this depends on)' },
193
203
  maxDepth: { type: 'number', description: 'Max relationship depth (default: 3)', default: 3 },
194
- relationTypes: { type: 'array', items: { type: 'string' }, description: 'Filter: CALLS, IMPORTS, EXTENDS, IMPLEMENTS, HAS_METHOD, OVERRIDES (default: usage-based)' },
204
+ relationTypes: { type: 'array', items: { type: 'string' }, description: 'Filter: CALLS, IMPORTS, EXTENDS, IMPLEMENTS, HAS_METHOD, HAS_PROPERTY, OVERRIDES, ACCESSES (default: usage-based, ACCESSES excluded by default)' },
195
205
  includeTests: { type: 'boolean', description: 'Include test files (default: false)' },
196
206
  minConfidence: { type: 'number', description: 'Minimum confidence 0-1 (default: 0.7)' },
197
207
  repo: { type: 'string', description: 'Repository name or path. Omit if only one repo is indexed.' },
@@ -5,6 +5,24 @@
5
5
  * Also hosts the MCP server over StreamableHTTP for remote AI tool access.
6
6
  *
7
7
  * Security: binds to 127.0.0.1 by default (use --host to override).
8
- * CORS is restricted to localhost and the deployed site.
8
+ * CORS is restricted to localhost, private/LAN networks, and the deployed site.
9
9
  */
10
+ /**
11
+ * Determine whether an HTTP Origin header value is allowed by CORS policy.
12
+ *
13
+ * Permitted origins:
14
+ * - No origin (non-browser requests such as curl or server-to-server calls)
15
+ * - http://localhost:<port> — local development
16
+ * - http://127.0.0.1:<port> — loopback alias
17
+ * - RFC 1918 private/LAN networks (any port):
18
+ * 10.0.0.0/8 → 10.x.x.x
19
+ * 172.16.0.0/12 → 172.16.x.x – 172.31.x.x
20
+ * 192.168.0.0/16 → 192.168.x.x
21
+ * - https://gitnexus.vercel.app — the deployed GitNexus web UI
22
+ *
23
+ * @param origin - The value of the HTTP `Origin` request header, or `undefined`
24
+ * when the header is absent (non-browser request).
25
+ * @returns `true` if the origin is allowed, `false` otherwise.
26
+ */
27
+ export declare const isAllowedOrigin: (origin: string | undefined) => boolean;
10
28
  export declare const createServer: (port: number, host?: string) => Promise<void>;
@@ -5,7 +5,7 @@
5
5
  * Also hosts the MCP server over StreamableHTTP for remote AI tool access.
6
6
  *
7
7
  * Security: binds to 127.0.0.1 by default (use --host to override).
8
- * CORS is restricted to localhost and the deployed site.
8
+ * CORS is restricted to localhost, private/LAN networks, and the deployed site.
9
9
  */
10
10
  import express from 'express';
11
11
  import cors from 'cors';
@@ -20,6 +20,69 @@ import { hybridSearch } from '../core/search/hybrid-search.js';
20
20
  // at server startup — crashes on unsupported Node ABI versions (#89)
21
21
  import { LocalBackend } from '../mcp/local/local-backend.js';
22
22
  import { mountMCPEndpoints } from './mcp-http.js';
23
+ /**
24
+ * Determine whether an HTTP Origin header value is allowed by CORS policy.
25
+ *
26
+ * Permitted origins:
27
+ * - No origin (non-browser requests such as curl or server-to-server calls)
28
+ * - http://localhost:<port> — local development
29
+ * - http://127.0.0.1:<port> — loopback alias
30
+ * - RFC 1918 private/LAN networks (any port):
31
+ * 10.0.0.0/8 → 10.x.x.x
32
+ * 172.16.0.0/12 → 172.16.x.x – 172.31.x.x
33
+ * 192.168.0.0/16 → 192.168.x.x
34
+ * - https://gitnexus.vercel.app — the deployed GitNexus web UI
35
+ *
36
+ * @param origin - The value of the HTTP `Origin` request header, or `undefined`
37
+ * when the header is absent (non-browser request).
38
+ * @returns `true` if the origin is allowed, `false` otherwise.
39
+ */
40
+ export const isAllowedOrigin = (origin) => {
41
+ if (origin === undefined) {
42
+ // Non-browser requests (curl, server-to-server) have no Origin header
43
+ return true;
44
+ }
45
+ if (origin.startsWith('http://localhost:')
46
+ || origin === 'http://localhost'
47
+ || origin.startsWith('http://127.0.0.1:')
48
+ || origin === 'http://127.0.0.1'
49
+ || origin.startsWith('http://[::1]:')
50
+ || origin === 'http://[::1]'
51
+ || origin === 'https://gitnexus.vercel.app') {
52
+ return true;
53
+ }
54
+ // RFC 1918 private network ranges — allow any port on these hosts.
55
+ // We parse the hostname out of the origin URL and check against each range.
56
+ let hostname;
57
+ let protocol;
58
+ try {
59
+ const parsed = new URL(origin);
60
+ hostname = parsed.hostname;
61
+ protocol = parsed.protocol;
62
+ }
63
+ catch {
64
+ // Malformed origin — reject
65
+ return false;
66
+ }
67
+ // Only allow HTTP(S) origins — reject ftp://, file://, etc.
68
+ if (protocol !== 'http:' && protocol !== 'https:')
69
+ return false;
70
+ const octets = hostname.split('.').map(Number);
71
+ if (octets.length !== 4 || octets.some(o => !Number.isInteger(o) || o < 0 || o > 255)) {
72
+ return false;
73
+ }
74
+ const [a, b] = octets;
75
+ // 10.0.0.0/8
76
+ if (a === 10)
77
+ return true;
78
+ // 172.16.0.0/12 → 172.16.x.x – 172.31.x.x
79
+ if (a === 172 && b >= 16 && b <= 31)
80
+ return true;
81
+ // 192.168.0.0/16
82
+ if (a === 192 && b === 168)
83
+ return true;
84
+ return false;
85
+ };
23
86
  const buildGraph = async () => {
24
87
  const nodes = [];
25
88
  for (const table of NODE_TABLES) {
@@ -101,14 +164,11 @@ const requestedRepo = (req) => {
101
164
  };
102
165
  export const createServer = async (port, host = '127.0.0.1') => {
103
166
  const app = express();
104
- // CORS: only allow localhost origins and the deployed site.
167
+ // CORS: allow localhost, private/LAN networks, and the deployed site.
105
168
  // Non-browser requests (curl, server-to-server) have no origin and are allowed.
106
169
  app.use(cors({
107
170
  origin: (origin, callback) => {
108
- if (!origin
109
- || origin.startsWith('http://localhost:')
110
- || origin.startsWith('http://127.0.0.1:')
111
- || origin === 'https://gitnexus.vercel.app') {
171
+ if (isAllowedOrigin(origin)) {
112
172
  callback(null, true);
113
173
  }
114
174
  else {
@@ -4,3 +4,15 @@ export declare const getCurrentCommit: (repoPath: string) => string;
4
4
  * Find the git repository root from any path inside the repo
5
5
  */
6
6
  export declare const getGitRoot: (fromPath: string) => string | null;
7
+ /**
8
+ * Check whether a directory contains a .git entry (file or folder).
9
+ *
10
+ * This is intentionally a simple filesystem check rather than running
11
+ * `git rev-parse`, so it works even when git is not installed or when
12
+ * the directory is a git-worktree root (which has a .git file, not a
13
+ * directory). Use `isGitRepo` for a definitive git answer.
14
+ *
15
+ * @param dirPath - Absolute path to the directory to inspect.
16
+ * @returns `true` when `.git` is present, `false` otherwise.
17
+ */
18
+ export declare const hasGitDir: (dirPath: string) => boolean;
@@ -1,4 +1,5 @@
1
1
  import { execSync } from 'child_process';
2
+ import { statSync } from 'fs';
2
3
  import path from 'path';
3
4
  // Git utilities for repository detection, commit tracking, and diff analysis
4
5
  export const isGitRepo = (repoPath) => {
@@ -33,3 +34,23 @@ export const getGitRoot = (fromPath) => {
33
34
  return null;
34
35
  }
35
36
  };
37
+ /**
38
+ * Check whether a directory contains a .git entry (file or folder).
39
+ *
40
+ * This is intentionally a simple filesystem check rather than running
41
+ * `git rev-parse`, so it works even when git is not installed or when
42
+ * the directory is a git-worktree root (which has a .git file, not a
43
+ * directory). Use `isGitRepo` for a definitive git answer.
44
+ *
45
+ * @param dirPath - Absolute path to the directory to inspect.
46
+ * @returns `true` when `.git` is present, `false` otherwise.
47
+ */
48
+ export const hasGitDir = (dirPath) => {
49
+ try {
50
+ statSync(path.join(dirPath, '.git'));
51
+ return true;
52
+ }
53
+ catch {
54
+ return false;
55
+ }
56
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.4.6",
3
+ "version": "1.4.8",
4
4
  "description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
5
5
  "author": "Abhigyan Patwari",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",
@@ -20,6 +20,7 @@
20
20
  "knowledge-graph",
21
21
  "cursor",
22
22
  "claude",
23
+ "codex",
23
24
  "ai-agent",
24
25
  "gitnexus",
25
26
  "static-analysis",
@@ -39,9 +40,9 @@
39
40
  "scripts": {
40
41
  "build": "tsc",
41
42
  "dev": "tsx watch src/cli/index.ts",
42
- "test": "vitest run test/unit",
43
+ "test": "vitest run",
44
+ "test:unit": "vitest run test/unit",
43
45
  "test:integration": "vitest run test/integration",
44
- "test:all": "vitest run",
45
46
  "test:watch": "vitest",
46
47
  "test:coverage": "vitest run --coverage",
47
48
  "prepare": "npm run build",
@@ -50,6 +51,7 @@
50
51
  },
51
52
  "dependencies": {
52
53
  "@huggingface/transformers": "^3.0.0",
54
+ "@ladybugdb/core": "^0.15.2",
53
55
  "@modelcontextprotocol/sdk": "^1.0.0",
54
56
  "cli-progress": "^3.12.0",
55
57
  "commander": "^12.0.0",
@@ -59,10 +61,10 @@
59
61
  "graphology": "^0.25.4",
60
62
  "graphology-indices": "^0.17.0",
61
63
  "graphology-utils": "^2.3.0",
62
- "@ladybugdb/core": "^0.15.1",
63
64
  "ignore": "^7.0.5",
64
65
  "lru-cache": "^11.0.0",
65
66
  "mnemonist": "^0.39.0",
67
+ "onnxruntime-node": "^1.24.0",
66
68
  "pandemonium": "^2.4.0",
67
69
  "tree-sitter": "^0.21.0",
68
70
  "tree-sitter-c": "^0.21.0",
@@ -89,10 +91,16 @@
89
91
  "@types/node": "^20.0.0",
90
92
  "@types/uuid": "^10.0.0",
91
93
  "@vitest/coverage-v8": "^4.0.18",
94
+ "husky": "^9.1.7",
92
95
  "tsx": "^4.0.0",
93
96
  "typescript": "^5.4.5",
94
97
  "vitest": "^4.0.18"
95
98
  },
99
+ "overrides": {
100
+ "@huggingface/transformers": {
101
+ "onnxruntime-node": "$onnxruntime-node"
102
+ }
103
+ },
96
104
  "engines": {
97
105
  "node": ">=18.0.0"
98
106
  }