@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
package/src/index.ts CHANGED
@@ -1,8 +1,8 @@
1
- /**
2
- * Main entry point exports
3
- */
4
-
5
- export * from './contracts/index.js';
6
- export * from './services/index.js';
7
- export * from './storage/index.js';
8
- export * from './utils/index.js';
1
+ /**
2
+ * Main entry point exports
3
+ */
4
+
5
+ export * from './contracts/index.js';
6
+ export * from './services/index.js';
7
+ export * from './storage/index.js';
8
+ export * from './utils/index.js';
@@ -1,5 +1,5 @@
1
- /**
2
- * Server exports
3
- */
4
-
5
- export * from './mcp-server.js';
1
+ /**
2
+ * Server exports
3
+ */
4
+
5
+ export * from './mcp-server.js';
@@ -1,162 +1,186 @@
1
- #!/usr/bin/env node
2
- /**
3
- * MCP Server - Model Context Protocol server implementation
4
- * Uses @modelcontextprotocol/sdk for protocol handling
5
- */
6
-
7
- import { readFileSync, existsSync } from 'fs';
8
- import { join, dirname } from 'path';
9
- import { fileURLToPath } from 'url';
10
- import { Server } from '@modelcontextprotocol/sdk/server/index.js';
11
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
12
- import {
13
- CallToolRequestSchema,
14
- ListToolsRequestSchema,
15
- } from '@modelcontextprotocol/sdk/types.js';
16
- import { MemoryService, ServiceError } from '../services/memory-service.js';
17
- import {
18
- MemoryLoadInputSchema,
19
- MemoryUpdateInputV2Schema,
20
- } from '../contracts/schemas.js';
21
- import { TOOL_DEFINITIONS, SERVER_INFO } from '../contracts/mcp.js';
22
- import { ErrorCode } from '../contracts/types.js';
23
-
24
- // Get package version
25
- const __filename = fileURLToPath(import.meta.url);
26
- const __dirname = dirname(__filename);
27
- interface PackageJson {
28
- version?: string;
29
- }
30
-
31
- // npm package runtime: dist/src/server -> package root
32
- // test runtime: src/server -> package root
33
- let packageJsonPath = join(__dirname, '../../../package.json');
34
- if (!existsSync(packageJsonPath)) {
35
- // Fallback for test environment (running from src/)
36
- packageJsonPath = join(__dirname, '../../package.json');
37
- }
38
-
39
- const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8')) as PackageJson;
40
-
41
- /**
42
- * Create McpServer instance using SDK
43
- */
44
- export function createMcpServer(): Server {
45
- const storagePath = process.env.MEMHUB_STORAGE_PATH || './memories';
46
- const vectorSearch = process.env.MEMHUB_VECTOR_SEARCH !== 'false';
47
- const memoryService = new MemoryService({ storagePath, vectorSearch });
48
-
49
- // Create server using SDK
50
- const server = new Server(
51
- {
52
- name: SERVER_INFO.name,
53
- version: packageJson.version || SERVER_INFO.version,
54
- },
55
- {
56
- capabilities: {
57
- tools: { listChanged: false },
58
- logging: {},
59
- },
60
- }
61
- );
62
-
63
- // Handle tools/list request
64
- server.setRequestHandler(ListToolsRequestSchema, () => {
65
- return { tools: TOOL_DEFINITIONS };
66
- });
67
-
68
- // Handle tools/call request
69
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
70
- const { name, arguments: args } = request.params;
71
-
72
- try {
73
- let result: unknown;
74
-
75
- switch (name) {
76
- case 'memory_load': {
77
- const input = MemoryLoadInputSchema.parse(args ?? {});
78
- result = await memoryService.memoryLoad(input);
79
- break;
80
- }
81
-
82
- case 'memory_update': {
83
- const input = MemoryUpdateInputV2Schema.parse(args ?? {});
84
- result = await memoryService.memoryUpdate(input);
85
- break;
86
- }
87
-
88
- default:
89
- throw new ServiceError(
90
- `Unknown tool: ${name}`,
91
- ErrorCode.METHOD_NOT_FOUND
92
- );
93
- }
94
-
95
- return {
96
- content: [
97
- {
98
- type: 'text' as const,
99
- text: JSON.stringify(result, null, 2),
100
- },
101
- ],
102
- };
103
- } catch (error) {
104
- if (error instanceof ServiceError) {
105
- return {
106
- content: [
107
- {
108
- type: 'text' as const,
109
- text: JSON.stringify({ error: error.message }, null, 2),
110
- },
111
- ],
112
- isError: true,
113
- };
114
- }
115
-
116
- if (error instanceof Error && error.name === 'ZodError') {
117
- return {
118
- content: [
119
- {
120
- type: 'text' as const,
121
- text: JSON.stringify({ error: `Invalid parameters: ${error.message}` }, null, 2),
122
- },
123
- ],
124
- isError: true,
125
- };
126
- }
127
-
128
- // Re-throw for SDK error handling
129
- throw error;
130
- }
131
- });
132
-
133
- return server;
134
- }
135
-
136
- /**
137
- * Start the MCP server
138
- */
139
- async function main(): Promise<void> {
140
- const server = createMcpServer();
141
- const transport = new StdioServerTransport();
142
-
143
- await server.connect(transport);
144
- console.error('MemHub MCP Server running on stdio');
145
- }
146
-
147
- main().catch((error) => {
148
- console.error('Fatal error:', error);
149
- process.exit(1);
150
- });
151
-
152
- // Check if this file is being run directly
153
- const isMain = import.meta.url === `file://${process.argv[1]}` || false;
154
- if (isMain) {
155
- // Defer main() execution to avoid blocking module loading
156
- setImmediate(() => {
157
- main().catch((error) => {
158
- console.error('Fatal error:', error);
159
- process.exit(1);
160
- });
161
- });
1
+ #!/usr/bin/env node
2
+ /**
3
+ * MCP Server - Model Context Protocol server implementation
4
+ * Uses @modelcontextprotocol/sdk for protocol handling
5
+ */
6
+
7
+ import { readFileSync, existsSync } from 'fs';
8
+ import { join, dirname, resolve } from 'path';
9
+ import { fileURLToPath } from 'url';
10
+ import { homedir } from 'os';
11
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
12
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
13
+ import {
14
+ CallToolRequestSchema,
15
+ ListToolsRequestSchema,
16
+ } from '@modelcontextprotocol/sdk/types.js';
17
+ import { MemoryService, ServiceError } from '../services/memory-service.js';
18
+ import {
19
+ MemoryLoadInputSchema,
20
+ MemoryUpdateInputV2Schema,
21
+ } from '../contracts/schemas.js';
22
+ import { TOOL_DEFINITIONS, SERVER_INFO } from '../contracts/mcp.js';
23
+ import { ErrorCode } from '../contracts/types.js';
24
+
25
+ // Get package version
26
+ const __filename = fileURLToPath(import.meta.url);
27
+ const __dirname = dirname(__filename);
28
+ interface PackageJson {
29
+ version?: string;
30
+ }
31
+
32
+ // npm package runtime: dist/src/server -> package root
33
+ // test runtime: src/server -> package root
34
+ let packageJsonPath = join(__dirname, '../../../package.json');
35
+ if (!existsSync(packageJsonPath)) {
36
+ // Fallback for test environment (running from src/)
37
+ packageJsonPath = join(__dirname, '../../package.json');
38
+ }
39
+
40
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8')) as PackageJson;
41
+
42
+ /**
43
+ * Resolve storage path with the following priority:
44
+ * 1. MEMHUB_STORAGE_PATH env var (if set):
45
+ * - Absolute path: use as-is
46
+ * - Relative path starting with '.': resolve from current working directory
47
+ * 2. Default: ~/.memhub (user home directory)
48
+ */
49
+ export function resolveStoragePath(): string {
50
+ const envPath = process.env.MEMHUB_STORAGE_PATH;
51
+
52
+ if (envPath) {
53
+ // If it's an absolute path, use as-is
54
+ if (envPath.startsWith('/') || envPath.match(/^[A-Z]:\\/i)) {
55
+ return envPath;
56
+ }
57
+ // Relative path: resolve from current working directory
58
+ return resolve(process.cwd(), envPath);
59
+ }
60
+
61
+ // Default: ~/.memhub
62
+ return join(homedir(), '.memhub');
63
+ }
64
+
65
+ /**
66
+ * Create McpServer instance using SDK
67
+ */
68
+ export function createMcpServer(): Server {
69
+ const storagePath = resolveStoragePath();
70
+ const vectorSearch = process.env.MEMHUB_VECTOR_SEARCH !== 'false';
71
+ const memoryService = new MemoryService({ storagePath, vectorSearch });
72
+
73
+ // Create server using SDK
74
+ const server = new Server(
75
+ {
76
+ name: SERVER_INFO.name,
77
+ version: packageJson.version || SERVER_INFO.version,
78
+ },
79
+ {
80
+ capabilities: {
81
+ tools: { listChanged: false },
82
+ logging: {},
83
+ },
84
+ }
85
+ );
86
+
87
+ // Handle tools/list request
88
+ server.setRequestHandler(ListToolsRequestSchema, () => {
89
+ return { tools: TOOL_DEFINITIONS };
90
+ });
91
+
92
+ // Handle tools/call request
93
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
94
+ const { name, arguments: args } = request.params;
95
+
96
+ try {
97
+ let result: unknown;
98
+
99
+ switch (name) {
100
+ case 'memory_load': {
101
+ const input = MemoryLoadInputSchema.parse(args ?? {});
102
+ result = await memoryService.memoryLoad(input);
103
+ break;
104
+ }
105
+
106
+ case 'memory_update': {
107
+ const input = MemoryUpdateInputV2Schema.parse(args ?? {});
108
+ result = await memoryService.memoryUpdate(input);
109
+ break;
110
+ }
111
+
112
+ default:
113
+ throw new ServiceError(
114
+ `Unknown tool: ${name}`,
115
+ ErrorCode.METHOD_NOT_FOUND
116
+ );
117
+ }
118
+
119
+ return {
120
+ content: [
121
+ {
122
+ type: 'text' as const,
123
+ text: JSON.stringify(result, null, 2),
124
+ },
125
+ ],
126
+ };
127
+ } catch (error) {
128
+ if (error instanceof ServiceError) {
129
+ return {
130
+ content: [
131
+ {
132
+ type: 'text' as const,
133
+ text: JSON.stringify({ error: error.message }, null, 2),
134
+ },
135
+ ],
136
+ isError: true,
137
+ };
138
+ }
139
+
140
+ if (error instanceof Error && error.name === 'ZodError') {
141
+ return {
142
+ content: [
143
+ {
144
+ type: 'text' as const,
145
+ text: JSON.stringify({ error: `Invalid parameters: ${error.message}` }, null, 2),
146
+ },
147
+ ],
148
+ isError: true,
149
+ };
150
+ }
151
+
152
+ // Re-throw for SDK error handling
153
+ throw error;
154
+ }
155
+ });
156
+
157
+ return server;
158
+ }
159
+
160
+ /**
161
+ * Start the MCP server
162
+ */
163
+ async function main(): Promise<void> {
164
+ const server = createMcpServer();
165
+ const transport = new StdioServerTransport();
166
+
167
+ await server.connect(transport);
168
+ console.error('MemHub MCP Server running on stdio');
169
+ }
170
+
171
+ main().catch((error) => {
172
+ console.error('Fatal error:', error);
173
+ process.exit(1);
174
+ });
175
+
176
+ // Check if this file is being run directly
177
+ const isMain = import.meta.url === `file://${process.argv[1]}` || false;
178
+ if (isMain) {
179
+ // Defer main() execution to avoid blocking module loading
180
+ setImmediate(() => {
181
+ main().catch((error) => {
182
+ console.error('Fatal error:', error);
183
+ process.exit(1);
184
+ });
185
+ });
162
186
  }
