engrams 0.1.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 +140 -0
- package/dist/cli.js +2114 -0
- package/dist/http.js +255 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2113 -0
- package/package.json +60 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2113 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
+
var __esm = (fn, res) => function __init() {
|
|
4
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
5
|
+
};
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// ../core/dist/schema.js
|
|
12
|
+
var schema_exports = {};
|
|
13
|
+
__export(schema_exports, {
|
|
14
|
+
agentPermissions: () => agentPermissions,
|
|
15
|
+
memories: () => memories,
|
|
16
|
+
memoryConnections: () => memoryConnections,
|
|
17
|
+
memoryEvents: () => memoryEvents
|
|
18
|
+
});
|
|
19
|
+
import { sqliteTable, text, real, integer } from "drizzle-orm/sqlite-core";
|
|
20
|
+
var memories, memoryConnections, memoryEvents, agentPermissions;
|
|
21
|
+
var init_schema = __esm({
|
|
22
|
+
"../core/dist/schema.js"() {
|
|
23
|
+
"use strict";
|
|
24
|
+
memories = sqliteTable("memories", {
|
|
25
|
+
id: text("id").primaryKey(),
|
|
26
|
+
content: text("content").notNull(),
|
|
27
|
+
detail: text("detail"),
|
|
28
|
+
domain: text("domain").notNull().default("general"),
|
|
29
|
+
sourceAgentId: text("source_agent_id").notNull(),
|
|
30
|
+
sourceAgentName: text("source_agent_name").notNull(),
|
|
31
|
+
crossAgentId: text("cross_agent_id"),
|
|
32
|
+
crossAgentName: text("cross_agent_name"),
|
|
33
|
+
sourceType: text("source_type").notNull(),
|
|
34
|
+
sourceDescription: text("source_description"),
|
|
35
|
+
confidence: real("confidence").notNull().default(0.7),
|
|
36
|
+
confirmedCount: integer("confirmed_count").notNull().default(0),
|
|
37
|
+
correctedCount: integer("corrected_count").notNull().default(0),
|
|
38
|
+
mistakeCount: integer("mistake_count").notNull().default(0),
|
|
39
|
+
usedCount: integer("used_count").notNull().default(0),
|
|
40
|
+
learnedAt: text("learned_at"),
|
|
41
|
+
confirmedAt: text("confirmed_at"),
|
|
42
|
+
lastUsedAt: text("last_used_at"),
|
|
43
|
+
deletedAt: text("deleted_at"),
|
|
44
|
+
hasPiiFlag: integer("has_pii_flag").notNull().default(0),
|
|
45
|
+
entityType: text("entity_type"),
|
|
46
|
+
entityName: text("entity_name"),
|
|
47
|
+
structuredData: text("structured_data")
|
|
48
|
+
});
|
|
49
|
+
memoryConnections = sqliteTable("memory_connections", {
|
|
50
|
+
sourceMemoryId: text("source_memory_id").notNull().references(() => memories.id),
|
|
51
|
+
targetMemoryId: text("target_memory_id").notNull().references(() => memories.id),
|
|
52
|
+
relationship: text("relationship").notNull()
|
|
53
|
+
});
|
|
54
|
+
memoryEvents = sqliteTable("memory_events", {
|
|
55
|
+
id: text("id").primaryKey(),
|
|
56
|
+
memoryId: text("memory_id").notNull().references(() => memories.id),
|
|
57
|
+
eventType: text("event_type").notNull(),
|
|
58
|
+
agentId: text("agent_id"),
|
|
59
|
+
agentName: text("agent_name"),
|
|
60
|
+
oldValue: text("old_value"),
|
|
61
|
+
newValue: text("new_value"),
|
|
62
|
+
timestamp: text("timestamp").notNull()
|
|
63
|
+
});
|
|
64
|
+
agentPermissions = sqliteTable("agent_permissions", {
|
|
65
|
+
agentId: text("agent_id").notNull(),
|
|
66
|
+
domain: text("domain").notNull(),
|
|
67
|
+
canRead: integer("can_read").notNull().default(1),
|
|
68
|
+
canWrite: integer("can_write").notNull().default(1)
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// ../core/dist/types.js
|
|
74
|
+
var init_types = __esm({
|
|
75
|
+
"../core/dist/types.js"() {
|
|
76
|
+
"use strict";
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// ../core/dist/confidence.js
|
|
81
|
+
function applyConfidenceDecay(sqlite) {
|
|
82
|
+
const now3 = /* @__PURE__ */ new Date();
|
|
83
|
+
const candidates = sqlite.prepare(`
|
|
84
|
+
SELECT id, confidence, last_used_at, confirmed_at, learned_at
|
|
85
|
+
FROM memories
|
|
86
|
+
WHERE deleted_at IS NULL AND confidence > ?
|
|
87
|
+
`).all(MIN_CONFIDENCE);
|
|
88
|
+
let decayed = 0;
|
|
89
|
+
for (const mem of candidates) {
|
|
90
|
+
const lastActivity = mem.last_used_at || mem.confirmed_at || mem.learned_at;
|
|
91
|
+
if (!lastActivity)
|
|
92
|
+
continue;
|
|
93
|
+
const elapsed = now3.getTime() - new Date(lastActivity).getTime();
|
|
94
|
+
const periods = Math.floor(elapsed / DECAY_INTERVAL_MS);
|
|
95
|
+
if (periods <= 0)
|
|
96
|
+
continue;
|
|
97
|
+
const newConfidence = Math.max(mem.confidence - DECAY_RATE * periods, MIN_CONFIDENCE);
|
|
98
|
+
if (newConfidence < mem.confidence) {
|
|
99
|
+
sqlite.prepare(`UPDATE memories SET confidence = ? WHERE id = ?`).run(newConfidence, mem.id);
|
|
100
|
+
decayed++;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return decayed;
|
|
104
|
+
}
|
|
105
|
+
function getInitialConfidence(sourceType) {
|
|
106
|
+
return INITIAL_CONFIDENCE[sourceType] ?? 0.7;
|
|
107
|
+
}
|
|
108
|
+
function applyConfirm(_current) {
|
|
109
|
+
return 0.99;
|
|
110
|
+
}
|
|
111
|
+
function applyCorrect() {
|
|
112
|
+
return 0.5;
|
|
113
|
+
}
|
|
114
|
+
function applyMistake(current) {
|
|
115
|
+
return Math.max(current - 0.15, 0.1);
|
|
116
|
+
}
|
|
117
|
+
var DECAY_RATE, MIN_CONFIDENCE, DECAY_INTERVAL_MS, INITIAL_CONFIDENCE;
|
|
118
|
+
var init_confidence = __esm({
|
|
119
|
+
"../core/dist/confidence.js"() {
|
|
120
|
+
"use strict";
|
|
121
|
+
DECAY_RATE = 0.01;
|
|
122
|
+
MIN_CONFIDENCE = 0.1;
|
|
123
|
+
DECAY_INTERVAL_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
124
|
+
INITIAL_CONFIDENCE = {
|
|
125
|
+
stated: 0.9,
|
|
126
|
+
observed: 0.75,
|
|
127
|
+
inferred: 0.65,
|
|
128
|
+
"cross-agent": 0.7
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// ../core/dist/fts.js
|
|
134
|
+
function setupFTS(sqlite) {
|
|
135
|
+
sqlite.exec(`
|
|
136
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS memory_fts USING fts5(
|
|
137
|
+
content,
|
|
138
|
+
detail,
|
|
139
|
+
source_agent_name,
|
|
140
|
+
entity_name,
|
|
141
|
+
content='memories',
|
|
142
|
+
content_rowid='rowid'
|
|
143
|
+
);
|
|
144
|
+
`);
|
|
145
|
+
sqlite.exec(`
|
|
146
|
+
CREATE TRIGGER IF NOT EXISTS memory_fts_insert AFTER INSERT ON memories BEGIN
|
|
147
|
+
INSERT INTO memory_fts(rowid, content, detail, source_agent_name, entity_name)
|
|
148
|
+
VALUES (new.rowid, new.content, new.detail, new.source_agent_name, new.entity_name);
|
|
149
|
+
END;
|
|
150
|
+
`);
|
|
151
|
+
sqlite.exec(`
|
|
152
|
+
CREATE TRIGGER IF NOT EXISTS memory_fts_delete AFTER DELETE ON memories BEGIN
|
|
153
|
+
INSERT INTO memory_fts(memory_fts, rowid, content, detail, source_agent_name, entity_name)
|
|
154
|
+
VALUES ('delete', old.rowid, old.content, old.detail, old.source_agent_name, old.entity_name);
|
|
155
|
+
END;
|
|
156
|
+
`);
|
|
157
|
+
sqlite.exec(`
|
|
158
|
+
CREATE TRIGGER IF NOT EXISTS memory_fts_update AFTER UPDATE ON memories BEGIN
|
|
159
|
+
INSERT INTO memory_fts(memory_fts, rowid, content, detail, source_agent_name, entity_name)
|
|
160
|
+
VALUES ('delete', old.rowid, old.content, old.detail, old.source_agent_name, old.entity_name);
|
|
161
|
+
INSERT INTO memory_fts(rowid, content, detail, source_agent_name, entity_name)
|
|
162
|
+
VALUES (new.rowid, new.content, new.detail, new.source_agent_name, new.entity_name);
|
|
163
|
+
END;
|
|
164
|
+
`);
|
|
165
|
+
}
|
|
166
|
+
function searchFTS(sqlite, query, limit = 20) {
|
|
167
|
+
const rows = sqlite.prepare(`SELECT rowid FROM memory_fts WHERE memory_fts MATCH ? ORDER BY rank LIMIT ?`).all(query, limit);
|
|
168
|
+
return rows;
|
|
169
|
+
}
|
|
170
|
+
var init_fts = __esm({
|
|
171
|
+
"../core/dist/fts.js"() {
|
|
172
|
+
"use strict";
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// ../core/dist/vec.js
|
|
177
|
+
import { createRequire } from "module";
|
|
178
|
+
function setupVec(sqlite) {
|
|
179
|
+
try {
|
|
180
|
+
const sqliteVec = require2("sqlite-vec");
|
|
181
|
+
sqliteVec.load(sqlite);
|
|
182
|
+
sqlite.exec(`
|
|
183
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS memory_embeddings USING vec0(
|
|
184
|
+
memory_id TEXT PRIMARY KEY,
|
|
185
|
+
embedding float[${EMBEDDING_DIM}]
|
|
186
|
+
);
|
|
187
|
+
`);
|
|
188
|
+
return true;
|
|
189
|
+
} catch (err) {
|
|
190
|
+
process.stderr.write(`[engrams] sqlite-vec not available \u2014 vector search disabled, falling back to FTS5 only: ${err}
|
|
191
|
+
`);
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
function toBuffer(embedding) {
|
|
196
|
+
return Buffer.from(embedding.buffer, embedding.byteOffset, embedding.byteLength);
|
|
197
|
+
}
|
|
198
|
+
function insertEmbedding(sqlite, memoryId, embedding) {
|
|
199
|
+
sqlite.prepare(`INSERT OR REPLACE INTO memory_embeddings (memory_id, embedding) VALUES (?, ?)`).run(memoryId, toBuffer(embedding));
|
|
200
|
+
}
|
|
201
|
+
function searchVec(sqlite, queryEmbedding, limit = 20) {
|
|
202
|
+
return sqlite.prepare(`SELECT memory_id, distance FROM memory_embeddings WHERE embedding MATCH ? ORDER BY distance LIMIT ?`).all(toBuffer(queryEmbedding), limit);
|
|
203
|
+
}
|
|
204
|
+
var require2, EMBEDDING_DIM;
|
|
205
|
+
var init_vec = __esm({
|
|
206
|
+
"../core/dist/vec.js"() {
|
|
207
|
+
"use strict";
|
|
208
|
+
require2 = createRequire(import.meta.url);
|
|
209
|
+
EMBEDDING_DIM = 384;
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// ../core/dist/db.js
|
|
214
|
+
import Database from "better-sqlite3";
|
|
215
|
+
import { drizzle } from "drizzle-orm/better-sqlite3";
|
|
216
|
+
import { resolve } from "path";
|
|
217
|
+
import { homedir } from "os";
|
|
218
|
+
import { mkdirSync, chmodSync } from "fs";
|
|
219
|
+
function runMigration(sqlite, name, fn) {
|
|
220
|
+
const exists = sqlite.prepare(`SELECT 1 FROM _migrations WHERE name = ?`).get(name);
|
|
221
|
+
if (!exists) {
|
|
222
|
+
try {
|
|
223
|
+
fn();
|
|
224
|
+
} catch {
|
|
225
|
+
}
|
|
226
|
+
sqlite.prepare(`INSERT OR IGNORE INTO _migrations (name) VALUES (?)`).run(name);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
function runMigrations(sqlite) {
|
|
230
|
+
sqlite.exec(`CREATE TABLE IF NOT EXISTS _migrations (name TEXT PRIMARY KEY)`);
|
|
231
|
+
runMigration(sqlite, "add_has_pii_flag", () => {
|
|
232
|
+
sqlite.exec(`ALTER TABLE memories ADD COLUMN has_pii_flag INTEGER NOT NULL DEFAULT 0`);
|
|
233
|
+
});
|
|
234
|
+
runMigration(sqlite, "add_entity_columns", () => {
|
|
235
|
+
sqlite.exec(`ALTER TABLE memories ADD COLUMN entity_type TEXT`);
|
|
236
|
+
sqlite.exec(`ALTER TABLE memories ADD COLUMN entity_name TEXT`);
|
|
237
|
+
sqlite.exec(`ALTER TABLE memories ADD COLUMN structured_data TEXT`);
|
|
238
|
+
sqlite.exec(`CREATE INDEX IF NOT EXISTS idx_memories_entity_type ON memories(entity_type) WHERE deleted_at IS NULL`);
|
|
239
|
+
sqlite.exec(`CREATE INDEX IF NOT EXISTS idx_memories_entity_name ON memories(entity_name) WHERE deleted_at IS NULL`);
|
|
240
|
+
});
|
|
241
|
+
runMigration(sqlite, "fts_add_entity_name", () => {
|
|
242
|
+
sqlite.exec(`DROP TRIGGER IF EXISTS memory_fts_insert`);
|
|
243
|
+
sqlite.exec(`DROP TRIGGER IF EXISTS memory_fts_delete`);
|
|
244
|
+
sqlite.exec(`DROP TRIGGER IF EXISTS memory_fts_update`);
|
|
245
|
+
sqlite.exec(`DROP TABLE IF EXISTS memory_fts`);
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
function createDatabase(dbPath) {
|
|
249
|
+
const dir = resolve(homedir(), ".engrams");
|
|
250
|
+
mkdirSync(dir, { recursive: true });
|
|
251
|
+
const path = dbPath ?? resolve(dir, "engrams.db");
|
|
252
|
+
const sqlite = new Database(path);
|
|
253
|
+
sqlite.pragma("journal_mode = WAL");
|
|
254
|
+
sqlite.pragma("foreign_keys = ON");
|
|
255
|
+
sqlite.exec(CREATE_TABLES_SQL);
|
|
256
|
+
runMigrations(sqlite);
|
|
257
|
+
setupFTS(sqlite);
|
|
258
|
+
try {
|
|
259
|
+
sqlite.exec(`INSERT INTO memory_fts(memory_fts) VALUES('rebuild')`);
|
|
260
|
+
} catch {
|
|
261
|
+
}
|
|
262
|
+
let vecAvailable = false;
|
|
263
|
+
try {
|
|
264
|
+
vecAvailable = setupVec(sqlite);
|
|
265
|
+
} catch {
|
|
266
|
+
}
|
|
267
|
+
try {
|
|
268
|
+
chmodSync(path, 384);
|
|
269
|
+
} catch {
|
|
270
|
+
}
|
|
271
|
+
const db = drizzle(sqlite, { schema: schema_exports });
|
|
272
|
+
return { db, sqlite, vecAvailable };
|
|
273
|
+
}
|
|
274
|
+
function bumpLastModified(sqlite) {
|
|
275
|
+
sqlite.prepare(`INSERT OR REPLACE INTO engrams_meta (key, value) VALUES ('last_modified', ?)`).run((/* @__PURE__ */ new Date()).toISOString());
|
|
276
|
+
}
|
|
277
|
+
var CREATE_TABLES_SQL;
|
|
278
|
+
var init_db = __esm({
|
|
279
|
+
"../core/dist/db.js"() {
|
|
280
|
+
"use strict";
|
|
281
|
+
init_schema();
|
|
282
|
+
init_fts();
|
|
283
|
+
init_vec();
|
|
284
|
+
CREATE_TABLES_SQL = `
|
|
285
|
+
CREATE TABLE IF NOT EXISTS memories (
|
|
286
|
+
id TEXT PRIMARY KEY,
|
|
287
|
+
content TEXT NOT NULL,
|
|
288
|
+
detail TEXT,
|
|
289
|
+
domain TEXT NOT NULL DEFAULT 'general',
|
|
290
|
+
source_agent_id TEXT NOT NULL,
|
|
291
|
+
source_agent_name TEXT NOT NULL,
|
|
292
|
+
cross_agent_id TEXT,
|
|
293
|
+
cross_agent_name TEXT,
|
|
294
|
+
source_type TEXT NOT NULL,
|
|
295
|
+
source_description TEXT,
|
|
296
|
+
confidence REAL NOT NULL DEFAULT 0.7,
|
|
297
|
+
confirmed_count INTEGER NOT NULL DEFAULT 0,
|
|
298
|
+
corrected_count INTEGER NOT NULL DEFAULT 0,
|
|
299
|
+
mistake_count INTEGER NOT NULL DEFAULT 0,
|
|
300
|
+
used_count INTEGER NOT NULL DEFAULT 0,
|
|
301
|
+
learned_at TEXT,
|
|
302
|
+
confirmed_at TEXT,
|
|
303
|
+
last_used_at TEXT,
|
|
304
|
+
deleted_at TEXT
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
CREATE TABLE IF NOT EXISTS memory_connections (
|
|
308
|
+
source_memory_id TEXT NOT NULL REFERENCES memories(id),
|
|
309
|
+
target_memory_id TEXT NOT NULL REFERENCES memories(id),
|
|
310
|
+
relationship TEXT NOT NULL
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
CREATE TABLE IF NOT EXISTS memory_events (
|
|
314
|
+
id TEXT PRIMARY KEY,
|
|
315
|
+
memory_id TEXT NOT NULL REFERENCES memories(id),
|
|
316
|
+
event_type TEXT NOT NULL,
|
|
317
|
+
agent_id TEXT,
|
|
318
|
+
agent_name TEXT,
|
|
319
|
+
old_value TEXT,
|
|
320
|
+
new_value TEXT,
|
|
321
|
+
timestamp TEXT NOT NULL
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
CREATE TABLE IF NOT EXISTS agent_permissions (
|
|
325
|
+
agent_id TEXT NOT NULL,
|
|
326
|
+
domain TEXT NOT NULL,
|
|
327
|
+
can_read INTEGER NOT NULL DEFAULT 1,
|
|
328
|
+
can_write INTEGER NOT NULL DEFAULT 1
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
CREATE TABLE IF NOT EXISTS engrams_meta (
|
|
332
|
+
key TEXT PRIMARY KEY,
|
|
333
|
+
value TEXT NOT NULL
|
|
334
|
+
);
|
|
335
|
+
INSERT OR IGNORE INTO engrams_meta (key, value) VALUES ('last_modified', datetime('now'));
|
|
336
|
+
`;
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
// ../core/dist/embeddings.js
|
|
341
|
+
import { resolve as resolve2 } from "path";
|
|
342
|
+
import { homedir as homedir2 } from "os";
|
|
343
|
+
async function getEmbedder() {
|
|
344
|
+
if (!embedder) {
|
|
345
|
+
const { pipeline } = await import("@huggingface/transformers");
|
|
346
|
+
const cacheDir = resolve2(homedir2(), ".engrams", "models");
|
|
347
|
+
const model = await pipeline("feature-extraction", MODEL_ID, {
|
|
348
|
+
cache_dir: cacheDir,
|
|
349
|
+
dtype: "q8"
|
|
350
|
+
});
|
|
351
|
+
embedder = model;
|
|
352
|
+
}
|
|
353
|
+
return embedder;
|
|
354
|
+
}
|
|
355
|
+
async function generateEmbedding(text2) {
|
|
356
|
+
const cached = embeddingCache.get(text2);
|
|
357
|
+
if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) {
|
|
358
|
+
return cached.embedding;
|
|
359
|
+
}
|
|
360
|
+
const model = await getEmbedder();
|
|
361
|
+
const output = await model(text2, { pooling: "mean", normalize: true });
|
|
362
|
+
const embedding = new Float32Array(output.data);
|
|
363
|
+
if (embeddingCache.size >= CACHE_MAX) {
|
|
364
|
+
const oldestKey = embeddingCache.keys().next().value;
|
|
365
|
+
if (oldestKey)
|
|
366
|
+
embeddingCache.delete(oldestKey);
|
|
367
|
+
}
|
|
368
|
+
embeddingCache.set(text2, { embedding, timestamp: Date.now() });
|
|
369
|
+
return embedding;
|
|
370
|
+
}
|
|
371
|
+
async function backfillEmbeddings(sqlite) {
|
|
372
|
+
const missing = sqlite.prepare(`
|
|
373
|
+
SELECT m.id, m.content, m.detail FROM memories m
|
|
374
|
+
LEFT JOIN memory_embeddings e ON m.id = e.memory_id
|
|
375
|
+
WHERE m.deleted_at IS NULL AND e.memory_id IS NULL
|
|
376
|
+
`).all();
|
|
377
|
+
let count = 0;
|
|
378
|
+
for (const mem of missing) {
|
|
379
|
+
try {
|
|
380
|
+
const text2 = mem.content + (mem.detail ? " " + mem.detail : "");
|
|
381
|
+
const embedding = await generateEmbedding(text2);
|
|
382
|
+
insertEmbedding(sqlite, mem.id, embedding);
|
|
383
|
+
count++;
|
|
384
|
+
} catch {
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
return count;
|
|
388
|
+
}
|
|
389
|
+
var MODEL_ID, embedder, CACHE_MAX, CACHE_TTL_MS, embeddingCache;
|
|
390
|
+
var init_embeddings = __esm({
|
|
391
|
+
"../core/dist/embeddings.js"() {
|
|
392
|
+
"use strict";
|
|
393
|
+
init_vec();
|
|
394
|
+
MODEL_ID = "Xenova/all-MiniLM-L6-v2";
|
|
395
|
+
embedder = null;
|
|
396
|
+
CACHE_MAX = 100;
|
|
397
|
+
CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
398
|
+
embeddingCache = /* @__PURE__ */ new Map();
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
// ../core/dist/search.js
|
|
403
|
+
function getStoredEmbedding(sqlite, memoryId) {
|
|
404
|
+
const row = sqlite.prepare(`SELECT embedding FROM memory_embeddings WHERE memory_id = ?`).get(memoryId);
|
|
405
|
+
if (!row)
|
|
406
|
+
return null;
|
|
407
|
+
return new Float32Array(row.embedding.buffer, row.embedding.byteOffset, row.embedding.byteLength / 4);
|
|
408
|
+
}
|
|
409
|
+
function cosineSimilarity(a, b) {
|
|
410
|
+
let dot = 0, normA = 0, normB = 0;
|
|
411
|
+
for (let i = 0; i < a.length; i++) {
|
|
412
|
+
dot += a[i] * b[i];
|
|
413
|
+
normA += a[i] * a[i];
|
|
414
|
+
normB += b[i] * b[i];
|
|
415
|
+
}
|
|
416
|
+
return dot / (Math.sqrt(normA) * Math.sqrt(normB));
|
|
417
|
+
}
|
|
418
|
+
function recencyBoost(learnedAt) {
|
|
419
|
+
if (!learnedAt)
|
|
420
|
+
return 1;
|
|
421
|
+
const ageMs = Date.now() - new Date(learnedAt).getTime();
|
|
422
|
+
const ageDays = ageMs / (1e3 * 60 * 60 * 24);
|
|
423
|
+
return 1 + Math.max(0, 0.1 * (1 - ageDays / 30));
|
|
424
|
+
}
|
|
425
|
+
function expandConnections(sqlite, results, queryEmbedding, maxDepth, similarityThreshold) {
|
|
426
|
+
if (!queryEmbedding) {
|
|
427
|
+
return results.map((r) => ({ ...r, connected: [] }));
|
|
428
|
+
}
|
|
429
|
+
const seen = new Set(results.map((r) => r.id));
|
|
430
|
+
return results.map((result) => {
|
|
431
|
+
const connected = [];
|
|
432
|
+
const queue = [{ memoryId: result.id, depth: 0 }];
|
|
433
|
+
while (queue.length > 0) {
|
|
434
|
+
const { memoryId, depth } = queue.shift();
|
|
435
|
+
if (depth >= maxDepth)
|
|
436
|
+
continue;
|
|
437
|
+
const outgoing = sqlite.prepare(`SELECT mc.target_memory_id as id, mc.relationship, m.*
|
|
438
|
+
FROM memory_connections mc
|
|
439
|
+
JOIN memories m ON m.id = mc.target_memory_id
|
|
440
|
+
WHERE mc.source_memory_id = ? AND m.deleted_at IS NULL`).all(memoryId);
|
|
441
|
+
const incoming = sqlite.prepare(`SELECT mc.source_memory_id as id, mc.relationship, m.*
|
|
442
|
+
FROM memory_connections mc
|
|
443
|
+
JOIN memories m ON m.id = mc.source_memory_id
|
|
444
|
+
WHERE mc.target_memory_id = ? AND m.deleted_at IS NULL`).all(memoryId);
|
|
445
|
+
for (const conn of [...outgoing, ...incoming]) {
|
|
446
|
+
if (seen.has(conn.id))
|
|
447
|
+
continue;
|
|
448
|
+
seen.add(conn.id);
|
|
449
|
+
const embedding = getStoredEmbedding(sqlite, conn.id);
|
|
450
|
+
if (!embedding)
|
|
451
|
+
continue;
|
|
452
|
+
const similarity = cosineSimilarity(queryEmbedding, embedding);
|
|
453
|
+
if (similarity < similarityThreshold)
|
|
454
|
+
continue;
|
|
455
|
+
connected.push({
|
|
456
|
+
memory: conn,
|
|
457
|
+
relationship: conn.relationship,
|
|
458
|
+
depth: depth + 1,
|
|
459
|
+
similarity
|
|
460
|
+
});
|
|
461
|
+
queue.push({ memoryId: conn.id, depth: depth + 1 });
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
connected.sort((a, b) => {
|
|
465
|
+
const simDiff = b.similarity - a.similarity;
|
|
466
|
+
if (Math.abs(simDiff) > 1e-10)
|
|
467
|
+
return simDiff;
|
|
468
|
+
return a.memory.id.localeCompare(b.memory.id);
|
|
469
|
+
});
|
|
470
|
+
return { ...result, connected };
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
async function hybridSearch(sqlite, query, options = {}) {
|
|
474
|
+
const limit = options.limit ?? 20;
|
|
475
|
+
const expand = options.expand ?? true;
|
|
476
|
+
const maxDepth = options.maxDepth ?? 3;
|
|
477
|
+
const similarityThreshold = options.similarityThreshold ?? 0.5;
|
|
478
|
+
const fetchLimit = limit * 3;
|
|
479
|
+
const cacheKey = JSON.stringify({ query, ...options });
|
|
480
|
+
const currentLastModified = sqlite.prepare(`SELECT value FROM engrams_meta WHERE key = 'last_modified'`).get();
|
|
481
|
+
const cachedEntry = resultCache.get(cacheKey);
|
|
482
|
+
if (cachedEntry && cachedEntry.lastModified === currentLastModified?.value) {
|
|
483
|
+
return { results: cachedEntry.results, cached: true };
|
|
484
|
+
}
|
|
485
|
+
const ftsIds = [];
|
|
486
|
+
try {
|
|
487
|
+
const ftsResults = searchFTS(sqlite, query, fetchLimit);
|
|
488
|
+
if (ftsResults.length > 0) {
|
|
489
|
+
const rowids = ftsResults.map((r) => r.rowid);
|
|
490
|
+
const placeholders2 = rowids.map(() => "?").join(",");
|
|
491
|
+
const rows2 = sqlite.prepare(`SELECT id FROM memories WHERE rowid IN (${placeholders2}) AND deleted_at IS NULL`).all(...rowids);
|
|
492
|
+
ftsIds.push(...rows2.map((r) => r.id));
|
|
493
|
+
}
|
|
494
|
+
} catch {
|
|
495
|
+
}
|
|
496
|
+
const vecIds = [];
|
|
497
|
+
let queryEmbedding = null;
|
|
498
|
+
try {
|
|
499
|
+
queryEmbedding = await generateEmbedding(query);
|
|
500
|
+
const vecResults = searchVec(sqlite, queryEmbedding, fetchLimit);
|
|
501
|
+
vecIds.push(...vecResults.map((r) => r.memory_id));
|
|
502
|
+
} catch {
|
|
503
|
+
}
|
|
504
|
+
const scores = /* @__PURE__ */ new Map();
|
|
505
|
+
ftsIds.forEach((id, rank) => {
|
|
506
|
+
scores.set(id, (scores.get(id) ?? 0) + 1 / (RRF_K + rank + 1));
|
|
507
|
+
});
|
|
508
|
+
vecIds.forEach((id, rank) => {
|
|
509
|
+
scores.set(id, (scores.get(id) ?? 0) + 1 / (RRF_K + rank + 1));
|
|
510
|
+
});
|
|
511
|
+
for (const [id, rawScore] of scores.entries()) {
|
|
512
|
+
const mem = sqlite.prepare(`SELECT confidence, learned_at FROM memories WHERE id = ? AND deleted_at IS NULL`).get(id);
|
|
513
|
+
if (mem) {
|
|
514
|
+
const confidenceBoost = 0.5 + mem.confidence * 0.5;
|
|
515
|
+
const recency = recencyBoost(mem.learned_at);
|
|
516
|
+
scores.set(id, rawScore * confidenceBoost * recency);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
const rankedIds = [...scores.keys()];
|
|
520
|
+
rankedIds.sort((a, b) => {
|
|
521
|
+
const scoreDiff = scores.get(b) - scores.get(a);
|
|
522
|
+
if (Math.abs(scoreDiff) > 1e-10)
|
|
523
|
+
return scoreDiff;
|
|
524
|
+
return a.localeCompare(b);
|
|
525
|
+
});
|
|
526
|
+
const topIds = rankedIds.slice(0, limit);
|
|
527
|
+
if (topIds.length === 0) {
|
|
528
|
+
return { results: [], cached: false };
|
|
529
|
+
}
|
|
530
|
+
const placeholders = topIds.map(() => "?").join(",");
|
|
531
|
+
let sql2 = `SELECT * FROM memories WHERE id IN (${placeholders}) AND deleted_at IS NULL`;
|
|
532
|
+
const params = [...topIds];
|
|
533
|
+
if (options.domain) {
|
|
534
|
+
sql2 += ` AND domain = ?`;
|
|
535
|
+
params.push(options.domain);
|
|
536
|
+
}
|
|
537
|
+
if (options.entityType) {
|
|
538
|
+
sql2 += ` AND entity_type = ?`;
|
|
539
|
+
params.push(options.entityType);
|
|
540
|
+
}
|
|
541
|
+
if (options.entityName) {
|
|
542
|
+
sql2 += ` AND entity_name = ? COLLATE NOCASE`;
|
|
543
|
+
params.push(options.entityName);
|
|
544
|
+
}
|
|
545
|
+
if (options.minConfidence !== void 0) {
|
|
546
|
+
sql2 += ` AND confidence >= ?`;
|
|
547
|
+
params.push(options.minConfidence);
|
|
548
|
+
}
|
|
549
|
+
const rows = sqlite.prepare(sql2).all(...params);
|
|
550
|
+
const rowMap = new Map(rows.map((r) => [r.id, r]));
|
|
551
|
+
const searchResults = topIds.filter((id) => rowMap.has(id)).map((id) => ({
|
|
552
|
+
id,
|
|
553
|
+
score: scores.get(id),
|
|
554
|
+
memory: rowMap.get(id)
|
|
555
|
+
}));
|
|
556
|
+
let expandedResults;
|
|
557
|
+
if (expand) {
|
|
558
|
+
expandedResults = expandConnections(sqlite, searchResults, queryEmbedding, maxDepth, similarityThreshold);
|
|
559
|
+
} else {
|
|
560
|
+
expandedResults = searchResults.map((r) => ({ ...r, connected: [] }));
|
|
561
|
+
}
|
|
562
|
+
if (currentLastModified) {
|
|
563
|
+
resultCache.set(cacheKey, { results: expandedResults, lastModified: currentLastModified.value });
|
|
564
|
+
}
|
|
565
|
+
return { results: expandedResults, cached: false };
|
|
566
|
+
}
|
|
567
|
+
var RRF_K, resultCache;
|
|
568
|
+
var init_search = __esm({
|
|
569
|
+
"../core/dist/search.js"() {
|
|
570
|
+
"use strict";
|
|
571
|
+
init_fts();
|
|
572
|
+
init_vec();
|
|
573
|
+
init_embeddings();
|
|
574
|
+
RRF_K = 60;
|
|
575
|
+
resultCache = /* @__PURE__ */ new Map();
|
|
576
|
+
}
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
// ../core/dist/pii.js
|
|
580
|
+
function isValidCreditCard(match) {
|
|
581
|
+
const digits = match.replace(/[^0-9]/g, "");
|
|
582
|
+
return digits.length >= 13 && digits.length <= 19;
|
|
583
|
+
}
|
|
584
|
+
function detectSensitiveData(text2) {
|
|
585
|
+
const results = [];
|
|
586
|
+
const coveredRanges = [];
|
|
587
|
+
for (const { type, pattern } of PII_PATTERNS) {
|
|
588
|
+
pattern.lastIndex = 0;
|
|
589
|
+
let m;
|
|
590
|
+
while ((m = pattern.exec(text2)) !== null) {
|
|
591
|
+
const start = m.index;
|
|
592
|
+
const end = m.index + m[0].length;
|
|
593
|
+
const overlaps = coveredRanges.some((r) => start < r.end && end > r.start);
|
|
594
|
+
if (overlaps)
|
|
595
|
+
continue;
|
|
596
|
+
if (type === "credit_card" && !isValidCreditCard(m[0]))
|
|
597
|
+
continue;
|
|
598
|
+
if (type === "phone") {
|
|
599
|
+
const digits = m[0].replace(/[^0-9]/g, "");
|
|
600
|
+
if (digits.length < 10)
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
results.push({ type, match: m[0], start, end });
|
|
604
|
+
coveredRanges.push({ start, end });
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
results.sort((a, b) => a.start - b.start);
|
|
608
|
+
return results;
|
|
609
|
+
}
|
|
610
|
+
function redactSensitiveData(text2) {
|
|
611
|
+
const matches = detectSensitiveData(text2);
|
|
612
|
+
if (matches.length === 0)
|
|
613
|
+
return { redacted: text2, matches };
|
|
614
|
+
const tokenMap = new Map(PII_PATTERNS.map((p) => [p.type, p.redactToken]));
|
|
615
|
+
let redacted = text2;
|
|
616
|
+
for (let i = matches.length - 1; i >= 0; i--) {
|
|
617
|
+
const m = matches[i];
|
|
618
|
+
const token = tokenMap.get(m.type) ?? `[${m.type.toUpperCase()}]`;
|
|
619
|
+
redacted = redacted.slice(0, m.start) + token + redacted.slice(m.end);
|
|
620
|
+
}
|
|
621
|
+
return { redacted, matches };
|
|
622
|
+
}
|
|
623
|
+
var PII_PATTERNS;
|
|
624
|
+
var init_pii = __esm({
|
|
625
|
+
"../core/dist/pii.js"() {
|
|
626
|
+
"use strict";
|
|
627
|
+
PII_PATTERNS = [
|
|
628
|
+
{
|
|
629
|
+
type: "ssn",
|
|
630
|
+
pattern: /\b\d{3}-\d{2}-\d{4}\b/g,
|
|
631
|
+
redactToken: "[SSN]"
|
|
632
|
+
},
|
|
633
|
+
{
|
|
634
|
+
type: "credit_card",
|
|
635
|
+
pattern: /\b(?:\d[ -]*?){13,19}\b/g,
|
|
636
|
+
redactToken: "[CREDIT_CARD]"
|
|
637
|
+
},
|
|
638
|
+
{
|
|
639
|
+
type: "api_key",
|
|
640
|
+
pattern: /\b(?:sk-[a-zA-Z0-9]{20,}|ghp_[a-zA-Z0-9]{36,}|xoxb-[a-zA-Z0-9-]+|xoxp-[a-zA-Z0-9-]+|AKIA[A-Z0-9]{16}|rk_live_[a-zA-Z0-9]+|rk_test_[a-zA-Z0-9]+|pk_live_[a-zA-Z0-9]+|pk_test_[a-zA-Z0-9]+)\b/g,
|
|
641
|
+
redactToken: "[API_KEY]"
|
|
642
|
+
},
|
|
643
|
+
{
|
|
644
|
+
type: "email",
|
|
645
|
+
pattern: /\b[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}\b/g,
|
|
646
|
+
redactToken: "[EMAIL]"
|
|
647
|
+
},
|
|
648
|
+
{
|
|
649
|
+
type: "phone",
|
|
650
|
+
pattern: /(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b/g,
|
|
651
|
+
redactToken: "[PHONE]"
|
|
652
|
+
},
|
|
653
|
+
{
|
|
654
|
+
type: "ip_address",
|
|
655
|
+
pattern: /\b(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\b/g,
|
|
656
|
+
redactToken: "[IP_ADDRESS]"
|
|
657
|
+
}
|
|
658
|
+
];
|
|
659
|
+
}
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
// ../core/dist/entity-extraction.js
|
|
663
|
+
async function extractEntity(content, detail, existingEntityNames) {
|
|
664
|
+
const { default: Anthropic2 } = await import("@anthropic-ai/sdk");
|
|
665
|
+
const client = new Anthropic2();
|
|
666
|
+
const existingNamesHint = existingEntityNames && existingEntityNames.length > 0 ? `
|
|
667
|
+
|
|
668
|
+
Existing entity names in the system (prefer matching these over creating new ones):
|
|
669
|
+
${existingEntityNames.slice(0, 50).map((n) => `- ${n}`).join("\n")}` : "";
|
|
670
|
+
const response = await client.messages.create({
|
|
671
|
+
model: "claude-sonnet-4-5-20250514",
|
|
672
|
+
max_tokens: 500,
|
|
673
|
+
messages: [
|
|
674
|
+
{
|
|
675
|
+
role: "user",
|
|
676
|
+
content: `Classify this memory and extract structured data.
|
|
677
|
+
|
|
678
|
+
Memory: ${content}${detail ? `
|
|
679
|
+
Detail: ${detail}` : ""}${existingNamesHint}
|
|
680
|
+
|
|
681
|
+
Respond with JSON only:
|
|
682
|
+
{
|
|
683
|
+
"entity_type": "person|organization|place|project|preference|event|goal|fact",
|
|
684
|
+
"entity_name": "canonical name or null if not applicable",
|
|
685
|
+
"structured_data": { type-specific fields },
|
|
686
|
+
"suggested_connections": [
|
|
687
|
+
{ "target_entity_name": "...", "target_entity_type": "...", "relationship": "works_at|involves|located_at|part_of|about|related" }
|
|
688
|
+
]
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
Entity type definitions:
|
|
692
|
+
- person: about a specific individual
|
|
693
|
+
- organization: about a company, team, or group
|
|
694
|
+
- place: about a location
|
|
695
|
+
- project: about a work project or initiative
|
|
696
|
+
- preference: about what the user likes/dislikes/prefers
|
|
697
|
+
- event: about something that happened or will happen
|
|
698
|
+
- goal: about something the user wants to achieve
|
|
699
|
+
- fact: general knowledge that doesn't fit other types
|
|
700
|
+
|
|
701
|
+
For entity_name, use the canonical form (e.g. "Sarah Chen" not "my manager Sarah"). If an existing entity name matches, use that exact spelling.
|
|
702
|
+
|
|
703
|
+
For structured_data, include relevant fields:
|
|
704
|
+
- person: name, role, organization, relationship_to_user
|
|
705
|
+
- organization: name, type, user_relationship
|
|
706
|
+
- place: name, context
|
|
707
|
+
- project: name, status, user_role
|
|
708
|
+
- preference: category, strength (strong/mild/contextual)
|
|
709
|
+
- event: what, when, who
|
|
710
|
+
- goal: what, timeline, status (active/achieved/abandoned)
|
|
711
|
+
- fact: category`
|
|
712
|
+
}
|
|
713
|
+
]
|
|
714
|
+
});
|
|
715
|
+
const text2 = response.content[0].type === "text" ? response.content[0].text : "";
|
|
716
|
+
const cleaned = text2.replace(/^```(?:json)?\s*\n?/i, "").replace(/\n?```\s*$/i, "").trim();
|
|
717
|
+
return JSON.parse(cleaned);
|
|
718
|
+
}
|
|
719
|
+
var init_entity_extraction = __esm({
|
|
720
|
+
"../core/dist/entity-extraction.js"() {
|
|
721
|
+
"use strict";
|
|
722
|
+
}
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
// ../core/dist/index.js
|
|
726
|
+
var init_dist = __esm({
|
|
727
|
+
"../core/dist/index.js"() {
|
|
728
|
+
"use strict";
|
|
729
|
+
init_schema();
|
|
730
|
+
init_types();
|
|
731
|
+
init_confidence();
|
|
732
|
+
init_db();
|
|
733
|
+
init_fts();
|
|
734
|
+
init_embeddings();
|
|
735
|
+
init_vec();
|
|
736
|
+
init_search();
|
|
737
|
+
init_db();
|
|
738
|
+
init_pii();
|
|
739
|
+
init_entity_extraction();
|
|
740
|
+
}
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
// src/http.ts
|
|
744
|
+
var http_exports = {};
|
|
745
|
+
__export(http_exports, {
|
|
746
|
+
startHttpApi: () => startHttpApi
|
|
747
|
+
});
|
|
748
|
+
import { createServer } from "http";
|
|
749
|
+
import { randomBytes } from "crypto";
|
|
750
|
+
import { eq, and, isNull } from "drizzle-orm";
|
|
751
|
+
function generateId() {
|
|
752
|
+
return randomBytes(16).toString("hex");
|
|
753
|
+
}
|
|
754
|
+
function now() {
|
|
755
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
756
|
+
}
|
|
757
|
+
function json(res, data, status = 200) {
|
|
758
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
759
|
+
res.end(JSON.stringify(data));
|
|
760
|
+
}
|
|
761
|
+
function parseBody(req) {
|
|
762
|
+
return new Promise((resolve3, reject) => {
|
|
763
|
+
let body = "";
|
|
764
|
+
req.on("data", (chunk) => body += chunk);
|
|
765
|
+
req.on("end", () => {
|
|
766
|
+
try {
|
|
767
|
+
resolve3(body ? JSON.parse(body) : {});
|
|
768
|
+
} catch {
|
|
769
|
+
reject(new Error("Invalid JSON"));
|
|
770
|
+
}
|
|
771
|
+
});
|
|
772
|
+
req.on("error", reject);
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
function startHttpApi(db, sqlite, port = 3838) {
|
|
776
|
+
const server = createServer(async (req, res) => {
|
|
777
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
778
|
+
res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
|
|
779
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
780
|
+
if (req.method === "OPTIONS") {
|
|
781
|
+
res.writeHead(200);
|
|
782
|
+
res.end();
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
const url = req.url ?? "";
|
|
786
|
+
try {
|
|
787
|
+
const confirmMatch = url.match(/^\/api\/memory\/([^/]+)\/confirm$/);
|
|
788
|
+
if (confirmMatch && req.method === "POST") {
|
|
789
|
+
const id = confirmMatch[1];
|
|
790
|
+
const existing = db.select().from(memories).where(and(eq(memories.id, id), isNull(memories.deletedAt))).get();
|
|
791
|
+
if (!existing) return json(res, { error: "Not found" }, 404);
|
|
792
|
+
const newConfidence = applyConfirm(existing.confidence);
|
|
793
|
+
const timestamp = now();
|
|
794
|
+
db.update(memories).set({
|
|
795
|
+
confidence: newConfidence,
|
|
796
|
+
confirmedCount: existing.confirmedCount + 1,
|
|
797
|
+
confirmedAt: timestamp
|
|
798
|
+
}).where(eq(memories.id, id)).run();
|
|
799
|
+
db.insert(memoryEvents).values({
|
|
800
|
+
id: generateId(),
|
|
801
|
+
memoryId: id,
|
|
802
|
+
eventType: "confirmed",
|
|
803
|
+
agentName: "dashboard",
|
|
804
|
+
oldValue: JSON.stringify({ confidence: existing.confidence }),
|
|
805
|
+
newValue: JSON.stringify({ confidence: newConfidence }),
|
|
806
|
+
timestamp
|
|
807
|
+
}).run();
|
|
808
|
+
return json(res, { id, newConfidence });
|
|
809
|
+
}
|
|
810
|
+
const correctMatch = url.match(/^\/api\/memory\/([^/]+)\/correct$/);
|
|
811
|
+
if (correctMatch && req.method === "POST") {
|
|
812
|
+
const id = correctMatch[1];
|
|
813
|
+
const body = await parseBody(req);
|
|
814
|
+
const content = body.content;
|
|
815
|
+
if (!content) return json(res, { error: "content required" }, 400);
|
|
816
|
+
const existing = db.select().from(memories).where(and(eq(memories.id, id), isNull(memories.deletedAt))).get();
|
|
817
|
+
if (!existing) return json(res, { error: "Not found" }, 404);
|
|
818
|
+
const newConfidence = applyCorrect();
|
|
819
|
+
const timestamp = now();
|
|
820
|
+
db.update(memories).set({
|
|
821
|
+
content,
|
|
822
|
+
confidence: newConfidence,
|
|
823
|
+
correctedCount: existing.correctedCount + 1
|
|
824
|
+
}).where(eq(memories.id, id)).run();
|
|
825
|
+
db.insert(memoryEvents).values({
|
|
826
|
+
id: generateId(),
|
|
827
|
+
memoryId: id,
|
|
828
|
+
eventType: "corrected",
|
|
829
|
+
agentName: "dashboard",
|
|
830
|
+
oldValue: JSON.stringify({ content: existing.content }),
|
|
831
|
+
newValue: JSON.stringify({ content, confidence: newConfidence }),
|
|
832
|
+
timestamp
|
|
833
|
+
}).run();
|
|
834
|
+
return json(res, { id, newConfidence });
|
|
835
|
+
}
|
|
836
|
+
const flagMatch = url.match(/^\/api\/memory\/([^/]+)\/flag$/);
|
|
837
|
+
if (flagMatch && req.method === "POST") {
|
|
838
|
+
const id = flagMatch[1];
|
|
839
|
+
const existing = db.select().from(memories).where(and(eq(memories.id, id), isNull(memories.deletedAt))).get();
|
|
840
|
+
if (!existing) return json(res, { error: "Not found" }, 404);
|
|
841
|
+
const newConfidence = applyMistake(existing.confidence);
|
|
842
|
+
const timestamp = now();
|
|
843
|
+
db.update(memories).set({
|
|
844
|
+
confidence: newConfidence,
|
|
845
|
+
mistakeCount: existing.mistakeCount + 1
|
|
846
|
+
}).where(eq(memories.id, id)).run();
|
|
847
|
+
db.insert(memoryEvents).values({
|
|
848
|
+
id: generateId(),
|
|
849
|
+
memoryId: id,
|
|
850
|
+
eventType: "confidence_changed",
|
|
851
|
+
agentName: "dashboard",
|
|
852
|
+
oldValue: JSON.stringify({ confidence: existing.confidence }),
|
|
853
|
+
newValue: JSON.stringify({ confidence: newConfidence, flaggedAsMistake: true }),
|
|
854
|
+
timestamp
|
|
855
|
+
}).run();
|
|
856
|
+
return json(res, { id, newConfidence });
|
|
857
|
+
}
|
|
858
|
+
const deleteMatch = url.match(/^\/api\/memory\/([^/]+)\/delete$/);
|
|
859
|
+
if (deleteMatch && req.method === "POST") {
|
|
860
|
+
const id = deleteMatch[1];
|
|
861
|
+
const timestamp = now();
|
|
862
|
+
db.update(memories).set({ deletedAt: timestamp }).where(eq(memories.id, id)).run();
|
|
863
|
+
db.insert(memoryEvents).values({
|
|
864
|
+
id: generateId(),
|
|
865
|
+
memoryId: id,
|
|
866
|
+
eventType: "removed",
|
|
867
|
+
agentName: "dashboard",
|
|
868
|
+
newValue: JSON.stringify({ reason: "deleted via dashboard" }),
|
|
869
|
+
timestamp
|
|
870
|
+
}).run();
|
|
871
|
+
return json(res, { id, deleted: true });
|
|
872
|
+
}
|
|
873
|
+
const updateMatch = url.match(/^\/api\/memory\/([^/]+)\/update$/);
|
|
874
|
+
if (updateMatch && req.method === "POST") {
|
|
875
|
+
const id = updateMatch[1];
|
|
876
|
+
const body = await parseBody(req);
|
|
877
|
+
const updates = {};
|
|
878
|
+
if (body.content) updates.content = body.content;
|
|
879
|
+
if (body.detail) updates.detail = body.detail;
|
|
880
|
+
if (body.domain) updates.domain = body.domain;
|
|
881
|
+
if (Object.keys(updates).length === 0)
|
|
882
|
+
return json(res, { error: "No fields" }, 400);
|
|
883
|
+
db.update(memories).set(updates).where(eq(memories.id, id)).run();
|
|
884
|
+
return json(res, { id, updated: true });
|
|
885
|
+
}
|
|
886
|
+
if (url === "/api/permissions" && req.method === "POST") {
|
|
887
|
+
const body = await parseBody(req);
|
|
888
|
+
const agentId = body.agentId;
|
|
889
|
+
const domain = body.domain;
|
|
890
|
+
const canRead = body.canRead !== false ? 1 : 0;
|
|
891
|
+
const canWrite = body.canWrite !== false ? 1 : 0;
|
|
892
|
+
const existing = db.select().from(agentPermissions).where(
|
|
893
|
+
and(
|
|
894
|
+
eq(agentPermissions.agentId, agentId),
|
|
895
|
+
eq(agentPermissions.domain, domain)
|
|
896
|
+
)
|
|
897
|
+
).get();
|
|
898
|
+
if (existing) {
|
|
899
|
+
db.update(agentPermissions).set({ canRead, canWrite }).where(
|
|
900
|
+
and(
|
|
901
|
+
eq(agentPermissions.agentId, agentId),
|
|
902
|
+
eq(agentPermissions.domain, domain)
|
|
903
|
+
)
|
|
904
|
+
).run();
|
|
905
|
+
} else {
|
|
906
|
+
db.insert(agentPermissions).values({ agentId, domain, canRead, canWrite }).run();
|
|
907
|
+
}
|
|
908
|
+
return json(res, { agentId, domain, canRead: !!canRead, canWrite: !!canWrite });
|
|
909
|
+
}
|
|
910
|
+
if (url === "/api/clear-all" && req.method === "POST") {
|
|
911
|
+
const timestamp = now();
|
|
912
|
+
sqlite.prepare(`UPDATE memories SET deleted_at = ? WHERE deleted_at IS NULL`).run(timestamp);
|
|
913
|
+
return json(res, { cleared: true });
|
|
914
|
+
}
|
|
915
|
+
json(res, { error: "Not found" }, 404);
|
|
916
|
+
} catch (e) {
|
|
917
|
+
const message = e instanceof Error ? e.message : "Unknown error";
|
|
918
|
+
json(res, { error: message }, 500);
|
|
919
|
+
}
|
|
920
|
+
});
|
|
921
|
+
server.listen(port, () => {
|
|
922
|
+
});
|
|
923
|
+
return server;
|
|
924
|
+
}
|
|
925
|
+
var init_http = __esm({
|
|
926
|
+
"src/http.ts"() {
|
|
927
|
+
"use strict";
|
|
928
|
+
init_dist();
|
|
929
|
+
}
|
|
930
|
+
});
|
|
931
|
+
|
|
932
|
+
// src/server.ts
|
|
933
|
+
init_dist();
|
|
934
|
+
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
935
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
936
|
+
import { z } from "zod";
|
|
937
|
+
import { eq as eq2, and as and2, isNull as isNull2 } from "drizzle-orm";
|
|
938
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
939
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
940
|
+
function generateId2() {
|
|
941
|
+
return randomBytes2(16).toString("hex");
|
|
942
|
+
}
|
|
943
|
+
function now2() {
|
|
944
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
945
|
+
}
|
|
946
|
+
function textResult(data) {
|
|
947
|
+
return {
|
|
948
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
async function startServer() {
|
|
952
|
+
const server = new McpServer({
|
|
953
|
+
name: "engrams",
|
|
954
|
+
version: "0.1.0"
|
|
955
|
+
});
|
|
956
|
+
const { db, sqlite, vecAvailable } = createDatabase();
|
|
957
|
+
let lastDecayRun = 0;
|
|
958
|
+
const DECAY_THROTTLE_MS = 60 * 60 * 1e3;
|
|
959
|
+
function maybeRunDecay() {
|
|
960
|
+
const now3 = Date.now();
|
|
961
|
+
if (now3 - lastDecayRun > DECAY_THROTTLE_MS) {
|
|
962
|
+
applyConfidenceDecay(sqlite);
|
|
963
|
+
lastDecayRun = now3;
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
if (vecAvailable) {
|
|
967
|
+
backfillEmbeddings(sqlite).then((count) => {
|
|
968
|
+
if (count > 0) process.stderr.write(`[engrams] Backfilled embeddings for ${count} memories
|
|
969
|
+
`);
|
|
970
|
+
}).catch(() => {
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
server.resource("memory-instructions", "memory://instructions", async (uri) => {
|
|
974
|
+
return {
|
|
975
|
+
contents: [
|
|
976
|
+
{
|
|
977
|
+
uri: uri.href,
|
|
978
|
+
mimeType: "text/plain",
|
|
979
|
+
text: `# Engrams \u2014 Memory Guidelines
|
|
980
|
+
|
|
981
|
+
You have access to Engrams, a persistent memory system shared across all AI tools this user connects. Memories you save here are available in future conversations and across other tools (Claude Code, Cursor, Claude Desktop, etc.).
|
|
982
|
+
|
|
983
|
+
## When to save a memory
|
|
984
|
+
- User states a preference ("I prefer morning meetings", "I use vim keybindings")
|
|
985
|
+
- User corrects you or provides factual context about themselves ("I'm a PM, not an engineer", "My team uses pnpm, not npm")
|
|
986
|
+
- User shares information useful across future conversations (goals, relationships, routines, project context)
|
|
987
|
+
- You observe a consistent pattern in the user's behavior (inferred, lower confidence)
|
|
988
|
+
- Another agent shared knowledge relevant to this conversation (cross-agent)
|
|
989
|
+
|
|
990
|
+
## When NOT to save a memory
|
|
991
|
+
- Ephemeral task details ("fix the bug on line 42") \u2014 these don't persist across conversations
|
|
992
|
+
- Information already in the codebase or git history \u2014 read the source of truth instead
|
|
993
|
+
- Debugging steps or temporary workarounds
|
|
994
|
+
- Anything the user asks you not to remember
|
|
995
|
+
|
|
996
|
+
## How to use memories
|
|
997
|
+
- Search Engrams at the start of conversations when context about the user would help
|
|
998
|
+
- Before asking the user a question, check if the answer is already in memory
|
|
999
|
+
- When a memory helps you give a better response, use it but don't narrate that you searched
|
|
1000
|
+
- If you act on a memory and the user confirms it was helpful, call memory_confirm
|
|
1001
|
+
- If you act on a memory and it was wrong, call memory_flag_mistake
|
|
1002
|
+
- If the user corrects a memory, call memory_correct with the updated content
|
|
1003
|
+
|
|
1004
|
+
## Source types
|
|
1005
|
+
- "stated": User explicitly told you (highest initial confidence: 0.90)
|
|
1006
|
+
- "observed": You noticed from the user's actions (0.75)
|
|
1007
|
+
- "inferred": You deduced from context (0.65)
|
|
1008
|
+
- "cross-agent": Another agent shared this (0.70)
|
|
1009
|
+
|
|
1010
|
+
## Domains
|
|
1011
|
+
Organize memories by life domain: general, work, health, finance, relationships, daily-life, learning, creative, or any domain that fits. Use consistent domain names.`
|
|
1012
|
+
}
|
|
1013
|
+
]
|
|
1014
|
+
};
|
|
1015
|
+
});
|
|
1016
|
+
const WRITE_SIMILARITY_THRESHOLD = 0.7;
|
|
1017
|
+
server.tool(
|
|
1018
|
+
"memory_write",
|
|
1019
|
+
"Store a new memory. If a similar memory already exists, returns the existing memory and resolution options instead of writing immediately. Call again with 'resolution' and 'existing_memory_id' to resolve. Pass resolution: 'keep_both' to skip dedup check and force a new memory.",
|
|
1020
|
+
{
|
|
1021
|
+
content: z.string().describe("The memory content"),
|
|
1022
|
+
domain: z.string().optional().describe("Life domain (default: general)"),
|
|
1023
|
+
detail: z.string().optional().describe("Extended context"),
|
|
1024
|
+
sourceAgentId: z.string().describe("Your agent ID"),
|
|
1025
|
+
sourceAgentName: z.string().describe("Your agent name"),
|
|
1026
|
+
sourceType: z.enum(["stated", "inferred", "observed", "cross-agent"]).describe("How this memory was acquired"),
|
|
1027
|
+
sourceDescription: z.string().optional().describe("Description of source"),
|
|
1028
|
+
entityType: z.enum(["person", "organization", "place", "project", "preference", "event", "goal", "fact"]).optional().describe("Entity classification. If omitted, auto-classification runs in background."),
|
|
1029
|
+
entityName: z.string().optional().describe("Canonical entity name (e.g. 'Sarah Chen', not 'my manager Sarah'). Helps with dedup."),
|
|
1030
|
+
structuredData: z.record(z.unknown()).optional().describe("Type-specific structured fields (schema depends on entityType)"),
|
|
1031
|
+
force: z.boolean().optional().describe("Deprecated \u2014 use resolution: 'keep_both' instead"),
|
|
1032
|
+
resolution: z.enum(["update", "correct", "add_detail", "keep_both", "skip"]).optional().describe("How to resolve a similarity match"),
|
|
1033
|
+
existingMemoryId: z.string().optional().describe("ID of existing memory to act on (required for update/correct/add_detail)")
|
|
1034
|
+
},
|
|
1035
|
+
async (params) => {
|
|
1036
|
+
if (params.resolution && params.resolution !== "keep_both") {
|
|
1037
|
+
if (params.resolution === "skip") {
|
|
1038
|
+
return textResult({ status: "skipped", message: "No changes made" });
|
|
1039
|
+
}
|
|
1040
|
+
if (!params.existingMemoryId) {
|
|
1041
|
+
return textResult({ error: "existing_memory_id is required for resolution: " + params.resolution });
|
|
1042
|
+
}
|
|
1043
|
+
const existing = db.select().from(memories).where(and2(eq2(memories.id, params.existingMemoryId), isNull2(memories.deletedAt))).get();
|
|
1044
|
+
if (!existing) {
|
|
1045
|
+
return textResult({ error: "Existing memory not found or deleted" });
|
|
1046
|
+
}
|
|
1047
|
+
const timestamp2 = now2();
|
|
1048
|
+
if (params.resolution === "update") {
|
|
1049
|
+
const newConfidence = Math.min(existing.confidence + 0.02, 0.99);
|
|
1050
|
+
db.update(memories).set({
|
|
1051
|
+
content: params.content,
|
|
1052
|
+
detail: params.detail ?? existing.detail,
|
|
1053
|
+
confidence: newConfidence
|
|
1054
|
+
}).where(eq2(memories.id, params.existingMemoryId)).run();
|
|
1055
|
+
if (vecAvailable) {
|
|
1056
|
+
try {
|
|
1057
|
+
const detail = params.detail ?? existing.detail;
|
|
1058
|
+
const embeddingText = params.content + (detail ? " " + detail : "");
|
|
1059
|
+
const embedding2 = await generateEmbedding(embeddingText);
|
|
1060
|
+
insertEmbedding(sqlite, params.existingMemoryId, embedding2);
|
|
1061
|
+
} catch {
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
db.insert(memoryEvents).values({
|
|
1065
|
+
id: generateId2(),
|
|
1066
|
+
memoryId: params.existingMemoryId,
|
|
1067
|
+
eventType: "updated",
|
|
1068
|
+
agentId: params.sourceAgentId,
|
|
1069
|
+
agentName: params.sourceAgentName,
|
|
1070
|
+
oldValue: JSON.stringify({ content: existing.content, detail: existing.detail }),
|
|
1071
|
+
newValue: JSON.stringify({ content: params.content, detail: params.detail ?? existing.detail }),
|
|
1072
|
+
timestamp: timestamp2
|
|
1073
|
+
}).run();
|
|
1074
|
+
bumpLastModified(sqlite);
|
|
1075
|
+
return textResult({
|
|
1076
|
+
status: "updated",
|
|
1077
|
+
id: params.existingMemoryId,
|
|
1078
|
+
previousConfidence: existing.confidence,
|
|
1079
|
+
newConfidence
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
if (params.resolution === "correct") {
|
|
1083
|
+
const newConfidence = Math.min(Math.max(existing.confidence, 0.85), 0.99);
|
|
1084
|
+
db.update(memories).set({
|
|
1085
|
+
content: params.content,
|
|
1086
|
+
detail: params.detail ?? existing.detail,
|
|
1087
|
+
confidence: newConfidence,
|
|
1088
|
+
correctedCount: existing.correctedCount + 1
|
|
1089
|
+
}).where(eq2(memories.id, params.existingMemoryId)).run();
|
|
1090
|
+
if (vecAvailable) {
|
|
1091
|
+
try {
|
|
1092
|
+
const detail = params.detail ?? existing.detail;
|
|
1093
|
+
const embeddingText = params.content + (detail ? " " + detail : "");
|
|
1094
|
+
const embedding2 = await generateEmbedding(embeddingText);
|
|
1095
|
+
insertEmbedding(sqlite, params.existingMemoryId, embedding2);
|
|
1096
|
+
} catch {
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
db.insert(memoryEvents).values({
|
|
1100
|
+
id: generateId2(),
|
|
1101
|
+
memoryId: params.existingMemoryId,
|
|
1102
|
+
eventType: "corrected",
|
|
1103
|
+
agentId: params.sourceAgentId,
|
|
1104
|
+
agentName: params.sourceAgentName,
|
|
1105
|
+
oldValue: JSON.stringify({ content: existing.content, confidence: existing.confidence }),
|
|
1106
|
+
newValue: JSON.stringify({ content: params.content, confidence: newConfidence }),
|
|
1107
|
+
timestamp: timestamp2
|
|
1108
|
+
}).run();
|
|
1109
|
+
bumpLastModified(sqlite);
|
|
1110
|
+
return textResult({
|
|
1111
|
+
status: "corrected",
|
|
1112
|
+
id: params.existingMemoryId,
|
|
1113
|
+
previousConfidence: existing.confidence,
|
|
1114
|
+
newConfidence,
|
|
1115
|
+
correctedCount: existing.correctedCount + 1
|
|
1116
|
+
});
|
|
1117
|
+
}
|
|
1118
|
+
if (params.resolution === "add_detail") {
|
|
1119
|
+
const separator = existing.detail ? "\n" : "";
|
|
1120
|
+
const newDetail = (existing.detail ?? "") + separator + params.content;
|
|
1121
|
+
db.update(memories).set({ detail: newDetail }).where(eq2(memories.id, params.existingMemoryId)).run();
|
|
1122
|
+
if (vecAvailable) {
|
|
1123
|
+
try {
|
|
1124
|
+
const embeddingText = existing.content + " " + newDetail;
|
|
1125
|
+
const embedding2 = await generateEmbedding(embeddingText);
|
|
1126
|
+
insertEmbedding(sqlite, params.existingMemoryId, embedding2);
|
|
1127
|
+
} catch {
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
db.insert(memoryEvents).values({
|
|
1131
|
+
id: generateId2(),
|
|
1132
|
+
memoryId: params.existingMemoryId,
|
|
1133
|
+
eventType: "updated",
|
|
1134
|
+
agentId: params.sourceAgentId,
|
|
1135
|
+
agentName: params.sourceAgentName,
|
|
1136
|
+
oldValue: JSON.stringify({ detail: existing.detail }),
|
|
1137
|
+
newValue: JSON.stringify({ detail: newDetail }),
|
|
1138
|
+
timestamp: timestamp2
|
|
1139
|
+
}).run();
|
|
1140
|
+
bumpLastModified(sqlite);
|
|
1141
|
+
return textResult({
|
|
1142
|
+
status: "detail_appended",
|
|
1143
|
+
id: params.existingMemoryId,
|
|
1144
|
+
newDetail
|
|
1145
|
+
});
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
const skipDedup = params.resolution === "keep_both" || params.force;
|
|
1149
|
+
let embedding = null;
|
|
1150
|
+
if (!skipDedup) {
|
|
1151
|
+
const embeddingText = params.content + (params.detail ? " " + params.detail : "");
|
|
1152
|
+
if (vecAvailable) {
|
|
1153
|
+
try {
|
|
1154
|
+
embedding = await generateEmbedding(embeddingText);
|
|
1155
|
+
const similar = searchVec(sqlite, embedding, 3);
|
|
1156
|
+
const closeMatches = similar.filter((s) => 1 - s.distance >= WRITE_SIMILARITY_THRESHOLD);
|
|
1157
|
+
if (closeMatches.length > 0) {
|
|
1158
|
+
const matchedMemories = closeMatches.map((m) => {
|
|
1159
|
+
const row = sqlite.prepare(`SELECT * FROM memories WHERE id = ? AND deleted_at IS NULL`).get(m.memory_id);
|
|
1160
|
+
if (!row) return null;
|
|
1161
|
+
return {
|
|
1162
|
+
id: row.id,
|
|
1163
|
+
content: row.content,
|
|
1164
|
+
detail: row.detail,
|
|
1165
|
+
confidence: row.confidence,
|
|
1166
|
+
similarity: Math.round((1 - m.distance) * 100) / 100
|
|
1167
|
+
};
|
|
1168
|
+
}).filter(Boolean);
|
|
1169
|
+
if (matchedMemories.length > 0) {
|
|
1170
|
+
return textResult({
|
|
1171
|
+
status: "similar_found",
|
|
1172
|
+
proposed: {
|
|
1173
|
+
content: params.content,
|
|
1174
|
+
detail: params.detail ?? null,
|
|
1175
|
+
domain: params.domain ?? "general"
|
|
1176
|
+
},
|
|
1177
|
+
similar: matchedMemories,
|
|
1178
|
+
options: [
|
|
1179
|
+
"update \u2014 replace the existing memory's content with the new content",
|
|
1180
|
+
"correct \u2014 existing was wrong; update it and boost confidence to min(max(existing, 0.85), 0.99)",
|
|
1181
|
+
"add_detail \u2014 append new content to the existing memory's detail field",
|
|
1182
|
+
"keep_both \u2014 store as a new memory (not a duplicate)",
|
|
1183
|
+
"skip \u2014 existing memory is already accurate, don't write anything"
|
|
1184
|
+
],
|
|
1185
|
+
message: "Similar memory found. Respond with memory_write again including resolution and existingMemoryId to proceed."
|
|
1186
|
+
});
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
} catch {
|
|
1190
|
+
}
|
|
1191
|
+
} else {
|
|
1192
|
+
const dedupResults = searchFTS(sqlite, params.content, 3);
|
|
1193
|
+
if (dedupResults.length > 0) {
|
|
1194
|
+
const rowids = dedupResults.map((r) => r.rowid);
|
|
1195
|
+
const placeholders = rowids.map(() => "?").join(",");
|
|
1196
|
+
const existing = sqlite.prepare(
|
|
1197
|
+
`SELECT * FROM memories WHERE rowid IN (${placeholders}) AND deleted_at IS NULL`
|
|
1198
|
+
).all(...rowids);
|
|
1199
|
+
if (existing.length > 0) {
|
|
1200
|
+
return textResult({
|
|
1201
|
+
status: "similar_found",
|
|
1202
|
+
proposed: {
|
|
1203
|
+
content: params.content,
|
|
1204
|
+
detail: params.detail ?? null,
|
|
1205
|
+
domain: params.domain ?? "general"
|
|
1206
|
+
},
|
|
1207
|
+
similar: existing.map((e) => ({
|
|
1208
|
+
id: e.id,
|
|
1209
|
+
content: e.content,
|
|
1210
|
+
detail: e.detail,
|
|
1211
|
+
confidence: e.confidence,
|
|
1212
|
+
similarity: null
|
|
1213
|
+
// FTS5 doesn't provide cosine similarity
|
|
1214
|
+
})),
|
|
1215
|
+
options: [
|
|
1216
|
+
"update \u2014 replace the existing memory's content with the new content",
|
|
1217
|
+
"correct \u2014 existing was wrong; update it and boost confidence to min(max(existing, 0.85), 0.99)",
|
|
1218
|
+
"add_detail \u2014 append new content to the existing memory's detail field",
|
|
1219
|
+
"keep_both \u2014 store as a new memory (not a duplicate)",
|
|
1220
|
+
"skip \u2014 existing memory is already accurate, don't write anything"
|
|
1221
|
+
],
|
|
1222
|
+
message: "Similar memory found. Respond with memory_write again including resolution and existingMemoryId to proceed."
|
|
1223
|
+
});
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
if (params.entityName && params.entityType) {
|
|
1228
|
+
const entityMatches = sqlite.prepare(
|
|
1229
|
+
`SELECT * FROM memories
|
|
1230
|
+
WHERE entity_type = ? AND entity_name = ? COLLATE NOCASE
|
|
1231
|
+
AND deleted_at IS NULL`
|
|
1232
|
+
).all(params.entityType, params.entityName);
|
|
1233
|
+
if (entityMatches.length > 0) {
|
|
1234
|
+
return textResult({
|
|
1235
|
+
status: "similar_found",
|
|
1236
|
+
proposed: {
|
|
1237
|
+
content: params.content,
|
|
1238
|
+
detail: params.detail ?? null,
|
|
1239
|
+
domain: params.domain ?? "general"
|
|
1240
|
+
},
|
|
1241
|
+
similar: entityMatches.map((e) => ({
|
|
1242
|
+
id: e.id,
|
|
1243
|
+
content: e.content,
|
|
1244
|
+
detail: e.detail,
|
|
1245
|
+
confidence: e.confidence,
|
|
1246
|
+
similarity: null,
|
|
1247
|
+
entity_match: true
|
|
1248
|
+
})),
|
|
1249
|
+
options: [
|
|
1250
|
+
"update \u2014 replace the existing memory's content with the new content",
|
|
1251
|
+
"correct \u2014 existing was wrong; update it and boost confidence to min(max(existing, 0.85), 0.99)",
|
|
1252
|
+
"add_detail \u2014 append new content to the existing memory's detail field",
|
|
1253
|
+
"keep_both \u2014 store as a new memory (not a duplicate)",
|
|
1254
|
+
"skip \u2014 existing memory is already accurate, don't write anything"
|
|
1255
|
+
],
|
|
1256
|
+
message: `Existing memory found for ${params.entityType} "${params.entityName}". Respond with memory_write again including resolution and existingMemoryId to proceed.`
|
|
1257
|
+
});
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
const VALID_ENTITY_TYPES = ["person", "organization", "place", "project", "preference", "event", "goal", "fact"];
|
|
1262
|
+
if (params.entityType && !VALID_ENTITY_TYPES.includes(params.entityType)) {
|
|
1263
|
+
return textResult({ error: `Invalid entity_type: "${params.entityType}". Must be one of: ${VALID_ENTITY_TYPES.join(", ")}` });
|
|
1264
|
+
}
|
|
1265
|
+
const id = generateId2();
|
|
1266
|
+
const confidence = getInitialConfidence(params.sourceType);
|
|
1267
|
+
const timestamp = now2();
|
|
1268
|
+
const piiText = params.content + (params.detail ? " " + params.detail : "");
|
|
1269
|
+
const piiMatches = detectSensitiveData(piiText);
|
|
1270
|
+
const hasPii = piiMatches.length > 0;
|
|
1271
|
+
db.insert(memories).values({
|
|
1272
|
+
id,
|
|
1273
|
+
content: params.content,
|
|
1274
|
+
detail: params.detail ?? null,
|
|
1275
|
+
domain: params.domain ?? "general",
|
|
1276
|
+
sourceAgentId: params.sourceAgentId,
|
|
1277
|
+
sourceAgentName: params.sourceAgentName,
|
|
1278
|
+
sourceType: params.sourceType,
|
|
1279
|
+
sourceDescription: params.sourceDescription ?? null,
|
|
1280
|
+
confidence,
|
|
1281
|
+
learnedAt: timestamp,
|
|
1282
|
+
hasPiiFlag: hasPii ? 1 : 0,
|
|
1283
|
+
entityType: params.entityType ?? null,
|
|
1284
|
+
entityName: params.entityName ?? null,
|
|
1285
|
+
structuredData: params.structuredData ? JSON.stringify(params.structuredData) : null
|
|
1286
|
+
}).run();
|
|
1287
|
+
if (vecAvailable) {
|
|
1288
|
+
try {
|
|
1289
|
+
if (!embedding) {
|
|
1290
|
+
const embeddingText = params.content + (params.detail ? " " + params.detail : "");
|
|
1291
|
+
embedding = await generateEmbedding(embeddingText);
|
|
1292
|
+
}
|
|
1293
|
+
insertEmbedding(sqlite, id, embedding);
|
|
1294
|
+
} catch {
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
db.insert(memoryEvents).values({
|
|
1298
|
+
id: generateId2(),
|
|
1299
|
+
memoryId: id,
|
|
1300
|
+
eventType: "created",
|
|
1301
|
+
agentId: params.sourceAgentId,
|
|
1302
|
+
agentName: params.sourceAgentName,
|
|
1303
|
+
newValue: JSON.stringify({ content: params.content, domain: params.domain ?? "general" }),
|
|
1304
|
+
timestamp
|
|
1305
|
+
}).run();
|
|
1306
|
+
if (!params.entityType && process.env.ANTHROPIC_API_KEY) {
|
|
1307
|
+
(async () => {
|
|
1308
|
+
try {
|
|
1309
|
+
const existingNames = sqlite.prepare(`SELECT DISTINCT entity_name FROM memories WHERE entity_name IS NOT NULL AND deleted_at IS NULL`).all();
|
|
1310
|
+
const extraction = await extractEntity(
|
|
1311
|
+
params.content,
|
|
1312
|
+
params.detail ?? null,
|
|
1313
|
+
existingNames.map((r) => r.entity_name)
|
|
1314
|
+
);
|
|
1315
|
+
const current = sqlite.prepare(`SELECT entity_type FROM memories WHERE id = ? AND deleted_at IS NULL`).get(id);
|
|
1316
|
+
if (!current || current.entity_type) return;
|
|
1317
|
+
sqlite.transaction(() => {
|
|
1318
|
+
sqlite.prepare(
|
|
1319
|
+
`UPDATE memories SET entity_type = ?, entity_name = ?, structured_data = ? WHERE id = ? AND entity_type IS NULL AND deleted_at IS NULL`
|
|
1320
|
+
).run(
|
|
1321
|
+
extraction.entity_type,
|
|
1322
|
+
extraction.entity_name,
|
|
1323
|
+
JSON.stringify(extraction.structured_data),
|
|
1324
|
+
id
|
|
1325
|
+
);
|
|
1326
|
+
for (const conn of extraction.suggested_connections) {
|
|
1327
|
+
const target = sqlite.prepare(`SELECT id FROM memories WHERE entity_name = ? COLLATE NOCASE AND deleted_at IS NULL LIMIT 1`).get(conn.target_entity_name);
|
|
1328
|
+
if (target && target.id !== id) {
|
|
1329
|
+
sqlite.prepare(
|
|
1330
|
+
`INSERT INTO memory_connections (source_memory_id, target_memory_id, relationship) VALUES (?, ?, ?)`
|
|
1331
|
+
).run(id, target.id, conn.relationship);
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
})();
|
|
1335
|
+
bumpLastModified(sqlite);
|
|
1336
|
+
} catch {
|
|
1337
|
+
}
|
|
1338
|
+
})();
|
|
1339
|
+
}
|
|
1340
|
+
const fullText = params.content + (params.detail ? " " + params.detail : "");
|
|
1341
|
+
const sentences = fullText.split(/(?<=[.!?])\s+/).filter((s) => s.length > 10);
|
|
1342
|
+
let splitSuggestion = null;
|
|
1343
|
+
if (sentences.length >= 3 && process.env.ANTHROPIC_API_KEY) {
|
|
1344
|
+
try {
|
|
1345
|
+
const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
|
|
1346
|
+
const resp = await client.messages.create({
|
|
1347
|
+
model: "claude-haiku-4-5-20251001",
|
|
1348
|
+
max_tokens: 512,
|
|
1349
|
+
messages: [
|
|
1350
|
+
{
|
|
1351
|
+
role: "user",
|
|
1352
|
+
content: `Analyze this memory and determine if it contains multiple distinct topics that should be stored separately. Only suggest splitting if the topics are genuinely independent (would be searched for separately).
|
|
1353
|
+
|
|
1354
|
+
Memory content: ${JSON.stringify(params.content)}
|
|
1355
|
+
Memory detail: ${JSON.stringify(params.detail ?? null)}
|
|
1356
|
+
|
|
1357
|
+
Respond with ONLY valid JSON:
|
|
1358
|
+
- If it should NOT be split: {"should_split": false}
|
|
1359
|
+
- If it SHOULD be split: {"should_split": true, "parts": [{"content": "...", "detail": "..."}, ...]}
|
|
1360
|
+
|
|
1361
|
+
Each part should have a concise "content" (one sentence) and optional "detail". Do not split if the content is a single coherent topic.`
|
|
1362
|
+
}
|
|
1363
|
+
]
|
|
1364
|
+
});
|
|
1365
|
+
const text2 = resp.content[0].type === "text" ? resp.content[0].text : "";
|
|
1366
|
+
splitSuggestion = JSON.parse(text2);
|
|
1367
|
+
} catch {
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
bumpLastModified(sqlite);
|
|
1371
|
+
const result = {
|
|
1372
|
+
id,
|
|
1373
|
+
confidence,
|
|
1374
|
+
domain: params.domain ?? "general",
|
|
1375
|
+
created: true,
|
|
1376
|
+
entityType: params.entityType ?? null,
|
|
1377
|
+
entityName: params.entityName ?? null
|
|
1378
|
+
};
|
|
1379
|
+
if (!params.entityType && process.env.ANTHROPIC_API_KEY) {
|
|
1380
|
+
result._background_classification = "running";
|
|
1381
|
+
}
|
|
1382
|
+
if (hasPii) {
|
|
1383
|
+
result._pii_detected = [...new Set(piiMatches.map((m) => m.type))];
|
|
1384
|
+
}
|
|
1385
|
+
if (splitSuggestion?.should_split && splitSuggestion.parts) {
|
|
1386
|
+
result.split_suggested = true;
|
|
1387
|
+
result.suggested_parts = splitSuggestion.parts;
|
|
1388
|
+
result.message = "This memory appears to contain multiple distinct topics. Consider calling memory_split to separate them.";
|
|
1389
|
+
}
|
|
1390
|
+
return textResult(result);
|
|
1391
|
+
}
|
|
1392
|
+
);
|
|
1393
|
+
server.tool(
|
|
1394
|
+
"memory_search",
|
|
1395
|
+
"Search the user's persistent memory for relevant context. Call this at the start of conversations or before answering questions where prior knowledge about the user would help. Also call before asking the user something \u2014 the answer may already be in memory.",
|
|
1396
|
+
{
|
|
1397
|
+
query: z.string().describe("Search query"),
|
|
1398
|
+
domain: z.string().optional().describe("Filter by domain"),
|
|
1399
|
+
entityType: z.enum(["person", "organization", "place", "project", "preference", "event", "goal", "fact"]).optional().describe("Filter by entity type"),
|
|
1400
|
+
entityName: z.string().optional().describe("Filter by entity name (case-insensitive)"),
|
|
1401
|
+
minConfidence: z.number().optional().describe("Minimum confidence threshold"),
|
|
1402
|
+
limit: z.number().optional().describe("Max results (default 20)"),
|
|
1403
|
+
expand: z.boolean().optional().describe("Include connected memories (default true)"),
|
|
1404
|
+
maxDepth: z.number().optional().describe("Max graph expansion depth (default 3)"),
|
|
1405
|
+
similarityThreshold: z.number().optional().describe("Min similarity for connected memories (default 0.5)")
|
|
1406
|
+
},
|
|
1407
|
+
async (params) => {
|
|
1408
|
+
maybeRunDecay();
|
|
1409
|
+
const limit = params.limit ?? 20;
|
|
1410
|
+
const { results: searchResults, cached: wasCached } = await hybridSearch(sqlite, params.query, {
|
|
1411
|
+
domain: params.domain,
|
|
1412
|
+
entityType: params.entityType,
|
|
1413
|
+
entityName: params.entityName,
|
|
1414
|
+
minConfidence: params.minConfidence,
|
|
1415
|
+
limit,
|
|
1416
|
+
expand: params.expand,
|
|
1417
|
+
maxDepth: params.maxDepth,
|
|
1418
|
+
similarityThreshold: params.similarityThreshold
|
|
1419
|
+
});
|
|
1420
|
+
if (searchResults.length === 0) {
|
|
1421
|
+
return textResult({ memories: [], count: 0, totalConnected: 0, cached: wasCached });
|
|
1422
|
+
}
|
|
1423
|
+
const timestamp = now2();
|
|
1424
|
+
const updateStmt = sqlite.prepare(
|
|
1425
|
+
`UPDATE memories SET used_count = used_count + 1, last_used_at = ? WHERE id = ?`
|
|
1426
|
+
);
|
|
1427
|
+
const insertEventStmt = sqlite.prepare(
|
|
1428
|
+
`INSERT INTO memory_events (id, memory_id, event_type, timestamp) VALUES (?, ?, 'used', ?)`
|
|
1429
|
+
);
|
|
1430
|
+
const batchUpdate = sqlite.transaction(() => {
|
|
1431
|
+
for (const r of searchResults) {
|
|
1432
|
+
updateStmt.run(timestamp, r.memory.id);
|
|
1433
|
+
insertEventStmt.run(generateId2(), r.memory.id, timestamp);
|
|
1434
|
+
}
|
|
1435
|
+
});
|
|
1436
|
+
batchUpdate();
|
|
1437
|
+
return textResult({
|
|
1438
|
+
memories: searchResults.map((r) => ({
|
|
1439
|
+
...r.memory,
|
|
1440
|
+
_searchScore: r.score,
|
|
1441
|
+
_connected: r.connected.map((c) => ({
|
|
1442
|
+
...c.memory,
|
|
1443
|
+
_relationship: c.relationship,
|
|
1444
|
+
_depth: c.depth,
|
|
1445
|
+
_similarity: c.similarity
|
|
1446
|
+
}))
|
|
1447
|
+
})),
|
|
1448
|
+
count: searchResults.length,
|
|
1449
|
+
totalConnected: searchResults.reduce((sum, r) => sum + r.connected.length, 0),
|
|
1450
|
+
cached: wasCached
|
|
1451
|
+
});
|
|
1452
|
+
}
|
|
1453
|
+
);
|
|
1454
|
+
server.tool(
|
|
1455
|
+
"memory_update",
|
|
1456
|
+
"Update an existing memory's content, detail, or domain",
|
|
1457
|
+
{
|
|
1458
|
+
id: z.string().describe("Memory ID to update"),
|
|
1459
|
+
content: z.string().optional().describe("New content"),
|
|
1460
|
+
detail: z.string().optional().describe("New detail"),
|
|
1461
|
+
domain: z.string().optional().describe("New domain"),
|
|
1462
|
+
agentId: z.string().optional().describe("Your agent ID"),
|
|
1463
|
+
agentName: z.string().optional().describe("Your agent name")
|
|
1464
|
+
},
|
|
1465
|
+
async (params) => {
|
|
1466
|
+
const existing = db.select().from(memories).where(and2(eq2(memories.id, params.id), isNull2(memories.deletedAt))).get();
|
|
1467
|
+
if (!existing) {
|
|
1468
|
+
return textResult({ error: "Memory not found or deleted" });
|
|
1469
|
+
}
|
|
1470
|
+
const updates = {};
|
|
1471
|
+
if (params.content !== void 0) updates.content = params.content;
|
|
1472
|
+
if (params.detail !== void 0) updates.detail = params.detail;
|
|
1473
|
+
if (params.domain !== void 0) updates.domain = params.domain;
|
|
1474
|
+
if (Object.keys(updates).length === 0) {
|
|
1475
|
+
return textResult({ error: "No fields to update" });
|
|
1476
|
+
}
|
|
1477
|
+
db.update(memories).set(updates).where(eq2(memories.id, params.id)).run();
|
|
1478
|
+
if (params.content !== void 0 && vecAvailable) {
|
|
1479
|
+
try {
|
|
1480
|
+
const detail = params.detail ?? existing.detail;
|
|
1481
|
+
const embeddingText = params.content + (detail ? " " + detail : "");
|
|
1482
|
+
const embedding = await generateEmbedding(embeddingText);
|
|
1483
|
+
insertEmbedding(sqlite, params.id, embedding);
|
|
1484
|
+
} catch {
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
db.insert(memoryEvents).values({
|
|
1488
|
+
id: generateId2(),
|
|
1489
|
+
memoryId: params.id,
|
|
1490
|
+
eventType: "confidence_changed",
|
|
1491
|
+
agentId: params.agentId ?? null,
|
|
1492
|
+
agentName: params.agentName ?? null,
|
|
1493
|
+
oldValue: JSON.stringify({
|
|
1494
|
+
content: existing.content,
|
|
1495
|
+
detail: existing.detail,
|
|
1496
|
+
domain: existing.domain
|
|
1497
|
+
}),
|
|
1498
|
+
newValue: JSON.stringify(updates),
|
|
1499
|
+
timestamp: now2()
|
|
1500
|
+
}).run();
|
|
1501
|
+
bumpLastModified(sqlite);
|
|
1502
|
+
return textResult({ id: params.id, updated: true, changes: updates });
|
|
1503
|
+
}
|
|
1504
|
+
);
|
|
1505
|
+
server.tool(
|
|
1506
|
+
"memory_remove",
|
|
1507
|
+
"Delete a memory the user no longer wants stored. Call when the user explicitly asks to forget something.",
|
|
1508
|
+
{
|
|
1509
|
+
id: z.string().describe("Memory ID to remove"),
|
|
1510
|
+
reason: z.string().optional().describe("Reason for removal"),
|
|
1511
|
+
agentId: z.string().optional().describe("Your agent ID"),
|
|
1512
|
+
agentName: z.string().optional().describe("Your agent name")
|
|
1513
|
+
},
|
|
1514
|
+
async (params) => {
|
|
1515
|
+
const existing = db.select().from(memories).where(and2(eq2(memories.id, params.id), isNull2(memories.deletedAt))).get();
|
|
1516
|
+
if (!existing) {
|
|
1517
|
+
return textResult({ error: "Memory not found or already deleted" });
|
|
1518
|
+
}
|
|
1519
|
+
const timestamp = now2();
|
|
1520
|
+
db.update(memories).set({ deletedAt: timestamp }).where(eq2(memories.id, params.id)).run();
|
|
1521
|
+
db.insert(memoryEvents).values({
|
|
1522
|
+
id: generateId2(),
|
|
1523
|
+
memoryId: params.id,
|
|
1524
|
+
eventType: "removed",
|
|
1525
|
+
agentId: params.agentId ?? null,
|
|
1526
|
+
agentName: params.agentName ?? null,
|
|
1527
|
+
newValue: JSON.stringify({ reason: params.reason }),
|
|
1528
|
+
timestamp
|
|
1529
|
+
}).run();
|
|
1530
|
+
bumpLastModified(sqlite);
|
|
1531
|
+
return textResult({ id: params.id, removed: true });
|
|
1532
|
+
}
|
|
1533
|
+
);
|
|
1534
|
+
server.tool(
|
|
1535
|
+
"memory_confirm",
|
|
1536
|
+
"Confirm a memory is still accurate. Call this when you act on a memory and the user validates it was correct, or when the user reaffirms something you already know.",
|
|
1537
|
+
{
|
|
1538
|
+
id: z.string().describe("Memory ID to confirm"),
|
|
1539
|
+
agentId: z.string().optional().describe("Your agent ID"),
|
|
1540
|
+
agentName: z.string().optional().describe("Your agent name")
|
|
1541
|
+
},
|
|
1542
|
+
async (params) => {
|
|
1543
|
+
const existing = db.select().from(memories).where(and2(eq2(memories.id, params.id), isNull2(memories.deletedAt))).get();
|
|
1544
|
+
if (!existing) {
|
|
1545
|
+
return textResult({ error: "Memory not found or deleted" });
|
|
1546
|
+
}
|
|
1547
|
+
const newConfidence = applyConfirm(existing.confidence);
|
|
1548
|
+
const timestamp = now2();
|
|
1549
|
+
db.update(memories).set({
|
|
1550
|
+
confidence: newConfidence,
|
|
1551
|
+
confirmedCount: existing.confirmedCount + 1,
|
|
1552
|
+
confirmedAt: timestamp
|
|
1553
|
+
}).where(eq2(memories.id, params.id)).run();
|
|
1554
|
+
db.insert(memoryEvents).values({
|
|
1555
|
+
id: generateId2(),
|
|
1556
|
+
memoryId: params.id,
|
|
1557
|
+
eventType: "confirmed",
|
|
1558
|
+
agentId: params.agentId ?? null,
|
|
1559
|
+
agentName: params.agentName ?? null,
|
|
1560
|
+
oldValue: JSON.stringify({ confidence: existing.confidence }),
|
|
1561
|
+
newValue: JSON.stringify({ confidence: newConfidence }),
|
|
1562
|
+
timestamp
|
|
1563
|
+
}).run();
|
|
1564
|
+
bumpLastModified(sqlite);
|
|
1565
|
+
return textResult({
|
|
1566
|
+
id: params.id,
|
|
1567
|
+
confirmed: true,
|
|
1568
|
+
previousConfidence: existing.confidence,
|
|
1569
|
+
newConfidence,
|
|
1570
|
+
confirmedCount: existing.confirmedCount + 1
|
|
1571
|
+
});
|
|
1572
|
+
}
|
|
1573
|
+
);
|
|
1574
|
+
server.tool(
|
|
1575
|
+
"memory_correct",
|
|
1576
|
+
"Replace a memory's content with corrected information. Call this when the user says a memory is wrong and provides the right answer. Resets confidence to 0.50.",
|
|
1577
|
+
{
|
|
1578
|
+
id: z.string().describe("Memory ID to correct"),
|
|
1579
|
+
content: z.string().describe("The corrected content"),
|
|
1580
|
+
agentId: z.string().optional().describe("Your agent ID"),
|
|
1581
|
+
agentName: z.string().optional().describe("Your agent name")
|
|
1582
|
+
},
|
|
1583
|
+
async (params) => {
|
|
1584
|
+
const existing = db.select().from(memories).where(and2(eq2(memories.id, params.id), isNull2(memories.deletedAt))).get();
|
|
1585
|
+
if (!existing) {
|
|
1586
|
+
return textResult({ error: "Memory not found or deleted" });
|
|
1587
|
+
}
|
|
1588
|
+
const newConfidence = applyCorrect();
|
|
1589
|
+
const timestamp = now2();
|
|
1590
|
+
db.update(memories).set({
|
|
1591
|
+
content: params.content,
|
|
1592
|
+
confidence: newConfidence,
|
|
1593
|
+
correctedCount: existing.correctedCount + 1
|
|
1594
|
+
}).where(eq2(memories.id, params.id)).run();
|
|
1595
|
+
if (vecAvailable) {
|
|
1596
|
+
try {
|
|
1597
|
+
const embedding = await generateEmbedding(params.content);
|
|
1598
|
+
insertEmbedding(sqlite, params.id, embedding);
|
|
1599
|
+
} catch {
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
db.insert(memoryEvents).values({
|
|
1603
|
+
id: generateId2(),
|
|
1604
|
+
memoryId: params.id,
|
|
1605
|
+
eventType: "corrected",
|
|
1606
|
+
agentId: params.agentId ?? null,
|
|
1607
|
+
agentName: params.agentName ?? null,
|
|
1608
|
+
oldValue: JSON.stringify({ content: existing.content, confidence: existing.confidence }),
|
|
1609
|
+
newValue: JSON.stringify({ content: params.content, confidence: newConfidence }),
|
|
1610
|
+
timestamp
|
|
1611
|
+
}).run();
|
|
1612
|
+
bumpLastModified(sqlite);
|
|
1613
|
+
return textResult({
|
|
1614
|
+
id: params.id,
|
|
1615
|
+
corrected: true,
|
|
1616
|
+
previousConfidence: existing.confidence,
|
|
1617
|
+
newConfidence,
|
|
1618
|
+
correctedCount: existing.correctedCount + 1
|
|
1619
|
+
});
|
|
1620
|
+
}
|
|
1621
|
+
);
|
|
1622
|
+
server.tool(
|
|
1623
|
+
"memory_flag_mistake",
|
|
1624
|
+
"Flag a memory as wrong or outdated. Call this when you act on a memory and the outcome was incorrect, or when the user says a memory is no longer true. Degrades confidence by 0.15.",
|
|
1625
|
+
{
|
|
1626
|
+
id: z.string().describe("Memory ID to flag"),
|
|
1627
|
+
agentId: z.string().optional().describe("Your agent ID"),
|
|
1628
|
+
agentName: z.string().optional().describe("Your agent name")
|
|
1629
|
+
},
|
|
1630
|
+
async (params) => {
|
|
1631
|
+
const existing = db.select().from(memories).where(and2(eq2(memories.id, params.id), isNull2(memories.deletedAt))).get();
|
|
1632
|
+
if (!existing) {
|
|
1633
|
+
return textResult({ error: "Memory not found or deleted" });
|
|
1634
|
+
}
|
|
1635
|
+
const newConfidence = applyMistake(existing.confidence);
|
|
1636
|
+
const timestamp = now2();
|
|
1637
|
+
db.update(memories).set({
|
|
1638
|
+
confidence: newConfidence,
|
|
1639
|
+
mistakeCount: existing.mistakeCount + 1
|
|
1640
|
+
}).where(eq2(memories.id, params.id)).run();
|
|
1641
|
+
db.insert(memoryEvents).values({
|
|
1642
|
+
id: generateId2(),
|
|
1643
|
+
memoryId: params.id,
|
|
1644
|
+
eventType: "confidence_changed",
|
|
1645
|
+
agentId: params.agentId ?? null,
|
|
1646
|
+
agentName: params.agentName ?? null,
|
|
1647
|
+
oldValue: JSON.stringify({ confidence: existing.confidence }),
|
|
1648
|
+
newValue: JSON.stringify({ confidence: newConfidence, flaggedAsMistake: true }),
|
|
1649
|
+
timestamp
|
|
1650
|
+
}).run();
|
|
1651
|
+
bumpLastModified(sqlite);
|
|
1652
|
+
return textResult({
|
|
1653
|
+
id: params.id,
|
|
1654
|
+
flagged: true,
|
|
1655
|
+
previousConfidence: existing.confidence,
|
|
1656
|
+
newConfidence,
|
|
1657
|
+
mistakeCount: existing.mistakeCount + 1
|
|
1658
|
+
});
|
|
1659
|
+
}
|
|
1660
|
+
);
|
|
1661
|
+
server.tool(
|
|
1662
|
+
"memory_connect",
|
|
1663
|
+
"Create a relationship between two memories",
|
|
1664
|
+
{
|
|
1665
|
+
sourceMemoryId: z.string().describe("Source memory ID"),
|
|
1666
|
+
targetMemoryId: z.string().describe("Target memory ID"),
|
|
1667
|
+
relationship: z.enum(["influences", "supports", "contradicts", "related", "learned-together", "works_at", "involves", "located_at", "part_of", "about"]).describe("Type of relationship")
|
|
1668
|
+
},
|
|
1669
|
+
async (params) => {
|
|
1670
|
+
if (params.sourceMemoryId === params.targetMemoryId) {
|
|
1671
|
+
return textResult({ error: "Cannot connect a memory to itself" });
|
|
1672
|
+
}
|
|
1673
|
+
const source = db.select().from(memories).where(eq2(memories.id, params.sourceMemoryId)).get();
|
|
1674
|
+
const target = db.select().from(memories).where(eq2(memories.id, params.targetMemoryId)).get();
|
|
1675
|
+
if (!source || !target) {
|
|
1676
|
+
return textResult({ error: "One or both memories not found" });
|
|
1677
|
+
}
|
|
1678
|
+
db.insert(memoryConnections).values({
|
|
1679
|
+
sourceMemoryId: params.sourceMemoryId,
|
|
1680
|
+
targetMemoryId: params.targetMemoryId,
|
|
1681
|
+
relationship: params.relationship
|
|
1682
|
+
}).run();
|
|
1683
|
+
bumpLastModified(sqlite);
|
|
1684
|
+
return textResult({
|
|
1685
|
+
connected: true,
|
|
1686
|
+
sourceMemoryId: params.sourceMemoryId,
|
|
1687
|
+
targetMemoryId: params.targetMemoryId,
|
|
1688
|
+
relationship: params.relationship
|
|
1689
|
+
});
|
|
1690
|
+
}
|
|
1691
|
+
);
|
|
1692
|
+
server.tool(
|
|
1693
|
+
"memory_split",
|
|
1694
|
+
"Split a memory that contains multiple distinct facts into separate memories. Call this when a memory covers more than one topic or would be more useful as individual pieces. Each new memory inherits the original's domain and source metadata, and the originals are connected with 'related' relationships.",
|
|
1695
|
+
{
|
|
1696
|
+
id: z.string().describe("Memory ID to split"),
|
|
1697
|
+
parts: z.array(
|
|
1698
|
+
z.object({
|
|
1699
|
+
content: z.string().describe("Content for this part"),
|
|
1700
|
+
detail: z.string().optional().describe("Optional detail for this part"),
|
|
1701
|
+
domain: z.string().optional().describe("Override domain for this part")
|
|
1702
|
+
})
|
|
1703
|
+
).min(2).describe("The separate memories to create from the original"),
|
|
1704
|
+
agentId: z.string().optional().describe("Your agent ID"),
|
|
1705
|
+
agentName: z.string().optional().describe("Your agent name")
|
|
1706
|
+
},
|
|
1707
|
+
async (params) => {
|
|
1708
|
+
const existing = db.select().from(memories).where(and2(eq2(memories.id, params.id), isNull2(memories.deletedAt))).get();
|
|
1709
|
+
if (!existing) {
|
|
1710
|
+
return textResult({ error: "Memory not found or deleted" });
|
|
1711
|
+
}
|
|
1712
|
+
const timestamp = now2();
|
|
1713
|
+
const newIds = [];
|
|
1714
|
+
for (const part of params.parts) {
|
|
1715
|
+
const newId = generateId2();
|
|
1716
|
+
newIds.push(newId);
|
|
1717
|
+
db.insert(memories).values({
|
|
1718
|
+
id: newId,
|
|
1719
|
+
content: part.content,
|
|
1720
|
+
detail: part.detail ?? null,
|
|
1721
|
+
domain: part.domain ?? existing.domain,
|
|
1722
|
+
sourceAgentId: existing.sourceAgentId,
|
|
1723
|
+
sourceAgentName: existing.sourceAgentName,
|
|
1724
|
+
sourceType: existing.sourceType,
|
|
1725
|
+
sourceDescription: existing.sourceDescription,
|
|
1726
|
+
confidence: Math.min(existing.confidence + 0.05, 0.99),
|
|
1727
|
+
learnedAt: timestamp
|
|
1728
|
+
}).run();
|
|
1729
|
+
if (vecAvailable) {
|
|
1730
|
+
try {
|
|
1731
|
+
const embeddingText = part.content + (part.detail ? " " + part.detail : "");
|
|
1732
|
+
const embedding = await generateEmbedding(embeddingText);
|
|
1733
|
+
insertEmbedding(sqlite, newId, embedding);
|
|
1734
|
+
} catch {
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
db.insert(memoryEvents).values({
|
|
1738
|
+
id: generateId2(),
|
|
1739
|
+
memoryId: newId,
|
|
1740
|
+
eventType: "created",
|
|
1741
|
+
agentId: params.agentId ?? null,
|
|
1742
|
+
agentName: params.agentName ?? null,
|
|
1743
|
+
newValue: JSON.stringify({ content: part.content, splitFrom: params.id }),
|
|
1744
|
+
timestamp
|
|
1745
|
+
}).run();
|
|
1746
|
+
}
|
|
1747
|
+
for (let i = 0; i < newIds.length; i++) {
|
|
1748
|
+
for (let j = i + 1; j < newIds.length; j++) {
|
|
1749
|
+
db.insert(memoryConnections).values({
|
|
1750
|
+
sourceMemoryId: newIds[i],
|
|
1751
|
+
targetMemoryId: newIds[j],
|
|
1752
|
+
relationship: "related"
|
|
1753
|
+
}).run();
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
db.update(memories).set({ deletedAt: timestamp }).where(eq2(memories.id, params.id)).run();
|
|
1757
|
+
db.insert(memoryEvents).values({
|
|
1758
|
+
id: generateId2(),
|
|
1759
|
+
memoryId: params.id,
|
|
1760
|
+
eventType: "removed",
|
|
1761
|
+
agentId: params.agentId ?? null,
|
|
1762
|
+
agentName: params.agentName ?? null,
|
|
1763
|
+
newValue: JSON.stringify({ reason: "split", splitInto: newIds }),
|
|
1764
|
+
timestamp
|
|
1765
|
+
}).run();
|
|
1766
|
+
bumpLastModified(sqlite);
|
|
1767
|
+
return textResult({
|
|
1768
|
+
split: true,
|
|
1769
|
+
originalId: params.id,
|
|
1770
|
+
newMemories: newIds,
|
|
1771
|
+
count: newIds.length
|
|
1772
|
+
});
|
|
1773
|
+
}
|
|
1774
|
+
);
|
|
1775
|
+
server.tool(
|
|
1776
|
+
"memory_scrub",
|
|
1777
|
+
"Scan a memory for sensitive data (PII, API keys, etc.) and optionally redact it. Use this to check memories before sharing or to clean up accidentally stored secrets.",
|
|
1778
|
+
{
|
|
1779
|
+
id: z.string().describe("Memory ID to scan"),
|
|
1780
|
+
redact: z.boolean().optional().describe("Replace detected PII with redaction tokens (default false)")
|
|
1781
|
+
},
|
|
1782
|
+
async (params) => {
|
|
1783
|
+
const existing = db.select().from(memories).where(and2(eq2(memories.id, params.id), isNull2(memories.deletedAt))).get();
|
|
1784
|
+
if (!existing) {
|
|
1785
|
+
return textResult({ error: "Memory not found or deleted" });
|
|
1786
|
+
}
|
|
1787
|
+
const fullText = existing.content + (existing.detail ? " " + existing.detail : "");
|
|
1788
|
+
const matches = detectSensitiveData(fullText);
|
|
1789
|
+
if (params.redact && matches.length > 0) {
|
|
1790
|
+
const { redacted: redactedContent } = redactSensitiveData(existing.content);
|
|
1791
|
+
const redactedDetail = existing.detail ? redactSensitiveData(existing.detail).redacted : null;
|
|
1792
|
+
db.update(memories).set({
|
|
1793
|
+
content: redactedContent,
|
|
1794
|
+
detail: redactedDetail,
|
|
1795
|
+
hasPiiFlag: 0
|
|
1796
|
+
}).where(eq2(memories.id, params.id)).run();
|
|
1797
|
+
if (vecAvailable) {
|
|
1798
|
+
try {
|
|
1799
|
+
const embeddingText = redactedContent + (redactedDetail ? " " + redactedDetail : "");
|
|
1800
|
+
const embedding = await generateEmbedding(embeddingText);
|
|
1801
|
+
insertEmbedding(sqlite, params.id, embedding);
|
|
1802
|
+
} catch {
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
db.insert(memoryEvents).values({
|
|
1806
|
+
id: generateId2(),
|
|
1807
|
+
memoryId: params.id,
|
|
1808
|
+
eventType: "corrected",
|
|
1809
|
+
agentName: "engrams:scrub",
|
|
1810
|
+
oldValue: JSON.stringify({ content: existing.content, detail: existing.detail }),
|
|
1811
|
+
newValue: JSON.stringify({ content: redactedContent, detail: redactedDetail }),
|
|
1812
|
+
timestamp: now2()
|
|
1813
|
+
}).run();
|
|
1814
|
+
bumpLastModified(sqlite);
|
|
1815
|
+
return textResult({
|
|
1816
|
+
id: params.id,
|
|
1817
|
+
scrubbed: true,
|
|
1818
|
+
detected: matches.map((m) => ({ type: m.type, start: m.start, end: m.end })),
|
|
1819
|
+
redactedContent,
|
|
1820
|
+
redactedDetail
|
|
1821
|
+
});
|
|
1822
|
+
}
|
|
1823
|
+
return textResult({
|
|
1824
|
+
id: params.id,
|
|
1825
|
+
detected: matches.map((m) => ({ type: m.type, start: m.start, end: m.end })),
|
|
1826
|
+
hasPii: matches.length > 0,
|
|
1827
|
+
count: matches.length
|
|
1828
|
+
});
|
|
1829
|
+
}
|
|
1830
|
+
);
|
|
1831
|
+
server.tool(
|
|
1832
|
+
"memory_get_connections",
|
|
1833
|
+
"Get all connections for a memory",
|
|
1834
|
+
{
|
|
1835
|
+
memoryId: z.string().describe("Memory ID to get connections for")
|
|
1836
|
+
},
|
|
1837
|
+
async (params) => {
|
|
1838
|
+
const outgoing = db.select().from(memoryConnections).where(eq2(memoryConnections.sourceMemoryId, params.memoryId)).all();
|
|
1839
|
+
const incoming = db.select().from(memoryConnections).where(eq2(memoryConnections.targetMemoryId, params.memoryId)).all();
|
|
1840
|
+
return textResult({
|
|
1841
|
+
memoryId: params.memoryId,
|
|
1842
|
+
outgoing,
|
|
1843
|
+
incoming,
|
|
1844
|
+
totalConnections: outgoing.length + incoming.length
|
|
1845
|
+
});
|
|
1846
|
+
}
|
|
1847
|
+
);
|
|
1848
|
+
server.tool(
|
|
1849
|
+
"memory_list_domains",
|
|
1850
|
+
"List all memory domains with counts",
|
|
1851
|
+
{},
|
|
1852
|
+
async () => {
|
|
1853
|
+
const results = sqlite.prepare(
|
|
1854
|
+
`SELECT domain, COUNT(*) as count FROM memories WHERE deleted_at IS NULL GROUP BY domain ORDER BY count DESC`
|
|
1855
|
+
).all();
|
|
1856
|
+
return textResult({ domains: results });
|
|
1857
|
+
}
|
|
1858
|
+
);
|
|
1859
|
+
server.tool(
|
|
1860
|
+
"memory_list",
|
|
1861
|
+
"Browse the user's memories by domain. Use this to show the user what you know about them in a specific area, or to review memories before a task.",
|
|
1862
|
+
{
|
|
1863
|
+
domain: z.string().optional().describe("Filter by domain"),
|
|
1864
|
+
entityType: z.enum(["person", "organization", "place", "project", "preference", "event", "goal", "fact"]).optional().describe("Filter by entity type"),
|
|
1865
|
+
entityName: z.string().optional().describe("Filter by entity name (case-insensitive)"),
|
|
1866
|
+
sortBy: z.enum(["confidence", "recency"]).optional().describe("Sort order (default: confidence)"),
|
|
1867
|
+
limit: z.number().optional().describe("Max results (default 20)"),
|
|
1868
|
+
offset: z.number().optional().describe("Offset for pagination")
|
|
1869
|
+
},
|
|
1870
|
+
async (params) => {
|
|
1871
|
+
maybeRunDecay();
|
|
1872
|
+
const limit = params.limit ?? 20;
|
|
1873
|
+
const offset = params.offset ?? 0;
|
|
1874
|
+
const sortBy = params.sortBy ?? "confidence";
|
|
1875
|
+
let query = `SELECT * FROM memories WHERE deleted_at IS NULL`;
|
|
1876
|
+
const queryParams = [];
|
|
1877
|
+
if (params.domain) {
|
|
1878
|
+
query += ` AND domain = ?`;
|
|
1879
|
+
queryParams.push(params.domain);
|
|
1880
|
+
}
|
|
1881
|
+
if (params.entityType) {
|
|
1882
|
+
query += ` AND entity_type = ?`;
|
|
1883
|
+
queryParams.push(params.entityType);
|
|
1884
|
+
}
|
|
1885
|
+
if (params.entityName) {
|
|
1886
|
+
query += ` AND entity_name = ? COLLATE NOCASE`;
|
|
1887
|
+
queryParams.push(params.entityName);
|
|
1888
|
+
}
|
|
1889
|
+
if (sortBy === "confidence") {
|
|
1890
|
+
query += ` ORDER BY confidence DESC`;
|
|
1891
|
+
} else {
|
|
1892
|
+
query += ` ORDER BY learned_at DESC`;
|
|
1893
|
+
}
|
|
1894
|
+
query += ` LIMIT ? OFFSET ?`;
|
|
1895
|
+
queryParams.push(limit, offset);
|
|
1896
|
+
const results = sqlite.prepare(query).all(...queryParams);
|
|
1897
|
+
const countQuery = params.domain ? sqlite.prepare(`SELECT COUNT(*) as total FROM memories WHERE deleted_at IS NULL AND domain = ?`).get(params.domain) : sqlite.prepare(`SELECT COUNT(*) as total FROM memories WHERE deleted_at IS NULL`).get();
|
|
1898
|
+
return textResult({
|
|
1899
|
+
memories: results,
|
|
1900
|
+
count: results.length,
|
|
1901
|
+
total: countQuery.total,
|
|
1902
|
+
offset,
|
|
1903
|
+
limit
|
|
1904
|
+
});
|
|
1905
|
+
}
|
|
1906
|
+
);
|
|
1907
|
+
server.tool(
|
|
1908
|
+
"memory_classify",
|
|
1909
|
+
"Batch-classify untyped memories using entity extraction. Runs in the background and returns progress. Use this to backfill entity types on existing memories.",
|
|
1910
|
+
{
|
|
1911
|
+
limit: z.number().optional().describe("Max memories to classify (default 50)"),
|
|
1912
|
+
domain: z.string().optional().describe("Only classify memories in this domain")
|
|
1913
|
+
},
|
|
1914
|
+
async (params) => {
|
|
1915
|
+
if (!process.env.ANTHROPIC_API_KEY) {
|
|
1916
|
+
return textResult({ error: "ANTHROPIC_API_KEY not set \u2014 entity extraction requires an API key" });
|
|
1917
|
+
}
|
|
1918
|
+
const classifyLimit = params.limit ?? 50;
|
|
1919
|
+
let query = `SELECT id, content, detail FROM memories WHERE entity_type IS NULL AND deleted_at IS NULL`;
|
|
1920
|
+
const queryParams = [];
|
|
1921
|
+
if (params.domain) {
|
|
1922
|
+
query += ` AND domain = ?`;
|
|
1923
|
+
queryParams.push(params.domain);
|
|
1924
|
+
}
|
|
1925
|
+
query += ` LIMIT ?`;
|
|
1926
|
+
queryParams.push(classifyLimit);
|
|
1927
|
+
const untyped = sqlite.prepare(query).all(...queryParams);
|
|
1928
|
+
if (untyped.length === 0) {
|
|
1929
|
+
return textResult({ status: "complete", classified: 0, message: "No untyped memories found" });
|
|
1930
|
+
}
|
|
1931
|
+
const existingNames = sqlite.prepare(`SELECT DISTINCT entity_name FROM memories WHERE entity_name IS NOT NULL AND deleted_at IS NULL`).all();
|
|
1932
|
+
const nameList = existingNames.map((r) => r.entity_name);
|
|
1933
|
+
let classified = 0;
|
|
1934
|
+
let errors = 0;
|
|
1935
|
+
for (const mem of untyped) {
|
|
1936
|
+
try {
|
|
1937
|
+
const extraction = await extractEntity(mem.content, mem.detail, nameList);
|
|
1938
|
+
sqlite.transaction(() => {
|
|
1939
|
+
sqlite.prepare(
|
|
1940
|
+
`UPDATE memories SET entity_type = ?, entity_name = ?, structured_data = ? WHERE id = ? AND entity_type IS NULL AND deleted_at IS NULL`
|
|
1941
|
+
).run(
|
|
1942
|
+
extraction.entity_type,
|
|
1943
|
+
extraction.entity_name,
|
|
1944
|
+
JSON.stringify(extraction.structured_data),
|
|
1945
|
+
mem.id
|
|
1946
|
+
);
|
|
1947
|
+
for (const conn of extraction.suggested_connections) {
|
|
1948
|
+
const target = sqlite.prepare(`SELECT id FROM memories WHERE entity_name = ? COLLATE NOCASE AND deleted_at IS NULL LIMIT 1`).get(conn.target_entity_name);
|
|
1949
|
+
if (target && target.id !== mem.id) {
|
|
1950
|
+
sqlite.prepare(
|
|
1951
|
+
`INSERT INTO memory_connections (source_memory_id, target_memory_id, relationship) VALUES (?, ?, ?)`
|
|
1952
|
+
).run(mem.id, target.id, conn.relationship);
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
})();
|
|
1956
|
+
classified++;
|
|
1957
|
+
if (extraction.entity_name) nameList.push(extraction.entity_name);
|
|
1958
|
+
} catch {
|
|
1959
|
+
errors++;
|
|
1960
|
+
}
|
|
1961
|
+
if (classified + errors < untyped.length) {
|
|
1962
|
+
await new Promise((resolve3) => setTimeout(resolve3, 200));
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
if (classified > 0) bumpLastModified(sqlite);
|
|
1966
|
+
return textResult({
|
|
1967
|
+
status: "complete",
|
|
1968
|
+
classified,
|
|
1969
|
+
errors,
|
|
1970
|
+
remaining: Math.max(0, sqlite.prepare(`SELECT COUNT(*) as c FROM memories WHERE entity_type IS NULL AND deleted_at IS NULL`).get().c)
|
|
1971
|
+
});
|
|
1972
|
+
}
|
|
1973
|
+
);
|
|
1974
|
+
server.tool(
|
|
1975
|
+
"memory_list_entities",
|
|
1976
|
+
"List all known entities grouped by type. Useful for discovering what the system knows about people, organizations, projects, etc.",
|
|
1977
|
+
{
|
|
1978
|
+
entityType: z.enum(["person", "organization", "place", "project", "preference", "event", "goal", "fact"]).optional().describe("Filter to a specific entity type")
|
|
1979
|
+
},
|
|
1980
|
+
async (params) => {
|
|
1981
|
+
let query = `SELECT entity_type, entity_name, COUNT(*) as memory_count
|
|
1982
|
+
FROM memories
|
|
1983
|
+
WHERE entity_type IS NOT NULL AND entity_name IS NOT NULL AND deleted_at IS NULL`;
|
|
1984
|
+
const queryParams = [];
|
|
1985
|
+
if (params.entityType) {
|
|
1986
|
+
query += ` AND entity_type = ?`;
|
|
1987
|
+
queryParams.push(params.entityType);
|
|
1988
|
+
}
|
|
1989
|
+
query += ` GROUP BY entity_type, entity_name ORDER BY entity_type, memory_count DESC`;
|
|
1990
|
+
const rows = sqlite.prepare(query).all(...queryParams);
|
|
1991
|
+
const grouped = {};
|
|
1992
|
+
for (const row of rows) {
|
|
1993
|
+
if (!grouped[row.entity_type]) grouped[row.entity_type] = [];
|
|
1994
|
+
grouped[row.entity_type].push({ name: row.entity_name, count: row.memory_count });
|
|
1995
|
+
}
|
|
1996
|
+
return textResult({
|
|
1997
|
+
entities: grouped,
|
|
1998
|
+
totalEntities: rows.length,
|
|
1999
|
+
totalMemoriesWithEntities: rows.reduce((sum, r) => sum + r.memory_count, 0)
|
|
2000
|
+
});
|
|
2001
|
+
}
|
|
2002
|
+
);
|
|
2003
|
+
server.tool(
|
|
2004
|
+
"memory_set_permissions",
|
|
2005
|
+
"Set per-agent read/write permissions for a domain",
|
|
2006
|
+
{
|
|
2007
|
+
agentId: z.string().describe("Agent ID"),
|
|
2008
|
+
domain: z.string().describe("Domain (* for all)"),
|
|
2009
|
+
canRead: z.boolean().optional().describe("Allow reading (default true)"),
|
|
2010
|
+
canWrite: z.boolean().optional().describe("Allow writing (default true)")
|
|
2011
|
+
},
|
|
2012
|
+
async (params) => {
|
|
2013
|
+
const existing = db.select().from(agentPermissions).where(
|
|
2014
|
+
and2(
|
|
2015
|
+
eq2(agentPermissions.agentId, params.agentId),
|
|
2016
|
+
eq2(agentPermissions.domain, params.domain)
|
|
2017
|
+
)
|
|
2018
|
+
).get();
|
|
2019
|
+
const canRead = params.canRead !== void 0 ? params.canRead ? 1 : 0 : 1;
|
|
2020
|
+
const canWrite = params.canWrite !== void 0 ? params.canWrite ? 1 : 0 : 1;
|
|
2021
|
+
if (existing) {
|
|
2022
|
+
db.update(agentPermissions).set({ canRead, canWrite }).where(
|
|
2023
|
+
and2(
|
|
2024
|
+
eq2(agentPermissions.agentId, params.agentId),
|
|
2025
|
+
eq2(agentPermissions.domain, params.domain)
|
|
2026
|
+
)
|
|
2027
|
+
).run();
|
|
2028
|
+
} else {
|
|
2029
|
+
db.insert(agentPermissions).values({
|
|
2030
|
+
agentId: params.agentId,
|
|
2031
|
+
domain: params.domain,
|
|
2032
|
+
canRead,
|
|
2033
|
+
canWrite
|
|
2034
|
+
}).run();
|
|
2035
|
+
}
|
|
2036
|
+
return textResult({
|
|
2037
|
+
agentId: params.agentId,
|
|
2038
|
+
domain: params.domain,
|
|
2039
|
+
canRead: !!canRead,
|
|
2040
|
+
canWrite: !!canWrite,
|
|
2041
|
+
updated: true
|
|
2042
|
+
});
|
|
2043
|
+
}
|
|
2044
|
+
);
|
|
2045
|
+
server.resource("memory-index", "memory://index", async (uri) => {
|
|
2046
|
+
const domains = sqlite.prepare(
|
|
2047
|
+
`SELECT domain, COUNT(*) as count FROM memories WHERE deleted_at IS NULL GROUP BY domain`
|
|
2048
|
+
).all();
|
|
2049
|
+
const totalResult = sqlite.prepare(`SELECT COUNT(*) as total FROM memories WHERE deleted_at IS NULL`).get();
|
|
2050
|
+
const confidenceDist = sqlite.prepare(`
|
|
2051
|
+
SELECT
|
|
2052
|
+
SUM(CASE WHEN confidence >= 0.9 THEN 1 ELSE 0 END) as high,
|
|
2053
|
+
SUM(CASE WHEN confidence >= 0.5 AND confidence < 0.9 THEN 1 ELSE 0 END) as medium,
|
|
2054
|
+
SUM(CASE WHEN confidence < 0.5 THEN 1 ELSE 0 END) as low
|
|
2055
|
+
FROM memories WHERE deleted_at IS NULL
|
|
2056
|
+
`).get();
|
|
2057
|
+
return {
|
|
2058
|
+
contents: [
|
|
2059
|
+
{
|
|
2060
|
+
uri: uri.href,
|
|
2061
|
+
mimeType: "application/json",
|
|
2062
|
+
text: JSON.stringify(
|
|
2063
|
+
{ total: totalResult.total, domains, confidenceDistribution: confidenceDist },
|
|
2064
|
+
null,
|
|
2065
|
+
2
|
|
2066
|
+
)
|
|
2067
|
+
}
|
|
2068
|
+
]
|
|
2069
|
+
};
|
|
2070
|
+
});
|
|
2071
|
+
server.resource(
|
|
2072
|
+
"memory-domain",
|
|
2073
|
+
new ResourceTemplate("memory://domain/{name}", { list: void 0 }),
|
|
2074
|
+
async (uri, params) => {
|
|
2075
|
+
const name = params.name;
|
|
2076
|
+
const results = sqlite.prepare(
|
|
2077
|
+
`SELECT * FROM memories WHERE deleted_at IS NULL AND domain = ? ORDER BY confidence DESC`
|
|
2078
|
+
).all(name);
|
|
2079
|
+
return {
|
|
2080
|
+
contents: [
|
|
2081
|
+
{
|
|
2082
|
+
uri: uri.href,
|
|
2083
|
+
mimeType: "application/json",
|
|
2084
|
+
text: JSON.stringify({ domain: name, memories: results }, null, 2)
|
|
2085
|
+
}
|
|
2086
|
+
]
|
|
2087
|
+
};
|
|
2088
|
+
}
|
|
2089
|
+
);
|
|
2090
|
+
server.resource("memory-recent", "memory://recent", async (uri) => {
|
|
2091
|
+
const results = sqlite.prepare(
|
|
2092
|
+
`SELECT * FROM memories WHERE deleted_at IS NULL ORDER BY learned_at DESC LIMIT 20`
|
|
2093
|
+
).all();
|
|
2094
|
+
return {
|
|
2095
|
+
contents: [
|
|
2096
|
+
{
|
|
2097
|
+
uri: uri.href,
|
|
2098
|
+
mimeType: "application/json",
|
|
2099
|
+
text: JSON.stringify({ memories: results, count: results.length }, null, 2)
|
|
2100
|
+
}
|
|
2101
|
+
]
|
|
2102
|
+
};
|
|
2103
|
+
});
|
|
2104
|
+
if (process.argv.includes("--http") || process.env.ENGRAMS_HTTP === "1") {
|
|
2105
|
+
const { startHttpApi: startHttpApi2 } = await Promise.resolve().then(() => (init_http(), http_exports));
|
|
2106
|
+
startHttpApi2(db, sqlite);
|
|
2107
|
+
}
|
|
2108
|
+
const transport = new StdioServerTransport();
|
|
2109
|
+
await server.connect(transport);
|
|
2110
|
+
}
|
|
2111
|
+
export {
|
|
2112
|
+
startServer
|
|
2113
|
+
};
|