@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,129 @@
1
+ /**
2
+ * LLM Configuration Module
3
+ * Supports OpenAI and Anthropic API keys via environment variables
4
+ */
5
+
6
+ export interface LLMConfig {
7
+ format: 'openai' | 'anthropic' | 'openai-compatible';
8
+ baseUrl: string;
9
+ apiKey: string;
10
+ model: string;
11
+ }
12
+
13
+ // Default configuration map for LLM providers
14
+ const LLM_DEFAULTS: Record<string, { baseUrl: string; model: string }> = {
15
+ anthropic: { baseUrl: 'https://api.anthropic.com', model: 'claude-3-haiku-20240307' },
16
+ openai: { baseUrl: 'https://api.openai.com/v1', model: 'gpt-4o-mini' },
17
+ 'openai-compatible': { baseUrl: 'https://api.openai.com/v1', model: 'gpt-4o-mini' }
18
+ };
19
+
20
+ export function getLLMConfig(): LLMConfig {
21
+ const format = (process.env.LLM_FORMAT as LLMConfig['format']) || 'openai';
22
+ const baseUrl = process.env.LLM_BASE_URL || getDefaultBaseUrl(format);
23
+ const apiKey = process.env.LLM_API_KEY || process.env.OPENAI_API_KEY || '';
24
+ const model = process.env.LLM_MODEL || getDefaultModel(format);
25
+
26
+ if (!apiKey) {
27
+ throw new Error('No LLM API key configured. Set LLM_API_KEY environment variable.');
28
+ }
29
+
30
+ return { format, baseUrl, apiKey, model };
31
+ }
32
+
33
+ function getDefaultBaseUrl(format: LLMConfig['format']): string {
34
+ return LLM_DEFAULTS[format]?.baseUrl ?? LLM_DEFAULTS.openai.baseUrl;
35
+ }
36
+
37
+ function getDefaultModel(format: LLMConfig['format']): string {
38
+ return LLM_DEFAULTS[format]?.model ?? LLM_DEFAULTS.openai.model;
39
+ }
40
+
41
+ export async function generateSummaryWithLLM(
42
+ report: string,
43
+ config?: LLMConfig
44
+ ): Promise<string> {
45
+ const llmConfig = config || getLLMConfig();
46
+
47
+ const systemPrompt = `你是一个智能助手,负责根据用户记忆数据生成周报总结。
48
+ 请根据提供的统计数据,从多个维度分析用户的记忆内容,并生成一段简洁、有价值的自然语言总结。
49
+ 总结应该:
50
+ 1. 概括本周的记忆活动概况
51
+ 2. 指出用户最关注的话题和标签
52
+ 3. 识别重要的记忆内容
53
+ 4. 给出有洞察力的观察
54
+
55
+ 请用中文输出总结,保持简洁但有信息量。`;
56
+
57
+ if (llmConfig.format === 'anthropic') {
58
+ return generateWithAnthropic(systemPrompt, report, llmConfig);
59
+ } else {
60
+ // openai 或 openai-compatible 都使用 generateWithOpenAI
61
+ const prefix = llmConfig.format === 'openai-compatible' ? 'OpenAI Compatible' : 'OpenAI';
62
+ return generateWithOpenAI(systemPrompt, report, llmConfig, prefix);
63
+ }
64
+ }
65
+
66
+ async function generateWithAnthropic(
67
+ systemPrompt: string,
68
+ userPrompt: string,
69
+ config: LLMConfig
70
+ ): Promise<string> {
71
+ // Ensure v1 path for Anthropic API
72
+ const baseUrl = config.baseUrl.includes('/anthropic') && !config.baseUrl.includes('/v1')
73
+ ? `${config.baseUrl}/v1`
74
+ : config.baseUrl;
75
+ const response = await fetch(`${baseUrl}/messages`, {
76
+ method: 'POST',
77
+ headers: {
78
+ 'Content-Type': 'application/json',
79
+ 'x-api-key': config.apiKey,
80
+ 'anthropic-version': '2023-06-01'
81
+ },
82
+ body: JSON.stringify({
83
+ model: config.model,
84
+ max_tokens: 1024,
85
+ system: systemPrompt,
86
+ messages: [{ role: 'user', content: userPrompt }]
87
+ })
88
+ });
89
+
90
+ if (!response.ok) {
91
+ const error = await response.text();
92
+ throw new Error(`Anthropic API error: ${error}`);
93
+ }
94
+
95
+ const data = await response.json() as { content: { text: string }[] };
96
+ return data.content[0]?.text || '总结生成失败';
97
+ }
98
+
99
+ async function generateWithOpenAI(
100
+ systemPrompt: string,
101
+ userPrompt: string,
102
+ config: LLMConfig,
103
+ errorPrefix: string = 'OpenAI'
104
+ ): Promise<string> {
105
+ const response = await fetch(`${config.baseUrl}/chat/completions`, {
106
+ method: 'POST',
107
+ headers: {
108
+ 'Content-Type': 'application/json',
109
+ 'Authorization': `Bearer ${config.apiKey}`
110
+ },
111
+ body: JSON.stringify({
112
+ model: config.model,
113
+ max_tokens: 1024,
114
+ messages: [
115
+ { role: 'system', content: systemPrompt },
116
+ { role: 'user', content: userPrompt }
117
+ ]
118
+ })
119
+ });
120
+
121
+ if (!response.ok) {
122
+ const error = await response.text();
123
+ throw new Error(`${errorPrefix} API error: ${error}`);
124
+ }
125
+
126
+ const data = await response.json() as { choices: { message: { content: string } }[] };
127
+ return data.choices[0]?.message?.content || '总结生成失败';
128
+ }
129
+
@@ -0,0 +1,47 @@
1
+ export interface PluginConfig {
2
+ enabled: boolean;
3
+ autoSave: boolean;
4
+ saveMode: 'qa' | 'full';
5
+ dataDir: string;
6
+ scheduler: {
7
+ enabled: boolean;
8
+ deduplicateTime: string;
9
+ dailyTime: string;
10
+ weeklyTime: string;
11
+ monthlyTime: string;
12
+ };
13
+ }
14
+
15
+ export function getConfig(config?: any): PluginConfig {
16
+ const defaultConfig: PluginConfig = {
17
+ enabled: true,
18
+ autoSave: true,
19
+ saveMode: 'qa',
20
+ dataDir: '~/.openclaw/claw-memory',
21
+ scheduler: {
22
+ enabled: true,
23
+ deduplicateTime: '01:00',
24
+ dailyTime: '02:00',
25
+ weeklyTime: '03:00',
26
+ monthlyTime: '04:00'
27
+ }
28
+ };
29
+
30
+ if (!config) {
31
+ return defaultConfig;
32
+ }
33
+
34
+ return {
35
+ enabled: config.enabled ?? defaultConfig.enabled,
36
+ autoSave: config.autoSave ?? defaultConfig.autoSave,
37
+ saveMode: config.saveMode ?? defaultConfig.saveMode,
38
+ dataDir: config.dataDir ?? defaultConfig.dataDir,
39
+ scheduler: {
40
+ enabled: config.scheduler?.enabled ?? defaultConfig.scheduler.enabled,
41
+ deduplicateTime: config.scheduler?.deduplicateTime ?? defaultConfig.scheduler.deduplicateTime,
42
+ dailyTime: config.scheduler?.dailyTime ?? defaultConfig.scheduler.dailyTime,
43
+ weeklyTime: config.scheduler?.weeklyTime ?? defaultConfig.scheduler.weeklyTime,
44
+ monthlyTime: config.scheduler?.monthlyTime ?? defaultConfig.scheduler.monthlyTime
45
+ }
46
+ };
47
+ }
@@ -0,0 +1,80 @@
1
+ import Database from 'better-sqlite3';
2
+ import { v4 as uuidv4 } from 'uuid';
3
+ import type { Entity } from '../types.js';
4
+
5
+ export interface CreateEntityInput {
6
+ name: string;
7
+ type: Entity['type'];
8
+ parentId?: string | null;
9
+ level?: number;
10
+ metadata?: Record<string, unknown>;
11
+ }
12
+
13
+ export class EntityRepository {
14
+ private db: Database.Database;
15
+
16
+ constructor(db: Database.Database) {
17
+ this.db = db;
18
+ }
19
+
20
+ create(input: CreateEntityInput): Entity {
21
+ const id = uuidv4();
22
+ const now = new Date();
23
+
24
+ this.db.prepare(`
25
+ INSERT INTO entities (id, name, type, parent_id, level, metadata, created_at)
26
+ VALUES (?, ?, ?, ?, ?, ?, ?)
27
+ `).run(
28
+ id,
29
+ input.name,
30
+ input.type,
31
+ input.parentId || null,
32
+ input.level ?? 0,
33
+ input.metadata ? JSON.stringify(input.metadata) : null,
34
+ now.toISOString()
35
+ );
36
+
37
+ return this.findById(id)!;
38
+ }
39
+
40
+ findById(id: string): Entity | null {
41
+ const row = this.db.prepare('SELECT * FROM entities WHERE id = ?').get(id) as any;
42
+ if (!row) return null;
43
+ return this.mapRowToEntity(row);
44
+ }
45
+
46
+ findByName(name: string): Entity | null {
47
+ const row = this.db.prepare('SELECT * FROM entities WHERE name = ?').get(name) as any;
48
+ if (!row) return null;
49
+ return this.mapRowToEntity(row);
50
+ }
51
+
52
+ findByType(type: Entity['type']): Entity[] {
53
+ const rows = this.db.prepare('SELECT * FROM entities WHERE type = ?').all(type) as any[];
54
+ return rows.map(row => this.mapRowToEntity(row));
55
+ }
56
+
57
+ findChildren(parentId: string): Entity[] {
58
+ const rows = this.db.prepare('SELECT * FROM entities WHERE parent_id = ?').all(parentId) as any[];
59
+ return rows.map(row => this.mapRowToEntity(row));
60
+ }
61
+
62
+ findOrCreate(input: CreateEntityInput): Entity {
63
+ const existing = this.findByName(input.name);
64
+ if (existing) return existing;
65
+ return this.create(input);
66
+ }
67
+
68
+ private mapRowToEntity(row: any): Entity {
69
+ return {
70
+ id: row.id,
71
+ name: row.name,
72
+ type: row.type as Entity['type'],
73
+ parentId: row.parent_id,
74
+ level: row.level,
75
+ embedding: row.embedding,
76
+ metadata: row.metadata ? JSON.parse(row.metadata) : null,
77
+ createdAt: new Date(row.created_at)
78
+ };
79
+ }
80
+ }
@@ -0,0 +1,106 @@
1
+ import Database from 'better-sqlite3';
2
+ import { v4 as uuidv4 } from 'uuid';
3
+ import type { Memory, IntegratedSummary } from '../types.js';
4
+
5
+ export interface CreateMemoryInput {
6
+ contentPath: string;
7
+ summary?: string;
8
+ integratedSummary?: IntegratedSummary;
9
+ importance?: number;
10
+ tokenCount?: number;
11
+ }
12
+
13
+ export class MemoryRepository {
14
+ private db: Database.Database;
15
+
16
+ constructor(db: Database.Database) {
17
+ this.db = db;
18
+ }
19
+
20
+ create(input: CreateMemoryInput): Memory {
21
+ const id = uuidv4();
22
+ const now = new Date();
23
+
24
+ this.db.prepare(`
25
+ INSERT INTO memories (id, content_path, summary, integrated_summary, importance, token_count, created_at, updated_at)
26
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
27
+ `).run(
28
+ id,
29
+ input.contentPath,
30
+ input.summary || null,
31
+ input.integratedSummary ? JSON.stringify(input.integratedSummary) : null,
32
+ input.importance ?? 0.5,
33
+ input.tokenCount ?? 0,
34
+ now.toISOString(),
35
+ now.toISOString()
36
+ );
37
+
38
+ return this.findById(id)!;
39
+ }
40
+
41
+ findById(id: string): Memory | null {
42
+ const row = this.db.prepare(`
43
+ SELECT * FROM memories WHERE id = ?
44
+ `).get(id) as any;
45
+
46
+ if (!row) return null;
47
+ return this.mapRowToMemory(row);
48
+ }
49
+
50
+ findAll(limit?: number, offset?: number): Memory[] {
51
+ let query = 'SELECT * FROM memories ORDER BY created_at DESC';
52
+ if (limit) {
53
+ query += ` LIMIT ${limit}`;
54
+ if (offset) query += ` OFFSET ${offset}`;
55
+ }
56
+
57
+ const rows = this.db.prepare(query).all() as any[];
58
+ return rows.map(row => this.mapRowToMemory(row));
59
+ }
60
+
61
+ delete(id: string): boolean {
62
+ const result = this.db.prepare('DELETE FROM memories WHERE id = ?').run(id);
63
+ return result.changes > 0;
64
+ }
65
+
66
+ updateLastAccessed(id: string): void {
67
+ this.db.prepare(`
68
+ UPDATE memories SET last_accessed_at = ?, access_count = access_count + 1 WHERE id = ?
69
+ `).run(new Date().toISOString(), id);
70
+ }
71
+
72
+ private mapRowToMemory(row: any): Memory {
73
+ return {
74
+ id: row.id,
75
+ contentPath: row.content_path,
76
+ summary: row.summary,
77
+ integratedSummary: row.integrated_summary ? this.safeParseJSON(row.integrated_summary) : null,
78
+ createdAt: new Date(row.created_at),
79
+ updatedAt: new Date(row.updated_at),
80
+ tokenCount: row.token_count,
81
+ importance: row.importance,
82
+ accessCount: row.access_count,
83
+ lastAccessedAt: row.last_accessed_at ? new Date(row.last_accessed_at) : null,
84
+ isArchived: Boolean(row.is_archived),
85
+ isDuplicate: Boolean(row.is_duplicate),
86
+ duplicateOf: row.duplicate_of
87
+ };
88
+ }
89
+
90
+ private safeParseJSON(json: string): IntegratedSummary | null {
91
+ try {
92
+ return JSON.parse(json);
93
+ } catch {
94
+ return null;
95
+ }
96
+ }
97
+
98
+ getLatestIntegratedSummary(): IntegratedSummary | null {
99
+ const row = this.db.prepare(`
100
+ SELECT integrated_summary FROM memories ORDER BY created_at DESC LIMIT 1
101
+ `).get() as { integrated_summary: string } | undefined;
102
+
103
+ if (!row || !row.integrated_summary) return null;
104
+ return this.safeParseJSON(row.integrated_summary);
105
+ }
106
+ }
@@ -0,0 +1,121 @@
1
+ import Database from 'better-sqlite3';
2
+
3
+ export function initializeDatabase(db: Database.Database): void {
4
+ // 1. 记忆表
5
+ db.exec(`
6
+ CREATE TABLE IF NOT EXISTS memories (
7
+ id TEXT PRIMARY KEY,
8
+ content_path TEXT NOT NULL,
9
+ summary TEXT,
10
+ integrated_summary JSON,
11
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
12
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
13
+ token_count INTEGER DEFAULT 0,
14
+ importance REAL DEFAULT 0.5,
15
+ access_count INTEGER DEFAULT 0,
16
+ last_accessed_at TIMESTAMP,
17
+ is_archived BOOLEAN DEFAULT FALSE,
18
+ is_duplicate BOOLEAN DEFAULT FALSE,
19
+ duplicate_of TEXT
20
+ )
21
+ `);
22
+
23
+ // 2. 实体表
24
+ db.exec(`
25
+ CREATE TABLE IF NOT EXISTS entities (
26
+ id TEXT PRIMARY KEY,
27
+ name TEXT NOT NULL,
28
+ type TEXT NOT NULL,
29
+ parent_id TEXT,
30
+ level INTEGER DEFAULT 0,
31
+ embedding BLOB,
32
+ metadata JSON,
33
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
34
+ FOREIGN KEY (parent_id) REFERENCES entities(id)
35
+ )
36
+ `);
37
+
38
+ // 3. 记忆-实体关联表
39
+ db.exec(`
40
+ CREATE TABLE IF NOT EXISTS memory_entities (
41
+ memory_id TEXT NOT NULL,
42
+ entity_id TEXT NOT NULL,
43
+ relevance REAL DEFAULT 1.0,
44
+ source TEXT DEFAULT 'auto',
45
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
46
+ PRIMARY KEY (memory_id, entity_id),
47
+ FOREIGN KEY (memory_id) REFERENCES memories(id),
48
+ FOREIGN KEY (entity_id) REFERENCES entities(id)
49
+ )
50
+ `);
51
+
52
+ // 4. 实体关系图
53
+ db.exec(`
54
+ CREATE TABLE IF NOT EXISTS entity_relations (
55
+ id TEXT PRIMARY KEY,
56
+ source_id TEXT NOT NULL,
57
+ target_id TEXT NOT NULL,
58
+ relation_type TEXT NOT NULL,
59
+ weight REAL DEFAULT 1.0,
60
+ evidence_count INTEGER DEFAULT 1,
61
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
62
+ FOREIGN KEY (source_id) REFERENCES entities(id),
63
+ FOREIGN KEY (target_id) REFERENCES entities(id),
64
+ UNIQUE(source_id, target_id, relation_type)
65
+ )
66
+ `);
67
+
68
+ // 5. 时间桶
69
+ db.exec(`
70
+ CREATE TABLE IF NOT EXISTS time_buckets (
71
+ date TEXT PRIMARY KEY,
72
+ memory_count INTEGER DEFAULT 0,
73
+ summary TEXT,
74
+ summary_generated_at TIMESTAMP,
75
+ key_topics JSON,
76
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
77
+ )
78
+ `);
79
+
80
+ // 6. 待办事项
81
+ db.exec(`
82
+ CREATE TABLE IF NOT EXISTS todos (
83
+ id TEXT PRIMARY KEY,
84
+ content TEXT NOT NULL,
85
+ period TEXT NOT NULL,
86
+ period_date TEXT,
87
+ created_at TEXT NOT NULL,
88
+ completed_at TEXT,
89
+ memory_id TEXT
90
+ )
91
+ `);
92
+
93
+ // 创建索引
94
+ db.exec(`
95
+ CREATE INDEX IF NOT EXISTS idx_memories_created ON memories(created_at);
96
+ CREATE INDEX IF NOT EXISTS idx_memories_importance ON memories(importance);
97
+ CREATE INDEX IF NOT EXISTS idx_entities_name ON entities(name);
98
+ CREATE INDEX IF NOT EXISTS idx_entities_type ON entities(type);
99
+ CREATE INDEX IF NOT EXISTS idx_entities_parent ON entities(parent_id);
100
+ CREATE INDEX IF NOT EXISTS idx_memory_entities_entity ON memory_entities(entity_id);
101
+ CREATE INDEX IF NOT EXISTS idx_entity_relations_source ON entity_relations(source_id);
102
+ CREATE INDEX IF NOT EXISTS idx_entity_relations_target ON entity_relations(target_id);
103
+ `);
104
+ }
105
+
106
+ let dbInstance: Database.Database | null = null;
107
+
108
+ export function resetDbInstance(): void {
109
+ if (dbInstance) {
110
+ dbInstance.close();
111
+ dbInstance = null;
112
+ }
113
+ }
114
+
115
+ export function getDatabase(dbPath: string = './memories/memory.db'): Database.Database {
116
+ if (!dbInstance) {
117
+ dbInstance = new Database(dbPath);
118
+ initializeDatabase(dbInstance);
119
+ }
120
+ return dbInstance;
121
+ }
@@ -0,0 +1,76 @@
1
+ import Database from 'better-sqlite3';
2
+ import { v4 as uuidv4 } from 'uuid';
3
+
4
+ export interface Todo {
5
+ id: string;
6
+ content: string;
7
+ period: 'day' | 'week' | 'month';
8
+ periodDate: string;
9
+ createdAt: Date;
10
+ completedAt: Date | null;
11
+ memoryId: string | null;
12
+ }
13
+
14
+ export interface CreateTodoInput {
15
+ content: string;
16
+ period: 'day' | 'week' | 'month';
17
+ periodDate: string;
18
+ memoryId?: string;
19
+ }
20
+
21
+ export class TodoRepository {
22
+ constructor(private db: Database.Database) {}
23
+
24
+ create(input: CreateTodoInput): Todo {
25
+ const id = uuidv4();
26
+ const createdAt = new Date().toISOString();
27
+
28
+ this.db.prepare(`
29
+ INSERT INTO todos (id, content, period, period_date, created_at, memory_id)
30
+ VALUES (?, ?, ?, ?, ?, ?)
31
+ `).run(id, input.content, input.period, input.periodDate, createdAt, input.memoryId || null);
32
+
33
+ return {
34
+ id,
35
+ content: input.content,
36
+ period: input.period,
37
+ periodDate: input.periodDate,
38
+ createdAt: new Date(createdAt),
39
+ completedAt: null,
40
+ memoryId: input.memoryId || null
41
+ };
42
+ }
43
+
44
+ findByPeriod(period: string, periodDate: string, includeCompleted = false): Todo[] {
45
+ const sql = includeCompleted
46
+ ? `SELECT * FROM todos WHERE period = ? AND period_date = ? ORDER BY created_at DESC`
47
+ : `SELECT * FROM todos WHERE period = ? AND period_date = ? AND completed_at IS NULL ORDER BY created_at DESC`;
48
+
49
+ const rows = this.db.prepare(sql).all(period, periodDate) as any[];
50
+ return rows.map(this.mapRowToTodo);
51
+ }
52
+
53
+ findById(id: string): Todo | null {
54
+ const row = this.db.prepare('SELECT * FROM todos WHERE id = ?').get(id) as any;
55
+ if (!row) return null;
56
+ return this.mapRowToTodo(row);
57
+ }
58
+
59
+ markCompleted(id: string): void {
60
+ this.db.prepare(`
61
+ UPDATE todos SET completed_at = ? WHERE id = ?
62
+ `).run(new Date().toISOString(), id);
63
+ }
64
+
65
+ private mapRowToTodo(row: any): Todo {
66
+ return {
67
+ id: row.id,
68
+ content: row.content,
69
+ period: row.period,
70
+ periodDate: row.period_date,
71
+ createdAt: new Date(row.created_at),
72
+ completedAt: row.completed_at ? new Date(row.completed_at) : null,
73
+ memoryId: row.memory_id
74
+ };
75
+ }
76
+ }
@@ -0,0 +1,36 @@
1
+ import type { Database } from 'better-sqlite3';
2
+
3
+ export async function handleAgentBootstrap(
4
+ db: Database
5
+ ): Promise<string> {
6
+ // 获取本周记忆摘要
7
+ const today = new Date();
8
+ const weekStart = new Date(today);
9
+ weekStart.setDate(today.getDate() - today.getDay());
10
+ const weekStartStr = weekStart.toISOString().split('T')[0];
11
+
12
+ const weekMemories = db.prepare(`
13
+ SELECT summary, importance
14
+ FROM memories
15
+ WHERE date(created_at) >= date(?)
16
+ ORDER BY importance DESC
17
+ LIMIT 10
18
+ `).all(weekStartStr) as { summary: string; importance: number }[];
19
+
20
+ if (weekMemories.length === 0) {
21
+ return '';
22
+ }
23
+
24
+ // 构建摘要文本
25
+ const lines = ['## 记忆摘要\n'];
26
+ lines.push(`本周共有 ${weekMemories.length} 条记忆记录。\n`);
27
+ lines.push('### 重点内容:\n');
28
+
29
+ for (const m of weekMemories.slice(0, 5)) {
30
+ if (m.summary) {
31
+ lines.push(`- ${m.summary}`);
32
+ }
33
+ }
34
+
35
+ return lines.join('\n');
36
+ }
@@ -0,0 +1,84 @@
1
+ import type { Database } from 'better-sqlite3';
2
+ import { v4 as uuidv4 } from 'uuid';
3
+ import * as path from 'path';
4
+ import * as fs from 'fs';
5
+ import { homedir } from 'os';
6
+
7
+ interface MessageEvent {
8
+ message: {
9
+ role: 'user' | 'assistant';
10
+ content: string;
11
+ };
12
+ session: {
13
+ id: string;
14
+ key: string;
15
+ };
16
+ conversation?: Array<{
17
+ role: 'user' | 'assistant';
18
+ content: string;
19
+ }>;
20
+ }
21
+
22
+ export async function handleMessageSent(
23
+ event: MessageEvent,
24
+ db: Database,
25
+ dataDir: string
26
+ ): Promise<void> {
27
+ // 只处理 assistant 消息(AI 回复后保存)
28
+ if (event.message.role !== 'assistant') {
29
+ return;
30
+ }
31
+
32
+ // 从 conversation 中获取最后一条 user 消息
33
+ let userMessage = '';
34
+ if (event.conversation && event.conversation.length > 0) {
35
+ // 找到最后一个 user 消息
36
+ for (let i = event.conversation.length - 1; i >= 0; i--) {
37
+ if (event.conversation[i].role === 'user') {
38
+ userMessage = event.conversation[i].content;
39
+ break;
40
+ }
41
+ }
42
+ }
43
+
44
+ const assistantMessage = event.message.content;
45
+
46
+ if (!userMessage || !assistantMessage) {
47
+ console.log('[ClawMemory] Skipping - no valid Q&A pair');
48
+ return;
49
+ }
50
+
51
+ // 构建 Q&A
52
+ const qaContent = `Q: ${userMessage}\n\nA: ${assistantMessage}`;
53
+
54
+ // 解析 dataDir 中的 ~
55
+ const resolvedDataDir = dataDir.replace(/^~/, homedir());
56
+
57
+ // 确保目录存在
58
+ if (!fs.existsSync(resolvedDataDir)) {
59
+ fs.mkdirSync(resolvedDataDir, { recursive: true });
60
+ }
61
+
62
+ const memoriesDir = path.join(resolvedDataDir, 'memories');
63
+ if (!fs.existsSync(memoriesDir)) {
64
+ fs.mkdirSync(memoriesDir, { recursive: true });
65
+ }
66
+
67
+ // 保存记忆
68
+ const memoryId = uuidv4();
69
+ const contentPath = path.join(memoriesDir, `${memoryId}.md`);
70
+
71
+ // 写入文件
72
+ fs.writeFileSync(contentPath, qaContent, 'utf-8');
73
+
74
+ // 提取摘要(取前200字符)
75
+ const summary = assistantMessage.substring(0, 200);
76
+
77
+ // 插入数据库
78
+ db.prepare(`
79
+ INSERT INTO memories (id, content_path, summary, created_at)
80
+ VALUES (?, ?, ?, datetime('now'))
81
+ `).run(memoryId, contentPath, summary);
82
+
83
+ console.log(`[ClawMemory] Saved memory: ${memoryId}`);
84
+ }