gitnexus 1.0.0

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 (110) hide show
  1. package/README.md +181 -0
  2. package/dist/cli/ai-context.d.ts +21 -0
  3. package/dist/cli/ai-context.js +219 -0
  4. package/dist/cli/analyze.d.ts +10 -0
  5. package/dist/cli/analyze.js +118 -0
  6. package/dist/cli/clean.d.ts +8 -0
  7. package/dist/cli/clean.js +29 -0
  8. package/dist/cli/index.d.ts +2 -0
  9. package/dist/cli/index.js +42 -0
  10. package/dist/cli/list.d.ts +6 -0
  11. package/dist/cli/list.js +27 -0
  12. package/dist/cli/mcp.d.ts +7 -0
  13. package/dist/cli/mcp.js +85 -0
  14. package/dist/cli/serve.d.ts +3 -0
  15. package/dist/cli/serve.js +5 -0
  16. package/dist/cli/status.d.ts +6 -0
  17. package/dist/cli/status.js +27 -0
  18. package/dist/config/ignore-service.d.ts +1 -0
  19. package/dist/config/ignore-service.js +208 -0
  20. package/dist/config/supported-languages.d.ts +11 -0
  21. package/dist/config/supported-languages.js +15 -0
  22. package/dist/core/embeddings/embedder.d.ts +60 -0
  23. package/dist/core/embeddings/embedder.js +205 -0
  24. package/dist/core/embeddings/embedding-pipeline.d.ts +50 -0
  25. package/dist/core/embeddings/embedding-pipeline.js +321 -0
  26. package/dist/core/embeddings/index.d.ts +9 -0
  27. package/dist/core/embeddings/index.js +9 -0
  28. package/dist/core/embeddings/text-generator.d.ts +24 -0
  29. package/dist/core/embeddings/text-generator.js +182 -0
  30. package/dist/core/embeddings/types.d.ts +87 -0
  31. package/dist/core/embeddings/types.js +32 -0
  32. package/dist/core/graph/graph.d.ts +2 -0
  33. package/dist/core/graph/graph.js +61 -0
  34. package/dist/core/graph/types.d.ts +50 -0
  35. package/dist/core/graph/types.js +1 -0
  36. package/dist/core/ingestion/ast-cache.d.ts +11 -0
  37. package/dist/core/ingestion/ast-cache.js +34 -0
  38. package/dist/core/ingestion/call-processor.d.ts +8 -0
  39. package/dist/core/ingestion/call-processor.js +269 -0
  40. package/dist/core/ingestion/cluster-enricher.d.ts +38 -0
  41. package/dist/core/ingestion/cluster-enricher.js +170 -0
  42. package/dist/core/ingestion/community-processor.d.ts +39 -0
  43. package/dist/core/ingestion/community-processor.js +269 -0
  44. package/dist/core/ingestion/entry-point-scoring.d.ts +39 -0
  45. package/dist/core/ingestion/entry-point-scoring.js +235 -0
  46. package/dist/core/ingestion/filesystem-walker.d.ts +5 -0
  47. package/dist/core/ingestion/filesystem-walker.js +26 -0
  48. package/dist/core/ingestion/framework-detection.d.ts +38 -0
  49. package/dist/core/ingestion/framework-detection.js +183 -0
  50. package/dist/core/ingestion/heritage-processor.d.ts +14 -0
  51. package/dist/core/ingestion/heritage-processor.js +134 -0
  52. package/dist/core/ingestion/import-processor.d.ts +8 -0
  53. package/dist/core/ingestion/import-processor.js +490 -0
  54. package/dist/core/ingestion/parsing-processor.d.ts +8 -0
  55. package/dist/core/ingestion/parsing-processor.js +249 -0
  56. package/dist/core/ingestion/pipeline.d.ts +2 -0
  57. package/dist/core/ingestion/pipeline.js +228 -0
  58. package/dist/core/ingestion/process-processor.d.ts +51 -0
  59. package/dist/core/ingestion/process-processor.js +278 -0
  60. package/dist/core/ingestion/structure-processor.d.ts +2 -0
  61. package/dist/core/ingestion/structure-processor.js +36 -0
  62. package/dist/core/ingestion/symbol-table.d.ts +33 -0
  63. package/dist/core/ingestion/symbol-table.js +38 -0
  64. package/dist/core/ingestion/tree-sitter-queries.d.ts +11 -0
  65. package/dist/core/ingestion/tree-sitter-queries.js +319 -0
  66. package/dist/core/ingestion/utils.d.ts +10 -0
  67. package/dist/core/ingestion/utils.js +44 -0
  68. package/dist/core/kuzu/csv-generator.d.ts +22 -0
  69. package/dist/core/kuzu/csv-generator.js +272 -0
  70. package/dist/core/kuzu/kuzu-adapter.d.ts +81 -0
  71. package/dist/core/kuzu/kuzu-adapter.js +568 -0
  72. package/dist/core/kuzu/schema.d.ts +53 -0
  73. package/dist/core/kuzu/schema.js +380 -0
  74. package/dist/core/search/bm25-index.d.ts +22 -0
  75. package/dist/core/search/bm25-index.js +52 -0
  76. package/dist/core/search/hybrid-search.d.ts +49 -0
  77. package/dist/core/search/hybrid-search.js +118 -0
  78. package/dist/core/tree-sitter/parser-loader.d.ts +4 -0
  79. package/dist/core/tree-sitter/parser-loader.js +42 -0
  80. package/dist/lib/utils.d.ts +1 -0
  81. package/dist/lib/utils.js +3 -0
  82. package/dist/mcp/core/embedder.d.ts +27 -0
  83. package/dist/mcp/core/embedder.js +93 -0
  84. package/dist/mcp/core/kuzu-adapter.d.ts +23 -0
  85. package/dist/mcp/core/kuzu-adapter.js +62 -0
  86. package/dist/mcp/local/local-backend.d.ts +73 -0
  87. package/dist/mcp/local/local-backend.js +752 -0
  88. package/dist/mcp/resources.d.ts +31 -0
  89. package/dist/mcp/resources.js +279 -0
  90. package/dist/mcp/server.d.ts +12 -0
  91. package/dist/mcp/server.js +130 -0
  92. package/dist/mcp/staleness.d.ts +15 -0
  93. package/dist/mcp/staleness.js +29 -0
  94. package/dist/mcp/tools.d.ts +24 -0
  95. package/dist/mcp/tools.js +160 -0
  96. package/dist/server/api.d.ts +6 -0
  97. package/dist/server/api.js +156 -0
  98. package/dist/storage/git.d.ts +7 -0
  99. package/dist/storage/git.js +39 -0
  100. package/dist/storage/repo-manager.d.ts +61 -0
  101. package/dist/storage/repo-manager.js +106 -0
  102. package/dist/types/pipeline.d.ts +28 -0
  103. package/dist/types/pipeline.js +16 -0
  104. package/package.json +80 -0
  105. package/skills/debugging.md +104 -0
  106. package/skills/exploring.md +112 -0
  107. package/skills/impact-analysis.md +114 -0
  108. package/skills/refactoring.md +119 -0
  109. package/vendor/leiden/index.cjs +355 -0
  110. package/vendor/leiden/utils.cjs +392 -0
