@synth-coder/memhub 0.1.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/.eslintrc.cjs +46 -0
- package/.github/workflows/ci.yml +74 -0
- package/.iflow/commands/opsx-apply.md +152 -0
- package/.iflow/commands/opsx-archive.md +157 -0
- package/.iflow/commands/opsx-explore.md +173 -0
- package/.iflow/commands/opsx-propose.md +106 -0
- package/.iflow/skills/openspec-apply-change/SKILL.md +156 -0
- package/.iflow/skills/openspec-archive-change/SKILL.md +114 -0
- package/.iflow/skills/openspec-explore/SKILL.md +288 -0
- package/.iflow/skills/openspec-propose/SKILL.md +110 -0
- package/.prettierrc +11 -0
- package/README.md +171 -0
- package/README.zh-CN.md +169 -0
- package/dist/src/contracts/index.d.ts +7 -0
- package/dist/src/contracts/index.d.ts.map +1 -0
- package/dist/src/contracts/index.js +10 -0
- package/dist/src/contracts/index.js.map +1 -0
- package/dist/src/contracts/mcp.d.ts +194 -0
- package/dist/src/contracts/mcp.d.ts.map +1 -0
- package/dist/src/contracts/mcp.js +112 -0
- package/dist/src/contracts/mcp.js.map +1 -0
- package/dist/src/contracts/schemas.d.ts +1153 -0
- package/dist/src/contracts/schemas.d.ts.map +1 -0
- package/dist/src/contracts/schemas.js +246 -0
- package/dist/src/contracts/schemas.js.map +1 -0
- package/dist/src/contracts/types.d.ts +328 -0
- package/dist/src/contracts/types.d.ts.map +1 -0
- package/dist/src/contracts/types.js +30 -0
- package/dist/src/contracts/types.js.map +1 -0
- package/dist/src/index.d.ts +8 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +8 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/server/index.d.ts +5 -0
- package/dist/src/server/index.d.ts.map +1 -0
- package/dist/src/server/index.js +5 -0
- package/dist/src/server/index.js.map +1 -0
- package/dist/src/server/mcp-server.d.ts +80 -0
- package/dist/src/server/mcp-server.d.ts.map +1 -0
- package/dist/src/server/mcp-server.js +263 -0
- package/dist/src/server/mcp-server.js.map +1 -0
- package/dist/src/services/index.d.ts +5 -0
- package/dist/src/services/index.d.ts.map +1 -0
- package/dist/src/services/index.js +5 -0
- package/dist/src/services/index.js.map +1 -0
- package/dist/src/services/memory-service.d.ts +105 -0
- package/dist/src/services/memory-service.d.ts.map +1 -0
- package/dist/src/services/memory-service.js +447 -0
- package/dist/src/services/memory-service.js.map +1 -0
- package/dist/src/storage/frontmatter-parser.d.ts +69 -0
- package/dist/src/storage/frontmatter-parser.d.ts.map +1 -0
- package/dist/src/storage/frontmatter-parser.js +207 -0
- package/dist/src/storage/frontmatter-parser.js.map +1 -0
- package/dist/src/storage/index.d.ts +6 -0
- package/dist/src/storage/index.d.ts.map +1 -0
- package/dist/src/storage/index.js +6 -0
- package/dist/src/storage/index.js.map +1 -0
- package/dist/src/storage/markdown-storage.d.ts +76 -0
- package/dist/src/storage/markdown-storage.d.ts.map +1 -0
- package/dist/src/storage/markdown-storage.js +193 -0
- package/dist/src/storage/markdown-storage.js.map +1 -0
- package/dist/src/utils/index.d.ts +5 -0
- package/dist/src/utils/index.d.ts.map +1 -0
- package/dist/src/utils/index.js +5 -0
- package/dist/src/utils/index.js.map +1 -0
- package/dist/src/utils/slugify.d.ts +24 -0
- package/dist/src/utils/slugify.d.ts.map +1 -0
- package/dist/src/utils/slugify.js +56 -0
- package/dist/src/utils/slugify.js.map +1 -0
- package/docs/architecture.md +349 -0
- package/docs/contracts.md +119 -0
- package/docs/prompt-template.md +79 -0
- package/docs/proposal-close-gates.md +58 -0
- package/docs/tool-calling-policy.md +107 -0
- package/package.json +53 -0
- package/src/contracts/index.ts +12 -0
- package/src/contracts/mcp.ts +303 -0
- package/src/contracts/schemas.ts +311 -0
- package/src/contracts/types.ts +414 -0
- package/src/index.ts +8 -0
- package/src/server/index.ts +5 -0
- package/src/server/mcp-server.ts +352 -0
- package/src/services/index.ts +5 -0
- package/src/services/memory-service.ts +548 -0
- package/src/storage/frontmatter-parser.ts +243 -0
- package/src/storage/index.ts +6 -0
- package/src/storage/markdown-storage.ts +236 -0
- package/src/utils/index.ts +5 -0
- package/src/utils/slugify.ts +63 -0
- package/test/contracts/schemas.test.ts +313 -0
- package/test/contracts/types.test.ts +21 -0
- package/test/frontmatter-parser-more.test.ts +94 -0
- package/test/server/mcp-server-internals.test.ts +257 -0
- package/test/server/mcp-server.test.ts +97 -0
- package/test/services/memory-service-edge.test.ts +248 -0
- package/test/services/memory-service.test.ts +279 -0
- package/test/storage/frontmatter-parser.test.ts +223 -0
- package/test/storage/markdown-storage.test.ts +217 -0
- package/test/storage/storage-edge.test.ts +238 -0
- package/test/utils/slugify-edge.test.ts +94 -0
- package/test/utils/slugify.test.ts +68 -0
- package/tsconfig.json +26 -0
- package/tsconfig.test.json +8 -0
- package/vitest.config.ts +27 -0
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory Service - Business logic for memory operations
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { randomUUID } from 'crypto';
|
|
6
|
+
import type {
|
|
7
|
+
Memory,
|
|
8
|
+
CreateMemoryInput,
|
|
9
|
+
ReadMemoryInput,
|
|
10
|
+
UpdateMemoryInput,
|
|
11
|
+
DeleteMemoryInput,
|
|
12
|
+
ListMemoryInput,
|
|
13
|
+
SearchMemoryInput,
|
|
14
|
+
CreateResult,
|
|
15
|
+
UpdateResult,
|
|
16
|
+
DeleteResult,
|
|
17
|
+
ListResult,
|
|
18
|
+
SearchResult,
|
|
19
|
+
GetCategoriesOutput,
|
|
20
|
+
GetTagsOutput,
|
|
21
|
+
SortField,
|
|
22
|
+
SortOrder,
|
|
23
|
+
MemoryLoadInput,
|
|
24
|
+
MemoryUpdateInput,
|
|
25
|
+
MemoryLoadOutput,
|
|
26
|
+
MemoryUpdateOutput,
|
|
27
|
+
} from '../contracts/types.js';
|
|
28
|
+
import { ErrorCode } from '../contracts/types.js';
|
|
29
|
+
import { MarkdownStorage, StorageError } from '../storage/markdown-storage.js';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Custom error for service operations
|
|
33
|
+
*/
|
|
34
|
+
export class ServiceError extends Error {
|
|
35
|
+
constructor(
|
|
36
|
+
message: string,
|
|
37
|
+
public readonly code: ErrorCode,
|
|
38
|
+
public readonly data?: Record<string, unknown>
|
|
39
|
+
) {
|
|
40
|
+
super(message);
|
|
41
|
+
this.name = 'ServiceError';
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Memory service configuration
|
|
47
|
+
*/
|
|
48
|
+
export interface MemoryServiceConfig {
|
|
49
|
+
storagePath: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Memory service implementation
|
|
54
|
+
*/
|
|
55
|
+
export class MemoryService {
|
|
56
|
+
private readonly storage: MarkdownStorage;
|
|
57
|
+
|
|
58
|
+
constructor(config: MemoryServiceConfig) {
|
|
59
|
+
this.storage = new MarkdownStorage({ storagePath: config.storagePath });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Creates a new memory entry
|
|
64
|
+
*
|
|
65
|
+
* @param input - Create memory input
|
|
66
|
+
* @returns Create result with ID, file path, and memory object
|
|
67
|
+
* @throws ServiceError if creation fails
|
|
68
|
+
*/
|
|
69
|
+
async create(input: CreateMemoryInput): Promise<CreateResult> {
|
|
70
|
+
const now = new Date().toISOString();
|
|
71
|
+
const id = randomUUID();
|
|
72
|
+
|
|
73
|
+
const memory: Memory = {
|
|
74
|
+
id,
|
|
75
|
+
createdAt: now,
|
|
76
|
+
updatedAt: now,
|
|
77
|
+
tags: input.tags ?? [],
|
|
78
|
+
category: input.category ?? 'general',
|
|
79
|
+
importance: input.importance ?? 3,
|
|
80
|
+
title: input.title,
|
|
81
|
+
content: input.content,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const filePath = await this.storage.write(memory);
|
|
86
|
+
return {
|
|
87
|
+
id,
|
|
88
|
+
filePath,
|
|
89
|
+
memory,
|
|
90
|
+
};
|
|
91
|
+
} catch (error) {
|
|
92
|
+
throw new ServiceError(
|
|
93
|
+
`Failed to create memory: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
94
|
+
ErrorCode.STORAGE_ERROR
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Reads a memory by ID
|
|
101
|
+
*
|
|
102
|
+
* @param input - Read memory input
|
|
103
|
+
* @returns Memory object
|
|
104
|
+
* @throws ServiceError if memory not found
|
|
105
|
+
*/
|
|
106
|
+
async read(input: ReadMemoryInput): Promise<{ memory: Memory }> {
|
|
107
|
+
try {
|
|
108
|
+
const memory = await this.storage.read(input.id);
|
|
109
|
+
return { memory };
|
|
110
|
+
} catch (error) {
|
|
111
|
+
if (error instanceof StorageError && error.message.includes('not found')) {
|
|
112
|
+
throw new ServiceError(`Memory not found: ${input.id}`, ErrorCode.NOT_FOUND);
|
|
113
|
+
}
|
|
114
|
+
throw new ServiceError(
|
|
115
|
+
`Failed to read memory: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
116
|
+
ErrorCode.STORAGE_ERROR
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Updates an existing memory
|
|
123
|
+
*
|
|
124
|
+
* @param input - Update memory input
|
|
125
|
+
* @returns Updated memory object
|
|
126
|
+
* @throws ServiceError if memory not found
|
|
127
|
+
*/
|
|
128
|
+
async update(input: UpdateMemoryInput): Promise<UpdateResult> {
|
|
129
|
+
// First read the existing memory
|
|
130
|
+
let existing: Memory;
|
|
131
|
+
try {
|
|
132
|
+
existing = await this.storage.read(input.id);
|
|
133
|
+
} catch (error) {
|
|
134
|
+
if (error instanceof StorageError && error.message.includes('not found')) {
|
|
135
|
+
throw new ServiceError(`Memory not found: ${input.id}`, ErrorCode.NOT_FOUND);
|
|
136
|
+
}
|
|
137
|
+
throw new ServiceError(
|
|
138
|
+
`Failed to read memory for update: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
139
|
+
ErrorCode.STORAGE_ERROR
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Apply updates
|
|
144
|
+
const updated: Memory = {
|
|
145
|
+
...existing,
|
|
146
|
+
updatedAt: new Date().toISOString(),
|
|
147
|
+
...(input.title !== undefined && { title: input.title }),
|
|
148
|
+
...(input.content !== undefined && { content: input.content }),
|
|
149
|
+
...(input.tags !== undefined && { tags: input.tags }),
|
|
150
|
+
...(input.category !== undefined && { category: input.category }),
|
|
151
|
+
...(input.importance !== undefined && { importance: input.importance }),
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
await this.storage.write(updated);
|
|
156
|
+
return { memory: updated };
|
|
157
|
+
} catch (error) {
|
|
158
|
+
throw new ServiceError(
|
|
159
|
+
`Failed to update memory: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
160
|
+
ErrorCode.STORAGE_ERROR
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Deletes a memory by ID
|
|
167
|
+
*
|
|
168
|
+
* @param input - Delete memory input
|
|
169
|
+
* @returns Delete result
|
|
170
|
+
* @throws ServiceError if memory not found
|
|
171
|
+
*/
|
|
172
|
+
async delete(input: DeleteMemoryInput): Promise<DeleteResult> {
|
|
173
|
+
try {
|
|
174
|
+
const filePath = await this.storage.delete(input.id);
|
|
175
|
+
return {
|
|
176
|
+
success: true,
|
|
177
|
+
filePath,
|
|
178
|
+
};
|
|
179
|
+
} catch (error) {
|
|
180
|
+
if (error instanceof StorageError && error.message.includes('not found')) {
|
|
181
|
+
throw new ServiceError(`Memory not found: ${input.id}`, ErrorCode.NOT_FOUND);
|
|
182
|
+
}
|
|
183
|
+
throw new ServiceError(
|
|
184
|
+
`Failed to delete memory: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
185
|
+
ErrorCode.STORAGE_ERROR
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Lists memories with filtering and pagination
|
|
192
|
+
*
|
|
193
|
+
* @param input - List memory input
|
|
194
|
+
* @returns List result with memories, total count, and hasMore flag
|
|
195
|
+
*/
|
|
196
|
+
async list(input: ListMemoryInput): Promise<ListResult> {
|
|
197
|
+
try {
|
|
198
|
+
const files = await this.storage.list();
|
|
199
|
+
|
|
200
|
+
// Parse all files into memories
|
|
201
|
+
let memories: Memory[] = [];
|
|
202
|
+
for (const file of files) {
|
|
203
|
+
try {
|
|
204
|
+
const memory = await this.storage.read(
|
|
205
|
+
this.extractIdFromContent(file.content)
|
|
206
|
+
);
|
|
207
|
+
memories.push(memory);
|
|
208
|
+
} catch {
|
|
209
|
+
// Skip invalid files
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Apply filters
|
|
215
|
+
if (input.category) {
|
|
216
|
+
memories = memories.filter(m => m.category === input.category);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (input.tags && input.tags.length > 0) {
|
|
220
|
+
memories = memories.filter(m =>
|
|
221
|
+
input.tags!.every(tag => m.tags.includes(tag))
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (input.fromDate) {
|
|
226
|
+
memories = memories.filter(m => m.createdAt >= input.fromDate!);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (input.toDate) {
|
|
230
|
+
memories = memories.filter(m => m.createdAt <= input.toDate!);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Sort
|
|
234
|
+
const sortBy: SortField = input.sortBy ?? 'createdAt';
|
|
235
|
+
const sortOrder: SortOrder = input.sortOrder ?? 'desc';
|
|
236
|
+
|
|
237
|
+
memories.sort((a, b) => {
|
|
238
|
+
let comparison = 0;
|
|
239
|
+
switch (sortBy) {
|
|
240
|
+
case 'createdAt':
|
|
241
|
+
comparison = a.createdAt.localeCompare(b.createdAt);
|
|
242
|
+
break;
|
|
243
|
+
case 'updatedAt':
|
|
244
|
+
comparison = a.updatedAt.localeCompare(b.updatedAt);
|
|
245
|
+
break;
|
|
246
|
+
case 'title':
|
|
247
|
+
comparison = a.title.localeCompare(b.title);
|
|
248
|
+
break;
|
|
249
|
+
case 'importance':
|
|
250
|
+
comparison = a.importance - b.importance;
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
return sortOrder === 'asc' ? comparison : -comparison;
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// Apply pagination
|
|
257
|
+
const total = memories.length;
|
|
258
|
+
const limit = input.limit ?? 20;
|
|
259
|
+
const offset = input.offset ?? 0;
|
|
260
|
+
|
|
261
|
+
const paginatedMemories = memories.slice(offset, offset + limit);
|
|
262
|
+
const hasMore = offset + limit < total;
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
memories: paginatedMemories,
|
|
266
|
+
total,
|
|
267
|
+
hasMore,
|
|
268
|
+
};
|
|
269
|
+
} catch (error) {
|
|
270
|
+
throw new ServiceError(
|
|
271
|
+
`Failed to list memories: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
272
|
+
ErrorCode.STORAGE_ERROR
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Searches memories by query
|
|
279
|
+
*
|
|
280
|
+
* @param input - Search memory input
|
|
281
|
+
* @returns Search results with scores and matches
|
|
282
|
+
*/
|
|
283
|
+
async search(input: SearchMemoryInput): Promise<{ results: SearchResult[]; total: number }> {
|
|
284
|
+
try {
|
|
285
|
+
const listResult = await this.list({
|
|
286
|
+
category: input.category,
|
|
287
|
+
tags: input.tags,
|
|
288
|
+
limit: 1000, // Get all for search
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
const query = input.query.toLowerCase();
|
|
292
|
+
const keywords = query.split(/\s+/).filter(k => k.length > 0);
|
|
293
|
+
|
|
294
|
+
const results: SearchResult[] = [];
|
|
295
|
+
|
|
296
|
+
for (const memory of listResult.memories) {
|
|
297
|
+
let score = 0;
|
|
298
|
+
const matches: string[] = [];
|
|
299
|
+
|
|
300
|
+
// Search in title (higher weight)
|
|
301
|
+
const titleLower = memory.title.toLowerCase();
|
|
302
|
+
if (titleLower.includes(query)) {
|
|
303
|
+
score += 10;
|
|
304
|
+
matches.push(memory.title);
|
|
305
|
+
} else {
|
|
306
|
+
// Check individual keywords in title
|
|
307
|
+
for (const keyword of keywords) {
|
|
308
|
+
if (titleLower.includes(keyword)) {
|
|
309
|
+
score += 5;
|
|
310
|
+
if (!matches.includes(memory.title)) {
|
|
311
|
+
matches.push(memory.title);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Search in content
|
|
318
|
+
const contentLower = memory.content.toLowerCase();
|
|
319
|
+
if (contentLower.includes(query)) {
|
|
320
|
+
score += 3;
|
|
321
|
+
// Extract matching snippet
|
|
322
|
+
const index = contentLower.indexOf(query);
|
|
323
|
+
const start = Math.max(0, index - 50);
|
|
324
|
+
const end = Math.min(contentLower.length, index + query.length + 50);
|
|
325
|
+
const snippet = memory.content.slice(start, end);
|
|
326
|
+
matches.push(snippet);
|
|
327
|
+
} else {
|
|
328
|
+
// Check individual keywords in content
|
|
329
|
+
for (const keyword of keywords) {
|
|
330
|
+
if (contentLower.includes(keyword)) {
|
|
331
|
+
score += 1;
|
|
332
|
+
const index = contentLower.indexOf(keyword);
|
|
333
|
+
const start = Math.max(0, index - 30);
|
|
334
|
+
const end = Math.min(contentLower.length, index + keyword.length + 30);
|
|
335
|
+
const snippet = memory.content.slice(start, end);
|
|
336
|
+
if (!matches.some(m => m.includes(snippet))) {
|
|
337
|
+
matches.push(snippet);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Search in tags
|
|
344
|
+
for (const tag of memory.tags) {
|
|
345
|
+
if (tag.toLowerCase().includes(query) || keywords.some(k => tag.toLowerCase().includes(k))) {
|
|
346
|
+
score += 2;
|
|
347
|
+
matches.push(`Tag: ${tag}`);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (score > 0) {
|
|
352
|
+
results.push({
|
|
353
|
+
memory,
|
|
354
|
+
score: Math.min(score / 20, 1), // Normalize to 0-1
|
|
355
|
+
matches: matches.slice(0, 3), // Limit matches
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Sort by score descending
|
|
361
|
+
results.sort((a, b) => b.score - a.score);
|
|
362
|
+
|
|
363
|
+
// Apply limit
|
|
364
|
+
const limit = input.limit ?? 10;
|
|
365
|
+
const limitedResults = results.slice(0, limit);
|
|
366
|
+
|
|
367
|
+
return {
|
|
368
|
+
results: limitedResults,
|
|
369
|
+
total: results.length,
|
|
370
|
+
};
|
|
371
|
+
} catch (error) {
|
|
372
|
+
throw new ServiceError(
|
|
373
|
+
`Failed to search memories: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
374
|
+
ErrorCode.STORAGE_ERROR
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* memory_load unified read API (STM-first)
|
|
381
|
+
*/
|
|
382
|
+
async memoryLoad(input: MemoryLoadInput): Promise<MemoryLoadOutput> {
|
|
383
|
+
if (input.id) {
|
|
384
|
+
const { memory } = await this.read({ id: input.id });
|
|
385
|
+
return { items: [memory], total: 1 };
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (input.query) {
|
|
389
|
+
const searched = await this.search({
|
|
390
|
+
query: input.query,
|
|
391
|
+
category: input.category,
|
|
392
|
+
tags: input.tags,
|
|
393
|
+
limit: input.limit,
|
|
394
|
+
});
|
|
395
|
+
let items = searched.results.map(r => r.memory);
|
|
396
|
+
if (input.sessionId) {
|
|
397
|
+
items = items.filter(m => m.sessionId === input.sessionId);
|
|
398
|
+
}
|
|
399
|
+
return { items, total: items.length };
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const listResult = await this.list({
|
|
403
|
+
category: input.category,
|
|
404
|
+
tags: input.tags,
|
|
405
|
+
limit: input.limit ?? 20,
|
|
406
|
+
sortBy: 'updatedAt',
|
|
407
|
+
sortOrder: 'desc',
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
let items = [...listResult.memories];
|
|
411
|
+
|
|
412
|
+
if (input.sessionId) {
|
|
413
|
+
items = items.filter(m => m.sessionId === input.sessionId);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (input.date) {
|
|
417
|
+
const date = input.date;
|
|
418
|
+
items = items.filter(m => m.createdAt.startsWith(date));
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return { items, total: items.length };
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* memory_update unified write API (append/upsert)
|
|
426
|
+
*/
|
|
427
|
+
async memoryUpdate(input: MemoryUpdateInput): Promise<MemoryUpdateOutput> {
|
|
428
|
+
const now = new Date().toISOString();
|
|
429
|
+
const sessionId = input.sessionId ?? randomUUID();
|
|
430
|
+
|
|
431
|
+
if (input.id) {
|
|
432
|
+
const updatedResult = await this.update({
|
|
433
|
+
id: input.id,
|
|
434
|
+
title: input.title,
|
|
435
|
+
content: input.content,
|
|
436
|
+
tags: input.tags,
|
|
437
|
+
category: input.category,
|
|
438
|
+
importance: input.importance,
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
const updatedMemory: Memory = {
|
|
442
|
+
...updatedResult.memory,
|
|
443
|
+
sessionId,
|
|
444
|
+
entryType: input.entryType,
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
const filePath = await this.storage.write(updatedMemory);
|
|
448
|
+
return {
|
|
449
|
+
id: updatedMemory.id,
|
|
450
|
+
sessionId,
|
|
451
|
+
filePath,
|
|
452
|
+
created: false,
|
|
453
|
+
updated: true,
|
|
454
|
+
memory: updatedMemory,
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const id = randomUUID();
|
|
459
|
+
const createdMemory: Memory = {
|
|
460
|
+
id,
|
|
461
|
+
createdAt: now,
|
|
462
|
+
updatedAt: now,
|
|
463
|
+
sessionId,
|
|
464
|
+
entryType: input.entryType,
|
|
465
|
+
tags: input.tags ?? [],
|
|
466
|
+
category: input.category ?? 'general',
|
|
467
|
+
importance: input.importance ?? 3,
|
|
468
|
+
title: input.title ?? 'memory note',
|
|
469
|
+
content: input.content,
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
const filePath = await this.storage.write(createdMemory);
|
|
473
|
+
return {
|
|
474
|
+
id,
|
|
475
|
+
sessionId,
|
|
476
|
+
filePath,
|
|
477
|
+
created: true,
|
|
478
|
+
updated: false,
|
|
479
|
+
memory: createdMemory,
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Gets all unique categories
|
|
485
|
+
*
|
|
486
|
+
* @returns Array of category names
|
|
487
|
+
*/
|
|
488
|
+
async getCategories(): Promise<GetCategoriesOutput> {
|
|
489
|
+
try {
|
|
490
|
+
const listResult = await this.list({ limit: 1000 });
|
|
491
|
+
const categories = new Set<string>();
|
|
492
|
+
|
|
493
|
+
for (const memory of listResult.memories) {
|
|
494
|
+
categories.add(memory.category);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
return {
|
|
498
|
+
categories: Array.from(categories).sort(),
|
|
499
|
+
};
|
|
500
|
+
} catch (error) {
|
|
501
|
+
throw new ServiceError(
|
|
502
|
+
`Failed to get categories: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
503
|
+
ErrorCode.STORAGE_ERROR
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Gets all unique tags
|
|
510
|
+
*
|
|
511
|
+
* @returns Array of tag names
|
|
512
|
+
*/
|
|
513
|
+
async getTags(): Promise<GetTagsOutput> {
|
|
514
|
+
try {
|
|
515
|
+
const listResult = await this.list({ limit: 1000 });
|
|
516
|
+
const tags = new Set<string>();
|
|
517
|
+
|
|
518
|
+
for (const memory of listResult.memories) {
|
|
519
|
+
for (const tag of memory.tags) {
|
|
520
|
+
tags.add(tag);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
return {
|
|
525
|
+
tags: Array.from(tags).sort(),
|
|
526
|
+
};
|
|
527
|
+
} catch (error) {
|
|
528
|
+
throw new ServiceError(
|
|
529
|
+
`Failed to get tags: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
530
|
+
ErrorCode.STORAGE_ERROR
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Extracts ID from file content
|
|
537
|
+
*
|
|
538
|
+
* @param content - File content
|
|
539
|
+
* @returns ID string
|
|
540
|
+
*/
|
|
541
|
+
private extractIdFromContent(content: string): string {
|
|
542
|
+
const match = content.match(/id:\s*"?([^"\n]+)"?/);
|
|
543
|
+
if (!match) {
|
|
544
|
+
throw new Error('Could not extract ID from content');
|
|
545
|
+
}
|
|
546
|
+
return match[1].trim();
|
|
547
|
+
}
|
|
548
|
+
}
|