remem-mcp 0.6.2 → 0.6.4

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/dist/index.js CHANGED
@@ -13,20 +13,26 @@ import {
13
13
  redact,
14
14
  searchWiki,
15
15
  stripQueryProperNouns
16
- } from "./chunk-ULMM6LKD.js";
16
+ } from "./chunk-X4ECGLYH.js";
17
17
  import {
18
- __dirname,
19
- __toCommonJS,
18
+ engine_exports,
20
19
  findCallees,
21
20
  findCallers,
22
21
  generateId,
23
22
  impactAnalysis,
24
23
  indexDirectory,
25
24
  indexFile,
26
- init_esm_shims,
25
+ init_engine,
26
+ init_ulid,
27
27
  listSymbols,
28
28
  searchSymbols
29
- } from "./chunk-SHLLMMMU.js";
29
+ } from "./chunk-FA324IBN.js";
30
+ import {
31
+ __dirname,
32
+ __require,
33
+ __toCommonJS,
34
+ init_esm_shims
35
+ } from "./chunk-XK4Y53J2.js";
30
36
 
31
37
  // src/index.ts
32
38
  init_esm_shims();
@@ -36,7 +42,7 @@ import { homedir as homedir12 } from "os";
36
42
  import { dirname as dirname10, join as join15 } from "path";
37
43
  import { fileURLToPath as fileURLToPath4 } from "url";
38
44
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
39
- import Database12 from "better-sqlite3";
45
+ import Database13 from "better-sqlite3";
40
46
  import * as sqliteVec4 from "sqlite-vec";
41
47
 
42
48
  // src/artifact.ts
@@ -262,8 +268,93 @@ async function atomsCommand(dbPath, flags) {
262
268
  }
263
269
  }
264
270
 
271
+ // src/cli/consolidate.ts
272
+ init_esm_shims();
273
+ init_ulid();
274
+ async function consolidateCommand(dbPath, flags) {
275
+ const storage = new SQLiteBackend(dbPath);
276
+ try {
277
+ if (flags.list !== void 0) {
278
+ const scenarios = await storage.listScenarios({
279
+ teamId: flags["team-id"],
280
+ limit: flags.limit ? Number(flags.limit) : 20
281
+ });
282
+ if (scenarios.length === 0) {
283
+ console.log("No scenarios found.");
284
+ return;
285
+ }
286
+ console.log(`Scenarios (${scenarios.length}):`);
287
+ for (const s of scenarios) {
288
+ const tags2 = s.personaTags ? ` [${s.personaTags.join(", ")}]` : "";
289
+ console.log(` ${s.id} ${s.atomIds.length} atoms${tags2}`);
290
+ console.log(` ${s.summary}`);
291
+ }
292
+ return;
293
+ }
294
+ if (flags.auto !== void 0) {
295
+ const atoms = await storage.listAtoms({
296
+ teamId: flags["team-id"],
297
+ limit: flags.limit ? Number(flags.limit) : 100
298
+ });
299
+ if (atoms.length < 5) {
300
+ console.log(`Not enough atoms to consolidate (${atoms.length} < 5).`);
301
+ return;
302
+ }
303
+ const groups = /* @__PURE__ */ new Map();
304
+ for (const a of atoms) {
305
+ const words = a.fact.toLowerCase().split(/\s+/).filter((w) => w.length > 3);
306
+ const key = words[0] ?? "misc";
307
+ if (!groups.has(key)) groups.set(key, []);
308
+ groups.get(key).push(a);
309
+ }
310
+ let count = 0;
311
+ for (const [topic, groupAtoms] of groups) {
312
+ if (groupAtoms.length < 3) continue;
313
+ const summary2 = groupAtoms.slice(0, 5).map((a) => a.fact).join("; ");
314
+ const id2 = generateId();
315
+ await storage.putScenario({
316
+ id: id2,
317
+ atomIds: groupAtoms.map((a) => a.id),
318
+ summary: summary2.slice(0, 300),
319
+ personaTags: [topic],
320
+ createdAt: Date.now()
321
+ });
322
+ console.log(` [${topic}] ${groupAtoms.length} atoms \u2192 ${id2}`);
323
+ count++;
324
+ }
325
+ console.log(`
326
+ Created ${count} scenario(s).`);
327
+ return;
328
+ }
329
+ const atomIdsStr = flags["atom-ids"];
330
+ const summary = flags.summary;
331
+ if (!atomIdsStr || !summary) {
332
+ console.error('Usage: remem-mcp consolidate --atom-ids <id1,id2,...> --summary "..."');
333
+ process.exit(1);
334
+ }
335
+ const atomIds = atomIdsStr.split(",").map((s) => s.trim()).filter(Boolean);
336
+ if (atomIds.length === 0) {
337
+ console.error("Error: No atom IDs provided.");
338
+ process.exit(1);
339
+ }
340
+ const tags = flags.tags ? flags.tags.split(",").map((s) => s.trim()) : void 0;
341
+ const id = generateId();
342
+ await storage.putScenario({
343
+ id,
344
+ atomIds,
345
+ summary,
346
+ personaTags: tags,
347
+ createdAt: Date.now()
348
+ });
349
+ console.log(`Consolidated ${atomIds.length} atoms into scenario ${id}.`);
350
+ } finally {
351
+ storage.close();
352
+ }
353
+ }
354
+
265
355
  // src/cli/demo.ts
266
356
  init_esm_shims();
357
+ init_engine();
267
358
  import { execSync, spawn } from "child_process";
268
359
  import { createHash as createHash2 } from "crypto";
269
360
  import { existsSync as existsSync3, mkdirSync as mkdirSync3, mkdtempSync, readFileSync as readFileSync2, rmSync, writeFileSync } from "fs";
@@ -813,6 +904,7 @@ init_local();
813
904
 
814
905
  // src/pipeline/atom.ts
815
906
  init_esm_shims();
