@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,243 +1,243 @@
1
- /**
2
- * FrontMatter Parser - Handles YAML Front Matter and Markdown content
3
- */
4
-
5
- import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
6
- import type { Memory, MemoryFrontMatter } from '../contracts/types.js';
7
- import { slugify } from '../utils/slugify.js';
8
-
9
- /**
10
- * Result of parsing a markdown file
11
- */
12
- export interface ParseResult {
13
- frontMatter: MemoryFrontMatter;
14
- title: string;
15
- content: string;
16
- }
17
-
18
- /**
19
- * Custom error for front matter parsing
20
- */
21
- export class FrontMatterError extends Error {
22
- constructor(
23
- message: string,
24
- public readonly cause?: unknown
25
- ) {
26
- super(message);
27
- this.name = 'FrontMatterError';
28
- }
29
- }
30
-
31
- /**
32
- * Parses a markdown file with YAML front matter
33
- *
34
- * Expected format:
35
- * ---
36
- * id: "uuid"
37
- * created_at: "ISO8601"
38
- * updated_at: "ISO8601"
39
- * tags: ["tag1", "tag2"]
40
- * category: "category"
41
- * importance: 3
42
- * ---
43
- *
44
- * # Title
45
- *
46
- * Content...
47
- *
48
- * @param markdown - The markdown content to parse
49
- * @returns Parsed front matter, title, and content
50
- * @throws FrontMatterError if parsing fails
51
- */
52
- export function parseFrontMatter(markdown: string): ParseResult {
53
- // Check for front matter delimiter
54
- if (!markdown.startsWith('---')) {
55
- throw new FrontMatterError('Missing front matter delimiter');
56
- }
57
-
58
- // Find the end of front matter
59
- const endMatch = markdown.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/);
60
- if (!endMatch) {
61
- throw new FrontMatterError('Invalid front matter format');
62
- }
63
-
64
- const frontMatterYaml = endMatch[1];
65
- const restOfContent = markdown.slice(endMatch[0].length);
66
-
67
- // Parse YAML front matter
68
- let frontMatter: unknown;
69
- try {
70
- frontMatter = parseYaml(frontMatterYaml);
71
- } catch (error) {
72
- throw new FrontMatterError('Invalid YAML in front matter', error);
73
- }
74
-
75
- // Validate required fields
76
- if (!isValidFrontMatter(frontMatter)) {
77
- throw new FrontMatterError('Missing required fields in front matter');
78
- }
79
-
80
- // Parse title and content from markdown body
81
- const { title, content } = parseMarkdownBody(restOfContent);
82
-
83
- return {
84
- frontMatter,
85
- title,
86
- content,
87
- };
88
- }
89
-
90
- /**
91
- * Converts front matter and content to markdown string
92
- *
93
- * @param frontMatter - The front matter data
94
- * @param title - The title (H1 heading)
95
- * @param content - The markdown content
96
- * @returns Complete markdown string
97
- */
98
- export function stringifyFrontMatter(
99
- frontMatter: MemoryFrontMatter,
100
- title: string,
101
- content: string
102
- ): string {
103
- // Convert camelCase to snake_case for YAML
104
- const yamlData = {
105
- id: frontMatter.id,
106
- created_at: frontMatter.created_at,
107
- updated_at: frontMatter.updated_at,
108
- ...(frontMatter.session_id ? { session_id: frontMatter.session_id } : {}),
109
- ...(frontMatter.entry_type ? { entry_type: frontMatter.entry_type } : {}),
110
- tags: frontMatter.tags.length > 0 ? frontMatter.tags : [],
111
- category: frontMatter.category,
112
- importance: frontMatter.importance,
113
- };
114
-
115
- // Stringify YAML with specific options for consistent formatting
116
- const yamlString = stringifyYaml(yamlData, {
117
- indent: 2,
118
- defaultKeyType: 'PLAIN',
119
- defaultStringType: 'QUOTE_DOUBLE',
120
- });
121
-
122
- // Build the complete markdown
123
- const parts: string[] = ['---', yamlString.trim(), '---', ''];
124
-
125
- // Add title if provided
126
- if (title) {
127
- parts.push(`# ${title}`, '');
128
- }
129
-
130
- // Add content if provided
131
- if (content) {
132
- parts.push(content);
133
- }
134
-
135
- // Ensure content ends with a single newline
136
- let result = parts.join('\n');
137
- if (!result.endsWith('\n')) {
138
- result += '\n';
139
- }
140
-
141
- return result;
142
- }
143
-
144
- /**
145
- * Parses the markdown body to extract title and content
146
- *
147
- * @param body - The markdown body (after front matter)
148
- * @returns Title and content
149
- */
150
- function parseMarkdownBody(body: string): { title: string; content: string } {
151
- const trimmed = body.trim();
152
-
153
- if (!trimmed) {
154
- return { title: '', content: '' };
155
- }
156
-
157
- // Try to extract H1 title
158
- const h1Match = trimmed.match(/^#\s+(.+)$/m);
159
- if (h1Match) {
160
- const title = h1Match[1].trim();
161
- // Remove the H1 line from content
162
- const content = trimmed.replace(/^#\s+.+$/m, '').trim();
163
- return { title, content };
164
- }
165
-
166
- // No H1 found, treat entire body as content
167
- return { title: '', content: trimmed };
168
- }
169
-
170
- /**
171
- * Type guard to validate front matter structure
172
- *
173
- * @param value - The value to check
174
- * @returns True if valid front matter
175
- */
176
- function isValidFrontMatter(value: unknown): value is MemoryFrontMatter {
177
- if (typeof value !== 'object' || value === null) {
178
- return false;
179
- }
180
-
181
- const fm = value as Record<string, unknown>;
182
-
183
- // Check required fields
184
- if (typeof fm.id !== 'string') return false;
185
- if (typeof fm.created_at !== 'string') return false;
186
- if (typeof fm.updated_at !== 'string') return false;
187
- if (fm.session_id !== undefined && typeof fm.session_id !== 'string') return false;
188
- if (fm.entry_type !== undefined && typeof fm.entry_type !== 'string') return false;
189
- if (!Array.isArray(fm.tags)) return false;
190
- if (typeof fm.category !== 'string') return false;
191
- if (typeof fm.importance !== 'number') return false;
192
-
193
- return true;
194
- }
195
-
196
- /**
197
- * Converts a Memory object to front matter format
198
- *
199
- * @param memory - The memory object
200
- * @returns Memory in front matter format
201
- */
202
- export function memoryToFrontMatter(memory: Memory): MemoryFrontMatter {
203
- return {
204
- id: memory.id,
205
- created_at: memory.createdAt,
206
- updated_at: memory.updatedAt,
207
- session_id: memory.sessionId,
208
- entry_type: memory.entryType,
209
- tags: memory.tags,
210
- category: memory.category,
211
- importance: memory.importance,
212
- };
213
- }
214
-
215
- /**
216
- * Converts front matter format to Memory object
217
- *
218
- * @param frontMatter - The front matter data
219
- * @param title - The memory title
220
- * @param content - The memory content
221
- * @returns Complete Memory object
222
- */
223
- export function frontMatterToMemory(
224
- frontMatter: MemoryFrontMatter,
225
- title: string,
226
- content: string
227
- ): Memory {
228
- return {
229
- id: frontMatter.id,
230
- createdAt: frontMatter.created_at,
231
- updatedAt: frontMatter.updated_at,
232
- sessionId: frontMatter.session_id,
233
- entryType: frontMatter.entry_type,
234
- tags: frontMatter.tags,
235
- category: frontMatter.category,
236
- importance: frontMatter.importance,
237
- title,
238
- content,
239
- };
240
- }
241
-
242
- // Re-export slugify for convenience
243
- export { slugify };
1
+ /**
2
+ * FrontMatter Parser - Handles YAML Front Matter and Markdown content
3
+ */
4
+
5
+ import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
6
+ import type { Memory, MemoryFrontMatter } from '../contracts/types.js';
7
+ import { slugify } from '../utils/slugify.js';
8
+
9
+ /**
10
+ * Result of parsing a markdown file
11
+ */
12
+ export interface ParseResult {
13
+ frontMatter: MemoryFrontMatter;
14
+ title: string;
15
+ content: string;
16
+ }
17
+
18
+ /**
19
+ * Custom error for front matter parsing
20
+ */
21
+ export class FrontMatterError extends Error {
22
+ constructor(
23
+ message: string,
24
+ public readonly cause?: unknown
25
+ ) {
26
+ super(message);
27
+ this.name = 'FrontMatterError';
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Parses a markdown file with YAML front matter
33
+ *
34
+ * Expected format:
35
+ * ---
36
+ * id: "uuid"
37
+ * created_at: "ISO8601"
38
+ * updated_at: "ISO8601"
39
+ * tags: ["tag1", "tag2"]
40
+ * category: "category"
41
+ * importance: 3
42
+ * ---
43
+ *
44
+ * # Title
45
+ *
46
+ * Content...
47
+ *
48
+ * @param markdown - The markdown content to parse
49
+ * @returns Parsed front matter, title, and content
50
+ * @throws FrontMatterError if parsing fails
51
+ */
52
+ export function parseFrontMatter(markdown: string): ParseResult {
53
+ // Check for front matter delimiter
54
+ if (!markdown.startsWith('---')) {
55
+ throw new FrontMatterError('Missing front matter delimiter');
56
+ }
57
+
58
+ // Find the end of front matter
59
+ const endMatch = markdown.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/);
60
+ if (!endMatch) {
61
+ throw new FrontMatterError('Invalid front matter format');
62
+ }
63
+
64
+ const frontMatterYaml = endMatch[1];
65
+ const restOfContent = markdown.slice(endMatch[0].length);
66
+
67
+ // Parse YAML front matter
68
+ let frontMatter: unknown;
69
+ try {
70
+ frontMatter = parseYaml(frontMatterYaml);
71
+ } catch (error) {
72
+ throw new FrontMatterError('Invalid YAML in front matter', error);
73
+ }
74
+
75
+ // Validate required fields
76
+ if (!isValidFrontMatter(frontMatter)) {
77
+ throw new FrontMatterError('Missing required fields in front matter');
78
+ }
79
+
80
+ // Parse title and content from markdown body
81
+ const { title, content } = parseMarkdownBody(restOfContent);
82
+
83
+ return {
84
+ frontMatter,
85
+ title,
86
+ content,
87
+ };
88
+ }
89
+
90
+ /**
91
+ * Converts front matter and content to markdown string
92
+ *
93
+ * @param frontMatter - The front matter data
94
+ * @param title - The title (H1 heading)
95
+ * @param content - The markdown content
96
+ * @returns Complete markdown string
97
+ */
98
+ export function stringifyFrontMatter(
99
+ frontMatter: MemoryFrontMatter,
100
+ title: string,
101
+ content: string
102
+ ): string {
103
+ // Convert camelCase to snake_case for YAML
104
+ const yamlData = {
105
+ id: frontMatter.id,
106
+ created_at: frontMatter.created_at,
107
+ updated_at: frontMatter.updated_at,
108
+ ...(frontMatter.session_id ? { session_id: frontMatter.session_id } : {}),
109
+ ...(frontMatter.entry_type ? { entry_type: frontMatter.entry_type } : {}),
110
+ tags: frontMatter.tags.length > 0 ? frontMatter.tags : [],
111
+ category: frontMatter.category,
112
+ importance: frontMatter.importance,
113
+ };
114
+
115
+ // Stringify YAML with specific options for consistent formatting
116
+ const yamlString = stringifyYaml(yamlData, {
117
+ indent: 2,
118
+ defaultKeyType: 'PLAIN',
119
+ defaultStringType: 'QUOTE_DOUBLE',
120
+ });
121
+
122
+ // Build the complete markdown
123
+ const parts: string[] = ['---', yamlString.trim(), '---', ''];
124
+
125
+ // Add title if provided
126
+ if (title) {
127
+ parts.push(`# ${title}`, '');
128
+ }
129
+
130
+ // Add content if provided
131
+ if (content) {
132
+ parts.push(content);
133
+ }
134
+
135
+ // Ensure content ends with a single newline
136
+ let result = parts.join('\n');
137
+ if (!result.endsWith('\n')) {
138
+ result += '\n';
139
+ }
140
+
141
+ return result;
142
+ }
143
+
144
+ /**
145
+ * Parses the markdown body to extract title and content
146
+ *
147
+ * @param body - The markdown body (after front matter)
148
+ * @returns Title and content
149
+ */
150
+ function parseMarkdownBody(body: string): { title: string; content: string } {
151
+ const trimmed = body.trim();
152
+
153
+ if (!trimmed) {
154
+ return { title: '', content: '' };
155
+ }
156
+
157
+ // Try to extract H1 title
158
+ const h1Match = trimmed.match(/^#\s+(.+)$/m);
159
+ if (h1Match) {
160
+ const title = h1Match[1].trim();
161
+ // Remove the H1 line from content
162
+ const content = trimmed.replace(/^#\s+.+$/m, '').trim();
163
+ return { title, content };
164
+ }
165
+
166
+ // No H1 found, treat entire body as content
167
+ return { title: '', content: trimmed };
168
+ }
169
+
170
+ /**
171
+ * Type guard to validate front matter structure
172
+ *
173
+ * @param value - The value to check
174
+ * @returns True if valid front matter
175
+ */
176
+ function isValidFrontMatter(value: unknown): value is MemoryFrontMatter {
177
+ if (typeof value !== 'object' || value === null) {
178
+ return false;
179
+ }
180
+
181
+ const fm = value as Record<string, unknown>;
182
+
183
+ // Check required fields
184
+ if (typeof fm.id !== 'string') return false;
185
+ if (typeof fm.created_at !== 'string') return false;
186
+ if (typeof fm.updated_at !== 'string') return false;
187
+ if (fm.session_id !== undefined && typeof fm.session_id !== 'string') return false;
188
+ if (fm.entry_type !== undefined && typeof fm.entry_type !== 'string') return false;
189
+ if (!Array.isArray(fm.tags)) return false;
190
+ if (typeof fm.category !== 'string') return false;
191
+ if (typeof fm.importance !== 'number') return false;
192
+
193
+ return true;
194
+ }
195
+
196
+ /**
197
+ * Converts a Memory object to front matter format
198
+ *
199
+ * @param memory - The memory object
200
+ * @returns Memory in front matter format
201
+ */
202
+ export function memoryToFrontMatter(memory: Memory): MemoryFrontMatter {
203
+ return {
204
+ id: memory.id,
205
+ created_at: memory.createdAt,
206
+ updated_at: memory.updatedAt,
207
+ session_id: memory.sessionId,
208
+ entry_type: memory.entryType,
209
+ tags: memory.tags,
210
+ category: memory.category,
211
+ importance: memory.importance,
212
+ };
213
+ }
214
+
215
+ /**
216
+ * Converts front matter format to Memory object
217
+ *
218
+ * @param frontMatter - The front matter data
219
+ * @param title - The memory title
220
+ * @param content - The memory content
221
+ * @returns Complete Memory object
222
+ */
223
+ export function frontMatterToMemory(
224
+ frontMatter: MemoryFrontMatter,
225
+ title: string,
226
+ content: string
227
+ ): Memory {
228
+ return {
229
+ id: frontMatter.id,
230
+ createdAt: frontMatter.created_at,
231
+ updatedAt: frontMatter.updated_at,
232
+ sessionId: frontMatter.session_id,
233
+ entryType: frontMatter.entry_type,
234
+ tags: frontMatter.tags,
235
+ category: frontMatter.category,
236
+ importance: frontMatter.importance,
237
+ title,
238
+ content,
239
+ };
240
+ }
241
+
242
+ // Re-export slugify for convenience
243
+ export { slugify };
@@ -1,6 +1,6 @@
1
- /**
2
- * Storage layer exports
3
- */
4
-
5
- export * from './frontmatter-parser.js';
6
- export * from './markdown-storage.js';
1
+ /**
2
+ * Storage layer exports
3
+ */
4
+
5
+ export * from './frontmatter-parser.js';
6
+ export * from './markdown-storage.js';