@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/index.js CHANGED
@@ -346,6 +346,25 @@ var MIGRATIONS = [
346
346
  ALTER TABLE memories ADD COLUMN recall_count INTEGER NOT NULL DEFAULT 0;
347
347
  CREATE INDEX IF NOT EXISTS idx_memories_recall_count ON memories(recall_count DESC);
348
348
  INSERT OR IGNORE INTO _migrations (id) VALUES (9);
349
+ `,
350
+ `
351
+ CREATE TABLE IF NOT EXISTS webhook_hooks (
352
+ id TEXT PRIMARY KEY,
353
+ type TEXT NOT NULL,
354
+ handler_url TEXT NOT NULL,
355
+ priority INTEGER NOT NULL DEFAULT 50,
356
+ blocking INTEGER NOT NULL DEFAULT 0,
357
+ agent_id TEXT,
358
+ project_id TEXT,
359
+ description TEXT,
360
+ enabled INTEGER NOT NULL DEFAULT 1,
361
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
362
+ invocation_count INTEGER NOT NULL DEFAULT 0,
363
+ failure_count INTEGER NOT NULL DEFAULT 0
364
+ );
365
+ CREATE INDEX IF NOT EXISTS idx_webhook_hooks_type ON webhook_hooks(type);
366
+ CREATE INDEX IF NOT EXISTS idx_webhook_hooks_enabled ON webhook_hooks(enabled);
367
+ INSERT OR IGNORE INTO _migrations (id) VALUES (10);
349
368
  `
350
369
  ];
351
370
  var _db = null;
@@ -443,6 +462,87 @@ function containsSecrets(text) {
443
462
  return false;
444
463
  }
445
464
 
465
+ // src/lib/hooks.ts
466
+ var _idCounter = 0;
467
+ function generateHookId() {
468
+ return `hook_${++_idCounter}_${Date.now().toString(36)}`;
469
+ }
470
+
471
+ class HookRegistry {
472
+ hooks = new Map;
473
+ register(reg) {
474
+ const id = generateHookId();
475
+ const hook = {
476
+ ...reg,
477
+ id,
478
+ priority: reg.priority ?? 50
479
+ };
480
+ this.hooks.set(id, hook);
481
+ return id;
482
+ }
483
+ unregister(hookId) {
484
+ const hook = this.hooks.get(hookId);
485
+ if (!hook)
486
+ return false;
487
+ if (hook.builtin)
488
+ return false;
489
+ this.hooks.delete(hookId);
490
+ return true;
491
+ }
492
+ list(type) {
493
+ const all = [...this.hooks.values()];
494
+ if (!type)
495
+ return all;
496
+ return all.filter((h) => h.type === type);
497
+ }
498
+ async runHooks(type, context) {
499
+ const matching = this.getMatchingHooks(type, context);
500
+ if (matching.length === 0)
501
+ return true;
502
+ matching.sort((a, b) => a.priority - b.priority);
503
+ for (const hook of matching) {
504
+ if (hook.blocking) {
505
+ try {
506
+ const result = await hook.handler(context);
507
+ if (result === false)
508
+ return false;
509
+ } catch (err) {
510
+ console.error(`[hooks] blocking hook ${hook.id} (${type}) threw:`, err);
511
+ }
512
+ } else {
513
+ Promise.resolve().then(() => hook.handler(context)).catch((err) => console.error(`[hooks] non-blocking hook ${hook.id} (${type}) threw:`, err));
514
+ }
515
+ }
516
+ return true;
517
+ }
518
+ getMatchingHooks(type, context) {
519
+ const ctx = context;
520
+ return [...this.hooks.values()].filter((hook) => {
521
+ if (hook.type !== type)
522
+ return false;
523
+ if (hook.agentId && hook.agentId !== ctx.agentId)
524
+ return false;
525
+ if (hook.projectId && hook.projectId !== ctx.projectId)
526
+ return false;
527
+ return true;
528
+ });
529
+ }
530
+ stats() {
531
+ const all = [...this.hooks.values()];
532
+ const byType = {};
533
+ for (const hook of all) {
534
+ byType[hook.type] = (byType[hook.type] ?? 0) + 1;
535
+ }
536
+ return {
537
+ total: all.length,
538
+ byType,
539
+ blocking: all.filter((h) => h.blocking).length,
540
+ nonBlocking: all.filter((h) => !h.blocking).length
541
+ };
542
+ }
543
+ }
544
+ var hookRegistry = new HookRegistry;
545
+
446
546
  // src/db/entity-memories.ts
447
547
  function parseEntityRow(row) {
448
548
  return {
@@ -628,9 +728,15 @@ function createMemory(input, dedupeMode = "merge", db) {
628
728
  insertTag.run(id, tag);
629
729
  }
630
730
  const memory = getMemory(id, d);
631
- try {
632
- runEntityExtraction(memory, input.project_id, d);
633
- } catch {}
731
+ runEntityExtraction(memory, input.project_id, d);
732
+ hookRegistry.runHooks("PostMemorySave", {
733
+ memory,
734
+ wasUpdated: false,
735
+ agentId: input.agent_id,
736
+ projectId: input.project_id,
737
+ sessionId: input.session_id,
738
+ timestamp: Date.now()
739
+ });
634
740
  return memory;
635
741
  }
636
742
  function getMemory(id, db) {
@@ -854,20 +960,33 @@ function updateMemory(id, input, db) {
854
960
  params.push(id);
855
961
  d.run(`UPDATE memories SET ${sets.join(", ")} WHERE id = ?`, params);
856
962
  const updated = getMemory(id, d);
857
- try {
858
- if (input.value !== undefined) {
963
+ if (input.value !== undefined) {
964
+ try {
859
965
  const oldLinks = getEntityMemoryLinks(undefined, updated.id, d);
860
966
  for (const link of oldLinks) {
861
967
  unlinkEntityFromMemory(link.entity_id, updated.id, d);
862
968
  }
863
- runEntityExtraction(updated, existing.project_id || undefined, d);
864
- }
865
- } catch {}
969
+ } catch {}
970
+ }
971
+ hookRegistry.runHooks("PostMemoryUpdate", {
972
+ memory: updated,
973
+ previousValue: existing.value,
974
+ agentId: existing.agent_id ?? undefined,
975
+ projectId: existing.project_id ?? undefined,
976
+ sessionId: existing.session_id ?? undefined,
977
+ timestamp: Date.now()
978
+ });
866
979
  return updated;
867
980
  }
868
981
  function deleteMemory(id, db) {
869
982
  const d = db || getDatabase();
870
983
  const result = d.run("DELETE FROM memories WHERE id = ?", [id]);
984
+ if (result.changes > 0) {
985
+ hookRegistry.runHooks("PostMemoryDelete", {
986
+ memoryId: id,
987
+ timestamp: Date.now()
988
+ });
989
+ }
871
990
  return result.changes > 0;
872
991
  }
873
992
  function bulkDeleteMemories(ids, db) {
@@ -1182,8 +1301,25 @@ class MemoryLockConflictError extends Error {
1182
1301
  // src/lib/focus.ts
1183
1302
  var sessionFocus = new Map;
1184
1303
  function setFocus(agentId, projectId) {
1304
+ const previous = getFocusCached(agentId);
1185
1305
  sessionFocus.set(agentId, projectId);
1186
1306
  updateAgent(agentId, { active_project_id: projectId });
1307
+ if (projectId && projectId !== previous) {
1308
+ hookRegistry.runHooks("OnSessionStart", {
1309
+ agentId,
1310
+ projectId,
1311
+ timestamp: Date.now()
1312
+ });
1313
+ } else if (!projectId && previous) {
1314
+ hookRegistry.runHooks("OnSessionEnd", {
1315
+ agentId,
1316
+ projectId: previous,
1317
+ timestamp: Date.now()
1318
+ });
1319
+ }
1320
+ }
1321
+ function getFocusCached(agentId) {
1322
+ return sessionFocus.get(agentId) ?? null;
1187
1323
  }
1188
1324
  function getFocus(agentId) {
1189
1325
  if (sessionFocus.has(agentId)) {
@@ -1319,6 +1455,13 @@ function createEntity(input, db) {
1319
1455
  timestamp,
1320
1456
  timestamp
1321
1457
  ]);
1458
+ hookRegistry.runHooks("PostEntityCreate", {
1459
+ entityId: id,
1460
+ name: input.name,
1461
+ entityType: input.type,
1462
+ projectId: input.project_id,
1463
+ timestamp: Date.now()
1464
+ });
1322
1465
  return getEntity(id, d);
1323
1466
  }
1324
1467
  function getEntity(id, db) {
@@ -2491,7 +2634,15 @@ function createRelation(input, db) {
2491
2634
  DO UPDATE SET weight = excluded.weight, metadata = excluded.metadata`, [id, input.source_entity_id, input.target_entity_id, input.relation_type, weight, metadata, timestamp]);
