@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.
Files changed (116) hide show
  1. package/.factory/commands/opsx-apply.md +150 -0
  2. package/.factory/commands/opsx-archive.md +155 -0
  3. package/.factory/commands/opsx-explore.md +171 -0
  4. package/.factory/commands/opsx-propose.md +104 -0
  5. package/.factory/skills/openspec-apply-change/SKILL.md +156 -0
  6. package/.factory/skills/openspec-archive-change/SKILL.md +114 -0
  7. package/.factory/skills/openspec-explore/SKILL.md +288 -0
  8. package/.factory/skills/openspec-propose/SKILL.md +110 -0
  9. package/.github/workflows/ci.yml +48 -12
  10. package/.github/workflows/release.yml +67 -0
  11. package/AGENTS.md +158 -17
  12. package/README.md +147 -66
  13. package/README.zh-CN.md +75 -23
  14. package/dist/src/cli/agents/claude-code.d.ts +5 -0
  15. package/dist/src/cli/agents/claude-code.d.ts.map +1 -0
  16. package/dist/src/cli/agents/claude-code.js +14 -0
  17. package/dist/src/cli/agents/claude-code.js.map +1 -0
  18. package/dist/src/cli/agents/cline.d.ts +5 -0
  19. package/dist/src/cli/agents/cline.d.ts.map +1 -0
  20. package/dist/src/cli/agents/cline.js +14 -0
  21. package/dist/src/cli/agents/cline.js.map +1 -0
  22. package/dist/src/cli/agents/codex.d.ts +5 -0
  23. package/dist/src/cli/agents/codex.d.ts.map +1 -0
  24. package/dist/src/cli/agents/codex.js +14 -0
  25. package/dist/src/cli/agents/codex.js.map +1 -0
  26. package/dist/src/cli/agents/cursor.d.ts +5 -0
  27. package/dist/src/cli/agents/cursor.d.ts.map +1 -0
  28. package/dist/src/cli/agents/cursor.js +14 -0
  29. package/dist/src/cli/agents/cursor.js.map +1 -0
  30. package/dist/src/cli/agents/factory-droid.d.ts +5 -0
  31. package/dist/src/cli/agents/factory-droid.d.ts.map +1 -0
  32. package/dist/src/cli/agents/factory-droid.js +14 -0
  33. package/dist/src/cli/agents/factory-droid.js.map +1 -0
  34. package/dist/src/cli/agents/gemini-cli.d.ts +5 -0
  35. package/dist/src/cli/agents/gemini-cli.d.ts.map +1 -0
  36. package/dist/src/cli/agents/gemini-cli.js +14 -0
  37. package/dist/src/cli/agents/gemini-cli.js.map +1 -0
  38. package/dist/src/cli/agents/index.d.ts +14 -0
  39. package/dist/src/cli/agents/index.d.ts.map +1 -0
  40. package/dist/src/cli/agents/index.js +30 -0
  41. package/dist/src/cli/agents/index.js.map +1 -0
  42. package/dist/src/cli/agents/windsurf.d.ts +5 -0
  43. package/dist/src/cli/agents/windsurf.d.ts.map +1 -0
  44. package/dist/src/cli/agents/windsurf.js +14 -0
  45. package/dist/src/cli/agents/windsurf.js.map +1 -0
  46. package/dist/src/cli/index.d.ts +8 -0
  47. package/dist/src/cli/index.d.ts.map +1 -0
  48. package/dist/src/cli/index.js +168 -0
  49. package/dist/src/cli/index.js.map +1 -0
  50. package/dist/src/cli/init.d.ts +34 -0
  51. package/dist/src/cli/init.d.ts.map +1 -0
  52. package/dist/src/cli/init.js +160 -0
  53. package/dist/src/cli/init.js.map +1 -0
  54. package/dist/src/cli/instructions.d.ts +29 -0
  55. package/dist/src/cli/instructions.d.ts.map +1 -0
  56. package/dist/src/cli/instructions.js +141 -0
  57. package/dist/src/cli/instructions.js.map +1 -0
  58. package/dist/src/cli/types.d.ts +22 -0
  59. package/dist/src/cli/types.d.ts.map +1 -0
  60. package/dist/src/cli/types.js +86 -0
  61. package/dist/src/cli/types.js.map +1 -0
  62. package/dist/src/contracts/schemas.js.map +1 -1
  63. package/dist/src/server/mcp-server.d.ts +8 -0
  64. package/dist/src/server/mcp-server.d.ts.map +1 -1
  65. package/dist/src/server/mcp-server.js +30 -16
  66. package/dist/src/server/mcp-server.js.map +1 -1
  67. package/dist/src/services/embedding-service.d.ts.map +1 -1
  68. package/dist/src/services/embedding-service.js +1 -1
  69. package/dist/src/services/embedding-service.js.map +1 -1
  70. package/dist/src/services/memory-service.d.ts +1 -0
  71. package/dist/src/services/memory-service.d.ts.map +1 -1
  72. package/dist/src/services/memory-service.js +125 -82
  73. package/dist/src/services/memory-service.js.map +1 -1
  74. package/dist/src/storage/markdown-storage.d.ts.map +1 -1
  75. package/dist/src/storage/markdown-storage.js +1 -1
  76. package/dist/src/storage/markdown-storage.js.map +1 -1
  77. package/dist/src/storage/vector-index.d.ts.map +1 -1
  78. package/dist/src/storage/vector-index.js +4 -5
  79. package/dist/src/storage/vector-index.js.map +1 -1
  80. package/docs/README.md +21 -0
  81. package/docs/mcp-tools.md +136 -0
  82. package/docs/user-guide.md +182 -0
  83. package/package.json +22 -19
  84. package/src/cli/agents/claude-code.ts +14 -0
  85. package/src/cli/agents/cline.ts +14 -0
  86. package/src/cli/agents/codex.ts +14 -0
  87. package/src/cli/agents/cursor.ts +14 -0
  88. package/src/cli/agents/factory-droid.ts +14 -0
  89. package/src/cli/agents/gemini-cli.ts +14 -0
  90. package/src/cli/agents/index.ts +36 -0
  91. package/src/cli/agents/windsurf.ts +14 -0
  92. package/src/cli/index.ts +192 -0
  93. package/src/cli/init.ts +218 -0
  94. package/src/cli/instructions.ts +156 -0
  95. package/src/cli/types.ts +112 -0
  96. package/src/contracts/mcp.ts +1 -1
  97. package/src/contracts/schemas.ts +4 -4
  98. package/src/contracts/types.ts +4 -4
  99. package/src/server/mcp-server.ts +36 -29
  100. package/src/services/embedding-service.ts +80 -80
  101. package/src/services/memory-service.ts +142 -107
  102. package/src/storage/markdown-storage.ts +1 -9
  103. package/src/storage/vector-index.ts +117 -118
  104. package/test/cli/init.test.ts +380 -0
  105. package/test/server/mcp-server.test.ts +45 -3
  106. package/test/services/memory-service.test.ts +16 -4
  107. package/test/storage/frontmatter-parser.test.ts +1 -1
  108. package/test/storage/markdown-storage.test.ts +19 -10
  109. package/test/storage/vector-index.test.ts +129 -133
  110. package/test/utils/slugify.test.ts +5 -1
  111. package/docs/architecture.md +0 -349
  112. package/docs/contracts.md +0 -119
  113. package/docs/prompt-template.md +0 -79
  114. package/docs/proposals/mcp-typescript-sdk-refactor.md +0 -568
  115. package/docs/proposals/proposal-close-gates.md +0 -58
  116. 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(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i);
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({ title: 'Work 1', content: 'Content', category: 'work', tags: ['project'] });
172
- await memoryService.create({ title: 'Work 2', content: 'Content', category: 'work', tags: ['meeting'] });
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
+ });
@@ -220,4 +220,4 @@ describe('frontMatterToMemory', () => {
220
220
  expect(result.title).toBe('Title');
221
221
  expect(result.content).toBe('Content');
222
222
  });
223
- });
223
+ });
@@ -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
- storage.read('550e8400-e29b-41d4-a716-446655440000')
108
- ).rejects.toThrow(StorageError);
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
- storage.delete('550e8400-e29b-41d4-a716-446655440000')
162
- ).rejects.toThrow(StorageError);
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(join(tempDir, '2024-03-15-a.md'), '---\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');
170
- writeFileSync(join(tempDir, '2024-03-16-b.md'), '---\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');
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(join(tempDir, 'test.md'), '---\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');
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
- 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);
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
- 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
- };
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
- 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 = [
138
- index.initialize(),
139
- index.initialize(),
140
- index.initialize(),
141
- ];
142
- await expect(Promise.all(promises)).resolves.not.toThrow();
143
- });
144
-
145
- it('should persist data across instances', async () => {
146
- const memory = makeMemory({ id: 'persistent-id' });
147
- await index.upsert(memory, randomVec());
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', ['hello-world', 'hello-world-1', 'hello-world-2']);
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
  });