composto-ai 0.3.0 → 0.4.1

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.
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/memory/pool.ts
4
+ import { Worker } from "worker_threads";
5
+ import { fileURLToPath } from "url";
6
+ import { dirname, join } from "path";
7
+ function resolveWorkerPath() {
8
+ const here = dirname(fileURLToPath(import.meta.url));
9
+ if (here.endsWith("/memory") || here.endsWith("\\memory")) {
10
+ return join(here, "worker.js");
11
+ }
12
+ return join(here, "memory", "worker.js");
13
+ }
14
+ var WorkerPool = class {
15
+ workers = [];
16
+ nextJobId = 1;
17
+ pending = /* @__PURE__ */ new Map();
18
+ constructor(opts = {}) {
19
+ const size = Math.max(1, opts.size ?? 1);
20
+ for (let i = 0; i < size; i++) this.spawn();
21
+ }
22
+ spawn() {
23
+ const worker = new Worker(resolveWorkerPath());
24
+ worker.on("message", (msg) => {
25
+ const job = this.pending.get(msg.jobId);
26
+ if (!job) return;
27
+ this.pending.delete(msg.jobId);
28
+ if (msg.type === "ingest_done") {
29
+ job.resolve({ status: "done", commits: msg.commits });
30
+ } else if (msg.type === "ingest_error") {
31
+ job.reject(new Error(msg.message));
32
+ }
33
+ });
34
+ worker.on("error", (err) => {
35
+ const error = err instanceof Error ? err : new Error(String(err));
36
+ for (const job of this.pending.values()) job.reject(error);
37
+ this.pending.clear();
38
+ });
39
+ this.workers.push(worker);
40
+ }
41
+ runIngest(args) {
42
+ const jobId = this.nextJobId++;
43
+ const worker = this.workers[jobId % this.workers.length];
44
+ return new Promise((resolve, reject) => {
45
+ this.pending.set(jobId, { resolve, reject });
46
+ worker.postMessage({ type: "ingest", jobId, ...args });
47
+ });
48
+ }
49
+ async close() {
50
+ await Promise.all(this.workers.map((w) => w.terminate()));
51
+ this.workers = [];
52
+ this.pending.clear();
53
+ }
54
+ };
55
+ export {
56
+ WorkerPool
57
+ };
@@ -0,0 +1,448 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/memory/worker.ts
4
+ import { parentPort } from "worker_threads";
5
+
6
+ // src/memory/db.ts
7
+ import Database from "better-sqlite3";
8
+ import { mkdirSync } from "fs";
9
+ import { dirname } from "path";
10
+ function openDatabase(path) {
11
+ mkdirSync(dirname(path), { recursive: true });
12
+ const db = new Database(path);
13
+ db.pragma("journal_mode = WAL");
14
+ db.pragma("synchronous = NORMAL");
15
+ db.pragma("foreign_keys = ON");
16
+ return db;
17
+ }
18
+
19
+ // src/memory/schema.ts
20
+ var CURRENT_VERSION = 1;
21
+ var V1_SQL = `
22
+ CREATE TABLE IF NOT EXISTS index_state (
23
+ key TEXT PRIMARY KEY,
24
+ value TEXT NOT NULL
25
+ );
26
+
27
+ CREATE TABLE IF NOT EXISTS commits (
28
+ sha TEXT PRIMARY KEY,
29
+ parent_sha TEXT,
30
+ author TEXT NOT NULL,
31
+ timestamp INTEGER NOT NULL,
32
+ subject TEXT NOT NULL,
33
+ is_fix INTEGER NOT NULL,
34
+ is_revert INTEGER NOT NULL,
35
+ reverts_sha TEXT,
36
+ FOREIGN KEY (reverts_sha) REFERENCES commits(sha)
37
+ );
38
+ CREATE INDEX IF NOT EXISTS idx_commits_timestamp ON commits(timestamp);
39
+ CREATE INDEX IF NOT EXISTS idx_commits_is_fix ON commits(is_fix) WHERE is_fix = 1;
40
+
41
+ CREATE TABLE IF NOT EXISTS file_touches (
42
+ commit_sha TEXT NOT NULL,
43
+ file_path TEXT NOT NULL,
44
+ adds INTEGER NOT NULL,
45
+ dels INTEGER NOT NULL,
46
+ change_type TEXT NOT NULL,
47
+ renamed_from TEXT,
48
+ PRIMARY KEY (commit_sha, file_path),
49
+ FOREIGN KEY (commit_sha) REFERENCES commits(sha)
50
+ );
51
+ CREATE INDEX IF NOT EXISTS idx_ft_file ON file_touches(file_path);
52
+
53
+ CREATE TABLE IF NOT EXISTS symbols (
54
+ id INTEGER PRIMARY KEY,
55
+ file_path TEXT NOT NULL,
56
+ kind TEXT NOT NULL,
57
+ qualified_name TEXT NOT NULL,
58
+ first_seen_sha TEXT NOT NULL,
59
+ last_seen_sha TEXT,
60
+ UNIQUE (file_path, kind, qualified_name)
61
+ );
62
+ CREATE INDEX IF NOT EXISTS idx_symbols_file ON symbols(file_path);
63
+
64
+ CREATE TABLE IF NOT EXISTS symbol_touches (
65
+ commit_sha TEXT NOT NULL,
66
+ symbol_id INTEGER NOT NULL,
67
+ change_type TEXT NOT NULL,
68
+ PRIMARY KEY (commit_sha, symbol_id),
69
+ FOREIGN KEY (commit_sha) REFERENCES commits(sha),
70
+ FOREIGN KEY (symbol_id) REFERENCES symbols(id)
71
+ );
72
+ CREATE INDEX IF NOT EXISTS idx_st_symbol ON symbol_touches(symbol_id);
73
+
74
+ CREATE TABLE IF NOT EXISTS fix_links (
75
+ fix_commit_sha TEXT NOT NULL,
76
+ suspected_break_sha TEXT NOT NULL,
77
+ evidence_type TEXT NOT NULL,
78
+ confidence REAL NOT NULL,
79
+ window_hours INTEGER,
80
+ PRIMARY KEY (fix_commit_sha, suspected_break_sha, evidence_type),
81
+ FOREIGN KEY (fix_commit_sha) REFERENCES commits(sha),
82
+ FOREIGN KEY (suspected_break_sha) REFERENCES commits(sha)
83
+ );
84
+ CREATE INDEX IF NOT EXISTS idx_fl_break ON fix_links(suspected_break_sha);
85
+
86
+ CREATE TABLE IF NOT EXISTS signal_calibration (
87
+ signal_type TEXT PRIMARY KEY,
88
+ precision REAL NOT NULL,
89
+ sample_size INTEGER NOT NULL,
90
+ last_computed_sha TEXT NOT NULL,
91
+ computed_at INTEGER NOT NULL
92
+ );
93
+
94
+ CREATE TABLE IF NOT EXISTS file_index_state (
95
+ file_path TEXT PRIMARY KEY,
96
+ last_commit_indexed TEXT NOT NULL,
97
+ last_blob_indexed TEXT,
98
+ indexed_at INTEGER NOT NULL,
99
+ parse_failed INTEGER NOT NULL DEFAULT 0,
100
+ FOREIGN KEY (last_commit_indexed) REFERENCES commits(sha)
101
+ );
102
+ `;
103
+ function runMigrations(db) {
104
+ const current = db.pragma("user_version", { simple: true });
105
+ if (current >= CURRENT_VERSION) return;
106
+ db.exec("BEGIN");
107
+ try {
108
+ db.exec(V1_SQL);
109
+ db.pragma(`user_version = ${CURRENT_VERSION}`);
110
+ db.exec("COMMIT");
111
+ } catch (err) {
112
+ db.exec("ROLLBACK");
113
+ throw err;
114
+ }
115
+ }
116
+
117
+ // src/memory/git.ts
118
+ import { execSync } from "child_process";
119
+ function logRange(cwd, from, to, timeoutMs = 6e4) {
120
+ const range = from ? `${from}..${to}` : to;
121
+ const fmt = "--format=%x1e%H%x00%P%x00%an%x00%at%x00%s%x00%b%x1f";
122
+ const cmd = `git log ${fmt} --numstat --no-renames ${range}`;
123
+ return execSync(cmd, { cwd, encoding: "utf-8", timeout: timeoutMs, maxBuffer: 256 * 1024 * 1024 });
124
+ }
125
+
126
+ // src/memory/commit-parser.ts
127
+ var FIX_PATTERNS = [
128
+ /\bfix(es|ed|ing)?\b/i,
129
+ /\bbugfix\b/i,
130
+ /\bhotfix\b/i,
131
+ /\bpatch\b/i,
132
+ /\bbug\b/i,
133
+ /closes?\s+#\d+/i,
134
+ /resolves?\s+#\d+/i
135
+ ];
136
+ var REVERT_SUBJECT = /^\s*revert\b/i;
137
+ var REVERT_BODY_SHA = /This reverts commit ([0-9a-f]{7,40})/i;
138
+ function parseCommit(subject, body) {
139
+ const is_revert = REVERT_SUBJECT.test(subject);
140
+ const match = is_revert ? body.match(REVERT_BODY_SHA) : null;
141
+ const reverts_sha = match ? match[1] : null;
142
+ const is_fix = !is_revert && FIX_PATTERNS.some((re) => re.test(subject));
143
+ return { is_fix, is_revert, reverts_sha };
144
+ }
145
+
146
+ // src/memory/ingest/fix-links.ts
147
+ var FOLLOWUP_WINDOW_HOURS = 72;
148
+ var CHAIN_WINDOW_DAYS = 14;
149
+ var CHAIN_MIN = 3;
150
+ function deriveFixLinks(db) {
151
+ const insert = db.prepare(`
152
+ INSERT OR IGNORE INTO fix_links
153
+ (fix_commit_sha, suspected_break_sha, evidence_type, confidence, window_hours)
154
+ VALUES (?, ?, ?, ?, ?)
155
+ `);
156
+ const reverts = db.prepare(`
157
+ SELECT sha, reverts_sha FROM commits
158
+ WHERE is_revert = 1 AND reverts_sha IS NOT NULL
159
+ `).all();
160
+ for (const r of reverts) {
161
+ const exists = db.prepare("SELECT 1 FROM commits WHERE sha = ?").get(r.reverts_sha);
162
+ if (exists) insert.run(r.sha, r.reverts_sha, "revert_marker", 1, null);
163
+ }
164
+ const fixes = db.prepare(`
165
+ SELECT sha, timestamp FROM commits WHERE is_fix = 1
166
+ `).all();
167
+ const priorByFile = db.prepare(`
168
+ SELECT c.sha AS prior_sha
169
+ FROM file_touches ft_fix
170
+ JOIN file_touches ft_prior ON ft_prior.file_path = ft_fix.file_path
171
+ JOIN commits c ON c.sha = ft_prior.commit_sha
172
+ WHERE ft_fix.commit_sha = ?
173
+ AND c.timestamp < ?
174
+ AND c.timestamp >= ?
175
+ AND c.sha != ?
176
+ AND c.is_fix = 0
177
+ AND c.is_revert = 0
178
+ `);
179
+ for (const f of fixes) {
180
+ const lowerBound = f.timestamp - FOLLOWUP_WINDOW_HOURS * 3600;
181
+ const priors = priorByFile.all(f.sha, f.timestamp, lowerBound, f.sha);
182
+ const unique = new Set(priors.map((p) => p.prior_sha));
183
+ for (const prior of unique) {
184
+ insert.run(f.sha, prior, "short_followup_fix", 0.7, FOLLOWUP_WINDOW_HOURS);
185
+ }
186
+ }
187
+ const filesWithFixes = db.prepare(`
188
+ SELECT DISTINCT ft.file_path
189
+ FROM file_touches ft
190
+ JOIN commits c ON c.sha = ft.commit_sha
191
+ WHERE c.is_fix = 1
192
+ `).all();
193
+ const fixesOnFile = db.prepare(`
194
+ SELECT c.sha, c.timestamp
195
+ FROM commits c
196
+ JOIN file_touches ft ON ft.commit_sha = c.sha
197
+ WHERE ft.file_path = ? AND c.is_fix = 1
198
+ ORDER BY c.timestamp ASC
199
+ `);
200
+ const priorNonFixOnFile = db.prepare(`
201
+ SELECT c.sha
202
+ FROM commits c
203
+ JOIN file_touches ft ON ft.commit_sha = c.sha
204
+ WHERE ft.file_path = ?
205
+ AND c.timestamp < ?
206
+ AND c.is_fix = 0 AND c.is_revert = 0
207
+ ORDER BY c.timestamp DESC
208
+ LIMIT 1
209
+ `);
210
+ for (const { file_path } of filesWithFixes) {
211
+ const rows = fixesOnFile.all(file_path);
212
+ const windowSec = CHAIN_WINDOW_DAYS * 86400;
213
+ for (let i = 0; i + CHAIN_MIN - 1 < rows.length; i++) {
214
+ const windowEnd = rows[i + CHAIN_MIN - 1];
215
+ if (windowEnd.timestamp - rows[i].timestamp > windowSec) continue;
216
+ const prior = priorNonFixOnFile.get(file_path, rows[i].timestamp);
217
+ if (!prior) continue;
218
+ for (let j = i; j < rows.length && rows[j].timestamp - rows[i].timestamp <= windowSec; j++) {
219
+ insert.run(rows[j].sha, prior.sha, "same_region_fix_chain", 0.4, CHAIN_WINDOW_DAYS * 24);
220
+ }
221
+ }
222
+ }
223
+ }
224
+
225
+ // src/memory/calibration.ts
226
+ var LOOKAHEAD_SECONDS = 14 * 86400;
227
+ var REFRESH_AFTER_SECONDS = 7 * 86400;
228
+ function validateRevertMatch(db) {
229
+ const total = db.prepare(`SELECT COUNT(*) AS n FROM fix_links`).get().n;
230
+ const hits = db.prepare(`
231
+ SELECT COUNT(*) AS n
232
+ FROM fix_links fl
233
+ JOIN commits fix_c ON fix_c.sha = fl.fix_commit_sha
234
+ JOIN commits break_c ON break_c.sha = fl.suspected_break_sha
235
+ WHERE fix_c.timestamp - break_c.timestamp <= ?
236
+ `).get(LOOKAHEAD_SECONDS).n;
237
+ return { total, hits };
238
+ }
239
+ function validateHotspot(db) {
240
+ const total = db.prepare(`
241
+ SELECT COUNT(DISTINCT file_path) AS n FROM file_touches
242
+ `).get().n;
243
+ const hits = db.prepare(`
244
+ SELECT COUNT(DISTINCT ft.file_path) AS n
245
+ FROM file_touches ft
246
+ JOIN fix_links fl ON fl.suspected_break_sha = ft.commit_sha
247
+ `).get().n;
248
+ return { total, hits };
249
+ }
250
+ function validateFixRatio(db) {
251
+ const total = db.prepare(`
252
+ SELECT COUNT(DISTINCT file_path) AS n FROM file_touches
253
+ `).get().n;
254
+ const hits = db.prepare(`
255
+ SELECT COUNT(DISTINCT ft.file_path) AS n
256
+ FROM file_touches ft
257
+ JOIN fix_links fl ON fl.suspected_break_sha = ft.commit_sha
258
+ `).get().n;
259
+ return { total, hits };
260
+ }
261
+ function validateCoverageDecline(_db) {
262
+ return { total: 0, hits: 0 };
263
+ }
264
+ function validateAuthorChurn(db) {
265
+ const total = db.prepare(`SELECT COUNT(*) AS n FROM file_touches`).get().n;
266
+ const hits = db.prepare(`SELECT COUNT(*) AS n FROM fix_links`).get().n;
267
+ return { total, hits };
268
+ }
269
+ var VALIDATORS = {
270
+ revert_match: validateRevertMatch,
271
+ hotspot: validateHotspot,
272
+ fix_ratio: validateFixRatio,
273
+ coverage_decline: validateCoverageDecline,
274
+ author_churn: validateAuthorChurn
275
+ };
276
+ function refreshCalibration(db, headSha) {
277
+ const now = Math.floor(Date.now() / 1e3);
278
+ const upsert = db.prepare(`
279
+ INSERT INTO signal_calibration (signal_type, precision, sample_size, last_computed_sha, computed_at)
280
+ VALUES (?, ?, ?, ?, ?)
281
+ ON CONFLICT(signal_type) DO UPDATE SET
282
+ precision = excluded.precision,
283
+ sample_size = excluded.sample_size,
284
+ last_computed_sha = excluded.last_computed_sha,
285
+ computed_at = excluded.computed_at
286
+ `);
287
+ for (const [type, validator] of Object.entries(VALIDATORS)) {
288
+ const v = validator(db);
289
+ const precision = v.total === 0 ? 0 : v.hits / v.total;
290
+ upsert.run(type, precision, v.total, headSha, now);
291
+ }
292
+ db.prepare(`
293
+ INSERT INTO index_state (key, value) VALUES ('calibration_last_refreshed_at', ?)
294
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value
295
+ `).run(String(now));
296
+ }
297
+ function shouldRefresh(db, currentHeadSha) {
298
+ const lastTimeRow = db.prepare(`SELECT value FROM index_state WHERE key = 'calibration_last_refreshed_at'`).get();
299
+ if (!lastTimeRow) return true;
300
+ const now = Math.floor(Date.now() / 1e3);
301
+ const lastTime = parseInt(lastTimeRow.value, 10);
302
+ if (now - lastTime >= REFRESH_AFTER_SECONDS) return true;
303
+ const anyCal = db.prepare(`SELECT last_computed_sha FROM signal_calibration LIMIT 1`).get();
304
+ if (!anyCal) return true;
305
+ return anyCal.last_computed_sha !== currentHeadSha;
306
+ }
307
+
308
+ // src/memory/ingest/tier1.ts
309
+ var RECORD_SEP = "";
310
+ var FIELD_SEP = "\0";
311
+ var RECORD_END = "";
312
+ function parseNumstatLine(line) {
313
+ const parts = line.split(" ");
314
+ if (parts.length < 3) return null;
315
+ const adds = parts[0] === "-" ? 0 : parseInt(parts[0], 10);
316
+ const dels = parts[1] === "-" ? 0 : parseInt(parts[1], 10);
317
+ if (Number.isNaN(adds) || Number.isNaN(dels)) return null;
318
+ return { adds, dels, path: parts.slice(2).join(" ").trim() };
319
+ }
320
+ function parseLogOutput(output) {
321
+ const commits = [];
322
+ const records = output.split(RECORD_SEP).slice(1);
323
+ for (const rec of records) {
324
+ const endIdx = rec.indexOf(RECORD_END);
325
+ if (endIdx === -1) continue;
326
+ const content = rec.slice(0, endIdx);
327
+ const tail = rec.slice(endIdx + 1);
328
+ const fields = content.split(FIELD_SEP);
329
+ if (fields.length < 6) continue;
330
+ const [sha, parent, author, tsStr, subject, ...rest] = fields;
331
+ const body = rest.join(FIELD_SEP);
332
+ const touches = [];
333
+ for (const line of tail.split("\n")) {
334
+ const trimmed = line.trim();
335
+ if (!trimmed) continue;
336
+ const parsed = parseNumstatLine(trimmed);
337
+ if (!parsed) continue;
338
+ touches.push({
339
+ file_path: parsed.path,
340
+ adds: parsed.adds,
341
+ dels: parsed.dels,
342
+ change_type: parsed.adds > 0 && parsed.dels === 0 ? "A" : parsed.dels > 0 && parsed.adds === 0 ? "D" : "M"
343
+ });
344
+ }
345
+ commits.push({
346
+ sha,
347
+ parent_sha: parent ? parent.split(" ")[0] : null,
348
+ author,
349
+ timestamp: parseInt(tsStr, 10),
350
+ subject,
351
+ body,
352
+ touches
353
+ });
354
+ }
355
+ return commits;
356
+ }
357
+ function resolveRevertsSha(raw, knownShas) {
358
+ if (!raw) return null;
359
+ if (knownShas.has(raw)) return raw;
360
+ if (raw.length < 40) {
361
+ for (const sha of knownShas) {
362
+ if (sha.startsWith(raw)) return sha;
363
+ }
364
+ }
365
+ return null;
366
+ }
367
+ function ingestRange(db, repoPath, range) {
368
+ const raw = logRange(repoPath, range.from, range.to);
369
+ const commits = parseLogOutput(raw);
370
+ commits.sort((a, b) => a.timestamp - b.timestamp);
371
+ const knownShas = new Set(commits.map((c) => c.sha));
372
+ for (const existing of db.prepare(`SELECT sha FROM commits`).all()) {
373
+ knownShas.add(existing.sha);
374
+ }
375
+ const insertCommit = db.prepare(`
376
+ INSERT OR IGNORE INTO commits
377
+ (sha, parent_sha, author, timestamp, subject, is_fix, is_revert, reverts_sha)
378
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
379
+ `);
380
+ const insertTouch = db.prepare(`
381
+ INSERT OR IGNORE INTO file_touches
382
+ (commit_sha, file_path, adds, dels, change_type, renamed_from)
383
+ VALUES (?, ?, ?, ?, ?, NULL)
384
+ `);
385
+ const upsertState = db.prepare(`
386
+ INSERT INTO index_state (key, value) VALUES (?, ?)
387
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value
388
+ `);
389
+ const tx = db.transaction((batch) => {
390
+ for (const c of batch) {
391
+ const parsed = parseCommit(c.subject, c.body);
392
+ insertCommit.run(
393
+ c.sha,
394
+ c.parent_sha,
395
+ c.author,
396
+ c.timestamp,
397
+ c.subject,
398
+ parsed.is_fix ? 1 : 0,
399
+ parsed.is_revert ? 1 : 0,
400
+ resolveRevertsSha(parsed.reverts_sha, knownShas)
401
+ );
402
+ for (const t of c.touches) {
403
+ insertTouch.run(c.sha, t.file_path, t.adds, t.dels, t.change_type);
404
+ }
405
+ }
406
+ });
407
+ db.pragma("foreign_keys = OFF");
408
+ try {
409
+ const BATCH = 1e3;
410
+ for (let i = 0; i < commits.length; i += BATCH) {
411
+ tx(commits.slice(i, i + BATCH));
412
+ }
413
+ } finally {
414
+ db.pragma("foreign_keys = ON");
415
+ }
416
+ deriveFixLinks(db);
417
+ if (shouldRefresh(db, range.to)) {
418
+ refreshCalibration(db, range.to);
419
+ }
420
+ upsertState.run("last_indexed_sha", range.to);
421
+ const totalRow = db.prepare("SELECT COUNT(*) AS n FROM commits").get();
422
+ upsertState.run("indexed_commits_total", String(totalRow.n));
423
+ return commits.length;
424
+ }
425
+
426
+ // src/memory/worker.ts
427
+ if (!parentPort) {
428
+ throw new Error("memory/worker.ts must run inside a Worker");
429
+ }
430
+ parentPort.on("message", (msg) => {
431
+ if (msg.type === "ingest") {
432
+ try {
433
+ const db = openDatabase(msg.dbPath);
434
+ runMigrations(db);
435
+ const n = ingestRange(db, msg.repoPath, msg.range);
436
+ db.close();
437
+ const out = { type: "ingest_done", jobId: msg.jobId, commits: n };
438
+ parentPort.postMessage(out);
439
+ } catch (err) {
440
+ const out = {
441
+ type: "ingest_error",
442
+ jobId: msg.jobId,
443
+ message: err instanceof Error ? err.message : String(err)
444
+ };
445
+ parentPort.postMessage(out);
446
+ }
447
+ }
448
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "composto-ai",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "description": "Proactive AI team companion — less tokens, more insight",
5
5
  "type": "module",
6
6
  "bin": {
@@ -32,6 +32,7 @@
32
32
  "license": "MIT",
33
33
  "packageManager": "pnpm@10.30.1",
34
34
  "devDependencies": {
35
+ "@types/better-sqlite3": "^7.6.13",
35
36
  "@types/node": "^25.5.2",
36
37
  "@types/picomatch": "^4.0.3",
37
38
  "tsup": "^8.5.1",
@@ -42,6 +43,8 @@
42
43
  "dependencies": {
43
44
  "@anthropic-ai/sdk": "^0.87.0",
44
45
  "@modelcontextprotocol/sdk": "^1.29.0",
46
+ "better-sqlite3": "^11.10.0",
47
+ "ignore": "^7.0.5",
45
48
  "picomatch": "^4.0.4",
46
49
  "web-tree-sitter": "^0.26.8",
47
50
  "yaml": "^2.8.3",