@synth-coder/memhub 0.2.3 → 0.2.5

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 (118) hide show
  1. package/.github/workflows/ci.yml +48 -12
  2. package/.github/workflows/release.yml +70 -0
  3. package/AGENTS.md +71 -73
  4. package/README.md +284 -195
  5. package/README.zh-CN.md +90 -30
  6. package/dist/src/cli/agents/claude-code.d.ts +5 -0
  7. package/dist/src/cli/agents/claude-code.d.ts.map +1 -0
  8. package/dist/src/cli/agents/claude-code.js +14 -0
  9. package/dist/src/cli/agents/claude-code.js.map +1 -0
  10. package/dist/src/cli/agents/cline.d.ts +5 -0
  11. package/dist/src/cli/agents/cline.d.ts.map +1 -0
  12. package/dist/src/cli/agents/cline.js +14 -0
  13. package/dist/src/cli/agents/cline.js.map +1 -0
  14. package/dist/src/cli/agents/cursor.d.ts +5 -0
  15. package/dist/src/cli/agents/cursor.d.ts.map +1 -0
  16. package/dist/src/cli/agents/cursor.js +14 -0
  17. package/dist/src/cli/agents/cursor.js.map +1 -0
  18. package/dist/src/cli/agents/factory-droid.d.ts +5 -0
  19. package/dist/src/cli/agents/factory-droid.d.ts.map +1 -0
  20. package/dist/src/cli/agents/factory-droid.js +14 -0
  21. package/dist/src/cli/agents/factory-droid.js.map +1 -0
  22. package/dist/src/cli/agents/gemini-cli.d.ts +5 -0
  23. package/dist/src/cli/agents/gemini-cli.d.ts.map +1 -0
  24. package/dist/src/cli/agents/gemini-cli.js +14 -0
  25. package/dist/src/cli/agents/gemini-cli.js.map +1 -0
  26. package/dist/src/cli/agents/index.d.ts +13 -0
  27. package/dist/src/cli/agents/index.d.ts.map +1 -0
  28. package/dist/src/cli/agents/index.js +27 -0
  29. package/dist/src/cli/agents/index.js.map +1 -0
  30. package/dist/src/cli/agents/windsurf.d.ts +5 -0
  31. package/dist/src/cli/agents/windsurf.d.ts.map +1 -0
  32. package/dist/src/cli/agents/windsurf.js +14 -0
  33. package/dist/src/cli/agents/windsurf.js.map +1 -0
  34. package/dist/src/cli/index.d.ts +8 -0
  35. package/dist/src/cli/index.d.ts.map +1 -0
  36. package/dist/src/cli/index.js +168 -0
  37. package/dist/src/cli/index.js.map +1 -0
  38. package/dist/src/cli/init.d.ts +34 -0
  39. package/dist/src/cli/init.d.ts.map +1 -0
  40. package/dist/src/cli/init.js +140 -0
  41. package/dist/src/cli/init.js.map +1 -0
  42. package/dist/src/cli/instructions.d.ts +29 -0
  43. package/dist/src/cli/instructions.d.ts.map +1 -0
  44. package/dist/src/cli/instructions.js +141 -0
  45. package/dist/src/cli/instructions.js.map +1 -0
  46. package/dist/src/cli/types.d.ts +22 -0
  47. package/dist/src/cli/types.d.ts.map +1 -0
  48. package/dist/src/cli/types.js +75 -0
  49. package/dist/src/cli/types.js.map +1 -0
  50. package/dist/src/contracts/mcp.js +34 -34
  51. package/dist/src/contracts/schemas.js.map +1 -1
  52. package/dist/src/server/mcp-server.d.ts.map +1 -1
  53. package/dist/src/server/mcp-server.js +7 -14
  54. package/dist/src/server/mcp-server.js.map +1 -1
  55. package/dist/src/services/embedding-service.d.ts.map +1 -1
  56. package/dist/src/services/embedding-service.js +1 -1
  57. package/dist/src/services/embedding-service.js.map +1 -1
  58. package/dist/src/services/memory-service.d.ts.map +1 -1
  59. package/dist/src/services/memory-service.js.map +1 -1
  60. package/dist/src/storage/markdown-storage.d.ts.map +1 -1
  61. package/dist/src/storage/markdown-storage.js +1 -1
  62. package/dist/src/storage/markdown-storage.js.map +1 -1
  63. package/dist/src/storage/vector-index.d.ts.map +1 -1
  64. package/dist/src/storage/vector-index.js +4 -5
  65. package/dist/src/storage/vector-index.js.map +1 -1
  66. package/docs/README.md +21 -0
  67. package/docs/mcp-tools.md +136 -0
  68. package/docs/user-guide.md +184 -0
  69. package/package.json +61 -59
  70. package/src/cli/agents/claude-code.ts +14 -0
  71. package/src/cli/agents/cline.ts +14 -0
  72. package/src/cli/agents/codex.ts +14 -0
  73. package/src/cli/agents/cursor.ts +14 -0
  74. package/src/cli/agents/factory-droid.ts +14 -0
  75. package/src/cli/agents/gemini-cli.ts +14 -0
  76. package/src/cli/agents/index.ts +36 -0
  77. package/src/cli/agents/windsurf.ts +14 -0
  78. package/src/cli/index.ts +192 -0
  79. package/src/cli/init.ts +218 -0
  80. package/src/cli/instructions.ts +156 -0
  81. package/src/cli/types.ts +112 -0
  82. package/src/contracts/index.ts +12 -12
  83. package/src/contracts/mcp.ts +223 -223
  84. package/src/contracts/schemas.ts +307 -307
  85. package/src/contracts/types.ts +410 -410
  86. package/src/index.ts +8 -8
  87. package/src/server/index.ts +5 -5
  88. package/src/server/mcp-server.ts +169 -186
  89. package/src/services/embedding-service.ts +114 -114
  90. package/src/services/index.ts +5 -5
  91. package/src/services/memory-service.ts +656 -663
  92. package/src/storage/frontmatter-parser.ts +243 -243
  93. package/src/storage/index.ts +6 -6
  94. package/src/storage/markdown-storage.ts +228 -236
  95. package/src/storage/vector-index.ts +159 -160
  96. package/src/utils/index.ts +5 -5
  97. package/src/utils/slugify.ts +63 -63
  98. package/test/cli/init.test.ts +402 -0
  99. package/test/contracts/schemas.test.ts +313 -313
  100. package/test/contracts/types.test.ts +21 -21
  101. package/test/frontmatter-parser-more.test.ts +94 -94
  102. package/test/server/mcp-server.test.ts +211 -210
  103. package/test/services/memory-service-edge.test.ts +248 -248
  104. package/test/services/memory-service.test.ts +291 -279
  105. package/test/storage/frontmatter-parser.test.ts +223 -223
  106. package/test/storage/markdown-storage.test.ts +226 -217
  107. package/test/storage/storage-edge.test.ts +238 -238
  108. package/test/storage/vector-index.test.ts +149 -153
  109. package/test/utils/slugify-edge.test.ts +94 -94
  110. package/test/utils/slugify.test.ts +72 -68
  111. package/docs/architecture-diagrams.md +0 -368
  112. package/docs/architecture.md +0 -381
  113. package/docs/contracts.md +0 -190
  114. package/docs/prompt-template.md +0 -33
  115. package/docs/proposals/mcp-typescript-sdk-refactor.md +0 -568
  116. package/docs/proposals/proposal-close-gates.md +0 -58
  117. package/docs/tool-calling-policy.md +0 -101
  118. package/docs/vector-search.md +0 -306
