@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
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';