@yun-zero/claw-memory 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.
Files changed (131) hide show
  1. package/.claude/settings.local.json +68 -0
  2. package/README.md +323 -0
  3. package/dist/config/llm.d.ts +13 -0
  4. package/dist/config/llm.d.ts.map +1 -0
  5. package/dist/config/llm.js +96 -0
  6. package/dist/config/llm.js.map +1 -0
  7. package/dist/config/plugin.d.ts +15 -0
  8. package/dist/config/plugin.d.ts.map +1 -0
  9. package/dist/config/plugin.js +32 -0
  10. package/dist/config/plugin.js.map +1 -0
  11. package/dist/db/entityRepository.d.ts +21 -0
  12. package/dist/db/entityRepository.d.ts.map +1 -0
  13. package/dist/db/entityRepository.js +55 -0
  14. package/dist/db/entityRepository.js.map +1 -0
  15. package/dist/db/repository.d.ts +22 -0
  16. package/dist/db/repository.d.ts.map +1 -0
  17. package/dist/db/repository.js +77 -0
  18. package/dist/db/repository.js.map +1 -0
  19. package/dist/db/schema.d.ts +5 -0
  20. package/dist/db/schema.d.ts.map +1 -0
  21. package/dist/db/schema.js +112 -0
  22. package/dist/db/schema.js.map +1 -0
  23. package/dist/db/todoRepository.d.ts +26 -0
  24. package/dist/db/todoRepository.d.ts.map +1 -0
  25. package/dist/db/todoRepository.js +54 -0
  26. package/dist/db/todoRepository.js.map +1 -0
  27. package/dist/hooks/bootstrap.d.ts +3 -0
  28. package/dist/hooks/bootstrap.d.ts.map +1 -0
  29. package/dist/hooks/bootstrap.js +28 -0
  30. package/dist/hooks/bootstrap.js.map +1 -0
  31. package/dist/hooks/message.d.ts +18 -0
  32. package/dist/hooks/message.d.ts.map +1 -0
  33. package/dist/hooks/message.js +52 -0
  34. package/dist/hooks/message.js.map +1 -0
  35. package/dist/index.d.ts +3 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +46 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/mcp/tools.d.ts +26 -0
  40. package/dist/mcp/tools.d.ts.map +1 -0
  41. package/dist/mcp/tools.js +360 -0
  42. package/dist/mcp/tools.js.map +1 -0
  43. package/dist/plugin.d.ts +18 -0
  44. package/dist/plugin.d.ts.map +1 -0
  45. package/dist/plugin.js +62 -0
  46. package/dist/plugin.js.map +1 -0
  47. package/dist/services/entityGraphService.d.ts +87 -0
  48. package/dist/services/entityGraphService.d.ts.map +1 -0
  49. package/dist/services/entityGraphService.js +271 -0
  50. package/dist/services/entityGraphService.js.map +1 -0
  51. package/dist/services/memory.d.ts +26 -0
  52. package/dist/services/memory.d.ts.map +1 -0
  53. package/dist/services/memory.js +281 -0
  54. package/dist/services/memory.js.map +1 -0
  55. package/dist/services/memoryIndex.d.ts +34 -0
  56. package/dist/services/memoryIndex.d.ts.map +1 -0
  57. package/dist/services/memoryIndex.js +100 -0
  58. package/dist/services/memoryIndex.js.map +1 -0
  59. package/dist/services/metadataExtractor.d.ts +16 -0
  60. package/dist/services/metadataExtractor.d.ts.map +1 -0
  61. package/dist/services/metadataExtractor.js +75 -0
  62. package/dist/services/metadataExtractor.js.map +1 -0
  63. package/dist/services/retrieval.d.ts +24 -0
  64. package/dist/services/retrieval.d.ts.map +1 -0
  65. package/dist/services/retrieval.js +40 -0
  66. package/dist/services/retrieval.js.map +1 -0
  67. package/dist/services/scheduler.d.ts +122 -0
  68. package/dist/services/scheduler.d.ts.map +1 -0
  69. package/dist/services/scheduler.js +434 -0
  70. package/dist/services/scheduler.js.map +1 -0
  71. package/dist/services/summarizer.d.ts +43 -0
  72. package/dist/services/summarizer.d.ts.map +1 -0
  73. package/dist/services/summarizer.js +252 -0
  74. package/dist/services/summarizer.js.map +1 -0
  75. package/dist/services/tagService.d.ts +64 -0
  76. package/dist/services/tagService.d.ts.map +1 -0
  77. package/dist/services/tagService.js +281 -0
  78. package/dist/services/tagService.js.map +1 -0
  79. package/dist/tools/memory.d.ts +3 -0
  80. package/dist/tools/memory.d.ts.map +1 -0
  81. package/dist/tools/memory.js +114 -0
  82. package/dist/tools/memory.js.map +1 -0
  83. package/dist/types.d.ts +128 -0
  84. package/dist/types.d.ts.map +1 -0
  85. package/dist/types.js +6 -0
  86. package/dist/types.js.map +1 -0
  87. package/docs/plans/2026-03-02-claw-memory-design.md +445 -0
  88. package/docs/plans/2026-03-02-incremental-summary-design.md +157 -0
  89. package/docs/plans/2026-03-02-incremental-summary-implementation.md +468 -0
  90. package/docs/plans/2026-03-02-memory-index-design.md +163 -0
  91. package/docs/plans/2026-03-02-memory-index-implementation.md +836 -0
  92. package/docs/plans/2026-03-02-mvp-implementation.md +1703 -0
  93. package/docs/plans/2026-03-02-testing-implementation.md +395 -0
  94. package/docs/plans/2026-03-02-testing-plan.md +93 -0
  95. package/docs/plans/2026-03-03-claw-memory-openclaw-plugin-design.md +285 -0
  96. package/docs/plans/2026-03-03-claw-memory-plugin-implementation.md +642 -0
  97. package/docs/plans/2026-03-03-entity-graph-design.md +121 -0
  98. package/docs/plans/2026-03-03-entity-graph-implementation.md +687 -0
  99. package/docs/plans/2026-03-03-llm-generic-config-design.md +43 -0
  100. package/docs/plans/2026-03-03-llm-generic-config-implementation.md +186 -0
  101. package/docs/plans/2026-03-03-memory-e2e-stress-test-design.md +110 -0
  102. package/docs/plans/2026-03-03-memory-e2e-stress-test-implementation.md +464 -0
  103. package/docs/plans/2026-03-03-minimax-llm-fix.md +156 -0
  104. package/docs/plans/2026-03-03-scheduler-design.md +165 -0
  105. package/docs/plans/2026-03-03-scheduler-implementation.md +777 -0
  106. package/docs/plans/2026-03-03-tags-visualization-design.md +73 -0
  107. package/docs/plans/2026-03-03-tags-visualization-implementation.md +539 -0
  108. package/openclaw.plugin.json +11 -0
  109. package/package.json +41 -0
  110. package/src/config/llm.ts +129 -0
  111. package/src/config/plugin.ts +47 -0
  112. package/src/db/entityRepository.ts +80 -0
  113. package/src/db/repository.ts +106 -0
  114. package/src/db/schema.ts +121 -0
  115. package/src/db/todoRepository.ts +76 -0
  116. package/src/hooks/bootstrap.ts +36 -0
  117. package/src/hooks/message.ts +84 -0
  118. package/src/index.ts +50 -0
  119. package/src/plugin.ts +85 -0
  120. package/src/services/entityGraphService.ts +367 -0
  121. package/src/services/memory.ts +338 -0
  122. package/src/services/memoryIndex.ts +140 -0
  123. package/src/services/metadataExtractor.ts +89 -0
  124. package/src/services/retrieval.ts +71 -0
  125. package/src/services/scheduler.ts +529 -0
  126. package/src/services/summarizer.ts +318 -0
  127. package/src/services/tagService.ts +335 -0
  128. package/src/tools/memory.ts +137 -0
  129. package/src/types.ts +139 -0
  130. package/tsconfig.json +20 -0
  131. package/vitest.config.ts +16 -0
