@synth-coder/memhub 0.2.3 → 0.2.4

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 (143) hide show
  1. package/.eslintrc.cjs +45 -45
  2. package/.factory/commands/opsx-apply.md +150 -150
  3. package/.factory/commands/opsx-archive.md +155 -155
  4. package/.factory/commands/opsx-explore.md +171 -171
  5. package/.factory/commands/opsx-propose.md +104 -104
  6. package/.factory/skills/openspec-apply-change/SKILL.md +156 -156
  7. package/.factory/skills/openspec-archive-change/SKILL.md +114 -114
  8. package/.factory/skills/openspec-explore/SKILL.md +288 -288
  9. package/.factory/skills/openspec-propose/SKILL.md +110 -110
  10. package/.github/workflows/ci.yml +110 -74
  11. package/.github/workflows/release.yml +67 -0
  12. package/.iflow/commands/opsx-apply.md +152 -152
  13. package/.iflow/commands/opsx-archive.md +157 -157
  14. package/.iflow/commands/opsx-explore.md +173 -173
  15. package/.iflow/commands/opsx-propose.md +106 -106
  16. package/.iflow/skills/openspec-apply-change/SKILL.md +156 -156
  17. package/.iflow/skills/openspec-archive-change/SKILL.md +114 -114
  18. package/.iflow/skills/openspec-explore/SKILL.md +288 -288
  19. package/.iflow/skills/openspec-propose/SKILL.md +110 -110
  20. package/.prettierrc +11 -11
  21. package/AGENTS.md +167 -169
  22. package/README.md +276 -195
  23. package/README.zh-CN.md +245 -193
  24. package/dist/src/cli/agents/claude-code.d.ts +5 -0
  25. package/dist/src/cli/agents/claude-code.d.ts.map +1 -0
  26. package/dist/src/cli/agents/claude-code.js +14 -0
  27. package/dist/src/cli/agents/claude-code.js.map +1 -0
  28. package/dist/src/cli/agents/cline.d.ts +5 -0
  29. package/dist/src/cli/agents/cline.d.ts.map +1 -0
  30. package/dist/src/cli/agents/cline.js +14 -0
  31. package/dist/src/cli/agents/cline.js.map +1 -0
  32. package/dist/src/cli/agents/codex.d.ts +5 -0
  33. package/dist/src/cli/agents/codex.d.ts.map +1 -0
  34. package/dist/src/cli/agents/codex.js +14 -0
  35. package/dist/src/cli/agents/codex.js.map +1 -0
  36. package/dist/src/cli/agents/cursor.d.ts +5 -0
  37. package/dist/src/cli/agents/cursor.d.ts.map +1 -0
  38. package/dist/src/cli/agents/cursor.js +14 -0
  39. package/dist/src/cli/agents/cursor.js.map +1 -0
  40. package/dist/src/cli/agents/factory-droid.d.ts +5 -0
  41. package/dist/src/cli/agents/factory-droid.d.ts.map +1 -0
  42. package/dist/src/cli/agents/factory-droid.js +14 -0
  43. package/dist/src/cli/agents/factory-droid.js.map +1 -0
  44. package/dist/src/cli/agents/gemini-cli.d.ts +5 -0
  45. package/dist/src/cli/agents/gemini-cli.d.ts.map +1 -0
  46. package/dist/src/cli/agents/gemini-cli.js +14 -0
  47. package/dist/src/cli/agents/gemini-cli.js.map +1 -0
  48. package/dist/src/cli/agents/index.d.ts +14 -0
  49. package/dist/src/cli/agents/index.d.ts.map +1 -0
  50. package/dist/src/cli/agents/index.js +30 -0
  51. package/dist/src/cli/agents/index.js.map +1 -0
  52. package/dist/src/cli/agents/windsurf.d.ts +5 -0
  53. package/dist/src/cli/agents/windsurf.d.ts.map +1 -0
  54. package/dist/src/cli/agents/windsurf.js +14 -0
  55. package/dist/src/cli/agents/windsurf.js.map +1 -0
  56. package/dist/src/cli/index.d.ts +8 -0
  57. package/dist/src/cli/index.d.ts.map +1 -0
  58. package/dist/src/cli/index.js +168 -0
  59. package/dist/src/cli/index.js.map +1 -0
  60. package/dist/src/cli/init.d.ts +34 -0
  61. package/dist/src/cli/init.d.ts.map +1 -0
  62. package/dist/src/cli/init.js +160 -0
  63. package/dist/src/cli/init.js.map +1 -0
  64. package/dist/src/cli/instructions.d.ts +29 -0
  65. package/dist/src/cli/instructions.d.ts.map +1 -0
  66. package/dist/src/cli/instructions.js +141 -0
  67. package/dist/src/cli/instructions.js.map +1 -0
  68. package/dist/src/cli/types.d.ts +22 -0
  69. package/dist/src/cli/types.d.ts.map +1 -0
  70. package/dist/src/cli/types.js +86 -0
  71. package/dist/src/cli/types.js.map +1 -0
  72. package/dist/src/contracts/mcp.js +34 -34
  73. package/dist/src/contracts/schemas.js.map +1 -1
  74. package/dist/src/server/mcp-server.d.ts.map +1 -1
  75. package/dist/src/server/mcp-server.js +7 -14
  76. package/dist/src/server/mcp-server.js.map +1 -1
  77. package/dist/src/services/embedding-service.d.ts.map +1 -1
  78. package/dist/src/services/embedding-service.js +1 -1
  79. package/dist/src/services/embedding-service.js.map +1 -1
  80. package/dist/src/services/memory-service.d.ts.map +1 -1
  81. package/dist/src/services/memory-service.js.map +1 -1
  82. package/dist/src/storage/markdown-storage.d.ts.map +1 -1
  83. package/dist/src/storage/markdown-storage.js +1 -1
  84. package/dist/src/storage/markdown-storage.js.map +1 -1
  85. package/dist/src/storage/vector-index.d.ts.map +1 -1
  86. package/dist/src/storage/vector-index.js +4 -5
  87. package/dist/src/storage/vector-index.js.map +1 -1
  88. package/docs/README.md +21 -0
  89. package/docs/mcp-tools.md +136 -0
  90. package/docs/user-guide.md +182 -0
  91. package/package.json +61 -59
  92. package/src/cli/agents/claude-code.ts +14 -0
  93. package/src/cli/agents/cline.ts +14 -0
  94. package/src/cli/agents/codex.ts +14 -0
  95. package/src/cli/agents/cursor.ts +14 -0
  96. package/src/cli/agents/factory-droid.ts +14 -0
  97. package/src/cli/agents/gemini-cli.ts +14 -0
  98. package/src/cli/agents/index.ts +36 -0
  99. package/src/cli/agents/windsurf.ts +14 -0
  100. package/src/cli/index.ts +192 -0
  101. package/src/cli/init.ts +218 -0
  102. package/src/cli/instructions.ts +156 -0
  103. package/src/cli/types.ts +112 -0
  104. package/src/contracts/index.ts +12 -12
  105. package/src/contracts/mcp.ts +223 -223
  106. package/src/contracts/schemas.ts +307 -307
  107. package/src/contracts/types.ts +410 -410
  108. package/src/index.ts +8 -8
  109. package/src/server/index.ts +5 -5
  110. package/src/server/mcp-server.ts +169 -186
  111. package/src/services/embedding-service.ts +114 -114
  112. package/src/services/index.ts +5 -5
  113. package/src/services/memory-service.ts +656 -663
  114. package/src/storage/frontmatter-parser.ts +243 -243
  115. package/src/storage/index.ts +6 -6
  116. package/src/storage/markdown-storage.ts +228 -236
  117. package/src/storage/vector-index.ts +159 -160
  118. package/src/utils/index.ts +5 -5
  119. package/src/utils/slugify.ts +63 -63
  120. package/test/cli/init.test.ts +380 -0
  121. package/test/contracts/schemas.test.ts +313 -313
  122. package/test/contracts/types.test.ts +21 -21
  123. package/test/frontmatter-parser-more.test.ts +94 -94
  124. package/test/server/mcp-server.test.ts +211 -210
  125. package/test/services/memory-service-edge.test.ts +248 -248
  126. package/test/services/memory-service.test.ts +291 -279
  127. package/test/storage/frontmatter-parser.test.ts +223 -223
  128. package/test/storage/markdown-storage.test.ts +226 -217
  129. package/test/storage/storage-edge.test.ts +238 -238
  130. package/test/storage/vector-index.test.ts +149 -153
  131. package/test/utils/slugify-edge.test.ts +94 -94
  132. package/test/utils/slugify.test.ts +72 -68
  133. package/tsconfig.json +25 -25
  134. package/tsconfig.test.json +8 -8
  135. package/vitest.config.ts +29 -29
  136. package/docs/architecture-diagrams.md +0 -368
  137. package/docs/architecture.md +0 -381
  138. package/docs/contracts.md +0 -190
  139. package/docs/prompt-template.md +0 -33
  140. package/docs/proposals/mcp-typescript-sdk-refactor.md +0 -568
  141. package/docs/proposals/proposal-close-gates.md +0 -58
  142. package/docs/tool-calling-policy.md +0 -101
  143. package/docs/vector-search.md +0 -306
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,186 +1,169 @@
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
- });
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, 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 { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
14
+ import { MemoryService, ServiceError } from '../services/memory-service.js';
15
+ import { MemoryLoadInputSchema, MemoryUpdateInputV2Schema } from '../contracts/schemas.js';
16
+ import { TOOL_DEFINITIONS, SERVER_INFO } from '../contracts/mcp.js';
17
+ import { ErrorCode } from '../contracts/types.js';
18
+
19
+ // Get package version
20
+ const __filename = fileURLToPath(import.meta.url);
21
+ const __dirname = dirname(__filename);
22
+ interface PackageJson {
23
+ version?: string;
24
+ }
25
+
26
+ // npm package runtime: dist/src/server -> package root
27
+ // test runtime: src/server -> package root
28
+ let packageJsonPath = join(__dirname, '../../../package.json');
29
+ if (!existsSync(packageJsonPath)) {
30
+ // Fallback for test environment (running from src/)
31
+ packageJsonPath = join(__dirname, '../../package.json');
32
+ }
33
+
34
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8')) as PackageJson;
35
+
36
+ /**
37
+ * Resolve storage path with the following priority:
38
+ * 1. MEMHUB_STORAGE_PATH env var (if set):
39
+ * - Absolute path: use as-is
40
+ * - Relative path starting with '.': resolve from current working directory
41
+ * 2. Default: ~/.memhub (user home directory)
42
+ */
43
+ export function resolveStoragePath(): string {
44
+ const envPath = process.env.MEMHUB_STORAGE_PATH;
45
+
46
+ if (envPath) {
47
+ // If it's an absolute path, use as-is
48
+ if (envPath.startsWith('/') || envPath.match(/^[A-Z]:\\/i)) {
49
+ return envPath;
50
+ }
51
+ // Relative path: resolve from current working directory
52
+ return resolve(process.cwd(), envPath);
53
+ }
54
+
55
+ // Default: ~/.memhub
56
+ return join(homedir(), '.memhub');
57
+ }
58
+
59
+ /**
60
+ * Create McpServer instance using SDK
61
+ */
62
+ export function createMcpServer(): Server {
63
+ const storagePath = resolveStoragePath();
64
+ const vectorSearch = process.env.MEMHUB_VECTOR_SEARCH !== 'false';
65
+ const memoryService = new MemoryService({ storagePath, vectorSearch });
66
+
67
+ // Create server using SDK
68
+ const server = new Server(
69
+ {
70
+ name: SERVER_INFO.name,
71
+ version: packageJson.version || SERVER_INFO.version,
72
+ },
73
+ {
74
+ capabilities: {
75
+ tools: { listChanged: false },
76
+ logging: {},
77
+ },
78
+ }
79
+ );
80
+
81
+ // Handle tools/list request
82
+ server.setRequestHandler(ListToolsRequestSchema, () => {
83
+ return { tools: TOOL_DEFINITIONS };
84
+ });
85
+
86
+ // Handle tools/call request
87
+ server.setRequestHandler(CallToolRequestSchema, async request => {
88
+ const { name, arguments: args } = request.params;
89
+
90
+ try {
91
+ let result: unknown;
92
+
93
+ switch (name) {
94
+ case 'memory_load': {
95
+ const input = MemoryLoadInputSchema.parse(args ?? {});
96
+ result = await memoryService.memoryLoad(input);
97
+ break;
98
+ }
99
+
100
+ case 'memory_update': {
101
+ const input = MemoryUpdateInputV2Schema.parse(args ?? {});
102
+ result = await memoryService.memoryUpdate(input);
103
+ break;
104
+ }
105
+
106
+ default:
107
+ throw new ServiceError(`Unknown tool: ${name}`, ErrorCode.METHOD_NOT_FOUND);
108
+ }
109
+
110
+ return {
111
+ content: [
112
+ {
113
+ type: 'text' as const,
114
+ text: JSON.stringify(result, null, 2),
115
+ },
116
+ ],
117
+ };
118
+ } catch (error) {
119
+ if (error instanceof ServiceError) {
120
+ return {
121
+ content: [
122
+ {
123
+ type: 'text' as const,
124
+ text: JSON.stringify({ error: error.message }, null, 2),
125
+ },
126
+ ],
127
+ isError: true,
128
+ };
129
+ }
130
+
131
+ if (error instanceof Error && error.name === 'ZodError') {
132
+ return {
133
+ content: [
134
+ {
135
+ type: 'text' as const,
136
+ text: JSON.stringify({ error: `Invalid parameters: ${error.message}` }, null, 2),
137
+ },
138
+ ],
139
+ isError: true,
140
+ };
141
+ }
142
+
143
+ // Re-throw for SDK error handling
144
+ throw error;
145
+ }
146
+ });
147
+
148
+ return server;
149
+ }
150
+
151
+ /**
152
+ * Start the MCP server (only when run directly)
153
+ */
154
+ async function main(): Promise<void> {
155
+ const server = createMcpServer();
156
+ const transport = new StdioServerTransport();
157
+
158
+ await server.connect(transport);
159
+ console.error('MemHub MCP Server running on stdio');
160
+ }
161
+
162
+ // Only run main() when this file is executed directly
163
+ const isMain = import.meta.url === `file://${process.argv[1]}` || false;
164
+ if (isMain) {
165
+ main().catch(error => {
166
+ console.error('Fatal error:', error);
167
+ process.exit(1);
168
+ });
169
+ }
@@ -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';