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.
@@ -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 DATA_DIR = process.env.KIRO_MEMORY_DATA_DIR || process.env.CONTEXTKIT_DATA_DIR || join2(homedir2(), ".contextkit");
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 DB_PATH = join2(DATA_DIR, "contextkit.db");
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
- constructor(dbPath = DB_PATH) {
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
- const migrationRunner = new MigrationRunner(this.db);
339
- migrationRunner.runAllMigrations();
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 = [query];
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 placeholders = ids.map(() => "?").join(",");
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(...ids);
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);