@synth-coder/memhub 0.2.1 → 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,236 +1,236 @@
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 {
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
+ }