supaclaw 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +871 -0
- package/SCHEMA.md +215 -0
- package/dist/clawdbot-integration.d.ts +171 -0
- package/dist/clawdbot-integration.js +339 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +1815 -0
- package/dist/context-manager.d.ts +143 -0
- package/dist/context-manager.js +360 -0
- package/dist/error-handling.d.ts +100 -0
- package/dist/error-handling.js +301 -0
- package/dist/index.d.ts +735 -0
- package/dist/index.js +2256 -0
- package/dist/parsers.d.ts +115 -0
- package/dist/parsers.js +406 -0
- package/migrations/001_initial.sql +153 -0
- package/migrations/002_vector_search.sql +219 -0
- package/migrations/003_entity_relationships.sql +143 -0
- package/package.json +66 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 9: Migration & Import Parsers
|
|
3
|
+
*
|
|
4
|
+
* Parsers for converting Clawdbot memory files to OpenClaw Memory database format:
|
|
5
|
+
* - MEMORY.md → memories table
|
|
6
|
+
* - memory/*.md → sessions + messages
|
|
7
|
+
* - TODO.md → tasks table
|
|
8
|
+
* - LEARNINGS.md → learnings table
|
|
9
|
+
*/
|
|
10
|
+
export interface ParsedMemory {
|
|
11
|
+
content: string;
|
|
12
|
+
category: string;
|
|
13
|
+
importance: number;
|
|
14
|
+
metadata?: Record<string, any>;
|
|
15
|
+
created_at?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface ParsedSession {
|
|
18
|
+
user_id: string;
|
|
19
|
+
channel?: string;
|
|
20
|
+
started_at: string;
|
|
21
|
+
ended_at?: string;
|
|
22
|
+
summary?: string;
|
|
23
|
+
messages: ParsedMessage[];
|
|
24
|
+
}
|
|
25
|
+
export interface ParsedMessage {
|
|
26
|
+
role: 'user' | 'assistant' | 'system';
|
|
27
|
+
content: string;
|
|
28
|
+
timestamp: string;
|
|
29
|
+
metadata?: Record<string, any>;
|
|
30
|
+
}
|
|
31
|
+
export interface ParsedTask {
|
|
32
|
+
title: string;
|
|
33
|
+
description?: string;
|
|
34
|
+
status: 'pending' | 'in_progress' | 'completed' | 'cancelled';
|
|
35
|
+
priority?: number;
|
|
36
|
+
due_date?: string;
|
|
37
|
+
metadata?: Record<string, any>;
|
|
38
|
+
}
|
|
39
|
+
export interface ParsedLearning {
|
|
40
|
+
category: string;
|
|
41
|
+
trigger: string;
|
|
42
|
+
lesson: string;
|
|
43
|
+
importance: number;
|
|
44
|
+
applied_count?: number;
|
|
45
|
+
created_at?: string;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Parse MEMORY.md into structured memories
|
|
49
|
+
*
|
|
50
|
+
* Expected format:
|
|
51
|
+
* # MEMORY.md
|
|
52
|
+
*
|
|
53
|
+
* ## Category Name
|
|
54
|
+
*
|
|
55
|
+
* - Memory item [importance: 0.9]
|
|
56
|
+
* - Another memory [2024-01-28]
|
|
57
|
+
*
|
|
58
|
+
* Regular paragraphs are also captured as memories.
|
|
59
|
+
*/
|
|
60
|
+
export declare function parseMemoryMd(filePath: string): ParsedMemory[];
|
|
61
|
+
/**
|
|
62
|
+
* Parse daily log files (memory/YYYY-MM-DD.md) into sessions and messages
|
|
63
|
+
*
|
|
64
|
+
* Expected format:
|
|
65
|
+
* # 2024-01-28
|
|
66
|
+
*
|
|
67
|
+
* ## Session: Trading Research
|
|
68
|
+
* Started: 09:00
|
|
69
|
+
*
|
|
70
|
+
* **User**: What's the stock price of TSLA?
|
|
71
|
+
*
|
|
72
|
+
* **Assistant**: Tesla is currently trading at $245.
|
|
73
|
+
*
|
|
74
|
+
* **User**: Should I buy?
|
|
75
|
+
*
|
|
76
|
+
* Summary: Discussed TSLA stock price...
|
|
77
|
+
*/
|
|
78
|
+
export declare function parseDailyLog(filePath: string, userId?: string): ParsedSession[];
|
|
79
|
+
/**
|
|
80
|
+
* Parse all daily logs in a directory (memory/*.md)
|
|
81
|
+
*/
|
|
82
|
+
export declare function parseAllDailyLogs(memoryDir: string, userId?: string): ParsedSession[];
|
|
83
|
+
/**
|
|
84
|
+
* Parse TODO.md into structured tasks
|
|
85
|
+
*
|
|
86
|
+
* Expected format:
|
|
87
|
+
* # TODO
|
|
88
|
+
*
|
|
89
|
+
* ## Priority: High
|
|
90
|
+
* - [ ] Incomplete task
|
|
91
|
+
* - [x] Completed task
|
|
92
|
+
* - [~] Cancelled task
|
|
93
|
+
*
|
|
94
|
+
* ## Category Name
|
|
95
|
+
* - [ ] Task with [due: 2024-02-01]
|
|
96
|
+
*/
|
|
97
|
+
export declare function parseTodoMd(filePath: string): ParsedTask[];
|
|
98
|
+
/**
|
|
99
|
+
* Parse LEARNINGS.md into structured learnings
|
|
100
|
+
*
|
|
101
|
+
* Expected format:
|
|
102
|
+
* # LEARNINGS
|
|
103
|
+
*
|
|
104
|
+
* ## Category: Corrections
|
|
105
|
+
*
|
|
106
|
+
* **Trigger**: User said "actually, I prefer Rust"
|
|
107
|
+
* **Lesson**: User prefers Rust over TypeScript
|
|
108
|
+
* **Importance**: 0.8
|
|
109
|
+
*
|
|
110
|
+
* ---
|
|
111
|
+
*
|
|
112
|
+
* ## Category: Errors
|
|
113
|
+
* ...
|
|
114
|
+
*/
|
|
115
|
+
export declare function parseLearningsMd(filePath: string): ParsedLearning[];
|
package/dist/parsers.js
ADDED
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Phase 9: Migration & Import Parsers
|
|
4
|
+
*
|
|
5
|
+
* Parsers for converting Clawdbot memory files to OpenClaw Memory database format:
|
|
6
|
+
* - MEMORY.md → memories table
|
|
7
|
+
* - memory/*.md → sessions + messages
|
|
8
|
+
* - TODO.md → tasks table
|
|
9
|
+
* - LEARNINGS.md → learnings table
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.parseMemoryMd = parseMemoryMd;
|
|
13
|
+
exports.parseDailyLog = parseDailyLog;
|
|
14
|
+
exports.parseAllDailyLogs = parseAllDailyLogs;
|
|
15
|
+
exports.parseTodoMd = parseTodoMd;
|
|
16
|
+
exports.parseLearningsMd = parseLearningsMd;
|
|
17
|
+
const fs_1 = require("fs");
|
|
18
|
+
const path_1 = require("path");
|
|
19
|
+
// ============ MEMORY.MD PARSER ============
|
|
20
|
+
/**
|
|
21
|
+
* Parse MEMORY.md into structured memories
|
|
22
|
+
*
|
|
23
|
+
* Expected format:
|
|
24
|
+
* # MEMORY.md
|
|
25
|
+
*
|
|
26
|
+
* ## Category Name
|
|
27
|
+
*
|
|
28
|
+
* - Memory item [importance: 0.9]
|
|
29
|
+
* - Another memory [2024-01-28]
|
|
30
|
+
*
|
|
31
|
+
* Regular paragraphs are also captured as memories.
|
|
32
|
+
*/
|
|
33
|
+
function parseMemoryMd(filePath) {
|
|
34
|
+
if (!(0, fs_1.existsSync)(filePath)) {
|
|
35
|
+
throw new Error(`File not found: ${filePath}`);
|
|
36
|
+
}
|
|
37
|
+
const content = (0, fs_1.readFileSync)(filePath, 'utf-8');
|
|
38
|
+
const memories = [];
|
|
39
|
+
const lines = content.split('\n');
|
|
40
|
+
let currentCategory = 'general';
|
|
41
|
+
let currentParagraph = '';
|
|
42
|
+
let lineNumber = 0;
|
|
43
|
+
const flushParagraph = () => {
|
|
44
|
+
if (currentParagraph.trim().length > 0) {
|
|
45
|
+
memories.push({
|
|
46
|
+
content: currentParagraph.trim(),
|
|
47
|
+
category: currentCategory,
|
|
48
|
+
importance: 0.6,
|
|
49
|
+
metadata: { source: 'MEMORY.md' }
|
|
50
|
+
});
|
|
51
|
+
currentParagraph = '';
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
for (const line of lines) {
|
|
55
|
+
lineNumber++;
|
|
56
|
+
const trimmed = line.trim();
|
|
57
|
+
// Skip title and empty lines at paragraph boundaries
|
|
58
|
+
if (trimmed.startsWith('# ') || trimmed === '') {
|
|
59
|
+
flushParagraph();
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
// Category header
|
|
63
|
+
if (trimmed.startsWith('## ')) {
|
|
64
|
+
flushParagraph();
|
|
65
|
+
currentCategory = trimmed.slice(3).trim().toLowerCase();
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
// List item memory
|
|
69
|
+
if (trimmed.startsWith('- ') || trimmed.startsWith('* ')) {
|
|
70
|
+
flushParagraph();
|
|
71
|
+
let memoryText = trimmed.slice(2).trim();
|
|
72
|
+
let importance = 0.6;
|
|
73
|
+
let createdAt;
|
|
74
|
+
// Extract [importance: X] tags
|
|
75
|
+
const importanceMatch = memoryText.match(/\[importance:\s*([\d.]+)\]/i);
|
|
76
|
+
if (importanceMatch) {
|
|
77
|
+
importance = parseFloat(importanceMatch[1]);
|
|
78
|
+
memoryText = memoryText.replace(importanceMatch[0], '').trim();
|
|
79
|
+
}
|
|
80
|
+
// Extract [YYYY-MM-DD] dates
|
|
81
|
+
const dateMatch = memoryText.match(/\[(\d{4}-\d{2}-\d{2})\]/);
|
|
82
|
+
if (dateMatch) {
|
|
83
|
+
createdAt = dateMatch[1];
|
|
84
|
+
memoryText = memoryText.replace(dateMatch[0], '').trim();
|
|
85
|
+
}
|
|
86
|
+
if (memoryText.length > 0) {
|
|
87
|
+
memories.push({
|
|
88
|
+
content: memoryText,
|
|
89
|
+
category: currentCategory,
|
|
90
|
+
importance,
|
|
91
|
+
created_at: createdAt,
|
|
92
|
+
metadata: { source: 'MEMORY.md', line: lineNumber }
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
// Regular text - accumulate into paragraph
|
|
98
|
+
if (trimmed.length > 0) {
|
|
99
|
+
currentParagraph += (currentParagraph ? ' ' : '') + trimmed;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
flushParagraph();
|
|
103
|
+
return memories;
|
|
104
|
+
}
|
|
105
|
+
// ============ DAILY LOG PARSER (memory/*.md) ============
|
|
106
|
+
/**
|
|
107
|
+
* Parse daily log files (memory/YYYY-MM-DD.md) into sessions and messages
|
|
108
|
+
*
|
|
109
|
+
* Expected format:
|
|
110
|
+
* # 2024-01-28
|
|
111
|
+
*
|
|
112
|
+
* ## Session: Trading Research
|
|
113
|
+
* Started: 09:00
|
|
114
|
+
*
|
|
115
|
+
* **User**: What's the stock price of TSLA?
|
|
116
|
+
*
|
|
117
|
+
* **Assistant**: Tesla is currently trading at $245.
|
|
118
|
+
*
|
|
119
|
+
* **User**: Should I buy?
|
|
120
|
+
*
|
|
121
|
+
* Summary: Discussed TSLA stock price...
|
|
122
|
+
*/
|
|
123
|
+
function parseDailyLog(filePath, userId = 'default') {
|
|
124
|
+
if (!(0, fs_1.existsSync)(filePath)) {
|
|
125
|
+
throw new Error(`File not found: ${filePath}`);
|
|
126
|
+
}
|
|
127
|
+
const content = (0, fs_1.readFileSync)(filePath, 'utf-8');
|
|
128
|
+
const sessions = [];
|
|
129
|
+
const lines = content.split('\n');
|
|
130
|
+
let currentSession = null;
|
|
131
|
+
let currentMessage = '';
|
|
132
|
+
let currentRole = null;
|
|
133
|
+
// Extract date from filename (YYYY-MM-DD.md)
|
|
134
|
+
const dateMatch = filePath.match(/(\d{4}-\d{2}-\d{2})/);
|
|
135
|
+
const fileDate = dateMatch ? dateMatch[1] : new Date().toISOString().split('T')[0];
|
|
136
|
+
const flushMessage = () => {
|
|
137
|
+
if (currentMessage.trim() && currentRole && currentSession) {
|
|
138
|
+
currentSession.messages.push({
|
|
139
|
+
role: currentRole,
|
|
140
|
+
content: currentMessage.trim(),
|
|
141
|
+
timestamp: currentSession.started_at
|
|
142
|
+
});
|
|
143
|
+
currentMessage = '';
|
|
144
|
+
currentRole = null;
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
const flushSession = () => {
|
|
148
|
+
if (currentSession) {
|
|
149
|
+
sessions.push(currentSession);
|
|
150
|
+
currentSession = null;
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
for (const line of lines) {
|
|
154
|
+
const trimmed = line.trim();
|
|
155
|
+
// Session header
|
|
156
|
+
if (trimmed.startsWith('## Session:') || trimmed.startsWith('## ')) {
|
|
157
|
+
flushMessage();
|
|
158
|
+
flushSession();
|
|
159
|
+
const sessionName = trimmed.replace(/^## (Session:?\s*)?/, '').trim();
|
|
160
|
+
currentSession = {
|
|
161
|
+
user_id: userId,
|
|
162
|
+
started_at: `${fileDate}T00:00:00Z`,
|
|
163
|
+
messages: [],
|
|
164
|
+
summary: sessionName
|
|
165
|
+
};
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
// Started/Ended timestamps
|
|
169
|
+
if (trimmed.startsWith('Started:') && currentSession) {
|
|
170
|
+
const time = trimmed.replace('Started:', '').trim();
|
|
171
|
+
currentSession.started_at = `${fileDate}T${time}:00Z`;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (trimmed.startsWith('Ended:') && currentSession) {
|
|
175
|
+
const time = trimmed.replace('Ended:', '').trim();
|
|
176
|
+
currentSession.ended_at = `${fileDate}T${time}:00Z`;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
// Summary
|
|
180
|
+
if (trimmed.startsWith('Summary:') && currentSession) {
|
|
181
|
+
currentSession.summary = trimmed.replace('Summary:', '').trim();
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
// Message markers
|
|
185
|
+
if (trimmed.startsWith('**User**:') || trimmed.startsWith('**User**')) {
|
|
186
|
+
flushMessage();
|
|
187
|
+
currentRole = 'user';
|
|
188
|
+
currentMessage = trimmed.replace(/^\*\*User\*\*:?\s*/, '');
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (trimmed.startsWith('**Assistant**:') || trimmed.startsWith('**Assistant**')) {
|
|
192
|
+
flushMessage();
|
|
193
|
+
currentRole = 'assistant';
|
|
194
|
+
currentMessage = trimmed.replace(/^\*\*Assistant\*\*:?\s*/, '');
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (trimmed.startsWith('**System**:') || trimmed.startsWith('**System**')) {
|
|
198
|
+
flushMessage();
|
|
199
|
+
currentRole = 'system';
|
|
200
|
+
currentMessage = trimmed.replace(/^\*\*System\*\*:?\s*/, '');
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
// Continue accumulating message content
|
|
204
|
+
if (currentRole && trimmed.length > 0) {
|
|
205
|
+
currentMessage += '\n' + trimmed;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
flushMessage();
|
|
209
|
+
flushSession();
|
|
210
|
+
// If no sessions were found but there's content, create a default session
|
|
211
|
+
if (sessions.length === 0 && content.trim().length > 0) {
|
|
212
|
+
sessions.push({
|
|
213
|
+
user_id: userId,
|
|
214
|
+
started_at: `${fileDate}T00:00:00Z`,
|
|
215
|
+
messages: [{
|
|
216
|
+
role: 'system',
|
|
217
|
+
content: content,
|
|
218
|
+
timestamp: `${fileDate}T00:00:00Z`
|
|
219
|
+
}],
|
|
220
|
+
summary: 'Daily log'
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
return sessions;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Parse all daily logs in a directory (memory/*.md)
|
|
227
|
+
*/
|
|
228
|
+
function parseAllDailyLogs(memoryDir, userId = 'default') {
|
|
229
|
+
if (!(0, fs_1.existsSync)(memoryDir)) {
|
|
230
|
+
throw new Error(`Directory not found: ${memoryDir}`);
|
|
231
|
+
}
|
|
232
|
+
const files = (0, fs_1.readdirSync)(memoryDir)
|
|
233
|
+
.filter(f => f.match(/^\d{4}-\d{2}-\d{2}\.md$/))
|
|
234
|
+
.sort();
|
|
235
|
+
const allSessions = [];
|
|
236
|
+
for (const file of files) {
|
|
237
|
+
const filePath = (0, path_1.join)(memoryDir, file);
|
|
238
|
+
try {
|
|
239
|
+
const sessions = parseDailyLog(filePath, userId);
|
|
240
|
+
allSessions.push(...sessions);
|
|
241
|
+
}
|
|
242
|
+
catch (err) {
|
|
243
|
+
console.error(`⚠️ Failed to parse ${file}:`, err);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return allSessions;
|
|
247
|
+
}
|
|
248
|
+
// ============ TODO.MD PARSER ============
|
|
249
|
+
/**
|
|
250
|
+
* Parse TODO.md into structured tasks
|
|
251
|
+
*
|
|
252
|
+
* Expected format:
|
|
253
|
+
* # TODO
|
|
254
|
+
*
|
|
255
|
+
* ## Priority: High
|
|
256
|
+
* - [ ] Incomplete task
|
|
257
|
+
* - [x] Completed task
|
|
258
|
+
* - [~] Cancelled task
|
|
259
|
+
*
|
|
260
|
+
* ## Category Name
|
|
261
|
+
* - [ ] Task with [due: 2024-02-01]
|
|
262
|
+
*/
|
|
263
|
+
function parseTodoMd(filePath) {
|
|
264
|
+
if (!(0, fs_1.existsSync)(filePath)) {
|
|
265
|
+
throw new Error(`File not found: ${filePath}`);
|
|
266
|
+
}
|
|
267
|
+
const content = (0, fs_1.readFileSync)(filePath, 'utf-8');
|
|
268
|
+
const tasks = [];
|
|
269
|
+
const lines = content.split('\n');
|
|
270
|
+
let currentPriority = 1;
|
|
271
|
+
let currentCategory = 'general';
|
|
272
|
+
for (const line of lines) {
|
|
273
|
+
const trimmed = line.trim();
|
|
274
|
+
// Skip title and empty lines
|
|
275
|
+
if (trimmed.startsWith('# ') || trimmed === '') {
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
// Category/Section headers
|
|
279
|
+
if (trimmed.startsWith('## ')) {
|
|
280
|
+
const header = trimmed.slice(3).trim();
|
|
281
|
+
// Check for priority indicators
|
|
282
|
+
if (header.toLowerCase().includes('priority')) {
|
|
283
|
+
if (header.toLowerCase().includes('high') || header.toLowerCase().includes('urgent')) {
|
|
284
|
+
currentPriority = 3;
|
|
285
|
+
}
|
|
286
|
+
else if (header.toLowerCase().includes('medium') || header.toLowerCase().includes('normal')) {
|
|
287
|
+
currentPriority = 2;
|
|
288
|
+
}
|
|
289
|
+
else if (header.toLowerCase().includes('low')) {
|
|
290
|
+
currentPriority = 1;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
currentCategory = header.toLowerCase();
|
|
295
|
+
}
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
// Task items
|
|
299
|
+
const taskMatch = trimmed.match(/^[-*]\s*\[([ x~])\]\s*(.+)$/i);
|
|
300
|
+
if (taskMatch) {
|
|
301
|
+
const statusChar = taskMatch[1];
|
|
302
|
+
let taskText = taskMatch[2].trim();
|
|
303
|
+
let status = 'pending';
|
|
304
|
+
if (statusChar.toLowerCase() === 'x')
|
|
305
|
+
status = 'completed';
|
|
306
|
+
if (statusChar === '~')
|
|
307
|
+
status = 'cancelled';
|
|
308
|
+
// Extract [due: YYYY-MM-DD] tags
|
|
309
|
+
let dueDate;
|
|
310
|
+
const dueDateMatch = taskText.match(/\[due:\s*(\d{4}-\d{2}-\d{2})\]/i);
|
|
311
|
+
if (dueDateMatch) {
|
|
312
|
+
dueDate = dueDateMatch[1];
|
|
313
|
+
taskText = taskText.replace(dueDateMatch[0], '').trim();
|
|
314
|
+
}
|
|
315
|
+
if (taskText.length > 0) {
|
|
316
|
+
tasks.push({
|
|
317
|
+
title: taskText,
|
|
318
|
+
status,
|
|
319
|
+
priority: currentPriority,
|
|
320
|
+
due_date: dueDate,
|
|
321
|
+
metadata: { source: 'TODO.md', category: currentCategory }
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return tasks;
|
|
327
|
+
}
|
|
328
|
+
// ============ LEARNINGS.MD PARSER ============
|
|
329
|
+
/**
|
|
330
|
+
* Parse LEARNINGS.md into structured learnings
|
|
331
|
+
*
|
|
332
|
+
* Expected format:
|
|
333
|
+
* # LEARNINGS
|
|
334
|
+
*
|
|
335
|
+
* ## Category: Corrections
|
|
336
|
+
*
|
|
337
|
+
* **Trigger**: User said "actually, I prefer Rust"
|
|
338
|
+
* **Lesson**: User prefers Rust over TypeScript
|
|
339
|
+
* **Importance**: 0.8
|
|
340
|
+
*
|
|
341
|
+
* ---
|
|
342
|
+
*
|
|
343
|
+
* ## Category: Errors
|
|
344
|
+
* ...
|
|
345
|
+
*/
|
|
346
|
+
function parseLearningsMd(filePath) {
|
|
347
|
+
if (!(0, fs_1.existsSync)(filePath)) {
|
|
348
|
+
throw new Error(`File not found: ${filePath}`);
|
|
349
|
+
}
|
|
350
|
+
const content = (0, fs_1.readFileSync)(filePath, 'utf-8');
|
|
351
|
+
const learnings = [];
|
|
352
|
+
// Split by --- separators
|
|
353
|
+
const blocks = content.split(/\n---+\n/);
|
|
354
|
+
for (const block of blocks) {
|
|
355
|
+
const lines = block.trim().split('\n');
|
|
356
|
+
let category = 'general';
|
|
357
|
+
let trigger = '';
|
|
358
|
+
let lesson = '';
|
|
359
|
+
let importance = 0.5;
|
|
360
|
+
let createdAt;
|
|
361
|
+
for (const line of lines) {
|
|
362
|
+
const trimmed = line.trim();
|
|
363
|
+
// Category header
|
|
364
|
+
if (trimmed.startsWith('## ')) {
|
|
365
|
+
const header = trimmed.slice(3).trim();
|
|
366
|
+
const catMatch = header.match(/Category:\s*(.+)/i);
|
|
367
|
+
if (catMatch) {
|
|
368
|
+
category = catMatch[1].trim().toLowerCase();
|
|
369
|
+
}
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
// Field lines
|
|
373
|
+
const triggerMatch = trimmed.match(/^\*\*Trigger\*\*:\s*(.+)/i);
|
|
374
|
+
if (triggerMatch) {
|
|
375
|
+
trigger = triggerMatch[1].trim();
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
const lessonMatch = trimmed.match(/^\*\*Lesson\*\*:\s*(.+)/i);
|
|
379
|
+
if (lessonMatch) {
|
|
380
|
+
lesson = lessonMatch[1].trim();
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
const importanceMatch = trimmed.match(/^\*\*Importance\*\*:\s*([\d.]+)/i);
|
|
384
|
+
if (importanceMatch) {
|
|
385
|
+
importance = parseFloat(importanceMatch[1]);
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
const dateMatch = trimmed.match(/^\*\*Date\*\*:\s*(\d{4}-\d{2}-\d{2})/i);
|
|
389
|
+
if (dateMatch) {
|
|
390
|
+
createdAt = dateMatch[1];
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
// Add if we have at minimum a lesson
|
|
395
|
+
if (lesson.length > 0) {
|
|
396
|
+
learnings.push({
|
|
397
|
+
category,
|
|
398
|
+
trigger: trigger || 'Unknown trigger',
|
|
399
|
+
lesson,
|
|
400
|
+
importance,
|
|
401
|
+
created_at: createdAt
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
return learnings;
|
|
406
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
-- OpenClaw Memory - Initial Schema
|
|
2
|
+
-- Run this in your Supabase SQL editor
|
|
3
|
+
|
|
4
|
+
-- Enable vector extension for semantic search
|
|
5
|
+
CREATE EXTENSION IF NOT EXISTS vector;
|
|
6
|
+
|
|
7
|
+
-- Sessions: Every conversation gets a session
|
|
8
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
9
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
10
|
+
agent_id TEXT NOT NULL,
|
|
11
|
+
user_id TEXT,
|
|
12
|
+
channel TEXT,
|
|
13
|
+
started_at TIMESTAMPTZ DEFAULT NOW(),
|
|
14
|
+
ended_at TIMESTAMPTZ,
|
|
15
|
+
summary TEXT,
|
|
16
|
+
metadata JSONB DEFAULT '{}'
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
CREATE INDEX IF NOT EXISTS sessions_agent_id_idx ON sessions(agent_id);
|
|
20
|
+
CREATE INDEX IF NOT EXISTS sessions_user_id_idx ON sessions(user_id);
|
|
21
|
+
CREATE INDEX IF NOT EXISTS sessions_started_at_idx ON sessions(started_at DESC);
|
|
22
|
+
|
|
23
|
+
-- Messages: Every message in every session
|
|
24
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
25
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
26
|
+
session_id UUID REFERENCES sessions(id) ON DELETE CASCADE,
|
|
27
|
+
role TEXT NOT NULL CHECK (role IN ('user', 'assistant', 'system', 'tool')),
|
|
28
|
+
content TEXT NOT NULL,
|
|
29
|
+
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
30
|
+
token_count INT,
|
|
31
|
+
metadata JSONB DEFAULT '{}'
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
CREATE INDEX IF NOT EXISTS messages_session_id_idx ON messages(session_id);
|
|
35
|
+
CREATE INDEX IF NOT EXISTS messages_created_at_idx ON messages(created_at);
|
|
36
|
+
|
|
37
|
+
-- Memories: Long-term memories extracted from sessions
|
|
38
|
+
CREATE TABLE IF NOT EXISTS memories (
|
|
39
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
40
|
+
agent_id TEXT NOT NULL,
|
|
41
|
+
user_id TEXT,
|
|
42
|
+
category TEXT,
|
|
43
|
+
content TEXT NOT NULL,
|
|
44
|
+
importance FLOAT DEFAULT 0.5 CHECK (importance >= 0 AND importance <= 1),
|
|
45
|
+
source_session_id UUID REFERENCES sessions(id),
|
|
46
|
+
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
47
|
+
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
|
48
|
+
expires_at TIMESTAMPTZ,
|
|
49
|
+
embedding VECTOR(1536),
|
|
50
|
+
metadata JSONB DEFAULT '{}'
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
CREATE INDEX IF NOT EXISTS memories_agent_id_idx ON memories(agent_id);
|
|
54
|
+
CREATE INDEX IF NOT EXISTS memories_user_id_idx ON memories(user_id);
|
|
55
|
+
CREATE INDEX IF NOT EXISTS memories_category_idx ON memories(category);
|
|
56
|
+
CREATE INDEX IF NOT EXISTS memories_importance_idx ON memories(importance DESC);
|
|
57
|
+
CREATE INDEX IF NOT EXISTS memories_created_at_idx ON memories(created_at DESC);
|
|
58
|
+
|
|
59
|
+
-- Vector similarity index (for semantic search)
|
|
60
|
+
CREATE INDEX IF NOT EXISTS memories_embedding_idx ON memories
|
|
61
|
+
USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100);
|
|
62
|
+
|
|
63
|
+
-- Entities: People, places, things the agent knows about
|
|
64
|
+
CREATE TABLE IF NOT EXISTS entities (
|
|
65
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
66
|
+
agent_id TEXT NOT NULL,
|
|
67
|
+
entity_type TEXT NOT NULL,
|
|
68
|
+
name TEXT NOT NULL,
|
|
69
|
+
aliases TEXT[],
|
|
70
|
+
description TEXT,
|
|
71
|
+
properties JSONB DEFAULT '{}',
|
|
72
|
+
first_seen_at TIMESTAMPTZ DEFAULT NOW(),
|
|
73
|
+
last_seen_at TIMESTAMPTZ DEFAULT NOW(),
|
|
74
|
+
mention_count INT DEFAULT 1,
|
|
75
|
+
embedding VECTOR(1536),
|
|
76
|
+
|
|
77
|
+
UNIQUE(agent_id, entity_type, name)
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
CREATE INDEX IF NOT EXISTS entities_agent_id_idx ON entities(agent_id);
|
|
81
|
+
CREATE INDEX IF NOT EXISTS entities_type_idx ON entities(entity_type);
|
|
82
|
+
CREATE INDEX IF NOT EXISTS entities_name_idx ON entities(name);
|
|
83
|
+
|
|
84
|
+
-- Tasks: Persistent task tracking
|
|
85
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
86
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
87
|
+
agent_id TEXT NOT NULL,
|
|
88
|
+
user_id TEXT,
|
|
89
|
+
title TEXT NOT NULL,
|
|
90
|
+
description TEXT,
|
|
91
|
+
status TEXT DEFAULT 'pending' CHECK (status IN ('pending', 'in_progress', 'blocked', 'done')),
|
|
92
|
+
priority INT DEFAULT 0,
|
|
93
|
+
due_at TIMESTAMPTZ,
|
|
94
|
+
completed_at TIMESTAMPTZ,
|
|
95
|
+
source_session_id UUID REFERENCES sessions(id),
|
|
96
|
+
parent_task_id UUID REFERENCES tasks(id),
|
|
97
|
+
metadata JSONB DEFAULT '{}',
|
|
98
|
+
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
99
|
+
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
CREATE INDEX IF NOT EXISTS tasks_agent_id_idx ON tasks(agent_id);
|
|
103
|
+
CREATE INDEX IF NOT EXISTS tasks_user_id_idx ON tasks(user_id);
|
|
104
|
+
CREATE INDEX IF NOT EXISTS tasks_status_idx ON tasks(status);
|
|
105
|
+
CREATE INDEX IF NOT EXISTS tasks_priority_idx ON tasks(priority DESC);
|
|
106
|
+
CREATE INDEX IF NOT EXISTS tasks_due_at_idx ON tasks(due_at);
|
|
107
|
+
|
|
108
|
+
-- Learnings: Self-improvement records
|
|
109
|
+
CREATE TABLE IF NOT EXISTS learnings (
|
|
110
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
111
|
+
agent_id TEXT NOT NULL,
|
|
112
|
+
category TEXT NOT NULL CHECK (category IN ('error', 'correction', 'improvement', 'capability_gap')),
|
|
113
|
+
trigger TEXT NOT NULL,
|
|
114
|
+
lesson TEXT NOT NULL,
|
|
115
|
+
action TEXT,
|
|
116
|
+
severity TEXT DEFAULT 'info' CHECK (severity IN ('info', 'warning', 'critical')),
|
|
117
|
+
source_session_id UUID REFERENCES sessions(id),
|
|
118
|
+
applied_count INT DEFAULT 0,
|
|
119
|
+
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
120
|
+
metadata JSONB DEFAULT '{}'
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
CREATE INDEX IF NOT EXISTS learnings_agent_id_idx ON learnings(agent_id);
|
|
124
|
+
CREATE INDEX IF NOT EXISTS learnings_category_idx ON learnings(category);
|
|
125
|
+
CREATE INDEX IF NOT EXISTS learnings_severity_idx ON learnings(severity);
|
|
126
|
+
|
|
127
|
+
-- Function to update updated_at timestamp
|
|
128
|
+
CREATE OR REPLACE FUNCTION update_updated_at()
|
|
129
|
+
RETURNS TRIGGER AS $$
|
|
130
|
+
BEGIN
|
|
131
|
+
NEW.updated_at = NOW();
|
|
132
|
+
RETURN NEW;
|
|
133
|
+
END;
|
|
134
|
+
$$ LANGUAGE plpgsql;
|
|
135
|
+
|
|
136
|
+
-- Triggers for updated_at
|
|
137
|
+
CREATE TRIGGER memories_updated_at
|
|
138
|
+
BEFORE UPDATE ON memories
|
|
139
|
+
FOR EACH ROW
|
|
140
|
+
EXECUTE FUNCTION update_updated_at();
|
|
141
|
+
|
|
142
|
+
CREATE TRIGGER tasks_updated_at
|
|
143
|
+
BEFORE UPDATE ON tasks
|
|
144
|
+
FOR EACH ROW
|
|
145
|
+
EXECUTE FUNCTION update_updated_at();
|
|
146
|
+
|
|
147
|
+
-- Row Level Security (optional - enable if using Supabase auth)
|
|
148
|
+
-- ALTER TABLE sessions ENABLE ROW LEVEL SECURITY;
|
|
149
|
+
-- ALTER TABLE messages ENABLE ROW LEVEL SECURITY;
|
|
150
|
+
-- ALTER TABLE memories ENABLE ROW LEVEL SECURITY;
|
|
151
|
+
-- ALTER TABLE entities ENABLE ROW LEVEL SECURITY;
|
|
152
|
+
-- ALTER TABLE tasks ENABLE ROW LEVEL SECURITY;
|
|
153
|
+
-- ALTER TABLE learnings ENABLE ROW LEVEL SECURITY;
|