gitnexus 1.4.6 → 1.4.7
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/dist/core/graph/types.d.ts +2 -2
- package/dist/core/ingestion/call-processor.d.ts +7 -1
- package/dist/core/ingestion/call-processor.js +308 -104
- package/dist/core/ingestion/call-routing.d.ts +17 -2
- package/dist/core/ingestion/call-routing.js +21 -0
- package/dist/core/ingestion/parsing-processor.d.ts +2 -1
- package/dist/core/ingestion/parsing-processor.js +32 -6
- package/dist/core/ingestion/pipeline.js +5 -1
- package/dist/core/ingestion/symbol-table.d.ts +13 -3
- package/dist/core/ingestion/symbol-table.js +23 -4
- package/dist/core/ingestion/tree-sitter-queries.d.ts +12 -12
- package/dist/core/ingestion/tree-sitter-queries.js +200 -0
- package/dist/core/ingestion/type-env.js +94 -38
- package/dist/core/ingestion/type-extractors/c-cpp.js +27 -2
- package/dist/core/ingestion/type-extractors/csharp.js +40 -0
- package/dist/core/ingestion/type-extractors/go.js +45 -0
- package/dist/core/ingestion/type-extractors/jvm.js +75 -3
- package/dist/core/ingestion/type-extractors/php.js +31 -4
- package/dist/core/ingestion/type-extractors/python.js +89 -17
- package/dist/core/ingestion/type-extractors/ruby.js +17 -2
- package/dist/core/ingestion/type-extractors/rust.js +37 -3
- package/dist/core/ingestion/type-extractors/shared.d.ts +12 -0
- package/dist/core/ingestion/type-extractors/shared.js +110 -3
- package/dist/core/ingestion/type-extractors/types.d.ts +17 -4
- package/dist/core/ingestion/type-extractors/typescript.js +30 -0
- package/dist/core/ingestion/utils.d.ts +25 -0
- package/dist/core/ingestion/utils.js +160 -1
- package/dist/core/ingestion/workers/parse-worker.d.ts +23 -7
- package/dist/core/ingestion/workers/parse-worker.js +68 -26
- package/dist/core/lbug/lbug-adapter.d.ts +2 -0
- package/dist/core/lbug/lbug-adapter.js +2 -0
- package/dist/core/lbug/schema.d.ts +1 -1
- package/dist/core/lbug/schema.js +1 -1
- package/dist/mcp/core/lbug-adapter.d.ts +22 -0
- package/dist/mcp/core/lbug-adapter.js +167 -23
- package/dist/mcp/local/local-backend.js +3 -3
- package/dist/mcp/resources.js +11 -0
- package/dist/mcp/server.js +26 -4
- package/dist/mcp/tools.js +15 -5
- package/package.json +4 -4
|
@@ -22,12 +22,12 @@ const MAX_POOL_SIZE = 5;
|
|
|
22
22
|
const IDLE_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
|
23
23
|
/** Max connections per repo (caps concurrent queries per repo) */
|
|
24
24
|
const MAX_CONNS_PER_REPO = 8;
|
|
25
|
-
/** Connections created eagerly on init */
|
|
26
|
-
const INITIAL_CONNS_PER_REPO = 2;
|
|
27
25
|
let idleTimer = null;
|
|
28
26
|
/** Saved real stdout.write — used to silence LadybugDB native output without race conditions */
|
|
29
|
-
const realStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
27
|
+
export const realStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
30
28
|
let stdoutSilenceCount = 0;
|
|
29
|
+
/** True while pre-warming connections — prevents watchdog from prematurely restoring stdout */
|
|
30
|
+
let preWarmActive = false;
|
|
31
31
|
/**
|
|
32
32
|
* Start the idle cleanup timer (runs every 60s)
|
|
33
33
|
*/
|
|
@@ -65,19 +65,42 @@ function evictLRU() {
|
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
/**
|
|
68
|
-
* Remove a repo from the pool and release its
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
* segfault on Linux/macOS. Pool databases are opened read-only, so
|
|
72
|
-
* there is no WAL to flush — just deleting the pool entry and letting
|
|
73
|
-
* the GC (or process exit) reclaim native resources is safe.
|
|
68
|
+
* Remove a repo from the pool, close its connections, and release its
|
|
69
|
+
* shared Database ref. Only closes the Database when no other repoIds
|
|
70
|
+
* reference it (refCount === 0).
|
|
74
71
|
*/
|
|
75
72
|
function closeOne(repoId) {
|
|
76
73
|
const entry = pool.get(repoId);
|
|
77
|
-
if (entry)
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
74
|
+
if (!entry)
|
|
75
|
+
return;
|
|
76
|
+
entry.closed = true;
|
|
77
|
+
// Close available connections — fire-and-forget with .catch() to prevent
|
|
78
|
+
// unhandled rejections. Native close() returns Promise<void> but can crash
|
|
79
|
+
// the N-API destructor on macOS/Windows; deferring to process exit lets
|
|
80
|
+
// dangerouslyIgnoreUnhandledErrors absorb the crash.
|
|
81
|
+
for (const conn of entry.available) {
|
|
82
|
+
conn.close().catch(() => { });
|
|
83
|
+
}
|
|
84
|
+
entry.available.length = 0;
|
|
85
|
+
// Checked-out connections can't be closed here — they're in-flight.
|
|
86
|
+
// The checkin() function detects entry.closed and closes them on return.
|
|
87
|
+
// Only close the Database when no other repoIds reference it.
|
|
88
|
+
// External databases (injected via initLbugWithDb) are never closed here —
|
|
89
|
+
// the core adapter owns them and handles their lifecycle.
|
|
90
|
+
const shared = dbCache.get(entry.dbPath);
|
|
91
|
+
if (shared) {
|
|
92
|
+
shared.refCount--;
|
|
93
|
+
if (shared.refCount === 0) {
|
|
94
|
+
if (shared.external) {
|
|
95
|
+
// External databases are owned by the core adapter — don't close
|
|
96
|
+
// or remove from cache. Keep the entry so future initLbug() calls
|
|
97
|
+
// for the same dbPath reuse it instead of hitting a file lock.
|
|
98
|
+
shared.refCount = 0;
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
shared.db.close().catch(() => { });
|
|
102
|
+
dbCache.delete(entry.dbPath);
|
|
103
|
+
}
|
|
81
104
|
}
|
|
82
105
|
}
|
|
83
106
|
pool.delete(repoId);
|
|
@@ -97,6 +120,14 @@ function restoreStdout() {
|
|
|
97
120
|
process.stdout.write = realStdoutWrite;
|
|
98
121
|
}
|
|
99
122
|
}
|
|
123
|
+
// Safety watchdog: restore stdout if it gets stuck silenced (e.g. native crash
|
|
124
|
+
// inside createConnection before restoreStdout runs).
|
|
125
|
+
setInterval(() => {
|
|
126
|
+
if (stdoutSilenceCount > 0 && !preWarmActive) {
|
|
127
|
+
stdoutSilenceCount = 0;
|
|
128
|
+
process.stdout.write = realStdoutWrite;
|
|
129
|
+
}
|
|
130
|
+
}, 1000).unref();
|
|
100
131
|
function createConnection(db) {
|
|
101
132
|
silenceStdout();
|
|
102
133
|
try {
|
|
@@ -112,9 +143,14 @@ const QUERY_TIMEOUT_MS = 30_000;
|
|
|
112
143
|
const WAITER_TIMEOUT_MS = 15_000;
|
|
113
144
|
const LOCK_RETRY_ATTEMPTS = 3;
|
|
114
145
|
const LOCK_RETRY_DELAY_MS = 2000;
|
|
146
|
+
/** Deduplicates concurrent initLbug calls for the same repoId */
|
|
147
|
+
const initPromises = new Map();
|
|
115
148
|
/**
|
|
116
149
|
* Initialize (or reuse) a Database + connection pool for a specific repo.
|
|
117
150
|
* Retries on lock errors (e.g., when `gitnexus analyze` is running).
|
|
151
|
+
*
|
|
152
|
+
* Concurrent calls for the same repoId are deduplicated — the second caller
|
|
153
|
+
* awaits the first's in-progress init rather than starting a redundant one.
|
|
118
154
|
*/
|
|
119
155
|
export const initLbug = async (repoId, dbPath) => {
|
|
120
156
|
const existing = pool.get(repoId);
|
|
@@ -122,6 +158,27 @@ export const initLbug = async (repoId, dbPath) => {
|
|
|
122
158
|
existing.lastUsed = Date.now();
|
|
123
159
|
return;
|
|
124
160
|
}
|
|
161
|
+
// Deduplicate concurrent init calls for the same repoId —
|
|
162
|
+
// prevents double-init race when multiple parallel tool calls
|
|
163
|
+
// trigger initialization for the same repo simultaneously.
|
|
164
|
+
const pending = initPromises.get(repoId);
|
|
165
|
+
if (pending)
|
|
166
|
+
return pending;
|
|
167
|
+
const promise = doInitLbug(repoId, dbPath);
|
|
168
|
+
initPromises.set(repoId, promise);
|
|
169
|
+
try {
|
|
170
|
+
await promise;
|
|
171
|
+
}
|
|
172
|
+
finally {
|
|
173
|
+
initPromises.delete(repoId);
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
/**
|
|
177
|
+
* Internal init — creates DB, pre-warms connections, loads FTS, then registers pool.
|
|
178
|
+
* Pool entry is registered LAST so concurrent executeQuery calls see either
|
|
179
|
+
* "not initialized" (and throw) or a fully ready pool — never a half-built one.
|
|
180
|
+
*/
|
|
181
|
+
async function doInitLbug(repoId, dbPath) {
|
|
125
182
|
// Check if database exists
|
|
126
183
|
try {
|
|
127
184
|
await fs.stat(dbPath);
|
|
@@ -166,14 +223,22 @@ export const initLbug = async (repoId, dbPath) => {
|
|
|
166
223
|
}
|
|
167
224
|
shared.refCount++;
|
|
168
225
|
const db = shared.db;
|
|
169
|
-
// Pre-create
|
|
226
|
+
// Pre-create the full pool upfront so createConnection() (which silences
|
|
227
|
+
// stdout) is never called lazily during active query execution.
|
|
228
|
+
// Mark preWarmActive so the watchdog timer doesn't interfere.
|
|
229
|
+
preWarmActive = true;
|
|
170
230
|
const available = [];
|
|
171
|
-
|
|
172
|
-
|
|
231
|
+
try {
|
|
232
|
+
for (let i = 0; i < MAX_CONNS_PER_REPO; i++) {
|
|
233
|
+
available.push(createConnection(db));
|
|
234
|
+
}
|
|
173
235
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
236
|
+
finally {
|
|
237
|
+
preWarmActive = false;
|
|
238
|
+
}
|
|
239
|
+
// Load FTS extension once per shared Database.
|
|
240
|
+
// Done BEFORE pool registration so no concurrent checkout can grab
|
|
241
|
+
// the connection while the async FTS load is in progress.
|
|
177
242
|
if (!shared.ftsLoaded) {
|
|
178
243
|
try {
|
|
179
244
|
await available[0].query('LOAD EXTENSION fts');
|
|
@@ -183,7 +248,67 @@ export const initLbug = async (repoId, dbPath) => {
|
|
|
183
248
|
// Extension may not be installed — FTS queries will fail gracefully
|
|
184
249
|
}
|
|
185
250
|
}
|
|
186
|
-
|
|
251
|
+
// Register pool entry only after all connections are pre-warmed and FTS is
|
|
252
|
+
// loaded. Concurrent executeQuery calls see either "not initialized"
|
|
253
|
+
// (and throw cleanly) or a fully ready pool — never a half-built one.
|
|
254
|
+
pool.set(repoId, { db, available, checkedOut: 0, waiters: [], lastUsed: Date.now(), dbPath, closed: false });
|
|
255
|
+
ensureIdleTimer();
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Initialize a pool entry from a pre-existing Database object.
|
|
259
|
+
*
|
|
260
|
+
* Used in tests to avoid the writable→close→read-only cycle that crashes
|
|
261
|
+
* on macOS due to N-API destructor segfaults. The pool adapter reuses
|
|
262
|
+
* the core adapter's writable Database instead of opening a new read-only one.
|
|
263
|
+
*
|
|
264
|
+
* The Database is registered in the shared dbCache so closeOne() decrements
|
|
265
|
+
* the refCount correctly. If the Database is already cached (e.g. another
|
|
266
|
+
* repoId already injected it), the existing entry is reused.
|
|
267
|
+
*/
|
|
268
|
+
export async function initLbugWithDb(repoId, existingDb, dbPath) {
|
|
269
|
+
const existing = pool.get(repoId);
|
|
270
|
+
if (existing) {
|
|
271
|
+
existing.lastUsed = Date.now();
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
// Register in dbCache with external: true so other initLbug() calls
|
|
275
|
+
// for the same dbPath reuse this Database instead of trying to open
|
|
276
|
+
// a new one (which would fail with a file lock error).
|
|
277
|
+
// closeOne() respects the external flag and skips db.close().
|
|
278
|
+
let shared = dbCache.get(dbPath);
|
|
279
|
+
if (!shared) {
|
|
280
|
+
shared = { db: existingDb, refCount: 0, ftsLoaded: false, external: true };
|
|
281
|
+
dbCache.set(dbPath, shared);
|
|
282
|
+
}
|
|
283
|
+
shared.refCount++;
|
|
284
|
+
const available = [];
|
|
285
|
+
preWarmActive = true;
|
|
286
|
+
try {
|
|
287
|
+
for (let i = 0; i < MAX_CONNS_PER_REPO; i++) {
|
|
288
|
+
available.push(createConnection(existingDb));
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
finally {
|
|
292
|
+
preWarmActive = false;
|
|
293
|
+
}
|
|
294
|
+
// Load FTS extension if not already loaded on this Database
|
|
295
|
+
try {
|
|
296
|
+
await available[0].query('LOAD EXTENSION fts');
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
// Extension may already be loaded or not installed
|
|
300
|
+
}
|
|
301
|
+
pool.set(repoId, {
|
|
302
|
+
db: existingDb,
|
|
303
|
+
available,
|
|
304
|
+
checkedOut: 0,
|
|
305
|
+
waiters: [],
|
|
306
|
+
lastUsed: Date.now(),
|
|
307
|
+
dbPath,
|
|
308
|
+
closed: false
|
|
309
|
+
});
|
|
310
|
+
ensureIdleTimer();
|
|
311
|
+
}
|
|
187
312
|
/**
|
|
188
313
|
* Checkout a connection from the pool.
|
|
189
314
|
* Returns an available connection, or creates a new one if under the cap.
|
|
@@ -195,11 +320,14 @@ function checkout(entry) {
|
|
|
195
320
|
entry.checkedOut++;
|
|
196
321
|
return Promise.resolve(entry.available.pop());
|
|
197
322
|
}
|
|
198
|
-
//
|
|
323
|
+
// Pool was pre-warmed to MAX_CONNS_PER_REPO during init. If we're here
|
|
324
|
+
// with fewer total connections, something leaked — surface the bug rather
|
|
325
|
+
// than silently creating a connection (which would silence stdout mid-query).
|
|
199
326
|
const totalConns = entry.available.length + entry.checkedOut;
|
|
200
327
|
if (totalConns < MAX_CONNS_PER_REPO) {
|
|
201
|
-
|
|
202
|
-
|
|
328
|
+
throw new Error(`Connection pool integrity error: expected ${MAX_CONNS_PER_REPO} ` +
|
|
329
|
+
`connections but found ${totalConns} (${entry.available.length} available, ` +
|
|
330
|
+
`${entry.checkedOut} checked out)`);
|
|
203
331
|
}
|
|
204
332
|
// At capacity — queue the caller with a timeout.
|
|
205
333
|
return new Promise((resolve, reject) => {
|
|
@@ -218,10 +346,17 @@ function checkout(entry) {
|
|
|
218
346
|
}
|
|
219
347
|
/**
|
|
220
348
|
* Return a connection to the pool after use.
|
|
349
|
+
* If the pool entry was closed while the connection was checked out (e.g.
|
|
350
|
+
* LRU eviction), close the orphaned connection instead of returning it.
|
|
221
351
|
* If there are queued waiters, hand the connection directly to the next one
|
|
222
352
|
* instead of putting it back in the available array (avoids race conditions).
|
|
223
353
|
*/
|
|
224
354
|
function checkin(entry, conn) {
|
|
355
|
+
if (entry.closed) {
|
|
356
|
+
// Pool entry was deleted during checkout — close the orphaned connection
|
|
357
|
+
conn.close().catch(() => { });
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
225
360
|
if (entry.waiters.length > 0) {
|
|
226
361
|
// Hand directly to the next waiter — no intermediate available state
|
|
227
362
|
const waiter = entry.waiters.shift();
|
|
@@ -249,6 +384,9 @@ export const executeQuery = async (repoId, cypher) => {
|
|
|
249
384
|
if (!entry) {
|
|
250
385
|
throw new Error(`LadybugDB not initialized for repo "${repoId}". Call initLbug first.`);
|
|
251
386
|
}
|
|
387
|
+
if (isWriteQuery(cypher)) {
|
|
388
|
+
throw new Error('Write operations are not allowed. The pool adapter is read-only.');
|
|
389
|
+
}
|
|
252
390
|
entry.lastUsed = Date.now();
|
|
253
391
|
const conn = await checkout(entry);
|
|
254
392
|
try {
|
|
@@ -309,3 +447,9 @@ export const closeLbug = async (repoId) => {
|
|
|
309
447
|
* Check if a specific repo's pool is active
|
|
310
448
|
*/
|
|
311
449
|
export const isLbugReady = (repoId) => pool.has(repoId);
|
|
450
|
+
/** Regex to detect write operations in user-supplied Cypher queries */
|
|
451
|
+
export const CYPHER_WRITE_RE = /\b(CREATE|DELETE|SET|MERGE|REMOVE|DROP|ALTER|COPY|DETACH)\b/i;
|
|
452
|
+
/** Check if a Cypher query contains write operations */
|
|
453
|
+
export function isWriteQuery(query) {
|
|
454
|
+
return CYPHER_WRITE_RE.test(query);
|
|
455
|
+
}
|
|
@@ -37,7 +37,7 @@ 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
41
|
/** Regex to detect write operations in user-supplied Cypher queries */
|
|
42
42
|
export const CYPHER_WRITE_RE = /\b(CREATE|DELETE|SET|MERGE|REMOVE|DROP|ALTER|COPY|DETACH)\b/i;
|
|
43
43
|
/** Check if a Cypher query contains write operations */
|
|
@@ -788,14 +788,14 @@ export class LocalBackend {
|
|
|
788
788
|
// Categorized incoming refs
|
|
789
789
|
const incomingRows = await executeParameterized(repo.id, `
|
|
790
790
|
MATCH (caller)-[r:CodeRelation]->(n {id: $symId})
|
|
791
|
-
WHERE r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS']
|
|
791
|
+
WHERE r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS', 'HAS_METHOD', 'HAS_PROPERTY', 'OVERRIDES', 'ACCESSES']
|
|
792
792
|
RETURN r.type AS relType, caller.id AS uid, caller.name AS name, caller.filePath AS filePath, labels(caller)[0] AS kind
|
|
793
793
|
LIMIT 30
|
|
794
794
|
`, { symId });
|
|
795
795
|
// Categorized outgoing refs
|
|
796
796
|
const outgoingRows = await executeParameterized(repo.id, `
|
|
797
797
|
MATCH (n {id: $symId})-[r:CodeRelation]->(target)
|
|
798
|
-
WHERE r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS']
|
|
798
|
+
WHERE r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS', 'HAS_METHOD', 'HAS_PROPERTY', 'OVERRIDES', 'ACCESSES']
|
|
799
799
|
RETURN r.type AS relType, target.id AS uid, target.name AS name, target.filePath AS filePath, labels(target)[0] AS kind
|
|
800
800
|
LIMIT 30
|
|
801
801
|
`, { symId });
|
package/dist/mcp/resources.js
CHANGED
|
@@ -271,6 +271,13 @@ 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
|
+
|
|
274
281
|
relationships:
|
|
275
282
|
- CONTAINS: File/Folder contains child
|
|
276
283
|
- DEFINES: File defines a symbol
|
|
@@ -278,6 +285,10 @@ relationships:
|
|
|
278
285
|
- IMPORTS: Module imports
|
|
279
286
|
- EXTENDS: Class inheritance
|
|
280
287
|
- IMPLEMENTS: Interface implementation
|
|
288
|
+
- HAS_METHOD: Class/Struct/Interface owns a Method
|
|
289
|
+
- HAS_PROPERTY: Class/Struct/Interface owns a Property (field)
|
|
290
|
+
- ACCESSES: Function/Method reads or writes a Property (reason: 'read' or 'write')
|
|
291
|
+
- OVERRIDES: Method overrides another Method (MRO)
|
|
281
292
|
- MEMBER_OF: Symbol belongs to community
|
|
282
293
|
- STEP_IN_PROCESS: Symbol is step N in process
|
|
283
294
|
|
package/dist/mcp/server.js
CHANGED
|
@@ -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
|
-
//
|
|
241
|
-
|
|
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(
|
|
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
|
|
|
@@ -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
|
-
|
|
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.' },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gitnexus",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.7",
|
|
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",
|
|
@@ -39,9 +39,9 @@
|
|
|
39
39
|
"scripts": {
|
|
40
40
|
"build": "tsc",
|
|
41
41
|
"dev": "tsx watch src/cli/index.ts",
|
|
42
|
-
"test": "vitest run
|
|
42
|
+
"test": "vitest run",
|
|
43
|
+
"test:unit": "vitest run test/unit",
|
|
43
44
|
"test:integration": "vitest run test/integration",
|
|
44
|
-
"test:all": "vitest run",
|
|
45
45
|
"test:watch": "vitest",
|
|
46
46
|
"test:coverage": "vitest run --coverage",
|
|
47
47
|
"prepare": "npm run build",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"graphology": "^0.25.4",
|
|
60
60
|
"graphology-indices": "^0.17.0",
|
|
61
61
|
"graphology-utils": "^2.3.0",
|
|
62
|
-
"@ladybugdb/core": "^0.15.
|
|
62
|
+
"@ladybugdb/core": "^0.15.2",
|
|
63
63
|
"ignore": "^7.0.5",
|
|
64
64
|
"lru-cache": "^11.0.0",
|
|
65
65
|
"mnemonist": "^0.39.0",
|