@pencil-agent/nano-mem 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/CLAUDE.md +258 -0
  2. package/README.md +146 -0
  3. package/dist/cli.d.ts +8 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +90 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/config.d.ts +46 -0
  8. package/dist/config.d.ts.map +1 -0
  9. package/dist/config.js +48 -0
  10. package/dist/config.js.map +1 -0
  11. package/dist/consolidation.d.ts +13 -0
  12. package/dist/consolidation.d.ts.map +1 -0
  13. package/dist/consolidation.js +111 -0
  14. package/dist/consolidation.js.map +1 -0
  15. package/dist/engine.d.ts +67 -0
  16. package/dist/engine.d.ts.map +1 -0
  17. package/dist/engine.js +492 -0
  18. package/dist/engine.js.map +1 -0
  19. package/dist/eviction.d.ts +16 -0
  20. package/dist/eviction.d.ts.map +1 -0
  21. package/dist/eviction.js +22 -0
  22. package/dist/eviction.js.map +1 -0
  23. package/dist/extension.d.ts +11 -0
  24. package/dist/extension.d.ts.map +1 -0
  25. package/dist/extension.js +264 -0
  26. package/dist/extension.js.map +1 -0
  27. package/dist/extraction.d.ts +10 -0
  28. package/dist/extraction.d.ts.map +1 -0
  29. package/dist/extraction.js +136 -0
  30. package/dist/extraction.js.map +1 -0
  31. package/dist/full-insights-html.d.ts +8 -0
  32. package/dist/full-insights-html.d.ts.map +1 -0
  33. package/dist/full-insights-html.js +311 -0
  34. package/dist/full-insights-html.js.map +1 -0
  35. package/dist/full-insights.d.ts +21 -0
  36. package/dist/full-insights.d.ts.map +1 -0
  37. package/dist/full-insights.js +327 -0
  38. package/dist/full-insights.js.map +1 -0
  39. package/dist/i18n.d.ts +50 -0
  40. package/dist/i18n.d.ts.map +1 -0
  41. package/dist/i18n.js +169 -0
  42. package/dist/i18n.js.map +1 -0
  43. package/dist/index.d.ts +18 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +14 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/insights-html.d.ts +8 -0
  48. package/dist/insights-html.d.ts.map +1 -0
  49. package/dist/insights-html.js +431 -0
  50. package/dist/insights-html.js.map +1 -0
  51. package/dist/linking.d.ts +11 -0
  52. package/dist/linking.d.ts.map +1 -0
  53. package/dist/linking.js +40 -0
  54. package/dist/linking.js.map +1 -0
  55. package/dist/privacy.d.ts +16 -0
  56. package/dist/privacy.d.ts.map +1 -0
  57. package/dist/privacy.js +52 -0
  58. package/dist/privacy.js.map +1 -0
  59. package/dist/scoring.d.ts +25 -0
  60. package/dist/scoring.d.ts.map +1 -0
  61. package/dist/scoring.js +63 -0
  62. package/dist/scoring.js.map +1 -0
  63. package/dist/store.d.ts +16 -0
  64. package/dist/store.d.ts.map +1 -0
  65. package/dist/store.js +68 -0
  66. package/dist/store.js.map +1 -0
  67. package/dist/types.d.ts +191 -0
  68. package/dist/types.d.ts.map +1 -0
  69. package/dist/types.js +7 -0
  70. package/dist/types.js.map +1 -0
  71. package/dist/update.d.ts +14 -0
  72. package/dist/update.d.ts.map +1 -0
  73. package/dist/update.js +126 -0
  74. package/dist/update.js.map +1 -0
  75. package/package.json +60 -0
  76. package/src/cli.ts +99 -0
  77. package/src/config.ts +72 -0
  78. package/src/consolidation.ts +127 -0
  79. package/src/engine.ts +699 -0
  80. package/src/eviction.ts +30 -0
  81. package/src/extension.ts +290 -0
  82. package/src/extraction.ts +152 -0
  83. package/src/full-insights-html.ts +342 -0
  84. package/src/full-insights.ts +396 -0
  85. package/src/i18n.ts +233 -0
  86. package/src/index.ts +50 -0
  87. package/src/insights-html.ts +476 -0
  88. package/src/linking.ts +43 -0
  89. package/src/privacy.ts +52 -0
  90. package/src/scoring.ts +94 -0
  91. package/src/store.ts +84 -0
  92. package/src/types.ts +209 -0
  93. package/src/update.ts +141 -0