@@ -0,0 +1,1703 @@
1
+ # Claw-Memory MVP 实现计划
2
+
3
+ > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
4
+
5
+ **Goal:** 实现 Claw-Memory MVP - SQLite 数据模型 + MCP 服务基础框架 + 记忆存储/检索核心功能
6
+
7
+ **Architecture:** 使用标准 MCP SDK + 垂直分层架构 (db/services/mcp 三层分离),符合 TypeScript 最佳实践,便于 TDD 测试驱动开发
8
+
9
+ **Tech Stack:** TypeScript, Node.js 18+, @modelcontextprotocol/sdk, better-sqlite3, Commander.js, Vitest
10
+
11
+ ---
12
+
13
+ ## Task 1: 项目初始化
14
+
15
+ **Files:**
16
+ - Create: `package.json`
17
+ - Create: `tsconfig.json`
18
+ - Create: `.gitignore`
19
+
20
+ **Step 1: 创建 package.json**
21
+
22
+ ```json
23
+ {
24
+ "name": "claw-memory",
25
+ "version": "0.1.0",
26
+ "description": "Lightweight AI memory system for OpenClaw and Claude Code",
27
+ "main": "dist/index.js",
28
+ "type": "module",
29
+ "bin": {
30
+ "claw-memory": "dist/index.js"
31
+ },
32
+ "scripts": {
33
+ "build": "tsc",
34
+ "start": "node dist/index.js",
35
+ "dev": "tsx src/index.ts",
36
+ "test": "vitest run",
37
+ "test:watch": "vitest"
38
+ },
39
+ "dependencies": {
40
+ "@modelcontextprotocol/sdk": "^1.0.0",
41
+ "better-sqlite3": "^11.0.0",
42
+ "commander": "^12.0.0",
43
+ "uuid": "^10.0.0"
44
+ },
45
+ "devDependencies": {
46
+ "@types/better-sqlite3": "^7.6.0",
47
+ "@types/node": "^20.0.0",
48
+ "@types/uuid": "^10.0.0",
49
+ "tsx": "^4.0.0",
50
+ "typescript": "^5.0.0",
51
+ "vitest": "^2.0.0"
52
+ }
53
+ }
54
+ ```
55
+
56
+ **Step 2: 创建 tsconfig.json**
57
+
58
+ ```json
59
+ {
60
+ "compilerOptions": {
61
+ "target": "ES2022",
62
+ "module": "ESNext",
63
+ "moduleResolution": "bundler",
64
+ "lib": ["ES2022"],
65
+ "outDir": "./dist",
66
+ "rootDir": "./src",
67
+ "strict": true,
68
+ "esModuleInterop": true,
69
+ "skipLibCheck": true,
70
+ "forceConsistentCasingInFileNames": true,
71
+ "declaration": true,
72
+ "declarationMap": true,
73
+ "sourceMap": true,
74
+ "resolveJsonModule": true
75
+ },
76
+ "include": ["src/**/*"],
77
+ "exclude": ["node_modules", "dist", "tests"]
78
+ }
79
+ ```
80
+
81
+ **Step 3: 创建 .gitignore**
82
+
83
+ ```
84
+ node_modules/
85
+ dist/
86
+ *.log
87
+ .env
88
+ memories/
89
+ *.db
90
+ .DS_Store
91
+ ```
92
+
93
+ **Step 4: 安装依赖**
94
+
95
+ Run: `npm install`
96
+ Expected: 依赖安装完成
97
+
98
+ **Step 5: 提交**
99
+
100
+ ```bash
101
+ git add package.json tsconfig.json .gitignore
102
+ git commit -m "chore: initialize project with TypeScript config"
103
+ ```
104
+
105
+ ---
106
+
107
+ ## Task 2: 类型定义
108
+
109
+ **Files:**
110
+ - Create: `src/types.ts`
111
+
112
+ **Step 1: 写入类型定义**
113
+
114
+ ```typescript
115
+ // src/types.ts
116
+
117
+ export interface Memory {
118
+ id: string;
119
+ contentPath: string;
120
+ summary: string | null;
121
+ createdAt: Date;
122
+ updatedAt: Date;
123
+ tokenCount: number;
124
+ importance: number;
125
+ accessCount: number;
126
+ lastAccessedAt: Date | null;
127
+ isArchived: boolean;
128
+ isDuplicate: boolean;
129
+ duplicateOf: string | null;
130
+ }
131
+
132
+ export interface Entity {
133
+ id: string;
134
+ name: string;
135
+ type: 'keyword' | 'tag' | 'subject' | 'person' | 'project';
136
+ parentId: string | null;
137
+ level: number;
138
+ embedding: Buffer | null;
139
+ metadata: Record<string, unknown> | null;
140
+ createdAt: Date;
141
+ }
142
+
143
+ export interface MemoryEntity {
144
+ memoryId: string;
145
+ entityId: string;
146
+ relevance: number;
147
+ source: 'auto' | 'manual';
148
+ createdAt: Date;
149
+ }
150
+
151
+ export interface EntityRelation {
152
+ id: string;
153
+ sourceId: string;
154
+ targetId: string;
155
+ relationType: 'related' | 'parent' | 'similar' | 'co_occur';
156
+ weight: number;
157
+ evidenceCount: number;
158
+ createdAt: Date;
159
+ }
160
+
161
+ export interface TimeBucket {
162
+ date: string; // YYYY-MM-DD
163
+ memoryCount: number;
164
+ summary: string | null;
165
+ summaryGeneratedAt: Date | null;
166
+ keyTopics: string[] | null;
167
+ createdAt: Date;
168
+ }
169
+
170
+ export interface SaveMemoryInput {
171
+ content: string;
172
+ metadata: {
173
+ tags?: string[];
174
+ subjects?: string[];
175
+ keywords?: string[];
176
+ importance?: number;
177
+ summary?: string;
178
+ };
179
+ userId?: string;
180
+ }
181
+
182
+ export interface SearchMemoryInput {
183
+ query: string;
184
+ timeRange?: 'today' | 'week' | 'month' | 'year' | 'all';
185
+ tags?: string[];
186
+ limit?: number;
187
+ maxTokens?: number;
188
+ }
189
+
190
+ export interface GetContextInput {
191
+ query: string;
192
+ maxTokens?: number;
193
+ }
194
+
195
+ export interface GetSummaryInput {
196
+ period: 'day' | 'week' | 'month';
197
+ date?: string;
198
+ }
199
+ ```
200
+
201
+ **Step 2: 提交**
202
+
203
+ ```bash
204
+ git add src/types.ts
205
+ git commit -m "feat: add TypeScript type definitions"
206
+ ```
207
+
208
+ ---
209
+
210
+ ## Task 3: SQLite 数据模型
211
+
212
+ **Files:**
213
+ - Create: `src/db/schema.ts`
214
+ - Create: `tests/db/schema.test.ts`
215
+
216
+ **Step 1: 创建测试**
217
+
218
+ ```typescript
219
+ // tests/db/schema.test.ts
220
+ import { describe, it, expect, beforeEach } from 'vitest';
221
+ import Database from 'better-sqlite3';
222
+ import { initializeDatabase, getDatabase } from '../../src/db/schema.js';
223
+
224
+ describe('Database Schema', () => {
225
+ let db: Database.Database;
226
+
227
+ beforeEach(() => {
228
+ db = new Database(':memory:');
229
+ initializeDatabase(db);
230
+ });
231
+
232
+ it('should create memories table', () => {
233
+ const result = db.prepare(`
234
+ SELECT name FROM sqlite_master WHERE type='table' AND name='memories'
235
+ `).get();
236
+ expect(result).toBeDefined();
237
+ });
238
+
239
+ it('should create entities table', () => {
240
+ const result = db.prepare(`
241
+ SELECT name FROM sqlite_master WHERE type='table' AND name='entities'
242
+ `).get();
243
+ expect(result).toBeDefined();
244
+ });
245
+
246
+ it('should create memory_entities table', () => {
247
+ const result = db.prepare(`
248
+ SELECT name FROM sqlite_master WHERE type='table' AND name='memory_entities'
249
+ `).get();
250
+ expect(result).toBeDefined();
251
+ });
252
+
253
+ it('should create entity_relations table', () => {
254
+ const result = db.prepare(`
255
+ SELECT name FROM sqlite_master WHERE type='table' AND name='entity_relations'
256
+ `).get();
257
+ expect(result).toBeDefined();
258
+ });
259
+
260
+ it('should create time_buckets table', () => {
261
+ const result = db.prepare(`
262
+ SELECT name FROM sqlite_master WHERE type='table' AND name='time_buckets'
263
+ `).get();
264
+ expect(result).toBeDefined();
265
+ });
266
+
267
+ it('should create required indexes', () => {
268
+ const indexes = db.prepare(`
269
+ SELECT name FROM sqlite_master WHERE type='index'
270
+ `).all();
271
+ expect(indexes.length).toBeGreaterThan(0);
272
+ });
273
+ });
274
+ ```
275
+
276
+ **Step 2: 运行测试验证失败**
277
+
278
+ Run: `npm test tests/db/schema.test.ts`
279
+ Expected: FAIL - "Cannot find module"
280
+
281
+ **Step 3: 创建 schema.ts 实现**
282
+
283
+ ```typescript
284
+ // src/db/schema.ts
285
+ import Database from 'better-sqlite3';
286
+
287
+ export function initializeDatabase(db: Database.Database): void {
288
+ // 1. 记忆表
289
+ db.exec(`
290
+ CREATE TABLE IF NOT EXISTS memories (
291
+ id TEXT PRIMARY KEY,
292
+ content_path TEXT NOT NULL,
293
+ summary TEXT,
294
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
295
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
296
+ token_count INTEGER DEFAULT 0,
297
+ importance REAL DEFAULT 0.5,
298
+ access_count INTEGER DEFAULT 0,
299
+ last_accessed_at TIMESTAMP,
300
+ is_archived BOOLEAN DEFAULT FALSE,
301
+ is_duplicate BOOLEAN DEFAULT FALSE,
302
+ duplicate_of TEXT
303
+ )
304
+ `);
305
+
306
+ // 2. 实体表
307
+ db.exec(`
308
+ CREATE TABLE IF NOT EXISTS entities (
309
+ id TEXT PRIMARY KEY,
310
+ name TEXT NOT NULL,
311
+ type TEXT NOT NULL,
312
+ parent_id TEXT,
313
+ level INTEGER DEFAULT 0,
314
+ embedding BLOB,
315
+ metadata JSON,
316
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
317
+ FOREIGN KEY (parent_id) REFERENCES entities(id)
318
+ )
319
+ `);
320
+
321
+ // 3. 记忆-实体关联表
322
+ db.exec(`
323
+ CREATE TABLE IF NOT EXISTS memory_entities (
324
+ memory_id TEXT NOT NULL,
325
+ entity_id TEXT NOT NULL,
326
+ relevance REAL DEFAULT 1.0,
327
+ source TEXT DEFAULT 'auto',
328
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
329
+ PRIMARY KEY (memory_id, entity_id),
330
+ FOREIGN KEY (memory_id) REFERENCES memories(id),
331
+ FOREIGN KEY (entity_id) REFERENCES entities(id)
332
+ )
333
+ `);
334
+
335
+ // 4. 实体关系图
336
+ db.exec(`
337
+ CREATE TABLE IF NOT EXISTS entity_relations (
338
+ id TEXT PRIMARY KEY,
339
+ source_id TEXT NOT NULL,
340
+ target_id TEXT NOT NULL,
341
+ relation_type TEXT NOT NULL,
342
+ weight REAL DEFAULT 1.0,
343
+ evidence_count INTEGER DEFAULT 1,
344
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
345
+ FOREIGN KEY (source_id) REFERENCES entities(id),
346
+ FOREIGN KEY (target_id) REFERENCES entities(id),
347
+ UNIQUE(source_id, target_id, relation_type)
348
+ )
349
+ `);
350
+
351
+ // 5. 时间桶
352
+ db.exec(`
353
+ CREATE TABLE IF NOT EXISTS time_buckets (
354
+ date TEXT PRIMARY KEY,
355
+ memory_count INTEGER DEFAULT 0,
356
+ summary TEXT,
357
+ summary_generated_at TIMESTAMP,
358
+ key_topics JSON,
359
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
360
+ )
361
+ `);
362
+
363
+ // 创建索引
364
+ db.exec(`
365
+ CREATE INDEX IF NOT EXISTS idx_memories_created ON memories(created_at);
366
+ CREATE INDEX IF NOT EXISTS idx_memories_importance ON memories(importance);
367
+ CREATE INDEX IF NOT EXISTS idx_entities_name ON entities(name);
368
+ CREATE INDEX IF NOT EXISTS idx_entities_type ON entities(type);
369
+ CREATE INDEX IF NOT EXISTS idx_entities_parent ON entities(parent_id);
370
+ CREATE INDEX IF NOT EXISTS idx_memory_entities_entity ON memory_entities(entity_id);
371
+ CREATE INDEX IF NOT EXISTS idx_entity_relations_source ON entity_relations(source_id);
372
+ CREATE INDEX IF NOT EXISTS idx_entity_relations_target ON entity_relations(target_id);
373
+ `);
374
+ }
375
+
376
+ let dbInstance: Database.Database | null = null;
377
+
378
+ export function getDatabase(dbPath: string = './memories/memory.db'): Database.Database {
379
+ if (!dbInstance) {
380
+ dbInstance = new Database(dbPath);
381
+ initializeDatabase(dbInstance);
382
+ }
383
+ return dbInstance;
384
+ }
385
+ ```
386
+
387
+ **Step 4: 运行测试验证通过**
388
+
389
+ Run: `npm test tests/db/schema.test.ts`
390
+ Expected: PASS
391
+
392
+ **Step 5: 提交**
393
+
394
+ ```bash
395
+ git add src/db/schema.ts tests/db/schema.test.ts
396
+ git commit -m "feat: add SQLite schema and initialization"
397
+ ```
398
+
399
+ ---
400
+
401
+ ## Task 4: Repository 数据访问层
402
+
403
+ **Files:**
404
+ - Create: `src/db/repository.ts`
405
+ - Create: `tests/db/repository.test.ts`
406
+
407
+ **Step 1: 创建测试**
408
+
409
+ ```typescript
410
+ // tests/db/repository.test.ts
411
+ import { describe, it, expect, beforeEach } from 'vitest';
412
+ import Database from 'better-sqlite3';
413
+ import { initializeDatabase } from '../../src/db/schema.js';
414
+ import { MemoryRepository } from '../../src/db/repository.js';
415
+
416
+ describe('MemoryRepository', () => {
417
+ let db: Database.Database;
418
+ let repo: MemoryRepository;
419
+
420
+ beforeEach(() => {
421
+ db = new Database(':memory:');
422
+ initializeDatabase(db);
423
+ repo = new MemoryRepository(db);
424
+ });
425
+
426
+ describe('create', () => {
427
+ it('should create a memory', () => {
428
+ const memory = repo.create({
429
+ contentPath: '/test/memory.md',
430
+ summary: 'Test summary',
431
+ importance: 0.8,
432
+ tokenCount: 100
433
+ });
434
+
435
+ expect(memory.id).toBeDefined();
436
+ expect(memory.summary).toBe('Test summary');
437
+ expect(memory.importance).toBe(0.8);
438
+ });
439
+ });
440
+
441
+ describe('findById', () => {
442
+ it('should find memory by id', () => {
443
+ const created = repo.create({
444
+ contentPath: '/test/memory.md',
445
+ summary: 'Test summary',
446
+ importance: 0.8
447
+ });
448
+
449
+ const found = repo.findById(created.id);
450
+ expect(found).toBeDefined();
451
+ expect(found?.summary).toBe('Test summary');
452
+ });
453
+
454
+ it('should return null for non-existent id', () => {
455
+ const found = repo.findById('non-existent');
456
+ expect(found).toBeNull();
457
+ });
458
+ });
459
+
460
+ describe('findAll', () => {
461
+ it('should return all memories', () => {
462
+ repo.create({ contentPath: '/test/1.md', summary: 'Summary 1' });
463
+ repo.create({ contentPath: '/test/2.md', summary: 'Summary 2' });
464
+
465
+ const memories = repo.findAll();
466
+ expect(memories.length).toBe(2);
467
+ });
468
+ });
469
+
470
+ describe('delete', () => {
471
+ it('should delete memory by id', () => {
472
+ const created = repo.create({
473
+ contentPath: '/test/memory.md',
474
+ summary: 'Test'
475
+ });
476
+
477
+ const result = repo.delete(created.id);
478
+ expect(result).toBe(true);
479
+
480
+ const found = repo.findById(created.id);
481
+ expect(found).toBeNull();
482
+ });
483
+ });
484
+ });
485
+ ```
486
+
487
+ **Step 2: 运行测试验证失败**
488
+
489
+ Run: `npm test tests/db/repository.test.ts`
490
+ Expected: FAIL - "Cannot find module"
491
+
492
+ **Step 3: 创建 repository.ts 实现**
493
+
494
+ ```typescript
495
+ // src/db/repository.ts
496
+ import Database from 'better-sqlite3';
497
+ import { v4 as uuidv4 } from 'uuid';
498
+ import type { Memory } from '../types.js';
499
+
500
+ export interface CreateMemoryInput {
501
+ contentPath: string;
502
+ summary?: string;
503
+ importance?: number;
504
+ tokenCount?: number;
505
+ }
506
+
507
+ export class MemoryRepository {
508
+ private db: Database.Database;
509
+
510
+ constructor(db: Database.Database) {
511
+ this.db = db;
512
+ }
513
+
514
+ create(input: CreateMemoryInput): Memory {
515
+ const id = uuidv4();
516
+ const now = new Date();
517
+
518
+ this.db.prepare(`
519
+ INSERT INTO memories (id, content_path, summary, importance, token_count, created_at, updated_at)
520
+ VALUES (?, ?, ?, ?, ?, ?, ?)
521
+ `).run(
522
+ id,
523
+ input.contentPath,
524
+ input.summary || null,
525
+ input.importance ?? 0.5,
526
+ input.tokenCount ?? 0,
527
+ now.toISOString(),
528
+ now.toISOString()
529
+ );
530
+
531
+ return this.findById(id)!;
532
+ }
533
+
534
+ findById(id: string): Memory | null {
535
+ const row = this.db.prepare(`
536
+ SELECT * FROM memories WHERE id = ?
537
+ `).get(id) as any;
538
+
539
+ if (!row) return null;
540
+ return this.mapRowToMemory(row);
541
+ }
542
+
543
+ findAll(limit?: number, offset?: number): Memory[] {
544
+ let query = 'SELECT * FROM memories ORDER BY created_at DESC';
545
+ if (limit) {
546
+ query += ` LIMIT ${limit}`;
547
+ if (offset) query += ` OFFSET ${offset}`;
548
+ }
549
+
550
+ const rows = this.db.prepare(query).all() as any[];
551
+ return rows.map(row => this.mapRowToMemory(row));
552
+ }
553
+
554
+ delete(id: string): boolean {
555
+ const result = this.db.prepare('DELETE FROM memories WHERE id = ?').run(id);
556
+ return result.changes > 0;
557
+ }
558
+
559
+ updateLastAccessed(id: string): void {
560
+ this.db.prepare(`
561
+ UPDATE memories SET last_accessed_at = ?, access_count = access_count + 1 WHERE id = ?
562
+ `).run(new Date().toISOString(), id);
563
+ }
564
+
565
+ private mapRowToMemory(row: any): Memory {
566
+ return {
567
+ id: row.id,
568
+ contentPath: row.content_path,
569
+ summary: row.summary,
570
+ createdAt: new Date(row.created_at),
571
+ updatedAt: new Date(row.updated_at),
572
+ tokenCount: row.token_count,
573
+ importance: row.importance,
574
+ accessCount: row.access_count,
575
+ lastAccessedAt: row.last_accessed_at ? new Date(row.last_accessed_at) : null,
576
+ isArchived: Boolean(row.is_archived),
577
+ isDuplicate: Boolean(row.is_duplicate),
578
+ duplicateOf: row.duplicate_of
579
+ };
580
+ }
581
+ }
582
+ ```
583
+
584
+ **Step 4: 运行测试验证通过**
585
+
586
+ Run: `npm test tests/db/repository.test.ts`
587
+ Expected: PASS
588
+
589
+ **Step 5: 提交**
590
+
591
+ ```bash
592
+ git add src/db/repository.ts tests/db/repository.test.ts
593
+ git commit -m "feat: add MemoryRepository for data access"
594
+ ```
595
+
596
+ ---
597
+
598
+ ## Task 5: 实体 Repository
599
+
600
+ **Files:**
601
+ - Create: `src/db/entityRepository.ts`
602
+ - Create: `tests/db/entityRepository.test.ts`
603
+
604
+ **Step 1: 创建测试**
605
+
606
+ ```typescript
607
+ // tests/db/entityRepository.test.ts
608
+ import { describe, it, expect, beforeEach } from 'vitest';
609
+ import Database from 'better-sqlite3';
610
+ import { initializeDatabase } from '../../src/db/schema.js';
611
+ import { EntityRepository } from '../../src/db/entityRepository.js';
612
+
613
+ describe('EntityRepository', () => {
614
+ let db: Database.Database;
615
+ let repo: EntityRepository;
616
+
617
+ beforeEach(() => {
618
+ db = new Database(':memory:');
619
+ initializeDatabase(db);
620
+ repo = new EntityRepository(db);
621
+ });
622
+
623
+ describe('create', () => {
624
+ it('should create an entity', () => {
625
+ const entity = repo.create({
626
+ name: 'React',
627
+ type: 'tag',
628
+ parentId: null,
629
+ level: 0
630
+ });
631
+
632
+ expect(entity.id).toBeDefined();
633
+ expect(entity.name).toBe('React');
634
+ expect(entity.type).toBe('tag');
635
+ });
636
+ });
637
+
638
+ describe('findByName', () => {
639
+ it('should find entity by name', () => {
640
+ repo.create({ name: 'React', type: 'tag' });
641
+
642
+ const found = repo.findByName('React');
643
+ expect(found).toBeDefined();
644
+ expect(found?.name).toBe('React');
645
+ });
646
+ });
647
+
648
+ describe('findByType', () => {
649
+ it('should find entities by type', () => {
650
+ repo.create({ name: 'React', type: 'tag' });
651
+ repo.create({ name: 'Vue', type: 'tag' });
652
+ repo.create({ name: 'John', type: 'person' });
653
+
654
+ const tags = repo.findByType('tag');
655
+ expect(tags.length).toBe(2);
656
+ });
657
+ });
658
+
659
+ describe('findChildren', () => {
660
+ it('should find child entities', () => {
661
+ const parent = repo.create({ name: '前端', type: 'tag', level: 0 });
662
+ repo.create({ name: 'React', type: 'tag', parentId: parent.id, level: 1 });
663
+
664
+ const children = repo.findChildren(parent.id);
665
+ expect(children.length).toBe(1);
666
+ });
667
+ });
668
+ });
669
+ ```
670
+
671
+ **Step 2: 运行测试验证失败**
672
+
673
+ Run: `npm test tests/db/entityRepository.test.ts`
674
+ Expected: FAIL
675
+
676
+ **Step 3: 创建 entityRepository.ts 实现**
677
+
678
+ ```typescript
679
+ // src/db/entityRepository.ts
680
+ import Database from 'better-sqlite3';
681
+ import { v4 as uuidv4 } from 'uuid';
682
+ import type { Entity } from '../types.js';
683
+
684
+ export interface CreateEntityInput {
685
+ name: string;
686
+ type: Entity['type'];
687
+ parentId?: string | null;
688
+ level?: number;
689
+ metadata?: Record<string, unknown>;
690
+ }
691
+
692
+ export class EntityRepository {
693
+ private db: Database.Database;
694
+
695
+ constructor(db: Database.Database) {
696
+ this.db = db;
697
+ }
698
+
699
+ create(input: CreateEntityInput): Entity {
700
+ const id = uuidv4();
701
+ const now = new Date();
702
+
703
+ this.db.prepare(`
704
+ INSERT INTO entities (id, name, type, parent_id, level, metadata, created_at)
705
+ VALUES (?, ?, ?, ?, ?, ?, ?)
706
+ `).run(
707
+ id,
708
+ input.name,
709
+ input.type,
710
+ input.parentId || null,
711
+ input.level ?? 0,
712
+ input.metadata ? JSON.stringify(input.metadata) : null,
713
+ now.toISOString()
714
+ );
715
+
716
+ return this.findById(id)!;
717
+ }
718
+
719
+ findById(id: string): Entity | null {
720
+ const row = this.db.prepare('SELECT * FROM entities WHERE id = ?').get(id) as any;
721
+ if (!row) return null;
722
+ return this.mapRowToEntity(row);
723
+ }
724
+
725
+ findByName(name: string): Entity | null {
726
+ const row = this.db.prepare('SELECT * FROM entities WHERE name = ?').get(name) as any;
727
+ if (!row) return null;
728
+ return this.mapRowToEntity(row);
729
+ }
730
+
731
+ findByType(type: Entity['type']): Entity[] {
732
+ const rows = this.db.prepare('SELECT * FROM entities WHERE type = ?').all(type) as any[];
733
+ return rows.map(row => this.mapRowToEntity(row));
734
+ }
735
+
736
+ findChildren(parentId: string): Entity[] {
737
+ const rows = this.db.prepare('SELECT * FROM entities WHERE parent_id = ?').all(parentId) as any[];
738
+ return rows.map(row => this.mapRowToEntity(row));
739
+ }
740
+
741
+ findOrCreate(input: CreateEntityInput): Entity {
742
+ const existing = this.findByName(input.name);
743
+ if (existing) return existing;
744
+ return this.create(input);
745
+ }
746
+
747
+ private mapRowToEntity(row: any): Entity {
748
+ return {
749
+ id: row.id,
750
+ name: row.name,
751
+ type: row.type as Entity['type'],
752
+ parentId: row.parent_id,
753
+ level: row.level,
754
+ embedding: row.embedding,
755
+ metadata: row.metadata ? JSON.parse(row.metadata) : null,
756
+ createdAt: new Date(row.created_at)
757
+ };
758
+ }
759
+ }
760
+ ```
761
+
762
+ **Step 4: 运行测试验证通过**
763
+
764
+ Run: `npm test tests/db/entityRepository.test.ts`
765
+ Expected: PASS
766
+
767
+ **Step 5: 提交**
768
+
769
+ ```bash
770
+ git add src/db/entityRepository.ts tests/db/entityRepository.test.ts
771
+ git commit -m "feat: add EntityRepository for entity management"
772
+ ```
773
+
774
+ ---
775
+
776
+ ## Task 6: 检索服务
777
+
778
+ **Files:**
779
+ - Create: `src/services/retrieval.ts`
780
+ - Create: `tests/services/retrieval.test.ts`
781
+
782
+ **Step 1: 创建测试**
783
+
784
+ ```typescript
785
+ // tests/services/retrieval.test.ts
786
+ import { describe, it, expect } from 'vitest';
787
+ import { calculateWeight, TimeDecayConfig } from '../../src/services/retrieval.js';
788
+
789
+ describe('Retrieval Service', () => {
790
+ describe('calculateWeight', () => {
791
+ it('should calculate high weight for today', () => {
792
+ const today = new Date().toISOString().split('T')[0];
793
+ const config: TimeDecayConfig = {
794
+ today: 30,
795
+ week: 20,
796
+ month: 10,
797
+ year: 5,
798
+ older: 0
799
+ };
800
+
801
+ const weight = calculateWeight({
802
+ entityMatch: 10,
803
+ timeDecay: config,
804
+ memoryDate: today,
805
+ tagMatch: 10,
806
+ importance: 0.8
807
+ });
808
+
809
+ expect(weight).toBeGreaterThan(0);
810
+ });
811
+
812
+ it('should calculate lower weight for older memories', () => {
813
+ const oldDate = '2020-01-01';
814
+ const config: TimeDecayConfig = {
815
+ today: 30,
816
+ week: 20,
817
+ month: 10,
818
+ year: 5,
819
+ older: 0
820
+ };
821
+
822
+ const weight = calculateWeight({
823
+ entityMatch: 10,
824
+ timeDecay: config,
825
+ memoryDate: oldDate,
826
+ tagMatch: 10,
827
+ importance: 0.8
828
+ });
829
+
830
+ expect(weight).toBeLessThan(30);
831
+ });
832
+ });
833
+ });
834
+ ```
835
+
836
+ **Step 2: 运行测试验证失败**
837
+
838
+ Run: `npm test tests/services/retrieval.test.ts`
839
+ Expected: FAIL
840
+
841
+ **Step 3: 创建 retrieval.ts 实现**
842
+
843
+ ```typescript
844
+ // src/services/retrieval.ts
845
+ import type { Memory, Entity } from '../types.js';
846
+
847
+ export interface TimeDecayConfig {
848
+ today: number;
849
+ week: number;
850
+ month: number;
851
+ year: number;
852
+ older: number;
853
+ }
854
+
855
+ export interface WeightInput {
856
+ entityMatch: number;
857
+ timeDecay: TimeDecayConfig;
858
+ memoryDate: string;
859
+ tagMatch: number;
860
+ importance: number;
861
+ }
862
+
863
+ export const DEFAULT_TIME_DECAY: TimeDecayConfig = {
864
+ today: 30,
865
+ week: 20,
866
+ month: 10,
867
+ year: 5,
868
+ older: 0
869
+ };
870
+
871
+ export function calculateWeight(input: WeightInput): number {
872
+ const { entityMatch, timeDecay, memoryDate, tagMatch, importance } = input;
873
+
874
+ // 实体匹配权重 (0-40)
875
+ const entityWeight = Math.min(entityMatch * 10, 40);
876
+
877
+ // 时间衰减权重 (0-30)
878
+ const timeWeight = getTimeWeight(memoryDate, timeDecay);
879
+
880
+ // 标签层级权重 (0-20)
881
+ const tagWeight = Math.min(tagMatch * 2, 20);
882
+
883
+ // 重要性权重 (0-10)
884
+ const importanceWeight = importance * 10;
885
+
886
+ // 总分 = 实体匹配 × 0.4 + 时间衰减 × 0.3 + 标签层级 × 0.2 + 重要性 × 0.1
887
+ // 归一化到 0-100
888
+ const total =
889
+ entityWeight * 0.4 +
890
+ timeWeight * 0.3 +
891
+ tagWeight * 0.2 +
892
+ importanceWeight * 0.1;
893
+
894
+ return Math.round(total * 10) / 10;
895
+ }
896
+
897
+ function getTimeWeight(memoryDate: string, config: TimeDecayConfig): number {
898
+ const today = new Date();
899
+ const memory = new Date(memoryDate);
900
+ const diffDays = Math.floor((today.getTime() - memory.getTime()) / (1000 * 60 * 60 * 24));
901
+
902
+ if (diffDays === 0) return config.today;
903
+ if (diffDays <= 7) return config.week;
904
+ if (diffDays <= 30) return config.month;
905
+ if (diffDays <= 365) return config.year;
906
+ return config.older;
907
+ }
908
+
909
+ export interface SearchOptions {
910
+ query: string;
911
+ timeRange?: 'today' | 'week' | 'month' | 'year' | 'all';
912
+ tags?: string[];
913
+ limit?: number;
914
+ maxTokens?: number;
915
+ }
916
+ ```
917
+
918
+ **Step 4: 运行测试验证通过**
919
+
920
+ Run: `npm test tests/services/retrieval.test.ts`
921
+ Expected: PASS
922
+
923
+ **Step 5: 提交**
924
+
925
+ ```bash
926
+ git add src/services/retrieval.ts tests/services/retrieval.test.ts
927
+ git commit -m "feat: add retrieval service with weight calculation"
928
+ ```
929
+
930
+ ---
931
+
932
+ ## Task 7: 记忆服务
933
+
934
+ **Files:**
935
+ - Create: `src/services/memory.ts`
936
+ - Create: `tests/services/memory.test.ts`
937
+
938
+ **Step 1: 创建测试**
939
+
940
+ ```typescript
941
+ // tests/services/memory.test.ts
942
+ import { describe, it, expect, beforeEach } from 'vitest';
943
+ import Database from 'better-sqlite3';
944
+ import { initializeDatabase } from '../../src/db/schema.js';
945
+ import { MemoryService } from '../../src/services/memory.js';
946
+
947
+ describe('MemoryService', () => {
948
+ let db: Database.Database;
949
+ let service: MemoryService;
950
+
951
+ beforeEach(() => {
952
+ db = new Database(':memory:');
953
+ initializeDatabase(db);
954
+ service = new MemoryService(db);
955
+ });
956
+
957
+ describe('saveMemory', () => {
958
+ it('should save memory with metadata', async () => {
959
+ const input = {
960
+ content: 'Test content about React hooks',
961
+ metadata: {
962
+ tags: ['技术/前端/React'],
963
+ subjects: ['React Hooks'],
964
+ keywords: ['useState', 'useEffect'],
965
+ importance: 0.8,
966
+ summary: '讨论 React Hooks'
967
+ }
968
+ };
969
+
970
+ const result = await service.saveMemory(input);
971
+ expect(result.id).toBeDefined();
972
+ expect(result.summary).toBe('讨论 React Hooks');
973
+ });
974
+ });
975
+
976
+ describe('searchMemory', () => {
977
+ it('should search memories', async () => {
978
+ // 先保存一条记忆
979
+ await service.saveMemory({
980
+ content: 'Test content',
981
+ metadata: { summary: 'Test' }
982
+ });
983
+
984
+ const results = await service.searchMemory({
985
+ query: 'Test',
986
+ limit: 10
987
+ });
988
+
989
+ expect(results.length).toBeGreaterThan(0);
990
+ });
991
+ });
992
+
993
+ describe('getContext', () => {
994
+ it('should get context within token limit', async () => {
995
+ await service.saveMemory({
996
+ content: 'Content 1',
997
+ metadata: { summary: 'Summary 1', importance: 0.9 }
998
+ });
999
+
1000
+ const context = await service.getContext({
1001
+ query: 'test',
1002
+ maxTokens: 1000
1003
+ });
1004
+
1005
+ expect(context).toBeDefined();
1006
+ });
1007
+ });
1008
+ });
1009
+ ```
1010
+
1011
+ **Step 2: 运行测试验证失败**
1012
+
1013
+ Run: `npm test tests/services/memory.test.ts`
1014
+ Expected: FAIL
1015
+
1016
+ **Step 3: 创建 memory.ts 实现**
1017
+
1018
+ ```typescript
1019
+ // src/services/memory.ts
1020
+ import Database from 'better-sqlite3';
1021
+ import { v4 as uuidv4 } from 'uuid';
1022
+ import { readFile, writeFile, mkdir } from 'fs/promises';
1023
+ import { dirname, join } from 'path';
1024
+ import { MemoryRepository, CreateMemoryInput } from '../db/repository.js';
1025
+ import { EntityRepository, CreateEntityInput } from '../db/entityRepository.js';
1026
+ import { calculateWeight, DEFAULT_TIME_DECAY, type SearchOptions } from './retrieval.js';
1027
+ import type { Memory, SaveMemoryInput, GetContextInput, TimeBucket } from '../types.js';
1028
+
1029
+ export class MemoryService {
1030
+ private db: Database.Database;
1031
+ private memoryRepo: MemoryRepository;
1032
+ private entityRepo: EntityRepository;
1033
+ private dataDir: string;
1034
+
1035
+ constructor(db: Database.Database, dataDir: string = './memories') {
1036
+ this.db = db;
1037
+ this.memoryRepo = new MemoryRepository(db);
1038
+ this.entityRepo = new EntityRepository(db);
1039
+ this.dataDir = dataDir;
1040
+ }
1041
+
1042
+ async saveMemory(input: SaveMemoryInput): Promise<Memory> {
1043
+ const { content, metadata } = input;
1044
+
1045
+ // 生成文件路径
1046
+ const date = new Date();
1047
+ const datePath = `${date.getFullYear()}/${String(date.getMonth() + 1).padStart(2, '0')}/${String(date.getDate()).padStart(2, '0')}`;
1048
+ const fileName = `${uuidv4()}.md`;
1049
+ const contentPath = join(this.dataDir, datePath, fileName);
1050
+
1051
+ // 保存内容到文件
1052
+ await this.saveContentToFile(contentPath, content);
1053
+
1054
+ // 创建记忆记录
1055
+ const memoryInput: CreateMemoryInput = {
1056
+ contentPath,
1057
+ summary: metadata.summary || null,
1058
+ importance: metadata.importance ?? 0.5,
1059
+ tokenCount: this.estimateTokens(content)
1060
+ };
1061
+
1062
+ const memory = this.memoryRepo.create(memoryInput);
1063
+
1064
+ // 处理实体关联
1065
+ await this.processEntities(memory.id, metadata);
1066
+
1067
+ return memory;
1068
+ }
1069
+
1070
+ async searchMemory(options: SearchOptions): Promise<Memory[]> {
1071
+ const { query, timeRange, tags, limit = 10 } = options;
1072
+
1073
+ // 构建时间过滤条件
1074
+ const dateFilter = this.buildDateFilter(timeRange);
1075
+
1076
+ // 获取所有候选记忆
1077
+ let memories = this.memoryRepo.findAll(100);
1078
+
1079
+ // 应用时间过滤
1080
+ if (dateFilter) {
1081
+ memories = memories.filter(m => {
1082
+ const created = m.createdAt.toISOString().split('T')[0];
1083
+ return created >= dateFilter.start && created <= dateFilter.end;
1084
+ });
1085
+ }
1086
+
1087
+ // 计算权重并排序
1088
+ const weightedMemories = memories.map(memory => ({
1089
+ memory,
1090
+ weight: calculateWeight({
1091
+ entityMatch: 0, // TODO: 匹配查询实体
1092
+ timeDecay: DEFAULT_TIME_DECAY,
1093
+ memoryDate: memory.createdAt.toISOString().split('T')[0],
1094
+ tagMatch: 0, // TODO: 匹配标签
1095
+ importance: memory.importance
1096
+ })
1097
+ }));
1098
+
1099
+ // 按权重排序
1100
+ weightedMemories.sort((a, b) => b.weight - a.weight);
1101
+
1102
+ // 返回结果
1103
+ return weightedMemories.slice(0, limit).map(w => w.memory);
1104
+ }
1105
+
1106
+ async getContext(input: GetContextInput): Promise<string> {
1107
+ const { query, maxTokens = 8000 } = input;
1108
+
1109
+ const memories = await this.searchMemory({
1110
+ query,
1111
+ limit: 20,
1112
+ maxTokens
1113
+ });
1114
+
1115
+ // 累积内容直到达到 token 限制
1116
+ let totalTokens = 0;
1117
+ const contextParts: string[] = [];
1118
+
1119
+ for (const memory of memories) {
1120
+ const content = await this.readContentFromFile(memory.contentPath);
1121
+ const tokens = this.estimateTokens(content);
1122
+
1123
+ if (totalTokens + tokens > maxTokens) {
1124
+ break;
1125
+ }
1126
+
1127
+ contextParts.push(content);
1128
+ totalTokens += tokens;
1129
+
1130
+ // 更新访问计数
1131
+ this.memoryRepo.updateLastAccessed(memory.id);
1132
+ }
1133
+
1134
+ return contextParts.join('\n\n---\n\n');
1135
+ }
1136
+
1137
+ async getSummary(period: 'day' | 'week' | 'month', date?: string): Promise<TimeBucket | null> {
1138
+ const targetDate = date || new Date().toISOString().split('T')[0];
1139
+
1140
+ // TODO: 实现实际的时间桶查询
1141
+ return {
1142
+ date: targetDate,
1143
+ memoryCount: 0,
1144
+ summary: null,
1145
+ summaryGeneratedAt: null,
1146
+ keyTopics: null,
1147
+ createdAt: new Date()
1148
+ };
1149
+ }
1150
+
1151
+ private async saveContentToFile(path: string, content: string): Promise<void> {
1152
+ const dir = dirname(path);
1153
+ await mkdir(dir, { recursive: true });
1154
+ await writeFile(path, content, 'utf-8');
1155
+ }
1156
+
1157
+ private async readContentFromFile(path: string): Promise<string> {
1158
+ try {
1159
+ return await readFile(path, 'utf-8');
1160
+ } catch {
1161
+ return '';
1162
+ }
1163
+ }
1164
+
1165
+ private async processEntities(memoryId: string, metadata: SaveMemoryInput['metadata']): Promise<void> {
1166
+ // 处理标签
1167
+ if (metadata.tags) {
1168
+ for (const tag of metadata.tags) {
1169
+ const entity = this.entityRepo.findOrCreate({
1170
+ name: tag,
1171
+ type: 'tag',
1172
+ level: tag.split('/').length - 1
1173
+ });
1174
+
1175
+ this.db.prepare(`
1176
+ INSERT OR IGNORE INTO memory_entities (memory_id, entity_id, relevance)
1177
+ VALUES (?, ?, ?)
1178
+ `).run(memoryId, entity.id, 1.0);
1179
+ }
1180
+ }
1181
+
1182
+ // 处理主题
1183
+ if (metadata.subjects) {
1184
+ for (const subject of metadata.subjects) {
1185
+ const entity = this.entityRepo.findOrCreate({
1186
+ name: subject,
1187
+ type: 'subject'
1188
+ });
1189
+
1190
+ this.db.prepare(`
1191
+ INSERT OR IGNORE INTO memory_entities (memory_id, entity_id, relevance)
1192
+ VALUES (?, ?, ?)
1193
+ `).run(memoryId, entity.id, 0.8);
1194
+ }
1195
+ }
1196
+
1197
+ // 处理关键词
1198
+ if (metadata.keywords) {
1199
+ for (const keyword of metadata.keywords) {
1200
+ const entity = this.entityRepo.findOrCreate({
1201
+ name: keyword,
1202
+ type: 'keyword'
1203
+ });
1204
+
1205
+ this.db.prepare(`
1206
+ INSERT OR IGNORE INTO memory_entities (memory_id, entity_id, relevance)
1207
+ VALUES (?, ?, ?)
1208
+ `).run(memoryId, entity.id, 0.6);
1209
+ }
1210
+ }
1211
+ }
1212
+
1213
+ private buildDateFilter(timeRange?: SearchOptions['timeRange']): { start: string; end: string } | null {
1214
+ if (!timeRange || timeRange === 'all') return null;
1215
+
1216
+ const now = new Date();
1217
+ const end = now.toISOString().split('T')[0];
1218
+ let start: string;
1219
+
1220
+ switch (timeRange) {
1221
+ case 'today':
1222
+ start = end;
1223
+ break;
1224
+ case 'week':
1225
+ now.setDate(now.getDate() - 7);
1226
+ start = now.toISOString().split('T')[0];
1227
+ break;
1228
+ case 'month':
1229
+ now.setMonth(now.getMonth() - 1);
1230
+ start = now.toISOString().split('T')[0];
1231
+ break;
1232
+ case 'year':
1233
+ now.setFullYear(now.getFullYear() - 1);
1234
+ start = now.toISOString().split('T')[0];
1235
+ break;
1236
+ default:
1237
+ return null;
1238
+ }
1239
+
1240
+ return { start, end };
1241
+ }
1242
+
1243
+ private estimateTokens(text: string): number {
1244
+ // 简单估算:中文约 1.5 字符/token,英文约 4 字符/token
1245
+ const chineseChars = (text.match(/[\u4e00-\u9fa5]/g) || []).length;
1246
+ const otherChars = text.length - chineseChars;
1247
+ return Math.ceil(chineseChars / 1.5 + otherChars / 4);
1248
+ }
1249
+ }
1250
+ ```
1251
+
1252
+ **Step 4: 运行测试验证通过**
1253
+
1254
+ Run: `npm test tests/services/memory.test.ts`
1255
+ Expected: PASS
1256
+
1257
+ **Step 5: 提交**
1258
+
1259
+ ```bash
1260
+ git add src/services/memory.ts tests/services/memory.test.ts
1261
+ git commit -m "feat: add MemoryService for storage and retrieval"
1262
+ ```
1263
+
1264
+ ---
1265
+
1266
+ ## Task 8: MCP 工具定义
1267
+
1268
+ **Files:**
1269
+ - Create: `src/mcp/tools.ts`
1270
+ - Create: `tests/mcp/tools.test.ts`
1271
+
1272
+ **Step 1: 创建测试**
1273
+
1274
+ ```typescript
1275
+ // tests/mcp/tools.test.ts
1276
+ import { describe, it, expect } from 'vitest';
1277
+ import { z } from 'zod';
1278
+ import { createSaveMemoryTool, createSearchMemoryTool, createGetContextTool } from '../../src/mcp/tools.js';
1279
+
1280
+ describe('MCP Tools', () => {
1281
+ describe('tool definitions', () => {
1282
+ it('should create save_memory tool', () => {
1283
+ const tool = createSaveMemoryTool({} as any);
1284
+ expect(tool.name).toBe('save_memory');
1285
+ expect(tool.inputSchema).toBeDefined();
1286
+ });
1287
+
1288
+ it('should create search_memory tool', () => {
1289
+ const tool = createSearchMemoryTool({} as any);
1290
+ expect(tool.name).toBe('search_memory');
1291
+ expect(tool.inputSchema).toBeDefined();
1292
+ });
1293
+
1294
+ it('should create get_context tool', () => {
1295
+ const tool = createGetContextTool({} as any);
1296
+ expect(tool.name).toBe('get_context');
1297
+ expect(tool.inputSchema).toBeDefined();
1298
+ });
1299
+ });
1300
+ });
1301
+ ```
1302
+
1303
+ **Step 2: 运行测试验证失败**
1304
+
1305
+ Run: `npm test tests/mcp/tools.test.ts`
1306
+ Expected: FAIL
1307
+
1308
+ **Step 3: 创建 tools.ts 实现**
1309
+
1310
+ ```typescript
1311
+ // src/mcp/tools.ts
1312
+ import type { MemoryService } from '../services/memory.js';
1313
+
1314
+ export function createSaveMemoryTool(memoryService: MemoryService) {
1315
+ return {
1316
+ name: 'save_memory',
1317
+ description: 'Save a conversation memory with structured metadata',
1318
+ inputSchema: {
1319
+ type: 'object' as const,
1320
+ properties: {
1321
+ content: {
1322
+ type: 'string',
1323
+ description: 'The conversation content to save'
1324
+ },
1325
+ metadata: {
1326
+ type: 'object',
1327
+ properties: {
1328
+ tags: {
1329
+ type: 'array',
1330
+ items: { type: 'string' },
1331
+ description: 'Hierarchical tags like 技术/前端/React'
1332
+ },
1333
+ subjects: {
1334
+ type: 'array',
1335
+ items: { type: 'string' },
1336
+ description: 'Main topics discussed'
1337
+ },
1338
+ keywords: {
1339
+ type: 'array',
1340
+ items: { type: 'string' },
1341
+ description: 'Key technical terms'
1342
+ },
1343
+ importance: {
1344
+ type: 'number',
1345
+ minimum: 0,
1346
+ maximum: 1,
1347
+ description: 'Importance level (0-1)'
1348
+ },
1349
+ summary: {
1350
+ type: 'string',
1351
+ description: 'Brief summary of the content'
1352
+ }
1353
+ }
1354
+ },
1355
+ userId: {
1356
+ type: 'string',
1357
+ description: 'User identifier (optional)'
1358
+ }
1359
+ },
1360
+ required: ['content']
1361
+ },
1362
+ handler: async (params: any) => {
1363
+ const result = await memoryService.saveMemory(params);
1364
+ return {
1365
+ success: true,
1366
+ memory_id: result.id,
1367
+ summary: result.summary
1368
+ };
1369
+ }
1370
+ };
1371
+ }
1372
+
1373
+ export function createSearchMemoryTool(memoryService: MemoryService) {
1374
+ return {
1375
+ name: 'search_memory',
1376
+ description: 'Search memories by query, time range, and tags',
1377
+ inputSchema: {
1378
+ type: 'object' as const,
1379
+ properties: {
1380
+ query: {
1381
+ type: 'string',
1382
+ description: 'Search query'
1383
+ },
1384
+ timeRange: {
1385
+ type: 'string',
1386
+ enum: ['today', 'week', 'month', 'year', 'all'],
1387
+ description: 'Time range filter'
1388
+ },
1389
+ tags: {
1390
+ type: 'array',
1391
+ items: { type: 'string' },
1392
+ description: 'Filter by tags'
1393
+ },
1394
+ limit: {
1395
+ type: 'number',
1396
+ default: 10,
1397
+ description: 'Maximum number of results'
1398
+ },
1399
+ maxTokens: {
1400
+ type: 'number',
1401
+ default: 4000,
1402
+ description: 'Maximum tokens to return'
1403
+ }
1404
+ }
1405
+ },
1406
+ handler: async (params: any) => {
1407
+ const memories = await memoryService.searchMemory(params);
1408
+ return {
1409
+ memories: memories.map(m => ({
1410
+ id: m.id,
1411
+ summary: m.summary,
1412
+ importance: m.importance,
1413
+ created_at: m.createdAt.toISOString()
1414
+ }))
1415
+ };
1416
+ }
1417
+ };
1418
+ }
1419
+
1420
+ export function createGetContextTool(memoryService: MemoryService) {
1421
+ return {
1422
+ name: 'get_context',
1423
+ description: 'Get weighted context for a query within token limit',
1424
+ inputSchema: {
1425
+ type: 'object' as const,
1426
+ properties: {
1427
+ query: {
1428
+ type: 'string',
1429
+ description: 'Context query'
1430
+ },
1431
+ maxTokens: {
1432
+ type: 'number',
1433
+ default: 8000,
1434
+ description: 'Maximum tokens to return'
1435
+ }
1436
+ },
1437
+ required: ['query']
1438
+ },
1439
+ handler: async (params: any) => {
1440
+ const context = await memoryService.getContext(params);
1441
+ return { context };
1442
+ }
1443
+ };
1444
+ }
1445
+
1446
+ export function createGetSummaryTool(memoryService: MemoryService) {
1447
+ return {
1448
+ name: 'get_summary',
1449
+ description: 'Get time period summary (day/week/month)',
1450
+ inputSchema: {
1451
+ type: 'object' as const,
1452
+ properties: {
1453
+ period: {
1454
+ type: 'string',
1455
+ enum: ['day', 'week', 'month'],
1456
+ description: 'Summary period'
1457
+ },
1458
+ date: {
1459
+ type: 'string',
1460
+ description: 'Specific date (YYYY-MM-DD)'
1461
+ }
1462
+ },
1463
+ required: ['period']
1464
+ },
1465
+ handler: async (params: any) => {
1466
+ const summary = await memoryService.getSummary(params.period, params.date);
1467
+ return summary || { error: 'No summary available' };
1468
+ }
1469
+ };
1470
+ }
1471
+
1472
+ export function createListMemoriesTool(memoryService: MemoryService) {
1473
+ return {
1474
+ name: 'list_memories',
1475
+ description: 'List memories with optional filters',
1476
+ inputSchema: {
1477
+ type: 'object' as const,
1478
+ properties: {
1479
+ limit: {
1480
+ type: 'number',
1481
+ default: 20
1482
+ },
1483
+ offset: {
1484
+ type: 'number',
1485
+ default: 0
1486
+ }
1487
+ }
1488
+ },
1489
+ handler: async (params: any) => {
1490
+ // TODO: 实现 list_memories
1491
+ return { memories: [] };
1492
+ }
1493
+ };
1494
+ }
1495
+
1496
+ export function createDeleteMemoryTool(memoryService: MemoryService) {
1497
+ return {
1498
+ name: 'delete_memory',
1499
+ description: 'Delete a memory by ID',
1500
+ inputSchema: {
1501
+ type: 'object' as const,
1502
+ properties: {
1503
+ id: {
1504
+ type: 'string',
1505
+ description: 'Memory ID to delete'
1506
+ }
1507
+ },
1508
+ required: ['id']
1509
+ },
1510
+ handler: async (params: any) => {
1511
+ // TODO: 实现 delete_memory
1512
+ return { success: true };
1513
+ }
1514
+ };
1515
+ }
1516
+ ```
1517
+
1518
+ **Step 4: 运行测试验证通过**
1519
+
1520
+ Run: `npm test tests/mcp/tools.test.ts`
1521
+ Expected: PASS
1522
+
1523
+ **Step 5: 提交**
1524
+
1525
+ ```bash
1526
+ git add src/mcp/tools.ts tests/mcp/tools.test.ts
1527
+ git commit -m "feat: add MCP tool definitions"
1528
+ ```
1529
+
1530
+ ---
1531
+
1532
+ ## Task 9: CLI 入口和服务启动
1533
+
1534
+ **Files:**
1535
+ - Create: `src/index.ts`
1536
+ - Create: `tests/index.test.ts`
1537
+
1538
+ **Step 1: 创建测试**
1539
+
1540
+ ```typescript
1541
+ // tests/index.test.ts
1542
+ import { describe, it, expect } from 'vitest';
1543
+
1544
+ describe('CLI Entry', () => {
1545
+ it('should export main functions', () => {
1546
+ // 简单验证入口文件可以导入
1547
+ expect(true).toBe(true);
1548
+ });
1549
+ });
1550
+ ```
1551
+
1552
+ **Step 2: 运行测试验证通过**
1553
+
1554
+ Run: `npm test tests/index.test.ts`
1555
+ Expected: PASS
1556
+
1557
+ **Step 3: 创建 index.ts 实现**
1558
+
1559
+ ```typescript
1560
+ #!/usr/bin/env node
1561
+
1562
+ // src/index.ts
1563
+ import { Command } from 'commander';
1564
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
1565
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
1566
+ import { getDatabase } from './db/schema.js';
1567
+ import { MemoryService } from './services/memory.js';
1568
+ import {
1569
+ createSaveMemoryTool,
1570
+ createSearchMemoryTool,
1571
+ createGetContextTool,
1572
+ createGetSummaryTool,
1573
+ createListMemoriesTool,
1574
+ createDeleteMemoryTool
1575
+ } from './mcp/tools.js';
1576
+
1577
+ const program = new Command();
1578
+
1579
+ program
1580
+ .name('claw-memory')
1581
+ .description('Lightweight AI memory system for OpenClaw and Claude Code')
1582
+ .version('0.1.0');
1583
+
1584
+ program
1585
+ .command('serve')
1586
+ .description('Start MCP server')
1587
+ .option('-p, --port <port>', 'Server port', '18790')
1588
+ .option('-d, --data-dir <dir>', 'Data directory', './memories')
1589
+ .action(async (options) => {
1590
+ const db = getDatabase(`${options.dataDir}/memory.db`);
1591
+ const memoryService = new MemoryService(db, options.dataDir);
1592
+
1593
+ // 创建 MCP 服务器
1594
+ const server = new Server(
1595
+ {
1596
+ name: 'claw-memory',
1597
+ version: '0.1.0'
1598
+ },
1599
+ {
1600
+ capabilities: {
1601
+ tools: {}
1602
+ }
1603
+ }
1604
+ );
1605
+
1606
+ // 注册工具
1607
+ server.setRequestHandler('tools/list', async () => {
1608
+ return {
1609
+ tools: [
1610
+ createSaveMemoryTool(memoryService),
1611
+ createSearchMemoryTool(memoryService),
1612
+ createGetContextTool(memoryService),
1613
+ createGetSummaryTool(memoryService),
1614
+ createListMemoriesTool(memoryService),
1615
+ createDeleteMemoryTool(memoryService)
1616
+ ]
1617
+ };
1618
+ });
1619
+
1620
+ server.setRequestHandler('tools/call', async (request) => {
1621
+ const { name, arguments: args } = request.params;
1622
+
1623
+ const tools = {
1624
+ save_memory: createSaveMemoryTool(memoryService),
1625
+ search_memory: createSearchMemoryTool(memoryService),
1626
+ get_context: createGetContextTool(memoryService),
1627
+ get_summary: createGetSummaryTool(memoryService),
1628
+ list_memories: createListMemoriesTool(memoryService),
1629
+ delete_memory: createDeleteMemoryTool(memoryService)
1630
+ };
1631
+
1632
+ const tool = tools[name as keyof typeof tools];
1633
+ if (!tool) {
1634
+ throw new Error(`Unknown tool: ${name}`);
1635
+ }
1636
+
1637
+ return await tool.handler(args);
1638
+ });
1639
+
1640
+ // 启动服务器
1641
+ const transport = new StdioServerTransport();
1642
+ await server.connect(transport);
1643
+ console.error('Claw-Memory MCP Server started');
1644
+ });
1645
+
1646
+ program
1647
+ .command('init')
1648
+ .description('Initialize database')
1649
+ .option('-d, --data-dir <dir>', 'Data directory', './memories')
1650
+ .action((options) => {
1651
+ const db = getDatabase(`${options.dataDir}/memory.db`);
1652
+ console.log('Database initialized');
1653
+ });
1654
+
1655
+ // 解析命令行参数
1656
+ program.parse();
1657
+ ```
1658
+
1659
+ **Step 4: 运行测试验证通过**
1660
+
1661
+ Run: `npm test`
1662
+ Expected: PASS
1663
+
1664
+ **Step 5: 提交**
1665
+
1666
+ ```bash
1667
+ git add src/index.ts tests/index.test.ts
1668
+ git commit -m "feat: add CLI entry and MCP server startup"
1669
+ ```
1670
+
1671
+ ---
1672
+
1673
+ ## Task 10: 构建和验证
1674
+
1675
+ **Files:**
1676
+ - Modify: `package.json` (添加 build 脚本)
1677
+
1678
+ **Step 1: 构建项目**
1679
+
1680
+ Run: `npm run build`
1681
+ Expected: 构建成功,生成 dist/ 目录
1682
+
1683
+ **Step 2: 测试 MCP 服务启动**
1684
+
1685
+ Run: `timeout 5 npm start serve -- --data-dir ./test_memories || true`
1686
+ Expected: 服务启动成功(超时自动退出)
1687
+
1688
+ **Step 3: 最终提交**
1689
+
1690
+ ```bash
1691
+ git add .
1692
+ git commit -m "feat: complete MVP - MCP server with memory storage and retrieval"
1693
+ ```
1694
+
1695
+ ---
1696
+
1697
+ ## 完成
1698
+
1699
+ MVP 实现完成!包含:
1700
+ - ✅ SQLite 数据模型
1701
+ - ✅ MCP 服务基础框架
1702
+ - ✅ 记忆存储/检索核心功能
1703
+ - ✅ TDD 测试驱动开发