kotadb 2.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 (52) hide show
  1. package/README.md +79 -0
  2. package/package.json +75 -0
  3. package/src/api/auto-reindex.ts +55 -0
  4. package/src/api/openapi/builder.ts +209 -0
  5. package/src/api/openapi/paths.ts +354 -0
  6. package/src/api/openapi/schemas.ts +608 -0
  7. package/src/api/queries.ts +1168 -0
  8. package/src/api/routes.ts +339 -0
  9. package/src/auth/middleware.ts +83 -0
  10. package/src/cli.ts +221 -0
  11. package/src/config/constants.ts +96 -0
  12. package/src/config/environment.ts +96 -0
  13. package/src/config/gitignore.ts +68 -0
  14. package/src/config/index.ts +20 -0
  15. package/src/config/project-root.ts +52 -0
  16. package/src/db/client.ts +72 -0
  17. package/src/db/sqlite/index.ts +35 -0
  18. package/src/db/sqlite/jsonl-exporter.ts +416 -0
  19. package/src/db/sqlite/jsonl-importer.ts +361 -0
  20. package/src/db/sqlite/sqlite-client.ts +536 -0
  21. package/src/index.ts +66 -0
  22. package/src/indexer/ast-parser.ts +146 -0
  23. package/src/indexer/ast-types.ts +54 -0
  24. package/src/indexer/circular-detector.ts +262 -0
  25. package/src/indexer/dependency-extractor.ts +352 -0
  26. package/src/indexer/extractors.ts +54 -0
  27. package/src/indexer/import-resolver.ts +167 -0
  28. package/src/indexer/parsers.ts +177 -0
  29. package/src/indexer/reference-extractor.ts +488 -0
  30. package/src/indexer/repos.ts +245 -0
  31. package/src/indexer/storage.ts +277 -0
  32. package/src/indexer/symbol-extractor.ts +660 -0
  33. package/src/instrument.ts +88 -0
  34. package/src/logging/context.ts +46 -0
  35. package/src/logging/logger.ts +193 -0
  36. package/src/logging/middleware.ts +107 -0
  37. package/src/mcp/github-integration.ts +293 -0
  38. package/src/mcp/headers.ts +101 -0
  39. package/src/mcp/impact-analysis.ts +495 -0
  40. package/src/mcp/jsonrpc.ts +141 -0
  41. package/src/mcp/lifecycle.ts +73 -0
  42. package/src/mcp/server.ts +202 -0
  43. package/src/mcp/session.ts +44 -0
  44. package/src/mcp/spec-validation.ts +491 -0
  45. package/src/mcp/tools.ts +889 -0
  46. package/src/sync/deletion-manifest.ts +210 -0
  47. package/src/sync/index.ts +16 -0
  48. package/src/sync/merge-driver.ts +172 -0
  49. package/src/sync/watcher.ts +221 -0
  50. package/src/types/rate-limit.ts +88 -0
  51. package/src/validation/common-schemas.ts +96 -0
  52. package/src/validation/schemas.ts +187 -0
