@synth-coder/memhub 0.2.2 → 0.2.3

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 (71) hide show
  1. package/.eslintrc.cjs +45 -45
  2. package/.factory/commands/opsx-apply.md +150 -0
  3. package/.factory/commands/opsx-archive.md +155 -0
  4. package/.factory/commands/opsx-explore.md +171 -0
  5. package/.factory/commands/opsx-propose.md +104 -0
  6. package/.factory/skills/openspec-apply-change/SKILL.md +156 -0
  7. package/.factory/skills/openspec-archive-change/SKILL.md +114 -0
  8. package/.factory/skills/openspec-explore/SKILL.md +288 -0
  9. package/.factory/skills/openspec-propose/SKILL.md +110 -0
  10. package/.github/workflows/ci.yml +74 -74
  11. package/.iflow/commands/opsx-apply.md +152 -152
  12. package/.iflow/commands/opsx-archive.md +157 -157
  13. package/.iflow/commands/opsx-explore.md +173 -173
  14. package/.iflow/commands/opsx-propose.md +106 -106
  15. package/.iflow/skills/openspec-apply-change/SKILL.md +156 -156
  16. package/.iflow/skills/openspec-archive-change/SKILL.md +114 -114
  17. package/.iflow/skills/openspec-explore/SKILL.md +288 -288
  18. package/.iflow/skills/openspec-propose/SKILL.md +110 -110
  19. package/.prettierrc +11 -11
  20. package/AGENTS.md +169 -26
  21. package/README.md +195 -195
  22. package/README.zh-CN.md +193 -193
  23. package/dist/src/contracts/mcp.js +34 -34
  24. package/dist/src/server/mcp-server.d.ts +8 -0
  25. package/dist/src/server/mcp-server.d.ts.map +1 -1
  26. package/dist/src/server/mcp-server.js +23 -2
  27. package/dist/src/server/mcp-server.js.map +1 -1
  28. package/dist/src/services/memory-service.d.ts +1 -0
  29. package/dist/src/services/memory-service.d.ts.map +1 -1
  30. package/dist/src/services/memory-service.js +125 -82
  31. package/dist/src/services/memory-service.js.map +1 -1
  32. package/docs/architecture-diagrams.md +368 -0
  33. package/docs/architecture.md +381 -349
  34. package/docs/contracts.md +190 -119
  35. package/docs/prompt-template.md +33 -79
  36. package/docs/proposals/mcp-typescript-sdk-refactor.md +568 -568
  37. package/docs/proposals/proposal-close-gates.md +58 -58
  38. package/docs/tool-calling-policy.md +101 -107
  39. package/docs/vector-search.md +306 -0
  40. package/package.json +59 -58
  41. package/src/contracts/index.ts +12 -12
  42. package/src/contracts/mcp.ts +222 -222
  43. package/src/contracts/schemas.ts +307 -307
  44. package/src/contracts/types.ts +410 -410
  45. package/src/index.ts +8 -8
  46. package/src/server/index.ts +5 -5
  47. package/src/server/mcp-server.ts +185 -161
  48. package/src/services/embedding-service.ts +114 -114
  49. package/src/services/index.ts +5 -5
  50. package/src/services/memory-service.ts +663 -621
  51. package/src/storage/frontmatter-parser.ts +243 -243
  52. package/src/storage/index.ts +6 -6
  53. package/src/storage/markdown-storage.ts +236 -236
  54. package/src/storage/vector-index.ts +160 -160
  55. package/src/utils/index.ts +5 -5
  56. package/src/utils/slugify.ts +63 -63
  57. package/test/contracts/schemas.test.ts +313 -313
  58. package/test/contracts/types.test.ts +21 -21
  59. package/test/frontmatter-parser-more.test.ts +94 -94
  60. package/test/server/mcp-server.test.ts +210 -169
  61. package/test/services/memory-service-edge.test.ts +248 -248
  62. package/test/services/memory-service.test.ts +278 -278
  63. package/test/storage/frontmatter-parser.test.ts +222 -222
  64. package/test/storage/markdown-storage.test.ts +216 -216
  65. package/test/storage/storage-edge.test.ts +238 -238
  66. package/test/storage/vector-index.test.ts +153 -153
  67. package/test/utils/slugify-edge.test.ts +94 -94
  68. package/test/utils/slugify.test.ts +68 -68
  69. package/tsconfig.json +25 -25
  70. package/tsconfig.test.json +8 -8
  71. package/vitest.config.ts +29 -29
