@arephan/clawdbot-memory-supabase 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +145 -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/migrations/004_cron_runs.sql +33 -0
- package/migrations/005_search_memories_alias.sql +47 -0
- package/package.json +34 -0
- package/plugin/clawdbot.plugin.json +42 -0
- package/plugin/index.ts +650 -0
- package/plugin/package-lock.json +597 -0
- package/plugin/package.json +10 -0
package/plugin/index.ts
ADDED
|
@@ -0,0 +1,650 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clawdbot Memory (Supabase) Plugin - Simple Edition
|
|
3
|
+
*
|
|
4
|
+
* Logs EVERY message (user + assistant) to Supabase.
|
|
5
|
+
* Provides date-based recall for perfect memory.
|
|
6
|
+
* No embeddings, no vector search - just complete logging.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Type } from "@sinclair/typebox";
|
|
10
|
+
import { createClient, SupabaseClient } from "@supabase/supabase-js";
|
|
11
|
+
import { randomUUID } from "node:crypto";
|
|
12
|
+
import type { ClawdbotPluginApi } from "clawdbot/plugin-sdk";
|
|
13
|
+
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// Types
|
|
16
|
+
// ============================================================================
|
|
17
|
+
|
|
18
|
+
type Session = {
|
|
19
|
+
id: string;
|
|
20
|
+
agent_id: string;
|
|
21
|
+
user_id?: string;
|
|
22
|
+
channel?: string;
|
|
23
|
+
started_at: string;
|
|
24
|
+
ended_at?: string;
|
|
25
|
+
summary?: string;
|
|
26
|
+
metadata: Record<string, unknown>;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
type Message = {
|
|
30
|
+
id: string;
|
|
31
|
+
session_id: string;
|
|
32
|
+
role: "user" | "assistant" | "system" | "tool";
|
|
33
|
+
content: string;
|
|
34
|
+
created_at: string;
|
|
35
|
+
metadata: Record<string, unknown>;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// ============================================================================
|
|
39
|
+
// Config
|
|
40
|
+
// ============================================================================
|
|
41
|
+
|
|
42
|
+
const configSchema = {
|
|
43
|
+
parse: (cfg: unknown) => {
|
|
44
|
+
const config = cfg as {
|
|
45
|
+
supabase: { url: string; anonKey: string };
|
|
46
|
+
agentId?: string;
|
|
47
|
+
};
|
|
48
|
+
return {
|
|
49
|
+
supabase: config.supabase,
|
|
50
|
+
agentId: config.agentId ?? "hans-assistant",
|
|
51
|
+
};
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// ============================================================================
|
|
56
|
+
// Supabase Client
|
|
57
|
+
// ============================================================================
|
|
58
|
+
|
|
59
|
+
class ConversationDB {
|
|
60
|
+
private client: SupabaseClient;
|
|
61
|
+
private agentId: string;
|
|
62
|
+
|
|
63
|
+
constructor(url: string, anonKey: string, agentId: string) {
|
|
64
|
+
this.client = createClient(url, anonKey);
|
|
65
|
+
this.agentId = agentId;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Session management
|
|
69
|
+
async createSession(params: {
|
|
70
|
+
userId?: string;
|
|
71
|
+
channel?: string;
|
|
72
|
+
metadata?: Record<string, unknown>;
|
|
73
|
+
}): Promise<Session> {
|
|
74
|
+
const id = randomUUID();
|
|
75
|
+
const { data, error } = await this.client
|
|
76
|
+
.from("sessions")
|
|
77
|
+
.insert({
|
|
78
|
+
id,
|
|
79
|
+
agent_id: this.agentId,
|
|
80
|
+
user_id: params.userId,
|
|
81
|
+
channel: params.channel,
|
|
82
|
+
started_at: new Date().toISOString(),
|
|
83
|
+
metadata: params.metadata ?? {},
|
|
84
|
+
})
|
|
85
|
+
.select()
|
|
86
|
+
.single();
|
|
87
|
+
|
|
88
|
+
if (error) throw new Error(`Failed to create session: ${error.message}`);
|
|
89
|
+
return data as Session;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async endSession(sessionId: string, summary?: string): Promise<void> {
|
|
93
|
+
const { error } = await this.client
|
|
94
|
+
.from("sessions")
|
|
95
|
+
.update({
|
|
96
|
+
ended_at: new Date().toISOString(),
|
|
97
|
+
summary,
|
|
98
|
+
})
|
|
99
|
+
.eq("id", sessionId);
|
|
100
|
+
|
|
101
|
+
if (error) throw new Error(`Failed to end session: ${error.message}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async addMessage(params: {
|
|
105
|
+
sessionId: string;
|
|
106
|
+
role: "user" | "assistant" | "system" | "tool";
|
|
107
|
+
content: string;
|
|
108
|
+
metadata?: Record<string, unknown>;
|
|
109
|
+
}): Promise<Message> {
|
|
110
|
+
const id = randomUUID();
|
|
111
|
+
const created_at = new Date().toISOString();
|
|
112
|
+
|
|
113
|
+
const { data, error } = await this.client
|
|
114
|
+
.from("messages")
|
|
115
|
+
.insert({
|
|
116
|
+
id,
|
|
117
|
+
session_id: params.sessionId,
|
|
118
|
+
role: params.role,
|
|
119
|
+
content: params.content,
|
|
120
|
+
created_at,
|
|
121
|
+
metadata: params.metadata ?? {},
|
|
122
|
+
})
|
|
123
|
+
.select()
|
|
124
|
+
.single();
|
|
125
|
+
|
|
126
|
+
if (error) throw new Error(`Failed to add message: ${error.message}`);
|
|
127
|
+
return data as Message;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Recall by date
|
|
131
|
+
async getMessagesByDate(date: string, keywords?: string): Promise<Array<Message & { session_started: string }>> {
|
|
132
|
+
let query = this.client
|
|
133
|
+
.from("messages")
|
|
134
|
+
.select(`
|
|
135
|
+
*,
|
|
136
|
+
sessions!inner (
|
|
137
|
+
agent_id,
|
|
138
|
+
started_at
|
|
139
|
+
)
|
|
140
|
+
`)
|
|
141
|
+
.eq("sessions.agent_id", this.agentId)
|
|
142
|
+
.gte("created_at", `${date}T00:00:00`)
|
|
143
|
+
.lt("created_at", `${date}T23:59:59`)
|
|
144
|
+
.order("created_at", { ascending: true });
|
|
145
|
+
|
|
146
|
+
if (keywords) {
|
|
147
|
+
query = query.ilike("content", `%${keywords}%`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const { data, error } = await query;
|
|
151
|
+
|
|
152
|
+
if (error) throw new Error(`Failed to get messages: ${error.message}`);
|
|
153
|
+
|
|
154
|
+
return (data || []).map((row: any) => ({
|
|
155
|
+
...row,
|
|
156
|
+
session_started: row.sessions?.started_at,
|
|
157
|
+
}));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Recall by date range
|
|
161
|
+
async getMessagesByDateRange(
|
|
162
|
+
startDate: string,
|
|
163
|
+
endDate: string,
|
|
164
|
+
keywords?: string
|
|
165
|
+
): Promise<Array<Message & { session_started: string }>> {
|
|
166
|
+
let query = this.client
|
|
167
|
+
.from("messages")
|
|
168
|
+
.select(`
|
|
169
|
+
*,
|
|
170
|
+
sessions!inner (
|
|
171
|
+
agent_id,
|
|
172
|
+
started_at
|
|
173
|
+
)
|
|
174
|
+
`)
|
|
175
|
+
.eq("sessions.agent_id", this.agentId)
|
|
176
|
+
.gte("created_at", `${startDate}T00:00:00`)
|
|
177
|
+
.lte("created_at", `${endDate}T23:59:59`)
|
|
178
|
+
.order("created_at", { ascending: true });
|
|
179
|
+
|
|
180
|
+
if (keywords) {
|
|
181
|
+
query = query.ilike("content", `%${keywords}%`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const { data, error } = await query;
|
|
185
|
+
|
|
186
|
+
if (error) throw new Error(`Failed to get messages: ${error.message}`);
|
|
187
|
+
|
|
188
|
+
return (data || []).map((row: any) => ({
|
|
189
|
+
...row,
|
|
190
|
+
session_started: row.sessions?.started_at,
|
|
191
|
+
}));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Search by keywords across all time
|
|
195
|
+
async searchMessages(keywords: string, limit = 50): Promise<Array<Message>> {
|
|
196
|
+
const { data, error } = await this.client
|
|
197
|
+
.from("messages")
|
|
198
|
+
.select(`
|
|
199
|
+
*,
|
|
200
|
+
sessions!inner (
|
|
201
|
+
agent_id
|
|
202
|
+
)
|
|
203
|
+
`)
|
|
204
|
+
.eq("sessions.agent_id", this.agentId)
|
|
205
|
+
.ilike("content", `%${keywords}%`)
|
|
206
|
+
.order("created_at", { ascending: false })
|
|
207
|
+
.limit(limit);
|
|
208
|
+
|
|
209
|
+
if (error) throw new Error(`Failed to search messages: ${error.message}`);
|
|
210
|
+
return data || [];
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Get recent sessions
|
|
214
|
+
async getRecentSessions(limit = 10): Promise<Session[]> {
|
|
215
|
+
const { data, error } = await this.client
|
|
216
|
+
.from("sessions")
|
|
217
|
+
.select("*")
|
|
218
|
+
.eq("agent_id", this.agentId)
|
|
219
|
+
.order("started_at", { ascending: false })
|
|
220
|
+
.limit(limit);
|
|
221
|
+
|
|
222
|
+
if (error) throw new Error(`Failed to get sessions: ${error.message}`);
|
|
223
|
+
return data as Session[];
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Get session with all messages
|
|
227
|
+
async getSessionWithMessages(sessionId: string): Promise<{ session: Session; messages: Message[] } | null> {
|
|
228
|
+
const { data: session, error: sessionError } = await this.client
|
|
229
|
+
.from("sessions")
|
|
230
|
+
.select("*")
|
|
231
|
+
.eq("id", sessionId)
|
|
232
|
+
.single();
|
|
233
|
+
|
|
234
|
+
if (sessionError || !session) return null;
|
|
235
|
+
|
|
236
|
+
const { data: messages, error: messagesError } = await this.client
|
|
237
|
+
.from("messages")
|
|
238
|
+
.select("*")
|
|
239
|
+
.eq("session_id", sessionId)
|
|
240
|
+
.order("created_at", { ascending: true });
|
|
241
|
+
|
|
242
|
+
if (messagesError) throw new Error(`Failed to get messages: ${messagesError.message}`);
|
|
243
|
+
|
|
244
|
+
return {
|
|
245
|
+
session: session as Session,
|
|
246
|
+
messages: messages as Message[],
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Stats
|
|
251
|
+
async getStats(): Promise<{ sessions: number; messages: number }> {
|
|
252
|
+
const { count: sessions } = await this.client
|
|
253
|
+
.from("sessions")
|
|
254
|
+
.select("*", { count: "exact", head: true })
|
|
255
|
+
.eq("agent_id", this.agentId);
|
|
256
|
+
|
|
257
|
+
const { count: messages } = await this.client
|
|
258
|
+
.from("messages")
|
|
259
|
+
.select("*, sessions!inner(agent_id)", { count: "exact", head: true })
|
|
260
|
+
.eq("sessions.agent_id", this.agentId);
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
sessions: sessions ?? 0,
|
|
264
|
+
messages: messages ?? 0,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ============================================================================
|
|
270
|
+
// Plugin Definition
|
|
271
|
+
// ============================================================================
|
|
272
|
+
|
|
273
|
+
const memoryPlugin = {
|
|
274
|
+
id: "memory-supabase",
|
|
275
|
+
name: "Memory (Supabase)",
|
|
276
|
+
description: "Complete conversation logging to Supabase with date-based recall",
|
|
277
|
+
kind: "memory" as const,
|
|
278
|
+
configSchema,
|
|
279
|
+
|
|
280
|
+
register(api: ClawdbotPluginApi) {
|
|
281
|
+
const cfg = configSchema.parse(api.pluginConfig);
|
|
282
|
+
const db = new ConversationDB(cfg.supabase.url, cfg.supabase.anonKey, cfg.agentId);
|
|
283
|
+
|
|
284
|
+
let currentSessionId: string | null = null;
|
|
285
|
+
|
|
286
|
+
api.logger.info(
|
|
287
|
+
`memory-supabase: plugin registered (url: ${cfg.supabase.url}, agent: ${cfg.agentId})`
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
// ========================================================================
|
|
291
|
+
// Tools
|
|
292
|
+
// ========================================================================
|
|
293
|
+
|
|
294
|
+
api.registerTool(
|
|
295
|
+
{
|
|
296
|
+
name: "recall_conversation",
|
|
297
|
+
label: "Recall Conversation",
|
|
298
|
+
description:
|
|
299
|
+
"Recall past conversations by date. Use when user references something from a specific date or you need historical context.",
|
|
300
|
+
parameters: Type.Object({
|
|
301
|
+
date: Type.String({
|
|
302
|
+
description: "Date in YYYY-MM-DD format (e.g., '2026-01-15')"
|
|
303
|
+
}),
|
|
304
|
+
keywords: Type.Optional(
|
|
305
|
+
Type.String({ description: "Optional keywords to filter by" })
|
|
306
|
+
),
|
|
307
|
+
}),
|
|
308
|
+
async execute(_toolCallId, params) {
|
|
309
|
+
const { date, keywords } = params as { date: string; keywords?: string };
|
|
310
|
+
|
|
311
|
+
const messages = await db.getMessagesByDate(date, keywords);
|
|
312
|
+
|
|
313
|
+
if (messages.length === 0) {
|
|
314
|
+
return {
|
|
315
|
+
content: [{
|
|
316
|
+
type: "text",
|
|
317
|
+
text: `No conversations found for ${date}${keywords ? ` with keywords "${keywords}"` : ""}.`
|
|
318
|
+
}],
|
|
319
|
+
details: { count: 0, date },
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const formatted = messages.map((m) => {
|
|
324
|
+
const time = new Date(m.created_at).toLocaleTimeString("en-US", {
|
|
325
|
+
hour: "2-digit",
|
|
326
|
+
minute: "2-digit",
|
|
327
|
+
});
|
|
328
|
+
return `[${time}] ${m.role.toUpperCase()}: ${m.content}`;
|
|
329
|
+
}).join("\n\n");
|
|
330
|
+
|
|
331
|
+
return {
|
|
332
|
+
content: [{
|
|
333
|
+
type: "text",
|
|
334
|
+
text: `Found ${messages.length} messages from ${date}:\n\n${formatted}`
|
|
335
|
+
}],
|
|
336
|
+
details: {
|
|
337
|
+
count: messages.length,
|
|
338
|
+
date,
|
|
339
|
+
messages: messages.map(m => ({
|
|
340
|
+
role: m.role,
|
|
341
|
+
content: m.content.slice(0, 200),
|
|
342
|
+
time: m.created_at,
|
|
343
|
+
})),
|
|
344
|
+
},
|
|
345
|
+
};
|
|
346
|
+
},
|
|
347
|
+
},
|
|
348
|
+
{ name: "recall_conversation" }
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
api.registerTool(
|
|
352
|
+
{
|
|
353
|
+
name: "recall_date_range",
|
|
354
|
+
label: "Recall Date Range",
|
|
355
|
+
description:
|
|
356
|
+
"Recall conversations across a date range. Use for 'last week', 'this month', etc.",
|
|
357
|
+
parameters: Type.Object({
|
|
358
|
+
startDate: Type.String({
|
|
359
|
+
description: "Start date in YYYY-MM-DD format"
|
|
360
|
+
}),
|
|
361
|
+
endDate: Type.String({
|
|
362
|
+
description: "End date in YYYY-MM-DD format"
|
|
363
|
+
}),
|
|
364
|
+
keywords: Type.Optional(
|
|
365
|
+
Type.String({ description: "Optional keywords to filter by" })
|
|
366
|
+
),
|
|
367
|
+
}),
|
|
368
|
+
async execute(_toolCallId, params) {
|
|
369
|
+
const { startDate, endDate, keywords } = params as {
|
|
370
|
+
startDate: string;
|
|
371
|
+
endDate: string;
|
|
372
|
+
keywords?: string;
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
const messages = await db.getMessagesByDateRange(startDate, endDate, keywords);
|
|
376
|
+
|
|
377
|
+
if (messages.length === 0) {
|
|
378
|
+
return {
|
|
379
|
+
content: [{
|
|
380
|
+
type: "text",
|
|
381
|
+
text: `No conversations found between ${startDate} and ${endDate}${keywords ? ` with keywords "${keywords}"` : ""}.`
|
|
382
|
+
}],
|
|
383
|
+
details: { count: 0, startDate, endDate },
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Group by date
|
|
388
|
+
const byDate: Record<string, typeof messages> = {};
|
|
389
|
+
for (const m of messages) {
|
|
390
|
+
const date = m.created_at.split("T")[0];
|
|
391
|
+
if (!byDate[date]) byDate[date] = [];
|
|
392
|
+
byDate[date].push(m);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const formatted = Object.entries(byDate)
|
|
396
|
+
.map(([date, msgs]) => {
|
|
397
|
+
const lines = msgs.map((m) => {
|
|
398
|
+
const time = new Date(m.created_at).toLocaleTimeString("en-US", {
|
|
399
|
+
hour: "2-digit",
|
|
400
|
+
minute: "2-digit",
|
|
401
|
+
});
|
|
402
|
+
return ` [${time}] ${m.role.toUpperCase()}: ${m.content.slice(0, 150)}${m.content.length > 150 ? "..." : ""}`;
|
|
403
|
+
}).join("\n");
|
|
404
|
+
return `📅 ${date} (${msgs.length} messages):\n${lines}`;
|
|
405
|
+
})
|
|
406
|
+
.join("\n\n");
|
|
407
|
+
|
|
408
|
+
return {
|
|
409
|
+
content: [{
|
|
410
|
+
type: "text",
|
|
411
|
+
text: `Found ${messages.length} messages across ${Object.keys(byDate).length} days:\n\n${formatted}`
|
|
412
|
+
}],
|
|
413
|
+
details: {
|
|
414
|
+
count: messages.length,
|
|
415
|
+
days: Object.keys(byDate).length,
|
|
416
|
+
startDate,
|
|
417
|
+
endDate,
|
|
418
|
+
},
|
|
419
|
+
};
|
|
420
|
+
},
|
|
421
|
+
},
|
|
422
|
+
{ name: "recall_date_range" }
|
|
423
|
+
);
|
|
424
|
+
|
|
425
|
+
api.registerTool(
|
|
426
|
+
{
|
|
427
|
+
name: "search_history",
|
|
428
|
+
label: "Search History",
|
|
429
|
+
description:
|
|
430
|
+
"Search all conversation history by keywords. Use when user asks about something but doesn't know the date.",
|
|
431
|
+
parameters: Type.Object({
|
|
432
|
+
keywords: Type.String({
|
|
433
|
+
description: "Keywords to search for"
|
|
434
|
+
}),
|
|
435
|
+
limit: Type.Optional(
|
|
436
|
+
Type.Number({ description: "Max results (default: 20)" })
|
|
437
|
+
),
|
|
438
|
+
}),
|
|
439
|
+
async execute(_toolCallId, params) {
|
|
440
|
+
const { keywords, limit = 20 } = params as { keywords: string; limit?: number };
|
|
441
|
+
|
|
442
|
+
const messages = await db.searchMessages(keywords, limit);
|
|
443
|
+
|
|
444
|
+
if (messages.length === 0) {
|
|
445
|
+
return {
|
|
446
|
+
content: [{
|
|
447
|
+
type: "text",
|
|
448
|
+
text: `No messages found containing "${keywords}".`
|
|
449
|
+
}],
|
|
450
|
+
details: { count: 0, keywords },
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const formatted = messages.map((m) => {
|
|
455
|
+
const date = new Date(m.created_at).toLocaleDateString("en-US", {
|
|
456
|
+
month: "short",
|
|
457
|
+
day: "numeric",
|
|
458
|
+
year: "numeric",
|
|
459
|
+
});
|
|
460
|
+
const time = new Date(m.created_at).toLocaleTimeString("en-US", {
|
|
461
|
+
hour: "2-digit",
|
|
462
|
+
minute: "2-digit",
|
|
463
|
+
});
|
|
464
|
+
return `[${date} ${time}] ${m.role.toUpperCase()}: ${m.content.slice(0, 200)}${m.content.length > 200 ? "..." : ""}`;
|
|
465
|
+
}).join("\n\n");
|
|
466
|
+
|
|
467
|
+
return {
|
|
468
|
+
content: [{
|
|
469
|
+
type: "text",
|
|
470
|
+
text: `Found ${messages.length} messages matching "${keywords}":\n\n${formatted}`
|
|
471
|
+
}],
|
|
472
|
+
details: {
|
|
473
|
+
count: messages.length,
|
|
474
|
+
keywords,
|
|
475
|
+
},
|
|
476
|
+
};
|
|
477
|
+
},
|
|
478
|
+
},
|
|
479
|
+
{ name: "search_history" }
|
|
480
|
+
);
|
|
481
|
+
|
|
482
|
+
api.registerTool(
|
|
483
|
+
{
|
|
484
|
+
name: "memory_stats",
|
|
485
|
+
label: "Memory Stats",
|
|
486
|
+
description: "Show conversation logging statistics.",
|
|
487
|
+
parameters: Type.Object({}),
|
|
488
|
+
async execute() {
|
|
489
|
+
const stats = await db.getStats();
|
|
490
|
+
const sessions = await db.getRecentSessions(5);
|
|
491
|
+
|
|
492
|
+
const recentList = sessions.map((s) => {
|
|
493
|
+
const date = new Date(s.started_at).toLocaleDateString("en-US", {
|
|
494
|
+
month: "short",
|
|
495
|
+
day: "numeric",
|
|
496
|
+
});
|
|
497
|
+
return `- ${date}: ${s.channel || "unknown"} session`;
|
|
498
|
+
}).join("\n");
|
|
499
|
+
|
|
500
|
+
return {
|
|
501
|
+
content: [{
|
|
502
|
+
type: "text",
|
|
503
|
+
text: `📊 Memory Stats:\n- Total sessions: ${stats.sessions}\n- Total messages: ${stats.messages}\n\nRecent sessions:\n${recentList}`
|
|
504
|
+
}],
|
|
505
|
+
details: stats,
|
|
506
|
+
};
|
|
507
|
+
},
|
|
508
|
+
},
|
|
509
|
+
{ name: "memory_stats" }
|
|
510
|
+
);
|
|
511
|
+
|
|
512
|
+
// ========================================================================
|
|
513
|
+
// CLI Commands
|
|
514
|
+
// ========================================================================
|
|
515
|
+
|
|
516
|
+
api.registerCli(
|
|
517
|
+
({ program }) => {
|
|
518
|
+
const memory = program
|
|
519
|
+
.command("supamem")
|
|
520
|
+
.description("Supabase memory commands");
|
|
521
|
+
|
|
522
|
+
memory
|
|
523
|
+
.command("stats")
|
|
524
|
+
.description("Show memory statistics")
|
|
525
|
+
.action(async () => {
|
|
526
|
+
const stats = await db.getStats();
|
|
527
|
+
console.log(`Sessions: ${stats.sessions}`);
|
|
528
|
+
console.log(`Messages: ${stats.messages}`);
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
memory
|
|
532
|
+
.command("recall")
|
|
533
|
+
.description("Recall messages from a date")
|
|
534
|
+
.argument("<date>", "Date (YYYY-MM-DD)")
|
|
535
|
+
.option("-k, --keywords <keywords>", "Filter by keywords")
|
|
536
|
+
.action(async (date, opts) => {
|
|
537
|
+
const messages = await db.getMessagesByDate(date, opts.keywords);
|
|
538
|
+
for (const m of messages) {
|
|
539
|
+
const time = new Date(m.created_at).toLocaleTimeString();
|
|
540
|
+
console.log(`[${time}] ${m.role}: ${m.content.slice(0, 100)}...`);
|
|
541
|
+
}
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
memory
|
|
545
|
+
.command("search")
|
|
546
|
+
.description("Search messages by keywords")
|
|
547
|
+
.argument("<keywords>", "Keywords to search")
|
|
548
|
+
.option("-l, --limit <n>", "Max results", "20")
|
|
549
|
+
.action(async (keywords, opts) => {
|
|
550
|
+
const messages = await db.searchMessages(keywords, parseInt(opts.limit));
|
|
551
|
+
for (const m of messages) {
|
|
552
|
+
const date = new Date(m.created_at).toLocaleString();
|
|
553
|
+
console.log(`[${date}] ${m.role}: ${m.content.slice(0, 100)}...`);
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
},
|
|
557
|
+
{ commands: ["supamem"] }
|
|
558
|
+
);
|
|
559
|
+
|
|
560
|
+
// ========================================================================
|
|
561
|
+
// Lifecycle Hooks - LOG EVERYTHING
|
|
562
|
+
// ========================================================================
|
|
563
|
+
|
|
564
|
+
// Start session when conversation begins
|
|
565
|
+
api.on("before_agent_start", async (event) => {
|
|
566
|
+
try {
|
|
567
|
+
const session = await db.createSession({
|
|
568
|
+
userId: (event as any).userId,
|
|
569
|
+
channel: (event as any).channel,
|
|
570
|
+
metadata: {
|
|
571
|
+
prompt: (event as any).prompt?.slice(0, 500),
|
|
572
|
+
startedAt: new Date().toISOString(),
|
|
573
|
+
},
|
|
574
|
+
});
|
|
575
|
+
currentSessionId = session.id;
|
|
576
|
+
api.logger.info?.(`memory-supabase: started session ${session.id}`);
|
|
577
|
+
} catch (err) {
|
|
578
|
+
api.logger.warn?.(`memory-supabase: session start failed: ${String(err)}`);
|
|
579
|
+
}
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
// Log all messages after agent ends
|
|
583
|
+
api.on("agent_end", async (event) => {
|
|
584
|
+
if (!currentSessionId) {
|
|
585
|
+
api.logger.warn?.("memory-supabase: no active session to log messages");
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
try {
|
|
590
|
+
const messages = (event as any).messages || [];
|
|
591
|
+
let logged = 0;
|
|
592
|
+
|
|
593
|
+
for (const msg of messages) {
|
|
594
|
+
if (!msg || typeof msg !== "object") continue;
|
|
595
|
+
|
|
596
|
+
const role = (msg as any).role as string;
|
|
597
|
+
if (role !== "user" && role !== "assistant") continue;
|
|
598
|
+
|
|
599
|
+
const content = (msg as any).content;
|
|
600
|
+
let text = "";
|
|
601
|
+
|
|
602
|
+
if (typeof content === "string") {
|
|
603
|
+
text = content;
|
|
604
|
+
} else if (Array.isArray(content)) {
|
|
605
|
+
for (const block of content) {
|
|
606
|
+
if (block?.type === "text" && typeof block.text === "string") {
|
|
607
|
+
text += block.text;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
if (text && text.length > 0) {
|
|
613
|
+
await db.addMessage({
|
|
614
|
+
sessionId: currentSessionId,
|
|
615
|
+
role: role as "user" | "assistant",
|
|
616
|
+
content: text,
|
|
617
|
+
});
|
|
618
|
+
logged++;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// End session
|
|
623
|
+
await db.endSession(currentSessionId);
|
|
624
|
+
api.logger.info?.(`memory-supabase: logged ${logged} messages, session ended`);
|
|
625
|
+
currentSessionId = null;
|
|
626
|
+
|
|
627
|
+
} catch (err) {
|
|
628
|
+
api.logger.warn?.(`memory-supabase: logging failed: ${String(err)}`);
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
// ========================================================================
|
|
633
|
+
// Service
|
|
634
|
+
// ========================================================================
|
|
635
|
+
|
|
636
|
+
api.registerService({
|
|
637
|
+
id: "memory-supabase",
|
|
638
|
+
start: () => {
|
|
639
|
+
api.logger.info(
|
|
640
|
+
`memory-supabase: initialized (logs every message to Supabase)`
|
|
641
|
+
);
|
|
642
|
+
},
|
|
643
|
+
stop: () => {
|
|
644
|
+
api.logger.info("memory-supabase: stopped");
|
|
645
|
+
},
|
|
646
|
+
});
|
|
647
|
+
},
|
|
648
|
+
};
|
|
649
|
+
|
|
650
|
+
export default memoryPlugin;
|