@hasna/mementos 0.4.41 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +2797 -1611
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/entities.d.ts.map +1 -1
- package/dist/db/memories.d.ts +1 -0
- package/dist/db/memories.d.ts.map +1 -1
- package/dist/db/relations.d.ts.map +1 -1
- package/dist/db/webhook_hooks.d.ts +25 -0
- package/dist/db/webhook_hooks.d.ts.map +1 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2187 -1331
- package/dist/lib/auto-memory-queue.d.ts +46 -0
- package/dist/lib/auto-memory-queue.d.ts.map +1 -0
- package/dist/lib/auto-memory.d.ts +18 -0
- package/dist/lib/auto-memory.d.ts.map +1 -0
- package/dist/lib/built-in-hooks.d.ts +12 -0
- package/dist/lib/built-in-hooks.d.ts.map +1 -0
- package/dist/lib/dedup.d.ts +33 -0
- package/dist/lib/dedup.d.ts.map +1 -0
- package/dist/lib/focus.d.ts +58 -0
- package/dist/lib/focus.d.ts.map +1 -0
- package/dist/lib/hooks.d.ts +50 -0
- package/dist/lib/hooks.d.ts.map +1 -0
- package/dist/lib/providers/anthropic.d.ts +21 -0
- package/dist/lib/providers/anthropic.d.ts.map +1 -0
- package/dist/lib/providers/base.d.ts +96 -0
- package/dist/lib/providers/base.d.ts.map +1 -0
- package/dist/lib/providers/cerebras.d.ts +20 -0
- package/dist/lib/providers/cerebras.d.ts.map +1 -0
- package/dist/lib/providers/grok.d.ts +19 -0
- package/dist/lib/providers/grok.d.ts.map +1 -0
- package/dist/lib/providers/index.d.ts +7 -0
- package/dist/lib/providers/index.d.ts.map +1 -0
- package/dist/lib/providers/openai-compat.d.ts +18 -0
- package/dist/lib/providers/openai-compat.d.ts.map +1 -0
- package/dist/lib/providers/openai.d.ts +20 -0
- package/dist/lib/providers/openai.d.ts.map +1 -0
- package/dist/lib/providers/registry.d.ts +38 -0
- package/dist/lib/providers/registry.d.ts.map +1 -0
- package/dist/lib/search.d.ts.map +1 -1
- package/dist/mcp/index.js +6851 -5544
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +2716 -1596
- package/dist/types/hooks.d.ts +136 -0
- package/dist/types/hooks.d.ts.map +1 -0
- package/dist/types/index.d.ts +7 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +2 -2
package/dist/server/index.js
CHANGED
|
@@ -16,62 +16,68 @@ var __toESM = (mod, isNodeMode, target) => {
|
|
|
16
16
|
});
|
|
17
17
|
return to;
|
|
18
18
|
};
|
|
19
|
+
var __export = (target, all) => {
|
|
20
|
+
for (var name in all)
|
|
21
|
+
__defProp(target, name, {
|
|
22
|
+
get: all[name],
|
|
23
|
+
enumerable: true,
|
|
24
|
+
configurable: true,
|
|
25
|
+
set: (newValue) => all[name] = () => newValue
|
|
26
|
+
});
|
|
27
|
+
};
|
|
28
|
+
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
19
29
|
var __require = import.meta.require;
|
|
20
30
|
|
|
21
|
-
// src/server/index.ts
|
|
22
|
-
import { existsSync as existsSync3 } from "fs";
|
|
23
|
-
import { dirname as dirname3, extname, join as join3, resolve as resolve3, sep } from "path";
|
|
24
|
-
import { fileURLToPath } from "url";
|
|
25
|
-
|
|
26
31
|
// src/types/index.ts
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
class DuplicateMemoryError extends Error {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
65
|
-
class VersionConflictError extends Error {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
32
|
+
var AgentConflictError, EntityNotFoundError, MemoryNotFoundError, DuplicateMemoryError, VersionConflictError;
|
|
33
|
+
var init_types = __esm(() => {
|
|
34
|
+
AgentConflictError = class AgentConflictError extends Error {
|
|
35
|
+
conflict = true;
|
|
36
|
+
existing_id;
|
|
37
|
+
existing_name;
|
|
38
|
+
last_seen_at;
|
|
39
|
+
session_hint;
|
|
40
|
+
working_dir;
|
|
41
|
+
constructor(opts) {
|
|
42
|
+
const msg = `Agent "${opts.existing_name}" is already active (session hint: ${opts.session_hint ?? "unknown"}, last seen ${opts.last_seen_at}). Wait 30 minutes or use a different name.`;
|
|
43
|
+
super(msg);
|
|
44
|
+
this.name = "AgentConflictError";
|
|
45
|
+
this.existing_id = opts.existing_id;
|
|
46
|
+
this.existing_name = opts.existing_name;
|
|
47
|
+
this.last_seen_at = opts.last_seen_at;
|
|
48
|
+
this.session_hint = opts.session_hint;
|
|
49
|
+
this.working_dir = opts.working_dir ?? null;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
EntityNotFoundError = class EntityNotFoundError extends Error {
|
|
53
|
+
constructor(id) {
|
|
54
|
+
super(`Entity not found: ${id}`);
|
|
55
|
+
this.name = "EntityNotFoundError";
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
MemoryNotFoundError = class MemoryNotFoundError extends Error {
|
|
59
|
+
constructor(id) {
|
|
60
|
+
super(`Memory not found: ${id}`);
|
|
61
|
+
this.name = "MemoryNotFoundError";
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
DuplicateMemoryError = class DuplicateMemoryError extends Error {
|
|
65
|
+
constructor(key, scope) {
|
|
66
|
+
super(`Memory already exists with key "${key}" in scope "${scope}"`);
|
|
67
|
+
this.name = "DuplicateMemoryError";
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
VersionConflictError = class VersionConflictError extends Error {
|
|
71
|
+
expected;
|
|
72
|
+
actual;
|
|
73
|
+
constructor(id, expected, actual) {
|
|
74
|
+
super(`Version conflict for memory ${id}: expected ${expected}, got ${actual}`);
|
|
75
|
+
this.name = "VersionConflictError";
|
|
76
|
+
this.expected = expected;
|
|
77
|
+
this.actual = actual;
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
});
|
|
75
81
|
|
|
76
82
|
// src/db/database.ts
|
|
77
83
|
import { Database } from "bun:sqlite";
|
|
@@ -130,8 +136,48 @@ function ensureDir(filePath) {
|
|
|
130
136
|
mkdirSync(dir, { recursive: true });
|
|
131
137
|
}
|
|
132
138
|
}
|
|
133
|
-
|
|
134
|
-
|
|
139
|
+
function getDatabase(dbPath) {
|
|
140
|
+
if (_db)
|
|
141
|
+
return _db;
|
|
142
|
+
const path = dbPath || getDbPath();
|
|
143
|
+
ensureDir(path);
|
|
144
|
+
_db = new Database(path, { create: true });
|
|
145
|
+
_db.run("PRAGMA journal_mode = WAL");
|
|
146
|
+
_db.run("PRAGMA busy_timeout = 5000");
|
|
147
|
+
_db.run("PRAGMA foreign_keys = ON");
|
|
148
|
+
runMigrations(_db);
|
|
149
|
+
return _db;
|
|
150
|
+
}
|
|
151
|
+
function runMigrations(db) {
|
|
152
|
+
try {
|
|
153
|
+
const result = db.query("SELECT MAX(id) as max_id FROM _migrations").get();
|
|
154
|
+
const currentLevel = result?.max_id ?? 0;
|
|
155
|
+
for (let i = currentLevel;i < MIGRATIONS.length; i++) {
|
|
156
|
+
try {
|
|
157
|
+
db.exec(MIGRATIONS[i]);
|
|
158
|
+
} catch {}
|
|
159
|
+
}
|
|
160
|
+
} catch {
|
|
161
|
+
for (const migration of MIGRATIONS) {
|
|
162
|
+
try {
|
|
163
|
+
db.exec(migration);
|
|
164
|
+
} catch {}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function now() {
|
|
169
|
+
return new Date().toISOString();
|
|
170
|
+
}
|
|
171
|
+
function uuid() {
|
|
172
|
+
return crypto.randomUUID();
|
|
173
|
+
}
|
|
174
|
+
function shortUuid() {
|
|
175
|
+
return crypto.randomUUID().slice(0, 8);
|
|
176
|
+
}
|
|
177
|
+
var MIGRATIONS, _db = null;
|
|
178
|
+
var init_database = __esm(() => {
|
|
179
|
+
MIGRATIONS = [
|
|
180
|
+
`
|
|
135
181
|
CREATE TABLE IF NOT EXISTS projects (
|
|
136
182
|
id TEXT PRIMARY KEY,
|
|
137
183
|
name TEXT NOT NULL,
|
|
@@ -218,7 +264,7 @@ var MIGRATIONS = [
|
|
|
218
264
|
|
|
219
265
|
INSERT OR IGNORE INTO _migrations (id) VALUES (1);
|
|
220
266
|
`,
|
|
221
|
-
|
|
267
|
+
`
|
|
222
268
|
CREATE TABLE IF NOT EXISTS memory_versions (
|
|
223
269
|
id TEXT PRIMARY KEY,
|
|
224
270
|
memory_id TEXT NOT NULL,
|
|
@@ -240,7 +286,7 @@ var MIGRATIONS = [
|
|
|
240
286
|
|
|
241
287
|
INSERT OR IGNORE INTO _migrations (id) VALUES (2);
|
|
242
288
|
`,
|
|
243
|
-
|
|
289
|
+
`
|
|
244
290
|
CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
|
|
245
291
|
key, value, summary,
|
|
246
292
|
content='memories',
|
|
@@ -264,7 +310,7 @@ var MIGRATIONS = [
|
|
|
264
310
|
|
|
265
311
|
INSERT OR IGNORE INTO _migrations (id) VALUES (3);
|
|
266
312
|
`,
|
|
267
|
-
|
|
313
|
+
`
|
|
268
314
|
CREATE TABLE IF NOT EXISTS search_history (
|
|
269
315
|
id TEXT PRIMARY KEY,
|
|
270
316
|
query TEXT NOT NULL,
|
|
@@ -278,7 +324,7 @@ var MIGRATIONS = [
|
|
|
278
324
|
|
|
279
325
|
INSERT OR IGNORE INTO _migrations (id) VALUES (4);
|
|
280
326
|
`,
|
|
281
|
-
|
|
327
|
+
`
|
|
282
328
|
CREATE TABLE IF NOT EXISTS entities (
|
|
283
329
|
id TEXT PRIMARY KEY,
|
|
284
330
|
name TEXT NOT NULL,
|
|
@@ -325,17 +371,17 @@ var MIGRATIONS = [
|
|
|
325
371
|
|
|
326
372
|
INSERT OR IGNORE INTO _migrations (id) VALUES (5);
|
|
327
373
|
`,
|
|
328
|
-
|
|
374
|
+
`
|
|
329
375
|
ALTER TABLE agents ADD COLUMN active_project_id TEXT REFERENCES projects(id) ON DELETE SET NULL;
|
|
330
376
|
CREATE INDEX IF NOT EXISTS idx_agents_active_project ON agents(active_project_id);
|
|
331
377
|
INSERT OR IGNORE INTO _migrations (id) VALUES (6);
|
|
332
378
|
`,
|
|
333
|
-
|
|
379
|
+
`
|
|
334
380
|
ALTER TABLE agents ADD COLUMN session_id TEXT;
|
|
335
381
|
CREATE INDEX IF NOT EXISTS idx_agents_session ON agents(session_id);
|
|
336
382
|
INSERT OR IGNORE INTO _migrations (id) VALUES (7);
|
|
337
383
|
`,
|
|
338
|
-
|
|
384
|
+
`
|
|
339
385
|
CREATE TABLE IF NOT EXISTS resource_locks (
|
|
340
386
|
id TEXT PRIMARY KEY,
|
|
341
387
|
resource_type TEXT NOT NULL CHECK(resource_type IN ('project', 'memory', 'entity', 'agent', 'connector')),
|
|
@@ -351,67 +397,35 @@ var MIGRATIONS = [
|
|
|
351
397
|
CREATE INDEX IF NOT EXISTS idx_resource_locks_agent ON resource_locks(agent_id);
|
|
352
398
|
CREATE INDEX IF NOT EXISTS idx_resource_locks_expires ON resource_locks(expires_at);
|
|
353
399
|
INSERT OR IGNORE INTO _migrations (id) VALUES (8);
|
|
400
|
+
`,
|
|
401
|
+
`
|
|
402
|
+
ALTER TABLE memories ADD COLUMN recall_count INTEGER NOT NULL DEFAULT 0;
|
|
403
|
+
CREATE INDEX IF NOT EXISTS idx_memories_recall_count ON memories(recall_count DESC);
|
|
404
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (9);
|
|
405
|
+
`,
|
|
406
|
+
`
|
|
407
|
+
CREATE TABLE IF NOT EXISTS webhook_hooks (
|
|
408
|
+
id TEXT PRIMARY KEY,
|
|
409
|
+
type TEXT NOT NULL,
|
|
410
|
+
handler_url TEXT NOT NULL,
|
|
411
|
+
priority INTEGER NOT NULL DEFAULT 50,
|
|
412
|
+
blocking INTEGER NOT NULL DEFAULT 0,
|
|
413
|
+
agent_id TEXT,
|
|
414
|
+
project_id TEXT,
|
|
415
|
+
description TEXT,
|
|
416
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
417
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
418
|
+
invocation_count INTEGER NOT NULL DEFAULT 0,
|
|
419
|
+
failure_count INTEGER NOT NULL DEFAULT 0
|
|
420
|
+
);
|
|
421
|
+
CREATE INDEX IF NOT EXISTS idx_webhook_hooks_type ON webhook_hooks(type);
|
|
422
|
+
CREATE INDEX IF NOT EXISTS idx_webhook_hooks_enabled ON webhook_hooks(enabled);
|
|
423
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (10);
|
|
354
424
|
`
|
|
355
|
-
];
|
|
356
|
-
|
|
357
|
-
function getDatabase(dbPath) {
|
|
358
|
-
if (_db)
|
|
359
|
-
return _db;
|
|
360
|
-
const path = dbPath || getDbPath();
|
|
361
|
-
ensureDir(path);
|
|
362
|
-
_db = new Database(path, { create: true });
|
|
363
|
-
_db.run("PRAGMA journal_mode = WAL");
|
|
364
|
-
_db.run("PRAGMA busy_timeout = 5000");
|
|
365
|
-
_db.run("PRAGMA foreign_keys = ON");
|
|
366
|
-
runMigrations(_db);
|
|
367
|
-
return _db;
|
|
368
|
-
}
|
|
369
|
-
function runMigrations(db) {
|
|
370
|
-
try {
|
|
371
|
-
const result = db.query("SELECT MAX(id) as max_id FROM _migrations").get();
|
|
372
|
-
const currentLevel = result?.max_id ?? 0;
|
|
373
|
-
for (let i = currentLevel;i < MIGRATIONS.length; i++) {
|
|
374
|
-
try {
|
|
375
|
-
db.exec(MIGRATIONS[i]);
|
|
376
|
-
} catch {}
|
|
377
|
-
}
|
|
378
|
-
} catch {
|
|
379
|
-
for (const migration of MIGRATIONS) {
|
|
380
|
-
try {
|
|
381
|
-
db.exec(migration);
|
|
382
|
-
} catch {}
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
function now() {
|
|
387
|
-
return new Date().toISOString();
|
|
388
|
-
}
|
|
389
|
-
function uuid() {
|
|
390
|
-
return crypto.randomUUID();
|
|
391
|
-
}
|
|
392
|
-
function shortUuid() {
|
|
393
|
-
return crypto.randomUUID().slice(0, 8);
|
|
394
|
-
}
|
|
425
|
+
];
|
|
426
|
+
});
|
|
395
427
|
|
|
396
428
|
// src/lib/redact.ts
|
|
397
|
-
var REDACTED = "[REDACTED]";
|
|
398
|
-
var SECRET_PATTERNS = [
|
|
399
|
-
{ name: "openai_key", pattern: /sk-[a-zA-Z0-9_-]{20,}/g },
|
|
400
|
-
{ name: "anthropic_key", pattern: /sk-ant-[a-zA-Z0-9_-]{20,}/g },
|
|
401
|
-
{ name: "generic_key", pattern: /(?:pk|tok|key|token|api[_-]?key)[_-][a-zA-Z0-9_-]{16,}/gi },
|
|
402
|
-
{ name: "aws_key", pattern: /AKIA[A-Z0-9]{16}/g },
|
|
403
|
-
{ name: "aws_secret", pattern: /(?<=AWS_SECRET_ACCESS_KEY\s*=\s*)[A-Za-z0-9/+=]{40}/g },
|
|
404
|
-
{ name: "github_token", pattern: /gh[ps]_[a-zA-Z0-9]{36,}/g },
|
|
405
|
-
{ name: "github_oauth", pattern: /gho_[a-zA-Z0-9]{36,}/g },
|
|
406
|
-
{ name: "npm_token", pattern: /npm_[a-zA-Z0-9]{36,}/g },
|
|
407
|
-
{ name: "bearer", pattern: /Bearer\s+[a-zA-Z0-9_\-.]{20,}/g },
|
|
408
|
-
{ name: "conn_string", pattern: /(?:postgres|postgresql|mysql|mongodb|redis|amqp|mqtt):\/\/[^\s"'`]+@[^\s"'`]+/gi },
|
|
409
|
-
{ name: "env_secret", pattern: /(?:SECRET|TOKEN|PASSWORD|PASSPHRASE|API_KEY|PRIVATE_KEY|AUTH|CREDENTIAL)[_A-Z]*\s*=\s*["']?[^\s"'\n]{8,}["']?/gi },
|
|
410
|
-
{ name: "stripe_key", pattern: /(?:sk|pk|rk)_(?:test|live)_[a-zA-Z0-9]{20,}/g },
|
|
411
|
-
{ name: "slack_token", pattern: /xox[bpras]-[a-zA-Z0-9-]{20,}/g },
|
|
412
|
-
{ name: "jwt", pattern: /eyJ[a-zA-Z0-9_-]{10,}\.eyJ[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,}/g },
|
|
413
|
-
{ name: "hex_secret", pattern: /(?<=(?:key|token|secret|password|hash)\s*[:=]\s*["']?)[0-9a-f]{32,}(?=["']?)/gi }
|
|
414
|
-
];
|
|
415
429
|
function redactSecrets(text) {
|
|
416
430
|
let result = text;
|
|
417
431
|
for (const { pattern } of SECRET_PATTERNS) {
|
|
@@ -420,492 +434,617 @@ function redactSecrets(text) {
|
|
|
420
434
|
}
|
|
421
435
|
return result;
|
|
422
436
|
}
|
|
437
|
+
var REDACTED = "[REDACTED]", SECRET_PATTERNS;
|
|
438
|
+
var init_redact = __esm(() => {
|
|
439
|
+
SECRET_PATTERNS = [
|
|
440
|
+
{ name: "openai_key", pattern: /sk-[a-zA-Z0-9_-]{20,}/g },
|
|
441
|
+
{ name: "anthropic_key", pattern: /sk-ant-[a-zA-Z0-9_-]{20,}/g },
|
|
442
|
+
{ name: "generic_key", pattern: /(?:pk|tok|key|token|api[_-]?key)[_-][a-zA-Z0-9_-]{16,}/gi },
|
|
443
|
+
{ name: "aws_key", pattern: /AKIA[A-Z0-9]{16}/g },
|
|
444
|
+
{ name: "aws_secret", pattern: /(?<=AWS_SECRET_ACCESS_KEY\s*=\s*)[A-Za-z0-9/+=]{40}/g },
|
|
445
|
+
{ name: "github_token", pattern: /gh[ps]_[a-zA-Z0-9]{36,}/g },
|
|
446
|
+
{ name: "github_oauth", pattern: /gho_[a-zA-Z0-9]{36,}/g },
|
|
447
|
+
{ name: "npm_token", pattern: /npm_[a-zA-Z0-9]{36,}/g },
|
|
448
|
+
{ name: "bearer", pattern: /Bearer\s+[a-zA-Z0-9_\-.]{20,}/g },
|
|
449
|
+
{ name: "conn_string", pattern: /(?:postgres|postgresql|mysql|mongodb|redis|amqp|mqtt):\/\/[^\s"'`]+@[^\s"'`]+/gi },
|
|
450
|
+
{ name: "env_secret", pattern: /(?:SECRET|TOKEN|PASSWORD|PASSPHRASE|API_KEY|PRIVATE_KEY|AUTH|CREDENTIAL)[_A-Z]*\s*=\s*["']?[^\s"'\n]{8,}["']?/gi },
|
|
451
|
+
{ name: "stripe_key", pattern: /(?:sk|pk|rk)_(?:test|live)_[a-zA-Z0-9]{20,}/g },
|
|
452
|
+
{ name: "slack_token", pattern: /xox[bpras]-[a-zA-Z0-9-]{20,}/g },
|
|
453
|
+
{ name: "jwt", pattern: /eyJ[a-zA-Z0-9_-]{10,}\.eyJ[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,}/g },
|
|
454
|
+
{ name: "hex_secret", pattern: /(?<=(?:key|token|secret|password|hash)\s*[:=]\s*["']?)[0-9a-f]{32,}(?=["']?)/gi }
|
|
455
|
+
];
|
|
456
|
+
});
|
|
423
457
|
|
|
424
|
-
// src/
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
return {
|
|
428
|
-
id: row["id"],
|
|
429
|
-
name: row["name"],
|
|
430
|
-
session_id: row["session_id"] || null,
|
|
431
|
-
description: row["description"] || null,
|
|
432
|
-
role: row["role"] || null,
|
|
433
|
-
metadata: JSON.parse(row["metadata"] || "{}"),
|
|
434
|
-
active_project_id: row["active_project_id"] || null,
|
|
435
|
-
created_at: row["created_at"],
|
|
436
|
-
last_seen_at: row["last_seen_at"]
|
|
437
|
-
};
|
|
458
|
+
// src/lib/hooks.ts
|
|
459
|
+
function generateHookId() {
|
|
460
|
+
return `hook_${++_idCounter}_${Date.now().toString(36)}`;
|
|
438
461
|
}
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
462
|
+
|
|
463
|
+
class HookRegistry {
|
|
464
|
+
hooks = new Map;
|
|
465
|
+
register(reg) {
|
|
466
|
+
const id = generateHookId();
|
|
467
|
+
const hook = {
|
|
468
|
+
...reg,
|
|
469
|
+
id,
|
|
470
|
+
priority: reg.priority ?? 50
|
|
471
|
+
};
|
|
472
|
+
this.hooks.set(id, hook);
|
|
473
|
+
return id;
|
|
474
|
+
}
|
|
475
|
+
unregister(hookId) {
|
|
476
|
+
const hook = this.hooks.get(hookId);
|
|
477
|
+
if (!hook)
|
|
478
|
+
return false;
|
|
479
|
+
if (hook.builtin)
|
|
480
|
+
return false;
|
|
481
|
+
this.hooks.delete(hookId);
|
|
482
|
+
return true;
|
|
483
|
+
}
|
|
484
|
+
list(type) {
|
|
485
|
+
const all = [...this.hooks.values()];
|
|
486
|
+
if (!type)
|
|
487
|
+
return all;
|
|
488
|
+
return all.filter((h) => h.type === type);
|
|
489
|
+
}
|
|
490
|
+
async runHooks(type, context) {
|
|
491
|
+
const matching = this.getMatchingHooks(type, context);
|
|
492
|
+
if (matching.length === 0)
|
|
493
|
+
return true;
|
|
494
|
+
matching.sort((a, b) => a.priority - b.priority);
|
|
495
|
+
for (const hook of matching) {
|
|
496
|
+
if (hook.blocking) {
|
|
497
|
+
try {
|
|
498
|
+
const result = await hook.handler(context);
|
|
499
|
+
if (result === false)
|
|
500
|
+
return false;
|
|
501
|
+
} catch (err) {
|
|
502
|
+
console.error(`[hooks] blocking hook ${hook.id} (${type}) threw:`, err);
|
|
503
|
+
}
|
|
504
|
+
} else {
|
|
505
|
+
Promise.resolve().then(() => hook.handler(context)).catch((err) => console.error(`[hooks] non-blocking hook ${hook.id} (${type}) threw:`, err));
|
|
459
506
|
}
|
|
460
507
|
}
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
])
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
508
|
+
return true;
|
|
509
|
+
}
|
|
510
|
+
getMatchingHooks(type, context) {
|
|
511
|
+
const ctx = context;
|
|
512
|
+
return [...this.hooks.values()].filter((hook) => {
|
|
513
|
+
if (hook.type !== type)
|
|
514
|
+
return false;
|
|
515
|
+
if (hook.agentId && hook.agentId !== ctx.agentId)
|
|
516
|
+
return false;
|
|
517
|
+
if (hook.projectId && hook.projectId !== ctx.projectId)
|
|
518
|
+
return false;
|
|
519
|
+
return true;
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
stats() {
|
|
523
|
+
const all = [...this.hooks.values()];
|
|
524
|
+
const byType = {};
|
|
525
|
+
for (const hook of all) {
|
|
526
|
+
byType[hook.type] = (byType[hook.type] ?? 0) + 1;
|
|
474
527
|
}
|
|
475
|
-
return
|
|
528
|
+
return {
|
|
529
|
+
total: all.length,
|
|
530
|
+
byType,
|
|
531
|
+
blocking: all.filter((h) => h.blocking).length,
|
|
532
|
+
nonBlocking: all.filter((h) => !h.blocking).length
|
|
533
|
+
};
|
|
476
534
|
}
|
|
477
|
-
const id = shortUuid();
|
|
478
|
-
d.run("INSERT INTO agents (id, name, session_id, description, role, active_project_id, created_at, last_seen_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", [id, normalizedName, sessionId ?? null, description || null, role || "agent", projectId ?? null, timestamp, timestamp]);
|
|
479
|
-
return getAgent(id, d);
|
|
480
535
|
}
|
|
481
|
-
|
|
536
|
+
var _idCounter = 0, hookRegistry;
|
|
537
|
+
var init_hooks = __esm(() => {
|
|
538
|
+
hookRegistry = new HookRegistry;
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
// src/db/entity-memories.ts
|
|
542
|
+
function parseEntityMemoryRow(row) {
|
|
543
|
+
return {
|
|
544
|
+
entity_id: row["entity_id"],
|
|
545
|
+
memory_id: row["memory_id"],
|
|
546
|
+
role: row["role"],
|
|
547
|
+
created_at: row["created_at"]
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
function linkEntityToMemory(entityId, memoryId, role = "context", db) {
|
|
482
551
|
const d = db || getDatabase();
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
row = d.query("SELECT * FROM
|
|
487
|
-
|
|
488
|
-
return parseAgentRow(row);
|
|
489
|
-
const rows = d.query("SELECT * FROM agents WHERE id LIKE ?").all(`${idOrName}%`);
|
|
490
|
-
if (rows.length === 1)
|
|
491
|
-
return parseAgentRow(rows[0]);
|
|
492
|
-
return null;
|
|
552
|
+
const timestamp = now();
|
|
553
|
+
d.run(`INSERT OR IGNORE INTO entity_memories (entity_id, memory_id, role, created_at)
|
|
554
|
+
VALUES (?, ?, ?, ?)`, [entityId, memoryId, role, timestamp]);
|
|
555
|
+
const row = d.query("SELECT * FROM entity_memories WHERE entity_id = ? AND memory_id = ?").get(entityId, memoryId);
|
|
556
|
+
return parseEntityMemoryRow(row);
|
|
493
557
|
}
|
|
494
|
-
function
|
|
558
|
+
function unlinkEntityFromMemory(entityId, memoryId, db) {
|
|
495
559
|
const d = db || getDatabase();
|
|
496
|
-
|
|
497
|
-
return rows.map(parseAgentRow);
|
|
560
|
+
d.run("DELETE FROM entity_memories WHERE entity_id = ? AND memory_id = ?", [entityId, memoryId]);
|
|
498
561
|
}
|
|
499
|
-
function
|
|
562
|
+
function getMemoriesForEntity(entityId, db) {
|
|
500
563
|
const d = db || getDatabase();
|
|
501
|
-
const rows = d.query(
|
|
502
|
-
|
|
564
|
+
const rows = d.query(`SELECT m.* FROM memories m
|
|
565
|
+
INNER JOIN entity_memories em ON em.memory_id = m.id
|
|
566
|
+
WHERE em.entity_id = ?
|
|
567
|
+
ORDER BY m.importance DESC, m.created_at DESC`).all(entityId);
|
|
568
|
+
return rows.map(parseMemoryRow);
|
|
503
569
|
}
|
|
504
|
-
function
|
|
570
|
+
function getEntityMemoryLinks(entityId, memoryId, db) {
|
|
505
571
|
const d = db || getDatabase();
|
|
506
|
-
const
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
const normalizedNewName = updates.name.trim().toLowerCase();
|
|
512
|
-
if (normalizedNewName !== agent.name) {
|
|
513
|
-
const existing = d.query("SELECT id FROM agents WHERE LOWER(name) = ? AND id != ?").get(normalizedNewName, agent.id);
|
|
514
|
-
if (existing) {
|
|
515
|
-
throw new Error(`Agent name already taken: ${normalizedNewName}`);
|
|
516
|
-
}
|
|
517
|
-
d.run("UPDATE agents SET name = ? WHERE id = ?", [normalizedNewName, agent.id]);
|
|
518
|
-
}
|
|
572
|
+
const conditions = [];
|
|
573
|
+
const params = [];
|
|
574
|
+
if (entityId) {
|
|
575
|
+
conditions.push("entity_id = ?");
|
|
576
|
+
params.push(entityId);
|
|
519
577
|
}
|
|
520
|
-
if (
|
|
521
|
-
|
|
578
|
+
if (memoryId) {
|
|
579
|
+
conditions.push("memory_id = ?");
|
|
580
|
+
params.push(memoryId);
|
|
522
581
|
}
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
if (updates.metadata !== undefined) {
|
|
527
|
-
d.run("UPDATE agents SET metadata = ? WHERE id = ?", [JSON.stringify(updates.metadata), agent.id]);
|
|
528
|
-
}
|
|
529
|
-
if ("active_project_id" in updates) {
|
|
530
|
-
d.run("UPDATE agents SET active_project_id = ? WHERE id = ?", [updates.active_project_id ?? null, agent.id]);
|
|
582
|
+
let sql = "SELECT * FROM entity_memories";
|
|
583
|
+
if (conditions.length > 0) {
|
|
584
|
+
sql += ` WHERE ${conditions.join(" AND ")}`;
|
|
531
585
|
}
|
|
532
|
-
|
|
533
|
-
|
|
586
|
+
sql += " ORDER BY created_at DESC";
|
|
587
|
+
const rows = d.query(sql).all(...params);
|
|
588
|
+
return rows.map(parseEntityMemoryRow);
|
|
534
589
|
}
|
|
590
|
+
var init_entity_memories = __esm(() => {
|
|
591
|
+
init_database();
|
|
592
|
+
init_memories();
|
|
593
|
+
});
|
|
535
594
|
|
|
536
|
-
// src/db/
|
|
537
|
-
|
|
595
|
+
// src/db/memories.ts
|
|
596
|
+
var exports_memories = {};
|
|
597
|
+
__export(exports_memories, {
|
|
598
|
+
updateMemory: () => updateMemory,
|
|
599
|
+
touchMemory: () => touchMemory,
|
|
600
|
+
parseMemoryRow: () => parseMemoryRow,
|
|
601
|
+
listMemories: () => listMemories,
|
|
602
|
+
incrementRecallCount: () => incrementRecallCount,
|
|
603
|
+
getMemoryVersions: () => getMemoryVersions,
|
|
604
|
+
getMemoryByKey: () => getMemoryByKey,
|
|
605
|
+
getMemory: () => getMemory,
|
|
606
|
+
getMemoriesByKey: () => getMemoriesByKey,
|
|
607
|
+
deleteMemory: () => deleteMemory,
|
|
608
|
+
createMemory: () => createMemory,
|
|
609
|
+
cleanExpiredMemories: () => cleanExpiredMemories,
|
|
610
|
+
bulkDeleteMemories: () => bulkDeleteMemories
|
|
611
|
+
});
|
|
612
|
+
function runEntityExtraction(_memory, _projectId, _d) {}
|
|
613
|
+
function parseMemoryRow(row) {
|
|
538
614
|
return {
|
|
539
615
|
id: row["id"],
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
616
|
+
key: row["key"],
|
|
617
|
+
value: row["value"],
|
|
618
|
+
category: row["category"],
|
|
619
|
+
scope: row["scope"],
|
|
620
|
+
summary: row["summary"] || null,
|
|
621
|
+
tags: JSON.parse(row["tags"] || "[]"),
|
|
622
|
+
importance: row["importance"],
|
|
623
|
+
source: row["source"],
|
|
624
|
+
status: row["status"],
|
|
625
|
+
pinned: !!row["pinned"],
|
|
626
|
+
agent_id: row["agent_id"] || null,
|
|
627
|
+
project_id: row["project_id"] || null,
|
|
628
|
+
session_id: row["session_id"] || null,
|
|
629
|
+
metadata: JSON.parse(row["metadata"] || "{}"),
|
|
630
|
+
access_count: row["access_count"],
|
|
631
|
+
version: row["version"],
|
|
632
|
+
expires_at: row["expires_at"] || null,
|
|
544
633
|
created_at: row["created_at"],
|
|
545
|
-
updated_at: row["updated_at"]
|
|
634
|
+
updated_at: row["updated_at"],
|
|
635
|
+
accessed_at: row["accessed_at"] || null
|
|
546
636
|
};
|
|
547
637
|
}
|
|
548
|
-
function
|
|
638
|
+
function createMemory(input, dedupeMode = "merge", db) {
|
|
549
639
|
const d = db || getDatabase();
|
|
550
640
|
const timestamp = now();
|
|
551
|
-
|
|
552
|
-
if (
|
|
553
|
-
|
|
554
|
-
d.run("UPDATE projects SET updated_at = ? WHERE id = ?", [
|
|
555
|
-
timestamp,
|
|
556
|
-
existingId
|
|
557
|
-
]);
|
|
558
|
-
return parseProjectRow(existing);
|
|
641
|
+
let expiresAt = input.expires_at || null;
|
|
642
|
+
if (input.ttl_ms && !expiresAt) {
|
|
643
|
+
expiresAt = new Date(Date.now() + input.ttl_ms).toISOString();
|
|
559
644
|
}
|
|
560
645
|
const id = uuid();
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
}
|
|
564
|
-
function getProject(idOrPath, db) {
|
|
565
|
-
const d = db || getDatabase();
|
|
566
|
-
let row = d.query("SELECT * FROM projects WHERE id = ?").get(idOrPath);
|
|
567
|
-
if (row)
|
|
568
|
-
return parseProjectRow(row);
|
|
569
|
-
row = d.query("SELECT * FROM projects WHERE path = ?").get(idOrPath);
|
|
570
|
-
if (row)
|
|
571
|
-
return parseProjectRow(row);
|
|
572
|
-
row = d.query("SELECT * FROM projects WHERE LOWER(name) = ?").get(idOrPath.toLowerCase());
|
|
573
|
-
if (row)
|
|
574
|
-
return parseProjectRow(row);
|
|
575
|
-
return null;
|
|
576
|
-
}
|
|
577
|
-
function listProjects(db) {
|
|
578
|
-
const d = db || getDatabase();
|
|
579
|
-
const rows = d.query("SELECT * FROM projects ORDER BY updated_at DESC").all();
|
|
580
|
-
return rows.map(parseProjectRow);
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
// src/lib/extractor.ts
|
|
584
|
-
var TECH_KEYWORDS = new Set([
|
|
585
|
-
"typescript",
|
|
586
|
-
"javascript",
|
|
587
|
-
"python",
|
|
588
|
-
"rust",
|
|
589
|
-
"go",
|
|
590
|
-
"java",
|
|
591
|
-
"ruby",
|
|
592
|
-
"swift",
|
|
593
|
-
"kotlin",
|
|
594
|
-
"react",
|
|
595
|
-
"vue",
|
|
596
|
-
"angular",
|
|
597
|
-
"svelte",
|
|
598
|
-
"nextjs",
|
|
599
|
-
"bun",
|
|
600
|
-
"node",
|
|
601
|
-
"deno",
|
|
602
|
-
"sqlite",
|
|
603
|
-
"postgres",
|
|
604
|
-
"mysql",
|
|
605
|
-
"redis",
|
|
606
|
-
"docker",
|
|
607
|
-
"kubernetes",
|
|
608
|
-
"git",
|
|
609
|
-
"npm",
|
|
610
|
-
"yarn",
|
|
611
|
-
"pnpm",
|
|
612
|
-
"webpack",
|
|
613
|
-
"vite",
|
|
614
|
-
"tailwind",
|
|
615
|
-
"prisma",
|
|
616
|
-
"drizzle",
|
|
617
|
-
"zod",
|
|
618
|
-
"commander",
|
|
619
|
-
"express",
|
|
620
|
-
"fastify",
|
|
621
|
-
"hono"
|
|
622
|
-
]);
|
|
623
|
-
var FILE_PATH_RE = /(?:^|\s)((?:\/|\.\/|~\/)?(?:[\w.-]+\/)+[\w.-]+\.\w+)/g;
|
|
624
|
-
var URL_RE = /https?:\/\/[^\s)]+/g;
|
|
625
|
-
var NPM_PACKAGE_RE = /@[\w-]+\/[\w.-]+/g;
|
|
626
|
-
var PASCAL_CASE_RE = /\b([A-Z][a-z]+(?:[A-Z][a-z]+)+)\b/g;
|
|
627
|
-
function getSearchText(memory) {
|
|
628
|
-
const parts = [memory.key, memory.value];
|
|
629
|
-
if (memory.summary)
|
|
630
|
-
parts.push(memory.summary);
|
|
631
|
-
return parts.join(" ");
|
|
632
|
-
}
|
|
633
|
-
function extractEntities(memory, db) {
|
|
634
|
-
const text = getSearchText(memory);
|
|
635
|
-
const entityMap = new Map;
|
|
636
|
-
function add(name, type, confidence) {
|
|
637
|
-
const normalized = name.toLowerCase();
|
|
638
|
-
if (normalized.length < 3)
|
|
639
|
-
return;
|
|
640
|
-
const existing = entityMap.get(normalized);
|
|
641
|
-
if (!existing || existing.confidence < confidence) {
|
|
642
|
-
entityMap.set(normalized, { name: normalized, type, confidence });
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
for (const match of text.matchAll(FILE_PATH_RE)) {
|
|
646
|
-
add(match[1].trim(), "file", 0.9);
|
|
647
|
-
}
|
|
648
|
-
for (const match of text.matchAll(URL_RE)) {
|
|
649
|
-
add(match[0], "api", 0.8);
|
|
650
|
-
}
|
|
651
|
-
for (const match of text.matchAll(NPM_PACKAGE_RE)) {
|
|
652
|
-
add(match[0], "tool", 0.85);
|
|
653
|
-
}
|
|
654
|
-
try {
|
|
655
|
-
const d = db || getDatabase();
|
|
656
|
-
const agents = listAgents(d);
|
|
657
|
-
const textLower2 = text.toLowerCase();
|
|
658
|
-
for (const agent of agents) {
|
|
659
|
-
const nameLower = agent.name.toLowerCase();
|
|
660
|
-
if (nameLower.length >= 3 && textLower2.includes(nameLower)) {
|
|
661
|
-
add(agent.name, "person", 0.95);
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
} catch {}
|
|
665
|
-
try {
|
|
666
|
-
const d = db || getDatabase();
|
|
667
|
-
const projects = listProjects(d);
|
|
668
|
-
const textLower2 = text.toLowerCase();
|
|
669
|
-
for (const project of projects) {
|
|
670
|
-
const nameLower = project.name.toLowerCase();
|
|
671
|
-
if (nameLower.length >= 3 && textLower2.includes(nameLower)) {
|
|
672
|
-
add(project.name, "project", 0.95);
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
} catch {}
|
|
676
|
-
const textLower = text.toLowerCase();
|
|
677
|
-
for (const keyword of TECH_KEYWORDS) {
|
|
678
|
-
const re = new RegExp(`\\b${keyword}\\b`, "i");
|
|
679
|
-
if (re.test(textLower)) {
|
|
680
|
-
add(keyword, "tool", 0.7);
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
for (const match of text.matchAll(PASCAL_CASE_RE)) {
|
|
684
|
-
add(match[1], "concept", 0.5);
|
|
685
|
-
}
|
|
686
|
-
return Array.from(entityMap.values()).sort((a, b) => b.confidence - a.confidence);
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
// src/db/entities.ts
|
|
690
|
-
function parseEntityRow(row) {
|
|
691
|
-
return {
|
|
692
|
-
id: row["id"],
|
|
693
|
-
name: row["name"],
|
|
694
|
-
type: row["type"],
|
|
695
|
-
description: row["description"] || null,
|
|
696
|
-
metadata: JSON.parse(row["metadata"] || "{}"),
|
|
697
|
-
project_id: row["project_id"] || null,
|
|
698
|
-
created_at: row["created_at"],
|
|
699
|
-
updated_at: row["updated_at"]
|
|
700
|
-
};
|
|
701
|
-
}
|
|
702
|
-
function createEntity(input, db) {
|
|
703
|
-
const d = db || getDatabase();
|
|
704
|
-
const timestamp = now();
|
|
646
|
+
const tags = input.tags || [];
|
|
647
|
+
const tagsJson = JSON.stringify(tags);
|
|
705
648
|
const metadataJson = JSON.stringify(input.metadata || {});
|
|
706
|
-
const
|
|
707
|
-
|
|
708
|
-
if (
|
|
709
|
-
const
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
649
|
+
const safeValue = redactSecrets(input.value);
|
|
650
|
+
const safeSummary = input.summary ? redactSecrets(input.summary) : null;
|
|
651
|
+
if (dedupeMode === "merge") {
|
|
652
|
+
const existing = d.query(`SELECT id, version FROM memories
|
|
653
|
+
WHERE key = ? AND scope = ?
|
|
654
|
+
AND COALESCE(agent_id, '') = ?
|
|
655
|
+
AND COALESCE(project_id, '') = ?
|
|
656
|
+
AND COALESCE(session_id, '') = ?`).get(input.key, input.scope || "private", input.agent_id || "", input.project_id || "", input.session_id || "");
|
|
657
|
+
if (existing) {
|
|
658
|
+
d.run(`UPDATE memories SET
|
|
659
|
+
value = ?, category = ?, summary = ?, tags = ?,
|
|
660
|
+
importance = ?, metadata = ?, expires_at = ?,
|
|
661
|
+
pinned = COALESCE(pinned, 0),
|
|
662
|
+
version = version + 1, updated_at = ?
|
|
663
|
+
WHERE id = ?`, [
|
|
664
|
+
safeValue,
|
|
665
|
+
input.category || "knowledge",
|
|
666
|
+
safeSummary,
|
|
667
|
+
tagsJson,
|
|
668
|
+
input.importance ?? 5,
|
|
669
|
+
metadataJson,
|
|
670
|
+
expiresAt,
|
|
671
|
+
timestamp,
|
|
672
|
+
existing.id
|
|
673
|
+
]);
|
|
674
|
+
d.run("DELETE FROM memory_tags WHERE memory_id = ?", [existing.id]);
|
|
675
|
+
const insertTag2 = d.prepare("INSERT OR IGNORE INTO memory_tags (memory_id, tag) VALUES (?, ?)");
|
|
676
|
+
for (const tag of tags) {
|
|
677
|
+
insertTag2.run(existing.id, tag);
|
|
678
|
+
}
|
|
679
|
+
const merged = getMemory(existing.id, d);
|
|
680
|
+
try {
|
|
681
|
+
const oldLinks = getEntityMemoryLinks(undefined, merged.id, d);
|
|
682
|
+
for (const link of oldLinks) {
|
|
683
|
+
unlinkEntityFromMemory(link.entity_id, merged.id, d);
|
|
684
|
+
}
|
|
685
|
+
runEntityExtraction(merged, input.project_id, d);
|
|
686
|
+
} catch {}
|
|
687
|
+
return merged;
|
|
718
688
|
}
|
|
719
|
-
const existingId = existing["id"];
|
|
720
|
-
params.push(existingId);
|
|
721
|
-
d.run(`UPDATE entities SET ${sets.join(", ")} WHERE id = ?`, params);
|
|
722
|
-
return getEntity(existingId, d);
|
|
723
689
|
}
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
690
|
+
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)
|
|
691
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'active', 0, ?, ?, ?, ?, 0, 1, ?, ?, ?)`, [
|
|
727
692
|
id,
|
|
728
|
-
input.
|
|
729
|
-
input.
|
|
730
|
-
input.
|
|
731
|
-
|
|
693
|
+
input.key,
|
|
694
|
+
input.value,
|
|
695
|
+
input.category || "knowledge",
|
|
696
|
+
input.scope || "private",
|
|
697
|
+
input.summary || null,
|
|
698
|
+
tagsJson,
|
|
699
|
+
input.importance ?? 5,
|
|
700
|
+
input.source || "agent",
|
|
701
|
+
input.agent_id || null,
|
|
732
702
|
input.project_id || null,
|
|
703
|
+
input.session_id || null,
|
|
704
|
+
metadataJson,
|
|
705
|
+
expiresAt,
|
|
733
706
|
timestamp,
|
|
734
707
|
timestamp
|
|
735
708
|
]);
|
|
736
|
-
|
|
709
|
+
const insertTag = d.prepare("INSERT OR IGNORE INTO memory_tags (memory_id, tag) VALUES (?, ?)");
|
|
710
|
+
for (const tag of tags) {
|
|
711
|
+
insertTag.run(id, tag);
|
|
712
|
+
}
|
|
713
|
+
const memory = getMemory(id, d);
|
|
714
|
+
runEntityExtraction(memory, input.project_id, d);
|
|
715
|
+
hookRegistry.runHooks("PostMemorySave", {
|
|
716
|
+
memory,
|
|
717
|
+
wasUpdated: false,
|
|
718
|
+
agentId: input.agent_id,
|
|
719
|
+
projectId: input.project_id,
|
|
720
|
+
sessionId: input.session_id,
|
|
721
|
+
timestamp: Date.now()
|
|
722
|
+
});
|
|
723
|
+
return memory;
|
|
737
724
|
}
|
|
738
|
-
function
|
|
725
|
+
function getMemory(id, db) {
|
|
739
726
|
const d = db || getDatabase();
|
|
740
|
-
const row = d.query("SELECT * FROM
|
|
727
|
+
const row = d.query("SELECT * FROM memories WHERE id = ?").get(id);
|
|
741
728
|
if (!row)
|
|
742
|
-
|
|
743
|
-
return
|
|
729
|
+
return null;
|
|
730
|
+
return parseMemoryRow(row);
|
|
744
731
|
}
|
|
745
|
-
function
|
|
732
|
+
function getMemoryByKey(key, scope, agentId, projectId, sessionId, db) {
|
|
746
733
|
const d = db || getDatabase();
|
|
747
|
-
let sql = "SELECT * FROM
|
|
748
|
-
const params = [
|
|
749
|
-
if (
|
|
750
|
-
sql += " AND
|
|
751
|
-
params.push(
|
|
734
|
+
let sql = "SELECT * FROM memories WHERE key = ?";
|
|
735
|
+
const params = [key];
|
|
736
|
+
if (scope) {
|
|
737
|
+
sql += " AND scope = ?";
|
|
738
|
+
params.push(scope);
|
|
752
739
|
}
|
|
753
|
-
if (
|
|
740
|
+
if (agentId) {
|
|
741
|
+
sql += " AND agent_id = ?";
|
|
742
|
+
params.push(agentId);
|
|
743
|
+
}
|
|
744
|
+
if (projectId) {
|
|
754
745
|
sql += " AND project_id = ?";
|
|
755
746
|
params.push(projectId);
|
|
756
747
|
}
|
|
757
|
-
|
|
748
|
+
if (sessionId) {
|
|
749
|
+
sql += " AND session_id = ?";
|
|
750
|
+
params.push(sessionId);
|
|
751
|
+
}
|
|
752
|
+
sql += " AND status = 'active' ORDER BY importance DESC LIMIT 1";
|
|
758
753
|
const row = d.query(sql).get(...params);
|
|
759
754
|
if (!row)
|
|
760
755
|
return null;
|
|
761
|
-
return
|
|
756
|
+
return parseMemoryRow(row);
|
|
762
757
|
}
|
|
763
|
-
function
|
|
758
|
+
function getMemoriesByKey(key, scope, agentId, projectId, db) {
|
|
764
759
|
const d = db || getDatabase();
|
|
765
|
-
|
|
766
|
-
const params = [];
|
|
767
|
-
if (
|
|
768
|
-
|
|
769
|
-
params.push(
|
|
770
|
-
}
|
|
771
|
-
if (filter.project_id) {
|
|
772
|
-
conditions.push("project_id = ?");
|
|
773
|
-
params.push(filter.project_id);
|
|
760
|
+
let sql = "SELECT * FROM memories WHERE key = ?";
|
|
761
|
+
const params = [key];
|
|
762
|
+
if (scope) {
|
|
763
|
+
sql += " AND scope = ?";
|
|
764
|
+
params.push(scope);
|
|
774
765
|
}
|
|
775
|
-
if (
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
params.push(term, term);
|
|
766
|
+
if (agentId) {
|
|
767
|
+
sql += " AND agent_id = ?";
|
|
768
|
+
params.push(agentId);
|
|
779
769
|
}
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
770
|
+
if (projectId) {
|
|
771
|
+
sql += " AND project_id = ?";
|
|
772
|
+
params.push(projectId);
|
|
783
773
|
}
|
|
784
|
-
sql += " ORDER BY
|
|
785
|
-
|
|
774
|
+
sql += " AND status = 'active' ORDER BY importance DESC";
|
|
775
|
+
const rows = d.query(sql).all(...params);
|
|
776
|
+
return rows.map(parseMemoryRow);
|
|
777
|
+
}
|
|
778
|
+
function listMemories(filter, db) {
|
|
779
|
+
const d = db || getDatabase();
|
|
780
|
+
const conditions = [];
|
|
781
|
+
const params = [];
|
|
782
|
+
if (filter) {
|
|
783
|
+
if (filter.scope) {
|
|
784
|
+
if (Array.isArray(filter.scope)) {
|
|
785
|
+
conditions.push(`scope IN (${filter.scope.map(() => "?").join(",")})`);
|
|
786
|
+
params.push(...filter.scope);
|
|
787
|
+
} else {
|
|
788
|
+
conditions.push("scope = ?");
|
|
789
|
+
params.push(filter.scope);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
if (filter.category) {
|
|
793
|
+
if (Array.isArray(filter.category)) {
|
|
794
|
+
conditions.push(`category IN (${filter.category.map(() => "?").join(",")})`);
|
|
795
|
+
params.push(...filter.category);
|
|
796
|
+
} else {
|
|
797
|
+
conditions.push("category = ?");
|
|
798
|
+
params.push(filter.category);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
if (filter.source) {
|
|
802
|
+
if (Array.isArray(filter.source)) {
|
|
803
|
+
conditions.push(`source IN (${filter.source.map(() => "?").join(",")})`);
|
|
804
|
+
params.push(...filter.source);
|
|
805
|
+
} else {
|
|
806
|
+
conditions.push("source = ?");
|
|
807
|
+
params.push(filter.source);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
if (filter.status) {
|
|
811
|
+
if (Array.isArray(filter.status)) {
|
|
812
|
+
conditions.push(`status IN (${filter.status.map(() => "?").join(",")})`);
|
|
813
|
+
params.push(...filter.status);
|
|
814
|
+
} else {
|
|
815
|
+
conditions.push("status = ?");
|
|
816
|
+
params.push(filter.status);
|
|
817
|
+
}
|
|
818
|
+
} else {
|
|
819
|
+
conditions.push("status = 'active'");
|
|
820
|
+
}
|
|
821
|
+
if (filter.project_id) {
|
|
822
|
+
conditions.push("project_id = ?");
|
|
823
|
+
params.push(filter.project_id);
|
|
824
|
+
}
|
|
825
|
+
if (filter.agent_id) {
|
|
826
|
+
conditions.push("agent_id = ?");
|
|
827
|
+
params.push(filter.agent_id);
|
|
828
|
+
}
|
|
829
|
+
if (filter.session_id) {
|
|
830
|
+
conditions.push("session_id = ?");
|
|
831
|
+
params.push(filter.session_id);
|
|
832
|
+
}
|
|
833
|
+
if (filter.min_importance) {
|
|
834
|
+
conditions.push("importance >= ?");
|
|
835
|
+
params.push(filter.min_importance);
|
|
836
|
+
}
|
|
837
|
+
if (filter.pinned !== undefined) {
|
|
838
|
+
conditions.push("pinned = ?");
|
|
839
|
+
params.push(filter.pinned ? 1 : 0);
|
|
840
|
+
}
|
|
841
|
+
if (filter.tags && filter.tags.length > 0) {
|
|
842
|
+
for (const tag of filter.tags) {
|
|
843
|
+
conditions.push("id IN (SELECT memory_id FROM memory_tags WHERE tag = ?)");
|
|
844
|
+
params.push(tag);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
if (filter.search) {
|
|
848
|
+
conditions.push("(key LIKE ? OR value LIKE ? OR summary LIKE ?)");
|
|
849
|
+
const term = `%${filter.search}%`;
|
|
850
|
+
params.push(term, term, term);
|
|
851
|
+
}
|
|
852
|
+
} else {
|
|
853
|
+
conditions.push("status = 'active'");
|
|
854
|
+
}
|
|
855
|
+
let sql = "SELECT * FROM memories";
|
|
856
|
+
if (conditions.length > 0) {
|
|
857
|
+
sql += ` WHERE ${conditions.join(" AND ")}`;
|
|
858
|
+
}
|
|
859
|
+
sql += " ORDER BY importance DESC, created_at DESC";
|
|
860
|
+
if (filter?.limit) {
|
|
786
861
|
sql += " LIMIT ?";
|
|
787
862
|
params.push(filter.limit);
|
|
788
863
|
}
|
|
789
|
-
if (filter
|
|
864
|
+
if (filter?.offset) {
|
|
790
865
|
sql += " OFFSET ?";
|
|
791
866
|
params.push(filter.offset);
|
|
792
867
|
}
|
|
793
868
|
const rows = d.query(sql).all(...params);
|
|
794
|
-
return rows.map(
|
|
869
|
+
return rows.map(parseMemoryRow);
|
|
795
870
|
}
|
|
796
|
-
function
|
|
871
|
+
function updateMemory(id, input, db) {
|
|
797
872
|
const d = db || getDatabase();
|
|
798
|
-
const existing =
|
|
873
|
+
const existing = getMemory(id, d);
|
|
799
874
|
if (!existing)
|
|
800
|
-
throw new
|
|
801
|
-
|
|
875
|
+
throw new MemoryNotFoundError(id);
|
|
876
|
+
if (existing.version !== input.version) {
|
|
877
|
+
throw new VersionConflictError(id, input.version, existing.version);
|
|
878
|
+
}
|
|
879
|
+
try {
|
|
880
|
+
d.run(`INSERT OR IGNORE INTO memory_versions (id, memory_id, version, value, importance, scope, category, tags, summary, pinned, status, created_at)
|
|
881
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
882
|
+
uuid(),
|
|
883
|
+
existing.id,
|
|
884
|
+
existing.version,
|
|
885
|
+
existing.value,
|
|
886
|
+
existing.importance,
|
|
887
|
+
existing.scope,
|
|
888
|
+
existing.category,
|
|
889
|
+
JSON.stringify(existing.tags),
|
|
890
|
+
existing.summary,
|
|
891
|
+
existing.pinned ? 1 : 0,
|
|
892
|
+
existing.status,
|
|
893
|
+
existing.updated_at
|
|
894
|
+
]);
|
|
895
|
+
} catch {}
|
|
896
|
+
const sets = ["version = version + 1", "updated_at = ?"];
|
|
802
897
|
const params = [now()];
|
|
803
|
-
if (input.
|
|
804
|
-
sets.push("
|
|
805
|
-
params.push(input.
|
|
898
|
+
if (input.value !== undefined) {
|
|
899
|
+
sets.push("value = ?");
|
|
900
|
+
params.push(redactSecrets(input.value));
|
|
806
901
|
}
|
|
807
|
-
if (input.
|
|
808
|
-
sets.push("
|
|
809
|
-
params.push(input.
|
|
902
|
+
if (input.category !== undefined) {
|
|
903
|
+
sets.push("category = ?");
|
|
904
|
+
params.push(input.category);
|
|
810
905
|
}
|
|
811
|
-
if (input.
|
|
812
|
-
sets.push("
|
|
813
|
-
params.push(input.
|
|
906
|
+
if (input.scope !== undefined) {
|
|
907
|
+
sets.push("scope = ?");
|
|
908
|
+
params.push(input.scope);
|
|
909
|
+
}
|
|
910
|
+
if (input.summary !== undefined) {
|
|
911
|
+
sets.push("summary = ?");
|
|
912
|
+
params.push(input.summary);
|
|
913
|
+
}
|
|
914
|
+
if (input.importance !== undefined) {
|
|
915
|
+
sets.push("importance = ?");
|
|
916
|
+
params.push(input.importance);
|
|
917
|
+
}
|
|
918
|
+
if (input.pinned !== undefined) {
|
|
919
|
+
sets.push("pinned = ?");
|
|
920
|
+
params.push(input.pinned ? 1 : 0);
|
|
921
|
+
}
|
|
922
|
+
if (input.status !== undefined) {
|
|
923
|
+
sets.push("status = ?");
|
|
924
|
+
params.push(input.status);
|
|
814
925
|
}
|
|
815
926
|
if (input.metadata !== undefined) {
|
|
816
927
|
sets.push("metadata = ?");
|
|
817
928
|
params.push(JSON.stringify(input.metadata));
|
|
818
929
|
}
|
|
930
|
+
if (input.expires_at !== undefined) {
|
|
931
|
+
sets.push("expires_at = ?");
|
|
932
|
+
params.push(input.expires_at);
|
|
933
|
+
}
|
|
934
|
+
if (input.tags !== undefined) {
|
|
935
|
+
sets.push("tags = ?");
|
|
936
|
+
params.push(JSON.stringify(input.tags));
|
|
937
|
+
d.run("DELETE FROM memory_tags WHERE memory_id = ?", [id]);
|
|
938
|
+
const insertTag = d.prepare("INSERT OR IGNORE INTO memory_tags (memory_id, tag) VALUES (?, ?)");
|
|
939
|
+
for (const tag of input.tags) {
|
|
940
|
+
insertTag.run(id, tag);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
819
943
|
params.push(id);
|
|
820
|
-
d.run(`UPDATE
|
|
821
|
-
|
|
944
|
+
d.run(`UPDATE memories SET ${sets.join(", ")} WHERE id = ?`, params);
|
|
945
|
+
const updated = getMemory(id, d);
|
|
946
|
+
if (input.value !== undefined) {
|
|
947
|
+
try {
|
|
948
|
+
const oldLinks = getEntityMemoryLinks(undefined, updated.id, d);
|
|
949
|
+
for (const link of oldLinks) {
|
|
950
|
+
unlinkEntityFromMemory(link.entity_id, updated.id, d);
|
|
951
|
+
}
|
|
952
|
+
} catch {}
|
|
953
|
+
}
|
|
954
|
+
hookRegistry.runHooks("PostMemoryUpdate", {
|
|
955
|
+
memory: updated,
|
|
956
|
+
previousValue: existing.value,
|
|
957
|
+
agentId: existing.agent_id ?? undefined,
|
|
958
|
+
projectId: existing.project_id ?? undefined,
|
|
959
|
+
sessionId: existing.session_id ?? undefined,
|
|
960
|
+
timestamp: Date.now()
|
|
961
|
+
});
|
|
962
|
+
return updated;
|
|
822
963
|
}
|
|
823
|
-
function
|
|
964
|
+
function deleteMemory(id, db) {
|
|
824
965
|
const d = db || getDatabase();
|
|
825
|
-
const result = d.run("DELETE FROM
|
|
826
|
-
if (result.changes
|
|
827
|
-
|
|
966
|
+
const result = d.run("DELETE FROM memories WHERE id = ?", [id]);
|
|
967
|
+
if (result.changes > 0) {
|
|
968
|
+
hookRegistry.runHooks("PostMemoryDelete", {
|
|
969
|
+
memoryId: id,
|
|
970
|
+
timestamp: Date.now()
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
return result.changes > 0;
|
|
828
974
|
}
|
|
829
|
-
function
|
|
975
|
+
function bulkDeleteMemories(ids, db) {
|
|
830
976
|
const d = db || getDatabase();
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
d.
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
d.run("DELETE FROM entity_memories WHERE entity_id = ?", [sourceId]);
|
|
841
|
-
d.run("DELETE FROM entities WHERE id = ?", [sourceId]);
|
|
842
|
-
d.run("UPDATE entities SET updated_at = ? WHERE id = ?", [now(), targetId]);
|
|
843
|
-
return getEntity(targetId, d);
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
// src/db/entity-memories.ts
|
|
847
|
-
function parseEntityMemoryRow(row) {
|
|
848
|
-
return {
|
|
849
|
-
entity_id: row["entity_id"],
|
|
850
|
-
memory_id: row["memory_id"],
|
|
851
|
-
role: row["role"],
|
|
852
|
-
created_at: row["created_at"]
|
|
853
|
-
};
|
|
977
|
+
if (ids.length === 0)
|
|
978
|
+
return 0;
|
|
979
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
980
|
+
const countRow = d.query(`SELECT COUNT(*) as c FROM memories WHERE id IN (${placeholders})`).get(...ids);
|
|
981
|
+
const count = countRow.c;
|
|
982
|
+
if (count > 0) {
|
|
983
|
+
d.run(`DELETE FROM memories WHERE id IN (${placeholders})`, ids);
|
|
984
|
+
}
|
|
985
|
+
return count;
|
|
854
986
|
}
|
|
855
|
-
function
|
|
987
|
+
function touchMemory(id, db) {
|
|
856
988
|
const d = db || getDatabase();
|
|
857
|
-
|
|
858
|
-
d.run(`INSERT OR IGNORE INTO entity_memories (entity_id, memory_id, role, created_at)
|
|
859
|
-
VALUES (?, ?, ?, ?)`, [entityId, memoryId, role, timestamp]);
|
|
860
|
-
const row = d.query("SELECT * FROM entity_memories WHERE entity_id = ? AND memory_id = ?").get(entityId, memoryId);
|
|
861
|
-
return parseEntityMemoryRow(row);
|
|
989
|
+
d.run("UPDATE memories SET access_count = access_count + 1, accessed_at = ? WHERE id = ?", [now(), id]);
|
|
862
990
|
}
|
|
863
|
-
function
|
|
991
|
+
function incrementRecallCount(id, db) {
|
|
864
992
|
const d = db || getDatabase();
|
|
865
|
-
|
|
993
|
+
try {
|
|
994
|
+
d.run("UPDATE memories SET recall_count = recall_count + 1, access_count = access_count + 1, accessed_at = ? WHERE id = ?", [now(), id]);
|
|
995
|
+
const row = d.query("SELECT recall_count, importance FROM memories WHERE id = ?").get(id);
|
|
996
|
+
if (!row)
|
|
997
|
+
return;
|
|
998
|
+
const promotions = Math.floor(row.recall_count / RECALL_PROMOTE_THRESHOLD);
|
|
999
|
+
if (promotions > 0 && row.importance < 10) {
|
|
1000
|
+
const newImportance = Math.min(10, row.importance + 1);
|
|
1001
|
+
d.run("UPDATE memories SET importance = ? WHERE id = ? AND importance < 10", [newImportance, id]);
|
|
1002
|
+
}
|
|
1003
|
+
} catch {}
|
|
866
1004
|
}
|
|
867
|
-
function
|
|
1005
|
+
function cleanExpiredMemories(db) {
|
|
868
1006
|
const d = db || getDatabase();
|
|
869
|
-
const
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
1007
|
+
const timestamp = now();
|
|
1008
|
+
const countRow = d.query("SELECT COUNT(*) as c FROM memories WHERE expires_at IS NOT NULL AND expires_at < ?").get(timestamp);
|
|
1009
|
+
const count = countRow.c;
|
|
1010
|
+
if (count > 0) {
|
|
1011
|
+
d.run("DELETE FROM memories WHERE expires_at IS NOT NULL AND expires_at < ?", [timestamp]);
|
|
1012
|
+
}
|
|
1013
|
+
return count;
|
|
874
1014
|
}
|
|
875
|
-
function
|
|
1015
|
+
function getMemoryVersions(memoryId, db) {
|
|
876
1016
|
const d = db || getDatabase();
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
1017
|
+
try {
|
|
1018
|
+
const rows = d.query("SELECT * FROM memory_versions WHERE memory_id = ? ORDER BY version ASC").all(memoryId);
|
|
1019
|
+
return rows.map((row) => ({
|
|
1020
|
+
id: row["id"],
|
|
1021
|
+
memory_id: row["memory_id"],
|
|
1022
|
+
version: row["version"],
|
|
1023
|
+
value: row["value"],
|
|
1024
|
+
importance: row["importance"],
|
|
1025
|
+
scope: row["scope"],
|
|
1026
|
+
category: row["category"],
|
|
1027
|
+
tags: JSON.parse(row["tags"] || "[]"),
|
|
1028
|
+
summary: row["summary"] || null,
|
|
1029
|
+
pinned: !!row["pinned"],
|
|
1030
|
+
status: row["status"],
|
|
1031
|
+
created_at: row["created_at"]
|
|
1032
|
+
}));
|
|
1033
|
+
} catch {
|
|
1034
|
+
return [];
|
|
890
1035
|
}
|
|
891
|
-
sql += " ORDER BY created_at DESC";
|
|
892
|
-
const rows = d.query(sql).all(...params);
|
|
893
|
-
return rows.map(parseEntityMemoryRow);
|
|
894
1036
|
}
|
|
1037
|
+
var RECALL_PROMOTE_THRESHOLD = 3;
|
|
1038
|
+
var init_memories = __esm(() => {
|
|
1039
|
+
init_types();
|
|
1040
|
+
init_database();
|
|
1041
|
+
init_redact();
|
|
1042
|
+
init_hooks();
|
|
1043
|
+
init_entity_memories();
|
|
1044
|
+
});
|
|
895
1045
|
|
|
896
|
-
// src/db/
|
|
897
|
-
function
|
|
898
|
-
return {
|
|
899
|
-
id: row["id"],
|
|
900
|
-
source_entity_id: row["source_entity_id"],
|
|
901
|
-
target_entity_id: row["target_entity_id"],
|
|
902
|
-
relation_type: row["relation_type"],
|
|
903
|
-
weight: row["weight"],
|
|
904
|
-
metadata: JSON.parse(row["metadata"] || "{}"),
|
|
905
|
-
created_at: row["created_at"]
|
|
906
|
-
};
|
|
907
|
-
}
|
|
908
|
-
function parseEntityRow2(row) {
|
|
1046
|
+
// src/db/entities.ts
|
|
1047
|
+
function parseEntityRow(row) {
|
|
909
1048
|
return {
|
|
910
1049
|
id: row["id"],
|
|
911
1050
|
name: row["name"],
|
|
@@ -917,307 +1056,164 @@ function parseEntityRow2(row) {
|
|
|
917
1056
|
updated_at: row["updated_at"]
|
|
918
1057
|
};
|
|
919
1058
|
}
|
|
920
|
-
function
|
|
1059
|
+
function createEntity(input, db) {
|
|
921
1060
|
const d = db || getDatabase();
|
|
922
|
-
const id = shortUuid();
|
|
923
1061
|
const timestamp = now();
|
|
924
|
-
const
|
|
925
|
-
const
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
1062
|
+
const metadataJson = JSON.stringify(input.metadata || {});
|
|
1063
|
+
const existing = d.query(`SELECT * FROM entities
|
|
1064
|
+
WHERE name = ? AND type = ? AND COALESCE(project_id, '') = ?`).get(input.name, input.type, input.project_id || "");
|
|
1065
|
+
if (existing) {
|
|
1066
|
+
const sets = ["updated_at = ?"];
|
|
1067
|
+
const params = [timestamp];
|
|
1068
|
+
if (input.description !== undefined) {
|
|
1069
|
+
sets.push("description = ?");
|
|
1070
|
+
params.push(input.description);
|
|
1071
|
+
}
|
|
1072
|
+
if (input.metadata !== undefined) {
|
|
1073
|
+
sets.push("metadata = ?");
|
|
1074
|
+
params.push(metadataJson);
|
|
1075
|
+
}
|
|
1076
|
+
const existingId = existing["id"];
|
|
1077
|
+
params.push(existingId);
|
|
1078
|
+
d.run(`UPDATE entities SET ${sets.join(", ")} WHERE id = ?`, params);
|
|
1079
|
+
return getEntity(existingId, d);
|
|
1080
|
+
}
|
|
1081
|
+
const id = shortUuid();
|
|
1082
|
+
d.run(`INSERT INTO entities (id, name, type, description, metadata, project_id, created_at, updated_at)
|
|
1083
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
1084
|
+
id,
|
|
1085
|
+
input.name,
|
|
1086
|
+
input.type,
|
|
1087
|
+
input.description || null,
|
|
1088
|
+
metadataJson,
|
|
1089
|
+
input.project_id || null,
|
|
1090
|
+
timestamp,
|
|
1091
|
+
timestamp
|
|
1092
|
+
]);
|
|
1093
|
+
hookRegistry.runHooks("PostEntityCreate", {
|
|
1094
|
+
entityId: id,
|
|
1095
|
+
name: input.name,
|
|
1096
|
+
entityType: input.type,
|
|
1097
|
+
projectId: input.project_id,
|
|
1098
|
+
timestamp: Date.now()
|
|
1099
|
+
});
|
|
1100
|
+
return getEntity(id, d);
|
|
933
1101
|
}
|
|
934
|
-
function
|
|
1102
|
+
function getEntity(id, db) {
|
|
935
1103
|
const d = db || getDatabase();
|
|
936
|
-
const row = d.query("SELECT * FROM
|
|
1104
|
+
const row = d.query("SELECT * FROM entities WHERE id = ?").get(id);
|
|
937
1105
|
if (!row)
|
|
938
|
-
throw new
|
|
939
|
-
return
|
|
1106
|
+
throw new EntityNotFoundError(id);
|
|
1107
|
+
return parseEntityRow(row);
|
|
940
1108
|
}
|
|
941
|
-
function
|
|
1109
|
+
function getEntityByName(name, type, projectId, db) {
|
|
942
1110
|
const d = db || getDatabase();
|
|
943
|
-
|
|
944
|
-
const params = [];
|
|
945
|
-
if (
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
conditions.push("source_entity_id = ?");
|
|
949
|
-
params.push(filter.entity_id);
|
|
950
|
-
} else if (dir === "incoming") {
|
|
951
|
-
conditions.push("target_entity_id = ?");
|
|
952
|
-
params.push(filter.entity_id);
|
|
953
|
-
} else {
|
|
954
|
-
conditions.push("(source_entity_id = ? OR target_entity_id = ?)");
|
|
955
|
-
params.push(filter.entity_id, filter.entity_id);
|
|
956
|
-
}
|
|
1111
|
+
let sql = "SELECT * FROM entities WHERE name = ?";
|
|
1112
|
+
const params = [name];
|
|
1113
|
+
if (type) {
|
|
1114
|
+
sql += " AND type = ?";
|
|
1115
|
+
params.push(type);
|
|
957
1116
|
}
|
|
958
|
-
if (
|
|
959
|
-
|
|
960
|
-
params.push(
|
|
1117
|
+
if (projectId !== undefined) {
|
|
1118
|
+
sql += " AND project_id = ?";
|
|
1119
|
+
params.push(projectId);
|
|
961
1120
|
}
|
|
962
|
-
|
|
963
|
-
const
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
const d = db || getDatabase();
|
|
968
|
-
const result = d.run("DELETE FROM relations WHERE id = ?", [id]);
|
|
969
|
-
if (result.changes === 0)
|
|
970
|
-
throw new Error(`Relation not found: ${id}`);
|
|
1121
|
+
sql += " LIMIT 1";
|
|
1122
|
+
const row = d.query(sql).get(...params);
|
|
1123
|
+
if (!row)
|
|
1124
|
+
return null;
|
|
1125
|
+
return parseEntityRow(row);
|
|
971
1126
|
}
|
|
972
|
-
function
|
|
1127
|
+
function listEntities(filter = {}, db) {
|
|
973
1128
|
const d = db || getDatabase();
|
|
974
|
-
const
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
WHERE g.depth < ?
|
|
980
|
-
)
|
|
981
|
-
SELECT DISTINCT e.* FROM entities e JOIN graph g ON e.id = g.id`).all(entityId, depth);
|
|
982
|
-
const entities = entityRows.map(parseEntityRow2);
|
|
983
|
-
const entityIds = new Set(entities.map((e) => e.id));
|
|
984
|
-
if (entityIds.size === 0) {
|
|
985
|
-
return { entities: [], relations: [] };
|
|
1129
|
+
const conditions = [];
|
|
1130
|
+
const params = [];
|
|
1131
|
+
if (filter.type) {
|
|
1132
|
+
conditions.push("type = ?");
|
|
1133
|
+
params.push(filter.type);
|
|
986
1134
|
}
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
AND target_entity_id IN (${placeholders})`).all(...Array.from(entityIds), ...Array.from(entityIds));
|
|
991
|
-
const relations = relationRows.map(parseRelationRow);
|
|
992
|
-
return { entities, relations };
|
|
993
|
-
}
|
|
994
|
-
function findPath(fromEntityId, toEntityId, maxDepth = 5, db) {
|
|
995
|
-
const d = db || getDatabase();
|
|
996
|
-
const rows = d.query(`WITH RECURSIVE path(id, trail, depth) AS (
|
|
997
|
-
SELECT ?, ?, 0
|
|
998
|
-
UNION
|
|
999
|
-
SELECT
|
|
1000
|
-
CASE WHEN r.source_entity_id = p.id THEN r.target_entity_id ELSE r.source_entity_id END,
|
|
1001
|
-
p.trail || ',' || CASE WHEN r.source_entity_id = p.id THEN r.target_entity_id ELSE r.source_entity_id END,
|
|
1002
|
-
p.depth + 1
|
|
1003
|
-
FROM relations r JOIN path p ON (r.source_entity_id = p.id OR r.target_entity_id = p.id)
|
|
1004
|
-
WHERE p.depth < ?
|
|
1005
|
-
AND INSTR(p.trail, CASE WHEN r.source_entity_id = p.id THEN r.target_entity_id ELSE r.source_entity_id END) = 0
|
|
1006
|
-
)
|
|
1007
|
-
SELECT trail FROM path WHERE id = ? ORDER BY depth ASC LIMIT 1`).get(fromEntityId, fromEntityId, maxDepth, toEntityId);
|
|
1008
|
-
if (!rows)
|
|
1009
|
-
return null;
|
|
1010
|
-
const ids = rows.trail.split(",");
|
|
1011
|
-
const entities = [];
|
|
1012
|
-
for (const id of ids) {
|
|
1013
|
-
const row = d.query("SELECT * FROM entities WHERE id = ?").get(id);
|
|
1014
|
-
if (row)
|
|
1015
|
-
entities.push(parseEntityRow2(row));
|
|
1135
|
+
if (filter.project_id) {
|
|
1136
|
+
conditions.push("project_id = ?");
|
|
1137
|
+
params.push(filter.project_id);
|
|
1016
1138
|
}
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, readdirSync, writeFileSync, unlinkSync } from "fs";
|
|
1022
|
-
import { homedir } from "os";
|
|
1023
|
-
import { basename, dirname as dirname2, join as join2, resolve as resolve2 } from "path";
|
|
1024
|
-
var DEFAULT_CONFIG = {
|
|
1025
|
-
default_scope: "private",
|
|
1026
|
-
default_category: "knowledge",
|
|
1027
|
-
default_importance: 5,
|
|
1028
|
-
max_entries: 1000,
|
|
1029
|
-
max_entries_per_scope: {
|
|
1030
|
-
global: 500,
|
|
1031
|
-
shared: 300,
|
|
1032
|
-
private: 200
|
|
1033
|
-
},
|
|
1034
|
-
injection: {
|
|
1035
|
-
max_tokens: 500,
|
|
1036
|
-
min_importance: 5,
|
|
1037
|
-
categories: ["preference", "fact"],
|
|
1038
|
-
refresh_interval: 5
|
|
1039
|
-
},
|
|
1040
|
-
extraction: {
|
|
1041
|
-
enabled: true,
|
|
1042
|
-
min_confidence: 0.5
|
|
1043
|
-
},
|
|
1044
|
-
sync_agents: ["claude", "codex", "gemini"],
|
|
1045
|
-
auto_cleanup: {
|
|
1046
|
-
enabled: true,
|
|
1047
|
-
expired_check_interval: 3600,
|
|
1048
|
-
unused_archive_days: 7,
|
|
1049
|
-
stale_deprioritize_days: 14
|
|
1139
|
+
if (filter.search) {
|
|
1140
|
+
conditions.push("(name LIKE ? OR description LIKE ?)");
|
|
1141
|
+
const term = `%${filter.search}%`;
|
|
1142
|
+
params.push(term, term);
|
|
1050
1143
|
}
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
for (const key of Object.keys(source)) {
|
|
1055
|
-
const sourceVal = source[key];
|
|
1056
|
-
const targetVal = result[key];
|
|
1057
|
-
if (sourceVal !== null && typeof sourceVal === "object" && !Array.isArray(sourceVal) && targetVal !== null && typeof targetVal === "object" && !Array.isArray(targetVal)) {
|
|
1058
|
-
result[key] = deepMerge(targetVal, sourceVal);
|
|
1059
|
-
} else {
|
|
1060
|
-
result[key] = sourceVal;
|
|
1061
|
-
}
|
|
1144
|
+
let sql = "SELECT * FROM entities";
|
|
1145
|
+
if (conditions.length > 0) {
|
|
1146
|
+
sql += ` WHERE ${conditions.join(" AND ")}`;
|
|
1062
1147
|
}
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
return
|
|
1074
|
-
}
|
|
1075
|
-
function isValidCategory(value) {
|
|
1076
|
-
return VALID_CATEGORIES.includes(value);
|
|
1148
|
+
sql += " ORDER BY updated_at DESC";
|
|
1149
|
+
if (filter.limit) {
|
|
1150
|
+
sql += " LIMIT ?";
|
|
1151
|
+
params.push(filter.limit);
|
|
1152
|
+
}
|
|
1153
|
+
if (filter.offset) {
|
|
1154
|
+
sql += " OFFSET ?";
|
|
1155
|
+
params.push(filter.offset);
|
|
1156
|
+
}
|
|
1157
|
+
const rows = d.query(sql).all(...params);
|
|
1158
|
+
return rows.map(parseEntityRow);
|
|
1077
1159
|
}
|
|
1078
|
-
function
|
|
1079
|
-
const
|
|
1080
|
-
|
|
1081
|
-
if (
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1160
|
+
function updateEntity(id, input, db) {
|
|
1161
|
+
const d = db || getDatabase();
|
|
1162
|
+
const existing = d.query("SELECT id FROM entities WHERE id = ?").get(id);
|
|
1163
|
+
if (!existing)
|
|
1164
|
+
throw new EntityNotFoundError(id);
|
|
1165
|
+
const sets = ["updated_at = ?"];
|
|
1166
|
+
const params = [now()];
|
|
1167
|
+
if (input.name !== undefined) {
|
|
1168
|
+
sets.push("name = ?");
|
|
1169
|
+
params.push(input.name);
|
|
1086
1170
|
}
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
merged.default_scope = envScope;
|
|
1171
|
+
if (input.type !== undefined) {
|
|
1172
|
+
sets.push("type = ?");
|
|
1173
|
+
params.push(input.type);
|
|
1091
1174
|
}
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1175
|
+
if (input.description !== undefined) {
|
|
1176
|
+
sets.push("description = ?");
|
|
1177
|
+
params.push(input.description);
|
|
1095
1178
|
}
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
if (!Number.isNaN(parsed) && parsed >= 1 && parsed <= 10) {
|
|
1100
|
-
merged.default_importance = parsed;
|
|
1101
|
-
}
|
|
1179
|
+
if (input.metadata !== undefined) {
|
|
1180
|
+
sets.push("metadata = ?");
|
|
1181
|
+
params.push(JSON.stringify(input.metadata));
|
|
1102
1182
|
}
|
|
1103
|
-
|
|
1183
|
+
params.push(id);
|
|
1184
|
+
d.run(`UPDATE entities SET ${sets.join(", ")} WHERE id = ?`, params);
|
|
1185
|
+
return getEntity(id, d);
|
|
1104
1186
|
}
|
|
1105
|
-
function
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
return candidate;
|
|
1111
|
-
}
|
|
1112
|
-
const parent = dirname2(dir);
|
|
1113
|
-
if (parent === dir) {
|
|
1114
|
-
return null;
|
|
1115
|
-
}
|
|
1116
|
-
dir = parent;
|
|
1117
|
-
}
|
|
1118
|
-
}
|
|
1119
|
-
function findGitRoot2() {
|
|
1120
|
-
let dir = process.cwd();
|
|
1121
|
-
while (true) {
|
|
1122
|
-
if (existsSync2(join2(dir, ".git"))) {
|
|
1123
|
-
return dir;
|
|
1124
|
-
}
|
|
1125
|
-
const parent = dirname2(dir);
|
|
1126
|
-
if (parent === dir) {
|
|
1127
|
-
return null;
|
|
1128
|
-
}
|
|
1129
|
-
dir = parent;
|
|
1130
|
-
}
|
|
1131
|
-
}
|
|
1132
|
-
function profilesDir() {
|
|
1133
|
-
return join2(homedir(), ".mementos", "profiles");
|
|
1134
|
-
}
|
|
1135
|
-
function globalConfigPath() {
|
|
1136
|
-
return join2(homedir(), ".mementos", "config.json");
|
|
1137
|
-
}
|
|
1138
|
-
function readGlobalConfig() {
|
|
1139
|
-
const p = globalConfigPath();
|
|
1140
|
-
if (!existsSync2(p))
|
|
1141
|
-
return {};
|
|
1142
|
-
try {
|
|
1143
|
-
return JSON.parse(readFileSync(p, "utf-8"));
|
|
1144
|
-
} catch {
|
|
1145
|
-
return {};
|
|
1146
|
-
}
|
|
1147
|
-
}
|
|
1148
|
-
function getActiveProfile() {
|
|
1149
|
-
const envProfile = process.env["MEMENTOS_PROFILE"];
|
|
1150
|
-
if (envProfile)
|
|
1151
|
-
return envProfile.trim();
|
|
1152
|
-
const cfg = readGlobalConfig();
|
|
1153
|
-
return cfg["active_profile"] || null;
|
|
1154
|
-
}
|
|
1155
|
-
function listProfiles() {
|
|
1156
|
-
const dir = profilesDir();
|
|
1157
|
-
if (!existsSync2(dir))
|
|
1158
|
-
return [];
|
|
1159
|
-
return readdirSync(dir).filter((f) => f.endsWith(".db")).map((f) => basename(f, ".db")).sort();
|
|
1160
|
-
}
|
|
1161
|
-
function getDbPath2() {
|
|
1162
|
-
const envDbPath = process.env["MEMENTOS_DB_PATH"];
|
|
1163
|
-
if (envDbPath) {
|
|
1164
|
-
const resolved = resolve2(envDbPath);
|
|
1165
|
-
ensureDir2(dirname2(resolved));
|
|
1166
|
-
return resolved;
|
|
1167
|
-
}
|
|
1168
|
-
const profile = getActiveProfile();
|
|
1169
|
-
if (profile) {
|
|
1170
|
-
const profilePath = join2(profilesDir(), `${profile}.db`);
|
|
1171
|
-
ensureDir2(dirname2(profilePath));
|
|
1172
|
-
return profilePath;
|
|
1173
|
-
}
|
|
1174
|
-
const dbScope = process.env["MEMENTOS_DB_SCOPE"];
|
|
1175
|
-
if (dbScope === "project") {
|
|
1176
|
-
const gitRoot = findGitRoot2();
|
|
1177
|
-
if (gitRoot) {
|
|
1178
|
-
const dbPath = join2(gitRoot, ".mementos", "mementos.db");
|
|
1179
|
-
ensureDir2(dirname2(dbPath));
|
|
1180
|
-
return dbPath;
|
|
1181
|
-
}
|
|
1182
|
-
}
|
|
1183
|
-
const found = findFileWalkingUp(join2(".mementos", "mementos.db"));
|
|
1184
|
-
if (found) {
|
|
1185
|
-
return found;
|
|
1186
|
-
}
|
|
1187
|
-
const fallback = join2(homedir(), ".mementos", "mementos.db");
|
|
1188
|
-
ensureDir2(dirname2(fallback));
|
|
1189
|
-
return fallback;
|
|
1187
|
+
function deleteEntity(id, db) {
|
|
1188
|
+
const d = db || getDatabase();
|
|
1189
|
+
const result = d.run("DELETE FROM entities WHERE id = ?", [id]);
|
|
1190
|
+
if (result.changes === 0)
|
|
1191
|
+
throw new EntityNotFoundError(id);
|
|
1190
1192
|
}
|
|
1191
|
-
function
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1193
|
+
function mergeEntities(sourceId, targetId, db) {
|
|
1194
|
+
const d = db || getDatabase();
|
|
1195
|
+
getEntity(sourceId, d);
|
|
1196
|
+
getEntity(targetId, d);
|
|
1197
|
+
d.run(`UPDATE OR IGNORE relations SET source_entity_id = ? WHERE source_entity_id = ?`, [targetId, sourceId]);
|
|
1198
|
+
d.run(`UPDATE OR IGNORE relations SET target_entity_id = ? WHERE target_entity_id = ?`, [targetId, sourceId]);
|
|
1199
|
+
d.run("DELETE FROM relations WHERE source_entity_id = ? OR target_entity_id = ?", [
|
|
1200
|
+
sourceId,
|
|
1201
|
+
sourceId
|
|
1202
|
+
]);
|
|
1203
|
+
d.run(`UPDATE OR IGNORE entity_memories SET entity_id = ? WHERE entity_id = ?`, [targetId, sourceId]);
|
|
1204
|
+
d.run("DELETE FROM entity_memories WHERE entity_id = ?", [sourceId]);
|
|
1205
|
+
d.run("DELETE FROM entities WHERE id = ?", [sourceId]);
|
|
1206
|
+
d.run("UPDATE entities SET updated_at = ? WHERE id = ?", [now(), targetId]);
|
|
1207
|
+
return getEntity(targetId, d);
|
|
1195
1208
|
}
|
|
1209
|
+
var init_entities = __esm(() => {
|
|
1210
|
+
init_database();
|
|
1211
|
+
init_types();
|
|
1212
|
+
init_hooks();
|
|
1213
|
+
});
|
|
1196
1214
|
|
|
1197
|
-
// src/
|
|
1198
|
-
function
|
|
1199
|
-
const config = loadConfig();
|
|
1200
|
-
if (config.extraction?.enabled === false)
|
|
1201
|
-
return;
|
|
1202
|
-
const extracted = extractEntities(memory, d);
|
|
1203
|
-
const minConfidence = config.extraction?.min_confidence ?? 0.5;
|
|
1204
|
-
const entityIds = [];
|
|
1205
|
-
for (const ext of extracted) {
|
|
1206
|
-
if (ext.confidence >= minConfidence) {
|
|
1207
|
-
const entity = createEntity({ name: ext.name, type: ext.type, project_id: projectId }, d);
|
|
1208
|
-
linkEntityToMemory(entity.id, memory.id, "context", d);
|
|
1209
|
-
entityIds.push(entity.id);
|
|
1210
|
-
}
|
|
1211
|
-
}
|
|
1212
|
-
for (let i = 0;i < entityIds.length; i++) {
|
|
1213
|
-
for (let j = i + 1;j < entityIds.length; j++) {
|
|
1214
|
-
try {
|
|
1215
|
-
createRelation({ source_entity_id: entityIds[i], target_entity_id: entityIds[j], relation_type: "related_to" }, d);
|
|
1216
|
-
} catch {}
|
|
1217
|
-
}
|
|
1218
|
-
}
|
|
1219
|
-
}
|
|
1220
|
-
function parseMemoryRow(row) {
|
|
1215
|
+
// src/lib/search.ts
|
|
1216
|
+
function parseMemoryRow2(row) {
|
|
1221
1217
|
return {
|
|
1222
1218
|
id: row["id"],
|
|
1223
1219
|
key: row["key"],
|
|
@@ -1242,923 +1238,1730 @@ function parseMemoryRow(row) {
|
|
|
1242
1238
|
accessed_at: row["accessed_at"] || null
|
|
1243
1239
|
};
|
|
1244
1240
|
}
|
|
1245
|
-
function
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
const
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
d.run("DELETE FROM memory_tags WHERE memory_id = ?", [existing.id]);
|
|
1282
|
-
const insertTag2 = d.prepare("INSERT OR IGNORE INTO memory_tags (memory_id, tag) VALUES (?, ?)");
|
|
1283
|
-
for (const tag of tags) {
|
|
1284
|
-
insertTag2.run(existing.id, tag);
|
|
1241
|
+
function preprocessQuery(query) {
|
|
1242
|
+
let q = query.trim();
|
|
1243
|
+
q = q.replace(/\s+/g, " ");
|
|
1244
|
+
q = q.normalize("NFC");
|
|
1245
|
+
return q;
|
|
1246
|
+
}
|
|
1247
|
+
function escapeLikePattern(s) {
|
|
1248
|
+
return s.replace(/%/g, "\\%").replace(/_/g, "\\_");
|
|
1249
|
+
}
|
|
1250
|
+
function removeStopWords(tokens) {
|
|
1251
|
+
if (tokens.length <= 1)
|
|
1252
|
+
return tokens;
|
|
1253
|
+
const filtered = tokens.filter((t) => !STOP_WORDS.has(t.toLowerCase()));
|
|
1254
|
+
return filtered.length > 0 ? filtered : tokens;
|
|
1255
|
+
}
|
|
1256
|
+
function extractHighlights(memory, queryLower) {
|
|
1257
|
+
const highlights = [];
|
|
1258
|
+
const tokens = queryLower.split(/\s+/).filter(Boolean);
|
|
1259
|
+
for (const field of ["key", "value", "summary"]) {
|
|
1260
|
+
const text = field === "summary" ? memory.summary : memory[field];
|
|
1261
|
+
if (!text)
|
|
1262
|
+
continue;
|
|
1263
|
+
const textLower = text.toLowerCase();
|
|
1264
|
+
const searchTerms = [queryLower, ...tokens].filter(Boolean);
|
|
1265
|
+
for (const term of searchTerms) {
|
|
1266
|
+
const idx = textLower.indexOf(term);
|
|
1267
|
+
if (idx !== -1) {
|
|
1268
|
+
const start = Math.max(0, idx - 30);
|
|
1269
|
+
const end = Math.min(text.length, idx + term.length + 30);
|
|
1270
|
+
const prefix = start > 0 ? "..." : "";
|
|
1271
|
+
const suffix = end < text.length ? "..." : "";
|
|
1272
|
+
highlights.push({
|
|
1273
|
+
field,
|
|
1274
|
+
snippet: prefix + text.slice(start, end) + suffix
|
|
1275
|
+
});
|
|
1276
|
+
break;
|
|
1285
1277
|
}
|
|
1286
|
-
const merged = getMemory(existing.id, d);
|
|
1287
|
-
try {
|
|
1288
|
-
const oldLinks = getEntityMemoryLinks(undefined, merged.id, d);
|
|
1289
|
-
for (const link of oldLinks) {
|
|
1290
|
-
unlinkEntityFromMemory(link.entity_id, merged.id, d);
|
|
1291
|
-
}
|
|
1292
|
-
runEntityExtraction(merged, input.project_id, d);
|
|
1293
|
-
} catch {}
|
|
1294
|
-
return merged;
|
|
1295
1278
|
}
|
|
1296
1279
|
}
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
input.value,
|
|
1302
|
-
input.category || "knowledge",
|
|
1303
|
-
input.scope || "private",
|
|
1304
|
-
input.summary || null,
|
|
1305
|
-
tagsJson,
|
|
1306
|
-
input.importance ?? 5,
|
|
1307
|
-
input.source || "agent",
|
|
1308
|
-
input.agent_id || null,
|
|
1309
|
-
input.project_id || null,
|
|
1310
|
-
input.session_id || null,
|
|
1311
|
-
metadataJson,
|
|
1312
|
-
expiresAt,
|
|
1313
|
-
timestamp,
|
|
1314
|
-
timestamp
|
|
1315
|
-
]);
|
|
1316
|
-
const insertTag = d.prepare("INSERT OR IGNORE INTO memory_tags (memory_id, tag) VALUES (?, ?)");
|
|
1317
|
-
for (const tag of tags) {
|
|
1318
|
-
insertTag.run(id, tag);
|
|
1280
|
+
for (const tag of memory.tags) {
|
|
1281
|
+
if (tag.toLowerCase().includes(queryLower) || tokens.some((t) => tag.toLowerCase().includes(t))) {
|
|
1282
|
+
highlights.push({ field: "tag", snippet: tag });
|
|
1283
|
+
}
|
|
1319
1284
|
}
|
|
1320
|
-
|
|
1321
|
-
try {
|
|
1322
|
-
runEntityExtraction(memory, input.project_id, d);
|
|
1323
|
-
} catch {}
|
|
1324
|
-
return memory;
|
|
1285
|
+
return highlights;
|
|
1325
1286
|
}
|
|
1326
|
-
function
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
if (
|
|
1330
|
-
return
|
|
1331
|
-
|
|
1287
|
+
function determineMatchType(memory, queryLower) {
|
|
1288
|
+
if (memory.key.toLowerCase() === queryLower)
|
|
1289
|
+
return "exact";
|
|
1290
|
+
if (memory.tags.some((t) => t.toLowerCase() === queryLower))
|
|
1291
|
+
return "tag";
|
|
1292
|
+
if (memory.tags.some((t) => t.toLowerCase().includes(queryLower)))
|
|
1293
|
+
return "tag";
|
|
1294
|
+
return "fuzzy";
|
|
1332
1295
|
}
|
|
1333
|
-
function
|
|
1334
|
-
const
|
|
1335
|
-
const
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1296
|
+
function computeScore(memory, queryLower) {
|
|
1297
|
+
const fieldScores = [];
|
|
1298
|
+
const keyLower = memory.key.toLowerCase();
|
|
1299
|
+
if (keyLower === queryLower) {
|
|
1300
|
+
fieldScores.push(10);
|
|
1301
|
+
} else if (keyLower.includes(queryLower)) {
|
|
1302
|
+
fieldScores.push(7);
|
|
1303
|
+
}
|
|
1304
|
+
if (memory.tags.some((t) => t.toLowerCase() === queryLower)) {
|
|
1305
|
+
fieldScores.push(6);
|
|
1306
|
+
} else if (memory.tags.some((t) => t.toLowerCase().includes(queryLower))) {
|
|
1307
|
+
fieldScores.push(3);
|
|
1308
|
+
}
|
|
1309
|
+
if (memory.summary && memory.summary.toLowerCase().includes(queryLower)) {
|
|
1310
|
+
fieldScores.push(4);
|
|
1311
|
+
}
|
|
1312
|
+
if (memory.value.toLowerCase().includes(queryLower)) {
|
|
1313
|
+
fieldScores.push(3);
|
|
1314
|
+
}
|
|
1315
|
+
const metadataStr = JSON.stringify(memory.metadata).toLowerCase();
|
|
1316
|
+
if (metadataStr !== "{}" && metadataStr.includes(queryLower)) {
|
|
1317
|
+
fieldScores.push(2);
|
|
1318
|
+
}
|
|
1319
|
+
fieldScores.sort((a, b) => b - a);
|
|
1320
|
+
const diminishingMultipliers = [1, 0.5, 0.25, 0.15, 0.15];
|
|
1321
|
+
let score = 0;
|
|
1322
|
+
for (let i = 0;i < fieldScores.length; i++) {
|
|
1323
|
+
score += fieldScores[i] * (diminishingMultipliers[i] ?? 0.15);
|
|
1324
|
+
}
|
|
1325
|
+
const { phrases } = extractQuotedPhrases(queryLower);
|
|
1326
|
+
for (const phrase of phrases) {
|
|
1327
|
+
if (keyLower.includes(phrase))
|
|
1328
|
+
score += 8;
|
|
1329
|
+
if (memory.value.toLowerCase().includes(phrase))
|
|
1330
|
+
score += 5;
|
|
1331
|
+
if (memory.summary && memory.summary.toLowerCase().includes(phrase))
|
|
1332
|
+
score += 4;
|
|
1333
|
+
}
|
|
1334
|
+
const { remainder } = extractQuotedPhrases(queryLower);
|
|
1335
|
+
const tokens = removeStopWords(remainder.split(/\s+/).filter(Boolean));
|
|
1336
|
+
if (tokens.length > 1) {
|
|
1337
|
+
let tokenScore = 0;
|
|
1338
|
+
for (const token of tokens) {
|
|
1339
|
+
if (keyLower === token) {
|
|
1340
|
+
tokenScore += 10 / tokens.length;
|
|
1341
|
+
} else if (keyLower.includes(token)) {
|
|
1342
|
+
tokenScore += 7 / tokens.length;
|
|
1354
1343
|
}
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
if (
|
|
1358
|
-
|
|
1359
|
-
params.push(...filter.source);
|
|
1360
|
-
} else {
|
|
1361
|
-
conditions.push("source = ?");
|
|
1362
|
-
params.push(filter.source);
|
|
1344
|
+
if (memory.tags.some((t) => t.toLowerCase() === token)) {
|
|
1345
|
+
tokenScore += 6 / tokens.length;
|
|
1346
|
+
} else if (memory.tags.some((t) => t.toLowerCase().includes(token))) {
|
|
1347
|
+
tokenScore += 3 / tokens.length;
|
|
1363
1348
|
}
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
if (Array.isArray(filter.status)) {
|
|
1367
|
-
conditions.push(`status IN (${filter.status.map(() => "?").join(",")})`);
|
|
1368
|
-
params.push(...filter.status);
|
|
1369
|
-
} else {
|
|
1370
|
-
conditions.push("status = ?");
|
|
1371
|
-
params.push(filter.status);
|
|
1349
|
+
if (memory.summary && memory.summary.toLowerCase().includes(token)) {
|
|
1350
|
+
tokenScore += 4 / tokens.length;
|
|
1372
1351
|
}
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
params.push(filter.project_id);
|
|
1379
|
-
}
|
|
1380
|
-
if (filter.agent_id) {
|
|
1381
|
-
conditions.push("agent_id = ?");
|
|
1382
|
-
params.push(filter.agent_id);
|
|
1383
|
-
}
|
|
1384
|
-
if (filter.session_id) {
|
|
1385
|
-
conditions.push("session_id = ?");
|
|
1386
|
-
params.push(filter.session_id);
|
|
1387
|
-
}
|
|
1388
|
-
if (filter.min_importance) {
|
|
1389
|
-
conditions.push("importance >= ?");
|
|
1390
|
-
params.push(filter.min_importance);
|
|
1391
|
-
}
|
|
1392
|
-
if (filter.pinned !== undefined) {
|
|
1393
|
-
conditions.push("pinned = ?");
|
|
1394
|
-
params.push(filter.pinned ? 1 : 0);
|
|
1395
|
-
}
|
|
1396
|
-
if (filter.tags && filter.tags.length > 0) {
|
|
1397
|
-
for (const tag of filter.tags) {
|
|
1398
|
-
conditions.push("id IN (SELECT memory_id FROM memory_tags WHERE tag = ?)");
|
|
1399
|
-
params.push(tag);
|
|
1352
|
+
if (memory.value.toLowerCase().includes(token)) {
|
|
1353
|
+
tokenScore += 3 / tokens.length;
|
|
1354
|
+
}
|
|
1355
|
+
if (metadataStr !== "{}" && metadataStr.includes(token)) {
|
|
1356
|
+
tokenScore += 2 / tokens.length;
|
|
1400
1357
|
}
|
|
1401
1358
|
}
|
|
1402
|
-
if (
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1359
|
+
if (score > 0) {
|
|
1360
|
+
score += tokenScore * 0.3;
|
|
1361
|
+
} else {
|
|
1362
|
+
score += tokenScore;
|
|
1406
1363
|
}
|
|
1407
|
-
} else {
|
|
1408
|
-
conditions.push("status = 'active'");
|
|
1409
|
-
}
|
|
1410
|
-
let sql = "SELECT * FROM memories";
|
|
1411
|
-
if (conditions.length > 0) {
|
|
1412
|
-
sql += ` WHERE ${conditions.join(" AND ")}`;
|
|
1413
1364
|
}
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1365
|
+
return score;
|
|
1366
|
+
}
|
|
1367
|
+
function extractQuotedPhrases(query) {
|
|
1368
|
+
const phrases = [];
|
|
1369
|
+
const remainder = query.replace(/"([^"]+)"/g, (_match, phrase) => {
|
|
1370
|
+
phrases.push(phrase);
|
|
1371
|
+
return "";
|
|
1372
|
+
});
|
|
1373
|
+
return { phrases, remainder: remainder.trim() };
|
|
1374
|
+
}
|
|
1375
|
+
function escapeFts5Query(query) {
|
|
1376
|
+
const { phrases, remainder } = extractQuotedPhrases(query);
|
|
1377
|
+
const parts = [];
|
|
1378
|
+
for (const phrase of phrases) {
|
|
1379
|
+
parts.push(`"${phrase.replace(/"/g, '""')}"`);
|
|
1418
1380
|
}
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1381
|
+
const tokens = removeStopWords(remainder.split(/\s+/).filter(Boolean));
|
|
1382
|
+
for (const t of tokens) {
|
|
1383
|
+
parts.push(`"${t.replace(/"/g, '""')}"`);
|
|
1422
1384
|
}
|
|
1423
|
-
|
|
1424
|
-
return rows.map(parseMemoryRow);
|
|
1385
|
+
return parts.join(" ");
|
|
1425
1386
|
}
|
|
1426
|
-
function
|
|
1427
|
-
const d = db || getDatabase();
|
|
1428
|
-
const existing = getMemory(id, d);
|
|
1429
|
-
if (!existing)
|
|
1430
|
-
throw new MemoryNotFoundError(id);
|
|
1431
|
-
if (existing.version !== input.version) {
|
|
1432
|
-
throw new VersionConflictError(id, input.version, existing.version);
|
|
1433
|
-
}
|
|
1387
|
+
function hasFts5Table(d) {
|
|
1434
1388
|
try {
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
existing.version,
|
|
1440
|
-
existing.value,
|
|
1441
|
-
existing.importance,
|
|
1442
|
-
existing.scope,
|
|
1443
|
-
existing.category,
|
|
1444
|
-
JSON.stringify(existing.tags),
|
|
1445
|
-
existing.summary,
|
|
1446
|
-
existing.pinned ? 1 : 0,
|
|
1447
|
-
existing.status,
|
|
1448
|
-
existing.updated_at
|
|
1449
|
-
]);
|
|
1450
|
-
} catch {}
|
|
1451
|
-
const sets = ["version = version + 1", "updated_at = ?"];
|
|
1452
|
-
const params = [now()];
|
|
1453
|
-
if (input.value !== undefined) {
|
|
1454
|
-
sets.push("value = ?");
|
|
1455
|
-
params.push(redactSecrets(input.value));
|
|
1456
|
-
}
|
|
1457
|
-
if (input.category !== undefined) {
|
|
1458
|
-
sets.push("category = ?");
|
|
1459
|
-
params.push(input.category);
|
|
1389
|
+
const row = d.query("SELECT name FROM sqlite_master WHERE type='table' AND name='memories_fts'").get();
|
|
1390
|
+
return !!row;
|
|
1391
|
+
} catch {
|
|
1392
|
+
return false;
|
|
1460
1393
|
}
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1394
|
+
}
|
|
1395
|
+
function buildFilterConditions(filter) {
|
|
1396
|
+
const conditions = [];
|
|
1397
|
+
const params = [];
|
|
1398
|
+
conditions.push("m.status = 'active'");
|
|
1399
|
+
conditions.push("(m.expires_at IS NULL OR m.expires_at >= datetime('now'))");
|
|
1400
|
+
if (!filter)
|
|
1401
|
+
return { conditions, params };
|
|
1402
|
+
if (filter.scope) {
|
|
1403
|
+
if (Array.isArray(filter.scope)) {
|
|
1404
|
+
conditions.push(`m.scope IN (${filter.scope.map(() => "?").join(",")})`);
|
|
1405
|
+
params.push(...filter.scope);
|
|
1406
|
+
} else {
|
|
1407
|
+
conditions.push("m.scope = ?");
|
|
1408
|
+
params.push(filter.scope);
|
|
1409
|
+
}
|
|
1464
1410
|
}
|
|
1465
|
-
if (
|
|
1466
|
-
|
|
1467
|
-
|
|
1411
|
+
if (filter.category) {
|
|
1412
|
+
if (Array.isArray(filter.category)) {
|
|
1413
|
+
conditions.push(`m.category IN (${filter.category.map(() => "?").join(",")})`);
|
|
1414
|
+
params.push(...filter.category);
|
|
1415
|
+
} else {
|
|
1416
|
+
conditions.push("m.category = ?");
|
|
1417
|
+
params.push(filter.category);
|
|
1418
|
+
}
|
|
1468
1419
|
}
|
|
1469
|
-
if (
|
|
1470
|
-
|
|
1471
|
-
|
|
1420
|
+
if (filter.source) {
|
|
1421
|
+
if (Array.isArray(filter.source)) {
|
|
1422
|
+
conditions.push(`m.source IN (${filter.source.map(() => "?").join(",")})`);
|
|
1423
|
+
params.push(...filter.source);
|
|
1424
|
+
} else {
|
|
1425
|
+
conditions.push("m.source = ?");
|
|
1426
|
+
params.push(filter.source);
|
|
1427
|
+
}
|
|
1472
1428
|
}
|
|
1473
|
-
if (
|
|
1474
|
-
|
|
1475
|
-
|
|
1429
|
+
if (filter.status) {
|
|
1430
|
+
conditions.shift();
|
|
1431
|
+
if (Array.isArray(filter.status)) {
|
|
1432
|
+
conditions.push(`m.status IN (${filter.status.map(() => "?").join(",")})`);
|
|
1433
|
+
params.push(...filter.status);
|
|
1434
|
+
} else {
|
|
1435
|
+
conditions.push("m.status = ?");
|
|
1436
|
+
params.push(filter.status);
|
|
1437
|
+
}
|
|
1476
1438
|
}
|
|
1477
|
-
if (
|
|
1478
|
-
|
|
1479
|
-
params.push(
|
|
1439
|
+
if (filter.project_id) {
|
|
1440
|
+
conditions.push("m.project_id = ?");
|
|
1441
|
+
params.push(filter.project_id);
|
|
1480
1442
|
}
|
|
1481
|
-
if (
|
|
1482
|
-
|
|
1483
|
-
params.push(
|
|
1443
|
+
if (filter.agent_id) {
|
|
1444
|
+
conditions.push("m.agent_id = ?");
|
|
1445
|
+
params.push(filter.agent_id);
|
|
1484
1446
|
}
|
|
1485
|
-
if (
|
|
1486
|
-
|
|
1487
|
-
params.push(
|
|
1447
|
+
if (filter.session_id) {
|
|
1448
|
+
conditions.push("m.session_id = ?");
|
|
1449
|
+
params.push(filter.session_id);
|
|
1488
1450
|
}
|
|
1489
|
-
if (
|
|
1490
|
-
|
|
1491
|
-
params.push(
|
|
1492
|
-
d.run("DELETE FROM memory_tags WHERE memory_id = ?", [id]);
|
|
1493
|
-
const insertTag = d.prepare("INSERT OR IGNORE INTO memory_tags (memory_id, tag) VALUES (?, ?)");
|
|
1494
|
-
for (const tag of input.tags) {
|
|
1495
|
-
insertTag.run(id, tag);
|
|
1496
|
-
}
|
|
1451
|
+
if (filter.min_importance) {
|
|
1452
|
+
conditions.push("m.importance >= ?");
|
|
1453
|
+
params.push(filter.min_importance);
|
|
1497
1454
|
}
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
}
|
|
1507
|
-
runEntityExtraction(updated, existing.project_id || undefined, d);
|
|
1455
|
+
if (filter.pinned !== undefined) {
|
|
1456
|
+
conditions.push("m.pinned = ?");
|
|
1457
|
+
params.push(filter.pinned ? 1 : 0);
|
|
1458
|
+
}
|
|
1459
|
+
if (filter.tags && filter.tags.length > 0) {
|
|
1460
|
+
for (const tag of filter.tags) {
|
|
1461
|
+
conditions.push("m.id IN (SELECT memory_id FROM memory_tags WHERE tag = ?)");
|
|
1462
|
+
params.push(tag);
|
|
1508
1463
|
}
|
|
1509
|
-
} catch {}
|
|
1510
|
-
return updated;
|
|
1511
|
-
}
|
|
1512
|
-
function deleteMemory(id, db) {
|
|
1513
|
-
const d = db || getDatabase();
|
|
1514
|
-
const result = d.run("DELETE FROM memories WHERE id = ?", [id]);
|
|
1515
|
-
return result.changes > 0;
|
|
1516
|
-
}
|
|
1517
|
-
function touchMemory(id, db) {
|
|
1518
|
-
const d = db || getDatabase();
|
|
1519
|
-
d.run("UPDATE memories SET access_count = access_count + 1, accessed_at = ? WHERE id = ?", [now(), id]);
|
|
1520
|
-
}
|
|
1521
|
-
function cleanExpiredMemories(db) {
|
|
1522
|
-
const d = db || getDatabase();
|
|
1523
|
-
const timestamp = now();
|
|
1524
|
-
const countRow = d.query("SELECT COUNT(*) as c FROM memories WHERE expires_at IS NOT NULL AND expires_at < ?").get(timestamp);
|
|
1525
|
-
const count = countRow.c;
|
|
1526
|
-
if (count > 0) {
|
|
1527
|
-
d.run("DELETE FROM memories WHERE expires_at IS NOT NULL AND expires_at < ?", [timestamp]);
|
|
1528
1464
|
}
|
|
1529
|
-
return
|
|
1465
|
+
return { conditions, params };
|
|
1530
1466
|
}
|
|
1531
|
-
function
|
|
1532
|
-
const
|
|
1467
|
+
function searchWithFts5(d, query, queryLower, filter, graphBoostedIds) {
|
|
1468
|
+
const ftsQuery = escapeFts5Query(query);
|
|
1469
|
+
if (!ftsQuery)
|
|
1470
|
+
return null;
|
|
1533
1471
|
try {
|
|
1534
|
-
const
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
category: row["category"],
|
|
1543
|
-
tags: JSON.parse(row["tags"] || "[]"),
|
|
1544
|
-
summary: row["summary"] || null,
|
|
1545
|
-
pinned: !!row["pinned"],
|
|
1546
|
-
status: row["status"],
|
|
1547
|
-
created_at: row["created_at"]
|
|
1548
|
-
}));
|
|
1472
|
+
const { conditions, params } = buildFilterConditions(filter);
|
|
1473
|
+
const queryParam = `%${query}%`;
|
|
1474
|
+
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 ?)`;
|
|
1475
|
+
const allConditions = [ftsCondition, ...conditions];
|
|
1476
|
+
const allParams = [ftsQuery, queryParam, queryParam, ...params];
|
|
1477
|
+
const candidateSql = `SELECT m.* FROM memories m WHERE ${allConditions.join(" AND ")}`;
|
|
1478
|
+
const rows = d.query(candidateSql).all(...allParams);
|
|
1479
|
+
return scoreResults(rows, queryLower, graphBoostedIds);
|
|
1549
1480
|
} catch {
|
|
1550
|
-
return
|
|
1481
|
+
return null;
|
|
1551
1482
|
}
|
|
1552
1483
|
}
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1484
|
+
function searchWithLike(d, query, queryLower, filter, graphBoostedIds) {
|
|
1485
|
+
const { conditions, params } = buildFilterConditions(filter);
|
|
1486
|
+
const rawTokens = query.trim().split(/\s+/).filter(Boolean);
|
|
1487
|
+
const tokens = removeStopWords(rawTokens);
|
|
1488
|
+
const escapedQuery = escapeLikePattern(query);
|
|
1489
|
+
const likePatterns = [`%${escapedQuery}%`];
|
|
1490
|
+
if (tokens.length > 1) {
|
|
1491
|
+
for (const t of tokens)
|
|
1492
|
+
likePatterns.push(`%${escapeLikePattern(t)}%`);
|
|
1493
|
+
}
|
|
1494
|
+
const fieldClauses = [];
|
|
1495
|
+
for (const pattern of likePatterns) {
|
|
1496
|
+
fieldClauses.push("m.key LIKE ? ESCAPE '\\'");
|
|
1497
|
+
params.push(pattern);
|
|
1498
|
+
fieldClauses.push("m.value LIKE ? ESCAPE '\\'");
|
|
1499
|
+
params.push(pattern);
|
|
1500
|
+
fieldClauses.push("m.summary LIKE ? ESCAPE '\\'");
|
|
1501
|
+
params.push(pattern);
|
|
1502
|
+
fieldClauses.push("m.metadata LIKE ? ESCAPE '\\'");
|
|
1503
|
+
params.push(pattern);
|
|
1504
|
+
fieldClauses.push("m.id IN (SELECT memory_id FROM memory_tags WHERE tag LIKE ? ESCAPE '\\')");
|
|
1505
|
+
params.push(pattern);
|
|
1506
|
+
}
|
|
1507
|
+
conditions.push(`(${fieldClauses.join(" OR ")})`);
|
|
1508
|
+
const sql = `SELECT DISTINCT m.* FROM memories m WHERE ${conditions.join(" AND ")}`;
|
|
1509
|
+
const rows = d.query(sql).all(...params);
|
|
1510
|
+
return scoreResults(rows, queryLower, graphBoostedIds);
|
|
1565
1511
|
}
|
|
1566
|
-
function
|
|
1567
|
-
const
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
const newExpiry = new Date(Date.now() + ttlSeconds * 1000).toISOString();
|
|
1572
|
-
d.run("UPDATE resource_locks SET expires_at = ? WHERE id = ?", [
|
|
1573
|
-
newExpiry,
|
|
1574
|
-
ownLock["id"]
|
|
1575
|
-
]);
|
|
1576
|
-
return parseLockRow({ ...ownLock, expires_at: newExpiry });
|
|
1512
|
+
function generateTrigrams(s) {
|
|
1513
|
+
const lower = s.toLowerCase();
|
|
1514
|
+
const trigrams = new Set;
|
|
1515
|
+
for (let i = 0;i <= lower.length - 3; i++) {
|
|
1516
|
+
trigrams.add(lower.slice(i, i + 3));
|
|
1577
1517
|
}
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1518
|
+
return trigrams;
|
|
1519
|
+
}
|
|
1520
|
+
function trigramSimilarity(a, b) {
|
|
1521
|
+
const triA = generateTrigrams(a);
|
|
1522
|
+
const triB = generateTrigrams(b);
|
|
1523
|
+
if (triA.size === 0 || triB.size === 0)
|
|
1524
|
+
return 0;
|
|
1525
|
+
let intersection = 0;
|
|
1526
|
+
for (const t of triA) {
|
|
1527
|
+
if (triB.has(t))
|
|
1528
|
+
intersection++;
|
|
1529
|
+
}
|
|
1530
|
+
const union = triA.size + triB.size - intersection;
|
|
1531
|
+
return union === 0 ? 0 : intersection / union;
|
|
1532
|
+
}
|
|
1533
|
+
function searchWithFuzzy(d, query, filter, graphBoostedIds) {
|
|
1534
|
+
const { conditions, params } = buildFilterConditions(filter);
|
|
1535
|
+
const sql = `SELECT m.* FROM memories m WHERE ${conditions.join(" AND ")}`;
|
|
1536
|
+
const rows = d.query(sql).all(...params);
|
|
1537
|
+
const MIN_SIMILARITY = 0.3;
|
|
1538
|
+
const results = [];
|
|
1539
|
+
for (const row of rows) {
|
|
1540
|
+
const memory = parseMemoryRow2(row);
|
|
1541
|
+
let bestSimilarity = 0;
|
|
1542
|
+
bestSimilarity = Math.max(bestSimilarity, trigramSimilarity(query, memory.key));
|
|
1543
|
+
bestSimilarity = Math.max(bestSimilarity, trigramSimilarity(query, memory.value.slice(0, 200)));
|
|
1544
|
+
if (memory.summary) {
|
|
1545
|
+
bestSimilarity = Math.max(bestSimilarity, trigramSimilarity(query, memory.summary));
|
|
1546
|
+
}
|
|
1547
|
+
for (const tag of memory.tags) {
|
|
1548
|
+
bestSimilarity = Math.max(bestSimilarity, trigramSimilarity(query, tag));
|
|
1549
|
+
}
|
|
1550
|
+
if (bestSimilarity >= MIN_SIMILARITY) {
|
|
1551
|
+
const graphBoost = graphBoostedIds?.has(memory.id) ? 2 : 0;
|
|
1552
|
+
const score = bestSimilarity * 5 * memory.importance / 10 + graphBoost;
|
|
1553
|
+
results.push({ memory, score, match_type: "fuzzy" });
|
|
1582
1554
|
}
|
|
1583
1555
|
}
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
const expiresAt = new Date(Date.now() + ttlSeconds * 1000).toISOString();
|
|
1587
|
-
d.run("INSERT INTO resource_locks (id, resource_type, resource_id, agent_id, lock_type, locked_at, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?)", [id, resourceType, resourceId, agentId, lockType, lockedAt, expiresAt]);
|
|
1588
|
-
return {
|
|
1589
|
-
id,
|
|
1590
|
-
resource_type: resourceType,
|
|
1591
|
-
resource_id: resourceId,
|
|
1592
|
-
agent_id: agentId,
|
|
1593
|
-
lock_type: lockType,
|
|
1594
|
-
locked_at: lockedAt,
|
|
1595
|
-
expires_at: expiresAt
|
|
1596
|
-
};
|
|
1556
|
+
results.sort((a, b) => b.score - a.score);
|
|
1557
|
+
return results;
|
|
1597
1558
|
}
|
|
1598
|
-
function
|
|
1599
|
-
const
|
|
1600
|
-
|
|
1601
|
-
|
|
1559
|
+
function getGraphBoostedMemoryIds(query, d) {
|
|
1560
|
+
const boostedIds = new Set;
|
|
1561
|
+
try {
|
|
1562
|
+
const matchingEntities = listEntities({ search: query, limit: 10 }, d);
|
|
1563
|
+
const exactMatch = getEntityByName(query, undefined, undefined, d);
|
|
1564
|
+
if (exactMatch && !matchingEntities.find((e) => e.id === exactMatch.id)) {
|
|
1565
|
+
matchingEntities.push(exactMatch);
|
|
1566
|
+
}
|
|
1567
|
+
for (const entity of matchingEntities) {
|
|
1568
|
+
const memories = getMemoriesForEntity(entity.id, d);
|
|
1569
|
+
for (const mem of memories) {
|
|
1570
|
+
boostedIds.add(mem.id);
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
} catch {}
|
|
1574
|
+
return boostedIds;
|
|
1602
1575
|
}
|
|
1603
|
-
function
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1576
|
+
function computeRecencyBoost(memory) {
|
|
1577
|
+
if (memory.pinned)
|
|
1578
|
+
return 1;
|
|
1579
|
+
const mostRecent = memory.accessed_at || memory.updated_at;
|
|
1580
|
+
if (!mostRecent)
|
|
1581
|
+
return 0;
|
|
1582
|
+
const daysSinceAccess = (Date.now() - Date.parse(mostRecent)) / (1000 * 60 * 60 * 24);
|
|
1583
|
+
return Math.max(0, 1 - daysSinceAccess / 30);
|
|
1607
1584
|
}
|
|
1608
|
-
function
|
|
1609
|
-
const
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1585
|
+
function scoreResults(rows, queryLower, graphBoostedIds) {
|
|
1586
|
+
const scored = [];
|
|
1587
|
+
for (const row of rows) {
|
|
1588
|
+
const memory = parseMemoryRow2(row);
|
|
1589
|
+
const rawScore = computeScore(memory, queryLower);
|
|
1590
|
+
if (rawScore === 0)
|
|
1591
|
+
continue;
|
|
1592
|
+
const weightedScore = rawScore * memory.importance / 10;
|
|
1593
|
+
const recencyBoost = computeRecencyBoost(memory);
|
|
1594
|
+
const accessBoost = Math.min(memory.access_count / 20, 0.2);
|
|
1595
|
+
const graphBoost = graphBoostedIds?.has(memory.id) ? 2 : 0;
|
|
1596
|
+
const finalScore = (weightedScore + graphBoost) * (1 + recencyBoost * 0.3) * (1 + accessBoost);
|
|
1597
|
+
const matchType = determineMatchType(memory, queryLower);
|
|
1598
|
+
scored.push({
|
|
1599
|
+
memory,
|
|
1600
|
+
score: finalScore,
|
|
1601
|
+
match_type: matchType,
|
|
1602
|
+
highlights: extractHighlights(memory, queryLower)
|
|
1603
|
+
});
|
|
1604
|
+
}
|
|
1605
|
+
scored.sort((a, b) => {
|
|
1606
|
+
if (b.score !== a.score)
|
|
1607
|
+
return b.score - a.score;
|
|
1608
|
+
return b.memory.importance - a.memory.importance;
|
|
1609
|
+
});
|
|
1610
|
+
return scored;
|
|
1614
1611
|
}
|
|
1615
|
-
function
|
|
1612
|
+
function searchMemories(query, filter, db) {
|
|
1616
1613
|
const d = db || getDatabase();
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1614
|
+
query = preprocessQuery(query);
|
|
1615
|
+
if (!query)
|
|
1616
|
+
return [];
|
|
1617
|
+
const queryLower = query.toLowerCase();
|
|
1618
|
+
const graphBoostedIds = getGraphBoostedMemoryIds(query, d);
|
|
1619
|
+
let scored;
|
|
1620
|
+
if (hasFts5Table(d)) {
|
|
1621
|
+
const ftsResult = searchWithFts5(d, query, queryLower, filter, graphBoostedIds);
|
|
1622
|
+
if (ftsResult !== null) {
|
|
1623
|
+
scored = ftsResult;
|
|
1624
|
+
} else {
|
|
1625
|
+
scored = searchWithLike(d, query, queryLower, filter, graphBoostedIds);
|
|
1626
|
+
}
|
|
1627
|
+
} else {
|
|
1628
|
+
scored = searchWithLike(d, query, queryLower, filter, graphBoostedIds);
|
|
1629
|
+
}
|
|
1630
|
+
if (scored.length < 3) {
|
|
1631
|
+
const fuzzyResults = searchWithFuzzy(d, query, filter, graphBoostedIds);
|
|
1632
|
+
const seenIds = new Set(scored.map((r) => r.memory.id));
|
|
1633
|
+
for (const fr of fuzzyResults) {
|
|
1634
|
+
if (!seenIds.has(fr.memory.id)) {
|
|
1635
|
+
scored.push(fr);
|
|
1636
|
+
seenIds.add(fr.memory.id);
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
scored.sort((a, b) => {
|
|
1640
|
+
if (b.score !== a.score)
|
|
1641
|
+
return b.score - a.score;
|
|
1642
|
+
return b.memory.importance - a.memory.importance;
|
|
1643
|
+
});
|
|
1644
|
+
}
|
|
1645
|
+
const offset = filter?.offset ?? 0;
|
|
1646
|
+
const limit = filter?.limit ?? scored.length;
|
|
1647
|
+
const finalResults = scored.slice(offset, offset + limit);
|
|
1648
|
+
if (finalResults.length > 0 && scored.length > 0) {
|
|
1649
|
+
const topScore = scored[0]?.score ?? 0;
|
|
1650
|
+
const secondScore = scored[1]?.score ?? 0;
|
|
1651
|
+
const confidence = topScore > 0 ? Math.max(0, Math.min(1, (topScore - secondScore) / topScore)) : 0;
|
|
1652
|
+
finalResults[0] = { ...finalResults[0], confidence };
|
|
1653
|
+
}
|
|
1654
|
+
logSearchQuery(query, scored.length, filter?.agent_id, filter?.project_id, d);
|
|
1655
|
+
return finalResults;
|
|
1620
1656
|
}
|
|
1621
|
-
function
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1657
|
+
function logSearchQuery(query, resultCount, agentId, projectId, db) {
|
|
1658
|
+
try {
|
|
1659
|
+
const d = db || getDatabase();
|
|
1660
|
+
const id = crypto.randomUUID().slice(0, 8);
|
|
1661
|
+
d.run("INSERT INTO search_history (id, query, result_count, agent_id, project_id) VALUES (?, ?, ?, ?, ?)", [id, query, resultCount, agentId || null, projectId || null]);
|
|
1662
|
+
} catch {}
|
|
1625
1663
|
}
|
|
1664
|
+
var STOP_WORDS;
|
|
1665
|
+
var init_search = __esm(() => {
|
|
1666
|
+
init_database();
|
|
1667
|
+
init_entities();
|
|
1668
|
+
init_entity_memories();
|
|
1669
|
+
STOP_WORDS = new Set([
|
|
1670
|
+
"a",
|
|
1671
|
+
"an",
|
|
1672
|
+
"the",
|
|
1673
|
+
"is",
|
|
1674
|
+
"are",
|
|
1675
|
+
"was",
|
|
1676
|
+
"were",
|
|
1677
|
+
"be",
|
|
1678
|
+
"been",
|
|
1679
|
+
"being",
|
|
1680
|
+
"have",
|
|
1681
|
+
"has",
|
|
1682
|
+
"had",
|
|
1683
|
+
"do",
|
|
1684
|
+
"does",
|
|
1685
|
+
"did",
|
|
1686
|
+
"will",
|
|
1687
|
+
"would",
|
|
1688
|
+
"could",
|
|
1689
|
+
"should",
|
|
1690
|
+
"may",
|
|
1691
|
+
"might",
|
|
1692
|
+
"shall",
|
|
1693
|
+
"can",
|
|
1694
|
+
"need",
|
|
1695
|
+
"dare",
|
|
1696
|
+
"ought",
|
|
1697
|
+
"used",
|
|
1698
|
+
"to",
|
|
1699
|
+
"of",
|
|
1700
|
+
"in",
|
|
1701
|
+
"for",
|
|
1702
|
+
"on",
|
|
1703
|
+
"with",
|
|
1704
|
+
"at",
|
|
1705
|
+
"by",
|
|
1706
|
+
"from",
|
|
1707
|
+
"as",
|
|
1708
|
+
"into",
|
|
1709
|
+
"through",
|
|
1710
|
+
"during",
|
|
1711
|
+
"before",
|
|
1712
|
+
"after",
|
|
1713
|
+
"above",
|
|
1714
|
+
"below",
|
|
1715
|
+
"between",
|
|
1716
|
+
"out",
|
|
1717
|
+
"off",
|
|
1718
|
+
"over",
|
|
1719
|
+
"under",
|
|
1720
|
+
"again",
|
|
1721
|
+
"further",
|
|
1722
|
+
"then",
|
|
1723
|
+
"once",
|
|
1724
|
+
"here",
|
|
1725
|
+
"there",
|
|
1726
|
+
"when",
|
|
1727
|
+
"where",
|
|
1728
|
+
"why",
|
|
1729
|
+
"how",
|
|
1730
|
+
"all",
|
|
1731
|
+
"each",
|
|
1732
|
+
"every",
|
|
1733
|
+
"both",
|
|
1734
|
+
"few",
|
|
1735
|
+
"more",
|
|
1736
|
+
"most",
|
|
1737
|
+
"other",
|
|
1738
|
+
"some",
|
|
1739
|
+
"such",
|
|
1740
|
+
"no",
|
|
1741
|
+
"not",
|
|
1742
|
+
"only",
|
|
1743
|
+
"own",
|
|
1744
|
+
"same",
|
|
1745
|
+
"so",
|
|
1746
|
+
"than",
|
|
1747
|
+
"too",
|
|
1748
|
+
"very",
|
|
1749
|
+
"just",
|
|
1750
|
+
"because",
|
|
1751
|
+
"but",
|
|
1752
|
+
"and",
|
|
1753
|
+
"or",
|
|
1754
|
+
"if",
|
|
1755
|
+
"while",
|
|
1756
|
+
"that",
|
|
1757
|
+
"this",
|
|
1758
|
+
"it"
|
|
1759
|
+
]);
|
|
1760
|
+
});
|
|
1626
1761
|
|
|
1627
|
-
// src/
|
|
1628
|
-
function
|
|
1762
|
+
// src/db/relations.ts
|
|
1763
|
+
function parseRelationRow(row) {
|
|
1629
1764
|
return {
|
|
1630
1765
|
id: row["id"],
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
summary: row["summary"] || null,
|
|
1636
|
-
tags: JSON.parse(row["tags"] || "[]"),
|
|
1637
|
-
importance: row["importance"],
|
|
1638
|
-
source: row["source"],
|
|
1639
|
-
status: row["status"],
|
|
1640
|
-
pinned: !!row["pinned"],
|
|
1641
|
-
agent_id: row["agent_id"] || null,
|
|
1642
|
-
project_id: row["project_id"] || null,
|
|
1643
|
-
session_id: row["session_id"] || null,
|
|
1766
|
+
source_entity_id: row["source_entity_id"],
|
|
1767
|
+
target_entity_id: row["target_entity_id"],
|
|
1768
|
+
relation_type: row["relation_type"],
|
|
1769
|
+
weight: row["weight"],
|
|
1644
1770
|
metadata: JSON.parse(row["metadata"] || "{}"),
|
|
1645
|
-
|
|
1646
|
-
version: row["version"],
|
|
1647
|
-
expires_at: row["expires_at"] || null,
|
|
1648
|
-
created_at: row["created_at"],
|
|
1649
|
-
updated_at: row["updated_at"],
|
|
1650
|
-
accessed_at: row["accessed_at"] || null
|
|
1771
|
+
created_at: row["created_at"]
|
|
1651
1772
|
};
|
|
1652
1773
|
}
|
|
1653
|
-
function
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1774
|
+
function parseEntityRow2(row) {
|
|
1775
|
+
return {
|
|
1776
|
+
id: row["id"],
|
|
1777
|
+
name: row["name"],
|
|
1778
|
+
type: row["type"],
|
|
1779
|
+
description: row["description"] || null,
|
|
1780
|
+
metadata: JSON.parse(row["metadata"] || "{}"),
|
|
1781
|
+
project_id: row["project_id"] || null,
|
|
1782
|
+
created_at: row["created_at"],
|
|
1783
|
+
updated_at: row["updated_at"]
|
|
1784
|
+
};
|
|
1658
1785
|
}
|
|
1659
|
-
function
|
|
1660
|
-
|
|
1786
|
+
function createRelation(input, db) {
|
|
1787
|
+
const d = db || getDatabase();
|
|
1788
|
+
const id = shortUuid();
|
|
1789
|
+
const timestamp = now();
|
|
1790
|
+
const weight = input.weight ?? 1;
|
|
1791
|
+
const metadata = JSON.stringify(input.metadata ?? {});
|
|
1792
|
+
d.run(`INSERT INTO relations (id, source_entity_id, target_entity_id, relation_type, weight, metadata, created_at)
|
|
1793
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
1794
|
+
ON CONFLICT(source_entity_id, target_entity_id, relation_type)
|
|
1795
|
+
DO UPDATE SET weight = excluded.weight, metadata = excluded.metadata`, [id, input.source_entity_id, input.target_entity_id, input.relation_type, weight, metadata, timestamp]);
|
|
1796
|
+
const row = d.query(`SELECT * FROM relations
|
|
1797
|
+
WHERE source_entity_id = ? AND target_entity_id = ? AND relation_type = ?`).get(input.source_entity_id, input.target_entity_id, input.relation_type);
|
|
1798
|
+
const relation = parseRelationRow(row);
|
|
1799
|
+
hookRegistry.runHooks("PostRelationCreate", {
|
|
1800
|
+
relationId: relation.id,
|
|
1801
|
+
sourceEntityId: relation.source_entity_id,
|
|
1802
|
+
targetEntityId: relation.target_entity_id,
|
|
1803
|
+
relationType: relation.relation_type,
|
|
1804
|
+
timestamp: Date.now()
|
|
1805
|
+
});
|
|
1806
|
+
return relation;
|
|
1661
1807
|
}
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
"
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
"was",
|
|
1669
|
-
"were",
|
|
1670
|
-
"be",
|
|
1671
|
-
"been",
|
|
1672
|
-
"being",
|
|
1673
|
-
"have",
|
|
1674
|
-
"has",
|
|
1675
|
-
"had",
|
|
1676
|
-
"do",
|
|
1677
|
-
"does",
|
|
1678
|
-
"did",
|
|
1679
|
-
"will",
|
|
1680
|
-
"would",
|
|
1681
|
-
"could",
|
|
1682
|
-
"should",
|
|
1683
|
-
"may",
|
|
1684
|
-
"might",
|
|
1685
|
-
"shall",
|
|
1686
|
-
"can",
|
|
1687
|
-
"need",
|
|
1688
|
-
"dare",
|
|
1689
|
-
"ought",
|
|
1690
|
-
"used",
|
|
1691
|
-
"to",
|
|
1692
|
-
"of",
|
|
1693
|
-
"in",
|
|
1694
|
-
"for",
|
|
1695
|
-
"on",
|
|
1696
|
-
"with",
|
|
1697
|
-
"at",
|
|
1698
|
-
"by",
|
|
1699
|
-
"from",
|
|
1700
|
-
"as",
|
|
1701
|
-
"into",
|
|
1702
|
-
"through",
|
|
1703
|
-
"during",
|
|
1704
|
-
"before",
|
|
1705
|
-
"after",
|
|
1706
|
-
"above",
|
|
1707
|
-
"below",
|
|
1708
|
-
"between",
|
|
1709
|
-
"out",
|
|
1710
|
-
"off",
|
|
1711
|
-
"over",
|
|
1712
|
-
"under",
|
|
1713
|
-
"again",
|
|
1714
|
-
"further",
|
|
1715
|
-
"then",
|
|
1716
|
-
"once",
|
|
1717
|
-
"here",
|
|
1718
|
-
"there",
|
|
1719
|
-
"when",
|
|
1720
|
-
"where",
|
|
1721
|
-
"why",
|
|
1722
|
-
"how",
|
|
1723
|
-
"all",
|
|
1724
|
-
"each",
|
|
1725
|
-
"every",
|
|
1726
|
-
"both",
|
|
1727
|
-
"few",
|
|
1728
|
-
"more",
|
|
1729
|
-
"most",
|
|
1730
|
-
"other",
|
|
1731
|
-
"some",
|
|
1732
|
-
"such",
|
|
1733
|
-
"no",
|
|
1734
|
-
"not",
|
|
1735
|
-
"only",
|
|
1736
|
-
"own",
|
|
1737
|
-
"same",
|
|
1738
|
-
"so",
|
|
1739
|
-
"than",
|
|
1740
|
-
"too",
|
|
1741
|
-
"very",
|
|
1742
|
-
"just",
|
|
1743
|
-
"because",
|
|
1744
|
-
"but",
|
|
1745
|
-
"and",
|
|
1746
|
-
"or",
|
|
1747
|
-
"if",
|
|
1748
|
-
"while",
|
|
1749
|
-
"that",
|
|
1750
|
-
"this",
|
|
1751
|
-
"it"
|
|
1752
|
-
]);
|
|
1753
|
-
function removeStopWords(tokens) {
|
|
1754
|
-
if (tokens.length <= 1)
|
|
1755
|
-
return tokens;
|
|
1756
|
-
const filtered = tokens.filter((t) => !STOP_WORDS.has(t.toLowerCase()));
|
|
1757
|
-
return filtered.length > 0 ? filtered : tokens;
|
|
1808
|
+
function getRelation(id, db) {
|
|
1809
|
+
const d = db || getDatabase();
|
|
1810
|
+
const row = d.query("SELECT * FROM relations WHERE id = ?").get(id);
|
|
1811
|
+
if (!row)
|
|
1812
|
+
throw new Error(`Relation not found: ${id}`);
|
|
1813
|
+
return parseRelationRow(row);
|
|
1758
1814
|
}
|
|
1759
|
-
function
|
|
1760
|
-
const
|
|
1761
|
-
const
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
const suffix = end < text.length ? "..." : "";
|
|
1775
|
-
highlights.push({
|
|
1776
|
-
field,
|
|
1777
|
-
snippet: prefix + text.slice(start, end) + suffix
|
|
1778
|
-
});
|
|
1779
|
-
break;
|
|
1780
|
-
}
|
|
1815
|
+
function listRelations(filter, db) {
|
|
1816
|
+
const d = db || getDatabase();
|
|
1817
|
+
const conditions = [];
|
|
1818
|
+
const params = [];
|
|
1819
|
+
if (filter.entity_id) {
|
|
1820
|
+
const dir = filter.direction || "both";
|
|
1821
|
+
if (dir === "outgoing") {
|
|
1822
|
+
conditions.push("source_entity_id = ?");
|
|
1823
|
+
params.push(filter.entity_id);
|
|
1824
|
+
} else if (dir === "incoming") {
|
|
1825
|
+
conditions.push("target_entity_id = ?");
|
|
1826
|
+
params.push(filter.entity_id);
|
|
1827
|
+
} else {
|
|
1828
|
+
conditions.push("(source_entity_id = ? OR target_entity_id = ?)");
|
|
1829
|
+
params.push(filter.entity_id, filter.entity_id);
|
|
1781
1830
|
}
|
|
1782
1831
|
}
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
}
|
|
1832
|
+
if (filter.relation_type) {
|
|
1833
|
+
conditions.push("relation_type = ?");
|
|
1834
|
+
params.push(filter.relation_type);
|
|
1787
1835
|
}
|
|
1788
|
-
|
|
1836
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
1837
|
+
const rows = d.query(`SELECT * FROM relations ${where} ORDER BY created_at DESC`).all(...params);
|
|
1838
|
+
return rows.map(parseRelationRow);
|
|
1789
1839
|
}
|
|
1790
|
-
function
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
if (
|
|
1794
|
-
|
|
1795
|
-
if (memory.tags.some((t) => t.toLowerCase().includes(queryLower)))
|
|
1796
|
-
return "tag";
|
|
1797
|
-
return "fuzzy";
|
|
1840
|
+
function deleteRelation(id, db) {
|
|
1841
|
+
const d = db || getDatabase();
|
|
1842
|
+
const result = d.run("DELETE FROM relations WHERE id = ?", [id]);
|
|
1843
|
+
if (result.changes === 0)
|
|
1844
|
+
throw new Error(`Relation not found: ${id}`);
|
|
1798
1845
|
}
|
|
1799
|
-
function
|
|
1800
|
-
const
|
|
1801
|
-
const
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1846
|
+
function getEntityGraph(entityId, depth = 2, db) {
|
|
1847
|
+
const d = db || getDatabase();
|
|
1848
|
+
const entityRows = d.query(`WITH RECURSIVE graph(id, depth) AS (
|
|
1849
|
+
VALUES(?, 0)
|
|
1850
|
+
UNION
|
|
1851
|
+
SELECT CASE WHEN r.source_entity_id = g.id THEN r.target_entity_id ELSE r.source_entity_id END, g.depth + 1
|
|
1852
|
+
FROM relations r JOIN graph g ON (r.source_entity_id = g.id OR r.target_entity_id = g.id)
|
|
1853
|
+
WHERE g.depth < ?
|
|
1854
|
+
)
|
|
1855
|
+
SELECT DISTINCT e.* FROM entities e JOIN graph g ON e.id = g.id`).all(entityId, depth);
|
|
1856
|
+
const entities = entityRows.map(parseEntityRow2);
|
|
1857
|
+
const entityIds = new Set(entities.map((e) => e.id));
|
|
1858
|
+
if (entityIds.size === 0) {
|
|
1859
|
+
return { entities: [], relations: [] };
|
|
1811
1860
|
}
|
|
1812
|
-
|
|
1813
|
-
|
|
1861
|
+
const placeholders = Array.from(entityIds).map(() => "?").join(",");
|
|
1862
|
+
const relationRows = d.query(`SELECT * FROM relations
|
|
1863
|
+
WHERE source_entity_id IN (${placeholders})
|
|
1864
|
+
AND target_entity_id IN (${placeholders})`).all(...Array.from(entityIds), ...Array.from(entityIds));
|
|
1865
|
+
const relations = relationRows.map(parseRelationRow);
|
|
1866
|
+
return { entities, relations };
|
|
1867
|
+
}
|
|
1868
|
+
function findPath(fromEntityId, toEntityId, maxDepth = 5, db) {
|
|
1869
|
+
const d = db || getDatabase();
|
|
1870
|
+
const rows = d.query(`WITH RECURSIVE path(id, trail, depth) AS (
|
|
1871
|
+
SELECT ?, ?, 0
|
|
1872
|
+
UNION
|
|
1873
|
+
SELECT
|
|
1874
|
+
CASE WHEN r.source_entity_id = p.id THEN r.target_entity_id ELSE r.source_entity_id END,
|
|
1875
|
+
p.trail || ',' || CASE WHEN r.source_entity_id = p.id THEN r.target_entity_id ELSE r.source_entity_id END,
|
|
1876
|
+
p.depth + 1
|
|
1877
|
+
FROM relations r JOIN path p ON (r.source_entity_id = p.id OR r.target_entity_id = p.id)
|
|
1878
|
+
WHERE p.depth < ?
|
|
1879
|
+
AND INSTR(p.trail, CASE WHEN r.source_entity_id = p.id THEN r.target_entity_id ELSE r.source_entity_id END) = 0
|
|
1880
|
+
)
|
|
1881
|
+
SELECT trail FROM path WHERE id = ? ORDER BY depth ASC LIMIT 1`).get(fromEntityId, fromEntityId, maxDepth, toEntityId);
|
|
1882
|
+
if (!rows)
|
|
1883
|
+
return null;
|
|
1884
|
+
const ids = rows.trail.split(",");
|
|
1885
|
+
const entities = [];
|
|
1886
|
+
for (const id of ids) {
|
|
1887
|
+
const row = d.query("SELECT * FROM entities WHERE id = ?").get(id);
|
|
1888
|
+
if (row)
|
|
1889
|
+
entities.push(parseEntityRow2(row));
|
|
1814
1890
|
}
|
|
1815
|
-
|
|
1816
|
-
|
|
1891
|
+
return entities.length > 0 ? entities : null;
|
|
1892
|
+
}
|
|
1893
|
+
var init_relations = __esm(() => {
|
|
1894
|
+
init_database();
|
|
1895
|
+
init_hooks();
|
|
1896
|
+
});
|
|
1897
|
+
|
|
1898
|
+
// src/lib/providers/base.ts
|
|
1899
|
+
class BaseProvider {
|
|
1900
|
+
config;
|
|
1901
|
+
constructor(config) {
|
|
1902
|
+
this.config = config;
|
|
1817
1903
|
}
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1904
|
+
parseJSON(raw) {
|
|
1905
|
+
try {
|
|
1906
|
+
const cleaned = raw.replace(/^```(?:json)?\s*/m, "").replace(/\s*```$/m, "").trim();
|
|
1907
|
+
return JSON.parse(cleaned);
|
|
1908
|
+
} catch {
|
|
1909
|
+
return null;
|
|
1910
|
+
}
|
|
1821
1911
|
}
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1912
|
+
clampImportance(value) {
|
|
1913
|
+
const n = Number(value);
|
|
1914
|
+
if (isNaN(n))
|
|
1915
|
+
return 5;
|
|
1916
|
+
return Math.max(0, Math.min(10, Math.round(n)));
|
|
1827
1917
|
}
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
if (
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1918
|
+
normaliseMemory(raw) {
|
|
1919
|
+
if (!raw || typeof raw !== "object")
|
|
1920
|
+
return null;
|
|
1921
|
+
const m = raw;
|
|
1922
|
+
if (typeof m.content !== "string" || !m.content.trim())
|
|
1923
|
+
return null;
|
|
1924
|
+
const validScopes = ["private", "shared", "global"];
|
|
1925
|
+
const validCategories = [
|
|
1926
|
+
"preference",
|
|
1927
|
+
"fact",
|
|
1928
|
+
"knowledge",
|
|
1929
|
+
"history"
|
|
1930
|
+
];
|
|
1931
|
+
return {
|
|
1932
|
+
content: m.content.trim(),
|
|
1933
|
+
category: validCategories.includes(m.category) ? m.category : "knowledge",
|
|
1934
|
+
importance: this.clampImportance(m.importance),
|
|
1935
|
+
tags: Array.isArray(m.tags) ? m.tags.filter((t) => typeof t === "string").map((t) => t.toLowerCase()) : [],
|
|
1936
|
+
suggestedScope: validScopes.includes(m.suggestedScope) ? m.suggestedScope : "shared",
|
|
1937
|
+
reasoning: typeof m.reasoning === "string" ? m.reasoning : undefined
|
|
1938
|
+
};
|
|
1836
1939
|
}
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1940
|
+
}
|
|
1941
|
+
var DEFAULT_AUTO_MEMORY_CONFIG, MEMORY_EXTRACTION_SYSTEM_PROMPT = `You are a precise memory extraction engine for an AI agent.
|
|
1942
|
+
Given text, extract facts worth remembering as structured JSON.
|
|
1943
|
+
Focus on: decisions made, preferences revealed, corrections, architectural choices, established facts, user preferences.
|
|
1944
|
+
Ignore: greetings, filler, questions without answers, temporary states.
|
|
1945
|
+
Output ONLY a JSON array \u2014 no markdown, no explanation.`, MEMORY_EXTRACTION_USER_TEMPLATE = (text, context) => `Extract memories from this text.
|
|
1946
|
+
${context.projectName ? `Project: ${context.projectName}` : ""}
|
|
1947
|
+
${context.existingMemoriesSummary ? `Existing memories (avoid duplicates):
|
|
1948
|
+
${context.existingMemoriesSummary}` : ""}
|
|
1949
|
+
|
|
1950
|
+
Text:
|
|
1951
|
+
${text}
|
|
1952
|
+
|
|
1953
|
+
Return a JSON array of objects with these exact fields:
|
|
1954
|
+
- content: string (the memory, concise and specific)
|
|
1955
|
+
- category: "preference" | "fact" | "knowledge" | "history"
|
|
1956
|
+
- importance: number 0-10 (10 = critical, 0 = trivial)
|
|
1957
|
+
- tags: string[] (lowercase keywords)
|
|
1958
|
+
- suggestedScope: "private" | "shared" | "global"
|
|
1959
|
+
- reasoning: string (one sentence why this is worth remembering)
|
|
1960
|
+
|
|
1961
|
+
Return [] if nothing is worth remembering.`, ENTITY_EXTRACTION_SYSTEM_PROMPT = `You are a knowledge graph entity extractor.
|
|
1962
|
+
Given text, identify named entities and their relationships.
|
|
1963
|
+
Output ONLY valid JSON \u2014 no markdown, no explanation.`, ENTITY_EXTRACTION_USER_TEMPLATE = (text) => `Extract entities and relations from this text.
|
|
1964
|
+
|
|
1965
|
+
Text: ${text}
|
|
1966
|
+
|
|
1967
|
+
Return JSON with this exact shape:
|
|
1968
|
+
{
|
|
1969
|
+
"entities": [
|
|
1970
|
+
{ "name": string, "type": "person"|"project"|"tool"|"concept"|"file"|"api"|"pattern"|"organization", "confidence": 0-1 }
|
|
1971
|
+
],
|
|
1972
|
+
"relations": [
|
|
1973
|
+
{ "from": string, "to": string, "type": "uses"|"knows"|"depends_on"|"created_by"|"related_to"|"contradicts"|"part_of"|"implements" }
|
|
1974
|
+
]
|
|
1975
|
+
}`;
|
|
1976
|
+
var init_base = __esm(() => {
|
|
1977
|
+
DEFAULT_AUTO_MEMORY_CONFIG = {
|
|
1978
|
+
provider: "anthropic",
|
|
1979
|
+
model: "claude-haiku-4-5",
|
|
1980
|
+
enabled: true,
|
|
1981
|
+
minImportance: 4,
|
|
1982
|
+
autoEntityLink: true,
|
|
1983
|
+
fallback: ["cerebras", "openai"]
|
|
1984
|
+
};
|
|
1985
|
+
});
|
|
1986
|
+
|
|
1987
|
+
// src/lib/providers/anthropic.ts
|
|
1988
|
+
var ANTHROPIC_MODELS, AnthropicProvider;
|
|
1989
|
+
var init_anthropic = __esm(() => {
|
|
1990
|
+
init_base();
|
|
1991
|
+
ANTHROPIC_MODELS = {
|
|
1992
|
+
default: "claude-haiku-4-5",
|
|
1993
|
+
premium: "claude-sonnet-4-5"
|
|
1994
|
+
};
|
|
1995
|
+
AnthropicProvider = class AnthropicProvider extends BaseProvider {
|
|
1996
|
+
name = "anthropic";
|
|
1997
|
+
baseUrl = "https://api.anthropic.com/v1";
|
|
1998
|
+
constructor(config) {
|
|
1999
|
+
const apiKey = config?.apiKey ?? process.env.ANTHROPIC_API_KEY ?? "";
|
|
2000
|
+
super({
|
|
2001
|
+
apiKey,
|
|
2002
|
+
model: config?.model ?? ANTHROPIC_MODELS.default,
|
|
2003
|
+
maxTokens: config?.maxTokens ?? 1024,
|
|
2004
|
+
temperature: config?.temperature ?? 0,
|
|
2005
|
+
timeoutMs: config?.timeoutMs ?? 15000
|
|
2006
|
+
});
|
|
2007
|
+
}
|
|
2008
|
+
async extractMemories(text, context) {
|
|
2009
|
+
if (!this.config.apiKey)
|
|
2010
|
+
return [];
|
|
2011
|
+
try {
|
|
2012
|
+
const response = await this.callAPI(MEMORY_EXTRACTION_SYSTEM_PROMPT, MEMORY_EXTRACTION_USER_TEMPLATE(text, context));
|
|
2013
|
+
const parsed = this.parseJSON(response);
|
|
2014
|
+
if (!Array.isArray(parsed))
|
|
2015
|
+
return [];
|
|
2016
|
+
return parsed.map((item) => this.normaliseMemory(item)).filter((m) => m !== null);
|
|
2017
|
+
} catch (err) {
|
|
2018
|
+
console.error("[anthropic] extractMemories failed:", err);
|
|
2019
|
+
return [];
|
|
1846
2020
|
}
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
2021
|
+
}
|
|
2022
|
+
async extractEntities(text) {
|
|
2023
|
+
const empty = { entities: [], relations: [] };
|
|
2024
|
+
if (!this.config.apiKey)
|
|
2025
|
+
return empty;
|
|
2026
|
+
try {
|
|
2027
|
+
const response = await this.callAPI(ENTITY_EXTRACTION_SYSTEM_PROMPT, ENTITY_EXTRACTION_USER_TEMPLATE(text));
|
|
2028
|
+
const parsed = this.parseJSON(response);
|
|
2029
|
+
if (!parsed || typeof parsed !== "object")
|
|
2030
|
+
return empty;
|
|
2031
|
+
return {
|
|
2032
|
+
entities: Array.isArray(parsed.entities) ? parsed.entities : [],
|
|
2033
|
+
relations: Array.isArray(parsed.relations) ? parsed.relations : []
|
|
2034
|
+
};
|
|
2035
|
+
} catch (err) {
|
|
2036
|
+
console.error("[anthropic] extractEntities failed:", err);
|
|
2037
|
+
return empty;
|
|
1851
2038
|
}
|
|
1852
|
-
|
|
1853
|
-
|
|
2039
|
+
}
|
|
2040
|
+
async scoreImportance(content, _context) {
|
|
2041
|
+
if (!this.config.apiKey)
|
|
2042
|
+
return 5;
|
|
2043
|
+
try {
|
|
2044
|
+
const response = await this.callAPI("You are an importance scorer. Return only a single integer 0-10. No explanation.", `How important is this memory for an AI agent to retain long-term?
|
|
2045
|
+
|
|
2046
|
+
"${content}"
|
|
2047
|
+
|
|
2048
|
+
Return only a number 0-10.`);
|
|
2049
|
+
return this.clampImportance(response.trim());
|
|
2050
|
+
} catch {
|
|
2051
|
+
return 5;
|
|
1854
2052
|
}
|
|
1855
|
-
|
|
1856
|
-
|
|
2053
|
+
}
|
|
2054
|
+
async callAPI(systemPrompt, userMessage) {
|
|
2055
|
+
const controller = new AbortController;
|
|
2056
|
+
const timeout = setTimeout(() => controller.abort(), this.config.timeoutMs ?? 15000);
|
|
2057
|
+
try {
|
|
2058
|
+
const res = await fetch(`${this.baseUrl}/messages`, {
|
|
2059
|
+
method: "POST",
|
|
2060
|
+
headers: {
|
|
2061
|
+
"Content-Type": "application/json",
|
|
2062
|
+
"x-api-key": this.config.apiKey,
|
|
2063
|
+
"anthropic-version": "2023-06-01"
|
|
2064
|
+
},
|
|
2065
|
+
body: JSON.stringify({
|
|
2066
|
+
model: this.config.model,
|
|
2067
|
+
max_tokens: this.config.maxTokens ?? 1024,
|
|
2068
|
+
temperature: this.config.temperature ?? 0,
|
|
2069
|
+
system: systemPrompt,
|
|
2070
|
+
messages: [{ role: "user", content: userMessage }]
|
|
2071
|
+
}),
|
|
2072
|
+
signal: controller.signal
|
|
2073
|
+
});
|
|
2074
|
+
if (!res.ok) {
|
|
2075
|
+
const body = await res.text().catch(() => "");
|
|
2076
|
+
throw new Error(`Anthropic API ${res.status}: ${body.slice(0, 200)}`);
|
|
2077
|
+
}
|
|
2078
|
+
const data = await res.json();
|
|
2079
|
+
return data.content?.[0]?.text ?? "";
|
|
2080
|
+
} finally {
|
|
2081
|
+
clearTimeout(timeout);
|
|
1857
2082
|
}
|
|
1858
|
-
|
|
1859
|
-
|
|
2083
|
+
}
|
|
2084
|
+
};
|
|
2085
|
+
});
|
|
2086
|
+
|
|
2087
|
+
// src/lib/providers/openai-compat.ts
|
|
2088
|
+
var OpenAICompatProvider;
|
|
2089
|
+
var init_openai_compat = __esm(() => {
|
|
2090
|
+
init_base();
|
|
2091
|
+
OpenAICompatProvider = class OpenAICompatProvider extends BaseProvider {
|
|
2092
|
+
constructor(config) {
|
|
2093
|
+
super(config);
|
|
2094
|
+
}
|
|
2095
|
+
async extractMemories(text, context) {
|
|
2096
|
+
if (!this.config.apiKey)
|
|
2097
|
+
return [];
|
|
2098
|
+
try {
|
|
2099
|
+
const response = await this.callWithRetry(MEMORY_EXTRACTION_SYSTEM_PROMPT, MEMORY_EXTRACTION_USER_TEMPLATE(text, context));
|
|
2100
|
+
const parsed = this.parseJSON(response);
|
|
2101
|
+
if (!Array.isArray(parsed))
|
|
2102
|
+
return [];
|
|
2103
|
+
return parsed.map((item) => this.normaliseMemory(item)).filter((m) => m !== null);
|
|
2104
|
+
} catch (err) {
|
|
2105
|
+
console.error(`[${this.name}] extractMemories failed:`, err);
|
|
2106
|
+
return [];
|
|
1860
2107
|
}
|
|
1861
2108
|
}
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
2109
|
+
async extractEntities(text) {
|
|
2110
|
+
const empty = { entities: [], relations: [] };
|
|
2111
|
+
if (!this.config.apiKey)
|
|
2112
|
+
return empty;
|
|
2113
|
+
try {
|
|
2114
|
+
const response = await this.callWithRetry(ENTITY_EXTRACTION_SYSTEM_PROMPT, ENTITY_EXTRACTION_USER_TEMPLATE(text));
|
|
2115
|
+
const parsed = this.parseJSON(response);
|
|
2116
|
+
if (!parsed || typeof parsed !== "object")
|
|
2117
|
+
return empty;
|
|
2118
|
+
return {
|
|
2119
|
+
entities: Array.isArray(parsed.entities) ? parsed.entities : [],
|
|
2120
|
+
relations: Array.isArray(parsed.relations) ? parsed.relations : []
|
|
2121
|
+
};
|
|
2122
|
+
} catch (err) {
|
|
2123
|
+
console.error(`[${this.name}] extractEntities failed:`, err);
|
|
2124
|
+
return empty;
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
async scoreImportance(content, _context) {
|
|
2128
|
+
if (!this.config.apiKey)
|
|
2129
|
+
return 5;
|
|
2130
|
+
try {
|
|
2131
|
+
const response = await this.callWithRetry("You are an importance scorer. Return only a single integer 0-10. No explanation.", `How important is this memory for an AI agent to retain long-term?
|
|
2132
|
+
|
|
2133
|
+
"${content}"
|
|
2134
|
+
|
|
2135
|
+
Return only a number 0-10.`);
|
|
2136
|
+
return this.clampImportance(response.trim());
|
|
2137
|
+
} catch {
|
|
2138
|
+
return 5;
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
async callWithRetry(systemPrompt, userMessage, retries = 3) {
|
|
2142
|
+
let lastError = null;
|
|
2143
|
+
for (let attempt = 0;attempt < retries; attempt++) {
|
|
2144
|
+
try {
|
|
2145
|
+
return await this.callAPI(systemPrompt, userMessage);
|
|
2146
|
+
} catch (err) {
|
|
2147
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
2148
|
+
const isRateLimit = lastError.message.includes("429") || lastError.message.toLowerCase().includes("rate limit");
|
|
2149
|
+
if (!isRateLimit || attempt === retries - 1)
|
|
2150
|
+
throw lastError;
|
|
2151
|
+
await new Promise((r) => setTimeout(r, 1000 * Math.pow(2, attempt)));
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
throw lastError ?? new Error("Unknown error");
|
|
2155
|
+
}
|
|
2156
|
+
async callAPI(systemPrompt, userMessage) {
|
|
2157
|
+
const controller = new AbortController;
|
|
2158
|
+
const timeout = setTimeout(() => controller.abort(), this.config.timeoutMs ?? 15000);
|
|
2159
|
+
try {
|
|
2160
|
+
const res = await fetch(`${this.baseUrl}/chat/completions`, {
|
|
2161
|
+
method: "POST",
|
|
2162
|
+
headers: {
|
|
2163
|
+
"Content-Type": "application/json",
|
|
2164
|
+
[this.authHeader]: `Bearer ${this.config.apiKey}`
|
|
2165
|
+
},
|
|
2166
|
+
body: JSON.stringify({
|
|
2167
|
+
model: this.config.model,
|
|
2168
|
+
max_tokens: this.config.maxTokens ?? 1024,
|
|
2169
|
+
temperature: this.config.temperature ?? 0,
|
|
2170
|
+
messages: [
|
|
2171
|
+
{ role: "system", content: systemPrompt },
|
|
2172
|
+
{ role: "user", content: userMessage }
|
|
2173
|
+
]
|
|
2174
|
+
}),
|
|
2175
|
+
signal: controller.signal
|
|
2176
|
+
});
|
|
2177
|
+
if (!res.ok) {
|
|
2178
|
+
const body = await res.text().catch(() => "");
|
|
2179
|
+
throw new Error(`${this.name} API ${res.status}: ${body.slice(0, 200)}`);
|
|
2180
|
+
}
|
|
2181
|
+
const data = await res.json();
|
|
2182
|
+
return data.choices?.[0]?.message?.content ?? "";
|
|
2183
|
+
} finally {
|
|
2184
|
+
clearTimeout(timeout);
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
};
|
|
2188
|
+
});
|
|
2189
|
+
|
|
2190
|
+
// src/lib/providers/openai.ts
|
|
2191
|
+
var OPENAI_MODELS, OpenAIProvider;
|
|
2192
|
+
var init_openai = __esm(() => {
|
|
2193
|
+
init_openai_compat();
|
|
2194
|
+
OPENAI_MODELS = {
|
|
2195
|
+
default: "gpt-4.1-nano",
|
|
2196
|
+
mini: "gpt-4.1-mini",
|
|
2197
|
+
full: "gpt-4.1"
|
|
2198
|
+
};
|
|
2199
|
+
OpenAIProvider = class OpenAIProvider extends OpenAICompatProvider {
|
|
2200
|
+
name = "openai";
|
|
2201
|
+
baseUrl = "https://api.openai.com/v1";
|
|
2202
|
+
authHeader = "Authorization";
|
|
2203
|
+
constructor(config) {
|
|
2204
|
+
super({
|
|
2205
|
+
apiKey: config?.apiKey ?? process.env.OPENAI_API_KEY ?? "",
|
|
2206
|
+
model: config?.model ?? OPENAI_MODELS.default,
|
|
2207
|
+
maxTokens: config?.maxTokens ?? 1024,
|
|
2208
|
+
temperature: config?.temperature ?? 0,
|
|
2209
|
+
timeoutMs: config?.timeoutMs ?? 15000
|
|
2210
|
+
});
|
|
2211
|
+
}
|
|
2212
|
+
};
|
|
2213
|
+
});
|
|
2214
|
+
|
|
2215
|
+
// src/lib/providers/cerebras.ts
|
|
2216
|
+
var CEREBRAS_MODELS, CerebrasProvider;
|
|
2217
|
+
var init_cerebras = __esm(() => {
|
|
2218
|
+
init_openai_compat();
|
|
2219
|
+
CEREBRAS_MODELS = {
|
|
2220
|
+
default: "llama-3.3-70b",
|
|
2221
|
+
fast: "llama3.1-8b"
|
|
2222
|
+
};
|
|
2223
|
+
CerebrasProvider = class CerebrasProvider extends OpenAICompatProvider {
|
|
2224
|
+
name = "cerebras";
|
|
2225
|
+
baseUrl = "https://api.cerebras.ai/v1";
|
|
2226
|
+
authHeader = "Authorization";
|
|
2227
|
+
constructor(config) {
|
|
2228
|
+
super({
|
|
2229
|
+
apiKey: config?.apiKey ?? process.env.CEREBRAS_API_KEY ?? "",
|
|
2230
|
+
model: config?.model ?? CEREBRAS_MODELS.default,
|
|
2231
|
+
maxTokens: config?.maxTokens ?? 1024,
|
|
2232
|
+
temperature: config?.temperature ?? 0,
|
|
2233
|
+
timeoutMs: config?.timeoutMs ?? 1e4
|
|
2234
|
+
});
|
|
2235
|
+
}
|
|
2236
|
+
};
|
|
2237
|
+
});
|
|
2238
|
+
|
|
2239
|
+
// src/lib/providers/grok.ts
|
|
2240
|
+
var GROK_MODELS, GrokProvider;
|
|
2241
|
+
var init_grok = __esm(() => {
|
|
2242
|
+
init_openai_compat();
|
|
2243
|
+
GROK_MODELS = {
|
|
2244
|
+
default: "grok-3-mini",
|
|
2245
|
+
premium: "grok-3"
|
|
2246
|
+
};
|
|
2247
|
+
GrokProvider = class GrokProvider extends OpenAICompatProvider {
|
|
2248
|
+
name = "grok";
|
|
2249
|
+
baseUrl = "https://api.x.ai/v1";
|
|
2250
|
+
authHeader = "Authorization";
|
|
2251
|
+
constructor(config) {
|
|
2252
|
+
super({
|
|
2253
|
+
apiKey: config?.apiKey ?? process.env.XAI_API_KEY ?? "",
|
|
2254
|
+
model: config?.model ?? GROK_MODELS.default,
|
|
2255
|
+
maxTokens: config?.maxTokens ?? 1024,
|
|
2256
|
+
temperature: config?.temperature ?? 0,
|
|
2257
|
+
timeoutMs: config?.timeoutMs ?? 15000
|
|
2258
|
+
});
|
|
2259
|
+
}
|
|
2260
|
+
};
|
|
2261
|
+
});
|
|
2262
|
+
|
|
2263
|
+
// src/lib/providers/registry.ts
|
|
2264
|
+
class ProviderRegistry {
|
|
2265
|
+
config = { ...DEFAULT_AUTO_MEMORY_CONFIG };
|
|
2266
|
+
_instances = new Map;
|
|
2267
|
+
configure(partial) {
|
|
2268
|
+
this.config = { ...this.config, ...partial };
|
|
2269
|
+
this._instances.clear();
|
|
2270
|
+
}
|
|
2271
|
+
getConfig() {
|
|
2272
|
+
return this.config;
|
|
2273
|
+
}
|
|
2274
|
+
getPrimary() {
|
|
2275
|
+
return this.getProvider(this.config.provider);
|
|
2276
|
+
}
|
|
2277
|
+
getFallbacks() {
|
|
2278
|
+
const fallbackNames = this.config.fallback ?? [];
|
|
2279
|
+
return fallbackNames.filter((n) => n !== this.config.provider).map((n) => this.getProvider(n)).filter((p) => p !== null);
|
|
2280
|
+
}
|
|
2281
|
+
getAvailable() {
|
|
2282
|
+
const primary = this.getPrimary();
|
|
2283
|
+
if (primary)
|
|
2284
|
+
return primary;
|
|
2285
|
+
const fallbacks = this.getFallbacks();
|
|
2286
|
+
return fallbacks[0] ?? null;
|
|
2287
|
+
}
|
|
2288
|
+
getProvider(name) {
|
|
2289
|
+
const cached = this._instances.get(name);
|
|
2290
|
+
if (cached)
|
|
2291
|
+
return cached;
|
|
2292
|
+
const provider = this.createProvider(name);
|
|
2293
|
+
if (!provider)
|
|
2294
|
+
return null;
|
|
2295
|
+
if (!provider.config.apiKey)
|
|
2296
|
+
return null;
|
|
2297
|
+
this._instances.set(name, provider);
|
|
2298
|
+
return provider;
|
|
2299
|
+
}
|
|
2300
|
+
health() {
|
|
2301
|
+
const providers = ["anthropic", "openai", "cerebras", "grok"];
|
|
2302
|
+
const result = {};
|
|
2303
|
+
for (const name of providers) {
|
|
2304
|
+
const p = this.createProvider(name);
|
|
2305
|
+
result[name] = {
|
|
2306
|
+
available: Boolean(p?.config.apiKey),
|
|
2307
|
+
model: p?.config.model ?? "unknown"
|
|
2308
|
+
};
|
|
2309
|
+
}
|
|
2310
|
+
return result;
|
|
2311
|
+
}
|
|
2312
|
+
createProvider(name) {
|
|
2313
|
+
const modelOverride = name === this.config.provider ? this.config.model : undefined;
|
|
2314
|
+
switch (name) {
|
|
2315
|
+
case "anthropic":
|
|
2316
|
+
return new AnthropicProvider(modelOverride ? { model: modelOverride } : undefined);
|
|
2317
|
+
case "openai":
|
|
2318
|
+
return new OpenAIProvider(modelOverride ? { model: modelOverride } : undefined);
|
|
2319
|
+
case "cerebras":
|
|
2320
|
+
return new CerebrasProvider(modelOverride ? { model: modelOverride } : undefined);
|
|
2321
|
+
case "grok":
|
|
2322
|
+
return new GrokProvider(modelOverride ? { model: modelOverride } : undefined);
|
|
2323
|
+
default:
|
|
2324
|
+
return null;
|
|
1866
2325
|
}
|
|
1867
2326
|
}
|
|
1868
|
-
return score;
|
|
1869
2327
|
}
|
|
1870
|
-
function
|
|
1871
|
-
const
|
|
1872
|
-
const
|
|
1873
|
-
|
|
1874
|
-
|
|
2328
|
+
function autoConfigureFromEnv() {
|
|
2329
|
+
const hasAnthropicKey = Boolean(process.env.ANTHROPIC_API_KEY);
|
|
2330
|
+
const hasCerebrasKey = Boolean(process.env.CEREBRAS_API_KEY);
|
|
2331
|
+
const hasOpenAIKey = Boolean(process.env.OPENAI_API_KEY);
|
|
2332
|
+
const hasGrokKey = Boolean(process.env.XAI_API_KEY);
|
|
2333
|
+
if (!hasAnthropicKey) {
|
|
2334
|
+
if (hasCerebrasKey) {
|
|
2335
|
+
providerRegistry.configure({ provider: "cerebras" });
|
|
2336
|
+
} else if (hasOpenAIKey) {
|
|
2337
|
+
providerRegistry.configure({ provider: "openai" });
|
|
2338
|
+
} else if (hasGrokKey) {
|
|
2339
|
+
providerRegistry.configure({ provider: "grok" });
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
const allProviders = ["anthropic", "cerebras", "openai", "grok"];
|
|
2343
|
+
const available = allProviders.filter((p) => {
|
|
2344
|
+
switch (p) {
|
|
2345
|
+
case "anthropic":
|
|
2346
|
+
return hasAnthropicKey;
|
|
2347
|
+
case "cerebras":
|
|
2348
|
+
return hasCerebrasKey;
|
|
2349
|
+
case "openai":
|
|
2350
|
+
return hasOpenAIKey;
|
|
2351
|
+
case "grok":
|
|
2352
|
+
return hasGrokKey;
|
|
2353
|
+
}
|
|
1875
2354
|
});
|
|
1876
|
-
|
|
2355
|
+
const primary = providerRegistry.getConfig().provider;
|
|
2356
|
+
const fallback = available.filter((p) => p !== primary);
|
|
2357
|
+
providerRegistry.configure({ fallback });
|
|
1877
2358
|
}
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
2359
|
+
var providerRegistry;
|
|
2360
|
+
var init_registry = __esm(() => {
|
|
2361
|
+
init_anthropic();
|
|
2362
|
+
init_openai();
|
|
2363
|
+
init_cerebras();
|
|
2364
|
+
init_grok();
|
|
2365
|
+
init_base();
|
|
2366
|
+
providerRegistry = new ProviderRegistry;
|
|
2367
|
+
autoConfigureFromEnv();
|
|
2368
|
+
});
|
|
2369
|
+
|
|
2370
|
+
// src/lib/auto-memory-queue.ts
|
|
2371
|
+
class AutoMemoryQueue {
|
|
2372
|
+
queue = [];
|
|
2373
|
+
handler = null;
|
|
2374
|
+
running = false;
|
|
2375
|
+
activeCount = 0;
|
|
2376
|
+
stats = {
|
|
2377
|
+
pending: 0,
|
|
2378
|
+
processing: 0,
|
|
2379
|
+
processed: 0,
|
|
2380
|
+
failed: 0,
|
|
2381
|
+
dropped: 0
|
|
2382
|
+
};
|
|
2383
|
+
setHandler(handler) {
|
|
2384
|
+
this.handler = handler;
|
|
2385
|
+
if (!this.running)
|
|
2386
|
+
this.startLoop();
|
|
2387
|
+
}
|
|
2388
|
+
enqueue(job) {
|
|
2389
|
+
if (this.queue.length >= MAX_QUEUE_SIZE) {
|
|
2390
|
+
this.queue.shift();
|
|
2391
|
+
this.stats.dropped++;
|
|
2392
|
+
this.stats.pending = Math.max(0, this.stats.pending - 1);
|
|
2393
|
+
}
|
|
2394
|
+
this.queue.push(job);
|
|
2395
|
+
this.stats.pending++;
|
|
2396
|
+
if (!this.running && this.handler)
|
|
2397
|
+
this.startLoop();
|
|
2398
|
+
}
|
|
2399
|
+
getStats() {
|
|
2400
|
+
return { ...this.stats, pending: this.queue.length };
|
|
2401
|
+
}
|
|
2402
|
+
startLoop() {
|
|
2403
|
+
this.running = true;
|
|
2404
|
+
this.loop();
|
|
2405
|
+
}
|
|
2406
|
+
async loop() {
|
|
2407
|
+
while (this.queue.length > 0 || this.activeCount > 0) {
|
|
2408
|
+
while (this.queue.length > 0 && this.activeCount < CONCURRENCY) {
|
|
2409
|
+
const job = this.queue.shift();
|
|
2410
|
+
if (!job)
|
|
2411
|
+
break;
|
|
2412
|
+
this.stats.pending = Math.max(0, this.stats.pending - 1);
|
|
2413
|
+
this.activeCount++;
|
|
2414
|
+
this.stats.processing = this.activeCount;
|
|
2415
|
+
this.processJob(job);
|
|
2416
|
+
}
|
|
2417
|
+
await new Promise((r) => setImmediate(r));
|
|
2418
|
+
}
|
|
2419
|
+
this.running = false;
|
|
1883
2420
|
}
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
2421
|
+
async processJob(job) {
|
|
2422
|
+
if (!this.handler) {
|
|
2423
|
+
this.activeCount--;
|
|
2424
|
+
this.stats.processing = this.activeCount;
|
|
2425
|
+
return;
|
|
2426
|
+
}
|
|
2427
|
+
try {
|
|
2428
|
+
await this.handler(job);
|
|
2429
|
+
this.stats.processed++;
|
|
2430
|
+
} catch (err) {
|
|
2431
|
+
this.stats.failed++;
|
|
2432
|
+
console.error("[auto-memory-queue] job failed:", err);
|
|
2433
|
+
} finally {
|
|
2434
|
+
this.activeCount--;
|
|
2435
|
+
this.stats.processing = this.activeCount;
|
|
2436
|
+
}
|
|
1887
2437
|
}
|
|
1888
|
-
return parts.join(" ");
|
|
1889
2438
|
}
|
|
1890
|
-
|
|
2439
|
+
var MAX_QUEUE_SIZE = 100, CONCURRENCY = 3, autoMemoryQueue;
|
|
2440
|
+
var init_auto_memory_queue = __esm(() => {
|
|
2441
|
+
autoMemoryQueue = new AutoMemoryQueue;
|
|
2442
|
+
});
|
|
2443
|
+
|
|
2444
|
+
// src/lib/auto-memory.ts
|
|
2445
|
+
var exports_auto_memory = {};
|
|
2446
|
+
__export(exports_auto_memory, {
|
|
2447
|
+
processConversationTurn: () => processConversationTurn,
|
|
2448
|
+
getAutoMemoryStats: () => getAutoMemoryStats,
|
|
2449
|
+
configureAutoMemory: () => configureAutoMemory
|
|
2450
|
+
});
|
|
2451
|
+
function isDuplicate(content, agentId, projectId) {
|
|
1891
2452
|
try {
|
|
1892
|
-
const
|
|
1893
|
-
|
|
2453
|
+
const query = content.split(/\s+/).filter((w) => w.length > 3).slice(0, 10).join(" ");
|
|
2454
|
+
if (!query)
|
|
2455
|
+
return false;
|
|
2456
|
+
const results = searchMemories(query, {
|
|
2457
|
+
agent_id: agentId,
|
|
2458
|
+
project_id: projectId,
|
|
2459
|
+
limit: 3
|
|
2460
|
+
});
|
|
2461
|
+
if (results.length === 0)
|
|
2462
|
+
return false;
|
|
2463
|
+
const contentWords = new Set(content.toLowerCase().split(/\W+/).filter((w) => w.length > 3));
|
|
2464
|
+
for (const result of results) {
|
|
2465
|
+
const existingWords = new Set(result.memory.value.toLowerCase().split(/\W+/).filter((w) => w.length > 3));
|
|
2466
|
+
if (contentWords.size === 0 || existingWords.size === 0)
|
|
2467
|
+
continue;
|
|
2468
|
+
const intersection = [...contentWords].filter((w) => existingWords.has(w)).length;
|
|
2469
|
+
const union = new Set([...contentWords, ...existingWords]).size;
|
|
2470
|
+
const similarity = intersection / union;
|
|
2471
|
+
if (similarity >= DEDUP_SIMILARITY_THRESHOLD)
|
|
2472
|
+
return true;
|
|
2473
|
+
}
|
|
2474
|
+
return false;
|
|
1894
2475
|
} catch {
|
|
1895
2476
|
return false;
|
|
1896
2477
|
}
|
|
1897
2478
|
}
|
|
1898
|
-
function
|
|
1899
|
-
const
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
} else {
|
|
1919
|
-
conditions.push("m.category = ?");
|
|
1920
|
-
params.push(filter.category);
|
|
1921
|
-
}
|
|
1922
|
-
}
|
|
1923
|
-
if (filter.source) {
|
|
1924
|
-
if (Array.isArray(filter.source)) {
|
|
1925
|
-
conditions.push(`m.source IN (${filter.source.map(() => "?").join(",")})`);
|
|
1926
|
-
params.push(...filter.source);
|
|
1927
|
-
} else {
|
|
1928
|
-
conditions.push("m.source = ?");
|
|
1929
|
-
params.push(filter.source);
|
|
1930
|
-
}
|
|
1931
|
-
}
|
|
1932
|
-
if (filter.status) {
|
|
1933
|
-
conditions.shift();
|
|
1934
|
-
if (Array.isArray(filter.status)) {
|
|
1935
|
-
conditions.push(`m.status IN (${filter.status.map(() => "?").join(",")})`);
|
|
1936
|
-
params.push(...filter.status);
|
|
1937
|
-
} else {
|
|
1938
|
-
conditions.push("m.status = ?");
|
|
1939
|
-
params.push(filter.status);
|
|
2479
|
+
async function linkEntitiesToMemory(memoryId, content, _agentId, projectId) {
|
|
2480
|
+
const provider = providerRegistry.getAvailable();
|
|
2481
|
+
if (!provider)
|
|
2482
|
+
return;
|
|
2483
|
+
try {
|
|
2484
|
+
const { entities, relations } = await provider.extractEntities(content);
|
|
2485
|
+
const entityIdMap = new Map;
|
|
2486
|
+
for (const extracted of entities) {
|
|
2487
|
+
if (extracted.confidence < 0.6)
|
|
2488
|
+
continue;
|
|
2489
|
+
try {
|
|
2490
|
+
const existing = getEntityByName(extracted.name);
|
|
2491
|
+
const entityId = existing ? existing.id : createEntity({
|
|
2492
|
+
name: extracted.name,
|
|
2493
|
+
type: extracted.type,
|
|
2494
|
+
project_id: projectId
|
|
2495
|
+
}).id;
|
|
2496
|
+
entityIdMap.set(extracted.name, entityId);
|
|
2497
|
+
linkEntityToMemory(entityId, memoryId, "subject");
|
|
2498
|
+
} catch {}
|
|
1940
2499
|
}
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
}
|
|
1954
|
-
if (filter.min_importance) {
|
|
1955
|
-
conditions.push("m.importance >= ?");
|
|
1956
|
-
params.push(filter.min_importance);
|
|
1957
|
-
}
|
|
1958
|
-
if (filter.pinned !== undefined) {
|
|
1959
|
-
conditions.push("m.pinned = ?");
|
|
1960
|
-
params.push(filter.pinned ? 1 : 0);
|
|
1961
|
-
}
|
|
1962
|
-
if (filter.tags && filter.tags.length > 0) {
|
|
1963
|
-
for (const tag of filter.tags) {
|
|
1964
|
-
conditions.push("m.id IN (SELECT memory_id FROM memory_tags WHERE tag = ?)");
|
|
1965
|
-
params.push(tag);
|
|
2500
|
+
for (const rel of relations) {
|
|
2501
|
+
const fromId = entityIdMap.get(rel.from);
|
|
2502
|
+
const toId = entityIdMap.get(rel.to);
|
|
2503
|
+
if (!fromId || !toId)
|
|
2504
|
+
continue;
|
|
2505
|
+
try {
|
|
2506
|
+
createRelation({
|
|
2507
|
+
source_entity_id: fromId,
|
|
2508
|
+
target_entity_id: toId,
|
|
2509
|
+
relation_type: rel.type
|
|
2510
|
+
});
|
|
2511
|
+
} catch {}
|
|
1966
2512
|
}
|
|
2513
|
+
} catch (err) {
|
|
2514
|
+
console.error("[auto-memory] entity linking failed:", err);
|
|
1967
2515
|
}
|
|
1968
|
-
return { conditions, params };
|
|
1969
2516
|
}
|
|
1970
|
-
function
|
|
1971
|
-
const
|
|
1972
|
-
if (
|
|
2517
|
+
async function saveExtractedMemory(extracted, context) {
|
|
2518
|
+
const minImportance = providerRegistry.getConfig().minImportance;
|
|
2519
|
+
if (extracted.importance < minImportance)
|
|
2520
|
+
return null;
|
|
2521
|
+
if (!extracted.content.trim())
|
|
1973
2522
|
return null;
|
|
2523
|
+
if (isDuplicate(extracted.content, context.agentId, context.projectId)) {
|
|
2524
|
+
return null;
|
|
2525
|
+
}
|
|
1974
2526
|
try {
|
|
1975
|
-
const
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
2527
|
+
const input = {
|
|
2528
|
+
key: extracted.content.slice(0, 120).replace(/\s+/g, "-").toLowerCase(),
|
|
2529
|
+
value: extracted.content,
|
|
2530
|
+
category: extracted.category,
|
|
2531
|
+
scope: extracted.suggestedScope,
|
|
2532
|
+
importance: extracted.importance,
|
|
2533
|
+
tags: [
|
|
2534
|
+
...extracted.tags,
|
|
2535
|
+
"auto-extracted",
|
|
2536
|
+
...context.sessionId ? [`session:${context.sessionId}`] : []
|
|
2537
|
+
],
|
|
2538
|
+
agent_id: context.agentId,
|
|
2539
|
+
project_id: context.projectId,
|
|
2540
|
+
session_id: context.sessionId,
|
|
2541
|
+
metadata: {
|
|
2542
|
+
reasoning: extracted.reasoning,
|
|
2543
|
+
auto_extracted: true,
|
|
2544
|
+
extracted_at: new Date().toISOString()
|
|
2545
|
+
}
|
|
2546
|
+
};
|
|
2547
|
+
const memory = createMemory(input, "merge");
|
|
2548
|
+
return memory.id;
|
|
2549
|
+
} catch (err) {
|
|
2550
|
+
console.error("[auto-memory] saveExtractedMemory failed:", err);
|
|
1984
2551
|
return null;
|
|
1985
2552
|
}
|
|
1986
2553
|
}
|
|
1987
|
-
function
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
const
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
2554
|
+
async function processJob(job) {
|
|
2555
|
+
if (!providerRegistry.getConfig().enabled)
|
|
2556
|
+
return;
|
|
2557
|
+
const provider = providerRegistry.getAvailable();
|
|
2558
|
+
if (!provider)
|
|
2559
|
+
return;
|
|
2560
|
+
const context = {
|
|
2561
|
+
agentId: job.agentId,
|
|
2562
|
+
projectId: job.projectId,
|
|
2563
|
+
sessionId: job.sessionId
|
|
2564
|
+
};
|
|
2565
|
+
let extracted = [];
|
|
2566
|
+
try {
|
|
2567
|
+
extracted = await provider.extractMemories(job.turn, context);
|
|
2568
|
+
} catch {
|
|
2569
|
+
const fallbacks = providerRegistry.getFallbacks();
|
|
2570
|
+
for (const fallback of fallbacks) {
|
|
2571
|
+
try {
|
|
2572
|
+
extracted = await fallback.extractMemories(job.turn, context);
|
|
2573
|
+
if (extracted.length > 0)
|
|
2574
|
+
break;
|
|
2575
|
+
} catch {
|
|
2576
|
+
continue;
|
|
2577
|
+
}
|
|
2578
|
+
}
|
|
1996
2579
|
}
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
params.push(pattern);
|
|
2007
|
-
fieldClauses.push("m.id IN (SELECT memory_id FROM memory_tags WHERE tag LIKE ? ESCAPE '\\')");
|
|
2008
|
-
params.push(pattern);
|
|
2580
|
+
if (extracted.length === 0)
|
|
2581
|
+
return;
|
|
2582
|
+
for (const memory of extracted) {
|
|
2583
|
+
const memoryId = await saveExtractedMemory(memory, context);
|
|
2584
|
+
if (!memoryId)
|
|
2585
|
+
continue;
|
|
2586
|
+
if (providerRegistry.getConfig().autoEntityLink) {
|
|
2587
|
+
linkEntitiesToMemory(memoryId, memory.content, job.agentId, job.projectId);
|
|
2588
|
+
}
|
|
2009
2589
|
}
|
|
2010
|
-
conditions.push(`(${fieldClauses.join(" OR ")})`);
|
|
2011
|
-
const sql = `SELECT DISTINCT m.* FROM memories m WHERE ${conditions.join(" AND ")}`;
|
|
2012
|
-
const rows = d.query(sql).all(...params);
|
|
2013
|
-
return scoreResults(rows, queryLower, graphBoostedIds);
|
|
2014
2590
|
}
|
|
2015
|
-
function
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2591
|
+
function processConversationTurn(turn, context, source = "turn") {
|
|
2592
|
+
if (!turn?.trim())
|
|
2593
|
+
return;
|
|
2594
|
+
autoMemoryQueue.enqueue({
|
|
2595
|
+
...context,
|
|
2596
|
+
turn,
|
|
2597
|
+
timestamp: Date.now(),
|
|
2598
|
+
source
|
|
2599
|
+
});
|
|
2600
|
+
}
|
|
2601
|
+
function getAutoMemoryStats() {
|
|
2602
|
+
return autoMemoryQueue.getStats();
|
|
2603
|
+
}
|
|
2604
|
+
function configureAutoMemory(config) {
|
|
2605
|
+
providerRegistry.configure(config);
|
|
2606
|
+
}
|
|
2607
|
+
var DEDUP_SIMILARITY_THRESHOLD = 0.85;
|
|
2608
|
+
var init_auto_memory = __esm(() => {
|
|
2609
|
+
init_memories();
|
|
2610
|
+
init_search();
|
|
2611
|
+
init_entities();
|
|
2612
|
+
init_relations();
|
|
2613
|
+
init_entity_memories();
|
|
2614
|
+
init_registry();
|
|
2615
|
+
init_auto_memory_queue();
|
|
2616
|
+
autoMemoryQueue.setHandler(processJob);
|
|
2617
|
+
});
|
|
2618
|
+
|
|
2619
|
+
// src/server/index.ts
|
|
2620
|
+
init_memories();
|
|
2621
|
+
import { existsSync as existsSync3 } from "fs";
|
|
2622
|
+
import { dirname as dirname3, extname, join as join3, resolve as resolve3, sep } from "path";
|
|
2623
|
+
import { fileURLToPath } from "url";
|
|
2624
|
+
|
|
2625
|
+
// src/db/agents.ts
|
|
2626
|
+
init_types();
|
|
2627
|
+
init_database();
|
|
2628
|
+
var CONFLICT_WINDOW_MS = 30 * 60 * 1000;
|
|
2629
|
+
function parseAgentRow(row) {
|
|
2630
|
+
return {
|
|
2631
|
+
id: row["id"],
|
|
2632
|
+
name: row["name"],
|
|
2633
|
+
session_id: row["session_id"] || null,
|
|
2634
|
+
description: row["description"] || null,
|
|
2635
|
+
role: row["role"] || null,
|
|
2636
|
+
metadata: JSON.parse(row["metadata"] || "{}"),
|
|
2637
|
+
active_project_id: row["active_project_id"] || null,
|
|
2638
|
+
created_at: row["created_at"],
|
|
2639
|
+
last_seen_at: row["last_seen_at"]
|
|
2640
|
+
};
|
|
2641
|
+
}
|
|
2642
|
+
function registerAgent(name, sessionId, description, role, projectId, db) {
|
|
2643
|
+
const d = db || getDatabase();
|
|
2644
|
+
const timestamp = now();
|
|
2645
|
+
const normalizedName = name.trim().toLowerCase();
|
|
2646
|
+
const existing = d.query("SELECT * FROM agents WHERE LOWER(name) = ?").get(normalizedName);
|
|
2647
|
+
if (existing) {
|
|
2648
|
+
const existingId = existing["id"];
|
|
2649
|
+
const existingSessionId = existing["session_id"] || null;
|
|
2650
|
+
const existingLastSeen = existing["last_seen_at"];
|
|
2651
|
+
if (sessionId && existingSessionId && existingSessionId !== sessionId) {
|
|
2652
|
+
const lastSeenMs = new Date(existingLastSeen).getTime();
|
|
2653
|
+
const nowMs = Date.now();
|
|
2654
|
+
if (nowMs - lastSeenMs < CONFLICT_WINDOW_MS) {
|
|
2655
|
+
throw new AgentConflictError({
|
|
2656
|
+
existing_id: existingId,
|
|
2657
|
+
existing_name: normalizedName,
|
|
2658
|
+
last_seen_at: existingLastSeen,
|
|
2659
|
+
session_hint: existingSessionId.slice(0, 8),
|
|
2660
|
+
working_dir: null
|
|
2661
|
+
});
|
|
2662
|
+
}
|
|
2663
|
+
}
|
|
2664
|
+
d.run("UPDATE agents SET last_seen_at = ?, session_id = ? WHERE id = ?", [
|
|
2665
|
+
timestamp,
|
|
2666
|
+
sessionId ?? existingSessionId,
|
|
2667
|
+
existingId
|
|
2668
|
+
]);
|
|
2669
|
+
if (description) {
|
|
2670
|
+
d.run("UPDATE agents SET description = ? WHERE id = ?", [description, existingId]);
|
|
2671
|
+
}
|
|
2672
|
+
if (role) {
|
|
2673
|
+
d.run("UPDATE agents SET role = ? WHERE id = ?", [role, existingId]);
|
|
2674
|
+
}
|
|
2675
|
+
if (projectId !== undefined) {
|
|
2676
|
+
d.run("UPDATE agents SET active_project_id = ? WHERE id = ?", [projectId, existingId]);
|
|
2677
|
+
}
|
|
2678
|
+
return getAgent(existingId, d);
|
|
2020
2679
|
}
|
|
2021
|
-
|
|
2680
|
+
const id = shortUuid();
|
|
2681
|
+
d.run("INSERT INTO agents (id, name, session_id, description, role, active_project_id, created_at, last_seen_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", [id, normalizedName, sessionId ?? null, description || null, role || "agent", projectId ?? null, timestamp, timestamp]);
|
|
2682
|
+
return getAgent(id, d);
|
|
2022
2683
|
}
|
|
2023
|
-
function
|
|
2024
|
-
const
|
|
2025
|
-
|
|
2026
|
-
if (
|
|
2027
|
-
return
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2684
|
+
function getAgent(idOrName, db) {
|
|
2685
|
+
const d = db || getDatabase();
|
|
2686
|
+
let row = d.query("SELECT * FROM agents WHERE id = ?").get(idOrName);
|
|
2687
|
+
if (row)
|
|
2688
|
+
return parseAgentRow(row);
|
|
2689
|
+
row = d.query("SELECT * FROM agents WHERE LOWER(name) = ?").get(idOrName.trim().toLowerCase());
|
|
2690
|
+
if (row)
|
|
2691
|
+
return parseAgentRow(row);
|
|
2692
|
+
const rows = d.query("SELECT * FROM agents WHERE id LIKE ?").all(`${idOrName}%`);
|
|
2693
|
+
if (rows.length === 1)
|
|
2694
|
+
return parseAgentRow(rows[0]);
|
|
2695
|
+
return null;
|
|
2696
|
+
}
|
|
2697
|
+
function listAgents(db) {
|
|
2698
|
+
const d = db || getDatabase();
|
|
2699
|
+
const rows = d.query("SELECT * FROM agents ORDER BY last_seen_at DESC").all();
|
|
2700
|
+
return rows.map(parseAgentRow);
|
|
2701
|
+
}
|
|
2702
|
+
function listAgentsByProject(projectId, db) {
|
|
2703
|
+
const d = db || getDatabase();
|
|
2704
|
+
const rows = d.query("SELECT * FROM agents WHERE active_project_id = ? ORDER BY last_seen_at DESC").all(projectId);
|
|
2705
|
+
return rows.map(parseAgentRow);
|
|
2706
|
+
}
|
|
2707
|
+
function updateAgent(id, updates, db) {
|
|
2708
|
+
const d = db || getDatabase();
|
|
2709
|
+
const agent = getAgent(id, d);
|
|
2710
|
+
if (!agent)
|
|
2711
|
+
return null;
|
|
2712
|
+
const timestamp = now();
|
|
2713
|
+
if (updates.name) {
|
|
2714
|
+
const normalizedNewName = updates.name.trim().toLowerCase();
|
|
2715
|
+
if (normalizedNewName !== agent.name) {
|
|
2716
|
+
const existing = d.query("SELECT id FROM agents WHERE LOWER(name) = ? AND id != ?").get(normalizedNewName, agent.id);
|
|
2717
|
+
if (existing) {
|
|
2718
|
+
throw new Error(`Agent name already taken: ${normalizedNewName}`);
|
|
2719
|
+
}
|
|
2720
|
+
d.run("UPDATE agents SET name = ? WHERE id = ?", [normalizedNewName, agent.id]);
|
|
2721
|
+
}
|
|
2032
2722
|
}
|
|
2033
|
-
|
|
2034
|
-
|
|
2723
|
+
if (updates.description !== undefined) {
|
|
2724
|
+
d.run("UPDATE agents SET description = ? WHERE id = ?", [updates.description, agent.id]);
|
|
2725
|
+
}
|
|
2726
|
+
if (updates.role !== undefined) {
|
|
2727
|
+
d.run("UPDATE agents SET role = ? WHERE id = ?", [updates.role, agent.id]);
|
|
2728
|
+
}
|
|
2729
|
+
if (updates.metadata !== undefined) {
|
|
2730
|
+
d.run("UPDATE agents SET metadata = ? WHERE id = ?", [JSON.stringify(updates.metadata), agent.id]);
|
|
2731
|
+
}
|
|
2732
|
+
if ("active_project_id" in updates) {
|
|
2733
|
+
d.run("UPDATE agents SET active_project_id = ? WHERE id = ?", [updates.active_project_id ?? null, agent.id]);
|
|
2734
|
+
}
|
|
2735
|
+
d.run("UPDATE agents SET last_seen_at = ? WHERE id = ?", [timestamp, agent.id]);
|
|
2736
|
+
return getAgent(agent.id, d);
|
|
2035
2737
|
}
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2738
|
+
|
|
2739
|
+
// src/db/locks.ts
|
|
2740
|
+
init_database();
|
|
2741
|
+
function parseLockRow(row) {
|
|
2742
|
+
return {
|
|
2743
|
+
id: row["id"],
|
|
2744
|
+
resource_type: row["resource_type"],
|
|
2745
|
+
resource_id: row["resource_id"],
|
|
2746
|
+
agent_id: row["agent_id"],
|
|
2747
|
+
lock_type: row["lock_type"],
|
|
2748
|
+
locked_at: row["locked_at"],
|
|
2749
|
+
expires_at: row["expires_at"]
|
|
2750
|
+
};
|
|
2751
|
+
}
|
|
2752
|
+
function acquireLock(agentId, resourceType, resourceId, lockType = "exclusive", ttlSeconds = 300, db) {
|
|
2753
|
+
const d = db || getDatabase();
|
|
2754
|
+
cleanExpiredLocks(d);
|
|
2755
|
+
const ownLock = d.query("SELECT * FROM resource_locks WHERE resource_type = ? AND resource_id = ? AND agent_id = ? AND lock_type = ? AND expires_at > datetime('now')").get(resourceType, resourceId, agentId, lockType);
|
|
2756
|
+
if (ownLock) {
|
|
2757
|
+
const newExpiry = new Date(Date.now() + ttlSeconds * 1000).toISOString();
|
|
2758
|
+
d.run("UPDATE resource_locks SET expires_at = ? WHERE id = ?", [
|
|
2759
|
+
newExpiry,
|
|
2760
|
+
ownLock["id"]
|
|
2761
|
+
]);
|
|
2762
|
+
return parseLockRow({ ...ownLock, expires_at: newExpiry });
|
|
2763
|
+
}
|
|
2764
|
+
if (lockType === "exclusive") {
|
|
2765
|
+
const existing = d.query("SELECT * FROM resource_locks WHERE resource_type = ? AND resource_id = ? AND lock_type = 'exclusive' AND agent_id != ? AND expires_at > datetime('now')").get(resourceType, resourceId, agentId);
|
|
2766
|
+
if (existing) {
|
|
2767
|
+
return null;
|
|
2049
2768
|
}
|
|
2050
|
-
|
|
2051
|
-
|
|
2769
|
+
}
|
|
2770
|
+
const id = shortUuid();
|
|
2771
|
+
const lockedAt = now();
|
|
2772
|
+
const expiresAt = new Date(Date.now() + ttlSeconds * 1000).toISOString();
|
|
2773
|
+
d.run("INSERT INTO resource_locks (id, resource_type, resource_id, agent_id, lock_type, locked_at, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?)", [id, resourceType, resourceId, agentId, lockType, lockedAt, expiresAt]);
|
|
2774
|
+
return {
|
|
2775
|
+
id,
|
|
2776
|
+
resource_type: resourceType,
|
|
2777
|
+
resource_id: resourceId,
|
|
2778
|
+
agent_id: agentId,
|
|
2779
|
+
lock_type: lockType,
|
|
2780
|
+
locked_at: lockedAt,
|
|
2781
|
+
expires_at: expiresAt
|
|
2782
|
+
};
|
|
2783
|
+
}
|
|
2784
|
+
function releaseLock(lockId, agentId, db) {
|
|
2785
|
+
const d = db || getDatabase();
|
|
2786
|
+
const result = d.run("DELETE FROM resource_locks WHERE id = ? AND agent_id = ?", [lockId, agentId]);
|
|
2787
|
+
return result.changes > 0;
|
|
2788
|
+
}
|
|
2789
|
+
function releaseAllAgentLocks(agentId, db) {
|
|
2790
|
+
const d = db || getDatabase();
|
|
2791
|
+
const result = d.run("DELETE FROM resource_locks WHERE agent_id = ?", [agentId]);
|
|
2792
|
+
return result.changes;
|
|
2793
|
+
}
|
|
2794
|
+
function checkLock(resourceType, resourceId, lockType, db) {
|
|
2795
|
+
const d = db || getDatabase();
|
|
2796
|
+
cleanExpiredLocks(d);
|
|
2797
|
+
const query = lockType ? "SELECT * FROM resource_locks WHERE resource_type = ? AND resource_id = ? AND lock_type = ? AND expires_at > datetime('now')" : "SELECT * FROM resource_locks WHERE resource_type = ? AND resource_id = ? AND expires_at > datetime('now')";
|
|
2798
|
+
const rows = lockType ? d.query(query).all(resourceType, resourceId, lockType) : d.query(query).all(resourceType, resourceId);
|
|
2799
|
+
return rows.map(parseLockRow);
|
|
2800
|
+
}
|
|
2801
|
+
function listAgentLocks(agentId, db) {
|
|
2802
|
+
const d = db || getDatabase();
|
|
2803
|
+
cleanExpiredLocks(d);
|
|
2804
|
+
const rows = d.query("SELECT * FROM resource_locks WHERE agent_id = ? AND expires_at > datetime('now') ORDER BY locked_at DESC").all(agentId);
|
|
2805
|
+
return rows.map(parseLockRow);
|
|
2806
|
+
}
|
|
2807
|
+
function cleanExpiredLocks(db) {
|
|
2808
|
+
const d = db || getDatabase();
|
|
2809
|
+
const result = d.run("DELETE FROM resource_locks WHERE expires_at <= datetime('now')");
|
|
2810
|
+
return result.changes;
|
|
2811
|
+
}
|
|
2812
|
+
|
|
2813
|
+
// src/db/projects.ts
|
|
2814
|
+
init_database();
|
|
2815
|
+
function parseProjectRow(row) {
|
|
2816
|
+
return {
|
|
2817
|
+
id: row["id"],
|
|
2818
|
+
name: row["name"],
|
|
2819
|
+
path: row["path"],
|
|
2820
|
+
description: row["description"] || null,
|
|
2821
|
+
memory_prefix: row["memory_prefix"] || null,
|
|
2822
|
+
created_at: row["created_at"],
|
|
2823
|
+
updated_at: row["updated_at"]
|
|
2824
|
+
};
|
|
2825
|
+
}
|
|
2826
|
+
function registerProject(name, path, description, memoryPrefix, db) {
|
|
2827
|
+
const d = db || getDatabase();
|
|
2828
|
+
const timestamp = now();
|
|
2829
|
+
const existing = d.query("SELECT * FROM projects WHERE path = ?").get(path);
|
|
2830
|
+
if (existing) {
|
|
2831
|
+
const existingId = existing["id"];
|
|
2832
|
+
d.run("UPDATE projects SET updated_at = ? WHERE id = ?", [
|
|
2833
|
+
timestamp,
|
|
2834
|
+
existingId
|
|
2835
|
+
]);
|
|
2836
|
+
return parseProjectRow(existing);
|
|
2837
|
+
}
|
|
2838
|
+
const id = uuid();
|
|
2839
|
+
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]);
|
|
2840
|
+
return getProject(id, d);
|
|
2841
|
+
}
|
|
2842
|
+
function getProject(idOrPath, db) {
|
|
2843
|
+
const d = db || getDatabase();
|
|
2844
|
+
let row = d.query("SELECT * FROM projects WHERE id = ?").get(idOrPath);
|
|
2845
|
+
if (row)
|
|
2846
|
+
return parseProjectRow(row);
|
|
2847
|
+
row = d.query("SELECT * FROM projects WHERE path = ?").get(idOrPath);
|
|
2848
|
+
if (row)
|
|
2849
|
+
return parseProjectRow(row);
|
|
2850
|
+
row = d.query("SELECT * FROM projects WHERE LOWER(name) = ?").get(idOrPath.toLowerCase());
|
|
2851
|
+
if (row)
|
|
2852
|
+
return parseProjectRow(row);
|
|
2853
|
+
return null;
|
|
2854
|
+
}
|
|
2855
|
+
function listProjects(db) {
|
|
2856
|
+
const d = db || getDatabase();
|
|
2857
|
+
const rows = d.query("SELECT * FROM projects ORDER BY updated_at DESC").all();
|
|
2858
|
+
return rows.map(parseProjectRow);
|
|
2859
|
+
}
|
|
2860
|
+
|
|
2861
|
+
// src/lib/config.ts
|
|
2862
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, readdirSync, writeFileSync, unlinkSync } from "fs";
|
|
2863
|
+
import { homedir } from "os";
|
|
2864
|
+
import { basename, dirname as dirname2, join as join2, resolve as resolve2 } from "path";
|
|
2865
|
+
function findFileWalkingUp(filename) {
|
|
2866
|
+
let dir = process.cwd();
|
|
2867
|
+
while (true) {
|
|
2868
|
+
const candidate = join2(dir, filename);
|
|
2869
|
+
if (existsSync2(candidate)) {
|
|
2870
|
+
return candidate;
|
|
2052
2871
|
}
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
results.push({ memory, score, match_type: "fuzzy" });
|
|
2872
|
+
const parent = dirname2(dir);
|
|
2873
|
+
if (parent === dir) {
|
|
2874
|
+
return null;
|
|
2057
2875
|
}
|
|
2876
|
+
dir = parent;
|
|
2058
2877
|
}
|
|
2059
|
-
results.sort((a, b) => b.score - a.score);
|
|
2060
|
-
return results;
|
|
2061
2878
|
}
|
|
2062
|
-
function
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
if (exactMatch && !matchingEntities.find((e) => e.id === exactMatch.id)) {
|
|
2068
|
-
matchingEntities.push(exactMatch);
|
|
2879
|
+
function findGitRoot2() {
|
|
2880
|
+
let dir = process.cwd();
|
|
2881
|
+
while (true) {
|
|
2882
|
+
if (existsSync2(join2(dir, ".git"))) {
|
|
2883
|
+
return dir;
|
|
2069
2884
|
}
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
boostedIds.add(mem.id);
|
|
2074
|
-
}
|
|
2885
|
+
const parent = dirname2(dir);
|
|
2886
|
+
if (parent === dir) {
|
|
2887
|
+
return null;
|
|
2075
2888
|
}
|
|
2076
|
-
|
|
2077
|
-
|
|
2889
|
+
dir = parent;
|
|
2890
|
+
}
|
|
2078
2891
|
}
|
|
2079
|
-
function
|
|
2080
|
-
|
|
2081
|
-
return 1;
|
|
2082
|
-
const mostRecent = memory.accessed_at || memory.updated_at;
|
|
2083
|
-
if (!mostRecent)
|
|
2084
|
-
return 0;
|
|
2085
|
-
const daysSinceAccess = (Date.now() - Date.parse(mostRecent)) / (1000 * 60 * 60 * 24);
|
|
2086
|
-
return Math.max(0, 1 - daysSinceAccess / 30);
|
|
2892
|
+
function profilesDir() {
|
|
2893
|
+
return join2(homedir(), ".mementos", "profiles");
|
|
2087
2894
|
}
|
|
2088
|
-
function
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
const finalScore = (weightedScore + graphBoost) * (1 + recencyBoost * 0.3) * (1 + accessBoost);
|
|
2100
|
-
const matchType = determineMatchType(memory, queryLower);
|
|
2101
|
-
scored.push({
|
|
2102
|
-
memory,
|
|
2103
|
-
score: finalScore,
|
|
2104
|
-
match_type: matchType,
|
|
2105
|
-
highlights: extractHighlights(memory, queryLower)
|
|
2106
|
-
});
|
|
2895
|
+
function globalConfigPath() {
|
|
2896
|
+
return join2(homedir(), ".mementos", "config.json");
|
|
2897
|
+
}
|
|
2898
|
+
function readGlobalConfig() {
|
|
2899
|
+
const p = globalConfigPath();
|
|
2900
|
+
if (!existsSync2(p))
|
|
2901
|
+
return {};
|
|
2902
|
+
try {
|
|
2903
|
+
return JSON.parse(readFileSync(p, "utf-8"));
|
|
2904
|
+
} catch {
|
|
2905
|
+
return {};
|
|
2107
2906
|
}
|
|
2108
|
-
scored.sort((a, b) => {
|
|
2109
|
-
if (b.score !== a.score)
|
|
2110
|
-
return b.score - a.score;
|
|
2111
|
-
return b.memory.importance - a.memory.importance;
|
|
2112
|
-
});
|
|
2113
|
-
return scored;
|
|
2114
2907
|
}
|
|
2115
|
-
function
|
|
2116
|
-
const
|
|
2117
|
-
|
|
2118
|
-
|
|
2908
|
+
function getActiveProfile() {
|
|
2909
|
+
const envProfile = process.env["MEMENTOS_PROFILE"];
|
|
2910
|
+
if (envProfile)
|
|
2911
|
+
return envProfile.trim();
|
|
2912
|
+
const cfg = readGlobalConfig();
|
|
2913
|
+
return cfg["active_profile"] || null;
|
|
2914
|
+
}
|
|
2915
|
+
function listProfiles() {
|
|
2916
|
+
const dir = profilesDir();
|
|
2917
|
+
if (!existsSync2(dir))
|
|
2119
2918
|
return [];
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
scored = searchWithLike(d, query, queryLower, filter, graphBoostedIds);
|
|
2129
|
-
}
|
|
2130
|
-
} else {
|
|
2131
|
-
scored = searchWithLike(d, query, queryLower, filter, graphBoostedIds);
|
|
2919
|
+
return readdirSync(dir).filter((f) => f.endsWith(".db")).map((f) => basename(f, ".db")).sort();
|
|
2920
|
+
}
|
|
2921
|
+
function getDbPath2() {
|
|
2922
|
+
const envDbPath = process.env["MEMENTOS_DB_PATH"];
|
|
2923
|
+
if (envDbPath) {
|
|
2924
|
+
const resolved = resolve2(envDbPath);
|
|
2925
|
+
ensureDir2(dirname2(resolved));
|
|
2926
|
+
return resolved;
|
|
2132
2927
|
}
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
const
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2928
|
+
const profile = getActiveProfile();
|
|
2929
|
+
if (profile) {
|
|
2930
|
+
const profilePath = join2(profilesDir(), `${profile}.db`);
|
|
2931
|
+
ensureDir2(dirname2(profilePath));
|
|
2932
|
+
return profilePath;
|
|
2933
|
+
}
|
|
2934
|
+
const dbScope = process.env["MEMENTOS_DB_SCOPE"];
|
|
2935
|
+
if (dbScope === "project") {
|
|
2936
|
+
const gitRoot = findGitRoot2();
|
|
2937
|
+
if (gitRoot) {
|
|
2938
|
+
const dbPath = join2(gitRoot, ".mementos", "mementos.db");
|
|
2939
|
+
ensureDir2(dirname2(dbPath));
|
|
2940
|
+
return dbPath;
|
|
2141
2941
|
}
|
|
2142
|
-
scored.sort((a, b) => {
|
|
2143
|
-
if (b.score !== a.score)
|
|
2144
|
-
return b.score - a.score;
|
|
2145
|
-
return b.memory.importance - a.memory.importance;
|
|
2146
|
-
});
|
|
2147
2942
|
}
|
|
2148
|
-
const
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2943
|
+
const found = findFileWalkingUp(join2(".mementos", "mementos.db"));
|
|
2944
|
+
if (found) {
|
|
2945
|
+
return found;
|
|
2946
|
+
}
|
|
2947
|
+
const fallback = join2(homedir(), ".mementos", "mementos.db");
|
|
2948
|
+
ensureDir2(dirname2(fallback));
|
|
2949
|
+
return fallback;
|
|
2153
2950
|
}
|
|
2154
|
-
function
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
d.run("INSERT INTO search_history (id, query, result_count, agent_id, project_id) VALUES (?, ?, ?, ?, ?)", [id, query, resultCount, agentId || null, projectId || null]);
|
|
2159
|
-
} catch {}
|
|
2951
|
+
function ensureDir2(dir) {
|
|
2952
|
+
if (!existsSync2(dir)) {
|
|
2953
|
+
mkdirSync2(dir, { recursive: true });
|
|
2954
|
+
}
|
|
2160
2955
|
}
|
|
2161
2956
|
|
|
2957
|
+
// src/server/index.ts
|
|
2958
|
+
init_database();
|
|
2959
|
+
init_search();
|
|
2960
|
+
init_types();
|
|
2961
|
+
init_entities();
|
|
2962
|
+
init_relations();
|
|
2963
|
+
init_entity_memories();
|
|
2964
|
+
|
|
2162
2965
|
// src/lib/duration.ts
|
|
2163
2966
|
var UNIT_MS = {
|
|
2164
2967
|
s: 1000,
|
|
@@ -2202,6 +3005,205 @@ var FORMAT_UNITS = [
|
|
|
2202
3005
|
["s", UNIT_MS["s"]]
|
|
2203
3006
|
];
|
|
2204
3007
|
|
|
3008
|
+
// src/server/index.ts
|
|
3009
|
+
init_auto_memory();
|
|
3010
|
+
init_registry();
|
|
3011
|
+
init_hooks();
|
|
3012
|
+
|
|
3013
|
+
// src/lib/built-in-hooks.ts
|
|
3014
|
+
init_hooks();
|
|
3015
|
+
|
|
3016
|
+
// src/db/webhook_hooks.ts
|
|
3017
|
+
init_database();
|
|
3018
|
+
function parseRow(row) {
|
|
3019
|
+
return {
|
|
3020
|
+
id: row["id"],
|
|
3021
|
+
type: row["type"],
|
|
3022
|
+
handlerUrl: row["handler_url"],
|
|
3023
|
+
priority: row["priority"],
|
|
3024
|
+
blocking: Boolean(row["blocking"]),
|
|
3025
|
+
agentId: row["agent_id"] || undefined,
|
|
3026
|
+
projectId: row["project_id"] || undefined,
|
|
3027
|
+
description: row["description"] || undefined,
|
|
3028
|
+
enabled: Boolean(row["enabled"]),
|
|
3029
|
+
createdAt: row["created_at"],
|
|
3030
|
+
invocationCount: row["invocation_count"],
|
|
3031
|
+
failureCount: row["failure_count"]
|
|
3032
|
+
};
|
|
3033
|
+
}
|
|
3034
|
+
function createWebhookHook(input, db) {
|
|
3035
|
+
const d = db || getDatabase();
|
|
3036
|
+
const id = shortUuid();
|
|
3037
|
+
const timestamp = now();
|
|
3038
|
+
d.run(`INSERT INTO webhook_hooks
|
|
3039
|
+
(id, type, handler_url, priority, blocking, agent_id, project_id, description, enabled, created_at, invocation_count, failure_count)
|
|
3040
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1, ?, 0, 0)`, [
|
|
3041
|
+
id,
|
|
3042
|
+
input.type,
|
|
3043
|
+
input.handlerUrl,
|
|
3044
|
+
input.priority ?? 50,
|
|
3045
|
+
input.blocking ? 1 : 0,
|
|
3046
|
+
input.agentId ?? null,
|
|
3047
|
+
input.projectId ?? null,
|
|
3048
|
+
input.description ?? null,
|
|
3049
|
+
timestamp
|
|
3050
|
+
]);
|
|
3051
|
+
return getWebhookHook(id, d);
|
|
3052
|
+
}
|
|
3053
|
+
function getWebhookHook(id, db) {
|
|
3054
|
+
const d = db || getDatabase();
|
|
3055
|
+
const row = d.query("SELECT * FROM webhook_hooks WHERE id = ?").get(id);
|
|
3056
|
+
return row ? parseRow(row) : null;
|
|
3057
|
+
}
|
|
3058
|
+
function listWebhookHooks(filter = {}, db) {
|
|
3059
|
+
const d = db || getDatabase();
|
|
3060
|
+
const conditions = [];
|
|
3061
|
+
const params = [];
|
|
3062
|
+
if (filter.type) {
|
|
3063
|
+
conditions.push("type = ?");
|
|
3064
|
+
params.push(filter.type);
|
|
3065
|
+
}
|
|
3066
|
+
if (filter.enabled !== undefined) {
|
|
3067
|
+
conditions.push("enabled = ?");
|
|
3068
|
+
params.push(filter.enabled ? 1 : 0);
|
|
3069
|
+
}
|
|
3070
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
3071
|
+
const rows = d.query(`SELECT * FROM webhook_hooks ${where} ORDER BY priority ASC, created_at ASC`).all(...params);
|
|
3072
|
+
return rows.map(parseRow);
|
|
3073
|
+
}
|
|
3074
|
+
function updateWebhookHook(id, updates, db) {
|
|
3075
|
+
const d = db || getDatabase();
|
|
3076
|
+
const existing = getWebhookHook(id, d);
|
|
3077
|
+
if (!existing)
|
|
3078
|
+
return null;
|
|
3079
|
+
const sets = [];
|
|
3080
|
+
const params = [];
|
|
3081
|
+
if (updates.enabled !== undefined) {
|
|
3082
|
+
sets.push("enabled = ?");
|
|
3083
|
+
params.push(updates.enabled ? 1 : 0);
|
|
3084
|
+
}
|
|
3085
|
+
if (updates.description !== undefined) {
|
|
3086
|
+
sets.push("description = ?");
|
|
3087
|
+
params.push(updates.description);
|
|
3088
|
+
}
|
|
3089
|
+
if (updates.priority !== undefined) {
|
|
3090
|
+
sets.push("priority = ?");
|
|
3091
|
+
params.push(updates.priority);
|
|
3092
|
+
}
|
|
3093
|
+
if (sets.length > 0) {
|
|
3094
|
+
params.push(id);
|
|
3095
|
+
d.run(`UPDATE webhook_hooks SET ${sets.join(", ")} WHERE id = ?`, params);
|
|
3096
|
+
}
|
|
3097
|
+
return getWebhookHook(id, d);
|
|
3098
|
+
}
|
|
3099
|
+
function deleteWebhookHook(id, db) {
|
|
3100
|
+
const d = db || getDatabase();
|
|
3101
|
+
const result = d.run("DELETE FROM webhook_hooks WHERE id = ?", [id]);
|
|
3102
|
+
return result.changes > 0;
|
|
3103
|
+
}
|
|
3104
|
+
function recordWebhookInvocation(id, success, db) {
|
|
3105
|
+
const d = db || getDatabase();
|
|
3106
|
+
if (success) {
|
|
3107
|
+
d.run("UPDATE webhook_hooks SET invocation_count = invocation_count + 1 WHERE id = ?", [id]);
|
|
3108
|
+
} else {
|
|
3109
|
+
d.run("UPDATE webhook_hooks SET invocation_count = invocation_count + 1, failure_count = failure_count + 1 WHERE id = ?", [id]);
|
|
3110
|
+
}
|
|
3111
|
+
}
|
|
3112
|
+
|
|
3113
|
+
// src/lib/built-in-hooks.ts
|
|
3114
|
+
var _processConversationTurn = null;
|
|
3115
|
+
async function getAutoMemory() {
|
|
3116
|
+
if (!_processConversationTurn) {
|
|
3117
|
+
const mod = await Promise.resolve().then(() => (init_auto_memory(), exports_auto_memory));
|
|
3118
|
+
_processConversationTurn = mod.processConversationTurn;
|
|
3119
|
+
}
|
|
3120
|
+
return _processConversationTurn;
|
|
3121
|
+
}
|
|
3122
|
+
hookRegistry.register({
|
|
3123
|
+
type: "PostMemorySave",
|
|
3124
|
+
blocking: false,
|
|
3125
|
+
builtin: true,
|
|
3126
|
+
priority: 100,
|
|
3127
|
+
description: "Trigger async LLM entity extraction when a memory is saved",
|
|
3128
|
+
handler: async (ctx) => {
|
|
3129
|
+
if (ctx.wasUpdated)
|
|
3130
|
+
return;
|
|
3131
|
+
const processConversationTurn2 = await getAutoMemory();
|
|
3132
|
+
processConversationTurn2(`${ctx.memory.key}: ${ctx.memory.value}`, {
|
|
3133
|
+
agentId: ctx.agentId,
|
|
3134
|
+
projectId: ctx.projectId,
|
|
3135
|
+
sessionId: ctx.sessionId
|
|
3136
|
+
});
|
|
3137
|
+
}
|
|
3138
|
+
});
|
|
3139
|
+
hookRegistry.register({
|
|
3140
|
+
type: "OnSessionStart",
|
|
3141
|
+
blocking: false,
|
|
3142
|
+
builtin: true,
|
|
3143
|
+
priority: 100,
|
|
3144
|
+
description: "Record session start as a history memory for analytics",
|
|
3145
|
+
handler: async (ctx) => {
|
|
3146
|
+
const { createMemory: createMemory2 } = await Promise.resolve().then(() => (init_memories(), exports_memories));
|
|
3147
|
+
try {
|
|
3148
|
+
createMemory2({
|
|
3149
|
+
key: `session-start-${ctx.agentId}`,
|
|
3150
|
+
value: `Agent ${ctx.agentId} started session on project ${ctx.projectId} at ${new Date(ctx.timestamp).toISOString()}`,
|
|
3151
|
+
category: "history",
|
|
3152
|
+
scope: "shared",
|
|
3153
|
+
importance: 3,
|
|
3154
|
+
source: "system",
|
|
3155
|
+
agent_id: ctx.agentId,
|
|
3156
|
+
project_id: ctx.projectId,
|
|
3157
|
+
session_id: ctx.sessionId
|
|
3158
|
+
});
|
|
3159
|
+
} catch {}
|
|
3160
|
+
}
|
|
3161
|
+
});
|
|
3162
|
+
var _webhooksLoaded = false;
|
|
3163
|
+
function loadWebhooksFromDb() {
|
|
3164
|
+
if (_webhooksLoaded)
|
|
3165
|
+
return;
|
|
3166
|
+
_webhooksLoaded = true;
|
|
3167
|
+
try {
|
|
3168
|
+
const webhooks = listWebhookHooks({ enabled: true });
|
|
3169
|
+
for (const wh of webhooks) {
|
|
3170
|
+
hookRegistry.register({
|
|
3171
|
+
type: wh.type,
|
|
3172
|
+
blocking: wh.blocking,
|
|
3173
|
+
priority: wh.priority,
|
|
3174
|
+
agentId: wh.agentId,
|
|
3175
|
+
projectId: wh.projectId,
|
|
3176
|
+
description: wh.description ?? `Webhook: ${wh.handlerUrl}`,
|
|
3177
|
+
handler: makeWebhookHandler(wh.id, wh.handlerUrl)
|
|
3178
|
+
});
|
|
3179
|
+
}
|
|
3180
|
+
if (webhooks.length > 0) {
|
|
3181
|
+
console.log(`[hooks] Loaded ${webhooks.length} webhook(s) from DB`);
|
|
3182
|
+
}
|
|
3183
|
+
} catch (err) {
|
|
3184
|
+
console.error("[hooks] Failed to load webhooks from DB:", err);
|
|
3185
|
+
}
|
|
3186
|
+
}
|
|
3187
|
+
function makeWebhookHandler(webhookId, url) {
|
|
3188
|
+
return async (context) => {
|
|
3189
|
+
try {
|
|
3190
|
+
const res = await fetch(url, {
|
|
3191
|
+
method: "POST",
|
|
3192
|
+
headers: { "Content-Type": "application/json" },
|
|
3193
|
+
body: JSON.stringify(context),
|
|
3194
|
+
signal: AbortSignal.timeout(1e4)
|
|
3195
|
+
});
|
|
3196
|
+
recordWebhookInvocation(webhookId, res.ok);
|
|
3197
|
+
} catch {
|
|
3198
|
+
recordWebhookInvocation(webhookId, false);
|
|
3199
|
+
}
|
|
3200
|
+
};
|
|
3201
|
+
}
|
|
3202
|
+
function reloadWebhooks() {
|
|
3203
|
+
_webhooksLoaded = false;
|
|
3204
|
+
loadWebhooksFromDb();
|
|
3205
|
+
}
|
|
3206
|
+
|
|
2205
3207
|
// src/server/index.ts
|
|
2206
3208
|
var DEFAULT_PORT = 19428;
|
|
2207
3209
|
function parsePort() {
|
|
@@ -3161,7 +4163,125 @@ async function findFreePort(start) {
|
|
|
3161
4163
|
}
|
|
3162
4164
|
return start;
|
|
3163
4165
|
}
|
|
4166
|
+
addRoute("POST", "/api/auto-memory/process", async (req) => {
|
|
4167
|
+
const body = await readJson(req);
|
|
4168
|
+
const turn = body?.turn;
|
|
4169
|
+
if (!turn)
|
|
4170
|
+
return errorResponse("turn is required", 400);
|
|
4171
|
+
processConversationTurn(turn, { agentId: body?.agent_id, projectId: body?.project_id, sessionId: body?.session_id });
|
|
4172
|
+
const stats = getAutoMemoryStats();
|
|
4173
|
+
return json({ queued: true, queue: stats }, 202);
|
|
4174
|
+
});
|
|
4175
|
+
addRoute("GET", "/api/auto-memory/status", () => {
|
|
4176
|
+
return json({
|
|
4177
|
+
queue: getAutoMemoryStats(),
|
|
4178
|
+
config: providerRegistry.getConfig(),
|
|
4179
|
+
providers: providerRegistry.health()
|
|
4180
|
+
});
|
|
4181
|
+
});
|
|
4182
|
+
addRoute("GET", "/api/auto-memory/config", () => {
|
|
4183
|
+
return json(providerRegistry.getConfig());
|
|
4184
|
+
});
|
|
4185
|
+
addRoute("PATCH", "/api/auto-memory/config", async (req) => {
|
|
4186
|
+
const body = await readJson(req) ?? {};
|
|
4187
|
+
const patch = {};
|
|
4188
|
+
if (body.provider)
|
|
4189
|
+
patch.provider = body.provider;
|
|
4190
|
+
if (body.model)
|
|
4191
|
+
patch.model = body.model;
|
|
4192
|
+
if (body.enabled !== undefined)
|
|
4193
|
+
patch.enabled = Boolean(body.enabled);
|
|
4194
|
+
if (body.min_importance !== undefined)
|
|
4195
|
+
patch.minImportance = Number(body.min_importance);
|
|
4196
|
+
if (body.auto_entity_link !== undefined)
|
|
4197
|
+
patch.autoEntityLink = Boolean(body.auto_entity_link);
|
|
4198
|
+
configureAutoMemory(patch);
|
|
4199
|
+
return json({ updated: true, config: providerRegistry.getConfig() });
|
|
4200
|
+
});
|
|
4201
|
+
addRoute("POST", "/api/auto-memory/test", async (req) => {
|
|
4202
|
+
const body = await readJson(req) ?? {};
|
|
4203
|
+
const { turn, provider: providerName, agent_id, project_id } = body;
|
|
4204
|
+
if (!turn)
|
|
4205
|
+
return errorResponse("turn is required", 400);
|
|
4206
|
+
const provider = providerName ? providerRegistry.getProvider(providerName) : providerRegistry.getAvailable();
|
|
4207
|
+
if (!provider)
|
|
4208
|
+
return errorResponse("No LLM provider configured. Set an API key (ANTHROPIC_API_KEY, OPENAI_API_KEY, CEREBRAS_API_KEY, or XAI_API_KEY).", 503);
|
|
4209
|
+
const memories = await provider.extractMemories(turn, { agentId: agent_id, projectId: project_id });
|
|
4210
|
+
return json({
|
|
4211
|
+
provider: provider.name,
|
|
4212
|
+
model: provider.config.model,
|
|
4213
|
+
extracted: memories,
|
|
4214
|
+
count: memories.length,
|
|
4215
|
+
note: "DRY RUN \u2014 nothing was saved"
|
|
4216
|
+
});
|
|
4217
|
+
});
|
|
4218
|
+
addRoute("GET", "/api/hooks", (_req, url) => {
|
|
4219
|
+
const type = url.searchParams.get("type") ?? undefined;
|
|
4220
|
+
const hooks = hookRegistry.list(type);
|
|
4221
|
+
return json(hooks.map((h) => ({
|
|
4222
|
+
id: h.id,
|
|
4223
|
+
type: h.type,
|
|
4224
|
+
blocking: h.blocking,
|
|
4225
|
+
priority: h.priority,
|
|
4226
|
+
builtin: h.builtin ?? false,
|
|
4227
|
+
agentId: h.agentId,
|
|
4228
|
+
projectId: h.projectId,
|
|
4229
|
+
description: h.description
|
|
4230
|
+
})));
|
|
4231
|
+
});
|
|
4232
|
+
addRoute("GET", "/api/hooks/stats", () => json(hookRegistry.stats()));
|
|
4233
|
+
addRoute("GET", "/api/webhooks", (_req, url) => {
|
|
4234
|
+
const type = url.searchParams.get("type") ?? undefined;
|
|
4235
|
+
const enabledParam = url.searchParams.get("enabled");
|
|
4236
|
+
const enabled = enabledParam !== null ? enabledParam === "true" : undefined;
|
|
4237
|
+
return json(listWebhookHooks({
|
|
4238
|
+
type,
|
|
4239
|
+
enabled
|
|
4240
|
+
}));
|
|
4241
|
+
});
|
|
4242
|
+
addRoute("POST", "/api/webhooks", async (req) => {
|
|
4243
|
+
const body = await readJson(req) ?? {};
|
|
4244
|
+
if (!body.type || !body.handler_url) {
|
|
4245
|
+
return errorResponse("type and handler_url are required", 400);
|
|
4246
|
+
}
|
|
4247
|
+
const wh = createWebhookHook({
|
|
4248
|
+
type: body.type,
|
|
4249
|
+
handlerUrl: body.handler_url,
|
|
4250
|
+
priority: body.priority,
|
|
4251
|
+
blocking: body.blocking,
|
|
4252
|
+
agentId: body.agent_id,
|
|
4253
|
+
projectId: body.project_id,
|
|
4254
|
+
description: body.description
|
|
4255
|
+
});
|
|
4256
|
+
reloadWebhooks();
|
|
4257
|
+
return json(wh, 201);
|
|
4258
|
+
});
|
|
4259
|
+
addRoute("GET", "/api/webhooks/:id", (_req, _url, params) => {
|
|
4260
|
+
const wh = getWebhookHook(params["id"]);
|
|
4261
|
+
if (!wh)
|
|
4262
|
+
return errorResponse("Webhook not found", 404);
|
|
4263
|
+
return json(wh);
|
|
4264
|
+
});
|
|
4265
|
+
addRoute("PATCH", "/api/webhooks/:id", async (req, _url, params) => {
|
|
4266
|
+
const body = await readJson(req) ?? {};
|
|
4267
|
+
const updated = updateWebhookHook(params["id"], {
|
|
4268
|
+
enabled: body.enabled,
|
|
4269
|
+
priority: body.priority,
|
|
4270
|
+
description: body.description
|
|
4271
|
+
});
|
|
4272
|
+
if (!updated)
|
|
4273
|
+
return errorResponse("Webhook not found", 404);
|
|
4274
|
+
reloadWebhooks();
|
|
4275
|
+
return json(updated);
|
|
4276
|
+
});
|
|
4277
|
+
addRoute("DELETE", "/api/webhooks/:id", (_req, _url, params) => {
|
|
4278
|
+
const deleted = deleteWebhookHook(params["id"]);
|
|
4279
|
+
if (!deleted)
|
|
4280
|
+
return errorResponse("Webhook not found", 404);
|
|
4281
|
+
return new Response(null, { status: 204 });
|
|
4282
|
+
});
|
|
3164
4283
|
function startServer(port) {
|
|
4284
|
+
loadWebhooksFromDb();
|
|
3165
4285
|
const hostname = process.env["MEMENTOS_HOST"] ?? "127.0.0.1";
|
|
3166
4286
|
Bun.serve({
|
|
3167
4287
|
port,
|