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.
- package/AGENTS.md +60 -0
- package/README.md +166 -2
- package/bootstrap-kb/decisions/decisions.md +244 -0
- package/bootstrap-kb/glossary/glossary.md +46 -0
- package/bootstrap-kb/modules/.claude-plugin.md +22 -0
- package/bootstrap-kb/modules/agents.md.md +15 -0
- package/bootstrap-kb/modules/claude.md.md +15 -0
- package/bootstrap-kb/modules/context.md.md +15 -0
- package/bootstrap-kb/modules/docs.md +18 -0
- package/bootstrap-kb/modules/handoff.md.md +15 -0
- package/bootstrap-kb/modules/package-lock.json.md +15 -0
- package/bootstrap-kb/modules/package.json.md +15 -0
- package/bootstrap-kb/modules/plan.md.md +15 -0
- package/bootstrap-kb/modules/readme.md.md +15 -0
- package/bootstrap-kb/modules/scripts.md +26 -0
- package/bootstrap-kb/modules/spec.md.md +15 -0
- package/bootstrap-kb/modules/specs.md +20 -0
- package/bootstrap-kb/modules/src.md +51 -0
- package/bootstrap-kb/modules/tests.md +42 -0
- package/bootstrap-kb/modules/tsconfig.json.md +15 -0
- package/bootstrap-kb/modules/vitest.config.ts.md +15 -0
- package/bootstrap-kb/overview/overview.md +40 -0
- package/bootstrap-kb/sources/manifest.json +950 -0
- package/bootstrap-kb/sources/manifest.md +227 -0
- package/bootstrap-kb/timeline/timeline.md +57 -0
- package/d.sh +3 -0
- package/deploy.sh +3 -0
- package/dist/cli/index.js +2389 -286
- package/dist/cli/index.js.map +4 -4
- package/dist/core/index.js +1017 -132
- package/dist/core/index.js.map +4 -4
- package/dist/hooks/post-tool-use.js +1347 -202
- package/dist/hooks/post-tool-use.js.map +4 -4
- package/dist/hooks/session-end.js +1339 -194
- package/dist/hooks/session-end.js.map +4 -4
- package/dist/hooks/session-start.js +1343 -198
- package/dist/hooks/session-start.js.map +4 -4
- package/dist/hooks/stop.js +1351 -206
- package/dist/hooks/stop.js.map +4 -4
- package/dist/hooks/user-prompt-submit.js +1347 -202
- package/dist/hooks/user-prompt-submit.js.map +4 -4
- package/dist/server/api/index.js +1436 -211
- package/dist/server/api/index.js.map +4 -4
- package/dist/server/index.js +1445 -220
- package/dist/server/index.js.map +4 -4
- package/dist/services/memory-service.js +1345 -199
- package/dist/services/memory-service.js.map +4 -4
- package/dist/ui/app.js +69 -2
- package/dist/ui/index.html +8 -0
- package/docs/MCP_MEMORY_SERVICE_COMPARATIVE_REVIEW.md +271 -0
- package/docs/MEMU_ADOPTION.md +40 -0
- package/memory/.claude-plugin/commands/2026-02-25.md +263 -0
- package/memory/_index.md +405 -0
- package/memory/default/uncategorized/2026-02-25.md +4839 -0
- package/memory/specs/20260207-dashboard-upgrade/2026-02-25.md +142 -0
- package/memory/specs/citations-system/2026-02-25.md +1121 -0
- package/memory/specs/endless-mode/2026-02-25.md +1392 -0
- package/memory/specs/entity-edge-model/2026-02-25.md +1263 -0
- package/memory/specs/evidence-aligner-v2/2026-02-25.md +1028 -0
- package/memory/specs/mcp-desktop-integration/2026-02-25.md +1334 -0
- package/memory/specs/post-tool-use-hook/2026-02-25.md +1164 -0
- package/memory/specs/private-tags/2026-02-25.md +1057 -0
- package/memory/specs/progressive-disclosure/2026-02-25.md +1436 -0
- package/memory/specs/task-entity-system/2026-02-25.md +924 -0
- package/memory/specs/vector-outbox-v2/2026-02-25.md +1510 -0
- package/memory/specs/web-viewer-ui/2026-02-25.md +1709 -0
- package/package.json +2 -1
- package/scripts/build.ts +6 -0
- package/src/cli/index.ts +281 -2
- package/src/core/consolidated-store.ts +63 -1
- package/src/core/consolidation-worker.ts +115 -6
- package/src/core/event-store.ts +14 -0
- package/src/core/index.ts +1 -0
- package/src/core/ingest-interceptor.ts +80 -0
- package/src/core/markdown-mirror.ts +70 -0
- package/src/core/md-mirror.ts +92 -0
- package/src/core/mongo-sync-config.ts +165 -0
- package/src/core/mongo-sync-worker.ts +381 -0
- package/src/core/retriever.ts +540 -150
- package/src/core/sqlite-event-store.ts +350 -1
- package/src/core/tag-taxonomy.ts +51 -0
- package/src/core/types.ts +28 -0
- package/src/server/api/health.ts +53 -0
- package/src/server/api/index.ts +3 -1
- package/src/server/api/stats.ts +46 -1
- package/src/services/bootstrap-organizer.ts +443 -0
- package/src/services/codex-session-history-importer.ts +474 -0
- package/src/services/memory-service.ts +373 -68
- package/src/ui/app.js +69 -2
- package/src/ui/index.html +8 -0
- package/tests/bootstrap-organizer.test.ts +111 -0
- package/tests/consolidation-worker.test.ts +75 -0
- package/tests/ingest-interceptor.test.ts +38 -0
- package/tests/markdown-mirror.test.ts +85 -0
- package/tests/md-mirror.test.ts +50 -0
- package/tests/retriever-fallback-chain.test.ts +223 -0
- package/tests/retriever-strategy-scope.test.ts +97 -0
- package/tests/retriever.memu-adoption.test.ts +122 -0
- package/tests/sqlite-event-store-replication.test.ts +92 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { Retriever } from '../src/core/retriever.js';
|
|
3
|
+
import type { MemoryEvent, MatchResult } from '../src/core/types.js';
|
|
4
|
+
import type { SearchResult } from '../src/core/vector-store.js';
|
|
5
|
+
|
|
6
|
+
class FakeEventStore {
|
|
7
|
+
constructor(private readonly events: Record<string, MemoryEvent>) {}
|
|
8
|
+
|
|
9
|
+
async getEvent(id: string): Promise<MemoryEvent | null> {
|
|
10
|
+
return this.events[id] || null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async getSessionEvents(sessionId: string): Promise<MemoryEvent[]> {
|
|
14
|
+
return Object.values(this.events).filter((e) => e.sessionId === sessionId);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async getRecentEvents(): Promise<MemoryEvent[]> {
|
|
18
|
+
return Object.values(this.events);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async keywordSearch(query: string): Promise<Array<{ event: MemoryEvent; rank: number }>> {
|
|
22
|
+
const lowered = query.toLowerCase();
|
|
23
|
+
const matches = Object.values(this.events)
|
|
24
|
+
.filter((event) => event.content.toLowerCase().includes(lowered))
|
|
25
|
+
.map((event, index) => ({ event, rank: -(index + 1) }));
|
|
26
|
+
return matches;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
class FakeVectorStore {
|
|
31
|
+
constructor(private readonly results: SearchResult[]) {}
|
|
32
|
+
|
|
33
|
+
async search(): Promise<SearchResult[]> {
|
|
34
|
+
return this.results;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
class FakeEmbedder {
|
|
39
|
+
async embed(): Promise<{ vector: number[] }> {
|
|
40
|
+
return { vector: [0.1, 0.2, 0.3] };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
class FakeMatcher {
|
|
45
|
+
matchSearchResults(results: SearchResult[]): MatchResult {
|
|
46
|
+
if (results.length === 0) {
|
|
47
|
+
return { match: null, confidence: 'none' };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const top = results[0];
|
|
51
|
+
return {
|
|
52
|
+
match: {
|
|
53
|
+
event: {
|
|
54
|
+
id: top.eventId,
|
|
55
|
+
eventType: top.eventType as MemoryEvent['eventType'],
|
|
56
|
+
sessionId: top.sessionId,
|
|
57
|
+
timestamp: new Date(top.timestamp),
|
|
58
|
+
content: top.content,
|
|
59
|
+
canonicalKey: top.eventId,
|
|
60
|
+
dedupeKey: top.eventId,
|
|
61
|
+
metadata: {}
|
|
62
|
+
},
|
|
63
|
+
score: top.score
|
|
64
|
+
},
|
|
65
|
+
confidence: 'suggested'
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function makeEvent(id: string, content: string, metadata?: Record<string, unknown>): MemoryEvent {
|
|
71
|
+
return {
|
|
72
|
+
id,
|
|
73
|
+
eventType: 'user_prompt',
|
|
74
|
+
sessionId: 's1',
|
|
75
|
+
timestamp: new Date('2026-01-01T00:00:00.000Z'),
|
|
76
|
+
content,
|
|
77
|
+
canonicalKey: id,
|
|
78
|
+
dedupeKey: id,
|
|
79
|
+
metadata
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
describe('Retriever memU-inspired enhancements', () => {
|
|
84
|
+
it('applies metadata scope filter with hierarchical key path', async () => {
|
|
85
|
+
const e1 = makeEvent('e1', 'first memory', { scope: { project: { id: 'alpha' } } });
|
|
86
|
+
const e2 = makeEvent('e2', 'second memory', { scope: { project: { id: 'beta' } } });
|
|
87
|
+
|
|
88
|
+
const retriever = new Retriever(
|
|
89
|
+
new FakeEventStore({ e1, e2 }) as any,
|
|
90
|
+
new FakeVectorStore([
|
|
91
|
+
{ id: '1', eventId: 'e1', content: e1.content, score: 0.9, sessionId: 's1', eventType: e1.eventType, timestamp: e1.timestamp.toISOString() },
|
|
92
|
+
{ id: '2', eventId: 'e2', content: e2.content, score: 0.89, sessionId: 's1', eventType: e2.eventType, timestamp: e2.timestamp.toISOString() }
|
|
93
|
+
]) as any,
|
|
94
|
+
new FakeEmbedder() as any,
|
|
95
|
+
new FakeMatcher() as any
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const result = await retriever.retrieve('memory', {
|
|
99
|
+
scope: { metadata: { 'scope.project.id': 'alpha' } }
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
expect(result.memories).toHaveLength(1);
|
|
103
|
+
expect(result.memories[0].event.id).toBe('e1');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('uses fast strategy keyword retrieval when requested', async () => {
|
|
107
|
+
const e1 = makeEvent('e1', 'fix deployment issue with nginx');
|
|
108
|
+
const e2 = makeEvent('e2', 'random unrelated text');
|
|
109
|
+
|
|
110
|
+
const retriever = new Retriever(
|
|
111
|
+
new FakeEventStore({ e1, e2 }) as any,
|
|
112
|
+
new FakeVectorStore([]) as any,
|
|
113
|
+
new FakeEmbedder() as any,
|
|
114
|
+
new FakeMatcher() as any
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
const result = await retriever.retrieve('deployment', { strategy: 'fast', topK: 5 });
|
|
118
|
+
|
|
119
|
+
expect(result.memories.length).toBeGreaterThan(0);
|
|
120
|
+
expect(result.memories[0].event.id).toBe('e1');
|
|
121
|
+
});
|
|
122
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for SQLiteEventStore replication helpers used by Mongo sync.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
import * as os from 'os';
|
|
9
|
+
|
|
10
|
+
import { SQLiteEventStore } from '../src/core/sqlite-event-store.js';
|
|
11
|
+
|
|
12
|
+
describe('SQLiteEventStore replication helpers', () => {
|
|
13
|
+
let tempDir: string;
|
|
14
|
+
let storeA: SQLiteEventStore;
|
|
15
|
+
let storeB: SQLiteEventStore;
|
|
16
|
+
|
|
17
|
+
beforeEach(async () => {
|
|
18
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-memory-layer-test-'));
|
|
19
|
+
storeA = new SQLiteEventStore(path.join(tempDir, 'a.sqlite'));
|
|
20
|
+
storeB = new SQLiteEventStore(path.join(tempDir, 'b.sqlite'));
|
|
21
|
+
await storeA.initialize();
|
|
22
|
+
await storeB.initialize();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
try { storeA.close(); } catch {}
|
|
27
|
+
try { storeB.close(); } catch {}
|
|
28
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('getEventsSinceRowid returns incremental batches in insertion order', async () => {
|
|
32
|
+
const sessionId = 'session-1';
|
|
33
|
+
|
|
34
|
+
await storeA.append({ eventType: 'user_prompt', sessionId, timestamp: new Date(), content: 'a' });
|
|
35
|
+
await storeA.append({ eventType: 'user_prompt', sessionId, timestamp: new Date(), content: 'b' });
|
|
36
|
+
await storeA.append({ eventType: 'user_prompt', sessionId, timestamp: new Date(), content: 'c' });
|
|
37
|
+
|
|
38
|
+
const batch1 = await storeA.getEventsSinceRowid(0, 10);
|
|
39
|
+
expect(batch1).toHaveLength(3);
|
|
40
|
+
expect(batch1.map(x => x.event.content)).toEqual(['a', 'b', 'c']);
|
|
41
|
+
|
|
42
|
+
// rowid should be strictly increasing
|
|
43
|
+
expect(batch1[0].rowid).toBeLessThan(batch1[1].rowid);
|
|
44
|
+
expect(batch1[1].rowid).toBeLessThan(batch1[2].rowid);
|
|
45
|
+
|
|
46
|
+
const lastRowid = batch1[2].rowid;
|
|
47
|
+
|
|
48
|
+
await storeA.append({ eventType: 'user_prompt', sessionId, timestamp: new Date(), content: 'd' });
|
|
49
|
+
|
|
50
|
+
const batch2 = await storeA.getEventsSinceRowid(lastRowid, 10);
|
|
51
|
+
expect(batch2).toHaveLength(1);
|
|
52
|
+
expect(batch2[0].event.content).toBe('d');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('importEvents preserves stable IDs and is idempotent via dedupeKey', async () => {
|
|
56
|
+
const sessionId = 'session-2';
|
|
57
|
+
const appendRes = await storeA.append({
|
|
58
|
+
eventType: 'user_prompt',
|
|
59
|
+
sessionId,
|
|
60
|
+
timestamp: new Date(),
|
|
61
|
+
content: 'hello world'
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
expect(appendRes.success).toBe(true);
|
|
65
|
+
const sourceEvent = await storeA.getEvent(appendRes.eventId!);
|
|
66
|
+
expect(sourceEvent).not.toBeNull();
|
|
67
|
+
|
|
68
|
+
const imported1 = await storeB.importEvents([sourceEvent!]);
|
|
69
|
+
expect(imported1.inserted).toBe(1);
|
|
70
|
+
expect(imported1.skipped).toBe(0);
|
|
71
|
+
|
|
72
|
+
const importedEvent = await storeB.getEvent(sourceEvent!.id);
|
|
73
|
+
expect(importedEvent?.content).toBe('hello world');
|
|
74
|
+
|
|
75
|
+
// Importing again should be a no-op
|
|
76
|
+
const imported2 = await storeB.importEvents([sourceEvent!]);
|
|
77
|
+
expect(imported2.inserted).toBe(0);
|
|
78
|
+
expect(imported2.skipped).toBe(1);
|
|
79
|
+
|
|
80
|
+
// append() should treat it as duplicate due to event_dedup entry
|
|
81
|
+
const dup = await storeB.append({
|
|
82
|
+
eventType: 'user_prompt',
|
|
83
|
+
sessionId,
|
|
84
|
+
timestamp: new Date(),
|
|
85
|
+
content: 'hello world'
|
|
86
|
+
});
|
|
87
|
+
expect(dup.success).toBe(true);
|
|
88
|
+
expect(dup.isDuplicate).toBe(true);
|
|
89
|
+
expect(dup.eventId).toBe(sourceEvent!.id);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|