@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
@@ -1,236 +1,228 @@
1
- /**
2
- * Markdown Storage - Handles file system operations for memory storage
3
- */
4
-
5
- import {
6
- readFile,
7
- writeFile,
8
- unlink,
9
- readdir,
10
- stat,
11
- access,
12
- mkdir,
13
- } from 'fs/promises';
14
- import { join, extname } from 'path';
15
- import { constants } from 'fs';
16
- import type { Memory, MemoryFile } from '../contracts/types.js';
17
- import {
18
- parseFrontMatter,
19
- stringifyFrontMatter,
20
- memoryToFrontMatter,
21
- frontMatterToMemory,
22
- FrontMatterError,
23
- } from './frontmatter-parser.js';
24
- import { slugify } from '../utils/slugify.js';
25
-
26
- /**
27
- * Custom error for storage operations
28
- */
29
- export class StorageError extends Error {
30
- constructor(
31
- message: string,
32
- public readonly cause?: unknown
33
- ) {
34
- super(message);
35
- this.name = 'StorageError';
36
- }
37
- }
38
-
39
- /**
40
- * Storage configuration
41
- */
42
- export interface StorageConfig {
43
- /** Root directory for memory storage */
44
- storagePath: string;
45
- }
46
-
47
- /**
48
- * Markdown file storage implementation
49
- */
50
- export class MarkdownStorage {
51
- private readonly storagePath: string;
52
-
53
- constructor(config: StorageConfig) {
54
- this.storagePath = config.storagePath;
55
- }
56
-
57
- /**
58
- * Ensures the storage directory exists
59
- */
60
- async initialize(): Promise<void> {
61
- try {
62
- await access(this.storagePath, constants.F_OK);
63
- } catch {
64
- // Directory doesn't exist, create it
65
- await mkdir(this.storagePath, { recursive: true });
66
- }
67
- }
68
-
69
- /**
70
- * Writes a memory to a markdown file
71
- *
72
- * @param memory - The memory to write
73
- * @returns The file path where the memory was stored
74
- * @throws StorageError if write fails
75
- */
76
- async write(memory: Memory): Promise<string> {
77
- await this.initialize();
78
-
79
- const { directoryPath, filename } = this.generatePathParts(memory);
80
- const filePath = join(directoryPath, filename);
81
- const frontMatter = memoryToFrontMatter(memory);
82
- const content = stringifyFrontMatter(frontMatter, memory.title, memory.content);
83
-
84
- try {
85
- await mkdir(directoryPath, { recursive: true });
86
- await writeFile(filePath, content, 'utf-8');
87
- return filePath;
88
- } catch (error) {
89
- throw new StorageError(`Failed to write memory file: ${filename}`, error);
90
- }
91
- }
92
-
93
- /**
94
- * Reads a memory by its ID
95
- *
96
- * @param id - The memory ID
97
- * @returns The memory object
98
- * @throws StorageError if memory not found or read fails
99
- */
100
- async read(id: string): Promise<Memory> {
101
- const filePath = await this.findById(id);
102
-
103
- if (!filePath) {
104
- throw new StorageError(`Memory not found: ${id}`);
105
- }
106
-
107
- try {
108
- const content = await readFile(filePath, 'utf-8');
109
- const { frontMatter, title, content: bodyContent } = parseFrontMatter(content);
110
- return frontMatterToMemory(frontMatter, title, bodyContent);
111
- } catch (error) {
112
- if (error instanceof FrontMatterError) {
113
- throw new StorageError(`Invalid memory file format: ${filePath}`, error);
114
- }
115
- throw new StorageError(`Failed to read memory file: ${filePath}`, error);
116
- }
117
- }
118
-
119
- /**
120
- * Deletes a memory by its ID
121
- *
122
- * @param id - The memory ID
123
- * @returns The file path of the deleted memory
124
- * @throws StorageError if memory not found or delete fails
125
- */
126
- async delete(id: string): Promise<string> {
127
- const filePath = await this.findById(id);
128
-
129
- if (!filePath) {
130
- throw new StorageError(`Memory not found: ${id}`);
131
- }
132
-
133
- try {
134
- await unlink(filePath);
135
- return filePath;
136
- } catch (error) {
137
- throw new StorageError(`Failed to delete memory file: ${filePath}`, error);
138
- }
139
- }
140
-
141
- /**
142
- * Lists all memory files
143
- *
144
- * @returns Array of memory file information
145
- * @throws StorageError if listing fails
146
- */
147
- async list(): Promise<MemoryFile[]> {
148
- await this.initialize();
149
-
150
- try {
151
- const markdownPaths = await this.collectMarkdownFiles(this.storagePath);
152
- const files: MemoryFile[] = [];
153
-
154
- for (const filePath of markdownPaths) {
155
- const stats = await stat(filePath);
156
- const content = await readFile(filePath, 'utf-8');
157
-
158
- files.push({
159
- path: filePath,
160
- filename: filePath.split(/[/\\]/).pop() ?? filePath,
161
- content,
162
- modifiedAt: stats.mtime.toISOString(),
163
- });
164
- }
165
-
166
- return files;
167
- } catch (error) {
168
- throw new StorageError('Failed to list memory files', error);
169
- }
170
- }
171
-
172
- /**
173
- * Finds a memory file by ID
174
- *
175
- * @param id - The memory ID to find
176
- * @returns The file path or null if not found
177
- * @throws StorageError if search fails
178
- */
179
- async findById(id: string): Promise<string | null> {
180
- await this.initialize();
181
-
182
- try {
183
- const markdownPaths = await this.collectMarkdownFiles(this.storagePath);
184
-
185
- for (const filePath of markdownPaths) {
186
- try {
187
- const content = await readFile(filePath, 'utf-8');
188
- const { frontMatter } = parseFrontMatter(content);
189
- if (frontMatter.id === id) {
190
- return filePath;
191
- }
192
- } catch {
193
- // Skip files that can't be parsed
194
- continue;
195
- }
196
- }
197
-
198
- return null;
199
- } catch (error) {
200
- throw new StorageError('Failed to search for memory file', error);
201
- }
202
- }
203
-
204
- /**
205
- * Generates nested path parts for a memory
206
- *
207
- * Format: {storage}/{YYYY-MM-DD}/{session_uuid}/{timestamp}-{slug}.md
208
- */
209
- private generatePathParts(memory: Memory): { directoryPath: string; filename: string } {
210
- const date = memory.createdAt.split('T')[0];
211
- const sessionId = memory.sessionId ?? 'default-session';
212
- const titleSlug = slugify(memory.title) || 'untitled';
213
- const timestamp = memory.createdAt.replace(/[:.]/g, '-');
214
- const filename = `${timestamp}-${titleSlug}.md`;
215
- const directoryPath = join(this.storagePath, date, sessionId);
216
-
217
- return { directoryPath, filename };
218
- }
219
-
220
- private async collectMarkdownFiles(rootDir: string): Promise<string[]> {
221
- const entries = await readdir(rootDir, { withFileTypes: true });
222
- const files: string[] = [];
223
-
224
- for (const entry of entries) {
225
- const entryPath = join(rootDir, entry.name);
226
- if (entry.isDirectory()) {
227
- const nested = await this.collectMarkdownFiles(entryPath);
228
- files.push(...nested);
229
- } else if (entry.isFile() && extname(entry.name) === '.md') {
230
- files.push(entryPath);
231
- }
232
- }
233
-
234
- return files;
235
- }
236
- }
1
+ /**
2
+ * Markdown Storage - Handles file system operations for memory storage
3
+ */
4
+
5
+ import { readFile, writeFile, unlink, readdir, stat, access, mkdir } from 'fs/promises';
6
+ import { join, extname } from 'path';
7
+ import { constants } from 'fs';
8
+ import type { Memory, MemoryFile } from '../contracts/types.js';
9
+ import {
10
+ parseFrontMatter,
11
+ stringifyFrontMatter,
12
+ memoryToFrontMatter,
13
+ frontMatterToMemory,
14
+ FrontMatterError,
15
+ } from './frontmatter-parser.js';
16
+ import { slugify } from '../utils/slugify.js';
17
+
18
+ /**
19
+ * Custom error for storage operations
20
+ */
21
+ export class StorageError extends Error {
22
+ constructor(
23
+ message: string,
24
+ public readonly cause?: unknown
25
+ ) {
26
+ super(message);
27
+ this.name = 'StorageError';
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Storage configuration
33
+ */
34
+ export interface StorageConfig {
35
+ /** Root directory for memory storage */
36
+ storagePath: string;
37
+ }
38
+
39
+ /**
40
+ * Markdown file storage implementation
41
+ */
42
+ export class MarkdownStorage {
43
+ private readonly storagePath: string;
44
+
45
+ constructor(config: StorageConfig) {
46
+ this.storagePath = config.storagePath;
47
+ }
48
+
49
+ /**
50
+ * Ensures the storage directory exists
51
+ */
52
+ async initialize(): Promise<void> {
53
+ try {
54
+ await access(this.storagePath, constants.F_OK);
55
+ } catch {
56
+ // Directory doesn't exist, create it
57
+ await mkdir(this.storagePath, { recursive: true });
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Writes a memory to a markdown file
63
+ *
64
+ * @param memory - The memory to write
65
+ * @returns The file path where the memory was stored
66
+ * @throws StorageError if write fails
67
+ */
68
+ async write(memory: Memory): Promise<string> {
69
+ await this.initialize();
70
+
71
+ const { directoryPath, filename } = this.generatePathParts(memory);
72
+ const filePath = join(directoryPath, filename);
73
+ const frontMatter = memoryToFrontMatter(memory);
74
+ const content = stringifyFrontMatter(frontMatter, memory.title, memory.content);
75
+
76
+ try {
77
+ await mkdir(directoryPath, { recursive: true });
78
+ await writeFile(filePath, content, 'utf-8');
79
+ return filePath;
80
+ } catch (error) {
81
+ throw new StorageError(`Failed to write memory file: ${filename}`, error);
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Reads a memory by its ID
87
+ *
88
+ * @param id - The memory ID
89
+ * @returns The memory object
90
+ * @throws StorageError if memory not found or read fails
91
+ */
92
+ async read(id: string): Promise<Memory> {
93
+ const filePath = await this.findById(id);
94
+
95
+ if (!filePath) {
96
+ throw new StorageError(`Memory not found: ${id}`);
97
+ }
98
+
99
+ try {
100
+ const content = await readFile(filePath, 'utf-8');
101
+ const { frontMatter, title, content: bodyContent } = parseFrontMatter(content);
102
+ return frontMatterToMemory(frontMatter, title, bodyContent);
103
+ } catch (error) {
104
+ if (error instanceof FrontMatterError) {
105
+ throw new StorageError(`Invalid memory file format: ${filePath}`, error);
106
+ }
107
+ throw new StorageError(`Failed to read memory file: ${filePath}`, error);
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Deletes a memory by its ID
113
+ *
114
+ * @param id - The memory ID
115
+ * @returns The file path of the deleted memory
116
+ * @throws StorageError if memory not found or delete fails
117
+ */
118
+ async delete(id: string): Promise<string> {
119
+ const filePath = await this.findById(id);
120
+
121
+ if (!filePath) {
122
+ throw new StorageError(`Memory not found: ${id}`);
123
+ }
124
+
125
+ try {
126
+ await unlink(filePath);
127
+ return filePath;
128
+ } catch (error) {
129
+ throw new StorageError(`Failed to delete memory file: ${filePath}`, error);
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Lists all memory files
135
+ *
136
+ * @returns Array of memory file information
137
+ * @throws StorageError if listing fails
138
+ */
139
+ async list(): Promise<MemoryFile[]> {
140
+ await this.initialize();
141
+
142
+ try {
143
+ const markdownPaths = await this.collectMarkdownFiles(this.storagePath);
144
+ const files: MemoryFile[] = [];
145
+
146
+ for (const filePath of markdownPaths) {
147
+ const stats = await stat(filePath);
148
+ const content = await readFile(filePath, 'utf-8');
149
+
150
+ files.push({
151
+ path: filePath,
152
+ filename: filePath.split(/[/\\]/).pop() ?? filePath,
153
+ content,
154
+ modifiedAt: stats.mtime.toISOString(),
155
+ });
156
+ }
157
+
158
+ return files;
159
+ } catch (error) {
160
+ throw new StorageError('Failed to list memory files', error);
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Finds a memory file by ID
166
+ *
167
+ * @param id - The memory ID to find
168
+ * @returns The file path or null if not found
169
+ * @throws StorageError if search fails
170
+ */
171
+ async findById(id: string): Promise<string | null> {
172
+ await this.initialize();
173
+
174
+ try {
175
+ const markdownPaths = await this.collectMarkdownFiles(this.storagePath);
176
+
177
+ for (const filePath of markdownPaths) {
178
+ try {
179
+ const content = await readFile(filePath, 'utf-8');
180
+ const { frontMatter } = parseFrontMatter(content);
181
+ if (frontMatter.id === id) {
182
+ return filePath;
183
+ }
184
+ } catch {
185
+ // Skip files that can't be parsed
186
+ continue;
187
+ }
188
+ }
189
+
190
+ return null;
191
+ } catch (error) {
192
+ throw new StorageError('Failed to search for memory file', error);
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Generates nested path parts for a memory
198
+ *
199
+ * Format: {storage}/{YYYY-MM-DD}/{session_uuid}/{timestamp}-{slug}.md
200
+ */
201
+ private generatePathParts(memory: Memory): { directoryPath: string; filename: string } {
202
+ const date = memory.createdAt.split('T')[0];
203
+ const sessionId = memory.sessionId ?? 'default-session';
204
+ const titleSlug = slugify(memory.title) || 'untitled';
205
+ const timestamp = memory.createdAt.replace(/[:.]/g, '-');
206
+ const filename = `${timestamp}-${titleSlug}.md`;
207
+ const directoryPath = join(this.storagePath, date, sessionId);
208
+
209
+ return { directoryPath, filename };
210
+ }
211
+
212
+ private async collectMarkdownFiles(rootDir: string): Promise<string[]> {
213
+ const entries = await readdir(rootDir, { withFileTypes: true });
214
+ const files: string[] = [];
215
+
216
+ for (const entry of entries) {
217
+ const entryPath = join(rootDir, entry.name);
218
+ if (entry.isDirectory()) {
219
+ const nested = await this.collectMarkdownFiles(entryPath);
220
+ files.push(...nested);
221
+ } else if (entry.isFile() && extname(entry.name) === '.md') {
222
+ files.push(entryPath);
223
+ }
224
+ }
225
+
226
+ return files;
227
+ }
228
+ }