@synth-coder/memhub 0.2.2 → 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.
- package/.eslintrc.cjs +45 -45
- package/.factory/commands/opsx-apply.md +150 -0
- package/.factory/commands/opsx-archive.md +155 -0
- package/.factory/commands/opsx-explore.md +171 -0
- package/.factory/commands/opsx-propose.md +104 -0
- package/.factory/skills/openspec-apply-change/SKILL.md +156 -0
- package/.factory/skills/openspec-archive-change/SKILL.md +114 -0
- package/.factory/skills/openspec-explore/SKILL.md +288 -0
- package/.factory/skills/openspec-propose/SKILL.md +110 -0
- package/.github/workflows/ci.yml +74 -74
- package/.iflow/commands/opsx-apply.md +152 -152
- package/.iflow/commands/opsx-archive.md +157 -157
- package/.iflow/commands/opsx-explore.md +173 -173
- package/.iflow/commands/opsx-propose.md +106 -106
- package/.iflow/skills/openspec-apply-change/SKILL.md +156 -156
- package/.iflow/skills/openspec-archive-change/SKILL.md +114 -114
- package/.iflow/skills/openspec-explore/SKILL.md +288 -288
- package/.iflow/skills/openspec-propose/SKILL.md +110 -110
- package/.prettierrc +11 -11
- package/AGENTS.md +169 -26
- package/README.md +195 -195
- package/README.zh-CN.md +193 -193
- package/dist/src/contracts/mcp.js +34 -34
- package/dist/src/server/mcp-server.d.ts +8 -0
- package/dist/src/server/mcp-server.d.ts.map +1 -1
- package/dist/src/server/mcp-server.js +23 -2
- package/dist/src/server/mcp-server.js.map +1 -1
- package/dist/src/services/memory-service.d.ts +1 -0
- package/dist/src/services/memory-service.d.ts.map +1 -1
- package/dist/src/services/memory-service.js +125 -82
- package/dist/src/services/memory-service.js.map +1 -1
- package/docs/architecture-diagrams.md +368 -0
- package/docs/architecture.md +381 -349
- package/docs/contracts.md +190 -119
- package/docs/prompt-template.md +33 -79
- package/docs/proposals/mcp-typescript-sdk-refactor.md +568 -568
- package/docs/proposals/proposal-close-gates.md +58 -58
- package/docs/tool-calling-policy.md +101 -107
- package/docs/vector-search.md +306 -0
- package/package.json +59 -58
- package/src/contracts/index.ts +12 -12
- package/src/contracts/mcp.ts +222 -222
- package/src/contracts/schemas.ts +307 -307
- package/src/contracts/types.ts +410 -410
- package/src/index.ts +8 -8
- package/src/server/index.ts +5 -5
- package/src/server/mcp-server.ts +185 -161
- package/src/services/embedding-service.ts +114 -114
- package/src/services/index.ts +5 -5
- package/src/services/memory-service.ts +663 -621
- package/src/storage/frontmatter-parser.ts +243 -243
- package/src/storage/index.ts +6 -6
- package/src/storage/markdown-storage.ts +236 -236
- package/src/storage/vector-index.ts +160 -160
- package/src/utils/index.ts +5 -5
- package/src/utils/slugify.ts +63 -63
- package/test/contracts/schemas.test.ts +313 -313
- package/test/contracts/types.test.ts +21 -21
- package/test/frontmatter-parser-more.test.ts +94 -94
- package/test/server/mcp-server.test.ts +210 -169
- package/test/services/memory-service-edge.test.ts +248 -248
- package/test/services/memory-service.test.ts +278 -278
- package/test/storage/frontmatter-parser.test.ts +222 -222
- package/test/storage/markdown-storage.test.ts +216 -216
- package/test/storage/storage-edge.test.ts +238 -238
- package/test/storage/vector-index.test.ts +153 -153
- package/test/utils/slugify-edge.test.ts +94 -94
- package/test/utils/slugify.test.ts +68 -68
- package/tsconfig.json +25 -25
- package/tsconfig.test.json +8 -8
- 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';
|
package/src/server/index.ts
CHANGED
|
@@ -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';
|
package/src/server/mcp-server.ts
CHANGED
|
@@ -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 {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
import {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
import {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
//
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
//
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
+
}
|
package/src/services/index.ts
CHANGED
|
@@ -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';
|