kiro-memory 1.6.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.
@@ -1,15 +1,429 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from 'module';const require = createRequire(import.meta.url);
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
5
  var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
4
6
  get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
5
7
  }) : x)(function(x) {
6
8
  if (typeof require !== "undefined") return require.apply(this, arguments);
7
9
  throw Error('Dynamic require of "' + x + '" is not supported');
8
10
  });
11
+ var __esm = (fn, res) => function __init() {
12
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
13
+ };
14
+ var __export = (target, all) => {
15
+ for (var name in all)
16
+ __defProp(target, name, { get: all[name], enumerable: true });
17
+ };
18
+
19
+ // src/services/sqlite/Observations.ts
20
+ var Observations_exports = {};
21
+ __export(Observations_exports, {
22
+ consolidateObservations: () => consolidateObservations,
23
+ createObservation: () => createObservation,
24
+ deleteObservation: () => deleteObservation,
25
+ getObservationsByProject: () => getObservationsByProject,
26
+ getObservationsBySession: () => getObservationsBySession,
27
+ searchObservations: () => searchObservations,
28
+ updateLastAccessed: () => updateLastAccessed
29
+ });
30
+ function escapeLikePattern(input) {
31
+ return input.replace(/[%_\\]/g, "\\$&");
32
+ }
33
+ function createObservation(db, memorySessionId, project, type, title, subtitle, text, narrative, facts, concepts, filesRead, filesModified, promptNumber) {
34
+ const now = /* @__PURE__ */ new Date();
35
+ const result = db.run(
36
+ `INSERT INTO observations
37
+ (memory_session_id, project, type, title, subtitle, text, narrative, facts, concepts, files_read, files_modified, prompt_number, created_at, created_at_epoch)
38
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
39
+ [memorySessionId, project, type, title, subtitle, text, narrative, facts, concepts, filesRead, filesModified, promptNumber, now.toISOString(), now.getTime()]
40
+ );
41
+ return Number(result.lastInsertRowid);
42
+ }
43
+ function getObservationsBySession(db, memorySessionId) {
44
+ const query = db.query(
45
+ "SELECT * FROM observations WHERE memory_session_id = ? ORDER BY prompt_number ASC"
46
+ );
47
+ return query.all(memorySessionId);
48
+ }
49
+ function getObservationsByProject(db, project, limit = 100) {
50
+ const query = db.query(
51
+ "SELECT * FROM observations WHERE project = ? ORDER BY created_at_epoch DESC LIMIT ?"
52
+ );
53
+ return query.all(project, limit);
54
+ }
55
+ function searchObservations(db, searchTerm, project) {
56
+ const sql = project ? `SELECT * FROM observations
57
+ WHERE project = ? AND (title LIKE ? ESCAPE '\\' OR text LIKE ? ESCAPE '\\' OR narrative LIKE ? ESCAPE '\\')
58
+ ORDER BY created_at_epoch DESC` : `SELECT * FROM observations
59
+ WHERE title LIKE ? ESCAPE '\\' OR text LIKE ? ESCAPE '\\' OR narrative LIKE ? ESCAPE '\\'
60
+ ORDER BY created_at_epoch DESC`;
61
+ const pattern = `%${escapeLikePattern(searchTerm)}%`;
62
+ const query = db.query(sql);
63
+ if (project) {
64
+ return query.all(project, pattern, pattern, pattern);
65
+ }
66
+ return query.all(pattern, pattern, pattern);
67
+ }
68
+ function deleteObservation(db, id) {
69
+ db.run("DELETE FROM observations WHERE id = ?", [id]);
70
+ }
71
+ function updateLastAccessed(db, ids) {
72
+ if (!Array.isArray(ids) || ids.length === 0) return;
73
+ const validIds = ids.filter((id) => typeof id === "number" && Number.isInteger(id) && id > 0).slice(0, 500);
74
+ if (validIds.length === 0) return;
75
+ const now = Date.now();
76
+ const placeholders = validIds.map(() => "?").join(",");
77
+ db.run(
78
+ `UPDATE observations SET last_accessed_epoch = ? WHERE id IN (${placeholders})`,
79
+ [now, ...validIds]
80
+ );
81
+ }
82
+ function consolidateObservations(db, project, options = {}) {
83
+ const minGroupSize = options.minGroupSize || 3;
84
+ const groups = db.query(`
85
+ SELECT type, files_modified, COUNT(*) as cnt, GROUP_CONCAT(id) as ids
86
+ FROM observations
87
+ WHERE project = ? AND files_modified IS NOT NULL AND files_modified != ''
88
+ GROUP BY type, files_modified
89
+ HAVING cnt >= ?
90
+ ORDER BY cnt DESC
91
+ `).all(project, minGroupSize);
92
+ if (groups.length === 0) return { merged: 0, removed: 0 };
93
+ let totalMerged = 0;
94
+ let totalRemoved = 0;
95
+ for (const group of groups) {
96
+ const obsIds = group.ids.split(",").map(Number);
97
+ const placeholders = obsIds.map(() => "?").join(",");
98
+ const observations = db.query(
99
+ `SELECT * FROM observations WHERE id IN (${placeholders}) ORDER BY created_at_epoch DESC`
100
+ ).all(...obsIds);
101
+ if (observations.length < minGroupSize) continue;
102
+ if (options.dryRun) {
103
+ totalMerged += 1;
104
+ totalRemoved += observations.length - 1;
105
+ continue;
106
+ }
107
+ const keeper = observations[0];
108
+ const others = observations.slice(1);
109
+ const uniqueTexts = /* @__PURE__ */ new Set();
110
+ if (keeper.text) uniqueTexts.add(keeper.text);
111
+ for (const obs of others) {
112
+ if (obs.text && !uniqueTexts.has(obs.text)) {
113
+ uniqueTexts.add(obs.text);
114
+ }
115
+ }
116
+ const consolidatedText = Array.from(uniqueTexts).join("\n---\n").substring(0, 1e5);
117
+ db.run(
118
+ "UPDATE observations SET text = ?, title = ? WHERE id = ?",
119
+ [consolidatedText, `[consolidato x${observations.length}] ${keeper.title}`, keeper.id]
120
+ );
121
+ const removeIds = others.map((o) => o.id);
122
+ const removePlaceholders = removeIds.map(() => "?").join(",");
123
+ db.run(`DELETE FROM observations WHERE id IN (${removePlaceholders})`, removeIds);
124
+ db.run(`DELETE FROM observation_embeddings WHERE observation_id IN (${removePlaceholders})`, removeIds);
125
+ totalMerged += 1;
126
+ totalRemoved += removeIds.length;
127
+ }
128
+ return { merged: totalMerged, removed: totalRemoved };
129
+ }
130
+ var init_Observations = __esm({
131
+ "src/services/sqlite/Observations.ts"() {
132
+ "use strict";
133
+ }
134
+ });
135
+
136
+ // src/services/sqlite/Search.ts
137
+ var Search_exports = {};
138
+ __export(Search_exports, {
139
+ getObservationsByIds: () => getObservationsByIds,
140
+ getProjectStats: () => getProjectStats,
141
+ getStaleObservations: () => getStaleObservations,
142
+ getTimeline: () => getTimeline,
143
+ markObservationsStale: () => markObservationsStale,
144
+ searchObservationsFTS: () => searchObservationsFTS,
145
+ searchObservationsFTSWithRank: () => searchObservationsFTSWithRank,
146
+ searchObservationsLIKE: () => searchObservationsLIKE,
147
+ searchSummariesFiltered: () => searchSummariesFiltered
148
+ });
149
+ import { existsSync as existsSync4, statSync } from "fs";
150
+ function escapeLikePattern3(input) {
151
+ return input.replace(/[%_\\]/g, "\\$&");
152
+ }
153
+ function sanitizeFTS5Query(query) {
154
+ const trimmed = query.length > 1e4 ? query.substring(0, 1e4) : query;
155
+ const terms = trimmed.replace(/[""]/g, "").split(/\s+/).filter((t) => t.length > 0).slice(0, 100).map((t) => `"${t}"`);
156
+ return terms.join(" ");
157
+ }
158
+ function searchObservationsFTS(db, query, filters = {}) {
159
+ const limit = filters.limit || 50;
160
+ try {
161
+ const safeQuery = sanitizeFTS5Query(query);
162
+ if (!safeQuery) return searchObservationsLIKE(db, query, filters);
163
+ let sql = `
164
+ SELECT o.* FROM observations o
165
+ JOIN observations_fts fts ON o.id = fts.rowid
166
+ WHERE observations_fts MATCH ?
167
+ `;
168
+ const params = [safeQuery];
169
+ if (filters.project) {
170
+ sql += " AND o.project = ?";
171
+ params.push(filters.project);
172
+ }
173
+ if (filters.type) {
174
+ sql += " AND o.type = ?";
175
+ params.push(filters.type);
176
+ }
177
+ if (filters.dateStart) {
178
+ sql += " AND o.created_at_epoch >= ?";
179
+ params.push(filters.dateStart);
180
+ }
181
+ if (filters.dateEnd) {
182
+ sql += " AND o.created_at_epoch <= ?";
183
+ params.push(filters.dateEnd);
184
+ }
185
+ sql += " ORDER BY rank LIMIT ?";
186
+ params.push(limit);
187
+ const stmt = db.query(sql);
188
+ return stmt.all(...params);
189
+ } catch {
190
+ return searchObservationsLIKE(db, query, filters);
191
+ }
192
+ }
193
+ function searchObservationsFTSWithRank(db, query, filters = {}) {
194
+ const limit = filters.limit || 50;
195
+ try {
196
+ const safeQuery = sanitizeFTS5Query(query);
197
+ if (!safeQuery) return [];
198
+ let sql = `
199
+ SELECT o.*, rank as fts5_rank FROM observations o
200
+ JOIN observations_fts fts ON o.id = fts.rowid
201
+ WHERE observations_fts MATCH ?
202
+ `;
203
+ const params = [safeQuery];
204
+ if (filters.project) {
205
+ sql += " AND o.project = ?";
206
+ params.push(filters.project);
207
+ }
208
+ if (filters.type) {
209
+ sql += " AND o.type = ?";
210
+ params.push(filters.type);
211
+ }
212
+ if (filters.dateStart) {
213
+ sql += " AND o.created_at_epoch >= ?";
214
+ params.push(filters.dateStart);
215
+ }
216
+ if (filters.dateEnd) {
217
+ sql += " AND o.created_at_epoch <= ?";
218
+ params.push(filters.dateEnd);
219
+ }
220
+ sql += " ORDER BY rank LIMIT ?";
221
+ params.push(limit);
222
+ const stmt = db.query(sql);
223
+ return stmt.all(...params);
224
+ } catch {
225
+ return [];
226
+ }
227
+ }
228
+ function searchObservationsLIKE(db, query, filters = {}) {
229
+ const limit = filters.limit || 50;
230
+ const pattern = `%${escapeLikePattern3(query)}%`;
231
+ let sql = `
232
+ SELECT * FROM observations
233
+ WHERE (title LIKE ? ESCAPE '\\' OR text LIKE ? ESCAPE '\\' OR narrative LIKE ? ESCAPE '\\' OR concepts LIKE ? ESCAPE '\\')
234
+ `;
235
+ const params = [pattern, pattern, pattern, pattern];
236
+ if (filters.project) {
237
+ sql += " AND project = ?";
238
+ params.push(filters.project);
239
+ }
240
+ if (filters.type) {
241
+ sql += " AND type = ?";
242
+ params.push(filters.type);
243
+ }
244
+ if (filters.dateStart) {
245
+ sql += " AND created_at_epoch >= ?";
246
+ params.push(filters.dateStart);
247
+ }
248
+ if (filters.dateEnd) {
249
+ sql += " AND created_at_epoch <= ?";
250
+ params.push(filters.dateEnd);
251
+ }
252
+ sql += " ORDER BY created_at_epoch DESC LIMIT ?";
253
+ params.push(limit);
254
+ const stmt = db.query(sql);
255
+ return stmt.all(...params);
256
+ }
257
+ function searchSummariesFiltered(db, query, filters = {}) {
258
+ const limit = filters.limit || 20;
259
+ const pattern = `%${escapeLikePattern3(query)}%`;
260
+ let sql = `
261
+ SELECT * FROM summaries
262
+ WHERE (request LIKE ? ESCAPE '\\' OR learned LIKE ? ESCAPE '\\' OR completed LIKE ? ESCAPE '\\' OR notes LIKE ? ESCAPE '\\' OR next_steps LIKE ? ESCAPE '\\')
263
+ `;
264
+ const params = [pattern, pattern, pattern, pattern, pattern];
265
+ if (filters.project) {
266
+ sql += " AND project = ?";
267
+ params.push(filters.project);
268
+ }
269
+ if (filters.dateStart) {
270
+ sql += " AND created_at_epoch >= ?";
271
+ params.push(filters.dateStart);
272
+ }
273
+ if (filters.dateEnd) {
274
+ sql += " AND created_at_epoch <= ?";
275
+ params.push(filters.dateEnd);
276
+ }
277
+ sql += " ORDER BY created_at_epoch DESC LIMIT ?";
278
+ params.push(limit);
279
+ const stmt = db.query(sql);
280
+ return stmt.all(...params);
281
+ }
282
+ function getObservationsByIds(db, ids) {
283
+ if (!Array.isArray(ids) || ids.length === 0) return [];
284
+ const validIds = ids.filter((id) => typeof id === "number" && Number.isInteger(id) && id > 0).slice(0, 500);
285
+ if (validIds.length === 0) return [];
286
+ const placeholders = validIds.map(() => "?").join(",");
287
+ const sql = `SELECT * FROM observations WHERE id IN (${placeholders}) ORDER BY created_at_epoch DESC`;
288
+ const stmt = db.query(sql);
289
+ return stmt.all(...validIds);
290
+ }
291
+ function getTimeline(db, anchorId, depthBefore = 5, depthAfter = 5) {
292
+ const anchorStmt = db.query("SELECT created_at_epoch FROM observations WHERE id = ?");
293
+ const anchor = anchorStmt.get(anchorId);
294
+ if (!anchor) return [];
295
+ const anchorEpoch = anchor.created_at_epoch;
296
+ const beforeStmt = db.query(`
297
+ SELECT id, 'observation' as type, title, text as content, project, created_at, created_at_epoch
298
+ FROM observations
299
+ WHERE created_at_epoch < ?
300
+ ORDER BY created_at_epoch DESC
301
+ LIMIT ?
302
+ `);
303
+ const before = beforeStmt.all(anchorEpoch, depthBefore).reverse();
304
+ const selfStmt = db.query(`
305
+ SELECT id, 'observation' as type, title, text as content, project, created_at, created_at_epoch
306
+ FROM observations WHERE id = ?
307
+ `);
308
+ const self = selfStmt.all(anchorId);
309
+ const afterStmt = db.query(`
310
+ SELECT id, 'observation' as type, title, text as content, project, created_at, created_at_epoch
311
+ FROM observations
312
+ WHERE created_at_epoch > ?
313
+ ORDER BY created_at_epoch ASC
314
+ LIMIT ?
315
+ `);
316
+ const after = afterStmt.all(anchorEpoch, depthAfter);
317
+ return [...before, ...self, ...after];
318
+ }
319
+ function getProjectStats(db, project) {
320
+ const obsStmt = db.query("SELECT COUNT(*) as count FROM observations WHERE project = ?");
321
+ const sumStmt = db.query("SELECT COUNT(*) as count FROM summaries WHERE project = ?");
322
+ const sesStmt = db.query("SELECT COUNT(*) as count FROM sessions WHERE project = ?");
323
+ const prmStmt = db.query("SELECT COUNT(*) as count FROM prompts WHERE project = ?");
324
+ return {
325
+ observations: obsStmt.get(project)?.count || 0,
326
+ summaries: sumStmt.get(project)?.count || 0,
327
+ sessions: sesStmt.get(project)?.count || 0,
328
+ prompts: prmStmt.get(project)?.count || 0
329
+ };
330
+ }
331
+ function getStaleObservations(db, project) {
332
+ const rows = db.query(`
333
+ SELECT * FROM observations
334
+ WHERE project = ? AND files_modified IS NOT NULL AND files_modified != ''
335
+ ORDER BY created_at_epoch DESC
336
+ LIMIT 500
337
+ `).all(project);
338
+ const staleObs = [];
339
+ for (const obs of rows) {
340
+ if (!obs.files_modified) continue;
341
+ const files = obs.files_modified.split(",").map((f) => f.trim()).filter(Boolean);
342
+ let isStale = false;
343
+ for (const filepath of files) {
344
+ try {
345
+ if (!existsSync4(filepath)) continue;
346
+ const stat = statSync(filepath);
347
+ if (stat.mtimeMs > obs.created_at_epoch) {
348
+ isStale = true;
349
+ break;
350
+ }
351
+ } catch {
352
+ }
353
+ }
354
+ if (isStale) {
355
+ staleObs.push(obs);
356
+ }
357
+ }
358
+ return staleObs;
359
+ }
360
+ function markObservationsStale(db, ids, stale) {
361
+ if (!Array.isArray(ids) || ids.length === 0) return;
362
+ const validIds = ids.filter((id) => typeof id === "number" && Number.isInteger(id) && id > 0).slice(0, 500);
363
+ if (validIds.length === 0) return;
364
+ const placeholders = validIds.map(() => "?").join(",");
365
+ db.run(
366
+ `UPDATE observations SET is_stale = ? WHERE id IN (${placeholders})`,
367
+ [stale ? 1 : 0, ...validIds]
368
+ );
369
+ }
370
+ var init_Search = __esm({
371
+ "src/services/sqlite/Search.ts"() {
372
+ "use strict";
373
+ }
374
+ });
9
375
 
10
376
  // src/hooks/utils.ts
11
377
  import { writeFileSync, mkdirSync, existsSync, readFileSync } from "fs";
12
378
  import { join } from "path";
379
+
380
+ // src/services/search/ScoringEngine.ts
381
+ var SEARCH_WEIGHTS = {
382
+ semantic: 0.4,
383
+ fts5: 0.3,
384
+ recency: 0.2,
385
+ projectMatch: 0.1
386
+ };
387
+ var CONTEXT_WEIGHTS = {
388
+ semantic: 0,
389
+ fts5: 0,
390
+ recency: 0.7,
391
+ projectMatch: 0.3
392
+ };
393
+ function recencyScore(createdAtEpoch, halfLifeHours = 168) {
394
+ if (!createdAtEpoch || createdAtEpoch <= 0) return 0;
395
+ const nowMs = Date.now();
396
+ const ageMs = nowMs - createdAtEpoch;
397
+ if (ageMs <= 0) return 1;
398
+ const ageHours = ageMs / (1e3 * 60 * 60);
399
+ return Math.exp(-ageHours * Math.LN2 / halfLifeHours);
400
+ }
401
+ function normalizeFTS5Rank(rank, allRanks) {
402
+ if (allRanks.length === 0) return 0;
403
+ if (allRanks.length === 1) return 1;
404
+ const minRank = Math.min(...allRanks);
405
+ const maxRank = Math.max(...allRanks);
406
+ if (minRank === maxRank) return 1;
407
+ return (maxRank - rank) / (maxRank - minRank);
408
+ }
409
+ function projectMatchScore(itemProject, targetProject) {
410
+ if (!itemProject || !targetProject) return 0;
411
+ return itemProject.toLowerCase() === targetProject.toLowerCase() ? 1 : 0;
412
+ }
413
+ function computeCompositeScore(signals, weights) {
414
+ return signals.semantic * weights.semantic + signals.fts5 * weights.fts5 + signals.recency * weights.recency + signals.projectMatch * weights.projectMatch;
415
+ }
416
+ var KNOWLEDGE_TYPE_BOOST = {
417
+ constraint: 1.3,
418
+ decision: 1.25,
419
+ heuristic: 1.15,
420
+ rejected: 1.1
421
+ };
422
+ function knowledgeTypeBoost(type) {
423
+ return KNOWLEDGE_TYPE_BOOST[type] ?? 1;
424
+ }
425
+
426
+ // src/hooks/utils.ts
13
427
  var DATA_DIR = process.env.KIRO_MEMORY_DATA_DIR || process.env.CONTEXTKIT_DATA_DIR || join(process.env.HOME || "/tmp", ".kiro-memory");
14
428
  var TOKEN_FILE = join(DATA_DIR, "worker.token");
15
429
  function debugLog(hookName, label, data) {
@@ -30,10 +444,19 @@ async function readStdin() {
30
444
  return new Promise((resolve, reject) => {
31
445
  let data = "";
32
446
  process.stdin.setEncoding("utf8");
447
+ const safetyTimeout = setTimeout(() => {
448
+ if (!data.trim()) {
449
+ resolve({
450
+ hook_event_name: "agentSpawn",
451
+ cwd: process.cwd()
452
+ });
453
+ }
454
+ }, 5e3);
33
455
  process.stdin.on("data", (chunk) => {
34
456
  data += chunk;
35
457
  });
36
458
  process.stdin.on("end", () => {
459
+ clearTimeout(safetyTimeout);
37
460
  try {
38
461
  if (!data.trim()) {
39
462
  resolve({
@@ -47,15 +470,10 @@ async function readStdin() {
47
470
  reject(new Error(`Errore parsing stdin JSON: ${err}`));
48
471
  }
49
472
  });
50
- process.stdin.on("error", reject);
51
- setTimeout(() => {
52
- if (!data.trim()) {
53
- resolve({
54
- hook_event_name: "agentSpawn",
55
- cwd: process.cwd()
56
- });
57
- }
58
- }, 5e3);
473
+ process.stdin.on("error", (err) => {
474
+ clearTimeout(safetyTimeout);
475
+ reject(err);
476
+ });
59
477
  });
60
478
  }
61
479
  function detectProject(cwd) {
@@ -99,6 +517,12 @@ async function notifyWorker(event, data) {
99
517
  async function runHook(name, handler) {
100
518
  try {
101
519
  const input = await readStdin();
520
+ if (!input.cwd && input.workspace_roots?.[0]) {
521
+ input.cwd = input.workspace_roots[0];
522
+ }
523
+ if (!input.session_id && input.conversation_id) {
524
+ input.session_id = input.conversation_id;
525
+ }
102
526
  debugLog(name, "stdin", input);
103
527
  await handler(input);
104
528
  debugLog(name, "completato", { success: true });
@@ -638,18 +1062,67 @@ var MigrationRunner = class {
638
1062
  `);
639
1063
  db.run("CREATE UNIQUE INDEX IF NOT EXISTS idx_project_aliases_name ON project_aliases(project_name)");
640
1064
  }
