@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.
- package/AGENTS.md +1 -0
- package/README.md +2 -2
- package/README.zh-CN.md +1 -1
- package/dist/src/contracts/mcp.d.ts +2 -5
- package/dist/src/contracts/mcp.d.ts.map +1 -1
- package/dist/src/contracts/mcp.js +77 -23
- package/dist/src/contracts/mcp.js.map +1 -1
- package/dist/src/contracts/schemas.d.ts +67 -76
- package/dist/src/contracts/schemas.d.ts.map +1 -1
- package/dist/src/contracts/schemas.js +4 -8
- package/dist/src/contracts/schemas.js.map +1 -1
- package/dist/src/contracts/types.d.ts +1 -4
- package/dist/src/contracts/types.d.ts.map +1 -1
- package/dist/src/contracts/types.js.map +1 -1
- package/dist/src/server/mcp-server.d.ts.map +1 -1
- package/dist/src/server/mcp-server.js +21 -4
- package/dist/src/server/mcp-server.js.map +1 -1
- package/dist/src/services/embedding-service.d.ts +43 -0
- package/dist/src/services/embedding-service.d.ts.map +1 -0
- package/dist/src/services/embedding-service.js +80 -0
- package/dist/src/services/embedding-service.js.map +1 -0
- package/dist/src/services/memory-service.d.ts +30 -44
- package/dist/src/services/memory-service.d.ts.map +1 -1
- package/dist/src/services/memory-service.js +212 -161
- package/dist/src/services/memory-service.js.map +1 -1
- package/dist/src/storage/vector-index.d.ts +62 -0
- package/dist/src/storage/vector-index.d.ts.map +1 -0
- package/dist/src/storage/vector-index.js +123 -0
- package/dist/src/storage/vector-index.js.map +1 -0
- package/package.json +16 -13
- package/src/contracts/mcp.ts +84 -29
- package/src/contracts/schemas.ts +4 -8
- package/src/contracts/types.ts +4 -8
- package/src/server/mcp-server.ts +23 -7
- package/src/services/embedding-service.ts +114 -0
- package/src/services/memory-service.ts +252 -179
- package/src/storage/vector-index.ts +160 -0
- package/test/server/mcp-server.test.ts +11 -9
- package/test/services/memory-service-edge.test.ts +1 -1
- package/test/services/memory-service.test.ts +1 -1
- package/test/storage/vector-index.test.ts +153 -0
- package/vitest.config.ts +3 -1
- /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.
|
|
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
|
-
"
|
|
37
|
-
"
|
|
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.
|
|
41
|
-
"@typescript-eslint/eslint-plugin": "^6.
|
|
42
|
-
"@typescript-eslint/parser": "^6.
|
|
43
|
-
"@vitest/coverage-v8": "^1.1
|
|
44
|
-
"eslint": "^8.
|
|
45
|
-
"eslint-config-prettier": "^9.1.
|
|
46
|
-
"eslint-plugin-import": "^2.
|
|
47
|
-
"prettier": "^3.
|
|
48
|
-
"typescript": "^5.
|
|
49
|
-
"vitest": "^1.1
|
|
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"
|
package/src/contracts/mcp.ts
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
|
|
60
|
-
sessionId: {
|
|
73
|
+
query: {
|
|
61
74
|
type: 'string',
|
|
62
|
-
description:
|
|
75
|
+
description:
|
|
76
|
+
'Search query to find relevant memories. Examples: "react preferences", "error handling approach", "database choice"',
|
|
63
77
|
},
|
|
64
|
-
|
|
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
|
-
|
|
72
|
-
|
|
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
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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: ['
|
|
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?: '
|
|
216
|
+
entryType?: 'preference' | 'decision' | 'context' | 'fact';
|
|
162
217
|
title?: string;
|
|
163
218
|
content: string;
|
|
164
219
|
tags?: string[];
|
package/src/contracts/schemas.ts
CHANGED
|
@@ -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
|
-
'
|
|
45
|
-
'
|
|
46
|
-
'
|
|
47
|
-
'
|
|
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 */
|
package/src/contracts/types.ts
CHANGED
|
@@ -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
|
-
| '
|
|
29
|
-
| '
|
|
30
|
-
| '
|
|
31
|
-
| '
|
|
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
|
/**
|
package/src/server/mcp-server.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
42
|
-
const
|
|
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
|
+
}
|