remem-mcp 0.6.8 → 0.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,45 +1,48 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- LocalEmbedder,
4
3
  Memory,
5
- SQLiteBackend,
6
4
  classifyGlobalContent,
7
5
  findOutdatedPages,
8
6
  getWikiPage,
9
7
  ingestDirectory,
10
8
  ingestFile,
11
- init_local,
12
- local_exports,
13
9
  redact,
14
- searchWiki,
15
- stripQueryProperNouns
16
- } from "./chunk-X4ECGLYH.js";
10
+ searchWiki
11
+ } from "./chunk-ITMR6THY.js";
17
12
  import {
18
13
  engine_exports,
19
14
  findCallees,
20
15
  findCallers,
21
- generateId,
22
16
  impactAnalysis,
23
17
  indexDirectory,
24
18
  indexFile,
25
19
  init_engine,
26
- init_ulid,
27
20
  listSymbols,
28
21
  searchSymbols
29
- } from "./chunk-FA324IBN.js";
22
+ } from "./chunk-YU7WBJU5.js";
23
+ import {
24
+ LocalEmbedder,
25
+ SQLiteBackend,
26
+ init_local,
27
+ local_exports,
28
+ stripQueryProperNouns
29
+ } from "./chunk-4WCDAEPP.js";
30
+ import {
31
+ generateId,
32
+ init_ulid
33
+ } from "./chunk-XJGDYQ34.js";
30
34
  import {
31
35
  __dirname,
32
- __require,
33
36
  __toCommonJS,
34
37
  init_esm_shims
35
- } from "./chunk-XK4Y53J2.js";
38
+ } from "./chunk-ONQHLUGM.js";
36
39
 
37
40
  // src/index.ts
38
41
  init_esm_shims();
39
42
  import { createHash as createHash6 } from "crypto";
40
- import { existsSync as existsSync15, readdirSync, readFileSync as readFileSync12, statSync as statSync2 } from "fs";
41
- import { homedir as homedir12 } from "os";
42
- import { dirname as dirname10, join as join15 } from "path";
43
+ import { existsSync as existsSync16, readdirSync as readdirSync2, readFileSync as readFileSync13, statSync as statSync3 } from "fs";
44
+ import { homedir as homedir13 } from "os";
45
+ import { dirname as dirname10, join as join16 } from "path";
43
46
  import { fileURLToPath as fileURLToPath4 } from "url";
44
47
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
45
48
  import Database13 from "better-sqlite3";
@@ -47,6 +50,7 @@ import * as sqliteVec4 from "sqlite-vec";
47
50
 
48
51
  // src/artifact.ts
49
52
  init_esm_shims();
53
+ init_ulid();
50
54
  import { createHash } from "crypto";
51
55
  import { appendFileSync, existsSync, mkdirSync, readFileSync } from "fs";
52
56
  import { dirname, join } from "path";