641
- }
642
- ];
643
- }
644
- };
645
-
646
- // src/services/sqlite/Sessions.ts
647
- function createSession(db, contentSessionId, project, userPrompt) {
648
- const now = /* @__PURE__ */ new Date();
649
- const result = db.run(
650
- `INSERT INTO sessions (content_session_id, project, user_prompt, status, started_at, started_at_epoch)
651
- VALUES (?, ?, ?, 'active', ?, ?)`,
652
- [contentSessionId, project, userPrompt, now.toISOString(), now.getTime()]
1065
+ },
1066
+ {
1067
+ version: 4,
1068
+ up: (db) => {
1069
+ db.run(`
1070
+ CREATE TABLE IF NOT EXISTS observation_embeddings (
1071
+ observation_id INTEGER PRIMARY KEY,
1072
+ embedding BLOB NOT NULL,
1073
+ model TEXT NOT NULL,
1074
+ dimensions INTEGER NOT NULL,
1075
+ created_at TEXT NOT NULL,
1076
+ FOREIGN KEY (observation_id) REFERENCES observations(id) ON DELETE CASCADE
1077
+ )
1078
+ `);
1079
+ db.run("CREATE INDEX IF NOT EXISTS idx_embeddings_model ON observation_embeddings(model)");
1080
+ }
1081
+ },
1082
+ {
1083
+ version: 5,
1084
+ up: (db) => {
1085
+ db.run("ALTER TABLE observations ADD COLUMN last_accessed_epoch INTEGER");
1086
+ db.run("ALTER TABLE observations ADD COLUMN is_stale INTEGER DEFAULT 0");
1087
+ db.run("CREATE INDEX IF NOT EXISTS idx_observations_last_accessed ON observations(last_accessed_epoch)");
1088
+ db.run("CREATE INDEX IF NOT EXISTS idx_observations_stale ON observations(is_stale)");
1089
+ }
1090
+ },
1091
+ {
1092
+ version: 6,
1093
+ up: (db) => {
1094
+ db.run(`
1095
+ CREATE TABLE IF NOT EXISTS checkpoints (
1096
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1097
+ session_id INTEGER NOT NULL,
1098
+ project TEXT NOT NULL,
1099
+ task TEXT NOT NULL,
1100
+ progress TEXT,
1101
+ next_steps TEXT,
1102
+ open_questions TEXT,
1103
+ relevant_files TEXT,
1104
+ context_snapshot TEXT,
1105
+ created_at TEXT NOT NULL,
1106
+ created_at_epoch INTEGER NOT NULL,
1107
+ FOREIGN KEY (session_id) REFERENCES sessions(id)
1108
+ )
1109
+ `);
1110
+ db.run("CREATE INDEX IF NOT EXISTS idx_checkpoints_session ON checkpoints(session_id)");
1111
+ db.run("CREATE INDEX IF NOT EXISTS idx_checkpoints_project ON checkpoints(project)");
1112
+ db.run("CREATE INDEX IF NOT EXISTS idx_checkpoints_epoch ON checkpoints(created_at_epoch)");
1113
+ }
1114
+ }
1115
+ ];
1116
+ }
1117
+ };
1118
+
1119
+ // src/services/sqlite/Sessions.ts
1120
+ function createSession(db, contentSessionId, project, userPrompt) {
1121
+ const now = /* @__PURE__ */ new Date();
1122
+ const result = db.run(
1123
+ `INSERT INTO sessions (content_session_id, project, user_prompt, status, started_at, started_at_epoch)
1124
+ VALUES (?, ?, ?, 'active', ?, ?)`,
1125
+ [contentSessionId, project, userPrompt, now.toISOString(), now.getTime()]
653
1126
  );
654
1127
  return Number(result.lastInsertRowid);
655
1128
  }
