@hasna/mementos 0.6.0 → 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 +496 -9
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/entities.d.ts.map +1 -1
- 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.js +160 -9
- package/dist/lib/built-in-hooks.d.ts +12 -0
- package/dist/lib/built-in-hooks.d.ts.map +1 -0
- package/dist/lib/focus.d.ts.map +1 -1
- package/dist/lib/hooks.d.ts +50 -0
- package/dist/lib/hooks.d.ts.map +1 -0
- package/dist/mcp/index.js +6805 -6178
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +2263 -1682
- package/dist/types/hooks.d.ts +136 -0
- package/dist/types/hooks.d.ts.map +1 -0
- package/package.json +1 -1
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')),
|
|
@@ -352,71 +398,34 @@ var MIGRATIONS = [
|
|
|
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);
|
|
354
400
|
`,
|
|
355
|
-
|
|
401
|
+
`
|
|
356
402
|
ALTER TABLE memories ADD COLUMN recall_count INTEGER NOT NULL DEFAULT 0;
|
|
357
403
|
CREATE INDEX IF NOT EXISTS idx_memories_recall_count ON memories(recall_count DESC);
|
|
358
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);
|
|
359
424
|
`
|
|
360
|
-
];
|
|
361
|
-
|
|
362
|
-
function getDatabase(dbPath) {
|
|
363
|
-
if (_db)
|
|
364
|
-
return _db;
|
|
365
|
-
const path = dbPath || getDbPath();
|
|
366
|
-
ensureDir(path);
|
|
367
|
-
_db = new Database(path, { create: true });
|
|
368
|
-
_db.run("PRAGMA journal_mode = WAL");
|
|
369
|
-
_db.run("PRAGMA busy_timeout = 5000");
|
|
370
|
-
_db.run("PRAGMA foreign_keys = ON");
|
|
371
|
-
runMigrations(_db);
|
|
372
|
-
return _db;
|
|
373
|
-
}
|
|
374
|
-
function runMigrations(db) {
|
|
375
|
-
try {
|
|
376
|
-
const result = db.query("SELECT MAX(id) as max_id FROM _migrations").get();
|
|
377
|
-
const currentLevel = result?.max_id ?? 0;
|
|
378
|
-
for (let i = currentLevel;i < MIGRATIONS.length; i++) {
|
|
379
|
-
try {
|
|
380
|
-
db.exec(MIGRATIONS[i]);
|
|
381
|
-
} catch {}
|
|
382
|
-
}
|
|
383
|
-
} catch {
|
|
384
|
-
for (const migration of MIGRATIONS) {
|
|
385
|
-
try {
|
|
386
|
-
db.exec(migration);
|
|
387
|
-
} catch {}
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
function now() {
|
|
392
|
-
return new Date().toISOString();
|
|
393
|
-
}
|
|
394
|
-
function uuid() {
|
|
395
|
-
return crypto.randomUUID();
|
|
396
|
-
}
|
|
397
|
-
function shortUuid() {
|
|
398
|
-
return crypto.randomUUID().slice(0, 8);
|
|
399
|
-
}
|
|
425
|
+
];
|
|
426
|
+
});
|
|
400
427
|
|
|
401
428
|
// src/lib/redact.ts
|
|
402
|
-
var REDACTED = "[REDACTED]";
|
|
403
|
-
var SECRET_PATTERNS = [
|
|
404
|
-
{ name: "openai_key", pattern: /sk-[a-zA-Z0-9_-]{20,}/g },
|
|
405
|
-
{ name: "anthropic_key", pattern: /sk-ant-[a-zA-Z0-9_-]{20,}/g },
|
|
406
|
-
{ name: "generic_key", pattern: /(?:pk|tok|key|token|api[_-]?key)[_-][a-zA-Z0-9_-]{16,}/gi },
|
|
407
|
-
{ name: "aws_key", pattern: /AKIA[A-Z0-9]{16}/g },
|
|
408
|
-
{ name: "aws_secret", pattern: /(?<=AWS_SECRET_ACCESS_KEY\s*=\s*)[A-Za-z0-9/+=]{40}/g },
|
|
409
|
-
{ name: "github_token", pattern: /gh[ps]_[a-zA-Z0-9]{36,}/g },
|
|
410
|
-
{ name: "github_oauth", pattern: /gho_[a-zA-Z0-9]{36,}/g },
|
|
411
|
-
{ name: "npm_token", pattern: /npm_[a-zA-Z0-9]{36,}/g },
|
|
412
|
-
{ name: "bearer", pattern: /Bearer\s+[a-zA-Z0-9_\-.]{20,}/g },
|
|
413
|
-
{ name: "conn_string", pattern: /(?:postgres|postgresql|mysql|mongodb|redis|amqp|mqtt):\/\/[^\s"'`]+@[^\s"'`]+/gi },
|
|
414
|
-
{ name: "env_secret", pattern: /(?:SECRET|TOKEN|PASSWORD|PASSPHRASE|API_KEY|PRIVATE_KEY|AUTH|CREDENTIAL)[_A-Z]*\s*=\s*["']?[^\s"'\n]{8,}["']?/gi },
|
|
415
|
-
{ name: "stripe_key", pattern: /(?:sk|pk|rk)_(?:test|live)_[a-zA-Z0-9]{20,}/g },
|
|
416
|
-
{ name: "slack_token", pattern: /xox[bpras]-[a-zA-Z0-9-]{20,}/g },
|
|
417
|
-
{ name: "jwt", pattern: /eyJ[a-zA-Z0-9_-]{10,}\.eyJ[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,}/g },
|
|
418
|
-
{ name: "hex_secret", pattern: /(?<=(?:key|token|secret|password|hash)\s*[:=]\s*["']?)[0-9a-f]{32,}(?=["']?)/gi }
|
|
419
|
-
];
|
|
420
429
|
function redactSecrets(text) {
|
|
421
430
|
let result = text;
|
|
422
431
|
for (const { pattern } of SECRET_PATTERNS) {
|
|
@@ -425,6 +434,109 @@ function redactSecrets(text) {
|
|
|
425
434
|
}
|
|
426
435
|
return result;
|
|
427
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
|
+
});
|
|
457
|
+
|
|
458
|
+
// src/lib/hooks.ts
|
|
459
|
+
function generateHookId() {
|
|
460
|
+
return `hook_${++_idCounter}_${Date.now().toString(36)}`;
|
|
461
|
+
}
|
|
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));
|
|
506
|
+
}
|
|
507
|
+
}
|
|
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;
|
|
527
|
+
}
|
|
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
|
+
};
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
var _idCounter = 0, hookRegistry;
|
|
537
|
+
var init_hooks = __esm(() => {
|
|
538
|
+
hookRegistry = new HookRegistry;
|
|
539
|
+
});
|
|
428
540
|
|
|
429
541
|
// src/db/entity-memories.ts
|
|
430
542
|
function parseEntityMemoryRow(row) {
|
|
@@ -475,8 +587,28 @@ function getEntityMemoryLinks(entityId, memoryId, db) {
|
|
|
475
587
|
const rows = d.query(sql).all(...params);
|
|
476
588
|
return rows.map(parseEntityMemoryRow);
|
|
477
589
|
}
|
|
590
|
+
var init_entity_memories = __esm(() => {
|
|
591
|
+
init_database();
|
|
592
|
+
init_memories();
|
|
593
|
+
});
|
|
478
594
|
|
|
479
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
|
+
});
|
|
480
612
|
function runEntityExtraction(_memory, _projectId, _d) {}
|
|
481
613
|
function parseMemoryRow(row) {
|
|
482
614
|
return {
|
|
@@ -579,9 +711,15 @@ function createMemory(input, dedupeMode = "merge", db) {
|
|
|
579
711
|
insertTag.run(id, tag);
|
|
580
712
|
}
|
|
581
713
|
const memory = getMemory(id, d);
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
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
|
+
});
|
|
585
723
|
return memory;
|
|
586
724
|
}
|
|
587
725
|
function getMemory(id, db) {
|
|
@@ -591,6 +729,52 @@ function getMemory(id, db) {
|
|
|
591
729
|
return null;
|
|
592
730
|
return parseMemoryRow(row);
|
|
593
731
|
}
|
|
732
|
+
function getMemoryByKey(key, scope, agentId, projectId, sessionId, db) {
|
|
733
|
+
const d = db || getDatabase();
|
|
734
|
+
let sql = "SELECT * FROM memories WHERE key = ?";
|
|
735
|
+
const params = [key];
|
|
736
|
+
if (scope) {
|
|
737
|
+
sql += " AND scope = ?";
|
|
738
|
+
params.push(scope);
|
|
739
|
+
}
|
|
740
|
+
if (agentId) {
|
|
741
|
+
sql += " AND agent_id = ?";
|
|
742
|
+
params.push(agentId);
|
|
743
|
+
}
|
|
744
|
+
if (projectId) {
|
|
745
|
+
sql += " AND project_id = ?";
|
|
746
|
+
params.push(projectId);
|
|
747
|
+
}
|
|
748
|
+
if (sessionId) {
|
|
749
|
+
sql += " AND session_id = ?";
|
|
750
|
+
params.push(sessionId);
|
|
751
|
+
}
|
|
752
|
+
sql += " AND status = 'active' ORDER BY importance DESC LIMIT 1";
|
|
753
|
+
const row = d.query(sql).get(...params);
|
|
754
|
+
if (!row)
|
|
755
|
+
return null;
|
|
756
|
+
return parseMemoryRow(row);
|
|
757
|
+
}
|
|
758
|
+
function getMemoriesByKey(key, scope, agentId, projectId, db) {
|
|
759
|
+
const d = db || getDatabase();
|
|
760
|
+
let sql = "SELECT * FROM memories WHERE key = ?";
|
|
761
|
+
const params = [key];
|
|
762
|
+
if (scope) {
|
|
763
|
+
sql += " AND scope = ?";
|
|
764
|
+
params.push(scope);
|
|
765
|
+
}
|
|
766
|
+
if (agentId) {
|
|
767
|
+
sql += " AND agent_id = ?";
|
|
768
|
+
params.push(agentId);
|
|
769
|
+
}
|
|
770
|
+
if (projectId) {
|
|
771
|
+
sql += " AND project_id = ?";
|
|
772
|
+
params.push(projectId);
|
|
773
|
+
}
|
|
774
|
+
sql += " AND status = 'active' ORDER BY importance DESC";
|
|
775
|
+
const rows = d.query(sql).all(...params);
|
|
776
|
+
return rows.map(parseMemoryRow);
|
|
777
|
+
}
|
|
594
778
|
function listMemories(filter, db) {
|
|
595
779
|
const d = db || getDatabase();
|
|
596
780
|
const conditions = [];
|
|
@@ -759,26 +943,65 @@ function updateMemory(id, input, db) {
|
|
|
759
943
|
params.push(id);
|
|
760
944
|
d.run(`UPDATE memories SET ${sets.join(", ")} WHERE id = ?`, params);
|
|
761
945
|
const updated = getMemory(id, d);
|
|
762
|
-
|
|
763
|
-
|
|
946
|
+
if (input.value !== undefined) {
|
|
947
|
+
try {
|
|
764
948
|
const oldLinks = getEntityMemoryLinks(undefined, updated.id, d);
|
|
765
949
|
for (const link of oldLinks) {
|
|
766
950
|
unlinkEntityFromMemory(link.entity_id, updated.id, d);
|
|
767
951
|
}
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
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
|
+
});
|
|
771
962
|
return updated;
|
|
772
963
|
}
|
|
773
964
|
function deleteMemory(id, db) {
|
|
774
965
|
const d = db || getDatabase();
|
|
775
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
|
+
}
|
|
776
973
|
return result.changes > 0;
|
|
777
974
|
}
|
|
975
|
+
function bulkDeleteMemories(ids, db) {
|
|
976
|
+
const d = db || getDatabase();
|
|
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;
|
|
986
|
+
}
|
|
778
987
|
function touchMemory(id, db) {
|
|
779
988
|
const d = db || getDatabase();
|
|
780
989
|
d.run("UPDATE memories SET access_count = access_count + 1, accessed_at = ? WHERE id = ?", [now(), id]);
|
|
781
990
|
}
|
|
991
|
+
function incrementRecallCount(id, db) {
|
|
992
|
+
const d = db || getDatabase();
|
|
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 {}
|
|
1004
|
+
}
|
|
782
1005
|
function cleanExpiredMemories(db) {
|
|
783
1006
|
const d = db || getDatabase();
|
|
784
1007
|
const timestamp = now();
|
|
@@ -811,994 +1034,595 @@ function getMemoryVersions(memoryId, db) {
|
|
|
811
1034
|
return [];
|
|
812
1035
|
}
|
|
813
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
|
+
});
|
|
814
1045
|
|
|
815
|
-
// src/db/
|
|
816
|
-
|
|
817
|
-
function parseAgentRow(row) {
|
|
1046
|
+
// src/db/entities.ts
|
|
1047
|
+
function parseEntityRow(row) {
|
|
818
1048
|
return {
|
|
819
1049
|
id: row["id"],
|
|
820
1050
|
name: row["name"],
|
|
821
|
-
|
|
1051
|
+
type: row["type"],
|
|
822
1052
|
description: row["description"] || null,
|
|
823
|
-
role: row["role"] || null,
|
|
824
1053
|
metadata: JSON.parse(row["metadata"] || "{}"),
|
|
825
|
-
|
|
1054
|
+
project_id: row["project_id"] || null,
|
|
826
1055
|
created_at: row["created_at"],
|
|
827
|
-
|
|
1056
|
+
updated_at: row["updated_at"]
|
|
828
1057
|
};
|
|
829
1058
|
}
|
|
830
|
-
function
|
|
1059
|
+
function createEntity(input, db) {
|
|
831
1060
|
const d = db || getDatabase();
|
|
832
1061
|
const timestamp = now();
|
|
833
|
-
const
|
|
834
|
-
const existing = d.query(
|
|
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 || "");
|
|
835
1065
|
if (existing) {
|
|
836
|
-
const
|
|
837
|
-
const
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
const nowMs = Date.now();
|
|
842
|
-
if (nowMs - lastSeenMs < CONFLICT_WINDOW_MS) {
|
|
843
|
-
throw new AgentConflictError({
|
|
844
|
-
existing_id: existingId,
|
|
845
|
-
existing_name: normalizedName,
|
|
846
|
-
last_seen_at: existingLastSeen,
|
|
847
|
-
session_hint: existingSessionId.slice(0, 8),
|
|
848
|
-
working_dir: null
|
|
849
|
-
});
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
d.run("UPDATE agents SET last_seen_at = ?, session_id = ? WHERE id = ?", [
|
|
853
|
-
timestamp,
|
|
854
|
-
sessionId ?? existingSessionId,
|
|
855
|
-
existingId
|
|
856
|
-
]);
|
|
857
|
-
if (description) {
|
|
858
|
-
d.run("UPDATE agents SET description = ? WHERE id = ?", [description, existingId]);
|
|
1066
|
+
const sets = ["updated_at = ?"];
|
|
1067
|
+
const params = [timestamp];
|
|
1068
|
+
if (input.description !== undefined) {
|
|
1069
|
+
sets.push("description = ?");
|
|
1070
|
+
params.push(input.description);
|
|
859
1071
|
}
|
|
860
|
-
if (
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
if (projectId !== undefined) {
|
|
864
|
-
d.run("UPDATE agents SET active_project_id = ? WHERE id = ?", [projectId, existingId]);
|
|
1072
|
+
if (input.metadata !== undefined) {
|
|
1073
|
+
sets.push("metadata = ?");
|
|
1074
|
+
params.push(metadataJson);
|
|
865
1075
|
}
|
|
866
|
-
|
|
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);
|
|
867
1080
|
}
|
|
868
1081
|
const id = shortUuid();
|
|
869
|
-
d.run(
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
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);
|
|
884
1101
|
}
|
|
885
|
-
function
|
|
1102
|
+
function getEntity(id, db) {
|
|
886
1103
|
const d = db || getDatabase();
|
|
887
|
-
const
|
|
888
|
-
|
|
1104
|
+
const row = d.query("SELECT * FROM entities WHERE id = ?").get(id);
|
|
1105
|
+
if (!row)
|
|
1106
|
+
throw new EntityNotFoundError(id);
|
|
1107
|
+
return parseEntityRow(row);
|
|
889
1108
|
}
|
|
890
|
-
function
|
|
1109
|
+
function getEntityByName(name, type, projectId, db) {
|
|
891
1110
|
const d = db || getDatabase();
|
|
892
|
-
|
|
893
|
-
|
|
1111
|
+
let sql = "SELECT * FROM entities WHERE name = ?";
|
|
1112
|
+
const params = [name];
|
|
1113
|
+
if (type) {
|
|
1114
|
+
sql += " AND type = ?";
|
|
1115
|
+
params.push(type);
|
|
1116
|
+
}
|
|
1117
|
+
if (projectId !== undefined) {
|
|
1118
|
+
sql += " AND project_id = ?";
|
|
1119
|
+
params.push(projectId);
|
|
1120
|
+
}
|
|
1121
|
+
sql += " LIMIT 1";
|
|
1122
|
+
const row = d.query(sql).get(...params);
|
|
1123
|
+
if (!row)
|
|
1124
|
+
return null;
|
|
1125
|
+
return parseEntityRow(row);
|
|
894
1126
|
}
|
|
895
|
-
function
|
|
1127
|
+
function listEntities(filter = {}, db) {
|
|
896
1128
|
const d = db || getDatabase();
|
|
897
|
-
const
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
const normalizedNewName = updates.name.trim().toLowerCase();
|
|
903
|
-
if (normalizedNewName !== agent.name) {
|
|
904
|
-
const existing = d.query("SELECT id FROM agents WHERE LOWER(name) = ? AND id != ?").get(normalizedNewName, agent.id);
|
|
905
|
-
if (existing) {
|
|
906
|
-
throw new Error(`Agent name already taken: ${normalizedNewName}`);
|
|
907
|
-
}
|
|
908
|
-
d.run("UPDATE agents SET name = ? WHERE id = ?", [normalizedNewName, agent.id]);
|
|
909
|
-
}
|
|
1129
|
+
const conditions = [];
|
|
1130
|
+
const params = [];
|
|
1131
|
+
if (filter.type) {
|
|
1132
|
+
conditions.push("type = ?");
|
|
1133
|
+
params.push(filter.type);
|
|
910
1134
|
}
|
|
911
|
-
if (
|
|
912
|
-
|
|
1135
|
+
if (filter.project_id) {
|
|
1136
|
+
conditions.push("project_id = ?");
|
|
1137
|
+
params.push(filter.project_id);
|
|
913
1138
|
}
|
|
914
|
-
if (
|
|
915
|
-
|
|
1139
|
+
if (filter.search) {
|
|
1140
|
+
conditions.push("(name LIKE ? OR description LIKE ?)");
|
|
1141
|
+
const term = `%${filter.search}%`;
|
|
1142
|
+
params.push(term, term);
|
|
916
1143
|
}
|
|
917
|
-
|
|
918
|
-
|
|
1144
|
+
let sql = "SELECT * FROM entities";
|
|
1145
|
+
if (conditions.length > 0) {
|
|
1146
|
+
sql += ` WHERE ${conditions.join(" AND ")}`;
|
|
919
1147
|
}
|
|
920
|
-
|
|
921
|
-
|
|
1148
|
+
sql += " ORDER BY updated_at DESC";
|
|
1149
|
+
if (filter.limit) {
|
|
1150
|
+
sql += " LIMIT ?";
|
|
1151
|
+
params.push(filter.limit);
|
|
922
1152
|
}
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
return {
|
|
930
|
-
id: row["id"],
|
|
931
|
-
resource_type: row["resource_type"],
|
|
932
|
-
resource_id: row["resource_id"],
|
|
933
|
-
agent_id: row["agent_id"],
|
|
934
|
-
lock_type: row["lock_type"],
|
|
935
|
-
locked_at: row["locked_at"],
|
|
936
|
-
expires_at: row["expires_at"]
|
|
937
|
-
};
|
|
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);
|
|
938
1159
|
}
|
|
939
|
-
function
|
|
1160
|
+
function updateEntity(id, input, db) {
|
|
940
1161
|
const d = db || getDatabase();
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
return parseLockRow({ ...ownLock, expires_at: newExpiry });
|
|
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);
|
|
950
1170
|
}
|
|
951
|
-
if (
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
return null;
|
|
955
|
-
}
|
|
1171
|
+
if (input.type !== undefined) {
|
|
1172
|
+
sets.push("type = ?");
|
|
1173
|
+
params.push(input.type);
|
|
956
1174
|
}
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
expires_at: expiresAt
|
|
969
|
-
};
|
|
970
|
-
}
|
|
971
|
-
function releaseLock(lockId, agentId, db) {
|
|
972
|
-
const d = db || getDatabase();
|
|
973
|
-
const result = d.run("DELETE FROM resource_locks WHERE id = ? AND agent_id = ?", [lockId, agentId]);
|
|
974
|
-
return result.changes > 0;
|
|
975
|
-
}
|
|
976
|
-
function releaseAllAgentLocks(agentId, db) {
|
|
977
|
-
const d = db || getDatabase();
|
|
978
|
-
const result = d.run("DELETE FROM resource_locks WHERE agent_id = ?", [agentId]);
|
|
979
|
-
return result.changes;
|
|
980
|
-
}
|
|
981
|
-
function checkLock(resourceType, resourceId, lockType, db) {
|
|
982
|
-
const d = db || getDatabase();
|
|
983
|
-
cleanExpiredLocks(d);
|
|
984
|
-
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')";
|
|
985
|
-
const rows = lockType ? d.query(query).all(resourceType, resourceId, lockType) : d.query(query).all(resourceType, resourceId);
|
|
986
|
-
return rows.map(parseLockRow);
|
|
1175
|
+
if (input.description !== undefined) {
|
|
1176
|
+
sets.push("description = ?");
|
|
1177
|
+
params.push(input.description);
|
|
1178
|
+
}
|
|
1179
|
+
if (input.metadata !== undefined) {
|
|
1180
|
+
sets.push("metadata = ?");
|
|
1181
|
+
params.push(JSON.stringify(input.metadata));
|
|
1182
|
+
}
|
|
1183
|
+
params.push(id);
|
|
1184
|
+
d.run(`UPDATE entities SET ${sets.join(", ")} WHERE id = ?`, params);
|
|
1185
|
+
return getEntity(id, d);
|
|
987
1186
|
}
|
|
988
|
-
function
|
|
1187
|
+
function deleteEntity(id, db) {
|
|
989
1188
|
const d = db || getDatabase();
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
1189
|
+
const result = d.run("DELETE FROM entities WHERE id = ?", [id]);
|
|
1190
|
+
if (result.changes === 0)
|
|
1191
|
+
throw new EntityNotFoundError(id);
|
|
993
1192
|
}
|
|
994
|
-
function
|
|
1193
|
+
function mergeEntities(sourceId, targetId, db) {
|
|
995
1194
|
const d = db || getDatabase();
|
|
996
|
-
|
|
997
|
-
|
|
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);
|
|
998
1208
|
}
|
|
1209
|
+
var init_entities = __esm(() => {
|
|
1210
|
+
init_database();
|
|
1211
|
+
init_types();
|
|
1212
|
+
init_hooks();
|
|
1213
|
+
});
|
|
999
1214
|
|
|
1000
|
-
// src/
|
|
1001
|
-
function
|
|
1215
|
+
// src/lib/search.ts
|
|
1216
|
+
function parseMemoryRow2(row) {
|
|
1002
1217
|
return {
|
|
1003
1218
|
id: row["id"],
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1219
|
+
key: row["key"],
|
|
1220
|
+
value: row["value"],
|
|
1221
|
+
category: row["category"],
|
|
1222
|
+
scope: row["scope"],
|
|
1223
|
+
summary: row["summary"] || null,
|
|
1224
|
+
tags: JSON.parse(row["tags"] || "[]"),
|
|
1225
|
+
importance: row["importance"],
|
|
1226
|
+
source: row["source"],
|
|
1227
|
+
status: row["status"],
|
|
1228
|
+
pinned: !!row["pinned"],
|
|
1229
|
+
agent_id: row["agent_id"] || null,
|
|
1230
|
+
project_id: row["project_id"] || null,
|
|
1231
|
+
session_id: row["session_id"] || null,
|
|
1232
|
+
metadata: JSON.parse(row["metadata"] || "{}"),
|
|
1233
|
+
access_count: row["access_count"],
|
|
1234
|
+
version: row["version"],
|
|
1235
|
+
expires_at: row["expires_at"] || null,
|
|
1008
1236
|
created_at: row["created_at"],
|
|
1009
|
-
updated_at: row["updated_at"]
|
|
1237
|
+
updated_at: row["updated_at"],
|
|
1238
|
+
accessed_at: row["accessed_at"] || null
|
|
1010
1239
|
};
|
|
1011
1240
|
}
|
|
1012
|
-
function
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
const existingId = existing["id"];
|
|
1018
|
-
d.run("UPDATE projects SET updated_at = ? WHERE id = ?", [
|
|
1019
|
-
timestamp,
|
|
1020
|
-
existingId
|
|
1021
|
-
]);
|
|
1022
|
-
return parseProjectRow(existing);
|
|
1023
|
-
}
|
|
1024
|
-
const id = uuid();
|
|
1025
|
-
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]);
|
|
1026
|
-
return getProject(id, d);
|
|
1241
|
+
function preprocessQuery(query) {
|
|
1242
|
+
let q = query.trim();
|
|
1243
|
+
q = q.replace(/\s+/g, " ");
|
|
1244
|
+
q = q.normalize("NFC");
|
|
1245
|
+
return q;
|
|
1027
1246
|
}
|
|
1028
|
-
function
|
|
1029
|
-
|
|
1030
|
-
let row = d.query("SELECT * FROM projects WHERE id = ?").get(idOrPath);
|
|
1031
|
-
if (row)
|
|
1032
|
-
return parseProjectRow(row);
|
|
1033
|
-
row = d.query("SELECT * FROM projects WHERE path = ?").get(idOrPath);
|
|
1034
|
-
if (row)
|
|
1035
|
-
return parseProjectRow(row);
|
|
1036
|
-
row = d.query("SELECT * FROM projects WHERE LOWER(name) = ?").get(idOrPath.toLowerCase());
|
|
1037
|
-
if (row)
|
|
1038
|
-
return parseProjectRow(row);
|
|
1039
|
-
return null;
|
|
1247
|
+
function escapeLikePattern(s) {
|
|
1248
|
+
return s.replace(/%/g, "\\%").replace(/_/g, "\\_");
|
|
1040
1249
|
}
|
|
1041
|
-
function
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
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;
|
|
1045
1255
|
}
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
const
|
|
1055
|
-
|
|
1056
|
-
|
|
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;
|
|
1277
|
+
}
|
|
1057
1278
|
}
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1279
|
+
}
|
|
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 });
|
|
1061
1283
|
}
|
|
1062
|
-
dir = parent;
|
|
1063
1284
|
}
|
|
1285
|
+
return highlights;
|
|
1064
1286
|
}
|
|
1065
|
-
function
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
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";
|
|
1295
|
+
}
|
|
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;
|
|
1343
|
+
}
|
|
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;
|
|
1348
|
+
}
|
|
1349
|
+
if (memory.summary && memory.summary.toLowerCase().includes(token)) {
|
|
1350
|
+
tokenScore += 4 / tokens.length;
|
|
1351
|
+
}
|
|
1352
|
+
if (memory.value.toLowerCase().includes(token)) {
|
|
1353
|
+
tokenScore += 3 / tokens.length;
|
|
1354
|
+
}
|
|
1355
|
+
if (metadataStr !== "{}" && metadataStr.includes(token)) {
|
|
1356
|
+
tokenScore += 2 / tokens.length;
|
|
1357
|
+
}
|
|
1070
1358
|
}
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1359
|
+
if (score > 0) {
|
|
1360
|
+
score += tokenScore * 0.3;
|
|
1361
|
+
} else {
|
|
1362
|
+
score += tokenScore;
|
|
1074
1363
|
}
|
|
1075
|
-
dir = parent;
|
|
1076
1364
|
}
|
|
1365
|
+
return score;
|
|
1077
1366
|
}
|
|
1078
|
-
function
|
|
1079
|
-
|
|
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() };
|
|
1080
1374
|
}
|
|
1081
|
-
function
|
|
1082
|
-
|
|
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, '""')}"`);
|
|
1380
|
+
}
|
|
1381
|
+
const tokens = removeStopWords(remainder.split(/\s+/).filter(Boolean));
|
|
1382
|
+
for (const t of tokens) {
|
|
1383
|
+
parts.push(`"${t.replace(/"/g, '""')}"`);
|
|
1384
|
+
}
|
|
1385
|
+
return parts.join(" ");
|
|
1083
1386
|
}
|
|
1084
|
-
function
|
|
1085
|
-
const p = globalConfigPath();
|
|
1086
|
-
if (!existsSync2(p))
|
|
1087
|
-
return {};
|
|
1387
|
+
function hasFts5Table(d) {
|
|
1088
1388
|
try {
|
|
1089
|
-
|
|
1389
|
+
const row = d.query("SELECT name FROM sqlite_master WHERE type='table' AND name='memories_fts'").get();
|
|
1390
|
+
return !!row;
|
|
1090
1391
|
} catch {
|
|
1091
|
-
return
|
|
1392
|
+
return false;
|
|
1092
1393
|
}
|
|
1093
1394
|
}
|
|
1094
|
-
function
|
|
1095
|
-
const
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
}
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
if (envDbPath) {
|
|
1110
|
-
const resolved = resolve2(envDbPath);
|
|
1111
|
-
ensureDir2(dirname2(resolved));
|
|
1112
|
-
return resolved;
|
|
1113
|
-
}
|
|
1114
|
-
const profile = getActiveProfile();
|
|
1115
|
-
if (profile) {
|
|
1116
|
-
const profilePath = join2(profilesDir(), `${profile}.db`);
|
|
1117
|
-
ensureDir2(dirname2(profilePath));
|
|
1118
|
-
return profilePath;
|
|
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
|
+
}
|
|
1119
1410
|
}
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
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);
|
|
1127
1418
|
}
|
|
1128
1419
|
}
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
}
|
|
1137
|
-
function ensureDir2(dir) {
|
|
1138
|
-
if (!existsSync2(dir)) {
|
|
1139
|
-
mkdirSync2(dir, { recursive: true });
|
|
1140
|
-
}
|
|
1141
|
-
}
|
|
1142
|
-
|
|
1143
|
-
// src/db/entities.ts
|
|
1144
|
-
function parseEntityRow(row) {
|
|
1145
|
-
return {
|
|
1146
|
-
id: row["id"],
|
|
1147
|
-
name: row["name"],
|
|
1148
|
-
type: row["type"],
|
|
1149
|
-
description: row["description"] || null,
|
|
1150
|
-
metadata: JSON.parse(row["metadata"] || "{}"),
|
|
1151
|
-
project_id: row["project_id"] || null,
|
|
1152
|
-
created_at: row["created_at"],
|
|
1153
|
-
updated_at: row["updated_at"]
|
|
1154
|
-
};
|
|
1155
|
-
}
|
|
1156
|
-
function createEntity(input, db) {
|
|
1157
|
-
const d = db || getDatabase();
|
|
1158
|
-
const timestamp = now();
|
|
1159
|
-
const metadataJson = JSON.stringify(input.metadata || {});
|
|
1160
|
-
const existing = d.query(`SELECT * FROM entities
|
|
1161
|
-
WHERE name = ? AND type = ? AND COALESCE(project_id, '') = ?`).get(input.name, input.type, input.project_id || "");
|
|
1162
|
-
if (existing) {
|
|
1163
|
-
const sets = ["updated_at = ?"];
|
|
1164
|
-
const params = [timestamp];
|
|
1165
|
-
if (input.description !== undefined) {
|
|
1166
|
-
sets.push("description = ?");
|
|
1167
|
-
params.push(input.description);
|
|
1168
|
-
}
|
|
1169
|
-
if (input.metadata !== undefined) {
|
|
1170
|
-
sets.push("metadata = ?");
|
|
1171
|
-
params.push(metadataJson);
|
|
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);
|
|
1172
1427
|
}
|
|
1173
|
-
const existingId = existing["id"];
|
|
1174
|
-
params.push(existingId);
|
|
1175
|
-
d.run(`UPDATE entities SET ${sets.join(", ")} WHERE id = ?`, params);
|
|
1176
|
-
return getEntity(existingId, d);
|
|
1177
|
-
}
|
|
1178
|
-
const id = shortUuid();
|
|
1179
|
-
d.run(`INSERT INTO entities (id, name, type, description, metadata, project_id, created_at, updated_at)
|
|
1180
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
1181
|
-
id,
|
|
1182
|
-
input.name,
|
|
1183
|
-
input.type,
|
|
1184
|
-
input.description || null,
|
|
1185
|
-
metadataJson,
|
|
1186
|
-
input.project_id || null,
|
|
1187
|
-
timestamp,
|
|
1188
|
-
timestamp
|
|
1189
|
-
]);
|
|
1190
|
-
return getEntity(id, d);
|
|
1191
|
-
}
|
|
1192
|
-
function getEntity(id, db) {
|
|
1193
|
-
const d = db || getDatabase();
|
|
1194
|
-
const row = d.query("SELECT * FROM entities WHERE id = ?").get(id);
|
|
1195
|
-
if (!row)
|
|
1196
|
-
throw new EntityNotFoundError(id);
|
|
1197
|
-
return parseEntityRow(row);
|
|
1198
|
-
}
|
|
1199
|
-
function getEntityByName(name, type, projectId, db) {
|
|
1200
|
-
const d = db || getDatabase();
|
|
1201
|
-
let sql = "SELECT * FROM entities WHERE name = ?";
|
|
1202
|
-
const params = [name];
|
|
1203
|
-
if (type) {
|
|
1204
|
-
sql += " AND type = ?";
|
|
1205
|
-
params.push(type);
|
|
1206
1428
|
}
|
|
1207
|
-
if (
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
}
|
|
1217
|
-
function listEntities(filter = {}, db) {
|
|
1218
|
-
const d = db || getDatabase();
|
|
1219
|
-
const conditions = [];
|
|
1220
|
-
const params = [];
|
|
1221
|
-
if (filter.type) {
|
|
1222
|
-
conditions.push("type = ?");
|
|
1223
|
-
params.push(filter.type);
|
|
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
|
+
}
|
|
1224
1438
|
}
|
|
1225
1439
|
if (filter.project_id) {
|
|
1226
|
-
conditions.push("project_id = ?");
|
|
1440
|
+
conditions.push("m.project_id = ?");
|
|
1227
1441
|
params.push(filter.project_id);
|
|
1228
1442
|
}
|
|
1229
|
-
if (filter.
|
|
1230
|
-
conditions.push("
|
|
1231
|
-
|
|
1232
|
-
params.push(term, term);
|
|
1443
|
+
if (filter.agent_id) {
|
|
1444
|
+
conditions.push("m.agent_id = ?");
|
|
1445
|
+
params.push(filter.agent_id);
|
|
1233
1446
|
}
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1447
|
+
if (filter.session_id) {
|
|
1448
|
+
conditions.push("m.session_id = ?");
|
|
1449
|
+
params.push(filter.session_id);
|
|
1237
1450
|
}
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
params.push(filter.limit);
|
|
1451
|
+
if (filter.min_importance) {
|
|
1452
|
+
conditions.push("m.importance >= ?");
|
|
1453
|
+
params.push(filter.min_importance);
|
|
1242
1454
|
}
|
|
1243
|
-
if (filter.
|
|
1244
|
-
|
|
1245
|
-
params.push(filter.
|
|
1455
|
+
if (filter.pinned !== undefined) {
|
|
1456
|
+
conditions.push("m.pinned = ?");
|
|
1457
|
+
params.push(filter.pinned ? 1 : 0);
|
|
1246
1458
|
}
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
const existing = d.query("SELECT id FROM entities WHERE id = ?").get(id);
|
|
1253
|
-
if (!existing)
|
|
1254
|
-
throw new EntityNotFoundError(id);
|
|
1255
|
-
const sets = ["updated_at = ?"];
|
|
1256
|
-
const params = [now()];
|
|
1257
|
-
if (input.name !== undefined) {
|
|
1258
|
-
sets.push("name = ?");
|
|
1259
|
-
params.push(input.name);
|
|
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);
|
|
1463
|
+
}
|
|
1260
1464
|
}
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1465
|
+
return { conditions, params };
|
|
1466
|
+
}
|
|
1467
|
+
function searchWithFts5(d, query, queryLower, filter, graphBoostedIds) {
|
|
1468
|
+
const ftsQuery = escapeFts5Query(query);
|
|
1469
|
+
if (!ftsQuery)
|
|
1470
|
+
return null;
|
|
1471
|
+
try {
|
|
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);
|
|
1480
|
+
} catch {
|
|
1481
|
+
return null;
|
|
1264
1482
|
}
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1483
|
+
}
|
|
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)}%`);
|
|
1268
1493
|
}
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
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);
|
|
1272
1506
|
}
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
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);
|
|
1276
1511
|
}
|
|
1277
|
-
function
|
|
1278
|
-
const
|
|
1279
|
-
const
|
|
1280
|
-
|
|
1281
|
-
|
|
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));
|
|
1517
|
+
}
|
|
1518
|
+
return trigrams;
|
|
1282
1519
|
}
|
|
1283
|
-
function
|
|
1284
|
-
const
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
d.run("DELETE FROM entities WHERE id = ?", [sourceId]);
|
|
1296
|
-
d.run("UPDATE entities SET updated_at = ? WHERE id = ?", [now(), targetId]);
|
|
1297
|
-
return getEntity(targetId, d);
|
|
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;
|
|
1298
1532
|
}
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
status: row["status"],
|
|
1313
|
-
pinned: !!row["pinned"],
|
|
1314
|
-
agent_id: row["agent_id"] || null,
|
|
1315
|
-
project_id: row["project_id"] || null,
|
|
1316
|
-
session_id: row["session_id"] || null,
|
|
1317
|
-
metadata: JSON.parse(row["metadata"] || "{}"),
|
|
1318
|
-
access_count: row["access_count"],
|
|
1319
|
-
version: row["version"],
|
|
1320
|
-
expires_at: row["expires_at"] || null,
|
|
1321
|
-
created_at: row["created_at"],
|
|
1322
|
-
updated_at: row["updated_at"],
|
|
1323
|
-
accessed_at: row["accessed_at"] || null
|
|
1324
|
-
};
|
|
1325
|
-
}
|
|
1326
|
-
function preprocessQuery(query) {
|
|
1327
|
-
let q = query.trim();
|
|
1328
|
-
q = q.replace(/\s+/g, " ");
|
|
1329
|
-
q = q.normalize("NFC");
|
|
1330
|
-
return q;
|
|
1331
|
-
}
|
|
1332
|
-
function escapeLikePattern(s) {
|
|
1333
|
-
return s.replace(/%/g, "\\%").replace(/_/g, "\\_");
|
|
1334
|
-
}
|
|
1335
|
-
var STOP_WORDS = new Set([
|
|
1336
|
-
"a",
|
|
1337
|
-
"an",
|
|
1338
|
-
"the",
|
|
1339
|
-
"is",
|
|
1340
|
-
"are",
|
|
1341
|
-
"was",
|
|
1342
|
-
"were",
|
|
1343
|
-
"be",
|
|
1344
|
-
"been",
|
|
1345
|
-
"being",
|
|
1346
|
-
"have",
|
|
1347
|
-
"has",
|
|
1348
|
-
"had",
|
|
1349
|
-
"do",
|
|
1350
|
-
"does",
|
|
1351
|
-
"did",
|
|
1352
|
-
"will",
|
|
1353
|
-
"would",
|
|
1354
|
-
"could",
|
|
1355
|
-
"should",
|
|
1356
|
-
"may",
|
|
1357
|
-
"might",
|
|
1358
|
-
"shall",
|
|
1359
|
-
"can",
|
|
1360
|
-
"need",
|
|
1361
|
-
"dare",
|
|
1362
|
-
"ought",
|
|
1363
|
-
"used",
|
|
1364
|
-
"to",
|
|
1365
|
-
"of",
|
|
1366
|
-
"in",
|
|
1367
|
-
"for",
|
|
1368
|
-
"on",
|
|
1369
|
-
"with",
|
|
1370
|
-
"at",
|
|
1371
|
-
"by",
|
|
1372
|
-
"from",
|
|
1373
|
-
"as",
|
|
1374
|
-
"into",
|
|
1375
|
-
"through",
|
|
1376
|
-
"during",
|
|
1377
|
-
"before",
|
|
1378
|
-
"after",
|
|
1379
|
-
"above",
|
|
1380
|
-
"below",
|
|
1381
|
-
"between",
|
|
1382
|
-
"out",
|
|
1383
|
-
"off",
|
|
1384
|
-
"over",
|
|
1385
|
-
"under",
|
|
1386
|
-
"again",
|
|
1387
|
-
"further",
|
|
1388
|
-
"then",
|
|
1389
|
-
"once",
|
|
1390
|
-
"here",
|
|
1391
|
-
"there",
|
|
1392
|
-
"when",
|
|
1393
|
-
"where",
|
|
1394
|
-
"why",
|
|
1395
|
-
"how",
|
|
1396
|
-
"all",
|
|
1397
|
-
"each",
|
|
1398
|
-
"every",
|
|
1399
|
-
"both",
|
|
1400
|
-
"few",
|
|
1401
|
-
"more",
|
|
1402
|
-
"most",
|
|
1403
|
-
"other",
|
|
1404
|
-
"some",
|
|
1405
|
-
"such",
|
|
1406
|
-
"no",
|
|
1407
|
-
"not",
|
|
1408
|
-
"only",
|
|
1409
|
-
"own",
|
|
1410
|
-
"same",
|
|
1411
|
-
"so",
|
|
1412
|
-
"than",
|
|
1413
|
-
"too",
|
|
1414
|
-
"very",
|
|
1415
|
-
"just",
|
|
1416
|
-
"because",
|
|
1417
|
-
"but",
|
|
1418
|
-
"and",
|
|
1419
|
-
"or",
|
|
1420
|
-
"if",
|
|
1421
|
-
"while",
|
|
1422
|
-
"that",
|
|
1423
|
-
"this",
|
|
1424
|
-
"it"
|
|
1425
|
-
]);
|
|
1426
|
-
function removeStopWords(tokens) {
|
|
1427
|
-
if (tokens.length <= 1)
|
|
1428
|
-
return tokens;
|
|
1429
|
-
const filtered = tokens.filter((t) => !STOP_WORDS.has(t.toLowerCase()));
|
|
1430
|
-
return filtered.length > 0 ? filtered : tokens;
|
|
1431
|
-
}
|
|
1432
|
-
function extractHighlights(memory, queryLower) {
|
|
1433
|
-
const highlights = [];
|
|
1434
|
-
const tokens = queryLower.split(/\s+/).filter(Boolean);
|
|
1435
|
-
for (const field of ["key", "value", "summary"]) {
|
|
1436
|
-
const text = field === "summary" ? memory.summary : memory[field];
|
|
1437
|
-
if (!text)
|
|
1438
|
-
continue;
|
|
1439
|
-
const textLower = text.toLowerCase();
|
|
1440
|
-
const searchTerms = [queryLower, ...tokens].filter(Boolean);
|
|
1441
|
-
for (const term of searchTerms) {
|
|
1442
|
-
const idx = textLower.indexOf(term);
|
|
1443
|
-
if (idx !== -1) {
|
|
1444
|
-
const start = Math.max(0, idx - 30);
|
|
1445
|
-
const end = Math.min(text.length, idx + term.length + 30);
|
|
1446
|
-
const prefix = start > 0 ? "..." : "";
|
|
1447
|
-
const suffix = end < text.length ? "..." : "";
|
|
1448
|
-
highlights.push({
|
|
1449
|
-
field,
|
|
1450
|
-
snippet: prefix + text.slice(start, end) + suffix
|
|
1451
|
-
});
|
|
1452
|
-
break;
|
|
1453
|
-
}
|
|
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));
|
|
1454
1546
|
}
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
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" });
|
|
1459
1554
|
}
|
|
1460
1555
|
}
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
function determineMatchType(memory, queryLower) {
|
|
1464
|
-
if (memory.key.toLowerCase() === queryLower)
|
|
1465
|
-
return "exact";
|
|
1466
|
-
if (memory.tags.some((t) => t.toLowerCase() === queryLower))
|
|
1467
|
-
return "tag";
|
|
1468
|
-
if (memory.tags.some((t) => t.toLowerCase().includes(queryLower)))
|
|
1469
|
-
return "tag";
|
|
1470
|
-
return "fuzzy";
|
|
1556
|
+
results.sort((a, b) => b.score - a.score);
|
|
1557
|
+
return results;
|
|
1471
1558
|
}
|
|
1472
|
-
function
|
|
1473
|
-
const
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
}
|
|
1480
|
-
if (memory.tags.some((t) => t.toLowerCase() === queryLower)) {
|
|
1481
|
-
fieldScores.push(6);
|
|
1482
|
-
} else if (memory.tags.some((t) => t.toLowerCase().includes(queryLower))) {
|
|
1483
|
-
fieldScores.push(3);
|
|
1484
|
-
}
|
|
1485
|
-
if (memory.summary && memory.summary.toLowerCase().includes(queryLower)) {
|
|
1486
|
-
fieldScores.push(4);
|
|
1487
|
-
}
|
|
1488
|
-
if (memory.value.toLowerCase().includes(queryLower)) {
|
|
1489
|
-
fieldScores.push(3);
|
|
1490
|
-
}
|
|
1491
|
-
const metadataStr = JSON.stringify(memory.metadata).toLowerCase();
|
|
1492
|
-
if (metadataStr !== "{}" && metadataStr.includes(queryLower)) {
|
|
1493
|
-
fieldScores.push(2);
|
|
1494
|
-
}
|
|
1495
|
-
fieldScores.sort((a, b) => b - a);
|
|
1496
|
-
const diminishingMultipliers = [1, 0.5, 0.25, 0.15, 0.15];
|
|
1497
|
-
let score = 0;
|
|
1498
|
-
for (let i = 0;i < fieldScores.length; i++) {
|
|
1499
|
-
score += fieldScores[i] * (diminishingMultipliers[i] ?? 0.15);
|
|
1500
|
-
}
|
|
1501
|
-
const { phrases } = extractQuotedPhrases(queryLower);
|
|
1502
|
-
for (const phrase of phrases) {
|
|
1503
|
-
if (keyLower.includes(phrase))
|
|
1504
|
-
score += 8;
|
|
1505
|
-
if (memory.value.toLowerCase().includes(phrase))
|
|
1506
|
-
score += 5;
|
|
1507
|
-
if (memory.summary && memory.summary.toLowerCase().includes(phrase))
|
|
1508
|
-
score += 4;
|
|
1509
|
-
}
|
|
1510
|
-
const { remainder } = extractQuotedPhrases(queryLower);
|
|
1511
|
-
const tokens = removeStopWords(remainder.split(/\s+/).filter(Boolean));
|
|
1512
|
-
if (tokens.length > 1) {
|
|
1513
|
-
let tokenScore = 0;
|
|
1514
|
-
for (const token of tokens) {
|
|
1515
|
-
if (keyLower === token) {
|
|
1516
|
-
tokenScore += 10 / tokens.length;
|
|
1517
|
-
} else if (keyLower.includes(token)) {
|
|
1518
|
-
tokenScore += 7 / tokens.length;
|
|
1519
|
-
}
|
|
1520
|
-
if (memory.tags.some((t) => t.toLowerCase() === token)) {
|
|
1521
|
-
tokenScore += 6 / tokens.length;
|
|
1522
|
-
} else if (memory.tags.some((t) => t.toLowerCase().includes(token))) {
|
|
1523
|
-
tokenScore += 3 / tokens.length;
|
|
1524
|
-
}
|
|
1525
|
-
if (memory.summary && memory.summary.toLowerCase().includes(token)) {
|
|
1526
|
-
tokenScore += 4 / tokens.length;
|
|
1527
|
-
}
|
|
1528
|
-
if (memory.value.toLowerCase().includes(token)) {
|
|
1529
|
-
tokenScore += 3 / tokens.length;
|
|
1530
|
-
}
|
|
1531
|
-
if (metadataStr !== "{}" && metadataStr.includes(token)) {
|
|
1532
|
-
tokenScore += 2 / tokens.length;
|
|
1533
|
-
}
|
|
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);
|
|
1534
1566
|
}
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
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
|
+
}
|
|
1539
1572
|
}
|
|
1540
|
-
}
|
|
1541
|
-
return
|
|
1542
|
-
}
|
|
1543
|
-
function extractQuotedPhrases(query) {
|
|
1544
|
-
const phrases = [];
|
|
1545
|
-
const remainder = query.replace(/"([^"]+)"/g, (_match, phrase) => {
|
|
1546
|
-
phrases.push(phrase);
|
|
1547
|
-
return "";
|
|
1548
|
-
});
|
|
1549
|
-
return { phrases, remainder: remainder.trim() };
|
|
1573
|
+
} catch {}
|
|
1574
|
+
return boostedIds;
|
|
1550
1575
|
}
|
|
1551
|
-
function
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
const
|
|
1558
|
-
|
|
1559
|
-
parts.push(`"${t.replace(/"/g, '""')}"`);
|
|
1560
|
-
}
|
|
1561
|
-
return parts.join(" ");
|
|
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);
|
|
1562
1584
|
}
|
|
1563
|
-
function
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
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
|
+
});
|
|
1569
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;
|
|
1570
1611
|
}
|
|
1571
|
-
function
|
|
1572
|
-
const
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1612
|
+
function searchMemories(query, filter, db) {
|
|
1613
|
+
const d = db || getDatabase();
|
|
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;
|
|
1582
1624
|
} else {
|
|
1583
|
-
|
|
1584
|
-
params.push(filter.scope);
|
|
1585
|
-
}
|
|
1586
|
-
}
|
|
1587
|
-
if (filter.category) {
|
|
1588
|
-
if (Array.isArray(filter.category)) {
|
|
1589
|
-
conditions.push(`m.category IN (${filter.category.map(() => "?").join(",")})`);
|
|
1590
|
-
params.push(...filter.category);
|
|
1591
|
-
} else {
|
|
1592
|
-
conditions.push("m.category = ?");
|
|
1593
|
-
params.push(filter.category);
|
|
1594
|
-
}
|
|
1595
|
-
}
|
|
1596
|
-
if (filter.source) {
|
|
1597
|
-
if (Array.isArray(filter.source)) {
|
|
1598
|
-
conditions.push(`m.source IN (${filter.source.map(() => "?").join(",")})`);
|
|
1599
|
-
params.push(...filter.source);
|
|
1600
|
-
} else {
|
|
1601
|
-
conditions.push("m.source = ?");
|
|
1602
|
-
params.push(filter.source);
|
|
1603
|
-
}
|
|
1604
|
-
}
|
|
1605
|
-
if (filter.status) {
|
|
1606
|
-
conditions.shift();
|
|
1607
|
-
if (Array.isArray(filter.status)) {
|
|
1608
|
-
conditions.push(`m.status IN (${filter.status.map(() => "?").join(",")})`);
|
|
1609
|
-
params.push(...filter.status);
|
|
1610
|
-
} else {
|
|
1611
|
-
conditions.push("m.status = ?");
|
|
1612
|
-
params.push(filter.status);
|
|
1613
|
-
}
|
|
1614
|
-
}
|
|
1615
|
-
if (filter.project_id) {
|
|
1616
|
-
conditions.push("m.project_id = ?");
|
|
1617
|
-
params.push(filter.project_id);
|
|
1618
|
-
}
|
|
1619
|
-
if (filter.agent_id) {
|
|
1620
|
-
conditions.push("m.agent_id = ?");
|
|
1621
|
-
params.push(filter.agent_id);
|
|
1622
|
-
}
|
|
1623
|
-
if (filter.session_id) {
|
|
1624
|
-
conditions.push("m.session_id = ?");
|
|
1625
|
-
params.push(filter.session_id);
|
|
1626
|
-
}
|
|
1627
|
-
if (filter.min_importance) {
|
|
1628
|
-
conditions.push("m.importance >= ?");
|
|
1629
|
-
params.push(filter.min_importance);
|
|
1630
|
-
}
|
|
1631
|
-
if (filter.pinned !== undefined) {
|
|
1632
|
-
conditions.push("m.pinned = ?");
|
|
1633
|
-
params.push(filter.pinned ? 1 : 0);
|
|
1634
|
-
}
|
|
1635
|
-
if (filter.tags && filter.tags.length > 0) {
|
|
1636
|
-
for (const tag of filter.tags) {
|
|
1637
|
-
conditions.push("m.id IN (SELECT memory_id FROM memory_tags WHERE tag = ?)");
|
|
1638
|
-
params.push(tag);
|
|
1639
|
-
}
|
|
1640
|
-
}
|
|
1641
|
-
return { conditions, params };
|
|
1642
|
-
}
|
|
1643
|
-
function searchWithFts5(d, query, queryLower, filter, graphBoostedIds) {
|
|
1644
|
-
const ftsQuery = escapeFts5Query(query);
|
|
1645
|
-
if (!ftsQuery)
|
|
1646
|
-
return null;
|
|
1647
|
-
try {
|
|
1648
|
-
const { conditions, params } = buildFilterConditions(filter);
|
|
1649
|
-
const queryParam = `%${query}%`;
|
|
1650
|
-
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 ?)`;
|
|
1651
|
-
const allConditions = [ftsCondition, ...conditions];
|
|
1652
|
-
const allParams = [ftsQuery, queryParam, queryParam, ...params];
|
|
1653
|
-
const candidateSql = `SELECT m.* FROM memories m WHERE ${allConditions.join(" AND ")}`;
|
|
1654
|
-
const rows = d.query(candidateSql).all(...allParams);
|
|
1655
|
-
return scoreResults(rows, queryLower, graphBoostedIds);
|
|
1656
|
-
} catch {
|
|
1657
|
-
return null;
|
|
1658
|
-
}
|
|
1659
|
-
}
|
|
1660
|
-
function searchWithLike(d, query, queryLower, filter, graphBoostedIds) {
|
|
1661
|
-
const { conditions, params } = buildFilterConditions(filter);
|
|
1662
|
-
const rawTokens = query.trim().split(/\s+/).filter(Boolean);
|
|
1663
|
-
const tokens = removeStopWords(rawTokens);
|
|
1664
|
-
const escapedQuery = escapeLikePattern(query);
|
|
1665
|
-
const likePatterns = [`%${escapedQuery}%`];
|
|
1666
|
-
if (tokens.length > 1) {
|
|
1667
|
-
for (const t of tokens)
|
|
1668
|
-
likePatterns.push(`%${escapeLikePattern(t)}%`);
|
|
1669
|
-
}
|
|
1670
|
-
const fieldClauses = [];
|
|
1671
|
-
for (const pattern of likePatterns) {
|
|
1672
|
-
fieldClauses.push("m.key LIKE ? ESCAPE '\\'");
|
|
1673
|
-
params.push(pattern);
|
|
1674
|
-
fieldClauses.push("m.value LIKE ? ESCAPE '\\'");
|
|
1675
|
-
params.push(pattern);
|
|
1676
|
-
fieldClauses.push("m.summary LIKE ? ESCAPE '\\'");
|
|
1677
|
-
params.push(pattern);
|
|
1678
|
-
fieldClauses.push("m.metadata LIKE ? ESCAPE '\\'");
|
|
1679
|
-
params.push(pattern);
|
|
1680
|
-
fieldClauses.push("m.id IN (SELECT memory_id FROM memory_tags WHERE tag LIKE ? ESCAPE '\\')");
|
|
1681
|
-
params.push(pattern);
|
|
1682
|
-
}
|
|
1683
|
-
conditions.push(`(${fieldClauses.join(" OR ")})`);
|
|
1684
|
-
const sql = `SELECT DISTINCT m.* FROM memories m WHERE ${conditions.join(" AND ")}`;
|
|
1685
|
-
const rows = d.query(sql).all(...params);
|
|
1686
|
-
return scoreResults(rows, queryLower, graphBoostedIds);
|
|
1687
|
-
}
|
|
1688
|
-
function generateTrigrams(s) {
|
|
1689
|
-
const lower = s.toLowerCase();
|
|
1690
|
-
const trigrams = new Set;
|
|
1691
|
-
for (let i = 0;i <= lower.length - 3; i++) {
|
|
1692
|
-
trigrams.add(lower.slice(i, i + 3));
|
|
1693
|
-
}
|
|
1694
|
-
return trigrams;
|
|
1695
|
-
}
|
|
1696
|
-
function trigramSimilarity(a, b) {
|
|
1697
|
-
const triA = generateTrigrams(a);
|
|
1698
|
-
const triB = generateTrigrams(b);
|
|
1699
|
-
if (triA.size === 0 || triB.size === 0)
|
|
1700
|
-
return 0;
|
|
1701
|
-
let intersection = 0;
|
|
1702
|
-
for (const t of triA) {
|
|
1703
|
-
if (triB.has(t))
|
|
1704
|
-
intersection++;
|
|
1705
|
-
}
|
|
1706
|
-
const union = triA.size + triB.size - intersection;
|
|
1707
|
-
return union === 0 ? 0 : intersection / union;
|
|
1708
|
-
}
|
|
1709
|
-
function searchWithFuzzy(d, query, filter, graphBoostedIds) {
|
|
1710
|
-
const { conditions, params } = buildFilterConditions(filter);
|
|
1711
|
-
const sql = `SELECT m.* FROM memories m WHERE ${conditions.join(" AND ")}`;
|
|
1712
|
-
const rows = d.query(sql).all(...params);
|
|
1713
|
-
const MIN_SIMILARITY = 0.3;
|
|
1714
|
-
const results = [];
|
|
1715
|
-
for (const row of rows) {
|
|
1716
|
-
const memory = parseMemoryRow2(row);
|
|
1717
|
-
let bestSimilarity = 0;
|
|
1718
|
-
bestSimilarity = Math.max(bestSimilarity, trigramSimilarity(query, memory.key));
|
|
1719
|
-
bestSimilarity = Math.max(bestSimilarity, trigramSimilarity(query, memory.value.slice(0, 200)));
|
|
1720
|
-
if (memory.summary) {
|
|
1721
|
-
bestSimilarity = Math.max(bestSimilarity, trigramSimilarity(query, memory.summary));
|
|
1722
|
-
}
|
|
1723
|
-
for (const tag of memory.tags) {
|
|
1724
|
-
bestSimilarity = Math.max(bestSimilarity, trigramSimilarity(query, tag));
|
|
1725
|
-
}
|
|
1726
|
-
if (bestSimilarity >= MIN_SIMILARITY) {
|
|
1727
|
-
const graphBoost = graphBoostedIds?.has(memory.id) ? 2 : 0;
|
|
1728
|
-
const score = bestSimilarity * 5 * memory.importance / 10 + graphBoost;
|
|
1729
|
-
results.push({ memory, score, match_type: "fuzzy" });
|
|
1730
|
-
}
|
|
1731
|
-
}
|
|
1732
|
-
results.sort((a, b) => b.score - a.score);
|
|
1733
|
-
return results;
|
|
1734
|
-
}
|
|
1735
|
-
function getGraphBoostedMemoryIds(query, d) {
|
|
1736
|
-
const boostedIds = new Set;
|
|
1737
|
-
try {
|
|
1738
|
-
const matchingEntities = listEntities({ search: query, limit: 10 }, d);
|
|
1739
|
-
const exactMatch = getEntityByName(query, undefined, undefined, d);
|
|
1740
|
-
if (exactMatch && !matchingEntities.find((e) => e.id === exactMatch.id)) {
|
|
1741
|
-
matchingEntities.push(exactMatch);
|
|
1742
|
-
}
|
|
1743
|
-
for (const entity of matchingEntities) {
|
|
1744
|
-
const memories = getMemoriesForEntity(entity.id, d);
|
|
1745
|
-
for (const mem of memories) {
|
|
1746
|
-
boostedIds.add(mem.id);
|
|
1747
|
-
}
|
|
1748
|
-
}
|
|
1749
|
-
} catch {}
|
|
1750
|
-
return boostedIds;
|
|
1751
|
-
}
|
|
1752
|
-
function computeRecencyBoost(memory) {
|
|
1753
|
-
if (memory.pinned)
|
|
1754
|
-
return 1;
|
|
1755
|
-
const mostRecent = memory.accessed_at || memory.updated_at;
|
|
1756
|
-
if (!mostRecent)
|
|
1757
|
-
return 0;
|
|
1758
|
-
const daysSinceAccess = (Date.now() - Date.parse(mostRecent)) / (1000 * 60 * 60 * 24);
|
|
1759
|
-
return Math.max(0, 1 - daysSinceAccess / 30);
|
|
1760
|
-
}
|
|
1761
|
-
function scoreResults(rows, queryLower, graphBoostedIds) {
|
|
1762
|
-
const scored = [];
|
|
1763
|
-
for (const row of rows) {
|
|
1764
|
-
const memory = parseMemoryRow2(row);
|
|
1765
|
-
const rawScore = computeScore(memory, queryLower);
|
|
1766
|
-
if (rawScore === 0)
|
|
1767
|
-
continue;
|
|
1768
|
-
const weightedScore = rawScore * memory.importance / 10;
|
|
1769
|
-
const recencyBoost = computeRecencyBoost(memory);
|
|
1770
|
-
const accessBoost = Math.min(memory.access_count / 20, 0.2);
|
|
1771
|
-
const graphBoost = graphBoostedIds?.has(memory.id) ? 2 : 0;
|
|
1772
|
-
const finalScore = (weightedScore + graphBoost) * (1 + recencyBoost * 0.3) * (1 + accessBoost);
|
|
1773
|
-
const matchType = determineMatchType(memory, queryLower);
|
|
1774
|
-
scored.push({
|
|
1775
|
-
memory,
|
|
1776
|
-
score: finalScore,
|
|
1777
|
-
match_type: matchType,
|
|
1778
|
-
highlights: extractHighlights(memory, queryLower)
|
|
1779
|
-
});
|
|
1780
|
-
}
|
|
1781
|
-
scored.sort((a, b) => {
|
|
1782
|
-
if (b.score !== a.score)
|
|
1783
|
-
return b.score - a.score;
|
|
1784
|
-
return b.memory.importance - a.memory.importance;
|
|
1785
|
-
});
|
|
1786
|
-
return scored;
|
|
1787
|
-
}
|
|
1788
|
-
function searchMemories(query, filter, db) {
|
|
1789
|
-
const d = db || getDatabase();
|
|
1790
|
-
query = preprocessQuery(query);
|
|
1791
|
-
if (!query)
|
|
1792
|
-
return [];
|
|
1793
|
-
const queryLower = query.toLowerCase();
|
|
1794
|
-
const graphBoostedIds = getGraphBoostedMemoryIds(query, d);
|
|
1795
|
-
let scored;
|
|
1796
|
-
if (hasFts5Table(d)) {
|
|
1797
|
-
const ftsResult = searchWithFts5(d, query, queryLower, filter, graphBoostedIds);
|
|
1798
|
-
if (ftsResult !== null) {
|
|
1799
|
-
scored = ftsResult;
|
|
1800
|
-
} else {
|
|
1801
|
-
scored = searchWithLike(d, query, queryLower, filter, graphBoostedIds);
|
|
1625
|
+
scored = searchWithLike(d, query, queryLower, filter, graphBoostedIds);
|
|
1802
1626
|
}
|
|
1803
1627
|
} else {
|
|
1804
1628
|
scored = searchWithLike(d, query, queryLower, filter, graphBoostedIds);
|
|
@@ -1837,6 +1661,103 @@ function logSearchQuery(query, resultCount, agentId, projectId, db) {
|
|
|
1837
1661
|
d.run("INSERT INTO search_history (id, query, result_count, agent_id, project_id) VALUES (?, ?, ?, ?, ?)", [id, query, resultCount, agentId || null, projectId || null]);
|
|
1838
1662
|
} catch {}
|
|
1839
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
|
+
});
|
|
1840
1761
|
|
|
1841
1762
|
// src/db/relations.ts
|
|
1842
1763
|
function parseRelationRow(row) {
|
|
@@ -1874,7 +1795,15 @@ function createRelation(input, db) {
|
|
|
1874
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]);
|
|
1875
1796
|
const row = d.query(`SELECT * FROM relations
|
|
1876
1797
|
WHERE source_entity_id = ? AND target_entity_id = ? AND relation_type = ?`).get(input.source_entity_id, input.target_entity_id, input.relation_type);
|
|
1877
|
-
|
|
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;
|
|
1878
1807
|
}
|
|
1879
1808
|
function getRelation(id, db) {
|
|
1880
1809
|
const d = db || getDatabase();
|
|
@@ -1961,732 +1890,1318 @@ function findPath(fromEntityId, toEntityId, maxDepth = 5, db) {
|
|
|
1961
1890
|
}
|
|
1962
1891
|
return entities.length > 0 ? entities : null;
|
|
1963
1892
|
}
|
|
1893
|
+
var init_relations = __esm(() => {
|
|
1894
|
+
init_database();
|
|
1895
|
+
init_hooks();
|
|
1896
|
+
});
|
|
1964
1897
|
|
|
1965
|
-
// src/lib/
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
d: 86400000,
|
|
1971
|
-
w: 604800000
|
|
1972
|
-
};
|
|
1973
|
-
var DURATION_RE = /^(\d+[smhdw])+$/;
|
|
1974
|
-
var SEGMENT_RE = /(\d+)([smhdw])/g;
|
|
1975
|
-
function parseDuration(input) {
|
|
1976
|
-
if (typeof input === "number")
|
|
1977
|
-
return input;
|
|
1978
|
-
const trimmed = input.trim();
|
|
1979
|
-
if (trimmed === "")
|
|
1980
|
-
throw new Error("Invalid duration: empty string");
|
|
1981
|
-
if (/^\d+$/.test(trimmed)) {
|
|
1982
|
-
return parseInt(trimmed, 10);
|
|
1898
|
+
// src/lib/providers/base.ts
|
|
1899
|
+
class BaseProvider {
|
|
1900
|
+
config;
|
|
1901
|
+
constructor(config) {
|
|
1902
|
+
this.config = config;
|
|
1983
1903
|
}
|
|
1984
|
-
|
|
1985
|
-
|
|
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
|
+
}
|
|
1986
1911
|
}
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
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)));
|
|
1917
|
+
}
|
|
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
|
+
};
|
|
1939
|
+
}
|
|
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 [];
|
|
2020
|
+
}
|
|
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;
|
|
2038
|
+
}
|
|
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;
|
|
2052
|
+
}
|
|
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);
|
|
2082
|
+
}
|
|
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 [];
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
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;
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
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
|
+
}
|
|
2354
|
+
});
|
|
2355
|
+
const primary = providerRegistry.getConfig().provider;
|
|
2356
|
+
const fallback = available.filter((p) => p !== primary);
|
|
2357
|
+
providerRegistry.configure({ fallback });
|
|
2358
|
+
}
|
|
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;
|
|
1994
2420
|
}
|
|
1995
|
-
|
|
1996
|
-
|
|
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
|
+
}
|
|
1997
2437
|
}
|
|
1998
|
-
return total;
|
|
1999
2438
|
}
|
|
2000
|
-
var
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
["m", UNIT_MS["m"]],
|
|
2005
|
-
["s", UNIT_MS["s"]]
|
|
2006
|
-
];
|
|
2007
|
-
|
|
2008
|
-
// src/lib/providers/base.ts
|
|
2009
|
-
var DEFAULT_AUTO_MEMORY_CONFIG = {
|
|
2010
|
-
provider: "anthropic",
|
|
2011
|
-
model: "claude-haiku-4-5",
|
|
2012
|
-
enabled: true,
|
|
2013
|
-
minImportance: 4,
|
|
2014
|
-
autoEntityLink: true,
|
|
2015
|
-
fallback: ["cerebras", "openai"]
|
|
2016
|
-
};
|
|
2439
|
+
var MAX_QUEUE_SIZE = 100, CONCURRENCY = 3, autoMemoryQueue;
|
|
2440
|
+
var init_auto_memory_queue = __esm(() => {
|
|
2441
|
+
autoMemoryQueue = new AutoMemoryQueue;
|
|
2442
|
+
});
|
|
2017
2443
|
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
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) {
|
|
2452
|
+
try {
|
|
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;
|
|
2475
|
+
} catch {
|
|
2476
|
+
return false;
|
|
2022
2477
|
}
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2478
|
+
}
|
|
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 {}
|
|
2499
|
+
}
|
|
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 {}
|
|
2029
2512
|
}
|
|
2513
|
+
} catch (err) {
|
|
2514
|
+
console.error("[auto-memory] entity linking failed:", err);
|
|
2030
2515
|
}
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
return
|
|
2516
|
+
}
|
|
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())
|
|
2522
|
+
return null;
|
|
2523
|
+
if (isDuplicate(extracted.content, context.agentId, context.projectId)) {
|
|
2524
|
+
return null;
|
|
2036
2525
|
}
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2526
|
+
try {
|
|
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
|
+
}
|
|
2057
2546
|
};
|
|
2547
|
+
const memory = createMemory(input, "merge");
|
|
2548
|
+
return memory.id;
|
|
2549
|
+
} catch (err) {
|
|
2550
|
+
console.error("[auto-memory] saveExtractedMemory failed:", err);
|
|
2551
|
+
return null;
|
|
2058
2552
|
}
|
|
2059
2553
|
}
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
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
|
+
}
|
|
2579
|
+
}
|
|
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
|
+
}
|
|
2589
|
+
}
|
|
2590
|
+
}
|
|
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
|
+
});
|
|
2098
2618
|
|
|
2099
|
-
// src/
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
};
|
|
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";
|
|
2104
2624
|
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
})
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
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
|
+
}
|
|
2130
2663
|
}
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
const parsed = this.parseJSON(response);
|
|
2139
|
-
if (!parsed || typeof parsed !== "object")
|
|
2140
|
-
return empty;
|
|
2141
|
-
return {
|
|
2142
|
-
entities: Array.isArray(parsed.entities) ? parsed.entities : [],
|
|
2143
|
-
relations: Array.isArray(parsed.relations) ? parsed.relations : []
|
|
2144
|
-
};
|
|
2145
|
-
} catch (err) {
|
|
2146
|
-
console.error("[anthropic] extractEntities failed:", err);
|
|
2147
|
-
return empty;
|
|
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]);
|
|
2148
2671
|
}
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
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?
|
|
2155
|
-
|
|
2156
|
-
"${content}"
|
|
2157
|
-
|
|
2158
|
-
Return only a number 0-10.`);
|
|
2159
|
-
return this.clampImportance(response.trim());
|
|
2160
|
-
} catch {
|
|
2161
|
-
return 5;
|
|
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]);
|
|
2162
2677
|
}
|
|
2678
|
+
return getAgent(existingId, d);
|
|
2163
2679
|
}
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
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);
|
|
2683
|
+
}
|
|
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}`);
|
|
2187
2719
|
}
|
|
2188
|
-
|
|
2189
|
-
return data.content?.[0]?.text ?? "";
|
|
2190
|
-
} finally {
|
|
2191
|
-
clearTimeout(timeout);
|
|
2720
|
+
d.run("UPDATE agents SET name = ? WHERE id = ?", [normalizedNewName, agent.id]);
|
|
2192
2721
|
}
|
|
2193
2722
|
}
|
|
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);
|
|
2194
2737
|
}
|
|
2195
2738
|
|
|
2196
|
-
// src/
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
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 });
|
|
2214
2763
|
}
|
|
2215
|
-
|
|
2216
|
-
const
|
|
2217
|
-
if (
|
|
2218
|
-
return
|
|
2219
|
-
try {
|
|
2220
|
-
const response = await this.callWithRetry(ENTITY_EXTRACTION_SYSTEM_PROMPT, ENTITY_EXTRACTION_USER_TEMPLATE(text));
|
|
2221
|
-
const parsed = this.parseJSON(response);
|
|
2222
|
-
if (!parsed || typeof parsed !== "object")
|
|
2223
|
-
return empty;
|
|
2224
|
-
return {
|
|
2225
|
-
entities: Array.isArray(parsed.entities) ? parsed.entities : [],
|
|
2226
|
-
relations: Array.isArray(parsed.relations) ? parsed.relations : []
|
|
2227
|
-
};
|
|
2228
|
-
} catch (err) {
|
|
2229
|
-
console.error(`[${this.name}] extractEntities failed:`, err);
|
|
2230
|
-
return empty;
|
|
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;
|
|
2231
2768
|
}
|
|
2232
2769
|
}
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
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
|
+
}
|
|
2238
2812
|
|
|
2239
|
-
|
|
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
|
+
}
|
|
2240
2860
|
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
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;
|
|
2245
2871
|
}
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
for (let attempt = 0;attempt < retries; attempt++) {
|
|
2250
|
-
try {
|
|
2251
|
-
return await this.callAPI(systemPrompt, userMessage);
|
|
2252
|
-
} catch (err) {
|
|
2253
|
-
lastError = err instanceof Error ? err : new Error(String(err));
|
|
2254
|
-
const isRateLimit = lastError.message.includes("429") || lastError.message.toLowerCase().includes("rate limit");
|
|
2255
|
-
if (!isRateLimit || attempt === retries - 1)
|
|
2256
|
-
throw lastError;
|
|
2257
|
-
await new Promise((r) => setTimeout(r, 1000 * Math.pow(2, attempt)));
|
|
2258
|
-
}
|
|
2872
|
+
const parent = dirname2(dir);
|
|
2873
|
+
if (parent === dir) {
|
|
2874
|
+
return null;
|
|
2259
2875
|
}
|
|
2260
|
-
|
|
2876
|
+
dir = parent;
|
|
2261
2877
|
}
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
body: JSON.stringify({
|
|
2273
|
-
model: this.config.model,
|
|
2274
|
-
max_tokens: this.config.maxTokens ?? 1024,
|
|
2275
|
-
temperature: this.config.temperature ?? 0,
|
|
2276
|
-
messages: [
|
|
2277
|
-
{ role: "system", content: systemPrompt },
|
|
2278
|
-
{ role: "user", content: userMessage }
|
|
2279
|
-
]
|
|
2280
|
-
}),
|
|
2281
|
-
signal: controller.signal
|
|
2282
|
-
});
|
|
2283
|
-
if (!res.ok) {
|
|
2284
|
-
const body = await res.text().catch(() => "");
|
|
2285
|
-
throw new Error(`${this.name} API ${res.status}: ${body.slice(0, 200)}`);
|
|
2286
|
-
}
|
|
2287
|
-
const data = await res.json();
|
|
2288
|
-
return data.choices?.[0]?.message?.content ?? "";
|
|
2289
|
-
} finally {
|
|
2290
|
-
clearTimeout(timeout);
|
|
2878
|
+
}
|
|
2879
|
+
function findGitRoot2() {
|
|
2880
|
+
let dir = process.cwd();
|
|
2881
|
+
while (true) {
|
|
2882
|
+
if (existsSync2(join2(dir, ".git"))) {
|
|
2883
|
+
return dir;
|
|
2884
|
+
}
|
|
2885
|
+
const parent = dirname2(dir);
|
|
2886
|
+
if (parent === dir) {
|
|
2887
|
+
return null;
|
|
2291
2888
|
}
|
|
2889
|
+
dir = parent;
|
|
2292
2890
|
}
|
|
2293
2891
|
}
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
var OPENAI_MODELS = {
|
|
2297
|
-
default: "gpt-4.1-nano",
|
|
2298
|
-
mini: "gpt-4.1-mini",
|
|
2299
|
-
full: "gpt-4.1"
|
|
2300
|
-
};
|
|
2301
|
-
|
|
2302
|
-
class OpenAIProvider extends OpenAICompatProvider {
|
|
2303
|
-
name = "openai";
|
|
2304
|
-
baseUrl = "https://api.openai.com/v1";
|
|
2305
|
-
authHeader = "Authorization";
|
|
2306
|
-
constructor(config) {
|
|
2307
|
-
super({
|
|
2308
|
-
apiKey: config?.apiKey ?? process.env.OPENAI_API_KEY ?? "",
|
|
2309
|
-
model: config?.model ?? OPENAI_MODELS.default,
|
|
2310
|
-
maxTokens: config?.maxTokens ?? 1024,
|
|
2311
|
-
temperature: config?.temperature ?? 0,
|
|
2312
|
-
timeoutMs: config?.timeoutMs ?? 15000
|
|
2313
|
-
});
|
|
2314
|
-
}
|
|
2892
|
+
function profilesDir() {
|
|
2893
|
+
return join2(homedir(), ".mementos", "profiles");
|
|
2315
2894
|
}
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
var CEREBRAS_MODELS = {
|
|
2319
|
-
default: "llama-3.3-70b",
|
|
2320
|
-
fast: "llama3.1-8b"
|
|
2321
|
-
};
|
|
2322
|
-
|
|
2323
|
-
class CerebrasProvider extends OpenAICompatProvider {
|
|
2324
|
-
name = "cerebras";
|
|
2325
|
-
baseUrl = "https://api.cerebras.ai/v1";
|
|
2326
|
-
authHeader = "Authorization";
|
|
2327
|
-
constructor(config) {
|
|
2328
|
-
super({
|
|
2329
|
-
apiKey: config?.apiKey ?? process.env.CEREBRAS_API_KEY ?? "",
|
|
2330
|
-
model: config?.model ?? CEREBRAS_MODELS.default,
|
|
2331
|
-
maxTokens: config?.maxTokens ?? 1024,
|
|
2332
|
-
temperature: config?.temperature ?? 0,
|
|
2333
|
-
timeoutMs: config?.timeoutMs ?? 1e4
|
|
2334
|
-
});
|
|
2335
|
-
}
|
|
2895
|
+
function globalConfigPath() {
|
|
2896
|
+
return join2(homedir(), ".mementos", "config.json");
|
|
2336
2897
|
}
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
name = "grok";
|
|
2346
|
-
baseUrl = "https://api.x.ai/v1";
|
|
2347
|
-
authHeader = "Authorization";
|
|
2348
|
-
constructor(config) {
|
|
2349
|
-
super({
|
|
2350
|
-
apiKey: config?.apiKey ?? process.env.XAI_API_KEY ?? "",
|
|
2351
|
-
model: config?.model ?? GROK_MODELS.default,
|
|
2352
|
-
maxTokens: config?.maxTokens ?? 1024,
|
|
2353
|
-
temperature: config?.temperature ?? 0,
|
|
2354
|
-
timeoutMs: config?.timeoutMs ?? 15000
|
|
2355
|
-
});
|
|
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 {};
|
|
2356
2906
|
}
|
|
2357
2907
|
}
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
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))
|
|
2918
|
+
return [];
|
|
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;
|
|
2366
2927
|
}
|
|
2367
|
-
|
|
2368
|
-
|
|
2928
|
+
const profile = getActiveProfile();
|
|
2929
|
+
if (profile) {
|
|
2930
|
+
const profilePath = join2(profilesDir(), `${profile}.db`);
|
|
2931
|
+
ensureDir2(dirname2(profilePath));
|
|
2932
|
+
return profilePath;
|
|
2369
2933
|
}
|
|
2370
|
-
|
|
2371
|
-
|
|
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;
|
|
2941
|
+
}
|
|
2372
2942
|
}
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
return
|
|
2943
|
+
const found = findFileWalkingUp(join2(".mementos", "mementos.db"));
|
|
2944
|
+
if (found) {
|
|
2945
|
+
return found;
|
|
2376
2946
|
}
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2947
|
+
const fallback = join2(homedir(), ".mementos", "mementos.db");
|
|
2948
|
+
ensureDir2(dirname2(fallback));
|
|
2949
|
+
return fallback;
|
|
2950
|
+
}
|
|
2951
|
+
function ensureDir2(dir) {
|
|
2952
|
+
if (!existsSync2(dir)) {
|
|
2953
|
+
mkdirSync2(dir, { recursive: true });
|
|
2383
2954
|
}
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2955
|
+
}
|
|
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
|
+
|
|
2965
|
+
// src/lib/duration.ts
|
|
2966
|
+
var UNIT_MS = {
|
|
2967
|
+
s: 1000,
|
|
2968
|
+
m: 60000,
|
|
2969
|
+
h: 3600000,
|
|
2970
|
+
d: 86400000,
|
|
2971
|
+
w: 604800000
|
|
2972
|
+
};
|
|
2973
|
+
var DURATION_RE = /^(\d+[smhdw])+$/;
|
|
2974
|
+
var SEGMENT_RE = /(\d+)([smhdw])/g;
|
|
2975
|
+
function parseDuration(input) {
|
|
2976
|
+
if (typeof input === "number")
|
|
2977
|
+
return input;
|
|
2978
|
+
const trimmed = input.trim();
|
|
2979
|
+
if (trimmed === "")
|
|
2980
|
+
throw new Error("Invalid duration: empty string");
|
|
2981
|
+
if (/^\d+$/.test(trimmed)) {
|
|
2982
|
+
return parseInt(trimmed, 10);
|
|
2395
2983
|
}
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
const result = {};
|
|
2399
|
-
for (const name of providers) {
|
|
2400
|
-
const p = this.createProvider(name);
|
|
2401
|
-
result[name] = {
|
|
2402
|
-
available: Boolean(p?.config.apiKey),
|
|
2403
|
-
model: p?.config.model ?? "unknown"
|
|
2404
|
-
};
|
|
2405
|
-
}
|
|
2406
|
-
return result;
|
|
2984
|
+
if (!DURATION_RE.test(trimmed)) {
|
|
2985
|
+
throw new Error(`Invalid duration format: "${trimmed}". Use combinations of Ns, Nm, Nh, Nd, Nw (e.g. "1d12h", "30m") or plain milliseconds.`);
|
|
2407
2986
|
}
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
case "cerebras":
|
|
2416
|
-
return new CerebrasProvider(modelOverride ? { model: modelOverride } : undefined);
|
|
2417
|
-
case "grok":
|
|
2418
|
-
return new GrokProvider(modelOverride ? { model: modelOverride } : undefined);
|
|
2419
|
-
default:
|
|
2420
|
-
return null;
|
|
2421
|
-
}
|
|
2987
|
+
let total = 0;
|
|
2988
|
+
let match;
|
|
2989
|
+
SEGMENT_RE.lastIndex = 0;
|
|
2990
|
+
while ((match = SEGMENT_RE.exec(trimmed)) !== null) {
|
|
2991
|
+
const value = parseInt(match[1], 10);
|
|
2992
|
+
const unit = match[2];
|
|
2993
|
+
total += value * UNIT_MS[unit];
|
|
2422
2994
|
}
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
function autoConfigureFromEnv() {
|
|
2426
|
-
const hasAnthropicKey = Boolean(process.env.ANTHROPIC_API_KEY);
|
|
2427
|
-
const hasCerebrasKey = Boolean(process.env.CEREBRAS_API_KEY);
|
|
2428
|
-
const hasOpenAIKey = Boolean(process.env.OPENAI_API_KEY);
|
|
2429
|
-
const hasGrokKey = Boolean(process.env.XAI_API_KEY);
|
|
2430
|
-
if (!hasAnthropicKey) {
|
|
2431
|
-
if (hasCerebrasKey) {
|
|
2432
|
-
providerRegistry.configure({ provider: "cerebras" });
|
|
2433
|
-
} else if (hasOpenAIKey) {
|
|
2434
|
-
providerRegistry.configure({ provider: "openai" });
|
|
2435
|
-
} else if (hasGrokKey) {
|
|
2436
|
-
providerRegistry.configure({ provider: "grok" });
|
|
2437
|
-
}
|
|
2995
|
+
if (total === 0) {
|
|
2996
|
+
throw new Error(`Invalid duration: "${trimmed}" resolves to 0ms`);
|
|
2438
2997
|
}
|
|
2439
|
-
|
|
2440
|
-
const available = allProviders.filter((p) => {
|
|
2441
|
-
switch (p) {
|
|
2442
|
-
case "anthropic":
|
|
2443
|
-
return hasAnthropicKey;
|
|
2444
|
-
case "cerebras":
|
|
2445
|
-
return hasCerebrasKey;
|
|
2446
|
-
case "openai":
|
|
2447
|
-
return hasOpenAIKey;
|
|
2448
|
-
case "grok":
|
|
2449
|
-
return hasGrokKey;
|
|
2450
|
-
}
|
|
2451
|
-
});
|
|
2452
|
-
const primary = providerRegistry.getConfig().provider;
|
|
2453
|
-
const fallback = available.filter((p) => p !== primary);
|
|
2454
|
-
providerRegistry.configure({ fallback });
|
|
2998
|
+
return total;
|
|
2455
2999
|
}
|
|
2456
|
-
|
|
3000
|
+
var FORMAT_UNITS = [
|
|
3001
|
+
["w", UNIT_MS["w"]],
|
|
3002
|
+
["d", UNIT_MS["d"]],
|
|
3003
|
+
["h", UNIT_MS["h"]],
|
|
3004
|
+
["m", UNIT_MS["m"]],
|
|
3005
|
+
["s", UNIT_MS["s"]]
|
|
3006
|
+
];
|
|
2457
3007
|
|
|
2458
|
-
// src/
|
|
2459
|
-
|
|
2460
|
-
|
|
3008
|
+
// src/server/index.ts
|
|
3009
|
+
init_auto_memory();
|
|
3010
|
+
init_registry();
|
|
3011
|
+
init_hooks();
|
|
2461
3012
|
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
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"]
|
|
2473
3032
|
};
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
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);
|
|
2478
3065
|
}
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
this.stats.dropped++;
|
|
2483
|
-
this.stats.pending = Math.max(0, this.stats.pending - 1);
|
|
2484
|
-
}
|
|
2485
|
-
this.queue.push(job);
|
|
2486
|
-
this.stats.pending++;
|
|
2487
|
-
if (!this.running && this.handler)
|
|
2488
|
-
this.startLoop();
|
|
3066
|
+
if (filter.enabled !== undefined) {
|
|
3067
|
+
conditions.push("enabled = ?");
|
|
3068
|
+
params.push(filter.enabled ? 1 : 0);
|
|
2489
3069
|
}
|
|
2490
|
-
|
|
2491
|
-
|
|
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);
|
|
2492
3084
|
}
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
3085
|
+
if (updates.description !== undefined) {
|
|
3086
|
+
sets.push("description = ?");
|
|
3087
|
+
params.push(updates.description);
|
|
2496
3088
|
}
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
const job = this.queue.shift();
|
|
2501
|
-
if (!job)
|
|
2502
|
-
break;
|
|
2503
|
-
this.stats.pending = Math.max(0, this.stats.pending - 1);
|
|
2504
|
-
this.activeCount++;
|
|
2505
|
-
this.stats.processing = this.activeCount;
|
|
2506
|
-
this.processJob(job);
|
|
2507
|
-
}
|
|
2508
|
-
await new Promise((r) => setImmediate(r));
|
|
2509
|
-
}
|
|
2510
|
-
this.running = false;
|
|
3089
|
+
if (updates.priority !== undefined) {
|
|
3090
|
+
sets.push("priority = ?");
|
|
3091
|
+
params.push(updates.priority);
|
|
2511
3092
|
}
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
this.stats.processing = this.activeCount;
|
|
2516
|
-
return;
|
|
2517
|
-
}
|
|
2518
|
-
try {
|
|
2519
|
-
await this.handler(job);
|
|
2520
|
-
this.stats.processed++;
|
|
2521
|
-
} catch (err) {
|
|
2522
|
-
this.stats.failed++;
|
|
2523
|
-
console.error("[auto-memory-queue] job failed:", err);
|
|
2524
|
-
} finally {
|
|
2525
|
-
this.activeCount--;
|
|
2526
|
-
this.stats.processing = this.activeCount;
|
|
2527
|
-
}
|
|
3093
|
+
if (sets.length > 0) {
|
|
3094
|
+
params.push(id);
|
|
3095
|
+
d.run(`UPDATE webhook_hooks SET ${sets.join(", ")} WHERE id = ?`, params);
|
|
2528
3096
|
}
|
|
3097
|
+
return getWebhookHook(id, d);
|
|
2529
3098
|
}
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
function isDuplicate(content, agentId, projectId) {
|
|
2535
|
-
try {
|
|
2536
|
-
const query = content.split(/\s+/).filter((w) => w.length > 3).slice(0, 10).join(" ");
|
|
2537
|
-
if (!query)
|
|
2538
|
-
return false;
|
|
2539
|
-
const results = searchMemories(query, {
|
|
2540
|
-
agent_id: agentId,
|
|
2541
|
-
project_id: projectId,
|
|
2542
|
-
limit: 3
|
|
2543
|
-
});
|
|
2544
|
-
if (results.length === 0)
|
|
2545
|
-
return false;
|
|
2546
|
-
const contentWords = new Set(content.toLowerCase().split(/\W+/).filter((w) => w.length > 3));
|
|
2547
|
-
for (const result of results) {
|
|
2548
|
-
const existingWords = new Set(result.memory.value.toLowerCase().split(/\W+/).filter((w) => w.length > 3));
|
|
2549
|
-
if (contentWords.size === 0 || existingWords.size === 0)
|
|
2550
|
-
continue;
|
|
2551
|
-
const intersection = [...contentWords].filter((w) => existingWords.has(w)).length;
|
|
2552
|
-
const union = new Set([...contentWords, ...existingWords]).size;
|
|
2553
|
-
const similarity = intersection / union;
|
|
2554
|
-
if (similarity >= DEDUP_SIMILARITY_THRESHOLD)
|
|
2555
|
-
return true;
|
|
2556
|
-
}
|
|
2557
|
-
return false;
|
|
2558
|
-
} catch {
|
|
2559
|
-
return false;
|
|
2560
|
-
}
|
|
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;
|
|
2561
3103
|
}
|
|
2562
|
-
|
|
2563
|
-
const
|
|
2564
|
-
if (
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
const entityIdMap = new Map;
|
|
2569
|
-
for (const extracted of entities) {
|
|
2570
|
-
if (extracted.confidence < 0.6)
|
|
2571
|
-
continue;
|
|
2572
|
-
try {
|
|
2573
|
-
const existing = getEntityByName(extracted.name);
|
|
2574
|
-
const entityId = existing ? existing.id : createEntity({
|
|
2575
|
-
name: extracted.name,
|
|
2576
|
-
type: extracted.type,
|
|
2577
|
-
project_id: projectId
|
|
2578
|
-
}).id;
|
|
2579
|
-
entityIdMap.set(extracted.name, entityId);
|
|
2580
|
-
linkEntityToMemory(entityId, memoryId, "subject");
|
|
2581
|
-
} catch {}
|
|
2582
|
-
}
|
|
2583
|
-
for (const rel of relations) {
|
|
2584
|
-
const fromId = entityIdMap.get(rel.from);
|
|
2585
|
-
const toId = entityIdMap.get(rel.to);
|
|
2586
|
-
if (!fromId || !toId)
|
|
2587
|
-
continue;
|
|
2588
|
-
try {
|
|
2589
|
-
createRelation({
|
|
2590
|
-
source_entity_id: fromId,
|
|
2591
|
-
target_entity_id: toId,
|
|
2592
|
-
relation_type: rel.type
|
|
2593
|
-
});
|
|
2594
|
-
} catch {}
|
|
2595
|
-
}
|
|
2596
|
-
} catch (err) {
|
|
2597
|
-
console.error("[auto-memory] entity linking failed:", err);
|
|
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]);
|
|
2598
3110
|
}
|
|
2599
3111
|
}
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
if (!
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
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
|
+
});
|
|
2608
3137
|
}
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
return memory.id;
|
|
2632
|
-
} catch (err) {
|
|
2633
|
-
console.error("[auto-memory] saveExtractedMemory failed:", err);
|
|
2634
|
-
return null;
|
|
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 {}
|
|
2635
3160
|
}
|
|
2636
|
-
}
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
const provider = providerRegistry.getAvailable();
|
|
2641
|
-
if (!provider)
|
|
3161
|
+
});
|
|
3162
|
+
var _webhooksLoaded = false;
|
|
3163
|
+
function loadWebhooksFromDb() {
|
|
3164
|
+
if (_webhooksLoaded)
|
|
2642
3165
|
return;
|
|
2643
|
-
|
|
2644
|
-
agentId: job.agentId,
|
|
2645
|
-
projectId: job.projectId,
|
|
2646
|
-
sessionId: job.sessionId
|
|
2647
|
-
};
|
|
2648
|
-
let extracted = [];
|
|
3166
|
+
_webhooksLoaded = true;
|
|
2649
3167
|
try {
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
}
|
|
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
|
+
});
|
|
2661
3179
|
}
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
return;
|
|
2665
|
-
for (const memory of extracted) {
|
|
2666
|
-
const memoryId = await saveExtractedMemory(memory, context);
|
|
2667
|
-
if (!memoryId)
|
|
2668
|
-
continue;
|
|
2669
|
-
if (providerRegistry.getConfig().autoEntityLink) {
|
|
2670
|
-
linkEntitiesToMemory(memoryId, memory.content, job.agentId, job.projectId);
|
|
3180
|
+
if (webhooks.length > 0) {
|
|
3181
|
+
console.log(`[hooks] Loaded ${webhooks.length} webhook(s) from DB`);
|
|
2671
3182
|
}
|
|
3183
|
+
} catch (err) {
|
|
3184
|
+
console.error("[hooks] Failed to load webhooks from DB:", err);
|
|
2672
3185
|
}
|
|
2673
3186
|
}
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
}
|
|
2685
|
-
|
|
2686
|
-
|
|
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
|
+
};
|
|
2687
3201
|
}
|
|
2688
|
-
function
|
|
2689
|
-
|
|
3202
|
+
function reloadWebhooks() {
|
|
3203
|
+
_webhooksLoaded = false;
|
|
3204
|
+
loadWebhooksFromDb();
|
|
2690
3205
|
}
|
|
2691
3206
|
|
|
2692
3207
|
// src/server/index.ts
|
|
@@ -3700,7 +4215,73 @@ addRoute("POST", "/api/auto-memory/test", async (req) => {
|
|
|
3700
4215
|
note: "DRY RUN \u2014 nothing was saved"
|
|
3701
4216
|
});
|
|
3702
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
|
+
});
|
|
3703
4283
|
function startServer(port) {
|
|
4284
|
+
loadWebhooksFromDb();
|
|
3704
4285
|
const hostname = process.env["MEMENTOS_HOST"] ?? "127.0.0.1";
|
|
3705
4286
|
Bun.serve({
|
|
3706
4287
|
port,
|