claude-memory-layer 1.0.11 → 1.0.12

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 (99) hide show
  1. package/AGENTS.md +60 -0
  2. package/README.md +166 -2
  3. package/bootstrap-kb/decisions/decisions.md +244 -0
  4. package/bootstrap-kb/glossary/glossary.md +46 -0
  5. package/bootstrap-kb/modules/.claude-plugin.md +22 -0
  6. package/bootstrap-kb/modules/agents.md.md +15 -0
  7. package/bootstrap-kb/modules/claude.md.md +15 -0
  8. package/bootstrap-kb/modules/context.md.md +15 -0
  9. package/bootstrap-kb/modules/docs.md +18 -0
  10. package/bootstrap-kb/modules/handoff.md.md +15 -0
  11. package/bootstrap-kb/modules/package-lock.json.md +15 -0
  12. package/bootstrap-kb/modules/package.json.md +15 -0
  13. package/bootstrap-kb/modules/plan.md.md +15 -0
  14. package/bootstrap-kb/modules/readme.md.md +15 -0
  15. package/bootstrap-kb/modules/scripts.md +26 -0
  16. package/bootstrap-kb/modules/spec.md.md +15 -0
  17. package/bootstrap-kb/modules/specs.md +20 -0
  18. package/bootstrap-kb/modules/src.md +51 -0
  19. package/bootstrap-kb/modules/tests.md +42 -0
  20. package/bootstrap-kb/modules/tsconfig.json.md +15 -0
  21. package/bootstrap-kb/modules/vitest.config.ts.md +15 -0
  22. package/bootstrap-kb/overview/overview.md +40 -0
  23. package/bootstrap-kb/sources/manifest.json +950 -0
  24. package/bootstrap-kb/sources/manifest.md +227 -0
  25. package/bootstrap-kb/timeline/timeline.md +57 -0
  26. package/d.sh +3 -0
  27. package/deploy.sh +3 -0
  28. package/dist/cli/index.js +2389 -286
  29. package/dist/cli/index.js.map +4 -4
  30. package/dist/core/index.js +1017 -132
  31. package/dist/core/index.js.map +4 -4
  32. package/dist/hooks/post-tool-use.js +1347 -202
  33. package/dist/hooks/post-tool-use.js.map +4 -4
  34. package/dist/hooks/session-end.js +1339 -194
  35. package/dist/hooks/session-end.js.map +4 -4
  36. package/dist/hooks/session-start.js +1343 -198
  37. package/dist/hooks/session-start.js.map +4 -4
  38. package/dist/hooks/stop.js +1351 -206
  39. package/dist/hooks/stop.js.map +4 -4
  40. package/dist/hooks/user-prompt-submit.js +1347 -202
  41. package/dist/hooks/user-prompt-submit.js.map +4 -4
  42. package/dist/server/api/index.js +1436 -211
  43. package/dist/server/api/index.js.map +4 -4
  44. package/dist/server/index.js +1445 -220
  45. package/dist/server/index.js.map +4 -4
  46. package/dist/services/memory-service.js +1345 -199
  47. package/dist/services/memory-service.js.map +4 -4
  48. package/dist/ui/app.js +69 -2
  49. package/dist/ui/index.html +8 -0
  50. package/docs/MCP_MEMORY_SERVICE_COMPARATIVE_REVIEW.md +271 -0
  51. package/docs/MEMU_ADOPTION.md +40 -0
  52. package/memory/.claude-plugin/commands/2026-02-25.md +263 -0
  53. package/memory/_index.md +405 -0
  54. package/memory/default/uncategorized/2026-02-25.md +4839 -0
  55. package/memory/specs/20260207-dashboard-upgrade/2026-02-25.md +142 -0
  56. package/memory/specs/citations-system/2026-02-25.md +1121 -0
  57. package/memory/specs/endless-mode/2026-02-25.md +1392 -0
  58. package/memory/specs/entity-edge-model/2026-02-25.md +1263 -0
  59. package/memory/specs/evidence-aligner-v2/2026-02-25.md +1028 -0
  60. package/memory/specs/mcp-desktop-integration/2026-02-25.md +1334 -0
  61. package/memory/specs/post-tool-use-hook/2026-02-25.md +1164 -0
  62. package/memory/specs/private-tags/2026-02-25.md +1057 -0
  63. package/memory/specs/progressive-disclosure/2026-02-25.md +1436 -0
  64. package/memory/specs/task-entity-system/2026-02-25.md +924 -0
  65. package/memory/specs/vector-outbox-v2/2026-02-25.md +1510 -0
  66. package/memory/specs/web-viewer-ui/2026-02-25.md +1709 -0
  67. package/package.json +2 -1
  68. package/scripts/build.ts +6 -0
  69. package/src/cli/index.ts +281 -2
  70. package/src/core/consolidated-store.ts +63 -1
  71. package/src/core/consolidation-worker.ts +115 -6
  72. package/src/core/event-store.ts +14 -0
  73. package/src/core/index.ts +1 -0
  74. package/src/core/ingest-interceptor.ts +80 -0
  75. package/src/core/markdown-mirror.ts +70 -0
  76. package/src/core/md-mirror.ts +92 -0
  77. package/src/core/mongo-sync-config.ts +165 -0
  78. package/src/core/mongo-sync-worker.ts +381 -0
  79. package/src/core/retriever.ts +540 -150
  80. package/src/core/sqlite-event-store.ts +350 -1
  81. package/src/core/tag-taxonomy.ts +51 -0
  82. package/src/core/types.ts +28 -0
  83. package/src/server/api/health.ts +53 -0
  84. package/src/server/api/index.ts +3 -1
  85. package/src/server/api/stats.ts +46 -1
  86. package/src/services/bootstrap-organizer.ts +443 -0
  87. package/src/services/codex-session-history-importer.ts +474 -0
  88. package/src/services/memory-service.ts +373 -68
  89. package/src/ui/app.js +69 -2
  90. package/src/ui/index.html +8 -0
  91. package/tests/bootstrap-organizer.test.ts +111 -0
  92. package/tests/consolidation-worker.test.ts +75 -0
  93. package/tests/ingest-interceptor.test.ts +38 -0
  94. package/tests/markdown-mirror.test.ts +85 -0
  95. package/tests/md-mirror.test.ts +50 -0
  96. package/tests/retriever-fallback-chain.test.ts +223 -0
  97. package/tests/retriever-strategy-scope.test.ts +97 -0
  98. package/tests/retriever.memu-adoption.test.ts +122 -0
  99. package/tests/sqlite-event-store-replication.test.ts +92 -0