@@ -667,38 +1140,13 @@ function completeSession(db, id) {
667
1140
  );
668
1141
  }
669
1142
 
670
- // src/services/sqlite/Observations.ts
671
- function createObservation(db, memorySessionId, project, type, title, subtitle, text, narrative, facts, concepts, filesRead, filesModified, promptNumber) {
672
- const now = /* @__PURE__ */ new Date();
673
- const result = db.run(
674
- `INSERT INTO observations
675
- (memory_session_id, project, type, title, subtitle, text, narrative, facts, concepts, files_read, files_modified, prompt_number, created_at, created_at_epoch)
676
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
677
- [memorySessionId, project, type, title, subtitle, text, narrative, facts, concepts, filesRead, filesModified, promptNumber, now.toISOString(), now.getTime()]
678
- );
679
- return Number(result.lastInsertRowid);
680
- }
681
- function getObservationsByProject(db, project, limit = 100) {
682
- const query = db.query(
683
- "SELECT * FROM observations WHERE project = ? ORDER BY created_at_epoch DESC LIMIT ?"
684
- );
685
- return query.all(project, limit);
686
- }
687
- function searchObservations(db, searchTerm, project) {
688
- const sql = project ? `SELECT * FROM observations
689
- WHERE project = ? AND (title LIKE ? OR text LIKE ? OR narrative LIKE ?)
690
- ORDER BY created_at_epoch DESC` : `SELECT * FROM observations
691
- WHERE title LIKE ? OR text LIKE ? OR narrative LIKE ?
692
- ORDER BY created_at_epoch DESC`;
693
- const pattern = `%${searchTerm}%`;
694
- const query = db.query(sql);
695
- if (project) {
696
- return query.all(project, pattern, pattern, pattern);
697
- }
698
- return query.all(pattern, pattern, pattern);
699
- }
1143
+ // src/services/sqlite/index.ts
1144
+ init_Observations();
700
1145
 
701
1146
  // src/services/sqlite/Summaries.ts
1147
+ function escapeLikePattern2(input) {
1148
+ return input.replace(/[%_\\]/g, "\\$&");
1149
+ }
702
1150
  function createSummary(db, sessionId, project, request, investigated, learned, completed, nextSteps, notes) {
703
1151
  const now = /* @__PURE__ */ new Date();
704
1152
  const result = db.run(
@@ -716,12 +1164,12 @@ function getSummariesByProject(db, project, limit = 50) {
716
1164
  return query.all(project, limit);
717
1165
  }
718
1166
  function searchSummaries(db, searchTerm, project) {
719
- const sql = project ? `SELECT * FROM summaries
720
- WHERE project = ? AND (request LIKE ? OR learned LIKE ? OR completed LIKE ? OR notes LIKE ?)
721
- ORDER BY created_at_epoch DESC` : `SELECT * FROM summaries
722
- WHERE request LIKE ? OR learned LIKE ? OR completed LIKE ? OR notes LIKE ?
1167
+ const sql = project ? `SELECT * FROM summaries
1168
+ WHERE project = ? AND (request LIKE ? ESCAPE '\\' OR learned LIKE ? ESCAPE '\\' OR completed LIKE ? ESCAPE '\\' OR notes LIKE ? ESCAPE '\\')
1169
+ ORDER BY created_at_epoch DESC` : `SELECT * FROM summaries
1170
+ WHERE request LIKE ? ESCAPE '\\' OR learned LIKE ? ESCAPE '\\' OR completed LIKE ? ESCAPE '\\' OR notes LIKE ? ESCAPE '\\'
723
1171
  ORDER BY created_at_epoch DESC`;
724
- const pattern = `%${searchTerm}%`;
1172
+ const pattern = `%${escapeLikePattern2(searchTerm)}%`;
725
1173
  const query = db.query(sql);
726
1174
  if (project) {
727
1175
  return query.all(project, pattern, pattern, pattern, pattern);
@@ -747,136 +1195,596 @@ function getPromptsByProject(db, project, limit = 100) {
747
1195
  return query.all(project, limit);
748
1196
  }
749
1197
 
750
- // src/services/sqlite/Search.ts
751
- function sanitizeFTS5Query(query) {
752
- const terms = query.replace(/[""]/g, "").split(/\s+/).filter((t) => t.length > 0).map((t) => `"${t}"`);
753
- return terms.join(" ");
1198
+ // src/services/sqlite/Checkpoints.ts
1199
+ function createCheckpoint(db, sessionId, project, data) {
1200
+ const now = /* @__PURE__ */ new Date();
1201
+ const result = db.run(
1202
+ `INSERT INTO checkpoints (session_id, project, task, progress, next_steps, open_questions, relevant_files, context_snapshot, created_at, created_at_epoch)
1203
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
1204
+ [
1205
+ sessionId,
1206
+ project,
1207
+ data.task,
1208
+ data.progress || null,
1209
+ data.nextSteps || null,
1210
+ data.openQuestions || null,
1211
+ data.relevantFiles || null,
1212
+ data.contextSnapshot || null,
1213
+ now.toISOString(),
1214
+ now.getTime()
1215
+ ]
1216
+ );
1217
+ return Number(result.lastInsertRowid);
754
1218
  }
755
- function searchObservationsFTS(db, query, filters = {}) {
756
- const limit = filters.limit || 50;
757
- try {
758
- const safeQuery = sanitizeFTS5Query(query);
759
- if (!safeQuery) return searchObservationsLIKE(db, query, filters);
760
- let sql = `
761
- SELECT o.* FROM observations o
762
- JOIN observations_fts fts ON o.id = fts.rowid
763
- WHERE observations_fts MATCH ?
764
- `;
765
- const params = [safeQuery];
766
- if (filters.project) {
767
- sql += " AND o.project = ?";
768
- params.push(filters.project);
1219
+ function getLatestCheckpoint(db, sessionId) {
1220
+ const query = db.query(
1221
+ "SELECT * FROM checkpoints WHERE session_id = ? ORDER BY created_at_epoch DESC LIMIT 1"
1222
+ );
1223
+ return query.get(sessionId);
1224
+ }
1225
+ function getLatestCheckpointByProject(db, project) {
1226
+ const query = db.query(
1227
+ "SELECT * FROM checkpoints WHERE project = ? ORDER BY created_at_epoch DESC LIMIT 1"
1228
+ );
1229
+ return query.get(project);
1230
+ }
1231
+
1232
+ // src/services/sqlite/Reports.ts
1233
+ function getReportData(db, project, startEpoch, endEpoch) {
1234
+ const startDate = new Date(startEpoch);
1235
+ const endDate = new Date(endEpoch);
1236
+ const days = Math.ceil((endEpoch - startEpoch) / (24 * 60 * 60 * 1e3));
1237
+ const label = days <= 7 ? "Weekly" : days <= 31 ? "Monthly" : "Custom";
1238
+ const countInRange = (table, epochCol = "created_at_epoch") => {
1239
+ const sql = project ? `SELECT COUNT(*) as count FROM ${table} WHERE project = ? AND ${epochCol} >= ? AND ${epochCol} <= ?` : `SELECT COUNT(*) as count FROM ${table} WHERE ${epochCol} >= ? AND ${epochCol} <= ?`;
1240
+ const stmt = db.query(sql);
1241
+ const row = project ? stmt.get(project, startEpoch, endEpoch) : stmt.get(startEpoch, endEpoch);
1242
+ return row?.count || 0;
1243
+ };
1244
+ const observations = countInRange("observations");
1245
+ const summaries = countInRange("summaries");
1246
+ const prompts = countInRange("prompts");
1247
+ const sessions = countInRange("sessions", "started_at_epoch");
1248
+ const timelineSql = project ? `SELECT DATE(datetime(created_at_epoch / 1000, 'unixepoch')) as day, COUNT(*) as count
1249
+ FROM observations
1250
+ WHERE project = ? AND created_at_epoch >= ? AND created_at_epoch <= ?
1251
+ GROUP BY day ORDER BY day ASC` : `SELECT DATE(datetime(created_at_epoch / 1000, 'unixepoch')) as day, COUNT(*) as count
1252
+ FROM observations
1253
+ WHERE created_at_epoch >= ? AND created_at_epoch <= ?
1254
+ GROUP BY day ORDER BY day ASC`;
1255
+ const timelineStmt = db.query(timelineSql);
1256
+ const timeline = project ? timelineStmt.all(project, startEpoch, endEpoch) : timelineStmt.all(startEpoch, endEpoch);
1257
+ const typeSql = project ? `SELECT type, COUNT(*) as count FROM observations
1258
+ WHERE project = ? AND created_at_epoch >= ? AND created_at_epoch <= ?
1259
+ GROUP BY type ORDER BY count DESC` : `SELECT type, COUNT(*) as count FROM observations
1260
+ WHERE created_at_epoch >= ? AND created_at_epoch <= ?
1261
+ GROUP BY type ORDER BY count DESC`;
1262
+ const typeStmt = db.query(typeSql);
1263
+ const typeDistribution = project ? typeStmt.all(project, startEpoch, endEpoch) : typeStmt.all(startEpoch, endEpoch);
1264
+ const sessionTotalSql = project ? `SELECT COUNT(*) as count FROM sessions WHERE project = ? AND started_at_epoch >= ? AND started_at_epoch <= ?` : `SELECT COUNT(*) as count FROM sessions WHERE started_at_epoch >= ? AND started_at_epoch <= ?`;
1265
+ const sessionTotal = (project ? db.query(sessionTotalSql).get(project, startEpoch, endEpoch)?.count : db.query(sessionTotalSql).get(startEpoch, endEpoch)?.count) || 0;
1266
+ const sessionCompletedSql = project ? `SELECT COUNT(*) as count FROM sessions WHERE project = ? AND started_at_epoch >= ? AND started_at_epoch <= ? AND status = 'completed'` : `SELECT COUNT(*) as count FROM sessions WHERE started_at_epoch >= ? AND started_at_epoch <= ? AND status = 'completed'`;
1267
+ const sessionCompleted = (project ? db.query(sessionCompletedSql).get(project, startEpoch, endEpoch)?.count : db.query(sessionCompletedSql).get(startEpoch, endEpoch)?.count) || 0;
1268
+ const sessionAvgSql = project ? `SELECT AVG((completed_at_epoch - started_at_epoch) / 1000.0 / 60.0) as avg_min
1269
+ FROM sessions
1270
+ WHERE project = ? AND started_at_epoch >= ? AND started_at_epoch <= ?
1271
+ AND status = 'completed' AND completed_at_epoch IS NOT NULL AND completed_at_epoch > started_at_epoch` : `SELECT AVG((completed_at_epoch - started_at_epoch) / 1000.0 / 60.0) as avg_min
1272
+ FROM sessions
1273
+ WHERE started_at_epoch >= ? AND started_at_epoch <= ?
1274
+ AND status = 'completed' AND completed_at_epoch IS NOT NULL AND completed_at_epoch > started_at_epoch`;
1275
+ const avgRow = project ? db.query(sessionAvgSql).get(project, startEpoch, endEpoch) : db.query(sessionAvgSql).get(startEpoch, endEpoch);
1276
+ const avgDurationMinutes = Math.round((avgRow?.avg_min || 0) * 10) / 10;
1277
+ const knowledgeSql = project ? `SELECT COUNT(*) as count FROM observations
1278
+ WHERE project = ? AND created_at_epoch >= ? AND created_at_epoch <= ?
1279
+ AND type IN ('constraint', 'decision', 'heuristic', 'rejected')` : `SELECT COUNT(*) as count FROM observations
1280
+ WHERE created_at_epoch >= ? AND created_at_epoch <= ?
1281
+ AND type IN ('constraint', 'decision', 'heuristic', 'rejected')`;
1282
+ const knowledgeCount = (project ? db.query(knowledgeSql).get(project, startEpoch, endEpoch)?.count : db.query(knowledgeSql).get(startEpoch, endEpoch)?.count) || 0;
1283
+ const staleSql = project ? `SELECT COUNT(*) as count FROM observations
1284
+ WHERE project = ? AND created_at_epoch >= ? AND created_at_epoch <= ? AND is_stale = 1` : `SELECT COUNT(*) as count FROM observations
1285
+ WHERE created_at_epoch >= ? AND created_at_epoch <= ? AND is_stale = 1`;
1286
+ const staleCount = (project ? db.query(staleSql).get(project, startEpoch, endEpoch)?.count : db.query(staleSql).get(startEpoch, endEpoch)?.count) || 0;
1287
+ const summarySql = project ? `SELECT learned, completed, next_steps FROM summaries
1288
+ WHERE project = ? AND created_at_epoch >= ? AND created_at_epoch <= ?
1289
+ ORDER BY created_at_epoch DESC` : `SELECT learned, completed, next_steps FROM summaries
1290
+ WHERE created_at_epoch >= ? AND created_at_epoch <= ?
1291
+ ORDER BY created_at_epoch DESC`;
1292
+ const summaryRows = project ? db.query(summarySql).all(project, startEpoch, endEpoch) : db.query(summarySql).all(startEpoch, endEpoch);
1293
+ const topLearnings = [];
1294
+ const completedTasks = [];
1295
+ const nextStepsArr = [];
1296
+ for (const row of summaryRows) {
1297
+ if (row.learned) {
1298
+ const parts = row.learned.split("; ").filter(Boolean);
1299
+ topLearnings.push(...parts);
769
1300
  }
770
- if (filters.type) {
771
- sql += " AND o.type = ?";
772
- params.push(filters.type);
1301
+ if (row.completed) {
1302
+ const parts = row.completed.split("; ").filter(Boolean);
1303
+ completedTasks.push(...parts);
773
1304
  }
774
- if (filters.dateStart) {
775
- sql += " AND o.created_at_epoch >= ?";
776
- params.push(filters.dateStart);
1305
+ if (row.next_steps) {
1306
+ const parts = row.next_steps.split("; ").filter(Boolean);
1307
+ nextStepsArr.push(...parts);
777
1308
  }
778
- if (filters.dateEnd) {
779
- sql += " AND o.created_at_epoch <= ?";
780
- params.push(filters.dateEnd);
1309
+ }
1310
+ const filesSql = project ? `SELECT files_modified FROM observations
1311
+ WHERE project = ? AND created_at_epoch >= ? AND created_at_epoch <= ?
1312
+ AND files_modified IS NOT NULL AND files_modified != ''` : `SELECT files_modified FROM observations
1313
+ WHERE created_at_epoch >= ? AND created_at_epoch <= ?
1314
+ AND files_modified IS NOT NULL AND files_modified != ''`;
1315
+ const fileRows = project ? db.query(filesSql).all(project, startEpoch, endEpoch) : db.query(filesSql).all(startEpoch, endEpoch);
1316
+ const fileCounts = /* @__PURE__ */ new Map();
1317
+ for (const row of fileRows) {
1318
+ const files = row.files_modified.split(",").map((f) => f.trim()).filter(Boolean);
1319
+ for (const file of files) {
1320
+ fileCounts.set(file, (fileCounts.get(file) || 0) + 1);
781
1321
  }
782
- sql += " ORDER BY rank LIMIT ?";
783
- params.push(limit);
784
- const stmt = db.query(sql);
785
- return stmt.all(...params);
786
- } catch {
787
- return searchObservationsLIKE(db, query, filters);
788
1322
  }
1323
+ const fileHotspots = Array.from(fileCounts.entries()).map(([file, count]) => ({ file, count })).sort((a, b) => b.count - a.count).slice(0, 15);
1324
+ return {
1325
+ period: {
1326
+ start: startDate.toISOString().split("T")[0],
1327
+ end: endDate.toISOString().split("T")[0],
1328
+ days,
1329
+ label
1330
+ },
1331
+ overview: {
1332
+ observations,
1333
+ summaries,
1334
+ sessions,
1335
+ prompts,
1336
+ knowledgeCount,
1337
+ staleCount
1338
+ },
1339
+ timeline,
1340
+ typeDistribution,
1341
+ sessionStats: {
1342
+ total: sessionTotal,
1343
+ completed: sessionCompleted,
1344
+ avgDurationMinutes
1345
+ },
1346
+ topLearnings: [...new Set(topLearnings)].slice(0, 10),
1347
+ completedTasks: [...new Set(completedTasks)].slice(0, 10),
1348
+ nextSteps: [...new Set(nextStepsArr)].slice(0, 10),
1349
+ fileHotspots
1350
+ };
789
1351
  }
790
- function searchObservationsLIKE(db, query, filters = {}) {
791
- const limit = filters.limit || 50;
792
- const pattern = `%${query}%`;
793
- let sql = `
794
- SELECT * FROM observations
795
- WHERE (title LIKE ? OR text LIKE ? OR narrative LIKE ? OR concepts LIKE ?)
796
- `;
797
- const params = [pattern, pattern, pattern, pattern];
798
- if (filters.project) {
799
- sql += " AND project = ?";
800
- params.push(filters.project);
1352
+
1353
+ // src/services/sqlite/index.ts
1354
+ init_Search();
1355
+
1356
+ // src/sdk/index.ts
1357
+ init_Observations();
1358
+ init_Search();
1359
+
1360
+ // src/services/search/EmbeddingService.ts
1361
+ var EmbeddingService = class {
1362
+ provider = null;
1363
+ model = null;
1364
+ initialized = false;
1365
+ initializing = null;
1366
+ /**
1367
+ * Inizializza il servizio di embedding.
1368
+ * Tenta fastembed, poi @huggingface/transformers, poi fallback a null.
1369
+ */
1370
+ async initialize() {
1371
+ if (this.initialized) return this.provider !== null;
1372
+ if (this.initializing) return this.initializing;
1373
+ this.initializing = this._doInitialize();
1374
+ const result = await this.initializing;
1375
+ this.initializing = null;
1376
+ return result;
801
1377
  }
802
- if (filters.type) {
803
- sql += " AND type = ?";
804
- params.push(filters.type);
1378
+ async _doInitialize() {
1379
+ try {
1380
+ const fastembed = await import("fastembed");
1381
+ const EmbeddingModel = fastembed.EmbeddingModel || fastembed.default?.EmbeddingModel;
1382
+ const FlagEmbedding = fastembed.FlagEmbedding || fastembed.default?.FlagEmbedding;
1383
+ if (FlagEmbedding && EmbeddingModel) {
1384
+ this.model = await FlagEmbedding.init({
1385
+ model: EmbeddingModel.BGESmallENV15
1386
+ });
1387
+ this.provider = "fastembed";
1388
+ this.initialized = true;
1389
+ logger.info("EMBEDDING", "Inizializzato con fastembed (BGE-small-en-v1.5)");
1390
+ return true;
1391
+ }
1392
+ } catch (error) {
1393
+ logger.debug("EMBEDDING", `fastembed non disponibile: ${error}`);
1394
+ }
1395
+ try {
1396
+ const transformers = await import("@huggingface/transformers");
1397
+ const pipeline = transformers.pipeline || transformers.default?.pipeline;
1398
+ if (pipeline) {
1399
+ this.model = await pipeline("feature-extraction", "Xenova/all-MiniLM-L6-v2", {
1400
+ quantized: true
1401
+ });
1402
+ this.provider = "transformers";
1403
+ this.initialized = true;
1404
+ logger.info("EMBEDDING", "Inizializzato con @huggingface/transformers (all-MiniLM-L6-v2)");
1405
+ return true;
1406
+ }
1407
+ } catch (error) {
1408
+ logger.debug("EMBEDDING", `@huggingface/transformers non disponibile: ${error}`);
1409
+ }
1410
+ this.provider = null;
1411
+ this.initialized = true;
1412
+ logger.warn("EMBEDDING", "Nessun provider embedding disponibile, ricerca semantica disabilitata");
1413
+ return false;
805
1414
  }
806
- if (filters.dateStart) {
807
- sql += " AND created_at_epoch >= ?";
808
- params.push(filters.dateStart);
1415
+ /**
1416
+ * Genera embedding per un singolo testo.
1417
+ * Ritorna Float32Array con 384 dimensioni, o null se non disponibile.
1418
+ */
1419
+ async embed(text) {
1420
+ if (!this.initialized) await this.initialize();
1421
+ if (!this.provider || !this.model) return null;
1422
+ try {
1423
+ const truncated = text.substring(0, 2e3);
1424
+ if (this.provider === "fastembed") {
1425
+ return await this._embedFastembed(truncated);
1426
+ } else if (this.provider === "transformers") {
1427
+ return await this._embedTransformers(truncated);
1428
+ }
1429
+ } catch (error) {
1430
+ logger.error("EMBEDDING", `Errore generazione embedding: ${error}`);
1431
+ }
1432
+ return null;
809
1433
  }
810
- if (filters.dateEnd) {
811
- sql += " AND created_at_epoch <= ?";
812
- params.push(filters.dateEnd);
1434
+ /**
1435
+ * Genera embeddings in batch.
1436
+ */
1437
+ async embedBatch(texts) {
1438
+ if (!this.initialized) await this.initialize();
1439
+ if (!this.provider || !this.model) return texts.map(() => null);
1440
+ const results = [];
1441
+ for (const text of texts) {
1442
+ try {
1443
+ const embedding = await this.embed(text);
1444
+ results.push(embedding);
1445
+ } catch {
1446
+ results.push(null);
1447
+ }
1448
+ }
1449
+ return results;
813
1450
  }
814
- sql += " ORDER BY created_at_epoch DESC LIMIT ?";
815
- params.push(limit);
816
- const stmt = db.query(sql);
817
- return stmt.all(...params);
818
- }
819
- function searchSummariesFiltered(db, query, filters = {}) {
820
- const limit = filters.limit || 20;
821
- const pattern = `%${query}%`;
822
- let sql = `
823
- SELECT * FROM summaries
824
- WHERE (request LIKE ? OR learned LIKE ? OR completed LIKE ? OR notes LIKE ? OR next_steps LIKE ?)
825
- `;
826
- const params = [pattern, pattern, pattern, pattern, pattern];
827
- if (filters.project) {
828
- sql += " AND project = ?";
829
- params.push(filters.project);
1451
+ /**
1452
+ * Verifica se il servizio è disponibile.
1453
+ */
1454
+ isAvailable() {
1455
+ return this.initialized && this.provider !== null;
830
1456
  }
831
- if (filters.dateStart) {
832
- sql += " AND created_at_epoch >= ?";
833
- params.push(filters.dateStart);
1457
+ /**
1458
+ * Nome del provider attivo.
1459
+ */
1460
+ getProvider() {
1461
+ return this.provider;
834
1462
  }
835
- if (filters.dateEnd) {
836
- sql += " AND created_at_epoch <= ?";
837
- params.push(filters.dateEnd);
1463
+ /**
1464
+ * Dimensioni del vettore embedding.
1465
+ */
1466
+ getDimensions() {
1467
+ return 384;
1468
+ }
1469
+ // --- Provider specifici ---
1470
+ async _embedFastembed(text) {
1471
+ const embeddings = this.model.embed([text], 1);
1472
+ for await (const batch of embeddings) {
1473
+ if (batch && batch.length > 0) {
1474
+ const vec = batch[0];
1475
+ return vec instanceof Float32Array ? vec : new Float32Array(vec);
1476
+ }
1477
+ }
1478
+ return null;
838
1479
  }
839
- sql += " ORDER BY created_at_epoch DESC LIMIT ?";
840
- params.push(limit);
841
- const stmt = db.query(sql);
842
- return stmt.all(...params);
1480
+ async _embedTransformers(text) {
1481
+ const output = await this.model(text, {
1482
+ pooling: "mean",
1483
+ normalize: true
1484
+ });
1485
+ if (output?.data) {
1486
+ return output.data instanceof Float32Array ? output.data : new Float32Array(output.data);
1487
+ }
1488
+ return null;
1489
+ }
1490
+ };
1491
+ var embeddingService = null;
1492
+ function getEmbeddingService() {
1493
+ if (!embeddingService) {
1494
+ embeddingService = new EmbeddingService();
1495
+ }
1496
+ return embeddingService;
843
1497
  }
844
- function getObservationsByIds(db, ids) {
845
- if (ids.length === 0) return [];
846
- const placeholders = ids.map(() => "?").join(",");
847
- const sql = `SELECT * FROM observations WHERE id IN (${placeholders}) ORDER BY created_at_epoch DESC`;
848
- const stmt = db.query(sql);
849
- return stmt.all(...ids);
1498
+
1499
+ // src/services/search/VectorSearch.ts
1500
+ function cosineSimilarity(a, b) {
1501
+ if (a.length !== b.length) return 0;
1502
+ let dotProduct = 0;
1503
+ let normA = 0;
1504
+ let normB = 0;
1505
+ for (let i = 0; i < a.length; i++) {
1506
+ dotProduct += a[i] * b[i];
1507
+ normA += a[i] * a[i];
1508
+ normB += b[i] * b[i];
1509
+ }
1510
+ const denominator = Math.sqrt(normA) * Math.sqrt(normB);
1511
+ if (denominator === 0) return 0;
1512
+ return dotProduct / denominator;
850
1513
  }
851
- function getTimeline(db, anchorId, depthBefore = 5, depthAfter = 5) {
852
- const anchorStmt = db.query("SELECT created_at_epoch FROM observations WHERE id = ?");
853
- const anchor = anchorStmt.get(anchorId);
854
- if (!anchor) return [];
855
- const anchorEpoch = anchor.created_at_epoch;
856
- const beforeStmt = db.query(`
857
- SELECT id, 'observation' as type, title, text as content, project, created_at, created_at_epoch
858
- FROM observations
859
- WHERE created_at_epoch < ?
860
- ORDER BY created_at_epoch DESC
861
- LIMIT ?
862
- `);
863
- const before = beforeStmt.all(anchorEpoch, depthBefore).reverse();
864
- const selfStmt = db.query(`
865
- SELECT id, 'observation' as type, title, text as content, project, created_at, created_at_epoch
866
- FROM observations WHERE id = ?
867
- `);
868
- const self = selfStmt.all(anchorId);
869
- const afterStmt = db.query(`
870
- SELECT id, 'observation' as type, title, text as content, project, created_at, created_at_epoch
871
- FROM observations
872
- WHERE created_at_epoch > ?
873
- ORDER BY created_at_epoch ASC
874
- LIMIT ?
875
- `);
876
- const after = afterStmt.all(anchorEpoch, depthAfter);
877
- return [...before, ...self, ...after];
1514
+ function float32ToBuffer(arr) {
1515
+ return Buffer.from(arr.buffer, arr.byteOffset, arr.byteLength);
1516
+ }
1517
+ function bufferToFloat32(buf) {
1518
+ const arrayBuffer = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
1519
+ return new Float32Array(arrayBuffer);
1520
+ }
1521
+ var VectorSearch = class {
1522
+ /**
1523
+ * Ricerca semantica: calcola cosine similarity tra query e tutti gli embeddings.
1524
+ */
1525
+ async search(db, queryEmbedding, options = {}) {
1526
+ const limit = options.limit || 10;
1527
+ const threshold = options.threshold || 0.3;
1528
+ try {
1529
+ let sql = `
1530
+ SELECT e.observation_id, e.embedding,
1531
+ o.title, o.text, o.type, o.project, o.created_at, o.created_at_epoch
1532
+ FROM observation_embeddings e
1533
+ JOIN observations o ON o.id = e.observation_id
1534
+ `;
1535
+ const params = [];
1536
+ if (options.project) {
1537
+ sql += " WHERE o.project = ?";
1538
+ params.push(options.project);
1539
+ }
1540
+ const rows = db.query(sql).all(...params);
1541
+ const scored = [];
1542
+ for (const row of rows) {
1543
+ const embedding = bufferToFloat32(row.embedding);
1544
+ const similarity = cosineSimilarity(queryEmbedding, embedding);
1545
+ if (similarity >= threshold) {
1546
+ scored.push({
1547
+ id: row.observation_id,
1548
+ observationId: row.observation_id,
1549
+ similarity,
1550
+ title: row.title,
1551
+ text: row.text,
1552
+ type: row.type,
1553
+ project: row.project,
1554
+ created_at: row.created_at,
1555
+ created_at_epoch: row.created_at_epoch
1556
+ });
1557
+ }
1558
+ }
1559
+ scored.sort((a, b) => b.similarity - a.similarity);
1560
+ return scored.slice(0, limit);
1561
+ } catch (error) {
1562
+ logger.error("VECTOR", `Errore ricerca vettoriale: ${error}`);
1563
+ return [];
1564
+ }
1565
+ }
1566
+ /**
1567
+ * Salva embedding per un'osservazione.
1568
+ */
1569
+ async storeEmbedding(db, observationId, embedding, model) {
1570
+ try {
1571
+ const blob = float32ToBuffer(embedding);
1572
+ db.query(`
1573
+ INSERT OR REPLACE INTO observation_embeddings
1574
+ (observation_id, embedding, model, dimensions, created_at)
1575
+ VALUES (?, ?, ?, ?, ?)
1576
+ `).run(
1577
+ observationId,
1578
+ blob,
1579
+ model,
1580
+ embedding.length,
1581
+ (/* @__PURE__ */ new Date()).toISOString()
1582
+ );
1583
+ logger.debug("VECTOR", `Embedding salvato per osservazione ${observationId}`);
1584
+ } catch (error) {
1585
+ logger.error("VECTOR", `Errore salvataggio embedding: ${error}`);
1586
+ }
1587
+ }
1588
+ /**
1589
+ * Genera embeddings per osservazioni che non li hanno ancora.
1590
+ */
1591
+ async backfillEmbeddings(db, batchSize = 50) {
1592
+ const embeddingService2 = getEmbeddingService();
1593
+ if (!await embeddingService2.initialize()) {
1594
+ logger.warn("VECTOR", "Embedding service non disponibile, backfill saltato");
1595
+ return 0;
1596
+ }
1597
+ const rows = db.query(`
1598
+ SELECT o.id, o.title, o.text, o.narrative, o.concepts
1599
+ FROM observations o
1600
+ LEFT JOIN observation_embeddings e ON e.observation_id = o.id
1601
+ WHERE e.observation_id IS NULL
1602
+ ORDER BY o.created_at_epoch DESC
1603
+ LIMIT ?
1604
+ `).all(batchSize);
1605
+ if (rows.length === 0) return 0;
1606
+ let count = 0;
1607
+ const model = embeddingService2.getProvider() || "unknown";
1608
+ for (const row of rows) {
1609
+ const parts = [row.title];
1610
+ if (row.text) parts.push(row.text);
1611
+ if (row.narrative) parts.push(row.narrative);
1612
+ if (row.concepts) parts.push(row.concepts);
1613
+ const fullText = parts.join(" ").substring(0, 2e3);
1614
+ const embedding = await embeddingService2.embed(fullText);
1615
+ if (embedding) {
1616
+ await this.storeEmbedding(db, row.id, embedding, model);
1617
+ count++;
1618
+ }
1619
+ }
1620
+ logger.info("VECTOR", `Backfill completato: ${count}/${rows.length} embeddings generati`);
1621
+ return count;
1622
+ }
1623
+ /**
1624
+ * Statistiche sugli embeddings.
1625
+ */
1626
+ getStats(db) {
1627
+ try {
1628
+ const totalRow = db.query("SELECT COUNT(*) as count FROM observations").get();
1629
+ const embeddedRow = db.query("SELECT COUNT(*) as count FROM observation_embeddings").get();
1630
+ const total = totalRow?.count || 0;
1631
+ const embedded = embeddedRow?.count || 0;
1632
+ const percentage = total > 0 ? Math.round(embedded / total * 100) : 0;
1633
+ return { total, embedded, percentage };
1634
+ } catch {
1635
+ return { total: 0, embedded: 0, percentage: 0 };
1636
+ }
1637
+ }
1638
+ };
1639
+ var vectorSearch = null;
1640
+ function getVectorSearch() {
1641
+ if (!vectorSearch) {
1642
+ vectorSearch = new VectorSearch();
1643
+ }
1644
+ return vectorSearch;
878
1645
  }
879
1646
 
1647
+ // src/services/search/HybridSearch.ts
1648
+ var HybridSearch = class {
1649
+ embeddingInitialized = false;
1650
+ /**
1651
+ * Inizializza il servizio di embedding (lazy, non bloccante)
1652
+ */
1653
+ async initialize() {
1654
+ try {
1655
+ const embeddingService2 = getEmbeddingService();
1656
+ await embeddingService2.initialize();
1657
+ this.embeddingInitialized = embeddingService2.isAvailable();
1658
+ logger.info("SEARCH", `HybridSearch inizializzato (embedding: ${this.embeddingInitialized ? "attivo" : "disattivato"})`);
1659
+ } catch (error) {
1660
+ logger.warn("SEARCH", "Inizializzazione embedding fallita, uso solo FTS5", {}, error);
1661
+ this.embeddingInitialized = false;
1662
+ }
1663
+ }
1664
+ /**
1665
+ * Ricerca ibrida con scoring a 4 segnali
1666
+ */
1667
+ async search(db, query, options = {}) {
1668
+ const limit = options.limit || 10;
1669
+ const weights = options.weights || SEARCH_WEIGHTS;
1670
+ const targetProject = options.project || "";
1671
+ const rawItems = /* @__PURE__ */ new Map();
1672
+ if (this.embeddingInitialized) {
1673
+ try {
1674
+ const embeddingService2 = getEmbeddingService();
1675
+ const queryEmbedding = await embeddingService2.embed(query);
1676
+ if (queryEmbedding) {
1677
+ const vectorSearch2 = getVectorSearch();
1678
+ const vectorResults = await vectorSearch2.search(db, queryEmbedding, {
1679
+ project: options.project,
1680
+ limit: limit * 2,
1681
+ // Prendiamo piu risultati per il ranking
1682
+ threshold: 0.3
1683
+ });
1684
+ for (const hit of vectorResults) {
1685
+ rawItems.set(String(hit.observationId), {
1686
+ id: String(hit.observationId),
1687
+ title: hit.title,
1688
+ content: hit.text || "",
1689
+ type: hit.type,
1690
+ project: hit.project,
1691
+ created_at: hit.created_at,
1692
+ created_at_epoch: hit.created_at_epoch,
1693
+ semanticScore: hit.similarity,
1694
+ fts5Rank: null,
1695
+ source: "vector"
1696
+ });
1697
+ }
1698
+ logger.debug("SEARCH", `Vector search: ${vectorResults.length} risultati`);
1699
+ }
1700
+ } catch (error) {
1701
+ logger.warn("SEARCH", "Ricerca vettoriale fallita, uso solo keyword", {}, error);
1702
+ }
1703
+ }
1704
+ try {
1705
+ const { searchObservationsFTSWithRank: searchObservationsFTSWithRank2 } = await Promise.resolve().then(() => (init_Search(), Search_exports));
1706
+ const keywordResults = searchObservationsFTSWithRank2(db, query, {
1707
+ project: options.project,
1708
+ limit: limit * 2
1709
+ });
1710
+ for (const obs of keywordResults) {
1711
+ const id = String(obs.id);
1712
+ const existing = rawItems.get(id);
1713
+ if (existing) {
1714
+ existing.fts5Rank = obs.fts5_rank;
1715
+ existing.source = "vector";
1716
+ } else {
1717
+ rawItems.set(id, {
1718
+ id,
1719
+ title: obs.title,
1720
+ content: obs.text || obs.narrative || "",
1721
+ type: obs.type,
1722
+ project: obs.project,
1723
+ created_at: obs.created_at,
1724
+ created_at_epoch: obs.created_at_epoch,
1725
+ semanticScore: 0,
1726
+ fts5Rank: obs.fts5_rank,
1727
+ source: "keyword"
1728
+ });
1729
+ }
1730
+ }
1731
+ logger.debug("SEARCH", `Keyword search: ${keywordResults.length} risultati`);
1732
+ } catch (error) {
1733
+ logger.error("SEARCH", "Ricerca keyword fallita", {}, error);
1734
+ }
1735
+ if (rawItems.size === 0) return [];
1736
+ const allFTS5Ranks = Array.from(rawItems.values()).filter((item) => item.fts5Rank !== null).map((item) => item.fts5Rank);
1737
+ const scored = [];
1738
+ for (const item of rawItems.values()) {
1739
+ const signals = {
1740
+ semantic: item.semanticScore,
1741
+ fts5: item.fts5Rank !== null ? normalizeFTS5Rank(item.fts5Rank, allFTS5Ranks) : 0,
1742
+ recency: recencyScore(item.created_at_epoch),
1743
+ projectMatch: targetProject ? projectMatchScore(item.project, targetProject) : 0
1744
+ };
1745
+ const score = computeCompositeScore(signals, weights);
1746
+ const isHybrid = item.semanticScore > 0 && item.fts5Rank !== null;
1747
+ const hybridBoost = isHybrid ? 1.15 : 1;
1748
+ const finalScore = Math.min(1, score * hybridBoost * knowledgeTypeBoost(item.type));
1749
+ scored.push({
1750
+ id: item.id,
1751
+ title: item.title,
1752
+ content: item.content,
1753
+ type: item.type,
1754
+ project: item.project,
1755
+ created_at: item.created_at,
1756
+ created_at_epoch: item.created_at_epoch,
1757
+ score: finalScore,
1758
+ source: isHybrid ? "hybrid" : item.source,
1759
+ signals
1760
+ });
1761
+ }
1762
+ scored.sort((a, b) => b.score - a.score);
1763
+ const finalResults = scored.slice(0, limit);
1764
+ if (finalResults.length > 0) {
1765
+ try {
1766
+ const { updateLastAccessed: updateLastAccessed3 } = await Promise.resolve().then(() => (init_Observations(), Observations_exports));
1767
+ const ids = finalResults.map((r) => parseInt(r.id, 10)).filter((id) => id > 0);
1768
+ if (ids.length > 0) {
1769
+ updateLastAccessed3(db, ids);
1770
+ }
1771
+ } catch {
1772
+ }
1773
+ }
1774
+ return finalResults;
1775
+ }
1776
+ };
1777
+ var hybridSearch = null;
1778
+ function getHybridSearch() {
1779
+ if (!hybridSearch) {
1780
+ hybridSearch = new HybridSearch();
1781
+ }
1782
+ return hybridSearch;
1783
+ }
1784
+
1785
+ // src/types/worker-types.ts
1786
+ var KNOWLEDGE_TYPES = ["constraint", "decision", "heuristic", "rejected"];
1787
+
880
1788
  // src/sdk/index.ts
881
1789
  var KiroMemorySDK = class {
882
1790
  db;
@@ -909,11 +1817,62 @@ var KiroMemorySDK = class {
909
1817
  recentPrompts: getPromptsByProject(this.db.db, this.project, 10)
910
1818
  };
911
1819
  }
1820
+ /**
1821
+ * Valida input per storeObservation
1822
+ */
1823
+ validateObservationInput(data) {
1824
+ if (!data.type || typeof data.type !== "string" || data.type.length > 100) {
1825
+ throw new Error("type \xE8 obbligatorio (stringa, max 100 caratteri)");
1826
+ }
1827
+ if (!data.title || typeof data.title !== "string" || data.title.length > 500) {
1828
+ throw new Error("title \xE8 obbligatorio (stringa, max 500 caratteri)");
1829
+ }
1830
+ if (!data.content || typeof data.content !== "string" || data.content.length > 1e5) {
1831
+ throw new Error("content \xE8 obbligatorio (stringa, max 100KB)");
1832
+ }
1833
+ }
1834
+ /**
1835
+ * Valida input per storeSummary
1836
+ */
1837
+ validateSummaryInput(data) {
1838
+ const MAX = 5e4;
1839
+ for (const [key, val] of Object.entries(data)) {
1840
+ if (val !== void 0 && val !== null) {
1841
+ if (typeof val !== "string") throw new Error(`${key} deve essere una stringa`);
1842
+ if (val.length > MAX) throw new Error(`${key} troppo grande (max 50KB)`);
1843
+ }
1844
+ }
1845
+ }
1846
+ /**
1847
+ * Genera e salva embedding per un'osservazione (fire-and-forget, non blocca)
1848
+ */
1849
+ async generateEmbeddingAsync(observationId, title, content, concepts) {
1850
+ try {
1851
+ const embeddingService2 = getEmbeddingService();
1852
+ if (!embeddingService2.isAvailable()) return;
1853
+ const parts = [title, content];
1854
+ if (concepts?.length) parts.push(concepts.join(", "));
1855
+ const fullText = parts.join(" ").substring(0, 2e3);
1856
+ const embedding = await embeddingService2.embed(fullText);
1857
+ if (embedding) {
1858
+ const vectorSearch2 = getVectorSearch();
1859
+ await vectorSearch2.storeEmbedding(
1860
+ this.db.db,
1861
+ observationId,
1862
+ embedding,
1863
+ embeddingService2.getProvider() || "unknown"
1864
+ );
1865
+ }
1866
+ } catch (error) {
1867
+ logger.debug("SDK", `Embedding generation fallita per obs ${observationId}: ${error}`);
1868
+ }
1869
+ }
912
1870
  /**
913
1871
  * Store a new observation
914
1872
  */
915
1873
  async storeObservation(data) {
916
- return createObservation(
1874
+ this.validateObservationInput(data);
1875
+ const observationId = createObservation(
917
1876
  this.db.db,
918
1877
  "sdk-" + Date.now(),
919
1878
  this.project,
@@ -934,11 +1893,76 @@ var KiroMemorySDK = class {
934
1893
  0
935
1894
  // prompt_number
936
1895
  );
1896
+ this.generateEmbeddingAsync(observationId, data.title, data.content, data.concepts).catch(() => {
1897
+ });
1898
+ return observationId;
1899
+ }
1900
+ /**
1901
+ * Salva conoscenza strutturata (constraint, decision, heuristic, rejected).
1902
+ * Usa il campo `type` per il knowledgeType e `facts` per i metadati JSON.
1903
+ */
1904
+ async storeKnowledge(data) {
1905
+ if (!KNOWLEDGE_TYPES.includes(data.knowledgeType)) {
1906
+ throw new Error(`knowledgeType non valido: ${data.knowledgeType}. Valori ammessi: ${KNOWLEDGE_TYPES.join(", ")}`);
1907
+ }
1908
+ this.validateObservationInput({ type: data.knowledgeType, title: data.title, content: data.content });
1909
+ const metadata = (() => {
1910
+ switch (data.knowledgeType) {
1911
+ case "constraint":
1912
+ return {
1913
+ knowledgeType: "constraint",
1914
+ severity: data.metadata?.severity || "soft",
1915
+ reason: data.metadata?.reason
1916
+ };
1917
+ case "decision":
1918
+ return {
1919
+ knowledgeType: "decision",
1920
+ alternatives: data.metadata?.alternatives,
1921
+ reason: data.metadata?.reason
1922
+ };
1923
+ case "heuristic":
1924
+ return {
1925
+ knowledgeType: "heuristic",
1926
+ context: data.metadata?.context,
1927
+ confidence: data.metadata?.confidence
1928
+ };
1929
+ case "rejected":
1930
+ return {
1931
+ knowledgeType: "rejected",
1932
+ reason: data.metadata?.reason || "",
1933
+ alternatives: data.metadata?.alternatives
1934
+ };
1935
+ }
1936
+ })();
1937
+ const observationId = createObservation(
1938
+ this.db.db,
1939
+ "sdk-" + Date.now(),
1940
+ data.project || this.project,
1941
+ data.knowledgeType,
1942
+ // type = knowledgeType
1943
+ data.title,
1944
+ null,
1945
+ // subtitle
1946
+ data.content,
1947
+ null,
1948
+ // narrative
1949
+ JSON.stringify(metadata),
1950
+ // facts = metadati JSON
1951
+ data.concepts?.join(", ") || null,
1952
+ data.files?.join(", ") || null,
1953
+ data.files?.join(", ") || null,
1954
+ 0
1955
+ // prompt_number
1956
+ );
1957
+ this.generateEmbeddingAsync(observationId, data.title, data.content, data.concepts).catch(() => {
1958
+ });
1959
+ return observationId;
937
1960
  }
938
1961
  /**
939
1962
  * Store a session summary
940
1963
  */
941
1964
  async storeSummary(data) {
1965
+ this.validateSummaryInput(data);
942
1966
  return createSummary(
943
1967
  this.db.db,
944
1968
  "sdk-" + Date.now(),
@@ -1034,6 +2058,227 @@ var KiroMemorySDK = class {
1034
2058
  getProject() {
1035
2059
  return this.project;
1036
2060
  }
2061
+ /**
2062
+ * Ricerca ibrida: vector search + keyword FTS5
2063
+ * Richiede inizializzazione HybridSearch (embedding service)
2064
+ */
2065
+ async hybridSearch(query, options = {}) {
2066
+ const hybridSearch2 = getHybridSearch();
2067
+ return hybridSearch2.search(this.db.db, query, {
2068
+ project: this.project,
2069
+ limit: options.limit || 10
2070
+ });
2071
+ }
2072
+ /**
2073
+ * Ricerca solo semantica (vector search)
2074
+ * Ritorna risultati basati su similarità coseno con gli embeddings
2075
+ */
2076
+ async semanticSearch(query, options = {}) {
2077
+ const embeddingService2 = getEmbeddingService();
2078
+ if (!embeddingService2.isAvailable()) {
2079
+ await embeddingService2.initialize();
2080
+ }
2081
+ if (!embeddingService2.isAvailable()) return [];
2082
+ const queryEmbedding = await embeddingService2.embed(query);
2083
+ if (!queryEmbedding) return [];
2084
+ const vectorSearch2 = getVectorSearch();
2085
+ const results = await vectorSearch2.search(this.db.db, queryEmbedding, {
2086
+ project: this.project,
2087
+ limit: options.limit || 10,
2088
+ threshold: options.threshold || 0.3
2089
+ });
2090
+ return results.map((r) => ({
2091
+ id: String(r.observationId),
2092
+ title: r.title,
2093
+ content: r.text || "",
2094
+ type: r.type,
2095
+ project: r.project,
2096
+ created_at: r.created_at,
2097
+ created_at_epoch: r.created_at_epoch,
2098
+ score: r.similarity,
2099
+ source: "vector",
2100
+ signals: {
2101
+ semantic: r.similarity,
2102
+ fts5: 0,
2103
+ recency: recencyScore(r.created_at_epoch),
2104
+ projectMatch: projectMatchScore(r.project, this.project)
2105
+ }
2106
+ }));
2107
+ }
2108
+ /**
2109
+ * Genera embeddings per osservazioni che non li hanno ancora
2110
+ */
2111
+ async backfillEmbeddings(batchSize = 50) {
2112
+ const vectorSearch2 = getVectorSearch();
2113
+ return vectorSearch2.backfillEmbeddings(this.db.db, batchSize);
2114
+ }
2115
+ /**
2116
+ * Statistiche sugli embeddings nel database
2117
+ */
2118
+ getEmbeddingStats() {
2119
+ const vectorSearch2 = getVectorSearch();
2120
+ return vectorSearch2.getStats(this.db.db);
2121
+ }
2122
+ /**
2123
+ * Inizializza il servizio di embedding (lazy, chiamare prima di hybridSearch)
2124
+ */
2125
+ async initializeEmbeddings() {
2126
+ const hybridSearch2 = getHybridSearch();
2127
+ await hybridSearch2.initialize();
2128
+ return getEmbeddingService().isAvailable();
2129
+ }
2130
+ /**
2131
+ * Contesto smart con ranking a 4 segnali e budget token.
2132
+ *
2133
+ * Se query presente: usa HybridSearch con SEARCH_WEIGHTS.
2134
+ * Se senza query: ranking per recency + project match (CONTEXT_WEIGHTS).
2135
+ */
2136
+ async getSmartContext(options = {}) {
2137
+ const tokenBudget = options.tokenBudget || parseInt(process.env.KIRO_MEMORY_CONTEXT_TOKENS || "0", 10) || 2e3;
2138
+ const summaries = getSummariesByProject(this.db.db, this.project, 5);
2139
+ let items;
2140
+ if (options.query) {
2141
+ const hybridSearch2 = getHybridSearch();
2142
+ const results = await hybridSearch2.search(this.db.db, options.query, {
2143
+ project: this.project,
2144
+ limit: 30
2145
+ });
2146
+ items = results.map((r) => ({
2147
+ id: parseInt(r.id, 10) || 0,
2148
+ title: r.title,
2149
+ content: r.content,
2150
+ type: r.type,
2151
+ project: r.project,
2152
+ created_at: r.created_at,
2153
+ created_at_epoch: r.created_at_epoch,
2154
+ score: r.score,
2155
+ signals: r.signals
2156
+ }));
2157
+ } else {
2158
+ const observations = getObservationsByProject(this.db.db, this.project, 30);
2159
+ items = observations.map((obs) => {
2160
+ const signals = {
2161
+ semantic: 0,
2162
+ fts5: 0,
2163
+ recency: recencyScore(obs.created_at_epoch),
2164
+ projectMatch: projectMatchScore(obs.project, this.project)
2165
+ };
2166
+ const baseScore = computeCompositeScore(signals, CONTEXT_WEIGHTS);
2167
+ const boostedScore = Math.min(1, baseScore * knowledgeTypeBoost(obs.type));
2168
+ return {
2169
+ id: obs.id,
2170
+ title: obs.title,
2171
+ content: obs.text || obs.narrative || "",
2172
+ type: obs.type,
2173
+ project: obs.project,
2174
+ created_at: obs.created_at,
2175
+ created_at_epoch: obs.created_at_epoch,
2176
+ score: boostedScore,
2177
+ signals
2178
+ };
2179
+ });
2180
+ items.sort((a, b) => b.score - a.score);
2181
+ }
2182
+ let tokensUsed = 0;
2183
+ for (const item of items) {
2184
+ tokensUsed += Math.ceil((item.title.length + item.content.length) / 4);
2185
+ if (tokensUsed > tokenBudget) break;
2186
+ }
2187
+ return {
2188
+ project: this.project,
2189
+ items,
2190
+ summaries,
2191
+ tokenBudget,
2192
+ tokensUsed: Math.min(tokensUsed, tokenBudget)
2193
+ };
2194
+ }
2195
+ /**
2196
+ * Rileva osservazioni stale (file modificati dopo la creazione) e le marca nel DB.
2197
+ * Ritorna il numero di osservazioni marcate come stale.
2198
+ */
2199
+ async detectStaleObservations() {
2200
+ const staleObs = getStaleObservations(this.db.db, this.project);
2201
+ if (staleObs.length > 0) {
2202
+ const ids = staleObs.map((o) => o.id);
2203
+ markObservationsStale(this.db.db, ids, true);
2204
+ }
2205
+ return staleObs.length;
2206
+ }
2207
+ /**
2208
+ * Consolida osservazioni duplicate sullo stesso file e tipo.
2209
+ * Raggruppa per (project, type, files_modified), mantiene la piu recente.
2210
+ */
2211
+ async consolidateObservations(options = {}) {
2212
+ return consolidateObservations(this.db.db, this.project, options);
2213
+ }
2214
+ /**
2215
+ * Statistiche decay: totale, stale, mai accedute, accedute di recente.
2216
+ */
2217
+ async getDecayStats() {
2218
+ const total = this.db.db.query(
2219
+ "SELECT COUNT(*) as count FROM observations WHERE project = ?"
2220
+ ).get(this.project)?.count || 0;
2221
+ const stale = this.db.db.query(
2222
+ "SELECT COUNT(*) as count FROM observations WHERE project = ? AND is_stale = 1"
2223
+ ).get(this.project)?.count || 0;
2224
+ const neverAccessed = this.db.db.query(
2225
+ "SELECT COUNT(*) as count FROM observations WHERE project = ? AND last_accessed_epoch IS NULL"
2226
+ ).get(this.project)?.count || 0;
2227
+ const recentThreshold = Date.now() - 48 * 60 * 60 * 1e3;
2228
+ const recentlyAccessed = this.db.db.query(
2229
+ "SELECT COUNT(*) as count FROM observations WHERE project = ? AND last_accessed_epoch > ?"
2230
+ ).get(this.project, recentThreshold)?.count || 0;
2231
+ return { total, stale, neverAccessed, recentlyAccessed };
2232
+ }
2233
+ /**
2234
+ * Crea un checkpoint strutturato per resume sessione.
2235
+ * Salva automaticamente un context_snapshot con le ultime 10 osservazioni.
2236
+ */
2237
+ async createCheckpoint(sessionId, data) {
2238
+ const recentObs = getObservationsByProject(this.db.db, this.project, 10);
2239
+ const contextSnapshot = JSON.stringify(
2240
+ recentObs.map((o) => ({ id: o.id, type: o.type, title: o.title, text: o.text?.substring(0, 200) }))
2241
+ );
2242
+ return createCheckpoint(this.db.db, sessionId, this.project, {
2243
+ task: data.task,
2244
+ progress: data.progress,
2245
+ nextSteps: data.nextSteps,
2246
+ openQuestions: data.openQuestions,
2247
+ relevantFiles: data.relevantFiles?.join(", "),
2248
+ contextSnapshot
2249
+ });
2250
+ }
2251
+ /**
2252
+ * Recupera l'ultimo checkpoint di una sessione specifica.
2253
+ */
2254
+ async getCheckpoint(sessionId) {
2255
+ return getLatestCheckpoint(this.db.db, sessionId);
2256
+ }
2257
+ /**
2258
+ * Recupera l'ultimo checkpoint per il progetto corrente.
2259
+ * Utile per resume automatico senza specificare session ID.
2260
+ */
2261
+ async getLatestProjectCheckpoint() {
2262
+ return getLatestCheckpointByProject(this.db.db, this.project);
2263
+ }
2264
+ /**
2265
+ * Genera un report di attività per il progetto corrente.
2266
+ * Aggrega osservazioni, sessioni, summaries e file per un periodo temporale.
2267
+ */
2268
+ async generateReport(options) {
2269
+ const now = /* @__PURE__ */ new Date();
2270
+ let startEpoch;
2271
+ let endEpoch = now.getTime();
2272
+ if (options?.startDate && options?.endDate) {
2273
+ startEpoch = options.startDate.getTime();
2274
+ endEpoch = options.endDate.getTime();
2275
+ } else {
2276
+ const period = options?.period || "weekly";
2277
+ const daysBack = period === "monthly" ? 30 : 7;
2278
+ startEpoch = endEpoch - daysBack * 24 * 60 * 60 * 1e3;
2279
+ }
2280
+ return getReportData(this.db.db, this.project, startEpoch, endEpoch);
2281
+ }
1037
2282
  /**
1038
2283
  * Getter for direct database access (for API routes)
1039
2284
  */
@@ -1053,10 +2298,19 @@ function createKiroMemory(config) {
1053
2298
 
1054
2299
  // src/hooks/postToolUse.ts
1055
2300
  runHook("postToolUse", async (input) => {
2301
+ if (input.hook_event_name === "afterFileEdit" && !input.tool_name) {
2302
+ input.tool_name = "Write";
2303
+ input.tool_input = { path: input.file_path };
2304
+ input.tool_response = { edits: input.edits };
2305
+ }
2306
+ if (input.hook_event_name === "afterShellExecution" && !input.tool_name) {
2307
+ input.tool_name = "Bash";
2308
+ input.tool_input = { command: input.command };
2309
+ }
1056
2310
  if (!input.tool_name) return;
1057
- const ignoredTools = ["introspect", "thinking", "todo"];
2311
+ const ignoredTools = ["introspect", "thinking", "todo", "TodoWrite"];
1058
2312
  if (ignoredTools.includes(input.tool_name)) return;
1059
- const readOnlyTools = ["glob", "grep", "fs_read", "read"];
2313
+ const readOnlyTools = ["glob", "grep", "fs_read", "read", "Read", "Glob", "Grep"];
1060
2314
  if (readOnlyTools.includes(input.tool_name)) {
1061
2315
  const project2 = detectProject(input.cwd);
1062
2316
  const sdk2 = createKiroMemory({ project: project2, skipMigrations: true });
@@ -1097,19 +2351,30 @@ runHook("postToolUse", async (input) => {
1097
2351
  function buildTitle(toolName, toolInput) {
1098
2352
  if (!toolInput) return `Tool: ${toolName}`;
1099
2353
  switch (toolName) {
2354
+ // Kiro CLI + Claude Code: scrittura file
1100
2355
  case "fs_write":
1101
2356
  case "write":
1102
- return `Written: ${toolInput.path || toolInput.file_path || "file"}`;
2357
+ case "Write":
2358
+ case "Edit":
2359
+ case "NotebookEdit":
2360
+ return `Written: ${toolInput.path || toolInput.file_path || toolInput.notebook_path || "file"}`;
2361
+ // Kiro CLI + Claude Code: comandi shell
1103
2362
  case "execute_bash":
1104
2363
  case "shell":
2364
+ case "Bash":
1105
2365
  return `Executed: ${(toolInput.command || "").substring(0, 80)}`;
2366
+ // Kiro CLI + Claude Code: ricerca web
1106
2367
  case "web_search":
2368
+ case "WebSearch":
1107
2369
  return `Searched: ${toolInput.query || ""}`;
1108
2370
  case "web_fetch":
2371
+ case "WebFetch":
1109
2372
  return `Fetch: ${toolInput.url || ""}`;
2373
+ // Kiro CLI + Claude Code: delegazione
1110
2374
  case "delegate":
1111
2375
  case "use_subagent":
1112
- return `Delegated: ${toolInput.task || toolInput.prompt || ""}`.substring(0, 100);
2376
+ case "Task":
2377
+ return `Delegated: ${toolInput.task || toolInput.prompt || toolInput.description || ""}`.substring(0, 100);
1113
2378
  default:
1114
2379
  return `${toolName}: ${JSON.stringify(toolInput).substring(0, 80)}`;
1115
2380
  }
@@ -1129,6 +2394,7 @@ function buildContent(toolName, toolInput, toolResponse) {
1129
2394
  }
1130
2395
  function categorizeToolUse(toolName) {
1131
2396
  const categories = {
2397
+ // Kiro CLI tool names
1132
2398
  "fs_write": "file-write",
1133
2399
  "write": "file-write",
1134
2400
  "fs_read": "file-read",
@@ -1143,7 +2409,18 @@ function categorizeToolUse(toolName) {
1143
2409
  "use_subagent": "delegation",
1144
2410
  "use_aws": "cloud-operation",
1145
2411
  "aws": "cloud-operation",
1146
- "code": "code-intelligence"
2412
+ "code": "code-intelligence",
2413
+ // Claude Code tool names (PascalCase)
2414
+ "Write": "file-write",
2415
+ "Edit": "file-write",
2416
+ "NotebookEdit": "file-write",
2417
+ "Read": "file-read",
2418
+ "Glob": "file-read",
2419
+ "Grep": "file-read",
2420
+ "Bash": "command",
2421
+ "WebSearch": "research",
2422
+ "WebFetch": "research",
2423
+ "Task": "delegation"
1147
2424
  };
1148
2425
  return categories[toolName] || "tool-use";
1149
2426
  }