kiro-memory 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/README.md +290 -0
- package/package.json +117 -0
- package/plugin/dist/cli/contextkit.js +1259 -0
- package/plugin/dist/hooks/agentSpawn.js +1187 -0
- package/plugin/dist/hooks/kiro-hooks.js +1184 -0
- package/plugin/dist/hooks/postToolUse.js +1219 -0
- package/plugin/dist/hooks/stop.js +1163 -0
- package/plugin/dist/hooks/userPromptSubmit.js +1152 -0
- package/plugin/dist/index.js +2103 -0
- package/plugin/dist/sdk/index.js +1083 -0
- package/plugin/dist/servers/mcp-server.js +266 -0
- package/plugin/dist/services/search/ChromaManager.js +357 -0
- package/plugin/dist/services/search/HybridSearch.js +502 -0
- package/plugin/dist/services/search/index.js +511 -0
- package/plugin/dist/services/sqlite/Database.js +625 -0
- package/plugin/dist/services/sqlite/Observations.js +46 -0
- package/plugin/dist/services/sqlite/Prompts.js +39 -0
- package/plugin/dist/services/sqlite/Search.js +143 -0
- package/plugin/dist/services/sqlite/Sessions.js +60 -0
- package/plugin/dist/services/sqlite/Summaries.js +44 -0
- package/plugin/dist/services/sqlite/index.js +951 -0
- package/plugin/dist/shared/paths.js +315 -0
- package/plugin/dist/types/worker-types.js +0 -0
- package/plugin/dist/utils/logger.js +222 -0
- package/plugin/dist/viewer.html +252 -0
- package/plugin/dist/viewer.js +23965 -0
- package/plugin/dist/worker-service.js +1782 -0
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
+
var __esm = (fn, res) => function __init() {
|
|
4
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
5
|
+
};
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// src/services/sqlite/Observations.ts
|
|
12
|
+
var Observations_exports = {};
|
|
13
|
+
__export(Observations_exports, {
|
|
14
|
+
createObservation: () => createObservation,
|
|
15
|
+
deleteObservation: () => deleteObservation,
|
|
16
|
+
getObservationsByProject: () => getObservationsByProject,
|
|
17
|
+
getObservationsBySession: () => getObservationsBySession,
|
|
18
|
+
searchObservations: () => searchObservations
|
|
19
|
+
});
|
|
20
|
+
function createObservation(db, memorySessionId, project, type, title, subtitle, text, narrative, facts, concepts, filesRead, filesModified, promptNumber) {
|
|
21
|
+
const now = /* @__PURE__ */ new Date();
|
|
22
|
+
const result = db.run(
|
|
23
|
+
`INSERT INTO observations
|
|
24
|
+
(memory_session_id, project, type, title, subtitle, text, narrative, facts, concepts, files_read, files_modified, prompt_number, created_at, created_at_epoch)
|
|
25
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
26
|
+
[memorySessionId, project, type, title, subtitle, text, narrative, facts, concepts, filesRead, filesModified, promptNumber, now.toISOString(), now.getTime()]
|
|
27
|
+
);
|
|
28
|
+
return Number(result.lastInsertRowid);
|
|
29
|
+
}
|
|
30
|
+
function getObservationsBySession(db, memorySessionId) {
|
|
31
|
+
const query = db.query(
|
|
32
|
+
"SELECT * FROM observations WHERE memory_session_id = ? ORDER BY prompt_number ASC"
|
|
33
|
+
);
|
|
34
|
+
return query.all(memorySessionId);
|
|
35
|
+
}
|
|
36
|
+
function getObservationsByProject(db, project, limit = 100) {
|
|
37
|
+
const query = db.query(
|
|
38
|
+
"SELECT * FROM observations WHERE project = ? ORDER BY created_at_epoch DESC LIMIT ?"
|
|
39
|
+
);
|
|
40
|
+
return query.all(project, limit);
|
|
41
|
+
}
|
|
42
|
+
function searchObservations(db, searchTerm, project) {
|
|
43
|
+
const sql = project ? `SELECT * FROM observations
|
|
44
|
+
WHERE project = ? AND (title LIKE ? OR text LIKE ? OR narrative LIKE ?)
|
|
45
|
+
ORDER BY created_at_epoch DESC` : `SELECT * FROM observations
|
|
46
|
+
WHERE title LIKE ? OR text LIKE ? OR narrative LIKE ?
|
|
47
|
+
ORDER BY created_at_epoch DESC`;
|
|
48
|
+
const pattern = `%${searchTerm}%`;
|
|
49
|
+
const query = db.query(sql);
|
|
50
|
+
if (project) {
|
|
51
|
+
return query.all(project, pattern, pattern, pattern);
|
|
52
|
+
}
|
|
53
|
+
return query.all(pattern, pattern, pattern);
|
|
54
|
+
}
|
|
55
|
+
function deleteObservation(db, id) {
|
|
56
|
+
db.run("DELETE FROM observations WHERE id = ?", [id]);
|
|
57
|
+
}
|
|
58
|
+
var init_Observations = __esm({
|
|
59
|
+
"src/services/sqlite/Observations.ts"() {
|
|
60
|
+
"use strict";
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// src/services/search/ChromaManager.ts
|
|
65
|
+
import { ChromaClient } from "chromadb";
|
|
66
|
+
import { join as join2 } from "path";
|
|
67
|
+
import { homedir as homedir2 } from "os";
|
|
68
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
69
|
+
|
|
70
|
+
// src/utils/logger.ts
|
|
71
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync } from "fs";
|
|
72
|
+
import { join } from "path";
|
|
73
|
+
import { homedir } from "os";
|
|
74
|
+
var LogLevel = /* @__PURE__ */ ((LogLevel2) => {
|
|
75
|
+
LogLevel2[LogLevel2["DEBUG"] = 0] = "DEBUG";
|
|
76
|
+
LogLevel2[LogLevel2["INFO"] = 1] = "INFO";
|
|
77
|
+
LogLevel2[LogLevel2["WARN"] = 2] = "WARN";
|
|
78
|
+
LogLevel2[LogLevel2["ERROR"] = 3] = "ERROR";
|
|
79
|
+
LogLevel2[LogLevel2["SILENT"] = 4] = "SILENT";
|
|
80
|
+
return LogLevel2;
|
|
81
|
+
})(LogLevel || {});
|
|
82
|
+
var DEFAULT_DATA_DIR = join(homedir(), ".contextkit");
|
|
83
|
+
var Logger = class {
|
|
84
|
+
level = null;
|
|
85
|
+
useColor;
|
|
86
|
+
logFilePath = null;
|
|
87
|
+
logFileInitialized = false;
|
|
88
|
+
constructor() {
|
|
89
|
+
this.useColor = process.stdout.isTTY ?? false;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Initialize log file path and ensure directory exists (lazy initialization)
|
|
93
|
+
*/
|
|
94
|
+
ensureLogFileInitialized() {
|
|
95
|
+
if (this.logFileInitialized) return;
|
|
96
|
+
this.logFileInitialized = true;
|
|
97
|
+
try {
|
|
98
|
+
const logsDir = join(DEFAULT_DATA_DIR, "logs");
|
|
99
|
+
if (!existsSync(logsDir)) {
|
|
100
|
+
mkdirSync(logsDir, { recursive: true });
|
|
101
|
+
}
|
|
102
|
+
const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
103
|
+
this.logFilePath = join(logsDir, `contextkit-${date}.log`);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
console.error("[LOGGER] Failed to initialize log file:", error);
|
|
106
|
+
this.logFilePath = null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Lazy-load log level from settings file
|
|
111
|
+
*/
|
|
112
|
+
getLevel() {
|
|
113
|
+
if (this.level === null) {
|
|
114
|
+
try {
|
|
115
|
+
const settingsPath = join(DEFAULT_DATA_DIR, "settings.json");
|
|
116
|
+
if (existsSync(settingsPath)) {
|
|
117
|
+
const settingsData = readFileSync(settingsPath, "utf-8");
|
|
118
|
+
const settings = JSON.parse(settingsData);
|
|
119
|
+
const envLevel = (settings.CONTEXTKIT_LOG_LEVEL || "INFO").toUpperCase();
|
|
120
|
+
this.level = LogLevel[envLevel] ?? 1 /* INFO */;
|
|
121
|
+
} else {
|
|
122
|
+
this.level = 1 /* INFO */;
|
|
123
|
+
}
|
|
124
|
+
} catch (error) {
|
|
125
|
+
this.level = 1 /* INFO */;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return this.level;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Create correlation ID for tracking an observation through the pipeline
|
|
132
|
+
*/
|
|
133
|
+
correlationId(sessionId, observationNum) {
|
|
134
|
+
return `obs-${sessionId}-${observationNum}`;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Create session correlation ID
|
|
138
|
+
*/
|
|
139
|
+
sessionId(sessionId) {
|
|
140
|
+
return `session-${sessionId}`;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Format data for logging - create compact summaries instead of full dumps
|
|
144
|
+
*/
|
|
145
|
+
formatData(data) {
|
|
146
|
+
if (data === null || data === void 0) return "";
|
|
147
|
+
if (typeof data === "string") return data;
|
|
148
|
+
if (typeof data === "number") return data.toString();
|
|
149
|
+
if (typeof data === "boolean") return data.toString();
|
|
150
|
+
if (typeof data === "object") {
|
|
151
|
+
if (data instanceof Error) {
|
|
152
|
+
return this.getLevel() === 0 /* DEBUG */ ? `${data.message}
|
|
153
|
+
${data.stack}` : data.message;
|
|
154
|
+
}
|
|
155
|
+
if (Array.isArray(data)) {
|
|
156
|
+
return `[${data.length} items]`;
|
|
157
|
+
}
|
|
158
|
+
const keys = Object.keys(data);
|
|
159
|
+
if (keys.length === 0) return "{}";
|
|
160
|
+
if (keys.length <= 3) {
|
|
161
|
+
return JSON.stringify(data);
|
|
162
|
+
}
|
|
163
|
+
return `{${keys.length} keys: ${keys.slice(0, 3).join(", ")}...}`;
|
|
164
|
+
}
|
|
165
|
+
return String(data);
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Format timestamp in local timezone (YYYY-MM-DD HH:MM:SS.mmm)
|
|
169
|
+
*/
|
|
170
|
+
formatTimestamp(date) {
|
|
171
|
+
const year = date.getFullYear();
|
|
172
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
173
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
174
|
+
const hours = String(date.getHours()).padStart(2, "0");
|
|
175
|
+
const minutes = String(date.getMinutes()).padStart(2, "0");
|
|
176
|
+
const seconds = String(date.getSeconds()).padStart(2, "0");
|
|
177
|
+
const ms = String(date.getMilliseconds()).padStart(3, "0");
|
|
178
|
+
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${ms}`;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Core logging method
|
|
182
|
+
*/
|
|
183
|
+
log(level, component, message, context, data) {
|
|
184
|
+
if (level < this.getLevel()) return;
|
|
185
|
+
this.ensureLogFileInitialized();
|
|
186
|
+
const timestamp = this.formatTimestamp(/* @__PURE__ */ new Date());
|
|
187
|
+
const levelStr = LogLevel[level].padEnd(5);
|
|
188
|
+
const componentStr = component.padEnd(6);
|
|
189
|
+
let correlationStr = "";
|
|
190
|
+
if (context?.correlationId) {
|
|
191
|
+
correlationStr = `[${context.correlationId}] `;
|
|
192
|
+
} else if (context?.sessionId) {
|
|
193
|
+
correlationStr = `[session-${context.sessionId}] `;
|
|
194
|
+
}
|
|
195
|
+
let dataStr = "";
|
|
196
|
+
if (data !== void 0 && data !== null) {
|
|
197
|
+
if (data instanceof Error) {
|
|
198
|
+
dataStr = this.getLevel() === 0 /* DEBUG */ ? `
|
|
199
|
+
${data.message}
|
|
200
|
+
${data.stack}` : ` ${data.message}`;
|
|
201
|
+
} else if (this.getLevel() === 0 /* DEBUG */ && typeof data === "object") {
|
|
202
|
+
dataStr = "\n" + JSON.stringify(data, null, 2);
|
|
203
|
+
} else {
|
|
204
|
+
dataStr = " " + this.formatData(data);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
let contextStr = "";
|
|
208
|
+
if (context) {
|
|
209
|
+
const { sessionId, memorySessionId, correlationId, ...rest } = context;
|
|
210
|
+
if (Object.keys(rest).length > 0) {
|
|
211
|
+
const pairs = Object.entries(rest).map(([k, v]) => `${k}=${v}`);
|
|
212
|
+
contextStr = ` {${pairs.join(", ")}}`;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
const logLine = `[${timestamp}] [${levelStr}] [${componentStr}] ${correlationStr}${message}${contextStr}${dataStr}`;
|
|
216
|
+
if (this.logFilePath) {
|
|
217
|
+
try {
|
|
218
|
+
appendFileSync(this.logFilePath, logLine + "\n", "utf8");
|
|
219
|
+
} catch (error) {
|
|
220
|
+
process.stderr.write(`[LOGGER] Failed to write to log file: ${error}
|
|
221
|
+
`);
|
|
222
|
+
}
|
|
223
|
+
} else {
|
|
224
|
+
process.stderr.write(logLine + "\n");
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
// Public logging methods
|
|
228
|
+
debug(component, message, context, data) {
|
|
229
|
+
this.log(0 /* DEBUG */, component, message, context, data);
|
|
230
|
+
}
|
|
231
|
+
info(component, message, context, data) {
|
|
232
|
+
this.log(1 /* INFO */, component, message, context, data);
|
|
233
|
+
}
|
|
234
|
+
warn(component, message, context, data) {
|
|
235
|
+
this.log(2 /* WARN */, component, message, context, data);
|
|
236
|
+
}
|
|
237
|
+
error(component, message, context, data) {
|
|
238
|
+
this.log(3 /* ERROR */, component, message, context, data);
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Log data flow: input → processing
|
|
242
|
+
*/
|
|
243
|
+
dataIn(component, message, context, data) {
|
|
244
|
+
this.info(component, `\u2192 ${message}`, context, data);
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Log data flow: processing → output
|
|
248
|
+
*/
|
|
249
|
+
dataOut(component, message, context, data) {
|
|
250
|
+
this.info(component, `\u2190 ${message}`, context, data);
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Log successful completion
|
|
254
|
+
*/
|
|
255
|
+
success(component, message, context, data) {
|
|
256
|
+
this.info(component, `\u2713 ${message}`, context, data);
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Log failure
|
|
260
|
+
*/
|
|
261
|
+
failure(component, message, context, data) {
|
|
262
|
+
this.error(component, `\u2717 ${message}`, context, data);
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Log timing information
|
|
266
|
+
*/
|
|
267
|
+
timing(component, message, durationMs, context) {
|
|
268
|
+
this.info(component, `\u23F1 ${message}`, context, { duration: `${durationMs}ms` });
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Happy Path Error - logs when the expected "happy path" fails but we have a fallback
|
|
272
|
+
*/
|
|
273
|
+
happyPathError(component, message, context, data, fallback = "") {
|
|
274
|
+
const stack = new Error().stack || "";
|
|
275
|
+
const stackLines = stack.split("\n");
|
|
276
|
+
const callerLine = stackLines[2] || "";
|
|
277
|
+
const callerMatch = callerLine.match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/);
|
|
278
|
+
const location = callerMatch ? `${callerMatch[1].split("/").pop()}:${callerMatch[2]}` : "unknown";
|
|
279
|
+
const enhancedContext = {
|
|
280
|
+
...context,
|
|
281
|
+
location
|
|
282
|
+
};
|
|
283
|
+
this.warn(component, `[HAPPY-PATH] ${message}`, enhancedContext, data);
|
|
284
|
+
return fallback;
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
var logger = new Logger();
|
|
288
|
+
|
|
289
|
+
// src/services/search/ChromaManager.ts
|
|
290
|
+
var VECTOR_DB_DIR = join2(homedir2(), ".contextkit", "vector-db");
|
|
291
|
+
var ChromaManager = class {
|
|
292
|
+
client;
|
|
293
|
+
collection = null;
|
|
294
|
+
isAvailable = false;
|
|
295
|
+
constructor() {
|
|
296
|
+
if (!existsSync2(VECTOR_DB_DIR)) {
|
|
297
|
+
mkdirSync2(VECTOR_DB_DIR, { recursive: true });
|
|
298
|
+
}
|
|
299
|
+
this.client = new ChromaClient({
|
|
300
|
+
path: process.env.CHROMADB_URL || "http://localhost:8000"
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Initialize ChromaDB connection and collection
|
|
305
|
+
*/
|
|
306
|
+
async initialize() {
|
|
307
|
+
try {
|
|
308
|
+
await this.client.heartbeat();
|
|
309
|
+
this.collection = await this.client.getOrCreateCollection({
|
|
310
|
+
name: "contextkit-observations",
|
|
311
|
+
metadata: { description: "ContextKit observation embeddings" }
|
|
312
|
+
});
|
|
313
|
+
this.isAvailable = true;
|
|
314
|
+
logger.info("CHROMA", "ChromaDB initialized successfully");
|
|
315
|
+
return true;
|
|
316
|
+
} catch (error) {
|
|
317
|
+
logger.warn("CHROMA", "ChromaDB not available, falling back to SQLite search", {}, error);
|
|
318
|
+
this.isAvailable = false;
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Add observation embedding to ChromaDB
|
|
324
|
+
*/
|
|
325
|
+
async addObservation(id, content, metadata) {
|
|
326
|
+
if (!this.isAvailable || !this.collection) {
|
|
327
|
+
logger.debug("CHROMA", "ChromaDB not available, skipping embedding");
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
try {
|
|
331
|
+
await this.collection.add({
|
|
332
|
+
ids: [id],
|
|
333
|
+
documents: [content],
|
|
334
|
+
metadatas: [metadata]
|
|
335
|
+
});
|
|
336
|
+
logger.debug("CHROMA", `Added observation ${id} to vector DB`);
|
|
337
|
+
} catch (error) {
|
|
338
|
+
logger.error("CHROMA", `Failed to add observation ${id}`, {}, error);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Search observations by semantic similarity
|
|
343
|
+
*/
|
|
344
|
+
async search(query, options = {}) {
|
|
345
|
+
if (!this.isAvailable || !this.collection) {
|
|
346
|
+
logger.debug("CHROMA", "ChromaDB not available, returning empty results");
|
|
347
|
+
return [];
|
|
348
|
+
}
|
|
349
|
+
try {
|
|
350
|
+
const where = options.project ? { project: options.project } : void 0;
|
|
351
|
+
const results = await this.collection.query({
|
|
352
|
+
queryTexts: [query],
|
|
353
|
+
nResults: options.limit || 10,
|
|
354
|
+
where
|
|
355
|
+
});
|
|
356
|
+
const hits = [];
|
|
357
|
+
if (results.ids && results.ids[0]) {
|
|
358
|
+
for (let i = 0; i < results.ids[0].length; i++) {
|
|
359
|
+
hits.push({
|
|
360
|
+
id: results.ids[0][i],
|
|
361
|
+
content: results.documents?.[0]?.[i] || "",
|
|
362
|
+
metadata: results.metadatas?.[0]?.[i] || {},
|
|
363
|
+
distance: results.distances?.[0]?.[i] || 0
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
logger.debug("CHROMA", `Search returned ${hits.length} results`);
|
|
368
|
+
return hits;
|
|
369
|
+
} catch (error) {
|
|
370
|
+
logger.error("CHROMA", "Search failed", {}, error);
|
|
371
|
+
return [];
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Delete observation from ChromaDB
|
|
376
|
+
*/
|
|
377
|
+
async deleteObservation(id) {
|
|
378
|
+
if (!this.isAvailable || !this.collection) {
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
try {
|
|
382
|
+
await this.collection.delete({ ids: [id] });
|
|
383
|
+
logger.debug("CHROMA", `Deleted observation ${id}`);
|
|
384
|
+
} catch (error) {
|
|
385
|
+
logger.error("CHROMA", `Failed to delete observation ${id}`, {}, error);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Check if ChromaDB is available
|
|
390
|
+
*/
|
|
391
|
+
isChromaAvailable() {
|
|
392
|
+
return this.isAvailable;
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Get collection stats
|
|
396
|
+
*/
|
|
397
|
+
async getStats() {
|
|
398
|
+
if (!this.isAvailable || !this.collection) {
|
|
399
|
+
return { count: 0 };
|
|
400
|
+
}
|
|
401
|
+
try {
|
|
402
|
+
const count = await this.collection.count();
|
|
403
|
+
return { count };
|
|
404
|
+
} catch (error) {
|
|
405
|
+
logger.error("CHROMA", "Failed to get stats", {}, error);
|
|
406
|
+
return { count: 0 };
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
// src/services/search/HybridSearch.ts
|
|
412
|
+
var HybridSearch = class {
|
|
413
|
+
chromaManager;
|
|
414
|
+
constructor() {
|
|
415
|
+
this.chromaManager = new ChromaManager();
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Initialize search (connects to ChromaDB if available)
|
|
419
|
+
*/
|
|
420
|
+
async initialize() {
|
|
421
|
+
await this.chromaManager.initialize();
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Perform hybrid search combining vector and keyword results
|
|
425
|
+
*/
|
|
426
|
+
async search(db, query, options = {}) {
|
|
427
|
+
const limit = options.limit || 10;
|
|
428
|
+
const results = [];
|
|
429
|
+
if (this.chromaManager.isChromaAvailable()) {
|
|
430
|
+
try {
|
|
431
|
+
const vectorResults = await this.chromaManager.search(query, {
|
|
432
|
+
project: options.project,
|
|
433
|
+
limit: Math.ceil(limit / 2)
|
|
434
|
+
});
|
|
435
|
+
for (const hit of vectorResults) {
|
|
436
|
+
results.push({
|
|
437
|
+
id: hit.id,
|
|
438
|
+
title: hit.metadata.title || "Untitled",
|
|
439
|
+
content: hit.content,
|
|
440
|
+
type: hit.metadata.type || "unknown",
|
|
441
|
+
project: hit.metadata.project || "unknown",
|
|
442
|
+
created_at: hit.metadata.created_at || (/* @__PURE__ */ new Date()).toISOString(),
|
|
443
|
+
score: 1 - hit.distance,
|
|
444
|
+
// Convert distance to similarity score
|
|
445
|
+
source: "vector"
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
logger.debug("SEARCH", `Vector search returned ${vectorResults.length} results`);
|
|
449
|
+
} catch (error) {
|
|
450
|
+
logger.warn("SEARCH", "Vector search failed, using keyword only", {}, error);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
try {
|
|
454
|
+
const { searchObservations: searchObservations2 } = await Promise.resolve().then(() => (init_Observations(), Observations_exports));
|
|
455
|
+
const keywordResults = searchObservations2(db, query, options.project);
|
|
456
|
+
for (const obs of keywordResults.slice(0, Math.ceil(limit / 2))) {
|
|
457
|
+
results.push({
|
|
458
|
+
id: String(obs.id),
|
|
459
|
+
title: obs.title,
|
|
460
|
+
content: obs.text || obs.narrative || "",
|
|
461
|
+
type: obs.type,
|
|
462
|
+
project: obs.project,
|
|
463
|
+
created_at: obs.created_at,
|
|
464
|
+
score: 0.5,
|
|
465
|
+
// Default score for keyword matches
|
|
466
|
+
source: "keyword"
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
logger.debug("SEARCH", `Keyword search returned ${keywordResults.length} results`);
|
|
470
|
+
} catch (error) {
|
|
471
|
+
logger.error("SEARCH", "Keyword search failed", {}, error);
|
|
472
|
+
}
|
|
473
|
+
const uniqueResults = this.deduplicateAndSort(results, limit);
|
|
474
|
+
return uniqueResults;
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Remove duplicates and sort by score
|
|
478
|
+
*/
|
|
479
|
+
deduplicateAndSort(results, limit) {
|
|
480
|
+
const seen = /* @__PURE__ */ new Set();
|
|
481
|
+
const unique = [];
|
|
482
|
+
for (const result of results) {
|
|
483
|
+
if (!seen.has(result.id)) {
|
|
484
|
+
seen.add(result.id);
|
|
485
|
+
unique.push(result);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
unique.sort((a, b) => b.score - a.score);
|
|
489
|
+
return unique.slice(0, limit);
|
|
490
|
+
}
|
|
491
|
+
};
|
|
492
|
+
var hybridSearch = null;
|
|
493
|
+
function getHybridSearch() {
|
|
494
|
+
if (!hybridSearch) {
|
|
495
|
+
hybridSearch = new HybridSearch();
|
|
496
|
+
}
|
|
497
|
+
return hybridSearch;
|
|
498
|
+
}
|
|
499
|
+
export {
|
|
500
|
+
HybridSearch,
|
|
501
|
+
getHybridSearch
|
|
502
|
+
};
|