gitnexus 1.3.6 → 1.3.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/cli/ai-context.js +77 -23
- package/dist/cli/analyze.js +0 -5
- package/dist/cli/eval-server.d.ts +7 -0
- package/dist/cli/eval-server.js +16 -7
- package/dist/cli/index.js +2 -20
- package/dist/cli/mcp.js +2 -0
- package/dist/cli/setup.js +6 -1
- package/dist/config/supported-languages.d.ts +1 -0
- package/dist/config/supported-languages.js +1 -0
- package/dist/core/ingestion/call-processor.d.ts +5 -1
- package/dist/core/ingestion/call-processor.js +78 -0
- package/dist/core/ingestion/framework-detection.d.ts +1 -0
- package/dist/core/ingestion/framework-detection.js +49 -2
- package/dist/core/ingestion/import-processor.js +90 -39
- package/dist/core/ingestion/parsing-processor.d.ts +12 -1
- package/dist/core/ingestion/parsing-processor.js +92 -51
- package/dist/core/ingestion/pipeline.js +21 -2
- package/dist/core/ingestion/process-processor.js +0 -1
- package/dist/core/ingestion/tree-sitter-queries.d.ts +1 -0
- package/dist/core/ingestion/tree-sitter-queries.js +80 -0
- package/dist/core/ingestion/utils.d.ts +5 -0
- package/dist/core/ingestion/utils.js +20 -0
- package/dist/core/ingestion/workers/parse-worker.d.ts +11 -0
- package/dist/core/ingestion/workers/parse-worker.js +473 -51
- package/dist/core/kuzu/csv-generator.d.ts +4 -0
- package/dist/core/kuzu/csv-generator.js +23 -9
- package/dist/core/kuzu/kuzu-adapter.js +9 -3
- package/dist/core/tree-sitter/parser-loader.d.ts +1 -0
- package/dist/core/tree-sitter/parser-loader.js +3 -0
- package/dist/mcp/core/kuzu-adapter.d.ts +4 -3
- package/dist/mcp/core/kuzu-adapter.js +79 -16
- package/dist/mcp/local/local-backend.d.ts +13 -0
- package/dist/mcp/local/local-backend.js +148 -105
- package/dist/mcp/server.js +26 -11
- package/dist/storage/git.js +4 -1
- package/dist/storage/repo-manager.js +16 -2
- package/hooks/claude/gitnexus-hook.cjs +28 -8
- package/hooks/claude/pre-tool-use.sh +2 -1
- package/package.json +11 -3
- package/dist/cli/claude-hooks.d.ts +0 -22
- package/dist/cli/claude-hooks.js +0 -97
- package/dist/cli/view.d.ts +0 -13
- package/dist/cli/view.js +0 -59
- package/dist/core/graph/html-graph-viewer.d.ts +0 -15
- package/dist/core/graph/html-graph-viewer.js +0 -542
- package/dist/core/graph/html-graph-viewer.test.d.ts +0 -1
- package/dist/core/graph/html-graph-viewer.test.js +0 -67
|
@@ -19,7 +19,7 @@ const FLUSH_EVERY = 500;
|
|
|
19
19
|
// ============================================================================
|
|
20
20
|
// CSV ESCAPE UTILITIES
|
|
21
21
|
// ============================================================================
|
|
22
|
-
const sanitizeUTF8 = (str) => {
|
|
22
|
+
export const sanitizeUTF8 = (str) => {
|
|
23
23
|
return str
|
|
24
24
|
.replace(/\r\n/g, '\n')
|
|
25
25
|
.replace(/\r/g, '\n')
|
|
@@ -27,14 +27,14 @@ const sanitizeUTF8 = (str) => {
|
|
|
27
27
|
.replace(/[\uD800-\uDFFF]/g, '')
|
|
28
28
|
.replace(/[\uFFFE\uFFFF]/g, '');
|
|
29
29
|
};
|
|
30
|
-
const escapeCSVField = (value) => {
|
|
30
|
+
export const escapeCSVField = (value) => {
|
|
31
31
|
if (value === undefined || value === null)
|
|
32
32
|
return '""';
|
|
33
33
|
let str = String(value);
|
|
34
34
|
str = sanitizeUTF8(str);
|
|
35
35
|
return `"${str.replace(/"/g, '""')}"`;
|
|
36
36
|
};
|
|
37
|
-
const escapeCSVNumber = (value, defaultValue = -1) => {
|
|
37
|
+
export const escapeCSVNumber = (value, defaultValue = -1) => {
|
|
38
38
|
if (value === undefined || value === null)
|
|
39
39
|
return String(defaultValue);
|
|
40
40
|
return String(value);
|
|
@@ -42,7 +42,7 @@ const escapeCSVNumber = (value, defaultValue = -1) => {
|
|
|
42
42
|
// ============================================================================
|
|
43
43
|
// CONTENT EXTRACTION (lazy — reads from disk on demand)
|
|
44
44
|
// ============================================================================
|
|
45
|
-
const isBinaryContent = (content) => {
|
|
45
|
+
export const isBinaryContent = (content) => {
|
|
46
46
|
if (!content || content.length === 0)
|
|
47
47
|
return false;
|
|
48
48
|
const sample = content.slice(0, 1000);
|
|
@@ -72,8 +72,15 @@ class FileContentCache {
|
|
|
72
72
|
if (!relativePath)
|
|
73
73
|
return '';
|
|
74
74
|
const cached = this.cache.get(relativePath);
|
|
75
|
-
if (cached !== undefined)
|
|
75
|
+
if (cached !== undefined) {
|
|
76
|
+
// Move to end of accessOrder (LRU promotion)
|
|
77
|
+
const idx = this.accessOrder.indexOf(relativePath);
|
|
78
|
+
if (idx !== -1) {
|
|
79
|
+
this.accessOrder.splice(idx, 1);
|
|
80
|
+
this.accessOrder.push(relativePath);
|
|
81
|
+
}
|
|
76
82
|
return cached;
|
|
83
|
+
}
|
|
77
84
|
try {
|
|
78
85
|
const fullPath = path.join(this.repoPath, relativePath);
|
|
79
86
|
const content = await fs.readFile(fullPath, 'utf-8');
|
|
@@ -150,11 +157,18 @@ class BufferedCSVWriter {
|
|
|
150
157
|
const chunk = this.buffer.join('\n') + '\n';
|
|
151
158
|
this.buffer.length = 0;
|
|
152
159
|
return new Promise((resolve, reject) => {
|
|
160
|
+
this.ws.once('error', reject);
|
|
153
161
|
const ok = this.ws.write(chunk);
|
|
154
|
-
if (ok)
|
|
162
|
+
if (ok) {
|
|
163
|
+
this.ws.removeListener('error', reject);
|
|
155
164
|
resolve();
|
|
156
|
-
|
|
157
|
-
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
this.ws.once('drain', () => {
|
|
168
|
+
this.ws.removeListener('error', reject);
|
|
169
|
+
resolve();
|
|
170
|
+
});
|
|
171
|
+
}
|
|
158
172
|
});
|
|
159
173
|
}
|
|
160
174
|
async finish() {
|
|
@@ -234,7 +248,7 @@ export const streamAllCSVsToDisk = async (graph, repoPath, csvDir) => {
|
|
|
234
248
|
break;
|
|
235
249
|
case 'Community': {
|
|
236
250
|
const keywords = node.properties.keywords || [];
|
|
237
|
-
const keywordsStr = `[${keywords.map((k) => `'${k.replace(/'/g, "''")}'`).join(',')}]`;
|
|
251
|
+
const keywordsStr = `[${keywords.map((k) => `'${k.replace(/\\/g, '\\\\').replace(/'/g, "''").replace(/,/g, '\\,')}'`).join(',')}]`;
|
|
238
252
|
await communityWriter.addRow([
|
|
239
253
|
escapeCSVField(node.id),
|
|
240
254
|
escapeCSVField(node.properties.name || ''),
|
|
@@ -663,11 +663,17 @@ export const loadFTSExtension = async () => {
|
|
|
663
663
|
try {
|
|
664
664
|
await conn.query('INSTALL fts');
|
|
665
665
|
await conn.query('LOAD EXTENSION fts');
|
|
666
|
+
ftsLoaded = true;
|
|
666
667
|
}
|
|
667
|
-
catch {
|
|
668
|
-
|
|
668
|
+
catch (err) {
|
|
669
|
+
const msg = err?.message || '';
|
|
670
|
+
if (msg.includes('already loaded') || msg.includes('already installed') || msg.includes('already exists')) {
|
|
671
|
+
ftsLoaded = true;
|
|
672
|
+
}
|
|
673
|
+
else {
|
|
674
|
+
console.error('GitNexus: FTS extension load failed:', msg);
|
|
675
|
+
}
|
|
669
676
|
}
|
|
670
|
-
ftsLoaded = true;
|
|
671
677
|
};
|
|
672
678
|
/**
|
|
673
679
|
* Create a full-text search index on a table
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import Parser from 'tree-sitter';
|
|
2
2
|
import { SupportedLanguages } from '../../config/supported-languages.js';
|
|
3
|
+
export declare const isLanguageAvailable: (language: SupportedLanguages) => boolean;
|
|
3
4
|
export declare const loadParser: () => Promise<Parser>;
|
|
4
5
|
export declare const loadLanguage: (language: SupportedLanguages, filePath?: string) => Promise<void>;
|
|
@@ -8,6 +8,7 @@ import CPP from 'tree-sitter-cpp';
|
|
|
8
8
|
import CSharp from 'tree-sitter-c-sharp';
|
|
9
9
|
import Go from 'tree-sitter-go';
|
|
10
10
|
import Rust from 'tree-sitter-rust';
|
|
11
|
+
import Kotlin from 'tree-sitter-kotlin';
|
|
11
12
|
import PHP from 'tree-sitter-php';
|
|
12
13
|
import { createRequire } from 'node:module';
|
|
13
14
|
import { SupportedLanguages } from '../../config/supported-languages.js';
|
|
@@ -30,9 +31,11 @@ const languageMap = {
|
|
|
30
31
|
[SupportedLanguages.CSharp]: CSharp,
|
|
31
32
|
[SupportedLanguages.Go]: Go,
|
|
32
33
|
[SupportedLanguages.Rust]: Rust,
|
|
34
|
+
[SupportedLanguages.Kotlin]: Kotlin,
|
|
33
35
|
[SupportedLanguages.PHP]: PHP.php_only,
|
|
34
36
|
...(Swift ? { [SupportedLanguages.Swift]: Swift } : {}),
|
|
35
37
|
};
|
|
38
|
+
export const isLanguageAvailable = (language) => language in languageMap;
|
|
36
39
|
export const loadParser = async () => {
|
|
37
40
|
if (parser)
|
|
38
41
|
return parser;
|
|
@@ -17,11 +17,12 @@
|
|
|
17
17
|
* Retries on lock errors (e.g., when `gitnexus analyze` is running).
|
|
18
18
|
*/
|
|
19
19
|
export declare const initKuzu: (repoId: string, dbPath: string) => Promise<void>;
|
|
20
|
+
export declare const executeQuery: (repoId: string, cypher: string) => Promise<any[]>;
|
|
20
21
|
/**
|
|
21
|
-
* Execute a query on a specific repo's connection pool.
|
|
22
|
-
*
|
|
22
|
+
* Execute a parameterized query on a specific repo's connection pool.
|
|
23
|
+
* Uses prepare/execute pattern to prevent Cypher injection.
|
|
23
24
|
*/
|
|
24
|
-
export declare const
|
|
25
|
+
export declare const executeParameterized: (repoId: string, cypher: string, params: Record<string, any>) => Promise<any[]>;
|
|
25
26
|
/**
|
|
26
27
|
* Close one or all repo pools.
|
|
27
28
|
* If repoId is provided, close only that repo's connections.
|
|
@@ -24,6 +24,9 @@ const MAX_CONNS_PER_REPO = 8;
|
|
|
24
24
|
/** Connections created eagerly on init */
|
|
25
25
|
const INITIAL_CONNS_PER_REPO = 2;
|
|
26
26
|
let idleTimer = null;
|
|
27
|
+
/** Saved real stdout.write — used to silence KuzuDB native output without race conditions */
|
|
28
|
+
const realStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
29
|
+
let stdoutSilenceCount = 0;
|
|
27
30
|
/**
|
|
28
31
|
* Start the idle cleanup timer (runs every 60s)
|
|
29
32
|
*/
|
|
@@ -33,7 +36,7 @@ function ensureIdleTimer() {
|
|
|
33
36
|
idleTimer = setInterval(() => {
|
|
34
37
|
const now = Date.now();
|
|
35
38
|
for (const [repoId, entry] of pool) {
|
|
36
|
-
if (now - entry.lastUsed > IDLE_TIMEOUT_MS) {
|
|
39
|
+
if (now - entry.lastUsed > IDLE_TIMEOUT_MS && entry.checkedOut === 0) {
|
|
37
40
|
closeOne(repoId);
|
|
38
41
|
}
|
|
39
42
|
}
|
|
@@ -51,7 +54,7 @@ function evictLRU() {
|
|
|
51
54
|
let oldestId = null;
|
|
52
55
|
let oldestTime = Infinity;
|
|
53
56
|
for (const [id, entry] of pool) {
|
|
54
|
-
if (entry.lastUsed < oldestTime) {
|
|
57
|
+
if (entry.checkedOut === 0 && entry.lastUsed < oldestTime) {
|
|
55
58
|
oldestTime = entry.lastUsed;
|
|
56
59
|
oldestId = id;
|
|
57
60
|
}
|
|
@@ -71,28 +74,46 @@ function closeOne(repoId) {
|
|
|
71
74
|
try {
|
|
72
75
|
conn.close();
|
|
73
76
|
}
|
|
74
|
-
catch {
|
|
77
|
+
catch (e) {
|
|
78
|
+
console.error('GitNexus [pool:close-conn]:', e instanceof Error ? e.message : e);
|
|
79
|
+
}
|
|
75
80
|
}
|
|
76
81
|
try {
|
|
77
82
|
entry.db.close();
|
|
78
83
|
}
|
|
79
|
-
catch {
|
|
84
|
+
catch (e) {
|
|
85
|
+
console.error('GitNexus [pool:close-db]:', e instanceof Error ? e.message : e);
|
|
86
|
+
}
|
|
80
87
|
pool.delete(repoId);
|
|
81
88
|
}
|
|
82
89
|
/**
|
|
83
90
|
* Create a new Connection from a repo's Database.
|
|
84
91
|
* Silences stdout to prevent native module output from corrupting MCP stdio.
|
|
85
92
|
*/
|
|
93
|
+
function silenceStdout() {
|
|
94
|
+
if (stdoutSilenceCount++ === 0) {
|
|
95
|
+
process.stdout.write = (() => true);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function restoreStdout() {
|
|
99
|
+
if (--stdoutSilenceCount <= 0) {
|
|
100
|
+
stdoutSilenceCount = 0;
|
|
101
|
+
process.stdout.write = realStdoutWrite;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
86
104
|
function createConnection(db) {
|
|
87
|
-
|
|
88
|
-
process.stdout.write = (() => true);
|
|
105
|
+
silenceStdout();
|
|
89
106
|
try {
|
|
90
107
|
return new kuzu.Connection(db);
|
|
91
108
|
}
|
|
92
109
|
finally {
|
|
93
|
-
|
|
110
|
+
restoreStdout();
|
|
94
111
|
}
|
|
95
112
|
}
|
|
113
|
+
/** Query timeout in milliseconds */
|
|
114
|
+
const QUERY_TIMEOUT_MS = 30_000;
|
|
115
|
+
/** Waiter queue timeout in milliseconds */
|
|
116
|
+
const WAITER_TIMEOUT_MS = 15_000;
|
|
96
117
|
const LOCK_RETRY_ATTEMPTS = 3;
|
|
97
118
|
const LOCK_RETRY_DELAY_MS = 2000;
|
|
98
119
|
/**
|
|
@@ -118,13 +139,12 @@ export const initKuzu = async (repoId, dbPath) => {
|
|
|
118
139
|
// avoids lock conflicts when `gitnexus analyze` is writing.
|
|
119
140
|
let lastError = null;
|
|
120
141
|
for (let attempt = 1; attempt <= LOCK_RETRY_ATTEMPTS; attempt++) {
|
|
121
|
-
|
|
122
|
-
process.stdout.write = (() => true);
|
|
142
|
+
silenceStdout();
|
|
123
143
|
try {
|
|
124
144
|
const db = new kuzu.Database(dbPath, 0, // bufferManagerSize (default)
|
|
125
145
|
false, // enableCompression (default)
|
|
126
146
|
true);
|
|
127
|
-
|
|
147
|
+
restoreStdout();
|
|
128
148
|
// Pre-create a small pool of connections
|
|
129
149
|
const available = [];
|
|
130
150
|
for (let i = 0; i < INITIAL_CONNS_PER_REPO; i++) {
|
|
@@ -135,7 +155,7 @@ export const initKuzu = async (repoId, dbPath) => {
|
|
|
135
155
|
return;
|
|
136
156
|
}
|
|
137
157
|
catch (err) {
|
|
138
|
-
|
|
158
|
+
restoreStdout();
|
|
139
159
|
lastError = err instanceof Error ? err : new Error(String(err));
|
|
140
160
|
const isLockError = lastError.message.includes('Could not set lock')
|
|
141
161
|
|| lastError.message.includes('lock');
|
|
@@ -164,10 +184,19 @@ function checkout(entry) {
|
|
|
164
184
|
entry.checkedOut++;
|
|
165
185
|
return Promise.resolve(createConnection(entry.db));
|
|
166
186
|
}
|
|
167
|
-
// At capacity — queue the caller
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
187
|
+
// At capacity — queue the caller with a timeout.
|
|
188
|
+
return new Promise((resolve, reject) => {
|
|
189
|
+
const waiter = (conn) => {
|
|
190
|
+
clearTimeout(timer);
|
|
191
|
+
resolve(conn);
|
|
192
|
+
};
|
|
193
|
+
const timer = setTimeout(() => {
|
|
194
|
+
const idx = entry.waiters.indexOf(waiter);
|
|
195
|
+
if (idx !== -1)
|
|
196
|
+
entry.waiters.splice(idx, 1);
|
|
197
|
+
reject(new Error(`Connection pool exhausted: timed out after ${WAITER_TIMEOUT_MS}ms waiting for a free connection`));
|
|
198
|
+
}, WAITER_TIMEOUT_MS);
|
|
199
|
+
entry.waiters.push(waiter);
|
|
171
200
|
});
|
|
172
201
|
}
|
|
173
202
|
/**
|
|
@@ -190,6 +219,14 @@ function checkin(entry, conn) {
|
|
|
190
219
|
* Execute a query on a specific repo's connection pool.
|
|
191
220
|
* Automatically checks out a connection, runs the query, and returns it.
|
|
192
221
|
*/
|
|
222
|
+
/** Race a promise against a timeout */
|
|
223
|
+
function withTimeout(promise, ms, label) {
|
|
224
|
+
let timer;
|
|
225
|
+
const timeout = new Promise((_, reject) => {
|
|
226
|
+
timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
|
|
227
|
+
});
|
|
228
|
+
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
|
|
229
|
+
}
|
|
193
230
|
export const executeQuery = async (repoId, cypher) => {
|
|
194
231
|
const entry = pool.get(repoId);
|
|
195
232
|
if (!entry) {
|
|
@@ -198,7 +235,33 @@ export const executeQuery = async (repoId, cypher) => {
|
|
|
198
235
|
entry.lastUsed = Date.now();
|
|
199
236
|
const conn = await checkout(entry);
|
|
200
237
|
try {
|
|
201
|
-
const queryResult = await conn.query(cypher);
|
|
238
|
+
const queryResult = await withTimeout(conn.query(cypher), QUERY_TIMEOUT_MS, 'Query');
|
|
239
|
+
const result = Array.isArray(queryResult) ? queryResult[0] : queryResult;
|
|
240
|
+
const rows = await result.getAll();
|
|
241
|
+
return rows;
|
|
242
|
+
}
|
|
243
|
+
finally {
|
|
244
|
+
checkin(entry, conn);
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
/**
|
|
248
|
+
* Execute a parameterized query on a specific repo's connection pool.
|
|
249
|
+
* Uses prepare/execute pattern to prevent Cypher injection.
|
|
250
|
+
*/
|
|
251
|
+
export const executeParameterized = async (repoId, cypher, params) => {
|
|
252
|
+
const entry = pool.get(repoId);
|
|
253
|
+
if (!entry) {
|
|
254
|
+
throw new Error(`KuzuDB not initialized for repo "${repoId}". Call initKuzu first.`);
|
|
255
|
+
}
|
|
256
|
+
entry.lastUsed = Date.now();
|
|
257
|
+
const conn = await checkout(entry);
|
|
258
|
+
try {
|
|
259
|
+
const stmt = await withTimeout(conn.prepare(cypher), QUERY_TIMEOUT_MS, 'Prepare');
|
|
260
|
+
if (!stmt.isSuccess()) {
|
|
261
|
+
const errMsg = await stmt.getErrorMessage();
|
|
262
|
+
throw new Error(`Prepare failed: ${errMsg}`);
|
|
263
|
+
}
|
|
264
|
+
const queryResult = await withTimeout(conn.execute(stmt, params), QUERY_TIMEOUT_MS, 'Execute');
|
|
202
265
|
const result = Array.isArray(queryResult) ? queryResult[0] : queryResult;
|
|
203
266
|
const rows = await result.getAll();
|
|
204
267
|
return rows;
|
|
@@ -6,6 +6,19 @@
|
|
|
6
6
|
* KuzuDB connections are opened lazily per repo on first query.
|
|
7
7
|
*/
|
|
8
8
|
import { type RegistryEntry } from '../../storage/repo-manager.js';
|
|
9
|
+
/**
|
|
10
|
+
* Quick test-file detection for filtering impact results.
|
|
11
|
+
* Matches common test file patterns across all supported languages.
|
|
12
|
+
*/
|
|
13
|
+
export declare function isTestFilePath(filePath: string): boolean;
|
|
14
|
+
/** Valid KuzuDB node labels for safe Cypher query construction */
|
|
15
|
+
export declare const VALID_NODE_LABELS: Set<string>;
|
|
16
|
+
/** Valid relation types for impact analysis filtering */
|
|
17
|
+
export declare const VALID_RELATION_TYPES: Set<string>;
|
|
18
|
+
/** Regex to detect write operations in user-supplied Cypher queries */
|
|
19
|
+
export declare const CYPHER_WRITE_RE: RegExp;
|
|
20
|
+
/** Check if a Cypher query contains write operations */
|
|
21
|
+
export declare function isWriteQuery(query: string): boolean;
|
|
9
22
|
export interface CodebaseContext {
|
|
10
23
|
projectName: string;
|
|
11
24
|
stats: {
|