@@ -1,114 +1,114 @@
1
- /**
2
- * Embedding Service - Text embedding using @xenova/transformers
3
- *
4
- * Uses the all-MiniLM-L6-v2 model (~23MB, downloaded on first use to ~/.cache/huggingface).
5
- * Singleton pattern with lazy initialization.
6
- *
7
- * Note: Uses dynamic imports to avoid loading native modules (sharp) during tests.
8
- */
9
-
10
- /** ONNX model identifier */
11
- const MODEL_NAME = 'Xenova/all-MiniLM-L6-v2';
12
-
13
- /** Output vector dimension for all-MiniLM-L6-v2 */
14
- export const VECTOR_DIM = 384;
15
-
16
- type FeatureExtractionPipeline = (
17
- text: string,
18
- options: { pooling: string; normalize: boolean }
19
- ) => Promise<{ data: Float32Array }>;
20
-
21
- type TransformersModule = {
22
- pipeline: (
23
- task: string,
24
- model: string,
25
- options?: { progress_callback?: null }
26
- ) => Promise<unknown>;
27
- env: {
28
- allowRemoteModels: boolean;
29
- allowLocalModels: boolean;
30
- };
31
- };
32
-
33
- /**
34
- * Singleton embedding service backed by a local ONNX model.
35
- * The model is downloaded once and cached in `~/.cache/huggingface`.
36
- */
37
- export class EmbeddingService {
38
- private static instance: EmbeddingService | null = null;
39
- private extractor: FeatureExtractionPipeline | null = null;
40
- private initPromise: Promise<void> | null = null;
41
- private transformers: TransformersModule | null = null;
42
-
43
- private constructor() {
44
- // Constructor is empty - initialization happens in initialize()
45
- }
46
-
47
- static getInstance(): EmbeddingService {
48
- if (!EmbeddingService.instance) {
49
- EmbeddingService.instance = new EmbeddingService();
50
- }
51
- return EmbeddingService.instance;
52
- }
53
-
54
- /**
55
- * Initializes the pipeline (idempotent, safe to call multiple times).
56
- */
57
- async initialize(): Promise<void> {
58
- if (this.extractor) return;
59
-
60
- if (!this.initPromise) {
61
- this.initPromise = (async () => {
62
- // Dynamic import to avoid loading sharp during tests
63
- this.transformers = await import('@xenova/transformers') as TransformersModule;
64
-
65
- // Configure environment
66
- this.transformers.env.allowRemoteModels = true;
67
- this.transformers.env.allowLocalModels = true;
68
-
69
- this.extractor = (await this.transformers.pipeline(
70
- 'feature-extraction',
71
- MODEL_NAME
72
- )) as FeatureExtractionPipeline;
73
- })();
74
- }
75
-
76
- await this.initPromise;
77
- }
78
-
79
- /**
80
- * Embeds `text` into a 384-dimension float vector.
81
- *
82
- * @param text - The text to embed (title + content recommended)
83
- * @returns Normalised float vector of length VECTOR_DIM
84
- */
85
- async embed(text: string): Promise<number[]> {
86
- await this.initialize();
87
-
88
- if (!this.extractor) {
89
- throw new Error('EmbeddingService: extractor not initialized');
90
- }
91
-
92
- const output = await this.extractor(text, {
93
- pooling: 'mean',
94
- normalize: true,
95
- });
96
-
97
- return Array.from(output.data);
98
- }
99
-
100
- /**
101
- * Convenience: embed a memory's title and content together.
102
- */
103
- async embedMemory(title: string, content: string): Promise<number[]> {
104
- return this.embed(`${title} ${content}`.trim());
105
- }
106
-
107
- /**
108
- * Reset the singleton instance.
109
- * @internal For testing purposes only. Do not use in production code.
110
- */
111
- static _reset(): void {
112
- EmbeddingService.instance = null;
113
- }
114
- }
1
+ /**
2
+ * Embedding Service - Text embedding using @xenova/transformers
3
+ *
4
+ * Uses the all-MiniLM-L6-v2 model (~23MB, downloaded on first use to ~/.cache/huggingface).
5
+ * Singleton pattern with lazy initialization.
6
+ *
7
+ * Note: Uses dynamic imports to avoid loading native modules (sharp) during tests.
8
+ */
9
+
10
+ /** ONNX model identifier */
11
+ const MODEL_NAME = 'Xenova/all-MiniLM-L6-v2';
12
+
13
+ /** Output vector dimension for all-MiniLM-L6-v2 */
14
+ export const VECTOR_DIM = 384;
15
+
16
+ type FeatureExtractionPipeline = (
17
+ text: string,
18
+ options: { pooling: string; normalize: boolean }
19
+ ) => Promise<{ data: Float32Array }>;
20
+
21
+ type TransformersModule = {
22
+ pipeline: (
23
+ task: string,
24
+ model: string,
25
+ options?: { progress_callback?: null }
26
+ ) => Promise<unknown>;
27
+ env: {
28
+ allowRemoteModels: boolean;
29
+ allowLocalModels: boolean;
30
+ };
31
+ };
32
+
33
+ /**
34
+ * Singleton embedding service backed by a local ONNX model.
35
+ * The model is downloaded once and cached in `~/.cache/huggingface`.
36
+ */
37
+ export class EmbeddingService {
38
+ private static instance: EmbeddingService | null = null;
39
+ private extractor: FeatureExtractionPipeline | null = null;
40
+ private initPromise: Promise<void> | null = null;
41
+ private transformers: TransformersModule | null = null;
42
+
43
+ private constructor() {
44
+ // Constructor is empty - initialization happens in initialize()
45
+ }
46
+
47
+ static getInstance(): EmbeddingService {
48
+ if (!EmbeddingService.instance) {
49
+ EmbeddingService.instance = new EmbeddingService();
50
+ }
51
+ return EmbeddingService.instance;
52
+ }
53
+
54
+ /**
55
+ * Initializes the pipeline (idempotent, safe to call multiple times).
56
+ */
57
+ async initialize(): Promise<void> {
58
+ if (this.extractor) return;
59
+
60
+ if (!this.initPromise) {
61
+ this.initPromise = (async () => {
62
+ // Dynamic import to avoid loading sharp during tests
63
+ this.transformers = await import('@xenova/transformers') as TransformersModule;
64
+
65
+ // Configure environment
66
+ this.transformers.env.allowRemoteModels = true;
67
+ this.transformers.env.allowLocalModels = true;
68
+
69
+ this.extractor = (await this.transformers.pipeline(
70
+ 'feature-extraction',
71
+ MODEL_NAME
72
+ )) as FeatureExtractionPipeline;
73
+ })();
74
+ }
75
+
76
+ await this.initPromise;
77
+ }
78
+
79
+ /**
80
+ * Embeds `text` into a 384-dimension float vector.
81
+ *
82
+ * @param text - The text to embed (title + content recommended)
83
+ * @returns Normalised float vector of length VECTOR_DIM
84
+ */
85
+ async embed(text: string): Promise<number[]> {
86
+ await this.initialize();
87
+
88
+ if (!this.extractor) {
89
+ throw new Error('EmbeddingService: extractor not initialized');
90
+ }
91
+
92
+ const output = await this.extractor(text, {
93
+ pooling: 'mean',
94
+ normalize: true,
95
+ });
96
+
97
+ return Array.from(output.data);
98
+ }
99
+
100
+ /**
101
+ * Convenience: embed a memory's title and content together.
102
+ */
103
+ async embedMemory(title: string, content: string): Promise<number[]> {
104
+ return this.embed(`${title} ${content}`.trim());
105
+ }
106
+
107
+ /**
108
+ * Reset the singleton instance.
109
+ * @internal For testing purposes only. Do not use in production code.
110
+ */
111
+ static _reset(): void {
112
+ EmbeddingService.instance = null;
113
+ }
114
+ }
@@ -1,5 +1,5 @@
1
- /**
2
- * Services exports
3
- */
4
-
5
- export * from './memory-service.js';
1
+ /**
2
+ * Services exports
3
+ */
4
+
5
+ export * from './memory-service.js';