@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
package/src/index.ts ADDED
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import { getDatabase } from './db/schema.js';
5
+ import { TagService } from './services/tagService.js';
6
+ import { writeFile } from 'fs/promises';
7
+
8
+ const program = new Command();
9
+
10
+ program
11
+ .name('claw-memory')
12
+ .description('OpenClaw 记忆插件 - CLI 工具')
13
+ .version('0.1.0');
14
+
15
+ program
16
+ .command('init')
17
+ .description('Initialize database')
18
+ .option('-d, --data-dir <dir>', 'Data directory', './memories')
19
+ .action((options) => {
20
+ const db = getDatabase(`${options.dataDir}/memory.db`);
21
+ console.log('Database initialized');
22
+ });
23
+
24
+ program
25
+ .command('tags <action>')
26
+ .description('标签管理命令')
27
+ .option('-o, --output <file>', '输出文件路径')
28
+ .option('-d, --data-dir <dir>', 'Data directory', './memories')
29
+ .action(async (action, options) => {
30
+ const db = getDatabase(`${options.dataDir}/memory.db`);
31
+ const tagService = new TagService(db);
32
+ const outputFile = options.output || (action === 'tree' ? 'tags-tree.html' : 'tags-stats.html');
33
+
34
+ if (action === 'tree') {
35
+ const data = await tagService.getTagTree();
36
+ const html = tagService.generateTreeHtml(data);
37
+ await writeFile(outputFile, html);
38
+ console.log(`标签树已生成: ${outputFile}`);
39
+ } else if (action === 'stats') {
40
+ const stats = await tagService.getTagStats();
41
+ const html = tagService.generateStatsHtml(stats);
42
+ await writeFile(outputFile, html);
43
+ console.log(`标签统计已生成: ${outputFile}`);
44
+ } else {
45
+ console.error('未知命令: tree 或 stats');
46
+ process.exit(1);
47
+ }
48
+ });
49
+
50
+ program.parse();
package/src/plugin.ts ADDED
@@ -0,0 +1,85 @@
1
+ import { getDatabase } from './db/schema.js';
2
+ import { getConfig, PluginConfig } from './config/plugin.js';
3
+ import { handleMessageSent } from './hooks/message.js';
4
+ import { handleAgentBootstrap } from './hooks/bootstrap.js';
5
+ import { registerMemoryTools } from './tools/memory.js';
6
+ import { Scheduler } from './services/scheduler.js';
7
+
8
+ export interface OpenClawPluginContext {
9
+ hooks: {
10
+ register: (event: string, handler: any) => Promise<void>;
11
+ };
12
+ tools: {
13
+ register: (tool: any) => void;
14
+ };
15
+ config?: any;
16
+ }
17
+
18
+ export interface OpenClawPlugin {
19
+ name: string;
20
+ version: string;
21
+ register: (context: OpenClawPluginContext) => Promise<void>;
22
+ }
23
+
24
+ export function createPlugin(config?: any): OpenClawPlugin {
25
+ const pluginConfig = getConfig(config);
26
+
27
+ return {
28
+ name: 'claw-memory',
29
+ version: '0.1.0',
30
+
31
+ async register(context: OpenClawPluginContext) {
32
+ if (!pluginConfig.enabled) {
33
+ console.log('[ClawMemory] Plugin disabled');
34
+ return;
35
+ }
36
+
37
+ console.log('[ClawMemory] Starting...');
38
+
39
+ // 初始化数据库
40
+ const db = getDatabase(pluginConfig.dataDir + '/memory.db');
41
+
42
+ // 注册 message:sent hook
43
+ await context.hooks.register('message:sent', async (event: any) => {
44
+ if (pluginConfig.autoSave) {
45
+ try {
46
+ await handleMessageSent(event, db, pluginConfig.dataDir);
47
+ } catch (error) {
48
+ console.error('[ClawMemory] Failed to save memory:', error);
49
+ }
50
+ }
51
+ });
52
+
53
+ // 注册 agent:bootstrap hook
54
+ await context.hooks.register('agent:bootstrap', async (bootstrapContext: any) => {
55
+ try {
56
+ const summary = await handleAgentBootstrap(db);
57
+ if (summary) {
58
+ bootstrapContext.context = bootstrapContext.context || '';
59
+ bootstrapContext.context += '\n\n' + summary;
60
+ }
61
+ } catch (error) {
62
+ console.error('[ClawMemory] Failed to inject summary:', error);
63
+ }
64
+ });
65
+
66
+ // 注册 Agent Tools
67
+ registerMemoryTools(context.tools, db, pluginConfig.dataDir);
68
+
69
+ // 启动 Scheduler
70
+ if (pluginConfig.scheduler.enabled) {
71
+ try {
72
+ const scheduler = new Scheduler(db, pluginConfig.scheduler);
73
+ scheduler.start();
74
+ } catch (error) {
75
+ console.error('[ClawMemory] Failed to start scheduler:', error);
76
+ }
77
+ }
78
+
79
+ console.log('[ClawMemory] Started successfully');
80
+ }
81
+ };
82
+ }
83
+
84
+ // 默认导出用于 OpenClaw 加载
85
+ export default createPlugin();
@@ -0,0 +1,367 @@
1
+ import Database from 'better-sqlite3';
2
+ import type { Entity, EntityRelation } from '../types.js';
3
+
4
+ /**
5
+ * Entity node for graph visualization
6
+ */
7
+ export interface EntityNode {
8
+ id: string;
9
+ name: string;
10
+ type: 'keyword' | 'tag' | 'subject' | 'person' | 'project';
11
+ }
12
+
13
+ /**
14
+ * Entity edge for graph visualization
15
+ */
16
+ export interface EntityEdge {
17
+ source: string;
18
+ target: string;
19
+ type: 'related' | 'parent' | 'similar' | 'co_occur';
20
+ weight: number;
21
+ }
22
+
23
+ /**
24
+ * Graph data structure for visualization
25
+ */
26
+ export interface GraphData {
27
+ nodes: EntityNode[];
28
+ edges: EntityEdge[];
29
+ }
30
+
31
+ /**
32
+ * Relation statistics
33
+ */
34
+ export interface RelationStats {
35
+ most_connected: { id: string; name: string; connection_count: number }[];
36
+ relation_types: Record<string, number>;
37
+ total_relations: number;
38
+ }
39
+
40
+ /**
41
+ * Query result for entity graph
42
+ */
43
+ export interface EntityGraphQueryResult {
44
+ path?: string[];
45
+ nodes: EntityNode[];
46
+ edges: EntityEdge[];
47
+ found: boolean;
48
+ }
49
+
50
+ /**
51
+ * Service for managing entity graph data and relationships
52
+ */
53
+ export class EntityGraphService {
54
+ private db: Database.Database;
55
+
56
+ constructor(db: Database.Database) {
57
+ this.db = db;
58
+ }
59
+
60
+ /**
61
+ * Get entity by name
62
+ */
63
+ private getEntityByName(name: string): { id: string; name: string; type: string } | undefined {
64
+ const row = this.db.prepare(`
65
+ SELECT id, name, type FROM entities WHERE name = ?
66
+ `).get(name) as { id: string; name: string; type: string } | undefined;
67
+ return row;
68
+ }
69
+
70
+ /**
71
+ * Get all entities and their relations as graph data
72
+ */
73
+ getGraphData(): GraphData {
74
+ // Get all entities as nodes
75
+ const entityRows = this.db.prepare(`
76
+ SELECT id, name, type FROM entities
77
+ `).all() as { id: string; name: string; type: string }[];
78
+
79
+ const nodes: EntityNode[] = entityRows.map(row => ({
80
+ id: row.id,
81
+ name: row.name,
82
+ type: row.type as EntityNode['type']
83
+ }));
84
+
85
+ // Get all relations as edges
86
+ const relationRows = this.db.prepare(`
87
+ SELECT source_id, target_id, relation_type, weight FROM entity_relations
88
+ `).all() as { source_id: string; target_id: string; relation_type: string; weight: number }[];
89
+
90
+ const edges: EntityEdge[] = relationRows.map(row => ({
91
+ source: row.source_id,
92
+ target: row.target_id,
93
+ type: row.relation_type as EntityEdge['type'],
94
+ weight: row.weight
95
+ }));
96
+
97
+ return { nodes, edges };
98
+ }
99
+
100
+ /**
101
+ * Get relations for a specific entity by name
102
+ */
103
+ getEntityRelations(entityName: string): EntityEdge[] {
104
+ // First get the entity by name
105
+ const entity = this.getEntityByName(entityName);
106
+ if (!entity) {
107
+ return [];
108
+ }
109
+
110
+ const rows = this.db.prepare(`
111
+ SELECT source_id, target_id, relation_type, weight
112
+ FROM entity_relations
113
+ WHERE source_id = ? OR target_id = ?
114
+ `).all(entity.id, entity.id) as { source_id: string; target_id: string; relation_type: string; weight: number }[];
115
+
116
+ return rows.map(row => ({
117
+ source: row.source_id,
118
+ target: row.target_id,
119
+ type: row.relation_type as EntityEdge['type'],
120
+ weight: row.weight
121
+ }));
122
+ }
123
+
124
+ /**
125
+ * Query entity graph - find path or subgraph between entities
126
+ */
127
+ queryEntityGraph(startEntityName: string, endEntityName?: string, maxHops: number = 2): EntityGraphQueryResult {
128
+ // Get start entity
129
+ const startEntity = this.getEntityByName(startEntityName);
130
+ if (!startEntity) {
131
+ return { nodes: [], edges: [], found: false };
132
+ }
133
+
134
+ // If no end entity, return subgraph around start entity
135
+ if (!endEntityName) {
136
+ const subgraph = this.getSubgraph(startEntity.id, maxHops);
137
+ return {
138
+ nodes: subgraph.nodes,
139
+ edges: subgraph.edges,
140
+ found: true
141
+ };
142
+ }
143
+
144
+ // Get end entity
145
+ const endEntity = this.getEntityByName(endEntityName);
146
+ if (!endEntity) {
147
+ return { nodes: [], edges: [], found: false };
148
+ }
149
+
150
+ // Find path using BFS
151
+ const path = this.findPath(startEntity.id, endEntity.id, maxHops);
152
+
153
+ if (!path) {
154
+ return { nodes: [], edges: [], found: false };
155
+ }
156
+
157
+ // Get nodes and edges for the path
158
+ const nodes: EntityNode[] = [];
159
+ const edges: EntityEdge[] = [];
160
+ const nodeIds = new Set<string>();
161
+
162
+ for (const entityId of path) {
163
+ const entityRow = this.db.prepare(`
164
+ SELECT id, name, type FROM entities WHERE id = ?
165
+ `).get(entityId) as { id: string; name: string; type: string } | undefined;
166
+
167
+ if (entityRow) {
168
+ nodes.push({
169
+ id: entityRow.id,
170
+ name: entityRow.name,
171
+ type: entityRow.type as EntityNode['type']
172
+ });
173
+ nodeIds.add(entityRow.id);
174
+ }
175
+ }
176
+
177
+ // Get edges between the path nodes
178
+ for (let i = 0; i < path.length - 1; i++) {
179
+ const edgeRows = this.db.prepare(`
180
+ SELECT source_id, target_id, relation_type, weight
181
+ FROM entity_relations
182
+ WHERE (source_id = ? AND target_id = ?) OR (source_id = ? AND target_id = ?)
183
+ `).all(path[i], path[i + 1], path[i + 1], path[i]) as { source_id: string; target_id: string; relation_type: string; weight: number }[];
184
+
185
+ for (const row of edgeRows) {
186
+ edges.push({
187
+ source: row.source_id,
188
+ target: row.target_id,
189
+ type: row.relation_type as EntityEdge['type'],
190
+ weight: row.weight
191
+ });
192
+ }
193
+ }
194
+
195
+ return {
196
+ path,
197
+ nodes,
198
+ edges,
199
+ found: true
200
+ };
201
+ }
202
+
203
+ /**
204
+ * Find shortest path between two entities using BFS
205
+ */
206
+ private findPath(startId: string, endId: string, maxHops: number): string[] | null {
207
+ if (startId === endId) {
208
+ return [startId];
209
+ }
210
+
211
+ const queue: { id: string; path: string[] }[] = [{ id: startId, path: [startId] }];
212
+ const visited = new Set<string>([startId]);
213
+
214
+ while (queue.length > 0) {
215
+ const { id, path } = queue.shift()!;
216
+
217
+ if (path.length > maxHops) {
218
+ continue;
219
+ }
220
+
221
+ // Get connected entities
222
+ const connectedRows = this.db.prepare(`
223
+ SELECT source_id, target_id FROM entity_relations
224
+ WHERE source_id = ? OR target_id = ?
225
+ `).all(id, id) as { source_id: string; target_id: string }[];
226
+
227
+ for (const row of connectedRows) {
228
+ const connectedId = row.source_id === id ? row.target_id : row.source_id;
229
+
230
+ if (connectedId === endId) {
231
+ return [...path, connectedId];
232
+ }
233
+
234
+ if (!visited.has(connectedId)) {
235
+ visited.add(connectedId);
236
+ queue.push({ id: connectedId, path: [...path, connectedId] });
237
+ }
238
+ }
239
+ }
240
+
241
+ return null;
242
+ }
243
+
244
+ /**
245
+ * Get statistics about entity relations
246
+ */
247
+ getRelationStats(): RelationStats {
248
+ // Get total relations count
249
+ const totalResult = this.db.prepare(`
250
+ SELECT COUNT(*) as count FROM entity_relations
251
+ `).get() as { count: number };
252
+ const total_relations = totalResult.count;
253
+
254
+ // Get count by relation type
255
+ const typeRows = this.db.prepare(`
256
+ SELECT relation_type, COUNT(*) as count
257
+ FROM entity_relations
258
+ GROUP BY relation_type
259
+ `).all() as { relation_type: string; count: number }[];
260
+
261
+ const relation_types: Record<string, number> = {};
262
+ for (const row of typeRows) {
263
+ relation_types[row.relation_type] = row.count;
264
+ }
265
+
266
+ // Get most connected entities
267
+ const connectedRows = this.db.prepare(`
268
+ SELECT e.id, e.name, COUNT(er.id) as connection_count
269
+ FROM entities e
270
+ LEFT JOIN (
271
+ SELECT source_id as entity_id, id FROM entity_relations
272
+ UNION ALL
273
+ SELECT target_id as entity_id, id FROM entity_relations
274
+ ) er ON e.id = er.entity_id
275
+ GROUP BY e.id, e.name
276
+ ORDER BY connection_count DESC
277
+ LIMIT 10
278
+ `).all() as { id: string; name: string; connection_count: number }[];
279
+
280
+ const most_connected = connectedRows.map(row => ({
281
+ id: row.id,
282
+ name: row.name,
283
+ connection_count: row.connection_count
284
+ }));
285
+
286
+ return {
287
+ most_connected,
288
+ relation_types,
289
+ total_relations
290
+ };
291
+ }
292
+
293
+ /**
294
+ * Create a relation between two entities
295
+ */
296
+ createRelation(sourceId: string, targetId: string, type: EntityEdge['type'], weight: number = 1.0): EntityRelation {
297
+ const { v4: uuidv4 } = require('uuid');
298
+ const id = uuidv4();
299
+ const now = new Date();
300
+
301
+ this.db.prepare(`
302
+ INSERT INTO entity_relations (id, source_id, target_id, relation_type, weight, evidence_count, created_at)
303
+ VALUES (?, ?, ?, ?, ?, 1, ?)
304
+ `).run(id, sourceId, targetId, type, weight, now.toISOString());
305
+
306
+ return {
307
+ id,
308
+ sourceId,
309
+ targetId,
310
+ relationType: type,
311
+ weight,
312
+ evidenceCount: 1,
313
+ createdAt: now
314
+ };
315
+ }
316
+
317
+ /**
318
+ * Get subgraph around a specific entity (depth: number of hops)
319
+ */
320
+ getSubgraph(entityId: string, depth: number = 1): GraphData {
321
+ const visited = new Set<string>();
322
+ const nodes: EntityNode[] = [];
323
+ const edges: EntityEdge[] = [];
324
+
325
+ const collectNodes = (currentId: string, currentDepth: number) => {
326
+ if (currentDepth > depth || visited.has(currentId)) return;
327
+ visited.add(currentId);
328
+
329
+ // Get entity info
330
+ const entityRow = this.db.prepare(`
331
+ SELECT id, name, type FROM entities WHERE id = ?
332
+ `).get(currentId) as { id: string; name: string; type: string } | undefined;
333
+
334
+ if (entityRow) {
335
+ nodes.push({
336
+ id: entityRow.id,
337
+ name: entityRow.name,
338
+ type: entityRow.type as EntityNode['type']
339
+ });
340
+ }
341
+
342
+ // Get connected entities
343
+ const connectedRows = this.db.prepare(`
344
+ SELECT source_id, target_id, relation_type, weight
345
+ FROM entity_relations
346
+ WHERE source_id = ? OR target_id = ?
347
+ `).all(currentId, currentId) as { source_id: string; target_id: string; relation_type: string; weight: number }[];
348
+
349
+ for (const row of connectedRows) {
350
+ const connectedId = row.source_id === currentId ? row.target_id : row.source_id;
351
+
352
+ edges.push({
353
+ source: row.source_id,
354
+ target: row.target_id,
355
+ type: row.relation_type as EntityEdge['type'],
356
+ weight: row.weight
357
+ });
358
+
359
+ collectNodes(connectedId, currentDepth + 1);
360
+ }
361
+ };
362
+
363
+ collectNodes(entityId, 0);
364
+
365
+ return { nodes, edges };
366
+ }
367
+ }