codetree-claude 1.4.7 → 1.5.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.
- package/README.md +144 -86
- package/dist/cli.js +69 -83
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +126 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +22 -0
- package/dist/config.js.map +1 -1
- package/dist/diff.d.ts +37 -0
- package/dist/diff.d.ts.map +1 -0
- package/dist/diff.js +181 -0
- package/dist/diff.js.map +1 -0
- package/dist/doctor.d.ts +5 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/doctor.js +139 -0
- package/dist/doctor.js.map +1 -0
- package/dist/hook/post-compact.js +1 -23
- package/dist/hook/post-compact.js.map +1 -1
- package/dist/hook/post-tool-use.js +15 -27
- package/dist/hook/post-tool-use.js.map +1 -1
- package/dist/hook/pre-compact.js +1 -22
- package/dist/hook/pre-compact.js.map +1 -1
- package/dist/hook/pre-tool-use.bundled.js +49 -69
- package/dist/hook/pre-tool-use.js +23 -80
- package/dist/hook/pre-tool-use.js.map +1 -1
- package/dist/hook/session-start.js +19 -23
- package/dist/hook/session-start.js.map +1 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -1
- package/dist/index.js.map +1 -1
- package/dist/indexer/extractor-registry.d.ts.map +1 -1
- package/dist/indexer/extractor-registry.js +6 -0
- package/dist/indexer/extractor-registry.js.map +1 -1
- package/dist/indexer/extractors/csharp.d.ts +12 -0
- package/dist/indexer/extractors/csharp.d.ts.map +1 -0
- package/dist/indexer/extractors/csharp.js +165 -0
- package/dist/indexer/extractors/csharp.js.map +1 -0
- package/dist/indexer/extractors/go.js +44 -1
- package/dist/indexer/extractors/go.js.map +1 -1
- package/dist/indexer/extractors/java.d.ts.map +1 -1
- package/dist/indexer/extractors/java.js +41 -3
- package/dist/indexer/extractors/java.js.map +1 -1
- package/dist/indexer/extractors/javascript.d.ts.map +1 -1
- package/dist/indexer/extractors/javascript.js +67 -3
- package/dist/indexer/extractors/javascript.js.map +1 -1
- package/dist/indexer/extractors/python.d.ts.map +1 -1
- package/dist/indexer/extractors/python.js +31 -2
- package/dist/indexer/extractors/python.js.map +1 -1
- package/dist/indexer/extractors/ruby.d.ts +10 -0
- package/dist/indexer/extractors/ruby.d.ts.map +1 -0
- package/dist/indexer/extractors/ruby.js +145 -0
- package/dist/indexer/extractors/ruby.js.map +1 -0
- package/dist/indexer/extractors/rust.d.ts +10 -0
- package/dist/indexer/extractors/rust.d.ts.map +1 -0
- package/dist/indexer/extractors/rust.js +194 -0
- package/dist/indexer/extractors/rust.js.map +1 -0
- package/dist/indexer/extractors/types.d.ts.map +1 -1
- package/dist/indexer/extractors/types.js +6 -0
- package/dist/indexer/extractors/types.js.map +1 -1
- package/dist/indexer/indexer.d.ts +23 -0
- package/dist/indexer/indexer.d.ts.map +1 -1
- package/dist/indexer/indexer.js +233 -14
- package/dist/indexer/indexer.js.map +1 -1
- package/dist/indexer/watcher.d.ts +3 -0
- package/dist/indexer/watcher.d.ts.map +1 -1
- package/dist/indexer/watcher.js +33 -1
- package/dist/indexer/watcher.js.map +1 -1
- package/dist/server/ipc.d.ts +10 -4
- package/dist/server/ipc.d.ts.map +1 -1
- package/dist/server/ipc.js +190 -109
- package/dist/server/ipc.js.map +1 -1
- package/dist/server/mcp-server.js +38 -29
- package/dist/server/mcp-server.js.map +1 -1
- package/dist/server/tools/codetree-edit.d.ts +2 -1
- package/dist/server/tools/codetree-edit.d.ts.map +1 -1
- package/dist/server/tools/codetree-edit.js +24 -6
- package/dist/server/tools/codetree-edit.js.map +1 -1
- package/dist/server/tools/codetree-find-refs.d.ts.map +1 -1
- package/dist/server/tools/codetree-find-refs.js +29 -15
- package/dist/server/tools/codetree-find-refs.js.map +1 -1
- package/dist/server/tools/codetree-memory.d.ts +3 -6
- package/dist/server/tools/codetree-memory.d.ts.map +1 -1
- package/dist/server/tools/codetree-memory.js +9 -10
- package/dist/server/tools/codetree-memory.js.map +1 -1
- package/dist/server/tools/codetree-outline.d.ts +57 -0
- package/dist/server/tools/codetree-outline.d.ts.map +1 -0
- package/dist/server/tools/codetree-outline.js +76 -0
- package/dist/server/tools/codetree-outline.js.map +1 -0
- package/dist/server/tools/codetree-probe.d.ts +53 -0
- package/dist/server/tools/codetree-probe.d.ts.map +1 -0
- package/dist/server/tools/codetree-probe.js +81 -0
- package/dist/server/tools/codetree-probe.js.map +1 -0
- package/dist/server/tools/codetree-read.d.ts +52 -3
- package/dist/server/tools/codetree-read.d.ts.map +1 -1
- package/dist/server/tools/codetree-read.js +249 -38
- package/dist/server/tools/codetree-read.js.map +1 -1
- package/dist/server/tools/codetree-search.d.ts +13 -1
- package/dist/server/tools/codetree-search.d.ts.map +1 -1
- package/dist/server/tools/codetree-search.js +89 -49
- package/dist/server/tools/codetree-search.js.map +1 -1
- package/dist/server/tools/codetree-structure.d.ts +25 -1
- package/dist/server/tools/codetree-structure.d.ts.map +1 -1
- package/dist/server/tools/codetree-structure.js +99 -31
- package/dist/server/tools/codetree-structure.js.map +1 -1
- package/dist/server/tools/codetree-summary.d.ts +2 -0
- package/dist/server/tools/codetree-summary.d.ts.map +1 -1
- package/dist/server/tools/codetree-summary.js +5 -0
- package/dist/server/tools/codetree-summary.js.map +1 -1
- package/dist/server/tools/codetree-write.d.ts +2 -1
- package/dist/server/tools/codetree-write.d.ts.map +1 -1
- package/dist/server/tools/codetree-write.js +9 -1
- package/dist/server/tools/codetree-write.js.map +1 -1
- package/dist/setup/install.d.ts +45 -1
- package/dist/setup/install.d.ts.map +1 -1
- package/dist/setup/install.js +163 -58
- package/dist/setup/install.js.map +1 -1
- package/dist/setup/template-load.d.ts +9 -0
- package/dist/setup/template-load.d.ts.map +1 -0
- package/dist/setup/template-load.js +39 -0
- package/dist/setup/template-load.js.map +1 -0
- package/dist/storage/database.d.ts +49 -0
- package/dist/storage/database.d.ts.map +1 -1
- package/dist/storage/database.js +398 -53
- package/dist/storage/database.js.map +1 -1
- package/dist/storage/disk-manager.d.ts.map +1 -1
- package/dist/storage/disk-manager.js +1 -5
- package/dist/storage/disk-manager.js.map +1 -1
- package/dist/utils.d.ts +23 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +102 -0
- package/dist/utils.js.map +1 -0
- package/package.json +63 -5
- package/templates/README.md +15 -0
- package/templates/agents-codetree.snippet.md +24 -0
- package/templates/claude-md-codetree.snippet.md +22 -0
- package/templates/claude-rules-codetree.md +23 -0
- package/templates/cursor-codetree.mdc +31 -0
- package/templates/gemini-codetree.snippet.md +6 -0
package/dist/storage/database.js
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
|
+
import { inflateSync } from "node:zlib";
|
|
4
|
+
/**
|
|
5
|
+
* Extract lowercase trigrams from content for the trigram index.
|
|
6
|
+
* Caps at 5000 unique trigrams per file to keep the index manageable.
|
|
7
|
+
*/
|
|
8
|
+
function extractTrigrams(content, maxTrigrams = 5000) {
|
|
9
|
+
const trigrams = new Set();
|
|
10
|
+
const lower = content.toLowerCase();
|
|
11
|
+
for (let i = 0; i < lower.length - 2 && trigrams.size < maxTrigrams; i++) {
|
|
12
|
+
const c0 = lower.charCodeAt(i);
|
|
13
|
+
// Only index trigrams from identifier-like characters (letters, digits, underscore)
|
|
14
|
+
if ((c0 >= 97 && c0 <= 122) || (c0 >= 48 && c0 <= 57) || c0 === 95) {
|
|
15
|
+
const tri = lower.slice(i, i + 3);
|
|
16
|
+
// Skip trigrams with whitespace/newlines
|
|
17
|
+
if (!/\s/.test(tri)) {
|
|
18
|
+
trigrams.add(tri);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return trigrams;
|
|
23
|
+
}
|
|
3
24
|
const SCHEMA_SQL = `
|
|
4
25
|
CREATE TABLE IF NOT EXISTS files (
|
|
5
26
|
path TEXT PRIMARY KEY,
|
|
@@ -123,6 +144,28 @@ CREATE TABLE IF NOT EXISTS compaction_snapshots (
|
|
|
123
144
|
|
|
124
145
|
CREATE INDEX IF NOT EXISTS idx_compaction_session ON compaction_snapshots(session_id);
|
|
125
146
|
CREATE INDEX IF NOT EXISTS idx_compaction_time ON compaction_snapshots(created_at);
|
|
147
|
+
|
|
148
|
+
-- Previous content: stores old file content for diff-based reads
|
|
149
|
+
CREATE TABLE IF NOT EXISTS previous_content (
|
|
150
|
+
hash TEXT PRIMARY KEY,
|
|
151
|
+
content BLOB NOT NULL,
|
|
152
|
+
saved_at INTEGER NOT NULL
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
-- Token postings: persisted inverted index for fast content search
|
|
156
|
+
CREATE TABLE IF NOT EXISTS token_postings (
|
|
157
|
+
token TEXT NOT NULL,
|
|
158
|
+
file_path TEXT NOT NULL,
|
|
159
|
+
PRIMARY KEY (token, file_path)
|
|
160
|
+
);
|
|
161
|
+
CREATE INDEX IF NOT EXISTS idx_token_postings_file ON token_postings(file_path);
|
|
162
|
+
|
|
163
|
+
-- Trigram index for fast content search (replaces sequential Grep scans)
|
|
164
|
+
CREATE TABLE IF NOT EXISTS trigrams (
|
|
165
|
+
trigram TEXT NOT NULL,
|
|
166
|
+
file_path TEXT NOT NULL,
|
|
167
|
+
PRIMARY KEY (trigram, file_path)
|
|
168
|
+
);
|
|
126
169
|
`;
|
|
127
170
|
export class Database {
|
|
128
171
|
config;
|
|
@@ -154,7 +197,7 @@ export class Database {
|
|
|
154
197
|
// Enable WAL-like behavior and foreign keys
|
|
155
198
|
this.db.run("PRAGMA journal_mode=MEMORY");
|
|
156
199
|
this.db.run("PRAGMA foreign_keys=ON");
|
|
157
|
-
this.db.run("PRAGMA synchronous=
|
|
200
|
+
this.db.run("PRAGMA synchronous=NORMAL");
|
|
158
201
|
// Create schema
|
|
159
202
|
this.db.run(SCHEMA_SQL);
|
|
160
203
|
// Schema version check — force re-extraction on upgrade
|
|
@@ -162,10 +205,21 @@ export class Database {
|
|
|
162
205
|
const result = this.db.exec("SELECT value FROM meta WHERE key = 'schema_version'");
|
|
163
206
|
const existingVersion = result.length > 0 ? String(result[0].values[0][0]) : "0";
|
|
164
207
|
if (existingVersion !== CURRENT_SCHEMA) {
|
|
165
|
-
// Clear all file hashes to force re-extraction on next scan
|
|
208
|
+
// Clear all file hashes AND mtime to force re-extraction on next scan.
|
|
209
|
+
// (mtime must be cleared too — otherwise indexFile's quickHash short-circuit
|
|
210
|
+
// matches and skips re-extraction even though we want to re-extract.)
|
|
166
211
|
this.db.run("DELETE FROM symbols");
|
|
167
212
|
this.db.run("DELETE FROM dependencies");
|
|
168
|
-
this.db.run("UPDATE files SET hash = 'force-reindex'");
|
|
213
|
+
this.db.run("UPDATE files SET hash = 'force-reindex', mtime_ms = 0");
|
|
214
|
+
// Drop derived in-memory/persisted indexes so they get rebuilt with the new logic
|
|
215
|
+
try {
|
|
216
|
+
this.db.run("DELETE FROM token_postings");
|
|
217
|
+
}
|
|
218
|
+
catch { /* table may not exist on first run */ }
|
|
219
|
+
try {
|
|
220
|
+
this.db.run("DELETE FROM trigrams");
|
|
221
|
+
}
|
|
222
|
+
catch { /* table may not exist on first run */ }
|
|
169
223
|
this.db.run(`INSERT OR REPLACE INTO meta (key, value) VALUES ('schema_version', '${CURRENT_SCHEMA}')`);
|
|
170
224
|
this.dirty = true;
|
|
171
225
|
this.persistToDisk();
|
|
@@ -271,8 +325,33 @@ export class Database {
|
|
|
271
325
|
searchSymbols(query, kind, limit = 50) {
|
|
272
326
|
if (!this.db)
|
|
273
327
|
throw new Error("Database not initialized");
|
|
274
|
-
|
|
275
|
-
|
|
328
|
+
// Try exact match first (uses idx_symbols_name index efficiently)
|
|
329
|
+
let sql;
|
|
330
|
+
let params;
|
|
331
|
+
const exactResults = this.searchSymbolsExact(query, kind, limit);
|
|
332
|
+
if (exactResults.length > 0)
|
|
333
|
+
return exactResults;
|
|
334
|
+
// Prefix match (still uses index for prefix portion)
|
|
335
|
+
sql = "SELECT * FROM symbols WHERE name LIKE ?";
|
|
336
|
+
params = [`${query}%`];
|
|
337
|
+
if (kind) {
|
|
338
|
+
sql += " AND kind = ?";
|
|
339
|
+
params.push(kind);
|
|
340
|
+
}
|
|
341
|
+
sql += ` LIMIT ${limit}`;
|
|
342
|
+
const prefixStmt = this.db.prepare(sql);
|
|
343
|
+
prefixStmt.bind(params);
|
|
344
|
+
const prefixResults = [];
|
|
345
|
+
while (prefixStmt.step()) {
|
|
346
|
+
const row = prefixStmt.getAsObject();
|
|
347
|
+
prefixResults.push(this.rowToSymbol(row));
|
|
348
|
+
}
|
|
349
|
+
prefixStmt.free();
|
|
350
|
+
if (prefixResults.length >= Math.min(5, limit))
|
|
351
|
+
return prefixResults;
|
|
352
|
+
// Fall back to substring match (no index benefit, but rare)
|
|
353
|
+
sql = "SELECT * FROM symbols WHERE name LIKE ?";
|
|
354
|
+
params = [`%${query}%`];
|
|
276
355
|
if (kind) {
|
|
277
356
|
sql += " AND kind = ?";
|
|
278
357
|
params.push(kind);
|
|
@@ -282,17 +361,38 @@ export class Database {
|
|
|
282
361
|
stmt.bind(params);
|
|
283
362
|
const results = [];
|
|
284
363
|
while (stmt.step()) {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
364
|
+
results.push(this.rowToSymbol(stmt.getAsObject()));
|
|
365
|
+
}
|
|
366
|
+
stmt.free();
|
|
367
|
+
return results;
|
|
368
|
+
}
|
|
369
|
+
rowToSymbol(row) {
|
|
370
|
+
return {
|
|
371
|
+
id: row.id,
|
|
372
|
+
file_path: row.file_path,
|
|
373
|
+
name: row.name,
|
|
374
|
+
kind: row.kind,
|
|
375
|
+
line_start: row.line_start,
|
|
376
|
+
line_end: row.line_end,
|
|
377
|
+
signature: row.signature,
|
|
378
|
+
exported: row.exported === 1,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
searchSymbolsExact(query, kind, limit = 50) {
|
|
382
|
+
if (!this.db)
|
|
383
|
+
throw new Error("Database not initialized");
|
|
384
|
+
let sql = "SELECT * FROM symbols WHERE name = ?";
|
|
385
|
+
const params = [query];
|
|
386
|
+
if (kind) {
|
|
387
|
+
sql += " AND kind = ?";
|
|
388
|
+
params.push(kind);
|
|
389
|
+
}
|
|
390
|
+
sql += ` LIMIT ${limit}`;
|
|
391
|
+
const stmt = this.db.prepare(sql);
|
|
392
|
+
stmt.bind(params);
|
|
393
|
+
const results = [];
|
|
394
|
+
while (stmt.step()) {
|
|
395
|
+
results.push(this.rowToSymbol(stmt.getAsObject()));
|
|
296
396
|
}
|
|
297
397
|
stmt.free();
|
|
298
398
|
return results;
|
|
@@ -385,23 +485,90 @@ export class Database {
|
|
|
385
485
|
const maxBytes = maxSizeMB * 1024 * 1024;
|
|
386
486
|
if (totalBytes <= maxBytes)
|
|
387
487
|
return;
|
|
388
|
-
//
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
)
|
|
488
|
+
// Iteratively delete oldest entries until under limit
|
|
489
|
+
let currentBytes = totalBytes;
|
|
490
|
+
while (currentBytes > maxBytes) {
|
|
491
|
+
// Delete the oldest 10% of entries each pass
|
|
492
|
+
const batchSize = Math.max(1, Math.ceil((this.db.exec("SELECT COUNT(*) FROM content_cache")[0]?.values[0][0] || 0) * 0.1));
|
|
493
|
+
const before = this.db.exec("SELECT SUM(LENGTH(content)) FROM content_cache");
|
|
494
|
+
this.db.run(`
|
|
495
|
+
DELETE FROM content_cache WHERE path IN (
|
|
496
|
+
SELECT path FROM content_cache
|
|
497
|
+
ORDER BY accessed_at ASC
|
|
498
|
+
LIMIT ${batchSize}
|
|
400
499
|
)
|
|
401
|
-
)
|
|
402
|
-
|
|
500
|
+
`);
|
|
501
|
+
const after = this.db.exec("SELECT SUM(LENGTH(content)) FROM content_cache");
|
|
502
|
+
currentBytes = after[0]?.values[0][0] || 0;
|
|
503
|
+
// Safety: if nothing was deleted, break to avoid infinite loop
|
|
504
|
+
const beforeBytes = before[0]?.values[0][0] || 0;
|
|
505
|
+
if (currentBytes >= beforeBytes)
|
|
506
|
+
break;
|
|
507
|
+
}
|
|
403
508
|
this.markDirty();
|
|
404
509
|
}
|
|
510
|
+
/**
|
|
511
|
+
* Look up cached content for a file that previously had a given hash.
|
|
512
|
+
* Used for diff-based reads: find old version to compute delta.
|
|
513
|
+
* Checks the previous_content table first, then falls back to content_cache.
|
|
514
|
+
* Returns decompressed content or null if not found.
|
|
515
|
+
*/
|
|
516
|
+
getCachedContentByHash(hash) {
|
|
517
|
+
if (!this.db)
|
|
518
|
+
throw new Error("Database not initialized");
|
|
519
|
+
// Check previous_content table (stores last version before update)
|
|
520
|
+
const stmt = this.db.prepare("SELECT content FROM previous_content WHERE hash = ? LIMIT 1");
|
|
521
|
+
stmt.bind([hash]);
|
|
522
|
+
if (stmt.step()) {
|
|
523
|
+
const row = stmt.get();
|
|
524
|
+
stmt.free();
|
|
525
|
+
try {
|
|
526
|
+
return inflateSync(Buffer.from(row[0])).toString("utf-8");
|
|
527
|
+
}
|
|
528
|
+
catch {
|
|
529
|
+
return null;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
stmt.free();
|
|
533
|
+
return null;
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Save previous content before overwriting, keyed by hash.
|
|
537
|
+
* Used for diff-based reads.
|
|
538
|
+
*
|
|
539
|
+
* Cleanup strategy:
|
|
540
|
+
* - TTL: 24h (long enough to survive long sessions / compaction).
|
|
541
|
+
* - Hard cap: 200 rows; oldest evicted first.
|
|
542
|
+
* Both run AFTER the insert so we never delete the row we just saved.
|
|
543
|
+
*/
|
|
544
|
+
savePreviousContent(hash, compressedContent) {
|
|
545
|
+
if (!this.db)
|
|
546
|
+
throw new Error("Database not initialized");
|
|
547
|
+
try {
|
|
548
|
+
const now = Date.now();
|
|
549
|
+
this.db.run("INSERT OR REPLACE INTO previous_content (hash, content, saved_at) VALUES (?, ?, ?)", [hash, compressedContent, now]);
|
|
550
|
+
// TTL eviction (24h). Use strict < so the just-saved row is never targeted.
|
|
551
|
+
this.db.run("DELETE FROM previous_content WHERE saved_at < ?", [now - 24 * 3600_000]);
|
|
552
|
+
// Row-count cap (oldest first). Keeps table size bounded for fast-moving repos.
|
|
553
|
+
const MAX_ROWS = 200;
|
|
554
|
+
this.db.run(`DELETE FROM previous_content WHERE hash IN (
|
|
555
|
+
SELECT hash FROM previous_content
|
|
556
|
+
ORDER BY saved_at ASC
|
|
557
|
+
LIMIT MAX(0, (SELECT COUNT(*) FROM previous_content) - ${MAX_ROWS})
|
|
558
|
+
)`);
|
|
559
|
+
this.markDirty();
|
|
560
|
+
}
|
|
561
|
+
catch { /* ignore if table doesn't exist yet */ }
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* Get the total size of the content_cache table in bytes (compressed content).
|
|
565
|
+
*/
|
|
566
|
+
getContentCacheSize() {
|
|
567
|
+
if (!this.db)
|
|
568
|
+
throw new Error("Database not initialized");
|
|
569
|
+
const result = this.db.exec("SELECT SUM(LENGTH(content)) FROM content_cache");
|
|
570
|
+
return result.length > 0 ? (result[0].values[0][0] || 0) : 0;
|
|
571
|
+
}
|
|
405
572
|
// ─── Search Operations ─────────────────────────────────────────
|
|
406
573
|
searchFiles(pattern) {
|
|
407
574
|
if (!this.db)
|
|
@@ -439,6 +606,23 @@ export class Database {
|
|
|
439
606
|
this.db.run("UPDATE sessions SET ended_at = ?, last_accessed = ?, summary = ? WHERE id = ?", [Date.now(), Date.now(), summary || null, sessionId]);
|
|
440
607
|
this.markDirty();
|
|
441
608
|
}
|
|
609
|
+
/**
|
|
610
|
+
* Most recent read timestamp by a session for a file (any tool).
|
|
611
|
+
* Returns 0 if there is no prior read.
|
|
612
|
+
*/
|
|
613
|
+
getLastSessionReadAt(sessionId, filePath) {
|
|
614
|
+
if (!this.db)
|
|
615
|
+
throw new Error("Database not initialized");
|
|
616
|
+
const stmt = this.db.prepare("SELECT MAX(read_at) as last FROM session_reads WHERE session_id = ? AND file_path = ?");
|
|
617
|
+
stmt.bind([sessionId, filePath]);
|
|
618
|
+
let last = 0;
|
|
619
|
+
if (stmt.step()) {
|
|
620
|
+
const row = stmt.getAsObject();
|
|
621
|
+
last = row.last || 0;
|
|
622
|
+
}
|
|
623
|
+
stmt.free();
|
|
624
|
+
return last;
|
|
625
|
+
}
|
|
442
626
|
logSessionRead(sessionId, filePath, toolName, lineStart, lineEnd) {
|
|
443
627
|
if (!this.db)
|
|
444
628
|
throw new Error("Database not initialized");
|
|
@@ -446,20 +630,23 @@ export class Database {
|
|
|
446
630
|
VALUES (?, ?, ?, ?, ?, ?)`);
|
|
447
631
|
stmt.run([sessionId, filePath, Date.now(), lineStart || null, lineEnd || null, toolName]);
|
|
448
632
|
stmt.free();
|
|
449
|
-
// Also update the session's files_read JSON array
|
|
450
|
-
const session = this.getSession(sessionId);
|
|
451
|
-
if (session) {
|
|
452
|
-
try {
|
|
453
|
-
const filesRead = JSON.parse(session.files_read || "[]");
|
|
454
|
-
if (!filesRead.includes(filePath)) {
|
|
455
|
-
filesRead.push(filePath);
|
|
456
|
-
this.db.run("UPDATE sessions SET files_read = ? WHERE id = ?", [JSON.stringify(filesRead), sessionId]);
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
catch { }
|
|
460
|
-
}
|
|
461
633
|
this.markDirty();
|
|
462
634
|
}
|
|
635
|
+
/**
|
|
636
|
+
* Get list of unique files read in a session (from session_reads table, not JSON column).
|
|
637
|
+
*/
|
|
638
|
+
getSessionFilesRead(sessionId) {
|
|
639
|
+
if (!this.db)
|
|
640
|
+
throw new Error("Database not initialized");
|
|
641
|
+
const stmt = this.db.prepare("SELECT DISTINCT file_path FROM session_reads WHERE session_id = ?");
|
|
642
|
+
stmt.bind([sessionId]);
|
|
643
|
+
const paths = [];
|
|
644
|
+
while (stmt.step()) {
|
|
645
|
+
paths.push(stmt.get()[0]);
|
|
646
|
+
}
|
|
647
|
+
stmt.free();
|
|
648
|
+
return paths;
|
|
649
|
+
}
|
|
463
650
|
logSessionModified(sessionId, filePath) {
|
|
464
651
|
if (!this.db)
|
|
465
652
|
throw new Error("Database not initialized");
|
|
@@ -492,6 +679,25 @@ export class Database {
|
|
|
492
679
|
}
|
|
493
680
|
this.markDirty();
|
|
494
681
|
}
|
|
682
|
+
logSessionGrep(sessionId, pattern) {
|
|
683
|
+
if (!this.db)
|
|
684
|
+
throw new Error("Database not initialized");
|
|
685
|
+
const session = this.getSession(sessionId);
|
|
686
|
+
if (session) {
|
|
687
|
+
try {
|
|
688
|
+
// Store grep patterns in the decisions JSON column (reusing existing column to avoid schema migration)
|
|
689
|
+
const decisions = JSON.parse(session.decisions || "{}");
|
|
690
|
+
const greps = Array.isArray(decisions._grep_patterns) ? decisions._grep_patterns : [];
|
|
691
|
+
if (!greps.includes(pattern)) {
|
|
692
|
+
greps.push(pattern);
|
|
693
|
+
decisions._grep_patterns = greps;
|
|
694
|
+
this.db.run("UPDATE sessions SET decisions = ? WHERE id = ?", [JSON.stringify(decisions), sessionId]);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
catch { }
|
|
698
|
+
}
|
|
699
|
+
this.markDirty();
|
|
700
|
+
}
|
|
495
701
|
addSessionInsight(sessionId, category, content, filePaths) {
|
|
496
702
|
if (!this.db)
|
|
497
703
|
throw new Error("Database not initialized");
|
|
@@ -579,24 +785,29 @@ export class Database {
|
|
|
579
785
|
if (!this.db)
|
|
580
786
|
throw new Error("Database not initialized");
|
|
581
787
|
const cutoff = Date.now() - maxAgeDays * 24 * 60 * 60 * 1000;
|
|
582
|
-
const
|
|
788
|
+
const stmt = this.db.prepare(`
|
|
583
789
|
SELECT sr.file_path,
|
|
584
790
|
COUNT(*) as total_reads,
|
|
585
791
|
COUNT(DISTINCT sr.session_id) as sessions
|
|
586
792
|
FROM session_reads sr
|
|
587
793
|
JOIN sessions s ON sr.session_id = s.id
|
|
588
|
-
WHERE s.last_accessed >
|
|
794
|
+
WHERE s.last_accessed > ?
|
|
589
795
|
GROUP BY sr.file_path
|
|
590
796
|
ORDER BY sessions DESC, total_reads DESC
|
|
591
|
-
LIMIT
|
|
797
|
+
LIMIT ?
|
|
592
798
|
`);
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
799
|
+
stmt.bind([cutoff, limit]);
|
|
800
|
+
const results = [];
|
|
801
|
+
while (stmt.step()) {
|
|
802
|
+
const row = stmt.getAsObject();
|
|
803
|
+
results.push({
|
|
804
|
+
file_path: row.file_path,
|
|
805
|
+
total_reads: row.total_reads,
|
|
806
|
+
sessions: row.sessions,
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
stmt.free();
|
|
810
|
+
return results;
|
|
600
811
|
}
|
|
601
812
|
/**
|
|
602
813
|
* Get files that changed since a given timestamp.
|
|
@@ -711,6 +922,15 @@ export class Database {
|
|
|
711
922
|
fileSummaries[filePath] = `${fileRecord.language || "unknown"}, ${fileRecord.line_count} lines`;
|
|
712
923
|
}
|
|
713
924
|
}
|
|
925
|
+
// Extract grep patterns from session decisions
|
|
926
|
+
let grepPatterns = [];
|
|
927
|
+
try {
|
|
928
|
+
const decisions = session ? JSON.parse(session.decisions || "{}") : {};
|
|
929
|
+
if (Array.isArray(decisions._grep_patterns)) {
|
|
930
|
+
grepPatterns = decisions._grep_patterns;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
catch { }
|
|
714
934
|
return {
|
|
715
935
|
filesRead: reads.map((r) => ({
|
|
716
936
|
path: r.file_path,
|
|
@@ -718,10 +938,135 @@ export class Database {
|
|
|
718
938
|
})),
|
|
719
939
|
filesModified,
|
|
720
940
|
symbolsDiscovered: symbolsSearched,
|
|
721
|
-
grepPatterns
|
|
941
|
+
grepPatterns,
|
|
722
942
|
fileSummaries,
|
|
723
943
|
};
|
|
724
944
|
}
|
|
945
|
+
// ─── Token Postings (persisted inverted index) ──────────────
|
|
946
|
+
saveTokenPostings(filePath, tokens) {
|
|
947
|
+
if (!this.db)
|
|
948
|
+
throw new Error("Database not initialized");
|
|
949
|
+
this.db.run("DELETE FROM token_postings WHERE file_path = ?", [filePath]);
|
|
950
|
+
if (tokens.size === 0)
|
|
951
|
+
return;
|
|
952
|
+
const stmt = this.db.prepare("INSERT OR IGNORE INTO token_postings (token, file_path) VALUES (?, ?)");
|
|
953
|
+
for (const token of tokens) {
|
|
954
|
+
stmt.run([token, filePath]);
|
|
955
|
+
}
|
|
956
|
+
stmt.free();
|
|
957
|
+
this.markDirty();
|
|
958
|
+
}
|
|
959
|
+
deleteTokenPostings(filePath) {
|
|
960
|
+
if (!this.db)
|
|
961
|
+
throw new Error("Database not initialized");
|
|
962
|
+
this.db.run("DELETE FROM token_postings WHERE file_path = ?", [filePath]);
|
|
963
|
+
this.markDirty();
|
|
964
|
+
}
|
|
965
|
+
loadAllTokenPostings() {
|
|
966
|
+
if (!this.db)
|
|
967
|
+
throw new Error("Database not initialized");
|
|
968
|
+
const result = this.db.exec("SELECT token, file_path FROM token_postings");
|
|
969
|
+
const postings = new Map();
|
|
970
|
+
if (result.length === 0)
|
|
971
|
+
return postings;
|
|
972
|
+
for (const row of result[0].values) {
|
|
973
|
+
const token = row[0];
|
|
974
|
+
const filePath = row[1];
|
|
975
|
+
let set = postings.get(token);
|
|
976
|
+
if (!set) {
|
|
977
|
+
set = new Set();
|
|
978
|
+
postings.set(token, set);
|
|
979
|
+
}
|
|
980
|
+
set.add(filePath);
|
|
981
|
+
}
|
|
982
|
+
return postings;
|
|
983
|
+
}
|
|
984
|
+
getTokenPostingsFileMap() {
|
|
985
|
+
if (!this.db)
|
|
986
|
+
throw new Error("Database not initialized");
|
|
987
|
+
const result = this.db.exec("SELECT file_path, token FROM token_postings");
|
|
988
|
+
const fileTokens = new Map();
|
|
989
|
+
if (result.length === 0)
|
|
990
|
+
return fileTokens;
|
|
991
|
+
for (const row of result[0].values) {
|
|
992
|
+
const filePath = row[0];
|
|
993
|
+
const token = row[1];
|
|
994
|
+
let set = fileTokens.get(filePath);
|
|
995
|
+
if (!set) {
|
|
996
|
+
set = new Set();
|
|
997
|
+
fileTokens.set(filePath, set);
|
|
998
|
+
}
|
|
999
|
+
set.add(token);
|
|
1000
|
+
}
|
|
1001
|
+
return fileTokens;
|
|
1002
|
+
}
|
|
1003
|
+
// ─── Trigram Index ──────────────────────────────────────────
|
|
1004
|
+
saveTrigrams(filePath, content) {
|
|
1005
|
+
if (!this.db)
|
|
1006
|
+
throw new Error("Database not initialized");
|
|
1007
|
+
this.db.run("DELETE FROM trigrams WHERE file_path = ?", [filePath]);
|
|
1008
|
+
const trigrams = extractTrigrams(content);
|
|
1009
|
+
if (trigrams.size === 0)
|
|
1010
|
+
return;
|
|
1011
|
+
const stmt = this.db.prepare("INSERT OR IGNORE INTO trigrams (trigram, file_path) VALUES (?, ?)");
|
|
1012
|
+
for (const tri of trigrams) {
|
|
1013
|
+
stmt.run([tri, filePath]);
|
|
1014
|
+
}
|
|
1015
|
+
stmt.free();
|
|
1016
|
+
// Don't markDirty here — called during indexing which already persists
|
|
1017
|
+
}
|
|
1018
|
+
deleteTrigrams(filePath) {
|
|
1019
|
+
if (!this.db)
|
|
1020
|
+
throw new Error("Database not initialized");
|
|
1021
|
+
this.db.run("DELETE FROM trigrams WHERE file_path = ?", [filePath]);
|
|
1022
|
+
}
|
|
1023
|
+
/**
|
|
1024
|
+
* Find files that contain ALL trigrams from the query string.
|
|
1025
|
+
* This narrows candidates before doing actual content search.
|
|
1026
|
+
*/
|
|
1027
|
+
searchByTrigrams(query, limit = 200) {
|
|
1028
|
+
if (!this.db)
|
|
1029
|
+
throw new Error("Database not initialized");
|
|
1030
|
+
const trigrams = Array.from(extractTrigrams(query));
|
|
1031
|
+
if (trigrams.length === 0)
|
|
1032
|
+
return [];
|
|
1033
|
+
// Intersect: files that have ALL trigrams
|
|
1034
|
+
// Start with the rarest trigram to minimize set size
|
|
1035
|
+
let sql = "SELECT file_path FROM trigrams WHERE trigram = ?";
|
|
1036
|
+
for (let i = 1; i < Math.min(trigrams.length, 6); i++) {
|
|
1037
|
+
sql += " INTERSECT SELECT file_path FROM trigrams WHERE trigram = ?";
|
|
1038
|
+
}
|
|
1039
|
+
sql += ` LIMIT ${limit}`;
|
|
1040
|
+
const stmt = this.db.prepare(sql);
|
|
1041
|
+
stmt.bind(trigrams.slice(0, 6));
|
|
1042
|
+
const results = [];
|
|
1043
|
+
while (stmt.step()) {
|
|
1044
|
+
results.push(stmt.get()[0]);
|
|
1045
|
+
}
|
|
1046
|
+
stmt.free();
|
|
1047
|
+
return results;
|
|
1048
|
+
}
|
|
1049
|
+
getSymbolCount() {
|
|
1050
|
+
if (!this.db)
|
|
1051
|
+
throw new Error("Database not initialized");
|
|
1052
|
+
const result = this.db.exec("SELECT COUNT(*) FROM symbols");
|
|
1053
|
+
return result.length > 0 ? result[0].values[0][0] : 0;
|
|
1054
|
+
}
|
|
1055
|
+
/**
|
|
1056
|
+
* Get symbol counts per file in a single query (avoids N+1).
|
|
1057
|
+
*/
|
|
1058
|
+
getSymbolCountsByFile() {
|
|
1059
|
+
if (!this.db)
|
|
1060
|
+
throw new Error("Database not initialized");
|
|
1061
|
+
const result = this.db.exec("SELECT file_path, COUNT(*) as cnt FROM symbols GROUP BY file_path");
|
|
1062
|
+
const counts = new Map();
|
|
1063
|
+
if (result.length === 0)
|
|
1064
|
+
return counts;
|
|
1065
|
+
for (const row of result[0].values) {
|
|
1066
|
+
counts.set(row[0], row[1]);
|
|
1067
|
+
}
|
|
1068
|
+
return counts;
|
|
1069
|
+
}
|
|
725
1070
|
// ─── Lifecycle ─────────────────────────────────────────────────
|
|
726
1071
|
close() {
|
|
727
1072
|
if (this.saveTimer) {
|