gitnexus 1.3.6 → 1.3.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 (47) hide show
  1. package/dist/cli/ai-context.js +77 -23
  2. package/dist/cli/analyze.js +4 -11
  3. package/dist/cli/eval-server.d.ts +7 -0
  4. package/dist/cli/eval-server.js +16 -7
  5. package/dist/cli/index.js +2 -20
  6. package/dist/cli/mcp.js +2 -0
  7. package/dist/cli/setup.js +6 -1
  8. package/dist/config/supported-languages.d.ts +1 -0
  9. package/dist/config/supported-languages.js +1 -0
  10. package/dist/core/ingestion/call-processor.d.ts +5 -1
  11. package/dist/core/ingestion/call-processor.js +78 -0
  12. package/dist/core/ingestion/framework-detection.d.ts +1 -0
  13. package/dist/core/ingestion/framework-detection.js +49 -2
  14. package/dist/core/ingestion/import-processor.js +90 -39
  15. package/dist/core/ingestion/parsing-processor.d.ts +12 -1
  16. package/dist/core/ingestion/parsing-processor.js +92 -51
  17. package/dist/core/ingestion/pipeline.js +21 -2
  18. package/dist/core/ingestion/process-processor.js +0 -1
  19. package/dist/core/ingestion/tree-sitter-queries.d.ts +1 -0
  20. package/dist/core/ingestion/tree-sitter-queries.js +80 -0
  21. package/dist/core/ingestion/utils.d.ts +5 -0
  22. package/dist/core/ingestion/utils.js +20 -0
  23. package/dist/core/ingestion/workers/parse-worker.d.ts +11 -0
  24. package/dist/core/ingestion/workers/parse-worker.js +473 -51
  25. package/dist/core/kuzu/csv-generator.d.ts +4 -0
  26. package/dist/core/kuzu/csv-generator.js +23 -9
  27. package/dist/core/kuzu/kuzu-adapter.js +9 -3
  28. package/dist/core/tree-sitter/parser-loader.d.ts +1 -0
  29. package/dist/core/tree-sitter/parser-loader.js +3 -0
  30. package/dist/mcp/core/kuzu-adapter.d.ts +4 -3
  31. package/dist/mcp/core/kuzu-adapter.js +79 -16
  32. package/dist/mcp/local/local-backend.d.ts +13 -0
  33. package/dist/mcp/local/local-backend.js +148 -105
  34. package/dist/mcp/server.js +26 -11
  35. package/dist/storage/git.js +4 -1
  36. package/dist/storage/repo-manager.js +16 -2
  37. package/hooks/claude/gitnexus-hook.cjs +28 -8
  38. package/hooks/claude/pre-tool-use.sh +2 -1
  39. package/package.json +11 -3
  40. package/dist/cli/claude-hooks.d.ts +0 -22
  41. package/dist/cli/claude-hooks.js +0 -97
  42. package/dist/cli/view.d.ts +0 -13
  43. package/dist/cli/view.js +0 -59
  44. package/dist/core/graph/html-graph-viewer.d.ts +0 -15
  45. package/dist/core/graph/html-graph-viewer.js +0 -542
  46. package/dist/core/graph/html-graph-viewer.test.d.ts +0 -1
  47. 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
- else
157
- this.ws.once('drain', resolve);
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
- // Extension may already be loaded
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
- * Automatically checks out a connection, runs the query, and returns it.
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 executeQuery: (repoId: string, cypher: string) => Promise<any[]>;
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
- const origWrite = process.stdout.write;
88
- process.stdout.write = (() => true);
105
+ silenceStdout();
89
106
  try {
90
107
  return new kuzu.Connection(db);
91
108
  }
92
109
  finally {
93
- process.stdout.write = origWrite;
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
- const origWrite = process.stdout.write;
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
- process.stdout.write = origWrite;
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
- process.stdout.write = origWrite;
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. checkin() will resolve this when
168
- // a connection is returned, handing it directly to the next waiter.
169
- return new Promise(resolve => {
170
- entry.waiters.push(resolve);
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: {