@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
package/src/cli.ts ADDED
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * [INPUT]: process.argv
4
+ * [OUTPUT]: stats | search <query> | forget <id> | export | insights — terminal output or JSON or HTML
5
+ * [POS]: Standalone CLI for NanoMem — no host dependency
6
+ */
7
+
8
+ import { writeFileSync } from "node:fs";
9
+ import { NanoMemEngine } from "./engine.js";
10
+ import { renderFullInsightsHtml } from "./full-insights-html.js";
11
+ import { renderInsightsHtml } from "./insights-html.js";
12
+
13
+ const args = process.argv.slice(2);
14
+ const sub = args[0];
15
+ const engine = new NanoMemEngine();
16
+
17
+ async function main(): Promise<void> {
18
+ if (!sub || sub === "help" || sub === "-h" || sub === "--help") {
19
+ console.log(`nanomem — NanoMem memory CLI
20
+
21
+ Usage:
22
+ nanomem stats Show memory counts (sessions, knowledge, lessons, preferences, work, episodes, facets)
23
+ nanomem search <query> Search memories by query text
24
+ nanomem forget <id> Remove a memory entry by ID
25
+ nanomem export Export all memories as JSON to stdout
26
+ nanomem insights [--output <path>] Generate full HTML insights report (default: ./nanomem-insights.html)
27
+ nanomem insights --simple [--output <path>] Generate simple insights report (rules-only, no LLM)
28
+ nanomem help Show this help
29
+ `);
30
+ return;
31
+ }
32
+
33
+ if (sub === "stats") {
34
+ const s = await engine.getStats();
35
+ console.log(`Sessions: ${s.totalSessions}`);
36
+ console.log(`Knowledge: ${s.knowledge}`);
37
+ console.log(`Lessons: ${s.lessons}`);
38
+ console.log(`Preferences: ${s.preferences}`);
39
+ console.log(`Work: ${s.work}`);
40
+ console.log(`Episodes: ${s.episodes}`);
41
+ return;
42
+ }
43
+
44
+ if (sub === "search") {
45
+ const query = args.slice(1).join(" ").trim() || " ";
46
+ const results = await engine.searchEntries(query);
47
+ if (!results.length) {
48
+ console.log("No matching memories.");
49
+ return;
50
+ }
51
+ for (const e of results) {
52
+ console.log(`[${e.type}] ${e.id} — ${e.content.slice(0, 100)}`);
53
+ }
54
+ return;
55
+ }
56
+
57
+ if (sub === "forget") {
58
+ const id = args[1];
59
+ if (!id) {
60
+ console.error("Usage: nanomem forget <id>");
61
+ process.exit(1);
62
+ }
63
+ const ok = await engine.forgetEntry(id);
64
+ console.log(ok ? `Removed entry ${id}` : `Entry ${id} not found`);
65
+ return;
66
+ }
67
+
68
+ if (sub === "export") {
69
+ const data = await engine.exportAll();
70
+ console.log(JSON.stringify(data, null, 2));
71
+ return;
72
+ }
73
+
74
+ if (sub === "insights") {
75
+ const simple = args.includes("--simple");
76
+ const outputIdx = args.indexOf("--output");
77
+ const outputPath = outputIdx >= 0 && args[outputIdx + 1] ? args[outputIdx + 1] : "./nanomem-insights.html";
78
+
79
+ if (simple) {
80
+ const report = await engine.generateInsights();
81
+ const html = renderInsightsHtml(report, engine.cfg.locale);
82
+ writeFileSync(outputPath, html, "utf-8");
83
+ } else {
84
+ const report = await engine.generateFullInsights();
85
+ const html = renderFullInsightsHtml(report, engine.cfg.locale);
86
+ writeFileSync(outputPath, html, "utf-8");
87
+ }
88
+ console.log(`Insights report written to: ${outputPath}`);
89
+ return;
90
+ }
91
+
92
+ console.error(`Unknown command: ${sub}. Run 'nanomem help' for usage.`);
93
+ process.exit(1);
94
+ }
95
+
96
+ main().catch((err) => {
97
+ console.error(err);
98
+ process.exit(1);
99
+ });
package/src/config.ts ADDED
@@ -0,0 +1,72 @@
1
+ /**
2
+ * [INPUT]: process.env, optional overrides
3
+ * [OUTPUT]: NanomemConfig — memory dir, token budget, scoring weights, etc.
4
+ * [POS]: Shared by engine and adapters; host products configure via this
5
+ */
6
+
7
+ import { homedir } from "node:os";
8
+ import { join } from "node:path";
9
+ import type { MemoryScope } from "./types.js";
10
+
11
+ export interface NanomemConfig {
12
+ memoryDir: string;
13
+ tokenBudget: number;
14
+ budget: { lessons: number; knowledge: number; episodes: number; preferences: number; work: number; facets: number };
15
+ halfLife: Record<string, number>;
16
+ maxEntries: { knowledge: number; lessons: number; preferences: number; work: number; facets: number };
17
+ consolidationThreshold: number;
18
+ /** Stanford-style retrieval scoring weights */
19
+ scoreWeights: { recency: number; importance: number; relevance: number };
20
+ /** Utility-weighted eviction: access frequency vs base impact */
21
+ evictionWeights: { accessFrequency: number; baseImpact: number };
22
+ /** Default scope for all operations */
23
+ defaultScope?: MemoryScope;
24
+ /** Locale for LLM prompts and injection templates */
25
+ locale: "en" | "zh";
26
+ /** Strength growth factor on each successful recall (spaced repetition) */
27
+ strengthGrowthFactor: number;
28
+ }
29
+
30
+ const DEFAULT_BUDGET = {
31
+ lessons: 0.2,
32
+ knowledge: 0.2,
33
+ episodes: 0.18,
34
+ preferences: 0.1,
35
+ work: 0.2,
36
+ facets: 0.12,
37
+ } as const;
38
+
39
+ const DEFAULT_HALF_LIFE: Record<string, number> = {
40
+ lesson: 90,
41
+ fact: 60,
42
+ episode: 14,
43
+ preference: 120,
44
+ decision: 45,
45
+ entity: 30,
46
+ work: 45,
47
+ pattern: 180,
48
+ struggle: 120,
49
+ };
50
+
51
+ const DEFAULT_MAX_ENTRIES = { knowledge: 200, lessons: 100, preferences: 50, work: 80, facets: 80 };
52
+ const DEFAULT_SCORE_WEIGHTS = { recency: 1, importance: 1, relevance: 1 };
53
+ const DEFAULT_EVICTION_WEIGHTS = { accessFrequency: 0.4, baseImpact: 0.6 };
54
+
55
+ export function getConfig(overrides?: Partial<NanomemConfig>): NanomemConfig {
56
+ const tokenBudget = Number(process.env.NANOMEM_TOKEN_BUDGET) || 6000;
57
+ const memoryDir = process.env.NANOMEM_MEMORY_DIR || overrides?.memoryDir || join(homedir(), ".nanomem", "memory");
58
+ const locale = (process.env.NANOMEM_LOCALE as "en" | "zh") || overrides?.locale || "en";
59
+ return {
60
+ memoryDir,
61
+ tokenBudget: overrides?.tokenBudget ?? tokenBudget,
62
+ budget: overrides?.budget ?? { ...DEFAULT_BUDGET },
63
+ halfLife: overrides?.halfLife ?? { ...DEFAULT_HALF_LIFE },
64
+ maxEntries: overrides?.maxEntries ?? { ...DEFAULT_MAX_ENTRIES },
65
+ consolidationThreshold: overrides?.consolidationThreshold ?? 10,
66
+ scoreWeights: overrides?.scoreWeights ?? { ...DEFAULT_SCORE_WEIGHTS },
67
+ evictionWeights: overrides?.evictionWeights ?? { ...DEFAULT_EVICTION_WEIGHTS },
68
+ defaultScope: overrides?.defaultScope,
69
+ locale,
70
+ strengthGrowthFactor: overrides?.strengthGrowthFactor ?? 1.5,
71
+ };
72
+ }
@@ -0,0 +1,127 @@
1
+ /**
2
+ * [INPUT]: unconsolidated episodes, LlmFn (optional), config
3
+ * [OUTPUT]: newly extracted MemoryEntries (facts + lessons) promoted to long-term storage
4
+ * [POS]: Episodic→Semantic consolidation — heart of multi-store memory model
5
+ *
6
+ * Two modes:
7
+ * LLM-powered (preferred): produces high-quality semantic extraction
8
+ * Heuristic fallback: frequency-based file/error extraction
9
+ */
10
+
11
+ import type { NanomemConfig } from "./config.js";
12
+ import { PROMPTS } from "./i18n.js";
13
+ import { extractTags } from "./scoring.js";
14
+ import type { Episode, LlmFn, MemoryEntry } from "./types.js";
15
+
16
+ function makeId(): string {
17
+ return `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
18
+ }
19
+
20
+ export async function consolidateEpisodes(
21
+ episodes: Episode[],
22
+ cfg: NanomemConfig,
23
+ llmFn?: LlmFn,
24
+ ): Promise<MemoryEntry[]> {
25
+ const unconsolidated = episodes.filter((ep) => !ep.consolidated);
26
+ if (unconsolidated.length < cfg.consolidationThreshold) return [];
27
+
28
+ let newEntries: MemoryEntry[];
29
+ if (llmFn) {
30
+ newEntries = await llmConsolidation(unconsolidated, cfg, llmFn);
31
+ } else {
32
+ newEntries = heuristicConsolidation(unconsolidated, cfg);
33
+ }
34
+
35
+ for (const ep of unconsolidated) ep.consolidated = true;
36
+ return newEntries;
37
+ }
38
+
39
+ async function llmConsolidation(episodes: Episode[], cfg: NanomemConfig, llmFn: LlmFn): Promise<MemoryEntry[]> {
40
+ const p = PROMPTS[cfg.locale] ?? PROMPTS.en;
41
+ const summary = episodes
42
+ .map(
43
+ (ep) =>
44
+ `[${ep.date}] ${ep.project}: ${ep.summary}\nFiles: ${ep.filesModified.join(", ")}\nErrors: ${ep.errors.join("; ") || "none"}`,
45
+ )
46
+ .join("\n\n");
47
+
48
+ const raw = await llmFn(p.consolidationSystem, summary);
49
+
50
+ try {
51
+ const items = JSON.parse(raw) as Array<{ type: string; content: string; importance?: number }>;
52
+ const now = new Date().toISOString();
53
+ return items.map((item) => {
54
+ const type: MemoryEntry["type"] = item.type === "lesson" ? "lesson" : "fact";
55
+ return {
56
+ id: makeId(),
57
+ type,
58
+ content: item.content,
59
+ tags: extractTags(item.content),
60
+ project: episodes[0]?.project ?? "unknown",
61
+ importance: item.importance ?? 6,
62
+ strength: cfg.halfLife[type] ?? 30,
63
+ created: now,
64
+ eventTime: now,
65
+ accessCount: 0,
66
+ relatedIds: [],
67
+ scope: cfg.defaultScope,
68
+ };
69
+ });
70
+ } catch {
71
+ return heuristicConsolidation(episodes, cfg);
72
+ }
73
+ }
74
+
75
+ function heuristicConsolidation(episodes: Episode[], cfg: NanomemConfig): MemoryEntry[] {
76
+ const now = new Date().toISOString();
77
+ const result: MemoryEntry[] = [];
78
+
79
+ const fileCounts = new Map<string, number>();
80
+ for (const ep of episodes) {
81
+ for (const f of ep.filesModified) fileCounts.set(f, (fileCounts.get(f) ?? 0) + 1);
82
+ }
83
+ const hotFiles = [...fileCounts.entries()]
84
+ .filter(([, c]) => c >= 3)
85
+ .sort((a, b) => b[1] - a[1])
86
+ .slice(0, 5);
87
+
88
+ if (hotFiles.length) {
89
+ const content = `Frequently modified files: ${hotFiles.map(([f, c]) => `${f} (${c}x)`).join(", ")}`;
90
+ result.push({
91
+ id: makeId(),
92
+ type: "fact",
93
+ content,
94
+ tags: extractTags(content),
95
+ project: episodes[0]?.project ?? "unknown",
96
+ importance: 5,
97
+ strength: cfg.halfLife.fact ?? 60,
98
+ created: now,
99
+ eventTime: now,
100
+ accessCount: 0,
101
+ relatedIds: [],
102
+ scope: cfg.defaultScope,
103
+ });
104
+ }
105
+
106
+ const allErrors = episodes.flatMap((ep) => ep.errors).filter(Boolean);
107
+ if (allErrors.length) {
108
+ const errorSet = [...new Set(allErrors)].slice(0, 5);
109
+ const content = `Recurring issues: ${errorSet.join("; ")}`;
110
+ result.push({
111
+ id: makeId(),
112
+ type: "lesson",
113
+ content,
114
+ tags: extractTags(content),
115
+ project: episodes[0]?.project ?? "unknown",
116
+ importance: 7,
117
+ strength: cfg.halfLife.lesson ?? 90,
118
+ created: now,
119
+ eventTime: now,
120
+ accessCount: 0,
121
+ relatedIds: [],
122
+ scope: cfg.defaultScope,
123
+ });
124
+ }
125
+
126
+ return result;
127
+ }