@hasna/mementos 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/LICENSE +190 -0
- package/README.md +276 -0
- package/dashboard/dist/assets/index-C8vbNL_5.css +1 -0
- package/dashboard/dist/assets/index-Dnhl5e5q.js +270 -0
- package/dashboard/dist/index.html +13 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +3643 -0
- package/dist/db/agents.d.ts +6 -0
- package/dist/db/agents.d.ts.map +1 -0
- package/dist/db/database.d.ts +10 -0
- package/dist/db/database.d.ts.map +1 -0
- package/dist/db/memories.d.ts +12 -0
- package/dist/db/memories.d.ts.map +1 -0
- package/dist/db/projects.d.ts +6 -0
- package/dist/db/projects.d.ts.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1174 -0
- package/dist/lib/config.d.ts +5 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/injector.d.ts +31 -0
- package/dist/lib/injector.d.ts.map +1 -0
- package/dist/lib/retention.d.ts +10 -0
- package/dist/lib/retention.d.ts.map +1 -0
- package/dist/lib/search.d.ts +11 -0
- package/dist/lib/search.d.ts.map +1 -0
- package/dist/lib/sync.d.ts +10 -0
- package/dist/lib/sync.d.ts.map +1 -0
- package/dist/mcp/index.d.ts +3 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +5185 -0
- package/dist/server/index.d.ts +7 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +1144 -0
- package/dist/types/index.d.ts +158 -0
- package/dist/types/index.d.ts.map +1 -0
- package/package.json +74 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1174 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/types/index.ts
|
|
3
|
+
class MemoryNotFoundError extends Error {
|
|
4
|
+
constructor(id) {
|
|
5
|
+
super(`Memory not found: ${id}`);
|
|
6
|
+
this.name = "MemoryNotFoundError";
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
class DuplicateMemoryError extends Error {
|
|
11
|
+
constructor(key, scope) {
|
|
12
|
+
super(`Memory already exists with key "${key}" in scope "${scope}"`);
|
|
13
|
+
this.name = "DuplicateMemoryError";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
class MemoryExpiredError extends Error {
|
|
18
|
+
constructor(id) {
|
|
19
|
+
super(`Memory has expired: ${id}`);
|
|
20
|
+
this.name = "MemoryExpiredError";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
class InvalidScopeError extends Error {
|
|
25
|
+
constructor(message) {
|
|
26
|
+
super(message);
|
|
27
|
+
this.name = "InvalidScopeError";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
class VersionConflictError extends Error {
|
|
32
|
+
expected;
|
|
33
|
+
actual;
|
|
34
|
+
constructor(id, expected, actual) {
|
|
35
|
+
super(`Version conflict for memory ${id}: expected ${expected}, got ${actual}`);
|
|
36
|
+
this.name = "VersionConflictError";
|
|
37
|
+
this.expected = expected;
|
|
38
|
+
this.actual = actual;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// src/db/database.ts
|
|
42
|
+
import { Database } from "bun:sqlite";
|
|
43
|
+
import { existsSync, mkdirSync } from "fs";
|
|
44
|
+
import { dirname, join, resolve } from "path";
|
|
45
|
+
function isInMemoryDb(path) {
|
|
46
|
+
return path === ":memory:" || path.startsWith("file::memory:");
|
|
47
|
+
}
|
|
48
|
+
function findNearestMementosDb(startDir) {
|
|
49
|
+
let dir = resolve(startDir);
|
|
50
|
+
while (true) {
|
|
51
|
+
const candidate = join(dir, ".mementos", "mementos.db");
|
|
52
|
+
if (existsSync(candidate))
|
|
53
|
+
return candidate;
|
|
54
|
+
const parent = dirname(dir);
|
|
55
|
+
if (parent === dir)
|
|
56
|
+
break;
|
|
57
|
+
dir = parent;
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
function findGitRoot(startDir) {
|
|
62
|
+
let dir = resolve(startDir);
|
|
63
|
+
while (true) {
|
|
64
|
+
if (existsSync(join(dir, ".git")))
|
|
65
|
+
return dir;
|
|
66
|
+
const parent = dirname(dir);
|
|
67
|
+
if (parent === dir)
|
|
68
|
+
break;
|
|
69
|
+
dir = parent;
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
function getDbPath() {
|
|
74
|
+
if (process.env["MEMENTOS_DB_PATH"]) {
|
|
75
|
+
return process.env["MEMENTOS_DB_PATH"];
|
|
76
|
+
}
|
|
77
|
+
const cwd = process.cwd();
|
|
78
|
+
const nearest = findNearestMementosDb(cwd);
|
|
79
|
+
if (nearest)
|
|
80
|
+
return nearest;
|
|
81
|
+
if (process.env["MEMENTOS_DB_SCOPE"] === "project") {
|
|
82
|
+
const gitRoot = findGitRoot(cwd);
|
|
83
|
+
if (gitRoot) {
|
|
84
|
+
return join(gitRoot, ".mementos", "mementos.db");
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
|
|
88
|
+
return join(home, ".mementos", "mementos.db");
|
|
89
|
+
}
|
|
90
|
+
function ensureDir(filePath) {
|
|
91
|
+
if (isInMemoryDb(filePath))
|
|
92
|
+
return;
|
|
93
|
+
const dir = dirname(resolve(filePath));
|
|
94
|
+
if (!existsSync(dir)) {
|
|
95
|
+
mkdirSync(dir, { recursive: true });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
var MIGRATIONS = [
|
|
99
|
+
`
|
|
100
|
+
CREATE TABLE IF NOT EXISTS projects (
|
|
101
|
+
id TEXT PRIMARY KEY,
|
|
102
|
+
name TEXT NOT NULL,
|
|
103
|
+
path TEXT UNIQUE NOT NULL,
|
|
104
|
+
description TEXT,
|
|
105
|
+
memory_prefix TEXT,
|
|
106
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
107
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
CREATE TABLE IF NOT EXISTS agents (
|
|
111
|
+
id TEXT PRIMARY KEY,
|
|
112
|
+
name TEXT NOT NULL UNIQUE,
|
|
113
|
+
description TEXT,
|
|
114
|
+
role TEXT DEFAULT 'agent',
|
|
115
|
+
metadata TEXT DEFAULT '{}',
|
|
116
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
117
|
+
last_seen_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
CREATE TABLE IF NOT EXISTS memories (
|
|
121
|
+
id TEXT PRIMARY KEY,
|
|
122
|
+
key TEXT NOT NULL,
|
|
123
|
+
value TEXT NOT NULL,
|
|
124
|
+
category TEXT NOT NULL DEFAULT 'knowledge' CHECK(category IN ('preference', 'fact', 'knowledge', 'history')),
|
|
125
|
+
scope TEXT NOT NULL DEFAULT 'private' CHECK(scope IN ('global', 'shared', 'private')),
|
|
126
|
+
summary TEXT,
|
|
127
|
+
tags TEXT DEFAULT '[]',
|
|
128
|
+
importance INTEGER NOT NULL DEFAULT 5 CHECK(importance >= 1 AND importance <= 10),
|
|
129
|
+
source TEXT NOT NULL DEFAULT 'agent' CHECK(source IN ('user', 'agent', 'system', 'auto', 'imported')),
|
|
130
|
+
status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'archived', 'expired')),
|
|
131
|
+
pinned INTEGER NOT NULL DEFAULT 0,
|
|
132
|
+
agent_id TEXT REFERENCES agents(id) ON DELETE SET NULL,
|
|
133
|
+
project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
|
|
134
|
+
session_id TEXT,
|
|
135
|
+
metadata TEXT DEFAULT '{}',
|
|
136
|
+
access_count INTEGER NOT NULL DEFAULT 0,
|
|
137
|
+
version INTEGER NOT NULL DEFAULT 1,
|
|
138
|
+
expires_at TEXT,
|
|
139
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
140
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
141
|
+
accessed_at TEXT
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
CREATE TABLE IF NOT EXISTS memory_tags (
|
|
145
|
+
memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
|
146
|
+
tag TEXT NOT NULL,
|
|
147
|
+
PRIMARY KEY (memory_id, tag)
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
151
|
+
id TEXT PRIMARY KEY,
|
|
152
|
+
agent_id TEXT REFERENCES agents(id) ON DELETE SET NULL,
|
|
153
|
+
project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
|
|
154
|
+
started_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
155
|
+
last_activity TEXT NOT NULL DEFAULT (datetime('now')),
|
|
156
|
+
metadata TEXT DEFAULT '{}'
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_memories_unique_key
|
|
160
|
+
ON memories(key, scope, COALESCE(agent_id, ''), COALESCE(project_id, ''), COALESCE(session_id, ''));
|
|
161
|
+
|
|
162
|
+
CREATE INDEX IF NOT EXISTS idx_memories_key ON memories(key);
|
|
163
|
+
CREATE INDEX IF NOT EXISTS idx_memories_scope ON memories(scope);
|
|
164
|
+
CREATE INDEX IF NOT EXISTS idx_memories_category ON memories(category);
|
|
165
|
+
CREATE INDEX IF NOT EXISTS idx_memories_status ON memories(status);
|
|
166
|
+
CREATE INDEX IF NOT EXISTS idx_memories_importance ON memories(importance);
|
|
167
|
+
CREATE INDEX IF NOT EXISTS idx_memories_agent ON memories(agent_id);
|
|
168
|
+
CREATE INDEX IF NOT EXISTS idx_memories_project ON memories(project_id);
|
|
169
|
+
CREATE INDEX IF NOT EXISTS idx_memories_session ON memories(session_id);
|
|
170
|
+
CREATE INDEX IF NOT EXISTS idx_memories_pinned ON memories(pinned);
|
|
171
|
+
CREATE INDEX IF NOT EXISTS idx_memories_expires ON memories(expires_at);
|
|
172
|
+
CREATE INDEX IF NOT EXISTS idx_memories_created ON memories(created_at);
|
|
173
|
+
CREATE INDEX IF NOT EXISTS idx_memory_tags_tag ON memory_tags(tag);
|
|
174
|
+
CREATE INDEX IF NOT EXISTS idx_memory_tags_memory ON memory_tags(memory_id);
|
|
175
|
+
CREATE INDEX IF NOT EXISTS idx_agents_name ON agents(name);
|
|
176
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_agent ON sessions(agent_id);
|
|
177
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_id);
|
|
178
|
+
|
|
179
|
+
CREATE TABLE IF NOT EXISTS _migrations (
|
|
180
|
+
id INTEGER PRIMARY KEY,
|
|
181
|
+
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (1);
|
|
185
|
+
`
|
|
186
|
+
];
|
|
187
|
+
var _db = null;
|
|
188
|
+
function getDatabase(dbPath) {
|
|
189
|
+
if (_db)
|
|
190
|
+
return _db;
|
|
191
|
+
const path = dbPath || getDbPath();
|
|
192
|
+
ensureDir(path);
|
|
193
|
+
_db = new Database(path, { create: true });
|
|
194
|
+
_db.run("PRAGMA journal_mode = WAL");
|
|
195
|
+
_db.run("PRAGMA busy_timeout = 5000");
|
|
196
|
+
_db.run("PRAGMA foreign_keys = ON");
|
|
197
|
+
runMigrations(_db);
|
|
198
|
+
return _db;
|
|
199
|
+
}
|
|
200
|
+
function runMigrations(db) {
|
|
201
|
+
try {
|
|
202
|
+
const result = db.query("SELECT MAX(id) as max_id FROM _migrations").get();
|
|
203
|
+
const currentLevel = result?.max_id ?? 0;
|
|
204
|
+
for (let i = currentLevel;i < MIGRATIONS.length; i++) {
|
|
205
|
+
try {
|
|
206
|
+
db.exec(MIGRATIONS[i]);
|
|
207
|
+
} catch {}
|
|
208
|
+
}
|
|
209
|
+
} catch {
|
|
210
|
+
for (const migration of MIGRATIONS) {
|
|
211
|
+
try {
|
|
212
|
+
db.exec(migration);
|
|
213
|
+
} catch {}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
function closeDatabase() {
|
|
218
|
+
if (_db) {
|
|
219
|
+
_db.close();
|
|
220
|
+
_db = null;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
function resetDatabase() {
|
|
224
|
+
_db = null;
|
|
225
|
+
}
|
|
226
|
+
function now() {
|
|
227
|
+
return new Date().toISOString();
|
|
228
|
+
}
|
|
229
|
+
function uuid() {
|
|
230
|
+
return crypto.randomUUID();
|
|
231
|
+
}
|
|
232
|
+
function shortUuid() {
|
|
233
|
+
return crypto.randomUUID().slice(0, 8);
|
|
234
|
+
}
|
|
235
|
+
function resolvePartialId(db, table, partialId) {
|
|
236
|
+
if (partialId.length >= 36) {
|
|
237
|
+
const row = db.query(`SELECT id FROM ${table} WHERE id = ?`).get(partialId);
|
|
238
|
+
return row?.id ?? null;
|
|
239
|
+
}
|
|
240
|
+
const rows = db.query(`SELECT id FROM ${table} WHERE id LIKE ?`).all(`${partialId}%`);
|
|
241
|
+
if (rows.length === 1) {
|
|
242
|
+
return rows[0].id;
|
|
243
|
+
}
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
// src/db/memories.ts
|
|
247
|
+
function parseMemoryRow(row) {
|
|
248
|
+
return {
|
|
249
|
+
id: row["id"],
|
|
250
|
+
key: row["key"],
|
|
251
|
+
value: row["value"],
|
|
252
|
+
category: row["category"],
|
|
253
|
+
scope: row["scope"],
|
|
254
|
+
summary: row["summary"] || null,
|
|
255
|
+
tags: JSON.parse(row["tags"] || "[]"),
|
|
256
|
+
importance: row["importance"],
|
|
257
|
+
source: row["source"],
|
|
258
|
+
status: row["status"],
|
|
259
|
+
pinned: !!row["pinned"],
|
|
260
|
+
agent_id: row["agent_id"] || null,
|
|
261
|
+
project_id: row["project_id"] || null,
|
|
262
|
+
session_id: row["session_id"] || null,
|
|
263
|
+
metadata: JSON.parse(row["metadata"] || "{}"),
|
|
264
|
+
access_count: row["access_count"],
|
|
265
|
+
version: row["version"],
|
|
266
|
+
expires_at: row["expires_at"] || null,
|
|
267
|
+
created_at: row["created_at"],
|
|
268
|
+
updated_at: row["updated_at"],
|
|
269
|
+
accessed_at: row["accessed_at"] || null
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
function createMemory(input, dedupeMode = "merge", db) {
|
|
273
|
+
const d = db || getDatabase();
|
|
274
|
+
const timestamp = now();
|
|
275
|
+
let expiresAt = input.expires_at || null;
|
|
276
|
+
if (input.ttl_ms && !expiresAt) {
|
|
277
|
+
expiresAt = new Date(Date.now() + input.ttl_ms).toISOString();
|
|
278
|
+
}
|
|
279
|
+
const id = uuid();
|
|
280
|
+
const tags = input.tags || [];
|
|
281
|
+
const tagsJson = JSON.stringify(tags);
|
|
282
|
+
const metadataJson = JSON.stringify(input.metadata || {});
|
|
283
|
+
if (dedupeMode === "merge") {
|
|
284
|
+
const existing = d.query(`SELECT id, version FROM memories
|
|
285
|
+
WHERE key = ? AND scope = ?
|
|
286
|
+
AND COALESCE(agent_id, '') = ?
|
|
287
|
+
AND COALESCE(project_id, '') = ?
|
|
288
|
+
AND COALESCE(session_id, '') = ?`).get(input.key, input.scope || "private", input.agent_id || "", input.project_id || "", input.session_id || "");
|
|
289
|
+
if (existing) {
|
|
290
|
+
d.run(`UPDATE memories SET
|
|
291
|
+
value = ?, category = ?, summary = ?, tags = ?,
|
|
292
|
+
importance = ?, metadata = ?, expires_at = ?,
|
|
293
|
+
pinned = COALESCE(pinned, 0),
|
|
294
|
+
version = version + 1, updated_at = ?
|
|
295
|
+
WHERE id = ?`, [
|
|
296
|
+
input.value,
|
|
297
|
+
input.category || "knowledge",
|
|
298
|
+
input.summary || null,
|
|
299
|
+
tagsJson,
|
|
300
|
+
input.importance ?? 5,
|
|
301
|
+
metadataJson,
|
|
302
|
+
expiresAt,
|
|
303
|
+
timestamp,
|
|
304
|
+
existing.id
|
|
305
|
+
]);
|
|
306
|
+
d.run("DELETE FROM memory_tags WHERE memory_id = ?", [existing.id]);
|
|
307
|
+
const insertTag2 = d.prepare("INSERT OR IGNORE INTO memory_tags (memory_id, tag) VALUES (?, ?)");
|
|
308
|
+
for (const tag of tags) {
|
|
309
|
+
insertTag2.run(existing.id, tag);
|
|
310
|
+
}
|
|
311
|
+
return getMemory(existing.id, d);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
d.run(`INSERT INTO memories (id, key, value, category, scope, summary, tags, importance, source, status, pinned, agent_id, project_id, session_id, metadata, access_count, version, expires_at, created_at, updated_at)
|
|
315
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'active', 0, ?, ?, ?, ?, 0, 1, ?, ?, ?)`, [
|
|
316
|
+
id,
|
|
317
|
+
input.key,
|
|
318
|
+
input.value,
|
|
319
|
+
input.category || "knowledge",
|
|
320
|
+
input.scope || "private",
|
|
321
|
+
input.summary || null,
|
|
322
|
+
tagsJson,
|
|
323
|
+
input.importance ?? 5,
|
|
324
|
+
input.source || "agent",
|
|
325
|
+
input.agent_id || null,
|
|
326
|
+
input.project_id || null,
|
|
327
|
+
input.session_id || null,
|
|
328
|
+
metadataJson,
|
|
329
|
+
expiresAt,
|
|
330
|
+
timestamp,
|
|
331
|
+
timestamp
|
|
332
|
+
]);
|
|
333
|
+
const insertTag = d.prepare("INSERT OR IGNORE INTO memory_tags (memory_id, tag) VALUES (?, ?)");
|
|
334
|
+
for (const tag of tags) {
|
|
335
|
+
insertTag.run(id, tag);
|
|
336
|
+
}
|
|
337
|
+
return getMemory(id, d);
|
|
338
|
+
}
|
|
339
|
+
function getMemory(id, db) {
|
|
340
|
+
const d = db || getDatabase();
|
|
341
|
+
const row = d.query("SELECT * FROM memories WHERE id = ?").get(id);
|
|
342
|
+
if (!row)
|
|
343
|
+
return null;
|
|
344
|
+
return parseMemoryRow(row);
|
|
345
|
+
}
|
|
346
|
+
function getMemoryByKey(key, scope, agentId, projectId, sessionId, db) {
|
|
347
|
+
const d = db || getDatabase();
|
|
348
|
+
let sql = "SELECT * FROM memories WHERE key = ?";
|
|
349
|
+
const params = [key];
|
|
350
|
+
if (scope) {
|
|
351
|
+
sql += " AND scope = ?";
|
|
352
|
+
params.push(scope);
|
|
353
|
+
}
|
|
354
|
+
if (agentId) {
|
|
355
|
+
sql += " AND agent_id = ?";
|
|
356
|
+
params.push(agentId);
|
|
357
|
+
}
|
|
358
|
+
if (projectId) {
|
|
359
|
+
sql += " AND project_id = ?";
|
|
360
|
+
params.push(projectId);
|
|
361
|
+
}
|
|
362
|
+
if (sessionId) {
|
|
363
|
+
sql += " AND session_id = ?";
|
|
364
|
+
params.push(sessionId);
|
|
365
|
+
}
|
|
366
|
+
sql += " AND status = 'active' ORDER BY importance DESC LIMIT 1";
|
|
367
|
+
const row = d.query(sql).get(...params);
|
|
368
|
+
if (!row)
|
|
369
|
+
return null;
|
|
370
|
+
return parseMemoryRow(row);
|
|
371
|
+
}
|
|
372
|
+
function listMemories(filter, db) {
|
|
373
|
+
const d = db || getDatabase();
|
|
374
|
+
const conditions = [];
|
|
375
|
+
const params = [];
|
|
376
|
+
if (filter) {
|
|
377
|
+
if (filter.scope) {
|
|
378
|
+
if (Array.isArray(filter.scope)) {
|
|
379
|
+
conditions.push(`scope IN (${filter.scope.map(() => "?").join(",")})`);
|
|
380
|
+
params.push(...filter.scope);
|
|
381
|
+
} else {
|
|
382
|
+
conditions.push("scope = ?");
|
|
383
|
+
params.push(filter.scope);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
if (filter.category) {
|
|
387
|
+
if (Array.isArray(filter.category)) {
|
|
388
|
+
conditions.push(`category IN (${filter.category.map(() => "?").join(",")})`);
|
|
389
|
+
params.push(...filter.category);
|
|
390
|
+
} else {
|
|
391
|
+
conditions.push("category = ?");
|
|
392
|
+
params.push(filter.category);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
if (filter.source) {
|
|
396
|
+
if (Array.isArray(filter.source)) {
|
|
397
|
+
conditions.push(`source IN (${filter.source.map(() => "?").join(",")})`);
|
|
398
|
+
params.push(...filter.source);
|
|
399
|
+
} else {
|
|
400
|
+
conditions.push("source = ?");
|
|
401
|
+
params.push(filter.source);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
if (filter.status) {
|
|
405
|
+
if (Array.isArray(filter.status)) {
|
|
406
|
+
conditions.push(`status IN (${filter.status.map(() => "?").join(",")})`);
|
|
407
|
+
params.push(...filter.status);
|
|
408
|
+
} else {
|
|
409
|
+
conditions.push("status = ?");
|
|
410
|
+
params.push(filter.status);
|
|
411
|
+
}
|
|
412
|
+
} else {
|
|
413
|
+
conditions.push("status = 'active'");
|
|
414
|
+
}
|
|
415
|
+
if (filter.project_id) {
|
|
416
|
+
conditions.push("project_id = ?");
|
|
417
|
+
params.push(filter.project_id);
|
|
418
|
+
}
|
|
419
|
+
if (filter.agent_id) {
|
|
420
|
+
conditions.push("agent_id = ?");
|
|
421
|
+
params.push(filter.agent_id);
|
|
422
|
+
}
|
|
423
|
+
if (filter.session_id) {
|
|
424
|
+
conditions.push("session_id = ?");
|
|
425
|
+
params.push(filter.session_id);
|
|
426
|
+
}
|
|
427
|
+
if (filter.min_importance) {
|
|
428
|
+
conditions.push("importance >= ?");
|
|
429
|
+
params.push(filter.min_importance);
|
|
430
|
+
}
|
|
431
|
+
if (filter.pinned !== undefined) {
|
|
432
|
+
conditions.push("pinned = ?");
|
|
433
|
+
params.push(filter.pinned ? 1 : 0);
|
|
434
|
+
}
|
|
435
|
+
if (filter.tags && filter.tags.length > 0) {
|
|
436
|
+
for (const tag of filter.tags) {
|
|
437
|
+
conditions.push("id IN (SELECT memory_id FROM memory_tags WHERE tag = ?)");
|
|
438
|
+
params.push(tag);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
if (filter.search) {
|
|
442
|
+
conditions.push("(key LIKE ? OR value LIKE ? OR summary LIKE ?)");
|
|
443
|
+
const term = `%${filter.search}%`;
|
|
444
|
+
params.push(term, term, term);
|
|
445
|
+
}
|
|
446
|
+
} else {
|
|
447
|
+
conditions.push("status = 'active'");
|
|
448
|
+
}
|
|
449
|
+
let sql = "SELECT * FROM memories";
|
|
450
|
+
if (conditions.length > 0) {
|
|
451
|
+
sql += ` WHERE ${conditions.join(" AND ")}`;
|
|
452
|
+
}
|
|
453
|
+
sql += " ORDER BY importance DESC, created_at DESC";
|
|
454
|
+
if (filter?.limit) {
|
|
455
|
+
sql += " LIMIT ?";
|
|
456
|
+
params.push(filter.limit);
|
|
457
|
+
}
|
|
458
|
+
if (filter?.offset) {
|
|
459
|
+
sql += " OFFSET ?";
|
|
460
|
+
params.push(filter.offset);
|
|
461
|
+
}
|
|
462
|
+
const rows = d.query(sql).all(...params);
|
|
463
|
+
return rows.map(parseMemoryRow);
|
|
464
|
+
}
|
|
465
|
+
function updateMemory(id, input, db) {
|
|
466
|
+
const d = db || getDatabase();
|
|
467
|
+
const existing = getMemory(id, d);
|
|
468
|
+
if (!existing)
|
|
469
|
+
throw new MemoryNotFoundError(id);
|
|
470
|
+
if (existing.version !== input.version) {
|
|
471
|
+
throw new VersionConflictError(id, input.version, existing.version);
|
|
472
|
+
}
|
|
473
|
+
const sets = ["version = version + 1", "updated_at = ?"];
|
|
474
|
+
const params = [now()];
|
|
475
|
+
if (input.value !== undefined) {
|
|
476
|
+
sets.push("value = ?");
|
|
477
|
+
params.push(input.value);
|
|
478
|
+
}
|
|
479
|
+
if (input.category !== undefined) {
|
|
480
|
+
sets.push("category = ?");
|
|
481
|
+
params.push(input.category);
|
|
482
|
+
}
|
|
483
|
+
if (input.scope !== undefined) {
|
|
484
|
+
sets.push("scope = ?");
|
|
485
|
+
params.push(input.scope);
|
|
486
|
+
}
|
|
487
|
+
if (input.summary !== undefined) {
|
|
488
|
+
sets.push("summary = ?");
|
|
489
|
+
params.push(input.summary);
|
|
490
|
+
}
|
|
491
|
+
if (input.importance !== undefined) {
|
|
492
|
+
sets.push("importance = ?");
|
|
493
|
+
params.push(input.importance);
|
|
494
|
+
}
|
|
495
|
+
if (input.pinned !== undefined) {
|
|
496
|
+
sets.push("pinned = ?");
|
|
497
|
+
params.push(input.pinned ? 1 : 0);
|
|
498
|
+
}
|
|
499
|
+
if (input.status !== undefined) {
|
|
500
|
+
sets.push("status = ?");
|
|
501
|
+
params.push(input.status);
|
|
502
|
+
}
|
|
503
|
+
if (input.metadata !== undefined) {
|
|
504
|
+
sets.push("metadata = ?");
|
|
505
|
+
params.push(JSON.stringify(input.metadata));
|
|
506
|
+
}
|
|
507
|
+
if (input.expires_at !== undefined) {
|
|
508
|
+
sets.push("expires_at = ?");
|
|
509
|
+
params.push(input.expires_at);
|
|
510
|
+
}
|
|
511
|
+
if (input.tags !== undefined) {
|
|
512
|
+
sets.push("tags = ?");
|
|
513
|
+
params.push(JSON.stringify(input.tags));
|
|
514
|
+
d.run("DELETE FROM memory_tags WHERE memory_id = ?", [id]);
|
|
515
|
+
const insertTag = d.prepare("INSERT OR IGNORE INTO memory_tags (memory_id, tag) VALUES (?, ?)");
|
|
516
|
+
for (const tag of input.tags) {
|
|
517
|
+
insertTag.run(id, tag);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
params.push(id);
|
|
521
|
+
d.run(`UPDATE memories SET ${sets.join(", ")} WHERE id = ?`, params);
|
|
522
|
+
return getMemory(id, d);
|
|
523
|
+
}
|
|
524
|
+
function deleteMemory(id, db) {
|
|
525
|
+
const d = db || getDatabase();
|
|
526
|
+
const result = d.run("DELETE FROM memories WHERE id = ?", [id]);
|
|
527
|
+
return result.changes > 0;
|
|
528
|
+
}
|
|
529
|
+
function bulkDeleteMemories(ids, db) {
|
|
530
|
+
const d = db || getDatabase();
|
|
531
|
+
if (ids.length === 0)
|
|
532
|
+
return 0;
|
|
533
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
534
|
+
const result = d.run(`DELETE FROM memories WHERE id IN (${placeholders})`, ids);
|
|
535
|
+
return result.changes;
|
|
536
|
+
}
|
|
537
|
+
function touchMemory(id, db) {
|
|
538
|
+
const d = db || getDatabase();
|
|
539
|
+
d.run("UPDATE memories SET access_count = access_count + 1, accessed_at = ? WHERE id = ?", [now(), id]);
|
|
540
|
+
}
|
|
541
|
+
function cleanExpiredMemories(db) {
|
|
542
|
+
const d = db || getDatabase();
|
|
543
|
+
const timestamp = now();
|
|
544
|
+
const result = d.run("DELETE FROM memories WHERE expires_at IS NOT NULL AND expires_at < ?", [timestamp]);
|
|
545
|
+
return result.changes;
|
|
546
|
+
}
|
|
547
|
+
// src/db/agents.ts
|
|
548
|
+
function parseAgentRow(row) {
|
|
549
|
+
return {
|
|
550
|
+
id: row["id"],
|
|
551
|
+
name: row["name"],
|
|
552
|
+
description: row["description"] || null,
|
|
553
|
+
role: row["role"] || null,
|
|
554
|
+
metadata: JSON.parse(row["metadata"] || "{}"),
|
|
555
|
+
created_at: row["created_at"],
|
|
556
|
+
last_seen_at: row["last_seen_at"]
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
function registerAgent(name, description, role, db) {
|
|
560
|
+
const d = db || getDatabase();
|
|
561
|
+
const timestamp = now();
|
|
562
|
+
const existing = d.query("SELECT * FROM agents WHERE name = ?").get(name);
|
|
563
|
+
if (existing) {
|
|
564
|
+
const existingId = existing["id"];
|
|
565
|
+
d.run("UPDATE agents SET last_seen_at = ? WHERE id = ?", [
|
|
566
|
+
timestamp,
|
|
567
|
+
existingId
|
|
568
|
+
]);
|
|
569
|
+
if (description) {
|
|
570
|
+
d.run("UPDATE agents SET description = ? WHERE id = ?", [
|
|
571
|
+
description,
|
|
572
|
+
existingId
|
|
573
|
+
]);
|
|
574
|
+
}
|
|
575
|
+
if (role) {
|
|
576
|
+
d.run("UPDATE agents SET role = ? WHERE id = ?", [
|
|
577
|
+
role,
|
|
578
|
+
existingId
|
|
579
|
+
]);
|
|
580
|
+
}
|
|
581
|
+
return getAgent(existingId, d);
|
|
582
|
+
}
|
|
583
|
+
const id = shortUuid();
|
|
584
|
+
d.run("INSERT INTO agents (id, name, description, role, created_at, last_seen_at) VALUES (?, ?, ?, ?, ?, ?)", [id, name, description || null, role || "agent", timestamp, timestamp]);
|
|
585
|
+
return getAgent(id, d);
|
|
586
|
+
}
|
|
587
|
+
function getAgent(idOrName, db) {
|
|
588
|
+
const d = db || getDatabase();
|
|
589
|
+
let row = d.query("SELECT * FROM agents WHERE id = ?").get(idOrName);
|
|
590
|
+
if (row)
|
|
591
|
+
return parseAgentRow(row);
|
|
592
|
+
row = d.query("SELECT * FROM agents WHERE name = ?").get(idOrName);
|
|
593
|
+
if (row)
|
|
594
|
+
return parseAgentRow(row);
|
|
595
|
+
const rows = d.query("SELECT * FROM agents WHERE id LIKE ?").all(`${idOrName}%`);
|
|
596
|
+
if (rows.length === 1)
|
|
597
|
+
return parseAgentRow(rows[0]);
|
|
598
|
+
return null;
|
|
599
|
+
}
|
|
600
|
+
function listAgents(db) {
|
|
601
|
+
const d = db || getDatabase();
|
|
602
|
+
const rows = d.query("SELECT * FROM agents ORDER BY last_seen_at DESC").all();
|
|
603
|
+
return rows.map(parseAgentRow);
|
|
604
|
+
}
|
|
605
|
+
// src/db/projects.ts
|
|
606
|
+
function parseProjectRow(row) {
|
|
607
|
+
return {
|
|
608
|
+
id: row["id"],
|
|
609
|
+
name: row["name"],
|
|
610
|
+
path: row["path"],
|
|
611
|
+
description: row["description"] || null,
|
|
612
|
+
memory_prefix: row["memory_prefix"] || null,
|
|
613
|
+
created_at: row["created_at"],
|
|
614
|
+
updated_at: row["updated_at"]
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
function registerProject(name, path, description, memoryPrefix, db) {
|
|
618
|
+
const d = db || getDatabase();
|
|
619
|
+
const timestamp = now();
|
|
620
|
+
const existing = d.query("SELECT * FROM projects WHERE path = ?").get(path);
|
|
621
|
+
if (existing) {
|
|
622
|
+
const existingId = existing["id"];
|
|
623
|
+
d.run("UPDATE projects SET updated_at = ? WHERE id = ?", [
|
|
624
|
+
timestamp,
|
|
625
|
+
existingId
|
|
626
|
+
]);
|
|
627
|
+
return parseProjectRow(existing);
|
|
628
|
+
}
|
|
629
|
+
const id = uuid();
|
|
630
|
+
d.run("INSERT INTO projects (id, name, path, description, memory_prefix, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)", [id, name, path, description || null, memoryPrefix || null, timestamp, timestamp]);
|
|
631
|
+
return getProject(id, d);
|
|
632
|
+
}
|
|
633
|
+
function getProject(idOrPath, db) {
|
|
634
|
+
const d = db || getDatabase();
|
|
635
|
+
let row = d.query("SELECT * FROM projects WHERE id = ?").get(idOrPath);
|
|
636
|
+
if (row)
|
|
637
|
+
return parseProjectRow(row);
|
|
638
|
+
row = d.query("SELECT * FROM projects WHERE path = ?").get(idOrPath);
|
|
639
|
+
if (row)
|
|
640
|
+
return parseProjectRow(row);
|
|
641
|
+
return null;
|
|
642
|
+
}
|
|
643
|
+
function listProjects(db) {
|
|
644
|
+
const d = db || getDatabase();
|
|
645
|
+
const rows = d.query("SELECT * FROM projects ORDER BY updated_at DESC").all();
|
|
646
|
+
return rows.map(parseProjectRow);
|
|
647
|
+
}
|
|
648
|
+
// src/lib/search.ts
|
|
649
|
+
function parseMemoryRow2(row) {
|
|
650
|
+
return {
|
|
651
|
+
id: row["id"],
|
|
652
|
+
key: row["key"],
|
|
653
|
+
value: row["value"],
|
|
654
|
+
category: row["category"],
|
|
655
|
+
scope: row["scope"],
|
|
656
|
+
summary: row["summary"] || null,
|
|
657
|
+
tags: JSON.parse(row["tags"] || "[]"),
|
|
658
|
+
importance: row["importance"],
|
|
659
|
+
source: row["source"],
|
|
660
|
+
status: row["status"],
|
|
661
|
+
pinned: !!row["pinned"],
|
|
662
|
+
agent_id: row["agent_id"] || null,
|
|
663
|
+
project_id: row["project_id"] || null,
|
|
664
|
+
session_id: row["session_id"] || null,
|
|
665
|
+
metadata: JSON.parse(row["metadata"] || "{}"),
|
|
666
|
+
access_count: row["access_count"],
|
|
667
|
+
version: row["version"],
|
|
668
|
+
expires_at: row["expires_at"] || null,
|
|
669
|
+
created_at: row["created_at"],
|
|
670
|
+
updated_at: row["updated_at"],
|
|
671
|
+
accessed_at: row["accessed_at"] || null
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
function determineMatchType(memory, queryLower) {
|
|
675
|
+
if (memory.key.toLowerCase() === queryLower)
|
|
676
|
+
return "exact";
|
|
677
|
+
if (memory.tags.some((t) => t.toLowerCase() === queryLower))
|
|
678
|
+
return "tag";
|
|
679
|
+
return "fuzzy";
|
|
680
|
+
}
|
|
681
|
+
function computeScore(memory, queryLower) {
|
|
682
|
+
let score = 0;
|
|
683
|
+
const keyLower = memory.key.toLowerCase();
|
|
684
|
+
if (keyLower === queryLower) {
|
|
685
|
+
score += 10;
|
|
686
|
+
} else if (keyLower.includes(queryLower)) {
|
|
687
|
+
score += 7;
|
|
688
|
+
}
|
|
689
|
+
if (memory.tags.some((t) => t.toLowerCase() === queryLower)) {
|
|
690
|
+
score += 6;
|
|
691
|
+
}
|
|
692
|
+
if (memory.summary && memory.summary.toLowerCase().includes(queryLower)) {
|
|
693
|
+
score += 4;
|
|
694
|
+
}
|
|
695
|
+
if (memory.value.toLowerCase().includes(queryLower)) {
|
|
696
|
+
score += 3;
|
|
697
|
+
}
|
|
698
|
+
return score;
|
|
699
|
+
}
|
|
700
|
+
function searchMemories(query, filter, db) {
|
|
701
|
+
const d = db || getDatabase();
|
|
702
|
+
const queryLower = query.toLowerCase();
|
|
703
|
+
const queryParam = `%${query}%`;
|
|
704
|
+
const conditions = [];
|
|
705
|
+
const params = [];
|
|
706
|
+
conditions.push("m.status = 'active'");
|
|
707
|
+
conditions.push("(m.expires_at IS NULL OR m.expires_at >= datetime('now'))");
|
|
708
|
+
conditions.push(`(m.key LIKE ? OR m.value LIKE ? OR m.summary LIKE ? OR m.id IN (SELECT memory_id FROM memory_tags WHERE tag LIKE ?))`);
|
|
709
|
+
params.push(queryParam, queryParam, queryParam, queryParam);
|
|
710
|
+
if (filter) {
|
|
711
|
+
if (filter.scope) {
|
|
712
|
+
if (Array.isArray(filter.scope)) {
|
|
713
|
+
conditions.push(`m.scope IN (${filter.scope.map(() => "?").join(",")})`);
|
|
714
|
+
params.push(...filter.scope);
|
|
715
|
+
} else {
|
|
716
|
+
conditions.push("m.scope = ?");
|
|
717
|
+
params.push(filter.scope);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
if (filter.category) {
|
|
721
|
+
if (Array.isArray(filter.category)) {
|
|
722
|
+
conditions.push(`m.category IN (${filter.category.map(() => "?").join(",")})`);
|
|
723
|
+
params.push(...filter.category);
|
|
724
|
+
} else {
|
|
725
|
+
conditions.push("m.category = ?");
|
|
726
|
+
params.push(filter.category);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
if (filter.source) {
|
|
730
|
+
if (Array.isArray(filter.source)) {
|
|
731
|
+
conditions.push(`m.source IN (${filter.source.map(() => "?").join(",")})`);
|
|
732
|
+
params.push(...filter.source);
|
|
733
|
+
} else {
|
|
734
|
+
conditions.push("m.source = ?");
|
|
735
|
+
params.push(filter.source);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
if (filter.status) {
|
|
739
|
+
conditions.shift();
|
|
740
|
+
if (Array.isArray(filter.status)) {
|
|
741
|
+
conditions.push(`m.status IN (${filter.status.map(() => "?").join(",")})`);
|
|
742
|
+
params.push(...filter.status);
|
|
743
|
+
} else {
|
|
744
|
+
conditions.push("m.status = ?");
|
|
745
|
+
params.push(filter.status);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
if (filter.project_id) {
|
|
749
|
+
conditions.push("m.project_id = ?");
|
|
750
|
+
params.push(filter.project_id);
|
|
751
|
+
}
|
|
752
|
+
if (filter.agent_id) {
|
|
753
|
+
conditions.push("m.agent_id = ?");
|
|
754
|
+
params.push(filter.agent_id);
|
|
755
|
+
}
|
|
756
|
+
if (filter.session_id) {
|
|
757
|
+
conditions.push("m.session_id = ?");
|
|
758
|
+
params.push(filter.session_id);
|
|
759
|
+
}
|
|
760
|
+
if (filter.min_importance) {
|
|
761
|
+
conditions.push("m.importance >= ?");
|
|
762
|
+
params.push(filter.min_importance);
|
|
763
|
+
}
|
|
764
|
+
if (filter.pinned !== undefined) {
|
|
765
|
+
conditions.push("m.pinned = ?");
|
|
766
|
+
params.push(filter.pinned ? 1 : 0);
|
|
767
|
+
}
|
|
768
|
+
if (filter.tags && filter.tags.length > 0) {
|
|
769
|
+
for (const tag of filter.tags) {
|
|
770
|
+
conditions.push("m.id IN (SELECT memory_id FROM memory_tags WHERE tag = ?)");
|
|
771
|
+
params.push(tag);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
const sql = `SELECT m.* FROM memories m WHERE ${conditions.join(" AND ")}`;
|
|
776
|
+
const rows = d.query(sql).all(...params);
|
|
777
|
+
const scored = [];
|
|
778
|
+
for (const row of rows) {
|
|
779
|
+
const memory = parseMemoryRow2(row);
|
|
780
|
+
const rawScore = computeScore(memory, queryLower);
|
|
781
|
+
if (rawScore === 0)
|
|
782
|
+
continue;
|
|
783
|
+
const weightedScore = rawScore * memory.importance / 10;
|
|
784
|
+
const matchType = determineMatchType(memory, queryLower);
|
|
785
|
+
scored.push({
|
|
786
|
+
memory,
|
|
787
|
+
score: weightedScore,
|
|
788
|
+
match_type: matchType
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
scored.sort((a, b) => {
|
|
792
|
+
if (b.score !== a.score)
|
|
793
|
+
return b.score - a.score;
|
|
794
|
+
return b.memory.importance - a.memory.importance;
|
|
795
|
+
});
|
|
796
|
+
const offset = filter?.offset ?? 0;
|
|
797
|
+
const limit = filter?.limit ?? scored.length;
|
|
798
|
+
return scored.slice(offset, offset + limit);
|
|
799
|
+
}
|
|
800
|
+
// src/lib/config.ts
|
|
801
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync } from "fs";
|
|
802
|
+
import { homedir } from "os";
|
|
803
|
+
import { dirname as dirname2, join as join2, resolve as resolve2 } from "path";
|
|
804
|
+
var DEFAULT_CONFIG = {
|
|
805
|
+
default_scope: "private",
|
|
806
|
+
default_category: "knowledge",
|
|
807
|
+
default_importance: 5,
|
|
808
|
+
max_entries: 1000,
|
|
809
|
+
max_entries_per_scope: {
|
|
810
|
+
global: 500,
|
|
811
|
+
shared: 300,
|
|
812
|
+
private: 200
|
|
813
|
+
},
|
|
814
|
+
injection: {
|
|
815
|
+
max_tokens: 500,
|
|
816
|
+
min_importance: 5,
|
|
817
|
+
categories: ["preference", "fact"],
|
|
818
|
+
refresh_interval: 5
|
|
819
|
+
},
|
|
820
|
+
sync_agents: ["claude", "codex", "gemini"],
|
|
821
|
+
auto_cleanup: {
|
|
822
|
+
enabled: true,
|
|
823
|
+
expired_check_interval: 3600
|
|
824
|
+
}
|
|
825
|
+
};
|
|
826
|
+
function deepMerge(target, source) {
|
|
827
|
+
const result = { ...target };
|
|
828
|
+
for (const key of Object.keys(source)) {
|
|
829
|
+
const sourceVal = source[key];
|
|
830
|
+
const targetVal = result[key];
|
|
831
|
+
if (sourceVal !== null && typeof sourceVal === "object" && !Array.isArray(sourceVal) && targetVal !== null && typeof targetVal === "object" && !Array.isArray(targetVal)) {
|
|
832
|
+
result[key] = deepMerge(targetVal, sourceVal);
|
|
833
|
+
} else {
|
|
834
|
+
result[key] = sourceVal;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
return result;
|
|
838
|
+
}
|
|
839
|
+
var VALID_SCOPES = ["global", "shared", "private"];
|
|
840
|
+
var VALID_CATEGORIES = [
|
|
841
|
+
"preference",
|
|
842
|
+
"fact",
|
|
843
|
+
"knowledge",
|
|
844
|
+
"history"
|
|
845
|
+
];
|
|
846
|
+
function isValidScope(value) {
|
|
847
|
+
return VALID_SCOPES.includes(value);
|
|
848
|
+
}
|
|
849
|
+
function isValidCategory(value) {
|
|
850
|
+
return VALID_CATEGORIES.includes(value);
|
|
851
|
+
}
|
|
852
|
+
function loadConfig() {
|
|
853
|
+
const configPath = join2(homedir(), ".mementos", "config.json");
|
|
854
|
+
let fileConfig = {};
|
|
855
|
+
if (existsSync2(configPath)) {
|
|
856
|
+
try {
|
|
857
|
+
const raw = readFileSync(configPath, "utf-8");
|
|
858
|
+
fileConfig = JSON.parse(raw);
|
|
859
|
+
} catch {}
|
|
860
|
+
}
|
|
861
|
+
const merged = deepMerge(DEFAULT_CONFIG, fileConfig);
|
|
862
|
+
const envScope = process.env["MEMENTOS_DEFAULT_SCOPE"];
|
|
863
|
+
if (envScope && isValidScope(envScope)) {
|
|
864
|
+
merged.default_scope = envScope;
|
|
865
|
+
}
|
|
866
|
+
const envCategory = process.env["MEMENTOS_DEFAULT_CATEGORY"];
|
|
867
|
+
if (envCategory && isValidCategory(envCategory)) {
|
|
868
|
+
merged.default_category = envCategory;
|
|
869
|
+
}
|
|
870
|
+
const envImportance = process.env["MEMENTOS_DEFAULT_IMPORTANCE"];
|
|
871
|
+
if (envImportance) {
|
|
872
|
+
const parsed = parseInt(envImportance, 10);
|
|
873
|
+
if (!Number.isNaN(parsed) && parsed >= 1 && parsed <= 10) {
|
|
874
|
+
merged.default_importance = parsed;
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
return merged;
|
|
878
|
+
}
|
|
879
|
+
// src/lib/injector.ts
|
|
880
|
+
class MemoryInjector {
|
|
881
|
+
config;
|
|
882
|
+
injectedIds = new Set;
|
|
883
|
+
constructor(config) {
|
|
884
|
+
this.config = config || loadConfig();
|
|
885
|
+
}
|
|
886
|
+
getInjectionContext(options = {}) {
|
|
887
|
+
const maxTokens = options.max_tokens || this.config.injection.max_tokens;
|
|
888
|
+
const minImportance = options.min_importance || this.config.injection.min_importance;
|
|
889
|
+
const categories = options.categories || this.config.injection.categories;
|
|
890
|
+
const db = options.db;
|
|
891
|
+
const allMemories = [];
|
|
892
|
+
const globalMems = listMemories({
|
|
893
|
+
scope: "global",
|
|
894
|
+
category: categories,
|
|
895
|
+
min_importance: minImportance,
|
|
896
|
+
status: "active",
|
|
897
|
+
limit: 100
|
|
898
|
+
}, db);
|
|
899
|
+
allMemories.push(...globalMems);
|
|
900
|
+
if (options.project_id) {
|
|
901
|
+
const sharedMems = listMemories({
|
|
902
|
+
scope: "shared",
|
|
903
|
+
category: categories,
|
|
904
|
+
min_importance: minImportance,
|
|
905
|
+
status: "active",
|
|
906
|
+
project_id: options.project_id,
|
|
907
|
+
limit: 100
|
|
908
|
+
}, db);
|
|
909
|
+
allMemories.push(...sharedMems);
|
|
910
|
+
}
|
|
911
|
+
if (options.agent_id) {
|
|
912
|
+
const privateMems = listMemories({
|
|
913
|
+
scope: "private",
|
|
914
|
+
category: categories,
|
|
915
|
+
min_importance: minImportance,
|
|
916
|
+
status: "active",
|
|
917
|
+
agent_id: options.agent_id,
|
|
918
|
+
limit: 100
|
|
919
|
+
}, db);
|
|
920
|
+
allMemories.push(...privateMems);
|
|
921
|
+
}
|
|
922
|
+
const seen = new Set;
|
|
923
|
+
const unique = allMemories.filter((m) => {
|
|
924
|
+
if (seen.has(m.id))
|
|
925
|
+
return false;
|
|
926
|
+
seen.add(m.id);
|
|
927
|
+
return true;
|
|
928
|
+
});
|
|
929
|
+
unique.sort((a, b) => {
|
|
930
|
+
if (a.pinned !== b.pinned)
|
|
931
|
+
return a.pinned ? -1 : 1;
|
|
932
|
+
if (b.importance !== a.importance)
|
|
933
|
+
return b.importance - a.importance;
|
|
934
|
+
return new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime();
|
|
935
|
+
});
|
|
936
|
+
const charBudget = maxTokens * 4;
|
|
937
|
+
const lines = [];
|
|
938
|
+
let totalChars = 0;
|
|
939
|
+
for (const m of unique) {
|
|
940
|
+
if (this.injectedIds.has(m.id))
|
|
941
|
+
continue;
|
|
942
|
+
const line = `- [${m.scope}/${m.category}] ${m.key}: ${m.value}`;
|
|
943
|
+
if (totalChars + line.length > charBudget)
|
|
944
|
+
break;
|
|
945
|
+
lines.push(line);
|
|
946
|
+
totalChars += line.length;
|
|
947
|
+
this.injectedIds.add(m.id);
|
|
948
|
+
touchMemory(m.id, db);
|
|
949
|
+
}
|
|
950
|
+
if (lines.length === 0) {
|
|
951
|
+
return "";
|
|
952
|
+
}
|
|
953
|
+
return `<agent-memories>
|
|
954
|
+
${lines.join(`
|
|
955
|
+
`)}
|
|
956
|
+
</agent-memories>`;
|
|
957
|
+
}
|
|
958
|
+
resetDedup() {
|
|
959
|
+
this.injectedIds.clear();
|
|
960
|
+
}
|
|
961
|
+
getInjectedCount() {
|
|
962
|
+
return this.injectedIds.size;
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
// src/lib/retention.ts
|
|
966
|
+
function enforceQuotas(config, db) {
|
|
967
|
+
const d = db || getDatabase();
|
|
968
|
+
let totalEvicted = 0;
|
|
969
|
+
const scopes = ["global", "shared", "private"];
|
|
970
|
+
for (const scope of scopes) {
|
|
971
|
+
const limit = config.max_entries_per_scope[scope];
|
|
972
|
+
if (!limit || limit <= 0)
|
|
973
|
+
continue;
|
|
974
|
+
const countRow = d.query("SELECT COUNT(*) as cnt FROM memories WHERE scope = ? AND status = 'active'").get(scope);
|
|
975
|
+
const count = countRow.cnt;
|
|
976
|
+
if (count <= limit)
|
|
977
|
+
continue;
|
|
978
|
+
const excess = count - limit;
|
|
979
|
+
const result = d.run(`DELETE FROM memories WHERE id IN (
|
|
980
|
+
SELECT id FROM memories
|
|
981
|
+
WHERE scope = ? AND status = 'active' AND pinned = 0
|
|
982
|
+
ORDER BY importance ASC, created_at ASC
|
|
983
|
+
LIMIT ?
|
|
984
|
+
)`, [scope, excess]);
|
|
985
|
+
totalEvicted += result.changes;
|
|
986
|
+
}
|
|
987
|
+
return totalEvicted;
|
|
988
|
+
}
|
|
989
|
+
function archiveStale(staleDays, db) {
|
|
990
|
+
const d = db || getDatabase();
|
|
991
|
+
const timestamp = now();
|
|
992
|
+
const cutoff = new Date(Date.now() - staleDays * 24 * 60 * 60 * 1000).toISOString();
|
|
993
|
+
const result = d.run(`UPDATE memories
|
|
994
|
+
SET status = 'archived', updated_at = ?
|
|
995
|
+
WHERE status = 'active'
|
|
996
|
+
AND pinned = 0
|
|
997
|
+
AND COALESCE(accessed_at, created_at) < ?`, [timestamp, cutoff]);
|
|
998
|
+
return result.changes;
|
|
999
|
+
}
|
|
1000
|
+
function runCleanup(config, db) {
|
|
1001
|
+
const d = db || getDatabase();
|
|
1002
|
+
const expired = cleanExpiredMemories(d);
|
|
1003
|
+
const evicted = enforceQuotas(config, d);
|
|
1004
|
+
const archived = archiveStale(90, d);
|
|
1005
|
+
return { expired, evicted, archived };
|
|
1006
|
+
}
|
|
1007
|
+
// src/lib/sync.ts
|
|
1008
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
1009
|
+
import { homedir as homedir2 } from "os";
|
|
1010
|
+
import { join as join3 } from "path";
|
|
1011
|
+
function getAgentSyncDir(agentName) {
|
|
1012
|
+
const dir = join3(homedir2(), ".mementos", "agents", agentName);
|
|
1013
|
+
if (!existsSync3(dir)) {
|
|
1014
|
+
mkdirSync3(dir, { recursive: true });
|
|
1015
|
+
}
|
|
1016
|
+
return dir;
|
|
1017
|
+
}
|
|
1018
|
+
function setHighWaterMark(agentDir, timestamp) {
|
|
1019
|
+
const markFile = join3(agentDir, ".highwatermark");
|
|
1020
|
+
writeFileSync(markFile, timestamp, "utf-8");
|
|
1021
|
+
}
|
|
1022
|
+
function resolveConflict(local, remote, resolution) {
|
|
1023
|
+
switch (resolution) {
|
|
1024
|
+
case "prefer-local":
|
|
1025
|
+
return "local";
|
|
1026
|
+
case "prefer-remote":
|
|
1027
|
+
return "remote";
|
|
1028
|
+
case "prefer-newer":
|
|
1029
|
+
return new Date(local.updated_at).getTime() >= new Date(remote.updated_at).getTime() ? "local" : "remote";
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
function pushMemories(agentName, agentId, projectId, db) {
|
|
1033
|
+
const agentDir = getAgentSyncDir(agentName);
|
|
1034
|
+
const memories = listMemories({
|
|
1035
|
+
agent_id: agentId,
|
|
1036
|
+
project_id: projectId,
|
|
1037
|
+
status: "active",
|
|
1038
|
+
limit: 1e4
|
|
1039
|
+
}, db);
|
|
1040
|
+
const outFile = join3(agentDir, "memories.json");
|
|
1041
|
+
writeFileSync(outFile, JSON.stringify(memories, null, 2), "utf-8");
|
|
1042
|
+
if (memories.length > 0) {
|
|
1043
|
+
const latest = memories.reduce((a, b) => new Date(a.updated_at).getTime() > new Date(b.updated_at).getTime() ? a : b);
|
|
1044
|
+
setHighWaterMark(agentDir, latest.updated_at);
|
|
1045
|
+
}
|
|
1046
|
+
return memories.length;
|
|
1047
|
+
}
|
|
1048
|
+
function pullMemories(agentName, conflictResolution = "prefer-newer", db) {
|
|
1049
|
+
const agentDir = getAgentSyncDir(agentName);
|
|
1050
|
+
const inFile = join3(agentDir, "memories.json");
|
|
1051
|
+
if (!existsSync3(inFile)) {
|
|
1052
|
+
return { pulled: 0, conflicts: 0 };
|
|
1053
|
+
}
|
|
1054
|
+
const raw = readFileSync2(inFile, "utf-8");
|
|
1055
|
+
let remoteMemories;
|
|
1056
|
+
try {
|
|
1057
|
+
remoteMemories = JSON.parse(raw);
|
|
1058
|
+
} catch {
|
|
1059
|
+
return { pulled: 0, conflicts: 0 };
|
|
1060
|
+
}
|
|
1061
|
+
let pulled = 0;
|
|
1062
|
+
let conflicts = 0;
|
|
1063
|
+
for (const remote of remoteMemories) {
|
|
1064
|
+
const localMemories = listMemories({
|
|
1065
|
+
search: remote.key,
|
|
1066
|
+
scope: remote.scope,
|
|
1067
|
+
agent_id: remote.agent_id || undefined,
|
|
1068
|
+
project_id: remote.project_id || undefined,
|
|
1069
|
+
limit: 1
|
|
1070
|
+
}, db);
|
|
1071
|
+
const local = localMemories.find((m) => m.key === remote.key);
|
|
1072
|
+
if (local) {
|
|
1073
|
+
const winner = resolveConflict(local, remote, conflictResolution);
|
|
1074
|
+
if (winner === "remote") {
|
|
1075
|
+
createMemory({
|
|
1076
|
+
key: remote.key,
|
|
1077
|
+
value: remote.value,
|
|
1078
|
+
category: remote.category,
|
|
1079
|
+
scope: remote.scope,
|
|
1080
|
+
summary: remote.summary || undefined,
|
|
1081
|
+
tags: remote.tags,
|
|
1082
|
+
importance: remote.importance,
|
|
1083
|
+
source: remote.source,
|
|
1084
|
+
agent_id: remote.agent_id || undefined,
|
|
1085
|
+
project_id: remote.project_id || undefined,
|
|
1086
|
+
session_id: remote.session_id || undefined,
|
|
1087
|
+
metadata: remote.metadata,
|
|
1088
|
+
expires_at: remote.expires_at || undefined
|
|
1089
|
+
}, "merge", db);
|
|
1090
|
+
pulled++;
|
|
1091
|
+
}
|
|
1092
|
+
conflicts++;
|
|
1093
|
+
} else {
|
|
1094
|
+
createMemory({
|
|
1095
|
+
key: remote.key,
|
|
1096
|
+
value: remote.value,
|
|
1097
|
+
category: remote.category,
|
|
1098
|
+
scope: remote.scope,
|
|
1099
|
+
summary: remote.summary || undefined,
|
|
1100
|
+
tags: remote.tags,
|
|
1101
|
+
importance: remote.importance,
|
|
1102
|
+
source: remote.source,
|
|
1103
|
+
agent_id: remote.agent_id || undefined,
|
|
1104
|
+
project_id: remote.project_id || undefined,
|
|
1105
|
+
session_id: remote.session_id || undefined,
|
|
1106
|
+
metadata: remote.metadata,
|
|
1107
|
+
expires_at: remote.expires_at || undefined
|
|
1108
|
+
}, "create", db);
|
|
1109
|
+
pulled++;
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
return { pulled, conflicts };
|
|
1113
|
+
}
|
|
1114
|
+
function syncMemories(agentName, direction = "both", options = {}) {
|
|
1115
|
+
const result = {
|
|
1116
|
+
pushed: 0,
|
|
1117
|
+
pulled: 0,
|
|
1118
|
+
conflicts: 0,
|
|
1119
|
+
errors: []
|
|
1120
|
+
};
|
|
1121
|
+
try {
|
|
1122
|
+
if (direction === "push" || direction === "both") {
|
|
1123
|
+
result.pushed = pushMemories(agentName, options.agent_id, options.project_id, options.db);
|
|
1124
|
+
}
|
|
1125
|
+
if (direction === "pull" || direction === "both") {
|
|
1126
|
+
const pullResult = pullMemories(agentName, options.conflict_resolution || "prefer-newer", options.db);
|
|
1127
|
+
result.pulled = pullResult.pulled;
|
|
1128
|
+
result.conflicts = pullResult.conflicts;
|
|
1129
|
+
}
|
|
1130
|
+
} catch (e) {
|
|
1131
|
+
result.errors.push(e instanceof Error ? e.message : String(e));
|
|
1132
|
+
}
|
|
1133
|
+
return result;
|
|
1134
|
+
}
|
|
1135
|
+
var defaultSyncAgents = ["claude", "codex", "gemini"];
|
|
1136
|
+
export {
|
|
1137
|
+
uuid,
|
|
1138
|
+
updateMemory,
|
|
1139
|
+
touchMemory,
|
|
1140
|
+
syncMemories,
|
|
1141
|
+
shortUuid,
|
|
1142
|
+
searchMemories,
|
|
1143
|
+
runCleanup,
|
|
1144
|
+
resolvePartialId,
|
|
1145
|
+
resetDatabase,
|
|
1146
|
+
registerProject,
|
|
1147
|
+
registerAgent,
|
|
1148
|
+
now,
|
|
1149
|
+
loadConfig,
|
|
1150
|
+
listProjects,
|
|
1151
|
+
listMemories,
|
|
1152
|
+
listAgents,
|
|
1153
|
+
getProject,
|
|
1154
|
+
getMemoryByKey,
|
|
1155
|
+
getMemory,
|
|
1156
|
+
getDbPath,
|
|
1157
|
+
getDatabase,
|
|
1158
|
+
getAgent,
|
|
1159
|
+
enforceQuotas,
|
|
1160
|
+
deleteMemory,
|
|
1161
|
+
defaultSyncAgents,
|
|
1162
|
+
createMemory,
|
|
1163
|
+
closeDatabase,
|
|
1164
|
+
cleanExpiredMemories,
|
|
1165
|
+
bulkDeleteMemories,
|
|
1166
|
+
archiveStale,
|
|
1167
|
+
VersionConflictError,
|
|
1168
|
+
MemoryNotFoundError,
|
|
1169
|
+
MemoryInjector,
|
|
1170
|
+
MemoryExpiredError,
|
|
1171
|
+
InvalidScopeError,
|
|
1172
|
+
DuplicateMemoryError,
|
|
1173
|
+
DEFAULT_CONFIG
|
|
1174
|
+
};
|