@synth-coder/memhub 0.1.5 → 0.2.0

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 (43) hide show
  1. package/AGENTS.md +1 -0
  2. package/README.md +2 -2
  3. package/README.zh-CN.md +1 -1
  4. package/dist/src/contracts/mcp.d.ts +2 -5
  5. package/dist/src/contracts/mcp.d.ts.map +1 -1
  6. package/dist/src/contracts/mcp.js +77 -23
  7. package/dist/src/contracts/mcp.js.map +1 -1
  8. package/dist/src/contracts/schemas.d.ts +67 -76
  9. package/dist/src/contracts/schemas.d.ts.map +1 -1
  10. package/dist/src/contracts/schemas.js +4 -8
  11. package/dist/src/contracts/schemas.js.map +1 -1
  12. package/dist/src/contracts/types.d.ts +1 -4
  13. package/dist/src/contracts/types.d.ts.map +1 -1
  14. package/dist/src/contracts/types.js.map +1 -1
  15. package/dist/src/server/mcp-server.d.ts.map +1 -1
  16. package/dist/src/server/mcp-server.js +21 -4
  17. package/dist/src/server/mcp-server.js.map +1 -1
  18. package/dist/src/services/embedding-service.d.ts +43 -0
  19. package/dist/src/services/embedding-service.d.ts.map +1 -0
  20. package/dist/src/services/embedding-service.js +80 -0
  21. package/dist/src/services/embedding-service.js.map +1 -0
  22. package/dist/src/services/memory-service.d.ts +30 -44
  23. package/dist/src/services/memory-service.d.ts.map +1 -1
  24. package/dist/src/services/memory-service.js +212 -161
  25. package/dist/src/services/memory-service.js.map +1 -1
  26. package/dist/src/storage/vector-index.d.ts +62 -0
  27. package/dist/src/storage/vector-index.d.ts.map +1 -0
  28. package/dist/src/storage/vector-index.js +123 -0
  29. package/dist/src/storage/vector-index.js.map +1 -0
  30. package/package.json +16 -13
  31. package/src/contracts/mcp.ts +84 -29
  32. package/src/contracts/schemas.ts +4 -8
  33. package/src/contracts/types.ts +4 -8
  34. package/src/server/mcp-server.ts +23 -7
  35. package/src/services/embedding-service.ts +114 -0
  36. package/src/services/memory-service.ts +252 -179
  37. package/src/storage/vector-index.ts +160 -0
  38. package/test/server/mcp-server.test.ts +11 -9
  39. package/test/services/memory-service-edge.test.ts +1 -1
  40. package/test/services/memory-service.test.ts +1 -1
  41. package/test/storage/vector-index.test.ts +153 -0
  42. package/vitest.config.ts +3 -1
  43. /package/docs/{proposal-close-gates.md → proposals/proposal-close-gates.md} +0 -0