@@ -0,0 +1,80 @@
1
+ import type { MemoryEventInput } from './types.js';
2
+
3
+ export type IngestStage = 'before' | 'after' | 'error';
4
+
5
+ export interface IngestContext {
6
+ stage: IngestStage;
7
+ operation: 'user_prompt' | 'agent_response' | 'session_summary' | 'tool_observation';
8
+ sessionId: string;
9
+ event: MemoryEventInput;
10
+ error?: Error;
11
+ }
12
+
13
+ export type IngestInterceptor = (context: IngestContext) => Promise<void> | void;
14
+
15
+ export class IngestInterceptorRegistry {
16
+ private before: IngestInterceptor[] = [];
17
+ private after: IngestInterceptor[] = [];
18
+ private onError: IngestInterceptor[] = [];
19
+
20
+ registerBefore(interceptor: IngestInterceptor): () => void {
21
+ this.before.push(interceptor);
22
+ return () => {
23
+ this.before = this.before.filter((i) => i !== interceptor);
24
+ };
25
+ }
26
+
27
+ registerAfter(interceptor: IngestInterceptor): () => void {
28
+ this.after.push(interceptor);
29
+ return () => {
30
+ this.after = this.after.filter((i) => i !== interceptor);
31
+ };
32
+ }
33
+
34
+ registerOnError(interceptor: IngestInterceptor): () => void {
35
+ this.onError.push(interceptor);
36
+ return () => {
37
+ this.onError = this.onError.filter((i) => i !== interceptor);
38
+ };
39
+ }
40
+
41
+ async run(stage: IngestStage, context: Omit<IngestContext, 'stage'>): Promise<void> {
42
+ const interceptors = stage === 'before'
43
+ ? this.before
44
+ : stage === 'after'
45
+ ? this.after
46
+ : this.onError;
47
+
48
+ for (const interceptor of interceptors) {
49
+ await interceptor({ ...context, stage });
50
+ }
51
+ }
52
+ }
53
+
54
+ export function mergeHierarchicalMetadata(
55
+ base: Record<string, unknown> | undefined,
56
+ patch: Record<string, unknown> | undefined
57
+ ): Record<string, unknown> | undefined {
58
+ if (!base && !patch) return undefined;
59
+ if (!base) return patch;
60
+ if (!patch) return base;
61
+
62
+ const result: Record<string, unknown> = { ...base };
63
+
64
+ for (const [key, value] of Object.entries(patch)) {
65
+ const current = result[key];
66
+ if (
67
+ typeof current === 'object' && current !== null && !Array.isArray(current) &&
68
+ typeof value === 'object' && value !== null && !Array.isArray(value)
69
+ ) {
70
+ result[key] = mergeHierarchicalMetadata(
71
+ current as Record<string, unknown>,
72
+ value as Record<string, unknown>
73
+ );
74
+ } else {
75
+ result[key] = value;
76
+ }
77
+ }
78
+
79
+ return result;
80
+ }
@@ -0,0 +1,70 @@
1
+ import * as fs from 'fs/promises';
2
+ import * as path from 'path';
3
+ import type { MemoryEvent } from './types.js';
4
+
5
+ const DEFAULT_NAMESPACE = 'default';
6
+ const DEFAULT_CATEGORY = 'uncategorized';
7
+
8
+ export function sanitizeSegment(input: unknown, fallback: string): string {
9
+ const raw = String(input ?? '').trim().toLowerCase();
10
+ const safe = raw
11
+ .normalize('NFKD')
12
+ .replace(/[^a-z0-9_-]+/g, '-')
13
+ .replace(/^-+|-+$/g, '');
14
+
15
+ if (!safe || safe === '.' || safe === '..') return fallback;
16
+ return safe;
17
+ }
18
+
19
+ function getCategorySegments(metadata: Record<string, unknown> | undefined, eventType: string): string[] {
20
+ const raw = metadata?.categoryPath;
21
+ if (Array.isArray(raw) && raw.length > 0) {
22
+ return raw.map((s) => sanitizeSegment(s, DEFAULT_CATEGORY));
23
+ }
24
+ const single = metadata?.category;
25
+ if (typeof single === 'string' && single.trim()) {
26
+ return [sanitizeSegment(single, DEFAULT_CATEGORY)];
27
+ }
28
+ return [sanitizeSegment(eventType, DEFAULT_CATEGORY)];
29
+ }
30
+
31
+ export function buildMirrorPath(rootDir: string, event: MemoryEvent): string {
32
+ const metadata = event.metadata as Record<string, unknown> | undefined;
33
+ const namespace = sanitizeSegment(metadata?.namespace, DEFAULT_NAMESPACE);
34
+ const categories = getCategorySegments(metadata, event.eventType);
35
+
36
+ const d = event.timestamp;
37
+ const yyyy = d.getFullYear();
38
+ const mm = String(d.getMonth() + 1).padStart(2, '0');
39
+ const dd = String(d.getDate()).padStart(2, '0');
40
+
41
+ return path.join(rootDir, 'memory', namespace, ...categories, `${yyyy}-${mm}-${dd}.md`);
42
+ }
43
+
44
+ export function formatMirrorEntry(event: MemoryEvent): string {
45
+ const category = Array.isArray((event.metadata as any)?.categoryPath)
46
+ ? ((event.metadata as any).categoryPath as unknown[]).join('/')
47
+ : String((event.metadata as any)?.category ?? event.eventType);
48
+
49
+ return [
50
+ '',
51
+ `- ts: ${event.timestamp.toISOString()}`,
52
+ ` id: ${event.id}`,
53
+ ` type: ${event.eventType}`,
54
+ ` session: ${event.sessionId}`,
55
+ ` category: ${category}`,
56
+ ' content: |',
57
+ ...event.content.split('\n').map((line) => ` ${line}`)
58
+ ].join('\n') + '\n';
59
+ }
60
+
61
+ export class MarkdownMirror {
62
+ constructor(private readonly rootDir: string) {}
63
+
64
+ async append(event: MemoryEvent): Promise<string> {
65
+ const outPath = buildMirrorPath(this.rootDir, event);
66
+ await fs.mkdir(path.dirname(outPath), { recursive: true });
67
+ await fs.appendFile(outPath, formatMirrorEntry(event), 'utf8');
68
+ return outPath;
69
+ }
70
+ }
@@ -0,0 +1,92 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import type { MemoryEventInput } from './types.js';
4
+
5
+ function sanitizeSegment(input: string | undefined, fallback: string): string {
6
+ const v = (input || '').trim().toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '');
7
+ return v || fallback;
8
+ }
9
+
10
+ function getAtPath(obj: Record<string, unknown> | undefined, dotted: string): unknown {
11
+ if (!obj) return undefined;
12
+ return dotted.split('.').reduce<unknown>((acc, key) => {
13
+ if (!acc || typeof acc !== 'object') return undefined;
14
+ return (acc as Record<string, unknown>)[key];
15
+ }, obj);
16
+ }
17
+
18
+ export function buildMirrorPath(rootDir: string, event: MemoryEventInput): string {
19
+ const meta = event.metadata as Record<string, unknown> | undefined;
20
+
21
+ const namespaceRaw = getAtPath(meta, 'namespace') ?? getAtPath(meta, 'scope.namespace') ?? event.eventType;
22
+ const namespace = sanitizeSegment(typeof namespaceRaw === 'string' ? namespaceRaw : undefined, 'general');
23
+
24
+ const categoryRaw = getAtPath(meta, 'categoryPath') ?? getAtPath(meta, 'scope.categoryPath');
25
+ const categoryPath = Array.isArray(categoryRaw) && categoryRaw.length > 0
26
+ ? categoryRaw.map((x) => sanitizeSegment(typeof x === 'string' ? x : undefined, 'uncategorized'))
27
+ : ['uncategorized'];
28
+
29
+ const d = event.timestamp;
30
+ const yyyy = d.getFullYear();
31
+ const mm = String(d.getMonth() + 1).padStart(2, '0');
32
+ const dd = String(d.getDate()).padStart(2, '0');
33
+
34
+ return path.join(rootDir, 'memory', namespace, ...categoryPath, `${yyyy}-${mm}-${dd}.md`);
35
+ }
36
+
37
+ export class MarkdownMirror {
38
+ constructor(private readonly rootDir: string) {}
39
+
40
+ async append(event: MemoryEventInput, eventId?: string): Promise<void> {
41
+ const out = buildMirrorPath(this.rootDir, event);
42
+ fs.mkdirSync(path.dirname(out), { recursive: true });
43
+
44
+ const lines = [
45
+ '',
46
+ `## ${event.timestamp.toISOString()} | ${eventId ?? 'pending-id'}`,
47
+ `- type: ${event.eventType}`,
48
+ `- session: ${event.sessionId}`,
49
+ event.content,
50
+ ];
51
+
52
+ await fs.promises.appendFile(out, lines.join('\n'), 'utf8');
53
+ await this.refreshIndex();
54
+ }
55
+
56
+ private async refreshIndex(): Promise<void> {
57
+ const memoryRoot = path.join(this.rootDir, 'memory');
58
+ await fs.promises.mkdir(memoryRoot, { recursive: true });
59
+
60
+ const files: string[] = [];
61
+ await this.walk(memoryRoot, files);
62
+
63
+ const mdFiles = files
64
+ .filter((f) => f.endsWith('.md'))
65
+ .map((f) => path.relative(this.rootDir, f))
66
+ .filter((rel) => rel !== path.join('memory', '_index.md'))
67
+ .sort();
68
+
69
+ const index = [
70
+ '# Memory Index',
71
+ '',
72
+ 'Generated automatically by MarkdownMirror.',
73
+ '',
74
+ ...mdFiles.map((rel) => `- ${rel}`),
75
+ '',
76
+ ].join('\n');
77
+
78
+ await fs.promises.writeFile(path.join(memoryRoot, '_index.md'), index, 'utf8');
79
+ }
80
+
81
+ private async walk(dir: string, out: string[]): Promise<void> {
82
+ const entries = await fs.promises.readdir(dir, { withFileTypes: true });
83
+ for (const e of entries) {
84
+ const full = path.join(dir, e.name);
85
+ if (e.isDirectory()) {
86
+ await this.walk(full, out);
87
+ } else {
88
+ out.push(full);
89
+ }
90
+ }
91
+ }
92
+ }
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Mongo Sync Config
3
+ * Persistent per-project config for enabling auto-sync from hooks (e.g., SessionEnd).
4
+ *
5
+ * Stored as JSON under the project's storagePath next to events.sqlite.
6
+ * Note: This may include credentials in plaintext (MongoDB URI). Treat accordingly.
7
+ */
8
+
9
+ import * as fs from 'fs';
10
+ import * as path from 'path';
11
+
12
+ import type { MongoSyncDirection } from './mongo-sync-worker.js';
13
+
14
+ export type MongoSyncConfig = {
15
+ version: 1;
16
+ enabled: boolean;
17
+ /** MongoDB connection URI (may include credentials). */
18
+ uri: string;
19
+ /** MongoDB database name. */
20
+ dbName: string;
21
+ /** Remote project key (shared across machines). */
22
+ projectKey: string;
23
+ /** push|pull|both */
24
+ direction: MongoSyncDirection;
25
+ /** Batch size for push/pull loops. */
26
+ batchSize: number;
27
+ /** If true, hooks will run a sync at SessionEnd. */
28
+ autoSyncOnSessionEnd: boolean;
29
+ createdAt: string;
30
+ updatedAt: string;
31
+ };
32
+
33
+ function isRecord(value: unknown): value is Record<string, unknown> {
34
+ return typeof value === 'object' && value !== null;
35
+ }
36
+
37
+ function asNonEmptyString(value: unknown): string | null {
38
+ if (typeof value !== 'string') return null;
39
+ const s = value.trim();
40
+ return s.length > 0 ? s : null;
41
+ }
42
+
43
+ function asPositiveInt(value: unknown): number | null {
44
+ if (typeof value === 'number' && Number.isFinite(value)) {
45
+ const n = Math.trunc(value);
46
+ return n > 0 ? n : null;
47
+ }
48
+ if (typeof value === 'string') {
49
+ const n = parseInt(value, 10);
50
+ return Number.isFinite(n) && n > 0 ? n : null;
51
+ }
52
+ return null;
53
+ }
54
+
55
+ function normalizeDirection(value: unknown): MongoSyncDirection | null {
56
+ if (typeof value !== 'string') return null;
57
+ const v = value.toLowerCase();
58
+ if (v === 'push' || v === 'pull' || v === 'both') return v;
59
+ return null;
60
+ }
61
+
62
+ export function getMongoSyncConfigPath(storagePath: string): string {
63
+ return path.join(storagePath, 'mongo-sync.json');
64
+ }
65
+
66
+ export function readMongoSyncConfig(storagePath: string): MongoSyncConfig | null {
67
+ const configPath = getMongoSyncConfigPath(storagePath);
68
+ if (!fs.existsSync(configPath)) return null;
69
+
70
+ try {
71
+ const raw = JSON.parse(fs.readFileSync(configPath, 'utf8')) as unknown;
72
+ if (!isRecord(raw)) return null;
73
+
74
+ const version = raw.version;
75
+ if (version !== 1) return null;
76
+
77
+ const enabled = raw.enabled;
78
+ if (typeof enabled !== 'boolean') return null;
79
+
80
+ const uri = asNonEmptyString(raw.uri);
81
+ const dbName = asNonEmptyString(raw.dbName);
82
+ const projectKey = asNonEmptyString(raw.projectKey);
83
+ const direction = normalizeDirection(raw.direction);
84
+ const batchSize = asPositiveInt(raw.batchSize) ?? 500;
85
+ const autoSyncOnSessionEnd = typeof raw.autoSyncOnSessionEnd === 'boolean' ? raw.autoSyncOnSessionEnd : true;
86
+ const createdAt = asNonEmptyString(raw.createdAt) ?? new Date(0).toISOString();
87
+ const updatedAt = asNonEmptyString(raw.updatedAt) ?? new Date(0).toISOString();
88
+
89
+ if (!uri || !dbName || !projectKey || !direction) return null;
90
+
91
+ return {
92
+ version: 1,
93
+ enabled,
94
+ uri,
95
+ dbName,
96
+ projectKey,
97
+ direction,
98
+ batchSize,
99
+ autoSyncOnSessionEnd,
100
+ createdAt,
101
+ updatedAt
102
+ };
103
+ } catch {
104
+ return null;
105
+ }
106
+ }
107
+
108
+ export function writeMongoSyncConfig(storagePath: string, config: Omit<MongoSyncConfig, 'version' | 'createdAt' | 'updatedAt'> & {
109
+ createdAt?: string;
110
+ updatedAt?: string;
111
+ }): MongoSyncConfig {
112
+ if (!fs.existsSync(storagePath)) {
113
+ fs.mkdirSync(storagePath, { recursive: true });
114
+ }
115
+
116
+ const now = new Date().toISOString();
117
+ const existing = readMongoSyncConfig(storagePath);
118
+
119
+ const normalized: MongoSyncConfig = {
120
+ version: 1,
121
+ enabled: config.enabled,
122
+ uri: config.uri,
123
+ dbName: config.dbName,
124
+ projectKey: config.projectKey,
125
+ direction: config.direction,
126
+ batchSize: config.batchSize,
127
+ autoSyncOnSessionEnd: config.autoSyncOnSessionEnd,
128
+ createdAt: config.createdAt ?? existing?.createdAt ?? now,
129
+ updatedAt: config.updatedAt ?? now
130
+ };
131
+
132
+ const configPath = getMongoSyncConfigPath(storagePath);
133
+ const tmpPath = `${configPath}.tmp`;
134
+
135
+ // 0600: contains secrets (Mongo URI may embed credentials)
136
+ fs.writeFileSync(tmpPath, JSON.stringify(normalized, null, 2), { mode: 0o600 });
137
+ fs.renameSync(tmpPath, configPath);
138
+
139
+ return normalized;
140
+ }
141
+
142
+ export function removeMongoSyncConfig(storagePath: string): boolean {
143
+ const configPath = getMongoSyncConfigPath(storagePath);
144
+ if (!fs.existsSync(configPath)) return false;
145
+ fs.unlinkSync(configPath);
146
+ return true;
147
+ }
148
+
149
+ export function redactMongoUri(uri: string): string {
150
+ // mongodb://user:pass@host:port/ -> mongodb://user:***@host:port/
151
+ // mongodb+srv://user:pass@host/ -> mongodb+srv://user:***@host/
152
+ const schemeIdx = uri.indexOf('://');
153
+ if (schemeIdx === -1) return uri;
154
+ const atIdx = uri.indexOf('@', schemeIdx + 3);
155
+ if (atIdx === -1) return uri;
156
+
157
+ const creds = uri.slice(schemeIdx + 3, atIdx); // user:pass
158
+ const colonIdx = creds.indexOf(':');
159
+ if (colonIdx === -1) return uri;
160
+
161
+ const prefix = uri.slice(0, schemeIdx + 3 + colonIdx + 1);
162
+ const suffix = uri.slice(atIdx);
163
+ return `${prefix}***${suffix}`;
164
+ }
165
+