@synth-coder/memhub 0.2.2 → 0.2.4
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/.factory/commands/opsx-apply.md +150 -0
- package/.factory/commands/opsx-archive.md +155 -0
- package/.factory/commands/opsx-explore.md +171 -0
- package/.factory/commands/opsx-propose.md +104 -0
- package/.factory/skills/openspec-apply-change/SKILL.md +156 -0
- package/.factory/skills/openspec-archive-change/SKILL.md +114 -0
- package/.factory/skills/openspec-explore/SKILL.md +288 -0
- package/.factory/skills/openspec-propose/SKILL.md +110 -0
- package/.github/workflows/ci.yml +48 -12
- package/.github/workflows/release.yml +67 -0
- package/AGENTS.md +158 -17
- package/README.md +147 -66
- package/README.zh-CN.md +75 -23
- package/dist/src/cli/agents/claude-code.d.ts +5 -0
- package/dist/src/cli/agents/claude-code.d.ts.map +1 -0
- package/dist/src/cli/agents/claude-code.js +14 -0
- package/dist/src/cli/agents/claude-code.js.map +1 -0
- package/dist/src/cli/agents/cline.d.ts +5 -0
- package/dist/src/cli/agents/cline.d.ts.map +1 -0
- package/dist/src/cli/agents/cline.js +14 -0
- package/dist/src/cli/agents/cline.js.map +1 -0
- package/dist/src/cli/agents/codex.d.ts +5 -0
- package/dist/src/cli/agents/codex.d.ts.map +1 -0
- package/dist/src/cli/agents/codex.js +14 -0
- package/dist/src/cli/agents/codex.js.map +1 -0
- package/dist/src/cli/agents/cursor.d.ts +5 -0
- package/dist/src/cli/agents/cursor.d.ts.map +1 -0
- package/dist/src/cli/agents/cursor.js +14 -0
- package/dist/src/cli/agents/cursor.js.map +1 -0
- package/dist/src/cli/agents/factory-droid.d.ts +5 -0
- package/dist/src/cli/agents/factory-droid.d.ts.map +1 -0
- package/dist/src/cli/agents/factory-droid.js +14 -0
- package/dist/src/cli/agents/factory-droid.js.map +1 -0
- package/dist/src/cli/agents/gemini-cli.d.ts +5 -0
- package/dist/src/cli/agents/gemini-cli.d.ts.map +1 -0
- package/dist/src/cli/agents/gemini-cli.js +14 -0
- package/dist/src/cli/agents/gemini-cli.js.map +1 -0
- package/dist/src/cli/agents/index.d.ts +14 -0
- package/dist/src/cli/agents/index.d.ts.map +1 -0
- package/dist/src/cli/agents/index.js +30 -0
- package/dist/src/cli/agents/index.js.map +1 -0
- package/dist/src/cli/agents/windsurf.d.ts +5 -0
- package/dist/src/cli/agents/windsurf.d.ts.map +1 -0
- package/dist/src/cli/agents/windsurf.js +14 -0
- package/dist/src/cli/agents/windsurf.js.map +1 -0
- package/dist/src/cli/index.d.ts +8 -0
- package/dist/src/cli/index.d.ts.map +1 -0
- package/dist/src/cli/index.js +168 -0
- package/dist/src/cli/index.js.map +1 -0
- package/dist/src/cli/init.d.ts +34 -0
- package/dist/src/cli/init.d.ts.map +1 -0
- package/dist/src/cli/init.js +160 -0
- package/dist/src/cli/init.js.map +1 -0
- package/dist/src/cli/instructions.d.ts +29 -0
- package/dist/src/cli/instructions.d.ts.map +1 -0
- package/dist/src/cli/instructions.js +141 -0
- package/dist/src/cli/instructions.js.map +1 -0
- package/dist/src/cli/types.d.ts +22 -0
- package/dist/src/cli/types.d.ts.map +1 -0
- package/dist/src/cli/types.js +86 -0
- package/dist/src/cli/types.js.map +1 -0
- package/dist/src/contracts/schemas.js.map +1 -1
- package/dist/src/server/mcp-server.d.ts +8 -0
- package/dist/src/server/mcp-server.d.ts.map +1 -1
- package/dist/src/server/mcp-server.js +30 -16
- package/dist/src/server/mcp-server.js.map +1 -1
- package/dist/src/services/embedding-service.d.ts.map +1 -1
- package/dist/src/services/embedding-service.js +1 -1
- package/dist/src/services/embedding-service.js.map +1 -1
- package/dist/src/services/memory-service.d.ts +1 -0
- package/dist/src/services/memory-service.d.ts.map +1 -1
- package/dist/src/services/memory-service.js +125 -82
- package/dist/src/services/memory-service.js.map +1 -1
- package/dist/src/storage/markdown-storage.d.ts.map +1 -1
- package/dist/src/storage/markdown-storage.js +1 -1
- package/dist/src/storage/markdown-storage.js.map +1 -1
- package/dist/src/storage/vector-index.d.ts.map +1 -1
- package/dist/src/storage/vector-index.js +4 -5
- package/dist/src/storage/vector-index.js.map +1 -1
- package/docs/README.md +21 -0
- package/docs/mcp-tools.md +136 -0
- package/docs/user-guide.md +182 -0
- package/package.json +22 -19
- package/src/cli/agents/claude-code.ts +14 -0
- package/src/cli/agents/cline.ts +14 -0
- package/src/cli/agents/codex.ts +14 -0
- package/src/cli/agents/cursor.ts +14 -0
- package/src/cli/agents/factory-droid.ts +14 -0
- package/src/cli/agents/gemini-cli.ts +14 -0
- package/src/cli/agents/index.ts +36 -0
- package/src/cli/agents/windsurf.ts +14 -0
- package/src/cli/index.ts +192 -0
- package/src/cli/init.ts +218 -0
- package/src/cli/instructions.ts +156 -0
- package/src/cli/types.ts +112 -0
- package/src/contracts/mcp.ts +1 -1
- package/src/contracts/schemas.ts +4 -4
- package/src/contracts/types.ts +4 -4
- package/src/server/mcp-server.ts +36 -29
- package/src/services/embedding-service.ts +80 -80
- package/src/services/memory-service.ts +142 -107
- package/src/storage/markdown-storage.ts +1 -9
- package/src/storage/vector-index.ts +117 -118
- package/test/cli/init.test.ts +380 -0
- package/test/server/mcp-server.test.ts +45 -3
- package/test/services/memory-service.test.ts +16 -4
- package/test/storage/frontmatter-parser.test.ts +1 -1
- package/test/storage/markdown-storage.test.ts +19 -10
- package/test/storage/vector-index.test.ts +129 -133
- package/test/utils/slugify.test.ts +5 -1
- package/docs/architecture.md +0 -349
- package/docs/contracts.md +0 -119
- package/docs/prompt-template.md +0 -79
- package/docs/proposals/mcp-typescript-sdk-refactor.md +0 -568
- package/docs/proposals/proposal-close-gates.md +0 -58
- package/docs/tool-calling-policy.md +0 -107
|
@@ -37,7 +37,9 @@ describe('MemoryService', () => {
|
|
|
37
37
|
|
|
38
38
|
const result = await memoryService.create(input);
|
|
39
39
|
expect(result.id).toBeDefined();
|
|
40
|
-
expect(result.id).toMatch(
|
|
40
|
+
expect(result.id).toMatch(
|
|
41
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
|
|
42
|
+
);
|
|
41
43
|
});
|
|
42
44
|
|
|
43
45
|
it('should create memory file with correct format', async () => {
|
|
@@ -168,8 +170,18 @@ describe('MemoryService', () => {
|
|
|
168
170
|
|
|
169
171
|
describe('list', () => {
|
|
170
172
|
beforeEach(async () => {
|
|
171
|
-
await memoryService.create({
|
|
172
|
-
|
|
173
|
+
await memoryService.create({
|
|
174
|
+
title: 'Work 1',
|
|
175
|
+
content: 'Content',
|
|
176
|
+
category: 'work',
|
|
177
|
+
tags: ['project'],
|
|
178
|
+
});
|
|
179
|
+
await memoryService.create({
|
|
180
|
+
title: 'Work 2',
|
|
181
|
+
content: 'Content',
|
|
182
|
+
category: 'work',
|
|
183
|
+
tags: ['meeting'],
|
|
184
|
+
});
|
|
173
185
|
await memoryService.create({ title: 'Personal', content: 'Content', category: 'personal' });
|
|
174
186
|
});
|
|
175
187
|
|
|
@@ -276,4 +288,4 @@ describe('MemoryService', () => {
|
|
|
276
288
|
expect(result.tags).toEqual([]);
|
|
277
289
|
});
|
|
278
290
|
});
|
|
279
|
-
});
|
|
291
|
+
});
|
|
@@ -103,9 +103,9 @@ This is the content.
|
|
|
103
103
|
});
|
|
104
104
|
|
|
105
105
|
it('should throw error when file not found', async () => {
|
|
106
|
-
await expect(
|
|
107
|
-
|
|
108
|
-
)
|
|
106
|
+
await expect(storage.read('550e8400-e29b-41d4-a716-446655440000')).rejects.toThrow(
|
|
107
|
+
StorageError
|
|
108
|
+
);
|
|
109
109
|
});
|
|
110
110
|
|
|
111
111
|
it('should parse front matter correctly', async () => {
|
|
@@ -157,17 +157,23 @@ importance: 3
|
|
|
157
157
|
});
|
|
158
158
|
|
|
159
159
|
it('should throw error when file not found', async () => {
|
|
160
|
-
await expect(
|
|
161
|
-
|
|
162
|
-
)
|
|
160
|
+
await expect(storage.delete('550e8400-e29b-41d4-a716-446655440000')).rejects.toThrow(
|
|
161
|
+
StorageError
|
|
162
|
+
);
|
|
163
163
|
});
|
|
164
164
|
});
|
|
165
165
|
|
|
166
166
|
describe('list', () => {
|
|
167
167
|
it('should list all memory files', async () => {
|
|
168
168
|
const { writeFileSync } = await import('fs');
|
|
169
|
-
writeFileSync(
|
|
170
|
-
|
|
169
|
+
writeFileSync(
|
|
170
|
+
join(tempDir, '2024-03-15-a.md'),
|
|
171
|
+
'---\nid: "a"\ncreated_at: "2024-03-15T10:30:00Z"\nupdated_at: "2024-03-15T10:30:00Z"\ntags: []\ncategory: "general"\nimportance: 3\n---\n\n# A'
|
|
172
|
+
);
|
|
173
|
+
writeFileSync(
|
|
174
|
+
join(tempDir, '2024-03-16-b.md'),
|
|
175
|
+
'---\nid: "b"\ncreated_at: "2024-03-16T10:30:00Z"\nupdated_at: "2024-03-16T10:30:00Z"\ntags: []\ncategory: "general"\nimportance: 3\n---\n\n# B'
|
|
176
|
+
);
|
|
171
177
|
|
|
172
178
|
const files = await storage.list();
|
|
173
179
|
expect(files).toHaveLength(2);
|
|
@@ -180,7 +186,10 @@ importance: 3
|
|
|
180
186
|
|
|
181
187
|
it('should only include .md files', async () => {
|
|
182
188
|
const { writeFileSync } = await import('fs');
|
|
183
|
-
writeFileSync(
|
|
189
|
+
writeFileSync(
|
|
190
|
+
join(tempDir, 'test.md'),
|
|
191
|
+
'---\nid: "test"\ncreated_at: "2024-03-15T10:30:00Z"\nupdated_at: "2024-03-15T10:30:00Z"\ntags: []\ncategory: "general"\nimportance: 3\n---\n\n# Test'
|
|
192
|
+
);
|
|
184
193
|
writeFileSync(join(tempDir, 'test.txt'), 'not markdown');
|
|
185
194
|
|
|
186
195
|
const files = await storage.list();
|
|
@@ -214,4 +223,4 @@ importance: 3
|
|
|
214
223
|
expect(filePath).toBeNull();
|
|
215
224
|
});
|
|
216
225
|
});
|
|
217
|
-
});
|
|
226
|
+
});
|
|
@@ -11,143 +11,139 @@ import type { Memory } from '../../src/contracts/types.js';
|
|
|
11
11
|
|
|
12
12
|
/** Build a random 384-dim float vector (avoids loading the real model) */
|
|
13
13
|
function randomVec(dim = 384): number[] {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
const vec = Array.from({ length: dim }, () => Math.random() * 2 - 1);
|
|
15
|
+
// L2-normalise so cosine distance is meaningful
|
|
16
|
+
const norm = Math.sqrt(vec.reduce((s, v) => s + v * v, 0));
|
|
17
|
+
return vec.map(v => v / norm);
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
function makeMemory(overrides: Partial<Memory> = {}): Memory {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
21
|
+
return {
|
|
22
|
+
id: 'test-id-' + Math.random().toString(36).slice(2),
|
|
23
|
+
createdAt: new Date().toISOString(),
|
|
24
|
+
updatedAt: new Date().toISOString(),
|
|
25
|
+
tags: [],
|
|
26
|
+
category: 'general',
|
|
27
|
+
importance: 3,
|
|
28
|
+
title: 'Test Memory',
|
|
29
|
+
content: 'Test content',
|
|
30
|
+
...overrides,
|
|
31
|
+
};
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
describe('VectorIndex', () => {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
// Create a new instance pointing to the same directory
|
|
150
|
-
const newIndex = new VectorIndex(tempDir);
|
|
151
|
-
expect(await newIndex.count()).toBe(1);
|
|
152
|
-
});
|
|
35
|
+
let tempDir: string;
|
|
36
|
+
let index: VectorIndex;
|
|
37
|
+
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
tempDir = mkdtempSync(join(tmpdir(), 'memhub-vec-test-'));
|
|
40
|
+
index = new VectorIndex(tempDir);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
afterEach(() => {
|
|
44
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should initialize without errors', async () => {
|
|
48
|
+
await expect(index.initialize()).resolves.not.toThrow();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should report 0 rows after initialization', async () => {
|
|
52
|
+
await index.initialize();
|
|
53
|
+
expect(await index.count()).toBe(0);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should upsert a memory and increase count', async () => {
|
|
57
|
+
const memory = makeMemory();
|
|
58
|
+
const vec = randomVec();
|
|
59
|
+
await index.upsert(memory, vec);
|
|
60
|
+
expect(await index.count()).toBe(1);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should overwrite existing row on upsert (same id)', async () => {
|
|
64
|
+
const memory = makeMemory({ id: 'fixed-id' });
|
|
65
|
+
await index.upsert(memory, randomVec());
|
|
66
|
+
await index.upsert({ ...memory, title: 'Updated' }, randomVec());
|
|
67
|
+
expect(await index.count()).toBe(1);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should delete a row by id', async () => {
|
|
71
|
+
const memory = makeMemory();
|
|
72
|
+
await index.upsert(memory, randomVec());
|
|
73
|
+
await index.delete(memory.id);
|
|
74
|
+
expect(await index.count()).toBe(0);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should not throw when deleting non-existent id', async () => {
|
|
78
|
+
await expect(index.delete('non-existent')).resolves.not.toThrow();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should search and return results', async () => {
|
|
82
|
+
const vec = randomVec();
|
|
83
|
+
const memory = makeMemory({ id: 'searchable-id' });
|
|
84
|
+
await index.upsert(memory, vec);
|
|
85
|
+
|
|
86
|
+
// Searching with the same vector should return that memory as top result
|
|
87
|
+
const results = await index.search(vec, 5);
|
|
88
|
+
expect(results.length).toBeGreaterThan(0);
|
|
89
|
+
expect(results[0].id).toBe('searchable-id');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should return empty results when index is empty', async () => {
|
|
93
|
+
const results = await index.search(randomVec(), 5);
|
|
94
|
+
expect(results).toHaveLength(0);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should respect the limit parameter', async () => {
|
|
98
|
+
for (let i = 0; i < 5; i++) {
|
|
99
|
+
await index.upsert(makeMemory(), randomVec());
|
|
100
|
+
}
|
|
101
|
+
const results = await index.search(randomVec(), 3);
|
|
102
|
+
expect(results.length).toBeLessThanOrEqual(3);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should return _distance field in results', async () => {
|
|
106
|
+
const vec = randomVec();
|
|
107
|
+
await index.upsert(makeMemory(), vec);
|
|
108
|
+
const results = await index.search(vec, 1);
|
|
109
|
+
expect(results[0]._distance).toBeDefined();
|
|
110
|
+
expect(typeof results[0]._distance).toBe('number');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('should handle id with single quotes (SQL injection prevention)', async () => {
|
|
114
|
+
const maliciousId = "test-' OR '1'='1";
|
|
115
|
+
const memory = makeMemory({ id: maliciousId });
|
|
116
|
+
await index.upsert(memory, randomVec());
|
|
117
|
+
expect(await index.count()).toBe(1);
|
|
118
|
+
|
|
119
|
+
// Should be able to delete by the same id
|
|
120
|
+
await index.delete(maliciousId);
|
|
121
|
+
expect(await index.count()).toBe(0);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should handle id with special characters', async () => {
|
|
125
|
+
const specialId = "id-with-'quotes'-and-more";
|
|
126
|
+
const memory = makeMemory({ id: specialId });
|
|
127
|
+
await index.upsert(memory, randomVec());
|
|
128
|
+
expect(await index.count()).toBe(1);
|
|
129
|
+
|
|
130
|
+
await index.delete(specialId);
|
|
131
|
+
expect(await index.count()).toBe(0);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Error scenario tests
|
|
135
|
+
it('should allow concurrent initialization calls', async () => {
|
|
136
|
+
// Multiple concurrent init calls should not cause issues
|
|
137
|
+
const promises = [index.initialize(), index.initialize(), index.initialize()];
|
|
138
|
+
await expect(Promise.all(promises)).resolves.not.toThrow();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should persist data across instances', async () => {
|
|
142
|
+
const memory = makeMemory({ id: 'persistent-id' });
|
|
143
|
+
await index.upsert(memory, randomVec());
|
|
144
|
+
|
|
145
|
+
// Create a new instance pointing to the same directory
|
|
146
|
+
const newIndex = new VectorIndex(tempDir);
|
|
147
|
+
expect(await newIndex.count()).toBe(1);
|
|
148
|
+
});
|
|
153
149
|
});
|
|
@@ -62,7 +62,11 @@ describe('generateUniqueSlug', () => {
|
|
|
62
62
|
});
|
|
63
63
|
|
|
64
64
|
it('should increment counter until unique', () => {
|
|
65
|
-
const result = generateUniqueSlug('hello world', [
|
|
65
|
+
const result = generateUniqueSlug('hello world', [
|
|
66
|
+
'hello-world',
|
|
67
|
+
'hello-world-1',
|
|
68
|
+
'hello-world-2',
|
|
69
|
+
]);
|
|
66
70
|
expect(result).toBe('hello-world-3');
|
|
67
71
|
});
|
|
68
72
|
});
|