recallnest 1.4.1 → 2.2.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.
Files changed (166) hide show
  1. package/.dockerignore +11 -0
  2. package/CLAUDE.md +7 -3
  3. package/Dockerfile +20 -0
  4. package/README.md +202 -130
  5. package/README_CN.md +263 -190
  6. package/ROADMAP.md +128 -81
  7. package/SKILL.md +250 -0
  8. package/assets/dashboard.png +0 -0
  9. package/assets/knowledge-graph.png +0 -0
  10. package/assets/screenshots/ui-full.png +0 -0
  11. package/assets/screenshots/ui-overview.png +0 -0
  12. package/assets/screenshots/ui-search.png +0 -0
  13. package/assets/ui/app.js +227 -3
  14. package/assets/ui/index.html +81 -11
  15. package/assets/ui/styles.css +178 -2
  16. package/bin/recallnest.mjs +62 -0
  17. package/bun.lock +337 -0
  18. package/daily-recall.sh +23 -0
  19. package/daily-reflections/2026-04-02.md +35 -0
  20. package/daily-reflections/2026-04-03.md +37 -0
  21. package/daily-reflections/2026-04-04.md +37 -0
  22. package/daily-reflections/2026-04-05.md +35 -0
  23. package/daily-reflections/2026-04-06.md +36 -0
  24. package/daily-reflections/2026-04-07.md +35 -0
  25. package/daily-reflections/2026-04-08.md +35 -0
  26. package/daily-reflections/2026-04-12.md +35 -0
  27. package/data/alias-map.example.json +22 -0
  28. package/docs/plans/2026-04-01-recall-boost.md +100 -0
  29. package/docs/superpowers/plans/2026-04-11-constructive-retrieval-emotion.md +1195 -0
  30. package/docs/superpowers/specs/2026-04-11-constructive-retrieval-emotion-design.md +398 -0
  31. package/glama.json +6 -0
  32. package/integrations/setup-all.sh +31 -0
  33. package/package.json +10 -3
  34. package/plan-philosophy-upgrades.md +363 -0
  35. package/plan-product-upgrades.md +155 -0
  36. package/plan.md +140 -0
  37. package/research-memory-evolution.md +440 -0
  38. package/research-rl-agent-memory.md +330 -0
  39. package/scripts/backfill-anchors.ts +130 -0
  40. package/scripts/diagnose-vector-search.ts +132 -0
  41. package/scripts/migrate-language.ts +75 -0
  42. package/scripts/verify-emotion-decay.ts +148 -0
  43. package/src/__tests__/activity-counter.test.ts +96 -0
  44. package/src/__tests__/admission-control.test.ts +206 -0
  45. package/src/__tests__/anchor-generator.test.ts +67 -0
  46. package/src/__tests__/babel-integration.test.ts +139 -0
  47. package/src/__tests__/capture-engine.test.ts +5 -3
  48. package/src/__tests__/cascade-forget.test.ts +1 -1
  49. package/src/__tests__/cc8-cc1-essential-context.test.ts +298 -0
  50. package/src/__tests__/circuit-breaker.test.ts +99 -0
  51. package/src/__tests__/cluster-consolidation.test.ts +712 -0
  52. package/src/__tests__/confidence-metadata.test.ts +125 -0
  53. package/src/__tests__/confidence-tracker.test.ts +53 -0
  54. package/src/__tests__/consolidation-engine.test.ts +1 -1
  55. package/src/__tests__/constructive-retrieval.test.ts +509 -0
  56. package/src/__tests__/context-collapse-renderer.test.ts +214 -0
  57. package/src/__tests__/context-reconstructor.test.ts +65 -0
  58. package/src/__tests__/conversation-importer.test.ts +692 -0
  59. package/src/__tests__/data-checkup.test.ts +225 -0
  60. package/src/__tests__/decay-exemption.test.ts +84 -0
  61. package/src/__tests__/dedup-guard.test.ts +39 -0
  62. package/src/__tests__/distill-lock.test.ts +176 -0
  63. package/src/__tests__/dream-pipeline.test.ts +232 -0
  64. package/src/__tests__/emotion-decay.test.ts +46 -0
  65. package/src/__tests__/emotion-detector.test.ts +56 -0
  66. package/src/__tests__/emotion-phase1-integration.test.ts +153 -0
  67. package/src/__tests__/emotion-retrieval.test.ts +56 -0
  68. package/src/__tests__/ethics-layer.test.ts +476 -0
  69. package/src/__tests__/feature-flags-integration.test.ts +54 -0
  70. package/src/__tests__/frequency-tracker.test.ts +113 -0
  71. package/src/__tests__/graph-export.test.ts +489 -0
  72. package/src/__tests__/interference-detector.test.ts +203 -0
  73. package/src/__tests__/mcp-efficiency.test.ts +288 -0
  74. package/src/__tests__/memory-evolution-b2-b3-e1.test.ts +243 -0
  75. package/src/__tests__/memory-evolution.test.ts +271 -0
  76. package/src/__tests__/memory-lint.test.ts +271 -0
  77. package/src/__tests__/narrative-architecture.test.ts +456 -0
  78. package/src/__tests__/pending-review.test.ts +77 -0
  79. package/src/__tests__/post-retrieval-filter.test.ts +178 -0
  80. package/src/__tests__/prediction-engine.test.ts +349 -0
  81. package/src/__tests__/predictive-prospective-memory.test.ts +416 -0
  82. package/src/__tests__/preference-slots.test.ts +99 -0
  83. package/src/__tests__/query-expander-alias.test.ts +89 -0
  84. package/src/__tests__/recall-governor.test.ts +227 -0
  85. package/src/__tests__/resume-context.test.ts +156 -1
  86. package/src/__tests__/salience-scoring.test.ts +106 -0
  87. package/src/__tests__/session-distiller.test.ts +611 -0
  88. package/src/__tests__/skill-engine.test.ts +546 -0
  89. package/src/__tests__/skill-promotion.test.ts +349 -0
  90. package/src/__tests__/supersede-chain.test.ts +154 -0
  91. package/src/__tests__/temporal-validity.test.ts +160 -0
  92. package/src/__tests__/topic-tag.test.ts +113 -0
  93. package/src/__tests__/trusted-memory.test.ts +828 -0
  94. package/src/__tests__/write-verifier.test.ts +122 -0
  95. package/src/access-tracker.ts +3 -2
  96. package/src/activity-counter.ts +109 -0
  97. package/src/adaptive-retrieval.ts +6 -5
  98. package/src/admission-control.ts +163 -0
  99. package/src/anchor-generator.ts +80 -0
  100. package/src/api-server.ts +10 -0
  101. package/src/audit-log.ts +122 -0
  102. package/src/auto-gc.ts +98 -29
  103. package/src/auto-recall.ts +33 -4
  104. package/src/capture-engine.ts +372 -23
  105. package/src/cascade-forget.ts +3 -2
  106. package/src/cli.ts +107 -5
  107. package/src/compat.ts +22 -0
  108. package/src/confidence-tracker.ts +210 -47
  109. package/src/conflict-store.ts +12 -3
  110. package/src/consolidation-engine.ts +296 -8
  111. package/src/context-collapse-renderer.ts +222 -0
  112. package/src/context-composer-stable-selection.ts +5 -1
  113. package/src/context-composer.ts +239 -4
  114. package/src/context-reconstructor.ts +404 -0
  115. package/src/conversation-importer.ts +441 -0
  116. package/src/data-checkup.ts +232 -0
  117. package/src/decay-engine.ts +84 -0
  118. package/src/distill-lock.ts +122 -0
  119. package/src/doctor.ts +8 -10
  120. package/src/dream-pipeline.ts +227 -0
  121. package/src/emotion-detector.ts +90 -0
  122. package/src/eval.ts +3 -2
  123. package/src/forget-engine.ts +269 -0
  124. package/src/frequency-tracker.ts +175 -0
  125. package/src/graph-export.ts +644 -0
  126. package/src/ingest.ts +36 -6
  127. package/src/interference-detector.ts +187 -0
  128. package/src/kg-extractor.ts +15 -24
  129. package/src/kg-store.ts +2 -1
  130. package/src/language-hook.ts +67 -0
  131. package/src/llm-client.ts +289 -6
  132. package/src/mcp-server.ts +707 -80
  133. package/src/memory-assets.ts +22 -5
  134. package/src/memory-evolution.ts +365 -0
  135. package/src/memory-lint.ts +429 -0
  136. package/src/memory-output.ts +69 -0
  137. package/src/memory-schema.ts +74 -2
  138. package/src/narrative-schema.ts +79 -0
  139. package/src/narrative-tagger.ts +164 -0
  140. package/src/pii-detector.ts +98 -0
  141. package/src/post-retrieval-filter.ts +76 -0
  142. package/src/prediction-engine.ts +253 -0
  143. package/src/preference-slots.ts +65 -2
  144. package/src/prospective-memory.ts +215 -0
  145. package/src/query-expander.ts +67 -0
  146. package/src/recall-governor.ts +183 -0
  147. package/src/retention-policy.ts +90 -0
  148. package/src/retriever.ts +828 -55
  149. package/src/rif.ts +34 -8
  150. package/src/runtime-config.ts +23 -8
  151. package/src/scope-inventory.ts +4 -3
  152. package/src/scope-policy.ts +4 -0
  153. package/src/session-distiller.ts +307 -0
  154. package/src/session-output.ts +40 -0
  155. package/src/session-schema.ts +37 -0
  156. package/src/session-store.ts +7 -2
  157. package/src/skill-engine.ts +208 -0
  158. package/src/skill-promotion.ts +293 -0
  159. package/src/skill-schema.ts +39 -0
  160. package/src/store.ts +96 -10
  161. package/src/topic-tag.ts +66 -0
  162. package/src/tracker.ts +3 -1
  163. package/src/ui-server.ts +80 -8
  164. package/src/workflow-observation-scope-review.ts +2 -1
  165. package/src/workflow-observation-store.ts +2 -1
  166. package/src/write-verifier.ts +126 -0
