claude-memory-layer 1.0.11 → 1.0.13

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 (101) 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/scripts/bump-patch-version.sh +18 -0
  70. package/src/cli/index.ts +281 -2
  71. package/src/core/consolidated-store.ts +63 -1
  72. package/src/core/consolidation-worker.ts +115 -6
  73. package/src/core/event-store.ts +14 -0
  74. package/src/core/index.ts +1 -0
  75. package/src/core/ingest-interceptor.ts +80 -0
  76. package/src/core/markdown-mirror.ts +70 -0
  77. package/src/core/md-mirror.ts +92 -0
  78. package/src/core/mongo-sync-config.ts +165 -0
  79. package/src/core/mongo-sync-worker.ts +381 -0
  80. package/src/core/retriever.ts +540 -150
  81. package/src/core/sqlite-event-store.ts +350 -1
  82. package/src/core/tag-taxonomy.ts +51 -0
  83. package/src/core/types.ts +28 -0
  84. package/src/server/api/health.ts +53 -0
  85. package/src/server/api/index.ts +3 -1
  86. package/src/server/api/stats.ts +46 -1
  87. package/src/services/bootstrap-organizer.ts +443 -0
  88. package/src/services/codex-session-history-importer.ts +474 -0
  89. package/src/services/memory-service.ts +373 -68
  90. package/src/services/session-history-importer.ts +53 -25
  91. package/src/ui/app.js +69 -2
  92. package/src/ui/index.html +8 -0
  93. package/tests/bootstrap-organizer.test.ts +111 -0
  94. package/tests/consolidation-worker.test.ts +75 -0
  95. package/tests/ingest-interceptor.test.ts +38 -0
  96. package/tests/markdown-mirror.test.ts +85 -0
  97. package/tests/md-mirror.test.ts +50 -0
  98. package/tests/retriever-fallback-chain.test.ts +223 -0
  99. package/tests/retriever-strategy-scope.test.ts +97 -0
  100. package/tests/retriever.memu-adoption.test.ts +122 -0
  101. package/tests/sqlite-event-store-replication.test.ts +92 -0
