gitnexus 1.4.7 → 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 (92) 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 +2 -1
  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 +48 -1
  24. package/dist/core/ingestion/call-processor.js +368 -7
  25. package/dist/core/ingestion/call-routing.d.ts +6 -0
  26. package/dist/core/ingestion/entry-point-scoring.js +36 -26
  27. package/dist/core/ingestion/framework-detection.d.ts +10 -2
  28. package/dist/core/ingestion/framework-detection.js +49 -12
  29. package/dist/core/ingestion/heritage-processor.js +47 -49
  30. package/dist/core/ingestion/import-processor.d.ts +1 -1
  31. package/dist/core/ingestion/import-processor.js +103 -194
  32. package/dist/core/ingestion/import-resolution.d.ts +101 -0
  33. package/dist/core/ingestion/import-resolution.js +251 -0
  34. package/dist/core/ingestion/language-config.d.ts +3 -0
  35. package/dist/core/ingestion/language-config.js +13 -0
  36. package/dist/core/ingestion/markdown-processor.d.ts +17 -0
  37. package/dist/core/ingestion/markdown-processor.js +124 -0
  38. package/dist/core/ingestion/mro-processor.js +8 -3
  39. package/dist/core/ingestion/named-binding-extraction.d.ts +9 -43
  40. package/dist/core/ingestion/named-binding-extraction.js +89 -79
  41. package/dist/core/ingestion/parsing-processor.d.ts +2 -2
  42. package/dist/core/ingestion/parsing-processor.js +14 -73
  43. package/dist/core/ingestion/pipeline.d.ts +10 -0
  44. package/dist/core/ingestion/pipeline.js +421 -4
  45. package/dist/core/ingestion/resolution-context.d.ts +5 -0
  46. package/dist/core/ingestion/resolution-context.js +7 -4
  47. package/dist/core/ingestion/resolvers/index.d.ts +1 -1
  48. package/dist/core/ingestion/resolvers/index.js +1 -1
  49. package/dist/core/ingestion/resolvers/jvm.d.ts +2 -1
  50. package/dist/core/ingestion/resolvers/jvm.js +25 -9
  51. package/dist/core/ingestion/resolvers/php.d.ts +14 -0
  52. package/dist/core/ingestion/resolvers/php.js +43 -3
  53. package/dist/core/ingestion/resolvers/utils.d.ts +5 -0
  54. package/dist/core/ingestion/resolvers/utils.js +16 -0
  55. package/dist/core/ingestion/symbol-table.d.ts +16 -0
  56. package/dist/core/ingestion/symbol-table.js +20 -6
  57. package/dist/core/ingestion/tree-sitter-queries.d.ts +4 -4
  58. package/dist/core/ingestion/tree-sitter-queries.js +43 -2
  59. package/dist/core/ingestion/type-env.d.ts +28 -1
  60. package/dist/core/ingestion/type-env.js +419 -96
  61. package/dist/core/ingestion/type-extractors/c-cpp.d.ts +5 -0
  62. package/dist/core/ingestion/type-extractors/c-cpp.js +119 -0
  63. package/dist/core/ingestion/type-extractors/csharp.js +149 -16
  64. package/dist/core/ingestion/type-extractors/index.d.ts +1 -1
  65. package/dist/core/ingestion/type-extractors/index.js +1 -1
  66. package/dist/core/ingestion/type-extractors/jvm.js +169 -66
  67. package/dist/core/ingestion/type-extractors/rust.js +35 -1
  68. package/dist/core/ingestion/type-extractors/shared.d.ts +0 -2
  69. package/dist/core/ingestion/type-extractors/shared.js +5 -10
  70. package/dist/core/ingestion/type-extractors/swift.js +7 -6
  71. package/dist/core/ingestion/type-extractors/types.d.ts +37 -7
  72. package/dist/core/ingestion/type-extractors/typescript.js +141 -9
  73. package/dist/core/ingestion/utils.d.ts +2 -120
  74. package/dist/core/ingestion/utils.js +3 -1051
  75. package/dist/core/ingestion/workers/parse-worker.d.ts +13 -4
  76. package/dist/core/ingestion/workers/parse-worker.js +66 -87
  77. package/dist/core/lbug/csv-generator.js +18 -1
  78. package/dist/core/lbug/lbug-adapter.d.ts +10 -0
  79. package/dist/core/lbug/lbug-adapter.js +69 -4
  80. package/dist/core/lbug/schema.d.ts +5 -3
  81. package/dist/core/lbug/schema.js +26 -2
  82. package/dist/mcp/core/embedder.js +11 -3
  83. package/dist/mcp/core/lbug-adapter.js +12 -1
  84. package/dist/mcp/local/local-backend.d.ts +22 -0
  85. package/dist/mcp/local/local-backend.js +133 -29
  86. package/dist/mcp/resources.js +2 -0
  87. package/dist/mcp/tools.js +2 -2
  88. package/dist/server/api.d.ts +19 -1
  89. package/dist/server/api.js +66 -6
  90. package/dist/storage/git.d.ts +12 -0
  91. package/dist/storage/git.js +21 -0
  92. package/package.json +10 -2
@@ -5,9 +5,9 @@
5
5
  * For MCP, we only need to compute query embeddings, not batch embed.
6
6
  */
7
7
  import { pipeline, env } from '@huggingface/transformers';
8
+ import { isHttpMode, getHttpDimensions, httpEmbedQuery } from '../../core/embeddings/http-client.js';
8
9
  // Model config
9
10
  const MODEL_ID = 'Snowflake/snowflake-arctic-embed-xs';