@@ -167,8 +171,8 @@ function importArtifact(dbPath, projectRoot) {
167
171
  const transaction = db.transaction(() => {
168
172
  for (const row of rows) {
169
173
  const result = insertStmt.run(
170
- row.id,
171
- row.session_key,
174
+ row.id ?? generateId(),
175
+ row.session_key ?? "import",
172
176
  row.agent_id,
173
177
  row.type,
174
178
  row.content,
@@ -190,7 +194,7 @@ function importArtifact(dbPath, projectRoot) {
190
194
  transaction();
191
195
  db.close();
192
196
  if (inserted > 0) {
193
- console.log(
197
+ console.error(
194
198
  `[remem-mcp] Imported ${inserted} captures from team artifact (${skipped} already exist).`
195
199
  );
196
200
  }
@@ -718,6 +722,9 @@ async function demoCodegraph() {
718
722
  const distDir = dirname3(fileURLToPath2(import.meta.url));
719
723
  const indexPath = join3(distDir, "index.js");
720
724
  const cleanDb = new Database2(dbPath);
725
+ cleanDb.exec(`CREATE TABLE IF NOT EXISTS symbols (id TEXT PRIMARY KEY, repo_path TEXT NOT NULL, file_path TEXT NOT NULL, name TEXT NOT NULL, kind TEXT NOT NULL, line INTEGER, end_line INTEGER, created_at INTEGER NOT NULL)`);
726
+ cleanDb.exec(`CREATE TABLE IF NOT EXISTS calls (id TEXT PRIMARY KEY, repo_path TEXT NOT NULL, caller_id TEXT NOT NULL, callee_name TEXT NOT NULL, callee_id TEXT, call_line INTEGER, created_at INTEGER NOT NULL)`);
727
+ cleanDb.exec(`CREATE TABLE IF NOT EXISTS imports (id TEXT PRIMARY KEY, repo_path TEXT NOT NULL, file_path TEXT NOT NULL, module_path TEXT NOT NULL, imported_names TEXT, created_at INTEGER NOT NULL)`);
721
728
  cleanDb.exec("DELETE FROM calls");
722
729
  cleanDb.exec("DELETE FROM symbols");
723
730
  cleanDb.exec("DELETE FROM imports");
@@ -1004,7 +1011,7 @@ function extractRuleBasedFacts(content, type2) {
1004
1011
  if (fact.length > 10) facts.push({ text: fact, confidence: 0.85 });
1005
1012
  }
1006
1013
  const useMatch = text.match(/use\s+\S+\s+for\s+\S+/i);
1007
- if (useMatch && facts.length < 3) {
1014
+ if (useMatch && useMatch.index !== void 0 && facts.length < 3) {
1008
1015
  const fact = text.slice(useMatch.index, useMatch.index + useMatch[0].length);
1009
1016
  if (fact.length > 10) facts.push({ text: fact, confidence: 0.8 });
1010
1017
  }
@@ -1125,7 +1132,9 @@ async function extractCommand(dbPath, flags) {
1125
1132
  captures = entry ? [entry] : [];
1126
1133
  } else {
1127
1134
  const allCaptures = await storage.listAll(limit, 0);
1128
- captures = allCaptures.filter((e) => ["decision", "learning", "error", "conversation"].includes(e.type));
1135
+ captures = allCaptures.filter(
1136
+ (e) => ["decision", "learning", "error", "conversation"].includes(e.type)
1137
+ );
1129
1138
  }
1130
1139
  if (captures.length === 0) {
1131
1140
  console.log("No captures to extract atoms from.");
@@ -1175,8 +1184,10 @@ async function extractCommand(dbPath, flags) {
1175
1184
  console.error(` ${capture.id}: FAILED \u2014 ${err}`);
1176
1185
  }
1177
1186
  }
1178
- console.log(`
1179
- Done. Extracted ${totalAtoms} atom(s) from ${captures.length - skipped} capture(s).`);
1187
+ console.log(
1188
+ `
1189
+ Done. Extracted ${totalAtoms} atom(s) from ${captures.length - skipped} capture(s).`
1190
+ );
1180
1191
  if (skipped > 0) console.log(`${skipped} capture(s) already had atoms (skipped).`);
1181
1192
  if (errors2 > 0) console.log(`${errors2} capture(s) failed.`);
1182
1193
  } finally {
@@ -1282,7 +1293,9 @@ async function personaCommand(dbPath, flags) {
1282
1293
  }
1283
1294
  const db = new Database3(dbPath, { readonly: true });
1284
1295
  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);
1296
+ const row = db.prepare(
1297
+ "SELECT content FROM persona WHERE team_id = ? AND user_id = ? ORDER BY updated_at DESC LIMIT 1"
1298
+ ).get(teamId, userId);
1286
1299
  if (!row || !row.content) {
1287
1300
  console.log('No persona set. Use --set "trait: value" to add one.');
1288
1301
  return;
@@ -1521,7 +1534,7 @@ async function runPipelineWorker(opts, embedder) {
1521
1534
  type: cap.type,
1522
1535
  tags: cap.tags ? JSON.parse(cap.tags) : [],
1523
1536
  sessionKey: cap.session_key,
1524
- teamId: cap.team_id,
1537
+ teamId: cap.team_id ?? void 0,
1525
1538
  userId: cap.user_id ?? void 0
1526
1539
  };
1527
1540
  const output2 = await pipeline.process(input, ctx);
@@ -1542,9 +1555,7 @@ async function runPipelineWorker(opts, embedder) {
1542
1555
  groups.get(key).push(a);
1543
1556
  }
1544
1557
  const existingScenarios = await storage.listScenarios({ limit: 100 });
1545
- const existingTopics = new Set(
1546
- existingScenarios.flatMap((s) => s.personaTags ?? [])
1547
- );
1558
+ const existingTopics = new Set(existingScenarios.flatMap((s) => s.personaTags ?? []));
1548
1559
  for (const [topic, groupAtoms] of groups) {
1549
1560
  if (groupAtoms.length < consolidateThreshold) continue;
1550
1561
  if (existingTopics.has(topic)) continue;
@@ -1675,7 +1686,7 @@ function loadConfig() {
1675
1686
  model: model ?? "gpt-4o-mini"
1676
1687
  } : void 0;
1677
1688
  const fileSecurity = file?.security ?? {};
1678
- return {
1689
+ const cfg = {
1679
1690
  storage: env.REMEM_STORAGE ?? file?.storage ?? "sqlite",
1680
1691
  pipeline: env.REMEM_PIPELINE ?? file?.pipeline ?? "noop",
1681
1692
  dbPath: env.REMEM_DB_PATH ?? file?.dbPath ?? defaultDbPath4,
@@ -1699,8 +1710,20 @@ function loadConfig() {
1699
1710
  fileSecurity.maxContentLength ?? 5e4
1700
1711
  ),
1701
1712
  auditLog: parseBool(env.REMEM_AUDIT_LOG, fileSecurity.auditLog ?? true)
1702
- }
1713
+ },
1714
+ offloadEnabled: parseBool(env.REMEM_OFFLOAD_ENABLED, false)
1703
1715
  };
1716
+ if (env.REMEM_FLOW === "full") {
1717
+ if (!env.REMEM_OFFLOAD_ENABLED) {
1718
+ process.env.REMEM_OFFLOAD_ENABLED = "true";
1719
+ cfg.offloadEnabled = true;
1720
+ }
1721
+ if (!env.REMEM_PIPELINE) {
1722
+ process.env.REMEM_PIPELINE = "skill";
1723
+ cfg.pipeline = "skill";
1724
+ }
1725
+ }
1726
+ return cfg;
1704
1727
  }
1705
1728
 
1706
1729
  // src/doctor.ts
@@ -1861,7 +1884,7 @@ function errors(dbPath = defaultDbPath2()) {
1861
1884
  try {
1862
1885
  db = new Database5(dbPath, { readonly: true });
1863
1886
  } catch {
1864
- console.error("Error: Could not open database at", dbPath);
1887
+ console.log("No database found. Run `remem-mcp setup` to get started.");
1865
1888
  process.exit(1);
1866
1889
  }
1867
1890
  console.log("remem-mcp errors \u2014 Error Learning Dashboard\n");
@@ -1980,7 +2003,7 @@ function errorsRetro(dbPath = defaultDbPath2()) {
1980
2003
  try {
1981
2004
  db = new Database5(dbPath, { readonly: true });
1982
2005
  } catch {
1983
- console.error("Error: Could not open database at", dbPath);
2006
+ console.log("No database found. Run `remem-mcp setup` to get started.");
1984
2007
  process.exit(1);
1985
2008
  }
1986
2009
  console.log("remem-mcp errors retro \u2014 Session Retrospective\n");
@@ -2335,7 +2358,7 @@ function errorsDrift(dbPath = defaultDbPath2()) {
2335
2358
  try {
2336
2359
  db = new Database5(dbPath, { readonly: true });
2337
2360
  } catch {
2338
- console.error("Error: Could not open database at", dbPath);
2361
+ console.log("No database found. Run `remem-mcp setup` to get started.");
2339
2362
  process.exit(1);
2340
2363
  }
2341
2364
  console.log("remem-mcp errors drift \u2014 Drift Detection Report\n");
@@ -2446,7 +2469,7 @@ function errorsLineage(dbPath = defaultDbPath2()) {
2446
2469
  try {
2447
2470
  db = new Database5(dbPath, { readonly: true });
2448
2471
  } catch {
2449
- console.error("Error: Could not open database at", dbPath);
2472
+ console.log("No database found. Run `remem-mcp setup` to get started.");
2450
2473
  process.exit(1);
2451
2474
  }
2452
2475
  console.log("remem-mcp errors lineage \u2014 Fix Lineage Chains\n");
@@ -2615,7 +2638,7 @@ function errorsByGoal(dbPath = defaultDbPath2()) {
2615
2638
  try {
2616
2639
  db = new Database5(dbPath, { readonly: true });
2617
2640
  } catch {
2618
- console.error("Error: Could not open database at", dbPath);
2641
+ console.log("No database found. Run `remem-mcp setup` to get started.");
2619
2642
  process.exit(1);
2620
2643
  }
2621
2644
  console.log("remem-mcp errors by-goal \u2014 Goal-Linked Error Report\n");
@@ -2672,7 +2695,7 @@ function errorsActions(dbPath = defaultDbPath2()) {
2672
2695
  try {
2673
2696
  db = new Database5(dbPath, { readonly: true });
2674
2697
  } catch {
2675
- console.error("Error: Could not open database at", dbPath);
2698
+ console.log("No database found. Run `remem-mcp setup` to get started.");
2676
2699
  process.exit(1);
2677
2700
  }
2678
2701
  console.log("remem-mcp errors actions \u2014 Action Item Tracker\n");
@@ -2793,7 +2816,7 @@ function errorsSeverity(dbPath = defaultDbPath2()) {
2793
2816
  try {
2794
2817
  db = new Database5(dbPath, { readonly: true });
2795
2818
  } catch {
2796
- console.error("Error: Could not open database at", dbPath);
2819
+ console.log("No database found. Run `remem-mcp setup` to get started.");
2797
2820
  process.exit(1);
2798
2821
  }
2799
2822
  console.log("remem-mcp errors severity \u2014 Impact Classification\n");
@@ -2904,7 +2927,7 @@ function errorsTemplates(dbPath = defaultDbPath2()) {
2904
2927
  try {
2905
2928
  db = new Database5(dbPath, { readonly: true });
2906
2929
  } catch {
2907
- console.error("Error: Could not open database at", dbPath);
2930
+ console.log("No database found. Run `remem-mcp setup` to get started.");
2908
2931
  process.exit(1);
2909
2932
  }
2910
2933
  console.log("remem-mcp errors templates \u2014 Fix Template Extraction\n");
@@ -2976,7 +2999,7 @@ function errorsCorrelations(dbPath = defaultDbPath2()) {
2976
2999
  try {
2977
3000
  db = new Database5(dbPath, { readonly: true });
2978
3001
  } catch {
2979
- console.error("Error: Could not open database at", dbPath);
3002
+ console.log("No database found. Run `remem-mcp setup` to get started.");
2980
3003
  process.exit(1);
2981
3004
  }
2982
3005
  console.log("remem-mcp errors correlations \u2014 Sequential Error Patterns\n");
@@ -3057,7 +3080,7 @@ function errorsPlaybooks(dbPath = defaultDbPath2()) {
3057
3080
  try {
3058
3081
  db = new Database5(dbPath, { readonly: true });
3059
3082
  } catch {
3060
- console.error("Error: Could not open database at", dbPath);
3083
+ console.log("No database found. Run `remem-mcp setup` to get started.");
3061
3084
  process.exit(1);
3062
3085
  }
3063
3086
  console.log("remem-mcp errors playbooks \u2014 Recovery Pattern Library\n");
@@ -3125,7 +3148,7 @@ function errorsStale(dbPath = defaultDbPath2()) {
3125
3148
  try {
3126
3149
  db = new Database5(dbPath, { readonly: true });
3127
3150
  } catch {
3128
- console.error("Error: Could not open database at", dbPath);
3151
+ console.log("No database found. Run `remem-mcp setup` to get started.");
3129
3152
  process.exit(1);
3130
3153
  }
3131
3154
  console.log("remem-mcp errors stale \u2014 Fix Staleness Report\n");
@@ -3203,7 +3226,7 @@ function errorsEscalations(dbPath = defaultDbPath2()) {
3203
3226
  try {
3204
3227
  db = new Database5(dbPath, { readonly: true });
3205
3228
  } catch {
3206
- console.error("Error: Could not open database at", dbPath);
3229
+ console.log("No database found. Run `remem-mcp setup` to get started.");
3207
3230
  process.exit(1);
3208
3231
  }
3209
3232
  console.log("remem-mcp errors escalations \u2014 Escalation Policy Report\n");
@@ -3297,7 +3320,7 @@ function errorsContext(dbPath = defaultDbPath2()) {
3297
3320
  try {
3298
3321
  db = new Database5(dbPath, { readonly: true });
3299
3322
  } catch {
3300
- console.error("Error: Could not open database at", dbPath);
3323
+ console.log("No database found. Run `remem-mcp setup` to get started.");
3301
3324
  process.exit(1);
3302
3325
  }
3303
3326
  console.log("remem-mcp errors context \u2014 Error Context Enrichment\n");
@@ -3371,7 +3394,7 @@ function errorsInherited(dbPath = defaultDbPath2()) {
3371
3394
  try {
3372
3395
  db = new Database5(dbPath, { readonly: true });
3373
3396
  } catch {
3374
- console.error("Error: Could not open database at", dbPath);
3397
+ console.log("No database found. Run `remem-mcp setup` to get started.");
3375
3398
  process.exit(1);
3376
3399
  }
3377
3400
  console.log("remem-mcp errors inherited \u2014 Cross-Project Fix Inheritance\n");
@@ -3444,7 +3467,7 @@ function errorsProvenance(dbPath = defaultDbPath2()) {
3444
3467
  try {
3445
3468
  db = new Database5(dbPath, { readonly: true });
3446
3469
  } catch {
3447
- console.error("Error: Could not open database at", dbPath);
3470
+ console.log("No database found. Run `remem-mcp setup` to get started.");
3448
3471
  process.exit(1);
3449
3472
  }
3450
3473
  console.log("remem-mcp errors provenance \u2014 Fix Provenance Chain\n");
@@ -3520,7 +3543,7 @@ function errorsPersona(dbPath = defaultDbPath2()) {
3520
3543
  try {
3521
3544
  db = new Database5(dbPath, { readonly: true });
3522
3545
  } catch {
3523
- console.error("Error: Could not open database at", dbPath);
3546
+ console.log("No database found. Run `remem-mcp setup` to get started.");
3524
3547
  process.exit(1);
3525
3548
  }
3526
3549
  console.log("remem-mcp errors persona \u2014 Error Profile per Project\n");
@@ -3640,7 +3663,7 @@ function decisionsDashboard(dbPath = defaultDbPath2()) {
3640
3663
  try {
3641
3664
  db = new Database5(dbPath, { readonly: true });
3642
3665
  } catch {
3643
- console.error("Error: Could not open database at", dbPath);
3666
+ console.log("No database found. Run `remem-mcp setup` to get started.");
3644
3667
  process.exit(1);
3645
3668
  }
3646
3669
  console.log("remem-mcp decisions \u2014 Decision Learning Dashboard\n");
@@ -3707,7 +3730,7 @@ function decisionsRetro(dbPath = defaultDbPath2()) {
3707
3730
  try {
3708
3731
  db = new Database5(dbPath, { readonly: true });
3709
3732
  } catch {
3710
- console.error("Error: Could not open database at", dbPath);
3733
+ console.log("No database found. Run `remem-mcp setup` to get started.");
3711
3734
  process.exit(1);
3712
3735
  }
3713
3736
  console.log("remem-mcp decisions retro \u2014 Decision Retrospective\n");
@@ -3768,7 +3791,7 @@ function patternsDashboard(dbPath = defaultDbPath2()) {
3768
3791
  try {
3769
3792
  db = new Database5(dbPath, { readonly: true });
3770
3793
  } catch {
3771
- console.error("Error: Could not open database at", dbPath);
3794
+ console.log("No database found. Run `remem-mcp setup` to get started.");
3772
3795
  process.exit(1);
3773
3796
  }
3774
3797
  console.log("remem-mcp patterns \u2014 Pattern Learning Dashboard\n");
@@ -3843,7 +3866,7 @@ function patternsRetro(dbPath = defaultDbPath2()) {
3843
3866
  try {
3844
3867
  db = new Database5(dbPath, { readonly: true });
3845
3868
  } catch {
3846
- console.error("Error: Could not open database at", dbPath);
3869
+ console.log("No database found. Run `remem-mcp setup` to get started.");
3847
3870
  process.exit(1);
3848
3871
  }
3849
3872
  console.log("remem-mcp patterns retro \u2014 Pattern Retrospective\n");
@@ -3897,7 +3920,7 @@ function decisionsConflicts(dbPath = defaultDbPath2()) {
3897
3920
  try {
3898
3921
  db = new Database5(dbPath, { readonly: true });
3899
3922
  } catch {
3900
- console.error("Error: Could not open database at", dbPath);
3923
+ console.log("No database found. Run `remem-mcp setup` to get started.");
3901
3924
  process.exit(1);
3902
3925
  }
3903
3926
  console.log("remem-mcp decisions conflicts \u2014 Decision Conflict Report\n");
@@ -3941,7 +3964,7 @@ function decisionsInherited(dbPath = defaultDbPath2()) {
3941
3964
  try {
3942
3965
  db = new Database5(dbPath, { readonly: true });
3943
3966
  } catch {
3944
- console.error("Error: Could not open database at", dbPath);
3967
+ console.log("No database found. Run `remem-mcp setup` to get started.");
3945
3968
  process.exit(1);
3946
3969
  }
3947
3970
  console.log("remem-mcp decisions inherited \u2014 Cross-Project Decision Inheritance\n");
@@ -3984,7 +4007,7 @@ function patternsConflicts(dbPath = defaultDbPath2()) {
3984
4007
  try {
3985
4008
  db = new Database5(dbPath, { readonly: true });
3986
4009
  } catch {
3987
- console.error("Error: Could not open database at", dbPath);
4010
+ console.log("No database found. Run `remem-mcp setup` to get started.");
3988
4011
  process.exit(1);
3989
4012
  }
3990
4013
  console.log("remem-mcp patterns conflicts \u2014 Pattern Conflict Report\n");
@@ -4029,7 +4052,7 @@ function patternsTemplates(dbPath = defaultDbPath2()) {
4029
4052
  try {
4030
4053
  db = new Database5(dbPath, { readonly: true });
4031
4054
  } catch {
4032
- console.error("Error: Could not open database at", dbPath);
4055
+ console.log("No database found. Run `remem-mcp setup` to get started.");
4033
4056
  process.exit(1);
4034
4057
  }
4035
4058
  console.log("remem-mcp patterns templates \u2014 Pattern Template Extraction\n");
@@ -4081,7 +4104,7 @@ function patternsInherited(dbPath = defaultDbPath2()) {
4081
4104
  try {
4082
4105
  db = new Database5(dbPath, { readonly: true });
4083
4106
  } catch {
4084
- console.error("Error: Could not open database at", dbPath);
4107
+ console.log("No database found. Run `remem-mcp setup` to get started.");
4085
4108
  process.exit(1);
4086
4109
  }
4087
4110
  console.log("remem-mcp patterns inherited \u2014 Cross-Project Pattern Inheritance\n");
@@ -4273,11 +4296,58 @@ function exportMarkdown(dbPath, outputPath, filters) {
4273
4296
  init_esm_shims();
4274
4297
  import { execSync as execSync2, spawn as spawn2 } from "child_process";
4275
4298
  import { createHash as createHash3 } from "crypto";
4276
- import { appendFileSync as appendFileSync2, existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
4277
- import { homedir as homedir6, tmpdir as tmpdir2 } from "os";
4278
- import { dirname as dirname4, join as join8 } from "path";
4299
+ import { appendFileSync as appendFileSync2, existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
4300
+ import { homedir as homedir7, tmpdir as tmpdir2 } from "os";
4301
+ import { dirname as dirname4, join as join9 } from "path";
4279
4302
  import Database8 from "better-sqlite3";
4303
+
4304
+ // src/storage/refs.ts
4305
+ init_esm_shims();
4306
+ import {
4307
+ existsSync as existsSync7,
4308
+ mkdirSync as mkdirSync4,
4309
+ readdirSync,
4310
+ readFileSync as readFileSync5,
4311
+ rmSync as rmSync2,
4312
+ statSync as statSync2,
4313
+ writeFileSync as writeFileSync4
4314
+ } from "fs";
4315
+ import { homedir as homedir6 } from "os";
4316
+ import { join as join8 } from "path";
4317
+ function defaultRefsDir() {
4318
+ const xdgData = process.env.XDG_DATA_HOME;
4319
+ if (xdgData) return join8(xdgData, "remem-mcp", "refs");
4320
+ return join8(homedir6(), ".local", "share", "remem-mcp", "refs");
4321
+ }
4322
+ function getRefsDir() {
4323
+ return process.env.REMEM_REFS_DIR ?? defaultRefsDir();
4324
+ }
4325
+ function getSessionRefsDir(sessionKey) {
4326
+ return join8(getRefsDir(), sessionKey);
4327
+ }
4328
+ function sanitizeSessionKey(sessionKey) {
4329
+ return sessionKey.replace(/[^a-zA-Z0-9_-]/g, "_");
4330
+ }
4331
+ function sanitizeNodeId(nodeId) {
4332
+ return nodeId.replace(/[^a-zA-Z0-9_-]/g, "_");
4333
+ }
4334
+ function writeRef(sessionKey, nodeId, content) {
4335
+ const dir = getSessionRefsDir(sanitizeSessionKey(sessionKey));
4336
+ if (!existsSync7(dir)) mkdirSync4(dir, { recursive: true });
4337
+ const filePath = join8(dir, `${sanitizeNodeId(nodeId)}.md`);
4338
+ writeFileSync4(filePath, content, "utf-8");
4339
+ return filePath;
4340
+ }
4341
+
4342
+ // src/hook-handlers.ts
4343
+ init_ulid();
4280
4344
  var SHOW_FEEDBACK = process.env.REMEM_QUIET !== "1";
4345
+ function isOffloadEnabled() {
4346
+ return process.env.REMEM_OFFLOAD_ENABLED === "true" || process.env.REMEM_OFFLOAD_ENABLED === "1" || process.env.REMEM_FLOW === "full";
4347
+ }
4348
+ function isSkillPipelineEnabled() {
4349
+ return process.env.REMEM_PIPELINE === "skill" || process.env.REMEM_FLOW === "full";
4350
+ }
4281
4351
  function feedback(emoji, message) {
4282
4352
  if (SHOW_FEEDBACK) {
4283
4353
  process.stderr.write(`
@@ -4287,12 +4357,12 @@ function feedback(emoji, message) {
4287
4357
  }
4288
4358
  }
4289
4359
  function defaultLogPath() {
4290
- return process.env.REMEM_HOOK_LOG_PATH ?? join8(homedir6(), ".local", "share", "remem-mcp", "session.log");
4360
+ return process.env.REMEM_HOOK_LOG_PATH ?? join9(homedir7(), ".local", "share", "remem-mcp", "session.log");
4291
4361
  }
4292
4362
  function logToFile(text) {
4293
4363
  try {
4294
4364
  const logPath = defaultLogPath();
4295
- mkdirSync4(dirname4(logPath), { recursive: true });
4365
+ mkdirSync5(dirname4(logPath), { recursive: true });
4296
4366
  const ts = (/* @__PURE__ */ new Date()).toISOString();
4297
4367
  appendFileSync2(logPath, `[${ts}] ${text}
4298
4368
  `);
@@ -4309,11 +4379,11 @@ function safeParseTags(raw) {
4309
4379
  }
4310
4380
  }
4311
4381
  function driftFilePath(sessionKey) {
4312
- return join8(tmpdir2(), `remem-drift-${sessionKey}.jsonl`);
4382
+ return join9(tmpdir2(), `remem-drift-${sessionKey}.jsonl`);
4313
4383
  }
4314
4384
  function logInjectionDrift(sessionKey, captureType, captureId, command) {
4315
4385
  try {
4316
- const path = join8(tmpdir2(), `remem-drift-${sessionKey}.jsonl`);
4386
+ const path = join9(tmpdir2(), `remem-drift-${sessionKey}.jsonl`);
4317
4387
  const record = JSON.stringify({
4318
4388
  type: captureType,
4319
4389
  content_hash: captureId,
@@ -4472,8 +4542,8 @@ function logDriftInjection(sessionKey, contentHash, errorId, command) {
4472
4542
  function checkDriftInjection(sessionKey, contentHash) {
4473
4543
  try {
4474
4544
  const path = driftFilePath(sessionKey);
4475
- if (!existsSync7(path)) return null;
4476
- const content = readFileSync5(path, "utf-8");
4545
+ if (!existsSync8(path)) return null;
4546
+ const content = readFileSync6(path, "utf-8");
4477
4547
  const lines = content.trim().split("\n").filter(Boolean);
4478
4548
  const now = Date.now();
4479
4549
  const maxAge = 30 * 60 * 1e3;
@@ -4492,7 +4562,7 @@ function checkDriftInjection(sessionKey, contentHash) {
4492
4562
  }
4493
4563
  }
4494
4564
  if (recent.length !== lines.length) {
4495
- writeFileSync4(path, recent.join("\n") + (recent.length > 0 ? "\n" : ""));
4565
+ writeFileSync5(path, recent.join("\n") + (recent.length > 0 ? "\n" : ""));
4496
4566
  }
4497
4567
  return match;
4498
4568
  } catch {
@@ -4502,8 +4572,8 @@ function checkDriftInjection(sessionKey, contentHash) {
4502
4572
  function checkAllDriftInjections(sessionKey) {
4503
4573
  try {
4504
4574
  const path = driftFilePath(sessionKey);
4505
- if (!existsSync7(path)) return [];
4506
- const content = readFileSync5(path, "utf-8");
4575
+ if (!existsSync8(path)) return [];
4576
+ const content = readFileSync6(path, "utf-8");
4507
4577
  const lines = content.trim().split("\n").filter(Boolean);
4508
4578
  const now = Date.now();
4509
4579
  const maxAge = 30 * 60 * 1e3;
@@ -4520,7 +4590,7 @@ function checkAllDriftInjections(sessionKey) {
4520
4590
  }
4521
4591
  }
4522
4592
  if (recent.length !== lines.length) {
4523
- writeFileSync4(path, recent.join("\n") + (recent.length > 0 ? "\n" : ""));
4593
+ writeFileSync5(path, recent.join("\n") + (recent.length > 0 ? "\n" : ""));
4524
4594
  }
4525
4595
  return results;
4526
4596
  } catch {
@@ -4595,7 +4665,9 @@ function hookRecall(dbPath) {
4595
4665
  lines.push(`- (${row.type}${tagStr}) ${date}: ${content}`);
4596
4666
  }
4597
4667
  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");
4668
+ const personaRow = db.prepare(
4669
+ "SELECT content FROM persona WHERE team_id = ? AND user_id = ? ORDER BY updated_at DESC LIMIT 1"
4670
+ ).get("default", "default");
4599
4671
  if (personaRow && personaRow.content) {
4600
4672
  lines.push("");
4601
4673
  lines.push(`## Persona (L3)`);
@@ -4662,11 +4734,20 @@ function extractUpdateFact(prompt2) {
4662
4734
  if (p.length < 15) return null;
4663
4735
  const updatePatterns = [
4664
4736
  // "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" },
4737
+ {
4738
+ re: /^(?:remember that|remember|don't forget|dont forget|note that|for future reference)\s*:?\s*(.+)/i,
4739
+ type: "learning"
4740
+ },
4666
4741
  { re: /^(?:nhớ là|nhớ rằng|đừng quên|ghi nhớ)\s*:?\s*(.+)/i, type: "learning" },
4667
4742
  // "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" },
4743
+ {
4744
+ re: /^(?:we use|we chose|we decided|we prefer|we always|our convention|our standard)\s*:?\s*(.+)/i,
4745
+ type: "decision"
4746
+ },
4747
+ {
4748
+ 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,
4749
+ type: "decision"
4750
+ },
4670
4751
  // "update X to Y" / "change X to Y" / "sửa X thành Y"
4671
4752
  { re: /^(?:update|change|set|configure)\s+.+\s+(?:to|as|=\s*)\s*(.+)/i, type: "decision" },
4672
4753
  { re: /^(?:sửa|đổi|cập nhật)\s+.+\s+(?:thành|sang|=\s*)\s*(.+)/i, type: "decision" },
@@ -4729,6 +4810,7 @@ function hookUserPromptSubmit(dbPath) {
4729
4810
  let db;
4730
4811
  try {
4731
4812
  db = new Database8(dbPath);
4813
+ ensureSchema2(db);
4732
4814
  } catch {
4733
4815
  try {
4734
4816
  db = new Database8(dbPath, { readonly: true });
@@ -4846,7 +4928,9 @@ ${scenarioLines.join("\n")}`;
4846
4928
  } catch {
4847
4929
  }
4848
4930
  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");
4931
+ const personaRow = db.prepare(
4932
+ "SELECT content FROM persona WHERE team_id = ? AND user_id = ? ORDER BY updated_at DESC LIMIT 1"
4933
+ ).get("default", "default");
4850
4934
  if (personaRow && personaRow.content) {
4851
4935
  if (recallContext) {
4852
4936
  recallContext += `
@@ -4863,7 +4947,9 @@ ${personaRow.content}`;
4863
4947
  try {
4864
4948
  const skills = db.prepare("SELECT name, description FROM skills ORDER BY updated_at DESC LIMIT 3").all();
4865
4949
  if (skills.length > 0) {
4866
- const skillLines = skills.map((s) => `- ${s.name}: ${(s.description || "").slice(0, 80)}`);
4950
+ const skillLines = skills.map(
4951
+ (s) => `- ${s.name}: ${(s.description || "").slice(0, 80)}`
4952
+ );
4867
4953
  if (recallContext) {
4868
4954
  recallContext += `
4869
4955
 
@@ -4941,9 +5027,12 @@ function hookStop(dbPath) {
4941
5027
  if (dbPath && input.session_id) {
4942
5028
  const sid = input.session_id;
4943
5029
  const tpath = input.transcript_path ?? null;
4944
- if (tpath && existsSync7(tpath)) {
5030
+ if (tpath && existsSync8(tpath)) {
4945
5031
  void captureSessionTranscript(dbPath, sid, tpath, input.cwd).then((capId) => {
4946
5032
  logToFile(`Stop: direct capture for session ${sid}, id=${capId ?? "skipped"}`);
5033
+ if (capId && isSkillPipelineEnabled()) {
5034
+ void extractSkillFromCapture(dbPath, capId).then(() => logToFile(`Stop: skill extraction done for ${capId}`)).catch((err) => logToFile(`Stop: skill extraction error - ${err}`));
5035
+ }
4947
5036
  }).catch((err) => {
4948
5037
  logToFile(`Stop: capture error - ${err}`);
4949
5038
  });
@@ -4958,11 +5047,10 @@ function hookStop(dbPath) {
4958
5047
  logToFile(`Stop: spawned background capture for session ${sid}`);
4959
5048
  }
4960
5049
  const scriptPath = process.argv[1];
4961
- const workerChild = spawn2(
4962
- process.execPath,
4963
- [scriptPath, "worker-run", dbPath],
4964
- { detached: true, stdio: "ignore" }
4965
- );
5050
+ const workerChild = spawn2(process.execPath, [scriptPath, "worker-run", dbPath], {
5051
+ detached: true,
5052
+ stdio: "ignore"
5053
+ });
4966
5054
  workerChild.unref();
4967
5055
  logToFile(`Stop: spawned background pipeline worker for session ${sid}`);
4968
5056
  }
@@ -4972,9 +5060,9 @@ function hookStop(dbPath) {
4972
5060
  }
4973
5061
  const isTrivial = (() => {
4974
5062
  const tpath = input.transcript_path;
4975
- if (!tpath || !existsSync7(tpath)) return false;
5063
+ if (!tpath || !existsSync8(tpath)) return false;
4976
5064
  try {
4977
- const lines = readFileSync5(tpath, "utf-8").trim().split("\n");
5065
+ const lines = readFileSync6(tpath, "utf-8").trim().split("\n");
4978
5066
  let userCount = 0;
4979
5067
  let hasEdit = false;
4980
5068
  for (const line2 of lines) {
@@ -5031,9 +5119,23 @@ function hookPostToolUse(dbPath) {
5031
5119
  const isBash = toolName === "Bash" || toolName === "exec";
5032
5120
  const isWriteEdit = toolName === "Write" || toolName === "Edit" || toolName === "MultiEdit";
5033
5121
  if (!isBash && !isWriteEdit) {
5122
+ if (isOffloadEnabled()) {
5123
+ try {
5124
+ offloadToolOutput(dbPath, toolName, toolInput, toolResponse, input.cwd);
5125
+ } catch (e) {
5126
+ logToFile(`PostToolUse: offload failed: ${e}`);
5127
+ }
5128
+ }
5034
5129
  process.stdout.write(JSON.stringify({}));
5035
5130
  return;
5036
5131
  }
5132
+ if (isOffloadEnabled()) {
5133
+ try {
5134
+ offloadToolOutput(dbPath, toolName, toolInput, toolResponse, input.cwd);
5135
+ } catch (e) {
5136
+ logToFile(`PostToolUse: offload failed: ${e}`);
5137
+ }
5138
+ }
5037
5139
  if (isWriteEdit) {
5038
5140
  const writeEditCwd = input.cwd ?? toolInput.workdir ?? process.cwd();
5039
5141
  const ignorePatterns = loadCaptureExclusions(writeEditCwd);
@@ -5048,6 +5150,7 @@ function hookPostToolUse(dbPath) {
5048
5150
  }
5049
5151
  try {
5050
5152
  const db2 = new Database8(dbPath);
5153
+ ensureSchema2(db2);
5051
5154
  const sessionKey2 = hashPath(writeEditCwd);
5052
5155
  const pattern = detectPattern(toolName, toolInput);
5053
5156
  if (pattern) {
@@ -5149,6 +5252,7 @@ function hookPostToolUse(dbPath) {
5149
5252
  const driftRecords = checkAllDriftInjections(sessionKey2);
5150
5253
  if (driftRecords.length > 0 && pattern) {
5151
5254
  const driftDb = new Database8(dbPath);
5255
+ ensureSchema2(driftDb);
5152
5256
  for (const dr of driftRecords) {
5153
5257
  if (dr.type === "pattern") {
5154
5258
  const injectedPat = driftDb.prepare("SELECT id, metadata FROM captures WHERE id = ?").get(dr.content_hash);
@@ -5206,6 +5310,7 @@ function hookPostToolUse(dbPath) {
5206
5310
  let db;
5207
5311
  try {
5208
5312
  db = new Database8(dbPath);
5313
+ ensureSchema2(db);
5209
5314
  } catch {
5210
5315
  process.stdout.write(JSON.stringify({}));
5211
5316
  return;
@@ -5588,6 +5693,7 @@ Error (${errorType}): ${normalizedError}`;
5588
5693
  logToFile(`PostToolUse: auto-captured ${errorType} error. id=${id}`);
5589
5694
  try {
5590
5695
  const corrDb = new Database8(dbPath);
5696
+ ensureSchema2(corrDb);
5591
5697
  const recentErrors = corrDb.prepare(
5592
5698
  `SELECT id, json_extract(metadata, '$.error_type') as etype, json_extract(metadata, '$.title') as title
5593
5699
  FROM captures
@@ -5661,6 +5767,7 @@ function hookPreToolUse(dbPath) {
5661
5767
  });
5662
5768
  process.stdin.on("end", () => {
5663
5769
  clearTimeout(stdinTimeout);
5770
+ const contextBlocks = [];
5664
5771
  try {
5665
5772
  const raw = Buffer.concat(chunks).toString("utf-8");
5666
5773
  if (!raw.trim()) {
@@ -5671,17 +5778,28 @@ function hookPreToolUse(dbPath) {
5671
5778
  const toolName = input.tool_name ?? "";
5672
5779
  const toolInput = input.tool_input ?? {};
5673
5780
  const command = toolInput.command ?? "";
5781
+ if (isOffloadEnabled()) {
5782
+ try {
5783
+ const canvasContext = getCanvasContext(dbPath, input.cwd ?? process.cwd());
5784
+ if (canvasContext) {
5785
+ contextBlocks.push(canvasContext);
5786
+ }
5787
+ } catch (e) {
5788
+ logToFile(`PreToolUse: canvas injection failed: ${e}`);
5789
+ }
5790
+ }
5791
+ try {
5792
+ const skillContext = getSkillContext(dbPath, command || toolName);
5793
+ if (skillContext) {
5794
+ contextBlocks.push(skillContext);
5795
+ }
5796
+ } catch (e) {
5797
+ logToFile(`PreToolUse: skill injection failed: ${e}`);
5798
+ }
5674
5799
  const dangerWarning = checkDangerousCommand(command);
5675
5800
  if (dangerWarning) {
5676
- const output3 = {
5677
- hookSpecificOutput: {
5678
- hookEventName: "PreToolUse",
5679
- additionalContext: dangerWarning
5680
- }
5681
- };
5801
+ contextBlocks.push(dangerWarning);
5682
5802
  logToFile(`PreToolUse: DANGER warning for: ${command.slice(0, 80)}`);
5683
- process.stdout.write(JSON.stringify(output3));
5684
- return;
5685
5803
  }
5686
5804
  if (toolName === "Write" || toolName === "Edit" || toolName === "MultiEdit") {
5687
5805
  const filePath = toolInput.file_path ?? toolInput.path ?? "";
@@ -5824,12 +5942,29 @@ function hookPreToolUse(dbPath) {
5824
5942
  } catch {
5825
5943
  }
5826
5944
  }
5827
- process.stdout.write(JSON.stringify({}));
5945
+ if (contextBlocks.length > 0) {
5946
+ process.stdout.write(JSON.stringify({
5947
+ hookSpecificOutput: {
5948
+ hookEventName: "PreToolUse",
5949
+ additionalContext: contextBlocks.join("\n\n")
5950
+ }
5951
+ }));
5952
+ } else {
5953
+ process.stdout.write(JSON.stringify({}));
5954
+ }
5828
5955
  return;
5829
5956
  }
5830
- const isRelevantCommand = /^(npm|npx|yarn|pnpm|biome|eslint|tsc|cargo|make|pytest|vitest|jest)\b/.test(command) || /\b(lint|test|build|typecheck|check|format)\b/.test(command);
5831
- if (!isRelevantCommand) {
5832
- process.stdout.write(JSON.stringify({}));
5957
+ if (!isRelevantBuildCommand(command) && !isDecisionLearningCommand(command)) {
5958
+ if (contextBlocks.length > 0) {
5959
+ process.stdout.write(JSON.stringify({
5960
+ hookSpecificOutput: {
5961
+ hookEventName: "PreToolUse",
5962
+ additionalContext: contextBlocks.join("\n\n")
5963
+ }
5964
+ }));
5965
+ } else {
5966
+ process.stdout.write(JSON.stringify({}));
5967
+ }
5833
5968
  return;
5834
5969
  }
5835
5970
  const cwd = input.cwd ?? toolInput.workdir ?? process.cwd();
@@ -5838,7 +5973,16 @@ function hookPreToolUse(dbPath) {
5838
5973
  try {
5839
5974
  db = new Database8(dbPath, { readonly: true });
5840
5975
  } catch {
5841
- process.stdout.write(JSON.stringify({}));
5976
+ if (contextBlocks.length > 0) {
5977
+ process.stdout.write(JSON.stringify({
5978
+ hookSpecificOutput: {
5979
+ hookEventName: "PreToolUse",
5980
+ additionalContext: contextBlocks.join("\n\n")
5981
+ }
5982
+ }));
5983
+ } else {
5984
+ process.stdout.write(JSON.stringify({}));
5985
+ }
5842
5986
  return;
5843
5987
  }
5844
5988
  const globalErrors = process.env.REMEM_GLOBAL_ERRORS === "1";
@@ -5887,7 +6031,8 @@ function hookPreToolUse(dbPath) {
5887
6031
  provenance: m.fix_provenance ?? "auto_captured",
5888
6032
  inherited: r.session_key !== sessionKey,
5889
6033
  rollbackPlan: m.rollback_plan ?? null,
5890
- autoNotes: m.auto_notes ?? []
6034
+ autoNotes: m.auto_notes ?? [],
6035
+ failedCommand: m.command ?? ""
5891
6036
  };
5892
6037
  });
5893
6038
  if (resolvedFixes.length < 2) {
@@ -5913,14 +6058,20 @@ function hookPreToolUse(dbPath) {
5913
6058
  provenance: "inherited",
5914
6059
  inherited: true,
5915
6060
  rollbackPlan: m.rollback_plan ?? null,
5916
- autoNotes: m.auto_notes ?? []
6061
+ autoNotes: m.auto_notes ?? [],
6062
+ failedCommand: m.command ?? ""
5917
6063
  });
5918
6064
  }
5919
6065
  }
5920
6066
  rDb.close();
5921
6067
  } catch {
5922
6068
  }
5923
- if (errors2.length === 0 && resolvedFixes.length === 0) {
6069
+ const relevantErrors = errors2.filter((err) => {
6070
+ const meta = err.metadata ? JSON.parse(err.metadata) : {};
6071
+ return isErrorRelevantToCommand(command, meta);
6072
+ });
6073
+ const relevantFixes = resolvedFixes;
6074
+ if (relevantErrors.length === 0 && relevantFixes.length === 0) {
5924
6075
  try {
5925
6076
  const decDb = new Database8(dbPath, { readonly: true });
5926
6077
  const lowerCmd = command.toLowerCase();
@@ -6009,19 +6160,19 @@ function hookPreToolUse(dbPath) {
6009
6160
  process.stdout.write(JSON.stringify({}));
6010
6161
  return;
6011
6162
  }
6012
- const validErrors = errors2.filter((err) => {
6013
- const meta = JSON.parse(err.metadata);
6163
+ const validErrors = relevantErrors.filter((err) => {
6164
+ const meta = err.metadata ? JSON.parse(err.metadata) : {};
6014
6165
  const filesInError = extractFilePaths(err.content);
6015
6166
  if (filesInError.length === 0) return true;
6016
- return filesInError.some((f) => existsSync7(join8(cwd, f)));
6167
+ return filesInError.some((f) => existsSync8(join9(cwd, f)));
6017
6168
  });
6018
- if (validErrors.length === 0 && resolvedFixes.length === 0) {
6169
+ if (validErrors.length === 0 && relevantFixes.length === 0) {
6019
6170
  logToFile("PreToolUse: all past errors are stale and no proven fixes");
6020
6171
  process.stdout.write(JSON.stringify({}));
6021
6172
  return;
6022
6173
  }
6023
6174
  const decayed = validErrors.map((err) => {
6024
- const meta = JSON.parse(err.metadata);
6175
+ const meta = err.metadata ? JSON.parse(err.metadata) : {};
6025
6176
  const base = meta.confidence ?? 2;
6026
6177
  const decayed2 = applyConfidenceDecay(base, err.created_at, meta.last_recurred);
6027
6178
  return { err, meta, decayedConfidence: decayed2 };
@@ -6054,12 +6205,12 @@ function hookPreToolUse(dbPath) {
6054
6205
  }
6055
6206
  }
6056
6207
  }
6057
- if (resolvedFixes.length > 0) {
6208
+ if (relevantFixes.length > 0) {
6058
6209
  lines.push("");
6059
6210
  lines.push("Proven fixes from past resolved errors:");
6060
6211
  const stalenessDays = Number(process.env.REMEM_FIX_STALENESS_DAYS ?? 180);
6061
6212
  const stalenessMs = stalenessDays * 864e5;
6062
- for (const fix of resolvedFixes) {
6213
+ for (const fix of relevantFixes) {
6063
6214
  const fixAgeMs = Date.now() - new Date(fix.resolvedAt).getTime();
6064
6215
  const isStale = fixAgeMs > stalenessMs;
6065
6216
  const staleTag = isStale ? " [STALE \u2014 verify before applying]" : "";
@@ -6155,7 +6306,7 @@ function hookPreToolUse(dbPath) {
6155
6306
  }
6156
6307
  } catch {
6157
6308
  }
6158
- const context = lines.join("\n");
6309
+ let context = lines.join("\n");
6159
6310
  const k = decayed.length;
6160
6311
  logToFile(
6161
6312
  `PreToolUse: injected ${k} past error(s) (k=${k}, decayed confidence, ${resolvedFixes.length} fixes) before: ${command.slice(0, 60)}`
@@ -6170,6 +6321,9 @@ function hookPreToolUse(dbPath) {
6170
6321
  logDriftInjection(sessionKey, err.content_hash, err.id, command);
6171
6322
  }
6172
6323
  }
6324
+ if (contextBlocks.length > 0) {
6325
+ context = contextBlocks.join("\n\n") + (context ? "\n\n" + context : "");
6326
+ }
6173
6327
  const output2 = {
6174
6328
  hookSpecificOutput: {
6175
6329
  hookEventName: "PreToolUse",
@@ -6181,7 +6335,16 @@ function hookPreToolUse(dbPath) {
6181
6335
  process.stderr.write(`[remem-mcp hook-pre-tool-use] Error: ${err}
6182
6336
  `);
6183
6337
  logToFile(`PreToolUse: error - ${err}`);
6184
- process.stdout.write(JSON.stringify({}));
6338
+ if (contextBlocks.length > 0) {
6339
+ process.stdout.write(JSON.stringify({
6340
+ hookSpecificOutput: {
6341
+ hookEventName: "PreToolUse",
6342
+ additionalContext: contextBlocks.join("\n\n")
6343
+ }
6344
+ }));
6345
+ } else {
6346
+ process.stdout.write(JSON.stringify({}));
6347
+ }
6185
6348
  }
6186
6349
  });
6187
6350
  }
@@ -6206,9 +6369,9 @@ function loadCaptureExclusions(cwd) {
6206
6369
  try {
6207
6370
  let dir = cwd;
6208
6371
  for (let i = 0; i < MAX_ANCESTORS; i++) {
6209
- const markerPath = join8(dir, ".remem.toml");
6210
- if (existsSync7(markerPath)) {
6211
- const content = readFileSync5(markerPath, "utf-8");
6372
+ const markerPath = join9(dir, ".remem.toml");
6373
+ if (existsSync8(markerPath)) {
6374
+ const content = readFileSync6(markerPath, "utf-8");
6212
6375
  patterns = parseCaptureExclusions(content);
6213
6376
  break;
6214
6377
  }
@@ -6230,7 +6393,8 @@ function shouldExcludePath(filePath, ignorePatterns) {
6230
6393
  if (normalized.endsWith(pattern.slice(1))) return true;
6231
6394
  continue;
6232
6395
  }
6233
- if (segments.includes(pattern)) return true;
6396
+ const stripped = pattern.replace(/\/\*.*$/, "").replace(/\*.*$/, "");
6397
+ if (stripped && segments.includes(stripped)) return true;
6234
6398
  }
6235
6399
  return false;
6236
6400
  }
@@ -6259,6 +6423,98 @@ function isNoiseCommand(command) {
6259
6423
  if (/git\s+diff\s+.*--exit-code/.test(lower)) return true;
6260
6424
  return false;
6261
6425
  }
6426
+ var COMMAND_INTENTS = {
6427
+ biome: "check",
6428
+ build: "build",
6429
+ cargo: "build",
6430
+ check: "check",
6431
+ eslint: "lint",
6432
+ format: "format",
6433
+ jest: "test",
6434
+ lint: "lint",
6435
+ make: "build",
6436
+ mvn: "build",
6437
+ mypy: "typecheck",
6438
+ pytest: "test",
6439
+ ruff: "lint",
6440
+ tsc: "typecheck",
6441
+ typecheck: "typecheck",
6442
+ vitest: "test"
6443
+ };
6444
+ function getCommandIntent(command) {
6445
+ for (const segment of command.split(/&&|\|\||[;|]/)) {
6446
+ let part = segment.trim();
6447
+ if (!part) continue;
6448
+ while (/^[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|'[^']*'|\S*)\s+/.test(part)) {
6449
+ part = part.replace(/^[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|'[^']*'|\S*)\s+/, "");
6450
+ }
6451
+ const tokens = part.split(/\s+/).filter(Boolean);
6452
+ if (tokens.length === 0) continue;
6453
+ while (tokens[0] === "sudo" || tokens[0] === "time") tokens.shift();
6454
+ const executable = (tokens[0] ?? "").split("/").pop() ?? "";
6455
+ if (["cd", "pushd", "popd"].includes(executable)) continue;
6456
+ if (["npm", "pnpm", "yarn"].includes(executable)) {
6457
+ const args = tokens.slice(1);
6458
+ const runAt = args.findIndex((arg) => arg === "run" || arg === "exec");
6459
+ const action = runAt >= 0 ? args[runAt + 1] : args.find((arg) => !arg.startsWith("-"));
6460
+ if (action && COMMAND_INTENTS[action]) return COMMAND_INTENTS[action];
6461
+ continue;
6462
+ }
6463
+ if (executable === "python" && tokens[1] === "-m") {
6464
+ const module = (tokens[2] ?? "").split("/").pop() ?? "";
6465
+ if (COMMAND_INTENTS[module]) return COMMAND_INTENTS[module];
6466
+ continue;
6467
+ }
6468
+ if (COMMAND_INTENTS[executable]) return COMMAND_INTENTS[executable];
6469
+ if (["cargo", "dotnet", "go", "gradle", "mvn"].includes(executable)) {
6470
+ const action = (tokens[1] ?? "").toLowerCase();
6471
+ if (action === "test") return "test";
6472
+ if (action === "fmt") return "format";
6473
+ if (["build", "vet", "clippy"].includes(action)) return "check";
6474
+ }
6475
+ }
6476
+ return null;
6477
+ }
6478
+ function isRelevantBuildCommand(command) {
6479
+ return getCommandIntent(command) !== null;
6480
+ }
6481
+ function isDecisionLearningCommand(command) {
6482
+ return /\b(?:npm|pnpm|yarn)\s+(?:install|i|add)\b/.test(command) || /\b(?:pip|uv)\s+install\b/.test(command) || /\bcargo\s+add\b/.test(command) || /\bgit\s+commit\b/.test(command);
6483
+ }
6484
+ function isErrorRelevantToCommand(command, meta) {
6485
+ const intent = getCommandIntent(command);
6486
+ if (!intent) return false;
6487
+ const failedCommand = typeof meta.command === "string" ? meta.command : "";
6488
+ if (failedCommand) {
6489
+ if (getCommandIntent(failedCommand) === intent) return true;
6490
+ const genericWords = /* @__PURE__ */ new Set([
6491
+ "and",
6492
+ "error",
6493
+ "failed",
6494
+ "false",
6495
+ "npm",
6496
+ "npx",
6497
+ "pnpm",
6498
+ "run",
6499
+ "the",
6500
+ "true",
6501
+ "yarn"
6502
+ ]);
6503
+ const words = (text) => new Set(
6504
+ text.toLowerCase().split(/[^a-z0-9@/_-]+/).filter(
6505
+ (word) => word.length >= 3 && !genericWords.has(word)
6506
+ )
6507
+ );
6508
+ const currentWords = words(command);
6509
+ const failedWords = words(failedCommand);
6510
+ for (const word of currentWords) {
6511
+ if (failedWords.has(word)) return true;
6512
+ }
6513
+ return false;
6514
+ }
6515
+ const errorType = typeof meta.error_type === "string" ? meta.error_type.toLowerCase() : "";
6516
+ return !errorType || errorType === intent || intent === "check";
6517
+ }
6262
6518
  function checkDangerousCommand(command) {
6263
6519
  const lower = command.toLowerCase();
6264
6520
  if (/git\s+push\s+(--force|-f)(?!\S)/.test(lower)) {
@@ -6630,13 +6886,118 @@ function extractFilePaths(content) {
6630
6886
  function hashPath(path) {
6631
6887
  return createHash3("sha256").update(path).digest("hex").slice(0, 16);
6632
6888
  }
6889
+ function ensureSchema2(db) {
6890
+ db.exec(`CREATE TABLE IF NOT EXISTS captures (
6891
+ id TEXT PRIMARY KEY,
6892
+ session_key TEXT NOT NULL,
6893
+ team_id TEXT,
6894
+ agent_id TEXT,
6895
+ user_id TEXT,
6896
+ type TEXT NOT NULL,
6897
+ content TEXT NOT NULL,
6898
+ tags TEXT,
6899
+ metadata TEXT,
6900
+ content_hash TEXT,
6901
+ created_at INTEGER NOT NULL,
6902
+ deleted_at INTEGER,
6903
+ trust_state TEXT DEFAULT 'trusted',
6904
+ rejection_reason TEXT,
6905
+ superseded_by TEXT,
6906
+ access_count INTEGER DEFAULT 0,
6907
+ last_accessed_at INTEGER,
6908
+ bayesian_alpha REAL DEFAULT 1.0,
6909
+ bayesian_beta REAL DEFAULT 1.0,
6910
+ fix_validated INTEGER,
6911
+ fix_validated_at INTEGER,
6912
+ escalation_level INTEGER DEFAULT 0
6913
+ )`);
6914
+ db.exec(`CREATE TABLE IF NOT EXISTS error_patterns (
6915
+ id TEXT PRIMARY KEY,
6916
+ session_key TEXT NOT NULL,
6917
+ command TEXT NOT NULL,
6918
+ error_type TEXT,
6919
+ error_message TEXT,
6920
+ content_hash TEXT,
6921
+ confidence REAL DEFAULT 2.0,
6922
+ occurrences INTEGER DEFAULT 1,
6923
+ first_seen INTEGER NOT NULL,
6924
+ last_seen INTEGER NOT NULL,
6925
+ resolved_at INTEGER,
6926
+ fix_command TEXT,
6927
+ fix_approach TEXT,
6928
+ fix_validated INTEGER,
6929
+ fix_recorded_at INTEGER,
6930
+ escalation_level INTEGER DEFAULT 0,
6931
+ root_cause TEXT,
6932
+ auto_notes TEXT,
6933
+ deleted_at INTEGER
6934
+ )`);
6935
+ db.exec(`CREATE TABLE IF NOT EXISTS decisions (
6936
+ id TEXT PRIMARY KEY,
6937
+ session_key TEXT NOT NULL,
6938
+ title TEXT NOT NULL,
6939
+ choice TEXT NOT NULL,
6940
+ rationale TEXT,
6941
+ context TEXT,
6942
+ created_at INTEGER NOT NULL
6943
+ )`);
6944
+ db.exec(`CREATE TABLE IF NOT EXISTS persona (
6945
+ team_id TEXT NOT NULL,
6946
+ user_id TEXT NOT NULL,
6947
+ content TEXT NOT NULL,
6948
+ updated_at INTEGER NOT NULL,
6949
+ PRIMARY KEY (team_id, user_id)
6950
+ )`);
6951
+ db.exec(`CREATE TABLE IF NOT EXISTS skills (
6952
+ id TEXT PRIMARY KEY,
6953
+ team_id TEXT NOT NULL,
6954
+ agent_id TEXT,
6955
+ name TEXT NOT NULL,
6956
+ description TEXT,
6957
+ content TEXT,
6958
+ version INTEGER DEFAULT 1,
6959
+ created_at INTEGER NOT NULL,
6960
+ updated_at INTEGER NOT NULL,
6961
+ trigger_conditions TEXT,
6962
+ steps TEXT,
6963
+ validation_rules TEXT,
6964
+ source_capture_ids TEXT,
6965
+ archived INTEGER DEFAULT 0
6966
+ )`);
6967
+ db.exec(`CREATE TABLE IF NOT EXISTS atoms (
6968
+ id TEXT PRIMARY KEY,
6969
+ capture_id TEXT NOT NULL,
6970
+ fact TEXT NOT NULL,
6971
+ confidence REAL NOT NULL DEFAULT 1.0,
6972
+ created_at INTEGER NOT NULL,
6973
+ team_id TEXT,
6974
+ agent_id TEXT,
6975
+ user_id TEXT
6976
+ )`);
6977
+ db.exec(`CREATE TABLE IF NOT EXISTS scenarios (
6978
+ id TEXT PRIMARY KEY,
6979
+ atom_ids TEXT NOT NULL,
6980
+ summary TEXT,
6981
+ created_at INTEGER NOT NULL,
6982
+ team_id TEXT
6983
+ )`);
6984
+ db.exec(`CREATE TABLE IF NOT EXISTS schema_version (version INTEGER NOT NULL)`);
6985
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_captures_session ON captures(session_key)`);
6986
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_captures_type ON captures(type)`);
6987
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_errors_session ON error_patterns(session_key)`);
6988
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_atoms_capture ON atoms(capture_id)`);
6989
+ }
6633
6990
  function defaultTranscriptDir() {
6634
- return process.env.DEVIN_TRANSCRIPTS_DIR ?? join8(homedir6(), ".local", "share", "devin", "cli", "transcripts");
6991
+ return process.env.DEVIN_TRANSCRIPTS_DIR ?? join9(homedir7(), ".local", "share", "devin", "cli", "transcripts");
6635
6992
  }
6636
6993
  function generateId2() {
6637
- const ts = Date.now().toString(36).toUpperCase();
6638
- const rand = Math.random().toString(36).slice(2, 12).toUpperCase();
6639
- return `01${ts}${rand}`;
6994
+ try {
6995
+ return generateId();
6996
+ } catch {
6997
+ const ts = Date.now().toString(36).toUpperCase();
6998
+ const rand = Math.random().toString(36).slice(2, 12).toUpperCase();
6999
+ return `01${ts}${rand}`;
7000
+ }
6640
7001
  }
6641
7002
  async function captureSessionTranscript(dbPath, sessionId, transcriptPath, cwd) {
6642
7003
  const sid = sessionId ?? "unknown";
@@ -6648,13 +7009,13 @@ async function captureSessionTranscript(dbPath, sessionId, transcriptPath, cwd)
6648
7009
  if (transcriptPath) {
6649
7010
  filePath = transcriptPath;
6650
7011
  } else {
6651
- filePath = join8(defaultTranscriptDir(), `${sid}.json`);
7012
+ filePath = join9(defaultTranscriptDir(), `${sid}.json`);
6652
7013
  }
6653
- if (!existsSync7(filePath)) {
7014
+ if (!existsSync8(filePath)) {
6654
7015
  logToFile(`Stop: transcript not found at ${filePath}`);
6655
7016
  return null;
6656
7017
  }
6657
- const raw = readFileSync5(filePath, "utf-8");
7018
+ const raw = readFileSync6(filePath, "utf-8");
6658
7019
  const userMessages = [];
6659
7020
  const assistantMessages = [];
6660
7021
  const trimmed = raw.trim();
@@ -6721,6 +7082,7 @@ Task: ${taskText}
6721
7082
  Outcome: ${outcomeText}`;
6722
7083
  const contentHash = createHash3("sha256").update(content).digest("hex");
6723
7084
  const db = new Database8(dbPath);
7085
+ ensureSchema2(db);
6724
7086
  try {
6725
7087
  const sqliteVec5 = await import("sqlite-vec");
6726
7088
  sqliteVec5.load(db);
@@ -6828,19 +7190,19 @@ async function waitAndCapture(dbPath, sessionId, transcriptPath) {
6828
7190
  if (transcriptPath) {
6829
7191
  filePath = transcriptPath;
6830
7192
  } else {
6831
- filePath = join8(defaultTranscriptDir(), `${sessionId}.json`);
7193
+ filePath = join9(defaultTranscriptDir(), `${sessionId}.json`);
6832
7194
  }
6833
7195
  const maxWait = 1e4;
6834
7196
  const interval = 500;
6835
7197
  const start = Date.now();
6836
7198
  while (Date.now() - start < maxWait) {
6837
- if (existsSync7(filePath)) {
7199
+ if (existsSync8(filePath)) {
6838
7200
  await sleep2(500);
6839
7201
  break;
6840
7202
  }
6841
7203
  await sleep2(interval);
6842
7204
  }
6843
- if (!existsSync7(filePath)) {
7205
+ if (!existsSync8(filePath)) {
6844
7206
  logToFile(`Stop: transcript never appeared at ${filePath} after 10s`);
6845
7207
  return;
6846
7208
  }
@@ -6858,8 +7220,8 @@ function sleep2(ms) {
6858
7220
  }
6859
7221
  async function hookPostCommit(dbPath) {
6860
7222
  try {
6861
- const { execSync: execSync3 } = await import("child_process");
6862
- const output2 = execSync3("git diff-tree --no-commit-id --name-only -r HEAD", {
7223
+ const { execSync: execSync4 } = await import("child_process");
7224
+ const output2 = execSync4("git diff-tree --no-commit-id --name-only -r HEAD", {
6863
7225
  encoding: "utf-8",
6864
7226
  stdio: ["pipe", "pipe", "pipe"]
6865
7227
  });
@@ -6869,18 +7231,19 @@ async function hookPostCommit(dbPath) {
6869
7231
  process.stdout.write(JSON.stringify({}));
6870
7232
  return;
6871
7233
  }
6872
- const { readFileSync: readFileSync13 } = await import("fs");
6873
- const { join: join16, dirname: dirname11 } = await import("path");
7234
+ const { readFileSync: readFileSync14 } = await import("fs");
7235
+ const { join: join17, dirname: dirname11 } = await import("path");
6874
7236
  const { fileURLToPath: fileURLToPath5 } = await import("url");
6875
7237
  const sqliteVec5 = await import("sqlite-vec");
6876
- const { indexFile: indexFile2 } = await import("./engine-LVKGZHBY.js");
7238
+ const { indexFile: indexFile2 } = await import("./engine-S47HHGAO.js");
6877
7239
  const db = new Database8(dbPath);
7240
+ ensureSchema2(db);
6878
7241
  try {
6879
7242
  db.pragma("journal_mode = WAL");
6880
7243
  sqliteVec5.load(db);
6881
- const schemaPath = join16(dirname11(fileURLToPath5(import.meta.url)), "storage", "schema.sql");
7244
+ const schemaPath = join17(dirname11(fileURLToPath5(import.meta.url)), "storage", "schema.sql");
6882
7245
  try {
6883
- const schema = readFileSync13(schemaPath, "utf-8");
7246
+ const schema = readFileSync14(schemaPath, "utf-8");
6884
7247
  db.exec(schema);
6885
7248
  } catch {
6886
7249
  }
@@ -6904,7 +7267,7 @@ async function hookPostCommit(dbPath) {
6904
7267
  ];
6905
7268
  let indexed = 0;
6906
7269
  let skipped = 0;
6907
- const repoRoot = execSync3("git rev-parse --show-toplevel", { encoding: "utf-8" }).trim();
7270
+ const repoRoot = execSync4("git rev-parse --show-toplevel", { encoding: "utf-8" }).trim();
6908
7271
  for (const file of files) {
6909
7272
  const ext = file.slice(file.lastIndexOf(".")).toLowerCase();
6910
7273
  if (!SUPPORTED_EXT.includes(ext)) {
@@ -6912,7 +7275,7 @@ async function hookPostCommit(dbPath) {
6912
7275
  continue;
6913
7276
  }
6914
7277
  try {
6915
- const fullPath = join16(repoRoot, file);
7278
+ const fullPath = join17(repoRoot, file);
6916
7279
  await indexFile2(db, fullPath, repoRoot, null);
6917
7280
  indexed++;
6918
7281
  } catch {
@@ -6948,6 +7311,7 @@ function hookPreCompact(dbPath) {
6948
7311
  const trigger = input.trigger ?? "unknown";
6949
7312
  const sessionKey = hashPath(input.cwd ?? process.cwd());
6950
7313
  const db = new Database8(dbPath);
7314
+ ensureSchema2(db);
6951
7315
  let transcriptNote = "";
6952
7316
  try {
6953
7317
  db.pragma("journal_mode = WAL");
@@ -6958,9 +7322,9 @@ function hookPreCompact(dbPath) {
6958
7322
  }
6959
7323
  try {
6960
7324
  const { fileURLToPath: fileURLToPath5 } = await import("url");
6961
- const schemaPath = join8(dirname4(fileURLToPath5(import.meta.url)), "storage", "schema.sql");
6962
- if (existsSync7(schemaPath)) {
6963
- db.exec(readFileSync5(schemaPath, "utf-8"));
7325
+ const schemaPath = join9(dirname4(fileURLToPath5(import.meta.url)), "storage", "schema.sql");
7326
+ if (existsSync8(schemaPath)) {
7327
+ db.exec(readFileSync6(schemaPath, "utf-8"));
6964
7328
  }
6965
7329
  } catch {
6966
7330
  }
@@ -6983,9 +7347,9 @@ function hookPreCompact(dbPath) {
6983
7347
  "remem-mcp-hook",
6984
7348
  Date.now()
6985
7349
  );
6986
- if (input.transcript_path && existsSync7(input.transcript_path)) {
7350
+ if (input.transcript_path && existsSync8(input.transcript_path)) {
6987
7351
  try {
6988
- const transcript = readFileSync5(input.transcript_path, "utf-8");
7352
+ const transcript = readFileSync6(input.transcript_path, "utf-8");
6989
7353
  const recent = transcript.slice(-2e3);
6990
7354
  const transcriptId = `txcpt-${createHash3("sha256").update(sessionKey + "transcript" + Date.now()).digest("hex").slice(0, 12)}`;
6991
7355
  db.prepare(
@@ -7056,7 +7420,7 @@ function hookPostCompaction(dbPath) {
7056
7420
  const sessionKey = hashPath(input.cwd ?? process.cwd());
7057
7421
  const globalKey = process.env.REMEM_GLOBAL_SESSION_KEY ?? null;
7058
7422
  const { fileURLToPath: fileURLToPath5 } = await import("url");
7059
- const schemaPath = join8(dirname4(fileURLToPath5(import.meta.url)), "storage", "schema.sql");
7423
+ const schemaPath = join9(dirname4(fileURLToPath5(import.meta.url)), "storage", "schema.sql");
7060
7424
  const db = new Database8(dbPath, { readonly: true });
7061
7425
  const cutoff = Date.now() - 24 * 60 * 60 * 1e3;
7062
7426
  const sessionKeys = globalKey ? [sessionKey, globalKey] : [sessionKey];
@@ -7107,23 +7471,365 @@ function hookPostCompaction(dbPath) {
7107
7471
  }
7108
7472
  });
7109
7473
  }
7474
+ function offloadToolOutput(dbPath, toolName, toolInput, toolResponse, cwd) {
7475
+ const sessionKey = hashPath(cwd ?? process.cwd());
7476
+ const nodeId = generateId2();
7477
+ const label = buildToolLabel(toolName, toolInput);
7478
+ const rawContent = buildRawRefContent(toolName, toolInput, toolResponse);
7479
+ writeRef(sessionKey, nodeId, rawContent);
7480
+ const db = new Database8(dbPath);
7481
+ ensureSchema2(db);
7482
+ try {
7483
+ db.exec(`CREATE TABLE IF NOT EXISTS canvases (
7484
+ id TEXT PRIMARY KEY,
7485
+ session_key TEXT NOT NULL,
7486
+ mermaid_text TEXT,
7487
+ node_count INTEGER DEFAULT 0,
7488
+ created_at INTEGER NOT NULL,
7489
+ updated_at INTEGER NOT NULL,
7490
+ team_id TEXT
7491
+ )`);
7492
+ db.exec(`CREATE TABLE IF NOT EXISTS canvas_nodes (
7493
+ id TEXT PRIMARY KEY,
7494
+ canvas_id TEXT NOT NULL,
7495
+ node_id TEXT NOT NULL,
7496
+ label TEXT NOT NULL,
7497
+ capture_id TEXT,
7498
+ seq INTEGER NOT NULL,
7499
+ created_at INTEGER NOT NULL,
7500
+ FOREIGN KEY (canvas_id) REFERENCES canvases(id)
7501
+ )`);
7502
+ db.exec(`CREATE TABLE IF NOT EXISTS canvas_edges (
7503
+ id TEXT PRIMARY KEY,
7504
+ canvas_id TEXT NOT NULL,
7505
+ from_node_id TEXT NOT NULL,
7506
+ to_node_id TEXT NOT NULL,
7507
+ label TEXT,
7508
+ created_at INTEGER NOT NULL,
7509
+ FOREIGN KEY (canvas_id) REFERENCES canvases(id)
7510
+ )`);
7511
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_canvases_session ON canvases(session_key)`);
7512
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_canvas_nodes_canvas ON canvas_nodes(canvas_id)`);
7513
+ const now = Date.now();
7514
+ let canvasRow = db.prepare("SELECT id FROM canvases WHERE session_key = ? ORDER BY updated_at DESC LIMIT 1").get(sessionKey);
7515
+ if (!canvasRow) {
7516
+ const canvasId = generateId2();
7517
+ db.prepare(
7518
+ "INSERT INTO canvases (id, session_key, mermaid_text, node_count, created_at, updated_at, team_id) VALUES (?, ?, NULL, 0, ?, ?, NULL)"
7519
+ ).run(canvasId, sessionKey, now, now);
7520
+ canvasRow = { id: canvasId };
7521
+ }
7522
+ const maxSeq = db.prepare("SELECT MAX(seq) as max_seq FROM canvas_nodes WHERE canvas_id = ?").get(canvasRow.id);
7523
+ const seq = (maxSeq?.max_seq ?? -1) + 1;
7524
+ db.prepare(
7525
+ "INSERT INTO canvas_nodes (id, canvas_id, node_id, label, capture_id, seq, created_at) VALUES (?, ?, ?, ?, NULL, ?, ?)"
7526
+ ).run(generateId2(), canvasRow.id, nodeId, label, seq, now);
7527
+ if (seq > 0) {
7528
+ const prevNode = db.prepare("SELECT node_id FROM canvas_nodes WHERE canvas_id = ? ORDER BY seq DESC LIMIT 1 OFFSET 1").get(canvasRow.id);
7529
+ if (prevNode) {
7530
+ db.prepare(
7531
+ "INSERT INTO canvas_edges (id, canvas_id, from_node_id, to_node_id, label, created_at) VALUES (?, ?, ?, ?, ?, ?)"
7532
+ ).run(generateId2(), canvasRow.id, prevNode.node_id, nodeId, inferEdgeLabel(toolName), now);
7533
+ }
7534
+ }
7535
+ const nodeCount = db.prepare("SELECT COUNT(*) as count FROM canvas_nodes WHERE canvas_id = ?").get(canvasRow.id);
7536
+ const mermaidText = renderCanvasMermaid(db, canvasRow.id);
7537
+ db.prepare("UPDATE canvases SET node_count = ?, updated_at = ?, mermaid_text = ? WHERE id = ?").run(
7538
+ nodeCount.count,
7539
+ now,
7540
+ mermaidText,
7541
+ canvasRow.id
7542
+ );
7543
+ } finally {
7544
+ db.close();
7545
+ }
7546
+ }
7547
+ function buildToolLabel(toolName, toolInput) {
7548
+ if (toolName === "Bash" || toolName === "exec") {
7549
+ const cmd = toolInput.command ?? "";
7550
+ return truncateLabel(cmd.replace(/\n/g, " ").trim(), 40);
7551
+ }
7552
+ if (toolName === "Write" || toolName === "Edit" || toolName === "MultiEdit") {
7553
+ const fp = toolInput.file_path ?? "";
7554
+ return truncateLabel(fp, 40);
7555
+ }
7556
+ if (toolName === "Read") {
7557
+ const fp = toolInput.file_path ?? "";
7558
+ return truncateLabel(`read ${fp}`, 40);
7559
+ }
7560
+ if (toolName === "grep" || toolName === "search") {
7561
+ const pattern = toolInput.pattern ?? toolInput.query ?? "";
7562
+ return truncateLabel(`search: ${pattern}`, 40);
7563
+ }
7564
+ return truncateLabel(toolName, 40);
7565
+ }
7566
+ function buildRawRefContent(toolName, toolInput, toolResponse) {
7567
+ const lines = [
7568
+ `# Tool: ${toolName}`,
7569
+ `## Input`,
7570
+ "```json",
7571
+ JSON.stringify(toolInput, null, 2),
7572
+ "```",
7573
+ `## Output`
7574
+ ];
7575
+ if (typeof toolResponse === "string") {
7576
+ lines.push("```", toolResponse, "```");
7577
+ } else if (toolResponse && typeof toolResponse === "object") {
7578
+ const resp = toolResponse;
7579
+ if (resp.content && Array.isArray(resp.content)) {
7580
+ for (const c of resp.content) {
7581
+ if (c.type === "text" && c.text) {
7582
+ lines.push("```", c.text, "```");
7583
+ }
7584
+ }
7585
+ } else {
7586
+ lines.push("```json", JSON.stringify(toolResponse, null, 2), "```");
7587
+ }
7588
+ } else {
7589
+ lines.push("(no output)");
7590
+ }
7591
+ return lines.join("\n");
7592
+ }
7593
+ function inferEdgeLabel(toolName) {
7594
+ if (toolName === "Bash" || toolName === "exec") return "ran";
7595
+ if (toolName === "Write" || toolName === "Edit" || toolName === "MultiEdit") return "edited";
7596
+ if (toolName === "Read") return "read";
7597
+ if (toolName === "grep" || toolName === "search") return "searched";
7598
+ return "next";
7599
+ }
7600
+ function truncateLabel(s, maxLen) {
7601
+ if (s.length <= maxLen) return s;
7602
+ return s.slice(0, maxLen - 3) + "...";
7603
+ }
7604
+ async function extractSkillFromCapture(dbPath, captureId) {
7605
+ const db = new Database8(dbPath, { readonly: true });
7606
+ try {
7607
+ const row = db.prepare("SELECT id, content, type, tags, team_id, agent_id FROM captures WHERE id = ? AND deleted_at IS NULL").get(captureId);
7608
+ if (!row || row.type !== "task") return;
7609
+ const tags = row.tags ? JSON.parse(row.tags) : [];
7610
+ const steps = extractStepsFromContent(row.content);
7611
+ if (steps.length < 2) return;
7612
+ const triggers = extractTriggersFromContent(row.content, tags);
7613
+ const skillName = extractSkillNameFromContent(row.content) ?? `skill-${captureId.slice(-8)}`;
7614
+ const description = row.content.split(/[.!]\s/)[0].slice(0, 120);
7615
+ const existing = db.prepare("SELECT id, version, created_at FROM skills WHERE name = ? ORDER BY version DESC LIMIT 1").get(skillName);
7616
+ const skillId = existing?.id ?? generateId2();
7617
+ const version = (existing?.version ?? 0) + 1;
7618
+ const now = Date.now();
7619
+ db.close();
7620
+ const writeDb = new Database8(dbPath);
7621
+ ensureSchema2(writeDb);
7622
+ try {
7623
+ writeDb.prepare(
7624
+ `INSERT INTO skills (id, team_id, agent_id, name, description, content, version, created_at, updated_at, trigger_conditions, steps, validation_rules, source_capture_ids, archived)
7625
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0)
7626
+ ON CONFLICT(id) DO UPDATE SET
7627
+ version = excluded.version, updated_at = excluded.updated_at,
7628
+ trigger_conditions = excluded.trigger_conditions, steps = excluded.steps,
7629
+ source_capture_ids = excluded.source_capture_ids`
7630
+ ).run(
7631
+ skillId,
7632
+ row.team_id ?? "default",
7633
+ row.agent_id ?? "pipeline",
7634
+ skillName,
7635
+ description,
7636
+ row.content,
7637
+ version,
7638
+ existing?.created_at ?? now,
7639
+ now,
7640
+ JSON.stringify(triggers),
7641
+ JSON.stringify(steps),
7642
+ null,
7643
+ JSON.stringify([captureId])
7644
+ );
7645
+ logToFile(`Stop: extracted skill "${skillName}" v${version} from capture ${captureId}`);
7646
+ } finally {
7647
+ writeDb.close();
7648
+ }
7649
+ } finally {
7650
+ if (db.open) db.close();
7651
+ }
7652
+ }
7653
+ function extractStepsFromContent(content) {
7654
+ const steps = [];
7655
+ const numberedMatch = content.match(/(?:^|\n)\s*(\d+)[.)]\s+(.+)/g);
7656
+ if (numberedMatch) {
7657
+ for (const match of numberedMatch) {
7658
+ const step = match.replace(/(?:^|\n)\s*\d+[.)]\s+/, "").trim();
7659
+ if (step.length > 5 && step.length < 200) steps.push(step);
7660
+ }
7661
+ }
7662
+ if (steps.length === 0) {
7663
+ const bulletMatch = content.match(/(?:^|\n)\s*[-*]\s+(.+)/g);
7664
+ if (bulletMatch) {
7665
+ for (const match of bulletMatch) {
7666
+ const step = match.replace(/(?:^|\n)\s*[-*]\s+/, "").trim();
7667
+ if (step.length > 5 && step.length < 200) steps.push(step);
7668
+ }
7669
+ }
7670
+ }
7671
+ return steps.slice(0, 10);
7672
+ }
7673
+ function extractTriggersFromContent(content, tags) {
7674
+ const triggers = /* @__PURE__ */ new Set();
7675
+ for (const tag of tags) {
7676
+ if (tag.length > 2 && tag !== "task") triggers.add(tag);
7677
+ }
7678
+ const firstWords = content.slice(0, 200).split(/\s+/).filter((w) => w.length > 4);
7679
+ for (const word of firstWords.slice(0, 5)) {
7680
+ triggers.add(word.toLowerCase().replace(/[^a-z0-9]/g, ""));
7681
+ }
7682
+ return Array.from(triggers).slice(0, 8);
7683
+ }
7684
+ function extractSkillNameFromContent(content) {
7685
+ const firstLine = content.split("\n")[0].trim();
7686
+ if (firstLine.length > 5 && firstLine.length < 60) {
7687
+ return firstLine.replace(/[^a-zA-Z0-9\s-]/g, "").trim().toLowerCase().replace(/\s+/g, "-");
7688
+ }
7689
+ return null;
7690
+ }
7691
+ function getCanvasContext(dbPath, cwd) {
7692
+ const sessionKey = hashPath(cwd);
7693
+ const db = new Database8(dbPath, { readonly: true });
7694
+ try {
7695
+ const row = db.prepare("SELECT mermaid_text, node_count FROM canvases WHERE session_key = ? ORDER BY updated_at DESC LIMIT 1").get(sessionKey);
7696
+ if (!row || !row.mermaid_text || row.node_count === 0) return null;
7697
+ return `## Task Canvas (${row.node_count} steps)
7698
+
7699
+ \`\`\`mermaid
7700
+ ${row.mermaid_text}
7701
+ \`\`\`
7702
+
7703
+ Drill down: call ref_read(node_id) to get raw output for any node.`;
7704
+ } finally {
7705
+ db.close();
7706
+ }
7707
+ }
7708
+ function getSkillContext(dbPath, query) {
7709
+ const db = new Database8(dbPath, { readonly: true });
7710
+ try {
7711
+ const archived = db.prepare(
7712
+ "SELECT name, description, steps FROM skills WHERE archived = 1 ORDER BY updated_at DESC LIMIT 3"
7713
+ ).all();
7714
+ const allSkills = db.prepare(
7715
+ "SELECT name, description, steps, trigger_conditions FROM skills WHERE archived = 0 ORDER BY updated_at DESC LIMIT 20"
7716
+ ).all();
7717
+ const matched = [];
7718
+ const queryLower = query.toLowerCase();
7719
+ for (const skill of allSkills) {
7720
+ if (!skill.trigger_conditions) continue;
7721
+ const triggers = JSON.parse(skill.trigger_conditions);
7722
+ if (triggers.some((t) => queryLower.includes(t.toLowerCase()))) {
7723
+ matched.push(skill);
7724
+ }
7725
+ }
7726
+ if (archived.length === 0 && matched.length === 0) return null;
7727
+ const lines = [];
7728
+ if (archived.length > 0) {
7729
+ lines.push("## Archived Skills (always available)");
7730
+ for (const s of archived) {
7731
+ const steps = s.steps ? JSON.parse(s.steps) : [];
7732
+ const stepStr = steps.length > 0 ? `
7733
+ Steps: ${steps.slice(0, 3).join(" \u2192 ")}` : "";
7734
+ lines.push(`- **${s.name}**: ${s.description ?? ""}${stepStr}`);
7735
+ }
7736
+ }
7737
+ if (matched.length > 0) {
7738
+ lines.push("## Matched Skills");
7739
+ for (const s of matched.slice(0, 2)) {
7740
+ const steps = s.steps ? JSON.parse(s.steps) : [];
7741
+ const stepStr = steps.length > 0 ? `
7742
+ Steps: ${steps.slice(0, 3).join(" \u2192 ")}` : "";
7743
+ lines.push(`- **${s.name}**: ${s.description ?? ""}${stepStr}`);
7744
+ }
7745
+ }
7746
+ return lines.join("\n");
7747
+ } finally {
7748
+ db.close();
7749
+ }
7750
+ }
7751
+ function renderCanvasMermaid(db, canvasId) {
7752
+ const nodeRows = db.prepare("SELECT * FROM canvas_nodes WHERE canvas_id = ? ORDER BY seq ASC").all(canvasId);
7753
+ const edgeRows = db.prepare("SELECT * FROM canvas_edges WHERE canvas_id = ? ORDER BY created_at ASC").all(canvasId);
7754
+ const lines = ["graph LR"];
7755
+ for (const node of nodeRows) {
7756
+ const safeLabel = node.label.replace(/"/g, "'");
7757
+ lines.push(` ${node.node_id}["${safeLabel}"]`);
7758
+ }
7759
+ for (const edge of edgeRows) {
7760
+ if (edge.label) {
7761
+ const safeLabel = edge.label.replace(/"/g, "'");
7762
+ lines.push(` ${edge.from_node_id} -->|${safeLabel}| ${edge.to_node_id}`);
7763
+ } else {
7764
+ lines.push(` ${edge.from_node_id} --> ${edge.to_node_id}`);
7765
+ }
7766
+ }
7767
+ return lines.join("\n");
7768
+ }
7110
7769
 
7111
7770
  // src/hooks.ts
7112
7771
  init_esm_shims();
7113
7772
  import { execFileSync as execFileSync2 } from "child_process";
7114
- import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
7115
- import { homedir as homedir7 } from "os";
7116
- import { dirname as dirname5, join as join9 } from "path";
7773
+ import { existsSync as existsSync9, mkdirSync as mkdirSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "fs";
7774
+ import { homedir as homedir8 } from "os";
7775
+ import { dirname as dirname5, join as join10 } from "path";
7117
7776
  function hookCommand(subcommand) {
7118
7777
  try {
7119
7778
  const binPath = execFileSync2("which", ["remem-mcp"], { encoding: "utf-8" }).trim();
7120
- if (binPath && existsSync8(binPath)) {
7779
+ if (binPath && existsSync9(binPath)) {
7121
7780
  return `${binPath} ${subcommand}`;
7122
7781
  }
7123
7782
  } catch {
7124
7783
  }
7125
7784
  return `npx --prefer-offline -y remem-mcp ${subcommand}`;
7126
7785
  }
7786
+ var CODEX_HOOK_SUBCOMMANDS = [
7787
+ "hook-recall",
7788
+ "hook-pre-tool-use",
7789
+ "hook-post-tool-use",
7790
+ "hook-post-compaction",
7791
+ "hook-user-prompt",
7792
+ "hook-stop",
7793
+ "hook-session-end"
7794
+ ];
7795
+ function hasCodexRememHooks(content) {
7796
+ return CODEX_HOOK_SUBCOMMANDS.some((subcommand) => content.includes(` ${subcommand}"`));
7797
+ }
7798
+ function removeCodexHooksFromToml(content) {
7799
+ const lines = content.split(/\r?\n/);
7800
+ const output2 = [];
7801
+ let index = 0;
7802
+ while (index < lines.length) {
7803
+ const match = lines[index].match(/^\[\[hooks\.([A-Za-z]+)\]\]\s*$/);
7804
+ if (!match) {
7805
+ output2.push(lines[index]);
7806
+ index++;
7807
+ continue;
7808
+ }
7809
+ const event = match[1];
7810
+ const nestedPrefix = `[[hooks.${event}.hooks]]`;
7811
+ let end = index + 1;
7812
+ while (end < lines.length) {
7813
+ const line2 = lines[end];
7814
+ if (line2.startsWith(nestedPrefix)) {
7815
+ end++;
7816
+ continue;
7817
+ }
7818
+ if (/^\[./.test(line2)) break;
7819
+ end++;
7820
+ }
7821
+ const block = lines.slice(index, end);
7822
+ if (CODEX_HOOK_SUBCOMMANDS.some((subcommand) => block.includes(` ${subcommand}"`))) {
7823
+ while (output2.length > 0 && (output2[output2.length - 1].trim() === "" || /^\s*#/.test(output2[output2.length - 1]) && output2[output2.length - 1].includes("remem-mcp"))) {
7824
+ output2.pop();
7825
+ }
7826
+ } else {
7827
+ output2.push(...block);
7828
+ }
7829
+ index = end;
7830
+ }
7831
+ return output2.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd();
7832
+ }
7127
7833
  var HOOKS_CONFIG = {
7128
7834
  SessionStart: [
7129
7835
  {
@@ -7214,17 +7920,17 @@ var HOOKS_CONFIG = {
7214
7920
  ]
7215
7921
  };
7216
7922
  function readJsonConfig(path) {
7217
- if (!existsSync8(path)) return {};
7923
+ if (!existsSync9(path)) return {};
7218
7924
  try {
7219
- return JSON.parse(readFileSync6(path, "utf-8"));
7925
+ return JSON.parse(readFileSync7(path, "utf-8"));
7220
7926
  } catch {
7221
7927
  return {};
7222
7928
  }
7223
7929
  }
7224
7930
  function writeJsonConfig(path, data) {
7225
7931
  const dir = dirname5(path);
7226
- if (!existsSync8(dir)) mkdirSync5(dir, { recursive: true });
7227
- writeFileSync5(path, `${JSON.stringify(data, null, 2)}
7932
+ if (!existsSync9(dir)) mkdirSync6(dir, { recursive: true });
7933
+ writeFileSync6(path, `${JSON.stringify(data, null, 2)}
7228
7934
  `, "utf-8");
7229
7935
  }
7230
7936
  function mergeHooks(config, hooks) {
@@ -7245,8 +7951,8 @@ function mergeHooks(config, hooks) {
7245
7951
  };
7246
7952
  }
7247
7953
  function installDevinHooks() {
7248
- const configPath = join9(homedir7(), ".config", "devin", "config.json");
7249
- if (!existsSync8(dirname5(configPath))) {
7954
+ const configPath = join10(homedir8(), ".config", "devin", "config.json");
7955
+ if (!existsSync9(dirname5(configPath))) {
7250
7956
  return false;
7251
7957
  }
7252
7958
  const config = readJsonConfig(configPath);
@@ -7256,9 +7962,9 @@ function installDevinHooks() {
7256
7962
  return true;
7257
7963
  }
7258
7964
  function installClaudeCodeHooks() {
7259
- const settingsPath = join9(homedir7(), ".claude", "settings.json");
7260
- const claudeDir = join9(homedir7(), ".claude");
7261
- if (!existsSync8(claudeDir)) {
7965
+ const settingsPath = join10(homedir8(), ".claude", "settings.json");
7966
+ const claudeDir = join10(homedir8(), ".claude");
7967
+ if (!existsSync9(claudeDir)) {
7262
7968
  return false;
7263
7969
  }
7264
7970
  const { PostCompaction: _omit, ...baseWithoutPostCompaction } = HOOKS_CONFIG;
@@ -7294,12 +8000,12 @@ function installClaudeCodeHooks() {
7294
8000
  return true;
7295
8001
  }
7296
8002
  function installCodexHooks() {
7297
- const configPath = join9(homedir7(), ".codex", "config.toml");
7298
- if (!existsSync8(configPath)) {
8003
+ const configPath = join10(homedir8(), ".codex", "config.toml");
8004
+ if (!existsSync9(configPath)) {
7299
8005
  return false;
7300
8006
  }
7301
- let content = readFileSync6(configPath, "utf-8");
7302
- if (content.includes(">>> remem-mcp SessionStart >>>")) {
8007
+ let content = readFileSync7(configPath, "utf-8");
8008
+ if (hasCodexRememHooks(content)) {
7303
8009
  console.log(` Codex CLI: Hooks already installed in ${configPath}`);
7304
8010
  return true;
7305
8011
  }
@@ -7371,7 +8077,7 @@ timeout = 10
7371
8077
  # <<< remem-mcp SessionEnd <<<
7372
8078
  `;
7373
8079
  content = content.trimEnd() + "\n" + hooksToml;
7374
- writeFileSync5(configPath, content, "utf-8");
8080
+ writeFileSync6(configPath, content, "utf-8");
7375
8081
  console.log(` Codex CLI: Hooks wired into ${configPath}`);
7376
8082
  console.log(` Note: Set sandbox_mode = "danger-full-access" for MCP tools to work.`);
7377
8083
  return true;
@@ -7416,8 +8122,8 @@ async function uninstallHooks() {
7416
8122
  "PostCompaction",
7417
8123
  "UserPromptSubmit"
7418
8124
  ];
7419
- const devinPath = join9(homedir7(), ".config", "devin", "config.json");
7420
- if (existsSync8(devinPath)) {
8125
+ const devinPath = join10(homedir8(), ".config", "devin", "config.json");
8126
+ if (existsSync9(devinPath)) {
7421
8127
  const config = readJsonConfig(devinPath);
7422
8128
  if (config.hooks) {
7423
8129
  const hooks = config.hooks;
@@ -7441,8 +8147,8 @@ async function uninstallHooks() {
7441
8147
  removed++;
7442
8148
  }
7443
8149
  }
7444
- const claudePath = join9(homedir7(), ".claude", "settings.json");
7445
- if (existsSync8(claudePath)) {
8150
+ const claudePath = join10(homedir8(), ".claude", "settings.json");
8151
+ if (existsSync9(claudePath)) {
7446
8152
  const config = readJsonConfig(claudePath);
7447
8153
  if (config.hooks) {
7448
8154
  const hooks = config.hooks;
@@ -7466,12 +8172,13 @@ async function uninstallHooks() {
7466
8172
  removed++;
7467
8173
  }
7468
8174
  }
7469
- const codexPath = join9(homedir7(), ".codex", "config.toml");
7470
- if (existsSync8(codexPath)) {
7471
- let content = readFileSync6(codexPath, "utf-8");
7472
- if (content.includes(">>> remem-mcp")) {
7473
- content = content.replace(/\n?# >>> remem-mcp[\s\S]*?# <<< remem-mcp[^<]*<<<\n?/g, "\n");
7474
- writeFileSync5(codexPath, content.trimEnd() + "\n", "utf-8");
8175
+ const codexPath = join10(homedir8(), ".codex", "config.toml");
8176
+ if (existsSync9(codexPath)) {
8177
+ let content = readFileSync7(codexPath, "utf-8");
8178
+ const cleaned = removeCodexHooksFromToml(content);
8179
+ if (cleaned !== content) {
8180
+ content = cleaned;
8181
+ writeFileSync6(codexPath, content.trimEnd() + "\n", "utf-8");
7475
8182
  console.log(` Codex CLI: Hooks removed from ${codexPath}`);
7476
8183
  removed++;
7477
8184
  }
@@ -7487,22 +8194,22 @@ Hooks removed from ${removed} agent(s).`);
7487
8194
  // src/import.ts
7488
8195
  init_esm_shims();
7489
8196
  init_ulid();
7490
- import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
7491
- import { dirname as dirname6, join as join10 } from "path";
8197
+ import { existsSync as existsSync10, readFileSync as readFileSync8 } from "fs";
8198
+ import { dirname as dirname6, join as join11 } from "path";
7492
8199
  import { fileURLToPath as fileURLToPath3 } from "url";
7493
8200
  import Database9 from "better-sqlite3";
7494
8201
  import * as sqliteVec3 from "sqlite-vec";
7495
8202
  var __dirname3 = dirname6(fileURLToPath3(import.meta.url));
7496
- function ensureSchema2(db) {
8203
+ function ensureSchema3(db) {
7497
8204
  const candidates = [
7498
- join10(__dirname3, "storage", "schema.sql"),
7499
- join10(__dirname3, "schema.sql"),
7500
- join10(__dirname3, "..", "storage", "schema.sql")
8205
+ join11(__dirname3, "storage", "schema.sql"),
8206
+ join11(__dirname3, "schema.sql"),
8207
+ join11(__dirname3, "..", "storage", "schema.sql")
7501
8208
  ];
7502
8209
  let schema = null;
7503
8210
  for (const path of candidates) {
7504
8211
  try {
7505
- schema = readFileSync7(path, "utf-8");
8212
+ schema = readFileSync8(path, "utf-8");
7506
8213
  break;
7507
8214
  } catch {
7508
8215
  }
@@ -7513,11 +8220,11 @@ function ensureSchema2(db) {
7513
8220
  db.exec(schema);
7514
8221
  }
7515
8222
  function importData(dbPath, inputPath) {
7516
- if (!existsSync9(inputPath)) {
8223
+ if (!existsSync10(inputPath)) {
7517
8224
  console.error(`Error: File not found: ${inputPath}`);
7518
8225
  process.exit(1);
7519
8226
  }
7520
- const raw = readFileSync7(inputPath, "utf-8");
8227
+ const raw = readFileSync8(inputPath, "utf-8");
7521
8228
  let data;
7522
8229
  try {
7523
8230
  data = JSON.parse(raw);
@@ -7533,7 +8240,7 @@ function importData(dbPath, inputPath) {
7533
8240
  db.pragma("journal_mode = WAL");
7534
8241
  db.pragma("synchronous = NORMAL");
7535
8242
  sqliteVec3.load(db);
7536
- ensureSchema2(db);
8243
+ ensureSchema3(db);
7537
8244
  let inserted = 0;
7538
8245
  let skipped = 0;
7539
8246
  let messagesInserted = 0;
@@ -7591,9 +8298,9 @@ function importData(dbPath, inputPath) {
7591
8298
 
7592
8299
  // src/install-mcp.ts
7593
8300
  init_esm_shims();
7594
- import { existsSync as existsSync10, mkdirSync as mkdirSync6, readFileSync as readFileSync8, writeFileSync as writeFileSync6 } from "fs";
7595
- import { homedir as homedir8 } from "os";
7596
- import { dirname as dirname7, join as join11 } from "path";
8301
+ import { existsSync as existsSync11, mkdirSync as mkdirSync7, readFileSync as readFileSync9, writeFileSync as writeFileSync7 } from "fs";
8302
+ import { homedir as homedir9 } from "os";
8303
+ import { dirname as dirname7, join as join12 } from "path";
7597
8304
  var MCP_SERVER_ENTRY = {
7598
8305
  command: "npx",
7599
8306
  args: ["-y", "remem-mcp"]
@@ -7608,26 +8315,26 @@ var MCP_SERVER_ENTRY_WITH_GLOBAL = {
7608
8315
  var JSON_TARGETS = [
7609
8316
  {
7610
8317
  name: "Claude Code",
7611
- path: join11(homedir8(), ".claude.json"),
8318
+ path: join12(homedir9(), ".claude.json"),
7612
8319
  key: "mcpServers"
7613
8320
  },
7614
8321
  {
7615
8322
  name: "Devin CLI",
7616
- path: join11(homedir8(), ".config", "devin", "mcp_config.json"),
8323
+ path: join12(homedir9(), ".config", "devin", "mcp_config.json"),
7617
8324
  key: "mcpServers",
7618
8325
  useGlobal: true
7619
8326
  },
7620
8327
  {
7621
8328
  name: "Cursor",
7622
- path: join11(homedir8(), ".cursor", "mcp.json"),
8329
+ path: join12(homedir9(), ".cursor", "mcp.json"),
7623
8330
  key: "mcpServers"
7624
8331
  }
7625
8332
  ];
7626
8333
  function registerJsonServer(target) {
7627
8334
  let config = {};
7628
- if (existsSync10(target.path)) {
8335
+ if (existsSync11(target.path)) {
7629
8336
  try {
7630
- config = JSON.parse(readFileSync8(target.path, "utf-8"));
8337
+ config = JSON.parse(readFileSync9(target.path, "utf-8"));
7631
8338
  } catch {
7632
8339
  console.log(` ${target.name}: Config file unreadable, skipping.`);
7633
8340
  return false;
@@ -7642,10 +8349,10 @@ function registerJsonServer(target) {
7642
8349
  servers["remem-mcp"] = entry;
7643
8350
  config[target.key] = servers;
7644
8351
  const dir = dirname7(target.path);
7645
- if (!existsSync10(dir)) {
7646
- mkdirSync6(dir, { recursive: true });
8352
+ if (!existsSync11(dir)) {
8353
+ mkdirSync7(dir, { recursive: true });
7647
8354
  }
7648
- writeFileSync6(target.path, JSON.stringify(config, null, 2) + "\n", "utf-8");
8355
+ writeFileSync7(target.path, JSON.stringify(config, null, 2) + "\n", "utf-8");
7649
8356
  console.log(` ${target.name}: MCP server registered.`);
7650
8357
  return true;
7651
8358
  }
@@ -7654,12 +8361,12 @@ async function installMcpServer() {
7654
8361
  let count = 0;
7655
8362
  for (const target of JSON_TARGETS) {
7656
8363
  const agentDir = dirname7(target.path);
7657
- if (!existsSync10(agentDir)) {
8364
+ if (!existsSync11(agentDir)) {
7658
8365
  if (target.name === "Cursor") {
7659
8366
  const cursorApp = "/Applications/Cursor.app";
7660
- const cursorAppHome = join11(homedir8(), "Library", "Application Support", "Cursor");
7661
- if (existsSync10(cursorApp) || existsSync10(cursorAppHome)) {
7662
- mkdirSync6(agentDir, { recursive: true });
8367
+ const cursorAppHome = join12(homedir9(), "Library", "Application Support", "Cursor");
8368
+ if (existsSync11(cursorApp) || existsSync11(cursorAppHome)) {
8369
+ mkdirSync7(agentDir, { recursive: true });
7663
8370
  } else {
7664
8371
  continue;
7665
8372
  }
@@ -7671,9 +8378,9 @@ async function installMcpServer() {
7671
8378
  count++;
7672
8379
  }
7673
8380
  }
7674
- const codexConfig = join11(homedir8(), ".codex", "config.toml");
7675
- if (existsSync10(codexConfig)) {
7676
- const content = readFileSync8(codexConfig, "utf-8");
8381
+ const codexConfig = join12(homedir9(), ".codex", "config.toml");
8382
+ if (existsSync11(codexConfig)) {
8383
+ const content = readFileSync9(codexConfig, "utf-8");
7677
8384
  if (content.includes("[mcp_servers.remem-mcp]")) {
7678
8385
  console.log(" Codex CLI: Already registered.");
7679
8386
  count++;
@@ -7686,7 +8393,7 @@ args = ["-y", "remem-mcp"]
7686
8393
  [mcp_servers.remem-mcp.env]
7687
8394
  REMEM_GLOBAL_SESSION_KEY = "global"
7688
8395
  `;
7689
- writeFileSync6(codexConfig, content + tomlEntry, "utf-8");
8396
+ writeFileSync7(codexConfig, content + tomlEntry, "utf-8");
7690
8397
  console.log(" Codex CLI: MCP server registered.");
7691
8398
  count++;
7692
8399
  }
@@ -7702,18 +8409,18 @@ MCP server registered in ${count} agent config(s).`);
7702
8409
 
7703
8410
  // src/install-skill.ts
7704
8411
  init_esm_shims();
7705
- import { existsSync as existsSync11, mkdirSync as mkdirSync7, readFileSync as readFileSync9, writeFileSync as writeFileSync7 } from "fs";
7706
- import { homedir as homedir9 } from "os";
7707
- import { dirname as dirname8, join as join12 } from "path";
8412
+ import { existsSync as existsSync12, mkdirSync as mkdirSync8, readFileSync as readFileSync10, writeFileSync as writeFileSync8 } from "fs";
8413
+ import { homedir as homedir10 } from "os";
8414
+ import { dirname as dirname8, join as join13 } from "path";
7708
8415
  function loadSkillContent() {
7709
8416
  const candidates = [
7710
- join12(process.cwd(), "skills", "remem-mcp", "SKILL.md"),
7711
- join12(__dirname, "..", "skills", "remem-mcp", "SKILL.md"),
7712
- join12(__dirname, "skills", "remem-mcp", "SKILL.md")
8417
+ join13(process.cwd(), "skills", "remem-mcp", "SKILL.md"),
8418
+ join13(__dirname, "..", "skills", "remem-mcp", "SKILL.md"),
8419
+ join13(__dirname, "skills", "remem-mcp", "SKILL.md")
7713
8420
  ];
7714
8421
  for (const path of candidates) {
7715
8422
  try {
7716
- return readFileSync9(path, "utf-8");
8423
+ return readFileSync10(path, "utf-8");
7717
8424
  } catch {
7718
8425
  }
7719
8426
  }
@@ -7724,19 +8431,19 @@ function loadSkillContent() {
7724
8431
  var SKILL_TARGETS = [
7725
8432
  {
7726
8433
  name: "Devin CLI",
7727
- path: join12(homedir9(), ".config", "devin", "skills", "remem-mcp", "SKILL.md")
8434
+ path: join13(homedir10(), ".config", "devin", "skills", "remem-mcp", "SKILL.md")
7728
8435
  },
7729
8436
  {
7730
8437
  name: "Claude Code",
7731
- path: join12(homedir9(), ".claude", "skills", "remem-mcp", "SKILL.md")
8438
+ path: join13(homedir10(), ".claude", "skills", "remem-mcp", "SKILL.md")
7732
8439
  },
7733
8440
  {
7734
8441
  name: "Codex CLI",
7735
- path: join12(homedir9(), ".codex", "skills", "remem-mcp", "SKILL.md")
8442
+ path: join13(homedir10(), ".codex", "skills", "remem-mcp", "SKILL.md")
7736
8443
  },
7737
8444
  {
7738
8445
  name: "Generic (.agents)",
7739
- path: join12(homedir9(), ".agents", "skills", "remem-mcp", "SKILL.md")
8446
+ path: join13(homedir10(), ".agents", "skills", "remem-mcp", "SKILL.md")
7740
8447
  }
7741
8448
  ];
7742
8449
  async function installSkill() {
@@ -7744,15 +8451,15 @@ async function installSkill() {
7744
8451
  let installed = 0;
7745
8452
  for (const target of SKILL_TARGETS) {
7746
8453
  const dir = dirname8(target.path);
7747
- if (!existsSync11(dir)) {
7748
- mkdirSync7(dir, { recursive: true });
8454
+ if (!existsSync12(dir)) {
8455
+ mkdirSync8(dir, { recursive: true });
7749
8456
  }
7750
- if (existsSync11(target.path)) {
8457
+ if (existsSync12(target.path)) {
7751
8458
  console.log(` ${target.name}: Already installed. Updated.`);
7752
8459
  } else {
7753
8460
  console.log(` ${target.name}: Installed.`);
7754
8461
  }
7755
- writeFileSync7(target.path, skillContent, "utf-8");
8462
+ writeFileSync8(target.path, skillContent, "utf-8");
7756
8463
  installed++;
7757
8464
  }
7758
8465
  console.log(`
@@ -7765,47 +8472,268 @@ Skill installed to ${installed} location(s).`);
7765
8472
  console.log(" - Forget only on explicit user request");
7766
8473
  }
7767
8474
 
7768
- // src/security/audit.ts
8475
+ // src/pipeline/mermaid.ts
7769
8476
  init_esm_shims();
7770
- import { createHash as createHash4 } from "crypto";
7771
- import { appendFileSync as appendFileSync3, existsSync as existsSync12, mkdirSync as mkdirSync8 } from "fs";
7772
- import { dirname as dirname9 } from "path";
7773
- var AuditLogger = class {
7774
- logPath;
7775
- enabled;
7776
- constructor(logPath, enabled) {
7777
- this.logPath = logPath;
7778
- this.enabled = enabled;
7779
- if (enabled) {
7780
- const dir = dirname9(logPath);
7781
- if (!existsSync12(dir)) mkdirSync8(dir, { recursive: true });
7782
- }
7783
- }
7784
- /** Log a tool call. */
7785
- log(entry) {
7786
- if (!this.enabled) return;
7787
- const fullEntry = {
7788
- ts: Date.now(),
7789
- ...entry
8477
+ init_ulid();
8478
+ var MermaidPipeline = class {
8479
+ name = "mermaid";
8480
+ requiresLLM = false;
8481
+ async process(input, ctx) {
8482
+ const nodeId = generateId();
8483
+ const label = extractNodeLabel(input);
8484
+ const node = {
8485
+ id: nodeId,
8486
+ label,
8487
+ captureId: input.id
7790
8488
  };
7791
- const line2 = JSON.stringify(fullEntry);
8489
+ const edges = [];
7792
8490
  try {
7793
- appendFileSync3(this.logPath, `${line2}
7794
- `);
7795
- } catch (err) {
7796
- console.error(`[remem-mcp] Audit log write failed: ${err}`);
8491
+ const prevNode = await ctx.storage.getLatestCanvasNode(input.sessionKey);
8492
+ if (prevNode) {
8493
+ edges.push({
8494
+ from: prevNode.id,
8495
+ to: nodeId,
8496
+ label: inferEdgeLabel2(prevNode, input)
8497
+ });
8498
+ }
8499
+ } catch {
7797
8500
  }
7798
- }
7799
- /** Hash the arguments for the audit log. The log does not store raw arguments. */
7800
- static hashArgs(args) {
7801
- const json = JSON.stringify(args);
7802
- return createHash4("sha256").update(json).digest("hex");
8501
+ try {
8502
+ await ctx.storage.appendCanvasNode(input.sessionKey, node, edges);
8503
+ } catch {
8504
+ }
8505
+ try {
8506
+ await ctx.storage.writeRef(input.sessionKey, nodeId, input.content);
8507
+ } catch {
8508
+ }
8509
+ const canvas = { nodes: [node], edges };
8510
+ return { canvas };
8511
+ }
8512
+ };
8513
+ var LLMMermaidPipeline = class {
8514
+ constructor(llm) {
8515
+ this.llm = llm;
8516
+ }
8517
+ llm;
8518
+ name = "mermaid-llm";
8519
+ requiresLLM = true;
8520
+ async process(input, ctx) {
8521
+ const nodeId = generateId();
8522
+ let label = extractNodeLabel(input);
8523
+ try {
8524
+ const prompt2 = `Summarize this ${input.type} in max 40 characters. Output ONLY the summary, no quotes:
8525
+
8526
+ ${input.content.slice(0, 500)}`;
8527
+ const response = await this.llm.complete(prompt2);
8528
+ label = response.trim().slice(0, 40);
8529
+ } catch {
8530
+ }
8531
+ const node = { id: nodeId, label, captureId: input.id };
8532
+ const edges = [];
8533
+ try {
8534
+ const prevNode = await ctx.storage.getLatestCanvasNode(input.sessionKey);
8535
+ if (prevNode) {
8536
+ let edgeLabel = inferEdgeLabel2(prevNode, input);
8537
+ try {
8538
+ const edgePrompt = `In one word, describe the transition from "${prevNode.label}" to "${label}". Output ONLY the word:`;
8539
+ const edgeResp = await this.llm.complete(edgePrompt);
8540
+ edgeLabel = edgeResp.trim().slice(0, 20);
8541
+ } catch {
8542
+ }
8543
+ edges.push({ from: prevNode.id, to: nodeId, label: edgeLabel });
8544
+ }
8545
+ } catch {
8546
+ }
8547
+ try {
8548
+ await ctx.storage.appendCanvasNode(input.sessionKey, node, edges);
8549
+ } catch {
8550
+ }
8551
+ try {
8552
+ await ctx.storage.writeRef(input.sessionKey, nodeId, input.content);
8553
+ } catch {
8554
+ }
8555
+ return { canvas: { nodes: [node], edges } };
8556
+ }
8557
+ };
8558
+ function extractNodeLabel(input) {
8559
+ const content = input.content.trim();
8560
+ if (input.type === "error") {
8561
+ const errorMatch = content.match(/Error[:\s]+(.+)/i);
8562
+ if (errorMatch) return truncate(errorMatch[1].trim(), 40);
8563
+ }
8564
+ if (input.type === "decision") {
8565
+ const firstSentence = content.split(/[.!]\s/)[0];
8566
+ return truncate(firstSentence, 40);
8567
+ }
8568
+ if (input.type === "task") {
8569
+ return truncate(content.split("\n")[0], 40);
8570
+ }
8571
+ const firstLine = content.split("\n").find((l) => l.trim().length > 0);
8572
+ return truncate(firstLine ?? "untitled", 40);
8573
+ }
8574
+ function inferEdgeLabel2(prev, current) {
8575
+ if (current.type === "error") return "failed";
8576
+ if (current.type === "decision") return "decided";
8577
+ if (current.type === "task") return "executed";
8578
+ if (current.type === "learning") return "learned";
8579
+ return "next";
8580
+ }
8581
+ function truncate(s, maxLen) {
8582
+ if (s.length <= maxLen) return s;
8583
+ return s.slice(0, maxLen - 3) + "...";
8584
+ }
8585
+
8586
+ // src/pipeline/skill.ts
8587
+ init_esm_shims();
8588
+ init_ulid();
8589
+ var SkillExtractionPipeline = class {
8590
+ name = "skill";
8591
+ requiresLLM = false;
8592
+ async process(input, ctx) {
8593
+ if (input.type !== "task") return {};
8594
+ const steps = extractSteps(input.content);
8595
+ const triggers = extractTriggers(input.content, input.tags);
8596
+ const validation = extractValidationRules(input.content);
8597
+ if (steps.length < 2) return {};
8598
+ const skillName = extractSkillName(input.content) ?? `skill-${input.id.slice(-8)}`;
8599
+ const description = extractDescription(input.content);
8600
+ let skillId = null;
8601
+ try {
8602
+ const now = Date.now();
8603
+ skillId = generateId();
8604
+ await ctx.storage.putSkill({
8605
+ id: skillId,
8606
+ teamId: input.teamId ?? "default",
8607
+ agentId: "pipeline",
8608
+ name: skillName,
8609
+ description,
8610
+ content: input.content,
8611
+ version: 1,
8612
+ createdAt: now,
8613
+ updatedAt: now,
8614
+ triggerConditions: triggers,
8615
+ steps,
8616
+ validationRules: validation,
8617
+ sourceCaptureIds: [input.id],
8618
+ archived: false
8619
+ });
8620
+ } catch (e) {
8621
+ return {};
8622
+ }
8623
+ return { skill: { id: skillId, name: skillName, steps, triggers, validation } };
8624
+ }
8625
+ };
8626
+ function extractSteps(content) {
8627
+ const steps = [];
8628
+ const numberedMatch = content.match(/(?:^|\n)\s*(\d+)[.)]\s+(.+)/g);
8629
+ if (numberedMatch) {
8630
+ for (const match of numberedMatch) {
8631
+ const step = match.replace(/(?:^|\n)\s*\d+[.)]\s+/, "").trim();
8632
+ if (step.length > 5 && step.length < 200) steps.push(step);
8633
+ }
8634
+ }
8635
+ if (steps.length === 0) {
8636
+ const bulletMatch = content.match(/(?:^|\n)\s*[-*]\s+(.+)/g);
8637
+ if (bulletMatch) {
8638
+ for (const match of bulletMatch) {
8639
+ const step = match.replace(/(?:^|\n)\s*[-*]\s+/, "").trim();
8640
+ if (step.length > 5 && step.length < 200) steps.push(step);
8641
+ }
8642
+ }
8643
+ }
8644
+ if (steps.length === 0) {
8645
+ const stepMatch = content.match(/(?:^|\n)\s*Step\s+\d+:\s*(.+)/gi);
8646
+ if (stepMatch) {
8647
+ for (const match of stepMatch) {
8648
+ const step = match.replace(/(?:^|\n)\s*Step\s+\d+:\s*/i, "").trim();
8649
+ if (step.length > 5 && step.length < 200) steps.push(step);
8650
+ }
8651
+ }
8652
+ }
8653
+ return steps.slice(0, 10);
8654
+ }
8655
+ function extractTriggers(content, tags) {
8656
+ const triggers = /* @__PURE__ */ new Set();
8657
+ for (const tag of tags) {
8658
+ if (tag.length > 2 && tag !== "task") triggers.add(tag);
8659
+ }
8660
+ const firstWords = content.slice(0, 200).split(/\s+/).filter((w) => w.length > 4);
8661
+ for (const word of firstWords.slice(0, 5)) {
8662
+ triggers.add(word.toLowerCase().replace(/[^a-z0-9]/g, ""));
8663
+ }
8664
+ return Array.from(triggers).slice(0, 8);
8665
+ }
8666
+ function extractValidationRules(content) {
8667
+ const rules = [];
8668
+ const patterns = [
8669
+ /(?:verify|check|validate|confirm)\s+[^.]+/gi,
8670
+ /(?:success|succeeded|passed)\s+[^.]+/gi,
8671
+ /(?:expected|should)\s+[^.]+/gi
8672
+ ];
8673
+ for (const pattern of patterns) {
8674
+ const matches = content.match(pattern);
8675
+ if (matches) {
8676
+ for (const match of matches.slice(0, 3)) {
8677
+ rules.push(match.trim());
8678
+ }
8679
+ }
8680
+ }
8681
+ return rules.slice(0, 5);
8682
+ }
8683
+ function extractSkillName(content) {
8684
+ const firstLine = content.split("\n")[0].trim();
8685
+ if (firstLine.length > 5 && firstLine.length < 60) {
8686
+ return firstLine.replace(/[^a-zA-Z0-9\s-]/g, "").trim().toLowerCase().replace(/\s+/g, "-");
8687
+ }
8688
+ return null;
8689
+ }
8690
+ function extractDescription(content) {
8691
+ const firstSentence = content.split(/[.!]\s/)[0];
8692
+ return firstSentence.slice(0, 120);
8693
+ }
8694
+
8695
+ // src/security/audit.ts
8696
+ init_esm_shims();
8697
+ import { createHash as createHash4 } from "crypto";
8698
+ import { appendFileSync as appendFileSync3, existsSync as existsSync13, mkdirSync as mkdirSync9 } from "fs";
8699
+ import { dirname as dirname9 } from "path";
8700
+ var AuditLogger = class {
8701
+ logPath;
8702
+ enabled;
8703
+ constructor(logPath, enabled) {
8704
+ this.logPath = logPath;
8705
+ this.enabled = enabled;
8706
+ if (enabled) {
8707
+ const dir = dirname9(logPath);
8708
+ if (!existsSync13(dir)) mkdirSync9(dir, { recursive: true });
8709
+ }
8710
+ }
8711
+ /** Log a tool call. */
8712
+ log(entry) {
8713
+ if (!this.enabled) return;
8714
+ const fullEntry = {
8715
+ ts: Date.now(),
8716
+ ...entry
8717
+ };
8718
+ const line2 = JSON.stringify(fullEntry);
8719
+ try {
8720
+ appendFileSync3(this.logPath, `${line2}
8721
+ `);
8722
+ } catch (err) {
8723
+ console.error(`[remem-mcp] Audit log write failed: ${err}`);
8724
+ }
8725
+ }
8726
+ /** Hash the arguments for the audit log. The log does not store raw arguments. */
8727
+ static hashArgs(args) {
8728
+ const json = JSON.stringify(args);
8729
+ return createHash4("sha256").update(json).digest("hex");
7803
8730
  }
7804
8731
  };
7805
8732
 
7806
8733
  // src/server.ts
7807
8734
  init_esm_shims();
7808
8735
  init_engine();
8736
+ import { execSync as execSync3 } from "child_process";
7809
8737
  import { createHash as createHash5 } from "crypto";
7810
8738
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
7811
8739
  import {
@@ -7869,6 +8797,7 @@ function formatResults(results) {
7869
8797
 
7870
8798
  // src/server.ts
7871
8799
  init_ulid();
8800
+ var indexingStatus = /* @__PURE__ */ new Map();
7872
8801
  function defaultSessionKey() {
7873
8802
  if (process.env.REMEM_SESSION_KEY) return process.env.REMEM_SESSION_KEY;
7874
8803
  const cwd = process.cwd();
@@ -8056,6 +8985,29 @@ var TOOLS = [
8056
8985
  }
8057
8986
  }
8058
8987
  },
8988
+ {
8989
+ name: "feedback",
8990
+ description: "Record a quality signal for a recalled memory. Call this AFTER using a recall result: use 'helpful' if the memory answered your question, 'not_helpful' if it was irrelevant, 'stale' if the information is outdated, or 'wrong' if it is incorrect. This creates a feedback flywheel \u2014 useful memories rise, unhelpful ones fade. Do not call this for memories you have not actually used.",
8991
+ inputSchema: {
8992
+ type: "object",
8993
+ properties: {
8994
+ capture_id: {
8995
+ type: "string",
8996
+ description: "The capture ID from a recall/search result."
8997
+ },
8998
+ signal: {
8999
+ type: "string",
9000
+ enum: ["helpful", "not_helpful", "stale", "wrong"],
9001
+ description: "helpful = memory was useful (salience +0.1, max 2.0). not_helpful = memory was irrelevant (salience -0.1, min 0.1). stale = information is outdated (salience floored at 0.3). wrong = information is incorrect (salience floored at 0.1)."
9002
+ },
9003
+ reason: {
9004
+ type: "string",
9005
+ description: "Optional explanation for the feedback signal."
9006
+ }
9007
+ },
9008
+ required: ["capture_id", "signal"]
9009
+ }
9010
+ },
8059
9011
  {
8060
9012
  name: "search",
8061
9013
  description: "Search memory by keyword or by semantic similarity. Use this tool when recall is too broad and you need specific facts. Automatically searches both project and global cross-project memory when REMEM_GLOBAL_SESSION_KEY is configured.",
@@ -8099,6 +9051,11 @@ var TOOLS = [
8099
9051
  enum: ["text", "json"],
8100
9052
  default: "text",
8101
9053
  description: "The response format. Use 'json' for structured data (e.g. benchmarks). Defaults to 'text'."
9054
+ },
9055
+ explain: {
9056
+ type: "boolean",
9057
+ default: false,
9058
+ description: "When true, attach per-hit score_details explaining ranking (BM25 rank, vector score, entity matches, authority multiplier, feedback salience, link provenance). Useful for debugging search quality."
8102
9059
  }
8103
9060
  },
8104
9061
  required: ["query"]
@@ -8492,7 +9449,7 @@ var TOOLS = [
8492
9449
  },
8493
9450
  {
8494
9451
  name: "codegraph_search",
8495
- description: "Search for code symbols by name. Returns matching functions, classes, methods, etc. with file paths and line numbers. Use this INSTEAD of grep when looking for function/class/method definitions. If the codebase hasn't been indexed yet, this auto-indexes src/ (or cwd) on first use \u2014 no manual setup needed.",
9452
+ description: "Search for code symbols by name. Returns matching functions, classes, methods, etc. with file paths and line numbers. Use this INSTEAD of grep when looking for function/class/method definitions. If the codebase hasn't been indexed yet, this starts background indexing and asks you to retry in a few seconds \u2014 non-blocking, so you can continue working while indexing runs.",
8496
9453
  inputSchema: {
8497
9454
  type: "object",
8498
9455
  properties: {
@@ -8597,6 +9554,20 @@ var TOOLS = [
8597
9554
  required: ["repo_path"]
8598
9555
  }
8599
9556
  },
9557
+ {
9558
+ name: "codegraph_stats",
9559
+ description: "Return indexed CodeGraph statistics: total symbols, calls, resolved calls, imports, and file counts. Optionally scoped to a repo_path. Use this to check indexing coverage before searching.",
9560
+ inputSchema: {
9561
+ type: "object",
9562
+ properties: {
9563
+ repo_path: {
9564
+ type: "string",
9565
+ description: "Optional: scope stats to this repository root path."
9566
+ },
9567
+ team_id: { type: "string", description: "The team ID for isolation." }
9568
+ }
9569
+ }
9570
+ },
8600
9571
  // ─── Wiki tools ───
8601
9572
  {
8602
9573
  name: "wiki_ingest",
@@ -8870,6 +9841,96 @@ var TOOLS = [
8870
9841
  type: "object",
8871
9842
  properties: {}
8872
9843
  }
9844
+ },
9845
+ {
9846
+ name: "canvas_get",
9847
+ description: "Get the Mermaid task canvas for the current session. The canvas is a symbolic graph of tool calls and state transitions \u2014 it replaces verbose tool logs in your context with a compact Mermaid diagram. Use this to see the task structure without re-reading full tool outputs. Requires REMEM_OFFLOAD_ENABLED=true and pipeline=mermaid.",
9848
+ inputSchema: {
9849
+ type: "object",
9850
+ properties: {
9851
+ session_key: {
9852
+ type: "string",
9853
+ description: "The session key. Defaults to hash(cwd). Use this to get the canvas from a different session."
9854
+ },
9855
+ format: {
9856
+ type: "string",
9857
+ enum: ["mermaid", "json"],
9858
+ default: "mermaid",
9859
+ description: "Output format. 'mermaid' returns the Mermaid graph text. 'json' returns structured nodes/edges."
9860
+ }
9861
+ }
9862
+ }
9863
+ },
9864
+ {
9865
+ name: "ref_read",
9866
+ description: "Read the raw tool output for a specific canvas node by node_id. Use this to drill down from the Mermaid canvas to the full output when you need details. Requires REMEM_OFFLOAD_ENABLED=true and pipeline=mermaid.",
9867
+ inputSchema: {
9868
+ type: "object",
9869
+ properties: {
9870
+ node_id: {
9871
+ type: "string",
9872
+ description: "The node_id from the Mermaid canvas (e.g., '01M0FFZKQT540SVZPAZ4EZVE4G')."
9873
+ }
9874
+ },
9875
+ required: ["node_id"]
9876
+ }
9877
+ },
9878
+ {
9879
+ name: "skill_create",
9880
+ description: "Create a reusable skill (SOP) from a successful task or conversation. Skills are injected into recall when matching trigger conditions are met. Use this after completing a non-trivial task to capture the workflow for reuse.",
9881
+ inputSchema: {
9882
+ type: "object",
9883
+ properties: {
9884
+ name: {
9885
+ type: "string",
9886
+ description: "Short name for the skill (e.g., 'deploy-to-vercel')."
9887
+ },
9888
+ description: {
9889
+ type: "string",
9890
+ description: "One-line description of what the skill does."
9891
+ },
9892
+ content: {
9893
+ type: "string",
9894
+ description: "Full SOP content in markdown. Include steps, commands, and validation."
9895
+ },
9896
+ trigger_conditions: {
9897
+ type: "array",
9898
+ items: { type: "string" },
9899
+ description: "Keywords or patterns that trigger this skill (e.g., ['deploy', 'vercel', 'production'])."
9900
+ },
9901
+ steps: {
9902
+ type: "array",
9903
+ items: { type: "string" },
9904
+ description: "Ordered execution steps."
9905
+ },
9906
+ validation_rules: {
9907
+ type: "array",
9908
+ items: { type: "string" },
9909
+ description: "How to verify the skill succeeded (e.g., ['curl returns 200', 'no errors in logs'])."
9910
+ },
9911
+ source_capture_ids: {
9912
+ type: "array",
9913
+ items: { type: "string" },
9914
+ description: "Capture IDs that this skill was extracted from (for traceability)."
9915
+ },
9916
+ ...TENANT_PARAMS
9917
+ },
9918
+ required: ["name", "content"]
9919
+ }
9920
+ },
9921
+ {
9922
+ name: "skill_archive",
9923
+ description: "Archive a skill \u2014 forces it to always be injected into recall, even when the query doesn't strongly match. Use this for critical SOPs that must always be available.",
9924
+ inputSchema: {
9925
+ type: "object",
9926
+ properties: {
9927
+ id: {
9928
+ type: "string",
9929
+ description: "The skill ID to archive."
9930
+ }
9931
+ },
9932
+ required: ["id"]
9933
+ }
8873
9934
  }
8874
9935
  ];
8875
9936
  var CORE_TOOL_NAMES = /* @__PURE__ */ new Set([
@@ -9004,6 +10065,8 @@ function createServer(opts) {
9004
10065
  return handleRecall(args, opts);
9005
10066
  case "capture":
9006
10067
  return handleCapture(args, opts);
10068
+ case "feedback":
10069
+ return handleFeedback(args, opts);
9007
10070
  case "search":
9008
10071
  return handleSearch(args, opts);
9009
10072
  case "related":
@@ -9050,6 +10113,8 @@ function createServer(opts) {
9050
10113
  return handleCodegraphList(args, opts);
9051
10114
  case "codegraph_detect_changes":
9052
10115
  return handleCodegraphDetectChanges(args, opts);
10116
+ case "codegraph_stats":
10117
+ return handleCodegraphStats(args, opts);
9053
10118
  case "wiki_ingest":
9054
10119
  return handleWikiIngest(args, opts);
9055
10120
  case "wiki_search":
@@ -9082,6 +10147,14 @@ function createServer(opts) {
9082
10147
  return handleSessionCheckpoint(args, opts);
9083
10148
  case "health":
9084
10149
  return handleHealth(args, opts);
10150
+ case "canvas_get":
10151
+ return handleCanvasGet(args, opts);
10152
+ case "ref_read":
10153
+ return handleRefRead(args, opts);
10154
+ case "skill_create":
10155
+ return handleSkillCreate(args, opts);
10156
+ case "skill_archive":
10157
+ return handleSkillArchive(args, opts);
9085
10158
  default:
9086
10159
  return {
9087
10160
  content: [{ type: "text", text: `Error: Unknown tool "${name}".` }],
@@ -9099,6 +10172,12 @@ function extractTenant(args) {
9099
10172
  taskId: args.task_id
9100
10173
  };
9101
10174
  }
10175
+ function textResult(text) {
10176
+ return { content: [{ type: "text", text }] };
10177
+ }
10178
+ function errorResult(text) {
10179
+ return { content: [{ type: "text", text }], isError: true };
10180
+ }
9102
10181
  async function handleRecall(args, opts) {
9103
10182
  const query = args.query;
9104
10183
  const sessionKey = args.session_key ?? defaultSessionKey();
@@ -9187,7 +10266,10 @@ async function handleRecall(args, opts) {
9187
10266
  trust_state: r.entry.trustState ?? "candidate"
9188
10267
  }));
9189
10268
  const jsonText = JSON.stringify(
9190
- vectorDegraded ? { results: jsonResults, _meta: { vector_degraded: true, channels_run: ["keyword"] } } : { results: jsonResults, _meta: { channels_run: mode === "vector" ? ["vector"] : ["keyword", "vector"] } }
10269
+ vectorDegraded ? { results: jsonResults, _meta: { vector_degraded: true, channels_run: ["keyword"] } } : {
10270
+ results: jsonResults,
10271
+ _meta: { channels_run: mode === "vector" ? ["vector"] : ["keyword", "vector"] }
10272
+ }
9191
10273
  );
9192
10274
  opts.audit.log({
9193
10275
  tool: "recall",
@@ -9234,9 +10316,7 @@ ${wikiLines.join("\n")}`;
9234
10316
  limit: 3
9235
10317
  });
9236
10318
  if (scenarios.length > 0) {
9237
- const scenarioLines = scenarios.map(
9238
- (s) => ` ${s.summary}`
9239
- );
10319
+ const scenarioLines = scenarios.map((s) => ` ${s.summary}`);
9240
10320
  text += `
9241
10321
 
9242
10322
  ## Scenarios (L2 summaries)
@@ -9258,6 +10338,27 @@ ${text}`;
9258
10338
  });
9259
10339
  return { content: [{ type: "text", text: finalText }] };
9260
10340
  }
10341
+ async function handleFeedback(args, opts) {
10342
+ const captureId = String(args.capture_id ?? "");
10343
+ const signal = String(args.signal ?? "");
10344
+ const reason = args.reason ? String(args.reason) : void 0;
10345
+ if (!captureId || !signal) {
10346
+ return errorResult("capture_id and signal are required");
10347
+ }
10348
+ const validSignals = ["helpful", "not_helpful", "stale", "wrong"];
10349
+ if (!validSignals.includes(signal)) {
10350
+ return errorResult(`signal must be one of: ${validSignals.join(", ")}`);
10351
+ }
10352
+ const storage = opts.storage;
10353
+ const entry = await storage.get(captureId);
10354
+ if (!entry) {
10355
+ return errorResult(`capture not found: ${captureId}`);
10356
+ }
10357
+ storage.recordFeedback(captureId, signal, reason, opts.agentId ?? detectAgentId());
10358
+ return textResult(
10359
+ `Recorded feedback: ${signal} for capture ${captureId.slice(-8)}. Salience multiplier adjusted. This memory will ${signal === "helpful" ? "rank higher" : signal === "not_helpful" ? "rank lower" : signal === "stale" ? "be deprioritized as outdated" : "be deprioritized as incorrect"} in future searches.`
10360
+ );
10361
+ }
9261
10362
  async function handleCapture(args, opts) {
9262
10363
  const type2 = args.type ?? "conversation";
9263
10364
  const tags = args.tags ?? [];
@@ -9353,7 +10454,15 @@ async function handleCapture(args, opts) {
9353
10454
  } else {
9354
10455
  opts.audit.log({
9355
10456
  tool: "capture",
9356
- argsHash: AuditLogger.hashArgs({ type: type2, tags, sessionKey, overrideReason, teamId, userId, taskId }),
10457
+ argsHash: AuditLogger.hashArgs({
10458
+ type: type2,
10459
+ tags,
10460
+ sessionKey,
10461
+ overrideReason,
10462
+ teamId,
10463
+ userId,
10464
+ taskId
10465
+ }),
9357
10466
  resultLen: 0,
9358
10467
  quotaHit: false,
9359
10468
  redacted: wasRedacted
@@ -9417,12 +10526,14 @@ async function handleCapture(args, opts) {
9417
10526
  };
9418
10527
  try {
9419
10528
  await opts.storage.put(entry);
9420
- } catch {
10529
+ } catch (err) {
10530
+ const errMsg = err instanceof Error ? err.message : String(err);
10531
+ const isBusy = /busy|locked|SQLITE_BUSY/i.test(errMsg);
9421
10532
  return {
9422
10533
  content: [
9423
10534
  {
9424
10535
  type: "text",
9425
- text: `Error: Database is read-only (sandbox restriction). Capture failed. Set sandbox_mode to "danger-full-access" or add the DB directory to writable roots.`
10536
+ text: isBusy ? `Error: Database is temporarily locked (concurrent write). Retry the capture.` : `Error: Database is read-only (sandbox restriction). Capture failed. Set sandbox_mode to "danger-full-access" or add the DB directory to writable roots.`
9426
10537
  }
9427
10538
  ],
9428
10539
  isError: true
@@ -9550,6 +10661,7 @@ async function handleSearch(args, opts) {
9550
10661
  const query = args.query;
9551
10662
  const mode = args.mode ?? "hybrid";
9552
10663
  const format = args.format ?? "text";
10664
+ const explain = args.explain ?? false;
9553
10665
  const filters = args.filters;
9554
10666
  const limit = Math.min(args.limit ?? 20, 500);
9555
10667
  let queryEmbedding = null;
@@ -9585,7 +10697,8 @@ async function handleSearch(args, opts) {
9585
10697
  limit: projectLimit,
9586
10698
  offset: 0,
9587
10699
  mode,
9588
- filters: searchFilters
10700
+ filters: searchFilters,
10701
+ explain
9589
10702
  });
9590
10703
  let globalResults = [];
9591
10704
  try {
@@ -9594,7 +10707,8 @@ async function handleSearch(args, opts) {
9594
10707
  limit: reservedGlobal,
9595
10708
  offset: 0,
9596
10709
  mode,
9597
- filters: searchFilters
10710
+ filters: searchFilters,
10711
+ explain
9598
10712
  });
9599
10713
  } catch {
9600
10714
  }
@@ -9607,7 +10721,8 @@ async function handleSearch(args, opts) {
9607
10721
  limit,
9608
10722
  offset: 0,
9609
10723
  mode,
9610
- filters: searchFilters
10724
+ filters: searchFilters,
10725
+ explain
9611
10726
  });
9612
10727
  }
9613
10728
  results = dedupByContentPrefix(results, 60);
@@ -9638,7 +10753,10 @@ async function handleSearch(args, opts) {
9638
10753
  trust_state: r.entry.trustState ?? "candidate"
9639
10754
  }));
9640
10755
  const jsonText = JSON.stringify(
9641
- vectorDegraded ? { results: jsonResults, _meta: { vector_degraded: true, channels_run: ["keyword"] } } : { results: jsonResults, _meta: { channels_run: mode === "vector" ? ["vector"] : ["keyword", "vector"] } }
10756
+ vectorDegraded ? { results: jsonResults, _meta: { vector_degraded: true, channels_run: ["keyword"] } } : {
10757
+ results: jsonResults,
10758
+ _meta: { channels_run: mode === "vector" ? ["vector"] : ["keyword", "vector"] }
10759
+ }
9642
10760
  );
9643
10761
  opts.audit.log({
9644
10762
  tool: "search",
@@ -10368,6 +11486,7 @@ async function handleConfirm(args, opts) {
10368
11486
  }
10369
11487
  try {
10370
11488
  db.prepare("UPDATE captures SET confirmations = confirmations + 1 WHERE id = ?").run(captureId);
11489
+ opts.storage.recordFeedback(captureId, "helpful", "auto: confirmed by agent", opts.agentId ?? detectAgentId());
10371
11490
  } catch (err) {
10372
11491
  return { content: [{ type: "text", text: `Error: ${err}` }], isError: true };
10373
11492
  }
@@ -10378,7 +11497,7 @@ async function handleConfirm(args, opts) {
10378
11497
  content: [
10379
11498
  {
10380
11499
  type: "text",
10381
- text: `Confirmed capture ${captureId}. Confidence: ${confidence.toFixed(2)} (${row.confirmations + 1} confirmations, ${row.corrections} corrections).`
11500
+ text: `Confirmed capture ${captureId}. Confidence: ${confidence.toFixed(2)} (${row.confirmations + 1} confirmations, ${row.corrections} corrections). Feedback salience boosted.`
10382
11501
  }
10383
11502
  ]
10384
11503
  };
@@ -10407,6 +11526,7 @@ async function handleCorrect(args, opts) {
10407
11526
  if (reason) {
10408
11527
  db.prepare("UPDATE captures SET rejection_reason = ? WHERE id = ?").run(reason, captureId);
10409
11528
  }
11529
+ opts.storage.recordFeedback(captureId, "wrong", reason ? `auto: ${reason}` : "auto: corrected by agent", opts.agentId ?? detectAgentId());
10410
11530
  } catch (err) {
10411
11531
  return { content: [{ type: "text", text: `Error: ${err}` }], isError: true };
10412
11532
  }
@@ -11071,6 +12191,12 @@ async function handleKnowledgeList(args, opts) {
11071
12191
  }
11072
12192
  async function handleKnowledgeDelete(args, opts) {
11073
12193
  const knowledgeIds = args.knowledge_ids;
12194
+ if (!Array.isArray(knowledgeIds) || knowledgeIds.length === 0) {
12195
+ return {
12196
+ content: [{ type: "text", text: "Error: knowledge_ids (non-empty array) is required." }],
12197
+ isError: true
12198
+ };
12199
+ }
11074
12200
  let count;
11075
12201
  try {
11076
12202
  count = await opts.storage.deleteKnowledge(knowledgeIds);
@@ -11096,10 +12222,16 @@ async function handleScenarioConsolidate(args, opts) {
11096
12222
  return { content: [{ type: "text", text: "Error: atom_ids is required." }], isError: true };
11097
12223
  }
11098
12224
  if (!summary || summary.trim().length < 10) {
11099
- return { content: [{ type: "text", text: "Error: summary must be at least 10 characters." }], isError: true };
12225
+ return {
12226
+ content: [{ type: "text", text: "Error: summary must be at least 10 characters." }],
12227
+ isError: true
12228
+ };
11100
12229
  }
11101
12230
  if (atomIds.length > 20) {
11102
- return { content: [{ type: "text", text: "Error: Maximum 20 atom_ids per consolidation." }], isError: true };
12231
+ return {
12232
+ content: [{ type: "text", text: "Error: Maximum 20 atom_ids per consolidation." }],
12233
+ isError: true
12234
+ };
11103
12235
  }
11104
12236
  const id = generateId();
11105
12237
  const scenario = {
@@ -11139,11 +12271,14 @@ async function handlePersonaUpdate(args, opts) {
11139
12271
  const { teamId, userId } = extractTenant(args);
11140
12272
  const agentId = args.agent_id ?? detectAgentId();
11141
12273
  if (!trait || !value) {
11142
- return { content: [{ type: "text", text: "Error: trait and value are required." }], isError: true };
12274
+ return {
12275
+ content: [{ type: "text", text: "Error: trait and value are required." }],
12276
+ isError: true
12277
+ };
11143
12278
  }
11144
12279
  const tid = teamId ?? "default";
11145
12280
  const uid = userId ?? "default";
11146
- let existing = await opts.storage.readPersona(tid, agentId, uid);
12281
+ const existing = await opts.storage.readPersona(tid, agentId, uid);
11147
12282
  let content;
11148
12283
  if (existing) {
11149
12284
  const lines = existing.content.split("\n").filter((l) => l.trim());
@@ -11232,10 +12367,10 @@ async function handleCodegraphIndex(args, opts) {
11232
12367
  return { content: [{ type: "text", text: "Error: path is required." }], isError: true };
11233
12368
  }
11234
12369
  const db = getDb(opts);
11235
- const { statSync: statSync3 } = await import("fs");
12370
+ const { statSync: statSync4 } = await import("fs");
11236
12371
  let stat;
11237
12372
  try {
11238
- stat = statSync3(path);
12373
+ stat = statSync4(path);
11239
12374
  } catch {
11240
12375
  return { content: [{ type: "text", text: `Error: path not found: ${path}` }], isError: true };
11241
12376
  }
@@ -11283,41 +12418,80 @@ async function handleCodegraphSearch(args, opts) {
11283
12418
  let symbols = searchSymbols(db, query, { teamId, kind, language, limit, repoPath });
11284
12419
  if (symbols.length === 0) {
11285
12420
  const cwd = process.cwd();
11286
- const { existsSync: existsSync16 } = await import("fs");
12421
+ const { existsSync: existsSync17 } = await import("fs");
11287
12422
  let indexPath;
11288
12423
  let repoRoot;
11289
12424
  if (customPath) {
11290
12425
  indexPath = customPath;
11291
12426
  repoRoot = customPath;
11292
12427
  } else {
11293
- indexPath = existsSync16(`${cwd}/src`) ? `${cwd}/src` : cwd;
12428
+ indexPath = existsSync17(`${cwd}/src`) ? `${cwd}/src` : cwd;
11294
12429
  repoRoot = cwd;
11295
12430
  }
11296
- try {
11297
- const results = await indexDirectory(db, indexPath, repoRoot, teamId, 500);
11298
- const indexed = results.filter((r) => !r.skipped);
11299
- if (indexed.length > 0) {
11300
- symbols = searchSymbols(db, query, { teamId, kind, language, limit, repoPath: repoRoot });
11301
- }
12431
+ const statusKey = `${repoRoot}:${teamId ?? "default"}`;
12432
+ const status2 = indexingStatus.get(statusKey);
12433
+ if (!status2 || status2.state === "error") {
12434
+ indexingStatus.set(statusKey, {
12435
+ state: "indexing",
12436
+ startedAt: Date.now()
12437
+ });
12438
+ indexDirectory(db, indexPath, repoRoot, teamId, 1e4).then((results) => {
12439
+ const indexed = results.filter((r) => !r.skipped);
12440
+ const totalSymbols = indexed.reduce((s, r) => s + r.symbols, 0);
12441
+ indexingStatus.set(statusKey, {
12442
+ state: "done",
12443
+ startedAt: status2?.startedAt ?? Date.now(),
12444
+ finishedAt: Date.now(),
12445
+ fileCount: indexed.length,
12446
+ symbolCount: totalSymbols
12447
+ });
12448
+ console.error(
12449
+ `[remem-mcp] background index complete: ${indexed.length} files, ${totalSymbols} symbols from ${indexPath}`
12450
+ );
12451
+ }).catch((err) => {
12452
+ indexingStatus.set(statusKey, {
12453
+ state: "error",
12454
+ startedAt: status2?.startedAt ?? Date.now(),
12455
+ finishedAt: Date.now(),
12456
+ error: String(err)
12457
+ });
12458
+ console.error(`[remem-mcp] background index failed: ${err}`);
12459
+ });
12460
+ return {
12461
+ content: [
12462
+ {
12463
+ type: "text",
12464
+ text: `No symbols found yet. Background indexing started for ${indexPath}.
12465
+ Retry codegraph_search in ~10-30s (large repos may take longer).
12466
+ You can continue working \u2014 indexing runs in the background.`
12467
+ }
12468
+ ]
12469
+ };
12470
+ } else if (status2.state === "indexing") {
12471
+ const elapsed = Math.round((Date.now() - status2.startedAt) / 1e3);
12472
+ return {
12473
+ content: [
12474
+ {
12475
+ type: "text",
12476
+ text: `No symbols found yet. Indexing in progress (${elapsed}s elapsed).
12477
+ Retry codegraph_search in a few seconds.
12478
+ Meanwhile, use grep/glob for immediate code navigation.`
12479
+ }
12480
+ ]
12481
+ };
12482
+ } else if (status2.state === "done") {
12483
+ symbols = searchSymbols(db, query, { teamId, kind, language, limit, repoPath: repoRoot });
11302
12484
  if (symbols.length === 0) {
11303
12485
  return {
11304
12486
  content: [
11305
12487
  {
11306
12488
  type: "text",
11307
- text: `No symbols found. Auto-indexed ${indexed.length} files from ${indexPath} \u2014 try a different search term, or call codegraph_index with a specific path.`
12489
+ text: `No symbols found for "${query}". Indexing completed: ${status2.fileCount} files, ${status2.symbolCount} symbols.
12490
+ Try a different search term, or call codegraph_index with a specific path.`
11308
12491
  }
11309
12492
  ]
11310
12493
  };
11311
12494
  }
11312
- } catch {
11313
- return {
11314
- content: [
11315
- {
11316
- type: "text",
11317
- text: "No symbols found. Call codegraph_index with a path to index your codebase first."
11318
- }
11319
- ]
11320
- };
11321
12495
  }
11322
12496
  }
11323
12497
  const lines = symbols.map(
@@ -11425,7 +12599,6 @@ function handleCodegraphDetectChanges(args, opts) {
11425
12599
  const db = getDb(opts);
11426
12600
  let changedFiles;
11427
12601
  try {
11428
- const { execSync: execSync3 } = __require("child_process");
11429
12602
  const output2 = execSync3("git diff --name-only HEAD", {
11430
12603
  cwd: repoPath,
11431
12604
  encoding: "utf-8",
@@ -11488,9 +12661,15 @@ function handleCodegraphDetectChanges(args, opts) {
11488
12661
  } catch {
11489
12662
  }
11490
12663
  }
11491
- const lines = [`Changed files: ${changedFiles.length}`, `Affected symbols: ${changedSymbols.length}`, ""];
12664
+ const lines = [
12665
+ `Changed files: ${changedFiles.length}`,
12666
+ `Affected symbols: ${changedSymbols.length}`,
12667
+ ""
12668
+ ];
11492
12669
  for (const r of results) {
11493
- lines.push(`[${r.risk.toUpperCase()}] ${r.symbol.kind} ${r.symbol.name} (${r.symbol.file}:${r.symbol.line})`);
12670
+ lines.push(
12671
+ `[${r.risk.toUpperCase()}] ${r.symbol.kind} ${r.symbol.name} (${r.symbol.file}:${r.symbol.line})`
12672
+ );
11494
12673
  if (r.callers.length > 0) {
11495
12674
  lines.push(` Callers (${r.callers.length} shown):`);
11496
12675
  for (const c of r.callers) {
@@ -11504,6 +12683,49 @@ function handleCodegraphDetectChanges(args, opts) {
11504
12683
  content: [{ type: "text", text: lines.join("\n") }]
11505
12684
  };
11506
12685
  }
12686
+ function handleCodegraphStats(args, opts) {
12687
+ const repoPath = args.repo_path ?? void 0;
12688
+ const teamId = args.team_id ?? void 0;
12689
+ const db = getDb(opts);
12690
+ const repoClause = repoPath ? "AND repo_path IS ?" : "";
12691
+ const repoParams = repoPath ? [repoPath] : [];
12692
+ const teamClause = teamId !== void 0 ? "AND team_id IS ?" : "";
12693
+ const teamParams = teamId !== void 0 ? [teamId] : [];
12694
+ const filter = `${repoClause} ${teamClause}`.trim();
12695
+ const symbolCount = db.prepare(`SELECT COUNT(*) as c FROM symbols WHERE 1=1 ${filter}`).get(...repoParams, ...teamParams);
12696
+ const callCount = db.prepare(
12697
+ `SELECT COUNT(*) as c FROM calls AS c
12698
+ JOIN symbols AS caller ON caller.id = c.caller_id
12699
+ WHERE 1=1 ${filter}`
12700
+ ).get(...repoParams, ...teamParams);
12701
+ const resolvedCount = db.prepare(
12702
+ `SELECT COUNT(*) as c FROM calls AS c
12703
+ JOIN symbols AS caller ON caller.id = c.caller_id
12704
+ WHERE c.callee_id IS NOT NULL ${filter}`
12705
+ ).get(...repoParams, ...teamParams);
12706
+ const importCount = db.prepare(`SELECT COUNT(*) as c FROM imports WHERE 1=1 ${filter}`).get(...repoParams, ...teamParams);
12707
+ const fileCount = db.prepare(`SELECT COUNT(DISTINCT file_path) as c FROM symbols WHERE 1=1 ${filter}`).get(...repoParams, ...teamParams);
12708
+ const byKind = db.prepare(
12709
+ `SELECT kind, COUNT(*) as c FROM symbols WHERE 1=1 ${filter} GROUP BY kind ORDER BY c DESC`
12710
+ ).all(...repoParams, ...teamParams);
12711
+ const lines = [
12712
+ `CodeGraph Statistics${repoPath ? ` (repo: ${repoPath})` : ""}`,
12713
+ ` files: ${fileCount.c}`,
12714
+ ` symbols: ${symbolCount.c}`,
12715
+ ` calls: ${callCount.c} (${resolvedCount.c} resolved, ${callCount.c - resolvedCount.c} unresolved)`,
12716
+ ` imports: ${importCount.c}`,
12717
+ ""
12718
+ ];
12719
+ if (byKind.length > 0) {
12720
+ lines.push("By kind:");
12721
+ for (const k of byKind) {
12722
+ lines.push(` ${k.kind.padEnd(12)} ${k.c}`);
12723
+ }
12724
+ }
12725
+ return {
12726
+ content: [{ type: "text", text: lines.join("\n") }]
12727
+ };
12728
+ }
11507
12729
  async function handleWikiIngest(args, opts) {
11508
12730
  const path = args.path;
11509
12731
  const repoPath = args.repo_path ?? path;
@@ -11513,10 +12735,10 @@ async function handleWikiIngest(args, opts) {
11513
12735
  return { content: [{ type: "text", text: "Error: path is required." }], isError: true };
11514
12736
  }
11515
12737
  const db = getDb(opts);
11516
- const { statSync: statSync3 } = await import("fs");
12738
+ const { statSync: statSync4 } = await import("fs");
11517
12739
  let stat;
11518
12740
  try {
11519
- stat = statSync3(path);
12741
+ stat = statSync4(path);
11520
12742
  } catch {
11521
12743
  return { content: [{ type: "text", text: `Error: path not found: ${path}` }], isError: true };
11522
12744
  }
@@ -11612,6 +12834,158 @@ ${lines.join("\n")}` }
11612
12834
  ]
11613
12835
  };
11614
12836
  }
12837
+ async function handleCanvasGet(args, opts) {
12838
+ const sessionKey = args.session_key ?? defaultSessionKey();
12839
+ const format = args.format ?? "mermaid";
12840
+ try {
12841
+ const mermaidText = await opts.storage.getCanvasMermaidText(sessionKey);
12842
+ if (!mermaidText) {
12843
+ return {
12844
+ content: [
12845
+ {
12846
+ type: "text",
12847
+ text: "No canvas found for this session. Enable REMEM_OFFLOAD_ENABLED=true and pipeline=mermaid to start building a task canvas."
12848
+ }
12849
+ ]
12850
+ };
12851
+ }
12852
+ if (format === "json") {
12853
+ return {
12854
+ content: [
12855
+ {
12856
+ type: "text",
12857
+ text: JSON.stringify(
12858
+ {
12859
+ sessionKey,
12860
+ mermaid: mermaidText,
12861
+ tokenEstimate: Math.ceil(mermaidText.length / 4)
12862
+ },
12863
+ null,
12864
+ 2
12865
+ )
12866
+ }
12867
+ ]
12868
+ };
12869
+ }
12870
+ return {
12871
+ content: [
12872
+ {
12873
+ type: "text",
12874
+ text: `## Task Canvas (Mermaid)
12875
+
12876
+ \`\`\`mermaid
12877
+ ${mermaidText}
12878
+ \`\`\`
12879
+
12880
+ Drill down: call ref_read(node_id) to get the raw output for any node.`
12881
+ }
12882
+ ]
12883
+ };
12884
+ } catch (err) {
12885
+ return {
12886
+ content: [{ type: "text", text: `Error reading canvas: ${err}` }]
12887
+ };
12888
+ }
12889
+ }
12890
+ async function handleRefRead(args, opts) {
12891
+ const nodeId = args.node_id;
12892
+ if (!nodeId) {
12893
+ return {
12894
+ content: [{ type: "text", text: "Error: node_id is required." }]
12895
+ };
12896
+ }
12897
+ try {
12898
+ const content = await opts.storage.readRef(nodeId);
12899
+ if (!content) {
12900
+ return {
12901
+ content: [
12902
+ {
12903
+ type: "text",
12904
+ text: `No ref found for node_id "${nodeId}". The ref file may have been cleaned up (REMEM_REFS_RETENTION_HOURS) or offloading is not enabled.`
12905
+ }
12906
+ ]
12907
+ };
12908
+ }
12909
+ return {
12910
+ content: [{ type: "text", text: content }]
12911
+ };
12912
+ } catch (err) {
12913
+ return {
12914
+ content: [{ type: "text", text: `Error reading ref: ${err}` }]
12915
+ };
12916
+ }
12917
+ }
12918
+ async function handleSkillCreate(args, opts) {
12919
+ const name = args.name;
12920
+ const content = args.content;
12921
+ if (!name || !content) {
12922
+ return {
12923
+ content: [{ type: "text", text: "Error: name and content are required." }]
12924
+ };
12925
+ }
12926
+ const tenant = extractTenant(args);
12927
+ const teamId = tenant.teamId ?? "default";
12928
+ const agentId = tenant.agentId ?? "default";
12929
+ const existing = await opts.storage.searchSkills(teamId, agentId, name, 1);
12930
+ let version = 1;
12931
+ let skillId = generateId();
12932
+ if (existing.length > 0 && existing[0].name === name) {
12933
+ version = existing[0].version + 1;
12934
+ skillId = existing[0].id;
12935
+ }
12936
+ const now = Date.now();
12937
+ await opts.storage.putSkill({
12938
+ id: skillId,
12939
+ teamId,
12940
+ agentId,
12941
+ name,
12942
+ description: args.description,
12943
+ content,
12944
+ version,
12945
+ createdAt: existing.length > 0 ? existing[0].createdAt : now,
12946
+ updatedAt: now,
12947
+ triggerConditions: args.trigger_conditions,
12948
+ steps: args.steps,
12949
+ validationRules: args.validation_rules,
12950
+ sourceCaptureIds: args.source_capture_ids,
12951
+ archived: false
12952
+ });
12953
+ return {
12954
+ content: [
12955
+ {
12956
+ type: "text",
12957
+ text: `Skill "${name}" created (v${version}, id: ${skillId}). It will be injected into recall when trigger conditions match.`
12958
+ }
12959
+ ]
12960
+ };
12961
+ }
12962
+ async function handleSkillArchive(args, opts) {
12963
+ const id = args.id;
12964
+ if (!id) {
12965
+ return {
12966
+ content: [{ type: "text", text: "Error: id is required." }]
12967
+ };
12968
+ }
12969
+ const skill = await opts.storage.getSkill(id);
12970
+ if (!skill) {
12971
+ return {
12972
+ content: [{ type: "text", text: `Error: Skill "${id}" not found.` }]
12973
+ };
12974
+ }
12975
+ await opts.storage.putSkill({
12976
+ ...skill,
12977
+ archived: true,
12978
+ updatedAt: Date.now()
12979
+ });
12980
+ return {
12981
+ content: [
12982
+ {
12983
+ type: "text",
12984
+ text: `Skill "${skill.name}" archived. It will now always be injected into recall.`
12985
+ }
12986
+ ]
12987
+ };
12988
+ }
11615
12989
 
11616
12990
  // src/stats.ts
11617
12991
  init_esm_shims();
@@ -11743,9 +13117,9 @@ Knowledge assets: ${knowledgeCount.count}`);
11743
13117
 
11744
13118
  // src/token-stats.ts
11745
13119
  init_esm_shims();
11746
- import { existsSync as existsSync13, readFileSync as readFileSync10 } from "fs";
11747
- import { homedir as homedir10 } from "os";
11748
- import { join as join13 } from "path";
13120
+ import { existsSync as existsSync14, readFileSync as readFileSync11 } from "fs";
13121
+ import { homedir as homedir11 } from "os";
13122
+ import { join as join14 } from "path";
11749
13123
  import Database11 from "better-sqlite3";
11750
13124
  import { encode } from "gpt-tokenizer";
11751
13125
  function countTokens(text) {
@@ -11753,7 +13127,7 @@ function countTokens(text) {
11753
13127
  return encode(text).length;
11754
13128
  }
11755
13129
  function tokenStats(dbPath) {
11756
- const logPath = process.env.REMEM_HOOK_LOG_PATH ?? join13(homedir10(), ".local", "share", "remem-mcp", "session.log");
13130
+ const logPath = process.env.REMEM_HOOK_LOG_PATH ?? join14(homedir11(), ".local", "share", "remem-mcp", "session.log");
11757
13131
  const db = new Database11(dbPath, { readonly: true });
11758
13132
  const totalCaptures = db.prepare("SELECT COUNT(*) as count FROM captures").get();
11759
13133
  const captures = db.prepare("SELECT id, type, content, tags, created_at FROM captures ORDER BY created_at ASC").all();
@@ -11768,8 +13142,8 @@ function tokenStats(dbPath) {
11768
13142
  let _noMemoryCount = 0;
11769
13143
  let captureCount = 0;
11770
13144
  const recallBlocks = [];
11771
- if (existsSync13(logPath)) {
11772
- const log = readFileSync10(logPath, "utf-8");
13145
+ if (existsSync14(logPath)) {
13146
+ const log = readFileSync11(logPath, "utf-8");
11773
13147
  const lines = log.split("\n");
11774
13148
  let inBlock = false;
11775
13149
  let blockText = [];
@@ -11905,10 +13279,10 @@ function printReactExample(fmt, _countTok) {
11905
13279
  // src/viewer.ts
11906
13280
  init_esm_shims();
11907
13281
  init_engine();
11908
- import { existsSync as existsSync14, readFileSync as readFileSync11 } from "fs";
13282
+ import { existsSync as existsSync15, readFileSync as readFileSync12 } from "fs";
11909
13283
  import { createServer as createHttpServer } from "http";
11910
- import { homedir as homedir11 } from "os";
11911
- import { join as join14 } from "path";
13284
+ import { homedir as homedir12 } from "os";
13285
+ import { join as join15 } from "path";
11912
13286
  import Database12 from "better-sqlite3";
11913
13287
  function startViewer(dbPath, port) {
11914
13288
  const db = new Database12(dbPath);
@@ -12184,7 +13558,7 @@ function startViewer(dbPath, port) {
12184
13558
  }
12185
13559
  if (url.pathname === "/api/token-stats") {
12186
13560
  try {
12187
- const logPath = process.env.REMEM_HOOK_LOG_PATH ?? join14(homedir11(), ".local", "share", "remem-mcp", "session.log");
13561
+ const logPath = process.env.REMEM_HOOK_LOG_PATH ?? join15(homedir12(), ".local", "share", "remem-mcp", "session.log");
12188
13562
  const captures = db.prepare("SELECT content FROM captures ORDER BY created_at ASC").all();
12189
13563
  const sessions = db.prepare("SELECT COUNT(DISTINCT session_key) as count FROM captures").get();
12190
13564
  const totalStored = captures.reduce(
@@ -12194,8 +13568,8 @@ function startViewer(dbPath, port) {
12194
13568
  let recallCount = 0;
12195
13569
  let totalInjected = 0;
12196
13570
  let captureCount = 0;
12197
- if (existsSync14(logPath)) {
12198
- const log = readFileSync11(logPath, "utf-8");
13571
+ if (existsSync15(logPath)) {
13572
+ const log = readFileSync12(logPath, "utf-8");
12199
13573
  const lines = log.split("\n");
12200
13574
  let inBlock = false;
12201
13575
  let blockChars = 0;
@@ -13629,7 +15003,7 @@ function renderPage() {
13629
15003
 
13630
15004
  // src/index.ts
13631
15005
  function defaultDbPath3() {
13632
- return process.env.REMEM_DB_PATH ?? join15(homedir12(), ".local", "share", "remem-mcp", "memory.db");
15006
+ return process.env.REMEM_DB_PATH ?? join16(homedir13(), ".local", "share", "remem-mcp", "memory.db");
13633
15007
  }
13634
15008
  function openDbWithSchema(dbPath) {
13635
15009
  const db = new Database13(dbPath);
@@ -13640,13 +15014,13 @@ function openDbWithSchema(dbPath) {
13640
15014
  if (!hasSymbols) {
13641
15015
  const distDir = dirname10(fileURLToPath4(import.meta.url));
13642
15016
  const candidates = [
13643
- join15(distDir, "storage", "schema.sql"),
13644
- join15(distDir, "schema.sql"),
13645
- join15(process.cwd(), "src", "storage", "schema.sql")
15017
+ join16(distDir, "storage", "schema.sql"),
15018
+ join16(distDir, "schema.sql"),
15019
+ join16(process.cwd(), "src", "storage", "schema.sql")
13646
15020
  ];
13647
15021
  for (const p of candidates) {
13648
15022
  try {
13649
- db.exec(readFileSync12(p, "utf-8"));
15023
+ db.exec(readFileSync13(p, "utf-8"));
13650
15024
  break;
13651
15025
  } catch {
13652
15026
  }
@@ -13671,25 +15045,25 @@ function parseFlags(argv) {
13671
15045
  }
13672
15046
  function detectPackageManager() {
13673
15047
  const cwd = process.cwd();
13674
- if (existsSync15(join15(cwd, "pnpm-lock.yaml"))) return "pnpm";
13675
- if (existsSync15(join15(cwd, "bun.lockb")) || existsSync15(join15(cwd, "bun.lock"))) return "bun";
13676
- if (existsSync15(join15(cwd, "yarn.lock"))) return "yarn";
13677
- if (existsSync15(join15(cwd, "package-lock.json"))) return "npm";
13678
- if (existsSync15(join15(cwd, "Cargo.toml"))) return "cargo";
13679
- if (existsSync15(join15(cwd, "go.mod"))) return "go";
13680
- if (existsSync15(join15(cwd, "pom.xml")) || existsSync15(join15(cwd, "build.gradle"))) return "maven";
13681
- if (existsSync15(join15(cwd, "build.gradle.kts"))) return "gradle";
13682
- if (existsSync15(join15(cwd, "Gemfile"))) return "bundle";
13683
- if (existsSync15(join15(cwd, "requirements.txt")) || existsSync15(join15(cwd, "pyproject.toml")))
15048
+ if (existsSync16(join16(cwd, "pnpm-lock.yaml"))) return "pnpm";
15049
+ if (existsSync16(join16(cwd, "bun.lockb")) || existsSync16(join16(cwd, "bun.lock"))) return "bun";
15050
+ if (existsSync16(join16(cwd, "yarn.lock"))) return "yarn";
15051
+ if (existsSync16(join16(cwd, "package-lock.json"))) return "npm";
15052
+ if (existsSync16(join16(cwd, "Cargo.toml"))) return "cargo";
15053
+ if (existsSync16(join16(cwd, "go.mod"))) return "go";
15054
+ if (existsSync16(join16(cwd, "pom.xml")) || existsSync16(join16(cwd, "build.gradle"))) return "maven";
15055
+ if (existsSync16(join16(cwd, "build.gradle.kts"))) return "gradle";
15056
+ if (existsSync16(join16(cwd, "Gemfile"))) return "bundle";
15057
+ if (existsSync16(join16(cwd, "requirements.txt")) || existsSync16(join16(cwd, "pyproject.toml")))
13684
15058
  return "pip";
13685
15059
  return null;
13686
15060
  }
13687
15061
  function detectPackageScripts() {
13688
15062
  const cwd = process.cwd();
13689
- const pkgPath = join15(cwd, "package.json");
13690
- if (!existsSync15(pkgPath)) return {};
15063
+ const pkgPath = join16(cwd, "package.json");
15064
+ if (!existsSync16(pkgPath)) return {};
13691
15065
  try {
13692
- const pkg = JSON.parse(readFileSync12(pkgPath, "utf-8"));
15066
+ const pkg = JSON.parse(readFileSync13(pkgPath, "utf-8"));
13693
15067
  const scripts = pkg.scripts ?? {};
13694
15068
  const important = {};
13695
15069
  for (const key of ["build", "test", "lint", "format", "dev", "start", "typecheck", "check"]) {
@@ -13702,14 +15076,14 @@ function detectPackageScripts() {
13702
15076
  }
13703
15077
  function detectFramework() {
13704
15078
  const cwd = process.cwd();
13705
- const pkgPath = join15(cwd, "package.json");
13706
- if (!existsSync15(pkgPath)) {
15079
+ const pkgPath = join16(cwd, "package.json");
15080
+ if (!existsSync16(pkgPath)) {
13707
15081
  const fw = detectNonJsFramework(cwd);
13708
15082
  if (fw) return fw;
13709
15083
  return null;
13710
15084
  }
13711
15085
  try {
13712
- const pkg = JSON.parse(readFileSync12(pkgPath, "utf-8"));
15086
+ const pkg = JSON.parse(readFileSync13(pkgPath, "utf-8"));
13713
15087
  const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
13714
15088
  if (deps.next) return "Next.js";
13715
15089
  if (deps["react-scripts"]) return "Create React App";
@@ -13731,10 +15105,10 @@ function detectFramework() {
13731
15105
  }
13732
15106
  }
13733
15107
  function detectNonJsFramework(cwd) {
13734
- const pyPath = join15(cwd, "pyproject.toml");
13735
- if (existsSync15(pyPath)) {
15108
+ const pyPath = join16(cwd, "pyproject.toml");
15109
+ if (existsSync16(pyPath)) {
13736
15110
  try {
13737
- const content = readFileSync12(pyPath, "utf-8");
15111
+ const content = readFileSync13(pyPath, "utf-8");
13738
15112
  if (/\bdjango\b/i.test(content)) return "Django";
13739
15113
  if (/\bfastapi\b/i.test(content)) return "FastAPI";
13740
15114
  if (/\bflask\b/i.test(content)) return "Flask";
@@ -13750,20 +15124,20 @@ function detectNonJsFramework(cwd) {
13750
15124
  } catch {
13751
15125
  }
13752
15126
  }
13753
- if (existsSync15(join15(cwd, "requirements.txt"))) {
15127
+ if (existsSync16(join16(cwd, "requirements.txt"))) {
13754
15128
  try {
13755
- const content = readFileSync12(join15(cwd, "requirements.txt"), "utf-8");
15129
+ const content = readFileSync13(join16(cwd, "requirements.txt"), "utf-8");
13756
15130
  if (/^\s*django\b/im.test(content)) return "Django";
13757
15131
  if (/^\s*fastapi\b/im.test(content)) return "FastAPI";
13758
15132
  if (/^\s*flask\b/im.test(content)) return "Flask";
13759
15133
  } catch {
13760
15134
  }
13761
15135
  }
13762
- if (existsSync15(join15(cwd, "manage.py")) && existsSync15(join15(cwd, "wsgi.py"))) return "Django";
13763
- const cargoPath = join15(cwd, "Cargo.toml");
13764
- if (existsSync15(cargoPath)) {
15136
+ if (existsSync16(join16(cwd, "manage.py")) && existsSync16(join16(cwd, "wsgi.py"))) return "Django";
15137
+ const cargoPath = join16(cwd, "Cargo.toml");
15138
+ if (existsSync16(cargoPath)) {
13765
15139
  try {
13766
- const content = readFileSync12(cargoPath, "utf-8");
15140
+ const content = readFileSync13(cargoPath, "utf-8");
13767
15141
  if (/\baxum\b/i.test(content)) return "Axum";
13768
15142
  if (/\bactix-web\b/i.test(content)) return "Actix Web";
13769
15143
  if (/\bwarp\b/i.test(content)) return "Warp";
@@ -13778,10 +15152,10 @@ function detectNonJsFramework(cwd) {
13778
15152
  } catch {
13779
15153
  }
13780
15154
  }
13781
- const goModPath = join15(cwd, "go.mod");
13782
- if (existsSync15(goModPath)) {
15155
+ const goModPath = join16(cwd, "go.mod");
15156
+ if (existsSync16(goModPath)) {
13783
15157
  try {
13784
- const content = readFileSync12(goModPath, "utf-8");
15158
+ const content = readFileSync13(goModPath, "utf-8");
13785
15159
  if (/\bgin-gin\b/i.test(content)) return "Gin";
13786
15160
  if (/\becho\b/i.test(content)) return "Echo";
13787
15161
  if (/\bfiber\b/i.test(content)) return "Fiber";
@@ -13792,10 +15166,10 @@ function detectNonJsFramework(cwd) {
13792
15166
  } catch {
13793
15167
  }
13794
15168
  }
13795
- const gemfilePath = join15(cwd, "Gemfile");
13796
- if (existsSync15(gemfilePath)) {
15169
+ const gemfilePath = join16(cwd, "Gemfile");
15170
+ if (existsSync16(gemfilePath)) {
13797
15171
  try {
13798
- const content = readFileSync12(gemfilePath, "utf-8");
15172
+ const content = readFileSync13(gemfilePath, "utf-8");
13799
15173
  if (/\brails\b/i.test(content)) return "Rails";
13800
15174
  if (/\bsinatra\b/i.test(content)) return "Sinatra";
13801
15175
  if (/\bhanami\b/i.test(content)) return "Hanami";
@@ -13805,9 +15179,9 @@ function detectNonJsFramework(cwd) {
13805
15179
  } catch {
13806
15180
  }
13807
15181
  }
13808
- if (existsSync15(join15(cwd, "pom.xml"))) {
15182
+ if (existsSync16(join16(cwd, "pom.xml"))) {
13809
15183
  try {
13810
- const content = readFileSync12(join15(cwd, "pom.xml"), "utf-8");
15184
+ const content = readFileSync13(join16(cwd, "pom.xml"), "utf-8");
13811
15185
  if (/\bspring-boot\b/i.test(content)) return "Spring Boot";
13812
15186
  if (/\bspring-webflux\b/i.test(content)) return "Spring WebFlux";
13813
15187
  if (/\bquarkus\b/i.test(content)) return "Quarkus";
@@ -13820,10 +15194,10 @@ function detectNonJsFramework(cwd) {
13820
15194
  } catch {
13821
15195
  }
13822
15196
  }
13823
- if (existsSync15(join15(cwd, "build.gradle")) || existsSync15(join15(cwd, "build.gradle.kts"))) {
15197
+ if (existsSync16(join16(cwd, "build.gradle")) || existsSync16(join16(cwd, "build.gradle.kts"))) {
13824
15198
  try {
13825
- const content = readFileSync12(
13826
- existsSync15(join15(cwd, "build.gradle.kts")) ? join15(cwd, "build.gradle.kts") : join15(cwd, "build.gradle"),
15199
+ const content = readFileSync13(
15200
+ existsSync16(join16(cwd, "build.gradle.kts")) ? join16(cwd, "build.gradle.kts") : join16(cwd, "build.gradle"),
13827
15201
  "utf-8"
13828
15202
  );
13829
15203
  if (/\bspring-boot\b/i.test(content)) return "Spring Boot";
@@ -13834,9 +15208,9 @@ function detectNonJsFramework(cwd) {
13834
15208
  }
13835
15209
  }
13836
15210
  try {
13837
- const dotnetFiles = readdirSync(cwd).filter((f) => f.endsWith(".csproj") || f.endsWith(".sln"));
15211
+ const dotnetFiles = readdirSync2(cwd).filter((f) => f.endsWith(".csproj") || f.endsWith(".sln"));
13838
15212
  if (dotnetFiles.length > 0) {
13839
- const content = readFileSync12(join15(cwd, dotnetFiles[0]), "utf-8");
15213
+ const content = readFileSync13(join16(cwd, dotnetFiles[0]), "utf-8");
13840
15214
  if (/\bMicrosoft\.NET\.Sdk\.Web\b/i.test(content)) return "ASP.NET Core";
13841
15215
  if (/\bMicrosoft\.NET\.Sdk\b/i.test(content)) return ".NET";
13842
15216
  return ".NET";
@@ -13847,10 +15221,10 @@ function detectNonJsFramework(cwd) {
13847
15221
  }
13848
15222
  function detectLintTool() {
13849
15223
  const cwd = process.cwd();
13850
- const pkgPath = join15(cwd, "package.json");
13851
- if (existsSync15(pkgPath)) {
15224
+ const pkgPath = join16(cwd, "package.json");
15225
+ if (existsSync16(pkgPath)) {
13852
15226
  try {
13853
- const pkg = JSON.parse(readFileSync12(pkgPath, "utf-8"));
15227
+ const pkg = JSON.parse(readFileSync13(pkgPath, "utf-8"));
13854
15228
  const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
13855
15229
  if (deps.biome) return "Biome";
13856
15230
  if (deps.eslint) return "ESLint";
@@ -13860,23 +15234,23 @@ function detectLintTool() {
13860
15234
  } catch {
13861
15235
  }
13862
15236
  }
13863
- if (existsSync15(join15(cwd, "biome.json")) || existsSync15(join15(cwd, "biome.jsonc"))) return "Biome";
13864
- if (existsSync15(join15(cwd, ".eslintrc")) || existsSync15(join15(cwd, ".eslintrc.js")) || existsSync15(join15(cwd, ".eslintrc.json")) || existsSync15(join15(cwd, "eslint.config.js")) || existsSync15(join15(cwd, "eslint.config.mjs")))
15237
+ if (existsSync16(join16(cwd, "biome.json")) || existsSync16(join16(cwd, "biome.jsonc"))) return "Biome";
15238
+ if (existsSync16(join16(cwd, ".eslintrc")) || existsSync16(join16(cwd, ".eslintrc.js")) || existsSync16(join16(cwd, ".eslintrc.json")) || existsSync16(join16(cwd, "eslint.config.js")) || existsSync16(join16(cwd, "eslint.config.mjs")))
13865
15239
  return "ESLint";
13866
- if (existsSync15(join15(cwd, ".prettierrc")) || existsSync15(join15(cwd, ".prettierrc.json")))
15240
+ if (existsSync16(join16(cwd, ".prettierrc")) || existsSync16(join16(cwd, ".prettierrc.json")))
13867
15241
  return "Prettier";
13868
- if (existsSync15(join15(cwd, "rustfmt.toml"))) return "rustfmt";
13869
- if (existsSync15(join15(cwd, ".rustfmt.toml"))) return "rustfmt";
13870
- if (existsSync15(join15(cwd, ".gofmt"))) return "gofmt";
13871
- if (existsSync15(join15(cwd, ".golangci.yml")) || existsSync15(join15(cwd, ".golangci.yaml")))
15242
+ if (existsSync16(join16(cwd, "rustfmt.toml"))) return "rustfmt";
15243
+ if (existsSync16(join16(cwd, ".rustfmt.toml"))) return "rustfmt";
15244
+ if (existsSync16(join16(cwd, ".gofmt"))) return "gofmt";
15245
+ if (existsSync16(join16(cwd, ".golangci.yml")) || existsSync16(join16(cwd, ".golangci.yaml")))
13872
15246
  return "golangci-lint";
13873
- if (existsSync15(join15(cwd, ".ruff.toml")) || existsSync15(join15(cwd, "ruff.toml"))) return "Ruff";
13874
- if (existsSync15(join15(cwd, ".flake8")) || existsSync15(join15(cwd, "setup.cfg")) || existsSync15(join15(cwd, "tox.ini")))
15247
+ if (existsSync16(join16(cwd, ".ruff.toml")) || existsSync16(join16(cwd, "ruff.toml"))) return "Ruff";
15248
+ if (existsSync16(join16(cwd, ".flake8")) || existsSync16(join16(cwd, "setup.cfg")) || existsSync16(join16(cwd, "tox.ini")))
13875
15249
  return "Flake8";
13876
- if (existsSync15(join15(cwd, ".pylintrc")) || existsSync15(join15(cwd, "pyproject.toml"))) {
15250
+ if (existsSync16(join16(cwd, ".pylintrc")) || existsSync16(join16(cwd, "pyproject.toml"))) {
13877
15251
  try {
13878
- if (existsSync15(join15(cwd, "pyproject.toml"))) {
13879
- const content = readFileSync12(join15(cwd, "pyproject.toml"), "utf-8");
15252
+ if (existsSync16(join16(cwd, "pyproject.toml"))) {
15253
+ const content = readFileSync13(join16(cwd, "pyproject.toml"), "utf-8");
13880
15254
  if (/\bruff\b/i.test(content)) return "Ruff";
13881
15255
  if (/\bblack\b/i.test(content)) return "Black";
13882
15256
  if (/\bflake8\b/i.test(content)) return "Flake8";
@@ -13886,15 +15260,15 @@ function detectLintTool() {
13886
15260
  } catch {
13887
15261
  }
13888
15262
  }
13889
- if (existsSync15(join15(cwd, ".black")) || existsSync15(join15(cwd, "black-config.toml"))) return "Black";
13890
- if (existsSync15(join15(cwd, ".rubocop.yml")) || existsSync15(join15(cwd, ".rubocop.yaml")))
15263
+ if (existsSync16(join16(cwd, ".black")) || existsSync16(join16(cwd, "black-config.toml"))) return "Black";
15264
+ if (existsSync16(join16(cwd, ".rubocop.yml")) || existsSync16(join16(cwd, ".rubocop.yaml")))
13891
15265
  return "RuboCop";
13892
- if (existsSync15(join15(cwd, ".standard.yml")) || existsSync15(join15(cwd, ".standard.yaml")))
15266
+ if (existsSync16(join16(cwd, ".standard.yml")) || existsSync16(join16(cwd, ".standard.yaml")))
13893
15267
  return "Standard";
13894
- if (existsSync15(join15(cwd, ".checkstyle"))) return "Checkstyle";
13895
- if (existsSync15(join15(cwd, "spotbugs-exclude.xml"))) return "SpotBugs";
15268
+ if (existsSync16(join16(cwd, ".checkstyle"))) return "Checkstyle";
15269
+ if (existsSync16(join16(cwd, "spotbugs-exclude.xml"))) return "SpotBugs";
13896
15270
  try {
13897
- const dotnetFiles = readdirSync(cwd).filter((f) => f.endsWith(".editorconfig"));
15271
+ const dotnetFiles = readdirSync2(cwd).filter((f) => f.endsWith(".editorconfig"));
13898
15272
  if (dotnetFiles.length > 0) return "EditorConfig";
13899
15273
  } catch {
13900
15274
  }
@@ -13903,10 +15277,10 @@ function detectLintTool() {
13903
15277
  function detectTestCommand(pkgManager) {
13904
15278
  const cwd = process.cwd();
13905
15279
  const pm = pkgManager ?? "npm";
13906
- const pkgPath = join15(cwd, "package.json");
13907
- if (existsSync15(pkgPath)) {
15280
+ const pkgPath = join16(cwd, "package.json");
15281
+ if (existsSync16(pkgPath)) {
13908
15282
  try {
13909
- const pkg = JSON.parse(readFileSync12(pkgPath, "utf-8"));
15283
+ const pkg = JSON.parse(readFileSync13(pkgPath, "utf-8"));
13910
15284
  const scripts = pkg.scripts ?? {};
13911
15285
  if (scripts.test && scripts.test !== 'echo "Error: no test specified" && exit 1') {
13912
15286
  return `${pm} run test`;
@@ -13919,32 +15293,32 @@ function detectTestCommand(pkgManager) {
13919
15293
  } catch {
13920
15294
  }
13921
15295
  }
13922
- if (existsSync15(join15(cwd, "Cargo.toml"))) return "cargo test";
13923
- if (existsSync15(join15(cwd, "go.mod"))) return "go test ./...";
13924
- if (existsSync15(join15(cwd, "pytest.ini")) || existsSync15(join15(cwd, "conftest.py"))) return "pytest";
13925
- if (existsSync15(join15(cwd, "pyproject.toml"))) {
15296
+ if (existsSync16(join16(cwd, "Cargo.toml"))) return "cargo test";
15297
+ if (existsSync16(join16(cwd, "go.mod"))) return "go test ./...";
15298
+ if (existsSync16(join16(cwd, "pytest.ini")) || existsSync16(join16(cwd, "conftest.py"))) return "pytest";
15299
+ if (existsSync16(join16(cwd, "pyproject.toml"))) {
13926
15300
  try {
13927
- const content = readFileSync12(join15(cwd, "pyproject.toml"), "utf-8");
15301
+ const content = readFileSync13(join16(cwd, "pyproject.toml"), "utf-8");
13928
15302
  if (/\bpytest\b/i.test(content)) return "pytest";
13929
15303
  if (/\bunittest\b/i.test(content)) return "python -m unittest";
13930
15304
  } catch {
13931
15305
  }
13932
15306
  }
13933
- if (existsSync15(join15(cwd, "tox.ini"))) return "tox";
13934
- if (existsSync15(join15(cwd, "Gemfile"))) {
15307
+ if (existsSync16(join16(cwd, "tox.ini"))) return "tox";
15308
+ if (existsSync16(join16(cwd, "Gemfile"))) {
13935
15309
  try {
13936
- const content = readFileSync12(join15(cwd, "Gemfile"), "utf-8");
15310
+ const content = readFileSync13(join16(cwd, "Gemfile"), "utf-8");
13937
15311
  if (/\brspec\b/i.test(content)) return "bundle exec rspec";
13938
15312
  if (/\bminitest\b/i.test(content)) return "bundle exec rake test";
13939
15313
  } catch {
13940
15314
  }
13941
15315
  return "bundle exec rspec";
13942
15316
  }
13943
- if (existsSync15(join15(cwd, "pom.xml"))) return "mvn test";
13944
- if (existsSync15(join15(cwd, "build.gradle")) || existsSync15(join15(cwd, "build.gradle.kts")))
15317
+ if (existsSync16(join16(cwd, "pom.xml"))) return "mvn test";
15318
+ if (existsSync16(join16(cwd, "build.gradle")) || existsSync16(join16(cwd, "build.gradle.kts")))
13945
15319
  return "gradle test";
13946
15320
  try {
13947
- const dotnetFiles = readdirSync(cwd).filter(
15321
+ const dotnetFiles = readdirSync2(cwd).filter(
13948
15322
  (f) => f.endsWith(".csproj") || f.endsWith(".fsproj")
13949
15323
  );
13950
15324
  if (dotnetFiles.length > 0) return "dotnet test";
@@ -13959,6 +15333,10 @@ async function main() {
13959
15333
  return;
13960
15334
  }
13961
15335
  if (arg === "init") {
15336
+ try {
15337
+ new SQLiteBackend(defaultDbPath3());
15338
+ } catch {
15339
+ }
13962
15340
  console.log(`remem-mcp init
13963
15341
 
13964
15342
  This command tells your agent to activate long-term memory for this session.
@@ -14051,7 +15429,7 @@ After that, the agent follows the rules automatically.`);
14051
15429
  if (captured > 0) {
14052
15430
  console.log(` Captured ${captured} project basics.`);
14053
15431
  } else {
14054
- const hasProjectFiles = existsSync15(join15(process.cwd(), "package.json")) || existsSync15(join15(process.cwd(), "Cargo.toml")) || existsSync15(join15(process.cwd(), "go.mod")) || existsSync15(join15(process.cwd(), "pyproject.toml"));
15432
+ const hasProjectFiles = existsSync16(join16(process.cwd(), "package.json")) || existsSync16(join16(process.cwd(), "Cargo.toml")) || existsSync16(join16(process.cwd(), "go.mod")) || existsSync16(join16(process.cwd(), "pyproject.toml"));
14055
15433
  if (hasProjectFiles) {
14056
15434
  console.log(" Project basics already captured (no changes since last setup).");
14057
15435
  } else {
@@ -14086,7 +15464,13 @@ After that, the agent follows the rules automatically.`);
14086
15464
  }
14087
15465
  if (arg === "recent" || arg === "list") {
14088
15466
  const limit = parseInt(process.argv[3] ?? "20", 10);
14089
- const db = new Database13(defaultDbPath3(), { readonly: true });
15467
+ let db;
15468
+ try {
15469
+ db = new Database13(defaultDbPath3(), { readonly: true });
15470
+ } catch {
15471
+ console.log("No captures found.");
15472
+ return;
15473
+ }
14090
15474
  const rows = db.prepare(
14091
15475
  `SELECT id, type, content, tags, created_at FROM captures WHERE deleted_at IS NULL ORDER BY created_at DESC LIMIT ?`
14092
15476
  ).all(limit);
@@ -14176,7 +15560,7 @@ ${sorted.length} unique tag(s).`);
14176
15560
  ).get();
14177
15561
  const oldest = db.prepare("SELECT MIN(created_at) as ts FROM captures WHERE deleted_at IS NULL").get();
14178
15562
  const newest = db.prepare("SELECT MAX(created_at) as ts FROM captures WHERE deleted_at IS NULL").get();
14179
- const dbSize = statSync2(defaultDbPath3()).size;
15563
+ const dbSize = statSync3(defaultDbPath3()).size;
14180
15564
  console.log("remem-mcp stats\n");
14181
15565
  console.log(` Total captures: ${total}`);
14182
15566
  console.log(
@@ -14367,13 +15751,13 @@ ${sorted.length} unique tag(s).`);
14367
15751
  return;
14368
15752
  }
14369
15753
  if (arg === "viewer") {
14370
- const port = Number(process.argv[4] ?? process.env.REMEM_VIEWER_PORT ?? 7331);
15754
+ const port = Number(process.argv[3] ?? process.env.REMEM_VIEWER_PORT ?? 7331);
14371
15755
  startViewer(defaultDbPath3(), port);
14372
15756
  return;
14373
15757
  }
14374
15758
  if (arg === "backup") {
14375
15759
  const dbPath = defaultDbPath3();
14376
- const auditPath = process.env.REMEM_AUDIT_LOG_PATH ?? join15(dirname10(dbPath), "audit.jsonl");
15760
+ const auditPath = process.env.REMEM_AUDIT_LOG_PATH ?? join16(dirname10(dbPath), "audit.jsonl");
14377
15761
  const outputDir = process.argv[3] ?? "-";
14378
15762
  backup(dbPath, auditPath, outputDir);
14379
15763
  return;
@@ -14394,7 +15778,7 @@ ${sorted.length} unique tag(s).`);
14394
15778
  }
14395
15779
  return;
14396
15780
  }
14397
- if (arg === "hook-recall") {
15781
+ if (arg === "hook-recall" || arg === "hook-session-start") {
14398
15782
  hookRecall(defaultDbPath3());
14399
15783
  return;
14400
15784
  }
@@ -14437,6 +15821,45 @@ ${sorted.length} unique tag(s).`);
14437
15821
  hookPostCompaction(defaultDbPath3());
14438
15822
  return;
14439
15823
  }
15824
+ if (arg === "proxy") {
15825
+ const { loadProxyConfig, startProxyServer } = await import("./server-5PWQTRGZ.js");
15826
+ const proxyConfig = loadProxyConfig();
15827
+ if (!proxyConfig.dbPath) {
15828
+ proxyConfig.dbPath = defaultDbPath3();
15829
+ }
15830
+ await startProxyServer(proxyConfig);
15831
+ return;
15832
+ }
15833
+ if (arg === "skill-extract") {
15834
+ const dbPath = defaultDbPath3();
15835
+ const db = new Database13(dbPath);
15836
+ sqliteVec4.load(db);
15837
+ const storage2 = new SQLiteBackend(dbPath);
15838
+ const pipeline2 = new SkillExtractionPipeline();
15839
+ const taskCaptures = db.prepare(
15840
+ "SELECT id, content, type, tags, team_id, agent_id FROM captures WHERE type = 'task' AND deleted_at IS NULL"
15841
+ ).all();
15842
+ console.log(`Found ${taskCaptures.length} task capture(s) to extract skills from.`);
15843
+ let extracted = 0;
15844
+ for (const row of taskCaptures) {
15845
+ const tags = row.tags ? JSON.parse(row.tags) : [];
15846
+ const input = {
15847
+ id: row.id,
15848
+ content: row.content,
15849
+ type: row.type,
15850
+ tags,
15851
+ sessionKey: "default",
15852
+ teamId: row.team_id ?? "default",
15853
+ agentId: row.agent_id ?? "default"
15854
+ };
15855
+ const ctx = { storage: storage2, embedder: new LocalEmbedder(), sessionKey: "default" };
15856
+ const result = await pipeline2.process(input, ctx);
15857
+ if (Object.keys(result).length > 0) extracted++;
15858
+ }
15859
+ console.log(`Extracted ${extracted} skill(s) from ${taskCaptures.length} task capture(s).`);
15860
+ db.close();
15861
+ return;
15862
+ }
14440
15863
  if (arg === "index") {
14441
15864
  const flags = parseFlags(process.argv.slice(3));
14442
15865
  const path = flags.path ?? flags.p ?? process.cwd();
@@ -14673,8 +16096,8 @@ ${sorted.length} unique tag(s).`);
14673
16096
  }
14674
16097
  if (arg === "version" || arg === "--version" || arg === "-v") {
14675
16098
  try {
14676
- const pkgPath = join15(dirname10(fileURLToPath4(import.meta.url)), "..", "package.json");
14677
- const pkg = JSON.parse(readFileSync12(pkgPath, "utf-8"));
16099
+ const pkgPath = join16(dirname10(fileURLToPath4(import.meta.url)), "..", "package.json");
16100
+ const pkg = JSON.parse(readFileSync13(pkgPath, "utf-8"));
14678
16101
  console.log(`remem-mcp v${pkg.version}`);
14679
16102
  } catch {
14680
16103
  console.log("remem-mcp (version unknown)");
@@ -14845,6 +16268,17 @@ To install the skill (Devin CLI only):
14845
16268
  });
14846
16269
  pipeline = new AtomPipeline();
14847
16270
  pipeline._llmClient = llmClient;
16271
+ } else if (config.pipeline === "mermaid" && config.llm) {
16272
+ const llmClient = new OpenAILLMClient({
16273
+ apiKey: config.llm.apiKey,
16274
+ baseUrl: config.llm.baseUrl,
16275
+ model: config.llm.model
16276
+ });
16277
+ pipeline = new LLMMermaidPipeline(llmClient);
16278
+ } else if (config.pipeline === "mermaid") {
16279
+ pipeline = new MermaidPipeline();
16280
+ } else if (config.pipeline === "skill") {
16281
+ pipeline = new SkillExtractionPipeline();
14848
16282
  } else {
14849
16283
  pipeline = new RuleBasedAtomPipeline();
14850
16284
  }
@@ -14902,8 +16336,8 @@ async function verifyMemory(dbPath, query) {
14902
16336
  storage.close();
14903
16337
  return;
14904
16338
  }
14905
- const { statSync: statSync3, readdirSync: readdirSync2 } = await import("fs");
14906
- const { join: join16, extname } = await import("path");
16339
+ const { statSync: statSync4, readdirSync: readdirSync3 } = await import("fs");
16340
+ const { join: join17, extname } = await import("path");
14907
16341
  const codeExts = [
14908
16342
  ".ts",
14909
16343
  ".tsx",
@@ -14925,16 +16359,16 @@ async function verifyMemory(dbPath, query) {
14925
16359
  if (depth > 2 || fileCount > 50) return;
14926
16360
  let entries;
14927
16361
  try {
14928
- entries = readdirSync2(dir);
16362
+ entries = readdirSync3(dir);
14929
16363
  } catch {
14930
16364
  return;
14931
16365
  }
14932
16366
  for (const name of entries) {
14933
16367
  if (name.startsWith(".") || name === "node_modules" || name === "dist" || name === "build")
14934
16368
  continue;
14935
- const full = join16(dir, name);
16369
+ const full = join17(dir, name);
14936
16370
  try {
14937
- const stat = statSync3(full);
16371
+ const stat = statSync4(full);
14938
16372
  if (stat.isDirectory()) {
14939
16373
  scanDir(full, depth + 1);
14940
16374
  } else if (codeExts.includes(extname(name))) {