squidclaw 0.8.2 → 0.9.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/lib/core/agent-tools-mixin.js +11 -0
- package/lib/engine.js +13 -1
- package/lib/features/reminders.js +144 -0
- package/lib/tools/image-gen.js +11 -32
- package/lib/tools/router.js +38 -14
- package/package.json +1 -1
|
@@ -53,6 +53,17 @@ export function addToolSupport(agent, toolRouter, knowledgeBase) {
|
|
|
53
53
|
const fullResponse = result.messages.join('\n');
|
|
54
54
|
const toolResult = await toolRouter.processResponse(fullResponse, agent.id);
|
|
55
55
|
|
|
56
|
+
if (toolResult.toolUsed && toolResult.toolName === 'remind' && toolResult.reminderTime) {
|
|
57
|
+
// Reminder requested — store it via engine's reminder manager
|
|
58
|
+
result._reminder = {
|
|
59
|
+
time: toolResult.reminderTime,
|
|
60
|
+
message: toolResult.reminderMessage,
|
|
61
|
+
contactId,
|
|
62
|
+
};
|
|
63
|
+
result.messages = ['Done! I will remind you at that time ⏰'];
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
|
|
56
67
|
if (toolResult.toolUsed && (toolResult.imageBase64 || toolResult.imageUrl)) {
|
|
57
68
|
// Image generated — pass through directly
|
|
58
69
|
result.image = { base64: toolResult.imageBase64, url: toolResult.imageUrl, mimeType: toolResult.mimeType };
|
package/lib/engine.js
CHANGED
|
@@ -235,13 +235,25 @@ export class SquidclawEngine {
|
|
|
235
235
|
}
|
|
236
236
|
|
|
237
237
|
if (result.messages && result.messages.length > 0) {
|
|
238
|
+
// Handle reminder if requested
|
|
239
|
+
if (result._reminder && this.reminders) {
|
|
240
|
+
const { time, message: remMsg } = result._reminder;
|
|
241
|
+
this.reminders.add(agentId, contactId, remMsg, time, 'telegram', metadata);
|
|
242
|
+
}
|
|
243
|
+
|
|
238
244
|
// Send image if generated
|
|
239
245
|
if (result.image) {
|
|
240
246
|
const photoData = result.image.url ? { url: result.image.url } : { base64: result.image.base64 };
|
|
241
247
|
const caption = result.messages?.[0] || '';
|
|
242
248
|
await this.telegramManager.sendPhoto(agentId, contactId, photoData, caption, metadata);
|
|
243
249
|
} else {
|
|
244
|
-
//
|
|
250
|
+
// Handle reminder if requested
|
|
251
|
+
if (result._reminder && this.reminders) {
|
|
252
|
+
const { time, message: remMsg } = result._reminder;
|
|
253
|
+
this.reminders.add(agentId, contactId, remMsg, time, 'telegram', metadata);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Send image if generated
|
|
245
257
|
if (result.image) {
|
|
246
258
|
await this.telegramManager.sendPhoto(agentId, contactId, result.image, result.messages?.[0] || '', metadata);
|
|
247
259
|
} else {
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 🦑 Reminders & Scheduled Messages
|
|
3
|
+
* Agent can set reminders and proactively message users at scheduled times
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { logger } from '../core/logger.js';
|
|
7
|
+
|
|
8
|
+
export class ReminderManager {
|
|
9
|
+
constructor(storage) {
|
|
10
|
+
this.storage = storage;
|
|
11
|
+
this.timers = new Map(); // id -> timeout
|
|
12
|
+
this.onFire = null; // callback(agentId, contactId, message, platform, metadata)
|
|
13
|
+
this._initDb();
|
|
14
|
+
this._loadPending();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
_initDb() {
|
|
18
|
+
try {
|
|
19
|
+
this.storage.db.exec(`
|
|
20
|
+
CREATE TABLE IF NOT EXISTS reminders (
|
|
21
|
+
id TEXT PRIMARY KEY,
|
|
22
|
+
agent_id TEXT NOT NULL,
|
|
23
|
+
contact_id TEXT NOT NULL,
|
|
24
|
+
message TEXT NOT NULL,
|
|
25
|
+
fire_at TEXT NOT NULL,
|
|
26
|
+
platform TEXT DEFAULT 'telegram',
|
|
27
|
+
metadata TEXT DEFAULT '{}',
|
|
28
|
+
fired INTEGER DEFAULT 0,
|
|
29
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
30
|
+
)
|
|
31
|
+
`);
|
|
32
|
+
} catch (err) {
|
|
33
|
+
logger.error('reminders', 'Failed to init DB: ' + err.message);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
_loadPending() {
|
|
38
|
+
try {
|
|
39
|
+
const rows = this.storage.db.prepare(
|
|
40
|
+
"SELECT * FROM reminders WHERE fired = 0 AND fire_at > datetime('now')"
|
|
41
|
+
).all();
|
|
42
|
+
|
|
43
|
+
for (const row of rows) {
|
|
44
|
+
this._schedule(row);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (rows.length > 0) {
|
|
48
|
+
logger.info('reminders', `Loaded ${rows.length} pending reminders`);
|
|
49
|
+
}
|
|
50
|
+
} catch (err) {
|
|
51
|
+
logger.error('reminders', 'Failed to load pending: ' + err.message);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
_schedule(reminder) {
|
|
56
|
+
const fireAt = new Date(reminder.fire_at + 'Z');
|
|
57
|
+
const now = Date.now();
|
|
58
|
+
const delay = fireAt.getTime() - now;
|
|
59
|
+
|
|
60
|
+
if (delay <= 0) {
|
|
61
|
+
// Already past — fire immediately
|
|
62
|
+
this._fire(reminder);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const timer = setTimeout(() => this._fire(reminder), delay);
|
|
67
|
+
this.timers.set(reminder.id, timer);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async _fire(reminder) {
|
|
71
|
+
logger.info('reminders', `Firing reminder ${reminder.id} for ${reminder.contact_id}`);
|
|
72
|
+
|
|
73
|
+
// Mark as fired
|
|
74
|
+
try {
|
|
75
|
+
this.storage.db.prepare('UPDATE reminders SET fired = 1 WHERE id = ?').run(reminder.id);
|
|
76
|
+
} catch {}
|
|
77
|
+
|
|
78
|
+
this.timers.delete(reminder.id);
|
|
79
|
+
|
|
80
|
+
// Call the callback
|
|
81
|
+
if (this.onFire) {
|
|
82
|
+
const metadata = JSON.parse(reminder.metadata || '{}');
|
|
83
|
+
metadata.platform = reminder.platform;
|
|
84
|
+
await this.onFire(reminder.agent_id, reminder.contact_id, reminder.message, reminder.platform, metadata);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Add a reminder
|
|
90
|
+
* @param {string} agentId
|
|
91
|
+
* @param {string} contactId
|
|
92
|
+
* @param {string} message - What to say when reminder fires
|
|
93
|
+
* @param {Date|string} fireAt - When to fire (UTC)
|
|
94
|
+
* @param {string} platform - telegram/whatsapp
|
|
95
|
+
* @param {object} metadata - chat metadata for sending
|
|
96
|
+
*/
|
|
97
|
+
add(agentId, contactId, message, fireAt, platform = 'telegram', metadata = {}) {
|
|
98
|
+
const id = 'rem_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
|
|
99
|
+
const fireAtStr = typeof fireAt === 'string' ? fireAt : fireAt.toISOString().replace('Z', '');
|
|
100
|
+
|
|
101
|
+
this.storage.db.prepare(
|
|
102
|
+
'INSERT INTO reminders (id, agent_id, contact_id, message, fire_at, platform, metadata) VALUES (?, ?, ?, ?, ?, ?, ?)'
|
|
103
|
+
).run(id, agentId, contactId, message, fireAtStr, platform, JSON.stringify(metadata));
|
|
104
|
+
|
|
105
|
+
this._schedule({ id, agent_id: agentId, contact_id: contactId, message, fire_at: fireAtStr, platform, metadata: JSON.stringify(metadata) });
|
|
106
|
+
|
|
107
|
+
logger.info('reminders', `Reminder ${id} set for ${fireAtStr}`);
|
|
108
|
+
return id;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* List pending reminders for a contact
|
|
113
|
+
*/
|
|
114
|
+
list(agentId, contactId) {
|
|
115
|
+
return this.storage.db.prepare(
|
|
116
|
+
"SELECT * FROM reminders WHERE agent_id = ? AND contact_id = ? AND fired = 0 AND fire_at > datetime('now') ORDER BY fire_at"
|
|
117
|
+
).all(agentId, contactId);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Cancel a reminder
|
|
122
|
+
*/
|
|
123
|
+
cancel(id) {
|
|
124
|
+
const timer = this.timers.get(id);
|
|
125
|
+
if (timer) clearTimeout(timer);
|
|
126
|
+
this.timers.delete(id);
|
|
127
|
+
this.storage.db.prepare('UPDATE reminders SET fired = 1 WHERE id = ?').run(id);
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Cancel all reminders for a contact
|
|
133
|
+
*/
|
|
134
|
+
cancelAll(agentId, contactId) {
|
|
135
|
+
const pending = this.list(agentId, contactId);
|
|
136
|
+
for (const r of pending) this.cancel(r.id);
|
|
137
|
+
return pending.length;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
destroy() {
|
|
141
|
+
for (const timer of this.timers.values()) clearTimeout(timer);
|
|
142
|
+
this.timers.clear();
|
|
143
|
+
}
|
|
144
|
+
}
|
package/lib/tools/image-gen.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 🦑 Image Generation Tool
|
|
3
|
-
* Supports: OpenAI DALL-E, Google Gemini
|
|
3
|
+
* Supports: OpenAI DALL-E 3, Google Gemini Flash Image
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { logger } from '../core/logger.js';
|
|
@@ -13,13 +13,14 @@ export class ImageGenTool {
|
|
|
13
13
|
async generate(prompt, provider) {
|
|
14
14
|
const providers = this.config.ai?.providers || {};
|
|
15
15
|
|
|
16
|
-
// Auto-select provider
|
|
17
16
|
if (!provider) {
|
|
18
17
|
if (providers.openai?.key) provider = 'openai';
|
|
19
18
|
else if (providers.google?.key) provider = 'google';
|
|
20
19
|
else throw new Error('No image generation API key configured');
|
|
21
20
|
}
|
|
22
21
|
|
|
22
|
+
logger.info('image-gen', `Generating with ${provider}: ${prompt.slice(0, 80)}...`);
|
|
23
|
+
|
|
23
24
|
if (provider === 'openai') return this.generateOpenAI(prompt, providers.openai.key);
|
|
24
25
|
if (provider === 'google') return this.generateGemini(prompt, providers.google.key);
|
|
25
26
|
throw new Error('Unsupported provider: ' + provider);
|
|
@@ -37,52 +38,30 @@ export class ImageGenTool {
|
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
async generateGemini(prompt, apiKey) {
|
|
40
|
-
//
|
|
41
|
-
const res = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/
|
|
41
|
+
// Try gemini-2.5-flash-image (native image generation)
|
|
42
|
+
const res = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-image:generateContent?key=${apiKey}`, {
|
|
42
43
|
method: 'POST',
|
|
43
44
|
headers: { 'Content-Type': 'application/json' },
|
|
44
45
|
body: JSON.stringify({
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
contents: [{ parts: [{ text: `Generate an image: ${prompt}` }] }],
|
|
47
|
+
generationConfig: { responseModalities: ['TEXT', 'IMAGE'] },
|
|
47
48
|
}),
|
|
48
49
|
});
|
|
49
50
|
const data = await res.json();
|
|
50
|
-
|
|
51
|
-
if (data.error) {
|
|
52
|
-
// Fallback: use Gemini 2.0 Flash native image generation
|
|
53
|
-
return this.generateGeminiFlash(prompt, apiKey);
|
|
54
|
-
}
|
|
55
51
|
|
|
56
|
-
if (data.
|
|
57
|
-
|
|
52
|
+
if (data.error) {
|
|
53
|
+
logger.error('image-gen', `Gemini error: ${data.error.message}`);
|
|
54
|
+
throw new Error(data.error.message);
|
|
58
55
|
}
|
|
59
56
|
|
|
60
|
-
// Fallback to Gemini Flash
|
|
61
|
-
return this.generateGeminiFlash(prompt, apiKey);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
async generateGeminiFlash(prompt, apiKey) {
|
|
65
|
-
const res = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-exp:generateContent?key=${apiKey}`, {
|
|
66
|
-
method: 'POST',
|
|
67
|
-
headers: { 'Content-Type': 'application/json' },
|
|
68
|
-
body: JSON.stringify({
|
|
69
|
-
contents: [{ parts: [{ text: `Generate an image: ${prompt}` }] }],
|
|
70
|
-
generationConfig: { responseModalities: ['TEXT', 'IMAGE'] },
|
|
71
|
-
}),
|
|
72
|
-
});
|
|
73
|
-
const data = await res.json();
|
|
74
|
-
|
|
75
|
-
if (data.error) throw new Error(data.error.message);
|
|
76
|
-
|
|
77
|
-
// Find image part in response
|
|
78
57
|
const parts = data.candidates?.[0]?.content?.parts || [];
|
|
79
58
|
for (const part of parts) {
|
|
80
59
|
if (part.inlineData) {
|
|
60
|
+
logger.info('image-gen', `Image generated! ${part.inlineData.mimeType}, ${part.inlineData.data?.length} bytes`);
|
|
81
61
|
return { base64: part.inlineData.data, mimeType: part.inlineData.mimeType || 'image/png' };
|
|
82
62
|
}
|
|
83
63
|
}
|
|
84
64
|
|
|
85
|
-
// Text-only response
|
|
86
65
|
const text = parts.find(p => p.text)?.text;
|
|
87
66
|
throw new Error(text || 'Gemini could not generate an image');
|
|
88
67
|
}
|
package/lib/tools/router.js
CHANGED
|
@@ -59,6 +59,12 @@ export class ToolRouter {
|
|
|
59
59
|
'Send an email.');
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
tools.push('', '### Set Reminder',
|
|
63
|
+
'---TOOL:remind:YYYY-MM-DDTHH:MM|Your reminder message---',
|
|
64
|
+
'Set a reminder to message the user at a specific time. Time must be in UTC.',
|
|
65
|
+
'Example: ---TOOL:remind:2026-03-03T08:30|Wake up! Time to start the day!---',
|
|
66
|
+
'The user will receive a proactive message at that time even if they are not chatting.');
|
|
67
|
+
|
|
62
68
|
tools.push('', '**Important:** Use tools when needed. The tool result will be injected into the conversation automatically. Only use one tool per response.');
|
|
63
69
|
|
|
64
70
|
return tools.join('\n');
|
|
@@ -85,23 +91,41 @@ export class ToolRouter {
|
|
|
85
91
|
toolResult = results.map(r => `• ${r.title}\n ${r.snippet}\n ${r.url}`).join('\n\n');
|
|
86
92
|
break;
|
|
87
93
|
|
|
94
|
+
case 'remind':
|
|
95
|
+
case 'reminder': {
|
|
96
|
+
try {
|
|
97
|
+
const pipeIdx = toolArg.indexOf('|');
|
|
98
|
+
if (pipeIdx === -1) {
|
|
99
|
+
toolResult = 'Invalid reminder format. Use: YYYY-MM-DDTHH:MM|message';
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
const timeStr = toolArg.slice(0, pipeIdx).trim();
|
|
103
|
+
const msg = toolArg.slice(pipeIdx + 1).trim();
|
|
104
|
+
// Store reminder request in result for engine to pick up
|
|
105
|
+
return { toolUsed: true, toolName: 'remind', reminderTime: timeStr, reminderMessage: msg, cleanResponse };
|
|
106
|
+
} catch (err) {
|
|
107
|
+
toolResult = 'Failed to set reminder: ' + err.message;
|
|
108
|
+
}
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
88
111
|
case 'imagine':
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
112
|
+
case 'draw':
|
|
113
|
+
case 'image': {
|
|
114
|
+
try {
|
|
115
|
+
const { ImageGenTool } = await import('./image-gen.js');
|
|
116
|
+
const gen = new ImageGenTool(this.config);
|
|
117
|
+
const imgResult = await gen.generate(toolArg);
|
|
118
|
+
if (imgResult.url) {
|
|
119
|
+
return { toolUsed: true, toolName: 'image', toolResult: imgResult.url, imageUrl: imgResult.url, cleanResponse };
|
|
120
|
+
} else if (imgResult.base64) {
|
|
121
|
+
return { toolUsed: true, toolName: 'image', toolResult: '[Image generated]', imageBase64: imgResult.base64, mimeType: imgResult.mimeType, cleanResponse };
|
|
122
|
+
}
|
|
123
|
+
} catch (err) {
|
|
124
|
+
toolResult = 'Image generation failed: ' + err.message;
|
|
99
125
|
}
|
|
100
|
-
|
|
101
|
-
return { toolUsed: true, toolName: 'image', toolResult: 'Image generation failed: ' + err.message };
|
|
126
|
+
break;
|
|
102
127
|
}
|
|
103
|
-
|
|
104
|
-
case 'read':
|
|
128
|
+
case 'read':
|
|
105
129
|
const page = await this.browser.readPage(toolArg, 3000);
|
|
106
130
|
toolResult = `Title: ${page.title}\n\n${page.content}`;
|
|
107
131
|
break;
|