@@ -0,0 +1,361 @@
1
+ /**
2
+ * JSONL import layer for SQLite database.
3
+ *
4
+ * Imports JSONL (JSON Lines) files into SQLite database for:
5
+ * - Database recovery from backups
6
+ * - Sync from git repository
7
+ * - Migration between machines
8
+ *
9
+ * Features:
10
+ * - Transactional imports (all-or-nothing per table)
11
+ * - Conflict handling (INSERT OR REPLACE)
12
+ * - Validation of required fields
13
+ *
14
+ * @module @db/sqlite/jsonl-importer
15
+ */
16
+
17
+ import { existsSync } from "node:fs";
18
+ import { join } from "node:path";
19
+ import { applyDeletionManifest } from "@sync/deletion-manifest.js";
20
+ import { createLogger } from "@logging/logger.js";
21
+ import type { KotaDatabase } from "./sqlite-client.js";
22
+
23
+ const logger = createLogger({ module: "jsonl-importer" });
24
+
25
+ /**
26
+ * Configuration for table imports
27
+ */
28
+ interface TableImportConfig {
29
+ /** Table name */
30
+ name: string;
31
+ /** Primary key column(s) */
32
+ primaryKey: string | string[];
33
+ /** Required fields for validation */
34
+ requiredFields?: string[];
35
+ /** Use INSERT OR IGNORE instead of INSERT OR REPLACE */
36
+ ignoreConflicts?: boolean;
37
+ }
38
+
39
+ /**
40
+ * Default table import configurations.
41
+ *
42
+ * Local-first essentials only - code intelligence tables for indexing and search.
43
+ * Cloud-centric tables (users, api_keys, rate_limit_*) are excluded from defaults
44
+ * per the local-first pivot (#534). Custom table configuration is still supported
45
+ * for future sync tier functionality.
46
+ */
47
+ const DEFAULT_IMPORT_CONFIGS: TableImportConfig[] = [
48
+ { name: "repositories", primaryKey: "id", requiredFields: ["id", "name"] },
49
+ { name: "indexed_files", primaryKey: "id", requiredFields: ["id", "repository_id", "path"] },
50
+ { name: "indexed_symbols", primaryKey: "id", requiredFields: ["id", "file_id", "name"] },
51
+ { name: "indexed_references", primaryKey: "id", requiredFields: ["id", "file_id", "symbol_name"] },
52
+ { name: "projects", primaryKey: "id", requiredFields: ["id", "name"] },
53
+ { name: "project_repositories", primaryKey: ["project_id", "repository_id"] },
54
+ ];
55
+
56
+ /**
57
+ * Result of a single table import
58
+ */
59
+ interface TableImportResult {
60
+ table: string;
61
+ status: "imported" | "skipped" | "error";
62
+ rowsImported: number;
63
+ rowsSkipped: number;
64
+ errors: string[];
65
+ }
66
+
67
+ /**
68
+ * Result of a full import operation
69
+ */
70
+ export interface ImportResult {
71
+ tablesImported: number;
72
+ tablesSkipped: number;
73
+ totalRowsImported: number;
74
+ totalRowsSkipped: number;
75
+ durationMs: number;
76
+ results: TableImportResult[];
77
+ errors: string[];
78
+ }
79
+
80
+ /**
81
+ * Import JSONL files from a directory into the database.
82
+ *
83
+ * @param db - Database to import into
84
+ * @param importDir - Directory containing JSONL files
85
+ * @param configs - Table configurations (optional, uses defaults)
86
+ * @returns Import result with statistics
87
+ */
88
+ export async function importFromJSONL(
89
+ db: KotaDatabase,
90
+ importDir: string,
91
+ configs: TableImportConfig[] = DEFAULT_IMPORT_CONFIGS,
92
+ ): Promise<ImportResult> {
93
+ const startTime = Date.now();
94
+ const results: TableImportResult[] = [];
95
+ const globalErrors: string[] = [];
96
+ let totalRowsImported = 0;
97
+ let totalRowsSkipped = 0;
98
+ let tablesImported = 0;
99
+ let tablesSkipped = 0;
100
+
101
+ logger.info("Starting JSONL import", {
102
+ import_dir: importDir,
103
+ tables: configs.map((c) => c.name),
104
+ });
105
+
106
+ // Apply deletion manifest first
107
+ const deletionManifestPath = join(importDir, ".deletions.jsonl");
108
+ if (existsSync(deletionManifestPath)) {
109
+ const deletionResult = await applyDeletionManifest(db, deletionManifestPath);
110
+ logger.info("Deletion manifest applied", {
111
+ deleted_count: deletionResult.deletedCount,
112
+ errors: deletionResult.errors
113
+ });
114
+ }
115
+
116
+
117
+ // Validate import directory exists
118
+ if (!existsSync(importDir)) {
119
+ const error = "Import directory not found: " + importDir;
120
+ logger.error(error);
121
+ return {
122
+ tablesImported: 0,
123
+ tablesSkipped: configs.length,
124
+ totalRowsImported: 0,
125
+ totalRowsSkipped: 0,
126
+ durationMs: Date.now() - startTime,
127
+ results: [],
128
+ errors: [error],
129
+ };
130
+ }
131
+
132
+ // Import tables in order (respecting foreign key constraints)
133
+ for (const config of configs) {
134
+ try {
135
+ const result = await importTable(db, importDir, config);
136
+ results.push(result);
137
+
138
+ if (result.status === "imported") {
139
+ tablesImported++;
140
+ totalRowsImported += result.rowsImported;
141
+ totalRowsSkipped += result.rowsSkipped;
142
+ } else {
143
+ tablesSkipped++;
144
+ }
145
+
146
+ if (result.errors.length > 0) {
147
+ globalErrors.push(...result.errors.map((e) => config.name + ": " + e));
148
+ }
149
+ } catch (error) {
150
+ const errorMessage = error instanceof Error ? error.message : String(error);
151
+ logger.error("Failed to import table", new Error(errorMessage), {
152
+ table: config.name,
153
+ });
154
+ results.push({
155
+ table: config.name,
156
+ status: "error",
157
+ rowsImported: 0,
158
+ rowsSkipped: 0,
159
+ errors: [errorMessage],
160
+ });
161
+ globalErrors.push(config.name + ": " + errorMessage);
162
+ tablesSkipped++;
163
+ }
164
+ }
165
+
166
+ const duration = Date.now() - startTime;
167
+
168
+ logger.info("JSONL import completed", {
169
+ tables_imported: tablesImported,
170
+ tables_skipped: tablesSkipped,
171
+ total_rows_imported: totalRowsImported,
172
+ total_rows_skipped: totalRowsSkipped,
173
+ duration_ms: duration,
174
+ error_count: globalErrors.length,
175
+ });
176
+
177
+ return {
178
+ tablesImported,
179
+ tablesSkipped,
180
+ totalRowsImported,
181
+ totalRowsSkipped,
182
+ durationMs: duration,
183
+ results,
184
+ errors: globalErrors,
185
+ };
186
+ }
187
+
188
+ /**
189
+ * Import a single JSONL file into a table
190
+ */
191
+ async function importTable(
192
+ db: KotaDatabase,
193
+ importDir: string,
194
+ config: TableImportConfig,
195
+ ): Promise<TableImportResult> {
196
+ const { name, requiredFields = [], ignoreConflicts = false } = config;
197
+ const filepath = join(importDir, name + ".jsonl");
198
+
199
+ // Check if file exists
200
+ if (!existsSync(filepath)) {
201
+ logger.debug("JSONL file not found, skipping", { table: name, file: filepath });
202
+ return {
203
+ table: name,
204
+ status: "skipped",
205
+ rowsImported: 0,
206
+ rowsSkipped: 0,
207
+ errors: [],
208
+ };
209
+ }
210
+
211
+ // Read and parse file
212
+ const content = await Bun.file(filepath).text();
213
+ const lines = content.trim().split("\n").filter(Boolean);
214
+
215
+ if (lines.length === 0) {
216
+ logger.debug("Empty JSONL file, skipping", { table: name });
217
+ return {
218
+ table: name,
219
+ status: "skipped",
220
+ rowsImported: 0,
221
+ rowsSkipped: 0,
222
+ errors: [],
223
+ };
224
+ }
225
+
226
+ const errors: string[] = [];
227
+ let rowsImported = 0;
228
+ let rowsSkipped = 0;
229
+
230
+ // Import within a transaction
231
+ try {
232
+ db.immediateTransaction(() => {
233
+ for (let idx = 0; idx < lines.length; idx++) {
234
+ const line = lines[idx];
235
+ if (!line) continue;
236
+
237
+ try {
238
+ const row = JSON.parse(line) as Record<string, unknown>;
239
+
240
+ // Validate required fields
241
+ const missingFields = requiredFields.filter((field) => !(field in row));
242
+ if (missingFields.length > 0) {
243
+ errors.push("Row " + (idx + 1) + ": Missing required fields: " + missingFields.join(", "));
244
+ rowsSkipped++;
245
+ continue;
246
+ }
247
+
248
+ // Build INSERT statement
249
+ const columns = Object.keys(row);
250
+ const placeholders = columns.map(() => "?").join(", ");
251
+ const values = columns.map((col) => {
252
+ const value = row[col];
253
+ // Convert objects/arrays to JSON strings
254
+ if (typeof value === "object" && value !== null) {
255
+ return JSON.stringify(value);
256
+ }
257
+ return value;
258
+ });
259
+
260
+ const conflictClause = ignoreConflicts ? "OR IGNORE" : "OR REPLACE";
261
+ const sql = "INSERT " + conflictClause + " INTO " + name + " (" + columns.join(", ") + ") VALUES (" + placeholders + ")";
262
+
263
+ db.run(sql, values as (string | number | bigint | boolean | null | Uint8Array)[]);
264
+ rowsImported++;
265
+ } catch (parseError) {
266
+ const errorMessage = parseError instanceof Error ? parseError.message : String(parseError);
267
+ errors.push("Row " + (idx + 1) + ": " + errorMessage);
268
+ rowsSkipped++;
269
+ }
270
+ }
271
+ });
272
+ } catch (txError) {
273
+ const errorMessage = txError instanceof Error ? txError.message : String(txError);
274
+ logger.error("Transaction failed for table import", new Error(errorMessage), {
275
+ table: name,
276
+ });
277
+ return {
278
+ table: name,
279
+ status: "error",
280
+ rowsImported: 0,
281
+ rowsSkipped: lines.length,
282
+ errors: [errorMessage],
283
+ };
284
+ }
285
+
286
+ logger.info("Imported table", {
287
+ table: name,
288
+ rows_imported: rowsImported,
289
+ rows_skipped: rowsSkipped,
290
+ error_count: errors.length,
291
+ });
292
+
293
+ return {
294
+ table: name,
295
+ status: "imported",
296
+ rowsImported,
297
+ rowsSkipped,
298
+ errors,
299
+ };
300
+ }
301
+
302
+ /**
303
+ * Import a specific table from JSONL
304
+ */
305
+ export async function importTableFromJSONL(
306
+ db: KotaDatabase,
307
+ filepath: string,
308
+ tableName: string,
309
+ options: Partial<TableImportConfig> = {},
310
+ ): Promise<TableImportResult> {
311
+ const config: TableImportConfig = {
312
+ name: tableName,
313
+ primaryKey: options.primaryKey || "id",
314
+ requiredFields: options.requiredFields,
315
+ ignoreConflicts: options.ignoreConflicts,
316
+ };
317
+
318
+ // Extract directory from filepath
319
+ const importDir = filepath.replace("/" + tableName + ".jsonl", "").replace("\\" + tableName + ".jsonl", "");
320
+
321
+ return importTable(db, importDir, config);
322
+ }
323
+
324
+ /**
325
+ * Validate a JSONL file without importing
326
+ */
327
+ export async function validateJSONL(filepath: string): Promise<{
328
+ valid: boolean;
329
+ lineCount: number;
330
+ errors: string[];
331
+ }> {
332
+ if (!existsSync(filepath)) {
333
+ return {
334
+ valid: false,
335
+ lineCount: 0,
336
+ errors: ["File not found"],
337
+ };
338
+ }
339
+
340
+ const content = await Bun.file(filepath).text();
341
+ const lines = content.trim().split("\n").filter(Boolean);
342
+ const errors: string[] = [];
343
+
344
+ for (let idx = 0; idx < lines.length; idx++) {
345
+ const line = lines[idx];
346
+ if (!line) continue;
347
+
348
+ try {
349
+ JSON.parse(line);
350
+ } catch (error) {
351
+ const message = error instanceof Error ? error.message : String(error);
352
+ errors.push("Line " + (idx + 1) + ": Invalid JSON - " + message);
353
+ }
354
+ }
355
+
356
+ return {
357
+ valid: errors.length === 0,
358
+ lineCount: lines.length,
359
+ errors,
360
+ };
361
+ }