@@ -0,0 +1,402 @@
1
+ /**
2
+ * Tests for CLI init command
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
6
+ import { mkdirSync, rmSync, existsSync, readFileSync, writeFileSync } from 'fs';
7
+ import { join } from 'path';
8
+ import { initAgent } from '../../src/cli/init.js';
9
+ import { AGENTS, type AgentType } from '../../src/cli/types.js';
10
+ import { parse as parseToml } from 'smol-toml';
11
+ import {
12
+ extractMemHubVersion,
13
+ needsUpdate,
14
+ updateInstructionsContent,
15
+ } from '../../src/cli/instructions.js';
16
+
17
+ const TEST_DIR = join(process.cwd(), 'test-temp-cli');
18
+
19
+ describe('CLI Init Command', () => {
20
+ beforeEach(() => {
21
+ if (!existsSync(TEST_DIR)) {
22
+ mkdirSync(TEST_DIR, { recursive: true });
23
+ }
24
+ });
25
+
26
+ afterEach(() => {
27
+ if (existsSync(TEST_DIR)) {
28
+ rmSync(TEST_DIR, { recursive: true, force: true });
29
+ }
30
+ });
31
+
32
+ describe('initAgent', () => {
33
+ it('should create local config and instructions for cursor', () => {
34
+ const result = initAgent({
35
+ agent: 'cursor',
36
+ local: true,
37
+ projectPath: TEST_DIR,
38
+ });
39
+
40
+ expect(result.success).toBe(true);
41
+ if (result.success) {
42
+ expect(result.agent.id).toBe('cursor');
43
+ expect(result.configPath).toBe('.cursor/mcp.json');
44
+ expect(result.instructionsPath).toBe('.cursorrules');
45
+ expect(existsSync(join(TEST_DIR, '.cursor/mcp.json'))).toBe(true);
46
+ expect(existsSync(join(TEST_DIR, '.cursorrules'))).toBe(true);
47
+ }
48
+ });
49
+
50
+ it('should create local config and instructions for claude-code', () => {
51
+ const result = initAgent({
52
+ agent: 'claude-code',
53
+ local: true,
54
+ projectPath: TEST_DIR,
55
+ });
56
+
57
+ expect(result.success).toBe(true);
58
+ if (result.success) {
59
+ expect(result.agent.id).toBe('claude-code');
60
+ expect(result.configPath).toBe('.mcp.json');
61
+ expect(result.instructionsPath).toBe('CLAUDE.md');
62
+ }
63
+ });
64
+
65
+ it('should create local config and instructions for cline', () => {
66
+ const result = initAgent({
67
+ agent: 'cline',
68
+ local: true,
69
+ projectPath: TEST_DIR,
70
+ });
71
+
72
+ expect(result.success).toBe(true);
73
+ if (result.success) {
74
+ expect(result.agent.id).toBe('cline');
75
+ expect(result.configPath).toBe('.cline/mcp.json');
76
+ expect(result.instructionsPath).toBe('.clinerules');
77
+ }
78
+ });
79
+
80
+ it('should create local config and instructions for windsurf', () => {
81
+ const result = initAgent({
82
+ agent: 'windsurf',
83
+ local: true,
84
+ projectPath: TEST_DIR,
85
+ });
86
+
87
+ expect(result.success).toBe(true);
88
+ if (result.success) {
89
+ expect(result.agent.id).toBe('windsurf');
90
+ expect(result.configPath).toBe('.codeium/windsurf/mcp_config.json');
91
+ expect(result.instructionsPath).toBe('.windsurfrules');
92
+ }
93
+ });
94
+
95
+ it('should create local config and instructions for factory-droid', () => {
96
+ const result = initAgent({
97
+ agent: 'factory-droid',
98
+ local: true,
99
+ projectPath: TEST_DIR,
100
+ });
101
+
102
+ expect(result.success).toBe(true);
103
+ if (result.success) {
104
+ expect(result.agent.id).toBe('factory-droid');
105
+ expect(result.configPath).toBe('.factory/mcp.json');
106
+ expect(result.instructionsPath).toBe('.factory/AGENTS.md');
107
+ }
108
+ });
109
+
110
+ it('should create local config and instructions for gemini-cli', () => {
111
+ const result = initAgent({
112
+ agent: 'gemini-cli',
113
+ local: true,
114
+ projectPath: TEST_DIR,
115
+ });
116
+
117
+ expect(result.success).toBe(true);
118
+ if (result.success) {
119
+ expect(result.agent.id).toBe('gemini-cli');
120
+ expect(result.configPath).toBe('.gemini/settings.json');
121
+ expect(result.instructionsPath).toBe('GEMINI.md');
122
+ }
123
+ });
124
+
125
+ it('should create local config and instructions for codex', () => {
126
+ const result = initAgent({
127
+ agent: 'codex',
128
+ local: true,
129
+ projectPath: TEST_DIR,
130
+ });
131
+
132
+ expect(result.success).toBe(true);
133
+ if (result.success) {
134
+ expect(result.agent.id).toBe('codex');
135
+ expect(result.configPath).toBe('.codex/config.toml');
136
+ expect(result.instructionsPath).toBe('AGENTS.md');
137
+ expect(existsSync(join(TEST_DIR, '.codex/config.toml'))).toBe(true);
138
+ expect(existsSync(join(TEST_DIR, 'AGENTS.md'))).toBe(true);
139
+ }
140
+ });
141
+
142
+ it('should fail for unknown agent', () => {
143
+ const result = initAgent({
144
+ agent: 'unknown-agent' as AgentType,
145
+ local: true,
146
+ projectPath: TEST_DIR,
147
+ });
148
+
149
+ expect(result.success).toBe(false);
150
+ if (!result.success) {
151
+ expect(result.error).toContain('Unknown agent');
152
+ }
153
+ });
154
+
155
+ it('should fail if memhub already configured without force', () => {
156
+ // First init
157
+ initAgent({
158
+ agent: 'claude-code',
159
+ local: true,
160
+ projectPath: TEST_DIR,
161
+ });
162
+
163
+ // Second init without force
164
+ const result = initAgent({
165
+ agent: 'claude-code',
166
+ local: true,
167
+ projectPath: TEST_DIR,
168
+ force: false,
169
+ });
170
+
171
+ expect(result.success).toBe(false);
172
+ if (!result.success) {
173
+ expect(result.error).toContain('already configured');
174
+ }
175
+ });
176
+
177
+ it('should update config with force', () => {
178
+ // First init
179
+ initAgent({
180
+ agent: 'claude-code',
181
+ local: true,
182
+ projectPath: TEST_DIR,
183
+ });
184
+
185
+ // Second init with force
186
+ const result = initAgent({
187
+ agent: 'claude-code',
188
+ local: true,
189
+ projectPath: TEST_DIR,
190
+ force: true,
191
+ });
192
+
193
+ expect(result.success).toBe(true);
194
+ });
195
+
196
+ it('should generate valid JSON config', () => {
197
+ initAgent({
198
+ agent: 'cursor',
199
+ local: true,
200
+ projectPath: TEST_DIR,
201
+ });
202
+
203
+ const configPath = join(TEST_DIR, '.cursor/mcp.json');
204
+ const content = readFileSync(configPath, 'utf-8');
205
+ const config = JSON.parse(content);
206
+
207
+ expect(config).toHaveProperty('mcpServers');
208
+ expect(config.mcpServers).toHaveProperty('memhub');
209
+ expect(config.mcpServers.memhub).toHaveProperty('command');
210
+ expect(config.mcpServers.memhub.command).toBe('npx');
211
+ });
212
+
213
+ it('should generate valid TOML config for codex', () => {
214
+ initAgent({
215
+ agent: 'codex',
216
+ local: true,
217
+ projectPath: TEST_DIR,
218
+ });
219
+
220
+ const configPath = join(TEST_DIR, '.codex/config.toml');
221
+ const content = readFileSync(configPath, 'utf-8');
222
+ const config = parseToml(content) as Record<string, unknown>;
223
+ const servers = config.mcp_servers as Record<string, unknown>;
224
+ const memhub = servers.memhub as Record<string, unknown>;
225
+ const args = memhub.args as string[];
226
+
227
+ expect(config).toHaveProperty('mcp_servers');
228
+ expect(servers).toHaveProperty('memhub');
229
+ expect(memhub.command).toBe('npx');
230
+ expect(args).toContain('@synth-coder/memhub@latest');
231
+ });
232
+
233
+ it('should generate instructions with version tag', () => {
234
+ initAgent({
235
+ agent: 'claude-code',
236
+ local: true,
237
+ projectPath: TEST_DIR,
238
+ });
239
+
240
+ const instructionsPath = join(TEST_DIR, 'CLAUDE.md');
241
+ const content = readFileSync(instructionsPath, 'utf-8');
242
+
243
+ expect(content).toContain('<!-- MEMHUB:v');
244
+ expect(content).toContain('<!-- MEMHUB:END -->');
245
+ });
246
+
247
+ it('should merge with existing config preserving other servers', () => {
248
+ // Create existing config with other servers
249
+ const configDir = join(TEST_DIR, '.mcp.json');
250
+ const existingConfig = {
251
+ mcpServers: {
252
+ github: {
253
+ command: 'npx',
254
+ args: ['-y', '@modelcontextprotocol/server-github'],
255
+ },
256
+ },
257
+ };
258
+ writeFileSync(configDir, JSON.stringify(existingConfig, null, 2), 'utf-8');
259
+
260
+ // Run init
261
+ const result = initAgent({
262
+ agent: 'claude-code',
263
+ local: true,
264
+ projectPath: TEST_DIR,
265
+ });
266
+
267
+ expect(result.success).toBe(true);
268
+
269
+ // Verify both servers exist
270
+ const content = readFileSync(configDir, 'utf-8');
271
+ const config = JSON.parse(content);
272
+
273
+ expect(config.mcpServers).toHaveProperty('github');
274
+ expect(config.mcpServers).toHaveProperty('memhub');
275
+ });
276
+
277
+ it('should fail if memhub already in config without force', () => {
278
+ // Create config with memhub already configured
279
+ const configDir = join(TEST_DIR, '.mcp.json');
280
+ const existingConfig = {
281
+ mcpServers: {
282
+ memhub: {
283
+ command: 'npx',
284
+ args: ['-y', '@synth-coder/memhub@latest'],
285
+ },
286
+ },
287
+ };
288
+ writeFileSync(configDir, JSON.stringify(existingConfig, null, 2), 'utf-8');
289
+
290
+ // Run init without force
291
+ const result = initAgent({
292
+ agent: 'claude-code',
293
+ local: true,
294
+ projectPath: TEST_DIR,
295
+ force: false,
296
+ });
297
+
298
+ expect(result.success).toBe(false);
299
+ if (!result.success) {
300
+ expect(result.error).toContain('already configured');
301
+ }
302
+ });
303
+
304
+ it('should update memhub with force preserving other servers', () => {
305
+ // Create config with existing servers including memhub
306
+ const configDir = join(TEST_DIR, '.mcp.json');
307
+ const existingConfig = {
308
+ mcpServers: {
309
+ github: {
310
+ command: 'npx',
311
+ args: ['-y', '@modelcontextprotocol/server-github'],
312
+ },
313
+ memhub: {
314
+ command: 'old-command',
315
+ args: [],
316
+ },
317
+ },
318
+ };
319
+ writeFileSync(configDir, JSON.stringify(existingConfig, null, 2), 'utf-8');
320
+
321
+ // Run init with force
322
+ const result = initAgent({
323
+ agent: 'claude-code',
324
+ local: true,
325
+ projectPath: TEST_DIR,
326
+ force: true,
327
+ });
328
+
329
+ expect(result.success).toBe(true);
330
+
331
+ // Verify github preserved, memhub updated
332
+ const content = readFileSync(configDir, 'utf-8');
333
+ const config = JSON.parse(content);
334
+
335
+ expect(config.mcpServers).toHaveProperty('github');
336
+ expect(config.mcpServers).toHaveProperty('memhub');
337
+ expect(config.mcpServers.memhub.command).toBe('npx');
338
+ });
339
+
340
+ });
341
+
342
+ describe('Instructions Update', () => {
343
+ it('should prepend instructions to empty file', () => {
344
+ const result = updateInstructionsContent('', AGENTS[1]); // claude-code
345
+
346
+ expect(result.updated).toBe(true);
347
+ expect(result.content).toContain('<!-- MEMHUB:v');
348
+ });
349
+
350
+ it('should prepend instructions to existing content', () => {
351
+ const existing = '# My Project\n\nSome instructions';
352
+ const result = updateInstructionsContent(existing, AGENTS[1]);
353
+
354
+ expect(result.updated).toBe(true);
355
+ expect(result.content).toContain('<!-- MEMHUB:v');
356
+ expect(result.content).toContain('# My Project');
357
+ });
358
+
359
+ it('should not update if already current version', () => {
360
+ const content = '<!-- MEMHUB:v0.2.3:START -->\nContent\n<!-- MEMHUB:END -->';
361
+ const result = updateInstructionsContent(content, AGENTS[1]);
362
+
363
+ expect(result.updated).toBe(false);
364
+ expect(result.reason).toBe('Already up to date');
365
+ });
366
+
367
+ it('should extract version correctly', () => {
368
+ expect(extractMemHubVersion('<!-- MEMHUB:v0.2.3:START -->')).toBe('0.2.3');
369
+ expect(extractMemHubVersion('No version here')).toBeNull();
370
+ });
371
+
372
+ it('should detect when update is needed', () => {
373
+ expect(needsUpdate('No MemHub content')).toBe(true);
374
+ expect(needsUpdate('<!-- MEMHUB:v0.1.0:START -->')).toBe(true);
375
+ });
376
+ });
377
+ });
378
+
379
+ describe('Agent Types', () => {
380
+ it('should have all expected agents', () => {
381
+ const agentIds = AGENTS.map(a => a.id);
382
+ expect(agentIds).toContain('cursor');
383
+ expect(agentIds).toContain('claude-code');
384
+ expect(agentIds).toContain('cline');
385
+ expect(agentIds).toContain('windsurf');
386
+ expect(agentIds).toContain('factory-droid');
387
+ expect(agentIds).toContain('gemini-cli');
388
+ expect(agentIds).toContain('codex');
389
+ });
390
+
391
+ it('should have valid config for each agent', () => {
392
+ AGENTS.forEach(agent => {
393
+ expect(agent.configFile).toBeTruthy();
394
+ expect(agent.globalConfigFile).toBeTruthy();
395
+ expect(agent.name).toBeTruthy();
396
+ expect(agent.configFormat).toMatch(/^(json|markdown|toml)$/);
397
+ expect(agent.instructionsFile).toBeTruthy();
398
+ expect(agent.globalInstructionsFile).toBeTruthy();
399
+ expect(agent.instructionsFormat).toMatch(/^(markdown|plain)$/);
400
+ });
401
+ });
402
+ });