@@ -0,0 +1,644 @@
1
+ /**
2
+ * Memory Knowledge Graph Export — generates an interactive HTML visualization
3
+ * of memory entries as a force-directed graph.
4
+ *
5
+ * Uses D3.js v7 for layout and rendering. The output is a self-contained HTML
6
+ * file that can be opened in any browser.
7
+ */
8
+
9
+ import type { MemoryEntry, MemoryStore } from "./store.js";
10
+ import { parseEvolution, isActiveMemory } from "./memory-evolution.js";
11
+ import { cosineSimilarity } from "./multi-vector.js";
12
+ import { parseNarrative } from "./narrative-schema.js";
13
+ import { writeFileSync, mkdirSync, existsSync } from "node:fs";
14
+ import { join, resolve } from "node:path";
15
+ import { metaDir } from "./compat.js";
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // Types
19
+ // ---------------------------------------------------------------------------
20
+
21
+ export interface GraphNode {
22
+ id: string;
23
+ label: string; // truncated text
24
+ category: string;
25
+ scope: string;
26
+ importance: number;
27
+ timestamp: number;
28
+ accessCount: number;
29
+ }
30
+
31
+ export interface GraphEdge {
32
+ source: string;
33
+ target: string;
34
+ type: "supersede" | "consolidation" | "cluster" | "scope" | "semantic" | "narrative";
35
+ }
36
+
37
+ export interface MemoryGraph {
38
+ nodes: GraphNode[];
39
+ edges: GraphEdge[];
40
+ }
41
+
42
+ export interface GraphExportOptions {
43
+ scope?: string;
44
+ maxNodes?: number; // default 200
45
+ outputPath?: string; // default data/exports/
46
+ }
47
+
48
+ // ---------------------------------------------------------------------------
49
+ // Helpers
50
+ // ---------------------------------------------------------------------------
51
+
52
+ function truncateText(text: string, maxLen: number): string {
53
+ if (text.length <= maxLen) return text;
54
+ return text.slice(0, maxLen - 1) + "\u2026";
55
+ }
56
+
57
+ interface ParsedMetadata {
58
+ evolution?: Record<string, unknown>;
59
+ clustered_with?: string;
60
+ cluster_members?: string[];
61
+ [key: string]: unknown;
62
+ }
63
+
64
+ function parseMetadataSafe(metadata: string | undefined): ParsedMetadata {
65
+ if (!metadata) return {};
66
+ try {
67
+ return JSON.parse(metadata) as ParsedMetadata;
68
+ } catch {
69
+ return {};
70
+ }
71
+ }
72
+
73
+ // ---------------------------------------------------------------------------
74
+ // Node Selection — diverse, connected, balanced
75
+ // ---------------------------------------------------------------------------
76
+
77
+ /**
78
+ * Select nodes for the graph with diversity in mind:
79
+ * 1. First, include entries that have evolution links (they form interesting connected subgraphs)
80
+ * 2. Then, fill remaining slots with round-robin across categories (sorted by importance within each)
81
+ *
82
+ * This avoids the "all patterns, no edges" problem that pure importance sorting causes.
83
+ */
84
+ function selectDiverseNodes(entries: MemoryEntry[], maxNodes: number): MemoryEntry[] {
85
+ if (entries.length <= maxNodes) return entries;
86
+
87
+ const selected = new Map<string, MemoryEntry>();
88
+
89
+ // Phase 1: Prioritize entries with evolution links (they create edges)
90
+ for (const entry of entries) {
91
+ if (selected.size >= maxNodes) break;
92
+ const evo = parseEvolution(entry.metadata, entry.timestamp);
93
+ const meta = parseMetadataSafe(entry.metadata);
94
+ const hasLinks = evo.supersededBy || evo.supersedes || evo.consolidatedInto ||
95
+ typeof meta.clustered_with === "string" || Array.isArray(meta.cluster_members);
96
+ if (hasLinks && !selected.has(entry.id)) {
97
+ selected.set(entry.id, entry);
98
+ }
99
+ }
100
+
101
+ // Phase 2: Round-robin across categories, picking top-importance entries
102
+ const byCategory = new Map<string, MemoryEntry[]>();
103
+ for (const entry of entries) {
104
+ if (selected.has(entry.id)) continue;
105
+ const list = byCategory.get(entry.category) ?? [];
106
+ list.push(entry);
107
+ byCategory.set(entry.category, list);
108
+ }
109
+ // Sort each category by importance descending
110
+ for (const [, list] of byCategory) {
111
+ list.sort((a, b) => b.importance - a.importance);
112
+ }
113
+
114
+ // Round-robin: take one from each category in turn
115
+ const categories = [...byCategory.keys()];
116
+ const indices = new Map<string, number>(categories.map(c => [c, 0]));
117
+ let added = true;
118
+ while (selected.size < maxNodes && added) {
119
+ added = false;
120
+ for (const cat of categories) {
121
+ if (selected.size >= maxNodes) break;
122
+ const list = byCategory.get(cat)!;
123
+ const idx = indices.get(cat)!;
124
+ if (idx < list.length) {
125
+ selected.set(list[idx].id, list[idx]);
126
+ indices.set(cat, idx + 1);
127
+ added = true;
128
+ }
129
+ }
130
+ }
131
+
132
+ return [...selected.values()];
133
+ }
134
+
135
+ // ---------------------------------------------------------------------------
136
+ // Graph Builder
137
+ // ---------------------------------------------------------------------------
138
+
139
+ export async function buildMemoryGraph(
140
+ store: Pick<MemoryStore, "list"> & Partial<Pick<MemoryStore, "getVectors">>,
141
+ options?: GraphExportOptions,
142
+ ): Promise<MemoryGraph> {
143
+ const maxNodes = options?.maxNodes ?? 200;
144
+ const scopeFilter = options?.scope ? [options.scope] : undefined;
145
+
146
+ // 1. List all entries
147
+ const entries = await store.list(scopeFilter, undefined, 10000, 0);
148
+
149
+ // 2. Filter to active entries only
150
+ const active = entries.filter(e => isActiveMemory(e.metadata));
151
+
152
+ // 3. Select nodes with diversity: prioritize connected nodes, then balance categories
153
+ const selected = selectDiverseNodes(active, maxNodes);
154
+ const nodeIds = new Set(selected.map(e => e.id));
155
+
156
+ // 4. Build nodes
157
+ const nodes: GraphNode[] = selected.map(entry => {
158
+ const evo = parseEvolution(entry.metadata, entry.timestamp);
159
+ return {
160
+ id: entry.id,
161
+ label: truncateText(entry.text, 80),
162
+ category: entry.category,
163
+ scope: entry.scope,
164
+ importance: entry.importance,
165
+ timestamp: entry.timestamp,
166
+ accessCount: evo.accessCount,
167
+ };
168
+ });
169
+
170
+ // 5. Build edges
171
+ const edges: GraphEdge[] = [];
172
+
173
+ for (const entry of selected) {
174
+ const evo = parseEvolution(entry.metadata, entry.timestamp);
175
+ const meta = parseMetadataSafe(entry.metadata);
176
+
177
+ // supersededBy
178
+ if (evo.supersededBy && nodeIds.has(evo.supersededBy)) {
179
+ edges.push({ source: entry.id, target: evo.supersededBy, type: "supersede" });
180
+ }
181
+
182
+ // supersedes
183
+ if (evo.supersedes && nodeIds.has(evo.supersedes)) {
184
+ edges.push({ source: evo.supersedes, target: entry.id, type: "supersede" });
185
+ }
186
+
187
+ // consolidatedInto
188
+ if (evo.consolidatedInto && nodeIds.has(evo.consolidatedInto)) {
189
+ edges.push({ source: entry.id, target: evo.consolidatedInto, type: "consolidation" });
190
+ }
191
+
192
+ // clustered_with (single ID stored at top level of metadata)
193
+ if (typeof meta.clustered_with === "string" && nodeIds.has(meta.clustered_with)) {
194
+ edges.push({ source: entry.id, target: meta.clustered_with, type: "cluster" });
195
+ }
196
+
197
+ // cluster_members (array at top level of metadata)
198
+ if (Array.isArray(meta.cluster_members)) {
199
+ for (const memberId of meta.cluster_members) {
200
+ if (typeof memberId === "string" && nodeIds.has(memberId)) {
201
+ edges.push({ source: entry.id, target: memberId, type: "cluster" });
202
+ }
203
+ }
204
+ }
205
+ }
206
+
207
+ // 6. Scope edges — group entries by scope, connect within groups (≤ 20 per scope)
208
+ const byScope = new Map<string, string[]>();
209
+ for (const node of nodes) {
210
+ const list = byScope.get(node.scope) ?? [];
211
+ list.push(node.id);
212
+ byScope.set(node.scope, list);
213
+ }
214
+
215
+ for (const [, ids] of byScope) {
216
+ if (ids.length > 15 || ids.length < 2) continue;
217
+ // Connect each node to the next in the group (chain, not full mesh)
218
+ for (let i = 0; i < ids.length - 1; i++) {
219
+ edges.push({ source: ids[i], target: ids[i + 1], type: "scope" });
220
+ }
221
+ }
222
+
223
+ // 7. HP-narrative: Narrative edges — connect entries sharing the same generalEventId
224
+ const byGeneralEvent = new Map<string, string[]>();
225
+ for (const entry of selected) {
226
+ const narrative = parseNarrative(entry.metadata);
227
+ if (!narrative) continue;
228
+ const list = byGeneralEvent.get(narrative.generalEventId) ?? [];
229
+ list.push(entry.id);
230
+ byGeneralEvent.set(narrative.generalEventId, list);
231
+ }
232
+ for (const [, ids] of byGeneralEvent) {
233
+ if (ids.length < 2 || ids.length > 15) continue;
234
+ // Chain: connect each node to the next in the narrative group
235
+ for (let i = 0; i < ids.length - 1; i++) {
236
+ edges.push({ source: ids[i], target: ids[i + 1], type: "narrative" });
237
+ }
238
+ }
239
+
240
+ // 8. Cross-scope semantic bridges — connect entries from DIFFERENT scopes
241
+ // that are semantically similar (cosine ≥ 0.65). This reveals hidden
242
+ // cross-domain knowledge connections that the user's interdisciplinary
243
+ // thinking creates but explicit metadata doesn't capture.
244
+ // Vectors must be fetched separately since list() omits them for perf.
245
+ const SEMANTIC_BRIDGE_THRESHOLD = 0.65;
246
+ const MAX_SEMANTIC_EDGES = 30; // cap to avoid visual clutter
247
+
248
+ if (store.getVectors) {
249
+ const vectorMap = await store.getVectors(selected.map(e => e.id));
250
+ const semanticCandidates: { source: string; target: string; sim: number }[] = [];
251
+
252
+ for (let i = 0; i < selected.length; i++) {
253
+ for (let j = i + 1; j < selected.length; j++) {
254
+ const a = selected[i];
255
+ const b = selected[j];
256
+ // Only cross-scope — same-scope connections are handled by scope chains
257
+ if (a.scope === b.scope) continue;
258
+ const vecA = vectorMap.get(a.id);
259
+ const vecB = vectorMap.get(b.id);
260
+ if (!vecA || !vecB) continue;
261
+ const sim = cosineSimilarity(vecA, vecB);
262
+ if (sim >= SEMANTIC_BRIDGE_THRESHOLD) {
263
+ semanticCandidates.push({ source: a.id, target: b.id, sim });
264
+ }
265
+ }
266
+ }
267
+
268
+ // Keep top-N by similarity to avoid clutter
269
+ semanticCandidates.sort((a, b) => b.sim - a.sim);
270
+ for (const { source, target } of semanticCandidates.slice(0, MAX_SEMANTIC_EDGES)) {
271
+ edges.push({ source, target, type: "semantic" });
272
+ }
273
+ }
274
+
275
+ // 8. Deduplicate edges
276
+ const edgeKey = (e: GraphEdge) => `${e.source}|${e.target}|${e.type}`;
277
+ const seen = new Set<string>();
278
+ const uniqueEdges: GraphEdge[] = [];
279
+ for (const edge of edges) {
280
+ const key = edgeKey(edge);
281
+ if (!seen.has(key)) {
282
+ seen.add(key);
283
+ uniqueEdges.push(edge);
284
+ }
285
+ }
286
+
287
+ return { nodes, edges: uniqueEdges };
288
+ }
289
+
290
+ // ---------------------------------------------------------------------------
291
+ // HTML Renderer
292
+ // ---------------------------------------------------------------------------
293
+
294
+ export function renderGraphHTML(graph: MemoryGraph): string {
295
+ const graphJSON = JSON.stringify(graph);
296
+
297
+ return `<!DOCTYPE html>
298
+ <html lang="en">
299
+ <head>
300
+ <meta charset="UTF-8">
301
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
302
+ <title>RecallNest Knowledge Graph</title>
303
+ <style>
304
+ * { margin: 0; padding: 0; box-sizing: border-box; }
305
+ body {
306
+ background: #0d1117;
307
+ color: #c9d1d9;
308
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
309
+ overflow: hidden;
310
+ }
311
+ #header {
312
+ position: fixed;
313
+ top: 16px;
314
+ left: 16px;
315
+ z-index: 10;
316
+ pointer-events: none;
317
+ }
318
+ #header h1 {
319
+ font-size: 20px;
320
+ font-weight: 600;
321
+ color: #e6edf3;
322
+ }
323
+ #header .stats {
324
+ font-size: 13px;
325
+ color: #8b949e;
326
+ margin-top: 4px;
327
+ }
328
+ #legend {
329
+ position: fixed;
330
+ top: 16px;
331
+ right: 16px;
332
+ z-index: 10;
333
+ background: rgba(22, 27, 34, 0.9);
334
+ border: 1px solid #30363d;
335
+ border-radius: 6px;
336
+ padding: 12px 16px;
337
+ font-size: 12px;
338
+ }
339
+ #legend h3 {
340
+ font-size: 13px;
341
+ margin-bottom: 8px;
342
+ color: #e6edf3;
343
+ }
344
+ .legend-item {
345
+ display: flex;
346
+ align-items: center;
347
+ gap: 8px;
348
+ margin-bottom: 4px;
349
+ }
350
+ .legend-dot {
351
+ width: 10px;
352
+ height: 10px;
353
+ border-radius: 50%;
354
+ flex-shrink: 0;
355
+ }
356
+ .legend-line {
357
+ width: 20px;
358
+ height: 0;
359
+ border-top: 2px solid #fbbf24;
360
+ flex-shrink: 0;
361
+ }
362
+ .legend-line.dashed { border-top-style: dashed; border-top-color: #60a5fa; }
363
+ .legend-line.dotted { border-top-style: dotted; border-top-color: #9ca3af; }
364
+ #tooltip {
365
+ position: fixed;
366
+ display: none;
367
+ background: rgba(22, 27, 34, 0.95);
368
+ border: 1px solid #30363d;
369
+ border-radius: 6px;
370
+ padding: 10px 14px;
371
+ font-size: 12px;
372
+ max-width: 360px;
373
+ pointer-events: none;
374
+ z-index: 20;
375
+ line-height: 1.5;
376
+ }
377
+ #tooltip .tt-label { font-weight: 600; color: #e6edf3; margin-bottom: 4px; word-break: break-word; }
378
+ #tooltip .tt-meta { color: #8b949e; }
379
+ #tooltip .tt-meta-line { margin-top: 2px; }
380
+ svg { width: 100vw; height: 100vh; }
381
+ </style>
382
+ </head>
383
+ <body>
384
+ <div id="header">
385
+ <h1>RecallNest Knowledge Graph</h1>
386
+ <div class="stats" id="stats"></div>
387
+ </div>
388
+
389
+ <div id="legend">
390
+ <h3>Categories</h3>
391
+ <div class="legend-item"><div class="legend-dot" style="background:#f59e0b"></div><span>profile</span></div>
392
+ <div class="legend-item"><div class="legend-dot" style="background:#3b82f6"></div><span>preferences</span></div>
393
+ <div class="legend-item"><div class="legend-dot" style="background:#10b981"></div><span>entities</span></div>
394
+ <div class="legend-item"><div class="legend-dot" style="background:#6b7280"></div><span>events</span></div>
395
+ <div class="legend-item"><div class="legend-dot" style="background:#ef4444"></div><span>cases</span></div>
396
+ <div class="legend-item"><div class="legend-dot" style="background:#8b5cf6"></div><span>patterns</span></div>
397
+ <h3 style="margin-top:10px">Edge Types</h3>
398
+ <div class="legend-item"><div class="legend-line"></div><span>supersede</span></div>
399
+ <div class="legend-item"><div class="legend-line dashed"></div><span>cluster</span></div>
400
+ <div class="legend-item"><div class="legend-line dotted"></div><span>scope</span></div>
401
+ <div class="legend-item"><div class="legend-line" style="border-top-color:#f472b6;border-top-style:dashed"></div><span>semantic bridge</span></div>
402
+ <div class="legend-item"><div class="legend-line" style="border-top-color:#34d399"></div><span>narrative</span></div>
403
+ </div>
404
+
405
+ <div id="tooltip">
406
+ <div class="tt-label" id="tt-label"></div>
407
+ <div class="tt-meta" id="tt-meta"></div>
408
+ </div>
409
+
410
+ <svg id="graph"></svg>
411
+
412
+ <script src="https://d3js.org/d3.v7.min.js"></script>
413
+ <script>
414
+ const GRAPH_DATA = ${graphJSON};
415
+
416
+ const CATEGORY_COLORS = {
417
+ profile: "#f59e0b",
418
+ preferences: "#3b82f6",
419
+ entities: "#10b981",
420
+ events: "#6b7280",
421
+ cases: "#ef4444",
422
+ patterns: "#8b5cf6",
423
+ };
424
+ const DEFAULT_COLOR = "#9ca3af";
425
+
426
+ function categoryColor(cat) {
427
+ return CATEGORY_COLORS[cat] || DEFAULT_COLOR;
428
+ }
429
+
430
+ function nodeRadius(importance) {
431
+ return 4 + importance * 8; // 4-12
432
+ }
433
+
434
+ function edgeStroke(type) {
435
+ if (type === "supersede") return "";
436
+ if (type === "cluster") return "8,4";
437
+ if (type === "consolidation") return "";
438
+ if (type === "semantic") return "6,3,2,3"; // dash-dot pattern
439
+ return "3,3";
440
+ }
441
+
442
+ function edgeColor(type) {
443
+ if (type === "supersede" || type === "consolidation") return "#fbbf24";
444
+ if (type === "cluster") return "#60a5fa";
445
+ if (type === "semantic") return "#f472b6";
446
+ if (type === "narrative") return "#34d399";
447
+ return "#9ca3af";
448
+ }
449
+
450
+ function edgeOpacity(type) {
451
+ if (type === "scope") return 0.4;
452
+ if (type === "cluster") return 0.65;
453
+ if (type === "semantic") return 0.6;
454
+ return 0.7;
455
+ }
456
+
457
+ // Stats
458
+ document.getElementById("stats").textContent =
459
+ GRAPH_DATA.nodes.length + " nodes, " + GRAPH_DATA.edges.length + " edges";
460
+
461
+ var svg = d3.select("#graph");
462
+ var width = window.innerWidth;
463
+ var height = window.innerHeight;
464
+
465
+ var g = svg.append("g");
466
+
467
+ // Zoom
468
+ var zoom = d3.zoom()
469
+ .scaleExtent([0.1, 8])
470
+ .on("zoom", function(event) { g.attr("transform", event.transform); });
471
+ svg.call(zoom);
472
+
473
+ // Simulation
474
+ var simulation = d3.forceSimulation(GRAPH_DATA.nodes)
475
+ .force("link", d3.forceLink(GRAPH_DATA.edges)
476
+ .id(function(d) { return d.id; })
477
+ .distance(80))
478
+ .force("charge", d3.forceManyBody().strength(-120))
479
+ .force("center", d3.forceCenter(width / 2, height / 2))
480
+ .force("collide", d3.forceCollide().radius(function(d) { return nodeRadius(d.importance) + 2; }));
481
+
482
+ // Edges
483
+ var link = g.append("g")
484
+ .selectAll("line")
485
+ .data(GRAPH_DATA.edges)
486
+ .join("line")
487
+ .attr("stroke", function(d) { return edgeColor(d.type); })
488
+ .attr("stroke-width", 2)
489
+ .attr("stroke-dasharray", function(d) { return edgeStroke(d.type); })
490
+ .attr("stroke-opacity", function(d) { return edgeOpacity(d.type); });
491
+
492
+ // Nodes
493
+ var node = g.append("g")
494
+ .selectAll("circle")
495
+ .data(GRAPH_DATA.nodes)
496
+ .join("circle")
497
+ .attr("r", function(d) { return nodeRadius(d.importance); })
498
+ .attr("fill", function(d) { return categoryColor(d.category); })
499
+ .attr("stroke", "#0d1117")
500
+ .attr("stroke-width", 1.5)
501
+ .call(d3.drag()
502
+ .on("start", dragstarted)
503
+ .on("drag", dragged)
504
+ .on("end", dragended));
505
+
506
+ // Tooltip
507
+ var tooltip = document.getElementById("tooltip");
508
+ var ttLabel = document.getElementById("tt-label");
509
+ var ttMeta = document.getElementById("tt-meta");
510
+
511
+ function formatAge(ts) {
512
+ var days = Math.floor((Date.now() - ts) / 86400000);
513
+ if (days < 1) return "today";
514
+ if (days === 1) return "1 day ago";
515
+ if (days < 30) return days + " days ago";
516
+ var months = Math.floor(days / 30);
517
+ return months + (months === 1 ? " month ago" : " months ago");
518
+ }
519
+
520
+ function updateTooltip(d) {
521
+ ttLabel.textContent = d.label;
522
+ // Clear previous meta lines
523
+ while (ttMeta.firstChild) ttMeta.removeChild(ttMeta.firstChild);
524
+ var lines = [
525
+ "Category: " + d.category,
526
+ "Scope: " + d.scope,
527
+ "Importance: " + d.importance.toFixed(2),
528
+ "Age: " + formatAge(d.timestamp),
529
+ "Accesses: " + d.accessCount,
530
+ ];
531
+ for (var i = 0; i < lines.length; i++) {
532
+ var div = document.createElement("div");
533
+ div.className = "tt-meta-line";
534
+ div.textContent = lines[i];
535
+ ttMeta.appendChild(div);
536
+ }
537
+ }
538
+
539
+ node
540
+ .on("mouseover", function(event, d) {
541
+ updateTooltip(d);
542
+ tooltip.style.display = "block";
543
+ })
544
+ .on("mousemove", function(event) {
545
+ tooltip.style.left = (event.clientX + 12) + "px";
546
+ tooltip.style.top = (event.clientY + 12) + "px";
547
+ })
548
+ .on("mouseout", function() {
549
+ tooltip.style.display = "none";
550
+ });
551
+
552
+ // Tick
553
+ simulation.on("tick", function() {
554
+ link
555
+ .attr("x1", function(d) { return d.source.x; })
556
+ .attr("y1", function(d) { return d.source.y; })
557
+ .attr("x2", function(d) { return d.target.x; })
558
+ .attr("y2", function(d) { return d.target.y; });
559
+ node
560
+ .attr("cx", function(d) { return d.x; })
561
+ .attr("cy", function(d) { return d.y; });
562
+ });
563
+
564
+ function dragstarted(event, d) {
565
+ if (!event.active) simulation.alphaTarget(0.3).restart();
566
+ d.fx = d.x;
567
+ d.fy = d.y;
568
+ }
569
+ function dragged(event, d) {
570
+ d.fx = event.x;
571
+ d.fy = event.y;
572
+ }
573
+ function dragended(event, d) {
574
+ if (!event.active) simulation.alphaTarget(0);
575
+ d.fx = null;
576
+ d.fy = null;
577
+ }
578
+ </script>
579
+ </body>
580
+ </html>`;
581
+ }
582
+
583
+ // ---------------------------------------------------------------------------
584
+ // Export
585
+ // ---------------------------------------------------------------------------
586
+
587
+ export async function exportMemoryGraph(
588
+ store: Pick<MemoryStore, "list">,
589
+ options?: GraphExportOptions,
590
+ ): Promise<{ path: string; graph: MemoryGraph }> {
591
+ const graph = await buildMemoryGraph(store, options);
592
+ const html = renderGraphHTML(graph);
593
+
594
+ const outputDir = options?.outputPath
595
+ ? resolve(options.outputPath, "..")
596
+ : resolve(metaDir(import.meta), "../data/exports");
597
+
598
+ if (!existsSync(outputDir)) {
599
+ mkdirSync(outputDir, { recursive: true });
600
+ }
601
+
602
+ const filePath = options?.outputPath
603
+ ?? join(outputDir, `memory-graph-${Date.now()}.html`);
604
+
605
+ writeFileSync(filePath, html, "utf-8");
606
+
607
+ return { path: filePath, graph };
608
+ }
609
+
610
+ // ---------------------------------------------------------------------------
611
+ // Format Result
612
+ // ---------------------------------------------------------------------------
613
+
614
+ export function formatGraphExportResult(path: string, graph: MemoryGraph): string {
615
+ // Count categories
616
+ const catCounts = new Map<string, number>();
617
+ for (const node of graph.nodes) {
618
+ catCounts.set(node.category, (catCounts.get(node.category) ?? 0) + 1);
619
+ }
620
+ const catSummary = Array.from(catCounts.entries())
621
+ .sort((a, b) => b[1] - a[1])
622
+ .map(([cat, count]) => `${cat}(${count})`)
623
+ .join(", ");
624
+
625
+ // Count edge types
626
+ const edgeTypeCounts = new Map<string, number>();
627
+ for (const edge of graph.edges) {
628
+ edgeTypeCounts.set(edge.type, (edgeTypeCounts.get(edge.type) ?? 0) + 1);
629
+ }
630
+ const semanticCount = edgeTypeCounts.get("semantic") ?? 0;
631
+ const narrativeCount = edgeTypeCounts.get("narrative") ?? 0;
632
+ const edgeDetails: string[] = [];
633
+ if (semanticCount > 0) edgeDetails.push(`${semanticCount} semantic`);
634
+ if (narrativeCount > 0) edgeDetails.push(`${narrativeCount} narrative`);
635
+ const edgeSummary = edgeDetails.length > 0
636
+ ? ` (${edgeDetails.join(", ")})`
637
+ : "";
638
+
639
+ return [
640
+ `Knowledge Graph exported: ${path}`,
641
+ `Nodes: ${graph.nodes.length} | Edges: ${graph.edges.length}${edgeSummary}`,
642
+ `Categories: ${catSummary || "none"}`,
643
+ ].join("\n");
644
+ }