remem-mcp 0.5.17

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.
@@ -0,0 +1,1676 @@
1
+ import {
2
+ __esm,
3
+ __export,
4
+ generateId,
5
+ init_esm_shims
6
+ } from "./chunk-RITPZHIB.js";
7
+
8
+ // src/embedding/local.ts
9
+ var local_exports = {};
10
+ __export(local_exports, {
11
+ LocalEmbedder: () => LocalEmbedder
12
+ });
13
+ import { pipeline } from "@huggingface/transformers";
14
+ var LocalEmbedder;
15
+ var init_local = __esm({
16
+ "src/embedding/local.ts"() {
17
+ "use strict";
18
+ init_esm_shims();
19
+ LocalEmbedder = class {
20
+ dimension = 384;
21
+ model = "Xenova/all-MiniLM-L6-v2";
22
+ extractor = null;
23
+ initPromise = null;
24
+ /** Initialize the model. This runs once. Subsequent calls return immediately. */
25
+ async init() {
26
+ if (this.extractor) return;
27
+ if (this.initPromise) {
28
+ await this.initPromise;
29
+ return;
30
+ }
31
+ this.initPromise = (async () => {
32
+ this.extractor = await pipeline("feature-extraction", this.model);
33
+ })();
34
+ await this.initPromise;
35
+ }
36
+ async embed(text) {
37
+ await this.init();
38
+ if (!this.extractor) throw new Error("Embedder failed to initialize");
39
+ const output = await this.extractor(text, { pooling: "mean", normalize: true });
40
+ return Array.from(output.data);
41
+ }
42
+ };
43
+ }
44
+ });
45
+
46
+ // src/sdk.ts
47
+ init_esm_shims();
48
+ init_local();
49
+ import { createHash as createHash3 } from "crypto";
50
+ import { homedir } from "os";
51
+ import { join as join3 } from "path";
52
+
53
+ // src/security/redactor.ts
54
+ init_esm_shims();
55
+ var SECRET_PATTERNS = [
56
+ // OpenAI API key
57
+ /sk-[a-zA-Z0-9]{20,}/g,
58
+ // Anthropic API key
59
+ /sk-ant-[a-zA-Z0-9-]+/g,
60
+ // GitHub personal access token
61
+ /ghp_[a-zA-Z0-9]{36}/g,
62
+ // GitHub OAuth token
63
+ /gho_[a-zA-Z0-9]{36}/g,
64
+ // GitHub fine-grained token
65
+ /github_pat_[a-zA-Z0-9_]{82}/g,
66
+ // Slack token
67
+ /xox[baprs]-[a-zA-Z0-9-]+/g,
68
+ // AWS access key ID
69
+ /AKIA[0-9A-Z]{16}/g,
70
+ // AWS secret access key (40 chars, base64-ish)
71
+ /(?<![A-Za-z0-9+/])[A-Za-z0-9+/]{40}(?![A-Za-z0-9+/])/g,
72
+ // PEM private key block
73
+ /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g,
74
+ // Google API key
75
+ /AIza[0-9A-Za-z\-_]{35}/g,
76
+ // Generic bearer token (require 20+ char token to avoid redacting "Bearer with JWT")
77
+ /Bearer\s+[a-zA-Z0-9\-._~+/]{20,}=*/g
78
+ ];
79
+ var MIN_LENGTH = 40;
80
+ var ENTROPY_THRESHOLD = 4.5;
81
+ function shannonEntropy(str) {
82
+ const freq = /* @__PURE__ */ new Map();
83
+ for (const ch of str) {
84
+ freq.set(ch, (freq.get(ch) ?? 0) + 1);
85
+ }
86
+ let entropy = 0;
87
+ const len = str.length;
88
+ for (const count of freq.values()) {
89
+ const p = count / len;
90
+ entropy -= p * Math.log2(p);
91
+ }
92
+ return entropy;
93
+ }
94
+ function findHighEntropyStrings(text) {
95
+ const results = [];
96
+ const regex = /[a-zA-Z0-9+/=]{40,}/g;
97
+ let match;
98
+ match = regex.exec(text);
99
+ while (match !== null) {
100
+ const str = match[0];
101
+ if (str.length >= MIN_LENGTH) {
102
+ const entropy = shannonEntropy(str);
103
+ if (entropy >= ENTROPY_THRESHOLD) {
104
+ results.push(str);
105
+ }
106
+ }
107
+ match = regex.exec(text);
108
+ }
109
+ return results;
110
+ }
111
+ function redact(text) {
112
+ let result = text;
113
+ let redacted = false;
114
+ for (const pattern of SECRET_PATTERNS) {
115
+ const before = result;
116
+ result = result.replace(pattern, "[REDACTED]");
117
+ if (result !== before) redacted = true;
118
+ }
119
+ const highEntropy = findHighEntropyStrings(result);
120
+ for (const str of highEntropy) {
121
+ result = result.replace(str, "[REDACTED]");
122
+ redacted = true;
123
+ }
124
+ return { text: result, redacted };
125
+ }
126
+
127
+ // src/storage/sqlite.ts
128
+ init_esm_shims();
129
+ import { createHash } from "crypto";
130
+ import { copyFileSync, existsSync, mkdirSync, readFileSync } from "fs";
131
+ import { dirname, join } from "path";
132
+ import { fileURLToPath } from "url";
133
+ import Database from "better-sqlite3";
134
+ import * as sqliteVec from "sqlite-vec";
135
+
136
+ // src/utils/rrf.ts
137
+ init_esm_shims();
138
+ var RRF_K = 40;
139
+ var VEC_WEIGHT = 2;
140
+ function rrfMerge(bm25Results, vecResults, limit) {
141
+ const scores = /* @__PURE__ */ new Map();
142
+ const sortedBm25 = [...bm25Results].sort((a, b) => a.score - b.score);
143
+ sortedBm25.forEach((r, i) => {
144
+ const rank = i + 1;
145
+ const rrfScore = 1 / (RRF_K + rank);
146
+ scores.set(r.id, (scores.get(r.id) ?? 0) + rrfScore);
147
+ });
148
+ const sortedVec = [...vecResults].sort((a, b) => a.score - b.score);
149
+ sortedVec.forEach((r, i) => {
150
+ const rank = i + 1;
151
+ const rrfScore = 1 / (RRF_K + rank) * VEC_WEIGHT;
152
+ scores.set(r.id, (scores.get(r.id) ?? 0) + rrfScore);
153
+ });
154
+ return [...scores.entries()].map(([id, score]) => ({ id, score })).sort((a, b) => b.score - a.score).slice(0, limit);
155
+ }
156
+
157
+ // src/storage/sqlite.ts
158
+ var __dirname2 = dirname(fileURLToPath(import.meta.url));
159
+ var CURRENT_SCHEMA_VERSION = 6;
160
+ var SQLiteBackend = class {
161
+ db;
162
+ /** Get the underlying database instance (for CodeGraph/Wiki operations). */
163
+ getDatabase() {
164
+ return this.db;
165
+ }
166
+ constructor(dbPath) {
167
+ const dir = dirname(dbPath);
168
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
169
+ let readonly = false;
170
+ try {
171
+ this.db = new Database(dbPath);
172
+ this.db.pragma("journal_mode = WAL");
173
+ } catch {
174
+ readonly = true;
175
+ this.db = new Database(dbPath, { readonly: true });
176
+ }
177
+ try {
178
+ this.db.pragma("busy_timeout = 5000");
179
+ } catch {
180
+ }
181
+ if (!readonly) {
182
+ this.db.pragma("synchronous = NORMAL");
183
+ this.db.pragma("foreign_keys = OFF");
184
+ }
185
+ sqliteVec.load(this.db);
186
+ if (!readonly) {
187
+ try {
188
+ this.detectAndMigrate(dbPath);
189
+ } catch {
190
+ }
191
+ }
192
+ }
193
+ /**
194
+ * Detect the database state and run the correct migration path.
195
+ */
196
+ detectAndMigrate(dbPath) {
197
+ const hasVersionTable = this.db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='schema_version'").get();
198
+ if (!hasVersionTable) {
199
+ const tables = this.db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'").all();
200
+ if (tables.length === 0) {
201
+ this.runSchema();
202
+ this.writeSchemaVersion(CURRENT_SCHEMA_VERSION);
203
+ } else {
204
+ this.backupDatabase(dbPath);
205
+ this.migrateV1ToV2();
206
+ this.migrateV2ToV3();
207
+ this.migrateV3ToV4();
208
+ this.migrateV4ToV5();
209
+ this.migrateV5ToV6();
210
+ this.runSchema();
211
+ this.writeSchemaVersion(CURRENT_SCHEMA_VERSION);
212
+ }
213
+ return;
214
+ }
215
+ const row = this.db.prepare("SELECT MAX(version) as version FROM schema_version").get();
216
+ const currentVersion = row?.version ?? 0;
217
+ if (currentVersion < 1) {
218
+ this.backupDatabase(dbPath);
219
+ this.runSchema();
220
+ this.writeSchemaVersion(1);
221
+ }
222
+ if (currentVersion < 2) {
223
+ this.backupDatabase(dbPath);
224
+ this.migrateV1ToV2();
225
+ this.writeSchemaVersion(2);
226
+ }
227
+ if (currentVersion < 3) {
228
+ this.backupDatabase(dbPath);
229
+ this.migrateV2ToV3();
230
+ this.writeSchemaVersion(3);
231
+ }
232
+ if (currentVersion < 4) {
233
+ this.backupDatabase(dbPath);
234
+ this.migrateV3ToV4();
235
+ this.writeSchemaVersion(4);
236
+ }
237
+ if (currentVersion < 5) {
238
+ this.backupDatabase(dbPath);
239
+ this.migrateV4ToV5();
240
+ this.writeSchemaVersion(5);
241
+ }
242
+ if (currentVersion < 6) {
243
+ this.runSchema();
244
+ this.migrateV5ToV6();
245
+ this.writeSchemaVersion(6);
246
+ }
247
+ }
248
+ /** Backup the database to a .bak file. */
249
+ backupDatabase(dbPath) {
250
+ const backupPath = `${dbPath}.bak`;
251
+ try {
252
+ this.db.pragma("wal_checkpoint(FULL)");
253
+ copyFileSync(dbPath, backupPath);
254
+ console.error(`[remem-mcp] Backed up database to ${backupPath}`);
255
+ } catch (err) {
256
+ console.error(`[remem-mcp] Backup failed: ${err}`);
257
+ }
258
+ }
259
+ /** Run the schema.sql file. Idempotent. */
260
+ runSchema() {
261
+ const candidates = [
262
+ join(__dirname2, "storage", "schema.sql"),
263
+ join(__dirname2, "schema.sql"),
264
+ join(__dirname2, "..", "storage", "schema.sql")
265
+ ];
266
+ let schema = null;
267
+ for (const path of candidates) {
268
+ try {
269
+ schema = readFileSync(path, "utf-8");
270
+ break;
271
+ } catch {
272
+ }
273
+ }
274
+ if (!schema) {
275
+ throw new Error("Could not find schema.sql. Make sure the build copied it to dist/storage/.");
276
+ }
277
+ this.db.exec(schema);
278
+ }
279
+ /** Write the schema version to the schema_version table. */
280
+ writeSchemaVersion(version) {
281
+ this.db.prepare("INSERT INTO schema_version (version, applied_at) VALUES (?, ?)").run(version, Date.now());
282
+ }
283
+ /** Migrate schema v1 → v2: add content_hash column + index. */
284
+ migrateV1ToV2() {
285
+ const cols = this.db.prepare("PRAGMA table_info(captures)").all();
286
+ const hasContentHash = cols.some((c) => c.name === "content_hash");
287
+ if (!hasContentHash) {
288
+ this.db.exec("ALTER TABLE captures ADD COLUMN content_hash TEXT");
289
+ console.error("[remem-mcp] Added content_hash column to captures");
290
+ }
291
+ const idxs = this.db.prepare("SELECT name FROM sqlite_master WHERE type='index' AND name='idx_captures_hash'").get();
292
+ if (!idxs) {
293
+ this.db.exec("CREATE INDEX IF NOT EXISTS idx_captures_hash ON captures (content_hash)");
294
+ }
295
+ const rows = this.db.prepare("SELECT id, content FROM captures WHERE content_hash IS NULL").all();
296
+ const stmt = this.db.prepare("UPDATE captures SET content_hash = ? WHERE id = ?");
297
+ for (const row of rows) {
298
+ const hash = createHash("sha256").update(row.content).digest("hex");
299
+ stmt.run(hash, row.id);
300
+ }
301
+ if (rows.length > 0) {
302
+ console.error(`[remem-mcp] Backfilled content_hash for ${rows.length} existing captures`);
303
+ }
304
+ }
305
+ /** Migrate schema v2 → v3: add multi-tenant columns + new tables (messages, knowledge, skills, persona). */
306
+ migrateV2ToV3() {
307
+ const addColumnIfMissing = (table, column, definition) => {
308
+ const tableExists = this.db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name=?").get(table);
309
+ if (!tableExists) return;
310
+ const cols = this.db.prepare(`PRAGMA table_info(${table})`).all();
311
+ if (!cols.some((c) => c.name === column)) {
312
+ this.db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
313
+ }
314
+ };
315
+ addColumnIfMissing("captures", "team_id", "TEXT");
316
+ addColumnIfMissing("captures", "user_id", "TEXT");
317
+ addColumnIfMissing("captures", "task_id", "TEXT");
318
+ addColumnIfMissing("atoms", "team_id", "TEXT");
319
+ addColumnIfMissing("atoms", "agent_id", "TEXT");
320
+ addColumnIfMissing("atoms", "user_id", "TEXT");
321
+ addColumnIfMissing("scenarios", "team_id", "TEXT");
322
+ addColumnIfMissing("scenarios", "agent_id", "TEXT");
323
+ addColumnIfMissing("scenarios", "user_id", "TEXT");
324
+ this.db.exec(`
325
+ CREATE TABLE IF NOT EXISTS messages (
326
+ id TEXT PRIMARY KEY,
327
+ capture_id TEXT NOT NULL REFERENCES captures(id) ON DELETE CASCADE,
328
+ role TEXT NOT NULL,
329
+ content TEXT NOT NULL,
330
+ seq INTEGER NOT NULL,
331
+ created_at INTEGER NOT NULL
332
+ );
333
+ CREATE TABLE IF NOT EXISTS persona (
334
+ team_id TEXT NOT NULL,
335
+ agent_id TEXT NOT NULL,
336
+ user_id TEXT NOT NULL,
337
+ content TEXT NOT NULL,
338
+ updated_at INTEGER NOT NULL,
339
+ PRIMARY KEY (team_id, agent_id, user_id)
340
+ );
341
+ CREATE TABLE IF NOT EXISTS knowledge (
342
+ id TEXT PRIMARY KEY,
343
+ team_id TEXT NOT NULL,
344
+ name TEXT NOT NULL,
345
+ type TEXT NOT NULL,
346
+ summary TEXT,
347
+ service_url TEXT,
348
+ repo_url TEXT,
349
+ branch TEXT,
350
+ created_at INTEGER NOT NULL
351
+ );
352
+ CREATE TABLE IF NOT EXISTS skills (
353
+ id TEXT PRIMARY KEY,
354
+ team_id TEXT NOT NULL,
355
+ agent_id TEXT,
356
+ name TEXT NOT NULL,
357
+ description TEXT,
358
+ content TEXT,
359
+ version INTEGER NOT NULL DEFAULT 1,
360
+ created_at INTEGER NOT NULL,
361
+ updated_at INTEGER NOT NULL
362
+ );
363
+ `);
364
+ console.error(
365
+ "[remem-mcp] Migrated schema v2 \u2192 v3 (multi-tenant + messages + knowledge + skills + persona)"
366
+ );
367
+ }
368
+ /** Migrate schema v3 → v4: add deleted_at column for soft delete (tombstone). */
369
+ migrateV3ToV4() {
370
+ const cols = this.db.prepare("PRAGMA table_info(captures)").all();
371
+ const hasDeletedAt = cols.some((c) => c.name === "deleted_at");
372
+ if (!hasDeletedAt) {
373
+ this.db.exec("ALTER TABLE captures ADD COLUMN deleted_at INTEGER");
374
+ console.error("[remem-mcp] Added deleted_at column to captures (tombstone support)");
375
+ }
376
+ console.error("[remem-mcp] Migrated schema v3 \u2192 v4 (tombstone / soft delete)");
377
+ }
378
+ /** Migrate schema v4 → v5: add trust_state, rejection_reason, superseded_by columns. */
379
+ migrateV4ToV5() {
380
+ const cols = this.db.prepare("PRAGMA table_info(captures)").all();
381
+ const hasTrustState = cols.some((c) => c.name === "trust_state");
382
+ if (!hasTrustState) {
383
+ this.db.exec("ALTER TABLE captures ADD COLUMN trust_state TEXT NOT NULL DEFAULT 'candidate'");
384
+ console.error("[remem-mcp] Added trust_state column to captures");
385
+ }
386
+ const hasRejectionReason = cols.some((c) => c.name === "rejection_reason");
387
+ if (!hasRejectionReason) {
388
+ this.db.exec("ALTER TABLE captures ADD COLUMN rejection_reason TEXT");
389
+ console.error("[remem-mcp] Added rejection_reason column to captures");
390
+ }
391
+ const hasSupersededBy = cols.some((c) => c.name === "superseded_by");
392
+ if (!hasSupersededBy) {
393
+ this.db.exec("ALTER TABLE captures ADD COLUMN superseded_by TEXT REFERENCES captures(id)");
394
+ console.error("[remem-mcp] Added superseded_by column to captures");
395
+ }
396
+ this.db.exec("CREATE INDEX IF NOT EXISTS idx_captures_trust ON captures (trust_state)");
397
+ this.db.exec(
398
+ "CREATE INDEX IF NOT EXISTS idx_captures_rejected_hash ON captures (content_hash) WHERE trust_state = 'rejected'"
399
+ );
400
+ console.error("[remem-mcp] Migrated schema v4 \u2192 v5 (trust state + correction)");
401
+ }
402
+ /** Migrate schema v5 → v6: add CodeGraph + Wiki tables (created by runSchema). */
403
+ migrateV5ToV6() {
404
+ console.error("[remem-mcp] Migrated schema v5 \u2192 v6 (CodeGraph + Wiki tables)");
405
+ }
406
+ async put(entry) {
407
+ const contentHash = entry.contentHash ?? createHash("sha256").update(entry.content).digest("hex");
408
+ const stmt = this.db.prepare(`
409
+ INSERT INTO captures (id, session_key, agent_id, type, content, content_hash, tags, created_at, metadata, team_id, user_id, task_id, trust_state)
410
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
411
+ `);
412
+ stmt.run(
413
+ entry.id,
414
+ entry.sessionKey,
415
+ entry.agentId,
416
+ entry.type,
417
+ entry.content,
418
+ contentHash,
419
+ JSON.stringify(entry.tags),
420
+ entry.createdAt,
421
+ entry.metadata ? JSON.stringify(entry.metadata) : null,
422
+ entry.teamId ?? null,
423
+ entry.userId ?? null,
424
+ entry.taskId ?? null,
425
+ entry.trustState ?? "candidate"
426
+ );
427
+ if (entry.messages && entry.messages.length > 0) {
428
+ const msgStmt = this.db.prepare(
429
+ "INSERT INTO messages (id, capture_id, role, content, seq, created_at) VALUES (?, ?, ?, ?, ?, ?)"
430
+ );
431
+ for (let i = 0; i < entry.messages.length; i++) {
432
+ const msg = entry.messages[i];
433
+ msgStmt.run(generateId(), entry.id, msg.role, msg.content, i, entry.createdAt);
434
+ }
435
+ }
436
+ }
437
+ async putVector(id, embedding) {
438
+ const buffer = new Float32Array(embedding);
439
+ const stmt = this.db.prepare("INSERT INTO captures_vec (id, embedding) VALUES (?, ?)");
440
+ stmt.run(id, Buffer.from(buffer.buffer));
441
+ }
442
+ async get(id) {
443
+ const row = this.db.prepare("SELECT * FROM captures WHERE id = ? AND deleted_at IS NULL").get(id);
444
+ if (!row) return null;
445
+ return rowToEntry(row);
446
+ }
447
+ async findRejectedByContentHash(contentHash, sessionKey) {
448
+ let sql = "SELECT * FROM captures WHERE content_hash = ? AND trust_state = 'rejected'";
449
+ const params = [contentHash];
450
+ if (sessionKey) {
451
+ sql += " AND session_key = ?";
452
+ params.push(sessionKey);
453
+ }
454
+ const rows = this.db.prepare(sql).all(...params);
455
+ return rows.map(rowToEntry);
456
+ }
457
+ async getMessages(captureId) {
458
+ const rows = this.db.prepare("SELECT * FROM messages WHERE capture_id = ? ORDER BY seq ASC").all(captureId);
459
+ return rows.map((r) => ({
460
+ id: r.id,
461
+ captureId: r.capture_id,
462
+ role: r.role,
463
+ content: r.content,
464
+ seq: r.seq,
465
+ createdAt: r.created_at
466
+ }));
467
+ }
468
+ async findByContentHash(contentHash, sessionKey) {
469
+ let sql = "SELECT * FROM captures WHERE content_hash = ? AND deleted_at IS NULL";
470
+ const params = [contentHash];
471
+ if (sessionKey) {
472
+ sql += " AND session_key = ?";
473
+ params.push(sessionKey);
474
+ }
475
+ const rows = this.db.prepare(sql).all(...params);
476
+ return rows.map(rowToEntry);
477
+ }
478
+ async listByTags(tags, limit = 50) {
479
+ if (tags.length === 0) return [];
480
+ const tagConditions = tags.map(() => "tags LIKE ?").join(" OR ");
481
+ const sql = `SELECT * FROM captures WHERE (${tagConditions}) AND deleted_at IS NULL AND trust_state != 'rejected' ORDER BY created_at DESC LIMIT ?`;
482
+ const params = [...tags.map((t) => `%"${t}"%`), limit];
483
+ const rows = this.db.prepare(sql).all(...params);
484
+ return rows.map(rowToEntry);
485
+ }
486
+ async search(query, queryEmbedding, opts) {
487
+ const { mode, limit, offset, sessionKey, filters } = opts;
488
+ const temporalIntent = /\b(currently|current|now|latest|new|present|today|active)\b/i.test(query);
489
+ let bm25Results = [];
490
+ let vecResults = [];
491
+ const bm25CandidateLimit = mode === "hybrid" ? limit * 2 : limit * 2;
492
+ const vecCandidateLimit = mode === "hybrid" ? Math.min(Math.max(limit * 10, 100), 1e3) : limit * 2;
493
+ if (mode === "hybrid" || mode === "keyword") {
494
+ bm25Results = this.bm25Search(query, bm25CandidateLimit, sessionKey, filters);
495
+ }
496
+ if ((mode === "hybrid" || mode === "vector") && queryEmbedding) {
497
+ vecResults = this.vectorSearch(queryEmbedding, vecCandidateLimit, sessionKey, filters);
498
+ }
499
+ if (mode === "keyword") {
500
+ return this.fetchEntries(bm25Results, limit, offset, temporalIntent);
501
+ }
502
+ if (mode === "vector") {
503
+ return this.fetchEntries(vecResults, limit, offset, temporalIntent);
504
+ }
505
+ const fused = rrfMerge(bm25Results, vecResults, limit + offset);
506
+ const paged = fused.slice(offset, offset + limit);
507
+ return this.fetchEntriesById(paged, temporalIntent);
508
+ }
509
+ /** Run a BM25 search via FTS5. */
510
+ bm25Search(query, limit, sessionKey, filters) {
511
+ const ftsQuery = this.escapeFtsQuery(query);
512
+ if (!ftsQuery) return [];
513
+ let sql;
514
+ const params = [ftsQuery];
515
+ try {
516
+ sql = `
517
+ SELECT fts.id as id, bm25(captures_fts) as score
518
+ FROM captures_fts fts
519
+ JOIN captures c ON c.id = fts.id
520
+ WHERE captures_fts MATCH ? AND c.deleted_at IS NULL AND c.trust_state != 'rejected'
521
+ `;
522
+ if (sessionKey) {
523
+ sql += " AND c.session_key = ?";
524
+ params.push(sessionKey);
525
+ }
526
+ if (filters?.type) {
527
+ sql += " AND c.type = ?";
528
+ params.push(filters.type);
529
+ }
530
+ if (filters?.agentId) {
531
+ sql += " AND c.agent_id = ?";
532
+ params.push(filters.agentId);
533
+ }
534
+ if (filters?.teamId) {
535
+ sql += " AND c.team_id = ?";
536
+ params.push(filters.teamId);
537
+ }
538
+ if (filters?.userId) {
539
+ sql += " AND c.user_id = ?";
540
+ params.push(filters.userId);
541
+ }
542
+ if (filters?.taskId) {
543
+ sql += " AND c.task_id = ?";
544
+ params.push(filters.taskId);
545
+ }
546
+ if (filters?.tags && filters.tags.length > 0) {
547
+ const tagConditions = filters.tags.map(() => "c.tags LIKE ?").join(" OR ");
548
+ sql += ` AND (${tagConditions})`;
549
+ params.push(...filters.tags.map((t) => `%"${t}"%`));
550
+ }
551
+ if (filters?.dateFrom) {
552
+ sql += " AND c.created_at >= ?";
553
+ params.push(new Date(filters.dateFrom).getTime());
554
+ }
555
+ if (filters?.dateTo) {
556
+ sql += " AND c.created_at <= ?";
557
+ params.push(new Date(filters.dateTo).getTime());
558
+ }
559
+ sql += " ORDER BY score LIMIT ?";
560
+ params.push(limit);
561
+ const rows = this.db.prepare(sql).all(...params);
562
+ return rows.map((r) => ({ id: r.id, score: r.score }));
563
+ } catch (err) {
564
+ if (err instanceof Error && err.message.includes("missing row")) {
565
+ this.db.exec("INSERT INTO captures_fts(captures_fts) VALUES('rebuild')");
566
+ const rows = this.db.prepare(sql).all(...params);
567
+ return rows.map((r) => ({ id: r.id, score: r.score }));
568
+ }
569
+ throw err;
570
+ }
571
+ }
572
+ /** Run a vector search via sqlite-vec. */
573
+ vectorSearch(embedding, limit, sessionKey, filters) {
574
+ const buffer = new Float32Array(embedding);
575
+ let sql = `
576
+ SELECT vec.id as id, vec.distance as score
577
+ FROM captures_vec vec
578
+ JOIN captures c ON c.id = vec.id
579
+ WHERE vec.embedding MATCH ? AND vec.k = ? AND c.deleted_at IS NULL AND c.trust_state != 'rejected'
580
+ `;
581
+ const params = [Buffer.from(buffer.buffer), limit];
582
+ if (sessionKey) {
583
+ sql += " AND c.session_key = ?";
584
+ params.push(sessionKey);
585
+ }
586
+ if (filters?.type) {
587
+ sql += " AND c.type = ?";
588
+ params.push(filters.type);
589
+ }
590
+ if (filters?.agentId) {
591
+ sql += " AND c.agent_id = ?";
592
+ params.push(filters.agentId);
593
+ }
594
+ if (filters?.teamId) {
595
+ sql += " AND c.team_id = ?";
596
+ params.push(filters.teamId);
597
+ }
598
+ if (filters?.userId) {
599
+ sql += " AND c.user_id = ?";
600
+ params.push(filters.userId);
601
+ }
602
+ if (filters?.taskId) {
603
+ sql += " AND c.task_id = ?";
604
+ params.push(filters.taskId);
605
+ }
606
+ if (filters?.tags && filters.tags.length > 0) {
607
+ const tagConditions = filters.tags.map(() => "c.tags LIKE ?").join(" OR ");
608
+ sql += ` AND (${tagConditions})`;
609
+ params.push(...filters.tags.map((t) => `%"${t}"%`));
610
+ }
611
+ if (filters?.dateFrom) {
612
+ sql += " AND c.created_at >= ?";
613
+ params.push(new Date(filters.dateFrom).getTime());
614
+ }
615
+ if (filters?.dateTo) {
616
+ sql += " AND c.created_at <= ?";
617
+ params.push(new Date(filters.dateTo).getTime());
618
+ }
619
+ sql += " ORDER BY vec.distance LIMIT ?";
620
+ params.push(limit);
621
+ const rows = this.db.prepare(sql).all(...params);
622
+ return rows.map((r) => ({ id: r.id, score: r.score }));
623
+ }
624
+ /** Fetch capture entries for a list of ranked results. */
625
+ async fetchEntries(results, limit, offset, temporalIntent = false) {
626
+ const paged = results.slice(offset, offset + limit);
627
+ return this.fetchEntriesById(paged, temporalIntent);
628
+ }
629
+ /** Fetch capture entries by ID, preserving the order of the input list. Applies memory decay and trust-state ranking. */
630
+ async fetchEntriesById(results, temporalIntent = false) {
631
+ if (results.length === 0) return [];
632
+ const ids = results.map((r) => r.id);
633
+ const placeholders = ids.map(() => "?").join(",");
634
+ const rows = this.db.prepare(`SELECT * FROM captures WHERE id IN (${placeholders})`).all(...ids);
635
+ const rowMap = new Map(rows.map((r) => [r.id, r]));
636
+ const now = Date.now();
637
+ const HALF_LIFE_MS = 30 * 24 * 60 * 60 * 1e3;
638
+ const TRUST_BOOST = {
639
+ verified: 1.5,
640
+ candidate: 1,
641
+ stale: 0.1,
642
+ rejected: 0
643
+ };
644
+ const recencyWeight = temporalIntent ? 0.5 : 0.3;
645
+ return results.map((r) => {
646
+ const row = rowMap.get(r.id);
647
+ if (!row) return null;
648
+ const ageMs = now - row.created_at;
649
+ const decay = 0.5 ** (ageMs / HALF_LIFE_MS);
650
+ const trustBoost = TRUST_BOOST[row.trust_state ?? "candidate"] ?? 1;
651
+ const decayed = Number.isNaN(r.score) ? 0 : r.score * decay;
652
+ const finalScore = decayed >= 0 ? decayed * trustBoost : decayed / trustBoost;
653
+ const recencyDecayMs = temporalIntent ? 1e3 : 1e4;
654
+ const recencyBias = 1 / (1 + ageMs / recencyDecayMs);
655
+ const biasedScore = finalScore >= 0 ? finalScore * (1 + recencyBias * recencyWeight) : finalScore / (1 + recencyBias * recencyWeight);
656
+ return { entry: rowToEntry(row), score: biasedScore };
657
+ }).filter((r) => r !== null).filter((r) => !temporalIntent || r.entry.trustState !== "stale").sort((a, b) => {
658
+ if (temporalIntent) {
659
+ const timeDiff = b.entry.createdAt - a.entry.createdAt;
660
+ if (Math.abs(timeDiff) > 50) return timeDiff;
661
+ }
662
+ return b.score - a.score;
663
+ });
664
+ }
665
+ /** Escape a query string for FTS5 MATCH.
666
+ * Uses OR semantics with stopword removal for high recall.
667
+ * AND semantics (all tokens must match) is too strict for natural language
668
+ * questions — a 15-word query rarely has every token in a single capture,
669
+ * causing BM25 to return zero results. OR semantics lets BM25 rank by
670
+ * relevance (documents matching more terms rank higher) while still
671
+ * returning partial matches. RRF fusion with weighted vector search
672
+ * filters out BM25 noise.
673
+ */
674
+ escapeFtsQuery(query) {
675
+ const FTS_STOPWORDS = /* @__PURE__ */ new Set([
676
+ "the",
677
+ "a",
678
+ "an",
679
+ "is",
680
+ "are",
681
+ "was",
682
+ "were",
683
+ "be",
684
+ "been",
685
+ "being",
686
+ "have",
687
+ "has",
688
+ "had",
689
+ "do",
690
+ "does",
691
+ "did",
692
+ "will",
693
+ "would",
694
+ "could",
695
+ "should",
696
+ "may",
697
+ "might",
698
+ "must",
699
+ "can",
700
+ "shall",
701
+ "i",
702
+ "you",
703
+ "he",
704
+ "she",
705
+ "it",
706
+ "we",
707
+ "they",
708
+ "me",
709
+ "him",
710
+ "her",
711
+ "us",
712
+ "them",
713
+ "my",
714
+ "your",
715
+ "his",
716
+ "its",
717
+ "our",
718
+ "their",
719
+ "this",
720
+ "that",
721
+ "these",
722
+ "those",
723
+ "and",
724
+ "or",
725
+ "but",
726
+ "not",
727
+ "no",
728
+ "nor",
729
+ "so",
730
+ "yet",
731
+ "in",
732
+ "on",
733
+ "at",
734
+ "to",
735
+ "for",
736
+ "of",
737
+ "with",
738
+ "by",
739
+ "from",
740
+ "as",
741
+ "about",
742
+ "into",
743
+ "through",
744
+ "during",
745
+ "before",
746
+ "after",
747
+ "what",
748
+ "when",
749
+ "where",
750
+ "which",
751
+ "who",
752
+ "how",
753
+ "why",
754
+ "since",
755
+ "because",
756
+ "if",
757
+ "then",
758
+ "than"
759
+ ]);
760
+ const tokens = query.trim().split(/\s+/).filter(Boolean);
761
+ if (tokens.length === 0) return "";
762
+ const filtered = tokens.filter((t) => !FTS_STOPWORDS.has(t.toLowerCase()));
763
+ const finalTokens = filtered.length > 0 ? filtered : tokens;
764
+ return finalTokens.map((t) => `"${t.replace(/"/g, '""')}"`).join(" OR ");
765
+ }
766
+ async delete(id) {
767
+ const now = Date.now();
768
+ const captureCount = this.db.prepare("UPDATE captures SET deleted_at = ? WHERE id = ? AND deleted_at IS NULL").run(now, id).changes;
769
+ if (captureCount > 0) {
770
+ this.db.prepare("DELETE FROM captures_vec WHERE id = ?").run(id);
771
+ const rowid = this.db.prepare("SELECT rowid FROM captures WHERE id = ?").get(id);
772
+ if (rowid) {
773
+ this.db.prepare(
774
+ "INSERT INTO captures_fts(captures_fts, rowid, content, tags, type) VALUES('delete', ?, '', '', '')"
775
+ ).run(rowid.rowid);
776
+ }
777
+ }
778
+ return {
779
+ captures: captureCount,
780
+ atoms: 0,
781
+ scenarios: 0
782
+ };
783
+ }
784
+ async deleteByFilter(filter) {
785
+ let sql = "SELECT id FROM captures WHERE deleted_at IS NULL";
786
+ const params = [];
787
+ if (filter.type) {
788
+ sql += " AND type = ?";
789
+ params.push(filter.type);
790
+ }
791
+ if (filter.dateBefore) {
792
+ sql += " AND created_at < ?";
793
+ params.push(new Date(filter.dateBefore).getTime());
794
+ }
795
+ if (filter.teamId) {
796
+ sql += " AND team_id = ?";
797
+ params.push(filter.teamId);
798
+ }
799
+ if (filter.userId) {
800
+ sql += " AND user_id = ?";
801
+ params.push(filter.userId);
802
+ }
803
+ if (filter.taskId) {
804
+ sql += " AND task_id = ?";
805
+ params.push(filter.taskId);
806
+ }
807
+ if (filter.tags && filter.tags.length > 0) {
808
+ const tagConditions = filter.tags.map(() => "tags LIKE ?").join(" OR ");
809
+ sql += ` AND (${tagConditions})`;
810
+ params.push(...filter.tags.map((t) => `%"${t}"%`));
811
+ }
812
+ const ids = this.db.prepare(sql).all(...params);
813
+ let captures = 0;
814
+ let atoms = 0;
815
+ let scenarios = 0;
816
+ for (const { id } of ids) {
817
+ const result = await this.delete(id);
818
+ captures += result.captures;
819
+ atoms += result.atoms;
820
+ scenarios += result.scenarios;
821
+ }
822
+ return { captures, atoms, scenarios };
823
+ }
824
+ async reject(id, reason) {
825
+ const now = Date.now();
826
+ const captureCount = this.db.prepare(
827
+ "UPDATE captures SET trust_state = 'rejected', rejection_reason = ?, deleted_at = ? WHERE id = ? AND deleted_at IS NULL AND trust_state != 'rejected'"
828
+ ).run(reason, now, id).changes;
829
+ if (captureCount > 0) {
830
+ this.db.prepare("DELETE FROM captures_vec WHERE id = ?").run(id);
831
+ const rowid = this.db.prepare("SELECT rowid FROM captures WHERE id = ?").get(id);
832
+ if (rowid) {
833
+ this.db.prepare(
834
+ "INSERT INTO captures_fts(captures_fts, rowid, content, tags, type) VALUES('delete', ?, '', '', '')"
835
+ ).run(rowid.rowid);
836
+ }
837
+ }
838
+ return { captures: captureCount, atoms: 0, scenarios: 0 };
839
+ }
840
+ async findConflicts(embedding, sessionKey, threshold) {
841
+ const buffer = new Float32Array(embedding);
842
+ const rows = this.db.prepare(
843
+ `SELECT vec.id as id, vec.distance as distance, c.content as content, c.trust_state as trust_state
844
+ FROM captures_vec vec
845
+ JOIN captures c ON c.id = vec.id
846
+ WHERE vec.embedding MATCH ? AND vec.k = 20
847
+ AND c.deleted_at IS NULL
848
+ AND c.trust_state IN ('candidate', 'verified')
849
+ AND c.session_key = ?
850
+ ORDER BY vec.distance
851
+ LIMIT 10`
852
+ ).all(Buffer.from(buffer.buffer), sessionKey);
853
+ return rows.filter((r) => {
854
+ const cosineDist = r.distance * r.distance / 2;
855
+ return cosineDist < threshold;
856
+ }).map((r) => ({
857
+ id: r.id,
858
+ content: r.content,
859
+ // Return cosine distance (not L2) so the caller gets a meaningful value.
860
+ distance: r.distance * r.distance / 2,
861
+ trustState: r.trust_state
862
+ }));
863
+ }
864
+ async supersede(loserId, winnerId) {
865
+ const updated = this.db.prepare(
866
+ "UPDATE captures SET trust_state = 'stale', superseded_by = ? WHERE id = ? AND deleted_at IS NULL AND trust_state != 'rejected'"
867
+ ).run(winnerId, loserId).changes;
868
+ return { winnerId, loserId, updated };
869
+ }
870
+ async setTrustState(id, state) {
871
+ return this.db.prepare("UPDATE captures SET trust_state = ? WHERE id = ? AND deleted_at IS NULL").run(state, id).changes;
872
+ }
873
+ // ─── L1 atoms ───────────────────────────────────────────────
874
+ async putAtom(atom) {
875
+ this.db.prepare(
876
+ "INSERT INTO atoms (id, capture_id, fact, confidence, created_at, team_id, agent_id, user_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
877
+ ).run(
878
+ atom.id,
879
+ atom.captureId,
880
+ atom.fact,
881
+ atom.confidence,
882
+ atom.createdAt,
883
+ atom.teamId ?? null,
884
+ atom.agentId ?? null,
885
+ atom.userId ?? null
886
+ );
887
+ }
888
+ async listAtoms(opts) {
889
+ let sql = "SELECT * FROM atoms WHERE 1=1";
890
+ const params = [];
891
+ if (opts.teamId) {
892
+ sql += " AND team_id = ?";
893
+ params.push(opts.teamId);
894
+ }
895
+ if (opts.agentId) {
896
+ sql += " AND agent_id = ?";
897
+ params.push(opts.agentId);
898
+ }
899
+ if (opts.userId) {
900
+ sql += " AND user_id = ?";
901
+ params.push(opts.userId);
902
+ }
903
+ if (opts.captureId) {
904
+ sql += " AND capture_id = ?";
905
+ params.push(opts.captureId);
906
+ }
907
+ sql += " ORDER BY created_at DESC LIMIT ? OFFSET ?";
908
+ params.push(opts.limit ?? 20, opts.offset ?? 0);
909
+ const rows = this.db.prepare(sql).all(...params);
910
+ return rows.map((r) => ({
911
+ id: r.id,
912
+ captureId: r.capture_id,
913
+ fact: r.fact,
914
+ confidence: r.confidence,
915
+ createdAt: r.created_at,
916
+ teamId: r.team_id ?? void 0,
917
+ agentId: r.agent_id ?? void 0,
918
+ userId: r.user_id ?? void 0
919
+ }));
920
+ }
921
+ async searchAtoms(query, opts = {}) {
922
+ let sql = "SELECT * FROM atoms WHERE fact LIKE ?";
923
+ const params = [`%${query}%`];
924
+ if (opts.teamId) {
925
+ sql += " AND team_id = ?";
926
+ params.push(opts.teamId);
927
+ }
928
+ if (opts.agentId) {
929
+ sql += " AND agent_id = ?";
930
+ params.push(opts.agentId);
931
+ }
932
+ if (opts.userId) {
933
+ sql += " AND user_id = ?";
934
+ params.push(opts.userId);
935
+ }
936
+ sql += " ORDER BY created_at DESC LIMIT ?";
937
+ params.push(opts.limit ?? 20);
938
+ const rows = this.db.prepare(sql).all(...params);
939
+ return rows.map((r) => ({
940
+ id: r.id,
941
+ captureId: r.capture_id,
942
+ fact: r.fact,
943
+ confidence: r.confidence,
944
+ createdAt: r.created_at,
945
+ teamId: r.team_id ?? void 0,
946
+ agentId: r.agent_id ?? void 0,
947
+ userId: r.user_id ?? void 0
948
+ }));
949
+ }
950
+ // ─── L2 scenarios ───────────────────────────────────────────
951
+ async putScenario(scenario) {
952
+ this.db.prepare(
953
+ "INSERT INTO scenarios (id, atom_ids, summary, persona_tags, created_at, team_id, agent_id, user_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
954
+ ).run(
955
+ scenario.id,
956
+ JSON.stringify(scenario.atomIds),
957
+ scenario.summary,
958
+ scenario.personaTags ? JSON.stringify(scenario.personaTags) : null,
959
+ scenario.createdAt,
960
+ scenario.teamId ?? null,
961
+ scenario.agentId ?? null,
962
+ scenario.userId ?? null
963
+ );
964
+ }
965
+ async listScenarios(opts) {
966
+ let sql = "SELECT * FROM scenarios WHERE 1=1";
967
+ const params = [];
968
+ if (opts.teamId) {
969
+ sql += " AND team_id = ?";
970
+ params.push(opts.teamId);
971
+ }
972
+ if (opts.agentId) {
973
+ sql += " AND agent_id = ?";
974
+ params.push(opts.agentId);
975
+ }
976
+ if (opts.userId) {
977
+ sql += " AND user_id = ?";
978
+ params.push(opts.userId);
979
+ }
980
+ sql += " ORDER BY created_at DESC LIMIT ? OFFSET ?";
981
+ params.push(opts.limit ?? 20, opts.offset ?? 0);
982
+ const rows = this.db.prepare(sql).all(...params);
983
+ return rows.map((r) => ({
984
+ id: r.id,
985
+ atomIds: JSON.parse(r.atom_ids),
986
+ summary: r.summary,
987
+ personaTags: r.persona_tags ? JSON.parse(r.persona_tags) : void 0,
988
+ createdAt: r.created_at,
989
+ teamId: r.team_id ?? void 0,
990
+ agentId: r.agent_id ?? void 0,
991
+ userId: r.user_id ?? void 0
992
+ }));
993
+ }
994
+ async getScenario(id) {
995
+ const row = this.db.prepare("SELECT * FROM scenarios WHERE id = ?").get(id);
996
+ if (!row) return null;
997
+ return {
998
+ id: row.id,
999
+ atomIds: JSON.parse(row.atom_ids),
1000
+ summary: row.summary,
1001
+ personaTags: row.persona_tags ? JSON.parse(row.persona_tags) : void 0,
1002
+ createdAt: row.created_at,
1003
+ teamId: row.team_id ?? void 0,
1004
+ agentId: row.agent_id ?? void 0,
1005
+ userId: row.user_id ?? void 0
1006
+ };
1007
+ }
1008
+ // ─── L3 persona ─────────────────────────────────────────────
1009
+ async readPersona(teamId, agentId, userId) {
1010
+ const row = this.db.prepare("SELECT * FROM persona WHERE team_id = ? AND agent_id = ? AND user_id = ?").get(teamId, agentId, userId);
1011
+ if (!row) return null;
1012
+ return {
1013
+ teamId: row.team_id,
1014
+ agentId: row.agent_id,
1015
+ userId: row.user_id,
1016
+ content: row.content,
1017
+ updatedAt: row.updated_at
1018
+ };
1019
+ }
1020
+ async writePersona(teamId, agentId, userId, content) {
1021
+ this.db.prepare(
1022
+ `INSERT INTO persona (team_id, agent_id, user_id, content, updated_at)
1023
+ VALUES (?, ?, ?, ?, ?)
1024
+ ON CONFLICT(team_id, agent_id, user_id) DO UPDATE SET content = excluded.content, updated_at = excluded.updated_at`
1025
+ ).run(teamId, agentId, userId, content, Date.now());
1026
+ }
1027
+ // ─── Knowledge ──────────────────────────────────────────────
1028
+ async putKnowledge(entry) {
1029
+ this.db.prepare(
1030
+ "INSERT INTO knowledge (id, team_id, name, type, summary, service_url, repo_url, branch, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"
1031
+ ).run(
1032
+ entry.id,
1033
+ entry.teamId,
1034
+ entry.name,
1035
+ entry.type,
1036
+ entry.summary ?? null,
1037
+ entry.serviceUrl ?? null,
1038
+ entry.repoUrl ?? null,
1039
+ entry.branch ?? null,
1040
+ entry.createdAt
1041
+ );
1042
+ }
1043
+ async getKnowledge(id) {
1044
+ const row = this.db.prepare("SELECT * FROM knowledge WHERE id = ?").get(id);
1045
+ if (!row) return null;
1046
+ return knowledgeRowToEntry(row);
1047
+ }
1048
+ async listKnowledge(teamId, type) {
1049
+ let sql = "SELECT * FROM knowledge WHERE team_id = ?";
1050
+ const params = [teamId];
1051
+ if (type) {
1052
+ sql += " AND type = ?";
1053
+ params.push(type);
1054
+ }
1055
+ sql += " ORDER BY created_at DESC";
1056
+ const rows = this.db.prepare(sql).all(...params);
1057
+ return rows.map(knowledgeRowToEntry);
1058
+ }
1059
+ async deleteKnowledge(ids) {
1060
+ if (ids.length === 0) return 0;
1061
+ const placeholders = ids.map(() => "?").join(",");
1062
+ const result = this.db.prepare(`DELETE FROM knowledge WHERE id IN (${placeholders})`).run(...ids);
1063
+ return result.changes;
1064
+ }
1065
+ // ─── Skills ─────────────────────────────────────────────────
1066
+ async putSkill(entry) {
1067
+ this.db.prepare(
1068
+ `INSERT INTO skills (id, team_id, agent_id, name, description, content, version, created_at, updated_at)
1069
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
1070
+ ON CONFLICT(id) DO UPDATE SET name = excluded.name, description = excluded.description, content = excluded.content, version = excluded.version, updated_at = excluded.updated_at`
1071
+ ).run(
1072
+ entry.id,
1073
+ entry.teamId,
1074
+ entry.agentId ?? null,
1075
+ entry.name,
1076
+ entry.description ?? null,
1077
+ entry.content ?? null,
1078
+ entry.version,
1079
+ entry.createdAt,
1080
+ entry.updatedAt
1081
+ );
1082
+ }
1083
+ async getSkill(id) {
1084
+ const row = this.db.prepare("SELECT * FROM skills WHERE id = ?").get(id);
1085
+ if (!row) return null;
1086
+ return skillRowToEntry(row);
1087
+ }
1088
+ async listSkills(teamId, agentId) {
1089
+ let sql = "SELECT * FROM skills WHERE team_id = ?";
1090
+ const params = [teamId];
1091
+ if (agentId) {
1092
+ sql += " AND (agent_id = ? OR agent_id IS NULL)";
1093
+ params.push(agentId);
1094
+ }
1095
+ sql += " ORDER BY updated_at DESC";
1096
+ const rows = this.db.prepare(sql).all(...params);
1097
+ return rows.map(skillRowToEntry);
1098
+ }
1099
+ async searchSkills(teamId, agentId, query, topK) {
1100
+ let sql = "SELECT * FROM skills WHERE team_id = ? AND (agent_id = ? OR agent_id IS NULL) AND (name LIKE ? OR description LIKE ?)";
1101
+ const params = [teamId, agentId, `%${query}%`, `%${query}%`];
1102
+ sql += " ORDER BY updated_at DESC LIMIT ?";
1103
+ params.push(topK ?? 10);
1104
+ const rows = this.db.prepare(sql).all(...params);
1105
+ return rows.map(skillRowToEntry);
1106
+ }
1107
+ close() {
1108
+ this.db.close();
1109
+ }
1110
+ };
1111
+ function rowToEntry(row) {
1112
+ return {
1113
+ id: row.id,
1114
+ sessionKey: row.session_key,
1115
+ agentId: row.agent_id,
1116
+ type: row.type,
1117
+ content: row.content,
1118
+ tags: row.tags ? JSON.parse(row.tags) : [],
1119
+ createdAt: row.created_at,
1120
+ metadata: row.metadata ? JSON.parse(row.metadata) : void 0,
1121
+ teamId: row.team_id ?? void 0,
1122
+ userId: row.user_id ?? void 0,
1123
+ taskId: row.task_id ?? void 0,
1124
+ trustState: row.trust_state ?? "candidate",
1125
+ rejectionReason: row.rejection_reason ?? void 0,
1126
+ supersededBy: row.superseded_by ?? void 0
1127
+ };
1128
+ }
1129
+ function knowledgeRowToEntry(row) {
1130
+ return {
1131
+ id: row.id,
1132
+ teamId: row.team_id,
1133
+ name: row.name,
1134
+ type: row.type,
1135
+ summary: row.summary ?? void 0,
1136
+ serviceUrl: row.service_url ?? void 0,
1137
+ repoUrl: row.repo_url ?? void 0,
1138
+ branch: row.branch ?? void 0,
1139
+ createdAt: row.created_at
1140
+ };
1141
+ }
1142
+ function skillRowToEntry(row) {
1143
+ return {
1144
+ id: row.id,
1145
+ teamId: row.team_id,
1146
+ agentId: row.agent_id ?? void 0,
1147
+ name: row.name,
1148
+ description: row.description ?? void 0,
1149
+ content: row.content ?? void 0,
1150
+ version: row.version,
1151
+ createdAt: row.created_at,
1152
+ updatedAt: row.updated_at
1153
+ };
1154
+ }
1155
+
1156
+ // src/wiki/engine.ts
1157
+ init_esm_shims();
1158
+ import { createHash as createHash2 } from "crypto";
1159
+ import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2, statSync } from "fs";
1160
+ import { basename, extname, join as join2, relative, sep } from "path";
1161
+ function parseFrontmatter(content) {
1162
+ if (!content.startsWith("---")) {
1163
+ return { frontmatter: null, body: content };
1164
+ }
1165
+ const end = content.indexOf("\n---", 3);
1166
+ if (end === -1) {
1167
+ return { frontmatter: null, body: content };
1168
+ }
1169
+ const frontmatter = content.slice(3, end).trim();
1170
+ const body = content.slice(end + 4).replace(/^\n/, "");
1171
+ return { frontmatter, body };
1172
+ }
1173
+ function extractTags(frontmatter) {
1174
+ if (!frontmatter) return null;
1175
+ const match = frontmatter.match(/^tags:\s*(.+)$/m);
1176
+ if (!match) return null;
1177
+ return match[1].replace(/[[\]]/g, "").trim();
1178
+ }
1179
+ function extractTitle(frontmatter, body, fileName) {
1180
+ if (frontmatter) {
1181
+ const titleMatch = frontmatter.match(/^title:\s*(.+)$/m);
1182
+ if (titleMatch) return titleMatch[1].replace(/["']/g, "").trim();
1183
+ }
1184
+ const h1Match = body.match(/^#\s+(.+)$/m);
1185
+ if (h1Match) return h1Match[1].trim();
1186
+ return basename(fileName, extname(fileName));
1187
+ }
1188
+ function extractWikilinks(content) {
1189
+ const links = [];
1190
+ const lines = content.split("\n");
1191
+ const regex = /\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g;
1192
+ for (let i = 0; i < lines.length; i++) {
1193
+ let match;
1194
+ regex.lastIndex = 0;
1195
+ match = regex.exec(lines[i]);
1196
+ while (match !== null) {
1197
+ links.push({
1198
+ target: match[1].trim(),
1199
+ text: (match[2] ?? match[1]).trim(),
1200
+ line: i + 1
1201
+ });
1202
+ match = regex.exec(lines[i]);
1203
+ }
1204
+ }
1205
+ return links;
1206
+ }
1207
+ function extractMarkdownLinks(content) {
1208
+ const links = [];
1209
+ const lines = content.split("\n");
1210
+ const regex = /(?<!!)\[([^\]]+)\]\(([^)]+)\)/g;
1211
+ for (let i = 0; i < lines.length; i++) {
1212
+ let match;
1213
+ regex.lastIndex = 0;
1214
+ match = regex.exec(lines[i]);
1215
+ while (match !== null) {
1216
+ const target = match[2].trim();
1217
+ if (!target.startsWith("http") && !target.startsWith("mailto:")) {
1218
+ links.push({
1219
+ target: target.replace(/\.md$/i, ""),
1220
+ text: match[1].trim(),
1221
+ line: i + 1
1222
+ });
1223
+ }
1224
+ match = regex.exec(lines[i]);
1225
+ }
1226
+ }
1227
+ return links;
1228
+ }
1229
+ function parseMarkdownFile(filePath, repoPath) {
1230
+ const ext = extname(filePath).toLowerCase();
1231
+ if (ext !== ".md" && ext !== ".markdown") return null;
1232
+ let source;
1233
+ try {
1234
+ source = readFileSync2(filePath, "utf-8");
1235
+ } catch {
1236
+ return null;
1237
+ }
1238
+ const relPath = relative(repoPath, filePath).split(sep).join("/");
1239
+ const { frontmatter, body } = parseFrontmatter(source);
1240
+ const title = extractTitle(frontmatter, body, filePath);
1241
+ const tags = extractTags(frontmatter);
1242
+ const contentHash = createHash2("sha256").update(source).digest("hex");
1243
+ const page = {
1244
+ id: generateId(),
1245
+ title,
1246
+ content: body,
1247
+ sourceFile: relPath,
1248
+ section: null,
1249
+ tags,
1250
+ frontmatter,
1251
+ contentHash
1252
+ };
1253
+ const wikilinks = extractWikilinks(body).map((l) => ({ ...l, type: "wikilink" }));
1254
+ const mdlinks = extractMarkdownLinks(body).map((l) => ({ ...l, type: "markdown" }));
1255
+ const links = [...wikilinks, ...mdlinks];
1256
+ return { page, links };
1257
+ }
1258
+ function ingestFile(db, filePath, repoPath, teamId) {
1259
+ const ext = extname(filePath).toLowerCase();
1260
+ if (ext !== ".md" && ext !== ".markdown") {
1261
+ return { file: filePath, pages: 0, links: 0, skipped: true, reason: "not markdown" };
1262
+ }
1263
+ const parsed = parseMarkdownFile(filePath, repoPath);
1264
+ if (!parsed) {
1265
+ return { file: filePath, pages: 0, links: 0, skipped: true, reason: "parse failed" };
1266
+ }
1267
+ const relPath = relative(repoPath, filePath).split(sep).join("/");
1268
+ const now = Date.now();
1269
+ const existing = db.prepare("SELECT id FROM wiki_pages WHERE source_file = ? AND team_id IS ?").get(relPath, teamId);
1270
+ let pageId = parsed.page.id;
1271
+ if (existing) {
1272
+ pageId = existing.id;
1273
+ db.prepare(
1274
+ "UPDATE wiki_pages SET title = ?, content = ?, tags = ?, frontmatter = ?, content_hash = ?, updated_at = ? WHERE id = ?"
1275
+ ).run(
1276
+ parsed.page.title,
1277
+ parsed.page.content,
1278
+ parsed.page.tags,
1279
+ parsed.page.frontmatter,
1280
+ parsed.page.contentHash,
1281
+ now,
1282
+ pageId
1283
+ );
1284
+ db.prepare("DELETE FROM wiki_links WHERE from_page_id = ?").run(pageId);
1285
+ } else {
1286
+ db.prepare(
1287
+ `INSERT INTO wiki_pages (id, title, content, source_file, section, tags, frontmatter, content_hash, team_id, created_at, updated_at)
1288
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
1289
+ ).run(
1290
+ parsed.page.id,
1291
+ parsed.page.title,
1292
+ parsed.page.content,
1293
+ parsed.page.sourceFile,
1294
+ parsed.page.section,
1295
+ parsed.page.tags,
1296
+ parsed.page.frontmatter,
1297
+ parsed.page.contentHash,
1298
+ teamId,
1299
+ now,
1300
+ now
1301
+ );
1302
+ }
1303
+ const linkStmt = db.prepare(
1304
+ "INSERT INTO wiki_links (from_page_id, to_page_id, to_title, link_text, link_type, line) VALUES (?, ?, ?, ?, ?, ?)"
1305
+ );
1306
+ for (const link of parsed.links) {
1307
+ const target = db.prepare("SELECT id FROM wiki_pages WHERE title = ? AND team_id IS ? LIMIT 1").get(link.target, teamId);
1308
+ linkStmt.run(pageId, target?.id ?? null, link.target, link.text, link.type, link.line);
1309
+ }
1310
+ return {
1311
+ file: relPath,
1312
+ pages: 1,
1313
+ links: parsed.links.length,
1314
+ skipped: false
1315
+ };
1316
+ }
1317
+ function ingestDirectory(db, dirPath, repoPath, teamId, maxFiles = 200) {
1318
+ const results = [];
1319
+ const files = [];
1320
+ const walk = (dir) => {
1321
+ if (files.length >= maxFiles) return;
1322
+ let entries;
1323
+ try {
1324
+ entries = readdirSync(dir);
1325
+ } catch {
1326
+ return;
1327
+ }
1328
+ for (const entry of entries) {
1329
+ if (files.length >= maxFiles) return;
1330
+ const fullPath = join2(dir, entry);
1331
+ let stat;
1332
+ try {
1333
+ stat = statSync(fullPath);
1334
+ } catch {
1335
+ continue;
1336
+ }
1337
+ if (stat.isDirectory()) {
1338
+ if (entry.startsWith(".") || entry === "node_modules" || entry === "vendor") continue;
1339
+ walk(fullPath);
1340
+ } else if (stat.isFile()) {
1341
+ const ext = extname(fullPath).toLowerCase();
1342
+ if (ext === ".md" || ext === ".markdown") {
1343
+ files.push(fullPath);
1344
+ }
1345
+ }
1346
+ }
1347
+ };
1348
+ walk(dirPath);
1349
+ for (const file of files) {
1350
+ const result = ingestFile(db, file, repoPath, teamId);
1351
+ results.push(result);
1352
+ }
1353
+ const unresolved = db.prepare("SELECT id, to_title FROM wiki_links WHERE to_page_id IS NULL").all();
1354
+ if (unresolved.length > 0) {
1355
+ const updateStmt = db.prepare("UPDATE wiki_links SET to_page_id = ? WHERE id = ?");
1356
+ for (const u of unresolved) {
1357
+ const target = db.prepare("SELECT id FROM wiki_pages WHERE title = ? AND team_id IS ? LIMIT 1").get(u.to_title, teamId);
1358
+ if (target) {
1359
+ updateStmt.run(target.id, u.id);
1360
+ }
1361
+ }
1362
+ }
1363
+ return results;
1364
+ }
1365
+ function searchWiki(db, query, opts = {}) {
1366
+ const limit = opts.limit ?? 10;
1367
+ let sql = `
1368
+ SELECT w.id, w.title, w.source_file, snippet(wiki_fts, 1, '<b>', '</b>', '...', 20) as snippet
1369
+ FROM wiki_fts f
1370
+ JOIN wiki_pages w ON w.id = f.id
1371
+ WHERE wiki_fts MATCH ?
1372
+ `;
1373
+ const params = [query];
1374
+ if (opts.teamId !== void 0) {
1375
+ sql += " AND w.team_id IS ?";
1376
+ params.push(opts.teamId);
1377
+ }
1378
+ sql += " LIMIT ?";
1379
+ params.push(limit);
1380
+ const rows = db.prepare(sql).all(...params);
1381
+ return rows.map((r) => ({
1382
+ id: r.id,
1383
+ title: r.title,
1384
+ sourceFile: r.source_file,
1385
+ snippet: r.snippet
1386
+ }));
1387
+ }
1388
+ function getWikiPage(db, pageId) {
1389
+ const row = db.prepare("SELECT * FROM wiki_pages WHERE id = ?").get(pageId);
1390
+ if (!row) return null;
1391
+ const page = {
1392
+ id: row.id,
1393
+ title: row.title,
1394
+ content: row.content,
1395
+ sourceFile: row.source_file,
1396
+ section: row.section,
1397
+ tags: row.tags,
1398
+ frontmatter: row.frontmatter,
1399
+ contentHash: row.content_hash
1400
+ };
1401
+ const linkRows = db.prepare("SELECT * FROM wiki_links WHERE from_page_id = ? ORDER BY line").all(pageId);
1402
+ const links = linkRows.map((r) => ({
1403
+ fromPageId: r.from_page_id,
1404
+ toPageId: r.to_page_id,
1405
+ toTitle: r.to_title,
1406
+ linkText: r.link_text,
1407
+ linkType: r.link_type,
1408
+ line: r.line
1409
+ }));
1410
+ const backlinkRows = db.prepare("SELECT * FROM wiki_links WHERE to_page_id = ? ORDER BY line").all(pageId);
1411
+ const backlinks = backlinkRows.map((r) => ({
1412
+ fromPageId: r.from_page_id,
1413
+ toPageId: r.to_page_id,
1414
+ toTitle: r.to_title,
1415
+ linkText: r.link_text,
1416
+ linkType: r.link_type,
1417
+ line: r.line
1418
+ }));
1419
+ return { page, links, backlinks };
1420
+ }
1421
+ function findOutdatedPages(db, repoPath, opts = {}) {
1422
+ const pages = db.prepare("SELECT id, title, source_file, content_hash FROM wiki_pages WHERE team_id IS ?").all(opts.teamId ?? null);
1423
+ const outdated = [];
1424
+ for (const page of pages) {
1425
+ const fullPath = join2(repoPath, page.source_file);
1426
+ if (!existsSync2(fullPath)) {
1427
+ outdated.push({
1428
+ id: page.id,
1429
+ title: page.title,
1430
+ sourceFile: page.source_file,
1431
+ reason: "file deleted"
1432
+ });
1433
+ continue;
1434
+ }
1435
+ try {
1436
+ const content = readFileSync2(fullPath, "utf-8");
1437
+ const hash = createHash2("sha256").update(content).digest("hex");
1438
+ if (hash !== page.content_hash) {
1439
+ outdated.push({
1440
+ id: page.id,
1441
+ title: page.title,
1442
+ sourceFile: page.source_file,
1443
+ reason: "content changed"
1444
+ });
1445
+ }
1446
+ } catch {
1447
+ outdated.push({
1448
+ id: page.id,
1449
+ title: page.title,
1450
+ sourceFile: page.source_file,
1451
+ reason: "read error"
1452
+ });
1453
+ }
1454
+ }
1455
+ return outdated;
1456
+ }
1457
+
1458
+ // src/sdk.ts
1459
+ var Memory = class {
1460
+ storage;
1461
+ embedder;
1462
+ sessionKey;
1463
+ redactSecrets;
1464
+ constructor(opts) {
1465
+ const dbPath = opts?.dbPath ?? join3(homedir(), ".local", "share", "remem-mcp", "memory.db");
1466
+ this.storage = new SQLiteBackend(dbPath);
1467
+ this.embedder = new LocalEmbedder();
1468
+ this.sessionKey = opts?.sessionKey ?? process.env.TDAI_SESSION_KEY ?? createHash3("sha256").update(process.cwd()).digest("hex").slice(0, 16);
1469
+ this.redactSecrets = opts?.redactSecrets ?? true;
1470
+ }
1471
+ /** Capture a memory entry. Returns the ID, or null if duplicate. */
1472
+ async capture(content, type, tags = []) {
1473
+ const { text: redactedContent } = this.redactSecrets ? redact(content) : { text: content };
1474
+ const contentHash = createHash3("sha256").update(redactedContent).digest("hex");
1475
+ const existing = await this.storage.findByContentHash(contentHash, this.sessionKey);
1476
+ if (existing.length > 0) return null;
1477
+ const id = generateId();
1478
+ const entry = {
1479
+ id,
1480
+ sessionKey: this.sessionKey,
1481
+ agentId: "sdk",
1482
+ type,
1483
+ content: redactedContent,
1484
+ tags,
1485
+ createdAt: Date.now()
1486
+ };
1487
+ await this.storage.put(entry);
1488
+ try {
1489
+ const embedding = await this.embedder.embed(redactedContent);
1490
+ await this.storage.putVector(id, embedding);
1491
+ } catch {
1492
+ }
1493
+ return id;
1494
+ }
1495
+ /** Recall relevant memory. */
1496
+ async recall(query, opts) {
1497
+ const limit = Math.min(opts?.limit ?? 10, 50);
1498
+ const mode = opts?.mode ?? "hybrid";
1499
+ let queryEmbedding = null;
1500
+ if (mode === "hybrid" || mode === "vector") {
1501
+ queryEmbedding = await this.embedder.embed(query);
1502
+ }
1503
+ return this.storage.search(query, queryEmbedding, {
1504
+ sessionKey: this.sessionKey,
1505
+ limit,
1506
+ offset: 0,
1507
+ mode
1508
+ });
1509
+ }
1510
+ /** Search with filters. */
1511
+ async search(query, opts) {
1512
+ const limit = Math.min(opts?.limit ?? 20, 100);
1513
+ const mode = opts?.mode ?? "hybrid";
1514
+ let queryEmbedding = null;
1515
+ if (mode === "hybrid" || mode === "vector") {
1516
+ queryEmbedding = await this.embedder.embed(query);
1517
+ }
1518
+ return this.storage.search(query, queryEmbedding, {
1519
+ limit,
1520
+ offset: 0,
1521
+ mode,
1522
+ filters: opts?.filters
1523
+ });
1524
+ }
1525
+ /** Delete a capture by ID. */
1526
+ async forget(id) {
1527
+ return this.storage.delete(id);
1528
+ }
1529
+ /** Create a handoff packet for the next agent session. */
1530
+ async handoff(opts) {
1531
+ const lines = [];
1532
+ lines.push(`# Handoff: ${opts.task}`);
1533
+ lines.push(`Status: ${opts.status}`);
1534
+ lines.push(`Date: ${(/* @__PURE__ */ new Date()).toISOString()}`);
1535
+ lines.push("");
1536
+ lines.push("## Progress");
1537
+ lines.push(opts.progress);
1538
+ lines.push("");
1539
+ if (opts.decisions && opts.decisions.length > 0) {
1540
+ lines.push("## Decisions");
1541
+ for (const d of opts.decisions) lines.push(`- ${d}`);
1542
+ lines.push("");
1543
+ }
1544
+ if (opts.files && opts.files.length > 0) {
1545
+ lines.push("## Files");
1546
+ for (const f of opts.files) lines.push(`- ${f}`);
1547
+ lines.push("");
1548
+ }
1549
+ if (opts.nextSteps && opts.nextSteps.length > 0) {
1550
+ lines.push("## Next steps");
1551
+ opts.nextSteps.forEach((s, i) => {
1552
+ lines.push(`${i + 1}. ${s}`);
1553
+ });
1554
+ lines.push("");
1555
+ }
1556
+ const content = lines.join("\n");
1557
+ const dedupPayload = JSON.stringify({
1558
+ task: opts.task,
1559
+ status: opts.status,
1560
+ progress: opts.progress,
1561
+ decisions: opts.decisions ?? [],
1562
+ files: opts.files ?? [],
1563
+ nextSteps: opts.nextSteps ?? []
1564
+ });
1565
+ const contentHash = createHash3("sha256").update(dedupPayload).digest("hex");
1566
+ const existing = await this.storage.findByContentHash(contentHash, this.sessionKey);
1567
+ if (existing.length > 0) return null;
1568
+ const id = generateId();
1569
+ const entry = {
1570
+ id,
1571
+ sessionKey: this.sessionKey,
1572
+ agentId: "sdk",
1573
+ type: "task",
1574
+ content,
1575
+ tags: ["handoff", `status:${opts.status}`],
1576
+ createdAt: Date.now(),
1577
+ metadata: {
1578
+ handoff: true,
1579
+ task: opts.task,
1580
+ status: opts.status,
1581
+ progress: opts.progress,
1582
+ decisions: opts.decisions ?? [],
1583
+ files: opts.files ?? [],
1584
+ nextSteps: opts.nextSteps ?? []
1585
+ },
1586
+ contentHash
1587
+ };
1588
+ await this.storage.put(entry);
1589
+ try {
1590
+ const embedding = await this.embedder.embed(content);
1591
+ await this.storage.putVector(id, embedding);
1592
+ } catch {
1593
+ }
1594
+ return id;
1595
+ }
1596
+ /** Record an Architecture Decision Record (ADR). */
1597
+ async adr(opts) {
1598
+ const lines = [];
1599
+ lines.push(`# ADR: ${opts.title}`);
1600
+ lines.push(`Date: ${(/* @__PURE__ */ new Date()).toISOString()}`);
1601
+ lines.push("");
1602
+ lines.push("## Context");
1603
+ lines.push(opts.context);
1604
+ lines.push("");
1605
+ lines.push("## Decision");
1606
+ lines.push(opts.decision);
1607
+ lines.push("");
1608
+ const alternatives = Array.isArray(opts.alternatives) ? opts.alternatives : typeof opts.alternatives === "string" && opts.alternatives ? [opts.alternatives] : [];
1609
+ if (alternatives.length > 0) {
1610
+ lines.push("## Alternatives considered");
1611
+ for (const alt of alternatives) lines.push(`- ${alt}`);
1612
+ lines.push("");
1613
+ }
1614
+ if (opts.consequences) {
1615
+ lines.push("## Consequences");
1616
+ lines.push(opts.consequences);
1617
+ lines.push("");
1618
+ }
1619
+ const content = lines.join("\n");
1620
+ const dedupPayload = JSON.stringify({
1621
+ title: opts.title,
1622
+ context: opts.context,
1623
+ decision: opts.decision,
1624
+ alternatives,
1625
+ consequences: opts.consequences ?? ""
1626
+ });
1627
+ const contentHash = createHash3("sha256").update(dedupPayload).digest("hex");
1628
+ const existing = await this.storage.findByContentHash(contentHash, this.sessionKey);
1629
+ if (existing.length > 0) return null;
1630
+ const id = generateId();
1631
+ const entry = {
1632
+ id,
1633
+ sessionKey: this.sessionKey,
1634
+ agentId: "sdk",
1635
+ type: "decision",
1636
+ content,
1637
+ tags: ["adr", ...opts.tags ?? []],
1638
+ createdAt: Date.now(),
1639
+ metadata: {
1640
+ adr: true,
1641
+ title: opts.title,
1642
+ context: opts.context,
1643
+ decision: opts.decision,
1644
+ alternatives,
1645
+ consequences: opts.consequences ?? ""
1646
+ },
1647
+ contentHash
1648
+ };
1649
+ await this.storage.put(entry);
1650
+ try {
1651
+ const embedding = await this.embedder.embed(content);
1652
+ await this.storage.putVector(id, embedding);
1653
+ } catch {
1654
+ }
1655
+ return id;
1656
+ }
1657
+ /** Close the database connection. */
1658
+ close() {
1659
+ this.storage.close();
1660
+ }
1661
+ };
1662
+
1663
+ export {
1664
+ SQLiteBackend,
1665
+ LocalEmbedder,
1666
+ local_exports,
1667
+ init_local,
1668
+ redact,
1669
+ ingestFile,
1670
+ ingestDirectory,
1671
+ searchWiki,
1672
+ getWikiPage,
1673
+ findOutdatedPages,
1674
+ Memory
1675
+ };
1676
+ //# sourceMappingURL=chunk-34TEZ5U4.js.map