@hasna/mementos 0.3.7 → 0.4.1
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/dashboard/dist/assets/index-B1yiOEw3.js +290 -0
- package/dashboard/dist/assets/index-DnpbasSl.css +1 -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 +6985 -0
- package/dist/db/agents.d.ts +12 -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/entities.d.ts +17 -0
- package/dist/db/entities.d.ts.map +1 -0
- package/dist/db/entity-memories.d.ts +34 -0
- package/dist/db/entity-memories.d.ts.map +1 -0
- package/dist/db/memories.d.ts +19 -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/db/relations.d.ts +29 -0
- package/dist/db/relations.d.ts.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2427 -0
- package/dist/lib/config.d.ts +5 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/duration.d.ts +28 -0
- package/dist/lib/duration.d.ts.map +1 -0
- package/dist/lib/extractor.d.ts +9 -0
- package/dist/lib/extractor.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/poll.d.ts +17 -0
- package/dist/lib/poll.d.ts.map +1 -0
- package/dist/lib/project-detect.d.ts +18 -0
- package/dist/lib/project-detect.d.ts.map +1 -0
- package/dist/lib/redact.d.ts +10 -0
- package/dist/lib/redact.d.ts.map +1 -0
- package/dist/lib/retention.d.ts +14 -0
- package/dist/lib/retention.d.ts.map +1 -0
- package/dist/lib/search.d.ts +22 -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 +6903 -0
- package/dist/server/index.d.ts +7 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +2616 -0
- package/dist/types/index.d.ts +237 -0
- package/dist/types/index.d.ts.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,2616 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// @bun
|
|
3
|
+
|
|
4
|
+
// src/server/index.ts
|
|
5
|
+
import { existsSync as existsSync3 } from "fs";
|
|
6
|
+
import { dirname as dirname3, extname, join as join3 } from "path";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
8
|
+
|
|
9
|
+
// src/types/index.ts
|
|
10
|
+
class EntityNotFoundError extends Error {
|
|
11
|
+
constructor(id) {
|
|
12
|
+
super(`Entity not found: ${id}`);
|
|
13
|
+
this.name = "EntityNotFoundError";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
class MemoryNotFoundError extends Error {
|
|
18
|
+
constructor(id) {
|
|
19
|
+
super(`Memory not found: ${id}`);
|
|
20
|
+
this.name = "MemoryNotFoundError";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
class DuplicateMemoryError extends Error {
|
|
25
|
+
constructor(key, scope) {
|
|
26
|
+
super(`Memory already exists with key "${key}" in scope "${scope}"`);
|
|
27
|
+
this.name = "DuplicateMemoryError";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
class VersionConflictError extends Error {
|
|
31
|
+
expected;
|
|
32
|
+
actual;
|
|
33
|
+
constructor(id, expected, actual) {
|
|
34
|
+
super(`Version conflict for memory ${id}: expected ${expected}, got ${actual}`);
|
|
35
|
+
this.name = "VersionConflictError";
|
|
36
|
+
this.expected = expected;
|
|
37
|
+
this.actual = actual;
|
|
38
|
+
}
|
|
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
|
+
CREATE TABLE IF NOT EXISTS memory_versions (
|
|
188
|
+
id TEXT PRIMARY KEY,
|
|
189
|
+
memory_id TEXT NOT NULL,
|
|
190
|
+
version INTEGER NOT NULL,
|
|
191
|
+
value TEXT NOT NULL,
|
|
192
|
+
importance INTEGER NOT NULL,
|
|
193
|
+
scope TEXT NOT NULL,
|
|
194
|
+
category TEXT NOT NULL,
|
|
195
|
+
tags TEXT NOT NULL DEFAULT '[]',
|
|
196
|
+
summary TEXT,
|
|
197
|
+
pinned INTEGER NOT NULL DEFAULT 0,
|
|
198
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
199
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
200
|
+
UNIQUE(memory_id, version)
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
CREATE INDEX IF NOT EXISTS idx_memory_versions_memory ON memory_versions(memory_id);
|
|
204
|
+
CREATE INDEX IF NOT EXISTS idx_memory_versions_version ON memory_versions(memory_id, version);
|
|
205
|
+
|
|
206
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (2);
|
|
207
|
+
`,
|
|
208
|
+
`
|
|
209
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
|
|
210
|
+
key, value, summary,
|
|
211
|
+
content='memories',
|
|
212
|
+
content_rowid='rowid'
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
CREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories BEGIN
|
|
216
|
+
INSERT INTO memories_fts(rowid, key, value, summary) VALUES (new.rowid, new.key, new.value, new.summary);
|
|
217
|
+
END;
|
|
218
|
+
|
|
219
|
+
CREATE TRIGGER IF NOT EXISTS memories_ad AFTER DELETE ON memories BEGIN
|
|
220
|
+
INSERT INTO memories_fts(memories_fts, rowid, key, value, summary) VALUES('delete', old.rowid, old.key, old.value, old.summary);
|
|
221
|
+
END;
|
|
222
|
+
|
|
223
|
+
CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE ON memories BEGIN
|
|
224
|
+
INSERT INTO memories_fts(memories_fts, rowid, key, value, summary) VALUES('delete', old.rowid, old.key, old.value, old.summary);
|
|
225
|
+
INSERT INTO memories_fts(rowid, key, value, summary) VALUES (new.rowid, new.key, new.value, new.summary);
|
|
226
|
+
END;
|
|
227
|
+
|
|
228
|
+
INSERT INTO memories_fts(memories_fts) VALUES('rebuild');
|
|
229
|
+
|
|
230
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (3);
|
|
231
|
+
`,
|
|
232
|
+
`
|
|
233
|
+
CREATE TABLE IF NOT EXISTS search_history (
|
|
234
|
+
id TEXT PRIMARY KEY,
|
|
235
|
+
query TEXT NOT NULL,
|
|
236
|
+
result_count INTEGER NOT NULL DEFAULT 0,
|
|
237
|
+
agent_id TEXT,
|
|
238
|
+
project_id TEXT,
|
|
239
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
240
|
+
);
|
|
241
|
+
CREATE INDEX IF NOT EXISTS idx_search_history_query ON search_history(query);
|
|
242
|
+
CREATE INDEX IF NOT EXISTS idx_search_history_created ON search_history(created_at);
|
|
243
|
+
|
|
244
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (4);
|
|
245
|
+
`,
|
|
246
|
+
`
|
|
247
|
+
CREATE TABLE IF NOT EXISTS entities (
|
|
248
|
+
id TEXT PRIMARY KEY,
|
|
249
|
+
name TEXT NOT NULL,
|
|
250
|
+
type TEXT NOT NULL CHECK (type IN ('person','project','tool','concept','file','api','pattern','organization')),
|
|
251
|
+
description TEXT,
|
|
252
|
+
metadata TEXT NOT NULL DEFAULT '{}',
|
|
253
|
+
project_id TEXT,
|
|
254
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
255
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
256
|
+
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE SET NULL
|
|
257
|
+
);
|
|
258
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_entities_unique_name_type_project
|
|
259
|
+
ON entities(name, type, COALESCE(project_id, ''));
|
|
260
|
+
CREATE INDEX IF NOT EXISTS idx_entities_name ON entities(name);
|
|
261
|
+
CREATE INDEX IF NOT EXISTS idx_entities_type ON entities(type);
|
|
262
|
+
CREATE INDEX IF NOT EXISTS idx_entities_project ON entities(project_id);
|
|
263
|
+
|
|
264
|
+
CREATE TABLE IF NOT EXISTS relations (
|
|
265
|
+
id TEXT PRIMARY KEY,
|
|
266
|
+
source_entity_id TEXT NOT NULL,
|
|
267
|
+
target_entity_id TEXT NOT NULL,
|
|
268
|
+
relation_type TEXT NOT NULL CHECK (relation_type IN ('uses','knows','depends_on','created_by','related_to','contradicts','part_of','implements')),
|
|
269
|
+
weight REAL NOT NULL DEFAULT 1.0,
|
|
270
|
+
metadata TEXT NOT NULL DEFAULT '{}',
|
|
271
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
272
|
+
UNIQUE(source_entity_id, target_entity_id, relation_type),
|
|
273
|
+
FOREIGN KEY (source_entity_id) REFERENCES entities(id) ON DELETE CASCADE,
|
|
274
|
+
FOREIGN KEY (target_entity_id) REFERENCES entities(id) ON DELETE CASCADE
|
|
275
|
+
);
|
|
276
|
+
CREATE INDEX IF NOT EXISTS idx_relations_source ON relations(source_entity_id);
|
|
277
|
+
CREATE INDEX IF NOT EXISTS idx_relations_target ON relations(target_entity_id);
|
|
278
|
+
CREATE INDEX IF NOT EXISTS idx_relations_type ON relations(relation_type);
|
|
279
|
+
|
|
280
|
+
CREATE TABLE IF NOT EXISTS entity_memories (
|
|
281
|
+
entity_id TEXT NOT NULL,
|
|
282
|
+
memory_id TEXT NOT NULL,
|
|
283
|
+
role TEXT NOT NULL DEFAULT 'context' CHECK (role IN ('subject','object','context')),
|
|
284
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
285
|
+
PRIMARY KEY (entity_id, memory_id),
|
|
286
|
+
FOREIGN KEY (entity_id) REFERENCES entities(id) ON DELETE CASCADE,
|
|
287
|
+
FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE CASCADE
|
|
288
|
+
);
|
|
289
|
+
CREATE INDEX IF NOT EXISTS idx_entity_memories_memory ON entity_memories(memory_id);
|
|
290
|
+
|
|
291
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (5);
|
|
292
|
+
`
|
|
293
|
+
];
|
|
294
|
+
var _db = null;
|
|
295
|
+
function getDatabase(dbPath) {
|
|
296
|
+
if (_db)
|
|
297
|
+
return _db;
|
|
298
|
+
const path = dbPath || getDbPath();
|
|
299
|
+
ensureDir(path);
|
|
300
|
+
_db = new Database(path, { create: true });
|
|
301
|
+
_db.run("PRAGMA journal_mode = WAL");
|
|
302
|
+
_db.run("PRAGMA busy_timeout = 5000");
|
|
303
|
+
_db.run("PRAGMA foreign_keys = ON");
|
|
304
|
+
runMigrations(_db);
|
|
305
|
+
return _db;
|
|
306
|
+
}
|
|
307
|
+
function runMigrations(db) {
|
|
308
|
+
try {
|
|
309
|
+
const result = db.query("SELECT MAX(id) as max_id FROM _migrations").get();
|
|
310
|
+
const currentLevel = result?.max_id ?? 0;
|
|
311
|
+
for (let i = currentLevel;i < MIGRATIONS.length; i++) {
|
|
312
|
+
try {
|
|
313
|
+
db.exec(MIGRATIONS[i]);
|
|
314
|
+
} catch {}
|
|
315
|
+
}
|
|
316
|
+
} catch {
|
|
317
|
+
for (const migration of MIGRATIONS) {
|
|
318
|
+
try {
|
|
319
|
+
db.exec(migration);
|
|
320
|
+
} catch {}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
function now() {
|
|
325
|
+
return new Date().toISOString();
|
|
326
|
+
}
|
|
327
|
+
function uuid() {
|
|
328
|
+
return crypto.randomUUID();
|
|
329
|
+
}
|
|
330
|
+
function shortUuid() {
|
|
331
|
+
return crypto.randomUUID().slice(0, 8);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// src/lib/redact.ts
|
|
335
|
+
var REDACTED = "[REDACTED]";
|
|
336
|
+
var SECRET_PATTERNS = [
|
|
337
|
+
{ name: "openai_key", pattern: /sk-[a-zA-Z0-9_-]{20,}/g },
|
|
338
|
+
{ name: "anthropic_key", pattern: /sk-ant-[a-zA-Z0-9_-]{20,}/g },
|
|
339
|
+
{ name: "generic_key", pattern: /(?:pk|tok|key|token|api[_-]?key)[_-][a-zA-Z0-9_-]{16,}/gi },
|
|
340
|
+
{ name: "aws_key", pattern: /AKIA[A-Z0-9]{16}/g },
|
|
341
|
+
{ name: "aws_secret", pattern: /(?<=AWS_SECRET_ACCESS_KEY\s*=\s*)[A-Za-z0-9/+=]{40}/g },
|
|
342
|
+
{ name: "github_token", pattern: /gh[ps]_[a-zA-Z0-9]{36,}/g },
|
|
343
|
+
{ name: "github_oauth", pattern: /gho_[a-zA-Z0-9]{36,}/g },
|
|
344
|
+
{ name: "npm_token", pattern: /npm_[a-zA-Z0-9]{36,}/g },
|
|
345
|
+
{ name: "bearer", pattern: /Bearer\s+[a-zA-Z0-9_\-.]{20,}/g },
|
|
346
|
+
{ name: "conn_string", pattern: /(?:postgres|postgresql|mysql|mongodb|redis|amqp|mqtt):\/\/[^\s"'`]+@[^\s"'`]+/gi },
|
|
347
|
+
{ name: "env_secret", pattern: /(?:SECRET|TOKEN|PASSWORD|PASSPHRASE|API_KEY|PRIVATE_KEY|AUTH|CREDENTIAL)[_A-Z]*\s*=\s*["']?[^\s"'\n]{8,}["']?/gi },
|
|
348
|
+
{ name: "stripe_key", pattern: /(?:sk|pk|rk)_(?:test|live)_[a-zA-Z0-9]{20,}/g },
|
|
349
|
+
{ name: "slack_token", pattern: /xox[bpras]-[a-zA-Z0-9-]{20,}/g },
|
|
350
|
+
{ name: "jwt", pattern: /eyJ[a-zA-Z0-9_-]{10,}\.eyJ[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,}/g },
|
|
351
|
+
{ name: "hex_secret", pattern: /(?<=(?:key|token|secret|password|hash)\s*[:=]\s*["']?)[0-9a-f]{32,}(?=["']?)/gi }
|
|
352
|
+
];
|
|
353
|
+
function redactSecrets(text) {
|
|
354
|
+
let result = text;
|
|
355
|
+
for (const { pattern } of SECRET_PATTERNS) {
|
|
356
|
+
pattern.lastIndex = 0;
|
|
357
|
+
result = result.replace(pattern, REDACTED);
|
|
358
|
+
}
|
|
359
|
+
return result;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// src/db/agents.ts
|
|
363
|
+
function parseAgentRow(row) {
|
|
364
|
+
return {
|
|
365
|
+
id: row["id"],
|
|
366
|
+
name: row["name"],
|
|
367
|
+
description: row["description"] || null,
|
|
368
|
+
role: row["role"] || null,
|
|
369
|
+
metadata: JSON.parse(row["metadata"] || "{}"),
|
|
370
|
+
created_at: row["created_at"],
|
|
371
|
+
last_seen_at: row["last_seen_at"]
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
function registerAgent(name, description, role, db) {
|
|
375
|
+
const d = db || getDatabase();
|
|
376
|
+
const timestamp = now();
|
|
377
|
+
const normalizedName = name.trim().toLowerCase();
|
|
378
|
+
const existing = d.query("SELECT * FROM agents WHERE LOWER(name) = ?").get(normalizedName);
|
|
379
|
+
if (existing) {
|
|
380
|
+
const existingId = existing["id"];
|
|
381
|
+
d.run("UPDATE agents SET last_seen_at = ? WHERE id = ?", [
|
|
382
|
+
timestamp,
|
|
383
|
+
existingId
|
|
384
|
+
]);
|
|
385
|
+
if (description) {
|
|
386
|
+
d.run("UPDATE agents SET description = ? WHERE id = ?", [
|
|
387
|
+
description,
|
|
388
|
+
existingId
|
|
389
|
+
]);
|
|
390
|
+
}
|
|
391
|
+
if (role) {
|
|
392
|
+
d.run("UPDATE agents SET role = ? WHERE id = ?", [
|
|
393
|
+
role,
|
|
394
|
+
existingId
|
|
395
|
+
]);
|
|
396
|
+
}
|
|
397
|
+
return getAgent(existingId, d);
|
|
398
|
+
}
|
|
399
|
+
const id = shortUuid();
|
|
400
|
+
d.run("INSERT INTO agents (id, name, description, role, created_at, last_seen_at) VALUES (?, ?, ?, ?, ?, ?)", [id, normalizedName, description || null, role || "agent", timestamp, timestamp]);
|
|
401
|
+
return getAgent(id, d);
|
|
402
|
+
}
|
|
403
|
+
function getAgent(idOrName, db) {
|
|
404
|
+
const d = db || getDatabase();
|
|
405
|
+
let row = d.query("SELECT * FROM agents WHERE id = ?").get(idOrName);
|
|
406
|
+
if (row)
|
|
407
|
+
return parseAgentRow(row);
|
|
408
|
+
row = d.query("SELECT * FROM agents WHERE LOWER(name) = ?").get(idOrName.trim().toLowerCase());
|
|
409
|
+
if (row)
|
|
410
|
+
return parseAgentRow(row);
|
|
411
|
+
const rows = d.query("SELECT * FROM agents WHERE id LIKE ?").all(`${idOrName}%`);
|
|
412
|
+
if (rows.length === 1)
|
|
413
|
+
return parseAgentRow(rows[0]);
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
function listAgents(db) {
|
|
417
|
+
const d = db || getDatabase();
|
|
418
|
+
const rows = d.query("SELECT * FROM agents ORDER BY last_seen_at DESC").all();
|
|
419
|
+
return rows.map(parseAgentRow);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// src/db/projects.ts
|
|
423
|
+
function parseProjectRow(row) {
|
|
424
|
+
return {
|
|
425
|
+
id: row["id"],
|
|
426
|
+
name: row["name"],
|
|
427
|
+
path: row["path"],
|
|
428
|
+
description: row["description"] || null,
|
|
429
|
+
memory_prefix: row["memory_prefix"] || null,
|
|
430
|
+
created_at: row["created_at"],
|
|
431
|
+
updated_at: row["updated_at"]
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
function registerProject(name, path, description, memoryPrefix, db) {
|
|
435
|
+
const d = db || getDatabase();
|
|
436
|
+
const timestamp = now();
|
|
437
|
+
const existing = d.query("SELECT * FROM projects WHERE path = ?").get(path);
|
|
438
|
+
if (existing) {
|
|
439
|
+
const existingId = existing["id"];
|
|
440
|
+
d.run("UPDATE projects SET updated_at = ? WHERE id = ?", [
|
|
441
|
+
timestamp,
|
|
442
|
+
existingId
|
|
443
|
+
]);
|
|
444
|
+
return parseProjectRow(existing);
|
|
445
|
+
}
|
|
446
|
+
const id = uuid();
|
|
447
|
+
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]);
|
|
448
|
+
return getProject(id, d);
|
|
449
|
+
}
|
|
450
|
+
function getProject(idOrPath, db) {
|
|
451
|
+
const d = db || getDatabase();
|
|
452
|
+
let row = d.query("SELECT * FROM projects WHERE id = ?").get(idOrPath);
|
|
453
|
+
if (row)
|
|
454
|
+
return parseProjectRow(row);
|
|
455
|
+
row = d.query("SELECT * FROM projects WHERE path = ?").get(idOrPath);
|
|
456
|
+
if (row)
|
|
457
|
+
return parseProjectRow(row);
|
|
458
|
+
return null;
|
|
459
|
+
}
|
|
460
|
+
function listProjects(db) {
|
|
461
|
+
const d = db || getDatabase();
|
|
462
|
+
const rows = d.query("SELECT * FROM projects ORDER BY updated_at DESC").all();
|
|
463
|
+
return rows.map(parseProjectRow);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// src/lib/extractor.ts
|
|
467
|
+
var TECH_KEYWORDS = new Set([
|
|
468
|
+
"typescript",
|
|
469
|
+
"javascript",
|
|
470
|
+
"python",
|
|
471
|
+
"rust",
|
|
472
|
+
"go",
|
|
473
|
+
"java",
|
|
474
|
+
"ruby",
|
|
475
|
+
"swift",
|
|
476
|
+
"kotlin",
|
|
477
|
+
"react",
|
|
478
|
+
"vue",
|
|
479
|
+
"angular",
|
|
480
|
+
"svelte",
|
|
481
|
+
"nextjs",
|
|
482
|
+
"bun",
|
|
483
|
+
"node",
|
|
484
|
+
"deno",
|
|
485
|
+
"sqlite",
|
|
486
|
+
"postgres",
|
|
487
|
+
"mysql",
|
|
488
|
+
"redis",
|
|
489
|
+
"docker",
|
|
490
|
+
"kubernetes",
|
|
491
|
+
"git",
|
|
492
|
+
"npm",
|
|
493
|
+
"yarn",
|
|
494
|
+
"pnpm",
|
|
495
|
+
"webpack",
|
|
496
|
+
"vite",
|
|
497
|
+
"tailwind",
|
|
498
|
+
"prisma",
|
|
499
|
+
"drizzle",
|
|
500
|
+
"zod",
|
|
501
|
+
"commander",
|
|
502
|
+
"express",
|
|
503
|
+
"fastify",
|
|
504
|
+
"hono"
|
|
505
|
+
]);
|
|
506
|
+
var FILE_PATH_RE = /(?:^|\s)((?:\/|\.\/|~\/)?(?:[\w.-]+\/)+[\w.-]+\.\w+)/g;
|
|
507
|
+
var URL_RE = /https?:\/\/[^\s)]+/g;
|
|
508
|
+
var NPM_PACKAGE_RE = /@[\w-]+\/[\w.-]+/g;
|
|
509
|
+
var PASCAL_CASE_RE = /\b([A-Z][a-z]+(?:[A-Z][a-z]+)+)\b/g;
|
|
510
|
+
function getSearchText(memory) {
|
|
511
|
+
const parts = [memory.key, memory.value];
|
|
512
|
+
if (memory.summary)
|
|
513
|
+
parts.push(memory.summary);
|
|
514
|
+
return parts.join(" ");
|
|
515
|
+
}
|
|
516
|
+
function extractEntities(memory, db) {
|
|
517
|
+
const text = getSearchText(memory);
|
|
518
|
+
const entityMap = new Map;
|
|
519
|
+
function add(name, type, confidence) {
|
|
520
|
+
const normalized = name.toLowerCase();
|
|
521
|
+
if (normalized.length < 3)
|
|
522
|
+
return;
|
|
523
|
+
const existing = entityMap.get(normalized);
|
|
524
|
+
if (!existing || existing.confidence < confidence) {
|
|
525
|
+
entityMap.set(normalized, { name: normalized, type, confidence });
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
for (const match of text.matchAll(FILE_PATH_RE)) {
|
|
529
|
+
add(match[1].trim(), "file", 0.9);
|
|
530
|
+
}
|
|
531
|
+
for (const match of text.matchAll(URL_RE)) {
|
|
532
|
+
add(match[0], "api", 0.8);
|
|
533
|
+
}
|
|
534
|
+
for (const match of text.matchAll(NPM_PACKAGE_RE)) {
|
|
535
|
+
add(match[0], "tool", 0.85);
|
|
536
|
+
}
|
|
537
|
+
try {
|
|
538
|
+
const d = db || getDatabase();
|
|
539
|
+
const agents = listAgents(d);
|
|
540
|
+
const textLower2 = text.toLowerCase();
|
|
541
|
+
for (const agent of agents) {
|
|
542
|
+
const nameLower = agent.name.toLowerCase();
|
|
543
|
+
if (nameLower.length >= 3 && textLower2.includes(nameLower)) {
|
|
544
|
+
add(agent.name, "person", 0.95);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
} catch {}
|
|
548
|
+
try {
|
|
549
|
+
const d = db || getDatabase();
|
|
550
|
+
const projects = listProjects(d);
|
|
551
|
+
const textLower2 = text.toLowerCase();
|
|
552
|
+
for (const project of projects) {
|
|
553
|
+
const nameLower = project.name.toLowerCase();
|
|
554
|
+
if (nameLower.length >= 3 && textLower2.includes(nameLower)) {
|
|
555
|
+
add(project.name, "project", 0.95);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
} catch {}
|
|
559
|
+
const textLower = text.toLowerCase();
|
|
560
|
+
for (const keyword of TECH_KEYWORDS) {
|
|
561
|
+
const re = new RegExp(`\\b${keyword}\\b`, "i");
|
|
562
|
+
if (re.test(textLower)) {
|
|
563
|
+
add(keyword, "tool", 0.7);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
for (const match of text.matchAll(PASCAL_CASE_RE)) {
|
|
567
|
+
add(match[1], "concept", 0.5);
|
|
568
|
+
}
|
|
569
|
+
return Array.from(entityMap.values()).sort((a, b) => b.confidence - a.confidence);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// src/db/entities.ts
|
|
573
|
+
function parseEntityRow(row) {
|
|
574
|
+
return {
|
|
575
|
+
id: row["id"],
|
|
576
|
+
name: row["name"],
|
|
577
|
+
type: row["type"],
|
|
578
|
+
description: row["description"] || null,
|
|
579
|
+
metadata: JSON.parse(row["metadata"] || "{}"),
|
|
580
|
+
project_id: row["project_id"] || null,
|
|
581
|
+
created_at: row["created_at"],
|
|
582
|
+
updated_at: row["updated_at"]
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
function createEntity(input, db) {
|
|
586
|
+
const d = db || getDatabase();
|
|
587
|
+
const timestamp = now();
|
|
588
|
+
const metadataJson = JSON.stringify(input.metadata || {});
|
|
589
|
+
const existing = d.query(`SELECT * FROM entities
|
|
590
|
+
WHERE name = ? AND type = ? AND COALESCE(project_id, '') = ?`).get(input.name, input.type, input.project_id || "");
|
|
591
|
+
if (existing) {
|
|
592
|
+
const sets = ["updated_at = ?"];
|
|
593
|
+
const params = [timestamp];
|
|
594
|
+
if (input.description !== undefined) {
|
|
595
|
+
sets.push("description = ?");
|
|
596
|
+
params.push(input.description);
|
|
597
|
+
}
|
|
598
|
+
if (input.metadata !== undefined) {
|
|
599
|
+
sets.push("metadata = ?");
|
|
600
|
+
params.push(metadataJson);
|
|
601
|
+
}
|
|
602
|
+
const existingId = existing["id"];
|
|
603
|
+
params.push(existingId);
|
|
604
|
+
d.run(`UPDATE entities SET ${sets.join(", ")} WHERE id = ?`, params);
|
|
605
|
+
return getEntity(existingId, d);
|
|
606
|
+
}
|
|
607
|
+
const id = shortUuid();
|
|
608
|
+
d.run(`INSERT INTO entities (id, name, type, description, metadata, project_id, created_at, updated_at)
|
|
609
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
610
|
+
id,
|
|
611
|
+
input.name,
|
|
612
|
+
input.type,
|
|
613
|
+
input.description || null,
|
|
614
|
+
metadataJson,
|
|
615
|
+
input.project_id || null,
|
|
616
|
+
timestamp,
|
|
617
|
+
timestamp
|
|
618
|
+
]);
|
|
619
|
+
return getEntity(id, d);
|
|
620
|
+
}
|
|
621
|
+
function getEntity(id, db) {
|
|
622
|
+
const d = db || getDatabase();
|
|
623
|
+
const row = d.query("SELECT * FROM entities WHERE id = ?").get(id);
|
|
624
|
+
if (!row)
|
|
625
|
+
throw new EntityNotFoundError(id);
|
|
626
|
+
return parseEntityRow(row);
|
|
627
|
+
}
|
|
628
|
+
function getEntityByName(name, type, projectId, db) {
|
|
629
|
+
const d = db || getDatabase();
|
|
630
|
+
let sql = "SELECT * FROM entities WHERE name = ?";
|
|
631
|
+
const params = [name];
|
|
632
|
+
if (type) {
|
|
633
|
+
sql += " AND type = ?";
|
|
634
|
+
params.push(type);
|
|
635
|
+
}
|
|
636
|
+
if (projectId !== undefined) {
|
|
637
|
+
sql += " AND project_id = ?";
|
|
638
|
+
params.push(projectId);
|
|
639
|
+
}
|
|
640
|
+
sql += " LIMIT 1";
|
|
641
|
+
const row = d.query(sql).get(...params);
|
|
642
|
+
if (!row)
|
|
643
|
+
return null;
|
|
644
|
+
return parseEntityRow(row);
|
|
645
|
+
}
|
|
646
|
+
function listEntities(filter = {}, db) {
|
|
647
|
+
const d = db || getDatabase();
|
|
648
|
+
const conditions = [];
|
|
649
|
+
const params = [];
|
|
650
|
+
if (filter.type) {
|
|
651
|
+
conditions.push("type = ?");
|
|
652
|
+
params.push(filter.type);
|
|
653
|
+
}
|
|
654
|
+
if (filter.project_id) {
|
|
655
|
+
conditions.push("project_id = ?");
|
|
656
|
+
params.push(filter.project_id);
|
|
657
|
+
}
|
|
658
|
+
if (filter.search) {
|
|
659
|
+
conditions.push("(name LIKE ? OR description LIKE ?)");
|
|
660
|
+
const term = `%${filter.search}%`;
|
|
661
|
+
params.push(term, term);
|
|
662
|
+
}
|
|
663
|
+
let sql = "SELECT * FROM entities";
|
|
664
|
+
if (conditions.length > 0) {
|
|
665
|
+
sql += ` WHERE ${conditions.join(" AND ")}`;
|
|
666
|
+
}
|
|
667
|
+
sql += " ORDER BY updated_at DESC";
|
|
668
|
+
if (filter.limit) {
|
|
669
|
+
sql += " LIMIT ?";
|
|
670
|
+
params.push(filter.limit);
|
|
671
|
+
}
|
|
672
|
+
if (filter.offset) {
|
|
673
|
+
sql += " OFFSET ?";
|
|
674
|
+
params.push(filter.offset);
|
|
675
|
+
}
|
|
676
|
+
const rows = d.query(sql).all(...params);
|
|
677
|
+
return rows.map(parseEntityRow);
|
|
678
|
+
}
|
|
679
|
+
function updateEntity(id, input, db) {
|
|
680
|
+
const d = db || getDatabase();
|
|
681
|
+
const existing = d.query("SELECT id FROM entities WHERE id = ?").get(id);
|
|
682
|
+
if (!existing)
|
|
683
|
+
throw new EntityNotFoundError(id);
|
|
684
|
+
const sets = ["updated_at = ?"];
|
|
685
|
+
const params = [now()];
|
|
686
|
+
if (input.name !== undefined) {
|
|
687
|
+
sets.push("name = ?");
|
|
688
|
+
params.push(input.name);
|
|
689
|
+
}
|
|
690
|
+
if (input.type !== undefined) {
|
|
691
|
+
sets.push("type = ?");
|
|
692
|
+
params.push(input.type);
|
|
693
|
+
}
|
|
694
|
+
if (input.description !== undefined) {
|
|
695
|
+
sets.push("description = ?");
|
|
696
|
+
params.push(input.description);
|
|
697
|
+
}
|
|
698
|
+
if (input.metadata !== undefined) {
|
|
699
|
+
sets.push("metadata = ?");
|
|
700
|
+
params.push(JSON.stringify(input.metadata));
|
|
701
|
+
}
|
|
702
|
+
params.push(id);
|
|
703
|
+
d.run(`UPDATE entities SET ${sets.join(", ")} WHERE id = ?`, params);
|
|
704
|
+
return getEntity(id, d);
|
|
705
|
+
}
|
|
706
|
+
function deleteEntity(id, db) {
|
|
707
|
+
const d = db || getDatabase();
|
|
708
|
+
const result = d.run("DELETE FROM entities WHERE id = ?", [id]);
|
|
709
|
+
if (result.changes === 0)
|
|
710
|
+
throw new EntityNotFoundError(id);
|
|
711
|
+
}
|
|
712
|
+
function mergeEntities(sourceId, targetId, db) {
|
|
713
|
+
const d = db || getDatabase();
|
|
714
|
+
getEntity(sourceId, d);
|
|
715
|
+
getEntity(targetId, d);
|
|
716
|
+
d.run(`UPDATE OR IGNORE relations SET source_entity_id = ? WHERE source_entity_id = ?`, [targetId, sourceId]);
|
|
717
|
+
d.run(`UPDATE OR IGNORE relations SET target_entity_id = ? WHERE target_entity_id = ?`, [targetId, sourceId]);
|
|
718
|
+
d.run("DELETE FROM relations WHERE source_entity_id = ? OR target_entity_id = ?", [
|
|
719
|
+
sourceId,
|
|
720
|
+
sourceId
|
|
721
|
+
]);
|
|
722
|
+
d.run(`UPDATE OR IGNORE entity_memories SET entity_id = ? WHERE entity_id = ?`, [targetId, sourceId]);
|
|
723
|
+
d.run("DELETE FROM entity_memories WHERE entity_id = ?", [sourceId]);
|
|
724
|
+
d.run("DELETE FROM entities WHERE id = ?", [sourceId]);
|
|
725
|
+
d.run("UPDATE entities SET updated_at = ? WHERE id = ?", [now(), targetId]);
|
|
726
|
+
return getEntity(targetId, d);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// src/db/entity-memories.ts
|
|
730
|
+
function parseEntityMemoryRow(row) {
|
|
731
|
+
return {
|
|
732
|
+
entity_id: row["entity_id"],
|
|
733
|
+
memory_id: row["memory_id"],
|
|
734
|
+
role: row["role"],
|
|
735
|
+
created_at: row["created_at"]
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
function linkEntityToMemory(entityId, memoryId, role = "context", db) {
|
|
739
|
+
const d = db || getDatabase();
|
|
740
|
+
const timestamp = now();
|
|
741
|
+
d.run(`INSERT OR IGNORE INTO entity_memories (entity_id, memory_id, role, created_at)
|
|
742
|
+
VALUES (?, ?, ?, ?)`, [entityId, memoryId, role, timestamp]);
|
|
743
|
+
const row = d.query("SELECT * FROM entity_memories WHERE entity_id = ? AND memory_id = ?").get(entityId, memoryId);
|
|
744
|
+
return parseEntityMemoryRow(row);
|
|
745
|
+
}
|
|
746
|
+
function unlinkEntityFromMemory(entityId, memoryId, db) {
|
|
747
|
+
const d = db || getDatabase();
|
|
748
|
+
d.run("DELETE FROM entity_memories WHERE entity_id = ? AND memory_id = ?", [entityId, memoryId]);
|
|
749
|
+
}
|
|
750
|
+
function getMemoriesForEntity(entityId, db) {
|
|
751
|
+
const d = db || getDatabase();
|
|
752
|
+
const rows = d.query(`SELECT m.* FROM memories m
|
|
753
|
+
INNER JOIN entity_memories em ON em.memory_id = m.id
|
|
754
|
+
WHERE em.entity_id = ?
|
|
755
|
+
ORDER BY m.importance DESC, m.created_at DESC`).all(entityId);
|
|
756
|
+
return rows.map(parseMemoryRow);
|
|
757
|
+
}
|
|
758
|
+
function getEntityMemoryLinks(entityId, memoryId, db) {
|
|
759
|
+
const d = db || getDatabase();
|
|
760
|
+
const conditions = [];
|
|
761
|
+
const params = [];
|
|
762
|
+
if (entityId) {
|
|
763
|
+
conditions.push("entity_id = ?");
|
|
764
|
+
params.push(entityId);
|
|
765
|
+
}
|
|
766
|
+
if (memoryId) {
|
|
767
|
+
conditions.push("memory_id = ?");
|
|
768
|
+
params.push(memoryId);
|
|
769
|
+
}
|
|
770
|
+
let sql = "SELECT * FROM entity_memories";
|
|
771
|
+
if (conditions.length > 0) {
|
|
772
|
+
sql += ` WHERE ${conditions.join(" AND ")}`;
|
|
773
|
+
}
|
|
774
|
+
sql += " ORDER BY created_at DESC";
|
|
775
|
+
const rows = d.query(sql).all(...params);
|
|
776
|
+
return rows.map(parseEntityMemoryRow);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// src/db/relations.ts
|
|
780
|
+
function parseRelationRow(row) {
|
|
781
|
+
return {
|
|
782
|
+
id: row["id"],
|
|
783
|
+
source_entity_id: row["source_entity_id"],
|
|
784
|
+
target_entity_id: row["target_entity_id"],
|
|
785
|
+
relation_type: row["relation_type"],
|
|
786
|
+
weight: row["weight"],
|
|
787
|
+
metadata: JSON.parse(row["metadata"] || "{}"),
|
|
788
|
+
created_at: row["created_at"]
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
function parseEntityRow2(row) {
|
|
792
|
+
return {
|
|
793
|
+
id: row["id"],
|
|
794
|
+
name: row["name"],
|
|
795
|
+
type: row["type"],
|
|
796
|
+
description: row["description"] || null,
|
|
797
|
+
metadata: JSON.parse(row["metadata"] || "{}"),
|
|
798
|
+
project_id: row["project_id"] || null,
|
|
799
|
+
created_at: row["created_at"],
|
|
800
|
+
updated_at: row["updated_at"]
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
function createRelation(input, db) {
|
|
804
|
+
const d = db || getDatabase();
|
|
805
|
+
const id = shortUuid();
|
|
806
|
+
const timestamp = now();
|
|
807
|
+
const weight = input.weight ?? 1;
|
|
808
|
+
const metadata = JSON.stringify(input.metadata ?? {});
|
|
809
|
+
d.run(`INSERT INTO relations (id, source_entity_id, target_entity_id, relation_type, weight, metadata, created_at)
|
|
810
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
811
|
+
ON CONFLICT(source_entity_id, target_entity_id, relation_type)
|
|
812
|
+
DO UPDATE SET weight = excluded.weight, metadata = excluded.metadata`, [id, input.source_entity_id, input.target_entity_id, input.relation_type, weight, metadata, timestamp]);
|
|
813
|
+
const row = d.query(`SELECT * FROM relations
|
|
814
|
+
WHERE source_entity_id = ? AND target_entity_id = ? AND relation_type = ?`).get(input.source_entity_id, input.target_entity_id, input.relation_type);
|
|
815
|
+
return parseRelationRow(row);
|
|
816
|
+
}
|
|
817
|
+
function getRelation(id, db) {
|
|
818
|
+
const d = db || getDatabase();
|
|
819
|
+
const row = d.query("SELECT * FROM relations WHERE id = ?").get(id);
|
|
820
|
+
if (!row)
|
|
821
|
+
throw new Error(`Relation not found: ${id}`);
|
|
822
|
+
return parseRelationRow(row);
|
|
823
|
+
}
|
|
824
|
+
function listRelations(filter, db) {
|
|
825
|
+
const d = db || getDatabase();
|
|
826
|
+
const conditions = [];
|
|
827
|
+
const params = [];
|
|
828
|
+
if (filter.entity_id) {
|
|
829
|
+
const dir = filter.direction || "both";
|
|
830
|
+
if (dir === "outgoing") {
|
|
831
|
+
conditions.push("source_entity_id = ?");
|
|
832
|
+
params.push(filter.entity_id);
|
|
833
|
+
} else if (dir === "incoming") {
|
|
834
|
+
conditions.push("target_entity_id = ?");
|
|
835
|
+
params.push(filter.entity_id);
|
|
836
|
+
} else {
|
|
837
|
+
conditions.push("(source_entity_id = ? OR target_entity_id = ?)");
|
|
838
|
+
params.push(filter.entity_id, filter.entity_id);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
if (filter.relation_type) {
|
|
842
|
+
conditions.push("relation_type = ?");
|
|
843
|
+
params.push(filter.relation_type);
|
|
844
|
+
}
|
|
845
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
846
|
+
const rows = d.query(`SELECT * FROM relations ${where} ORDER BY created_at DESC`).all(...params);
|
|
847
|
+
return rows.map(parseRelationRow);
|
|
848
|
+
}
|
|
849
|
+
function deleteRelation(id, db) {
|
|
850
|
+
const d = db || getDatabase();
|
|
851
|
+
const result = d.run("DELETE FROM relations WHERE id = ?", [id]);
|
|
852
|
+
if (result.changes === 0)
|
|
853
|
+
throw new Error(`Relation not found: ${id}`);
|
|
854
|
+
}
|
|
855
|
+
function getEntityGraph(entityId, depth = 2, db) {
|
|
856
|
+
const d = db || getDatabase();
|
|
857
|
+
const entityRows = d.query(`WITH RECURSIVE graph(id, depth) AS (
|
|
858
|
+
VALUES(?, 0)
|
|
859
|
+
UNION
|
|
860
|
+
SELECT CASE WHEN r.source_entity_id = g.id THEN r.target_entity_id ELSE r.source_entity_id END, g.depth + 1
|
|
861
|
+
FROM relations r JOIN graph g ON (r.source_entity_id = g.id OR r.target_entity_id = g.id)
|
|
862
|
+
WHERE g.depth < ?
|
|
863
|
+
)
|
|
864
|
+
SELECT DISTINCT e.* FROM entities e JOIN graph g ON e.id = g.id`).all(entityId, depth);
|
|
865
|
+
const entities = entityRows.map(parseEntityRow2);
|
|
866
|
+
const entityIds = new Set(entities.map((e) => e.id));
|
|
867
|
+
if (entityIds.size === 0) {
|
|
868
|
+
return { entities: [], relations: [] };
|
|
869
|
+
}
|
|
870
|
+
const placeholders = Array.from(entityIds).map(() => "?").join(",");
|
|
871
|
+
const relationRows = d.query(`SELECT * FROM relations
|
|
872
|
+
WHERE source_entity_id IN (${placeholders})
|
|
873
|
+
AND target_entity_id IN (${placeholders})`).all(...Array.from(entityIds), ...Array.from(entityIds));
|
|
874
|
+
const relations = relationRows.map(parseRelationRow);
|
|
875
|
+
return { entities, relations };
|
|
876
|
+
}
|
|
877
|
+
function findPath(fromEntityId, toEntityId, maxDepth = 5, db) {
|
|
878
|
+
const d = db || getDatabase();
|
|
879
|
+
const rows = d.query(`WITH RECURSIVE path(id, trail, depth) AS (
|
|
880
|
+
SELECT ?, ?, 0
|
|
881
|
+
UNION
|
|
882
|
+
SELECT
|
|
883
|
+
CASE WHEN r.source_entity_id = p.id THEN r.target_entity_id ELSE r.source_entity_id END,
|
|
884
|
+
p.trail || ',' || CASE WHEN r.source_entity_id = p.id THEN r.target_entity_id ELSE r.source_entity_id END,
|
|
885
|
+
p.depth + 1
|
|
886
|
+
FROM relations r JOIN path p ON (r.source_entity_id = p.id OR r.target_entity_id = p.id)
|
|
887
|
+
WHERE p.depth < ?
|
|
888
|
+
AND INSTR(p.trail, CASE WHEN r.source_entity_id = p.id THEN r.target_entity_id ELSE r.source_entity_id END) = 0
|
|
889
|
+
)
|
|
890
|
+
SELECT trail FROM path WHERE id = ? ORDER BY depth ASC LIMIT 1`).get(fromEntityId, fromEntityId, maxDepth, toEntityId);
|
|
891
|
+
if (!rows)
|
|
892
|
+
return null;
|
|
893
|
+
const ids = rows.trail.split(",");
|
|
894
|
+
const entities = [];
|
|
895
|
+
for (const id of ids) {
|
|
896
|
+
const row = d.query("SELECT * FROM entities WHERE id = ?").get(id);
|
|
897
|
+
if (row)
|
|
898
|
+
entities.push(parseEntityRow2(row));
|
|
899
|
+
}
|
|
900
|
+
return entities.length > 0 ? entities : null;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
// src/lib/config.ts
|
|
904
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync } from "fs";
|
|
905
|
+
import { homedir } from "os";
|
|
906
|
+
import { dirname as dirname2, join as join2, resolve as resolve2 } from "path";
|
|
907
|
+
var DEFAULT_CONFIG = {
|
|
908
|
+
default_scope: "private",
|
|
909
|
+
default_category: "knowledge",
|
|
910
|
+
default_importance: 5,
|
|
911
|
+
max_entries: 1000,
|
|
912
|
+
max_entries_per_scope: {
|
|
913
|
+
global: 500,
|
|
914
|
+
shared: 300,
|
|
915
|
+
private: 200
|
|
916
|
+
},
|
|
917
|
+
injection: {
|
|
918
|
+
max_tokens: 500,
|
|
919
|
+
min_importance: 5,
|
|
920
|
+
categories: ["preference", "fact"],
|
|
921
|
+
refresh_interval: 5
|
|
922
|
+
},
|
|
923
|
+
extraction: {
|
|
924
|
+
enabled: true,
|
|
925
|
+
min_confidence: 0.5
|
|
926
|
+
},
|
|
927
|
+
sync_agents: ["claude", "codex", "gemini"],
|
|
928
|
+
auto_cleanup: {
|
|
929
|
+
enabled: true,
|
|
930
|
+
expired_check_interval: 3600,
|
|
931
|
+
unused_archive_days: 7,
|
|
932
|
+
stale_deprioritize_days: 14
|
|
933
|
+
}
|
|
934
|
+
};
|
|
935
|
+
function deepMerge(target, source) {
|
|
936
|
+
const result = { ...target };
|
|
937
|
+
for (const key of Object.keys(source)) {
|
|
938
|
+
const sourceVal = source[key];
|
|
939
|
+
const targetVal = result[key];
|
|
940
|
+
if (sourceVal !== null && typeof sourceVal === "object" && !Array.isArray(sourceVal) && targetVal !== null && typeof targetVal === "object" && !Array.isArray(targetVal)) {
|
|
941
|
+
result[key] = deepMerge(targetVal, sourceVal);
|
|
942
|
+
} else {
|
|
943
|
+
result[key] = sourceVal;
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
return result;
|
|
947
|
+
}
|
|
948
|
+
var VALID_SCOPES = ["global", "shared", "private"];
|
|
949
|
+
var VALID_CATEGORIES = [
|
|
950
|
+
"preference",
|
|
951
|
+
"fact",
|
|
952
|
+
"knowledge",
|
|
953
|
+
"history"
|
|
954
|
+
];
|
|
955
|
+
function isValidScope(value) {
|
|
956
|
+
return VALID_SCOPES.includes(value);
|
|
957
|
+
}
|
|
958
|
+
function isValidCategory(value) {
|
|
959
|
+
return VALID_CATEGORIES.includes(value);
|
|
960
|
+
}
|
|
961
|
+
function loadConfig() {
|
|
962
|
+
const configPath = join2(homedir(), ".mementos", "config.json");
|
|
963
|
+
let fileConfig = {};
|
|
964
|
+
if (existsSync2(configPath)) {
|
|
965
|
+
try {
|
|
966
|
+
const raw = readFileSync(configPath, "utf-8");
|
|
967
|
+
fileConfig = JSON.parse(raw);
|
|
968
|
+
} catch {}
|
|
969
|
+
}
|
|
970
|
+
const merged = deepMerge(DEFAULT_CONFIG, fileConfig);
|
|
971
|
+
const envScope = process.env["MEMENTOS_DEFAULT_SCOPE"];
|
|
972
|
+
if (envScope && isValidScope(envScope)) {
|
|
973
|
+
merged.default_scope = envScope;
|
|
974
|
+
}
|
|
975
|
+
const envCategory = process.env["MEMENTOS_DEFAULT_CATEGORY"];
|
|
976
|
+
if (envCategory && isValidCategory(envCategory)) {
|
|
977
|
+
merged.default_category = envCategory;
|
|
978
|
+
}
|
|
979
|
+
const envImportance = process.env["MEMENTOS_DEFAULT_IMPORTANCE"];
|
|
980
|
+
if (envImportance) {
|
|
981
|
+
const parsed = parseInt(envImportance, 10);
|
|
982
|
+
if (!Number.isNaN(parsed) && parsed >= 1 && parsed <= 10) {
|
|
983
|
+
merged.default_importance = parsed;
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
return merged;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// src/db/memories.ts
|
|
990
|
+
function runEntityExtraction(memory, projectId, d) {
|
|
991
|
+
const config = loadConfig();
|
|
992
|
+
if (config.extraction?.enabled === false)
|
|
993
|
+
return;
|
|
994
|
+
const extracted = extractEntities(memory, d);
|
|
995
|
+
const minConfidence = config.extraction?.min_confidence ?? 0.5;
|
|
996
|
+
const entityIds = [];
|
|
997
|
+
for (const ext of extracted) {
|
|
998
|
+
if (ext.confidence >= minConfidence) {
|
|
999
|
+
const entity = createEntity({ name: ext.name, type: ext.type, project_id: projectId }, d);
|
|
1000
|
+
linkEntityToMemory(entity.id, memory.id, "context", d);
|
|
1001
|
+
entityIds.push(entity.id);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
for (let i = 0;i < entityIds.length; i++) {
|
|
1005
|
+
for (let j = i + 1;j < entityIds.length; j++) {
|
|
1006
|
+
try {
|
|
1007
|
+
createRelation({ source_entity_id: entityIds[i], target_entity_id: entityIds[j], relation_type: "related_to" }, d);
|
|
1008
|
+
} catch {}
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
function parseMemoryRow(row) {
|
|
1013
|
+
return {
|
|
1014
|
+
id: row["id"],
|
|
1015
|
+
key: row["key"],
|
|
1016
|
+
value: row["value"],
|
|
1017
|
+
category: row["category"],
|
|
1018
|
+
scope: row["scope"],
|
|
1019
|
+
summary: row["summary"] || null,
|
|
1020
|
+
tags: JSON.parse(row["tags"] || "[]"),
|
|
1021
|
+
importance: row["importance"],
|
|
1022
|
+
source: row["source"],
|
|
1023
|
+
status: row["status"],
|
|
1024
|
+
pinned: !!row["pinned"],
|
|
1025
|
+
agent_id: row["agent_id"] || null,
|
|
1026
|
+
project_id: row["project_id"] || null,
|
|
1027
|
+
session_id: row["session_id"] || null,
|
|
1028
|
+
metadata: JSON.parse(row["metadata"] || "{}"),
|
|
1029
|
+
access_count: row["access_count"],
|
|
1030
|
+
version: row["version"],
|
|
1031
|
+
expires_at: row["expires_at"] || null,
|
|
1032
|
+
created_at: row["created_at"],
|
|
1033
|
+
updated_at: row["updated_at"],
|
|
1034
|
+
accessed_at: row["accessed_at"] || null
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
function createMemory(input, dedupeMode = "merge", db) {
|
|
1038
|
+
const d = db || getDatabase();
|
|
1039
|
+
const timestamp = now();
|
|
1040
|
+
let expiresAt = input.expires_at || null;
|
|
1041
|
+
if (input.ttl_ms && !expiresAt) {
|
|
1042
|
+
expiresAt = new Date(Date.now() + input.ttl_ms).toISOString();
|
|
1043
|
+
}
|
|
1044
|
+
const id = uuid();
|
|
1045
|
+
const tags = input.tags || [];
|
|
1046
|
+
const tagsJson = JSON.stringify(tags);
|
|
1047
|
+
const metadataJson = JSON.stringify(input.metadata || {});
|
|
1048
|
+
const safeValue = redactSecrets(input.value);
|
|
1049
|
+
const safeSummary = input.summary ? redactSecrets(input.summary) : null;
|
|
1050
|
+
if (dedupeMode === "merge") {
|
|
1051
|
+
const existing = d.query(`SELECT id, version FROM memories
|
|
1052
|
+
WHERE key = ? AND scope = ?
|
|
1053
|
+
AND COALESCE(agent_id, '') = ?
|
|
1054
|
+
AND COALESCE(project_id, '') = ?
|
|
1055
|
+
AND COALESCE(session_id, '') = ?`).get(input.key, input.scope || "private", input.agent_id || "", input.project_id || "", input.session_id || "");
|
|
1056
|
+
if (existing) {
|
|
1057
|
+
d.run(`UPDATE memories SET
|
|
1058
|
+
value = ?, category = ?, summary = ?, tags = ?,
|
|
1059
|
+
importance = ?, metadata = ?, expires_at = ?,
|
|
1060
|
+
pinned = COALESCE(pinned, 0),
|
|
1061
|
+
version = version + 1, updated_at = ?
|
|
1062
|
+
WHERE id = ?`, [
|
|
1063
|
+
safeValue,
|
|
1064
|
+
input.category || "knowledge",
|
|
1065
|
+
safeSummary,
|
|
1066
|
+
tagsJson,
|
|
1067
|
+
input.importance ?? 5,
|
|
1068
|
+
metadataJson,
|
|
1069
|
+
expiresAt,
|
|
1070
|
+
timestamp,
|
|
1071
|
+
existing.id
|
|
1072
|
+
]);
|
|
1073
|
+
d.run("DELETE FROM memory_tags WHERE memory_id = ?", [existing.id]);
|
|
1074
|
+
const insertTag2 = d.prepare("INSERT OR IGNORE INTO memory_tags (memory_id, tag) VALUES (?, ?)");
|
|
1075
|
+
for (const tag of tags) {
|
|
1076
|
+
insertTag2.run(existing.id, tag);
|
|
1077
|
+
}
|
|
1078
|
+
const merged = getMemory(existing.id, d);
|
|
1079
|
+
try {
|
|
1080
|
+
const oldLinks = getEntityMemoryLinks(undefined, merged.id, d);
|
|
1081
|
+
for (const link of oldLinks) {
|
|
1082
|
+
unlinkEntityFromMemory(link.entity_id, merged.id, d);
|
|
1083
|
+
}
|
|
1084
|
+
runEntityExtraction(merged, input.project_id, d);
|
|
1085
|
+
} catch {}
|
|
1086
|
+
return merged;
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
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)
|
|
1090
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'active', 0, ?, ?, ?, ?, 0, 1, ?, ?, ?)`, [
|
|
1091
|
+
id,
|
|
1092
|
+
input.key,
|
|
1093
|
+
input.value,
|
|
1094
|
+
input.category || "knowledge",
|
|
1095
|
+
input.scope || "private",
|
|
1096
|
+
input.summary || null,
|
|
1097
|
+
tagsJson,
|
|
1098
|
+
input.importance ?? 5,
|
|
1099
|
+
input.source || "agent",
|
|
1100
|
+
input.agent_id || null,
|
|
1101
|
+
input.project_id || null,
|
|
1102
|
+
input.session_id || null,
|
|
1103
|
+
metadataJson,
|
|
1104
|
+
expiresAt,
|
|
1105
|
+
timestamp,
|
|
1106
|
+
timestamp
|
|
1107
|
+
]);
|
|
1108
|
+
const insertTag = d.prepare("INSERT OR IGNORE INTO memory_tags (memory_id, tag) VALUES (?, ?)");
|
|
1109
|
+
for (const tag of tags) {
|
|
1110
|
+
insertTag.run(id, tag);
|
|
1111
|
+
}
|
|
1112
|
+
const memory = getMemory(id, d);
|
|
1113
|
+
try {
|
|
1114
|
+
runEntityExtraction(memory, input.project_id, d);
|
|
1115
|
+
} catch {}
|
|
1116
|
+
return memory;
|
|
1117
|
+
}
|
|
1118
|
+
function getMemory(id, db) {
|
|
1119
|
+
const d = db || getDatabase();
|
|
1120
|
+
const row = d.query("SELECT * FROM memories WHERE id = ?").get(id);
|
|
1121
|
+
if (!row)
|
|
1122
|
+
return null;
|
|
1123
|
+
return parseMemoryRow(row);
|
|
1124
|
+
}
|
|
1125
|
+
function listMemories(filter, db) {
|
|
1126
|
+
const d = db || getDatabase();
|
|
1127
|
+
const conditions = [];
|
|
1128
|
+
const params = [];
|
|
1129
|
+
if (filter) {
|
|
1130
|
+
if (filter.scope) {
|
|
1131
|
+
if (Array.isArray(filter.scope)) {
|
|
1132
|
+
conditions.push(`scope IN (${filter.scope.map(() => "?").join(",")})`);
|
|
1133
|
+
params.push(...filter.scope);
|
|
1134
|
+
} else {
|
|
1135
|
+
conditions.push("scope = ?");
|
|
1136
|
+
params.push(filter.scope);
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
if (filter.category) {
|
|
1140
|
+
if (Array.isArray(filter.category)) {
|
|
1141
|
+
conditions.push(`category IN (${filter.category.map(() => "?").join(",")})`);
|
|
1142
|
+
params.push(...filter.category);
|
|
1143
|
+
} else {
|
|
1144
|
+
conditions.push("category = ?");
|
|
1145
|
+
params.push(filter.category);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
if (filter.source) {
|
|
1149
|
+
if (Array.isArray(filter.source)) {
|
|
1150
|
+
conditions.push(`source IN (${filter.source.map(() => "?").join(",")})`);
|
|
1151
|
+
params.push(...filter.source);
|
|
1152
|
+
} else {
|
|
1153
|
+
conditions.push("source = ?");
|
|
1154
|
+
params.push(filter.source);
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
if (filter.status) {
|
|
1158
|
+
if (Array.isArray(filter.status)) {
|
|
1159
|
+
conditions.push(`status IN (${filter.status.map(() => "?").join(",")})`);
|
|
1160
|
+
params.push(...filter.status);
|
|
1161
|
+
} else {
|
|
1162
|
+
conditions.push("status = ?");
|
|
1163
|
+
params.push(filter.status);
|
|
1164
|
+
}
|
|
1165
|
+
} else {
|
|
1166
|
+
conditions.push("status = 'active'");
|
|
1167
|
+
}
|
|
1168
|
+
if (filter.project_id) {
|
|
1169
|
+
conditions.push("project_id = ?");
|
|
1170
|
+
params.push(filter.project_id);
|
|
1171
|
+
}
|
|
1172
|
+
if (filter.agent_id) {
|
|
1173
|
+
conditions.push("agent_id = ?");
|
|
1174
|
+
params.push(filter.agent_id);
|
|
1175
|
+
}
|
|
1176
|
+
if (filter.session_id) {
|
|
1177
|
+
conditions.push("session_id = ?");
|
|
1178
|
+
params.push(filter.session_id);
|
|
1179
|
+
}
|
|
1180
|
+
if (filter.min_importance) {
|
|
1181
|
+
conditions.push("importance >= ?");
|
|
1182
|
+
params.push(filter.min_importance);
|
|
1183
|
+
}
|
|
1184
|
+
if (filter.pinned !== undefined) {
|
|
1185
|
+
conditions.push("pinned = ?");
|
|
1186
|
+
params.push(filter.pinned ? 1 : 0);
|
|
1187
|
+
}
|
|
1188
|
+
if (filter.tags && filter.tags.length > 0) {
|
|
1189
|
+
for (const tag of filter.tags) {
|
|
1190
|
+
conditions.push("id IN (SELECT memory_id FROM memory_tags WHERE tag = ?)");
|
|
1191
|
+
params.push(tag);
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
if (filter.search) {
|
|
1195
|
+
conditions.push("(key LIKE ? OR value LIKE ? OR summary LIKE ?)");
|
|
1196
|
+
const term = `%${filter.search}%`;
|
|
1197
|
+
params.push(term, term, term);
|
|
1198
|
+
}
|
|
1199
|
+
} else {
|
|
1200
|
+
conditions.push("status = 'active'");
|
|
1201
|
+
}
|
|
1202
|
+
let sql = "SELECT * FROM memories";
|
|
1203
|
+
if (conditions.length > 0) {
|
|
1204
|
+
sql += ` WHERE ${conditions.join(" AND ")}`;
|
|
1205
|
+
}
|
|
1206
|
+
sql += " ORDER BY importance DESC, created_at DESC";
|
|
1207
|
+
if (filter?.limit) {
|
|
1208
|
+
sql += " LIMIT ?";
|
|
1209
|
+
params.push(filter.limit);
|
|
1210
|
+
}
|
|
1211
|
+
if (filter?.offset) {
|
|
1212
|
+
sql += " OFFSET ?";
|
|
1213
|
+
params.push(filter.offset);
|
|
1214
|
+
}
|
|
1215
|
+
const rows = d.query(sql).all(...params);
|
|
1216
|
+
return rows.map(parseMemoryRow);
|
|
1217
|
+
}
|
|
1218
|
+
function updateMemory(id, input, db) {
|
|
1219
|
+
const d = db || getDatabase();
|
|
1220
|
+
const existing = getMemory(id, d);
|
|
1221
|
+
if (!existing)
|
|
1222
|
+
throw new MemoryNotFoundError(id);
|
|
1223
|
+
if (existing.version !== input.version) {
|
|
1224
|
+
throw new VersionConflictError(id, input.version, existing.version);
|
|
1225
|
+
}
|
|
1226
|
+
try {
|
|
1227
|
+
d.run(`INSERT OR IGNORE INTO memory_versions (id, memory_id, version, value, importance, scope, category, tags, summary, pinned, status, created_at)
|
|
1228
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
1229
|
+
uuid(),
|
|
1230
|
+
existing.id,
|
|
1231
|
+
existing.version,
|
|
1232
|
+
existing.value,
|
|
1233
|
+
existing.importance,
|
|
1234
|
+
existing.scope,
|
|
1235
|
+
existing.category,
|
|
1236
|
+
JSON.stringify(existing.tags),
|
|
1237
|
+
existing.summary,
|
|
1238
|
+
existing.pinned ? 1 : 0,
|
|
1239
|
+
existing.status,
|
|
1240
|
+
existing.updated_at
|
|
1241
|
+
]);
|
|
1242
|
+
} catch {}
|
|
1243
|
+
const sets = ["version = version + 1", "updated_at = ?"];
|
|
1244
|
+
const params = [now()];
|
|
1245
|
+
if (input.value !== undefined) {
|
|
1246
|
+
sets.push("value = ?");
|
|
1247
|
+
params.push(redactSecrets(input.value));
|
|
1248
|
+
}
|
|
1249
|
+
if (input.category !== undefined) {
|
|
1250
|
+
sets.push("category = ?");
|
|
1251
|
+
params.push(input.category);
|
|
1252
|
+
}
|
|
1253
|
+
if (input.scope !== undefined) {
|
|
1254
|
+
sets.push("scope = ?");
|
|
1255
|
+
params.push(input.scope);
|
|
1256
|
+
}
|
|
1257
|
+
if (input.summary !== undefined) {
|
|
1258
|
+
sets.push("summary = ?");
|
|
1259
|
+
params.push(input.summary);
|
|
1260
|
+
}
|
|
1261
|
+
if (input.importance !== undefined) {
|
|
1262
|
+
sets.push("importance = ?");
|
|
1263
|
+
params.push(input.importance);
|
|
1264
|
+
}
|
|
1265
|
+
if (input.pinned !== undefined) {
|
|
1266
|
+
sets.push("pinned = ?");
|
|
1267
|
+
params.push(input.pinned ? 1 : 0);
|
|
1268
|
+
}
|
|
1269
|
+
if (input.status !== undefined) {
|
|
1270
|
+
sets.push("status = ?");
|
|
1271
|
+
params.push(input.status);
|
|
1272
|
+
}
|
|
1273
|
+
if (input.metadata !== undefined) {
|
|
1274
|
+
sets.push("metadata = ?");
|
|
1275
|
+
params.push(JSON.stringify(input.metadata));
|
|
1276
|
+
}
|
|
1277
|
+
if (input.expires_at !== undefined) {
|
|
1278
|
+
sets.push("expires_at = ?");
|
|
1279
|
+
params.push(input.expires_at);
|
|
1280
|
+
}
|
|
1281
|
+
if (input.tags !== undefined) {
|
|
1282
|
+
sets.push("tags = ?");
|
|
1283
|
+
params.push(JSON.stringify(input.tags));
|
|
1284
|
+
d.run("DELETE FROM memory_tags WHERE memory_id = ?", [id]);
|
|
1285
|
+
const insertTag = d.prepare("INSERT OR IGNORE INTO memory_tags (memory_id, tag) VALUES (?, ?)");
|
|
1286
|
+
for (const tag of input.tags) {
|
|
1287
|
+
insertTag.run(id, tag);
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
params.push(id);
|
|
1291
|
+
d.run(`UPDATE memories SET ${sets.join(", ")} WHERE id = ?`, params);
|
|
1292
|
+
const updated = getMemory(id, d);
|
|
1293
|
+
try {
|
|
1294
|
+
if (input.value !== undefined) {
|
|
1295
|
+
const oldLinks = getEntityMemoryLinks(undefined, updated.id, d);
|
|
1296
|
+
for (const link of oldLinks) {
|
|
1297
|
+
unlinkEntityFromMemory(link.entity_id, updated.id, d);
|
|
1298
|
+
}
|
|
1299
|
+
runEntityExtraction(updated, existing.project_id || undefined, d);
|
|
1300
|
+
}
|
|
1301
|
+
} catch {}
|
|
1302
|
+
return updated;
|
|
1303
|
+
}
|
|
1304
|
+
function deleteMemory(id, db) {
|
|
1305
|
+
const d = db || getDatabase();
|
|
1306
|
+
const result = d.run("DELETE FROM memories WHERE id = ?", [id]);
|
|
1307
|
+
return result.changes > 0;
|
|
1308
|
+
}
|
|
1309
|
+
function touchMemory(id, db) {
|
|
1310
|
+
const d = db || getDatabase();
|
|
1311
|
+
d.run("UPDATE memories SET access_count = access_count + 1, accessed_at = ? WHERE id = ?", [now(), id]);
|
|
1312
|
+
}
|
|
1313
|
+
function cleanExpiredMemories(db) {
|
|
1314
|
+
const d = db || getDatabase();
|
|
1315
|
+
const timestamp = now();
|
|
1316
|
+
const countRow = d.query("SELECT COUNT(*) as c FROM memories WHERE expires_at IS NOT NULL AND expires_at < ?").get(timestamp);
|
|
1317
|
+
const count = countRow.c;
|
|
1318
|
+
if (count > 0) {
|
|
1319
|
+
d.run("DELETE FROM memories WHERE expires_at IS NOT NULL AND expires_at < ?", [timestamp]);
|
|
1320
|
+
}
|
|
1321
|
+
return count;
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
// src/lib/search.ts
|
|
1325
|
+
function parseMemoryRow2(row) {
|
|
1326
|
+
return {
|
|
1327
|
+
id: row["id"],
|
|
1328
|
+
key: row["key"],
|
|
1329
|
+
value: row["value"],
|
|
1330
|
+
category: row["category"],
|
|
1331
|
+
scope: row["scope"],
|
|
1332
|
+
summary: row["summary"] || null,
|
|
1333
|
+
tags: JSON.parse(row["tags"] || "[]"),
|
|
1334
|
+
importance: row["importance"],
|
|
1335
|
+
source: row["source"],
|
|
1336
|
+
status: row["status"],
|
|
1337
|
+
pinned: !!row["pinned"],
|
|
1338
|
+
agent_id: row["agent_id"] || null,
|
|
1339
|
+
project_id: row["project_id"] || null,
|
|
1340
|
+
session_id: row["session_id"] || null,
|
|
1341
|
+
metadata: JSON.parse(row["metadata"] || "{}"),
|
|
1342
|
+
access_count: row["access_count"],
|
|
1343
|
+
version: row["version"],
|
|
1344
|
+
expires_at: row["expires_at"] || null,
|
|
1345
|
+
created_at: row["created_at"],
|
|
1346
|
+
updated_at: row["updated_at"],
|
|
1347
|
+
accessed_at: row["accessed_at"] || null
|
|
1348
|
+
};
|
|
1349
|
+
}
|
|
1350
|
+
function preprocessQuery(query) {
|
|
1351
|
+
let q = query.trim();
|
|
1352
|
+
q = q.replace(/\s+/g, " ");
|
|
1353
|
+
q = q.normalize("NFC");
|
|
1354
|
+
return q;
|
|
1355
|
+
}
|
|
1356
|
+
function escapeLikePattern(s) {
|
|
1357
|
+
return s.replace(/%/g, "\\%").replace(/_/g, "\\_");
|
|
1358
|
+
}
|
|
1359
|
+
var STOP_WORDS = new Set([
|
|
1360
|
+
"a",
|
|
1361
|
+
"an",
|
|
1362
|
+
"the",
|
|
1363
|
+
"is",
|
|
1364
|
+
"are",
|
|
1365
|
+
"was",
|
|
1366
|
+
"were",
|
|
1367
|
+
"be",
|
|
1368
|
+
"been",
|
|
1369
|
+
"being",
|
|
1370
|
+
"have",
|
|
1371
|
+
"has",
|
|
1372
|
+
"had",
|
|
1373
|
+
"do",
|
|
1374
|
+
"does",
|
|
1375
|
+
"did",
|
|
1376
|
+
"will",
|
|
1377
|
+
"would",
|
|
1378
|
+
"could",
|
|
1379
|
+
"should",
|
|
1380
|
+
"may",
|
|
1381
|
+
"might",
|
|
1382
|
+
"shall",
|
|
1383
|
+
"can",
|
|
1384
|
+
"need",
|
|
1385
|
+
"dare",
|
|
1386
|
+
"ought",
|
|
1387
|
+
"used",
|
|
1388
|
+
"to",
|
|
1389
|
+
"of",
|
|
1390
|
+
"in",
|
|
1391
|
+
"for",
|
|
1392
|
+
"on",
|
|
1393
|
+
"with",
|
|
1394
|
+
"at",
|
|
1395
|
+
"by",
|
|
1396
|
+
"from",
|
|
1397
|
+
"as",
|
|
1398
|
+
"into",
|
|
1399
|
+
"through",
|
|
1400
|
+
"during",
|
|
1401
|
+
"before",
|
|
1402
|
+
"after",
|
|
1403
|
+
"above",
|
|
1404
|
+
"below",
|
|
1405
|
+
"between",
|
|
1406
|
+
"out",
|
|
1407
|
+
"off",
|
|
1408
|
+
"over",
|
|
1409
|
+
"under",
|
|
1410
|
+
"again",
|
|
1411
|
+
"further",
|
|
1412
|
+
"then",
|
|
1413
|
+
"once",
|
|
1414
|
+
"here",
|
|
1415
|
+
"there",
|
|
1416
|
+
"when",
|
|
1417
|
+
"where",
|
|
1418
|
+
"why",
|
|
1419
|
+
"how",
|
|
1420
|
+
"all",
|
|
1421
|
+
"each",
|
|
1422
|
+
"every",
|
|
1423
|
+
"both",
|
|
1424
|
+
"few",
|
|
1425
|
+
"more",
|
|
1426
|
+
"most",
|
|
1427
|
+
"other",
|
|
1428
|
+
"some",
|
|
1429
|
+
"such",
|
|
1430
|
+
"no",
|
|
1431
|
+
"not",
|
|
1432
|
+
"only",
|
|
1433
|
+
"own",
|
|
1434
|
+
"same",
|
|
1435
|
+
"so",
|
|
1436
|
+
"than",
|
|
1437
|
+
"too",
|
|
1438
|
+
"very",
|
|
1439
|
+
"just",
|
|
1440
|
+
"because",
|
|
1441
|
+
"but",
|
|
1442
|
+
"and",
|
|
1443
|
+
"or",
|
|
1444
|
+
"if",
|
|
1445
|
+
"while",
|
|
1446
|
+
"that",
|
|
1447
|
+
"this",
|
|
1448
|
+
"it"
|
|
1449
|
+
]);
|
|
1450
|
+
function removeStopWords(tokens) {
|
|
1451
|
+
if (tokens.length <= 1)
|
|
1452
|
+
return tokens;
|
|
1453
|
+
const filtered = tokens.filter((t) => !STOP_WORDS.has(t.toLowerCase()));
|
|
1454
|
+
return filtered.length > 0 ? filtered : tokens;
|
|
1455
|
+
}
|
|
1456
|
+
function extractHighlights(memory, queryLower) {
|
|
1457
|
+
const highlights = [];
|
|
1458
|
+
const tokens = queryLower.split(/\s+/).filter(Boolean);
|
|
1459
|
+
for (const field of ["key", "value", "summary"]) {
|
|
1460
|
+
const text = field === "summary" ? memory.summary : memory[field];
|
|
1461
|
+
if (!text)
|
|
1462
|
+
continue;
|
|
1463
|
+
const textLower = text.toLowerCase();
|
|
1464
|
+
const searchTerms = [queryLower, ...tokens].filter(Boolean);
|
|
1465
|
+
for (const term of searchTerms) {
|
|
1466
|
+
const idx = textLower.indexOf(term);
|
|
1467
|
+
if (idx !== -1) {
|
|
1468
|
+
const start = Math.max(0, idx - 30);
|
|
1469
|
+
const end = Math.min(text.length, idx + term.length + 30);
|
|
1470
|
+
const prefix = start > 0 ? "..." : "";
|
|
1471
|
+
const suffix = end < text.length ? "..." : "";
|
|
1472
|
+
highlights.push({
|
|
1473
|
+
field,
|
|
1474
|
+
snippet: prefix + text.slice(start, end) + suffix
|
|
1475
|
+
});
|
|
1476
|
+
break;
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
for (const tag of memory.tags) {
|
|
1481
|
+
if (tag.toLowerCase().includes(queryLower) || tokens.some((t) => tag.toLowerCase().includes(t))) {
|
|
1482
|
+
highlights.push({ field: "tag", snippet: tag });
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
return highlights;
|
|
1486
|
+
}
|
|
1487
|
+
function determineMatchType(memory, queryLower) {
|
|
1488
|
+
if (memory.key.toLowerCase() === queryLower)
|
|
1489
|
+
return "exact";
|
|
1490
|
+
if (memory.tags.some((t) => t.toLowerCase() === queryLower))
|
|
1491
|
+
return "tag";
|
|
1492
|
+
if (memory.tags.some((t) => t.toLowerCase().includes(queryLower)))
|
|
1493
|
+
return "tag";
|
|
1494
|
+
return "fuzzy";
|
|
1495
|
+
}
|
|
1496
|
+
function computeScore(memory, queryLower) {
|
|
1497
|
+
const fieldScores = [];
|
|
1498
|
+
const keyLower = memory.key.toLowerCase();
|
|
1499
|
+
if (keyLower === queryLower) {
|
|
1500
|
+
fieldScores.push(10);
|
|
1501
|
+
} else if (keyLower.includes(queryLower)) {
|
|
1502
|
+
fieldScores.push(7);
|
|
1503
|
+
}
|
|
1504
|
+
if (memory.tags.some((t) => t.toLowerCase() === queryLower)) {
|
|
1505
|
+
fieldScores.push(6);
|
|
1506
|
+
} else if (memory.tags.some((t) => t.toLowerCase().includes(queryLower))) {
|
|
1507
|
+
fieldScores.push(3);
|
|
1508
|
+
}
|
|
1509
|
+
if (memory.summary && memory.summary.toLowerCase().includes(queryLower)) {
|
|
1510
|
+
fieldScores.push(4);
|
|
1511
|
+
}
|
|
1512
|
+
if (memory.value.toLowerCase().includes(queryLower)) {
|
|
1513
|
+
fieldScores.push(3);
|
|
1514
|
+
}
|
|
1515
|
+
const metadataStr = JSON.stringify(memory.metadata).toLowerCase();
|
|
1516
|
+
if (metadataStr !== "{}" && metadataStr.includes(queryLower)) {
|
|
1517
|
+
fieldScores.push(2);
|
|
1518
|
+
}
|
|
1519
|
+
fieldScores.sort((a, b) => b - a);
|
|
1520
|
+
const diminishingMultipliers = [1, 0.5, 0.25, 0.15, 0.15];
|
|
1521
|
+
let score = 0;
|
|
1522
|
+
for (let i = 0;i < fieldScores.length; i++) {
|
|
1523
|
+
score += fieldScores[i] * (diminishingMultipliers[i] ?? 0.15);
|
|
1524
|
+
}
|
|
1525
|
+
const { phrases } = extractQuotedPhrases(queryLower);
|
|
1526
|
+
for (const phrase of phrases) {
|
|
1527
|
+
if (keyLower.includes(phrase))
|
|
1528
|
+
score += 8;
|
|
1529
|
+
if (memory.value.toLowerCase().includes(phrase))
|
|
1530
|
+
score += 5;
|
|
1531
|
+
if (memory.summary && memory.summary.toLowerCase().includes(phrase))
|
|
1532
|
+
score += 4;
|
|
1533
|
+
}
|
|
1534
|
+
const { remainder } = extractQuotedPhrases(queryLower);
|
|
1535
|
+
const tokens = removeStopWords(remainder.split(/\s+/).filter(Boolean));
|
|
1536
|
+
if (tokens.length > 1) {
|
|
1537
|
+
let tokenScore = 0;
|
|
1538
|
+
for (const token of tokens) {
|
|
1539
|
+
if (keyLower === token) {
|
|
1540
|
+
tokenScore += 10 / tokens.length;
|
|
1541
|
+
} else if (keyLower.includes(token)) {
|
|
1542
|
+
tokenScore += 7 / tokens.length;
|
|
1543
|
+
}
|
|
1544
|
+
if (memory.tags.some((t) => t.toLowerCase() === token)) {
|
|
1545
|
+
tokenScore += 6 / tokens.length;
|
|
1546
|
+
} else if (memory.tags.some((t) => t.toLowerCase().includes(token))) {
|
|
1547
|
+
tokenScore += 3 / tokens.length;
|
|
1548
|
+
}
|
|
1549
|
+
if (memory.summary && memory.summary.toLowerCase().includes(token)) {
|
|
1550
|
+
tokenScore += 4 / tokens.length;
|
|
1551
|
+
}
|
|
1552
|
+
if (memory.value.toLowerCase().includes(token)) {
|
|
1553
|
+
tokenScore += 3 / tokens.length;
|
|
1554
|
+
}
|
|
1555
|
+
if (metadataStr !== "{}" && metadataStr.includes(token)) {
|
|
1556
|
+
tokenScore += 2 / tokens.length;
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
if (score > 0) {
|
|
1560
|
+
score += tokenScore * 0.3;
|
|
1561
|
+
} else {
|
|
1562
|
+
score += tokenScore;
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
return score;
|
|
1566
|
+
}
|
|
1567
|
+
function extractQuotedPhrases(query) {
|
|
1568
|
+
const phrases = [];
|
|
1569
|
+
const remainder = query.replace(/"([^"]+)"/g, (_match, phrase) => {
|
|
1570
|
+
phrases.push(phrase);
|
|
1571
|
+
return "";
|
|
1572
|
+
});
|
|
1573
|
+
return { phrases, remainder: remainder.trim() };
|
|
1574
|
+
}
|
|
1575
|
+
function escapeFts5Query(query) {
|
|
1576
|
+
const { phrases, remainder } = extractQuotedPhrases(query);
|
|
1577
|
+
const parts = [];
|
|
1578
|
+
for (const phrase of phrases) {
|
|
1579
|
+
parts.push(`"${phrase.replace(/"/g, '""')}"`);
|
|
1580
|
+
}
|
|
1581
|
+
const tokens = removeStopWords(remainder.split(/\s+/).filter(Boolean));
|
|
1582
|
+
for (const t of tokens) {
|
|
1583
|
+
parts.push(`"${t.replace(/"/g, '""')}"`);
|
|
1584
|
+
}
|
|
1585
|
+
return parts.join(" ");
|
|
1586
|
+
}
|
|
1587
|
+
function hasFts5Table(d) {
|
|
1588
|
+
try {
|
|
1589
|
+
const row = d.query("SELECT name FROM sqlite_master WHERE type='table' AND name='memories_fts'").get();
|
|
1590
|
+
return !!row;
|
|
1591
|
+
} catch {
|
|
1592
|
+
return false;
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
function buildFilterConditions(filter) {
|
|
1596
|
+
const conditions = [];
|
|
1597
|
+
const params = [];
|
|
1598
|
+
conditions.push("m.status = 'active'");
|
|
1599
|
+
conditions.push("(m.expires_at IS NULL OR m.expires_at >= datetime('now'))");
|
|
1600
|
+
if (!filter)
|
|
1601
|
+
return { conditions, params };
|
|
1602
|
+
if (filter.scope) {
|
|
1603
|
+
if (Array.isArray(filter.scope)) {
|
|
1604
|
+
conditions.push(`m.scope IN (${filter.scope.map(() => "?").join(",")})`);
|
|
1605
|
+
params.push(...filter.scope);
|
|
1606
|
+
} else {
|
|
1607
|
+
conditions.push("m.scope = ?");
|
|
1608
|
+
params.push(filter.scope);
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
if (filter.category) {
|
|
1612
|
+
if (Array.isArray(filter.category)) {
|
|
1613
|
+
conditions.push(`m.category IN (${filter.category.map(() => "?").join(",")})`);
|
|
1614
|
+
params.push(...filter.category);
|
|
1615
|
+
} else {
|
|
1616
|
+
conditions.push("m.category = ?");
|
|
1617
|
+
params.push(filter.category);
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
if (filter.source) {
|
|
1621
|
+
if (Array.isArray(filter.source)) {
|
|
1622
|
+
conditions.push(`m.source IN (${filter.source.map(() => "?").join(",")})`);
|
|
1623
|
+
params.push(...filter.source);
|
|
1624
|
+
} else {
|
|
1625
|
+
conditions.push("m.source = ?");
|
|
1626
|
+
params.push(filter.source);
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
if (filter.status) {
|
|
1630
|
+
conditions.shift();
|
|
1631
|
+
if (Array.isArray(filter.status)) {
|
|
1632
|
+
conditions.push(`m.status IN (${filter.status.map(() => "?").join(",")})`);
|
|
1633
|
+
params.push(...filter.status);
|
|
1634
|
+
} else {
|
|
1635
|
+
conditions.push("m.status = ?");
|
|
1636
|
+
params.push(filter.status);
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
if (filter.project_id) {
|
|
1640
|
+
conditions.push("m.project_id = ?");
|
|
1641
|
+
params.push(filter.project_id);
|
|
1642
|
+
}
|
|
1643
|
+
if (filter.agent_id) {
|
|
1644
|
+
conditions.push("m.agent_id = ?");
|
|
1645
|
+
params.push(filter.agent_id);
|
|
1646
|
+
}
|
|
1647
|
+
if (filter.session_id) {
|
|
1648
|
+
conditions.push("m.session_id = ?");
|
|
1649
|
+
params.push(filter.session_id);
|
|
1650
|
+
}
|
|
1651
|
+
if (filter.min_importance) {
|
|
1652
|
+
conditions.push("m.importance >= ?");
|
|
1653
|
+
params.push(filter.min_importance);
|
|
1654
|
+
}
|
|
1655
|
+
if (filter.pinned !== undefined) {
|
|
1656
|
+
conditions.push("m.pinned = ?");
|
|
1657
|
+
params.push(filter.pinned ? 1 : 0);
|
|
1658
|
+
}
|
|
1659
|
+
if (filter.tags && filter.tags.length > 0) {
|
|
1660
|
+
for (const tag of filter.tags) {
|
|
1661
|
+
conditions.push("m.id IN (SELECT memory_id FROM memory_tags WHERE tag = ?)");
|
|
1662
|
+
params.push(tag);
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
return { conditions, params };
|
|
1666
|
+
}
|
|
1667
|
+
function searchWithFts5(d, query, queryLower, filter, graphBoostedIds) {
|
|
1668
|
+
const ftsQuery = escapeFts5Query(query);
|
|
1669
|
+
if (!ftsQuery)
|
|
1670
|
+
return null;
|
|
1671
|
+
try {
|
|
1672
|
+
const { conditions, params } = buildFilterConditions(filter);
|
|
1673
|
+
const queryParam = `%${query}%`;
|
|
1674
|
+
const ftsCondition = `(m.rowid IN (SELECT f.rowid FROM memories_fts f WHERE memories_fts MATCH ?) ` + `OR m.id IN (SELECT memory_id FROM memory_tags WHERE tag LIKE ?) ` + `OR m.metadata LIKE ?)`;
|
|
1675
|
+
const allConditions = [ftsCondition, ...conditions];
|
|
1676
|
+
const allParams = [ftsQuery, queryParam, queryParam, ...params];
|
|
1677
|
+
const candidateSql = `SELECT m.* FROM memories m WHERE ${allConditions.join(" AND ")}`;
|
|
1678
|
+
const rows = d.query(candidateSql).all(...allParams);
|
|
1679
|
+
return scoreResults(rows, queryLower, graphBoostedIds);
|
|
1680
|
+
} catch {
|
|
1681
|
+
return null;
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
function searchWithLike(d, query, queryLower, filter, graphBoostedIds) {
|
|
1685
|
+
const { conditions, params } = buildFilterConditions(filter);
|
|
1686
|
+
const rawTokens = query.trim().split(/\s+/).filter(Boolean);
|
|
1687
|
+
const tokens = removeStopWords(rawTokens);
|
|
1688
|
+
const escapedQuery = escapeLikePattern(query);
|
|
1689
|
+
const likePatterns = [`%${escapedQuery}%`];
|
|
1690
|
+
if (tokens.length > 1) {
|
|
1691
|
+
for (const t of tokens)
|
|
1692
|
+
likePatterns.push(`%${escapeLikePattern(t)}%`);
|
|
1693
|
+
}
|
|
1694
|
+
const fieldClauses = [];
|
|
1695
|
+
for (const pattern of likePatterns) {
|
|
1696
|
+
fieldClauses.push("m.key LIKE ? ESCAPE '\\'");
|
|
1697
|
+
params.push(pattern);
|
|
1698
|
+
fieldClauses.push("m.value LIKE ? ESCAPE '\\'");
|
|
1699
|
+
params.push(pattern);
|
|
1700
|
+
fieldClauses.push("m.summary LIKE ? ESCAPE '\\'");
|
|
1701
|
+
params.push(pattern);
|
|
1702
|
+
fieldClauses.push("m.metadata LIKE ? ESCAPE '\\'");
|
|
1703
|
+
params.push(pattern);
|
|
1704
|
+
fieldClauses.push("m.id IN (SELECT memory_id FROM memory_tags WHERE tag LIKE ? ESCAPE '\\')");
|
|
1705
|
+
params.push(pattern);
|
|
1706
|
+
}
|
|
1707
|
+
conditions.push(`(${fieldClauses.join(" OR ")})`);
|
|
1708
|
+
const sql = `SELECT DISTINCT m.* FROM memories m WHERE ${conditions.join(" AND ")}`;
|
|
1709
|
+
const rows = d.query(sql).all(...params);
|
|
1710
|
+
return scoreResults(rows, queryLower, graphBoostedIds);
|
|
1711
|
+
}
|
|
1712
|
+
function generateTrigrams(s) {
|
|
1713
|
+
const lower = s.toLowerCase();
|
|
1714
|
+
const trigrams = new Set;
|
|
1715
|
+
for (let i = 0;i <= lower.length - 3; i++) {
|
|
1716
|
+
trigrams.add(lower.slice(i, i + 3));
|
|
1717
|
+
}
|
|
1718
|
+
return trigrams;
|
|
1719
|
+
}
|
|
1720
|
+
function trigramSimilarity(a, b) {
|
|
1721
|
+
const triA = generateTrigrams(a);
|
|
1722
|
+
const triB = generateTrigrams(b);
|
|
1723
|
+
if (triA.size === 0 || triB.size === 0)
|
|
1724
|
+
return 0;
|
|
1725
|
+
let intersection = 0;
|
|
1726
|
+
for (const t of triA) {
|
|
1727
|
+
if (triB.has(t))
|
|
1728
|
+
intersection++;
|
|
1729
|
+
}
|
|
1730
|
+
const union = triA.size + triB.size - intersection;
|
|
1731
|
+
return union === 0 ? 0 : intersection / union;
|
|
1732
|
+
}
|
|
1733
|
+
function searchWithFuzzy(d, query, filter, graphBoostedIds) {
|
|
1734
|
+
const { conditions, params } = buildFilterConditions(filter);
|
|
1735
|
+
const sql = `SELECT m.* FROM memories m WHERE ${conditions.join(" AND ")}`;
|
|
1736
|
+
const rows = d.query(sql).all(...params);
|
|
1737
|
+
const MIN_SIMILARITY = 0.3;
|
|
1738
|
+
const results = [];
|
|
1739
|
+
for (const row of rows) {
|
|
1740
|
+
const memory = parseMemoryRow2(row);
|
|
1741
|
+
let bestSimilarity = 0;
|
|
1742
|
+
bestSimilarity = Math.max(bestSimilarity, trigramSimilarity(query, memory.key));
|
|
1743
|
+
bestSimilarity = Math.max(bestSimilarity, trigramSimilarity(query, memory.value.slice(0, 200)));
|
|
1744
|
+
if (memory.summary) {
|
|
1745
|
+
bestSimilarity = Math.max(bestSimilarity, trigramSimilarity(query, memory.summary));
|
|
1746
|
+
}
|
|
1747
|
+
for (const tag of memory.tags) {
|
|
1748
|
+
bestSimilarity = Math.max(bestSimilarity, trigramSimilarity(query, tag));
|
|
1749
|
+
}
|
|
1750
|
+
if (bestSimilarity >= MIN_SIMILARITY) {
|
|
1751
|
+
const graphBoost = graphBoostedIds?.has(memory.id) ? 2 : 0;
|
|
1752
|
+
const score = bestSimilarity * 5 * memory.importance / 10 + graphBoost;
|
|
1753
|
+
results.push({ memory, score, match_type: "fuzzy" });
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
results.sort((a, b) => b.score - a.score);
|
|
1757
|
+
return results;
|
|
1758
|
+
}
|
|
1759
|
+
function getGraphBoostedMemoryIds(query, d) {
|
|
1760
|
+
const boostedIds = new Set;
|
|
1761
|
+
try {
|
|
1762
|
+
const matchingEntities = listEntities({ search: query, limit: 10 }, d);
|
|
1763
|
+
const exactMatch = getEntityByName(query, undefined, undefined, d);
|
|
1764
|
+
if (exactMatch && !matchingEntities.find((e) => e.id === exactMatch.id)) {
|
|
1765
|
+
matchingEntities.push(exactMatch);
|
|
1766
|
+
}
|
|
1767
|
+
for (const entity of matchingEntities) {
|
|
1768
|
+
const memories = getMemoriesForEntity(entity.id, d);
|
|
1769
|
+
for (const mem of memories) {
|
|
1770
|
+
boostedIds.add(mem.id);
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
} catch {}
|
|
1774
|
+
return boostedIds;
|
|
1775
|
+
}
|
|
1776
|
+
function computeRecencyBoost(memory) {
|
|
1777
|
+
if (memory.pinned)
|
|
1778
|
+
return 1;
|
|
1779
|
+
const mostRecent = memory.accessed_at || memory.updated_at;
|
|
1780
|
+
if (!mostRecent)
|
|
1781
|
+
return 0;
|
|
1782
|
+
const daysSinceAccess = (Date.now() - Date.parse(mostRecent)) / (1000 * 60 * 60 * 24);
|
|
1783
|
+
return Math.max(0, 1 - daysSinceAccess / 30);
|
|
1784
|
+
}
|
|
1785
|
+
function scoreResults(rows, queryLower, graphBoostedIds) {
|
|
1786
|
+
const scored = [];
|
|
1787
|
+
for (const row of rows) {
|
|
1788
|
+
const memory = parseMemoryRow2(row);
|
|
1789
|
+
const rawScore = computeScore(memory, queryLower);
|
|
1790
|
+
if (rawScore === 0)
|
|
1791
|
+
continue;
|
|
1792
|
+
const weightedScore = rawScore * memory.importance / 10;
|
|
1793
|
+
const recencyBoost = computeRecencyBoost(memory);
|
|
1794
|
+
const accessBoost = Math.min(memory.access_count / 20, 0.2);
|
|
1795
|
+
const graphBoost = graphBoostedIds?.has(memory.id) ? 2 : 0;
|
|
1796
|
+
const finalScore = (weightedScore + graphBoost) * (1 + recencyBoost * 0.3) * (1 + accessBoost);
|
|
1797
|
+
const matchType = determineMatchType(memory, queryLower);
|
|
1798
|
+
scored.push({
|
|
1799
|
+
memory,
|
|
1800
|
+
score: finalScore,
|
|
1801
|
+
match_type: matchType,
|
|
1802
|
+
highlights: extractHighlights(memory, queryLower)
|
|
1803
|
+
});
|
|
1804
|
+
}
|
|
1805
|
+
scored.sort((a, b) => {
|
|
1806
|
+
if (b.score !== a.score)
|
|
1807
|
+
return b.score - a.score;
|
|
1808
|
+
return b.memory.importance - a.memory.importance;
|
|
1809
|
+
});
|
|
1810
|
+
return scored;
|
|
1811
|
+
}
|
|
1812
|
+
function searchMemories(query, filter, db) {
|
|
1813
|
+
const d = db || getDatabase();
|
|
1814
|
+
query = preprocessQuery(query);
|
|
1815
|
+
if (!query)
|
|
1816
|
+
return [];
|
|
1817
|
+
const queryLower = query.toLowerCase();
|
|
1818
|
+
const graphBoostedIds = getGraphBoostedMemoryIds(query, d);
|
|
1819
|
+
let scored;
|
|
1820
|
+
if (hasFts5Table(d)) {
|
|
1821
|
+
const ftsResult = searchWithFts5(d, query, queryLower, filter, graphBoostedIds);
|
|
1822
|
+
if (ftsResult !== null) {
|
|
1823
|
+
scored = ftsResult;
|
|
1824
|
+
} else {
|
|
1825
|
+
scored = searchWithLike(d, query, queryLower, filter, graphBoostedIds);
|
|
1826
|
+
}
|
|
1827
|
+
} else {
|
|
1828
|
+
scored = searchWithLike(d, query, queryLower, filter, graphBoostedIds);
|
|
1829
|
+
}
|
|
1830
|
+
if (scored.length < 3) {
|
|
1831
|
+
const fuzzyResults = searchWithFuzzy(d, query, filter, graphBoostedIds);
|
|
1832
|
+
const seenIds = new Set(scored.map((r) => r.memory.id));
|
|
1833
|
+
for (const fr of fuzzyResults) {
|
|
1834
|
+
if (!seenIds.has(fr.memory.id)) {
|
|
1835
|
+
scored.push(fr);
|
|
1836
|
+
seenIds.add(fr.memory.id);
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
scored.sort((a, b) => {
|
|
1840
|
+
if (b.score !== a.score)
|
|
1841
|
+
return b.score - a.score;
|
|
1842
|
+
return b.memory.importance - a.memory.importance;
|
|
1843
|
+
});
|
|
1844
|
+
}
|
|
1845
|
+
const offset = filter?.offset ?? 0;
|
|
1846
|
+
const limit = filter?.limit ?? scored.length;
|
|
1847
|
+
const finalResults = scored.slice(offset, offset + limit);
|
|
1848
|
+
logSearchQuery(query, scored.length, filter?.agent_id, filter?.project_id, d);
|
|
1849
|
+
return finalResults;
|
|
1850
|
+
}
|
|
1851
|
+
function logSearchQuery(query, resultCount, agentId, projectId, db) {
|
|
1852
|
+
try {
|
|
1853
|
+
const d = db || getDatabase();
|
|
1854
|
+
const id = crypto.randomUUID().slice(0, 8);
|
|
1855
|
+
d.run("INSERT INTO search_history (id, query, result_count, agent_id, project_id) VALUES (?, ?, ?, ?, ?)", [id, query, resultCount, agentId || null, projectId || null]);
|
|
1856
|
+
} catch {}
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
// src/lib/duration.ts
|
|
1860
|
+
var UNIT_MS = {
|
|
1861
|
+
s: 1000,
|
|
1862
|
+
m: 60000,
|
|
1863
|
+
h: 3600000,
|
|
1864
|
+
d: 86400000,
|
|
1865
|
+
w: 604800000
|
|
1866
|
+
};
|
|
1867
|
+
var DURATION_RE = /^(\d+[smhdw])+$/;
|
|
1868
|
+
var SEGMENT_RE = /(\d+)([smhdw])/g;
|
|
1869
|
+
function parseDuration(input) {
|
|
1870
|
+
if (typeof input === "number")
|
|
1871
|
+
return input;
|
|
1872
|
+
const trimmed = input.trim();
|
|
1873
|
+
if (trimmed === "")
|
|
1874
|
+
throw new Error("Invalid duration: empty string");
|
|
1875
|
+
if (/^\d+$/.test(trimmed)) {
|
|
1876
|
+
return parseInt(trimmed, 10);
|
|
1877
|
+
}
|
|
1878
|
+
if (!DURATION_RE.test(trimmed)) {
|
|
1879
|
+
throw new Error(`Invalid duration format: "${trimmed}". Use combinations of Ns, Nm, Nh, Nd, Nw (e.g. "1d12h", "30m") or plain milliseconds.`);
|
|
1880
|
+
}
|
|
1881
|
+
let total = 0;
|
|
1882
|
+
let match;
|
|
1883
|
+
SEGMENT_RE.lastIndex = 0;
|
|
1884
|
+
while ((match = SEGMENT_RE.exec(trimmed)) !== null) {
|
|
1885
|
+
const value = parseInt(match[1], 10);
|
|
1886
|
+
const unit = match[2];
|
|
1887
|
+
total += value * UNIT_MS[unit];
|
|
1888
|
+
}
|
|
1889
|
+
if (total === 0) {
|
|
1890
|
+
throw new Error(`Invalid duration: "${trimmed}" resolves to 0ms`);
|
|
1891
|
+
}
|
|
1892
|
+
return total;
|
|
1893
|
+
}
|
|
1894
|
+
var FORMAT_UNITS = [
|
|
1895
|
+
["w", UNIT_MS["w"]],
|
|
1896
|
+
["d", UNIT_MS["d"]],
|
|
1897
|
+
["h", UNIT_MS["h"]],
|
|
1898
|
+
["m", UNIT_MS["m"]],
|
|
1899
|
+
["s", UNIT_MS["s"]]
|
|
1900
|
+
];
|
|
1901
|
+
|
|
1902
|
+
// src/server/index.ts
|
|
1903
|
+
var DEFAULT_PORT = 19428;
|
|
1904
|
+
function parsePort() {
|
|
1905
|
+
const envPort = process.env["PORT"];
|
|
1906
|
+
if (envPort) {
|
|
1907
|
+
const p = parseInt(envPort, 10);
|
|
1908
|
+
if (!Number.isNaN(p))
|
|
1909
|
+
return p;
|
|
1910
|
+
}
|
|
1911
|
+
const portArg = process.argv.find((a) => a === "--port" || a.startsWith("--port="));
|
|
1912
|
+
if (portArg) {
|
|
1913
|
+
if (portArg.includes("=")) {
|
|
1914
|
+
return parseInt(portArg.split("=")[1], 10) || DEFAULT_PORT;
|
|
1915
|
+
}
|
|
1916
|
+
const idx = process.argv.indexOf(portArg);
|
|
1917
|
+
return parseInt(process.argv[idx + 1], 10) || DEFAULT_PORT;
|
|
1918
|
+
}
|
|
1919
|
+
return DEFAULT_PORT;
|
|
1920
|
+
}
|
|
1921
|
+
function resolveDashboardDir() {
|
|
1922
|
+
const candidates = [];
|
|
1923
|
+
try {
|
|
1924
|
+
const scriptDir = dirname3(fileURLToPath(import.meta.url));
|
|
1925
|
+
candidates.push(join3(scriptDir, "..", "dashboard", "dist"));
|
|
1926
|
+
candidates.push(join3(scriptDir, "..", "..", "dashboard", "dist"));
|
|
1927
|
+
} catch {}
|
|
1928
|
+
if (process.argv[1]) {
|
|
1929
|
+
const mainDir = dirname3(process.argv[1]);
|
|
1930
|
+
candidates.push(join3(mainDir, "..", "dashboard", "dist"));
|
|
1931
|
+
candidates.push(join3(mainDir, "..", "..", "dashboard", "dist"));
|
|
1932
|
+
}
|
|
1933
|
+
candidates.push(join3(process.cwd(), "dashboard", "dist"));
|
|
1934
|
+
for (const c of candidates) {
|
|
1935
|
+
if (existsSync3(c))
|
|
1936
|
+
return c;
|
|
1937
|
+
}
|
|
1938
|
+
return join3(process.cwd(), "dashboard", "dist");
|
|
1939
|
+
}
|
|
1940
|
+
var MIME_TYPES = {
|
|
1941
|
+
".html": "text/html; charset=utf-8",
|
|
1942
|
+
".js": "application/javascript",
|
|
1943
|
+
".css": "text/css",
|
|
1944
|
+
".json": "application/json",
|
|
1945
|
+
".svg": "image/svg+xml",
|
|
1946
|
+
".png": "image/png",
|
|
1947
|
+
".ico": "image/x-icon",
|
|
1948
|
+
".woff": "font/woff",
|
|
1949
|
+
".woff2": "font/woff2"
|
|
1950
|
+
};
|
|
1951
|
+
function serveStaticFile(filePath) {
|
|
1952
|
+
if (!existsSync3(filePath))
|
|
1953
|
+
return null;
|
|
1954
|
+
const ct = MIME_TYPES[extname(filePath)] || "application/octet-stream";
|
|
1955
|
+
return new Response(Bun.file(filePath), {
|
|
1956
|
+
headers: { "Content-Type": ct }
|
|
1957
|
+
});
|
|
1958
|
+
}
|
|
1959
|
+
var CORS_HEADERS = {
|
|
1960
|
+
"Access-Control-Allow-Origin": "*",
|
|
1961
|
+
"Access-Control-Allow-Methods": "GET, POST, PATCH, DELETE, OPTIONS",
|
|
1962
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
|
1963
|
+
"Access-Control-Max-Age": "86400"
|
|
1964
|
+
};
|
|
1965
|
+
function json(data, status = 200) {
|
|
1966
|
+
return new Response(JSON.stringify(data), {
|
|
1967
|
+
status,
|
|
1968
|
+
headers: { "Content-Type": "application/json", ...CORS_HEADERS }
|
|
1969
|
+
});
|
|
1970
|
+
}
|
|
1971
|
+
function errorResponse(message, status, details) {
|
|
1972
|
+
const body = { error: message };
|
|
1973
|
+
if (details !== undefined)
|
|
1974
|
+
body["details"] = details;
|
|
1975
|
+
return json(body, status);
|
|
1976
|
+
}
|
|
1977
|
+
async function readJson(req) {
|
|
1978
|
+
try {
|
|
1979
|
+
return await req.json();
|
|
1980
|
+
} catch {
|
|
1981
|
+
return null;
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
function getSearchParams(url) {
|
|
1985
|
+
const params = {};
|
|
1986
|
+
url.searchParams.forEach((v, k) => {
|
|
1987
|
+
params[k] = v;
|
|
1988
|
+
});
|
|
1989
|
+
return params;
|
|
1990
|
+
}
|
|
1991
|
+
var routes = [];
|
|
1992
|
+
function addRoute(method, path, handler) {
|
|
1993
|
+
const paramNames = [];
|
|
1994
|
+
const patternStr = path.replace(/:(\w+)/g, (_match, name) => {
|
|
1995
|
+
paramNames.push(name);
|
|
1996
|
+
return "([^/]+)";
|
|
1997
|
+
});
|
|
1998
|
+
routes.push({
|
|
1999
|
+
method,
|
|
2000
|
+
pattern: new RegExp(`^${patternStr}$`),
|
|
2001
|
+
paramNames,
|
|
2002
|
+
handler
|
|
2003
|
+
});
|
|
2004
|
+
}
|
|
2005
|
+
function matchRoute(method, pathname) {
|
|
2006
|
+
for (const route of routes) {
|
|
2007
|
+
if (route.method !== method)
|
|
2008
|
+
continue;
|
|
2009
|
+
const match = pathname.match(route.pattern);
|
|
2010
|
+
if (match) {
|
|
2011
|
+
const params = {};
|
|
2012
|
+
route.paramNames.forEach((name, i) => {
|
|
2013
|
+
params[name] = match[i + 1];
|
|
2014
|
+
});
|
|
2015
|
+
return { handler: route.handler, params };
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
return null;
|
|
2019
|
+
}
|
|
2020
|
+
addRoute("GET", "/api/memories", (_req, url) => {
|
|
2021
|
+
const q = getSearchParams(url);
|
|
2022
|
+
const filter = {};
|
|
2023
|
+
if (q["scope"])
|
|
2024
|
+
filter.scope = q["scope"];
|
|
2025
|
+
if (q["category"])
|
|
2026
|
+
filter.category = q["category"];
|
|
2027
|
+
if (q["tags"])
|
|
2028
|
+
filter.tags = q["tags"].split(",");
|
|
2029
|
+
if (q["min_importance"])
|
|
2030
|
+
filter.min_importance = parseInt(q["min_importance"], 10);
|
|
2031
|
+
if (q["pinned"] !== undefined && q["pinned"] !== "")
|
|
2032
|
+
filter.pinned = q["pinned"] === "true";
|
|
2033
|
+
if (q["agent_id"])
|
|
2034
|
+
filter.agent_id = q["agent_id"];
|
|
2035
|
+
if (q["project_id"])
|
|
2036
|
+
filter.project_id = q["project_id"];
|
|
2037
|
+
if (q["limit"])
|
|
2038
|
+
filter.limit = parseInt(q["limit"], 10);
|
|
2039
|
+
if (q["offset"])
|
|
2040
|
+
filter.offset = parseInt(q["offset"], 10);
|
|
2041
|
+
const memories = listMemories(filter);
|
|
2042
|
+
if (q["fields"]) {
|
|
2043
|
+
const fields = q["fields"].split(",").map((f) => f.trim());
|
|
2044
|
+
const filtered = memories.map((m) => Object.fromEntries(fields.map((f) => [f, m[f]]).filter(([, v]) => v !== undefined)));
|
|
2045
|
+
return json({ memories: filtered, count: filtered.length });
|
|
2046
|
+
}
|
|
2047
|
+
return json({ memories, count: memories.length });
|
|
2048
|
+
});
|
|
2049
|
+
addRoute("GET", "/api/memories/stats", (_req) => {
|
|
2050
|
+
const db = getDatabase();
|
|
2051
|
+
const total = db.query("SELECT COUNT(*) as c FROM memories WHERE status = 'active'").get().c;
|
|
2052
|
+
const byScope = db.query("SELECT scope, COUNT(*) as c FROM memories WHERE status = 'active' GROUP BY scope").all();
|
|
2053
|
+
const byCategory = db.query("SELECT category, COUNT(*) as c FROM memories WHERE status = 'active' GROUP BY category").all();
|
|
2054
|
+
const byStatus = db.query("SELECT status, COUNT(*) as c FROM memories GROUP BY status").all();
|
|
2055
|
+
const pinnedCount = db.query("SELECT COUNT(*) as c FROM memories WHERE pinned = 1 AND status = 'active'").get().c;
|
|
2056
|
+
const expiredCount = db.query("SELECT COUNT(*) as c FROM memories WHERE status = 'expired' OR (expires_at IS NOT NULL AND expires_at < datetime('now'))").get().c;
|
|
2057
|
+
const stats = {
|
|
2058
|
+
total,
|
|
2059
|
+
by_scope: { global: 0, shared: 0, private: 0 },
|
|
2060
|
+
by_category: { preference: 0, fact: 0, knowledge: 0, history: 0 },
|
|
2061
|
+
by_status: { active: 0, archived: 0, expired: 0 },
|
|
2062
|
+
by_agent: {},
|
|
2063
|
+
pinned_count: pinnedCount,
|
|
2064
|
+
expired_count: expiredCount
|
|
2065
|
+
};
|
|
2066
|
+
for (const row of byScope)
|
|
2067
|
+
stats.by_scope[row.scope] = row.c;
|
|
2068
|
+
for (const row of byCategory)
|
|
2069
|
+
stats.by_category[row.category] = row.c;
|
|
2070
|
+
for (const row of byStatus) {
|
|
2071
|
+
if (row.status in stats.by_status) {
|
|
2072
|
+
stats.by_status[row.status] = row.c;
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
const byAgent = db.query("SELECT agent_id, COUNT(*) as c FROM memories WHERE status = 'active' AND agent_id IS NOT NULL GROUP BY agent_id").all();
|
|
2076
|
+
for (const row of byAgent)
|
|
2077
|
+
stats.by_agent[row.agent_id] = row.c;
|
|
2078
|
+
return json(stats);
|
|
2079
|
+
});
|
|
2080
|
+
addRoute("POST", "/api/memories/search", async (req) => {
|
|
2081
|
+
const body = await readJson(req);
|
|
2082
|
+
if (!body || typeof body["query"] !== "string") {
|
|
2083
|
+
return errorResponse("Missing required field: query", 400);
|
|
2084
|
+
}
|
|
2085
|
+
const filter = {};
|
|
2086
|
+
if (body["scope"])
|
|
2087
|
+
filter.scope = body["scope"];
|
|
2088
|
+
if (body["category"])
|
|
2089
|
+
filter.category = body["category"];
|
|
2090
|
+
if (body["tags"])
|
|
2091
|
+
filter.tags = body["tags"];
|
|
2092
|
+
if (body["limit"])
|
|
2093
|
+
filter.limit = body["limit"];
|
|
2094
|
+
const results = searchMemories(body["query"], filter);
|
|
2095
|
+
return json({ results, count: results.length });
|
|
2096
|
+
});
|
|
2097
|
+
addRoute("POST", "/api/memories/export", async (req) => {
|
|
2098
|
+
const body = await readJson(req) || {};
|
|
2099
|
+
const filter = {};
|
|
2100
|
+
if (body["scope"])
|
|
2101
|
+
filter.scope = body["scope"];
|
|
2102
|
+
if (body["category"])
|
|
2103
|
+
filter.category = body["category"];
|
|
2104
|
+
if (body["agent_id"])
|
|
2105
|
+
filter.agent_id = body["agent_id"];
|
|
2106
|
+
if (body["project_id"])
|
|
2107
|
+
filter.project_id = body["project_id"];
|
|
2108
|
+
if (body["tags"])
|
|
2109
|
+
filter.tags = body["tags"];
|
|
2110
|
+
filter.limit = body["limit"] || 1e4;
|
|
2111
|
+
const memories = listMemories(filter);
|
|
2112
|
+
return json({ memories, count: memories.length });
|
|
2113
|
+
});
|
|
2114
|
+
addRoute("POST", "/api/memories/import", async (req) => {
|
|
2115
|
+
const body = await readJson(req);
|
|
2116
|
+
if (!body || !Array.isArray(body["memories"])) {
|
|
2117
|
+
return errorResponse("Missing required field: memories (array)", 400);
|
|
2118
|
+
}
|
|
2119
|
+
const overwrite = body["overwrite"] !== false;
|
|
2120
|
+
const dedupeMode = overwrite ? "merge" : "create";
|
|
2121
|
+
const memoriesArr = body["memories"];
|
|
2122
|
+
let imported = 0;
|
|
2123
|
+
const errors = [];
|
|
2124
|
+
for (const mem of memoriesArr) {
|
|
2125
|
+
try {
|
|
2126
|
+
createMemory({
|
|
2127
|
+
...mem,
|
|
2128
|
+
source: mem["source"] || "imported"
|
|
2129
|
+
}, dedupeMode);
|
|
2130
|
+
imported++;
|
|
2131
|
+
} catch (e) {
|
|
2132
|
+
errors.push(`Failed to import "${mem["key"]}": ${e instanceof Error ? e.message : String(e)}`);
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
return json({ imported, errors, total: memoriesArr.length }, 201);
|
|
2136
|
+
});
|
|
2137
|
+
addRoute("POST", "/api/memories/clean", () => {
|
|
2138
|
+
const cleaned = cleanExpiredMemories();
|
|
2139
|
+
return json({ cleaned });
|
|
2140
|
+
});
|
|
2141
|
+
addRoute("POST", "/api/memories", async (req) => {
|
|
2142
|
+
const body = await readJson(req);
|
|
2143
|
+
if (!body) {
|
|
2144
|
+
return errorResponse("Invalid JSON body", 400);
|
|
2145
|
+
}
|
|
2146
|
+
if (!body["key"] || !body["value"]) {
|
|
2147
|
+
return errorResponse("Missing required fields: key, value", 400);
|
|
2148
|
+
}
|
|
2149
|
+
try {
|
|
2150
|
+
if (body["ttl_ms"] !== undefined && typeof body["ttl_ms"] === "string") {
|
|
2151
|
+
body["ttl_ms"] = parseDuration(body["ttl_ms"]);
|
|
2152
|
+
}
|
|
2153
|
+
const memory = createMemory(body);
|
|
2154
|
+
return json(memory, 201);
|
|
2155
|
+
} catch (e) {
|
|
2156
|
+
if (e instanceof DuplicateMemoryError) {
|
|
2157
|
+
return errorResponse(e.message, 409);
|
|
2158
|
+
}
|
|
2159
|
+
throw e;
|
|
2160
|
+
}
|
|
2161
|
+
});
|
|
2162
|
+
addRoute("GET", "/api/memories/:id", (_req, _url, params) => {
|
|
2163
|
+
const memory = getMemory(params["id"]);
|
|
2164
|
+
if (!memory) {
|
|
2165
|
+
return errorResponse("Memory not found", 404);
|
|
2166
|
+
}
|
|
2167
|
+
touchMemory(memory.id);
|
|
2168
|
+
return json(memory);
|
|
2169
|
+
});
|
|
2170
|
+
addRoute("PATCH", "/api/memories/:id", async (req, _url, params) => {
|
|
2171
|
+
const body = await readJson(req);
|
|
2172
|
+
if (!body) {
|
|
2173
|
+
return errorResponse("Invalid JSON body", 400);
|
|
2174
|
+
}
|
|
2175
|
+
if (body["version"] === undefined) {
|
|
2176
|
+
return errorResponse("Missing required field: version", 400);
|
|
2177
|
+
}
|
|
2178
|
+
try {
|
|
2179
|
+
const memory = updateMemory(params["id"], body);
|
|
2180
|
+
return json(memory);
|
|
2181
|
+
} catch (e) {
|
|
2182
|
+
if (e instanceof MemoryNotFoundError) {
|
|
2183
|
+
return errorResponse(e.message, 404);
|
|
2184
|
+
}
|
|
2185
|
+
if (e instanceof VersionConflictError) {
|
|
2186
|
+
return errorResponse(e.message, 409, {
|
|
2187
|
+
expected: e.expected,
|
|
2188
|
+
actual: e.actual
|
|
2189
|
+
});
|
|
2190
|
+
}
|
|
2191
|
+
throw e;
|
|
2192
|
+
}
|
|
2193
|
+
});
|
|
2194
|
+
addRoute("DELETE", "/api/memories/:id", (_req, _url, params) => {
|
|
2195
|
+
const deleted = deleteMemory(params["id"]);
|
|
2196
|
+
if (!deleted) {
|
|
2197
|
+
return errorResponse("Memory not found", 404);
|
|
2198
|
+
}
|
|
2199
|
+
return json({ deleted: true });
|
|
2200
|
+
});
|
|
2201
|
+
addRoute("GET", "/api/agents", () => {
|
|
2202
|
+
const agents = listAgents();
|
|
2203
|
+
return json({ agents, count: agents.length });
|
|
2204
|
+
});
|
|
2205
|
+
addRoute("POST", "/api/agents", async (req) => {
|
|
2206
|
+
const body = await readJson(req);
|
|
2207
|
+
if (!body || !body["name"]) {
|
|
2208
|
+
return errorResponse("Missing required field: name", 400);
|
|
2209
|
+
}
|
|
2210
|
+
const agent = registerAgent(body["name"], body["description"], body["role"]);
|
|
2211
|
+
return json(agent, 201);
|
|
2212
|
+
});
|
|
2213
|
+
addRoute("GET", "/api/agents/:id", (_req, _url, params) => {
|
|
2214
|
+
const agent = getAgent(params["id"]);
|
|
2215
|
+
if (!agent) {
|
|
2216
|
+
return errorResponse("Agent not found", 404);
|
|
2217
|
+
}
|
|
2218
|
+
return json(agent);
|
|
2219
|
+
});
|
|
2220
|
+
addRoute("GET", "/api/projects", () => {
|
|
2221
|
+
const projects = listProjects();
|
|
2222
|
+
return json({ projects, count: projects.length });
|
|
2223
|
+
});
|
|
2224
|
+
addRoute("POST", "/api/projects", async (req) => {
|
|
2225
|
+
const body = await readJson(req);
|
|
2226
|
+
if (!body || !body["name"] || !body["path"]) {
|
|
2227
|
+
return errorResponse("Missing required fields: name, path", 400);
|
|
2228
|
+
}
|
|
2229
|
+
const project = registerProject(body["name"], body["path"], body["description"], body["memory_prefix"]);
|
|
2230
|
+
return json(project, 201);
|
|
2231
|
+
});
|
|
2232
|
+
addRoute("GET", "/api/inject", (_req, url) => {
|
|
2233
|
+
const q = getSearchParams(url);
|
|
2234
|
+
const maxTokens = q["max_tokens"] ? parseInt(q["max_tokens"], 10) : 500;
|
|
2235
|
+
const minImportance = 3;
|
|
2236
|
+
const categories = [
|
|
2237
|
+
"preference",
|
|
2238
|
+
"fact",
|
|
2239
|
+
"knowledge"
|
|
2240
|
+
];
|
|
2241
|
+
const allMemories = [];
|
|
2242
|
+
const globalMems = listMemories({
|
|
2243
|
+
scope: "global",
|
|
2244
|
+
category: categories,
|
|
2245
|
+
min_importance: minImportance,
|
|
2246
|
+
status: "active",
|
|
2247
|
+
project_id: q["project_id"],
|
|
2248
|
+
limit: 50
|
|
2249
|
+
});
|
|
2250
|
+
allMemories.push(...globalMems);
|
|
2251
|
+
if (q["project_id"]) {
|
|
2252
|
+
const sharedMems = listMemories({
|
|
2253
|
+
scope: "shared",
|
|
2254
|
+
category: categories,
|
|
2255
|
+
min_importance: minImportance,
|
|
2256
|
+
status: "active",
|
|
2257
|
+
project_id: q["project_id"],
|
|
2258
|
+
limit: 50
|
|
2259
|
+
});
|
|
2260
|
+
allMemories.push(...sharedMems);
|
|
2261
|
+
}
|
|
2262
|
+
if (q["agent_id"]) {
|
|
2263
|
+
const privateMems = listMemories({
|
|
2264
|
+
scope: "private",
|
|
2265
|
+
category: categories,
|
|
2266
|
+
min_importance: minImportance,
|
|
2267
|
+
status: "active",
|
|
2268
|
+
agent_id: q["agent_id"],
|
|
2269
|
+
limit: 50
|
|
2270
|
+
});
|
|
2271
|
+
allMemories.push(...privateMems);
|
|
2272
|
+
}
|
|
2273
|
+
const seen = new Set;
|
|
2274
|
+
const unique = allMemories.filter((m) => {
|
|
2275
|
+
if (seen.has(m.id))
|
|
2276
|
+
return false;
|
|
2277
|
+
seen.add(m.id);
|
|
2278
|
+
return true;
|
|
2279
|
+
});
|
|
2280
|
+
unique.sort((a, b) => {
|
|
2281
|
+
if (b.importance !== a.importance)
|
|
2282
|
+
return b.importance - a.importance;
|
|
2283
|
+
return new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime();
|
|
2284
|
+
});
|
|
2285
|
+
const charBudget = maxTokens * 4;
|
|
2286
|
+
const lines = [];
|
|
2287
|
+
let totalChars = 0;
|
|
2288
|
+
for (const m of unique) {
|
|
2289
|
+
const line = `- [${m.scope}/${m.category}] ${m.key}: ${m.value}`;
|
|
2290
|
+
if (totalChars + line.length > charBudget)
|
|
2291
|
+
break;
|
|
2292
|
+
lines.push(line);
|
|
2293
|
+
totalChars += line.length;
|
|
2294
|
+
touchMemory(m.id);
|
|
2295
|
+
}
|
|
2296
|
+
if (lines.length === 0) {
|
|
2297
|
+
return json({ context: "", memories_count: 0 });
|
|
2298
|
+
}
|
|
2299
|
+
const context = `<agent-memories>
|
|
2300
|
+
${lines.join(`
|
|
2301
|
+
`)}
|
|
2302
|
+
</agent-memories>`;
|
|
2303
|
+
return json({ context, memories_count: lines.length });
|
|
2304
|
+
});
|
|
2305
|
+
addRoute("GET", "/api/entities", (_req, url) => {
|
|
2306
|
+
const q = getSearchParams(url);
|
|
2307
|
+
const filter = {};
|
|
2308
|
+
if (q["type"])
|
|
2309
|
+
filter.type = q["type"];
|
|
2310
|
+
if (q["project_id"])
|
|
2311
|
+
filter.project_id = q["project_id"];
|
|
2312
|
+
if (q["search"])
|
|
2313
|
+
filter.search = q["search"];
|
|
2314
|
+
if (q["limit"])
|
|
2315
|
+
filter.limit = parseInt(q["limit"], 10);
|
|
2316
|
+
if (q["offset"])
|
|
2317
|
+
filter.offset = parseInt(q["offset"], 10);
|
|
2318
|
+
const entities = listEntities(filter);
|
|
2319
|
+
return json({ entities, count: entities.length });
|
|
2320
|
+
});
|
|
2321
|
+
addRoute("POST", "/api/entities/merge", async (req) => {
|
|
2322
|
+
const body = await readJson(req);
|
|
2323
|
+
if (!body || !body["source_id"] || !body["target_id"]) {
|
|
2324
|
+
return errorResponse("Missing required fields: source_id, target_id", 400);
|
|
2325
|
+
}
|
|
2326
|
+
try {
|
|
2327
|
+
const merged = mergeEntities(body["source_id"], body["target_id"]);
|
|
2328
|
+
return json(merged);
|
|
2329
|
+
} catch (e) {
|
|
2330
|
+
if (e instanceof EntityNotFoundError) {
|
|
2331
|
+
return errorResponse(e.message, 404);
|
|
2332
|
+
}
|
|
2333
|
+
throw e;
|
|
2334
|
+
}
|
|
2335
|
+
});
|
|
2336
|
+
addRoute("POST", "/api/entities", async (req) => {
|
|
2337
|
+
const body = await readJson(req);
|
|
2338
|
+
if (!body || !body["name"] || !body["type"]) {
|
|
2339
|
+
return errorResponse("Missing required fields: name, type", 400);
|
|
2340
|
+
}
|
|
2341
|
+
try {
|
|
2342
|
+
const entity = createEntity(body);
|
|
2343
|
+
return json(entity, 201);
|
|
2344
|
+
} catch (e) {
|
|
2345
|
+
throw e;
|
|
2346
|
+
}
|
|
2347
|
+
});
|
|
2348
|
+
addRoute("GET", "/api/entities/:id/memories", (_req, _url, params) => {
|
|
2349
|
+
try {
|
|
2350
|
+
getEntity(params["id"]);
|
|
2351
|
+
const memories = getMemoriesForEntity(params["id"]);
|
|
2352
|
+
return json({ memories, count: memories.length });
|
|
2353
|
+
} catch (e) {
|
|
2354
|
+
if (e instanceof EntityNotFoundError) {
|
|
2355
|
+
return errorResponse(e.message, 404);
|
|
2356
|
+
}
|
|
2357
|
+
throw e;
|
|
2358
|
+
}
|
|
2359
|
+
});
|
|
2360
|
+
addRoute("POST", "/api/entities/:id/memories", async (req, _url, params) => {
|
|
2361
|
+
const body = await readJson(req);
|
|
2362
|
+
if (!body || !body["memory_id"]) {
|
|
2363
|
+
return errorResponse("Missing required field: memory_id", 400);
|
|
2364
|
+
}
|
|
2365
|
+
try {
|
|
2366
|
+
const link = linkEntityToMemory(params["id"], body["memory_id"], body["role"] || undefined);
|
|
2367
|
+
return json(link, 201);
|
|
2368
|
+
} catch (e) {
|
|
2369
|
+
if (e instanceof EntityNotFoundError) {
|
|
2370
|
+
return errorResponse(e.message, 404);
|
|
2371
|
+
}
|
|
2372
|
+
throw e;
|
|
2373
|
+
}
|
|
2374
|
+
});
|
|
2375
|
+
addRoute("GET", "/api/entities/:id/relations", (_req, url, params) => {
|
|
2376
|
+
const q = getSearchParams(url);
|
|
2377
|
+
try {
|
|
2378
|
+
getEntity(params["id"]);
|
|
2379
|
+
const relations = listRelations({
|
|
2380
|
+
entity_id: params["id"],
|
|
2381
|
+
relation_type: q["type"],
|
|
2382
|
+
direction: q["direction"]
|
|
2383
|
+
});
|
|
2384
|
+
return json({ relations, count: relations.length });
|
|
2385
|
+
} catch (e) {
|
|
2386
|
+
if (e instanceof EntityNotFoundError) {
|
|
2387
|
+
return errorResponse(e.message, 404);
|
|
2388
|
+
}
|
|
2389
|
+
throw e;
|
|
2390
|
+
}
|
|
2391
|
+
});
|
|
2392
|
+
addRoute("DELETE", "/api/entities/:entityId/memories/:memoryId", (_req, _url, params) => {
|
|
2393
|
+
unlinkEntityFromMemory(params["entityId"], params["memoryId"]);
|
|
2394
|
+
return json({ deleted: true });
|
|
2395
|
+
});
|
|
2396
|
+
addRoute("GET", "/api/entities/:id", (_req, _url, params) => {
|
|
2397
|
+
try {
|
|
2398
|
+
const entity = getEntity(params["id"]);
|
|
2399
|
+
const relations = listRelations({ entity_id: params["id"] });
|
|
2400
|
+
const memories = getMemoriesForEntity(params["id"]);
|
|
2401
|
+
return json({ ...entity, relations, memories });
|
|
2402
|
+
} catch (e) {
|
|
2403
|
+
if (e instanceof EntityNotFoundError) {
|
|
2404
|
+
return errorResponse(e.message, 404);
|
|
2405
|
+
}
|
|
2406
|
+
throw e;
|
|
2407
|
+
}
|
|
2408
|
+
});
|
|
2409
|
+
addRoute("PATCH", "/api/entities/:id", async (req, _url, params) => {
|
|
2410
|
+
const body = await readJson(req);
|
|
2411
|
+
if (!body) {
|
|
2412
|
+
return errorResponse("Invalid JSON body", 400);
|
|
2413
|
+
}
|
|
2414
|
+
try {
|
|
2415
|
+
const entity = updateEntity(params["id"], body);
|
|
2416
|
+
return json(entity);
|
|
2417
|
+
} catch (e) {
|
|
2418
|
+
if (e instanceof EntityNotFoundError) {
|
|
2419
|
+
return errorResponse(e.message, 404);
|
|
2420
|
+
}
|
|
2421
|
+
throw e;
|
|
2422
|
+
}
|
|
2423
|
+
});
|
|
2424
|
+
addRoute("DELETE", "/api/entities/:id", (_req, _url, params) => {
|
|
2425
|
+
try {
|
|
2426
|
+
deleteEntity(params["id"]);
|
|
2427
|
+
return json({ deleted: true });
|
|
2428
|
+
} catch (e) {
|
|
2429
|
+
if (e instanceof EntityNotFoundError) {
|
|
2430
|
+
return errorResponse(e.message, 404);
|
|
2431
|
+
}
|
|
2432
|
+
throw e;
|
|
2433
|
+
}
|
|
2434
|
+
});
|
|
2435
|
+
addRoute("POST", "/api/relations", async (req) => {
|
|
2436
|
+
const body = await readJson(req);
|
|
2437
|
+
if (!body || !body["source_entity_id"] || !body["target_entity_id"] || !body["relation_type"]) {
|
|
2438
|
+
return errorResponse("Missing required fields: source_entity_id, target_entity_id, relation_type", 400);
|
|
2439
|
+
}
|
|
2440
|
+
try {
|
|
2441
|
+
const relation = createRelation(body);
|
|
2442
|
+
return json(relation, 201);
|
|
2443
|
+
} catch (e) {
|
|
2444
|
+
throw e;
|
|
2445
|
+
}
|
|
2446
|
+
});
|
|
2447
|
+
addRoute("GET", "/api/relations/:id", (_req, _url, params) => {
|
|
2448
|
+
try {
|
|
2449
|
+
const relation = getRelation(params["id"]);
|
|
2450
|
+
return json(relation);
|
|
2451
|
+
} catch (e) {
|
|
2452
|
+
if (e instanceof Error && e.message.startsWith("Relation not found")) {
|
|
2453
|
+
return errorResponse(e.message, 404);
|
|
2454
|
+
}
|
|
2455
|
+
throw e;
|
|
2456
|
+
}
|
|
2457
|
+
});
|
|
2458
|
+
addRoute("DELETE", "/api/relations/:id", (_req, _url, params) => {
|
|
2459
|
+
try {
|
|
2460
|
+
deleteRelation(params["id"]);
|
|
2461
|
+
return json({ deleted: true });
|
|
2462
|
+
} catch (e) {
|
|
2463
|
+
if (e instanceof Error && e.message.startsWith("Relation not found")) {
|
|
2464
|
+
return errorResponse(e.message, 404);
|
|
2465
|
+
}
|
|
2466
|
+
throw e;
|
|
2467
|
+
}
|
|
2468
|
+
});
|
|
2469
|
+
addRoute("GET", "/api/graph/path", (_req, url) => {
|
|
2470
|
+
const q = getSearchParams(url);
|
|
2471
|
+
if (!q["from"] || !q["to"]) {
|
|
2472
|
+
return errorResponse("Missing required query params: from, to", 400);
|
|
2473
|
+
}
|
|
2474
|
+
const maxDepth = q["max_depth"] ? parseInt(q["max_depth"], 10) : 5;
|
|
2475
|
+
try {
|
|
2476
|
+
const path = findPath(q["from"], q["to"], maxDepth);
|
|
2477
|
+
if (!path) {
|
|
2478
|
+
return json({ path: null, found: false });
|
|
2479
|
+
}
|
|
2480
|
+
return json({ path, found: true, length: path.length });
|
|
2481
|
+
} catch (e) {
|
|
2482
|
+
if (e instanceof EntityNotFoundError) {
|
|
2483
|
+
return errorResponse(e.message, 404);
|
|
2484
|
+
}
|
|
2485
|
+
throw e;
|
|
2486
|
+
}
|
|
2487
|
+
});
|
|
2488
|
+
addRoute("GET", "/api/graph/stats", () => {
|
|
2489
|
+
const db = getDatabase();
|
|
2490
|
+
const entityCount = db.query("SELECT COUNT(*) as c FROM entities").get().c;
|
|
2491
|
+
const relationCount = db.query("SELECT COUNT(*) as c FROM relations").get().c;
|
|
2492
|
+
const entitiesByType = db.query("SELECT type, COUNT(*) as c FROM entities GROUP BY type").all();
|
|
2493
|
+
const relationsByType = db.query("SELECT relation_type, COUNT(*) as c FROM relations GROUP BY relation_type").all();
|
|
2494
|
+
return json({
|
|
2495
|
+
entities: {
|
|
2496
|
+
total: entityCount,
|
|
2497
|
+
by_type: Object.fromEntries(entitiesByType.map((r) => [r.type, r.c]))
|
|
2498
|
+
},
|
|
2499
|
+
relations: {
|
|
2500
|
+
total: relationCount,
|
|
2501
|
+
by_type: Object.fromEntries(relationsByType.map((r) => [r.relation_type, r.c]))
|
|
2502
|
+
}
|
|
2503
|
+
});
|
|
2504
|
+
});
|
|
2505
|
+
addRoute("GET", "/api/graph/:entityId", (_req, url, params) => {
|
|
2506
|
+
const q = getSearchParams(url);
|
|
2507
|
+
const depth = q["depth"] ? parseInt(q["depth"], 10) : 2;
|
|
2508
|
+
try {
|
|
2509
|
+
const graph = getEntityGraph(params["entityId"], depth);
|
|
2510
|
+
return json(graph);
|
|
2511
|
+
} catch (e) {
|
|
2512
|
+
if (e instanceof EntityNotFoundError) {
|
|
2513
|
+
return errorResponse(e.message, 404);
|
|
2514
|
+
}
|
|
2515
|
+
throw e;
|
|
2516
|
+
}
|
|
2517
|
+
});
|
|
2518
|
+
async function findFreePort(start) {
|
|
2519
|
+
for (let port = start;port < start + 100; port++) {
|
|
2520
|
+
try {
|
|
2521
|
+
const server = Bun.serve({ port, fetch: () => new Response("") });
|
|
2522
|
+
server.stop(true);
|
|
2523
|
+
return port;
|
|
2524
|
+
} catch {}
|
|
2525
|
+
}
|
|
2526
|
+
return start;
|
|
2527
|
+
}
|
|
2528
|
+
function startServer(port) {
|
|
2529
|
+
Bun.serve({
|
|
2530
|
+
port,
|
|
2531
|
+
async fetch(req) {
|
|
2532
|
+
const url = new URL(req.url);
|
|
2533
|
+
const { pathname } = url;
|
|
2534
|
+
if (req.method === "OPTIONS") {
|
|
2535
|
+
return new Response(null, { status: 204, headers: CORS_HEADERS });
|
|
2536
|
+
}
|
|
2537
|
+
if (pathname === "/api/health" || pathname === "/health") {
|
|
2538
|
+
return json({ status: "ok", version: "0.1.0" });
|
|
2539
|
+
}
|
|
2540
|
+
if (pathname === "/api/memories/stream" && req.method === "GET") {
|
|
2541
|
+
const stream = new ReadableStream({
|
|
2542
|
+
start(controller) {
|
|
2543
|
+
const encoder = new TextEncoder;
|
|
2544
|
+
let lastSeen = new Date().toISOString();
|
|
2545
|
+
const send = (data) => {
|
|
2546
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}
|
|
2547
|
+
|
|
2548
|
+
`));
|
|
2549
|
+
};
|
|
2550
|
+
send({ type: "connected", timestamp: lastSeen });
|
|
2551
|
+
const interval = setInterval(() => {
|
|
2552
|
+
try {
|
|
2553
|
+
const db = getDatabase();
|
|
2554
|
+
const rows = db.query("SELECT * FROM memories WHERE updated_at > ? OR created_at > ? ORDER BY updated_at DESC LIMIT 50").all(lastSeen, lastSeen);
|
|
2555
|
+
if (rows.length > 0) {
|
|
2556
|
+
lastSeen = new Date().toISOString();
|
|
2557
|
+
send({ type: "memories", data: rows, count: rows.length });
|
|
2558
|
+
}
|
|
2559
|
+
} catch {}
|
|
2560
|
+
}, 1000);
|
|
2561
|
+
req.signal.addEventListener("abort", () => {
|
|
2562
|
+
clearInterval(interval);
|
|
2563
|
+
controller.close();
|
|
2564
|
+
});
|
|
2565
|
+
}
|
|
2566
|
+
});
|
|
2567
|
+
return new Response(stream, {
|
|
2568
|
+
headers: {
|
|
2569
|
+
"Content-Type": "text/event-stream",
|
|
2570
|
+
"Cache-Control": "no-cache",
|
|
2571
|
+
Connection: "keep-alive",
|
|
2572
|
+
...CORS_HEADERS
|
|
2573
|
+
}
|
|
2574
|
+
});
|
|
2575
|
+
}
|
|
2576
|
+
const matched = matchRoute(req.method, pathname);
|
|
2577
|
+
if (!matched) {
|
|
2578
|
+
if (pathname.startsWith("/api/")) {
|
|
2579
|
+
return errorResponse("Not found", 404);
|
|
2580
|
+
}
|
|
2581
|
+
const dashDir = resolveDashboardDir();
|
|
2582
|
+
if (existsSync3(dashDir) && (req.method === "GET" || req.method === "HEAD")) {
|
|
2583
|
+
if (pathname !== "/") {
|
|
2584
|
+
const staticRes = serveStaticFile(join3(dashDir, pathname));
|
|
2585
|
+
if (staticRes)
|
|
2586
|
+
return staticRes;
|
|
2587
|
+
}
|
|
2588
|
+
const indexRes = serveStaticFile(join3(dashDir, "index.html"));
|
|
2589
|
+
if (indexRes)
|
|
2590
|
+
return indexRes;
|
|
2591
|
+
}
|
|
2592
|
+
return errorResponse("Not found", 404);
|
|
2593
|
+
}
|
|
2594
|
+
try {
|
|
2595
|
+
return await matched.handler(req, url, matched.params);
|
|
2596
|
+
} catch (e) {
|
|
2597
|
+
console.error(`[mementos-serve] ${req.method} ${pathname}:`, e);
|
|
2598
|
+
const message = e instanceof Error ? e.message : "Internal server error";
|
|
2599
|
+
return errorResponse(message, 500);
|
|
2600
|
+
}
|
|
2601
|
+
}
|
|
2602
|
+
});
|
|
2603
|
+
console.log(`Mementos server listening on http://localhost:${port}`);
|
|
2604
|
+
}
|
|
2605
|
+
async function main() {
|
|
2606
|
+
const requestedPort = parsePort();
|
|
2607
|
+
const port = await findFreePort(requestedPort);
|
|
2608
|
+
if (port !== requestedPort) {
|
|
2609
|
+
console.log(`Port ${requestedPort} in use, using ${port}`);
|
|
2610
|
+
}
|
|
2611
|
+
startServer(port);
|
|
2612
|
+
}
|
|
2613
|
+
main();
|
|
2614
|
+
export {
|
|
2615
|
+
startServer
|
|
2616
|
+
};
|