907
+ init_ulid();
816
908
  var AtomPipeline = class {
817
909
  name = "atom";
818
910
  requiresLLM = true;
@@ -869,10 +961,10 @@ var RuleBasedAtomPipeline = class {
869
961
  name = "rule-atom";
870
962
  requiresLLM = false;
871
963
  async process(input, ctx) {
872
- if (input.type !== "conversation") {
964
+ if (!["decision", "learning", "error", "conversation"].includes(input.type)) {
873
965
  return {};
874
966
  }
875
- const facts = extractMigrationFacts(input.content);
967
+ const facts = extractRuleBasedFacts(input.content, input.type);
876
968
  if (facts.length === 0) {
877
969
  return {};
878
970
  }
@@ -897,16 +989,50 @@ var RuleBasedAtomPipeline = class {
897
989
  };
898
990
  }
899
991
  };
900
- function extractMigrationFacts(content) {
992
+ function extractRuleBasedFacts(content, type2) {
901
993
  const facts = [];
902
- const fromToPattern = /\bfrom\s+[A-Za-z0-9][A-Za-z0-9._]*(?:\s+[A-Za-z0-9._]+)*\s+to\b/gi;
903
- if (fromToPattern.test(content)) {
904
- const cleaned = content.replace(fromToPattern, "to").replace(/\s+/g, " ").trim();
905
- if (cleaned !== content && cleaned.length > 10) {
906
- facts.push({ text: cleaned, confidence: 0.85 });
994
+ const text = content.trim();
995
+ if (type2 === "decision") {
996
+ const decisionMatch = text.match(/^Decision:\s*(.+)/i);
997
+ if (decisionMatch) {
998
+ const fact = decisionMatch[1].trim();
999
+ if (fact.length > 10) facts.push({ text: fact, confidence: 0.9 });
1000
+ }
1001
+ const choseMatch = text.match(/chose\s+(.+?)\s+over\s+/i);
1002
+ if (choseMatch) {
1003
+ const fact = `Use ${choseMatch[1].trim()}`;
1004
+ if (fact.length > 10) facts.push({ text: fact, confidence: 0.85 });
1005
+ }
1006
+ const useMatch = text.match(/use\s+\S+\s+for\s+\S+/i);
1007
+ if (useMatch && facts.length < 3) {
1008
+ const fact = text.slice(useMatch.index, useMatch.index + useMatch[0].length);
1009
+ if (fact.length > 10) facts.push({ text: fact, confidence: 0.8 });
1010
+ }
1011
+ } else if (type2 === "learning") {
1012
+ const firstSentence = text.split(/[.!?]/)[0].trim();
1013
+ if (firstSentence.length > 15 && firstSentence.length < 200) {
1014
+ facts.push({ text: firstSentence, confidence: 0.8 });
1015
+ }
1016
+ } else if (type2 === "error") {
1017
+ const cmdMatch = text.match(/Command failed:\s*(.+)/i);
1018
+ if (cmdMatch) {
1019
+ const cmd = cmdMatch[1].trim().slice(0, 100);
1020
+ facts.push({ text: `Error: ${cmd} fails`, confidence: 0.85 });
1021
+ }
1022
+ const exitMatch = text.match(/Exit code\s+(\d+)/i);
1023
+ if (exitMatch && facts.length < 3) {
1024
+ facts.push({ text: `Exit code ${exitMatch[1]}`, confidence: 0.7 });
1025
+ }
1026
+ } else if (type2 === "conversation") {
1027
+ const fromToPattern = /\bfrom\s+[A-Za-z0-9][A-Za-z0-9._]*(?:\s+[A-Za-z0-9._]+)*\s+to\b/gi;
1028
+ if (fromToPattern.test(text)) {
1029
+ const cleaned = text.replace(fromToPattern, "to").replace(/\s+/g, " ").trim();
1030
+ if (cleaned !== text && cleaned.length > 10) {
1031
+ facts.push({ text: cleaned, confidence: 0.85 });
1032
+ }
907
1033
  }
908
1034
  }
909
- return facts;
1035
+ return facts.slice(0, 3);
910
1036
  }
911
1037
  function parseFacts(response, sourceId) {
912
1038
  const lines = response.trim().split("\n");
@@ -972,17 +1098,25 @@ var OpenAILLMClient = class {
972
1098
  // src/cli/extract.ts
973
1099
  async function extractCommand(dbPath, flags) {
974
1100
  const apiKey = process.env.REMEM_LLM_API_KEY ?? process.env.OPENAI_API_KEY;
975
- if (!apiKey) {
976
- console.error("Error: Set REMEM_LLM_API_KEY (or OPENAI_API_KEY) to run atom extraction.");
977
- process.exit(1);
978
- }
979
- const baseUrl = process.env.REMEM_LLM_BASE_URL ?? "https://api.openai.com/v1";
980
- const model = process.env.REMEM_LLM_MODEL ?? "gpt-4o-mini";
1101
+ const useLLM = flags.llm !== void 0 && apiKey;
981
1102
  const storage = new SQLiteBackend(dbPath);
982
1103
  try {
983
1104
  const embedder = new LocalEmbedder();
984
- const pipeline = new AtomPipeline();
985
- const llmClient = new OpenAILLMClient({ apiKey, baseUrl, model });
1105
+ let pipeline;
1106
+ let llmClient = null;
1107
+ if (useLLM) {
1108
+ const baseUrl = process.env.REMEM_LLM_BASE_URL ?? "https://api.openai.com/v1";
1109
+ const model = process.env.REMEM_LLM_MODEL ?? "gpt-4o-mini";
1110
+ pipeline = new AtomPipeline();
1111
+ llmClient = new OpenAILLMClient({ apiKey, baseUrl, model });
1112
+ console.log("Mode: LLM-based extraction (gpt-4o-mini)");
1113
+ } else {
1114
+ pipeline = new RuleBasedAtomPipeline();
1115
+ console.log("Mode: rule-based extraction (no LLM, zero cost)");
1116
+ if (flags.llm !== void 0 && !apiKey) {
1117
+ console.log(" (--llm requested but no API key found, falling back to rule-based)");
1118
+ }
1119
+ }
986
1120
  const limit = flags.limit ? Number(flags.limit) : 50;
987
1121
  const captureId = flags["capture-id"];
988
1122
  let captures;
@@ -990,29 +1124,31 @@ async function extractCommand(dbPath, flags) {
990
1124
  const entry = await storage.get(captureId);
991
1125
  captures = entry ? [entry] : [];
992
1126
  } else {
993
- const results = await storage.search("", null, {
994
- limit,
995
- offset: 0,
996
- mode: "keyword",
997
- filters: flags["team-id"] ? { teamId: flags["team-id"] } : void 0
998
- });
999
- captures = results.map((r) => r.entry).filter((e) => ["decision", "learning", "error"].includes(e.type));
1127
+ const allCaptures = await storage.listAll(limit, 0);
1128
+ captures = allCaptures.filter((e) => ["decision", "learning", "error", "conversation"].includes(e.type));
1000
1129
  }
1001
1130
  if (captures.length === 0) {
1002
1131
  console.log("No captures to extract atoms from.");
1003
1132
  return;
1004
1133
  }
1005
- console.log(`Extracting atoms from ${captures.length} capture(s)...`);
1134
+ console.log(`Extracting atoms from ${captures.length} capture(s)...
1135
+ `);
1006
1136
  const ctx = {
1007
- llmClient,
1137
+ llmClient: llmClient ?? void 0,
1008
1138
  storage,
1009
1139
  embedder,
1010
1140
  sessionKey: ""
1011
1141
  };
1012
1142
  let totalAtoms = 0;
1013
1143
  let errors2 = 0;
1144
+ let skipped = 0;
1014
1145
  for (const capture of captures) {
1015
1146
  try {
1147
+ const existing = await storage.listAtoms({ captureId: capture.id, limit: 1 });
1148
+ if (existing.length > 0) {
1149
+ skipped++;
1150
+ continue;
1151
+ }
1016
1152
  const output2 = await pipeline.process(
1017
1153
  {
1018
1154
  id: capture.id,
@@ -1028,17 +1164,21 @@ async function extractCommand(dbPath, flags) {
1028
1164
  );
1029
1165
  const atomCount = output2.atoms?.length ?? 0;
1030
1166
  totalAtoms += atomCount;
1031
- console.log(` ${capture.id}: ${atomCount} atom(s)`);
1167
+ if (atomCount > 0) {
1168
+ console.log(` [${capture.type}] ${capture.id}: ${atomCount} atom(s)`);
1169
+ for (const a of output2.atoms ?? []) {
1170
+ console.log(` \u2192 ${a.fact.slice(0, 80)}`);
1171
+ }
1172
+ }
1032
1173
  } catch (err) {
1033
1174
  errors2++;
1034
1175
  console.error(` ${capture.id}: FAILED \u2014 ${err}`);
1035
1176
  }
1036
1177
  }
1037
1178
  console.log(`
1038
- Done. Extracted ${totalAtoms} atom(s) from ${captures.length} capture(s).`);
1039
- if (errors2 > 0) {
1040
- console.log(`${errors2} capture(s) failed.`);
1041
- }
1179
+ Done. Extracted ${totalAtoms} atom(s) from ${captures.length - skipped} capture(s).`);
1180
+ if (skipped > 0) console.log(`${skipped} capture(s) already had atoms (skipped).`);
1181
+ if (errors2 > 0) console.log(`${errors2} capture(s) failed.`);
1042
1182
  } finally {
1043
1183
  storage.close();
1044
1184
  }
@@ -1046,6 +1186,7 @@ Done. Extracted ${totalAtoms} atom(s) from ${captures.length} capture(s).`);
1046
1186
 
1047
1187
  // src/cli/knowledge.ts
1048
1188
  init_esm_shims();
1189
+ init_ulid();
1049
1190
  async function knowledgeCommand(dbPath, flags) {
1050
1191
  const storage = new SQLiteBackend(dbPath);
1051
1192
  try {
@@ -1102,28 +1243,55 @@ async function knowledgeCommand(dbPath, flags) {
1102
1243
 
1103
1244
  // src/cli/persona.ts
1104
1245
  init_esm_shims();
1246
+ import Database3 from "better-sqlite3";
1105
1247
  async function personaCommand(dbPath, flags) {
1106
- const teamId = flags["team-id"];
1107
- const agentId = flags["agent-id"];
1108
- const userId = flags["user-id"];
1109
- if (!teamId || !agentId || !userId) {
1110
- console.error("Error: --team-id, --agent-id, and --user-id are required.");
1111
- process.exit(1);
1112
- }
1113
1248
  const storage = new SQLiteBackend(dbPath);
1114
1249
  try {
1115
- if (flags.write) {
1116
- await storage.writePersona(teamId, agentId, userId, flags.write);
1117
- console.log(`Persona written for team=${teamId} agent=${agentId} user=${userId}.`);
1250
+ const teamId = flags["team-id"] ?? "default";
1251
+ const agentId = flags["agent-id"] ?? "unknown";
1252
+ const userId = flags["user-id"] ?? "default";
1253
+ if (flags.clear !== void 0) {
1254
+ await storage.writePersona(teamId, agentId, userId, "");
1255
+ console.log("Persona cleared.");
1118
1256
  return;
1119
1257
  }
1120
- const persona = await storage.readPersona(teamId, agentId, userId);
1121
- if (!persona) {
1122
- console.log("No persona found. Use --write <content> to create one.");
1258
+ if (flags.set) {
1259
+ const existing = await storage.readPersona(teamId, agentId, userId);
1260
+ const entry = flags.set;
1261
+ const [trait] = entry.split(":");
1262
+ let content;
1263
+ if (existing && existing.content) {
1264
+ const lines = existing.content.split("\n").filter((l) => l.trim());
1265
+ const traitPattern = new RegExp(
1266
+ `^${trait.trim().replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}:`,
1267
+ "i"
1268
+ );
1269
+ const idx = lines.findIndex((l) => traitPattern.test(l));
1270
+ if (idx >= 0) {
1271
+ lines[idx] = entry;
1272
+ } else {
1273
+ lines.push(entry);
1274
+ }
1275
+ content = lines.join("\n");
1276
+ } else {
1277
+ content = entry;
1278
+ }
1279
+ await storage.writePersona(teamId, agentId, userId, content);
1280
+ console.log(`Persona updated: ${entry}`);
1123
1281
  return;
1124
1282
  }
1125
- console.log(`Persona (updated ${new Date(persona.updatedAt).toISOString()}):`);
1126
- console.log(persona.content);
1283
+ const db = new Database3(dbPath, { readonly: true });
1284
+ try {
1285
+ const row = db.prepare("SELECT content FROM persona WHERE team_id = ? AND user_id = ? ORDER BY updated_at DESC LIMIT 1").get(teamId, userId);
1286
+ if (!row || !row.content) {
1287
+ console.log('No persona set. Use --set "trait: value" to add one.');
1288
+ return;
1289
+ }
1290
+ console.log("Persona (L3):");
1291
+ console.log(row.content);
1292
+ } finally {
1293
+ db.close();
1294
+ }
1127
1295
  } finally {
1128
1296
  storage.close();
1129
1297
  }
@@ -1206,7 +1374,7 @@ init_esm_shims();
1206
1374
  import { existsSync as existsSync4, statSync } from "fs";
1207
1375
  import { homedir as homedir2 } from "os";
1208
1376
  import { join as join4 } from "path";
1209
- import Database3 from "better-sqlite3";
1377
+ import Database4 from "better-sqlite3";
1210
1378
  function defaultDbPath() {
1211
1379
  return process.env.REMEM_DB_PATH ?? join4(homedir2(), ".local", "share", "remem-mcp", "memory.db");
1212
1380
  }
@@ -1229,7 +1397,7 @@ function status(dbPath = defaultDbPath()) {
1229
1397
  }
1230
1398
  let db;
1231
1399
  try {
1232
- db = new Database3(dbPath, { readonly: true });
1400
+ db = new Database4(dbPath, { readonly: true });
1233
1401
  } catch {
1234
1402
  console.error(" \u2717 Could not open database.");
1235
1403
  console.log(bar + "\n");
@@ -1308,6 +1476,151 @@ Memory: ${total.c} captures across ${sessions.c} project(s)`);
1308
1476
  db.close();
1309
1477
  }
1310
1478
 
1479
+ // src/cli/worker.ts
1480
+ init_esm_shims();
1481
+ init_local();
1482
+
1483
+ // src/pipeline/worker.ts
1484
+ init_esm_shims();
1485
+ init_ulid();
1486
+ async function runPipelineWorker(opts, embedder) {
1487
+ const storage = new SQLiteBackend(opts.dbPath);
1488
+ const result = {
1489
+ capturesProcessed: 0,
1490
+ atomsExtracted: 0,
1491
+ scenariosCreated: 0,
1492
+ personaUpdated: false,
1493
+ errors: []
1494
+ };
1495
+ try {
1496
+ const teamId = opts.teamId ?? "default";
1497
+ const userId = opts.userId ?? "default";
1498
+ const batchSize = opts.batchSize ?? 50;
1499
+ const consolidateThreshold = opts.consolidateThreshold ?? 5;
1500
+ const personaThreshold = opts.personaThreshold ?? 2;
1501
+ const pipeline = new RuleBasedAtomPipeline();
1502
+ const ctx = {
1503
+ storage,
1504
+ embedder,
1505
+ sessionKey: "worker"
1506
+ };
1507
+ const db = storage.db;
1508
+ const captures = db.prepare(
1509
+ `SELECT id, content, type, tags, session_key, team_id, user_id, created_at
1510
+ FROM captures
1511
+ WHERE (team_id = ? OR team_id IS NULL OR team_id = 'default')
1512
+ AND id NOT IN (SELECT DISTINCT capture_id FROM atoms)
1513
+ ORDER BY created_at DESC
1514
+ LIMIT ?`
1515
+ ).all(teamId, batchSize);
1516
+ for (const cap of captures) {
1517
+ try {
1518
+ const input = {
1519
+ id: cap.id,
1520
+ content: cap.content,
1521
+ type: cap.type,
1522
+ tags: cap.tags ? JSON.parse(cap.tags) : [],
1523
+ sessionKey: cap.session_key,
1524
+ teamId: cap.team_id,
1525
+ userId: cap.user_id ?? void 0
1526
+ };
1527
+ const output2 = await pipeline.process(input, ctx);
1528
+ if (output2.atoms) {
1529
+ result.atomsExtracted += output2.atoms.length;
1530
+ }
1531
+ result.capturesProcessed++;
1532
+ } catch (err) {
1533
+ result.errors.push(`capture ${cap.id}: ${err}`);
1534
+ }
1535
+ }
1536
+ const atoms = await storage.listAtoms({ limit: 200 });
1537
+ const groups = /* @__PURE__ */ new Map();
1538
+ for (const a of atoms) {
1539
+ const words = a.fact.toLowerCase().split(/[^a-z0-9]+/).filter((w) => w.length > 3);
1540
+ const key = words.slice(0, 2).join("-") || "misc";
1541
+ if (!groups.has(key)) groups.set(key, []);
1542
+ groups.get(key).push(a);
1543
+ }
1544
+ const existingScenarios = await storage.listScenarios({ limit: 100 });
1545
+ const existingTopics = new Set(
1546
+ existingScenarios.flatMap((s) => s.personaTags ?? [])
1547
+ );
1548
+ for (const [topic, groupAtoms] of groups) {
1549
+ if (groupAtoms.length < consolidateThreshold) continue;
1550
+ if (existingTopics.has(topic)) continue;
1551
+ const summary = groupAtoms.slice(0, 5).map((a) => a.fact).join("; ");
1552
+ const id = generateId();
1553
+ await storage.putScenario({
1554
+ id,
1555
+ atomIds: groupAtoms.map((a) => a.id),
1556
+ summary: summary.slice(0, 300),
1557
+ personaTags: [topic],
1558
+ createdAt: Date.now()
1559
+ });
1560
+ result.scenariosCreated++;
1561
+ }
1562
+ const tagCounts = db.prepare(
1563
+ `SELECT tags FROM captures
1564
+ WHERE (team_id = ? OR team_id IS NULL OR team_id = 'default')
1565
+ ORDER BY created_at DESC LIMIT 100`
1566
+ ).all(teamId);
1567
+ const tagFreq = /* @__PURE__ */ new Map();
1568
+ for (const row of tagCounts) {
1569
+ if (!row.tags) continue;
1570
+ try {
1571
+ const tags = JSON.parse(row.tags);
1572
+ for (const t of tags) {
1573
+ tagFreq.set(t, (tagFreq.get(t) ?? 0) + 1);
1574
+ }
1575
+ } catch {
1576
+ }
1577
+ }
1578
+ const existingPersonaRow = db.prepare(
1579
+ "SELECT content FROM persona WHERE team_id = ? AND user_id = ? ORDER BY updated_at DESC LIMIT 1"
1580
+ ).get(teamId, userId);
1581
+ const existingContent = existingPersonaRow?.content ?? "";
1582
+ const repeatedTags = Array.from(tagFreq.entries()).filter(([tag, count]) => count >= personaThreshold && !existingContent.includes(tag)).sort((a, b) => b[1] - a[1]).slice(0, 3);
1583
+ if (repeatedTags.length > 0) {
1584
+ const newTraits = repeatedTags.map(([tag, count]) => `${tag}: ${count} occurrences`);
1585
+ const updatedContent = existingContent ? `${existingContent}
1586
+ ${newTraits.join("\n")}` : newTraits.join("\n");
1587
+ await storage.writePersona(teamId, "default", userId, updatedContent);
1588
+ result.personaUpdated = true;
1589
+ }
1590
+ } finally {
1591
+ storage.close();
1592
+ }
1593
+ return result;
1594
+ }
1595
+
1596
+ // src/cli/worker.ts
1597
+ async function workerCommand(dbPath, flags) {
1598
+ const embedder = new LocalEmbedder();
1599
+ const result = await runPipelineWorker(
1600
+ {
1601
+ dbPath,
1602
+ batchSize: flags["batch-size"] ? Number(flags["batch-size"]) : 50,
1603
+ teamId: flags["team-id"],
1604
+ userId: flags["user-id"]
1605
+ },
1606
+ embedder
1607
+ );
1608
+ console.log("Pipeline worker results:");
1609
+ console.log(` Captures processed (L0\u2192L1): ${result.capturesProcessed}`);
1610
+ console.log(` Atoms extracted: ${result.atomsExtracted}`);
1611
+ console.log(` Scenarios created (L1\u2192L2): ${result.scenariosCreated}`);
1612
+ console.log(` Persona updated (L2\u2192L3): ${result.personaUpdated ? "yes" : "no"}`);
1613
+ if (result.errors.length > 0) {
1614
+ console.log(` Errors: ${result.errors.length}`);
1615
+ for (const e of result.errors.slice(0, 5)) {
1616
+ console.log(` - ${e}`);
1617
+ }
1618
+ }
1619
+ }
1620
+
1621
+ // src/index.ts
1622
+ init_engine();
1623
+
1311
1624
  // src/config.ts
1312
1625
  init_esm_shims();
1313
1626
  import { existsSync as existsSync5, readFileSync as readFileSync3 } from "fs";
@@ -1422,7 +1735,14 @@ function checkHooksConfig(name, path) {
1422
1735
  (h) => h.hooks?.some((hook) => hook.command?.includes("remem-mcp"))
1423
1736
  );
1424
1737
  const required = ["SessionStart", "Stop"];
1425
- const optional = ["PreToolUse", "PostToolUse", "SessionEnd", "PreCompact", "PostCompaction"];
1738
+ const optional = [
1739
+ "PreToolUse",
1740
+ "PostToolUse",
1741
+ "SessionEnd",
1742
+ "PreCompact",
1743
+ "PostCompact",
1744
+ "PostCompaction"
1745
+ ];
1426
1746
  const missing = required.filter((ev) => !hasTdai(ev));
1427
1747
  const presentOptional = optional.filter((ev) => hasTdai(ev));
1428
1748
  if (missing.length > 0) {
@@ -1532,14 +1852,14 @@ init_local();
1532
1852
  init_esm_shims();
1533
1853
  import { homedir as homedir5 } from "os";
1534
1854
  import { join as join7 } from "path";
1535
- import Database4 from "better-sqlite3";
1855
+ import Database5 from "better-sqlite3";
1536
1856
  function defaultDbPath2() {
1537
1857
  return process.env.REMEM_DB_PATH ?? join7(homedir5(), ".local", "share", "remem-mcp", "memory.db");
1538
1858
  }
1539
1859
  function errors(dbPath = defaultDbPath2()) {
1540
1860
  let db;
1541
1861
  try {
1542
- db = new Database4(dbPath, { readonly: true });
1862
+ db = new Database5(dbPath, { readonly: true });
1543
1863
  } catch {
1544
1864
  console.error("Error: Could not open database at", dbPath);
1545
1865
  process.exit(1);
@@ -1658,7 +1978,7 @@ function errors(dbPath = defaultDbPath2()) {
1658
1978
  function errorsRetro(dbPath = defaultDbPath2()) {
1659
1979
  let db;
1660
1980
  try {
1661
- db = new Database4(dbPath, { readonly: true });
1981
+ db = new Database5(dbPath, { readonly: true });
1662
1982
  } catch {
1663
1983
  console.error("Error: Could not open database at", dbPath);
1664
1984
  process.exit(1);
@@ -2013,7 +2333,7 @@ function errorsRetro(dbPath = defaultDbPath2()) {
2013
2333
  function errorsDrift(dbPath = defaultDbPath2()) {
2014
2334
  let db;
2015
2335
  try {
2016
- db = new Database4(dbPath, { readonly: true });
2336
+ db = new Database5(dbPath, { readonly: true });
2017
2337
  } catch {
2018
2338
  console.error("Error: Could not open database at", dbPath);
2019
2339
  process.exit(1);
@@ -2124,7 +2444,7 @@ function errorsDrift(dbPath = defaultDbPath2()) {
2124
2444
  function errorsLineage(dbPath = defaultDbPath2()) {
2125
2445
  let db;
2126
2446
  try {
2127
- db = new Database4(dbPath, { readonly: true });
2447
+ db = new Database5(dbPath, { readonly: true });
2128
2448
  } catch {
2129
2449
  console.error("Error: Could not open database at", dbPath);
2130
2450
  process.exit(1);
@@ -2293,7 +2613,7 @@ function errorsLineage(dbPath = defaultDbPath2()) {
2293
2613
  function errorsByGoal(dbPath = defaultDbPath2()) {
2294
2614
  let db;
2295
2615
  try {
2296
- db = new Database4(dbPath, { readonly: true });
2616
+ db = new Database5(dbPath, { readonly: true });
2297
2617
  } catch {
2298
2618
  console.error("Error: Could not open database at", dbPath);
2299
2619
  process.exit(1);
@@ -2350,7 +2670,7 @@ function errorsByGoal(dbPath = defaultDbPath2()) {
2350
2670
  function errorsActions(dbPath = defaultDbPath2()) {
2351
2671
  let db;
2352
2672
  try {
2353
- db = new Database4(dbPath, { readonly: true });
2673
+ db = new Database5(dbPath, { readonly: true });
2354
2674
  } catch {
2355
2675
  console.error("Error: Could not open database at", dbPath);
2356
2676
  process.exit(1);
@@ -2471,7 +2791,7 @@ function errorsActions(dbPath = defaultDbPath2()) {
2471
2791
  function errorsSeverity(dbPath = defaultDbPath2()) {
2472
2792
  let db;
2473
2793
  try {
2474
- db = new Database4(dbPath, { readonly: true });
2794
+ db = new Database5(dbPath, { readonly: true });
2475
2795
  } catch {
2476
2796
  console.error("Error: Could not open database at", dbPath);
2477
2797
  process.exit(1);
@@ -2582,7 +2902,7 @@ function errorsSeverity(dbPath = defaultDbPath2()) {
2582
2902
  function errorsTemplates(dbPath = defaultDbPath2()) {
2583
2903
  let db;
2584
2904
  try {
2585
- db = new Database4(dbPath, { readonly: true });
2905
+ db = new Database5(dbPath, { readonly: true });
2586
2906
  } catch {
2587
2907
  console.error("Error: Could not open database at", dbPath);
2588
2908
  process.exit(1);
@@ -2654,7 +2974,7 @@ function errorsTemplates(dbPath = defaultDbPath2()) {
2654
2974
  function errorsCorrelations(dbPath = defaultDbPath2()) {
2655
2975
  let db;
2656
2976
  try {
2657
- db = new Database4(dbPath, { readonly: true });
2977
+ db = new Database5(dbPath, { readonly: true });
2658
2978
  } catch {
2659
2979
  console.error("Error: Could not open database at", dbPath);
2660
2980
  process.exit(1);
@@ -2735,7 +3055,7 @@ function errorsCorrelations(dbPath = defaultDbPath2()) {
2735
3055
  function errorsPlaybooks(dbPath = defaultDbPath2()) {
2736
3056
  let db;
2737
3057
  try {
2738
- db = new Database4(dbPath, { readonly: true });
3058
+ db = new Database5(dbPath, { readonly: true });
2739
3059
  } catch {
2740
3060
  console.error("Error: Could not open database at", dbPath);
2741
3061
  process.exit(1);
@@ -2803,7 +3123,7 @@ function errorsPlaybooks(dbPath = defaultDbPath2()) {
2803
3123
  function errorsStale(dbPath = defaultDbPath2()) {
2804
3124
  let db;
2805
3125
  try {
2806
- db = new Database4(dbPath, { readonly: true });
3126
+ db = new Database5(dbPath, { readonly: true });
2807
3127
  } catch {
2808
3128
  console.error("Error: Could not open database at", dbPath);
2809
3129
  process.exit(1);
@@ -2881,7 +3201,7 @@ function errorsStale(dbPath = defaultDbPath2()) {
2881
3201
  function errorsEscalations(dbPath = defaultDbPath2()) {
2882
3202
  let db;
2883
3203
  try {
2884
- db = new Database4(dbPath, { readonly: true });
3204
+ db = new Database5(dbPath, { readonly: true });
2885
3205
  } catch {
2886
3206
  console.error("Error: Could not open database at", dbPath);
2887
3207
  process.exit(1);
@@ -2975,7 +3295,7 @@ function errorsEscalations(dbPath = defaultDbPath2()) {
2975
3295
  function errorsContext(dbPath = defaultDbPath2()) {
2976
3296
  let db;
2977
3297
  try {
2978
- db = new Database4(dbPath, { readonly: true });
3298
+ db = new Database5(dbPath, { readonly: true });
2979
3299
  } catch {
2980
3300
  console.error("Error: Could not open database at", dbPath);
2981
3301
  process.exit(1);
@@ -3049,7 +3369,7 @@ function errorsContext(dbPath = defaultDbPath2()) {
3049
3369
  function errorsInherited(dbPath = defaultDbPath2()) {
3050
3370
  let db;
3051
3371
  try {
3052
- db = new Database4(dbPath, { readonly: true });
3372
+ db = new Database5(dbPath, { readonly: true });
3053
3373
  } catch {
3054
3374
  console.error("Error: Could not open database at", dbPath);
3055
3375
  process.exit(1);
@@ -3122,7 +3442,7 @@ function errorsInherited(dbPath = defaultDbPath2()) {
3122
3442
  function errorsProvenance(dbPath = defaultDbPath2()) {
3123
3443
  let db;
3124
3444
  try {
3125
- db = new Database4(dbPath, { readonly: true });
3445
+ db = new Database5(dbPath, { readonly: true });
3126
3446
  } catch {
3127
3447
  console.error("Error: Could not open database at", dbPath);
3128
3448
  process.exit(1);
@@ -3198,7 +3518,7 @@ function errorsProvenance(dbPath = defaultDbPath2()) {
3198
3518
  function errorsPersona(dbPath = defaultDbPath2()) {
3199
3519
  let db;
3200
3520
  try {
3201
- db = new Database4(dbPath, { readonly: true });
3521
+ db = new Database5(dbPath, { readonly: true });
3202
3522
  } catch {
3203
3523
  console.error("Error: Could not open database at", dbPath);
3204
3524
  process.exit(1);
@@ -3318,7 +3638,7 @@ function errorsPersona(dbPath = defaultDbPath2()) {
3318
3638
  function decisionsDashboard(dbPath = defaultDbPath2()) {
3319
3639
  let db;
3320
3640
  try {
3321
- db = new Database4(dbPath, { readonly: true });
3641
+ db = new Database5(dbPath, { readonly: true });
3322
3642
  } catch {
3323
3643
  console.error("Error: Could not open database at", dbPath);
3324
3644
  process.exit(1);
@@ -3385,7 +3705,7 @@ ${"\u2500".repeat(60)}`);
3385
3705
  function decisionsRetro(dbPath = defaultDbPath2()) {
3386
3706
  let db;
3387
3707
  try {
3388
- db = new Database4(dbPath, { readonly: true });
3708
+ db = new Database5(dbPath, { readonly: true });
3389
3709
  } catch {
3390
3710
  console.error("Error: Could not open database at", dbPath);
3391
3711
  process.exit(1);
@@ -3446,7 +3766,7 @@ function decisionsRetro(dbPath = defaultDbPath2()) {
3446
3766
  function patternsDashboard(dbPath = defaultDbPath2()) {
3447
3767
  let db;
3448
3768
  try {
3449
- db = new Database4(dbPath, { readonly: true });
3769
+ db = new Database5(dbPath, { readonly: true });
3450
3770
  } catch {
3451
3771
  console.error("Error: Could not open database at", dbPath);
3452
3772
  process.exit(1);
@@ -3521,7 +3841,7 @@ ${"\u2500".repeat(60)}`);
3521
3841
  function patternsRetro(dbPath = defaultDbPath2()) {
3522
3842
  let db;
3523
3843
  try {
3524
- db = new Database4(dbPath, { readonly: true });
3844
+ db = new Database5(dbPath, { readonly: true });
3525
3845
  } catch {
3526
3846
  console.error("Error: Could not open database at", dbPath);
3527
3847
  process.exit(1);
@@ -3575,7 +3895,7 @@ function patternsRetro(dbPath = defaultDbPath2()) {
3575
3895
  function decisionsConflicts(dbPath = defaultDbPath2()) {
3576
3896
  let db;
3577
3897
  try {
3578
- db = new Database4(dbPath, { readonly: true });
3898
+ db = new Database5(dbPath, { readonly: true });
3579
3899
  } catch {
3580
3900
  console.error("Error: Could not open database at", dbPath);
3581
3901
  process.exit(1);
@@ -3619,7 +3939,7 @@ ${"\u2500".repeat(60)}`);
3619
3939
  function decisionsInherited(dbPath = defaultDbPath2()) {
3620
3940
  let db;
3621
3941
  try {
3622
- db = new Database4(dbPath, { readonly: true });
3942
+ db = new Database5(dbPath, { readonly: true });
3623
3943
  } catch {
3624
3944
  console.error("Error: Could not open database at", dbPath);
3625
3945
  process.exit(1);
@@ -3662,7 +3982,7 @@ ${"\u2500".repeat(60)}`);
3662
3982
  function patternsConflicts(dbPath = defaultDbPath2()) {
3663
3983
  let db;
3664
3984
  try {
3665
- db = new Database4(dbPath, { readonly: true });
3985
+ db = new Database5(dbPath, { readonly: true });
3666
3986
  } catch {
3667
3987
  console.error("Error: Could not open database at", dbPath);
3668
3988
  process.exit(1);
@@ -3707,7 +4027,7 @@ ${"\u2500".repeat(60)}`);
3707
4027
  function patternsTemplates(dbPath = defaultDbPath2()) {
3708
4028
  let db;
3709
4029
  try {
3710
- db = new Database4(dbPath, { readonly: true });
4030
+ db = new Database5(dbPath, { readonly: true });
3711
4031
  } catch {
3712
4032
  console.error("Error: Could not open database at", dbPath);
3713
4033
  process.exit(1);
@@ -3759,7 +4079,7 @@ ${"\u2500".repeat(60)}`);
3759
4079
  function patternsInherited(dbPath = defaultDbPath2()) {
3760
4080
  let db;
3761
4081
  try {
3762
- db = new Database4(dbPath, { readonly: true });
4082
+ db = new Database5(dbPath, { readonly: true });
3763
4083
  } catch {
3764
4084
  console.error("Error: Could not open database at", dbPath);
3765
4085
  process.exit(1);
@@ -3806,9 +4126,9 @@ ${"\u2500".repeat(60)}`);
3806
4126
  // src/export.ts
3807
4127
  init_esm_shims();
3808
4128
  import { writeFileSync as writeFileSync2 } from "fs";
3809
- import Database5 from "better-sqlite3";
4129
+ import Database6 from "better-sqlite3";
3810
4130
  function exportData(dbPath, outputPath, filters) {
3811
- const db = new Database5(dbPath, { readonly: true });
4131
+ const db = new Database6(dbPath, { readonly: true });
3812
4132
  let sql = "SELECT * FROM captures";
3813
4133
  const params = [];
3814
4134
  const conditions = [];
@@ -3855,7 +4175,7 @@ function exportData(dbPath, outputPath, filters) {
3855
4175
  // src/export-md.ts
3856
4176
  init_esm_shims();
3857
4177
  import { writeFileSync as writeFileSync3 } from "fs";
3858
- import Database6 from "better-sqlite3";
4178
+ import Database7 from "better-sqlite3";
3859
4179
  var TYPE_ORDER = ["learning", "decision", "error", "task", "conversation", "atom", "pattern"];
3860
4180
  function escapeMd(text) {
3861
4181
  return text.replace(/\n{3,}/g, "\n\n");
@@ -3873,7 +4193,7 @@ function parseTags(raw) {
3873
4193
  }
3874
4194
  }
3875
4195
  function exportMarkdown(dbPath, outputPath, filters) {
3876
- const db = new Database6(dbPath, { readonly: true });
4196
+ const db = new Database7(dbPath, { readonly: true });
3877
4197
  let sql = "SELECT id, session_key, agent_id, type, content, tags, created_at, trust_state FROM captures WHERE deleted_at IS NULL";
3878
4198
  const params = [];
3879
4199
  const conditions = [];
@@ -3956,7 +4276,7 @@ import { createHash as createHash3 } from "crypto";
3956
4276
  import { appendFileSync as appendFileSync2, existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
3957
4277
  import { homedir as homedir6, tmpdir as tmpdir2 } from "os";
3958
4278
  import { dirname as dirname4, join as join8 } from "path";
3959
- import Database7 from "better-sqlite3";
4279
+ import Database8 from "better-sqlite3";
3960
4280
  var SHOW_FEEDBACK = process.env.REMEM_QUIET !== "1";
3961
4281
  function feedback(emoji, message) {
3962
4282
  if (SHOW_FEEDBACK) {
@@ -4225,9 +4545,9 @@ function hookRecall(dbPath) {
4225
4545
  const autoCaptureKey = input.session_id ? input.session_id.slice(0, 16) : void 0;
4226
4546
  let db;
4227
4547
  try {
4228
- db = new Database7(dbPath, { readonly: true, immutable: true });
4548
+ db = new Database8(dbPath, { readonly: true, immutable: true });
4229
4549
  } catch {
4230
- db = new Database7(dbPath, { readonly: true });
4550
+ db = new Database8(dbPath, { readonly: true });
4231
4551
  }
4232
4552
  const errorSql = `
4233
4553
  SELECT id, type, content, tags, created_at
@@ -4260,8 +4580,8 @@ function hookRecall(dbPath) {
4260
4580
  rows.push(...autoOthers.filter((r) => !seen.has(r.id)));
4261
4581
  }
4262
4582
  }
4263
- db.close();
4264
4583
  if (rows.length === 0) {
4584
+ db.close();
4265
4585
  logToFile("SessionStart: no recent memory found");
4266
4586
  process.stdout.write(JSON.stringify({}));
4267
4587
  return;
@@ -4274,6 +4594,34 @@ function hookRecall(dbPath) {
4274
4594
  const content = row.content.length > 200 ? `${row.content.slice(0, 200)}...` : row.content;
4275
4595
  lines.push(`- (${row.type}${tagStr}) ${date}: ${content}`);
4276
4596
  }
4597
+ try {
4598
+ const personaRow = db.prepare("SELECT content FROM persona WHERE team_id = ? AND user_id = ? ORDER BY updated_at DESC LIMIT 1").get("default", "default");
4599
+ if (personaRow && personaRow.content) {
4600
+ lines.push("");
4601
+ lines.push(`## Persona (L3)`);
4602
+ lines.push(personaRow.content);
4603
+ }
4604
+ } catch {
4605
+ }
4606
+ try {
4607
+ const scenarios = db.prepare("SELECT summary FROM scenarios ORDER BY created_at DESC LIMIT 3").all();
4608
+ if (scenarios.length > 0) {
4609
+ lines.push("");
4610
+ lines.push(`## Scenarios (L2)`);
4611
+ for (const s of scenarios) lines.push(`- ${s.summary}`);
4612
+ }
4613
+ } catch {
4614
+ }
4615
+ try {
4616
+ const skills = db.prepare("SELECT name, description FROM skills ORDER BY updated_at DESC LIMIT 3").all();
4617
+ if (skills.length > 0) {
4618
+ lines.push("");
4619
+ lines.push(`## Skills`);
4620
+ for (const s of skills) lines.push(`- ${s.name}: ${(s.description || "").slice(0, 80)}`);
4621
+ }
4622
+ } catch {
4623
+ }
4624
+ db.close();
4277
4625
  lines.push("");
4278
4626
  lines.push("Use these memories to inform your work. Call recall() for more details.");
4279
4627
  lines.push(
@@ -4303,6 +4651,266 @@ ${context}`);
4303
4651
  }
4304
4652
  });
4305
4653
  }
4654
+ function isAckPrompt(prompt2) {
4655
+ const p = prompt2.toLowerCase().trim();
4656
+ if (p.length < 5) return true;
4657
+ const ackPatterns = /^(ok|okay|k|yes|no|yep|nope|thanks|thank you|cool|nice|great|done|got it|understood|continue|go|next|tiếp|tiếp đi|xong|ok rồi|được rồi|đúng|sai|uh|uhm|ừm|👍|✅|❌)\b/i;
4658
+ return ackPatterns.test(p);
4659
+ }
4660
+ function extractUpdateFact(prompt2) {
4661
+ const p = prompt2.trim();
4662
+ if (p.length < 15) return null;
4663
+ const updatePatterns = [
4664
+ // "remember that ..." / "nhớ là ..." / "đừng quên ..."
4665
+ { re: /^(?:remember that|remember|don't forget|dont forget|note that|for future reference)\s*:?\s*(.+)/i, type: "learning" },
4666
+ { re: /^(?:nhớ là|nhớ rằng|đừng quên|ghi nhớ)\s*:?\s*(.+)/i, type: "learning" },
4667
+ // "we use/chose/decided ..." / "mình dùng/chọn/quyết định ..."
4668
+ { re: /^(?:we use|we chose|we decided|we prefer|we always|our convention|our standard)\s*:?\s*(.+)/i, type: "decision" },
4669
+ { re: /^(?:mình dùng|mình chọn|mình quyết định|chúng ta dùng|chúng ta chọn)\s*:?\s*(.+)/i, type: "decision" },
4670
+ // "update X to Y" / "change X to Y" / "sửa X thành Y"
4671
+ { re: /^(?:update|change|set|configure)\s+.+\s+(?:to|as|=\s*)\s*(.+)/i, type: "decision" },
4672
+ { re: /^(?:sửa|đổi|cập nhật)\s+.+\s+(?:thành|sang|=\s*)\s*(.+)/i, type: "decision" },
4673
+ // "actually ..." / "no, it's ..." / "không, là ..." (correction)
4674
+ { re: /^(?:actually|no,?\s*(?:it's|it is|the|this)?\s*:?\s*)(.+)/i, type: "learning" },
4675
+ { re: /^(?:không,?\s*(?:là|nó là|thực ra)?\s*:?\s*)(.+)/i, type: "learning" },
4676
+ // "the port/config/X is Y" / "port là Y" (direct fact statement)
4677
+ { re: /^(?:the\s+)?(\w+\s+(?:is|are|was|should be|must be)\s+.+)/i, type: "learning" },
4678
+ { re: /^(\w+\s+(?:là|phải là|nên là)\s+.+)/i, type: "learning" }
4679
+ ];
4680
+ for (const { re, type: type2 } of updatePatterns) {
4681
+ const match = re.exec(p);
4682
+ if (match) {
4683
+ const fact = match[1]?.trim();
4684
+ if (!fact || fact.length < 10) return null;
4685
+ if (fact.endsWith("?") || fact.endsWith("??")) return null;
4686
+ if (fact.split(/\s+/).length < 2) return null;
4687
+ return { fact, type: type2 };
4688
+ }
4689
+ }
4690
+ return null;
4691
+ }
4692
+ function hookUserPromptSubmit(dbPath) {
4693
+ const chunks = [];
4694
+ process.stdin.setEncoding("utf-8");
4695
+ process.stdin.on("error", () => process.stdout.write(JSON.stringify({})));
4696
+ const stdinTimeout = setTimeout(() => process.stdout.write(JSON.stringify({})), 5e3);
4697
+ stdinTimeout.unref();
4698
+ process.stdin.on("data", (chunk) => {
4699
+ chunks.push(Buffer.from(chunk));
4700
+ });
4701
+ process.stdin.on("end", () => {
4702
+ clearTimeout(stdinTimeout);
4703
+ try {
4704
+ const raw = Buffer.concat(chunks).toString("utf-8");
4705
+ if (!raw.trim()) {
4706
+ process.stdout.write(JSON.stringify({}));
4707
+ return;
4708
+ }
4709
+ const input = JSON.parse(raw);
4710
+ const prompt2 = input.prompt ?? input.user_prompt ?? input.message ?? "";
4711
+ if (!prompt2) {
4712
+ process.stdout.write(JSON.stringify({}));
4713
+ return;
4714
+ }
4715
+ const cwd = input.cwd ?? process.cwd();
4716
+ const sessionKey = hashPath(cwd);
4717
+ const isAck = isAckPrompt(prompt2);
4718
+ const updateFact = isAck ? null : extractUpdateFact(prompt2);
4719
+ if (isAck) {
4720
+ const output2 = {
4721
+ hookSpecificOutput: {
4722
+ hookEventName: "UserPromptSubmit",
4723
+ additionalContext: "[remem-mcp] recall() is available \u2014 call it if you need past project context."
4724
+ }
4725
+ };
4726
+ process.stdout.write(JSON.stringify(output2));
4727
+ return;
4728
+ }
4729
+ let db;
4730
+ try {
4731
+ db = new Database8(dbPath);
4732
+ } catch {
4733
+ try {
4734
+ db = new Database8(dbPath, { readonly: true });
4735
+ } catch {
4736
+ process.stdout.write(JSON.stringify({}));
4737
+ return;
4738
+ }
4739
+ }
4740
+ let captured = false;
4741
+ if (updateFact) {
4742
+ try {
4743
+ const content = updateFact.type === "decision" ? `Decision: ${updateFact.fact}` : updateFact.fact;
4744
+ const contentHash = createHash3("sha256").update(content).digest("hex").slice(0, 16);
4745
+ const existing = db.prepare("SELECT id FROM captures WHERE content_hash = ? AND deleted_at IS NULL").get(contentHash);
4746
+ if (existing) {
4747
+ logToFile(`UserPromptSubmit: duplicate fact (hash match), skipping capture`);
4748
+ } else {
4749
+ const id = generateId2();
4750
+ const now = Date.now();
4751
+ db.prepare(
4752
+ "INSERT INTO captures (id, session_key, agent_id, type, content, content_hash, tags, created_at, metadata) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"
4753
+ ).run(
4754
+ id,
4755
+ sessionKey,
4756
+ "user-prompt",
4757
+ updateFact.type,
4758
+ content,
4759
+ contentHash,
4760
+ JSON.stringify(["auto-capture", "user-prompt"]),
4761
+ now,
4762
+ JSON.stringify({
4763
+ source: "UserPromptSubmit",
4764
+ captured_from: prompt2.slice(0, 200)
4765
+ })
4766
+ );
4767
+ captured = true;
4768
+ logToFile(
4769
+ `UserPromptSubmit: captured ${updateFact.type}: ${updateFact.fact.slice(0, 80)}`
4770
+ );
4771
+ feedback("\u{1F4DD}", `remem-mcp: saved ${updateFact.type} from your prompt`);
4772
+ }
4773
+ } catch (capErr) {
4774
+ logToFile(`UserPromptSubmit: capture error - ${capErr}`);
4775
+ }
4776
+ }
4777
+ let recallContext = "";
4778
+ {
4779
+ const sql = `
4780
+ SELECT fts.id as id, bm25(captures_fts) as score,
4781
+ c.type as type, c.content as content, c.tags as tags,
4782
+ c.created_at as created_at
4783
+ FROM captures_fts fts
4784
+ JOIN captures c ON c.id = fts.id
4785
+ WHERE captures_fts MATCH ?
4786
+ AND c.deleted_at IS NULL
4787
+ AND c.trust_state <> 'rejected'
4788
+ AND c.superseded_by IS NULL
4789
+ AND c.session_key = ?
4790
+ AND c.type IN ('error', 'decision', 'learning', 'task')
4791
+ ORDER BY score
4792
+ LIMIT 5
4793
+ `;
4794
+ const keywords = prompt2.toLowerCase().split(/[^a-z0-9_]+/i).filter((w) => w.length >= 3).slice(0, 8).map((w) => w.replace(/["'*:()]/g, ""));
4795
+ if (keywords.length > 0) {
4796
+ const ftsQuery = keywords.map((w) => `"${w}"`).join(" OR ");
4797
+ let rows = [];
4798
+ try {
4799
+ rows = db.prepare(sql).all(ftsQuery, sessionKey);
4800
+ } catch {
4801
+ }
4802
+ const globalKey = process.env.REMEM_GLOBAL_SESSION_KEY;
4803
+ if (globalKey && globalKey !== sessionKey) {
4804
+ try {
4805
+ const globalRows = db.prepare(sql).all(ftsQuery, globalKey);
4806
+ const seen = new Set(rows.map((r) => r.id));
4807
+ rows.push(...globalRows.filter((r) => !seen.has(r.id)).slice(0, 2));
4808
+ } catch {
4809
+ }
4810
+ }
4811
+ if (rows.length > 0) {
4812
+ const lines = [`[remem-mcp] Memory relevant to your prompt (BM25, shallow):`];
4813
+ for (const row of rows.slice(0, 5)) {
4814
+ const date = new Date(row.created_at).toISOString().split("T")[0];
4815
+ const tags = safeParseTags(row.tags);
4816
+ const tagStr = tags.length > 0 ? ` [${tags.join(", ")}]` : "";
4817
+ const content = row.content.length > 200 ? `${row.content.slice(0, 200)}...` : row.content;
4818
+ lines.push(`- (${row.type}${tagStr}) ${date}: ${content}`);
4819
+ }
4820
+ lines.push("");
4821
+ lines.push(
4822
+ "This is BM25-only (fast, shallow). MUST call recall() for hybrid search (BM25 + vector) with more results and filters."
4823
+ );
4824
+ recallContext = lines.join("\n");
4825
+ logToFile(
4826
+ `UserPromptSubmit: injected ${rows.length} memorie(s) for prompt: ${prompt2.slice(0, 80)}`
4827
+ );
4828
+ feedback("\u{1F4A1}", `remem-mcp: injected ${rows.length} memorie(s) matching your prompt`);
4829
+ }
4830
+ }
4831
+ }
4832
+ try {
4833
+ const scenarios = db.prepare("SELECT summary FROM scenarios ORDER BY created_at DESC LIMIT 3").all();
4834
+ if (scenarios.length > 0) {
4835
+ const scenarioLines = scenarios.map((s) => `- ${s.summary}`);
4836
+ if (recallContext) {
4837
+ recallContext += `
4838
+
4839
+ ## Scenarios (L2 summaries)
4840
+ ${scenarioLines.join("\n")}`;
4841
+ } else {
4842
+ recallContext = `[remem-mcp] Scenarios (L2 summaries):
4843
+ ${scenarioLines.join("\n")}`;
4844
+ }
4845
+ }
4846
+ } catch {
4847
+ }
4848
+ try {
4849
+ const personaRow = db.prepare("SELECT content FROM persona WHERE team_id = ? AND user_id = ? ORDER BY updated_at DESC LIMIT 1").get("default", "default");
4850
+ if (personaRow && personaRow.content) {
4851
+ if (recallContext) {
4852
+ recallContext += `
4853
+
4854
+ ## Persona (L3)
4855
+ ${personaRow.content}`;
4856
+ } else {
4857
+ recallContext = `[remem-mcp] Persona (L3):
4858
+ ${personaRow.content}`;
4859
+ }
4860
+ }
4861
+ } catch {
4862
+ }
4863
+ try {
4864
+ const skills = db.prepare("SELECT name, description FROM skills ORDER BY updated_at DESC LIMIT 3").all();
4865
+ if (skills.length > 0) {
4866
+ const skillLines = skills.map((s) => `- ${s.name}: ${(s.description || "").slice(0, 80)}`);
4867
+ if (recallContext) {
4868
+ recallContext += `
4869
+
4870
+ ## Skills
4871
+ ${skillLines.join("\n")}`;
4872
+ } else {
4873
+ recallContext = `[remem-mcp] Skills:
4874
+ ${skillLines.join("\n")}`;
4875
+ }
4876
+ }
4877
+ } catch {
4878
+ }
4879
+ db.close();
4880
+ if (recallContext || captured) {
4881
+ const parts = [];
4882
+ if (captured) {
4883
+ parts.push(
4884
+ `[remem-mcp] Saved your ${updateFact.type} to memory: "${updateFact.fact.slice(0, 100)}"`
4885
+ );
4886
+ }
4887
+ if (recallContext) {
4888
+ parts.push(recallContext);
4889
+ }
4890
+ const output2 = {
4891
+ hookSpecificOutput: {
4892
+ hookEventName: "UserPromptSubmit",
4893
+ additionalContext: parts.join("\n\n")
4894
+ }
4895
+ };
4896
+ process.stdout.write(JSON.stringify(output2));
4897
+ } else {
4898
+ const output2 = {
4899
+ hookSpecificOutput: {
4900
+ hookEventName: "UserPromptSubmit",
4901
+ additionalContext: "[remem-mcp] No BM25 matches found. Call recall() for hybrid search (BM25 + vector) if you need project context."
4902
+ }
4903
+ };
4904
+ process.stdout.write(JSON.stringify(output2));
4905
+ }
4906
+ } catch (err) {
4907
+ process.stderr.write(`[remem-mcp hook-user-prompt] Error: ${err}
4908
+ `);
4909
+ logToFile(`UserPromptSubmit: error - ${err}`);
4910
+ process.stdout.write(JSON.stringify({}));
4911
+ }
4912
+ });
4913
+ }
4306
4914
  function hookStop(dbPath) {
4307
4915
  const chunks = [];
4308
4916
  process.stdin.setEncoding("utf-8");
@@ -4340,15 +4948,23 @@ function hookStop(dbPath) {
4340
4948
  logToFile(`Stop: capture error - ${err}`);
4341
4949
  });
4342
4950
  } else {
4343
- const scriptPath = process.argv[1];
4951
+ const scriptPath2 = process.argv[1];
4344
4952
  const child = spawn2(
4345
4953
  process.execPath,
4346
- [scriptPath, "--wait-and-capture", dbPath, sid, tpath ?? ""],
4954
+ [scriptPath2, "--wait-and-capture", dbPath, sid, tpath ?? ""],
4347
4955
  { detached: true, stdio: "ignore" }
4348
4956
  );
4349
4957
  child.unref();
4350
4958
  logToFile(`Stop: spawned background capture for session ${sid}`);
4351
4959
  }
4960
+ const scriptPath = process.argv[1];
4961
+ const workerChild = spawn2(
4962
+ process.execPath,
4963
+ [scriptPath, "worker-run", dbPath],
4964
+ { detached: true, stdio: "ignore" }
4965
+ );
4966
+ workerChild.unref();
4967
+ logToFile(`Stop: spawned background pipeline worker for session ${sid}`);
4352
4968
  }
4353
4969
  if (input.stop_hook_active) {
4354
4970
  process.stdout.write(JSON.stringify({}));
@@ -4420,7 +5036,7 @@ function hookPostToolUse(dbPath) {
4420
5036
  }
4421
5037
  if (isWriteEdit) {
4422
5038
  try {
4423
- const db2 = new Database7(dbPath);
5039
+ const db2 = new Database8(dbPath);
4424
5040
  const sessionKey2 = hashPath(input.cwd ?? toolInput.workdir ?? process.cwd());
4425
5041
  const pattern = detectPattern(toolName, toolInput);
4426
5042
  if (pattern) {
@@ -4521,7 +5137,7 @@ function hookPostToolUse(dbPath) {
4521
5137
  try {
4522
5138
  const driftRecords = checkAllDriftInjections(sessionKey2);
4523
5139
  if (driftRecords.length > 0 && pattern) {
4524
- const driftDb = new Database7(dbPath);
5140
+ const driftDb = new Database8(dbPath);
4525
5141
  for (const dr of driftRecords) {
4526
5142
  if (dr.type === "pattern") {
4527
5143
  const injectedPat = driftDb.prepare("SELECT id, metadata FROM captures WHERE id = ?").get(dr.content_hash);
@@ -4570,7 +5186,7 @@ function hookPostToolUse(dbPath) {
4570
5186
  }
4571
5187
  let db;
4572
5188
  try {
4573
- db = new Database7(dbPath);
5189
+ db = new Database8(dbPath);
4574
5190
  } catch {
4575
5191
  process.stdout.write(JSON.stringify({}));
4576
5192
  return;
@@ -4952,7 +5568,7 @@ Error (${errorType}): ${normalizedError}`;
4952
5568
  db.close();
4953
5569
  logToFile(`PostToolUse: auto-captured ${errorType} error. id=${id}`);
4954
5570
  try {
4955
- const corrDb = new Database7(dbPath);
5571
+ const corrDb = new Database8(dbPath);
4956
5572
  const recentErrors = corrDb.prepare(
4957
5573
  `SELECT id, json_extract(metadata, '$.error_type') as etype, json_extract(metadata, '$.title') as title
4958
5574
  FROM captures
@@ -5054,7 +5670,7 @@ function hookPreToolUse(dbPath) {
5054
5670
  const cwd2 = input.cwd ?? toolInput.workdir ?? process.cwd();
5055
5671
  const sessionKey2 = hashPath(cwd2);
5056
5672
  try {
5057
- const predDb = new Database7(dbPath, { readonly: true });
5673
+ const predDb = new Database8(dbPath, { readonly: true });
5058
5674
  const basename2 = filePath.split("/").pop() ?? filePath;
5059
5675
  const fileErrors = predDb.prepare(
5060
5676
  `SELECT
@@ -5119,7 +5735,7 @@ function hookPreToolUse(dbPath) {
5119
5735
  };
5120
5736
  const language = langMap[ext] ?? "";
5121
5737
  if (language) {
5122
- const patDb = new Database7(dbPath, { readonly: true });
5738
+ const patDb = new Database8(dbPath, { readonly: true });
5123
5739
  const patterns = patDb.prepare(
5124
5740
  `SELECT
5125
5741
  json_extract(metadata, '$.title') as title,
@@ -5201,7 +5817,7 @@ function hookPreToolUse(dbPath) {
5201
5817
  const sessionKey = hashPath(cwd);
5202
5818
  let db;
5203
5819
  try {
5204
- db = new Database7(dbPath, { readonly: true });
5820
+ db = new Database8(dbPath, { readonly: true });
5205
5821
  } catch {
5206
5822
  process.stdout.write(JSON.stringify({}));
5207
5823
  return;
@@ -5229,7 +5845,7 @@ function hookPreToolUse(dbPath) {
5229
5845
  db.close();
5230
5846
  let resolvedFixes = [];
5231
5847
  try {
5232
- const rDb = new Database7(dbPath, { readonly: true });
5848
+ const rDb = new Database8(dbPath, { readonly: true });
5233
5849
  const resolved = rDb.prepare(
5234
5850
  `SELECT content, metadata, created_at, session_key FROM captures
5235
5851
  WHERE type = 'error' ${sessionFilter}
@@ -5287,7 +5903,7 @@ function hookPreToolUse(dbPath) {
5287
5903
  }
5288
5904
  if (errors2.length === 0 && resolvedFixes.length === 0) {
5289
5905
  try {
5290
- const decDb = new Database7(dbPath, { readonly: true });
5906
+ const decDb = new Database8(dbPath, { readonly: true });
5291
5907
  const lowerCmd = command.toLowerCase();
5292
5908
  let decQuery = "";
5293
5909
  const decParams = [sessionKey, Date.now() - 90 * 864e5];
@@ -5446,7 +6062,7 @@ function hookPreToolUse(dbPath) {
5446
6062
  lines.push("");
5447
6063
  lines.push("Fix these issues BEFORE running the command.");
5448
6064
  try {
5449
- const decDb = new Database7(dbPath, { readonly: true });
6065
+ const decDb = new Database8(dbPath, { readonly: true });
5450
6066
  const lowerCmd = command.toLowerCase();
5451
6067
  let decQuery = "";
5452
6068
  const decParams = [sessionKey, Date.now() - 90 * 864e5];
@@ -5808,7 +6424,7 @@ function detectPattern(toolName, toolInput) {
5808
6424
  }
5809
6425
  function detectCrossProjectPattern(dbPath, currentSessionKey, errorType, command) {
5810
6426
  try {
5811
- const db = new Database7(dbPath, { readonly: true });
6427
+ const db = new Database8(dbPath, { readonly: true });
5812
6428
  const cmdRoot = extractCommandRoot(command);
5813
6429
  const others = db.prepare(
5814
6430
  `SELECT session_key, content, metadata, created_at FROM captures
@@ -6024,7 +6640,12 @@ async function captureSessionTranscript(dbPath, sessionId, transcriptPath, cwd)
6024
6640
  Task: ${taskText}
6025
6641
  Outcome: ${outcomeText}`;
6026
6642
  const contentHash = createHash3("sha256").update(content).digest("hex");
6027
- const db = new Database7(dbPath);
6643
+ const db = new Database8(dbPath);
6644
+ try {
6645
+ const sqliteVec5 = await import("sqlite-vec");
6646
+ sqliteVec5.load(db);
6647
+ } catch {
6648
+ }
6028
6649
  const sessionKey = hashPath(cwd ?? process.cwd());
6029
6650
  const now = Date.now();
6030
6651
  const id = generateId2();
@@ -6039,11 +6660,15 @@ Outcome: ${outcomeText}`;
6039
6660
  ).all(sessionKey, sid);
6040
6661
  if (stale.length > 0) {
6041
6662
  const delStmt = db.prepare("DELETE FROM captures WHERE id = ?");
6042
- const delVec = db.prepare("DELETE FROM captures_vec WHERE id = ?");
6663
+ let delVec = null;
6664
+ try {
6665
+ delVec = db.prepare("DELETE FROM captures_vec WHERE id = ?");
6666
+ } catch {
6667
+ }
6043
6668
  const delAtoms = db.prepare("DELETE FROM atoms WHERE capture_id = ?");
6044
6669
  for (const row of stale) {
6045
6670
  delStmt.run(row.id);
6046
- delVec.run(row.id);
6671
+ delVec?.run(row.id);
6047
6672
  delAtoms.run(row.id);
6048
6673
  }
6049
6674
  logToFile(`Stop: removed ${stale.length} previous capture(s) for session ${sid}`);
@@ -6067,8 +6692,6 @@ Outcome: ${outcomeText}`;
6067
6692
  })
6068
6693
  );
6069
6694
  try {
6070
- const sqliteVec5 = await import("sqlite-vec");
6071
- sqliteVec5.load(db);
6072
6695
  const { LocalEmbedder: LocalEmbedder2 } = (init_local(), __toCommonJS(local_exports));
6073
6696
  const embedder = new LocalEmbedder2();
6074
6697
  const embedding = await embedder.embed(content);
@@ -6170,8 +6793,8 @@ async function hookPostCommit(dbPath) {
6170
6793
  const { join: join16, dirname: dirname11 } = await import("path");
6171
6794
  const { fileURLToPath: fileURLToPath5 } = await import("url");
6172
6795
  const sqliteVec5 = await import("sqlite-vec");
6173
- const { indexFile: indexFile2 } = await import("./engine-EIOBO7ZE.js");
6174
- const db = new Database7(dbPath);
6796
+ const { indexFile: indexFile2 } = await import("./engine-LVKGZHBY.js");
6797
+ const db = new Database8(dbPath);
6175
6798
  try {
6176
6799
  db.pragma("journal_mode = WAL");
6177
6800
  sqliteVec5.load(db);
@@ -6244,7 +6867,7 @@ function hookPreCompact(dbPath) {
6244
6867
  const input = raw.trim() ? JSON.parse(raw) : {};
6245
6868
  const trigger = input.trigger ?? "unknown";
6246
6869
  const sessionKey = hashPath(input.cwd ?? process.cwd());
6247
- const db = new Database7(dbPath);
6870
+ const db = new Database8(dbPath);
6248
6871
  let transcriptNote = "";
6249
6872
  try {
6250
6873
  db.pragma("journal_mode = WAL");
@@ -6354,7 +6977,7 @@ function hookPostCompaction(dbPath) {
6354
6977
  const globalKey = process.env.REMEM_GLOBAL_SESSION_KEY ?? null;
6355
6978
  const { fileURLToPath: fileURLToPath5 } = await import("url");
6356
6979
  const schemaPath = join8(dirname4(fileURLToPath5(import.meta.url)), "storage", "schema.sql");
6357
- const db = new Database7(dbPath, { readonly: true });
6980
+ const db = new Database8(dbPath, { readonly: true });
6358
6981
  const cutoff = Date.now() - 24 * 60 * 60 * 1e3;
6359
6982
  const sessionKeys = globalKey ? [sessionKey, globalKey] : [sessionKey];
6360
6983
  const placeholders = sessionKeys.map(() => "?").join(",");
@@ -6476,6 +7099,17 @@ var HOOKS_CONFIG = {
6476
7099
  // Devin supports: PreToolUse, PostToolUse, UserPromptSubmit, Stop,
6477
7100
  // PostCompaction, SessionStart, SessionEnd, PermissionRequest.
6478
7101
  // Error capture is handled via PostToolUse with exit code 2.
7102
+ UserPromptSubmit: [
7103
+ {
7104
+ hooks: [
7105
+ {
7106
+ type: "command",
7107
+ command: hookCommand("hook-user-prompt"),
7108
+ timeout: 5
7109
+ }
7110
+ ]
7111
+ }
7112
+ ],
6479
7113
  Stop: [
6480
7114
  {
6481
7115
  hooks: [
@@ -6547,14 +7181,26 @@ function installClaudeCodeHooks() {
6547
7181
  if (!existsSync8(claudeDir)) {
6548
7182
  return false;
6549
7183
  }
7184
+ const { PostCompaction: _omit, ...baseWithoutPostCompaction } = HOOKS_CONFIG;
6550
7185
  const claudeHooksConfig = {
6551
- ...HOOKS_CONFIG,
7186
+ ...baseWithoutPostCompaction,
6552
7187
  PreCompact: [
6553
7188
  {
6554
7189
  hooks: [
6555
7190
  {
6556
7191
  type: "command",
6557
- command: hookCommand("hook-pre-compact"),
7192
+ command: hookCommand("hook-pre-compact"),
7193
+ timeout: 10
7194
+ }
7195
+ ]
7196
+ }
7197
+ ],
7198
+ PostCompact: [
7199
+ {
7200
+ hooks: [
7201
+ {
7202
+ type: "command",
7203
+ command: hookCommand("hook-post-compaction"),
6558
7204
  timeout: 10
6559
7205
  }
6560
7206
  ]
@@ -6617,6 +7263,15 @@ command = "${hookCommand("hook-post-compaction")}"
6617
7263
  timeout = 10
6618
7264
  # <<< remem-mcp PostCompaction <<<
6619
7265
 
7266
+ # >>> remem-mcp UserPromptSubmit >>>
7267
+ [[hooks.UserPromptSubmit]]
7268
+
7269
+ [[hooks.UserPromptSubmit.hooks]]
7270
+ type = "command"
7271
+ command = "${hookCommand("hook-user-prompt")}"
7272
+ timeout = 5
7273
+ # <<< remem-mcp UserPromptSubmit <<<
7274
+
6620
7275
  # >>> remem-mcp Stop >>>
6621
7276
  [[hooks.Stop]]
6622
7277
 
@@ -6656,10 +7311,12 @@ async function installHooks() {
6656
7311
  Hooks wired to ${installed} agent(s).`);
6657
7312
  console.log("\nHooks installed:");
6658
7313
  console.log(" SessionStart \u2192 auto-recall recent memory into agent context");
7314
+ console.log(" UserPromptSubmit \u2192 heuristic recall: inject memory matching user prompt");
6659
7315
  console.log(" PreToolUse \u2192 inject past errors before lint/build/test commands");
6660
7316
  console.log(" PostToolUse \u2192 auto-capture failed commands as error memories");
6661
- console.log(" PreCompact \u2192 save checkpoint before compaction (Claude Code only)");
6662
- console.log(" PostCompaction \u2192 re-inject memory after compaction (all agents)");
7317
+ console.log(" PreCompact \u2192 save checkpoint before compaction (Claude Code only)");
7318
+ console.log(" PostCompact \u2192 re-inject memory after compaction (Claude Code)");
7319
+ console.log(" PostCompaction \u2192 re-inject memory after compaction (Devin CLI, Codex CLI)");
6663
7320
  console.log(" Stop \u2192 auto-capture session transcript + remind to save");
6664
7321
  console.log(" SessionEnd \u2192 silently capture session summary to memory DB");
6665
7322
  console.log("\nRestart your agent for hooks to take effect.");
@@ -6675,7 +7332,9 @@ async function uninstallHooks() {
6675
7332
  "PreToolUse",
6676
7333
  "PostToolUse",
6677
7334
  "PreCompact",
6678
- "PostCompaction"
7335
+ "PostCompact",
7336
+ "PostCompaction",
7337
+ "UserPromptSubmit"
6679
7338
  ];
6680
7339
  const devinPath = join9(homedir7(), ".config", "devin", "config.json");
6681
7340
  if (existsSync8(devinPath)) {
@@ -6750,7 +7409,7 @@ init_esm_shims();
6750
7409
  import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
6751
7410
  import { dirname as dirname6, join as join10 } from "path";
6752
7411
  import { fileURLToPath as fileURLToPath3 } from "url";
6753
- import Database8 from "better-sqlite3";
7412
+ import Database9 from "better-sqlite3";
6754
7413
  import * as sqliteVec3 from "sqlite-vec";
6755
7414
  var __dirname3 = dirname6(fileURLToPath3(import.meta.url));
6756
7415
  function ensureSchema2(db) {
@@ -6789,7 +7448,7 @@ function importData(dbPath, inputPath) {
6789
7448
  console.error("Error: No captures array in the file.");
6790
7449
  process.exit(1);
6791
7450
  }
6792
- const db = new Database8(dbPath);
7451
+ const db = new Database9(dbPath);
6793
7452
  db.pragma("journal_mode = WAL");
6794
7453
  db.pragma("synchronous = NORMAL");
6795
7454
  sqliteVec3.load(db);
@@ -7025,16 +7684,6 @@ Skill installed to ${installed} location(s).`);
7025
7684
  console.log(" - Forget only on explicit user request");
7026
7685
  }
7027
7686
 
7028
- // src/pipeline/noop.ts
7029
- init_esm_shims();
7030
- var NoopPipeline = class {
7031
- name = "noop";
7032
- requiresLLM = false;
7033
- async process(_input, _ctx) {
7034
- return {};
7035
- }
7036
- };
7037
-
7038
7687
  // src/security/audit.ts
7039
7688
  init_esm_shims();
7040
7689
  import { createHash as createHash4 } from "crypto";
@@ -7075,6 +7724,7 @@ var AuditLogger = class {
7075
7724
 
7076
7725
  // src/server.ts
7077
7726
  init_esm_shims();
7727
+ init_engine();
7078
7728
  import { createHash as createHash5 } from "crypto";
7079
7729
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
7080
7730
  import {
@@ -7137,6 +7787,7 @@ function formatResults(results) {
7137
7787
  }
7138
7788
 
7139
7789
  // src/server.ts
7790
+ init_ulid();
7140
7791
  function defaultSessionKey() {
7141
7792
  if (process.env.REMEM_SESSION_KEY) return process.env.REMEM_SESSION_KEY;
7142
7793
  const cwd = process.cwd();
@@ -7149,6 +7800,8 @@ function detectAgentId() {
7149
7800
  if (process.env.DEVIN_SESSION_ID) return "devin";
7150
7801
  if (process.env.CLAUDE_CODE_ENTRYPOINT) return "claude";
7151
7802
  if (process.env.CURSOR_DEBUG) return "cursor";
7803
+ if (process.env.CODEX_HOME) return "codex";
7804
+ if (process.env.WINDSURF_USER_KEY) return "windsurf";
7152
7805
  return "unknown";
7153
7806
  }
7154
7807
  function dedupByContentPrefix(results, prefixLen = 60) {
@@ -7298,11 +7951,20 @@ var TOOLS = [
7298
7951
  default: false,
7299
7952
  description: "Set this to true to force capture even if the content was previously rejected. Use this only when the rejection reason no longer applies."
7300
7953
  },
7954
+ override_reason: {
7955
+ type: "string",
7956
+ description: "Required when override_rejection is true. Explain why the rejection no longer applies. Logged to audit."
7957
+ },
7301
7958
  auto_global: {
7302
7959
  type: "boolean",
7303
7960
  default: false,
7304
7961
  description: "Set to true to auto-classify: generic rules/learnings go to global, project-specific content stays in project. Only applies when session_key is not explicitly set. Requires REMEM_GLOBAL_SESSION_KEY to be configured."
7305
7962
  },
7963
+ atoms: {
7964
+ type: "array",
7965
+ items: { type: "string" },
7966
+ description: "Optional L1 atom facts distilled from this capture. Each atom is a short, self-contained fact useful on its own (e.g., 'vitest config missing causes npm test exit 1'). When provided, recall() returns these atoms instead of the raw content \u2014 90% fewer tokens. Write 1-3 atoms for decisions, learnings, and errors. Skip for conversations."
7967
+ },
7306
7968
  format: {
7307
7969
  type: "string",
7308
7970
  enum: ["text", "json"],
@@ -7629,6 +8291,52 @@ var TOOLS = [
7629
8291
  required: ["knowledge_ids"]
7630
8292
  }
7631
8293
  },
8294
+ // ─── L2 scenario consolidation ───────────────────────────────
8295
+ {
8296
+ name: "scenario_create",
8297
+ description: "Consolidate multiple L1 atoms into an L2 scenario summary. Call this when you have 5+ atoms about the same topic \u2014 it creates a high-signal summary that recall injects in ~100 tokens instead of 5+ individual atoms. No LLM needed \u2014 you write the summary yourself based on the atoms you've seen.",
8298
+ inputSchema: {
8299
+ type: "object",
8300
+ properties: {
8301
+ atom_ids: {
8302
+ type: "array",
8303
+ items: { type: "string" },
8304
+ description: "The L1 atom IDs to consolidate (1-20)."
8305
+ },
8306
+ summary: {
8307
+ type: "string",
8308
+ description: "A 1-3 sentence summary that captures the key insight from these atoms. Write it as a self-contained fact useful on its own."
8309
+ },
8310
+ persona_tags: {
8311
+ type: "array",
8312
+ items: { type: "string" },
8313
+ description: "Optional tags for categorization (e.g., ['database', 'migration'])."
8314
+ },
8315
+ ...TENANT_PARAMS
8316
+ },
8317
+ required: ["atom_ids", "summary"]
8318
+ }
8319
+ },
8320
+ // ─── L3 persona update ───────────────────────────────────────
8321
+ {
8322
+ name: "persona_update",
8323
+ description: "Update the L3 persona profile for this user/team. Call this when you notice a user preference or pattern (e.g., 'prefers concise output', 'works in Vietnamese', 'uses AZR project'). Persona is injected at SessionStart \u2014 every session gets it automatically in ~50 tokens. No LLM needed \u2014 you write the trait/value yourself.",
8324
+ inputSchema: {
8325
+ type: "object",
8326
+ properties: {
8327
+ trait: {
8328
+ type: "string",
8329
+ description: "The trait name (e.g., 'language', 'output_style', 'project', 'timezone')."
8330
+ },
8331
+ value: {
8332
+ type: "string",
8333
+ description: "The trait value (e.g., 'Vietnamese', 'concise', 'AZR', 'Asia/Ho_Chi_Minh')."
8334
+ },
8335
+ ...TENANT_PARAMS
8336
+ },
8337
+ required: ["trait", "value"]
8338
+ }
8339
+ },
7632
8340
  // ─── Skill management tools ──────────────────────────────────
7633
8341
  {
7634
8342
  name: "skill_get",
@@ -7787,6 +8495,27 @@ var TOOLS = [
7787
8495
  required: ["file_path"]
7788
8496
  }
7789
8497
  },
8498
+ {
8499
+ name: "codegraph_detect_changes",
8500
+ description: "Detect uncommitted git changes and map them to affected symbols with blast radius. Runs `git diff --name-only` to find changed files, finds symbols in those files, then traces callers to determine impact. Returns affected symbols + risk classification.",
8501
+ inputSchema: {
8502
+ type: "object",
8503
+ properties: {
8504
+ repo_path: {
8505
+ type: "string",
8506
+ description: "The repository root path (absolute)."
8507
+ },
8508
+ team_id: { type: "string", description: "The team ID for isolation." },
8509
+ max_depth: {
8510
+ type: "integer",
8511
+ default: 3,
8512
+ maximum: 5,
8513
+ description: "Max depth for caller traversal (blast radius)."
8514
+ }
8515
+ },
8516
+ required: ["repo_path"]
8517
+ }
8518
+ },
7790
8519
  // ─── Wiki tools ───
7791
8520
  {
7792
8521
  name: "wiki_ingest",
@@ -8143,7 +8872,7 @@ function createServer(opts) {
8143
8872
  tags: r.tags ? JSON.parse(r.tags) : [],
8144
8873
  createdAt: r.created_at,
8145
8874
  trustState: r.trust_state ?? "candidate",
8146
- supersededBy: r.superseded_by,
8875
+ supersededBy: r.superseded_by ?? void 0,
8147
8876
  teamId: r.team_id,
8148
8877
  userId: r.user_id,
8149
8878
  taskId: r.task_id,
@@ -8216,6 +8945,10 @@ function createServer(opts) {
8216
8945
  return handleKnowledgeList(args, opts);
8217
8946
  case "knowledge_delete":
8218
8947
  return handleKnowledgeDelete(args, opts);
8948
+ case "scenario_create":
8949
+ return handleScenarioConsolidate(args, opts);
8950
+ case "persona_update":
8951
+ return handlePersonaUpdate(args, opts);
8219
8952
  case "skill_get":
8220
8953
  return handleSkillGet(args, opts);
8221
8954
  case "skill_list":
@@ -8234,6 +8967,8 @@ function createServer(opts) {
8234
8967
  return handleCodegraphImpact(args, opts);
8235
8968
  case "codegraph_list":
8236
8969
  return handleCodegraphList(args, opts);
8970
+ case "codegraph_detect_changes":
8971
+ return handleCodegraphDetectChanges(args, opts);
8237
8972
  case "wiki_ingest":
8238
8973
  return handleWikiIngest(args, opts);
8239
8974
  case "wiki_search":
@@ -8307,11 +9042,11 @@ async function handleRecall(args, opts) {
8307
9042
  }
8308
9043
  }
8309
9044
  const globalKey = globalSessionKey();
8310
- const useGlobalFallback = globalKey && !args.session_key && globalKey !== sessionKey;
9045
+ const useGlobalFallback = !!globalKey && !args.session_key && globalKey !== sessionKey;
8311
9046
  let results;
8312
9047
  if (useGlobalFallback) {
8313
9048
  const reservedGlobal = Math.min(3, limit);
8314
- const projectLimit = limit - reservedGlobal;
9049
+ const projectLimit = Math.max(1, limit - reservedGlobal);
8315
9050
  const projectResults = await opts.storage.search(query, queryEmbedding, {
8316
9051
  sessionKey,
8317
9052
  limit: projectLimit,
@@ -8343,28 +9078,36 @@ async function handleRecall(args, opts) {
8343
9078
  });
8344
9079
  }
8345
9080
  results = dedupByContentPrefix(results, 60);
8346
- let text = formatResults(results);
8347
- if (format === "json") {
8348
- const atomMap = /* @__PURE__ */ new Map();
8349
- for (const r of results) {
8350
- try {
8351
- const atoms = await opts.pipelineCtx.storage.listAtoms({ captureId: r.entry.id, limit: 5 });
8352
- if (atoms.length > 0) {
8353
- atomMap.set(r.entry.id, atoms.map((a) => a.fact).join(" | "));
8354
- }
8355
- } catch {
9081
+ const atomMap = /* @__PURE__ */ new Map();
9082
+ for (const r of results) {
9083
+ try {
9084
+ const atoms = await opts.pipelineCtx.storage.listAtoms({ captureId: r.entry.id, limit: 5 });
9085
+ if (atoms.length > 0) {
9086
+ atomMap.set(r.entry.id, atoms.map((a) => a.fact).join(" | "));
8356
9087
  }
9088
+ } catch {
9089
+ }
9090
+ }
9091
+ for (const r of results) {
9092
+ const atomContent = atomMap.get(r.entry.id);
9093
+ if (atomContent) {
9094
+ r.entry = { ...r.entry, content: atomContent };
8357
9095
  }
9096
+ }
9097
+ let text = formatResults(results);
9098
+ if (format === "json") {
8358
9099
  const jsonResults = results.map((r) => ({
8359
9100
  id: r.entry.id,
8360
- content: atomMap.get(r.entry.id) ?? r.entry.content,
9101
+ content: r.entry.content,
8361
9102
  score: r.score,
8362
9103
  type: r.entry.type,
8363
9104
  tags: r.entry.tags,
8364
9105
  created_at: new Date(r.entry.createdAt).toISOString(),
8365
9106
  trust_state: r.entry.trustState ?? "candidate"
8366
9107
  }));
8367
- const jsonText = JSON.stringify(jsonResults);
9108
+ const jsonText = JSON.stringify(
9109
+ vectorDegraded ? { results: jsonResults, _meta: { vector_degraded: true, channels_run: ["keyword"] } } : { results: jsonResults, _meta: { channels_run: mode === "vector" ? ["vector"] : ["keyword", "vector"] } }
9110
+ );
8368
9111
  opts.audit.log({
8369
9112
  tool: "recall",
8370
9113
  argsHash: AuditLogger.hashArgs({ query, limit, offset, mode, teamId, userId, taskId }),
@@ -8402,6 +9145,24 @@ ${wikiLines.join("\n")}`;
8402
9145
  }
8403
9146
  } catch {
8404
9147
  }
9148
+ try {
9149
+ const scenarios = await opts.storage.listScenarios({
9150
+ teamId,
9151
+ agentId,
9152
+ userId,
9153
+ limit: 3
9154
+ });
9155
+ if (scenarios.length > 0) {
9156
+ const scenarioLines = scenarios.map(
9157
+ (s) => ` ${s.summary}`
9158
+ );
9159
+ text += `
9160
+
9161
+ ## Scenarios (L2 summaries)
9162
+ ${scenarioLines.join("\n")}`;
9163
+ }
9164
+ } catch {
9165
+ }
8405
9166
  if (vectorDegraded) {
8406
9167
  text = `[note: vector search unavailable, results are keyword-only]
8407
9168
  ${text}`;
@@ -8442,6 +9203,7 @@ async function handleCapture(args, opts) {
8442
9203
  const verified = args.verified ?? false;
8443
9204
  const supersedes = args.supersedes;
8444
9205
  const overrideRejection = args.override_rejection ?? false;
9206
+ const overrideReason = args.override_reason;
8445
9207
  const format = args.format ?? (args.agent_id !== void 0 ? "json" : "text");
8446
9208
  let content;
8447
9209
  let messages;
@@ -8491,12 +9253,30 @@ async function handleCapture(args, opts) {
8491
9253
  content: [
8492
9254
  {
8493
9255
  type: "text",
8494
- text: `Blocked: This content was previously rejected (${rejected[0].id}). Reason: ${reason}. Set override_rejection to true to force capture.`
9256
+ text: `Blocked: This content was previously rejected (${rejected[0].id}). Reason: ${reason}. Set override_rejection: true and override_reason: "<why>" to force capture.`
8495
9257
  }
8496
9258
  ],
8497
9259
  isError: true
8498
9260
  };
8499
9261
  }
9262
+ } else if (!overrideReason) {
9263
+ return {
9264
+ content: [
9265
+ {
9266
+ type: "text",
9267
+ text: "Error: override_rejection is true but override_reason is missing. Provide a reason explaining why the rejection no longer applies."
9268
+ }
9269
+ ],
9270
+ isError: true
9271
+ };
9272
+ } else {
9273
+ opts.audit.log({
9274
+ tool: "capture",
9275
+ argsHash: AuditLogger.hashArgs({ type: type2, tags, sessionKey, overrideReason, teamId, userId, taskId }),
9276
+ resultLen: 0,
9277
+ quotaHit: false,
9278
+ redacted: wasRedacted
9279
+ });
8500
9280
  }
8501
9281
  const existing = await opts.storage.findByContentHash(contentHash, sessionKey, agentId);
8502
9282
  if (existing.length > 0) {
@@ -8618,6 +9398,31 @@ Call resolve to mark one as superseding the other.`;
8618
9398
  console.error(`[remem-mcp] Pipeline failed: ${err}`);
8619
9399
  }
8620
9400
  }
9401
+ const agentAtoms = args.atoms;
9402
+ if (agentAtoms && agentAtoms.length > 0) {
9403
+ try {
9404
+ try {
9405
+ opts.pipelineCtx.storage.deleteAtomsByCaptureId(id);
9406
+ } catch {
9407
+ }
9408
+ for (const fact of agentAtoms.slice(0, 5)) {
9409
+ const atomId = generateId();
9410
+ await opts.pipelineCtx.storage.putAtom({
9411
+ id: atomId,
9412
+ captureId: id,
9413
+ fact,
9414
+ confidence: 0.95,
9415
+ // agent-written atoms are high confidence
9416
+ createdAt: Date.now(),
9417
+ teamId,
9418
+ agentId,
9419
+ userId
9420
+ });
9421
+ }
9422
+ } catch (err) {
9423
+ console.error(`[remem-mcp] Agent atom save failed: ${err}`);
9424
+ }
9425
+ }
8621
9426
  opts.audit.log({
8622
9427
  tool: "capture",
8623
9428
  argsHash: AuditLogger.hashArgs({ type: type2, tags, sessionKey, teamId, userId, taskId }),
@@ -8679,7 +9484,7 @@ async function handleSearch(args, opts) {
8679
9484
  }
8680
9485
  const sessionKey = args.session_key ?? defaultSessionKey();
8681
9486
  const globalKey = globalSessionKey();
8682
- const useGlobalFallback = globalKey && !args.session_key && globalKey !== sessionKey;
9487
+ const useGlobalFallback = !!globalKey && !args.session_key && globalKey !== sessionKey;
8683
9488
  const searchFilters = filters ? {
8684
9489
  type: filters.type,
8685
9490
  tags: filters.tags,
@@ -8693,7 +9498,7 @@ async function handleSearch(args, opts) {
8693
9498
  let results;
8694
9499
  if (useGlobalFallback) {
8695
9500
  const reservedGlobal = Math.min(3, limit);
8696
- const projectLimit = limit - reservedGlobal;
9501
+ const projectLimit = Math.max(1, limit - reservedGlobal);
8697
9502
  const projectResults = await opts.storage.search(query, queryEmbedding, {
8698
9503
  sessionKey,
8699
9504
  limit: projectLimit,
@@ -8725,27 +9530,35 @@ async function handleSearch(args, opts) {
8725
9530
  });
8726
9531
  }
8727
9532
  results = dedupByContentPrefix(results, 60);
8728
- if (format === "json") {
8729
- const atomMap = /* @__PURE__ */ new Map();
8730
- for (const r of results) {
8731
- try {
8732
- const atoms = await opts.pipelineCtx.storage.listAtoms({ captureId: r.entry.id, limit: 5 });
8733
- if (atoms.length > 0) {
8734
- atomMap.set(r.entry.id, atoms.map((a) => a.fact).join(" | "));
8735
- }
8736
- } catch {
9533
+ const atomMap = /* @__PURE__ */ new Map();
9534
+ for (const r of results) {
9535
+ try {
9536
+ const atoms = await opts.pipelineCtx.storage.listAtoms({ captureId: r.entry.id, limit: 5 });
9537
+ if (atoms.length > 0) {
9538
+ atomMap.set(r.entry.id, atoms.map((a) => a.fact).join(" | "));
8737
9539
  }
9540
+ } catch {
8738
9541
  }
9542
+ }
9543
+ for (const r of results) {
9544
+ const atomContent = atomMap.get(r.entry.id);
9545
+ if (atomContent) {
9546
+ r.entry = { ...r.entry, content: atomContent };
9547
+ }
9548
+ }
9549
+ if (format === "json") {
8739
9550
  const jsonResults = results.map((r) => ({
8740
9551
  id: r.entry.id,
8741
- content: atomMap.get(r.entry.id) ?? r.entry.content,
9552
+ content: r.entry.content,
8742
9553
  score: r.score,
8743
9554
  type: r.entry.type,
8744
9555
  tags: r.entry.tags,
8745
9556
  created_at: new Date(r.entry.createdAt).toISOString(),
8746
9557
  trust_state: r.entry.trustState ?? "candidate"
8747
9558
  }));
8748
- const jsonText = JSON.stringify(jsonResults);
9559
+ const jsonText = JSON.stringify(
9560
+ vectorDegraded ? { results: jsonResults, _meta: { vector_degraded: true, channels_run: ["keyword"] } } : { results: jsonResults, _meta: { channels_run: mode === "vector" ? ["vector"] : ["keyword", "vector"] } }
9561
+ );
8749
9562
  opts.audit.log({
8750
9563
  tool: "search",
8751
9564
  argsHash: AuditLogger.hashArgs({ query, mode, filters }),
@@ -8921,7 +9734,7 @@ async function handleExplainRecall(args, opts) {
8921
9734
  const captureId = args.capture_id;
8922
9735
  const sessionKey = args.session_key ?? defaultSessionKey();
8923
9736
  const globalKey = globalSessionKey();
8924
- const useGlobalFallback = globalKey && !args.session_key && globalKey !== sessionKey;
9737
+ const useGlobalFallback = !!globalKey && !args.session_key && globalKey !== sessionKey;
8925
9738
  const mode = args.mode ?? "hybrid";
8926
9739
  const limit = Math.min(args.limit ?? 10, 50);
8927
9740
  const { teamId, userId, taskId } = extractTenant(args);
@@ -8940,7 +9753,7 @@ async function handleExplainRecall(args, opts) {
8940
9753
  queryEmbedding,
8941
9754
  useGlobalFallback,
8942
9755
  sessionKey,
8943
- globalKey,
9756
+ globalKey: globalKey ?? void 0,
8944
9757
  teamId,
8945
9758
  userId,
8946
9759
  taskId
@@ -9558,10 +10371,18 @@ async function handleSupersede(args, opts) {
9558
10371
  isError: true
9559
10372
  };
9560
10373
  }
9561
- db.prepare("UPDATE captures SET superseded_by = ?, trust_state = 'stale' WHERE id = ?").run(
9562
- newId,
9563
- oldId
9564
- );
10374
+ const { updated } = await opts.storage.supersede(oldId, newId);
10375
+ if (updated === 0) {
10376
+ return {
10377
+ content: [
10378
+ {
10379
+ type: "text",
10380
+ text: `Error: Old capture ${oldId} could not be superseded (it may already be rejected).`
10381
+ }
10382
+ ],
10383
+ isError: true
10384
+ };
10385
+ }
9565
10386
  return {
9566
10387
  content: [
9567
10388
  {
@@ -9627,13 +10448,13 @@ async function handleCorrectionKPIs(_args, opts) {
9627
10448
  async function handleSessionStart(args, opts) {
9628
10449
  const sessionKey = args.session_key ?? defaultSessionKey();
9629
10450
  const globalKey = globalSessionKey();
9630
- const useGlobalFallback = globalKey && !args.session_key && globalKey !== sessionKey;
10451
+ const useGlobalFallback = !!globalKey && !args.session_key && globalKey !== sessionKey;
9631
10452
  const contextQuery = args.context_query;
9632
10453
  const db = getDb(opts);
9633
10454
  const sessionKeys = useGlobalFallback ? [sessionKey, globalKey] : [sessionKey];
9634
10455
  const placeholders = sessionKeys.map(() => "?").join(",");
9635
10456
  const recent = db.prepare(
9636
- `SELECT id, type, content, tags, created_at FROM captures WHERE deleted_at IS NULL AND session_key IN (${placeholders}) ORDER BY created_at DESC LIMIT 5`
10457
+ `SELECT id, session_key, type, content, tags, created_at FROM captures WHERE deleted_at IS NULL AND session_key IN (${placeholders}) ORDER BY created_at DESC LIMIT 5`
9637
10458
  ).all(...sessionKeys);
9638
10459
  const kpis = opts.storage.getCorrectionKPIs();
9639
10460
  let contextResults = [];
@@ -10184,6 +11005,99 @@ async function handleKnowledgeDelete(args, opts) {
10184
11005
  });
10185
11006
  return { content: [{ type: "text", text: `Deleted ${count} knowledge asset(s).` }] };
10186
11007
  }
11008
+ async function handleScenarioConsolidate(args, opts) {
11009
+ const atomIds = args.atom_ids;
11010
+ const summary = args.summary;
11011
+ const personaTags = args.persona_tags;
11012
+ const { teamId, userId, taskId } = extractTenant(args);
11013
+ const agentId = args.agent_id ?? detectAgentId();
11014
+ if (!atomIds || atomIds.length === 0) {
11015
+ return { content: [{ type: "text", text: "Error: atom_ids is required." }], isError: true };
11016
+ }
11017
+ if (!summary || summary.trim().length < 10) {
11018
+ return { content: [{ type: "text", text: "Error: summary must be at least 10 characters." }], isError: true };
11019
+ }
11020
+ if (atomIds.length > 20) {
11021
+ return { content: [{ type: "text", text: "Error: Maximum 20 atom_ids per consolidation." }], isError: true };
11022
+ }
11023
+ const id = generateId();
11024
+ const scenario = {
11025
+ id,
11026
+ atomIds,
11027
+ summary: summary.trim(),
11028
+ personaTags,
11029
+ createdAt: Date.now(),
11030
+ teamId,
11031
+ agentId,
11032
+ userId
11033
+ };
11034
+ try {
11035
+ await opts.storage.putScenario(scenario);
11036
+ } catch (err) {
11037
+ return { content: [{ type: "text", text: `Error: ${err}` }], isError: true };
11038
+ }
11039
+ opts.audit.log({
11040
+ tool: "consolidate",
11041
+ argsHash: AuditLogger.hashArgs({ atomIds, summary }),
11042
+ resultLen: id.length,
11043
+ quotaHit: false,
11044
+ redacted: false
11045
+ });
11046
+ return {
11047
+ content: [
11048
+ {
11049
+ type: "text",
11050
+ text: `Consolidated ${atomIds.length} atoms into scenario ${id}. Recall will inject this summary automatically.`
11051
+ }
11052
+ ]
11053
+ };
11054
+ }
11055
+ async function handlePersonaUpdate(args, opts) {
11056
+ const trait = args.trait;
11057
+ const value = args.value;
11058
+ const { teamId, userId } = extractTenant(args);
11059
+ const agentId = args.agent_id ?? detectAgentId();
11060
+ if (!trait || !value) {
11061
+ return { content: [{ type: "text", text: "Error: trait and value are required." }], isError: true };
11062
+ }
11063
+ const tid = teamId ?? "default";
11064
+ const uid = userId ?? "default";
11065
+ let existing = await opts.storage.readPersona(tid, agentId, uid);
11066
+ let content;
11067
+ if (existing) {
11068
+ const lines = existing.content.split("\n").filter((l) => l.trim());
11069
+ const traitPattern = new RegExp(`^${trait.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}:`, "i");
11070
+ const idx = lines.findIndex((l) => traitPattern.test(l));
11071
+ if (idx >= 0) {
11072
+ lines[idx] = `${trait}: ${value}`;
11073
+ } else {
11074
+ lines.push(`${trait}: ${value}`);
11075
+ }
11076
+ content = lines.join("\n");
11077
+ } else {
11078
+ content = `${trait}: ${value}`;
11079
+ }
11080
+ try {
11081
+ await opts.storage.writePersona(tid, agentId, uid, content);
11082
+ } catch (err) {
11083
+ return { content: [{ type: "text", text: `Error: ${err}` }], isError: true };
11084
+ }
11085
+ opts.audit.log({
11086
+ tool: "persona_update",
11087
+ argsHash: AuditLogger.hashArgs({ trait, value }),
11088
+ resultLen: content.length,
11089
+ quotaHit: false,
11090
+ redacted: false
11091
+ });
11092
+ return {
11093
+ content: [
11094
+ {
11095
+ type: "text",
11096
+ text: `Persona updated: ${trait} = ${value}. SessionStart will inject this automatically.`
11097
+ }
11098
+ ]
11099
+ };
11100
+ }
10187
11101
  async function handleSkillGet(args, opts) {
10188
11102
  const skillId = args.skill_id;
10189
11103
  const entry = await opts.storage.getSkill(skillId);
@@ -10420,6 +11334,95 @@ ${lines.join("\n")}`
10420
11334
  ]
10421
11335
  };
10422
11336
  }
11337
+ function handleCodegraphDetectChanges(args, opts) {
11338
+ const repoPath = args.repo_path;
11339
+ const teamId = args.team_id ?? void 0;
11340
+ const maxDepth = args.max_depth ?? 3;
11341
+ if (!repoPath) {
11342
+ return { content: [{ type: "text", text: "Error: repo_path is required." }], isError: true };
11343
+ }
11344
+ const db = getDb(opts);
11345
+ let changedFiles;
11346
+ try {
11347
+ const { execSync: execSync3 } = __require("child_process");
11348
+ const output2 = execSync3("git diff --name-only HEAD", {
11349
+ cwd: repoPath,
11350
+ encoding: "utf-8",
11351
+ timeout: 5e3
11352
+ }).trim();
11353
+ changedFiles = output2 ? output2.split("\n") : [];
11354
+ const staged = execSync3("git diff --cached --name-only", {
11355
+ cwd: repoPath,
11356
+ encoding: "utf-8",
11357
+ timeout: 5e3
11358
+ }).trim();
11359
+ if (staged) {
11360
+ const stagedFiles = staged.split("\n");
11361
+ changedFiles = [.../* @__PURE__ */ new Set([...changedFiles, ...stagedFiles])];
11362
+ }
11363
+ } catch {
11364
+ return {
11365
+ content: [{ type: "text", text: "Error: Failed to run git diff. Is this a git repository?" }],
11366
+ isError: true
11367
+ };
11368
+ }
11369
+ if (changedFiles.length === 0) {
11370
+ return { content: [{ type: "text", text: "No uncommitted changes detected." }] };
11371
+ }
11372
+ const placeholders = changedFiles.map(() => "?").join(",");
11373
+ const changedSymbols = db.prepare(
11374
+ `SELECT id, name, kind, file_path, line_start, line_end FROM symbols WHERE file_path IN (${placeholders})`
11375
+ ).all(...changedFiles);
11376
+ if (changedSymbols.length === 0) {
11377
+ return {
11378
+ content: [
11379
+ {
11380
+ type: "text",
11381
+ text: `Changed files: ${changedFiles.length}, but no indexed symbols found. Run codegraph_index first.`
11382
+ }
11383
+ ]
11384
+ };
11385
+ }
11386
+ const { impactAnalysis: impactAnalysis2 } = (init_engine(), __toCommonJS(engine_exports));
11387
+ const results = [];
11388
+ for (const sym of changedSymbols) {
11389
+ try {
11390
+ const impact = impactAnalysis2(db, sym.id, { maxDepth });
11391
+ const callerCount = impact.affected.length;
11392
+ const risk = callerCount >= 10 ? "high" : callerCount >= 3 ? "medium" : "low";
11393
+ results.push({
11394
+ symbol: {
11395
+ name: sym.name,
11396
+ kind: sym.kind,
11397
+ file: sym.file_path,
11398
+ line: sym.line_start
11399
+ },
11400
+ callers: impact.affected.slice(0, 5).map((a) => ({
11401
+ name: a.symbol.name,
11402
+ depth: a.depth,
11403
+ path: a.path.join(" \u2192 ")
11404
+ })),
11405
+ risk
11406
+ });
11407
+ } catch {
11408
+ }
11409
+ }
11410
+ const lines = [`Changed files: ${changedFiles.length}`, `Affected symbols: ${changedSymbols.length}`, ""];
11411
+ for (const r of results) {
11412
+ lines.push(`[${r.risk.toUpperCase()}] ${r.symbol.kind} ${r.symbol.name} (${r.symbol.file}:${r.symbol.line})`);
11413
+ if (r.callers.length > 0) {
11414
+ lines.push(` Callers (${r.callers.length} shown):`);
11415
+ for (const c of r.callers) {
11416
+ lines.push(` d${c.depth}: ${c.path}`);
11417
+ }
11418
+ } else {
11419
+ lines.push(` No callers found (leaf node).`);
11420
+ }
11421
+ }
11422
+ return {
11423
+ content: [{ type: "text", text: lines.join("\n") }]
11424
+ };
11425
+ }
10423
11426
  async function handleWikiIngest(args, opts) {
10424
11427
  const path = args.path;
10425
11428
  const repoPath = args.repo_path ?? path;
@@ -10531,9 +11534,9 @@ ${lines.join("\n")}` }
10531
11534
 
10532
11535
  // src/stats.ts
10533
11536
  init_esm_shims();
10534
- import Database9 from "better-sqlite3";
11537
+ import Database10 from "better-sqlite3";
10535
11538
  function stats(dbPath) {
10536
- const db = new Database9(dbPath, { readonly: true });
11539
+ const db = new Database10(dbPath, { readonly: true });
10537
11540
  const total = db.prepare("SELECT COUNT(*) as count FROM captures").get();
10538
11541
  if (total.count === 0) {
10539
11542
  console.log("No captures found. The database is empty.");
@@ -10662,7 +11665,7 @@ init_esm_shims();
10662
11665
  import { existsSync as existsSync13, readFileSync as readFileSync10 } from "fs";
10663
11666
  import { homedir as homedir10 } from "os";
10664
11667
  import { join as join13 } from "path";
10665
- import Database10 from "better-sqlite3";
11668
+ import Database11 from "better-sqlite3";
10666
11669
  import { encode } from "gpt-tokenizer";
10667
11670
  function countTokens(text) {
10668
11671
  if (!text) return 0;
@@ -10670,7 +11673,7 @@ function countTokens(text) {
10670
11673
  }
10671
11674
  function tokenStats(dbPath) {
10672
11675
  const logPath = process.env.REMEM_HOOK_LOG_PATH ?? join13(homedir10(), ".local", "share", "remem-mcp", "session.log");
10673
- const db = new Database10(dbPath, { readonly: true });
11676
+ const db = new Database11(dbPath, { readonly: true });
10674
11677
  const totalCaptures = db.prepare("SELECT COUNT(*) as count FROM captures").get();
10675
11678
  const captures = db.prepare("SELECT id, type, content, tags, created_at FROM captures ORDER BY created_at ASC").all();
10676
11679
  const sessions = db.prepare("SELECT COUNT(DISTINCT session_key) as count FROM captures").get();
@@ -10820,13 +11823,14 @@ function printReactExample(fmt, _countTok) {
10820
11823
 
10821
11824
  // src/viewer.ts
10822
11825
  init_esm_shims();
11826
+ init_engine();
10823
11827
  import { existsSync as existsSync14, readFileSync as readFileSync11 } from "fs";
10824
11828
  import { createServer as createHttpServer } from "http";
10825
11829
  import { homedir as homedir11 } from "os";
10826
11830
  import { join as join14 } from "path";
10827
- import Database11 from "better-sqlite3";
11831
+ import Database12 from "better-sqlite3";
10828
11832
  function startViewer(dbPath, port) {
10829
- const db = new Database11(dbPath);
11833
+ const db = new Database12(dbPath);
10830
11834
  const server = createHttpServer((req, res) => {
10831
11835
  const url = new URL(req.url ?? "/", `http://localhost:${port}`);
10832
11836
  if (url.pathname === "/" || url.pathname === "/index.html") {
@@ -12547,7 +13551,7 @@ function defaultDbPath3() {
12547
13551
  return process.env.REMEM_DB_PATH ?? join15(homedir12(), ".local", "share", "remem-mcp", "memory.db");
12548
13552
  }
12549
13553
  function openDbWithSchema(dbPath) {
12550
- const db = new Database12(dbPath);
13554
+ const db = new Database13(dbPath);
12551
13555
  db.pragma("journal_mode = WAL");
12552
13556
  db.pragma("foreign_keys = OFF");
12553
13557
  sqliteVec4.load(db);
@@ -12572,9 +13576,14 @@ function openDbWithSchema(dbPath) {
12572
13576
  function parseFlags(argv) {
12573
13577
  const flags = {};
12574
13578
  for (let i = 0; i < argv.length; i++) {
12575
- if (argv[i]?.startsWith("--") && argv[i + 1]) {
12576
- flags[argv[i].slice(2)] = argv[i + 1];
12577
- i++;
13579
+ if (argv[i]?.startsWith("--")) {
13580
+ const key = argv[i].slice(2);
13581
+ if (argv[i + 1] && !argv[i + 1].startsWith("--")) {
13582
+ flags[key] = argv[i + 1];
13583
+ i++;
13584
+ } else {
13585
+ flags[key] = "true";
13586
+ }
12578
13587
  }
12579
13588
  }
12580
13589
  return flags;
@@ -12868,6 +13877,31 @@ async function main() {
12868
13877
  await installSkill();
12869
13878
  return;
12870
13879
  }
13880
+ if (arg === "init") {
13881
+ console.log(`remem-mcp init
13882
+
13883
+ This command tells your agent to activate long-term memory for this session.
13884
+
13885
+ \u2192 In your agent chat, type:
13886
+
13887
+ invoke skill remem-mcp
13888
+
13889
+ The skill will teach the agent to:
13890
+ 1. Call recall() with your current task as query
13891
+ 2. Index code with codegraph_search (first use auto-indexes src/)
13892
+ 3. Capture decisions/learnings after completing work
13893
+ 4. Use search instead of grep for symbol lookup
13894
+
13895
+ Hooks are already active (installed during setup):
13896
+ - SessionStart: recent memory auto-injected
13897
+ - UserPromptSubmit: memory matching your prompt auto-injected
13898
+ - PostToolUse: failed commands auto-captured
13899
+ - Stop: session transcript auto-captured
13900
+
13901
+ You only need to invoke the skill once per session.
13902
+ After that, the agent follows the rules automatically.`);
13903
+ return;
13904
+ }
12871
13905
  if (arg === "install-hooks") {
12872
13906
  await installHooks();
12873
13907
  return;
@@ -12971,7 +14005,7 @@ async function main() {
12971
14005
  }
12972
14006
  if (arg === "recent" || arg === "list") {
12973
14007
  const limit = parseInt(process.argv[3] ?? "20", 10);
12974
- const db = new Database12(defaultDbPath3(), { readonly: true });
14008
+ const db = new Database13(defaultDbPath3(), { readonly: true });
12975
14009
  const rows = db.prepare(
12976
14010
  `SELECT id, type, content, tags, created_at FROM captures WHERE deleted_at IS NULL ORDER BY created_at DESC LIMIT ?`
12977
14011
  ).all(limit);
@@ -12993,7 +14027,7 @@ ${rows.length} capture(s).`);
12993
14027
  return;
12994
14028
  }
12995
14029
  if (arg === "sessions") {
12996
- const db = new Database12(defaultDbPath3(), { readonly: true });
14030
+ const db = new Database13(defaultDbPath3(), { readonly: true });
12997
14031
  const rows = db.prepare(
12998
14032
  `SELECT session_key, COUNT(*) as cnt, MIN(created_at) as oldest, MAX(created_at) as newest
12999
14033
  FROM captures WHERE deleted_at IS NULL
@@ -13021,7 +14055,7 @@ ${rows.length} session(s). Current: ${cwdHash}`);
13021
14055
  return;
13022
14056
  }
13023
14057
  if (arg === "tags") {
13024
- const db = new Database12(defaultDbPath3(), { readonly: true });
14058
+ const db = new Database13(defaultDbPath3(), { readonly: true });
13025
14059
  const rows = db.prepare(
13026
14060
  "SELECT tags FROM captures WHERE deleted_at IS NULL AND tags IS NOT NULL AND tags != '[]'"
13027
14061
  ).all();
@@ -13283,6 +14317,10 @@ ${sorted.length} unique tag(s).`);
13283
14317
  hookRecall(defaultDbPath3());
13284
14318
  return;
13285
14319
  }
14320
+ if (arg === "hook-user-prompt") {
14321
+ hookUserPromptSubmit(defaultDbPath3());
14322
+ return;
14323
+ }
13286
14324
  if (arg === "hook-stop") {
13287
14325
  hookStop(defaultDbPath3());
13288
14326
  return;
@@ -13526,6 +14564,16 @@ ${sorted.length} unique tag(s).`);
13526
14564
  await extractCommand(defaultDbPath3(), flags);
13527
14565
  return;
13528
14566
  }
14567
+ if (arg === "consolidate") {
14568
+ const flags = parseFlags(process.argv.slice(3));
14569
+ await consolidateCommand(defaultDbPath3(), flags);
14570
+ return;
14571
+ }
14572
+ if (arg === "worker-run") {
14573
+ const flags = parseFlags(process.argv.slice(3));
14574
+ await workerCommand(defaultDbPath3(), flags);
14575
+ return;
14576
+ }
13529
14577
  if (arg === "knowledge") {
13530
14578
  const flags = parseFlags(process.argv.slice(3));
13531
14579
  await knowledgeCommand(defaultDbPath3(), flags);
@@ -13581,6 +14629,7 @@ ${"\u2500".repeat(60)}
13581
14629
  Setup & maintenance:
13582
14630
  remem-mcp Start the MCP server (stdio)
13583
14631
  remem-mcp setup Install MCP + hooks + skill (one command)
14632
+ remem-mcp init Show how to activate memory in your agent session
13584
14633
  remem-mcp install-skill Install the agent skill for Devin CLI
13585
14634
  remem-mcp install-hooks Install lifecycle hooks (SessionStart, Stop, SessionEnd)
13586
14635
  remem-mcp uninstall-hooks Remove lifecycle hooks
@@ -13647,11 +14696,13 @@ Data:
13647
14696
  remem-mcp sync-export Export memory to .remem-mcp/ in project root
13648
14697
  remem-mcp sync-import Import memory from .remem-mcp/ (auto on startup)
13649
14698
 
13650
- L1-L3 pipeline (require REMEM_LLM_API_KEY for extract):
13651
- remem-mcp extract Run L1 atom extraction on existing captures
13652
- remem-mcp atoms List or search L1 atoms
14699
+ L1-L3 pipeline:
14700
+ remem-mcp extract Run L1 atom extraction (rule-based, no LLM needed)
14701
+ remem-mcp extract --llm Run L1 atom extraction with LLM (needs REMEM_LLM_API_KEY)
14702
+ remem-mcp atoms List or search L1 atoms (--query <text> for search)
14703
+ remem-mcp consolidate Create L2 scenario from atoms (--atom-ids, --summary, --auto)
13653
14704
  remem-mcp scenarios List L2 scenarios
13654
- remem-mcp persona Read or write L3 persona
14705
+ remem-mcp persona Read or write L3 persona (--set "trait: value")
13655
14706
 
13656
14707
  Knowledge & skills:
13657
14708
  remem-mcp knowledge List knowledge assets for a team
@@ -13707,10 +14758,8 @@ To install the skill (Devin CLI only):
13707
14758
  });
13708
14759
  pipeline = new AtomPipeline();
13709
14760
  pipeline._llmClient = llmClient;
13710
- } else if (config.pipeline === "noop") {
13711
- pipeline = new RuleBasedAtomPipeline();
13712
14761
  } else {
13713
- pipeline = new NoopPipeline();
14762
+ pipeline = new RuleBasedAtomPipeline();
13714
14763
  }
13715
14764
  const audit = new AuditLogger(config.auditLogPath, config.security.auditLog);
13716
14765
  const pipelineCtx = {