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.
@@ -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
- // Send image if generated
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
+ }
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * 🦑 Image Generation Tool
3
- * Supports: OpenAI DALL-E, Google Gemini/Imagen
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
- // Gemini Imagen 3 via generateImages endpoint
41
- const res = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/imagen-3.0-generate-002:predict?key=${apiKey}`, {
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
- instances: [{ prompt }],
46
- parameters: { sampleCount: 1, aspectRatio: '1:1' },
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.predictions?.[0]?.bytesBase64Encoded) {
57
- return { base64: data.predictions[0].bytesBase64Encoded, mimeType: 'image/png' };
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
  }
@@ -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
- case 'draw':
90
- case 'image': {
91
- try {
92
- const { ImageGenTool } = await import('./image-gen.js');
93
- const gen = new ImageGenTool(this.config);
94
- const result = await gen.generate(args);
95
- if (result.url) {
96
- return { toolUsed: true, toolName: 'image', toolResult: result.url, imageUrl: result.url };
97
- } else if (result.base64) {
98
- return { toolUsed: true, toolName: 'image', toolResult: '[Image generated]', imageBase64: result.base64, mimeType: result.mimeType };
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
- } catch (err) {
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "squidclaw",
3
- "version": "0.8.2",
3
+ "version": "0.9.0",
4
4
  "description": "\ud83e\udd91 AI agent platform \u2014 human-like agents for WhatsApp, Telegram & more",
5
5
  "main": "lib/engine.js",
6
6
  "bin": {