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.
- package/README.md +22 -1
- package/dist/cli/ai-context.d.ts +1 -1
- package/dist/cli/ai-context.js +1 -1
- package/dist/cli/analyze.d.ts +2 -0
- package/dist/cli/analyze.js +54 -21
- package/dist/cli/index.js +2 -1
- package/dist/cli/setup.js +78 -1
- package/dist/config/supported-languages.d.ts +30 -0
- package/dist/config/supported-languages.js +30 -0
- package/dist/core/embeddings/embedder.d.ts +6 -1
- package/dist/core/embeddings/embedder.js +65 -5
- package/dist/core/embeddings/embedding-pipeline.js +11 -9
- package/dist/core/embeddings/http-client.d.ts +31 -0
- package/dist/core/embeddings/http-client.js +179 -0
- package/dist/core/embeddings/index.d.ts +1 -0
- package/dist/core/embeddings/index.js +1 -0
- package/dist/core/embeddings/types.d.ts +1 -1
- package/dist/core/graph/types.d.ts +2 -1
- package/dist/core/ingestion/ast-helpers.d.ts +80 -0
- package/dist/core/ingestion/ast-helpers.js +738 -0
- package/dist/core/ingestion/call-analysis.d.ts +73 -0
- package/dist/core/ingestion/call-analysis.js +490 -0
- package/dist/core/ingestion/call-processor.d.ts +48 -1
- package/dist/core/ingestion/call-processor.js +368 -7
- package/dist/core/ingestion/call-routing.d.ts +6 -0
- package/dist/core/ingestion/entry-point-scoring.js +36 -26
- package/dist/core/ingestion/framework-detection.d.ts +10 -2
- package/dist/core/ingestion/framework-detection.js +49 -12
- package/dist/core/ingestion/heritage-processor.js +47 -49
- package/dist/core/ingestion/import-processor.d.ts +1 -1
- package/dist/core/ingestion/import-processor.js +103 -194
- package/dist/core/ingestion/import-resolution.d.ts +101 -0
- package/dist/core/ingestion/import-resolution.js +251 -0
- package/dist/core/ingestion/language-config.d.ts +3 -0
- package/dist/core/ingestion/language-config.js +13 -0
- package/dist/core/ingestion/markdown-processor.d.ts +17 -0
- package/dist/core/ingestion/markdown-processor.js +124 -0
- package/dist/core/ingestion/mro-processor.js +8 -3
- package/dist/core/ingestion/named-binding-extraction.d.ts +9 -43
- package/dist/core/ingestion/named-binding-extraction.js +89 -79
- package/dist/core/ingestion/parsing-processor.d.ts +2 -2
- package/dist/core/ingestion/parsing-processor.js +14 -73
- package/dist/core/ingestion/pipeline.d.ts +10 -0
- package/dist/core/ingestion/pipeline.js +421 -4
- package/dist/core/ingestion/resolution-context.d.ts +5 -0
- package/dist/core/ingestion/resolution-context.js +7 -4
- package/dist/core/ingestion/resolvers/index.d.ts +1 -1
- package/dist/core/ingestion/resolvers/index.js +1 -1
- package/dist/core/ingestion/resolvers/jvm.d.ts +2 -1
- package/dist/core/ingestion/resolvers/jvm.js +25 -9
- package/dist/core/ingestion/resolvers/php.d.ts +14 -0
- package/dist/core/ingestion/resolvers/php.js +43 -3
- package/dist/core/ingestion/resolvers/utils.d.ts +5 -0
- package/dist/core/ingestion/resolvers/utils.js +16 -0
- package/dist/core/ingestion/symbol-table.d.ts +16 -0
- package/dist/core/ingestion/symbol-table.js +20 -6
- package/dist/core/ingestion/tree-sitter-queries.d.ts +4 -4
- package/dist/core/ingestion/tree-sitter-queries.js +43 -2
- package/dist/core/ingestion/type-env.d.ts +28 -1
- package/dist/core/ingestion/type-env.js +419 -96
- package/dist/core/ingestion/type-extractors/c-cpp.d.ts +5 -0
- package/dist/core/ingestion/type-extractors/c-cpp.js +119 -0
- package/dist/core/ingestion/type-extractors/csharp.js +149 -16
- package/dist/core/ingestion/type-extractors/index.d.ts +1 -1
- package/dist/core/ingestion/type-extractors/index.js +1 -1
- package/dist/core/ingestion/type-extractors/jvm.js +169 -66
- package/dist/core/ingestion/type-extractors/rust.js +35 -1
- package/dist/core/ingestion/type-extractors/shared.d.ts +0 -2
- package/dist/core/ingestion/type-extractors/shared.js +5 -10
- package/dist/core/ingestion/type-extractors/swift.js +7 -6
- package/dist/core/ingestion/type-extractors/types.d.ts +37 -7
- package/dist/core/ingestion/type-extractors/typescript.js +141 -9
- package/dist/core/ingestion/utils.d.ts +2 -120
- package/dist/core/ingestion/utils.js +3 -1051
- package/dist/core/ingestion/workers/parse-worker.d.ts +13 -4
- package/dist/core/ingestion/workers/parse-worker.js +66 -87
- package/dist/core/lbug/csv-generator.js +18 -1
- package/dist/core/lbug/lbug-adapter.d.ts +10 -0
- package/dist/core/lbug/lbug-adapter.js +69 -4
- package/dist/core/lbug/schema.d.ts +5 -3
- package/dist/core/lbug/schema.js +26 -2
- package/dist/mcp/core/embedder.js +11 -3
- package/dist/mcp/core/lbug-adapter.js +12 -1
- package/dist/mcp/local/local-backend.d.ts +22 -0
- package/dist/mcp/local/local-backend.js +133 -29
- package/dist/mcp/resources.js +2 -0
- package/dist/mcp/tools.js +2 -2
- package/dist/server/api.d.ts +19 -1
- package/dist/server/api.js +66 -6
- package/dist/storage/git.d.ts +12 -0
- package/dist/storage/git.js +21 -0
- 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 = () =>
|
|
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
|
-
//
|
|
199
|
-
|
|
200
|
-
|
|
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
|
|
1257
|
-
confidence:
|
|
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
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
const
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
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],
|
package/dist/mcp/resources.js
CHANGED
|
@@ -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',
|
package/dist/server/api.d.ts
CHANGED
|
@@ -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>;
|
package/dist/server/api.js
CHANGED
|
@@ -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:
|
|
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 (
|
|
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 {
|
package/dist/storage/git.d.ts
CHANGED
|
@@ -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;
|
package/dist/storage/git.js
CHANGED
|
@@ -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.
|
|
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
|
}
|