@@ -1,217 +1,217 @@
1
- /**
2
- * Markdown Storage Tests
3
- * Tests for the MarkdownStorage class
4
- */
5
-
6
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
7
- import { mkdtempSync, rmSync, existsSync, readFileSync } from 'fs';
8
- import { tmpdir } from 'os';
9
- import { join } from 'path';
10
- import { MarkdownStorage, StorageError } from '../../src/storage/markdown-storage.js';
11
- import type { Memory } from '../../src/contracts/types.js';
12
-
13
- describe('MarkdownStorage', () => {
14
- let tempDir: string;
15
- let storage: MarkdownStorage;
16
-
17
- beforeEach(() => {
18
- tempDir = mkdtempSync(join(tmpdir(), 'memhub-storage-test-'));
19
- storage = new MarkdownStorage({ storagePath: tempDir });
20
- });
21
-
22
- afterEach(() => {
23
- rmSync(tempDir, { recursive: true, force: true });
24
- });
25
-
26
- describe('write', () => {
27
- const sampleMemory: Memory = {
28
- id: '550e8400-e29b-41d4-a716-446655440000',
29
- createdAt: '2024-03-15T10:30:00Z',
30
- updatedAt: '2024-03-15T10:30:00Z',
31
- sessionId: '550e8400-e29b-41d4-a716-446655440999',
32
- tags: ['test', 'memory'],
33
- category: 'testing',
34
- importance: 3,
35
- title: 'Test Memory',
36
- content: 'This is the content of the test memory.',
37
- };
38
-
39
- it('should write memory to markdown file', async () => {
40
- const result = await storage.write(sampleMemory);
41
- expect(existsSync(result)).toBe(true);
42
- expect(result).toContain(`${sampleMemory.createdAt.split('T')[0]}`);
43
- expect(result).toContain(sampleMemory.sessionId as string);
44
- });
45
-
46
- it('should write valid YAML front matter', async () => {
47
- const filePath = await storage.write(sampleMemory);
48
- const content = readFileSync(filePath, 'utf-8');
49
- expect(content).toMatch(/^---\n/);
50
- expect(content).toContain('id: "550e8400-e29b-41d4-a716-446655440000"');
51
- expect(content).toContain('created_at: "2024-03-15T10:30:00Z"');
52
- expect(content).toContain('session_id: "550e8400-e29b-41d4-a716-446655440999"');
53
- });
54
-
55
- it('should write markdown body with title', async () => {
56
- const filePath = await storage.write(sampleMemory);
57
- const content = readFileSync(filePath, 'utf-8');
58
- expect(content).toContain('# Test Memory');
59
- expect(content).toContain('This is the content');
60
- });
61
-
62
- it('should write tags as YAML array', async () => {
63
- const filePath = await storage.write(sampleMemory);
64
- const content = readFileSync(filePath, 'utf-8');
65
- expect(content).toContain('tags:');
66
- expect(content).toContain('test');
67
- expect(content).toContain('memory');
68
- });
69
-
70
- it('should return nested file path', async () => {
71
- const result = await storage.write(sampleMemory);
72
- expect(result).toContain('test-memory.md');
73
- expect(result).toContain('2024-03-15');
74
- expect(result).toContain('550e8400-e29b-41d4-a716-446655440999');
75
- });
76
- });
77
-
78
- describe('read', () => {
79
- it('should read memory from markdown file', async () => {
80
- // Create a test file first
81
- const testContent = `---
82
- id: "550e8400-e29b-41d4-a716-446655440000"
83
- created_at: "2024-03-15T10:30:00Z"
84
- updated_at: "2024-03-15T10:30:00Z"
85
- tags:
86
- - test
87
- category: "testing"
88
- importance: 3
89
- ---
90
-
91
- # Test Memory
92
-
93
- This is the content.
94
- `;
95
- const filePath = join(tempDir, 'test.md');
96
- // Use sync write for test setup
97
- const { writeFileSync } = await import('fs');
98
- writeFileSync(filePath, testContent);
99
-
100
- const memory = await storage.read('550e8400-e29b-41d4-a716-446655440000');
101
- expect(memory.id).toBe('550e8400-e29b-41d4-a716-446655440000');
102
- expect(memory.title).toBe('Test Memory');
103
- });
104
-
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);
109
- });
110
-
111
- it('should parse front matter correctly', async () => {
112
- const testContent = `---
113
- id: "550e8400-e29b-41d4-a716-446655440000"
114
- created_at: "2024-03-15T10:30:00Z"
115
- updated_at: "2024-03-15T14:20:00Z"
116
- tags:
117
- - tag1
118
- - tag2
119
- category: "work"
120
- importance: 4
121
- ---
122
-
123
- # Test Title
124
-
125
- Test content.
126
- `;
127
- const { writeFileSync } = await import('fs');
128
- writeFileSync(join(tempDir, 'test.md'), testContent);
129
-
130
- const memory = await storage.read('550e8400-e29b-41d4-a716-446655440000');
131
- expect(memory.tags).toEqual(['tag1', 'tag2']);
132
- expect(memory.category).toBe('work');
133
- expect(memory.importance).toBe(4);
134
- });
135
- });
136
-
137
- describe('delete', () => {
138
- it('should delete memory file', async () => {
139
- // Create a test file first
140
- const testContent = `---
141
- id: "550e8400-e29b-41d4-a716-446655440000"
142
- created_at: "2024-03-15T10:30:00Z"
143
- updated_at: "2024-03-15T10:30:00Z"
144
- tags: []
145
- category: "general"
146
- importance: 3
147
- ---
148
-
149
- # Test
150
- `;
151
- const filePath = join(tempDir, 'test.md');
152
- const { writeFileSync } = await import('fs');
153
- writeFileSync(filePath, testContent);
154
-
155
- await storage.delete('550e8400-e29b-41d4-a716-446655440000');
156
- expect(existsSync(filePath)).toBe(false);
157
- });
158
-
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);
163
- });
164
- });
165
-
166
- describe('list', () => {
167
- it('should list all memory files', async () => {
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');
171
-
172
- const files = await storage.list();
173
- expect(files).toHaveLength(2);
174
- });
175
-
176
- it('should return empty array when no files exist', async () => {
177
- const files = await storage.list();
178
- expect(files).toEqual([]);
179
- });
180
-
181
- it('should only include .md files', async () => {
182
- 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');
184
- writeFileSync(join(tempDir, 'test.txt'), 'not markdown');
185
-
186
- const files = await storage.list();
187
- expect(files).toHaveLength(1);
188
- expect(files[0].filename).toBe('test.md');
189
- });
190
- });
191
-
192
- describe('findById', () => {
193
- it('should find file by memory ID', async () => {
194
- const testContent = `---
195
- id: "550e8400-e29b-41d4-a716-446655440000"
196
- created_at: "2024-03-15T10:30:00Z"
197
- updated_at: "2024-03-15T10:30:00Z"
198
- tags: []
199
- category: "general"
200
- importance: 3
201
- ---
202
-
203
- # Test
204
- `;
205
- const { writeFileSync } = await import('fs');
206
- writeFileSync(join(tempDir, 'test.md'), testContent);
207
-
208
- const filePath = await storage.findById('550e8400-e29b-41d4-a716-446655440000');
209
- expect(filePath).toContain('test.md');
210
- });
211
-
212
- it('should return null when ID not found', async () => {
213
- const filePath = await storage.findById('550e8400-e29b-41d4-a716-446655440000');
214
- expect(filePath).toBeNull();
215
- });
216
- });
1
+ /**
2
+ * Markdown Storage Tests
3
+ * Tests for the MarkdownStorage class
4
+ */
5
+
6
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
7
+ import { mkdtempSync, rmSync, existsSync, readFileSync } from 'fs';
8
+ import { tmpdir } from 'os';
9
+ import { join } from 'path';
10
+ import { MarkdownStorage, StorageError } from '../../src/storage/markdown-storage.js';
11
+ import type { Memory } from '../../src/contracts/types.js';
12
+
13
+ describe('MarkdownStorage', () => {
14
+ let tempDir: string;
15
+ let storage: MarkdownStorage;
16
+
17
+ beforeEach(() => {
18
+ tempDir = mkdtempSync(join(tmpdir(), 'memhub-storage-test-'));
19
+ storage = new MarkdownStorage({ storagePath: tempDir });
20
+ });
21
+
22
+ afterEach(() => {
23
+ rmSync(tempDir, { recursive: true, force: true });
24
+ });
25
+
26
+ describe('write', () => {
27
+ const sampleMemory: Memory = {
28
+ id: '550e8400-e29b-41d4-a716-446655440000',
29
+ createdAt: '2024-03-15T10:30:00Z',
30
+ updatedAt: '2024-03-15T10:30:00Z',
31
+ sessionId: '550e8400-e29b-41d4-a716-446655440999',
32
+ tags: ['test', 'memory'],
33
+ category: 'testing',
34
+ importance: 3,
35
+ title: 'Test Memory',
36
+ content: 'This is the content of the test memory.',
37
+ };
38
+
39
+ it('should write memory to markdown file', async () => {
40
+ const result = await storage.write(sampleMemory);
41
+ expect(existsSync(result)).toBe(true);
42
+ expect(result).toContain(`${sampleMemory.createdAt.split('T')[0]}`);
43
+ expect(result).toContain(sampleMemory.sessionId as string);
44
+ });
45
+
46
+ it('should write valid YAML front matter', async () => {
47
+ const filePath = await storage.write(sampleMemory);
48
+ const content = readFileSync(filePath, 'utf-8');
49
+ expect(content).toMatch(/^---\n/);
50
+ expect(content).toContain('id: "550e8400-e29b-41d4-a716-446655440000"');
51
+ expect(content).toContain('created_at: "2024-03-15T10:30:00Z"');
52
+ expect(content).toContain('session_id: "550e8400-e29b-41d4-a716-446655440999"');
53
+ });
54
+
55
+ it('should write markdown body with title', async () => {
56
+ const filePath = await storage.write(sampleMemory);
57
+ const content = readFileSync(filePath, 'utf-8');
58
+ expect(content).toContain('# Test Memory');
59
+ expect(content).toContain('This is the content');
60
+ });
61
+
62
+ it('should write tags as YAML array', async () => {
63
+ const filePath = await storage.write(sampleMemory);
64
+ const content = readFileSync(filePath, 'utf-8');
65
+ expect(content).toContain('tags:');
66
+ expect(content).toContain('test');
67
+ expect(content).toContain('memory');
68
+ });
69
+
70
+ it('should return nested file path', async () => {
71
+ const result = await storage.write(sampleMemory);
72
+ expect(result).toContain('test-memory.md');
73
+ expect(result).toContain('2024-03-15');
74
+ expect(result).toContain('550e8400-e29b-41d4-a716-446655440999');
75
+ });
76
+ });
77
+
78
+ describe('read', () => {
79
+ it('should read memory from markdown file', async () => {
80
+ // Create a test file first
81
+ const testContent = `---
82
+ id: "550e8400-e29b-41d4-a716-446655440000"
83
+ created_at: "2024-03-15T10:30:00Z"
84
+ updated_at: "2024-03-15T10:30:00Z"
85
+ tags:
86
+ - test
87
+ category: "testing"
88
+ importance: 3
89
+ ---
90
+
91
+ # Test Memory
92
+
93
+ This is the content.
94
+ `;
95
+ const filePath = join(tempDir, 'test.md');
96
+ // Use sync write for test setup
97
+ const { writeFileSync } = await import('fs');
98
+ writeFileSync(filePath, testContent);
99
+
100
+ const memory = await storage.read('550e8400-e29b-41d4-a716-446655440000');
101
+ expect(memory.id).toBe('550e8400-e29b-41d4-a716-446655440000');
102
+ expect(memory.title).toBe('Test Memory');
103
+ });
104
+
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);
109
+ });
110
+
111
+ it('should parse front matter correctly', async () => {
112
+ const testContent = `---
113
+ id: "550e8400-e29b-41d4-a716-446655440000"
114
+ created_at: "2024-03-15T10:30:00Z"
115
+ updated_at: "2024-03-15T14:20:00Z"
116
+ tags:
117
+ - tag1
118
+ - tag2
119
+ category: "work"
120
+ importance: 4
121
+ ---
122
+
123
+ # Test Title
124
+
125
+ Test content.
126
+ `;
127
+ const { writeFileSync } = await import('fs');
128
+ writeFileSync(join(tempDir, 'test.md'), testContent);
129
+
130
+ const memory = await storage.read('550e8400-e29b-41d4-a716-446655440000');
131
+ expect(memory.tags).toEqual(['tag1', 'tag2']);
132
+ expect(memory.category).toBe('work');
133
+ expect(memory.importance).toBe(4);
134
+ });
135
+ });
136
+
137
+ describe('delete', () => {
138
+ it('should delete memory file', async () => {
139
+ // Create a test file first
140
+ const testContent = `---
141
+ id: "550e8400-e29b-41d4-a716-446655440000"
142
+ created_at: "2024-03-15T10:30:00Z"
143
+ updated_at: "2024-03-15T10:30:00Z"
144
+ tags: []
145
+ category: "general"
146
+ importance: 3
147
+ ---
148
+
149
+ # Test
150
+ `;
151
+ const filePath = join(tempDir, 'test.md');
152
+ const { writeFileSync } = await import('fs');
153
+ writeFileSync(filePath, testContent);
154
+
155
+ await storage.delete('550e8400-e29b-41d4-a716-446655440000');
156
+ expect(existsSync(filePath)).toBe(false);
157
+ });
158
+
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);
163
+ });
164
+ });
165
+
166
+ describe('list', () => {
167
+ it('should list all memory files', async () => {
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');
171
+
172
+ const files = await storage.list();
173
+ expect(files).toHaveLength(2);
174
+ });
175
+
176
+ it('should return empty array when no files exist', async () => {
177
+ const files = await storage.list();
178
+ expect(files).toEqual([]);
179
+ });
180
+
181
+ it('should only include .md files', async () => {
182
+ 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');
184
+ writeFileSync(join(tempDir, 'test.txt'), 'not markdown');
185
+
186
+ const files = await storage.list();
187
+ expect(files).toHaveLength(1);
188
+ expect(files[0].filename).toBe('test.md');
189
+ });
190
+ });
191
+
192
+ describe('findById', () => {
193
+ it('should find file by memory ID', async () => {
194
+ const testContent = `---
195
+ id: "550e8400-e29b-41d4-a716-446655440000"
196
+ created_at: "2024-03-15T10:30:00Z"
197
+ updated_at: "2024-03-15T10:30:00Z"
198
+ tags: []
199
+ category: "general"
200
+ importance: 3
201
+ ---
202
+
203
+ # Test
204
+ `;
205
+ const { writeFileSync } = await import('fs');
206
+ writeFileSync(join(tempDir, 'test.md'), testContent);
207
+
208
+ const filePath = await storage.findById('550e8400-e29b-41d4-a716-446655440000');
209
+ expect(filePath).toContain('test.md');
210
+ });
211
+
212
+ it('should return null when ID not found', async () => {
213
+ const filePath = await storage.findById('550e8400-e29b-41d4-a716-446655440000');
214
+ expect(filePath).toBeNull();
215
+ });
216
+ });
217
217
  });