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/README.md +85 -125
- package/dist/chunk-FA324IBN.js +903 -0
- package/dist/chunk-FA324IBN.js.map +1 -0
- package/dist/{chunk-ULMM6LKD.js → chunk-X4ECGLYH.js} +60 -16
- package/dist/chunk-X4ECGLYH.js.map +1 -0
- package/dist/chunk-XK4Y53J2.js +54 -0
- package/dist/chunk-XK4Y53J2.js.map +1 -0
- package/dist/{engine-EIOBO7ZE.js → engine-LVKGZHBY.js} +5 -2
- package/dist/index.js +1234 -185
- package/dist/index.js.map +1 -1
- package/dist/resolver-AVK7XKX7.js +300 -0
- package/dist/resolver-AVK7XKX7.js.map +1 -0
- package/dist/sdk.js +3 -2
- package/dist/storage/schema.sql +6 -2
- package/package.json +1 -2
- package/scripts/dogfood-claude-cli.sh +156 -0
- package/scripts/dogfood-codex.sh +145 -0
- package/skills/remem-mcp/SKILL.md +45 -91
- package/dist/chunk-SHLLMMMU.js +0 -480
- package/dist/chunk-SHLLMMMU.js.map +0 -1
- package/dist/chunk-ULMM6LKD.js.map +0 -1
- /package/dist/{engine-EIOBO7ZE.js.map → engine-LVKGZHBY.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -13,20 +13,26 @@ import {
|
|
|
13
13
|
redact,
|
|
14
14
|
searchWiki,
|
|
15
15
|
stripQueryProperNouns
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-X4ECGLYH.js";
|
|
17
17
|
import {
|
|
18
|
-
|
|
19
|
-
__toCommonJS,
|
|
18
|
+
engine_exports,
|
|
20
19
|
findCallees,
|
|
21
20
|
findCallers,
|
|
22
21
|
generateId,
|
|
23
22
|
impactAnalysis,
|
|
24
23
|
indexDirectory,
|
|
25
24
|
indexFile,
|
|
26
|
-
|
|
25
|
+
init_engine,
|
|
26
|
+
init_ulid,
|
|
27
27
|
listSymbols,
|
|
28
28
|
searchSymbols
|
|
29
|
-
} from "./chunk-
|
|
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
|
|
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 (
|
|
964
|
+
if (!["decision", "learning", "error", "conversation"].includes(input.type)) {
|
|
873
965
|
return {};
|
|
874
966
|
}
|
|
875
|
-
const facts =
|
|
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
|
|
992
|
+
function extractRuleBasedFacts(content, type2) {
|
|
901
993
|
const facts = [];
|
|
902
|
-
const
|
|
903
|
-
if (
|
|
904
|
-
const
|
|
905
|
-
if (
|
|
906
|
-
|
|
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
|
-
|
|
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
|
-
|
|
985
|
-
|
|
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
|
|
994
|
-
|
|
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
|
-
|
|
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 (
|
|
1040
|
-
|
|
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
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
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
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
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
|
-
|
|
1126
|
-
|
|
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
|
|
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
|
|
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 = [
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
4129
|
+
import Database6 from "better-sqlite3";
|
|
3810
4130
|
function exportData(dbPath, outputPath, filters) {
|
|
3811
|
-
const db = new
|
|
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
|
|
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
|
|
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
|
|
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
|
|
4548
|
+
db = new Database8(dbPath, { readonly: true, immutable: true });
|
|
4229
4549
|
} catch {
|
|
4230
|
-
db = new
|
|
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
|
|
4951
|
+
const scriptPath2 = process.argv[1];
|
|
4344
4952
|
const child = spawn2(
|
|
4345
4953
|
process.execPath,
|
|
4346
|
-
[
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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-
|
|
6174
|
-
const db = new
|
|
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
|
|
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
|
|
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
|
-
...
|
|
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
|
|
6662
|
-
console.log("
|
|
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
|
-
"
|
|
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
|
|
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
|
|
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
|
-
|
|
8347
|
-
|
|
8348
|
-
|
|
8349
|
-
|
|
8350
|
-
|
|
8351
|
-
|
|
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:
|
|
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(
|
|
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
|
|
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
|
-
|
|
8729
|
-
|
|
8730
|
-
|
|
8731
|
-
|
|
8732
|
-
|
|
8733
|
-
|
|
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:
|
|
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(
|
|
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
|
-
|
|
9562
|
-
|
|
9563
|
-
|
|
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
|
|
11537
|
+
import Database10 from "better-sqlite3";
|
|
10535
11538
|
function stats(dbPath) {
|
|
10536
|
-
const db = new
|
|
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
|
|
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
|
|
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
|
|
11831
|
+
import Database12 from "better-sqlite3";
|
|
10828
11832
|
function startViewer(dbPath, port) {
|
|
10829
|
-
const db = new
|
|
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
|
|
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("--")
|
|
12576
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
13651
|
-
remem-mcp extract Run L1 atom extraction
|
|
13652
|
-
remem-mcp
|
|
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
|
|
14762
|
+
pipeline = new RuleBasedAtomPipeline();
|
|
13714
14763
|
}
|
|
13715
14764
|
const audit = new AuditLogger(config.auditLogPath, config.security.auditLog);
|
|
13716
14765
|
const pipelineCtx = {
|