2492
2635
  const row = d.query(`SELECT * FROM relations
2493
2636
  WHERE source_entity_id = ? AND target_entity_id = ? AND relation_type = ?`).get(input.source_entity_id, input.target_entity_id, input.relation_type);
2494
- return parseRelationRow(row);
2637
+ const relation = parseRelationRow(row);
2638
+ hookRegistry.runHooks("PostRelationCreate", {
2639
+ relationId: relation.id,
2640
+ sourceEntityId: relation.source_entity_id,
2641
+ targetEntityId: relation.target_entity_id,
2642
+ relationType: relation.relation_type,
2643
+ timestamp: Date.now()
2644
+ });
2645
+ return relation;
2495
2646
  }
2496
2647
  function getRelation(id, db) {
2497
2648
  const d = db || getDatabase();
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Built-in hooks — registered at server/MCP startup.
3
+ * These are system-level hooks that power the auto-memory pipeline.
4
+ *
5
+ * Built-in hooks cannot be unregistered (builtin: true).
6
+ * They are always non-blocking so they never delay the calling operation.
7
+ */
8
+ import type { HookType } from "../types/hooks.js";
9
+ export declare function loadWebhooksFromDb(): void;
10
+ export declare function reloadWebhooks(): void;
11
+ export type { HookType };
12
+ //# sourceMappingURL=built-in-hooks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"built-in-hooks.d.ts","sourceRoot":"","sources":["../../src/lib/built-in-hooks.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAOH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AA2ElD,wBAAgB,kBAAkB,IAAI,IAAI,CAyBzC;AAuBD,wBAAgB,cAAc,IAAI,IAAI,CAGrC;AAGD,YAAY,EAAE,QAAQ,EAAE,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"focus.d.ts","sourceRoot":"","sources":["../../src/lib/focus.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAOH;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAIxE;AAED;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAWvD;AAED;;GAEG;AACH,wBAAgB,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAE7C;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,EAClC,iBAAiB,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAC3C,MAAM,GAAG,IAAI,CAUf;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,gBAAgB;IAC/B,sEAAsE;IACtE,SAAS,EAAE,IAAI,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,EAClC,iBAAiB,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,EAC5C,aAAa,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GACvC,gBAAgB,GAAG,IAAI,CAazB;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,GAChB;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CAKnC"}
1
+ {"version":3,"file":"focus.d.ts","sourceRoot":"","sources":["../../src/lib/focus.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAQH;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAqBxE;AAOD;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAWvD;AAED;;GAEG;AACH,wBAAgB,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAE7C;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,EAClC,iBAAiB,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAC3C,MAAM,GAAG,IAAI,CAUf;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,gBAAgB;IAC/B,sEAAsE;IACtE,SAAS,EAAE,IAAI,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,EAClC,iBAAiB,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,EAC5C,aAAa,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GACvC,gBAAgB,GAAG,IAAI,CAazB;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,GAChB;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CAKnC"}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Hook registry — the central nervous system connecting all memory operations.
3
+ *
4
+ * Blocking hooks: await handler, return false = cancel the operation.
5
+ * Non-blocking hooks: fire-and-forget in background, never delay caller.
6
+ *
7
+ * Hooks run in priority order (ascending — lower number first).
8
+ * Per-agent and per-project scoping supported.
9
+ */
10
+ import type { Hook, HookType, HookHandler, HookRegistration, HookContextMap } from "../types/hooks.js";
11
+ declare class HookRegistry {
12
+ private hooks;
13
+ /**
14
+ * Register a hook. Returns the assigned hookId.
15
+ * Built-in hooks (builtin: true) cannot be unregistered.
16
+ */
17
+ register<T extends HookType>(reg: HookRegistration<T>): string;
18
+ /**
19
+ * Unregister a hook by ID.
20
+ * Returns false if hook not found or is a built-in.
21
+ */
22
+ unregister(hookId: string): boolean;
23
+ /** List all hooks, optionally filtered by type */
24
+ list(type?: HookType): Hook[];
25
+ /**
26
+ * Run all hooks of a given type for a given context.
27
+ *
28
+ * Returns true if the operation should proceed.
29
+ * Returns false if any blocking hook cancelled it.
30
+ *
31
+ * Non-blocking hooks are fired async and never delay the return.
32
+ */
33
+ runHooks<T extends HookType>(type: T, context: HookContextMap[T]): Promise<boolean>;
34
+ /**
35
+ * Get hooks matching type + agent/project scope.
36
+ * A hook with no agentId/projectId matches everything.
37
+ */
38
+ private getMatchingHooks;
39
+ /** Get stats about registered hooks */
40
+ stats(): {
41
+ total: number;
42
+ byType: Record<string, number>;
43
+ blocking: number;
44
+ nonBlocking: number;
45
+ };
46
+ }
47
+ /** Singleton — shared across the whole process */
48
+ export declare const hookRegistry: HookRegistry;
49
+ export type { Hook, HookType, HookHandler, HookRegistration };
50
+ //# sourceMappingURL=hooks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../../src/lib/hooks.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EACV,IAAI,EACJ,QAAQ,EACR,WAAW,EACX,gBAAgB,EAChB,cAAc,EACf,MAAM,mBAAmB,CAAC;AAO3B,cAAM,YAAY;IAChB,OAAO,CAAC,KAAK,CAA2B;IAExC;;;OAGG;IACH,QAAQ,CAAC,CAAC,SAAS,QAAQ,EAAE,GAAG,EAAE,gBAAgB,CAAC,CAAC,CAAC,GAAG,MAAM;IAW9D;;;OAGG;IACH,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO;IAQnC,kDAAkD;IAClD,IAAI,CAAC,IAAI,CAAC,EAAE,QAAQ,GAAG,IAAI,EAAE;IAM7B;;;;;;;OAOG;IACG,QAAQ,CAAC,CAAC,SAAS,QAAQ,EAC/B,IAAI,EAAE,CAAC,EACP,OAAO,EAAE,cAAc,CAAC,CAAC,CAAC,GACzB,OAAO,CAAC,OAAO,CAAC;IA8BnB;;;OAGG;IACH,OAAO,CAAC,gBAAgB;IAaxB,uCAAuC;IACvC,KAAK,IAAI;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE;CAalG;AAED,kDAAkD;AAClD,eAAO,MAAM,YAAY,cAAqB,CAAC;AAG/C,YAAY,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,gBAAgB,EAAE,CAAC"}