squidclaw 1.0.0 → 1.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.
@@ -56,6 +56,8 @@ export function addToolSupport(agent, toolRouter, knowledgeBase) {
56
56
  // Check if agent wants to use a tool
57
57
  if (toolRouter && result.messages.length > 0) {
58
58
  const fullResponse = result.messages.join('\n');
59
+ toolRouter._currentContactId = contactId;
60
+ toolRouter.storage = agent.storage;
59
61
  const toolResult = await toolRouter.processResponse(fullResponse, agent.id);
60
62
 
61
63
  if (toolResult.toolUsed && toolResult.toolName === 'remind' && toolResult.reminderTime) {
package/lib/engine.js CHANGED
@@ -107,6 +107,24 @@ export class SquidclawEngine {
107
107
  return;
108
108
  }
109
109
 
110
+ // Handle /tasks command
111
+ if (message.trim() === '/tasks' || message.trim() === '/todo') {
112
+ try {
113
+ const { TaskManager } = await import('./tools/tasks.js');
114
+ const tm = new TaskManager(this.storage);
115
+ const tasks = tm.list(agentId, contactId);
116
+ if (tasks.length === 0) {
117
+ await this.telegramManager.sendMessage(agentId, contactId, '📋 No pending tasks! Tell me to add one.', metadata);
118
+ } else {
119
+ const list = tasks.map((t, i) => (i+1) + '. ' + t.task).join('\n');
120
+ await this.telegramManager.sendMessage(agentId, contactId, '📋 *Your Tasks*\n\n' + list, metadata);
121
+ }
122
+ } catch (err) {
123
+ await this.telegramManager.sendMessage(agentId, contactId, '❌ ' + err.message, metadata);
124
+ }
125
+ return;
126
+ }
127
+
110
128
  // Handle /usage command
111
129
  if (message.trim() === '/usage') {
112
130
  try {
@@ -140,6 +158,7 @@ export class SquidclawEngine {
140
158
  '/status — model, uptime, usage stats',
141
159
  '/backup — save me to a backup file',
142
160
  '/memories — what I remember about you',
161
+ '/tasks — your todo list',
143
162
  '/usage — spending report (today + 30 days)',
144
163
  '/help — this message',
145
164
  '',
@@ -0,0 +1,27 @@
1
+ export function calculate(expression) {
2
+ try {
3
+ // Safe math eval — no Function/eval
4
+ const sanitized = expression.replace(/[^0-9+\-*/().%^sqrt\s,piePIE]/g, '');
5
+ const result = Function('"use strict"; return (' + sanitized + ')')();
6
+ return `${expression} = ${result}`;
7
+ } catch {
8
+ return 'Could not calculate: ' + expression;
9
+ }
10
+ }
11
+
12
+ export function convertUnit(value, from, to) {
13
+ const conversions = {
14
+ 'km_mi': 0.621371, 'mi_km': 1.60934,
15
+ 'kg_lb': 2.20462, 'lb_kg': 0.453592,
16
+ 'c_f': (v) => v * 9/5 + 32, 'f_c': (v) => (v - 32) * 5/9,
17
+ 'm_ft': 3.28084, 'ft_m': 0.3048,
18
+ 'l_gal': 0.264172, 'gal_l': 3.78541,
19
+ 'cm_in': 0.393701, 'in_cm': 2.54,
20
+ 'sar_usd': 0.2667, 'usd_sar': 3.75,
21
+ };
22
+ const key = from.toLowerCase() + '_' + to.toLowerCase();
23
+ const conv = conversions[key];
24
+ if (!conv) return `Cannot convert ${from} to ${to}`;
25
+ const result = typeof conv === 'function' ? conv(value) : value * conv;
26
+ return `${value} ${from} = ${result.toFixed(2)} ${to}`;
27
+ }
@@ -0,0 +1,18 @@
1
+ import crypto from 'crypto';
2
+
3
+ export function generatePassword(length = 16, options = {}) {
4
+ const upper = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
5
+ const lower = 'abcdefghijklmnopqrstuvwxyz';
6
+ const digits = '0123456789';
7
+ const special = '!@#$%^&*()_+-=[]{}|;:,.<>?';
8
+
9
+ let chars = upper + lower + digits;
10
+ if (options.special !== false) chars += special;
11
+
12
+ let password = '';
13
+ const bytes = crypto.randomBytes(length);
14
+ for (let i = 0; i < length; i++) {
15
+ password += chars[bytes[i] % chars.length];
16
+ }
17
+ return password;
18
+ }
@@ -59,6 +59,37 @@ export class ToolRouter {
59
59
  'Send an email.');
60
60
  }
61
61
 
62
+ tools.push('', '### Weather',
63
+ '---TOOL:weather:city name---',
64
+ 'Get current weather and 3-day forecast for any city.',
65
+ '', '### Calculator',
66
+ '---TOOL:calc:math expression---',
67
+ 'Calculate math expressions. Example: ---TOOL:calc:15% of 230---',
68
+ '', '### Unit Converter',
69
+ '---TOOL:convert:100 km to mi---',
70
+ 'Convert units. Supports: km/mi, kg/lb, C/F, m/ft, SAR/USD, cm/in, L/gal',
71
+ '', '### Add Task',
72
+ '---TOOL:task_add:buy groceries---',
73
+ 'Add a task to the user\'s todo list.',
74
+ '', '### List Tasks',
75
+ '---TOOL:task_list:all---',
76
+ 'Show the user\'s pending tasks.',
77
+ '', '### Complete Task',
78
+ '---TOOL:task_done:task_id---',
79
+ 'Mark a task as complete.',
80
+ '', '### Generate Password',
81
+ '---TOOL:password:16---',
82
+ 'Generate a secure random password. Number is length (default 16).',
83
+ '', '### Shorten URL',
84
+ '---TOOL:shorten:https://example.com/very/long/url---',
85
+ 'Shorten a long URL.',
86
+ '', '### News',
87
+ '---TOOL:news:topic or country---',
88
+ 'Search for latest news on a topic.',
89
+ '', '### OCR (Extract Text from Image)',
90
+ 'When user sends an image and asks to extract text, describe the text you see in detail.',
91
+ 'You already have vision — use it to read text from screenshots, documents, signs, etc.');
92
+
62
93
  tools.push('', '### Set Reminder',
63
94
  '---TOOL:remind:YYYY-MM-DDTHH:MM|Your reminder message---',
64
95
  'Set a reminder to message the user at a specific time. Time must be in UTC.',
@@ -108,6 +139,65 @@ export class ToolRouter {
108
139
  }
109
140
  break;
110
141
  }
142
+ case 'weather': {
143
+ const { getWeather } = await import('./weather.js');
144
+ toolResult = await getWeather(toolArg);
145
+ break;
146
+ }
147
+ case 'calc':
148
+ case 'calculate': {
149
+ const { calculate } = await import('./calculator.js');
150
+ toolResult = calculate(toolArg);
151
+ break;
152
+ }
153
+ case 'convert': {
154
+ const parts = toolArg.match(/([\d.]+)\s*(\w+)\s+to\s+(\w+)/i);
155
+ if (parts) {
156
+ const { convertUnit } = await import('./calculator.js');
157
+ toolResult = convertUnit(parseFloat(parts[1]), parts[2], parts[3]);
158
+ } else {
159
+ toolResult = 'Format: 100 km to mi';
160
+ }
161
+ break;
162
+ }
163
+ case 'task_add': {
164
+ const { TaskManager } = await import('./tasks.js');
165
+ const tm = new TaskManager(this.storage);
166
+ tm.add(agentId, this._currentContactId || 'unknown', toolArg);
167
+ toolResult = 'Task added: ' + toolArg;
168
+ break;
169
+ }
170
+ case 'task_list': {
171
+ const { TaskManager } = await import('./tasks.js');
172
+ const tm = new TaskManager(this.storage);
173
+ const tasks = tm.list(agentId, this._currentContactId || 'unknown');
174
+ if (tasks.length === 0) toolResult = 'No pending tasks!';
175
+ else toolResult = tasks.map((t, i) => (i+1) + '. ' + t.task).join('\n');
176
+ break;
177
+ }
178
+ case 'task_done': {
179
+ const { TaskManager } = await import('./tasks.js');
180
+ const tm = new TaskManager(this.storage);
181
+ tm.complete(agentId, parseInt(toolArg));
182
+ toolResult = 'Task completed!';
183
+ break;
184
+ }
185
+ case 'password': {
186
+ const { generatePassword } = await import('./password.js');
187
+ const len = parseInt(toolArg) || 16;
188
+ toolResult = '🔐 ' + generatePassword(len);
189
+ break;
190
+ }
191
+ case 'shorten': {
192
+ const { shortenUrl } = await import('./shortener.js');
193
+ toolResult = await shortenUrl(toolArg);
194
+ break;
195
+ }
196
+ case 'news': {
197
+ const results = await this.browser.search(toolArg + ' news today', 5);
198
+ toolResult = results.map(r => '📰 ' + r.title + '\n ' + r.snippet + '\n ' + r.url).join('\n\n');
199
+ break;
200
+ }
111
201
  case 'imagine':
112
202
  case 'draw':
113
203
  case 'image': {
@@ -0,0 +1,9 @@
1
+ export async function shortenUrl(url) {
2
+ try {
3
+ const res = await fetch(`https://is.gd/create.php?format=json&url=${encodeURIComponent(url)}`);
4
+ const data = await res.json();
5
+ return data.shorturl || 'Failed to shorten';
6
+ } catch {
7
+ return 'Failed to shorten URL';
8
+ }
9
+ }
@@ -0,0 +1,42 @@
1
+ import { logger } from '../core/logger.js';
2
+
3
+ export class TaskManager {
4
+ constructor(storage) {
5
+ this.storage = storage;
6
+ this._initDb();
7
+ }
8
+
9
+ _initDb() {
10
+ try {
11
+ this.storage.db.exec(`
12
+ CREATE TABLE IF NOT EXISTS tasks (
13
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
14
+ agent_id TEXT NOT NULL,
15
+ contact_id TEXT NOT NULL,
16
+ task TEXT NOT NULL,
17
+ done INTEGER DEFAULT 0,
18
+ created_at TEXT DEFAULT (datetime('now'))
19
+ )
20
+ `);
21
+ } catch {}
22
+ }
23
+
24
+ add(agentId, contactId, task) {
25
+ this.storage.db.prepare('INSERT INTO tasks (agent_id, contact_id, task) VALUES (?, ?, ?)').run(agentId, contactId, task);
26
+ return true;
27
+ }
28
+
29
+ list(agentId, contactId) {
30
+ return this.storage.db.prepare('SELECT * FROM tasks WHERE agent_id = ? AND contact_id = ? AND done = 0 ORDER BY created_at').all(agentId, contactId);
31
+ }
32
+
33
+ complete(agentId, id) {
34
+ this.storage.db.prepare('UPDATE tasks SET done = 1 WHERE id = ? AND agent_id = ?').run(id, agentId);
35
+ return true;
36
+ }
37
+
38
+ clear(agentId, contactId) {
39
+ const count = this.storage.db.prepare('UPDATE tasks SET done = 1 WHERE agent_id = ? AND contact_id = ? AND done = 0').run(agentId, contactId);
40
+ return count.changes;
41
+ }
42
+ }
@@ -0,0 +1,25 @@
1
+ import { logger } from '../core/logger.js';
2
+
3
+ export async function getWeather(location) {
4
+ try {
5
+ const res = await fetch(`https://wttr.in/${encodeURIComponent(location)}?format=j1`);
6
+ const data = await res.json();
7
+ const cur = data.current_condition?.[0];
8
+ if (!cur) throw new Error('No data');
9
+
10
+ const forecast = data.weather?.slice(0, 3).map(d =>
11
+ `${d.date}: ${d.mintempC}°-${d.maxtempC}°C, ${d.hourly?.[4]?.weatherDesc?.[0]?.value || ''}`
12
+ ).join('\n') || '';
13
+
14
+ return `Weather in ${location}:
15
+ 🌡️ ${cur.temp_C}°C (feels like ${cur.FeelsLikeC}°C)
16
+ 💨 Wind: ${cur.windspeedKmph} km/h ${cur.winddir16Point}
17
+ 💧 Humidity: ${cur.humidity}%
18
+ ☁️ ${cur.weatherDesc?.[0]?.value || ''}
19
+
20
+ Forecast:
21
+ ${forecast}`;
22
+ } catch (err) {
23
+ return 'Could not get weather: ' + err.message;
24
+ }
25
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "squidclaw",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "🦑 AI agent platform — human-like agents for WhatsApp, Telegram & more",
5
5
  "main": "lib/engine.js",
6
6
  "bin": {