kiro-memory 1.6.0 → 1.7.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.
@@ -636,6 +636,55 @@ var MigrationRunner = class {
636
636
  `);
637
637
  db.run("CREATE UNIQUE INDEX IF NOT EXISTS idx_project_aliases_name ON project_aliases(project_name)");
638
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
+ }
639
688
  }
640
689
  ];
641
690
  }
@@ -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,8 +1,13 @@
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
+ }
4
8
  function sanitizeFTS5Query(query) {
5
- const terms = query.replace(/[""]/g, "").split(/\s+/).filter((t) => t.length > 0).map((t) => `"${t}"`);
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}"`);
6
11
  return terms.join(" ");
7
12
  }
8
13
  function searchObservationsFTS(db, query, filters = {}) {
@@ -40,12 +45,47 @@ function searchObservationsFTS(db, query, filters = {}) {
40
45
  return searchObservationsLIKE(db, query, filters);
41
46
  }
42
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
+ }
43
83
  function searchObservationsLIKE(db, query, filters = {}) {
44
84
  const limit = filters.limit || 50;
45
- const pattern = `%${query}%`;
85
+ const pattern = `%${escapeLikePattern(query)}%`;
46
86
  let sql = `
47
87
  SELECT * FROM observations
48
- 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 '\\')
49
89
  `;
50
90
  const params = [pattern, pattern, pattern, pattern];
51
91
  if (filters.project) {
@@ -71,10 +111,10 @@ function searchObservationsLIKE(db, query, filters = {}) {
71
111
  }
72
112
  function searchSummariesFiltered(db, query, filters = {}) {
73
113
  const limit = filters.limit || 20;
74
- const pattern = `%${query}%`;
114
+ const pattern = `%${escapeLikePattern(query)}%`;
75
115
  let sql = `
76
116
  SELECT * FROM summaries
77
- 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 '\\')
78
118
  `;
79
119
  const params = [pattern, pattern, pattern, pattern, pattern];
80
120
  if (filters.project) {
@@ -95,11 +135,13 @@ function searchSummariesFiltered(db, query, filters = {}) {
95
135
  return stmt.all(...params);
96
136
  }
97
137
  function getObservationsByIds(db, ids) {
98
- if (ids.length === 0) return [];
99
- 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(",");
100
142
  const sql = `SELECT * FROM observations WHERE id IN (${placeholders}) ORDER BY created_at_epoch DESC`;
101
143
  const stmt = db.query(sql);
102
- return stmt.all(...ids);
144
+ return stmt.all(...validIds);
103
145
  }
104
146
  function getTimeline(db, anchorId, depthBefore = 5, depthAfter = 5) {
105
147
  const anchorStmt = db.query("SELECT created_at_epoch FROM observations WHERE id = ?");
@@ -141,11 +183,53 @@ function getProjectStats(db, project) {
141
183
  prompts: prmStmt.get(project)?.count || 0
142
184
  };
143
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
+ }
144
225
  export {
145
226
  getObservationsByIds,
146
227
  getProjectStats,
228
+ getStaleObservations,
147
229
  getTimeline,
230
+ markObservationsStale,
148
231
  searchObservationsFTS,
232
+ searchObservationsFTSWithRank,
149
233
  searchObservationsLIKE,
150
234
  searchSummariesFiltered
151
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);