@synth-coder/memhub 0.2.2 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +48 -12
- package/.github/workflows/release.yml +67 -0
- package/AGENTS.md +158 -17
- package/README.md +147 -66
- package/README.zh-CN.md +75 -23
- package/dist/src/cli/agents/claude-code.d.ts +5 -0
- package/dist/src/cli/agents/claude-code.d.ts.map +1 -0
- package/dist/src/cli/agents/claude-code.js +14 -0
- package/dist/src/cli/agents/claude-code.js.map +1 -0
- package/dist/src/cli/agents/cline.d.ts +5 -0
- package/dist/src/cli/agents/cline.d.ts.map +1 -0
- package/dist/src/cli/agents/cline.js +14 -0
- package/dist/src/cli/agents/cline.js.map +1 -0
- package/dist/src/cli/agents/codex.d.ts +5 -0
- package/dist/src/cli/agents/codex.d.ts.map +1 -0
- package/dist/src/cli/agents/codex.js +14 -0
- package/dist/src/cli/agents/codex.js.map +1 -0
- package/dist/src/cli/agents/cursor.d.ts +5 -0
- package/dist/src/cli/agents/cursor.d.ts.map +1 -0
- package/dist/src/cli/agents/cursor.js +14 -0
- package/dist/src/cli/agents/cursor.js.map +1 -0
- package/dist/src/cli/agents/factory-droid.d.ts +5 -0
- package/dist/src/cli/agents/factory-droid.d.ts.map +1 -0
- package/dist/src/cli/agents/factory-droid.js +14 -0
- package/dist/src/cli/agents/factory-droid.js.map +1 -0
- package/dist/src/cli/agents/gemini-cli.d.ts +5 -0
- package/dist/src/cli/agents/gemini-cli.d.ts.map +1 -0
- package/dist/src/cli/agents/gemini-cli.js +14 -0
- package/dist/src/cli/agents/gemini-cli.js.map +1 -0
- package/dist/src/cli/agents/index.d.ts +14 -0
- package/dist/src/cli/agents/index.d.ts.map +1 -0
- package/dist/src/cli/agents/index.js +30 -0
- package/dist/src/cli/agents/index.js.map +1 -0
- package/dist/src/cli/agents/windsurf.d.ts +5 -0
- package/dist/src/cli/agents/windsurf.d.ts.map +1 -0
- package/dist/src/cli/agents/windsurf.js +14 -0
- package/dist/src/cli/agents/windsurf.js.map +1 -0
- package/dist/src/cli/index.d.ts +8 -0
- package/dist/src/cli/index.d.ts.map +1 -0
- package/dist/src/cli/index.js +168 -0
- package/dist/src/cli/index.js.map +1 -0
- package/dist/src/cli/init.d.ts +34 -0
- package/dist/src/cli/init.d.ts.map +1 -0
- package/dist/src/cli/init.js +160 -0
- package/dist/src/cli/init.js.map +1 -0
- package/dist/src/cli/instructions.d.ts +29 -0
- package/dist/src/cli/instructions.d.ts.map +1 -0
- package/dist/src/cli/instructions.js +141 -0
- package/dist/src/cli/instructions.js.map +1 -0
- package/dist/src/cli/types.d.ts +22 -0
- package/dist/src/cli/types.d.ts.map +1 -0
- package/dist/src/cli/types.js +86 -0
- package/dist/src/cli/types.js.map +1 -0
- package/dist/src/contracts/schemas.js.map +1 -1
- 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 +30 -16
- package/dist/src/server/mcp-server.js.map +1 -1
- package/dist/src/services/embedding-service.d.ts.map +1 -1
- package/dist/src/services/embedding-service.js +1 -1
- package/dist/src/services/embedding-service.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/dist/src/storage/markdown-storage.d.ts.map +1 -1
- package/dist/src/storage/markdown-storage.js +1 -1
- package/dist/src/storage/markdown-storage.js.map +1 -1
- package/dist/src/storage/vector-index.d.ts.map +1 -1
- package/dist/src/storage/vector-index.js +4 -5
- package/dist/src/storage/vector-index.js.map +1 -1
- package/docs/README.md +21 -0
- package/docs/mcp-tools.md +136 -0
- package/docs/user-guide.md +182 -0
- package/package.json +22 -19
- package/src/cli/agents/claude-code.ts +14 -0
- package/src/cli/agents/cline.ts +14 -0
- package/src/cli/agents/codex.ts +14 -0
- package/src/cli/agents/cursor.ts +14 -0
- package/src/cli/agents/factory-droid.ts +14 -0
- package/src/cli/agents/gemini-cli.ts +14 -0
- package/src/cli/agents/index.ts +36 -0
- package/src/cli/agents/windsurf.ts +14 -0
- package/src/cli/index.ts +192 -0
- package/src/cli/init.ts +218 -0
- package/src/cli/instructions.ts +156 -0
- package/src/cli/types.ts +112 -0
- package/src/contracts/mcp.ts +1 -1
- package/src/contracts/schemas.ts +4 -4
- package/src/contracts/types.ts +4 -4
- package/src/server/mcp-server.ts +36 -29
- package/src/services/embedding-service.ts +80 -80
- package/src/services/memory-service.ts +142 -107
- package/src/storage/markdown-storage.ts +1 -9
- package/src/storage/vector-index.ts +117 -118
- package/test/cli/init.test.ts +380 -0
- package/test/server/mcp-server.test.ts +45 -3
- package/test/services/memory-service.test.ts +16 -4
- package/test/storage/frontmatter-parser.test.ts +1 -1
- package/test/storage/markdown-storage.test.ts +19 -10
- package/test/storage/vector-index.test.ts +129 -133
- package/test/utils/slugify.test.ts +5 -1
- package/docs/architecture.md +0 -349
- package/docs/contracts.md +0 -119
- package/docs/prompt-template.md +0 -79
- package/docs/proposals/mcp-typescript-sdk-refactor.md +0 -568
- package/docs/proposals/proposal-close-gates.md +0 -58
- package/docs/tool-calling-policy.md +0 -107
package/src/contracts/types.ts
CHANGED
|
@@ -25,10 +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
|
-
| 'preference'
|
|
29
|
-
| 'decision'
|
|
30
|
-
| 'context'
|
|
31
|
-
| 'fact';
|
|
28
|
+
| 'preference' // User likes/dislikes
|
|
29
|
+
| 'decision' // Technical choices with reasoning
|
|
30
|
+
| 'context' // Project/environment information
|
|
31
|
+
| 'fact'; // Objective knowledge
|
|
32
32
|
|
|
33
33
|
export interface Memory {
|
|
34
34
|
/** UUID v4 unique identifier */
|
package/src/server/mcp-server.ts
CHANGED
|
@@ -5,19 +5,14 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { readFileSync, existsSync } from 'fs';
|
|
8
|
-
import { join, dirname } from 'path';
|
|
8
|
+
import { join, dirname, resolve } from 'path';
|
|
9
9
|
import { fileURLToPath } from 'url';
|
|
10
|
+
import { homedir } from 'os';
|
|
10
11
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
11
12
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
12
|
-
import {
|
|
13
|
-
CallToolRequestSchema,
|
|
14
|
-
ListToolsRequestSchema,
|
|
15
|
-
} from '@modelcontextprotocol/sdk/types.js';
|
|
13
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
16
14
|
import { MemoryService, ServiceError } from '../services/memory-service.js';
|
|
17
|
-
import {
|
|
18
|
-
MemoryLoadInputSchema,
|
|
19
|
-
MemoryUpdateInputV2Schema,
|
|
20
|
-
} from '../contracts/schemas.js';
|
|
15
|
+
import { MemoryLoadInputSchema, MemoryUpdateInputV2Schema } from '../contracts/schemas.js';
|
|
21
16
|
import { TOOL_DEFINITIONS, SERVER_INFO } from '../contracts/mcp.js';
|
|
22
17
|
import { ErrorCode } from '../contracts/types.js';
|
|
23
18
|
|
|
@@ -38,11 +33,34 @@ if (!existsSync(packageJsonPath)) {
|
|
|
38
33
|
|
|
39
34
|
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8')) as PackageJson;
|
|
40
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
|
+
|
|
41
59
|
/**
|
|
42
60
|
* Create McpServer instance using SDK
|
|
43
61
|
*/
|
|
44
62
|
export function createMcpServer(): Server {
|
|
45
|
-
const storagePath =
|
|
63
|
+
const storagePath = resolveStoragePath();
|
|
46
64
|
const vectorSearch = process.env.MEMHUB_VECTOR_SEARCH !== 'false';
|
|
47
65
|
const memoryService = new MemoryService({ storagePath, vectorSearch });
|
|
48
66
|
|
|
@@ -66,7 +84,7 @@ export function createMcpServer(): Server {
|
|
|
66
84
|
});
|
|
67
85
|
|
|
68
86
|
// Handle tools/call request
|
|
69
|
-
server.setRequestHandler(CallToolRequestSchema, async
|
|
87
|
+
server.setRequestHandler(CallToolRequestSchema, async request => {
|
|
70
88
|
const { name, arguments: args } = request.params;
|
|
71
89
|
|
|
72
90
|
try {
|
|
@@ -86,10 +104,7 @@ export function createMcpServer(): Server {
|
|
|
86
104
|
}
|
|
87
105
|
|
|
88
106
|
default:
|
|
89
|
-
throw new ServiceError(
|
|
90
|
-
`Unknown tool: ${name}`,
|
|
91
|
-
ErrorCode.METHOD_NOT_FOUND
|
|
92
|
-
);
|
|
107
|
+
throw new ServiceError(`Unknown tool: ${name}`, ErrorCode.METHOD_NOT_FOUND);
|
|
93
108
|
}
|
|
94
109
|
|
|
95
110
|
return {
|
|
@@ -134,7 +149,7 @@ export function createMcpServer(): Server {
|
|
|
134
149
|
}
|
|
135
150
|
|
|
136
151
|
/**
|
|
137
|
-
* Start the MCP server
|
|
152
|
+
* Start the MCP server (only when run directly)
|
|
138
153
|
*/
|
|
139
154
|
async function main(): Promise<void> {
|
|
140
155
|
const server = createMcpServer();
|
|
@@ -144,19 +159,11 @@ async function main(): Promise<void> {
|
|
|
144
159
|
console.error('MemHub MCP Server running on stdio');
|
|
145
160
|
}
|
|
146
161
|
|
|
147
|
-
main()
|
|
148
|
-
console.error('Fatal error:', error);
|
|
149
|
-
process.exit(1);
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
// Check if this file is being run directly
|
|
162
|
+
// Only run main() when this file is executed directly
|
|
153
163
|
const isMain = import.meta.url === `file://${process.argv[1]}` || false;
|
|
154
164
|
if (isMain) {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
console.error('Fatal error:', error);
|
|
159
|
-
process.exit(1);
|
|
160
|
-
});
|
|
165
|
+
main().catch(error => {
|
|
166
|
+
console.error('Fatal error:', error);
|
|
167
|
+
process.exit(1);
|
|
161
168
|
});
|
|
162
|
-
}
|
|
169
|
+
}
|
|
@@ -14,20 +14,20 @@ const MODEL_NAME = 'Xenova/all-MiniLM-L6-v2';
|
|
|
14
14
|
export const VECTOR_DIM = 384;
|
|
15
15
|
|
|
16
16
|
type FeatureExtractionPipeline = (
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
text: string,
|
|
18
|
+
options: { pooling: string; normalize: boolean }
|
|
19
19
|
) => Promise<{ data: Float32Array }>;
|
|
20
20
|
|
|
21
21
|
type TransformersModule = {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
31
|
};
|
|
32
32
|
|
|
33
33
|
/**
|
|
@@ -35,80 +35,80 @@ type TransformersModule = {
|
|
|
35
35
|
* The model is downloaded once and cached in `~/.cache/huggingface`.
|
|
36
36
|
*/
|
|
37
37
|
export class EmbeddingService {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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();
|
|
45
50
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
+
})();
|
|
52
74
|
}
|
|
53
75
|
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
}
|
|
76
|
+
await this.initPromise;
|
|
77
|
+
}
|
|
78
78
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
}
|
|
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();
|
|
99
87
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
*/
|
|
103
|
-
async embedMemory(title: string, content: string): Promise<number[]> {
|
|
104
|
-
return this.embed(`${title} ${content}`.trim());
|
|
88
|
+
if (!this.extractor) {
|
|
89
|
+
throw new Error('EmbeddingService: extractor not initialized');
|
|
105
90
|
}
|
|
106
91
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
114
|
}
|
|
@@ -23,6 +23,9 @@ import type {
|
|
|
23
23
|
} from '../contracts/types.js';
|
|
24
24
|
import { ErrorCode } from '../contracts/types.js';
|
|
25
25
|
import { MarkdownStorage, StorageError } from '../storage/markdown-storage.js';
|
|
26
|
+
import lockfile from 'proper-lockfile';
|
|
27
|
+
|
|
28
|
+
const LOCK_TIMEOUT = 5000; // 5 seconds
|
|
26
29
|
|
|
27
30
|
/** Minimal interface required from VectorIndex (avoids static import of native module) */
|
|
28
31
|
interface IVectorIndex {
|
|
@@ -68,12 +71,14 @@ export interface MemoryServiceConfig {
|
|
|
68
71
|
* Memory service implementation
|
|
69
72
|
*/
|
|
70
73
|
export class MemoryService {
|
|
74
|
+
private readonly storagePath: string;
|
|
71
75
|
private readonly storage: MarkdownStorage;
|
|
72
76
|
private readonly vectorIndex: IVectorIndex | null;
|
|
73
77
|
private readonly embedding: IEmbeddingService | null;
|
|
74
78
|
private readonly vectorSearchEnabled: boolean;
|
|
75
79
|
|
|
76
80
|
constructor(config: MemoryServiceConfig) {
|
|
81
|
+
this.storagePath = config.storagePath;
|
|
77
82
|
this.storage = new MarkdownStorage({ storagePath: config.storagePath });
|
|
78
83
|
this.vectorSearchEnabled = config.vectorSearch !== false;
|
|
79
84
|
|
|
@@ -101,7 +106,7 @@ export class MemoryService {
|
|
|
101
106
|
await initPromise;
|
|
102
107
|
return resolvedVectorIndex!.upsert(memory, vector);
|
|
103
108
|
},
|
|
104
|
-
delete: async
|
|
109
|
+
delete: async id => {
|
|
105
110
|
await initPromise;
|
|
106
111
|
return resolvedVectorIndex!.delete(id);
|
|
107
112
|
},
|
|
@@ -116,7 +121,7 @@ export class MemoryService {
|
|
|
116
121
|
await initPromise;
|
|
117
122
|
return resolvedEmbedding!.embedMemory(title, content);
|
|
118
123
|
},
|
|
119
|
-
embed: async
|
|
124
|
+
embed: async text => {
|
|
120
125
|
await initPromise;
|
|
121
126
|
return resolvedEmbedding!.embed(text);
|
|
122
127
|
},
|
|
@@ -172,29 +177,36 @@ export class MemoryService {
|
|
|
172
177
|
* Creates a new memory entry
|
|
173
178
|
*/
|
|
174
179
|
async create(input: CreateMemoryInput): Promise<CreateResult> {
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
const memory: Memory = {
|
|
179
|
-
id,
|
|
180
|
-
createdAt: now,
|
|
181
|
-
updatedAt: now,
|
|
182
|
-
tags: input.tags ?? [],
|
|
183
|
-
category: input.category ?? 'general',
|
|
184
|
-
importance: input.importance ?? 3,
|
|
185
|
-
title: input.title,
|
|
186
|
-
content: input.content,
|
|
187
|
-
};
|
|
188
|
-
|
|
180
|
+
const release = await lockfile.lock(this.storagePath, {
|
|
181
|
+
retries: { retries: 100, minTimeout: 50, maxTimeout: LOCK_TIMEOUT / 100 },
|
|
182
|
+
});
|
|
189
183
|
try {
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
184
|
+
const now = new Date().toISOString();
|
|
185
|
+
const id = randomUUID();
|
|
186
|
+
|
|
187
|
+
const memory: Memory = {
|
|
188
|
+
id,
|
|
189
|
+
createdAt: now,
|
|
190
|
+
updatedAt: now,
|
|
191
|
+
tags: input.tags ?? [],
|
|
192
|
+
category: input.category ?? 'general',
|
|
193
|
+
importance: input.importance ?? 3,
|
|
194
|
+
title: input.title,
|
|
195
|
+
content: input.content,
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
const filePath = await this.storage.write(memory);
|
|
200
|
+
this.scheduleVectorUpsert(memory);
|
|
201
|
+
return { id, filePath, memory };
|
|
202
|
+
} catch (error) {
|
|
203
|
+
throw new ServiceError(
|
|
204
|
+
`Failed to create memory: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
205
|
+
ErrorCode.STORAGE_ERROR
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
} finally {
|
|
209
|
+
await release();
|
|
198
210
|
}
|
|
199
211
|
}
|
|
200
212
|
|
|
@@ -220,38 +232,45 @@ export class MemoryService {
|
|
|
220
232
|
* Updates an existing memory
|
|
221
233
|
*/
|
|
222
234
|
async update(input: UpdateMemoryInput): Promise<UpdateResult> {
|
|
223
|
-
|
|
235
|
+
const release = await lockfile.lock(this.storagePath, {
|
|
236
|
+
retries: { retries: 100, minTimeout: 50, maxTimeout: LOCK_TIMEOUT / 100 },
|
|
237
|
+
});
|
|
224
238
|
try {
|
|
225
|
-
existing
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
239
|
+
let existing: Memory;
|
|
240
|
+
try {
|
|
241
|
+
existing = await this.storage.read(input.id);
|
|
242
|
+
} catch (error) {
|
|
243
|
+
if (error instanceof StorageError && error.message.includes('not found')) {
|
|
244
|
+
throw new ServiceError(`Memory not found: ${input.id}`, ErrorCode.NOT_FOUND);
|
|
245
|
+
}
|
|
246
|
+
throw new ServiceError(
|
|
247
|
+
`Failed to read memory for update: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
248
|
+
ErrorCode.STORAGE_ERROR
|
|
249
|
+
);
|
|
229
250
|
}
|
|
230
|
-
throw new ServiceError(
|
|
231
|
-
`Failed to read memory for update: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
232
|
-
ErrorCode.STORAGE_ERROR
|
|
233
|
-
);
|
|
234
|
-
}
|
|
235
251
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
252
|
+
const updated: Memory = {
|
|
253
|
+
...existing,
|
|
254
|
+
updatedAt: new Date().toISOString(),
|
|
255
|
+
...(input.title !== undefined && { title: input.title }),
|
|
256
|
+
...(input.content !== undefined && { content: input.content }),
|
|
257
|
+
...(input.tags !== undefined && { tags: input.tags }),
|
|
258
|
+
...(input.category !== undefined && { category: input.category }),
|
|
259
|
+
...(input.importance !== undefined && { importance: input.importance }),
|
|
260
|
+
};
|
|
245
261
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
262
|
+
try {
|
|
263
|
+
await this.storage.write(updated);
|
|
264
|
+
this.scheduleVectorUpsert(updated);
|
|
265
|
+
return { memory: updated };
|
|
266
|
+
} catch (error) {
|
|
267
|
+
throw new ServiceError(
|
|
268
|
+
`Failed to update memory: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
269
|
+
ErrorCode.STORAGE_ERROR
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
} finally {
|
|
273
|
+
await release();
|
|
255
274
|
}
|
|
256
275
|
}
|
|
257
276
|
|
|
@@ -259,6 +278,9 @@ export class MemoryService {
|
|
|
259
278
|
* Deletes a memory by ID
|
|
260
279
|
*/
|
|
261
280
|
async delete(input: DeleteMemoryInput): Promise<DeleteResult> {
|
|
281
|
+
const release = await lockfile.lock(this.storagePath, {
|
|
282
|
+
retries: { retries: 100, minTimeout: 50, maxTimeout: LOCK_TIMEOUT / 100 },
|
|
283
|
+
});
|
|
262
284
|
try {
|
|
263
285
|
const filePath = await this.storage.delete(input.id);
|
|
264
286
|
await this.removeFromVectorIndex(input.id);
|
|
@@ -271,6 +293,8 @@ export class MemoryService {
|
|
|
271
293
|
`Failed to delete memory: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
272
294
|
ErrorCode.STORAGE_ERROR
|
|
273
295
|
);
|
|
296
|
+
} finally {
|
|
297
|
+
await release();
|
|
274
298
|
}
|
|
275
299
|
}
|
|
276
300
|
|
|
@@ -288,9 +312,7 @@ export class MemoryService {
|
|
|
288
312
|
let memories: Memory[] = [];
|
|
289
313
|
for (const file of files) {
|
|
290
314
|
try {
|
|
291
|
-
const memory = await this.storage.read(
|
|
292
|
-
this.extractIdFromContent(file.content)
|
|
293
|
-
);
|
|
315
|
+
const memory = await this.storage.read(this.extractIdFromContent(file.content));
|
|
294
316
|
memories.push(memory);
|
|
295
317
|
} catch {
|
|
296
318
|
continue;
|
|
@@ -301,9 +323,7 @@ export class MemoryService {
|
|
|
301
323
|
memories = memories.filter(m => m.category === input.category);
|
|
302
324
|
}
|
|
303
325
|
if (input.tags && input.tags.length > 0) {
|
|
304
|
-
memories = memories.filter(m =>
|
|
305
|
-
input.tags!.every(tag => m.tags.includes(tag))
|
|
306
|
-
);
|
|
326
|
+
memories = memories.filter(m => input.tags!.every(tag => m.tags.includes(tag)));
|
|
307
327
|
}
|
|
308
328
|
if (input.fromDate) {
|
|
309
329
|
memories = memories.filter(m => m.createdAt >= input.fromDate!);
|
|
@@ -361,10 +381,7 @@ export class MemoryService {
|
|
|
361
381
|
if (this.vectorSearchEnabled && this.vectorIndex && this.embedding) {
|
|
362
382
|
try {
|
|
363
383
|
const queryVec = await this.embedding.embed(input.query);
|
|
364
|
-
const vectorResults = await this.vectorIndex.search(
|
|
365
|
-
queryVec,
|
|
366
|
-
input.limit ?? 10
|
|
367
|
-
);
|
|
384
|
+
const vectorResults = await this.vectorIndex.search(queryVec, input.limit ?? 10);
|
|
368
385
|
|
|
369
386
|
const results: SearchResult[] = [];
|
|
370
387
|
for (const vr of vectorResults) {
|
|
@@ -514,63 +531,81 @@ export class MemoryService {
|
|
|
514
531
|
* memory_update — unified write API (append/upsert)
|
|
515
532
|
*/
|
|
516
533
|
async memoryUpdate(input: MemoryUpdateInput): Promise<MemoryUpdateOutput> {
|
|
517
|
-
const
|
|
518
|
-
|
|
534
|
+
const release = await lockfile.lock(this.storagePath, {
|
|
535
|
+
retries: { retries: 100, minTimeout: 50, maxTimeout: LOCK_TIMEOUT / 100 },
|
|
536
|
+
});
|
|
537
|
+
try {
|
|
538
|
+
const now = new Date().toISOString();
|
|
539
|
+
const sessionId = input.sessionId ?? randomUUID();
|
|
519
540
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
541
|
+
if (input.id) {
|
|
542
|
+
// Inline update logic to avoid nested lock
|
|
543
|
+
let existing: Memory;
|
|
544
|
+
try {
|
|
545
|
+
existing = await this.storage.read(input.id);
|
|
546
|
+
} catch (error) {
|
|
547
|
+
if (error instanceof StorageError && error.message.includes('not found')) {
|
|
548
|
+
throw new ServiceError(`Memory not found: ${input.id}`, ErrorCode.NOT_FOUND);
|
|
549
|
+
}
|
|
550
|
+
throw new ServiceError(
|
|
551
|
+
`Failed to read memory for update: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
552
|
+
ErrorCode.STORAGE_ERROR
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const updatedMemory: Memory = {
|
|
557
|
+
...existing,
|
|
558
|
+
updatedAt: now,
|
|
559
|
+
sessionId,
|
|
560
|
+
entryType: input.entryType,
|
|
561
|
+
...(input.title !== undefined && { title: input.title }),
|
|
562
|
+
...(input.content !== undefined && { content: input.content }),
|
|
563
|
+
...(input.tags !== undefined && { tags: input.tags }),
|
|
564
|
+
...(input.category !== undefined && { category: input.category }),
|
|
565
|
+
...(input.importance !== undefined && { importance: input.importance }),
|
|
566
|
+
};
|
|
567
|
+
|
|
568
|
+
const filePath = await this.storage.write(updatedMemory);
|
|
569
|
+
this.scheduleVectorUpsert(updatedMemory);
|
|
570
|
+
|
|
571
|
+
return {
|
|
572
|
+
id: updatedMemory.id,
|
|
573
|
+
sessionId,
|
|
574
|
+
filePath,
|
|
575
|
+
created: false,
|
|
576
|
+
updated: true,
|
|
577
|
+
memory: updatedMemory,
|
|
578
|
+
};
|
|
579
|
+
}
|
|
529
580
|
|
|
530
|
-
const
|
|
531
|
-
|
|
581
|
+
const id = randomUUID();
|
|
582
|
+
const createdMemory: Memory = {
|
|
583
|
+
id,
|
|
584
|
+
createdAt: now,
|
|
585
|
+
updatedAt: now,
|
|
532
586
|
sessionId,
|
|
533
587
|
entryType: input.entryType,
|
|
588
|
+
tags: input.tags ?? [],
|
|
589
|
+
category: input.category ?? 'general',
|
|
590
|
+
importance: input.importance ?? 3,
|
|
591
|
+
title: input.title ?? 'memory note',
|
|
592
|
+
content: input.content,
|
|
534
593
|
};
|
|
535
594
|
|
|
536
|
-
const filePath = await this.storage.write(
|
|
537
|
-
this.scheduleVectorUpsert(
|
|
595
|
+
const filePath = await this.storage.write(createdMemory);
|
|
596
|
+
this.scheduleVectorUpsert(createdMemory);
|
|
538
597
|
|
|
539
598
|
return {
|
|
540
|
-
id
|
|
599
|
+
id,
|
|
541
600
|
sessionId,
|
|
542
601
|
filePath,
|
|
543
|
-
created:
|
|
544
|
-
updated:
|
|
545
|
-
memory:
|
|
602
|
+
created: true,
|
|
603
|
+
updated: false,
|
|
604
|
+
memory: createdMemory,
|
|
546
605
|
};
|
|
606
|
+
} finally {
|
|
607
|
+
await release();
|
|
547
608
|
}
|
|
548
|
-
|
|
549
|
-
const id = randomUUID();
|
|
550
|
-
const createdMemory: Memory = {
|
|
551
|
-
id,
|
|
552
|
-
createdAt: now,
|
|
553
|
-
updatedAt: now,
|
|
554
|
-
sessionId,
|
|
555
|
-
entryType: input.entryType,
|
|
556
|
-
tags: input.tags ?? [],
|
|
557
|
-
category: input.category ?? 'general',
|
|
558
|
-
importance: input.importance ?? 3,
|
|
559
|
-
title: input.title ?? 'memory note',
|
|
560
|
-
content: input.content,
|
|
561
|
-
};
|
|
562
|
-
|
|
563
|
-
const filePath = await this.storage.write(createdMemory);
|
|
564
|
-
this.scheduleVectorUpsert(createdMemory);
|
|
565
|
-
|
|
566
|
-
return {
|
|
567
|
-
id,
|
|
568
|
-
sessionId,
|
|
569
|
-
filePath,
|
|
570
|
-
created: true,
|
|
571
|
-
updated: false,
|
|
572
|
-
memory: createdMemory,
|
|
573
|
-
};
|
|
574
609
|
}
|
|
575
610
|
|
|
576
611
|
// ---------------------------------------------------------------------------
|
|
@@ -2,15 +2,7 @@
|
|
|
2
2
|
* Markdown Storage - Handles file system operations for memory storage
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
readFile,
|
|
7
|
-
writeFile,
|
|
8
|
-
unlink,
|
|
9
|
-
readdir,
|
|
10
|
-
stat,
|
|
11
|
-
access,
|
|
12
|
-
mkdir,
|
|
13
|
-
} from 'fs/promises';
|
|
5
|
+
import { readFile, writeFile, unlink, readdir, stat, access, mkdir } from 'fs/promises';
|
|
14
6
|
import { join, extname } from 'path';
|
|
15
7
|
import { constants } from 'fs';
|
|
16
8
|
import type { Memory, MemoryFile } from '../contracts/types.js';
|