@@ -0,0 +1,81 @@
1
+ import kuzu from 'kuzu';
2
+ import { KnowledgeGraph } from '../graph/types.js';
3
+ export declare const initKuzu: (dbPath: string) => Promise<{
4
+ db: kuzu.Database;
5
+ conn: kuzu.Connection;
6
+ }>;
7
+ export declare const loadGraphToKuzu: (graph: KnowledgeGraph, fileContents: Map<string, string>, storagePath: string) => Promise<{
8
+ success: boolean;
9
+ insertedRels: number;
10
+ skippedRels: number;
11
+ }>;
12
+ /**
13
+ * Insert a single node to KuzuDB
14
+ * @param label - Node type (File, Function, Class, etc.)
15
+ * @param properties - Node properties
16
+ * @param dbPath - Path to KuzuDB database (optional if already initialized)
17
+ */
18
+ export declare const insertNodeToKuzu: (label: string, properties: Record<string, any>, dbPath?: string) => Promise<boolean>;
19
+ /**
20
+ * Batch insert multiple nodes to KuzuDB using a single connection
21
+ * @param nodes - Array of {label, properties} to insert
22
+ * @param dbPath - Path to KuzuDB database
23
+ * @returns Object with success count and error count
24
+ */
25
+ export declare const batchInsertNodesToKuzu: (nodes: Array<{
26
+ label: string;
27
+ properties: Record<string, any>;
28
+ }>, dbPath: string) => Promise<{
29
+ inserted: number;
30
+ failed: number;
31
+ }>;
32
+ export declare const executeQuery: (cypher: string) => Promise<any[]>;
33
+ export declare const executeWithReusedStatement: (cypher: string, paramsList: Array<Record<string, any>>) => Promise<void>;
34
+ export declare const getKuzuStats: () => Promise<{
35
+ nodes: number;
36
+ edges: number;
37
+ }>;
38
+ export declare const closeKuzu: () => Promise<void>;
39
+ export declare const isKuzuReady: () => boolean;
40
+ /**
41
+ * Delete all nodes (and their relationships) for a specific file from KuzuDB
42
+ * @param filePath - The file path to delete nodes for
43
+ * @param dbPath - Optional path to KuzuDB for per-query connection
44
+ * @returns Object with counts of deleted nodes
45
+ */
46
+ export declare const deleteNodesForFile: (filePath: string, dbPath?: string) => Promise<{
47
+ deletedNodes: number;
48
+ }>;
49
+ export declare const getEmbeddingTableName: () => string;
50
+ /**
51
+ * Load the FTS extension (required before using FTS functions)
52
+ */
53
+ export declare const loadFTSExtension: () => Promise<void>;
54
+ /**
55
+ * Create a full-text search index on a table
56
+ * @param tableName - The node table name (e.g., 'File', 'CodeSymbol')
57
+ * @param indexName - Name for the FTS index
58
+ * @param properties - List of properties to index (e.g., ['name', 'code'])
59
+ * @param stemmer - Stemming algorithm (default: 'porter')
60
+ */
61
+ export declare const createFTSIndex: (tableName: string, indexName: string, properties: string[], stemmer?: string) => Promise<void>;
62
+ /**
63
+ * Query a full-text search index
64
+ * @param tableName - The node table name
65
+ * @param indexName - FTS index name
66
+ * @param query - Search query string
67
+ * @param limit - Maximum results
68
+ * @param conjunctive - If true, all terms must match (AND); if false, any term matches (OR)
69
+ * @returns Array of { node properties, score }
70
+ */
71
+ export declare const queryFTS: (tableName: string, indexName: string, query: string, limit?: number, conjunctive?: boolean) => Promise<Array<{
72
+ nodeId: string;
73
+ name: string;
74
+ filePath: string;
75
+ score: number;
76
+ [key: string]: any;
77
+ }>>;
78
+ /**
79
+ * Drop an FTS index
80
+ */
81
+ export declare const dropFTSIndex: (tableName: string, indexName: string) => Promise<void>;
@@ -0,0 +1,568 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import kuzu from 'kuzu';
4
+ import { NODE_TABLES, REL_TABLE_NAME, SCHEMA_QUERIES, EMBEDDING_TABLE_NAME, } from './schema.js';
5
+ import { generateAllCSVs } from './csv-generator.js';
6
+ let db = null;
7
+ let conn = null;
8
+ const normalizeCopyPath = (filePath) => filePath.replace(/\\/g, '/');
9
+ export const initKuzu = async (dbPath) => {
10
+ if (conn)
11
+ return { db, conn };
12
+ // kuzu v0.11 stores the database as a single file (not a directory).
13
+ // If the path already exists, it must be a valid kuzu database file.
14
+ // Remove stale empty directories or files from older versions.
15
+ try {
16
+ const stat = await fs.stat(dbPath);
17
+ if (stat.isDirectory()) {
18
+ // Old-style directory database or empty leftover - remove it
19
+ const files = await fs.readdir(dbPath);
20
+ if (files.length === 0) {
21
+ await fs.rmdir(dbPath);
22
+ }
23
+ else {
24
+ // Non-empty directory from older kuzu version - remove entire directory
25
+ await fs.rm(dbPath, { recursive: true, force: true });
26
+ }
27
+ }
28
+ // If it's a file, assume it's an existing kuzu database - kuzu will open it
29
+ }
30
+ catch {
31
+ // Path doesn't exist, which is what kuzu wants for a new database
32
+ }
33
+ // Ensure parent directory exists
34
+ const parentDir = path.dirname(dbPath);
35
+ await fs.mkdir(parentDir, { recursive: true });
36
+ db = new kuzu.Database(dbPath);
37
+ conn = new kuzu.Connection(db);
38
+ for (const schemaQuery of SCHEMA_QUERIES) {
39
+ try {
40
+ await conn.query(schemaQuery);
41
+ }
42
+ catch (err) {
43
+ // Only ignore "already exists" errors - log everything else
44
+ const msg = err instanceof Error ? err.message : String(err);
45
+ if (!msg.includes('already exists')) {
46
+ console.warn(`⚠️ Schema creation warning: ${msg.slice(0, 120)}`);
47
+ }
48
+ }
49
+ }
50
+ return { db, conn };
51
+ };
52
+ export const loadGraphToKuzu = async (graph, fileContents, storagePath) => {
53
+ if (!conn) {
54
+ throw new Error('KuzuDB not initialized. Call initKuzu first.');
55
+ }
56
+ const csvData = generateAllCSVs(graph, fileContents);
57
+ const csvDir = path.join(storagePath, 'csv');
58
+ await fs.mkdir(csvDir, { recursive: true });
59
+ const nodeFiles = [];
60
+ for (const [tableName, csv] of csvData.nodes.entries()) {
61
+ if (csv.split('\n').length <= 1)
62
+ continue;
63
+ const filePath = path.join(csvDir, `${tableName.toLowerCase()}.csv`);
64
+ await fs.writeFile(filePath, csv, 'utf-8');
65
+ nodeFiles.push({ table: tableName, path: filePath });
66
+ }
67
+ const relLines = csvData.relCSV.split('\n').slice(1).filter(line => line.trim());
68
+ for (const { table, path: filePath } of nodeFiles) {
69
+ const normalizedPath = normalizeCopyPath(filePath);
70
+ const copyQuery = getCopyQuery(table, normalizedPath);
71
+ // Log CSV stats for diagnostics
72
+ const csvContent = await fs.readFile(filePath, 'utf-8');
73
+ const csvLines = csvContent.split('\n').length;
74
+ console.log(` COPY ${table}: ${csvLines - 1} rows`);
75
+ try {
76
+ await conn.query(copyQuery);
77
+ }
78
+ catch (err) {
79
+ const errMsg = err instanceof Error ? err.message : String(err);
80
+ console.warn(`⚠️ COPY failed for ${table} (${csvLines - 1} rows): ${errMsg}`);
81
+ // Retry with IGNORE_ERRORS=true to skip malformed rows
82
+ console.log(` Retrying ${table} with IGNORE_ERRORS=true...`);
83
+ try {
84
+ const retryQuery = copyQuery.replace('auto_detect=false)', 'auto_detect=false, IGNORE_ERRORS=true)');
85
+ await conn.query(retryQuery);
86
+ console.log(` ✅ ${table} loaded with IGNORE_ERRORS (some rows may have been skipped)`);
87
+ }
88
+ catch (retryErr) {
89
+ const retryMsg = retryErr instanceof Error ? retryErr.message : String(retryErr);
90
+ console.error(`❌ COPY failed for ${table} even with IGNORE_ERRORS: ${retryMsg}`);
91
+ throw retryErr;
92
+ }
93
+ }
94
+ }
95
+ console.log('✅ All COPY commands succeeded. Starting relationship insertion...');
96
+ // Build a set of valid table names for fast lookup
97
+ const validTables = new Set(NODE_TABLES);
98
+ const getNodeLabel = (nodeId) => {
99
+ if (nodeId.startsWith('comm_'))
100
+ return 'Community';
101
+ if (nodeId.startsWith('proc_'))
102
+ return 'Process';
103
+ return nodeId.split(':')[0];
104
+ };
105
+ // All multi-language tables are created with backticks - must always reference them with backticks
106
+ const escapeLabel = (label) => {
107
+ return BACKTICK_TABLES.has(label) ? `\`${label}\`` : label;
108
+ };
109
+ let insertedRels = 0;
110
+ let skippedRels = 0;
111
+ for (const line of relLines) {
112
+ try {
113
+ const match = line.match(/"([^"]*)","([^"]*)","([^"]*)",([0-9.]+),"([^"]*)",([0-9-]+)/);
114
+ if (!match)
115
+ continue;
116
+ const [, fromId, toId, relType, confidenceStr, reason, stepStr] = match;
117
+ const fromLabel = getNodeLabel(fromId);
118
+ const toLabel = getNodeLabel(toId);
119
+ // Skip relationships where either node's label doesn't have a table in KuzuDB
120
+ // (e.g. Variable, Import, Type nodes that aren't in the schema)
121
+ // Querying a non-existent table causes a fatal native crash
122
+ if (!validTables.has(fromLabel) || !validTables.has(toLabel)) {
123
+ skippedRels++;
124
+ continue;
125
+ }
126
+ const confidence = parseFloat(confidenceStr) || 1.0;
127
+ const step = parseInt(stepStr) || 0;
128
+ const insertQuery = `
129
+ MATCH (a:${escapeLabel(fromLabel)} {id: '${fromId.replace(/'/g, "''")}' }),
130
+ (b:${escapeLabel(toLabel)} {id: '${toId.replace(/'/g, "''")}' })
131
+ CREATE (a)-[:${REL_TABLE_NAME} {type: '${relType}', confidence: ${confidence}, reason: '${reason.replace(/'/g, "''")}', step: ${step}}]->(b)
132
+ `;
133
+ await conn.query(insertQuery);
134
+ insertedRels++;
135
+ }
136
+ catch {
137
+ skippedRels++;
138
+ }
139
+ }
140
+ // Cleanup CSVs
141
+ for (const { path: filePath } of nodeFiles) {
142
+ try {
143
+ await fs.unlink(filePath);
144
+ }
145
+ catch {
146
+ // ignore
147
+ }
148
+ }
149
+ // Remove empty csv directory
150
+ try {
151
+ await fs.rmdir(csvDir);
152
+ }
153
+ catch {
154
+ // ignore if not empty or other error
155
+ }
156
+ return { success: true, insertedRels, skippedRels };
157
+ };
158
+ // KuzuDB default ESCAPE is '\' (backslash), but our CSV uses RFC 4180 escaping ("" for literal quotes).
159
+ // Source code content is full of backslashes which confuse the auto-detection.
160
+ // We MUST explicitly set ESCAPE='"' to use RFC 4180 escaping, and disable auto_detect to prevent
161
+ // KuzuDB from overriding our settings based on sample rows.
162
+ const COPY_CSV_OPTS = `(HEADER=true, ESCAPE='"', DELIM=',', QUOTE='"', PARALLEL=false, auto_detect=false)`;
163
+ // Multi-language table names that were created with backticks in CODE_ELEMENT_BASE
164
+ // and must always be referenced with backticks in queries
165
+ const BACKTICK_TABLES = new Set([
166
+ 'Struct', 'Enum', 'Macro', 'Typedef', 'Union', 'Namespace', 'Trait', 'Impl',
167
+ 'TypeAlias', 'Const', 'Static', 'Property', 'Record', 'Delegate', 'Annotation',
168
+ 'Constructor', 'Template', 'Module',
169
+ ]);
170
+ const escapeTableName = (table) => {
171
+ return BACKTICK_TABLES.has(table) ? `\`${table}\`` : table;
172
+ };
173
+ const getCopyQuery = (table, filePath) => {
174
+ const t = escapeTableName(table);
175
+ if (table === 'File') {
176
+ return `COPY ${t}(id, name, filePath, content) FROM "${filePath}" ${COPY_CSV_OPTS}`;
177
+ }
178
+ if (table === 'Folder') {
179
+ return `COPY ${t}(id, name, filePath) FROM "${filePath}" ${COPY_CSV_OPTS}`;
180
+ }
181
+ if (table === 'Community') {
182
+ return `COPY ${t}(id, label, heuristicLabel, keywords, description, enrichedBy, cohesion, symbolCount) FROM "${filePath}" ${COPY_CSV_OPTS}`;
183
+ }
184
+ if (table === 'Process') {
185
+ return `COPY ${t}(id, label, heuristicLabel, processType, stepCount, communities, entryPointId, terminalId) FROM "${filePath}" ${COPY_CSV_OPTS}`;
186
+ }
187
+ // Code element tables (Function, Class, Interface, Method, CodeElement, and multi-language)
188
+ return `COPY ${t}(id, name, filePath, startLine, endLine, isExported, content) FROM "${filePath}" ${COPY_CSV_OPTS}`;
189
+ };
190
+ /**
191
+ * Insert a single node to KuzuDB
192
+ * @param label - Node type (File, Function, Class, etc.)
193
+ * @param properties - Node properties
194
+ * @param dbPath - Path to KuzuDB database (optional if already initialized)
195
+ */
196
+ export const insertNodeToKuzu = async (label, properties, dbPath) => {
197
+ // Use provided dbPath or fall back to module-level db
198
+ const targetDbPath = dbPath || (db ? undefined : null);
199
+ if (!targetDbPath && !db) {
200
+ throw new Error('KuzuDB not initialized. Provide dbPath or call initKuzu first.');
201
+ }
202
+ try {
203
+ const escapeValue = (v) => {
204
+ if (v === null || v === undefined)
205
+ return 'NULL';
206
+ if (typeof v === 'number')
207
+ return String(v);
208
+ // Escape backslashes first (for Windows paths), then single quotes
209
+ return `'${String(v).replace(/\\/g, '\\\\').replace(/'/g, "''")}'`;
210
+ };
211
+ // Build INSERT query based on node type
212
+ let query;
213
+ if (label === 'File') {
214
+ query = `CREATE (n:File {id: ${escapeValue(properties.id)}, name: ${escapeValue(properties.name)}, filePath: ${escapeValue(properties.filePath)}, content: ${escapeValue(properties.content || '')}})`;
215
+ }
216
+ else if (label === 'Folder') {
217
+ query = `CREATE (n:Folder {id: ${escapeValue(properties.id)}, name: ${escapeValue(properties.name)}, filePath: ${escapeValue(properties.filePath)}})`;
218
+ }
219
+ else {
220
+ // Function, Class, Method, Interface, etc. - standard code element schema
221
+ query = `CREATE (n:${label} {id: ${escapeValue(properties.id)}, name: ${escapeValue(properties.name)}, filePath: ${escapeValue(properties.filePath)}, startLine: ${properties.startLine || 0}, endLine: ${properties.endLine || 0}, content: ${escapeValue(properties.content || '')}})`;
222
+ }
223
+ // Use per-query connection if dbPath provided (avoids lock conflicts)
224
+ if (targetDbPath) {
225
+ const tempDb = new kuzu.Database(targetDbPath);
226
+ const tempConn = new kuzu.Connection(tempDb);
227
+ try {
228
+ await tempConn.query(query);
229
+ return true;
230
+ }
231
+ finally {
232
+ try {
233
+ await tempConn.close();
234
+ }
235
+ catch { }
236
+ try {
237
+ await tempDb.close();
238
+ }
239
+ catch { }
240
+ }
241
+ }
242
+ else if (conn) {
243
+ // Use existing persistent connection (when called from analyze)
244
+ await conn.query(query);
245
+ return true;
246
+ }
247
+ return false;
248
+ }
249
+ catch (e) {
250
+ // Node may already exist or other error
251
+ console.error(`Failed to insert ${label} node:`, e.message);
252
+ return false;
253
+ }
254
+ };
255
+ /**
256
+ * Batch insert multiple nodes to KuzuDB using a single connection
257
+ * @param nodes - Array of {label, properties} to insert
258
+ * @param dbPath - Path to KuzuDB database
259
+ * @returns Object with success count and error count
260
+ */
261
+ export const batchInsertNodesToKuzu = async (nodes, dbPath) => {
262
+ if (nodes.length === 0)
263
+ return { inserted: 0, failed: 0 };
264
+ const escapeValue = (v) => {
265
+ if (v === null || v === undefined)
266
+ return 'NULL';
267
+ if (typeof v === 'number')
268
+ return String(v);
269
+ // Escape backslashes first (for Windows paths), then single quotes
270
+ return `'${String(v).replace(/\\/g, '\\\\').replace(/'/g, "''")}'`;
271
+ };
272
+ // Open a single connection for all inserts
273
+ const tempDb = new kuzu.Database(dbPath);
274
+ const tempConn = new kuzu.Connection(tempDb);
275
+ let inserted = 0;
276
+ let failed = 0;
277
+ try {
278
+ for (const { label, properties } of nodes) {
279
+ try {
280
+ let query;
281
+ // Use MERGE instead of CREATE for upsert behavior (handles duplicates gracefully)
282
+ if (label === 'File') {
283
+ query = `MERGE (n:File {id: ${escapeValue(properties.id)}}) SET n.name = ${escapeValue(properties.name)}, n.filePath = ${escapeValue(properties.filePath)}, n.content = ${escapeValue(properties.content || '')}`;
284
+ }
285
+ else if (label === 'Folder') {
286
+ query = `MERGE (n:Folder {id: ${escapeValue(properties.id)}}) SET n.name = ${escapeValue(properties.name)}, n.filePath = ${escapeValue(properties.filePath)}`;
287
+ }
288
+ else {
289
+ query = `MERGE (n:${label} {id: ${escapeValue(properties.id)}}) SET n.name = ${escapeValue(properties.name)}, n.filePath = ${escapeValue(properties.filePath)}, n.startLine = ${properties.startLine || 0}, n.endLine = ${properties.endLine || 0}, n.content = ${escapeValue(properties.content || '')}`;
290
+ }
291
+ await tempConn.query(query);
292
+ inserted++;
293
+ }
294
+ catch (e) {
295
+ // Don't console.error here - it corrupts MCP JSON-RPC on stderr
296
+ failed++;
297
+ }
298
+ }
299
+ }
300
+ finally {
301
+ try {
302
+ await tempConn.close();
303
+ }
304
+ catch { }
305
+ try {
306
+ await tempDb.close();
307
+ }
308
+ catch { }
309
+ }
310
+ return { inserted, failed };
311
+ };
312
+ export const executeQuery = async (cypher) => {
313
+ if (!conn) {
314
+ throw new Error('KuzuDB not initialized. Call initKuzu first.');
315
+ }
316
+ const queryResult = await conn.query(cypher);
317
+ // kuzu v0.11 uses getAll() instead of hasNext()/getNext()
318
+ // Query returns QueryResult for single queries, QueryResult[] for multi-statement
319
+ const result = Array.isArray(queryResult) ? queryResult[0] : queryResult;
320
+ const rows = await result.getAll();
321
+ return rows;
322
+ };
323
+ export const executeWithReusedStatement = async (cypher, paramsList) => {
324
+ if (!conn) {
325
+ throw new Error('KuzuDB not initialized. Call initKuzu first.');
326
+ }
327
+ if (paramsList.length === 0)
328
+ return;
329
+ const SUB_BATCH_SIZE = 4;
330
+ for (let i = 0; i < paramsList.length; i += SUB_BATCH_SIZE) {
331
+ const subBatch = paramsList.slice(i, i + SUB_BATCH_SIZE);
332
+ const stmt = await conn.prepare(cypher);
333
+ if (!stmt.isSuccess()) {
334
+ const errMsg = await stmt.getErrorMessage();
335
+ throw new Error(`Prepare failed: ${errMsg}`);
336
+ }
337
+ try {
338
+ for (const params of subBatch) {
339
+ await conn.execute(stmt, params);
340
+ }
341
+ }
342
+ catch (e) {
343
+ // Log the error and continue with next batch
344
+ console.warn('Batch execution error:', e);
345
+ }
346
+ // Note: kuzu 0.8.2 PreparedStatement doesn't require explicit close()
347
+ }
348
+ };
349
+ export const getKuzuStats = async () => {
350
+ if (!conn)
351
+ return { nodes: 0, edges: 0 };
352
+ let totalNodes = 0;
353
+ for (const tableName of NODE_TABLES) {
354
+ try {
355
+ const queryResult = await conn.query(`MATCH (n:${tableName}) RETURN count(n) AS cnt`);
356
+ const nodeResult = Array.isArray(queryResult) ? queryResult[0] : queryResult;
357
+ const nodeRows = await nodeResult.getAll();
358
+ if (nodeRows.length > 0) {
359
+ totalNodes += Number(nodeRows[0]?.cnt ?? nodeRows[0]?.[0] ?? 0);
360
+ }
361
+ }
362
+ catch {
363
+ // ignore
364
+ }
365
+ }
366
+ let totalEdges = 0;
367
+ try {
368
+ const queryResult = await conn.query(`MATCH ()-[r:${REL_TABLE_NAME}]->() RETURN count(r) AS cnt`);
369
+ const edgeResult = Array.isArray(queryResult) ? queryResult[0] : queryResult;
370
+ const edgeRows = await edgeResult.getAll();
371
+ if (edgeRows.length > 0) {
372
+ totalEdges = Number(edgeRows[0]?.cnt ?? edgeRows[0]?.[0] ?? 0);
373
+ }
374
+ }
375
+ catch {
376
+ // ignore
377
+ }
378
+ return { nodes: totalNodes, edges: totalEdges };
379
+ };
380
+ export const closeKuzu = async () => {
381
+ if (conn) {
382
+ try {
383
+ await conn.close();
384
+ }
385
+ catch { }
386
+ conn = null;
387
+ }
388
+ if (db) {
389
+ try {
390
+ await db.close();
391
+ }
392
+ catch { }
393
+ db = null;
394
+ }
395
+ };
396
+ export const isKuzuReady = () => conn !== null && db !== null;
397
+ /**
398
+ * Delete all nodes (and their relationships) for a specific file from KuzuDB
399
+ * @param filePath - The file path to delete nodes for
400
+ * @param dbPath - Optional path to KuzuDB for per-query connection
401
+ * @returns Object with counts of deleted nodes
402
+ */
403
+ export const deleteNodesForFile = async (filePath, dbPath) => {
404
+ const usePerQuery = !!dbPath;
405
+ // Set up connection (either use existing or create per-query)
406
+ let tempDb = null;
407
+ let tempConn = null;
408
+ let targetConn = conn;
409
+ if (usePerQuery) {
410
+ tempDb = new kuzu.Database(dbPath);
411
+ tempConn = new kuzu.Connection(tempDb);
412
+ targetConn = tempConn;
413
+ }
414
+ else if (!conn) {
415
+ throw new Error('KuzuDB not initialized. Provide dbPath or call initKuzu first.');
416
+ }
417
+ try {
418
+ let deletedNodes = 0;
419
+ const escapedPath = filePath.replace(/'/g, "''");
420
+ // Delete nodes from each table that has filePath
421
+ // DETACH DELETE removes the node and all its relationships
422
+ for (const tableName of NODE_TABLES) {
423
+ // Skip tables that don't have filePath (Community, Process)
424
+ if (tableName === 'Community' || tableName === 'Process')
425
+ continue;
426
+ try {
427
+ // First count how many we'll delete
428
+ const countResult = await targetConn.query(`MATCH (n:${tableName}) WHERE n.filePath = '${escapedPath}' RETURN count(n) AS cnt`);
429
+ const result = Array.isArray(countResult) ? countResult[0] : countResult;
430
+ const rows = await result.getAll();
431
+ const count = Number(rows[0]?.cnt ?? rows[0]?.[0] ?? 0);
432
+ if (count > 0) {
433
+ // Delete nodes (and implicitly their relationships via DETACH)
434
+ await targetConn.query(`MATCH (n:${tableName}) WHERE n.filePath = '${escapedPath}' DETACH DELETE n`);
435
+ deletedNodes += count;
436
+ }
437
+ }
438
+ catch (e) {
439
+ // Some tables may not support this query, skip
440
+ }
441
+ }
442
+ // Also delete any embeddings for nodes in this file
443
+ try {
444
+ await targetConn.query(`MATCH (e:${EMBEDDING_TABLE_NAME}) WHERE e.nodeId STARTS WITH '${escapedPath}' DELETE e`);
445
+ }
446
+ catch {
447
+ // Embedding table may not exist or nodeId format may differ
448
+ }
449
+ return { deletedNodes };
450
+ }
451
+ finally {
452
+ // Close per-query connection if used
453
+ if (tempConn) {
454
+ try {
455
+ await tempConn.close();
456
+ }
457
+ catch { }
458
+ }
459
+ if (tempDb) {
460
+ try {
461
+ await tempDb.close();
462
+ }
463
+ catch { }
464
+ }
465
+ }
466
+ };
467
+ export const getEmbeddingTableName = () => EMBEDDING_TABLE_NAME;
468
+ // ============================================================================
469
+ // Full-Text Search (FTS) Functions
470
+ // ============================================================================
471
+ /**
472
+ * Load the FTS extension (required before using FTS functions)
473
+ */
474
+ export const loadFTSExtension = async () => {
475
+ if (!conn) {
476
+ throw new Error('KuzuDB not initialized. Call initKuzu first.');
477
+ }
478
+ try {
479
+ await conn.query('INSTALL fts');
480
+ await conn.query('LOAD EXTENSION fts');
481
+ }
482
+ catch {
483
+ // Extension may already be loaded
484
+ }
485
+ };
486
+ /**
487
+ * Create a full-text search index on a table
488
+ * @param tableName - The node table name (e.g., 'File', 'CodeSymbol')
489
+ * @param indexName - Name for the FTS index
490
+ * @param properties - List of properties to index (e.g., ['name', 'code'])
491
+ * @param stemmer - Stemming algorithm (default: 'porter')
492
+ */
493
+ export const createFTSIndex = async (tableName, indexName, properties, stemmer = 'porter') => {
494
+ if (!conn) {
495
+ throw new Error('KuzuDB not initialized. Call initKuzu first.');
496
+ }
497
+ await loadFTSExtension();
498
+ const propList = properties.map(p => `'${p}'`).join(', ');
499
+ const query = `CALL CREATE_FTS_INDEX('${tableName}', '${indexName}', [${propList}], stemmer := '${stemmer}')`;
500
+ try {
501
+ await conn.query(query);
502
+ }
503
+ catch (e) {
504
+ // Index may already exist
505
+ if (!e.message?.includes('already exists')) {
506
+ throw e;
507
+ }
508
+ }
509
+ };
510
+ /**
511
+ * Query a full-text search index
512
+ * @param tableName - The node table name
513
+ * @param indexName - FTS index name
514
+ * @param query - Search query string
515
+ * @param limit - Maximum results
516
+ * @param conjunctive - If true, all terms must match (AND); if false, any term matches (OR)
517
+ * @returns Array of { node properties, score }
518
+ */
519
+ export const queryFTS = async (tableName, indexName, query, limit = 20, conjunctive = false) => {
520
+ if (!conn) {
521
+ throw new Error('KuzuDB not initialized. Call initKuzu first.');
522
+ }
523
+ // Escape single quotes in query
524
+ const escapedQuery = query.replace(/'/g, "''");
525
+ const cypher = `
526
+ CALL QUERY_FTS_INDEX('${tableName}', '${indexName}', '${escapedQuery}', conjunctive := ${conjunctive})
527
+ RETURN node, score
528
+ ORDER BY score DESC
529
+ LIMIT ${limit}
530
+ `;
531
+ try {
532
+ const queryResult = await conn.query(cypher);
533
+ const result = Array.isArray(queryResult) ? queryResult[0] : queryResult;
534
+ const rows = await result.getAll();
535
+ return rows.map((row) => {
536
+ const node = row.node || row[0] || {};
537
+ const score = row.score ?? row[1] ?? 0;
538
+ return {
539
+ nodeId: node.nodeId || node.id || '',
540
+ name: node.name || '',
541
+ filePath: node.filePath || '',
542
+ score: typeof score === 'number' ? score : parseFloat(score) || 0,
543
+ ...node,
544
+ };
545
+ });
546
+ }
547
+ catch (e) {
548
+ // Return empty if index doesn't exist yet
549
+ if (e.message?.includes('does not exist')) {
550
+ return [];
551
+ }
552
+ throw e;
553
+ }
554
+ };
555
+ /**
556
+ * Drop an FTS index
557
+ */
558
+ export const dropFTSIndex = async (tableName, indexName) => {
559
+ if (!conn) {
560
+ throw new Error('KuzuDB not initialized. Call initKuzu first.');
561
+ }
562
+ try {
563
+ await conn.query(`CALL DROP_FTS_INDEX('${tableName}', '${indexName}')`);
564
+ }
565
+ catch {
566
+ // Index may not exist
567
+ }
568
+ };