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.
- package/lib/core/agent-tools-mixin.js +2 -0
- package/lib/engine.js +19 -0
- package/lib/tools/calculator.js +27 -0
- package/lib/tools/password.js +18 -0
- package/lib/tools/router.js +90 -0
- package/lib/tools/shortener.js +9 -0
- package/lib/tools/tasks.js +42 -0
- package/lib/tools/weather.js +25 -0
- package/package.json +1 -1
|
@@ -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
|
+
}
|
package/lib/tools/router.js
CHANGED
|
@@ -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
|
+
}
|