kiro-memory 1.5.0 → 1.7.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.
- package/README.md +120 -101
- package/package.json +18 -8
- package/plugin/dist/cli/contextkit.js +2609 -593
- package/plugin/dist/hooks/agentSpawn.js +1399 -279
- package/plugin/dist/hooks/kiro-hooks.js +1307 -222
- package/plugin/dist/hooks/postToolUse.js +1404 -264
- package/plugin/dist/hooks/stop.js +1381 -261
- package/plugin/dist/hooks/userPromptSubmit.js +1369 -258
- package/plugin/dist/index.js +1352 -255
- package/plugin/dist/sdk/index.js +1306 -220
- package/plugin/dist/servers/mcp-server.js +203 -2
- package/plugin/dist/services/search/EmbeddingService.js +363 -0
- package/plugin/dist/services/search/HybridSearch.js +703 -151
- package/plugin/dist/services/search/ScoringEngine.js +75 -0
- package/plugin/dist/services/search/VectorSearch.js +512 -0
- package/plugin/dist/services/search/index.js +776 -64
- package/plugin/dist/services/sqlite/Database.js +73 -6
- package/plugin/dist/services/sqlite/Observations.js +70 -6
- package/plugin/dist/services/sqlite/Search.js +98 -8
- package/plugin/dist/services/sqlite/Summaries.js +8 -5
- package/plugin/dist/services/sqlite/index.js +413 -24
- package/plugin/dist/shared/paths.js +6 -3
- package/plugin/dist/types/worker-types.js +6 -0
- package/plugin/dist/viewer.js +381 -73
- package/plugin/dist/worker-service.js +4585 -195
|
@@ -70,7 +70,7 @@ var BunQueryCompat = class {
|
|
|
70
70
|
// src/shared/paths.ts
|
|
71
71
|
import { join as join2, dirname, basename } from "path";
|
|
72
72
|
import { homedir as homedir2 } from "os";
|
|
73
|
-
import { mkdirSync as mkdirSync2 } from "fs";
|
|
73
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
74
74
|
import { fileURLToPath } from "url";
|
|
75
75
|
|
|
76
76
|
// src/utils/logger.ts
|
|
@@ -300,7 +300,9 @@ function getDirname() {
|
|
|
300
300
|
return dirname(fileURLToPath(import.meta.url));
|
|
301
301
|
}
|
|
302
302
|
var _dirname = getDirname();
|
|
303
|
-
var
|
|
303
|
+
var _legacyDir = join2(homedir2(), ".contextkit");
|
|
304
|
+
var _defaultDir = existsSync2(_legacyDir) ? _legacyDir : join2(homedir2(), ".kiro-memory");
|
|
305
|
+
var DATA_DIR = process.env.KIRO_MEMORY_DATA_DIR || process.env.CONTEXTKIT_DATA_DIR || _defaultDir;
|
|
304
306
|
var KIRO_CONFIG_DIR = process.env.KIRO_CONFIG_DIR || join2(homedir2(), ".kiro");
|
|
305
307
|
var PLUGIN_ROOT = join2(KIRO_CONFIG_DIR, "plugins", "kiro-memory");
|
|
306
308
|
var ARCHIVES_DIR = join2(DATA_DIR, "archives");
|
|
@@ -309,7 +311,8 @@ var TRASH_DIR = join2(DATA_DIR, "trash");
|
|
|
309
311
|
var BACKUPS_DIR = join2(DATA_DIR, "backups");
|
|
310
312
|
var MODES_DIR = join2(DATA_DIR, "modes");
|
|
311
313
|
var USER_SETTINGS_PATH = join2(DATA_DIR, "settings.json");
|
|
312
|
-
var
|
|
314
|
+
var _legacyDb = join2(DATA_DIR, "contextkit.db");
|
|
315
|
+
var DB_PATH = existsSync2(_legacyDb) ? _legacyDb : join2(DATA_DIR, "kiro-memory.db");
|
|
313
316
|
var VECTOR_DB_DIR = join2(DATA_DIR, "vector-db");
|
|
314
317
|
var OBSERVER_SESSIONS_DIR = join2(DATA_DIR, "observer-sessions");
|
|
315
318
|
var KIRO_SETTINGS_PATH = join2(KIRO_CONFIG_DIR, "settings.json");
|
|
@@ -324,7 +327,11 @@ var SQLITE_CACHE_SIZE_PAGES = 1e4;
|
|
|
324
327
|
var dbInstance = null;
|
|
325
328
|
var KiroMemoryDatabase = class {
|
|
326
329
|
db;
|
|
327
|
-
|
|
330
|
+
/**
|
|
331
|
+
* @param dbPath - Percorso al file SQLite (default: DB_PATH)
|
|
332
|
+
* @param skipMigrations - Se true, salta il migration runner (per hook ad alta frequenza)
|
|
333
|
+
*/
|
|
334
|
+
constructor(dbPath = DB_PATH, skipMigrations = false) {
|
|
328
335
|
if (dbPath !== ":memory:") {
|
|
329
336
|
ensureDir(DATA_DIR);
|
|
330
337
|
}
|
|
@@ -335,8 +342,18 @@ var KiroMemoryDatabase = class {
|
|
|
335
342
|
this.db.run("PRAGMA temp_store = memory");
|
|
336
343
|
this.db.run(`PRAGMA mmap_size = ${SQLITE_MMAP_SIZE_BYTES}`);
|
|
337
344
|
this.db.run(`PRAGMA cache_size = ${SQLITE_CACHE_SIZE_PAGES}`);
|
|
338
|
-
|
|
339
|
-
|
|
345
|
+
if (!skipMigrations) {
|
|
346
|
+
const migrationRunner = new MigrationRunner(this.db);
|
|
347
|
+
migrationRunner.runAllMigrations();
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Esegue una funzione all'interno di una transazione atomica.
|
|
352
|
+
* Se fn() lancia un errore, la transazione viene annullata automaticamente.
|
|
353
|
+
*/
|
|
354
|
+
withTransaction(fn) {
|
|
355
|
+
const transaction = this.db.transaction(fn);
|
|
356
|
+
return transaction(this.db);
|
|
340
357
|
}
|
|
341
358
|
/**
|
|
342
359
|
* Close the database connection
|
|
@@ -619,6 +636,55 @@ var MigrationRunner = class {
|
|
|
619
636
|
`);
|
|
620
637
|
db.run("CREATE UNIQUE INDEX IF NOT EXISTS idx_project_aliases_name ON project_aliases(project_name)");
|
|
621
638
|
}
|
|
639
|
+
},
|
|
640
|
+
{
|
|
641
|
+
version: 4,
|
|
642
|
+
up: (db) => {
|
|
643
|
+
db.run(`
|
|
644
|
+
CREATE TABLE IF NOT EXISTS observation_embeddings (
|
|
645
|
+
observation_id INTEGER PRIMARY KEY,
|
|
646
|
+
embedding BLOB NOT NULL,
|
|
647
|
+
model TEXT NOT NULL,
|
|
648
|
+
dimensions INTEGER NOT NULL,
|
|
649
|
+
created_at TEXT NOT NULL,
|
|
650
|
+
FOREIGN KEY (observation_id) REFERENCES observations(id) ON DELETE CASCADE
|
|
651
|
+
)
|
|
652
|
+
`);
|
|
653
|
+
db.run("CREATE INDEX IF NOT EXISTS idx_embeddings_model ON observation_embeddings(model)");
|
|
654
|
+
}
|
|
655
|
+
},
|
|
656
|
+
{
|
|
657
|
+
version: 5,
|
|
658
|
+
up: (db) => {
|
|
659
|
+
db.run("ALTER TABLE observations ADD COLUMN last_accessed_epoch INTEGER");
|
|
660
|
+
db.run("ALTER TABLE observations ADD COLUMN is_stale INTEGER DEFAULT 0");
|
|
661
|
+
db.run("CREATE INDEX IF NOT EXISTS idx_observations_last_accessed ON observations(last_accessed_epoch)");
|
|
662
|
+
db.run("CREATE INDEX IF NOT EXISTS idx_observations_stale ON observations(is_stale)");
|
|
663
|
+
}
|
|
664
|
+
},
|
|
665
|
+
{
|
|
666
|
+
version: 6,
|
|
667
|
+
up: (db) => {
|
|
668
|
+
db.run(`
|
|
669
|
+
CREATE TABLE IF NOT EXISTS checkpoints (
|
|
670
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
671
|
+
session_id INTEGER NOT NULL,
|
|
672
|
+
project TEXT NOT NULL,
|
|
673
|
+
task TEXT NOT NULL,
|
|
674
|
+
progress TEXT,
|
|
675
|
+
next_steps TEXT,
|
|
676
|
+
open_questions TEXT,
|
|
677
|
+
relevant_files TEXT,
|
|
678
|
+
context_snapshot TEXT,
|
|
679
|
+
created_at TEXT NOT NULL,
|
|
680
|
+
created_at_epoch INTEGER NOT NULL,
|
|
681
|
+
FOREIGN KEY (session_id) REFERENCES sessions(id)
|
|
682
|
+
)
|
|
683
|
+
`);
|
|
684
|
+
db.run("CREATE INDEX IF NOT EXISTS idx_checkpoints_session ON checkpoints(session_id)");
|
|
685
|
+
db.run("CREATE INDEX IF NOT EXISTS idx_checkpoints_project ON checkpoints(project)");
|
|
686
|
+
db.run("CREATE INDEX IF NOT EXISTS idx_checkpoints_epoch ON checkpoints(created_at_epoch)");
|
|
687
|
+
}
|
|
622
688
|
}
|
|
623
689
|
];
|
|
624
690
|
}
|
|
@@ -634,6 +700,7 @@ async function initializeDatabase() {
|
|
|
634
700
|
return await manager.initialize();
|
|
635
701
|
}
|
|
636
702
|
export {
|
|
703
|
+
KiroMemoryDatabase as ContextKitDatabase,
|
|
637
704
|
Database,
|
|
638
705
|
DatabaseManager,
|
|
639
706
|
KiroMemoryDatabase,
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { createRequire } from 'module';const require = createRequire(import.meta.url);
|
|
2
2
|
|
|
3
3
|
// src/services/sqlite/Observations.ts
|
|
4
|
+
function escapeLikePattern(input) {
|
|
5
|
+
return input.replace(/[%_\\]/g, "\\$&");
|
|
6
|
+
}
|
|
4
7
|
function createObservation(db, memorySessionId, project, type, title, subtitle, text, narrative, facts, concepts, filesRead, filesModified, promptNumber) {
|
|
5
8
|
const now = /* @__PURE__ */ new Date();
|
|
6
9
|
const result = db.run(
|
|
@@ -24,12 +27,12 @@ function getObservationsByProject(db, project, limit = 100) {
|
|
|
24
27
|
return query.all(project, limit);
|
|
25
28
|
}
|
|
26
29
|
function searchObservations(db, searchTerm, project) {
|
|
27
|
-
const sql = project ? `SELECT * FROM observations
|
|
28
|
-
WHERE project = ? AND (title LIKE ? OR text LIKE ? OR narrative LIKE ?)
|
|
29
|
-
ORDER BY created_at_epoch DESC` : `SELECT * FROM observations
|
|
30
|
-
WHERE title LIKE ? OR text LIKE ? OR narrative LIKE ?
|
|
30
|
+
const sql = project ? `SELECT * FROM observations
|
|
31
|
+
WHERE project = ? AND (title LIKE ? ESCAPE '\\' OR text LIKE ? ESCAPE '\\' OR narrative LIKE ? ESCAPE '\\')
|
|
32
|
+
ORDER BY created_at_epoch DESC` : `SELECT * FROM observations
|
|
33
|
+
WHERE title LIKE ? ESCAPE '\\' OR text LIKE ? ESCAPE '\\' OR narrative LIKE ? ESCAPE '\\'
|
|
31
34
|
ORDER BY created_at_epoch DESC`;
|
|
32
|
-
const pattern = `%${searchTerm}%`;
|
|
35
|
+
const pattern = `%${escapeLikePattern(searchTerm)}%`;
|
|
33
36
|
const query = db.query(sql);
|
|
34
37
|
if (project) {
|
|
35
38
|
return query.all(project, pattern, pattern, pattern);
|
|
@@ -39,10 +42,71 @@ function searchObservations(db, searchTerm, project) {
|
|
|
39
42
|
function deleteObservation(db, id) {
|
|
40
43
|
db.run("DELETE FROM observations WHERE id = ?", [id]);
|
|
41
44
|
}
|
|
45
|
+
function updateLastAccessed(db, ids) {
|
|
46
|
+
if (!Array.isArray(ids) || ids.length === 0) return;
|
|
47
|
+
const validIds = ids.filter((id) => typeof id === "number" && Number.isInteger(id) && id > 0).slice(0, 500);
|
|
48
|
+
if (validIds.length === 0) return;
|
|
49
|
+
const now = Date.now();
|
|
50
|
+
const placeholders = validIds.map(() => "?").join(",");
|
|
51
|
+
db.run(
|
|
52
|
+
`UPDATE observations SET last_accessed_epoch = ? WHERE id IN (${placeholders})`,
|
|
53
|
+
[now, ...validIds]
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
function consolidateObservations(db, project, options = {}) {
|
|
57
|
+
const minGroupSize = options.minGroupSize || 3;
|
|
58
|
+
const groups = db.query(`
|
|
59
|
+
SELECT type, files_modified, COUNT(*) as cnt, GROUP_CONCAT(id) as ids
|
|
60
|
+
FROM observations
|
|
61
|
+
WHERE project = ? AND files_modified IS NOT NULL AND files_modified != ''
|
|
62
|
+
GROUP BY type, files_modified
|
|
63
|
+
HAVING cnt >= ?
|
|
64
|
+
ORDER BY cnt DESC
|
|
65
|
+
`).all(project, minGroupSize);
|
|
66
|
+
if (groups.length === 0) return { merged: 0, removed: 0 };
|
|
67
|
+
let totalMerged = 0;
|
|
68
|
+
let totalRemoved = 0;
|
|
69
|
+
for (const group of groups) {
|
|
70
|
+
const obsIds = group.ids.split(",").map(Number);
|
|
71
|
+
const placeholders = obsIds.map(() => "?").join(",");
|
|
72
|
+
const observations = db.query(
|
|
73
|
+
`SELECT * FROM observations WHERE id IN (${placeholders}) ORDER BY created_at_epoch DESC`
|
|
74
|
+
).all(...obsIds);
|
|
75
|
+
if (observations.length < minGroupSize) continue;
|
|
76
|
+
if (options.dryRun) {
|
|
77
|
+
totalMerged += 1;
|
|
78
|
+
totalRemoved += observations.length - 1;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const keeper = observations[0];
|
|
82
|
+
const others = observations.slice(1);
|
|
83
|
+
const uniqueTexts = /* @__PURE__ */ new Set();
|
|
84
|
+
if (keeper.text) uniqueTexts.add(keeper.text);
|
|
85
|
+
for (const obs of others) {
|
|
86
|
+
if (obs.text && !uniqueTexts.has(obs.text)) {
|
|
87
|
+
uniqueTexts.add(obs.text);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const consolidatedText = Array.from(uniqueTexts).join("\n---\n").substring(0, 1e5);
|
|
91
|
+
db.run(
|
|
92
|
+
"UPDATE observations SET text = ?, title = ? WHERE id = ?",
|
|
93
|
+
[consolidatedText, `[consolidato x${observations.length}] ${keeper.title}`, keeper.id]
|
|
94
|
+
);
|
|
95
|
+
const removeIds = others.map((o) => o.id);
|
|
96
|
+
const removePlaceholders = removeIds.map(() => "?").join(",");
|
|
97
|
+
db.run(`DELETE FROM observations WHERE id IN (${removePlaceholders})`, removeIds);
|
|
98
|
+
db.run(`DELETE FROM observation_embeddings WHERE observation_id IN (${removePlaceholders})`, removeIds);
|
|
99
|
+
totalMerged += 1;
|
|
100
|
+
totalRemoved += removeIds.length;
|
|
101
|
+
}
|
|
102
|
+
return { merged: totalMerged, removed: totalRemoved };
|
|
103
|
+
}
|
|
42
104
|
export {
|
|
105
|
+
consolidateObservations,
|
|
43
106
|
createObservation,
|
|
44
107
|
deleteObservation,
|
|
45
108
|
getObservationsByProject,
|
|
46
109
|
getObservationsBySession,
|
|
47
|
-
searchObservations
|
|
110
|
+
searchObservations,
|
|
111
|
+
updateLastAccessed
|
|
48
112
|
};
|
|
@@ -1,15 +1,26 @@
|
|
|
1
1
|
import { createRequire } from 'module';const require = createRequire(import.meta.url);
|
|
2
2
|
|
|
3
3
|
// src/services/sqlite/Search.ts
|
|
4
|
+
import { existsSync, statSync } from "fs";
|
|
5
|
+
function escapeLikePattern(input) {
|
|
6
|
+
return input.replace(/[%_\\]/g, "\\$&");
|
|
7
|
+
}
|
|
8
|
+
function sanitizeFTS5Query(query) {
|
|
9
|
+
const trimmed = query.length > 1e4 ? query.substring(0, 1e4) : query;
|
|
10
|
+
const terms = trimmed.replace(/[""]/g, "").split(/\s+/).filter((t) => t.length > 0).slice(0, 100).map((t) => `"${t}"`);
|
|
11
|
+
return terms.join(" ");
|
|
12
|
+
}
|
|
4
13
|
function searchObservationsFTS(db, query, filters = {}) {
|
|
5
14
|
const limit = filters.limit || 50;
|
|
6
15
|
try {
|
|
16
|
+
const safeQuery = sanitizeFTS5Query(query);
|
|
17
|
+
if (!safeQuery) return searchObservationsLIKE(db, query, filters);
|
|
7
18
|
let sql = `
|
|
8
19
|
SELECT o.* FROM observations o
|
|
9
20
|
JOIN observations_fts fts ON o.id = fts.rowid
|
|
10
21
|
WHERE observations_fts MATCH ?
|
|
11
22
|
`;
|
|
12
|
-
const params = [
|
|
23
|
+
const params = [safeQuery];
|
|
13
24
|
if (filters.project) {
|
|
14
25
|
sql += " AND o.project = ?";
|
|
15
26
|
params.push(filters.project);
|
|
@@ -34,12 +45,47 @@ function searchObservationsFTS(db, query, filters = {}) {
|
|
|
34
45
|
return searchObservationsLIKE(db, query, filters);
|
|
35
46
|
}
|
|
36
47
|
}
|
|
48
|
+
function searchObservationsFTSWithRank(db, query, filters = {}) {
|
|
49
|
+
const limit = filters.limit || 50;
|
|
50
|
+
try {
|
|
51
|
+
const safeQuery = sanitizeFTS5Query(query);
|
|
52
|
+
if (!safeQuery) return [];
|
|
53
|
+
let sql = `
|
|
54
|
+
SELECT o.*, rank as fts5_rank FROM observations o
|
|
55
|
+
JOIN observations_fts fts ON o.id = fts.rowid
|
|
56
|
+
WHERE observations_fts MATCH ?
|
|
57
|
+
`;
|
|
58
|
+
const params = [safeQuery];
|
|
59
|
+
if (filters.project) {
|
|
60
|
+
sql += " AND o.project = ?";
|
|
61
|
+
params.push(filters.project);
|
|
62
|
+
}
|
|
63
|
+
if (filters.type) {
|
|
64
|
+
sql += " AND o.type = ?";
|
|
65
|
+
params.push(filters.type);
|
|
66
|
+
}
|
|
67
|
+
if (filters.dateStart) {
|
|
68
|
+
sql += " AND o.created_at_epoch >= ?";
|
|
69
|
+
params.push(filters.dateStart);
|
|
70
|
+
}
|
|
71
|
+
if (filters.dateEnd) {
|
|
72
|
+
sql += " AND o.created_at_epoch <= ?";
|
|
73
|
+
params.push(filters.dateEnd);
|
|
74
|
+
}
|
|
75
|
+
sql += " ORDER BY rank LIMIT ?";
|
|
76
|
+
params.push(limit);
|
|
77
|
+
const stmt = db.query(sql);
|
|
78
|
+
return stmt.all(...params);
|
|
79
|
+
} catch {
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
}
|
|
37
83
|
function searchObservationsLIKE(db, query, filters = {}) {
|
|
38
84
|
const limit = filters.limit || 50;
|
|
39
|
-
const pattern = `%${query}%`;
|
|
85
|
+
const pattern = `%${escapeLikePattern(query)}%`;
|
|
40
86
|
let sql = `
|
|
41
87
|
SELECT * FROM observations
|
|
42
|
-
WHERE (title LIKE ? OR text LIKE ? OR narrative LIKE ? OR concepts LIKE ?)
|
|
88
|
+
WHERE (title LIKE ? ESCAPE '\\' OR text LIKE ? ESCAPE '\\' OR narrative LIKE ? ESCAPE '\\' OR concepts LIKE ? ESCAPE '\\')
|
|
43
89
|
`;
|
|
44
90
|
const params = [pattern, pattern, pattern, pattern];
|
|
45
91
|
if (filters.project) {
|
|
@@ -65,10 +111,10 @@ function searchObservationsLIKE(db, query, filters = {}) {
|
|
|
65
111
|
}
|
|
66
112
|
function searchSummariesFiltered(db, query, filters = {}) {
|
|
67
113
|
const limit = filters.limit || 20;
|
|
68
|
-
const pattern = `%${query}%`;
|
|
114
|
+
const pattern = `%${escapeLikePattern(query)}%`;
|
|
69
115
|
let sql = `
|
|
70
116
|
SELECT * FROM summaries
|
|
71
|
-
WHERE (request LIKE ? OR learned LIKE ? OR completed LIKE ? OR notes LIKE ? OR next_steps LIKE ?)
|
|
117
|
+
WHERE (request LIKE ? ESCAPE '\\' OR learned LIKE ? ESCAPE '\\' OR completed LIKE ? ESCAPE '\\' OR notes LIKE ? ESCAPE '\\' OR next_steps LIKE ? ESCAPE '\\')
|
|
72
118
|
`;
|
|
73
119
|
const params = [pattern, pattern, pattern, pattern, pattern];
|
|
74
120
|
if (filters.project) {
|
|
@@ -89,11 +135,13 @@ function searchSummariesFiltered(db, query, filters = {}) {
|
|
|
89
135
|
return stmt.all(...params);
|
|
90
136
|
}
|
|
91
137
|
function getObservationsByIds(db, ids) {
|
|
92
|
-
if (ids.length === 0) return [];
|
|
93
|
-
const
|
|
138
|
+
if (!Array.isArray(ids) || ids.length === 0) return [];
|
|
139
|
+
const validIds = ids.filter((id) => typeof id === "number" && Number.isInteger(id) && id > 0).slice(0, 500);
|
|
140
|
+
if (validIds.length === 0) return [];
|
|
141
|
+
const placeholders = validIds.map(() => "?").join(",");
|
|
94
142
|
const sql = `SELECT * FROM observations WHERE id IN (${placeholders}) ORDER BY created_at_epoch DESC`;
|
|
95
143
|
const stmt = db.query(sql);
|
|
96
|
-
return stmt.all(...
|
|
144
|
+
return stmt.all(...validIds);
|
|
97
145
|
}
|
|
98
146
|
function getTimeline(db, anchorId, depthBefore = 5, depthAfter = 5) {
|
|
99
147
|
const anchorStmt = db.query("SELECT created_at_epoch FROM observations WHERE id = ?");
|
|
@@ -135,11 +183,53 @@ function getProjectStats(db, project) {
|
|
|
135
183
|
prompts: prmStmt.get(project)?.count || 0
|
|
136
184
|
};
|
|
137
185
|
}
|
|
186
|
+
function getStaleObservations(db, project) {
|
|
187
|
+
const rows = db.query(`
|
|
188
|
+
SELECT * FROM observations
|
|
189
|
+
WHERE project = ? AND files_modified IS NOT NULL AND files_modified != ''
|
|
190
|
+
ORDER BY created_at_epoch DESC
|
|
191
|
+
LIMIT 500
|
|
192
|
+
`).all(project);
|
|
193
|
+
const staleObs = [];
|
|
194
|
+
for (const obs of rows) {
|
|
195
|
+
if (!obs.files_modified) continue;
|
|
196
|
+
const files = obs.files_modified.split(",").map((f) => f.trim()).filter(Boolean);
|
|
197
|
+
let isStale = false;
|
|
198
|
+
for (const filepath of files) {
|
|
199
|
+
try {
|
|
200
|
+
if (!existsSync(filepath)) continue;
|
|
201
|
+
const stat = statSync(filepath);
|
|
202
|
+
if (stat.mtimeMs > obs.created_at_epoch) {
|
|
203
|
+
isStale = true;
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
} catch {
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (isStale) {
|
|
210
|
+
staleObs.push(obs);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return staleObs;
|
|
214
|
+
}
|
|
215
|
+
function markObservationsStale(db, ids, stale) {
|
|
216
|
+
if (!Array.isArray(ids) || ids.length === 0) return;
|
|
217
|
+
const validIds = ids.filter((id) => typeof id === "number" && Number.isInteger(id) && id > 0).slice(0, 500);
|
|
218
|
+
if (validIds.length === 0) return;
|
|
219
|
+
const placeholders = validIds.map(() => "?").join(",");
|
|
220
|
+
db.run(
|
|
221
|
+
`UPDATE observations SET is_stale = ? WHERE id IN (${placeholders})`,
|
|
222
|
+
[stale ? 1 : 0, ...validIds]
|
|
223
|
+
);
|
|
224
|
+
}
|
|
138
225
|
export {
|
|
139
226
|
getObservationsByIds,
|
|
140
227
|
getProjectStats,
|
|
228
|
+
getStaleObservations,
|
|
141
229
|
getTimeline,
|
|
230
|
+
markObservationsStale,
|
|
142
231
|
searchObservationsFTS,
|
|
232
|
+
searchObservationsFTSWithRank,
|
|
143
233
|
searchObservationsLIKE,
|
|
144
234
|
searchSummariesFiltered
|
|
145
235
|
};
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { createRequire } from 'module';const require = createRequire(import.meta.url);
|
|
2
2
|
|
|
3
3
|
// src/services/sqlite/Summaries.ts
|
|
4
|
+
function escapeLikePattern(input) {
|
|
5
|
+
return input.replace(/[%_\\]/g, "\\$&");
|
|
6
|
+
}
|
|
4
7
|
function createSummary(db, sessionId, project, request, investigated, learned, completed, nextSteps, notes) {
|
|
5
8
|
const now = /* @__PURE__ */ new Date();
|
|
6
9
|
const result = db.run(
|
|
@@ -22,12 +25,12 @@ function getSummariesByProject(db, project, limit = 50) {
|
|
|
22
25
|
return query.all(project, limit);
|
|
23
26
|
}
|
|
24
27
|
function searchSummaries(db, searchTerm, project) {
|
|
25
|
-
const sql = project ? `SELECT * FROM summaries
|
|
26
|
-
WHERE project = ? AND (request LIKE ? OR learned LIKE ? OR completed LIKE ? OR notes LIKE ?)
|
|
27
|
-
ORDER BY created_at_epoch DESC` : `SELECT * FROM summaries
|
|
28
|
-
WHERE request LIKE ? OR learned LIKE ? OR completed LIKE ? OR notes LIKE ?
|
|
28
|
+
const sql = project ? `SELECT * FROM summaries
|
|
29
|
+
WHERE project = ? AND (request LIKE ? ESCAPE '\\' OR learned LIKE ? ESCAPE '\\' OR completed LIKE ? ESCAPE '\\' OR notes LIKE ? ESCAPE '\\')
|
|
30
|
+
ORDER BY created_at_epoch DESC` : `SELECT * FROM summaries
|
|
31
|
+
WHERE request LIKE ? ESCAPE '\\' OR learned LIKE ? ESCAPE '\\' OR completed LIKE ? ESCAPE '\\' OR notes LIKE ? ESCAPE '\\'
|
|
29
32
|
ORDER BY created_at_epoch DESC`;
|
|
30
|
-
const pattern = `%${searchTerm}%`;
|
|
33
|
+
const pattern = `%${escapeLikePattern(searchTerm)}%`;
|
|
31
34
|
const query = db.query(sql);
|
|
32
35
|
if (project) {
|
|
33
36
|
return query.all(project, pattern, pattern, pattern, pattern);
|