@@ -0,0 +1,62 @@
1
+ /**
2
+ * VectorIndex - LanceDB-backed vector search index for memories.
3
+ *
4
+ * This is a search cache only. Markdown files remain the source of truth.
5
+ * The index can be rebuilt from Markdown files at any time.
6
+ */
7
+ import type { Memory } from '../contracts/types.js';
8
+ /**
9
+ * Row stored in the LanceDB table.
10
+ * The `vector` field is the only one required by LanceDB; all others are metadata filters.
11
+ */
12
+ export interface VectorRow {
13
+ id: string;
14
+ vector: number[];
15
+ title: string;
16
+ category: string;
17
+ tags: string;
18
+ importance: number;
19
+ createdAt: string;
20
+ updatedAt: string;
21
+ }
22
+ export interface VectorSearchResult {
23
+ id: string;
24
+ /** Cosine distance (lower = more similar). Converted to 0-1 score by caller. */
25
+ _distance: number;
26
+ }
27
+ /**
28
+ * LanceDB vector index wrapper.
29
+ * Data lives at `{storagePath}/.lancedb/`.
30
+ */
31
+ export declare class VectorIndex {
32
+ private readonly dbPath;
33
+ private db;
34
+ private table;
35
+ private initPromise;
36
+ constructor(storagePath: string);
37
+ /** Idempotent initialisation — safe to call multiple times. */
38
+ initialize(): Promise<void>;
39
+ private _init;
40
+ /**
41
+ * Upserts a memory row into the index.
42
+ * LanceDB doesn't have a native upsert so we delete-then-add.
43
+ */
44
+ upsert(memory: Memory, vector: number[]): Promise<void>;
45
+ /**
46
+ * Removes a memory from the index by ID.
47
+ */
48
+ delete(id: string): Promise<void>;
49
+ /**
50
+ * Searches for the nearest neighbours to `vector`.
51
+ *
52
+ * @param vector - Query embedding (must be 384-dim)
53
+ * @param limit - Max results to return
54
+ * @returns Array ordered by ascending distance (most similar first)
55
+ */
56
+ search(vector: number[], limit?: number): Promise<VectorSearchResult[]>;
57
+ /**
58
+ * Returns the number of rows in the index.
59
+ */
60
+ count(): Promise<number>;
61
+ }
62
+ //# sourceMappingURL=vector-index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vector-index.d.ts","sourceRoot":"","sources":["../../../src/storage/vector-index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AASpD;;;GAGG;AACH,MAAM,WAAW,SAAS;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,kBAAkB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,gFAAgF;IAChF,SAAS,EAAE,MAAM,CAAC;CACrB;AAED;;;GAGG;AACH,qBAAa,WAAW;IACpB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,EAAE,CAAmC;IAC7C,OAAO,CAAC,KAAK,CAA8B;IAC3C,OAAO,CAAC,WAAW,CAA8B;gBAErC,WAAW,EAAE,MAAM;IAI/B,+DAA+D;IACzD,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;YASnB,KAAK;IAgCnB;;;OAGG;IACG,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAsB7D;;OAEG;IACG,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAKvC;;;;;;OAMG;IACG,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,KAAK,SAAK,GAAG,OAAO,CAAC,kBAAkB,EAAE,CAAC;IAczE;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC;CAIjC"}
@@ -0,0 +1,123 @@
1
+ /**
2
+ * VectorIndex - LanceDB-backed vector search index for memories.
3
+ *
4
+ * This is a search cache only. Markdown files remain the source of truth.
5
+ * The index can be rebuilt from Markdown files at any time.
6
+ */
7
+ import * as lancedb from '@lancedb/lancedb';
8
+ import { mkdir, access } from 'fs/promises';
9
+ import { join } from 'path';
10
+ import { constants } from 'fs';
11
+ const TABLE_NAME = 'memories';
12
+ /** Escape single quotes in id strings to prevent SQL injection */
13
+ function escapeId(id) {
14
+ return id.replace(/'/g, "''");
15
+ }
16
+ /**
17
+ * LanceDB vector index wrapper.
18
+ * Data lives at `{storagePath}/.lancedb/`.
19
+ */
20
+ export class VectorIndex {
21
+ dbPath;
22
+ db = null;
23
+ table = null;
24
+ initPromise = null;
25
+ constructor(storagePath) {
26
+ this.dbPath = join(storagePath, '.lancedb');
27
+ }
28
+ /** Idempotent initialisation — safe to call multiple times. */
29
+ async initialize() {
30
+ if (this.table)
31
+ return;
32
+ if (!this.initPromise) {
33
+ this.initPromise = this._init();
34
+ }
35
+ await this.initPromise;
36
+ }
37
+ async _init() {
38
+ // Ensure the directory exists
39
+ try {
40
+ await access(this.dbPath, constants.F_OK);
41
+ }
42
+ catch {
43
+ await mkdir(this.dbPath, { recursive: true });
44
+ }
45
+ this.db = await lancedb.connect(this.dbPath);
46
+ const existingTables = await this.db.tableNames();
47
+ if (existingTables.includes(TABLE_NAME)) {
48
+ this.table = await this.db.openTable(TABLE_NAME);
49
+ }
50
+ else {
51
+ // Create table with a dummy row so schema is established, then delete it
52
+ const dummy = {
53
+ id: '__init__',
54
+ vector: new Array(384).fill(0),
55
+ title: '',
56
+ category: '',
57
+ tags: '[]',
58
+ importance: 0,
59
+ createdAt: '',
60
+ updatedAt: '',
61
+ };
62
+ // LanceDB expects Record<string, unknown>[] but our VectorRow is typed more strictly
63
+ // Cast is safe here as VectorRow is a subset of Record<string, unknown>
64
+ this.table = await this.db.createTable(TABLE_NAME, [dummy]);
65
+ await this.table.delete(`id = '__init__'`);
66
+ }
67
+ }
68
+ /**
69
+ * Upserts a memory row into the index.
70
+ * LanceDB doesn't have a native upsert so we delete-then-add.
71
+ */
72
+ async upsert(memory, vector) {
73
+ await this.initialize();
74
+ const table = this.table;
75
+ // Remove existing row (if any)
76
+ await table.delete(`id = '${escapeId(memory.id)}'`);
77
+ const row = {
78
+ id: memory.id,
79
+ vector,
80
+ title: memory.title,
81
+ category: memory.category,
82
+ tags: JSON.stringify(memory.tags),
83
+ importance: memory.importance,
84
+ createdAt: memory.createdAt,
85
+ updatedAt: memory.updatedAt,
86
+ };
87
+ // LanceDB expects Record<string, unknown>[] but our VectorRow is typed more strictly
88
+ await table.add([row]);
89
+ }
90
+ /**
91
+ * Removes a memory from the index by ID.
92
+ */
93
+ async delete(id) {
94
+ await this.initialize();
95
+ await this.table.delete(`id = '${escapeId(id)}'`);
96
+ }
97
+ /**
98
+ * Searches for the nearest neighbours to `vector`.
99
+ *
100
+ * @param vector - Query embedding (must be 384-dim)
101
+ * @param limit - Max results to return
102
+ * @returns Array ordered by ascending distance (most similar first)
103
+ */
104
+ async search(vector, limit = 10) {
105
+ await this.initialize();
106
+ const results = await this.table
107
+ .vectorSearch(vector)
108
+ .limit(limit)
109
+ .toArray();
110
+ return results.map((row) => ({
111
+ id: row['id'],
112
+ _distance: row['_distance'],
113
+ }));
114
+ }
115
+ /**
116
+ * Returns the number of rows in the index.
117
+ */
118
+ async count() {
119
+ await this.initialize();
120
+ return this.table.countRows();
121
+ }
122
+ }
123
+ //# sourceMappingURL=vector-index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vector-index.js","sourceRoot":"","sources":["../../../src/storage/vector-index.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@synth-coder/memhub",
3
- "version": "0.1.5",
3
+ "version": "0.2.0",
4
4
  "description": "A Git-friendly memory hub using Markdown-based storage with YAML Front Matter",
5
5
  "type": "module",
6
6
  "main": "dist/src/index.js",
@@ -32,21 +32,24 @@
32
32
  "author": "",
33
33
  "license": "MIT",
34
34
  "dependencies": {
35
+ "@lancedb/lancedb": "^0.26.2",
35
36
  "@modelcontextprotocol/sdk": "^1.27.1",
36
- "yaml": "^2.3.4",
37
- "zod": "^3.22.4"
37
+ "@xenova/transformers": "^2.17.2",
38
+ "sharp": "^0.34.5",
39
+ "yaml": "^2.8.2",
40
+ "zod": "^3.25.76"
38
41
  },
39
42
  "devDependencies": {
40
- "@types/node": "^20.10.0",
41
- "@typescript-eslint/eslint-plugin": "^6.15.0",
42
- "@typescript-eslint/parser": "^6.15.0",
43
- "@vitest/coverage-v8": "^1.1.0",
44
- "eslint": "^8.56.0",
45
- "eslint-config-prettier": "^9.1.0",
46
- "eslint-plugin-import": "^2.29.1",
47
- "prettier": "^3.1.1",
48
- "typescript": "^5.3.3",
49
- "vitest": "^1.1.0"
43
+ "@types/node": "^20.19.35",
44
+ "@typescript-eslint/eslint-plugin": "^6.21.0",
45
+ "@typescript-eslint/parser": "^6.21.0",
46
+ "@vitest/coverage-v8": "^1.6.1",
47
+ "eslint": "^8.57.1",
48
+ "eslint-config-prettier": "^9.1.2",
49
+ "eslint-plugin-import": "^2.32.0",
50
+ "prettier": "^3.8.1",
51
+ "typescript": "^5.9.3",
52
+ "vitest": "^1.6.1"
50
53
  },
51
54
  "engines": {
52
55
  "node": ">=18.0.0"
@@ -15,7 +15,7 @@ export const MCP_PROTOCOL_VERSION = '2024-11-05';
15
15
  /** Server information */
16
16
  export const SERVER_INFO = {
17
17
  name: 'memhub',
18
- version: '0.1.0',
18
+ version: '0.2.0',
19
19
  } as const;
20
20
 
21
21
  // ============================================================================
@@ -51,25 +51,47 @@ export type ToolName = (typeof TOOL_NAMES)[number];
51
51
  export const TOOL_DEFINITIONS: readonly Tool[] = [
52
52
  {
53
53
  name: 'memory_load',
54
- description:
55
- 'STM first step. Call at the first turn after receiving user prompt to load short-term memory context for this session/task.',
54
+ description: `Retrieve stored memories to recall user preferences, past decisions, project context, and learned knowledge.
55
+
56
+ WHEN TO USE (call proactively):
57
+ • Starting a new conversation or task
58
+ • User references past discussions ("remember when...", "as I mentioned before")
59
+ • Need context about user's coding style, preferences, or project decisions
60
+ • Uncertain about user's existing preferences or constraints
61
+ • Before making assumptions about user requirements
62
+
63
+ WHAT IT PROVIDES:
64
+ • User preferences (coding style, frameworks, naming conventions)
65
+ • Past decisions and their rationale
66
+ • Project-specific context and constraints
67
+ • Previously learned knowledge about the user
68
+
69
+ Call this early to provide personalized, context-aware responses.`,
56
70
  inputSchema: {
57
71
  type: 'object',
58
72
  properties: {
59
- id: { type: 'string', description: 'Optional memory id for direct fetch' },
60
- sessionId: {
73
+ query: {
61
74
  type: 'string',
62
- description: 'Optional session UUID to load current CLI/task context',
75
+ description:
76
+ 'Search query to find relevant memories. Examples: "react preferences", "error handling approach", "database choice"',
63
77
  },
64
- date: { type: 'string', description: 'Optional date filter (YYYY-MM-DD)' },
65
- query: { type: 'string', description: 'Optional text query for relevant context' },
66
- category: { type: 'string' },
67
- tags: { type: 'array', items: { type: 'string' } },
68
- limit: { type: 'number', description: 'Max results, default 20' },
69
- scope: {
78
+ id: {
70
79
  type: 'string',
71
- enum: ['stm', 'all'],
72
- description: 'stm for short-term context window; all for broader retrieval',
80
+ description: 'Direct lookup by memory ID (if known)',
81
+ },
82
+ category: {
83
+ type: 'string',
84
+ description:
85
+ 'Filter by category: "general" (default), "preference", "decision", "knowledge", "project"',
86
+ },
87
+ tags: {
88
+ type: 'array',
89
+ items: { type: 'string' },
90
+ description: 'Filter by tags. Example: ["typescript", "backend"]',
91
+ },
92
+ limit: {
93
+ type: 'number',
94
+ description: 'Max results (default: 20, max: 100)',
73
95
  },
74
96
  },
75
97
  additionalProperties: false,
@@ -77,23 +99,59 @@ export const TOOL_DEFINITIONS: readonly Tool[] = [
77
99
  },
78
100
  {
79
101
  name: 'memory_update',
80
- description:
81
- 'STM write-back step. Call at the final turn to append/upsert new decisions, preferences, task-state changes, and key outputs.',
102
+ description: `Store important information to remember for future conversations.
103
+
104
+ WHEN TO USE (call when learning something worth remembering):
105
+ • User explicitly states a preference ("I prefer functional components")
106
+ • User makes a decision with rationale ("We'll use PostgreSQL because...")
107
+ • You discover important project context (tech stack, constraints, patterns)
108
+ • User corrects your assumption ("Actually, I don't use Redux")
109
+ • Task state changes that should persist
110
+
111
+ WHAT TO STORE:
112
+ • Preferences: coding style, frameworks, naming conventions
113
+ • Decisions: architecture choices, library selections, with reasoning
114
+ • Knowledge: project-specific patterns, gotchas, conventions
115
+ • Context: team structure, deployment process, testing approach
116
+
117
+ TIPS:
118
+ • content is required and most important
119
+ • title helps with search (auto-generated if omitted)
120
+ • Use entryType to categorize: "preference", "decision", "context", "fact"
121
+ • importance: 1-5 (default: 3), higher = more critical to remember`,
82
122
  inputSchema: {
83
123
  type: 'object',
84
124
  properties: {
85
- id: { type: 'string', description: 'Optional id. Present = update existing record' },
86
- sessionId: { type: 'string', description: 'Session UUID. Auto-created if omitted' },
87
- mode: { type: 'string', enum: ['append', 'upsert'], description: 'Default append' },
125
+ content: {
126
+ type: 'string',
127
+ description:
128
+ 'The information to remember. Be specific and include context. Example: "User prefers TypeScript with strict mode. Uses functional React components with hooks. Avoids class components."',
129
+ },
130
+ title: {
131
+ type: 'string',
132
+ description:
133
+ 'Brief title for the memory (auto-generated from content if omitted). Example: "TypeScript and React preferences"',
134
+ },
88
135
  entryType: {
89
136
  type: 'string',
90
- enum: ['decision', 'preference', 'knowledge', 'todo', 'state_change'],
137
+ enum: ['preference', 'decision', 'context', 'fact'],
138
+ description:
139
+ 'Type of memory. "preference" for user likes/dislikes, "decision" for choices made, "context" for project info, "fact" for learned facts',
140
+ },
141
+ category: {
142
+ type: 'string',
143
+ description:
144
+ 'Category for grouping. Default: "general". Example: "frontend", "backend", "project-alpha"',
145
+ },
146
+ tags: {
147
+ type: 'array',
148
+ items: { type: 'string' },
149
+ description: 'Tags for filtering. Example: ["typescript", "react", "coding-style"]',
150
+ },
151
+ importance: {
152
+ type: 'number',
153
+ description: 'Importance level 1-5. 5 = critical, always recall. Default: 3',
91
154
  },
92
- title: { type: 'string', description: 'Optional title' },
93
- content: { type: 'string', description: 'Required memory body' },
94
- tags: { type: 'array', items: { type: 'string' } },
95
- category: { type: 'string' },
96
- importance: { type: 'number' },
97
155
  },
98
156
  required: ['content'],
99
157
  additionalProperties: false,
@@ -145,20 +203,17 @@ export type ToolResult<T extends ToolName> = T extends 'memory_load'
145
203
  export type ToolInput<T extends ToolName> = T extends 'memory_load'
146
204
  ? {
147
205
  id?: string;
148
- sessionId?: string;
149
- date?: string;
150
206
  query?: string;
151
207
  category?: string;
152
208
  tags?: string[];
153
209
  limit?: number;
154
- scope?: 'stm' | 'all';
155
210
  }
156
211
  : T extends 'memory_update'
157
212
  ? {
158
213
  id?: string;
159
214
  sessionId?: string;
160
215
  mode?: 'append' | 'upsert';
161
- entryType?: 'decision' | 'preference' | 'knowledge' | 'todo' | 'state_change';
216
+ entryType?: 'preference' | 'decision' | 'context' | 'fact';
162
217
  title?: string;
163
218
  content: string;
164
219
  tags?: string[];
@@ -41,11 +41,10 @@ export const ImportanceSchema = z.number().int().min(1).max(5);
41
41
 
42
42
  /** STM memory entry type */
43
43
  export const MemoryEntryTypeSchema = z.enum([
44
- 'decision',
45
- 'preference',
46
- 'knowledge',
47
- 'todo',
48
- 'state_change',
44
+ 'preference', // User likes/dislikes
45
+ 'decision', // Technical choices with reasoning
46
+ 'context', // Project/environment information
47
+ 'fact', // Objective knowledge
49
48
  ]);
50
49
 
51
50
  // ============================================================================
@@ -208,13 +207,10 @@ export const SearchMemoryInputSchema = z.object({
208
207
  /** memory_load input schema */
209
208
  export const MemoryLoadInputSchema = z.object({
210
209
  id: UUIDSchema.optional(),
211
- sessionId: UUIDSchema.optional(),
212
- date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
213
210
  query: z.string().min(1).max(1000).optional(),
214
211
  category: CategorySchema.optional(),
215
212
  tags: z.array(TagSchema).optional(),
216
213
  limit: z.number().int().min(1).max(100).optional(),
217
- scope: z.enum(['stm', 'all']).optional(),
218
214
  });
219
215
 
220
216
  /** memory_update (upsert/append) input schema */
@@ -25,11 +25,10 @@ export type Slug = string;
25
25
  * Content is split between YAML Front Matter (metadata) and Markdown body
26
26
  */
27
27
  export type MemoryEntryType =
28
- | 'decision'
29
- | 'preference'
30
- | 'knowledge'
31
- | 'todo'
32
- | 'state_change';
28
+ | 'preference' // User likes/dislikes
29
+ | 'decision' // Technical choices with reasoning
30
+ | 'context' // Project/environment information
31
+ | 'fact'; // Objective knowledge
33
32
 
34
33
  export interface Memory {
35
34
  /** UUID v4 unique identifier */
@@ -249,11 +248,8 @@ export interface SearchMemoryInput extends Partial<MemoryFilter> {
249
248
  */
250
249
  export interface MemoryLoadInput extends Partial<MemoryFilter> {
251
250
  readonly id?: UUID;
252
- readonly sessionId?: UUID;
253
- readonly date?: string;
254
251
  readonly query?: string;
255
252
  readonly limit?: number;
256
- readonly scope?: 'stm' | 'all';
257
253
  }
258
254
 
259
255
  /**
@@ -4,10 +4,9 @@
4
4
  * Uses @modelcontextprotocol/sdk for protocol handling
5
5
  */
6
6
 
7
- import { readFileSync } from 'fs';
7
+ import { readFileSync, existsSync } from 'fs';
8
8
  import { join, dirname } from 'path';
9
9
  import { fileURLToPath } from 'url';
10
- import { homedir } from 'os';
11
10
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
12
11
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
13
12
  import {
@@ -30,7 +29,12 @@ interface PackageJson {
30
29
  }
31
30
 
32
31
  // npm package runtime: dist/src/server -> package root
33
- const packageJsonPath = join(__dirname, '../../../package.json');
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
+ }
34
38
 
35
39
  const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8')) as PackageJson;
36
40
 
@@ -38,9 +42,9 @@ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8')) as Packag
38
42
  * Create McpServer instance using SDK
39
43
  */
40
44
  export function createMcpServer(): Server {
41
- const defaultStoragePath = join(homedir(), '.memhub');
42
- const storagePath = process.env.MEMHUB_STORAGE_PATH || defaultStoragePath;
43
- const memoryService = new MemoryService({ storagePath });
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 });
44
48
 
45
49
  // Create server using SDK
46
50
  const server = new Server(
@@ -143,4 +147,16 @@ async function main(): Promise<void> {
143
147
  main().catch((error) => {
144
148
  console.error('Fatal error:', error);
145
149
  process.exit(1);
146
- });
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
+ });
162
+ }
@@ -0,0 +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
+ }