remem-mcp 0.6.7 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +170 -32
- package/dist/{chunk-XK4Y53J2.js → chunk-ONQHLUGM.js} +1 -8
- package/dist/{chunk-XK4Y53J2.js.map → chunk-ONQHLUGM.js.map} +1 -1
- package/dist/{chunk-X4ECGLYH.js → chunk-OYUNKUZK.js} +111 -817
- package/dist/chunk-OYUNKUZK.js.map +1 -0
- package/dist/chunk-TGLOMFIE.js +819 -0
- package/dist/chunk-TGLOMFIE.js.map +1 -0
- package/dist/chunk-XJGDYQ34.js +22 -0
- package/dist/chunk-XJGDYQ34.js.map +1 -0
- package/dist/{chunk-FA324IBN.js → chunk-ZXI67YIV.js} +11 -18
- package/dist/chunk-ZXI67YIV.js.map +1 -0
- package/dist/{engine-LVKGZHBY.js → engine-3JUSB3RK.js} +4 -3
- package/dist/index.js +1467 -321
- package/dist/index.js.map +1 -1
- package/dist/{resolver-AVK7XKX7.js → resolver-YIFWRNMS.js} +7 -9
- package/dist/resolver-YIFWRNMS.js.map +1 -0
- package/dist/sdk.js +8 -5
- package/dist/server-SUKPVQWL.js +517 -0
- package/dist/server-SUKPVQWL.js.map +1 -0
- package/dist/storage/schema.sql +49 -1
- package/package.json +1 -1
- package/skills/remem-mcp/SKILL.md +33 -0
- package/dist/chunk-FA324IBN.js.map +0 -1
- package/dist/chunk-X4ECGLYH.js.map +0 -1
- package/dist/resolver-AVK7XKX7.js.map +0 -1
- /package/dist/{engine-LVKGZHBY.js.map → engine-3JUSB3RK.js.map} +0 -0
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
generateId,
|
|
3
|
-
init_engine,
|
|
4
3
|
init_ulid
|
|
5
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-XJGDYQ34.js";
|
|
6
5
|
import {
|
|
7
6
|
__esm,
|
|
8
7
|
__export,
|
|
9
8
|
init_esm_shims
|
|
10
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-ONQHLUGM.js";
|
|
11
10
|
|
|
12
11
|
// src/embedding/local.ts
|
|
13
12
|
var local_exports = {};
|
|
@@ -50,91 +49,11 @@ var init_local = __esm({
|
|
|
50
49
|
}
|
|
51
50
|
});
|
|
52
51
|
|
|
53
|
-
// src/sdk.ts
|
|
54
|
-
init_esm_shims();
|
|
55
|
-
init_local();
|
|
56
|
-
import { createHash as createHash3 } from "crypto";
|
|
57
|
-
import { homedir } from "os";
|
|
58
|
-
import { join as join3 } from "path";
|
|
59
|
-
|
|
60
|
-
// src/security/redactor.ts
|
|
61
|
-
init_esm_shims();
|
|
62
|
-
var SECRET_PATTERNS = [
|
|
63
|
-
// OpenAI API key
|
|
64
|
-
/sk-[a-zA-Z0-9]{20,}/g,
|
|
65
|
-
// Anthropic API key
|
|
66
|
-
/sk-ant-[a-zA-Z0-9-]+/g,
|
|
67
|
-
// GitHub personal access token
|
|
68
|
-
/ghp_[a-zA-Z0-9]{36}/g,
|
|
69
|
-
// GitHub OAuth token
|
|
70
|
-
/gho_[a-zA-Z0-9]{36}/g,
|
|
71
|
-
// GitHub fine-grained token
|
|
72
|
-
/github_pat_[a-zA-Z0-9_]{82}/g,
|
|
73
|
-
// Slack token
|
|
74
|
-
/xox[baprs]-[a-zA-Z0-9-]+/g,
|
|
75
|
-
// AWS access key ID
|
|
76
|
-
/AKIA[0-9A-Z]{16}/g,
|
|
77
|
-
// AWS secret access key (40 chars, base64-ish)
|
|
78
|
-
/(?<![A-Za-z0-9+/])[A-Za-z0-9+/]{40}(?![A-Za-z0-9+/])/g,
|
|
79
|
-
// PEM private key block
|
|
80
|
-
/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g,
|
|
81
|
-
// Google API key
|
|
82
|
-
/AIza[0-9A-Za-z\-_]{35}/g,
|
|
83
|
-
// Generic bearer token (require 20+ char token to avoid redacting "Bearer with JWT")
|
|
84
|
-
/Bearer\s+[a-zA-Z0-9\-._~+/]{20,}=*/g
|
|
85
|
-
];
|
|
86
|
-
var MIN_LENGTH = 40;
|
|
87
|
-
var ENTROPY_THRESHOLD = 4.5;
|
|
88
|
-
function shannonEntropy(str) {
|
|
89
|
-
const freq = /* @__PURE__ */ new Map();
|
|
90
|
-
for (const ch of str) {
|
|
91
|
-
freq.set(ch, (freq.get(ch) ?? 0) + 1);
|
|
92
|
-
}
|
|
93
|
-
let entropy = 0;
|
|
94
|
-
const len = str.length;
|
|
95
|
-
for (const count of freq.values()) {
|
|
96
|
-
const p = count / len;
|
|
97
|
-
entropy -= p * Math.log2(p);
|
|
98
|
-
}
|
|
99
|
-
return entropy;
|
|
100
|
-
}
|
|
101
|
-
function findHighEntropyStrings(text) {
|
|
102
|
-
const results = [];
|
|
103
|
-
const regex = /[a-zA-Z0-9+/=]{40,}/g;
|
|
104
|
-
let match;
|
|
105
|
-
match = regex.exec(text);
|
|
106
|
-
while (match !== null) {
|
|
107
|
-
const str = match[0];
|
|
108
|
-
if (str.length >= MIN_LENGTH) {
|
|
109
|
-
const entropy = shannonEntropy(str);
|
|
110
|
-
if (entropy >= ENTROPY_THRESHOLD) {
|
|
111
|
-
results.push(str);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
match = regex.exec(text);
|
|
115
|
-
}
|
|
116
|
-
return results;
|
|
117
|
-
}
|
|
118
|
-
function redact(text) {
|
|
119
|
-
let result = text;
|
|
120
|
-
let redacted = false;
|
|
121
|
-
for (const pattern of SECRET_PATTERNS) {
|
|
122
|
-
const before = result;
|
|
123
|
-
result = result.replace(pattern, "[REDACTED]");
|
|
124
|
-
if (result !== before) redacted = true;
|
|
125
|
-
}
|
|
126
|
-
const highEntropy = findHighEntropyStrings(result);
|
|
127
|
-
for (const str of highEntropy) {
|
|
128
|
-
result = result.replace(str, "[REDACTED]");
|
|
129
|
-
redacted = true;
|
|
130
|
-
}
|
|
131
|
-
return { text: result, redacted };
|
|
132
|
-
}
|
|
133
|
-
|
|
134
52
|
// src/storage/sqlite.ts
|
|
135
53
|
init_esm_shims();
|
|
136
54
|
import { createHash } from "crypto";
|
|
137
|
-
import { copyFileSync, existsSync, mkdirSync, readFileSync } from "fs";
|
|
55
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "fs";
|
|
56
|
+
import { homedir } from "os";
|
|
138
57
|
import { dirname, join } from "path";
|
|
139
58
|
import { fileURLToPath } from "url";
|
|
140
59
|
import Database from "better-sqlite3";
|
|
@@ -266,7 +185,7 @@ function stripQueryProperNouns(query) {
|
|
|
266
185
|
if (noProperNouns.length > 0) return noProperNouns.join(" ");
|
|
267
186
|
return filtered.length > 0 ? filtered.join(" ") : tokens.join(" ");
|
|
268
187
|
}
|
|
269
|
-
var CURRENT_SCHEMA_VERSION =
|
|
188
|
+
var CURRENT_SCHEMA_VERSION = 9;
|
|
270
189
|
var EVERGREEN_TAGS = new Set(
|
|
271
190
|
(process.env.REMEM_EVERGREEN_TAGS ?? "evergreen,never-forget").split(",").map((t) => t.trim().toLowerCase()).filter(Boolean)
|
|
272
191
|
);
|
|
@@ -292,7 +211,7 @@ var SQLiteBackend = class {
|
|
|
292
211
|
this.db = new Database(dbPath, { readonly: true });
|
|
293
212
|
}
|
|
294
213
|
try {
|
|
295
|
-
this.db.pragma("busy_timeout =
|
|
214
|
+
this.db.pragma("busy_timeout = 10000");
|
|
296
215
|
} catch {
|
|
297
216
|
}
|
|
298
217
|
if (!readonly) {
|
|
@@ -647,20 +566,28 @@ var SQLiteBackend = class {
|
|
|
647
566
|
try {
|
|
648
567
|
this.db.exec("ALTER TABLE symbols ADD COLUMN module_path TEXT");
|
|
649
568
|
addedAny = true;
|
|
650
|
-
} catch {
|
|
569
|
+
} catch (err) {
|
|
570
|
+
if (!String(err).includes("duplicate column"))
|
|
571
|
+
console.error(`[remem-mcp] migrateV8ToV9: ${err}`);
|
|
651
572
|
}
|
|
652
573
|
try {
|
|
653
574
|
this.db.exec("ALTER TABLE calls ADD COLUMN confidence REAL");
|
|
654
575
|
addedAny = true;
|
|
655
|
-
} catch {
|
|
576
|
+
} catch (err) {
|
|
577
|
+
if (!String(err).includes("duplicate column"))
|
|
578
|
+
console.error(`[remem-mcp] migrateV8ToV9: ${err}`);
|
|
656
579
|
}
|
|
657
580
|
try {
|
|
658
581
|
this.db.exec("ALTER TABLE calls ADD COLUMN call_type TEXT NOT NULL DEFAULT 'direct'");
|
|
659
582
|
addedAny = true;
|
|
660
|
-
} catch {
|
|
583
|
+
} catch (err) {
|
|
584
|
+
if (!String(err).includes("duplicate column"))
|
|
585
|
+
console.error(`[remem-mcp] migrateV8ToV9: ${err}`);
|
|
661
586
|
}
|
|
662
587
|
if (addedAny) {
|
|
663
|
-
console.error(
|
|
588
|
+
console.error(
|
|
589
|
+
"[remem-mcp] Added CodeGraph call resolution columns (module_path, confidence, call_type)"
|
|
590
|
+
);
|
|
664
591
|
}
|
|
665
592
|
console.error("[remem-mcp] Migrated schema v8 \u2192 v9 (CodeGraph call resolution)");
|
|
666
593
|
}
|
|
@@ -832,7 +759,7 @@ var SQLiteBackend = class {
|
|
|
832
759
|
}
|
|
833
760
|
async listAll(limit = 50, offset = 0) {
|
|
834
761
|
const rows = this.db.prepare(
|
|
835
|
-
"SELECT * FROM captures WHERE deleted_at IS NULL AND trust_state != 'rejected' AND superseded_by IS NULL ORDER BY created_at DESC LIMIT ? OFFSET ?"
|
|
762
|
+
"SELECT * FROM captures WHERE deleted_at IS NULL AND trust_state != 'rejected' AND superseded_by IS NULL AND id IS NOT NULL ORDER BY created_at DESC LIMIT ? OFFSET ?"
|
|
836
763
|
).all(limit, offset);
|
|
837
764
|
return rows.map(rowToEntry);
|
|
838
765
|
}
|
|
@@ -1454,9 +1381,9 @@ var SQLiteBackend = class {
|
|
|
1454
1381
|
// ─── Skills ─────────────────────────────────────────────────
|
|
1455
1382
|
async putSkill(entry) {
|
|
1456
1383
|
this.db.prepare(
|
|
1457
|
-
`INSERT INTO skills (id, team_id, agent_id, name, description, content, version, created_at, updated_at)
|
|
1458
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1459
|
-
ON CONFLICT(id) DO UPDATE SET name = excluded.name, description = excluded.description, content = excluded.content, version = excluded.version, updated_at = excluded.updated_at`
|
|
1384
|
+
`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)
|
|
1385
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1386
|
+
ON CONFLICT(id) DO UPDATE SET name = excluded.name, description = excluded.description, content = excluded.content, version = excluded.version, updated_at = excluded.updated_at, trigger_conditions = excluded.trigger_conditions, steps = excluded.steps, validation_rules = excluded.validation_rules, source_capture_ids = excluded.source_capture_ids, archived = excluded.archived`
|
|
1460
1387
|
).run(
|
|
1461
1388
|
entry.id,
|
|
1462
1389
|
entry.teamId,
|
|
@@ -1466,7 +1393,12 @@ var SQLiteBackend = class {
|
|
|
1466
1393
|
entry.content ?? null,
|
|
1467
1394
|
entry.version,
|
|
1468
1395
|
entry.createdAt,
|
|
1469
|
-
entry.updatedAt
|
|
1396
|
+
entry.updatedAt,
|
|
1397
|
+
entry.triggerConditions ? JSON.stringify(entry.triggerConditions) : null,
|
|
1398
|
+
entry.steps ? JSON.stringify(entry.steps) : null,
|
|
1399
|
+
entry.validationRules ? JSON.stringify(entry.validationRules) : null,
|
|
1400
|
+
entry.sourceCaptureIds ? JSON.stringify(entry.sourceCaptureIds) : null,
|
|
1401
|
+
entry.archived ? 1 : 0
|
|
1470
1402
|
);
|
|
1471
1403
|
}
|
|
1472
1404
|
async getSkill(id) {
|
|
@@ -1497,6 +1429,81 @@ var SQLiteBackend = class {
|
|
|
1497
1429
|
close() {
|
|
1498
1430
|
this.db.close();
|
|
1499
1431
|
}
|
|
1432
|
+
// ─── Canvas + Refs (v9: symbolic short-term memory) ───────────────────
|
|
1433
|
+
// These methods implement the StorageBackend interface for F1 (Mermaid canvas).
|
|
1434
|
+
async appendCanvasNode(sessionKey, node, edges, teamId) {
|
|
1435
|
+
const now = Date.now();
|
|
1436
|
+
let canvasRow = this.db.prepare("SELECT id FROM canvases WHERE session_key = ? ORDER BY updated_at DESC LIMIT 1").get(sessionKey);
|
|
1437
|
+
if (!canvasRow) {
|
|
1438
|
+
const canvasId = `01${now.toString(36).toUpperCase()}${Math.random().toString(36).slice(2, 10).toUpperCase()}`;
|
|
1439
|
+
this.db.prepare(
|
|
1440
|
+
"INSERT INTO canvases (id, session_key, mermaid_text, node_count, created_at, updated_at, team_id) VALUES (?, ?, NULL, 0, ?, ?, ?)"
|
|
1441
|
+
).run(canvasId, sessionKey, now, now, teamId ?? null);
|
|
1442
|
+
canvasRow = { id: canvasId };
|
|
1443
|
+
}
|
|
1444
|
+
const maxSeq = this.db.prepare("SELECT MAX(seq) as max_seq FROM canvas_nodes WHERE canvas_id = ?").get(canvasRow.id);
|
|
1445
|
+
const seq = (maxSeq.max_seq ?? 0) + 1;
|
|
1446
|
+
this.db.prepare(
|
|
1447
|
+
"INSERT INTO canvas_nodes (id, canvas_id, node_id, label, seq, created_at) VALUES (?, ?, ?, ?, ?, ?)"
|
|
1448
|
+
).run(`01${now.toString(36).toUpperCase()}${Math.random().toString(36).slice(2, 10).toUpperCase()}`, canvasRow.id, node.id, node.label, seq, now);
|
|
1449
|
+
for (const edge of edges) {
|
|
1450
|
+
this.db.prepare(
|
|
1451
|
+
"INSERT INTO canvas_edges (id, canvas_id, from_node_id, to_node_id, label, created_at) VALUES (?, ?, ?, ?, ?, ?)"
|
|
1452
|
+
).run(`01${now.toString(36).toUpperCase()}${Math.random().toString(36).slice(2, 10).toUpperCase()}`, canvasRow.id, edge.from, edge.to, edge.label ?? null, now);
|
|
1453
|
+
}
|
|
1454
|
+
this.db.prepare("UPDATE canvases SET node_count = node_count + 1, updated_at = ? WHERE id = ?").run(now, canvasRow.id);
|
|
1455
|
+
const allNodes = this.db.prepare("SELECT node_id, label FROM canvas_nodes WHERE canvas_id = ? ORDER BY seq ASC").all(canvasRow.id);
|
|
1456
|
+
const allEdges = this.db.prepare("SELECT from_node_id, to_node_id, label FROM canvas_edges WHERE canvas_id = ?").all(canvasRow.id);
|
|
1457
|
+
const mermaidLines = ["graph LR"];
|
|
1458
|
+
for (const n of allNodes) {
|
|
1459
|
+
mermaidLines.push(` ${n.node_id}["${n.label.replace(/"/g, "'")}"]`);
|
|
1460
|
+
}
|
|
1461
|
+
for (const e of allEdges) {
|
|
1462
|
+
if (e.label) {
|
|
1463
|
+
mermaidLines.push(` ${e.from_node_id} -->|${e.label.replace(/"/g, "'")}| ${e.to_node_id}`);
|
|
1464
|
+
} else {
|
|
1465
|
+
mermaidLines.push(` ${e.from_node_id} --> ${e.to_node_id}`);
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
this.db.prepare("UPDATE canvases SET mermaid_text = ? WHERE id = ?").run(mermaidLines.join("\n"), canvasRow.id);
|
|
1469
|
+
}
|
|
1470
|
+
async getLatestCanvasNode(sessionKey) {
|
|
1471
|
+
const row = this.db.prepare(
|
|
1472
|
+
`SELECT cn.node_id as id, cn.label, cn.node_id as captureId
|
|
1473
|
+
FROM canvas_nodes cn
|
|
1474
|
+
JOIN canvases c ON cn.canvas_id = c.id
|
|
1475
|
+
WHERE c.session_key = ?
|
|
1476
|
+
ORDER BY cn.seq DESC LIMIT 1`
|
|
1477
|
+
).get(sessionKey);
|
|
1478
|
+
return row ?? null;
|
|
1479
|
+
}
|
|
1480
|
+
async getCanvasMermaidText(sessionKey) {
|
|
1481
|
+
const row = this.db.prepare("SELECT mermaid_text FROM canvases WHERE session_key = ? ORDER BY updated_at DESC LIMIT 1").get(sessionKey);
|
|
1482
|
+
return row?.mermaid_text ?? null;
|
|
1483
|
+
}
|
|
1484
|
+
async writeRef(sessionKey, nodeId, content) {
|
|
1485
|
+
const refsDir = join(homedir(), ".local", "share", "remem-mcp", "refs", sessionKey);
|
|
1486
|
+
try {
|
|
1487
|
+
mkdirSync(refsDir, { recursive: true });
|
|
1488
|
+
writeFileSync(join(refsDir, `${nodeId}.md`), content, "utf-8");
|
|
1489
|
+
} catch (err) {
|
|
1490
|
+
console.error(`[remem-mcp] writeRef failed: ${err}`);
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
async readRef(nodeId) {
|
|
1494
|
+
const refsBase = join(homedir(), ".local", "share", "remem-mcp", "refs");
|
|
1495
|
+
try {
|
|
1496
|
+
const sessions = readdirSync(refsBase);
|
|
1497
|
+
for (const session of sessions) {
|
|
1498
|
+
const refPath = join(refsBase, session, `${nodeId}.md`);
|
|
1499
|
+
if (existsSync(refPath)) {
|
|
1500
|
+
return readFileSync(refPath, "utf-8");
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
} catch {
|
|
1504
|
+
}
|
|
1505
|
+
return null;
|
|
1506
|
+
}
|
|
1500
1507
|
};
|
|
1501
1508
|
function rowToEntry(row) {
|
|
1502
1509
|
return {
|
|
@@ -1539,733 +1546,20 @@ function skillRowToEntry(row) {
|
|
|
1539
1546
|
content: row.content ?? void 0,
|
|
1540
1547
|
version: row.version,
|
|
1541
1548
|
createdAt: row.created_at,
|
|
1542
|
-
updatedAt: row.updated_at
|
|
1549
|
+
updatedAt: row.updated_at,
|
|
1550
|
+
triggerConditions: row.trigger_conditions ? JSON.parse(row.trigger_conditions) : void 0,
|
|
1551
|
+
steps: row.steps ? JSON.parse(row.steps) : void 0,
|
|
1552
|
+
validationRules: row.validation_rules ? JSON.parse(row.validation_rules) : void 0,
|
|
1553
|
+
sourceCaptureIds: row.source_capture_ids ? JSON.parse(row.source_capture_ids) : void 0,
|
|
1554
|
+
archived: row.archived === 1
|
|
1543
1555
|
};
|
|
1544
1556
|
}
|
|
1545
1557
|
|
|
1546
|
-
// src/sdk.ts
|
|
1547
|
-
init_ulid();
|
|
1548
|
-
init_engine();
|
|
1549
|
-
|
|
1550
|
-
// src/wiki/engine.ts
|
|
1551
|
-
init_esm_shims();
|
|
1552
|
-
init_ulid();
|
|
1553
|
-
import { createHash as createHash2 } from "crypto";
|
|
1554
|
-
import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2, statSync } from "fs";
|
|
1555
|
-
import { basename, extname, join as join2, relative, sep } from "path";
|
|
1556
|
-
function parseFrontmatter(content) {
|
|
1557
|
-
if (!content.startsWith("---")) {
|
|
1558
|
-
return { frontmatter: null, body: content };
|
|
1559
|
-
}
|
|
1560
|
-
const end = content.indexOf("\n---", 3);
|
|
1561
|
-
if (end === -1) {
|
|
1562
|
-
return { frontmatter: null, body: content };
|
|
1563
|
-
}
|
|
1564
|
-
const frontmatter = content.slice(3, end).trim();
|
|
1565
|
-
const body = content.slice(end + 4).replace(/^\n/, "");
|
|
1566
|
-
return { frontmatter, body };
|
|
1567
|
-
}
|
|
1568
|
-
function extractTags(frontmatter) {
|
|
1569
|
-
if (!frontmatter) return null;
|
|
1570
|
-
const match = frontmatter.match(/^tags:\s*(.+)$/m);
|
|
1571
|
-
if (!match) return null;
|
|
1572
|
-
return match[1].replace(/[[\]]/g, "").trim();
|
|
1573
|
-
}
|
|
1574
|
-
function extractTitle(frontmatter, body, fileName) {
|
|
1575
|
-
if (frontmatter) {
|
|
1576
|
-
const titleMatch = frontmatter.match(/^title:\s*(.+)$/m);
|
|
1577
|
-
if (titleMatch) return titleMatch[1].replace(/["']/g, "").trim();
|
|
1578
|
-
}
|
|
1579
|
-
const h1Match = body.match(/^#\s+(.+)$/m);
|
|
1580
|
-
if (h1Match) return h1Match[1].trim();
|
|
1581
|
-
return basename(fileName, extname(fileName));
|
|
1582
|
-
}
|
|
1583
|
-
function extractWikilinks(content) {
|
|
1584
|
-
const links = [];
|
|
1585
|
-
const lines = content.split("\n");
|
|
1586
|
-
const regex = /\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g;
|
|
1587
|
-
for (let i = 0; i < lines.length; i++) {
|
|
1588
|
-
let match;
|
|
1589
|
-
regex.lastIndex = 0;
|
|
1590
|
-
match = regex.exec(lines[i]);
|
|
1591
|
-
while (match !== null) {
|
|
1592
|
-
links.push({
|
|
1593
|
-
target: match[1].trim(),
|
|
1594
|
-
text: (match[2] ?? match[1]).trim(),
|
|
1595
|
-
line: i + 1
|
|
1596
|
-
});
|
|
1597
|
-
match = regex.exec(lines[i]);
|
|
1598
|
-
}
|
|
1599
|
-
}
|
|
1600
|
-
return links;
|
|
1601
|
-
}
|
|
1602
|
-
function extractMarkdownLinks(content) {
|
|
1603
|
-
const links = [];
|
|
1604
|
-
const lines = content.split("\n");
|
|
1605
|
-
const regex = /(?<!!)\[([^\]]+)\]\(([^)]+)\)/g;
|
|
1606
|
-
for (let i = 0; i < lines.length; i++) {
|
|
1607
|
-
let match;
|
|
1608
|
-
regex.lastIndex = 0;
|
|
1609
|
-
match = regex.exec(lines[i]);
|
|
1610
|
-
while (match !== null) {
|
|
1611
|
-
const target = match[2].trim();
|
|
1612
|
-
if (!target.startsWith("http") && !target.startsWith("mailto:")) {
|
|
1613
|
-
links.push({
|
|
1614
|
-
target: target.replace(/\.md$/i, ""),
|
|
1615
|
-
text: match[1].trim(),
|
|
1616
|
-
line: i + 1
|
|
1617
|
-
});
|
|
1618
|
-
}
|
|
1619
|
-
match = regex.exec(lines[i]);
|
|
1620
|
-
}
|
|
1621
|
-
}
|
|
1622
|
-
return links;
|
|
1623
|
-
}
|
|
1624
|
-
function parseMarkdownFile(filePath, repoPath) {
|
|
1625
|
-
const ext = extname(filePath).toLowerCase();
|
|
1626
|
-
if (ext !== ".md" && ext !== ".markdown") return null;
|
|
1627
|
-
let source;
|
|
1628
|
-
try {
|
|
1629
|
-
source = readFileSync2(filePath, "utf-8");
|
|
1630
|
-
} catch {
|
|
1631
|
-
return null;
|
|
1632
|
-
}
|
|
1633
|
-
const relPath = relative(repoPath, filePath).split(sep).join("/");
|
|
1634
|
-
const { frontmatter, body } = parseFrontmatter(source);
|
|
1635
|
-
const title = extractTitle(frontmatter, body, filePath);
|
|
1636
|
-
const tags = extractTags(frontmatter);
|
|
1637
|
-
const contentHash = createHash2("sha256").update(source).digest("hex");
|
|
1638
|
-
const page = {
|
|
1639
|
-
id: generateId(),
|
|
1640
|
-
title,
|
|
1641
|
-
content: body,
|
|
1642
|
-
sourceFile: relPath,
|
|
1643
|
-
section: null,
|
|
1644
|
-
tags,
|
|
1645
|
-
frontmatter,
|
|
1646
|
-
contentHash
|
|
1647
|
-
};
|
|
1648
|
-
const wikilinks = extractWikilinks(body).map((l) => ({ ...l, type: "wikilink" }));
|
|
1649
|
-
const mdlinks = extractMarkdownLinks(body).map((l) => ({ ...l, type: "markdown" }));
|
|
1650
|
-
const links = [...wikilinks, ...mdlinks];
|
|
1651
|
-
return { page, links };
|
|
1652
|
-
}
|
|
1653
|
-
function ingestFile(db, filePath, repoPath, teamId) {
|
|
1654
|
-
const ext = extname(filePath).toLowerCase();
|
|
1655
|
-
if (ext !== ".md" && ext !== ".markdown") {
|
|
1656
|
-
return { file: filePath, pages: 0, links: 0, skipped: true, reason: "not markdown" };
|
|
1657
|
-
}
|
|
1658
|
-
const parsed = parseMarkdownFile(filePath, repoPath);
|
|
1659
|
-
if (!parsed) {
|
|
1660
|
-
return { file: filePath, pages: 0, links: 0, skipped: true, reason: "parse failed" };
|
|
1661
|
-
}
|
|
1662
|
-
const relPath = relative(repoPath, filePath).split(sep).join("/");
|
|
1663
|
-
const now = Date.now();
|
|
1664
|
-
const existing = db.prepare("SELECT id FROM wiki_pages WHERE source_file = ? AND team_id IS ?").get(relPath, teamId);
|
|
1665
|
-
let pageId = parsed.page.id;
|
|
1666
|
-
if (existing) {
|
|
1667
|
-
pageId = existing.id;
|
|
1668
|
-
db.prepare(
|
|
1669
|
-
"UPDATE wiki_pages SET title = ?, content = ?, tags = ?, frontmatter = ?, content_hash = ?, updated_at = ? WHERE id = ?"
|
|
1670
|
-
).run(
|
|
1671
|
-
parsed.page.title,
|
|
1672
|
-
parsed.page.content,
|
|
1673
|
-
parsed.page.tags,
|
|
1674
|
-
parsed.page.frontmatter,
|
|
1675
|
-
parsed.page.contentHash,
|
|
1676
|
-
now,
|
|
1677
|
-
pageId
|
|
1678
|
-
);
|
|
1679
|
-
db.prepare("DELETE FROM wiki_links WHERE from_page_id = ?").run(pageId);
|
|
1680
|
-
} else {
|
|
1681
|
-
db.prepare(
|
|
1682
|
-
`INSERT INTO wiki_pages (id, title, content, source_file, section, tags, frontmatter, content_hash, team_id, created_at, updated_at)
|
|
1683
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
1684
|
-
).run(
|
|
1685
|
-
parsed.page.id,
|
|
1686
|
-
parsed.page.title,
|
|
1687
|
-
parsed.page.content,
|
|
1688
|
-
parsed.page.sourceFile,
|
|
1689
|
-
parsed.page.section,
|
|
1690
|
-
parsed.page.tags,
|
|
1691
|
-
parsed.page.frontmatter,
|
|
1692
|
-
parsed.page.contentHash,
|
|
1693
|
-
teamId,
|
|
1694
|
-
now,
|
|
1695
|
-
now
|
|
1696
|
-
);
|
|
1697
|
-
}
|
|
1698
|
-
const linkStmt = db.prepare(
|
|
1699
|
-
"INSERT INTO wiki_links (from_page_id, to_page_id, to_title, link_text, link_type, line) VALUES (?, ?, ?, ?, ?, ?)"
|
|
1700
|
-
);
|
|
1701
|
-
for (const link of parsed.links) {
|
|
1702
|
-
const target = db.prepare("SELECT id FROM wiki_pages WHERE title = ? AND team_id IS ? LIMIT 1").get(link.target, teamId);
|
|
1703
|
-
linkStmt.run(pageId, target?.id ?? null, link.target, link.text, link.type, link.line);
|
|
1704
|
-
}
|
|
1705
|
-
return {
|
|
1706
|
-
file: relPath,
|
|
1707
|
-
pages: 1,
|
|
1708
|
-
links: parsed.links.length,
|
|
1709
|
-
skipped: false
|
|
1710
|
-
};
|
|
1711
|
-
}
|
|
1712
|
-
function ingestDirectory(db, dirPath, repoPath, teamId, maxFiles = 200) {
|
|
1713
|
-
const results = [];
|
|
1714
|
-
const files = [];
|
|
1715
|
-
const walk = (dir) => {
|
|
1716
|
-
if (files.length >= maxFiles) return;
|
|
1717
|
-
let entries;
|
|
1718
|
-
try {
|
|
1719
|
-
entries = readdirSync(dir);
|
|
1720
|
-
} catch {
|
|
1721
|
-
return;
|
|
1722
|
-
}
|
|
1723
|
-
for (const entry of entries) {
|
|
1724
|
-
if (files.length >= maxFiles) return;
|
|
1725
|
-
const fullPath = join2(dir, entry);
|
|
1726
|
-
let stat;
|
|
1727
|
-
try {
|
|
1728
|
-
stat = statSync(fullPath);
|
|
1729
|
-
} catch {
|
|
1730
|
-
continue;
|
|
1731
|
-
}
|
|
1732
|
-
if (stat.isDirectory()) {
|
|
1733
|
-
if (entry.startsWith(".") || entry === "node_modules" || entry === "vendor") continue;
|
|
1734
|
-
walk(fullPath);
|
|
1735
|
-
} else if (stat.isFile()) {
|
|
1736
|
-
const ext = extname(fullPath).toLowerCase();
|
|
1737
|
-
if (ext === ".md" || ext === ".markdown") {
|
|
1738
|
-
files.push(fullPath);
|
|
1739
|
-
}
|
|
1740
|
-
}
|
|
1741
|
-
}
|
|
1742
|
-
};
|
|
1743
|
-
walk(dirPath);
|
|
1744
|
-
for (const file of files) {
|
|
1745
|
-
const result = ingestFile(db, file, repoPath, teamId);
|
|
1746
|
-
results.push(result);
|
|
1747
|
-
}
|
|
1748
|
-
const unresolved = db.prepare("SELECT id, to_title FROM wiki_links WHERE to_page_id IS NULL").all();
|
|
1749
|
-
if (unresolved.length > 0) {
|
|
1750
|
-
const updateStmt = db.prepare("UPDATE wiki_links SET to_page_id = ? WHERE id = ?");
|
|
1751
|
-
for (const u of unresolved) {
|
|
1752
|
-
const target = db.prepare("SELECT id FROM wiki_pages WHERE title = ? AND team_id IS ? LIMIT 1").get(u.to_title, teamId);
|
|
1753
|
-
if (target) {
|
|
1754
|
-
updateStmt.run(target.id, u.id);
|
|
1755
|
-
}
|
|
1756
|
-
}
|
|
1757
|
-
}
|
|
1758
|
-
return results;
|
|
1759
|
-
}
|
|
1760
|
-
function searchWiki(db, query, opts = {}) {
|
|
1761
|
-
const limit = opts.limit ?? 10;
|
|
1762
|
-
let sql = `
|
|
1763
|
-
SELECT w.id, w.title, w.source_file, snippet(wiki_fts, 1, '<b>', '</b>', '...', 20) as snippet
|
|
1764
|
-
FROM wiki_fts f
|
|
1765
|
-
JOIN wiki_pages w ON w.id = f.id
|
|
1766
|
-
WHERE wiki_fts MATCH ?
|
|
1767
|
-
`;
|
|
1768
|
-
const params = [query];
|
|
1769
|
-
if (opts.teamId !== void 0) {
|
|
1770
|
-
sql += " AND w.team_id IS ?";
|
|
1771
|
-
params.push(opts.teamId);
|
|
1772
|
-
}
|
|
1773
|
-
sql += " LIMIT ?";
|
|
1774
|
-
params.push(limit);
|
|
1775
|
-
const rows = db.prepare(sql).all(...params);
|
|
1776
|
-
return rows.map((r) => ({
|
|
1777
|
-
id: r.id,
|
|
1778
|
-
title: r.title,
|
|
1779
|
-
sourceFile: r.source_file,
|
|
1780
|
-
snippet: r.snippet
|
|
1781
|
-
}));
|
|
1782
|
-
}
|
|
1783
|
-
function getWikiPage(db, pageId) {
|
|
1784
|
-
const row = db.prepare("SELECT * FROM wiki_pages WHERE id = ?").get(pageId);
|
|
1785
|
-
if (!row) return null;
|
|
1786
|
-
const page = {
|
|
1787
|
-
id: row.id,
|
|
1788
|
-
title: row.title,
|
|
1789
|
-
content: row.content,
|
|
1790
|
-
sourceFile: row.source_file,
|
|
1791
|
-
section: row.section,
|
|
1792
|
-
tags: row.tags,
|
|
1793
|
-
frontmatter: row.frontmatter,
|
|
1794
|
-
contentHash: row.content_hash
|
|
1795
|
-
};
|
|
1796
|
-
const linkRows = db.prepare("SELECT * FROM wiki_links WHERE from_page_id = ? ORDER BY line").all(pageId);
|
|
1797
|
-
const links = linkRows.map((r) => ({
|
|
1798
|
-
fromPageId: r.from_page_id,
|
|
1799
|
-
toPageId: r.to_page_id,
|
|
1800
|
-
toTitle: r.to_title,
|
|
1801
|
-
linkText: r.link_text,
|
|
1802
|
-
linkType: r.link_type,
|
|
1803
|
-
line: r.line
|
|
1804
|
-
}));
|
|
1805
|
-
const backlinkRows = db.prepare("SELECT * FROM wiki_links WHERE to_page_id = ? ORDER BY line").all(pageId);
|
|
1806
|
-
const backlinks = backlinkRows.map((r) => ({
|
|
1807
|
-
fromPageId: r.from_page_id,
|
|
1808
|
-
toPageId: r.to_page_id,
|
|
1809
|
-
toTitle: r.to_title,
|
|
1810
|
-
linkText: r.link_text,
|
|
1811
|
-
linkType: r.link_type,
|
|
1812
|
-
line: r.line
|
|
1813
|
-
}));
|
|
1814
|
-
return { page, links, backlinks };
|
|
1815
|
-
}
|
|
1816
|
-
function findOutdatedPages(db, repoPath, opts = {}) {
|
|
1817
|
-
const pages = db.prepare("SELECT id, title, source_file, content_hash FROM wiki_pages WHERE team_id IS ?").all(opts.teamId ?? null);
|
|
1818
|
-
const outdated = [];
|
|
1819
|
-
for (const page of pages) {
|
|
1820
|
-
const fullPath = join2(repoPath, page.source_file);
|
|
1821
|
-
if (!existsSync2(fullPath)) {
|
|
1822
|
-
outdated.push({
|
|
1823
|
-
id: page.id,
|
|
1824
|
-
title: page.title,
|
|
1825
|
-
sourceFile: page.source_file,
|
|
1826
|
-
reason: "file deleted"
|
|
1827
|
-
});
|
|
1828
|
-
continue;
|
|
1829
|
-
}
|
|
1830
|
-
try {
|
|
1831
|
-
const content = readFileSync2(fullPath, "utf-8");
|
|
1832
|
-
const hash = createHash2("sha256").update(content).digest("hex");
|
|
1833
|
-
if (hash !== page.content_hash) {
|
|
1834
|
-
outdated.push({
|
|
1835
|
-
id: page.id,
|
|
1836
|
-
title: page.title,
|
|
1837
|
-
sourceFile: page.source_file,
|
|
1838
|
-
reason: "content changed"
|
|
1839
|
-
});
|
|
1840
|
-
}
|
|
1841
|
-
} catch {
|
|
1842
|
-
outdated.push({
|
|
1843
|
-
id: page.id,
|
|
1844
|
-
title: page.title,
|
|
1845
|
-
sourceFile: page.source_file,
|
|
1846
|
-
reason: "read error"
|
|
1847
|
-
});
|
|
1848
|
-
}
|
|
1849
|
-
}
|
|
1850
|
-
return outdated;
|
|
1851
|
-
}
|
|
1852
|
-
|
|
1853
|
-
// src/sdk.ts
|
|
1854
|
-
function classifyGlobalContent(content, type) {
|
|
1855
|
-
if (type !== "learning" && type !== "decision" && type !== "task") return false;
|
|
1856
|
-
const projectSignals = [
|
|
1857
|
-
/\b\/[a-z]/i,
|
|
1858
|
-
/\b\w+\/\w+\.\w{1,5}\b/,
|
|
1859
|
-
/line\s*\d+/i,
|
|
1860
|
-
/\b\d{4,}\b/,
|
|
1861
|
-
/\bcommit\s+[0-9a-f]{7,}/i,
|
|
1862
|
-
/\bsrc\//,
|
|
1863
|
-
/\bdist\//,
|
|
1864
|
-
/\btests?\//
|
|
1865
|
-
];
|
|
1866
|
-
for (const sig of projectSignals) {
|
|
1867
|
-
if (sig.test(content)) return false;
|
|
1868
|
-
}
|
|
1869
|
-
const lower = content.toLowerCase();
|
|
1870
|
-
const globalSignals = [
|
|
1871
|
-
/\balways\s+/i,
|
|
1872
|
-
/\bnever\s+/i,
|
|
1873
|
-
/\brule\b/i,
|
|
1874
|
-
/\bconvention\b/i,
|
|
1875
|
-
/\bpattern\b/i,
|
|
1876
|
-
/\bprefer\b/i,
|
|
1877
|
-
/\bavoid\b/i,
|
|
1878
|
-
/\bshould\b/i,
|
|
1879
|
-
/\bbest\s+practice\b/i
|
|
1880
|
-
];
|
|
1881
|
-
let score = 0;
|
|
1882
|
-
for (const sig of globalSignals) {
|
|
1883
|
-
if (sig.test(lower)) score++;
|
|
1884
|
-
}
|
|
1885
|
-
if (content.length < 300 && score >= 1) return true;
|
|
1886
|
-
if (content.length < 150 && type === "learning") return true;
|
|
1887
|
-
return false;
|
|
1888
|
-
}
|
|
1889
|
-
var Memory = class {
|
|
1890
|
-
storage;
|
|
1891
|
-
embedder;
|
|
1892
|
-
sessionKey;
|
|
1893
|
-
globalKey;
|
|
1894
|
-
redactSecrets;
|
|
1895
|
-
constructor(opts) {
|
|
1896
|
-
const dbPath = opts?.dbPath ?? join3(homedir(), ".local", "share", "remem-mcp", "memory.db");
|
|
1897
|
-
this.storage = new SQLiteBackend(dbPath);
|
|
1898
|
-
this.embedder = new LocalEmbedder();
|
|
1899
|
-
this.sessionKey = opts?.sessionKey ?? process.env.REMEM_SESSION_KEY ?? createHash3("sha256").update(process.cwd()).digest("hex").slice(0, 16);
|
|
1900
|
-
this.globalKey = opts?.globalSessionKey ?? process.env.REMEM_GLOBAL_SESSION_KEY ?? null;
|
|
1901
|
-
this.redactSecrets = opts?.redactSecrets ?? true;
|
|
1902
|
-
}
|
|
1903
|
-
/** Capture a memory entry. Returns the ID, or null if duplicate. */
|
|
1904
|
-
async capture(content, type, tags = [], opts) {
|
|
1905
|
-
const { text: redactedContent } = this.redactSecrets ? redact(content) : { text: content };
|
|
1906
|
-
let sessionKey = opts?.sessionKey ?? this.sessionKey;
|
|
1907
|
-
if (opts?.autoGlobal && !opts?.sessionKey && this.globalKey && this.globalKey !== sessionKey) {
|
|
1908
|
-
if (classifyGlobalContent(content, type)) {
|
|
1909
|
-
sessionKey = this.globalKey;
|
|
1910
|
-
}
|
|
1911
|
-
}
|
|
1912
|
-
const contentHash = createHash3("sha256").update(redactedContent).digest("hex");
|
|
1913
|
-
const existing = await this.storage.findByContentHash(contentHash, sessionKey);
|
|
1914
|
-
if (existing.length > 0) return null;
|
|
1915
|
-
const id = generateId();
|
|
1916
|
-
const entry = {
|
|
1917
|
-
id,
|
|
1918
|
-
sessionKey,
|
|
1919
|
-
agentId: "sdk",
|
|
1920
|
-
type,
|
|
1921
|
-
content: redactedContent,
|
|
1922
|
-
tags,
|
|
1923
|
-
createdAt: Date.now()
|
|
1924
|
-
};
|
|
1925
|
-
await this.storage.put(entry);
|
|
1926
|
-
try {
|
|
1927
|
-
const embedding = await this.embedder.embed(redactedContent);
|
|
1928
|
-
await this.storage.putVector(id, embedding);
|
|
1929
|
-
} catch {
|
|
1930
|
-
}
|
|
1931
|
-
return id;
|
|
1932
|
-
}
|
|
1933
|
-
/** Recall relevant memory. Searches project + global (if configured). */
|
|
1934
|
-
async recall(query, opts) {
|
|
1935
|
-
const limit = Math.min(opts?.limit ?? 10, 50);
|
|
1936
|
-
const mode = opts?.mode ?? "hybrid";
|
|
1937
|
-
const sessionKey = opts?.sessionKey ?? this.sessionKey;
|
|
1938
|
-
let queryEmbedding = null;
|
|
1939
|
-
if (mode === "hybrid" || mode === "vector") {
|
|
1940
|
-
const stripped = stripQueryProperNouns(query);
|
|
1941
|
-
queryEmbedding = await this.embedder.embed(stripped || query);
|
|
1942
|
-
}
|
|
1943
|
-
if (this.globalKey && !opts?.sessionKey && this.globalKey !== sessionKey) {
|
|
1944
|
-
const reservedGlobal = Math.min(3, limit);
|
|
1945
|
-
const projectLimit = limit - reservedGlobal;
|
|
1946
|
-
const projectResults = await this.storage.search(query, queryEmbedding, {
|
|
1947
|
-
sessionKey,
|
|
1948
|
-
limit: projectLimit,
|
|
1949
|
-
offset: 0,
|
|
1950
|
-
mode
|
|
1951
|
-
});
|
|
1952
|
-
let globalResults = [];
|
|
1953
|
-
try {
|
|
1954
|
-
globalResults = await this.storage.search(query, queryEmbedding, {
|
|
1955
|
-
sessionKey: this.globalKey,
|
|
1956
|
-
limit: reservedGlobal,
|
|
1957
|
-
offset: 0,
|
|
1958
|
-
mode
|
|
1959
|
-
});
|
|
1960
|
-
} catch {
|
|
1961
|
-
}
|
|
1962
|
-
const seen = new Set(projectResults.map((r) => r.entry.id));
|
|
1963
|
-
return [...projectResults, ...globalResults.filter((r) => !seen.has(r.entry.id))].slice(
|
|
1964
|
-
0,
|
|
1965
|
-
limit
|
|
1966
|
-
);
|
|
1967
|
-
}
|
|
1968
|
-
return this.storage.search(query, queryEmbedding, {
|
|
1969
|
-
sessionKey,
|
|
1970
|
-
limit,
|
|
1971
|
-
offset: 0,
|
|
1972
|
-
mode
|
|
1973
|
-
});
|
|
1974
|
-
}
|
|
1975
|
-
/** Search with filters. Searches project + global (if configured). */
|
|
1976
|
-
async search(query, opts) {
|
|
1977
|
-
const limit = Math.min(opts?.limit ?? 20, 100);
|
|
1978
|
-
const mode = opts?.mode ?? "hybrid";
|
|
1979
|
-
const sessionKey = opts?.sessionKey ?? this.sessionKey;
|
|
1980
|
-
let queryEmbedding = null;
|
|
1981
|
-
if (mode === "hybrid" || mode === "vector") {
|
|
1982
|
-
const stripped = stripQueryProperNouns(query);
|
|
1983
|
-
queryEmbedding = await this.embedder.embed(stripped || query);
|
|
1984
|
-
}
|
|
1985
|
-
if (this.globalKey && !opts?.sessionKey && this.globalKey !== sessionKey) {
|
|
1986
|
-
const reservedGlobal = Math.min(3, limit);
|
|
1987
|
-
const projectLimit = limit - reservedGlobal;
|
|
1988
|
-
const projectResults = await this.storage.search(query, queryEmbedding, {
|
|
1989
|
-
sessionKey,
|
|
1990
|
-
limit: projectLimit,
|
|
1991
|
-
offset: 0,
|
|
1992
|
-
mode,
|
|
1993
|
-
filters: opts?.filters
|
|
1994
|
-
});
|
|
1995
|
-
let globalResults = [];
|
|
1996
|
-
try {
|
|
1997
|
-
globalResults = await this.storage.search(query, queryEmbedding, {
|
|
1998
|
-
sessionKey: this.globalKey,
|
|
1999
|
-
limit: reservedGlobal,
|
|
2000
|
-
offset: 0,
|
|
2001
|
-
mode,
|
|
2002
|
-
filters: opts?.filters
|
|
2003
|
-
});
|
|
2004
|
-
} catch {
|
|
2005
|
-
}
|
|
2006
|
-
const seen = new Set(projectResults.map((r) => r.entry.id));
|
|
2007
|
-
return [...projectResults, ...globalResults.filter((r) => !seen.has(r.entry.id))].slice(
|
|
2008
|
-
0,
|
|
2009
|
-
limit
|
|
2010
|
-
);
|
|
2011
|
-
}
|
|
2012
|
-
return this.storage.search(query, queryEmbedding, {
|
|
2013
|
-
sessionKey,
|
|
2014
|
-
limit,
|
|
2015
|
-
offset: 0,
|
|
2016
|
-
mode,
|
|
2017
|
-
filters: opts?.filters
|
|
2018
|
-
});
|
|
2019
|
-
}
|
|
2020
|
-
/** Compute the embedding vector for a piece of text using the configured embedder. */
|
|
2021
|
-
async embed(text) {
|
|
2022
|
-
return this.embedder.embed(text);
|
|
2023
|
-
}
|
|
2024
|
-
/** Delete a capture by ID. */
|
|
2025
|
-
async forget(id) {
|
|
2026
|
-
return this.storage.delete(id);
|
|
2027
|
-
}
|
|
2028
|
-
/** Update an existing capture's content, tags, type, or trust state. */
|
|
2029
|
-
async update(id, opts) {
|
|
2030
|
-
const existing = await this.storage.get(id);
|
|
2031
|
-
if (!existing) return false;
|
|
2032
|
-
const newContent = opts.content ?? existing.content;
|
|
2033
|
-
const newTags = opts.tags ?? existing.tags;
|
|
2034
|
-
const newType = opts.type ?? existing.type;
|
|
2035
|
-
const newTrust = opts.verified ? "verified" : existing.trustState ?? "candidate";
|
|
2036
|
-
const contentHash = createHash3("sha256").update(newContent).digest("hex");
|
|
2037
|
-
const db = this.storage.getDatabase();
|
|
2038
|
-
db.prepare(
|
|
2039
|
-
"UPDATE captures SET content = ?, tags = ?, type = ?, trust_state = ?, content_hash = ? WHERE id = ?"
|
|
2040
|
-
).run(newContent, JSON.stringify(newTags), newType, newTrust, contentHash, id);
|
|
2041
|
-
const rowid = db.prepare("SELECT rowid FROM captures WHERE id = ?").get(id)?.rowid;
|
|
2042
|
-
if (rowid) {
|
|
2043
|
-
db.prepare(
|
|
2044
|
-
"INSERT INTO captures_fts(captures_fts, rowid, content, tags, type) VALUES('delete', ?, '', '', '')"
|
|
2045
|
-
).run(rowid);
|
|
2046
|
-
db.prepare(
|
|
2047
|
-
"INSERT INTO captures_fts (rowid, id, content, tags, type) VALUES (?, ?, ?, ?, ?)"
|
|
2048
|
-
).run(rowid, id, newContent, JSON.stringify(newTags), newType);
|
|
2049
|
-
}
|
|
2050
|
-
if (opts.content && opts.content !== existing.content) {
|
|
2051
|
-
try {
|
|
2052
|
-
const embedding = await this.embedder.embed(newContent);
|
|
2053
|
-
await this.storage.putVector(id, embedding);
|
|
2054
|
-
} catch {
|
|
2055
|
-
}
|
|
2056
|
-
}
|
|
2057
|
-
return true;
|
|
2058
|
-
}
|
|
2059
|
-
/** Find and optionally merge duplicate captures by content similarity (Jaccard). */
|
|
2060
|
-
async consolidate(opts) {
|
|
2061
|
-
const threshold = opts.threshold ?? 0.75;
|
|
2062
|
-
const confirm = opts.confirm ?? false;
|
|
2063
|
-
const sessionKey = opts.sessionKey ?? this.sessionKey;
|
|
2064
|
-
const db = this.storage.getDatabase();
|
|
2065
|
-
let sql = "SELECT id, content, type, tags, created_at FROM captures WHERE deleted_at IS NULL";
|
|
2066
|
-
const params = [];
|
|
2067
|
-
if (sessionKey !== "all") {
|
|
2068
|
-
sql += " AND session_key = ?";
|
|
2069
|
-
params.push(sessionKey);
|
|
2070
|
-
}
|
|
2071
|
-
sql += " ORDER BY created_at DESC";
|
|
2072
|
-
const rows = db.prepare(sql).all(...params);
|
|
2073
|
-
if (rows.length < 2) return { groups: [], merged: 0 };
|
|
2074
|
-
const groups = [];
|
|
2075
|
-
const seen = /* @__PURE__ */ new Set();
|
|
2076
|
-
for (let i = 0; i < rows.length; i++) {
|
|
2077
|
-
if (seen.has(rows[i].id)) continue;
|
|
2078
|
-
const words1 = new Set(rows[i].content.toLowerCase().split(/\s+/));
|
|
2079
|
-
const group = [rows[i].id];
|
|
2080
|
-
for (let j = i + 1; j < rows.length; j++) {
|
|
2081
|
-
if (seen.has(rows[j].id)) continue;
|
|
2082
|
-
const words2 = new Set(rows[j].content.toLowerCase().split(/\s+/));
|
|
2083
|
-
const intersection = [...words1].filter((w) => words2.has(w)).length;
|
|
2084
|
-
const union = (/* @__PURE__ */ new Set([...words1, ...words2])).size;
|
|
2085
|
-
const sim = union > 0 ? intersection / union : 0;
|
|
2086
|
-
if (sim >= threshold) {
|
|
2087
|
-
group.push(rows[j].id);
|
|
2088
|
-
seen.add(rows[j].id);
|
|
2089
|
-
}
|
|
2090
|
-
}
|
|
2091
|
-
if (group.length > 1) {
|
|
2092
|
-
seen.add(rows[i].id);
|
|
2093
|
-
groups.push({
|
|
2094
|
-
ids: group,
|
|
2095
|
-
similarity: threshold,
|
|
2096
|
-
preview: rows[i].content.slice(0, 80).replace(/\n/g, " ")
|
|
2097
|
-
});
|
|
2098
|
-
}
|
|
2099
|
-
}
|
|
2100
|
-
if (!confirm || groups.length === 0) {
|
|
2101
|
-
return { groups, merged: 0 };
|
|
2102
|
-
}
|
|
2103
|
-
let merged = 0;
|
|
2104
|
-
const softDelete = db.prepare("UPDATE captures SET deleted_at = ? WHERE id = ?");
|
|
2105
|
-
const deleteVec = db.prepare("DELETE FROM captures_vec WHERE id = ?");
|
|
2106
|
-
const deleteAtoms = db.prepare("DELETE FROM atoms WHERE capture_id = ?");
|
|
2107
|
-
const mergeTx = db.transaction(() => {
|
|
2108
|
-
for (const g of groups) {
|
|
2109
|
-
const groupRows = g.ids.map((id) => rows.find((r) => r.id === id)).filter(Boolean).sort((a, b) => a.created_at - b.created_at);
|
|
2110
|
-
const dups = groupRows.slice(1);
|
|
2111
|
-
for (const dup of dups) {
|
|
2112
|
-
softDelete.run(Date.now(), dup.id);
|
|
2113
|
-
deleteVec.run(dup.id);
|
|
2114
|
-
deleteAtoms.run(dup.id);
|
|
2115
|
-
merged++;
|
|
2116
|
-
}
|
|
2117
|
-
}
|
|
2118
|
-
});
|
|
2119
|
-
mergeTx();
|
|
2120
|
-
return { groups, merged };
|
|
2121
|
-
}
|
|
2122
|
-
/** Create a handoff packet for the next agent session. */
|
|
2123
|
-
async handoff(opts) {
|
|
2124
|
-
const lines = [];
|
|
2125
|
-
lines.push(`# Handoff: ${opts.task}`);
|
|
2126
|
-
lines.push(`Status: ${opts.status}`);
|
|
2127
|
-
lines.push(`Date: ${(/* @__PURE__ */ new Date()).toISOString()}`);
|
|
2128
|
-
lines.push("");
|
|
2129
|
-
lines.push("## Progress");
|
|
2130
|
-
lines.push(opts.progress);
|
|
2131
|
-
lines.push("");
|
|
2132
|
-
if (opts.decisions && opts.decisions.length > 0) {
|
|
2133
|
-
lines.push("## Decisions");
|
|
2134
|
-
for (const d of opts.decisions) lines.push(`- ${d}`);
|
|
2135
|
-
lines.push("");
|
|
2136
|
-
}
|
|
2137
|
-
if (opts.files && opts.files.length > 0) {
|
|
2138
|
-
lines.push("## Files");
|
|
2139
|
-
for (const f of opts.files) lines.push(`- ${f}`);
|
|
2140
|
-
lines.push("");
|
|
2141
|
-
}
|
|
2142
|
-
if (opts.nextSteps && opts.nextSteps.length > 0) {
|
|
2143
|
-
lines.push("## Next steps");
|
|
2144
|
-
opts.nextSteps.forEach((s, i) => {
|
|
2145
|
-
lines.push(`${i + 1}. ${s}`);
|
|
2146
|
-
});
|
|
2147
|
-
lines.push("");
|
|
2148
|
-
}
|
|
2149
|
-
const content = lines.join("\n");
|
|
2150
|
-
const dedupPayload = JSON.stringify({
|
|
2151
|
-
task: opts.task,
|
|
2152
|
-
status: opts.status,
|
|
2153
|
-
progress: opts.progress,
|
|
2154
|
-
decisions: opts.decisions ?? [],
|
|
2155
|
-
files: opts.files ?? [],
|
|
2156
|
-
nextSteps: opts.nextSteps ?? []
|
|
2157
|
-
});
|
|
2158
|
-
const contentHash = createHash3("sha256").update(dedupPayload).digest("hex");
|
|
2159
|
-
const existing = await this.storage.findByContentHash(contentHash, this.sessionKey);
|
|
2160
|
-
if (existing.length > 0) return null;
|
|
2161
|
-
const id = generateId();
|
|
2162
|
-
const entry = {
|
|
2163
|
-
id,
|
|
2164
|
-
sessionKey: this.sessionKey,
|
|
2165
|
-
agentId: "sdk",
|
|
2166
|
-
type: "task",
|
|
2167
|
-
content,
|
|
2168
|
-
tags: ["handoff", `status:${opts.status}`],
|
|
2169
|
-
createdAt: Date.now(),
|
|
2170
|
-
metadata: {
|
|
2171
|
-
handoff: true,
|
|
2172
|
-
task: opts.task,
|
|
2173
|
-
status: opts.status,
|
|
2174
|
-
progress: opts.progress,
|
|
2175
|
-
decisions: opts.decisions ?? [],
|
|
2176
|
-
files: opts.files ?? [],
|
|
2177
|
-
nextSteps: opts.nextSteps ?? []
|
|
2178
|
-
},
|
|
2179
|
-
contentHash
|
|
2180
|
-
};
|
|
2181
|
-
await this.storage.put(entry);
|
|
2182
|
-
try {
|
|
2183
|
-
const embedding = await this.embedder.embed(content);
|
|
2184
|
-
await this.storage.putVector(id, embedding);
|
|
2185
|
-
} catch {
|
|
2186
|
-
}
|
|
2187
|
-
return id;
|
|
2188
|
-
}
|
|
2189
|
-
/** Record an Architecture Decision Record (ADR). */
|
|
2190
|
-
async adr(opts) {
|
|
2191
|
-
const lines = [];
|
|
2192
|
-
lines.push(`# ADR: ${opts.title}`);
|
|
2193
|
-
lines.push(`Date: ${(/* @__PURE__ */ new Date()).toISOString()}`);
|
|
2194
|
-
lines.push("");
|
|
2195
|
-
lines.push("## Context");
|
|
2196
|
-
lines.push(opts.context);
|
|
2197
|
-
lines.push("");
|
|
2198
|
-
lines.push("## Decision");
|
|
2199
|
-
lines.push(opts.decision);
|
|
2200
|
-
lines.push("");
|
|
2201
|
-
const alternatives = Array.isArray(opts.alternatives) ? opts.alternatives : typeof opts.alternatives === "string" && opts.alternatives ? [opts.alternatives] : [];
|
|
2202
|
-
if (alternatives.length > 0) {
|
|
2203
|
-
lines.push("## Alternatives considered");
|
|
2204
|
-
for (const alt of alternatives) lines.push(`- ${alt}`);
|
|
2205
|
-
lines.push("");
|
|
2206
|
-
}
|
|
2207
|
-
if (opts.consequences) {
|
|
2208
|
-
lines.push("## Consequences");
|
|
2209
|
-
lines.push(opts.consequences);
|
|
2210
|
-
lines.push("");
|
|
2211
|
-
}
|
|
2212
|
-
const content = lines.join("\n");
|
|
2213
|
-
const dedupPayload = JSON.stringify({
|
|
2214
|
-
title: opts.title,
|
|
2215
|
-
context: opts.context,
|
|
2216
|
-
decision: opts.decision,
|
|
2217
|
-
alternatives,
|
|
2218
|
-
consequences: opts.consequences ?? ""
|
|
2219
|
-
});
|
|
2220
|
-
const contentHash = createHash3("sha256").update(dedupPayload).digest("hex");
|
|
2221
|
-
const existing = await this.storage.findByContentHash(contentHash, this.sessionKey);
|
|
2222
|
-
if (existing.length > 0) return null;
|
|
2223
|
-
const id = generateId();
|
|
2224
|
-
const entry = {
|
|
2225
|
-
id,
|
|
2226
|
-
sessionKey: this.sessionKey,
|
|
2227
|
-
agentId: "sdk",
|
|
2228
|
-
type: "decision",
|
|
2229
|
-
content,
|
|
2230
|
-
tags: ["adr", ...opts.tags ?? []],
|
|
2231
|
-
createdAt: Date.now(),
|
|
2232
|
-
metadata: {
|
|
2233
|
-
adr: true,
|
|
2234
|
-
title: opts.title,
|
|
2235
|
-
context: opts.context,
|
|
2236
|
-
decision: opts.decision,
|
|
2237
|
-
alternatives,
|
|
2238
|
-
consequences: opts.consequences ?? ""
|
|
2239
|
-
},
|
|
2240
|
-
contentHash
|
|
2241
|
-
};
|
|
2242
|
-
await this.storage.put(entry);
|
|
2243
|
-
try {
|
|
2244
|
-
const embedding = await this.embedder.embed(content);
|
|
2245
|
-
await this.storage.putVector(id, embedding);
|
|
2246
|
-
} catch {
|
|
2247
|
-
}
|
|
2248
|
-
return id;
|
|
2249
|
-
}
|
|
2250
|
-
/** Close the database connection. */
|
|
2251
|
-
close() {
|
|
2252
|
-
this.storage.close();
|
|
2253
|
-
}
|
|
2254
|
-
};
|
|
2255
|
-
|
|
2256
1558
|
export {
|
|
2257
1559
|
stripQueryProperNouns,
|
|
2258
1560
|
SQLiteBackend,
|
|
2259
1561
|
LocalEmbedder,
|
|
2260
1562
|
local_exports,
|
|
2261
|
-
init_local
|
|
2262
|
-
redact,
|
|
2263
|
-
ingestFile,
|
|
2264
|
-
ingestDirectory,
|
|
2265
|
-
searchWiki,
|
|
2266
|
-
getWikiPage,
|
|
2267
|
-
findOutdatedPages,
|
|
2268
|
-
classifyGlobalContent,
|
|
2269
|
-
Memory
|
|
1563
|
+
init_local
|
|
2270
1564
|
};
|
|
2271
|
-
//# sourceMappingURL=chunk-
|
|
1565
|
+
//# sourceMappingURL=chunk-OYUNKUZK.js.map
|