10
- const EMBEDDING_DIMS = 384;
11
11
  // Module-level state for singleton pattern
12
12
  let embedderInstance = null;
13
13
  let isInitializing = false;
@@ -16,6 +16,9 @@ let initPromise = null;
16
16
  * Initialize the embedding model (lazy, on first search)
17
17
  */
18
18
  export const initEmbedder = async () => {
19
+ if (isHttpMode()) {
20
+ throw new Error('initEmbedder() should not be called in HTTP mode.');
21
+ }
19
22
  if (embedderInstance) {
20
23
  return embedderInstance;
21
24
  }
@@ -75,11 +78,14 @@ export const initEmbedder = async () => {
75
78
  /**
76
79
  * Check if embedder is ready
77
80
  */
78
- export const isEmbedderReady = () => embedderInstance !== null;
81
+ export const isEmbedderReady = () => isHttpMode() || embedderInstance !== null;
79
82
  /**
80
83
  * Embed a query text for semantic search
81
84
  */
82
85
  export const embedQuery = async (query) => {
86
+ if (isHttpMode()) {
87
+ return httpEmbedQuery(query);
88
+ }
83
89
  const embedder = await initEmbedder();
84
90
  const result = await embedder(query, {
85
91
  pooling: 'mean',
@@ -90,7 +96,9 @@ export const embedQuery = async (query) => {
90
96
  /**
91
97
  * Get embedding dimensions
92
98
  */
93
- export const getEmbeddingDims = () => EMBEDDING_DIMS;
99
+ export const getEmbeddingDims = () => {
100
+ return getHttpDimensions() ?? 384;
101
+ };
94
102
  /**
95
103
  * Cleanup embedder
96
104
  */
@@ -109,6 +109,7 @@ function closeOne(repoId) {
109
109
  * Create a new Connection from a repo's Database.
110
110
  * Silences stdout to prevent native module output from corrupting MCP stdio.
111
111
  */
112
+ let activeQueryCount = 0;
112
113
  function silenceStdout() {
113
114
  if (stdoutSilenceCount++ === 0) {
114
115
  process.stdout.write = (() => true);
@@ -122,8 +123,10 @@ function restoreStdout() {
122
123
  }
123
124
  // Safety watchdog: restore stdout if it gets stuck silenced (e.g. native crash
124
125
  // inside createConnection before restoreStdout runs).
126
+ // Exempts active queries and pre-warm — these legitimately hold silence for
127
+ // longer than 1 second (queries can take up to QUERY_TIMEOUT_MS = 30s).
125
128
  setInterval(() => {
126
- if (stdoutSilenceCount > 0 && !preWarmActive) {
129
+ if (stdoutSilenceCount > 0 && !preWarmActive && activeQueryCount === 0) {
127
130
  stdoutSilenceCount = 0;
128
131
  process.stdout.write = realStdoutWrite;
129
132
  }
@@ -389,6 +392,8 @@ export const executeQuery = async (repoId, cypher) => {
389
392
  }
390
393
  entry.lastUsed = Date.now();
391
394
  const conn = await checkout(entry);
395
+ silenceStdout();
396
+ activeQueryCount++;
392
397
  try {
393
398
  const queryResult = await withTimeout(conn.query(cypher), QUERY_TIMEOUT_MS, 'Query');
394
399
  const result = Array.isArray(queryResult) ? queryResult[0] : queryResult;
@@ -396,6 +401,8 @@ export const executeQuery = async (repoId, cypher) => {
396
401
  return rows;
397
402
  }
398
403
  finally {
404
+ activeQueryCount--;
405
+ restoreStdout();
399
406
  checkin(entry, conn);
400
407
  }
401
408
  };
@@ -410,6 +417,8 @@ export const executeParameterized = async (repoId, cypher, params) => {
410
417
  }
411
418
  entry.lastUsed = Date.now();
412
419
  const conn = await checkout(entry);
420
+ silenceStdout();
421
+ activeQueryCount++;
413
422
  try {
414
423
  const stmt = await withTimeout(conn.prepare(cypher), QUERY_TIMEOUT_MS, 'Prepare');
415
424
  if (!stmt.isSuccess()) {
@@ -422,6 +431,8 @@ export const executeParameterized = async (repoId, cypher, params) => {
422
431
  return rows;
423
432
  }
424
433
  finally {
434
+ activeQueryCount--;
435
+ restoreStdout();
425
436
  checkin(entry, conn);
426
437
  }
427
438
  };
@@ -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.
@@ -38,6 +38,41 @@ export const VALID_NODE_LABELS = new Set([
38
38
  ]);
39
39
  /** Valid relation types for impact analysis filtering */
40
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);
@@ -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],
@@ -277,6 +277,8 @@ node_properties:
277
277
  Function: "parameterCount (INT32), returnType (STRING), isVariadic (BOOL)"
278
278
  Property: "declaredType (STRING) — the field's type annotation (e.g., 'Address', 'City'). Used for field-access chain resolution."
279
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)"
280
282
 
281
283
  relationships:
282
284
  - CONTAINS: File/Folder contains child
package/dist/mcp/tools.js CHANGED
@@ -93,8 +93,8 @@ OUTPUT: Returns { markdown, row_count } — results formatted as a Markdown tabl
93
93
 
94
94
  TIPS:
95
95
  - All relationships use single CodeRelation table — filter with {type: 'CALLS'} etc.
96
- - Community = auto-detected functional area (Leiden algorithm)
97
- - 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
98
98
  - Use heuristicLabel (not label) for human-readable community/process names`,
99
99
  inputSchema: {
100
100
  type: 'object',
@@ -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.7",
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",
@@ -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.2",
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
  }