@@ -0,0 +1,381 @@
1
+ /**
2
+ * Mongo Sync Worker
3
+ * Optional: sync per-project SQLite events to a shared MongoDB database.
4
+ *
5
+ * Design goals:
6
+ * - Optional and decoupled (doesn't affect default local-only flow)
7
+ * - Idempotent (safe to retry)
8
+ * - Incremental push (SQLite rowid) and incremental pull (Mongo seq per project)
9
+ *
10
+ * NOTE:
11
+ * - We only sync immutable L0 events (events table). Derived tables can be rebuilt.
12
+ */
13
+
14
+ import { randomUUID } from 'crypto';
15
+ import * as os from 'os';
16
+ import { MongoClient } from 'mongodb';
17
+ import type { Collection, Db } from 'mongodb';
18
+
19
+ import type { MemoryEvent } from './types.js';
20
+ import { SQLiteEventStore } from './sqlite-event-store.js';
21
+
22
+ export type MongoSyncDirection = 'push' | 'pull' | 'both';
23
+
24
+ export interface MongoSyncWorkerConfig {
25
+ uri: string;
26
+ dbName: string;
27
+ projectKey: string;
28
+ direction?: MongoSyncDirection;
29
+ intervalMs?: number;
30
+ batchSize?: number;
31
+ instanceId?: string;
32
+ }
33
+
34
+ export interface MongoSyncStats {
35
+ lastSyncAt: Date | null;
36
+ pushedEvents: number;
37
+ pulledEvents: number;
38
+ errors: number;
39
+ status: 'idle' | 'syncing' | 'error' | 'stopped';
40
+ }
41
+
42
+ interface CounterDoc {
43
+ _id: string;
44
+ seq: number;
45
+ }
46
+
47
+ interface RemoteEventDoc {
48
+ _id: string;
49
+ projectKey: string;
50
+ seq: number;
51
+ eventId: string;
52
+ eventType: string;
53
+ sessionId: string;
54
+ timestamp: Date;
55
+ content: string;
56
+ canonicalKey: string;
57
+ dedupeKey: string;
58
+ metadata?: Record<string, unknown> | null;
59
+ insertedAt: Date;
60
+ updatedAt: Date;
61
+ source?: {
62
+ hostname?: string;
63
+ instanceId?: string;
64
+ };
65
+ }
66
+
67
+ function redactMongoUri(uri: string): string {
68
+ // mongodb://user:pass@host:port/ -> mongodb://user:***@host:port/
69
+ // mongodb+srv://user:pass@host/ -> mongodb+srv://user:***@host/
70
+ const schemeIdx = uri.indexOf('://');
71
+ if (schemeIdx === -1) return uri;
72
+ const atIdx = uri.indexOf('@', schemeIdx + 3);
73
+ if (atIdx === -1) return uri;
74
+
75
+ const creds = uri.slice(schemeIdx + 3, atIdx); // user:pass
76
+ const colonIdx = creds.indexOf(':');
77
+ if (colonIdx === -1) return uri;
78
+
79
+ const prefix = uri.slice(0, schemeIdx + 3 + colonIdx + 1);
80
+ const suffix = uri.slice(atIdx);
81
+ return `${prefix}***${suffix}`;
82
+ }
83
+
84
+ function parseIntOrZero(value: string | null | undefined): number {
85
+ if (!value) return 0;
86
+ const n = parseInt(value, 10);
87
+ return Number.isFinite(n) ? n : 0;
88
+ }
89
+
90
+ export class MongoSyncWorker {
91
+ private readonly config: Required<Omit<MongoSyncWorkerConfig, 'instanceId'>> & { instanceId: string };
92
+ private intervalHandle: NodeJS.Timeout | null = null;
93
+ private running = false;
94
+
95
+ private client: MongoClient | null = null;
96
+ private db: Db | null = null;
97
+ private counters: Collection<CounterDoc> | null = null;
98
+ private events: Collection<RemoteEventDoc> | null = null;
99
+ private indexesEnsured = false;
100
+
101
+ private stats: MongoSyncStats = {
102
+ lastSyncAt: null,
103
+ pushedEvents: 0,
104
+ pulledEvents: 0,
105
+ errors: 0,
106
+ status: 'idle'
107
+ };
108
+
109
+ constructor(
110
+ private readonly sqliteStore: SQLiteEventStore,
111
+ config: MongoSyncWorkerConfig
112
+ ) {
113
+ this.config = {
114
+ uri: config.uri,
115
+ dbName: config.dbName,
116
+ projectKey: config.projectKey,
117
+ direction: config.direction ?? 'both',
118
+ intervalMs: config.intervalMs ?? 30000,
119
+ batchSize: config.batchSize ?? 500,
120
+ instanceId: config.instanceId ?? randomUUID()
121
+ };
122
+ }
123
+
124
+ start(): void {
125
+ if (this.running) return;
126
+ this.running = true;
127
+ this.stats.status = 'idle';
128
+
129
+ // Initial sync
130
+ this.syncNow().catch((err) => {
131
+ console.error('[MongoSyncWorker] Initial sync failed:', err);
132
+ });
133
+
134
+ // Periodic sync
135
+ this.intervalHandle = setInterval(() => {
136
+ this.syncNow().catch((err) => {
137
+ console.error('[MongoSyncWorker] Periodic sync failed:', err);
138
+ });
139
+ }, this.config.intervalMs);
140
+ }
141
+
142
+ stop(): void {
143
+ this.running = false;
144
+ this.stats.status = 'stopped';
145
+
146
+ if (this.intervalHandle) {
147
+ clearInterval(this.intervalHandle);
148
+ this.intervalHandle = null;
149
+ }
150
+ }
151
+
152
+ async shutdown(): Promise<void> {
153
+ this.stop();
154
+ await this.disconnect();
155
+ }
156
+
157
+ getStats(): MongoSyncStats {
158
+ return { ...this.stats };
159
+ }
160
+
161
+ isRunning(): boolean {
162
+ return this.running;
163
+ }
164
+
165
+ async syncNow(): Promise<{ pushed: number; pulled: number }> {
166
+ if (this.stats.status === 'syncing') return { pushed: 0, pulled: 0 };
167
+
168
+ this.stats.status = 'syncing';
169
+ let pushed = 0;
170
+ let pulled = 0;
171
+
172
+ try {
173
+ await this.sqliteStore.initialize();
174
+ await this.ensureConnected();
175
+ await this.ensureIndexes();
176
+
177
+ if (this.config.direction === 'push' || this.config.direction === 'both') {
178
+ pushed = await this.pushEvents();
179
+ this.stats.pushedEvents += pushed;
180
+ }
181
+
182
+ if (this.config.direction === 'pull' || this.config.direction === 'both') {
183
+ pulled = await this.pullEvents();
184
+ this.stats.pulledEvents += pulled;
185
+ }
186
+
187
+ this.stats.lastSyncAt = new Date();
188
+ this.stats.status = 'idle';
189
+ return { pushed, pulled };
190
+ } catch (error) {
191
+ this.stats.errors++;
192
+ this.stats.status = 'error';
193
+ throw error;
194
+ }
195
+ }
196
+
197
+ private async ensureConnected(): Promise<void> {
198
+ if (this.client && this.db && this.counters && this.events) return;
199
+
200
+ try {
201
+ this.client = new MongoClient(this.config.uri, {
202
+ appName: 'claude-memory-layer',
203
+ serverSelectionTimeoutMS: 5000
204
+ });
205
+ await this.client.connect();
206
+ this.db = this.client.db(this.config.dbName);
207
+ this.counters = this.db.collection<CounterDoc>('cml_counters');
208
+ this.events = this.db.collection<RemoteEventDoc>('cml_events');
209
+ } catch (err) {
210
+ // Avoid leaking credentials in logs
211
+ const safeUri = redactMongoUri(this.config.uri);
212
+ throw new Error(`MongoDB connection failed (${safeUri}, db=${this.config.dbName}): ${String(err)}`);
213
+ }
214
+ }
215
+
216
+ private async disconnect(): Promise<void> {
217
+ try {
218
+ await this.client?.close();
219
+ } finally {
220
+ this.client = null;
221
+ this.db = null;
222
+ this.counters = null;
223
+ this.events = null;
224
+ this.indexesEnsured = false;
225
+ }
226
+ }
227
+
228
+ private async ensureIndexes(): Promise<void> {
229
+ if (this.indexesEnsured) return;
230
+ if (!this.events || !this.counters) throw new Error('Mongo not connected');
231
+
232
+ // Best-effort: if the user lacks index privileges, sync can still work (slower)
233
+ try {
234
+ await this.events.createIndex({ projectKey: 1, seq: 1 }, { unique: true });
235
+ await this.events.createIndex({ projectKey: 1, eventId: 1 }, { unique: true });
236
+ await this.events.createIndex({ projectKey: 1, dedupeKey: 1 });
237
+ } catch (err) {
238
+ console.warn('[MongoSyncWorker] Failed to ensure indexes (continuing):', err);
239
+ }
240
+
241
+ this.indexesEnsured = true;
242
+ }
243
+
244
+ private counterKey(kind: 'events'): string {
245
+ return `${kind}:${this.config.projectKey}`;
246
+ }
247
+
248
+ private async allocateSeqRange(kind: 'events', count: number): Promise<number> {
249
+ if (!this.counters) throw new Error('Mongo not connected');
250
+ if (count <= 0) return 1;
251
+
252
+ const key = this.counterKey(kind);
253
+ const doc = await this.counters.findOneAndUpdate(
254
+ { _id: key },
255
+ { $inc: { seq: count } },
256
+ { upsert: true, returnDocument: 'after' }
257
+ );
258
+
259
+ const endSeq = doc?.seq;
260
+ if (typeof endSeq !== 'number') {
261
+ throw new Error(`Failed to allocate seq range for ${key}`);
262
+ }
263
+
264
+ return endSeq - count + 1;
265
+ }
266
+
267
+ private pushTargetName(): string {
268
+ return `mongo_push_events_rowid:${this.config.projectKey}`;
269
+ }
270
+
271
+ private pullTargetName(): string {
272
+ return `mongo_pull_events_seq:${this.config.projectKey}`;
273
+ }
274
+
275
+ private async pushEvents(): Promise<number> {
276
+ if (!this.events) throw new Error('Mongo not connected');
277
+
278
+ const position = await this.sqliteStore.getSyncPosition(this.pushTargetName());
279
+ let lastRowid = parseIntOrZero(position.lastEventId);
280
+
281
+ let pushed = 0;
282
+
283
+ while (true) {
284
+ const batch = await this.sqliteStore.getEventsSinceRowid(lastRowid, this.config.batchSize);
285
+ if (batch.length === 0) break;
286
+
287
+ const startSeq = await this.allocateSeqRange('events', batch.length);
288
+ const now = new Date();
289
+ const hostname = os.hostname();
290
+
291
+ const ops = batch.map((item, idx) => {
292
+ const event = item.event as unknown as MemoryEvent;
293
+ const seq = startSeq + idx;
294
+ const docId = `${this.config.projectKey}:${event.id}`;
295
+
296
+ return {
297
+ updateOne: {
298
+ filter: { _id: docId },
299
+ update: {
300
+ $setOnInsert: {
301
+ _id: docId,
302
+ projectKey: this.config.projectKey,
303
+ seq,
304
+ eventId: event.id,
305
+ eventType: event.eventType,
306
+ sessionId: event.sessionId,
307
+ timestamp: event.timestamp,
308
+ content: event.content,
309
+ canonicalKey: event.canonicalKey,
310
+ dedupeKey: event.dedupeKey,
311
+ metadata: event.metadata ?? null,
312
+ insertedAt: now,
313
+ updatedAt: now,
314
+ source: { hostname, instanceId: this.config.instanceId }
315
+ }
316
+ },
317
+ upsert: true
318
+ }
319
+ };
320
+ });
321
+
322
+ await this.events.bulkWrite(ops, { ordered: false });
323
+
324
+ const last = batch[batch.length - 1];
325
+ lastRowid = last.rowid;
326
+ await this.sqliteStore.updateSyncPosition(
327
+ this.pushTargetName(),
328
+ String(lastRowid),
329
+ last.event.timestamp.toISOString()
330
+ );
331
+
332
+ pushed += batch.length;
333
+ if (batch.length < this.config.batchSize) break;
334
+ }
335
+
336
+ return pushed;
337
+ }
338
+
339
+ private async pullEvents(): Promise<number> {
340
+ if (!this.events) throw new Error('Mongo not connected');
341
+
342
+ const position = await this.sqliteStore.getSyncPosition(this.pullTargetName());
343
+ let lastSeq = parseIntOrZero(position.lastEventId);
344
+
345
+ let pulled = 0;
346
+
347
+ while (true) {
348
+ const docs = await this.events.find(
349
+ { projectKey: this.config.projectKey, seq: { $gt: lastSeq } },
350
+ { sort: { seq: 1 }, limit: this.config.batchSize }
351
+ ).toArray();
352
+
353
+ if (docs.length === 0) break;
354
+
355
+ const events: MemoryEvent[] = docs.map((d) => ({
356
+ id: d.eventId,
357
+ eventType: d.eventType as any,
358
+ sessionId: d.sessionId,
359
+ timestamp: d.timestamp instanceof Date ? d.timestamp : new Date(d.timestamp),
360
+ content: d.content,
361
+ canonicalKey: d.canonicalKey,
362
+ dedupeKey: d.dedupeKey,
363
+ metadata: d.metadata ?? undefined
364
+ }));
365
+
366
+ const result = await this.sqliteStore.importEvents(events);
367
+ pulled += result.inserted;
368
+
369
+ lastSeq = docs[docs.length - 1].seq;
370
+ await this.sqliteStore.updateSyncPosition(
371
+ this.pullTargetName(),
372
+ String(lastSeq),
373
+ new Date().toISOString()
374
+ );
375
+
376
+ if (docs.length < this.config.batchSize) break;
377
+ }
378
+
379
+ return pulled;
380
+ }
381
+ }