@@ -0,0 +1,476 @@
1
+ /**
2
+ * [INPUT]: InsightsReport, locale
3
+ * [OUTPUT]: Standalone HTML report
4
+ * [POS]: Pure renderer, no side effects
5
+ */
6
+
7
+ import { PROMPTS } from "./i18n.js";
8
+ import type { InsightsReport, MemoryEntry, PatternInsight, StruggleInsight } from "./types.js";
9
+
10
+ type UiText = {
11
+ subtitle: string;
12
+ atAGlance: string;
13
+ whatsWorking: string;
14
+ whatsHindering: string;
15
+ quickWins: string;
16
+ focusArea: string;
17
+ noneYet: string;
18
+ workOn: string;
19
+ workOnIntro: string;
20
+ sessionWord: string;
21
+ distribution: string;
22
+ topProjects: string;
23
+ frequentTags: string;
24
+ successes: string;
25
+ successesIntro: string;
26
+ frictions: string;
27
+ frictionsIntro: string;
28
+ details: string;
29
+ weight: string;
30
+ access: string;
31
+ importance: string;
32
+ attempts: string;
33
+ solution: string;
34
+ status: string;
35
+ resolved: string;
36
+ unresolved: string;
37
+ knowledgePrefs: string;
38
+ lessons: string;
39
+ knowledge: string;
40
+ preferences: string;
41
+ generatedBy: string;
42
+ };
43
+
44
+ const UI: Record<"en" | "zh", UiText> = {
45
+ en: {
46
+ subtitle: "sessions analyzed",
47
+ atAGlance: "At a Glance",
48
+ whatsWorking: "What's Working",
49
+ whatsHindering: "What's Hindering",
50
+ quickWins: "Quick Wins",
51
+ focusArea: "Focus Area",
52
+ noneYet: "No strong signal yet.",
53
+ workOn: "What You Work On",
54
+ workOnIntro: "Top project areas inferred from your memory graph.",
55
+ sessionWord: "sessions",
56
+ distribution: "Focus Distribution",
57
+ topProjects: "Top Projects",
58
+ frequentTags: "Frequent Tags",
59
+ successes: "Impressive Things You Solved",
60
+ successesIntro: "Resolved struggles and high-value lessons worth repeating.",
61
+ frictions: "Where Things Still Go Wrong",
62
+ frictionsIntro: "Open struggles that are still costing time.",
63
+ details: "Details",
64
+ weight: "Weight",
65
+ access: "Access",
66
+ importance: "Importance",
67
+ attempts: "Attempts",
68
+ solution: "Solution",
69
+ status: "Status",
70
+ resolved: "resolved",
71
+ unresolved: "open",
72
+ knowledgePrefs: "Knowledge and Preferences",
73
+ lessons: "Top Lessons",
74
+ knowledge: "Knowledge Base",
75
+ preferences: "User Preferences",
76
+ generatedBy: "Generated by NanoMem",
77
+ },
78
+ zh: {
79
+ subtitle: "已分析会话",
80
+ atAGlance: "总览",
81
+ whatsWorking: "做得好的地方",
82
+ whatsHindering: "当前阻碍",
83
+ quickWins: "快速改进",
84
+ focusArea: "主要方向",
85
+ noneYet: "暂时没有明显信号。",
86
+ workOn: "你在做什么",
87
+ workOnIntro: "基于记忆图谱推断的项目重点领域。",
88
+ sessionWord: "次会话",
89
+ distribution: "关注分布",
90
+ topProjects: "主要项目",
91
+ frequentTags: "高频标签",
92
+ successes: "你解决得很好的事",
93
+ successesIntro: "已解决问题与高价值经验,建议重复复用。",
94
+ frictions: "仍在反复消耗的点",
95
+ frictionsIntro: "尚未关闭的问题,持续影响效率。",
96
+ details: "详情",
97
+ weight: "权重",
98
+ access: "访问",
99
+ importance: "重要度",
100
+ attempts: "尝试",
101
+ solution: "解法",
102
+ status: "状态",
103
+ resolved: "已解决",
104
+ unresolved: "待处理",
105
+ knowledgePrefs: "知识与偏好",
106
+ lessons: "关键经验",
107
+ knowledge: "知识库",
108
+ preferences: "用户偏好",
109
+ generatedBy: "由 NanoMem 自动生成",
110
+ },
111
+ };
112
+
113
+ function escapeHtml(str: string): string {
114
+ return str
115
+ .replace(/&/g, "&amp;")
116
+ .replace(/</g, "&lt;")
117
+ .replace(/>/g, "&gt;")
118
+ .replace(/"/g, "&quot;")
119
+ .replace(/'/g, "&#039;");
120
+ }
121
+
122
+ function formatDate(iso: string, locale: string): string {
123
+ const d = new Date(iso);
124
+ if (Number.isNaN(d.getTime())) return iso;
125
+ return d.toLocaleString(locale === "zh" ? "zh-CN" : "en-US");
126
+ }
127
+
128
+ function dedupeById(entries: MemoryEntry[]): MemoryEntry[] {
129
+ const map = new Map<string, MemoryEntry>();
130
+ for (const entry of entries) map.set(entry.id, entry);
131
+ return [...map.values()];
132
+ }
133
+
134
+ function topTags(entries: MemoryEntry[], limit = 8): Array<{ label: string; value: number }> {
135
+ const counts = new Map<string, number>();
136
+ for (const entry of entries) {
137
+ for (const tag of entry.tags ?? []) counts.set(tag, (counts.get(tag) ?? 0) + 1);
138
+ }
139
+ return [...counts.entries()]
140
+ .map(([label, value]) => ({ label, value }))
141
+ .sort((a, b) => b.value - a.value)
142
+ .slice(0, limit);
143
+ }
144
+
145
+ function topProjects(entries: MemoryEntry[], limit = 6): Array<{ label: string; value: number }> {
146
+ const counts = new Map<string, number>();
147
+ for (const entry of entries) {
148
+ if (!entry.project) continue;
149
+ counts.set(entry.project, (counts.get(entry.project) ?? 0) + 1);
150
+ }
151
+ return [...counts.entries()]
152
+ .map(([label, value]) => ({ label, value }))
153
+ .sort((a, b) => b.value - a.value)
154
+ .slice(0, limit);
155
+ }
156
+
157
+ function renderBarRows(rows: Array<{ label: string; value: number }>, colorClass: string, empty: string): string {
158
+ if (!rows.length) return `<p class="empty">${escapeHtml(empty)}</p>`;
159
+ const max = Math.max(...rows.map((row) => row.value), 1);
160
+ return rows
161
+ .map((row) => {
162
+ const width = Math.max(8, Math.round((row.value / max) * 100));
163
+ return `<div class="bar-row">
164
+ <div class="bar-label" title="${escapeHtml(row.label)}">${escapeHtml(row.label)}</div>
165
+ <div class="bar-track"><div class="bar-fill ${colorClass}" style="width:${width}%"></div></div>
166
+ <div class="bar-value">${row.value}</div>
167
+ </div>`;
168
+ })
169
+ .join("");
170
+ }
171
+
172
+ function renderProjectAreas(projects: Array<{ label: string; value: number }>, entries: MemoryEntry[], ui: UiText): string {
173
+ if (!projects.length) return `<p class="empty">${escapeHtml(ui.noneYet)}</p>`;
174
+ return projects
175
+ .map((project) => {
176
+ const scopeEntries = entries.filter((entry) => entry.project === project.label);
177
+ const tags = topTags(scopeEntries, 4).map((item) => item.label);
178
+ const desc =
179
+ tags.length > 0
180
+ ? `Top signals: ${tags.map((tag) => `#${escapeHtml(tag)}`).join(" ")}`
181
+ : "No dominant tag signal yet.";
182
+ return `<article class="project-area">
183
+ <div class="area-header">
184
+ <span class="area-name">${escapeHtml(project.label)}</span>
185
+ <span class="area-count">~${project.value} ${escapeHtml(ui.sessionWord)}</span>
186
+ </div>
187
+ <p class="area-desc">${desc}</p>
188
+ </article>`;
189
+ })
190
+ .join("");
191
+ }
192
+
193
+ function renderPatternRows(patterns: PatternInsight[], ui: UiText): string {
194
+ if (!patterns.length) return `<p class="empty">${escapeHtml(ui.noneYet)}</p>`;
195
+ const maxWeight = Math.max(...patterns.map((item) => item.weight), 1);
196
+ return patterns
197
+ .slice(0, 12)
198
+ .map((item) => {
199
+ const width = Math.max(8, Math.round((item.weight / maxWeight) * 100));
200
+ return `<article class="item-card">
201
+ <div class="item-title"><strong>${escapeHtml(item.trigger)}</strong> <span class="arrow">-></span> ${escapeHtml(item.behavior)}</div>
202
+ <div class="weight-track"><div class="weight-fill" style="width:${width}%"></div></div>
203
+ <div class="meta-row">
204
+ <span>${escapeHtml(ui.weight)}: ${item.weight.toFixed(2)}</span>
205
+ <span>${escapeHtml(ui.access)}: ${item.entry.accessCount}</span>
206
+ <span>${escapeHtml(ui.importance)}: ${item.entry.importance}</span>
207
+ </div>
208
+ </article>`;
209
+ })
210
+ .join("");
211
+ }
212
+
213
+ function renderStruggleRows(struggles: StruggleInsight[], ui: UiText): string {
214
+ if (!struggles.length) return `<p class="empty">${escapeHtml(ui.noneYet)}</p>`;
215
+ return struggles
216
+ .slice(0, 12)
217
+ .map((item) => {
218
+ const attempts = item.attempts.length
219
+ ? `<div class="sub-line"><strong>${escapeHtml(ui.attempts)}:</strong> ${item.attempts.map((a) => escapeHtml(a)).join(" | ")}</div>`
220
+ : "";
221
+ const solution = item.solution
222
+ ? `<div class="sub-line"><strong>${escapeHtml(ui.solution)}:</strong> ${escapeHtml(item.solution)}</div>`
223
+ : "";
224
+ return `<article class="item-card ${item.resolved ? "ok" : "warn"}">
225
+ <div class="item-title">${escapeHtml(item.problem)}</div>
226
+ ${attempts}
227
+ ${solution}
228
+ <div class="meta-row">
229
+ <span>${escapeHtml(ui.status)}: ${item.resolved ? escapeHtml(ui.resolved) : escapeHtml(ui.unresolved)}</span>
230
+ <span>${escapeHtml(ui.weight)}: ${item.weight.toFixed(2)}</span>
231
+ </div>
232
+ </article>`;
233
+ })
234
+ .join("");
235
+ }
236
+
237
+ function renderMemoryList(entries: MemoryEntry[], empty: string): string {
238
+ if (!entries.length) return `<p class="empty">${escapeHtml(empty)}</p>`;
239
+ return `<ul class="list">${entries
240
+ .map((entry) => `<li>${escapeHtml(entry.content)}${entry.tags.length ? ` <span class="tags">${entry.tags.slice(0, 4).map((tag) => `#${escapeHtml(tag)}`).join(" ")}</span>` : ""}</li>`)
241
+ .join("")}</ul>`;
242
+ }
243
+
244
+ function renderAtAGlance(report: InsightsReport, projects: Array<{ label: string; value: number }>, ui: UiText): string {
245
+ const unresolved = report.struggles.filter((item) => !item.resolved);
246
+ const resolved = report.struggles.filter((item) => item.resolved);
247
+ const topPattern = report.patterns[0];
248
+ const topRec = report.recommendations[0];
249
+ const topProject = projects[0];
250
+ const working =
251
+ resolved.length > 0
252
+ ? `${resolved.length} struggles are already resolved; keep reusing those fixes.`
253
+ : report.topLessons.length
254
+ ? `${report.topLessons.length} lessons captured; convert the top ones into repeatable checklists.`
255
+ : ui.noneYet;
256
+ const hindering = unresolved.length
257
+ ? `${unresolved.length} open struggles remain. Most frequent: "${unresolved[0]?.problem ?? ""}".`
258
+ : "No unresolved struggles currently visible.";
259
+ const quickWins = topRec ? topRec : ui.noneYet;
260
+ const focus = topProject
261
+ ? `${topProject.label} appears most often (${topProject.value} memories).`
262
+ : topPattern
263
+ ? `Dominant behavior: when "${topPattern.trigger}", you often "${topPattern.behavior}".`
264
+ : ui.noneYet;
265
+ return `<section class="glance">
266
+ <h2>${escapeHtml(ui.atAGlance)}</h2>
267
+ <div class="glance-grid">
268
+ <article class="glance-card">
269
+ <h3>${escapeHtml(ui.whatsWorking)}</h3>
270
+ <p>${escapeHtml(working)}</p>
271
+ </article>
272
+ <article class="glance-card warn">
273
+ <h3>${escapeHtml(ui.whatsHindering)}</h3>
274
+ <p>${escapeHtml(hindering)}</p>
275
+ </article>
276
+ <article class="glance-card">
277
+ <h3>${escapeHtml(ui.quickWins)}</h3>
278
+ <p>${escapeHtml(quickWins)}</p>
279
+ </article>
280
+ <article class="glance-card">
281
+ <h3>${escapeHtml(ui.focusArea)}</h3>
282
+ <p>${escapeHtml(focus)}</p>
283
+ </article>
284
+ </div>
285
+ </section>`;
286
+ }
287
+
288
+ function renderRecommendations(report: InsightsReport, title: string, empty: string): string {
289
+ if (!report.recommendations.length) return `<section class="section"><h2>${escapeHtml(title)}</h2><p class="empty">${escapeHtml(empty)}</p></section>`;
290
+ return `<section id="section-recommendations" class="section">
291
+ <h2>${escapeHtml(title)}</h2>
292
+ <ul class="recommend-list">${report.recommendations.map((rec) => `<li>${escapeHtml(rec)}</li>`).join("")}</ul>
293
+ </section>`;
294
+ }
295
+
296
+ export function renderInsightsHtml(report: InsightsReport, locale: string): string {
297
+ const isZh = locale === "zh";
298
+ const ui = UI[isZh ? "zh" : "en"];
299
+ const prompts = PROMPTS[locale] ?? PROMPTS.en;
300
+
301
+ const allEntries = dedupeById([...report.topKnowledge, ...report.topLessons, ...report.preferences]);
302
+ const projects = topProjects(allEntries);
303
+ const tags = topTags(allEntries);
304
+ const unresolved = report.struggles.filter((item) => !item.resolved);
305
+ const resolved = report.struggles.filter((item) => item.resolved);
306
+
307
+ const statsRow = [
308
+ { label: "Sessions", value: report.stats.totalSessions },
309
+ { label: "Knowledge", value: report.stats.knowledge },
310
+ { label: "Lessons", value: report.stats.lessons },
311
+ { label: "Preferences", value: report.stats.preferences },
312
+ { label: "Struggles", value: report.struggles.length },
313
+ { label: "Patterns", value: report.patterns.length },
314
+ ];
315
+
316
+ const css = `
317
+ *{box-sizing:border-box}
318
+ body{margin:0;padding:48px 24px;font-family:Inter,"Segoe UI",Arial,sans-serif;background:#f8fafc;color:#334155;line-height:1.65}
319
+ .container{max-width:960px;margin:0 auto}
320
+ h1{font-size:34px;font-weight:700;color:#0f172a;margin:0 0 8px}
321
+ h2{font-size:22px;font-weight:700;color:#0f172a;margin:0 0 14px}
322
+ h3{font-size:15px;font-weight:700;color:#1e293b;margin:0 0 8px}
323
+ .subtitle{color:#64748b;font-size:14px;margin:0 0 24px}
324
+ .toc{display:flex;flex-wrap:wrap;gap:8px;padding:14px;background:#fff;border:1px solid #e2e8f0;border-radius:10px;margin:0 0 24px}
325
+ .toc a{font-size:12px;color:#475569;background:#f1f5f9;border-radius:6px;padding:6px 10px;text-decoration:none}
326
+ .toc a:hover{background:#e2e8f0}
327
+ .stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:12px;padding:20px 0;border-top:1px solid #e2e8f0;border-bottom:1px solid #e2e8f0;margin:0 0 28px}
328
+ .stat{text-align:center}
329
+ .stat-value{font-size:24px;font-weight:700;color:#0f172a}
330
+ .stat-label{font-size:11px;color:#64748b;text-transform:uppercase}
331
+ .glance{background:linear-gradient(135deg,#fef3c7 0%,#fde68a 100%);border:1px solid #f59e0b;border-radius:12px;padding:20px 22px;margin:0 0 28px}
332
+ .glance-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:10px}
333
+ .glance-card{background:rgba(255,255,255,.62);border:1px solid rgba(245,158,11,.28);border-radius:8px;padding:12px}
334
+ .glance-card.warn{background:rgba(255,241,242,.82);border-color:#fca5a5}
335
+ .glance-card p{margin:0;font-size:13px;color:#78350f}
336
+ .section{background:#fff;border:1px solid #e2e8f0;border-radius:10px;padding:18px;margin:0 0 16px}
337
+ .section-intro{font-size:13px;color:#64748b;margin:0 0 12px}
338
+ .project-list{display:flex;flex-direction:column;gap:10px}
339
+ .project-area{background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px;padding:14px}
340
+ .area-header{display:flex;justify-content:space-between;align-items:center;gap:10px}
341
+ .area-name{font-size:15px;font-weight:700;color:#0f172a}
342
+ .area-count{font-size:12px;color:#64748b;background:#f1f5f9;padding:2px 8px;border-radius:4px}
343
+ .area-desc{margin:8px 0 0;font-size:13px;color:#475569}
344
+ .charts{display:grid;grid-template-columns:1fr 1fr;gap:16px}
345
+ .chart{background:#fff;border:1px solid #e2e8f0;border-radius:8px;padding:14px}
346
+ .chart-title{font-size:12px;color:#64748b;text-transform:uppercase;margin:0 0 10px}
347
+ .bar-row{display:flex;align-items:center;gap:8px;margin-bottom:7px}
348
+ .bar-label{width:120px;flex-shrink:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12px;color:#475569}
349
+ .bar-track{flex:1;height:7px;background:#f1f5f9;border-radius:99px}
350
+ .bar-fill{height:100%;border-radius:99px}
351
+ .bar-fill.blue{background:#2563eb}
352
+ .bar-fill.cyan{background:#0891b2}
353
+ .bar-value{width:24px;text-align:right;font-size:12px;color:#64748b}
354
+ .recommend-list{margin:0;padding:0;list-style:none;display:flex;flex-direction:column;gap:8px}
355
+ .recommend-list li{background:#f0fdf4;border:1px solid #bbf7d0;border-left:4px solid #16a34a;border-radius:8px;padding:11px 12px;font-size:14px}
356
+ .item-grid{display:flex;flex-direction:column;gap:10px}
357
+ .item-card{background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px;padding:12px}
358
+ .item-card.ok{background:#f0fdf4;border-color:#86efac}
359
+ .item-card.warn{background:#fef2f2;border-color:#fca5a5}
360
+ .item-title{font-size:14px;color:#0f172a}
361
+ .arrow{color:#94a3b8}
362
+ .weight-track{margin-top:8px;height:6px;background:#e2e8f0;border-radius:99px}
363
+ .weight-fill{height:100%;background:#2563eb;border-radius:99px}
364
+ .sub-line{margin-top:8px;font-size:13px;color:#334155}
365
+ .meta-row{display:flex;gap:12px;flex-wrap:wrap;margin-top:8px;font-size:12px;color:#64748b}
366
+ details{border:1px solid #e2e8f0;border-radius:8px;background:#f8fafc;padding:10px 12px;margin-bottom:10px}
367
+ summary{cursor:pointer;font-size:14px;font-weight:700;color:#334155}
368
+ .list{margin:10px 0 0;padding-left:18px}
369
+ .list li{margin:0 0 8px;font-size:13px;color:#334155}
370
+ .tags{font-size:12px;color:#64748b}
371
+ .empty{font-size:13px;color:#94a3b8}
372
+ footer{margin-top:22px;text-align:center;font-size:12px;color:#94a3b8}
373
+ @media (max-width:760px){body{padding:28px 14px}.charts{grid-template-columns:1fr}.bar-label{width:96px}}
374
+ `;
375
+
376
+ return `<!doctype html>
377
+ <html lang="${isZh ? "zh-CN" : "en"}">
378
+ <head>
379
+ <meta charset="utf-8" />
380
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
381
+ <title>${escapeHtml(prompts.insightsTitle)}</title>
382
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
383
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
384
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
385
+ <style>${css}</style>
386
+ </head>
387
+ <body>
388
+ <main class="container">
389
+ <h1>${escapeHtml(prompts.insightsTitle)}</h1>
390
+ <p class="subtitle">${report.stats.totalSessions} ${escapeHtml(ui.subtitle)} | ${escapeHtml(prompts.insightsGeneratedAt)}: ${escapeHtml(formatDate(report.generatedAt, locale))}</p>
391
+
392
+ <nav class="toc">
393
+ <a href="#section-work">${escapeHtml(ui.workOn)}</a>
394
+ <a href="#section-distribution">${escapeHtml(ui.distribution)}</a>
395
+ <a href="#section-recommendations">${escapeHtml(prompts.insightsSectionRecommendations)}</a>
396
+ <a href="#section-patterns">${escapeHtml(prompts.insightsSectionPatterns)}</a>
397
+ <a href="#section-struggles">${escapeHtml(prompts.insightsSectionStruggles)}</a>
398
+ <a href="#section-memory">${escapeHtml(ui.knowledgePrefs)}</a>
399
+ </nav>
400
+
401
+ <section class="stats">
402
+ ${statsRow
403
+ .map((item) => `<div class="stat"><div class="stat-value">${item.value}</div><div class="stat-label">${escapeHtml(item.label)}</div></div>`)
404
+ .join("")}
405
+ </section>
406
+
407
+ ${renderAtAGlance(report, projects, ui)}
408
+
409
+ <section id="section-work" class="section">
410
+ <h2>${escapeHtml(ui.workOn)}</h2>
411
+ <p class="section-intro">${escapeHtml(ui.workOnIntro)}</p>
412
+ <div class="project-list">${renderProjectAreas(projects, allEntries, ui)}</div>
413
+ </section>
414
+
415
+ <section id="section-distribution" class="section">
416
+ <h2>${escapeHtml(ui.distribution)}</h2>
417
+ <div class="charts">
418
+ <div class="chart">
419
+ <p class="chart-title">${escapeHtml(ui.topProjects)}</p>
420
+ ${renderBarRows(projects, "blue", ui.noneYet)}
421
+ </div>
422
+ <div class="chart">
423
+ <p class="chart-title">${escapeHtml(ui.frequentTags)}</p>
424
+ ${renderBarRows(tags, "cyan", ui.noneYet)}
425
+ </div>
426
+ </div>
427
+ </section>
428
+
429
+ ${renderRecommendations(report, prompts.insightsSectionRecommendations, prompts.insightsNoData)}
430
+
431
+ <section class="section">
432
+ <h2>${escapeHtml(ui.successes)}</h2>
433
+ <p class="section-intro">${escapeHtml(ui.successesIntro)}</p>
434
+ <div class="item-grid">
435
+ ${renderStruggleRows(resolved, ui)}
436
+ ${renderMemoryList(report.topLessons.slice(0, 5), ui.noneYet)}
437
+ </div>
438
+ </section>
439
+
440
+ <section class="section">
441
+ <h2>${escapeHtml(ui.frictions)}</h2>
442
+ <p class="section-intro">${escapeHtml(ui.frictionsIntro)}</p>
443
+ <div class="item-grid">${renderStruggleRows(unresolved, ui)}</div>
444
+ </section>
445
+
446
+ <section id="section-patterns" class="section">
447
+ <h2>${escapeHtml(prompts.insightsSectionPatterns)}</h2>
448
+ <div class="item-grid">${renderPatternRows(report.patterns, ui)}</div>
449
+ </section>
450
+
451
+ <section id="section-struggles" class="section">
452
+ <h2>${escapeHtml(prompts.insightsSectionStruggles)}</h2>
453
+ <div class="item-grid">${renderStruggleRows(report.struggles, ui)}</div>
454
+ </section>
455
+
456
+ <section id="section-memory" class="section">
457
+ <h2>${escapeHtml(ui.knowledgePrefs)}</h2>
458
+ <details open>
459
+ <summary>${escapeHtml(ui.lessons)} (${report.topLessons.length})</summary>
460
+ ${renderMemoryList(report.topLessons.slice(0, 12), ui.noneYet)}
461
+ </details>
462
+ <details>
463
+ <summary>${escapeHtml(ui.knowledge)} (${report.topKnowledge.length})</summary>
464
+ ${renderMemoryList(report.topKnowledge.slice(0, 12), ui.noneYet)}
465
+ </details>
466
+ <details>
467
+ <summary>${escapeHtml(ui.preferences)} (${report.preferences.length})</summary>
468
+ ${renderMemoryList(report.preferences.slice(0, 12), ui.noneYet)}
469
+ </details>
470
+ </section>
471
+
472
+ <footer>${escapeHtml(ui.generatedBy)}</footer>
473
+ </main>
474
+ </body>
475
+ </html>`;
476
+ }
package/src/linking.ts ADDED
@@ -0,0 +1,43 @@
1
+ /**
2
+ * [INPUT]: new MemoryEntry, existing entries
3
+ * [OUTPUT]: bidirectional relatedIds links established
4
+ * [POS]: A-MEM style Zettelkasten linking — atomic storage + dynamic associations
5
+ */
6
+
7
+ import { tagOverlap } from "./scoring.js";
8
+ import type { MemoryEntry } from "./types.js";
9
+
10
+ const LINK_THRESHOLD = 0.5;
11
+ const MAX_LINKS = 5;
12
+
13
+ /** Find related entries by tag overlap and establish bidirectional links */
14
+ export function linkNewEntry(newEntry: MemoryEntry, allEntries: MemoryEntry[]): void {
15
+ if (!newEntry.relatedIds) newEntry.relatedIds = [];
16
+
17
+ const candidates = allEntries
18
+ .filter((e) => e.id !== newEntry.id)
19
+ .map((e) => ({ entry: e, overlap: tagOverlap(newEntry.tags, e.tags) }))
20
+ .filter((c) => c.overlap >= LINK_THRESHOLD)
21
+ .sort((a, b) => b.overlap - a.overlap)
22
+ .slice(0, MAX_LINKS);
23
+
24
+ for (const { entry } of candidates) {
25
+ if (!newEntry.relatedIds.includes(entry.id)) {
26
+ newEntry.relatedIds.push(entry.id);
27
+ }
28
+ if (!entry.relatedIds) entry.relatedIds = [];
29
+ if (!entry.relatedIds.includes(newEntry.id)) {
30
+ entry.relatedIds.push(newEntry.id);
31
+ }
32
+ }
33
+ }
34
+
35
+ /** Get content summaries for related entries (for injection enrichment) */
36
+ export function getRelatedSummaries(entry: MemoryEntry, allEntries: MemoryEntry[], maxCount = 3): string[] {
37
+ if (!entry.relatedIds?.length) return [];
38
+ const idSet = new Set(entry.relatedIds);
39
+ return allEntries
40
+ .filter((e) => idSet.has(e.id))
41
+ .slice(0, maxCount)
42
+ .map((e) => e.content.slice(0, 80));
43
+ }
package/src/privacy.ts ADDED
@@ -0,0 +1,52 @@
1
+ /**
2
+ * [INPUT]: text content, MemoryEntry with scope/ttl
3
+ * [OUTPUT]: sanitized text, scope-filtered entries, ttl-expired entries removed
4
+ * [POS]: MemoryOps layer — scope isolation, TTL enforcement, PII filtering
5
+ */
6
+
7
+ import { daysSince } from "./scoring.js";
8
+ import type { MemoryEntry, MemoryScope, WorkEntry } from "./types.js";
9
+
10
+ const PII_PATTERNS = [
11
+ /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g, // email
12
+ /\b(?:\+?1[-.]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b/g, // US phone
13
+ /\b1[3-9]\d{9}\b/g, // CN mobile
14
+ /\b\d{3}-\d{2}-\d{4}\b/g, // US SSN
15
+ /\b[1-9]\d{5}(?:19|20)\d{2}(?:0[1-9]|1[0-2])(?:0[1-9]|[12]\d|3[01])\d{3}[\dXx]\b/g, // CN ID
16
+ /\b(?:4\d{3}|5[1-5]\d{2}|6011|3[47]\d{2})[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b/g, // credit card
17
+ ];
18
+
19
+ export function filterPII(text: string): string {
20
+ let result = text;
21
+ for (const pattern of PII_PATTERNS) {
22
+ result = result.replace(pattern, "[REDACTED]");
23
+ }
24
+ return result;
25
+ }
26
+
27
+ export function matchesScope(entry: { scope?: MemoryScope }, required?: MemoryScope): boolean {
28
+ if (!required) return true;
29
+ if (!entry.scope) return true;
30
+ if (required.userId && entry.scope.userId && entry.scope.userId !== required.userId) return false;
31
+ if (required.agentId && entry.scope.agentId && entry.scope.agentId !== required.agentId) return false;
32
+ return true;
33
+ }
34
+
35
+ export function filterByScope<T extends { scope?: MemoryScope }>(entries: T[], scope?: MemoryScope): T[] {
36
+ if (!scope) return entries;
37
+ return entries.filter((e) => matchesScope(e, scope));
38
+ }
39
+
40
+ export function evictExpiredEntries(entries: MemoryEntry[]): MemoryEntry[] {
41
+ return entries.filter((e) => {
42
+ if (e.ttl === undefined) return true;
43
+ return daysSince(e.created) < e.ttl;
44
+ });
45
+ }
46
+
47
+ export function evictExpiredWork(entries: WorkEntry[]): WorkEntry[] {
48
+ return entries.filter((w) => {
49
+ if (w.ttl === undefined) return true;
50
+ return daysSince(w.created) < w.ttl;
51
+ });
52
+ }
package/src/scoring.ts ADDED
@@ -0,0 +1,94 @@
1
+ /**
2
+ * [INPUT]: MemoryEntry/Episode/WorkEntry, project, context tags, config weights
3
+ * [OUTPUT]: retrieval score (Stanford: Recency + Importance + Relevance)
4
+ * [POS]: Core ranking algorithm — used by engine for injection budget allocation
5
+ *
6
+ * Key innovation: uses per-entry adaptive strength (not global half-life)
7
+ * for Recency, implementing Ebbinghaus spaced repetition.
8
+ */
9
+
10
+ import type { Episode, MemoryEntry, WorkEntry } from "./types.js";
11
+
12
+ export function daysSince(iso: string): number {
13
+ return Math.max(0, (Date.now() - new Date(iso).getTime()) / 86_400_000);
14
+ }
15
+
16
+ /** Ebbinghaus decay: R = e^(-t * ln2 / S) where S = per-entry strength */
17
+ export function decay(ageDays: number, strength: number): number {
18
+ return Math.exp((-ageDays * Math.LN2) / Math.max(1, strength));
19
+ }
20
+
21
+ export function extractTags(text: string): string[] {
22
+ return [...new Set(text.toLowerCase().match(/[a-z0-9\u4e00-\u9fff_.-]{2,}/g) || [])].slice(0, 30);
23
+ }
24
+
25
+ export function tagOverlap(a: string[], b: string[]): number {
26
+ if (!a.length || !b.length) return 0;
27
+ const setB = new Set(b);
28
+ return a.filter((t) => setB.has(t)).length / Math.max(a.length, b.length);
29
+ }
30
+
31
+ export interface ScoreWeights {
32
+ recency: number;
33
+ importance: number;
34
+ relevance: number;
35
+ }
36
+
37
+ export function scoreEntry(
38
+ e: MemoryEntry,
39
+ project: string,
40
+ ctx: string[],
41
+ defaultHalfLife: Record<string, number>,
42
+ weights: ScoreWeights,
43
+ ): number {
44
+ const strength = e.strength || defaultHalfLife[e.type] || 30;
45
+ const recency = decay(daysSince(e.created), strength);
46
+ const importanceNorm = Math.min(1, e.importance / 10);
47
+ const projectMatch = e.project === project ? 1 : 0.5;
48
+ const relevance = projectMatch * (0.3 + 0.7 * tagOverlap(e.tags, ctx));
49
+ return weights.recency * recency + weights.importance * importanceNorm + weights.relevance * relevance;
50
+ }
51
+
52
+ export function scoreEpisode(
53
+ ep: Episode,
54
+ project: string,
55
+ ctx: string[],
56
+ defaultHalfLife: Record<string, number>,
57
+ weights: ScoreWeights,
58
+ ): number {
59
+ const strength = defaultHalfLife.episode || 14;
60
+ const recency = decay(daysSince(ep.date), strength);
61
+ const importanceNorm = Math.min(1, ep.importance / 10);
62
+ const projectMatch = ep.project === project ? 1 : 0.5;
63
+ const relevance = projectMatch * (0.3 + 0.7 * tagOverlap(ep.tags, ctx));
64
+ return weights.recency * recency + weights.importance * importanceNorm + weights.relevance * relevance;
65
+ }
66
+
67
+ export function scoreWorkEntry(
68
+ w: WorkEntry,
69
+ project: string,
70
+ ctx: string[],
71
+ defaultHalfLife: Record<string, number>,
72
+ weights: ScoreWeights,
73
+ ): number {
74
+ const strength = w.strength || defaultHalfLife.work || 45;
75
+ const recency = decay(daysSince(w.created), strength);
76
+ const importanceNorm = Math.min(1, w.importance / 10);
77
+ const projectMatch = w.project === project ? 1 : 0.5;
78
+ const relevance = projectMatch * (0.3 + 0.7 * tagOverlap(w.tags, ctx));
79
+ return weights.recency * recency + weights.importance * importanceNorm + weights.relevance * relevance;
80
+ }
81
+
82
+ /** Budget-constrained top-k selection by score */
83
+ export function pickTop<T>(items: T[], scoreFn: (t: T) => number, lenFn: (t: T) => number, budget: number): T[] {
84
+ const scored = items.map((t) => ({ item: t, score: scoreFn(t) })).sort((a, b) => b.score - a.score);
85
+ const result: T[] = [];
86
+ let used = 0;
87
+ for (const { item } of scored) {
88
+ const len = lenFn(item);
89
+ if (used + len > budget) continue;
90
+ used += len;
91
+ result.push(item);
92
+ }
93
+ return result;
94
+ }