squidclaw 1.5.0 → 2.0.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/engine.js CHANGED
@@ -67,7 +67,11 @@ export class SquidclawEngine {
67
67
  pipeline.use('skill-check', skillCheckMiddleware);
68
68
  pipeline.use('typing', typingMiddleware); // wraps AI call with typing indicator
69
69
  pipeline.use('ai-process', aiProcessMiddleware);
70
+ const { proactiveMiddleware } = await import('./middleware/proactive.js');
71
+ const { learningMiddleware } = await import('./middleware/learning.js');
70
72
  pipeline.use('usage-alerts', usageAlertsMiddleware);
73
+ pipeline.use('proactive', proactiveMiddleware);
74
+ pipeline.use('learning', learningMiddleware);
71
75
  pipeline.use('response-sender', responseSenderMiddleware);
72
76
 
73
77
  return pipeline;
@@ -191,6 +195,7 @@ export class SquidclawEngine {
191
195
  this.knowledgeBase = new KnowledgeBase(this.storage, this.config);
192
196
  this.toolRouter = new ToolRouter(this.config, this.knowledgeBase);
193
197
 
198
+ this.toolRouter.setContext(this, this.aiGateway, this.config.ai?.defaultModel);
194
199
  for (const agent of agents) {
195
200
  addToolSupport(agent, this.toolRouter, this.knowledgeBase);
196
201
  }
@@ -0,0 +1,15 @@
1
+ import { logger } from '../core/logger.js';
2
+
3
+ export async function summarizeConversation(storage, aiGateway, agentId, contactId, model) {
4
+ const messages = await storage.getConversation(agentId, contactId, 100);
5
+ if (messages.length === 0) return 'No conversation to summarize.';
6
+
7
+ const chatText = messages.map(m => `${m.role}: ${m.content}`).join('\n');
8
+
9
+ const response = await aiGateway.chat([
10
+ { role: 'system', content: 'Summarize this conversation concisely. Include key decisions, action items, and important information discussed. Be brief.' },
11
+ { role: 'user', content: chatText },
12
+ ], { model });
13
+
14
+ return response.content;
15
+ }
@@ -0,0 +1,48 @@
1
+ import { logger } from '../core/logger.js';
2
+
3
+ export async function generateBriefing(engine, agentId) {
4
+ const parts = [];
5
+
6
+ // Weather (default Riyadh)
7
+ try {
8
+ const { getWeather } = await import('../tools/weather.js');
9
+ parts.push(await getWeather('Riyadh'));
10
+ } catch {}
11
+
12
+ // Pending tasks
13
+ try {
14
+ const { TaskManager } = await import('../tools/tasks.js');
15
+ const tm = new TaskManager(engine.storage);
16
+ // Get all contacts' tasks for this agent
17
+ const tasks = engine.storage.db.prepare('SELECT * FROM tasks WHERE agent_id = ? AND done = 0 ORDER BY created_at LIMIT 10').all(agentId);
18
+ if (tasks.length > 0) {
19
+ parts.push('📋 *Pending Tasks:*\n' + tasks.map((t, i) => (i+1) + '. ' + t.task).join('\n'));
20
+ }
21
+ } catch {}
22
+
23
+ // Pending reminders
24
+ try {
25
+ const reminders = engine.storage.db.prepare("SELECT * FROM reminders WHERE agent_id = ? AND fired = 0 AND fire_at > datetime('now') ORDER BY fire_at LIMIT 5").all(agentId);
26
+ if (reminders.length > 0) {
27
+ parts.push('⏰ *Upcoming Reminders:*\n' + reminders.map(r => '• ' + r.fire_at.slice(11, 16) + ' — ' + r.message).join('\n'));
28
+ }
29
+ } catch {}
30
+
31
+ // Memories count
32
+ try {
33
+ const memCount = engine.storage.db.prepare('SELECT COUNT(*) as c FROM memories WHERE agent_id = ?').get(agentId);
34
+ parts.push('🧠 Memories: ' + (memCount?.c || 0) + ' facts stored');
35
+ } catch {}
36
+
37
+ // News
38
+ try {
39
+ const { BrowserTool } = await import('../tools/browser.js');
40
+ const b = new BrowserTool({});
41
+ const news = await b.search('Saudi Arabia news today', 3);
42
+ if (news.length > 0) {
43
+ parts.push('📰 *Top News:*\n' + news.map(n => '• ' + n.title).join('\n'));
44
+ }
45
+ } catch {}
46
+
47
+ return '☀️ *Good Morning! Daily Briefing*\n\n' + parts.join('\n\n');
48
+ }
@@ -0,0 +1,31 @@
1
+ import { logger } from '../core/logger.js';
2
+
3
+ export class HandoffManager {
4
+ constructor(storage) {
5
+ this.storage = storage;
6
+ this.activeHandoffs = new Map(); // contactId -> { to, startedAt }
7
+ }
8
+
9
+ start(agentId, contactId, to, reason) {
10
+ this.activeHandoffs.set(contactId, {
11
+ to, reason, startedAt: new Date(), agentId
12
+ });
13
+ logger.info('handoff', `${contactId} → ${to}: ${reason}`);
14
+ return true;
15
+ }
16
+
17
+ isActive(contactId) {
18
+ return this.activeHandoffs.has(contactId);
19
+ }
20
+
21
+ end(contactId) {
22
+ this.activeHandoffs.delete(contactId);
23
+ return true;
24
+ }
25
+
26
+ getActive() {
27
+ return Array.from(this.activeHandoffs.entries()).map(([id, h]) => ({
28
+ contactId: id, ...h
29
+ }));
30
+ }
31
+ }
@@ -0,0 +1,47 @@
1
+ import { logger } from '../core/logger.js';
2
+
3
+ export class ScheduledReports {
4
+ constructor(engine) {
5
+ this.engine = engine;
6
+ this.timers = [];
7
+ }
8
+
9
+ startWeeklyReport(agentId, contactId, platform, metadata) {
10
+ // Every Sunday at 9 AM UTC
11
+ const check = () => {
12
+ const now = new Date();
13
+ if (now.getDay() === 0 && now.getHours() === 9 && now.getMinutes() === 0) {
14
+ this._sendWeeklyReport(agentId, contactId, platform, metadata);
15
+ }
16
+ };
17
+ const timer = setInterval(check, 60000);
18
+ this.timers.push(timer);
19
+ }
20
+
21
+ async _sendWeeklyReport(agentId, contactId, platform, metadata) {
22
+ try {
23
+ const usage = this.engine.storage.db.prepare(
24
+ "SELECT COUNT(*) as msgs, SUM(cost_usd) as cost, SUM(input_tokens + output_tokens) as tokens FROM usage WHERE agent_id = ? AND created_at >= date('now', '-7 days')"
25
+ ).get(agentId);
26
+
27
+ const fmtT = (n) => n >= 1e6 ? (n/1e6).toFixed(1)+'M' : n >= 1e3 ? (n/1e3).toFixed(1)+'K' : String(n || 0);
28
+
29
+ const report = [
30
+ '📊 *Weekly Report*', '',
31
+ '💬 Messages: ' + (usage?.msgs || 0),
32
+ '🪙 Tokens: ' + fmtT(usage?.tokens),
33
+ '💰 Cost: $' + (usage?.cost || 0).toFixed(4),
34
+ ].join('\n');
35
+
36
+ if (platform === 'telegram' && this.engine.telegramManager) {
37
+ await this.engine.telegramManager.sendMessage(agentId, contactId, report, metadata);
38
+ }
39
+ } catch (err) {
40
+ logger.error('reports', 'Weekly report failed: ' + err.message);
41
+ }
42
+ }
43
+
44
+ stop() {
45
+ this.timers.forEach(t => clearInterval(t));
46
+ }
47
+ }
@@ -0,0 +1,55 @@
1
+ import { logger } from '../core/logger.js';
2
+
3
+ export class WebhookManager {
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 webhooks (
13
+ id TEXT PRIMARY KEY,
14
+ agent_id TEXT NOT NULL,
15
+ name TEXT NOT NULL,
16
+ url TEXT NOT NULL,
17
+ event TEXT DEFAULT 'message',
18
+ active INTEGER DEFAULT 1,
19
+ created_at TEXT DEFAULT (datetime('now'))
20
+ )
21
+ `);
22
+ } catch {}
23
+ }
24
+
25
+ add(agentId, name, url, event = 'message') {
26
+ const id = 'wh_' + Date.now().toString(36);
27
+ this.storage.db.prepare('INSERT INTO webhooks (id, agent_id, name, url, event) VALUES (?, ?, ?, ?, ?)').run(id, agentId, name, url, event);
28
+ return id;
29
+ }
30
+
31
+ async fire(agentId, event, data) {
32
+ const hooks = this.storage.db.prepare('SELECT * FROM webhooks WHERE agent_id = ? AND event = ? AND active = 1').all(agentId, event);
33
+
34
+ for (const hook of hooks) {
35
+ try {
36
+ await fetch(hook.url, {
37
+ method: 'POST',
38
+ headers: { 'Content-Type': 'application/json' },
39
+ body: JSON.stringify({ event, agentId, data, timestamp: new Date().toISOString() }),
40
+ });
41
+ logger.info('webhooks', `Fired ${hook.name} → ${hook.url}`);
42
+ } catch (err) {
43
+ logger.error('webhooks', `Failed ${hook.name}: ${err.message}`);
44
+ }
45
+ }
46
+ }
47
+
48
+ list(agentId) {
49
+ return this.storage.db.prepare('SELECT * FROM webhooks WHERE agent_id = ?').all(agentId);
50
+ }
51
+
52
+ remove(id) {
53
+ this.storage.db.prepare('DELETE FROM webhooks WHERE id = ?').run(id);
54
+ }
55
+ }
@@ -16,7 +16,13 @@ export async function commandsMiddleware(ctx, next) {
16
16
  '/memories — what I remember about you',
17
17
  '/tasks — your todo list',
18
18
  '/usage — spending report',
19
- '/configmanage settings',
19
+ '/notesnotes',
20
+ '/contacts — contact book',
21
+ '/briefing — daily briefing',
22
+ '/summary — summarize chat',
23
+ '/trivia — trivia game',
24
+ '/flip — coin flip',
25
+ '/config — manage settings',
20
26
  '/exec <cmd> — run a shell command',
21
27
  '/files — list sandbox files',
22
28
  '/subagents — list background tasks',
@@ -121,6 +127,86 @@ export async function commandsMiddleware(ctx, next) {
121
127
  return;
122
128
  }
123
129
 
130
+ if (cmd === '/notes') {
131
+ try {
132
+ const { NotesManager } = await import('../tools/notes.js');
133
+ const nm = new NotesManager(ctx.storage);
134
+ const args = msg.slice(7).trim();
135
+ if (args) {
136
+ nm.add(ctx.agentId, ctx.contactId, args);
137
+ await ctx.reply('📝 Note saved!');
138
+ } else {
139
+ const notes = nm.list(ctx.agentId, ctx.contactId);
140
+ if (notes.length === 0) await ctx.reply('📝 No notes yet. Use /notes <text> to add one.');
141
+ else await ctx.reply('📝 *Your Notes*\n\n' + notes.map((n, i) => (i+1) + '. ' + n.content.slice(0, 80)).join('\n'));
142
+ }
143
+ } catch (err) { await ctx.reply('❌ ' + err.message); }
144
+ return;
145
+ }
146
+
147
+ if (cmd === '/contacts') {
148
+ try {
149
+ const { ContactBook } = await import('../tools/contacts.js');
150
+ const cb = new ContactBook(ctx.storage);
151
+ const args = msg.slice(10).trim();
152
+ if (args) {
153
+ const found = cb.find(ctx.agentId, ctx.contactId, args);
154
+ if (found.length === 0) await ctx.reply('📇 No contacts found for: ' + args);
155
+ else await ctx.reply('📇 *Contacts*\n\n' + found.map(c => '• ' + c.name + (c.phone ? ' — ' + c.phone : '')).join('\n'));
156
+ } else {
157
+ const all = cb.list(ctx.agentId, ctx.contactId);
158
+ if (all.length === 0) await ctx.reply('📇 No contacts saved yet.');
159
+ else await ctx.reply('📇 *Contact Book*\n\n' + all.map(c => '• ' + c.name + (c.phone ? ' — ' + c.phone : '')).join('\n'));
160
+ }
161
+ } catch (err) { await ctx.reply('❌ ' + err.message); }
162
+ return;
163
+ }
164
+
165
+ if (cmd === '/briefing') {
166
+ try {
167
+ const { generateBriefing } = await import('../features/daily-briefing.js');
168
+ const briefing = await generateBriefing(ctx.engine, ctx.agentId);
169
+ await ctx.reply(briefing);
170
+ } catch (err) { await ctx.reply('❌ ' + err.message); }
171
+ return;
172
+ }
173
+
174
+ if (cmd === '/summary') {
175
+ try {
176
+ const { summarizeConversation } = await import('../features/conversation-summary.js');
177
+ const summary = await summarizeConversation(ctx.storage, ctx.engine.aiGateway, ctx.agentId, ctx.contactId, ctx.agent?.model);
178
+ await ctx.reply('📋 *Conversation Summary*\n\n' + summary);
179
+ } catch (err) { await ctx.reply('❌ ' + err.message); }
180
+ return;
181
+ }
182
+
183
+ if (cmd === '/flip') {
184
+ const { coinFlip } = await import('../tools/games.js');
185
+ await ctx.reply(coinFlip());
186
+ return;
187
+ }
188
+
189
+ if (cmd === '/roll') {
190
+ const { rollDice } = await import('../tools/games.js');
191
+ const sides = parseInt(msg.slice(6)) || 6;
192
+ await ctx.reply('🎲 ' + rollDice(sides));
193
+ return;
194
+ }
195
+
196
+ if (cmd === '/trivia') {
197
+ const { getTrivia } = await import('../tools/games.js');
198
+ const t = getTrivia();
199
+ await ctx.reply('🎯 *Trivia!*\n\n' + t.q);
200
+ return;
201
+ }
202
+
203
+ if (cmd === '/riddle') {
204
+ const { getRiddle } = await import('../tools/games.js');
205
+ const r = getRiddle();
206
+ await ctx.reply('🧩 ' + r.q);
207
+ return;
208
+ }
209
+
124
210
  if (cmd === '/config') {
125
211
  const args = msg.slice(8).trim();
126
212
  if (!args || args === 'help') {
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Learning mode — track preferences and patterns
3
+ */
4
+ import { logger } from '../core/logger.js';
5
+
6
+ export async function learningMiddleware(ctx, next) {
7
+ // Track interaction patterns
8
+ try {
9
+ const hour = new Date().getHours();
10
+ const key = 'active_hours_' + (hour < 10 ? '0' : '') + hour;
11
+
12
+ // Increment activity counter for this hour
13
+ const existing = ctx.storage.db.prepare(
14
+ 'SELECT value FROM memories WHERE agent_id = ? AND key = ?'
15
+ ).get(ctx.agentId, key);
16
+
17
+ const count = existing ? parseInt(existing.value) + 1 : 1;
18
+ await ctx.storage.saveMemory(ctx.agentId, key, String(count), 'pattern');
19
+
20
+ // Track language preference
21
+ const hasArabic = /[\u0600-\u06FF]/.test(ctx.message);
22
+ if (hasArabic) {
23
+ await ctx.storage.saveMemory(ctx.agentId, 'prefers_arabic', 'true', 'pattern');
24
+ }
25
+
26
+ // Track message length preference
27
+ if (ctx.message.length < 20) {
28
+ await ctx.storage.saveMemory(ctx.agentId, 'prefers_short_messages', 'true', 'pattern');
29
+ }
30
+ } catch {}
31
+
32
+ await next();
33
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Proactive check-ins — notify about upcoming events
3
+ */
4
+ export async function proactiveMiddleware(ctx, next) {
5
+ await next();
6
+
7
+ // After responding, check if there are upcoming reminders (< 1 hour)
8
+ if (!ctx.engine.reminders) return;
9
+
10
+ try {
11
+ const upcoming = ctx.engine.storage.db.prepare(
12
+ "SELECT * FROM reminders WHERE agent_id = ? AND contact_id = ? AND fired = 0 AND fire_at BETWEEN datetime('now') AND datetime('now', '+1 hour') LIMIT 1"
13
+ ).get(ctx.agentId, ctx.contactId);
14
+
15
+ if (upcoming && ctx.response?.messages) {
16
+ const mins = Math.round((new Date(upcoming.fire_at + 'Z') - Date.now()) / 60000);
17
+ if (mins > 0 && mins <= 60) {
18
+ ctx.response.messages.push('⏰ Heads up — you have a reminder in ' + mins + ' minutes: ' + upcoming.message);
19
+ }
20
+ }
21
+ } catch {}
22
+ }
@@ -0,0 +1,44 @@
1
+ import { logger } from '../core/logger.js';
2
+
3
+ export class ContactBook {
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 contact_book (
13
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
14
+ agent_id TEXT NOT NULL,
15
+ owner_id TEXT NOT NULL,
16
+ name TEXT NOT NULL,
17
+ phone TEXT,
18
+ email TEXT,
19
+ notes TEXT DEFAULT '',
20
+ created_at TEXT DEFAULT (datetime('now'))
21
+ )
22
+ `);
23
+ } catch {}
24
+ }
25
+
26
+ add(agentId, ownerId, name, phone, email, notes) {
27
+ this.storage.db.prepare('INSERT INTO contact_book (agent_id, owner_id, name, phone, email, notes) VALUES (?, ?, ?, ?, ?, ?)')
28
+ .run(agentId, ownerId, name, phone || null, email || null, notes || '');
29
+ return true;
30
+ }
31
+
32
+ find(agentId, ownerId, query) {
33
+ return this.storage.db.prepare('SELECT * FROM contact_book WHERE agent_id = ? AND owner_id = ? AND (name LIKE ? OR phone LIKE ? OR email LIKE ?) LIMIT 10')
34
+ .all(agentId, ownerId, `%${query}%`, `%${query}%`, `%${query}%`);
35
+ }
36
+
37
+ list(agentId, ownerId) {
38
+ return this.storage.db.prepare('SELECT * FROM contact_book WHERE agent_id = ? AND owner_id = ? ORDER BY name').all(agentId, ownerId);
39
+ }
40
+
41
+ delete(agentId, id) {
42
+ this.storage.db.prepare('DELETE FROM contact_book WHERE id = ? AND agent_id = ?').run(id, agentId);
43
+ }
44
+ }
@@ -0,0 +1,44 @@
1
+ import crypto from 'crypto';
2
+
3
+ const TRIVIA = [
4
+ { q: 'What is the capital of Saudi Arabia?', a: 'Riyadh' },
5
+ { q: 'In what year was the iPhone first released?', a: '2007' },
6
+ { q: 'What planet is known as the Red Planet?', a: 'Mars' },
7
+ { q: 'How many legs does an octopus have?', a: '8' },
8
+ { q: 'What is the largest ocean on Earth?', a: 'Pacific Ocean' },
9
+ { q: 'Who painted the Mona Lisa?', a: 'Leonardo da Vinci' },
10
+ { q: 'What is the chemical symbol for gold?', a: 'Au' },
11
+ { q: 'What year did World War II end?', a: '1945' },
12
+ { q: 'What is the tallest building in the world?', a: 'Burj Khalifa' },
13
+ { q: 'How many continents are there?', a: '7' },
14
+ ];
15
+
16
+ const RIDDLES = [
17
+ { q: 'I have cities, but no houses. I have mountains, but no trees. What am I?', a: 'A map' },
18
+ { q: 'What has hands but cannot clap?', a: 'A clock' },
19
+ { q: 'I speak without a mouth and hear without ears. What am I?', a: 'An echo' },
20
+ { q: 'The more you take, the more you leave behind. What am I?', a: 'Footsteps' },
21
+ { q: 'What can you break, even if you never pick it up or touch it?', a: 'A promise' },
22
+ ];
23
+
24
+ export function getTrivia() {
25
+ const i = crypto.randomInt(TRIVIA.length);
26
+ return TRIVIA[i];
27
+ }
28
+
29
+ export function getRiddle() {
30
+ const i = crypto.randomInt(RIDDLES.length);
31
+ return RIDDLES[i];
32
+ }
33
+
34
+ export function coinFlip() {
35
+ return crypto.randomInt(2) === 0 ? 'Heads! 🪙' : 'Tails! 🪙';
36
+ }
37
+
38
+ export function rollDice(sides = 6) {
39
+ return crypto.randomInt(sides) + 1;
40
+ }
41
+
42
+ export function randomNumber(min = 1, max = 100) {
43
+ return crypto.randomInt(min, max + 1);
44
+ }
@@ -0,0 +1,44 @@
1
+ import { logger } from '../core/logger.js';
2
+
3
+ export class NotesManager {
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 notes (
13
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
14
+ agent_id TEXT NOT NULL,
15
+ contact_id TEXT NOT NULL,
16
+ title TEXT,
17
+ content TEXT NOT NULL,
18
+ tags TEXT DEFAULT '',
19
+ created_at TEXT DEFAULT (datetime('now')),
20
+ updated_at TEXT DEFAULT (datetime('now'))
21
+ )
22
+ `);
23
+ } catch {}
24
+ }
25
+
26
+ add(agentId, contactId, content, title = null) {
27
+ this.storage.db.prepare('INSERT INTO notes (agent_id, contact_id, title, content) VALUES (?, ?, ?, ?)').run(agentId, contactId, title, content);
28
+ return true;
29
+ }
30
+
31
+ list(agentId, contactId, limit = 10) {
32
+ return this.storage.db.prepare('SELECT * FROM notes WHERE agent_id = ? AND contact_id = ? ORDER BY created_at DESC LIMIT ?').all(agentId, contactId, limit);
33
+ }
34
+
35
+ search(agentId, contactId, query) {
36
+ return this.storage.db.prepare('SELECT * FROM notes WHERE agent_id = ? AND contact_id = ? AND (content LIKE ? OR title LIKE ?) ORDER BY created_at DESC LIMIT 10')
37
+ .all(agentId, contactId, `%${query}%`, `%${query}%`);
38
+ }
39
+
40
+ delete(agentId, id) {
41
+ this.storage.db.prepare('DELETE FROM notes WHERE id = ? AND agent_id = ?').run(id, agentId);
42
+ return true;
43
+ }
44
+ }
@@ -59,6 +59,43 @@ export class ToolRouter {
59
59
  'Send an email.');
60
60
  }
61
61
 
62
+ tools.push('', '### Save Note',
63
+ '---TOOL:note:content of the note---',
64
+ 'Save a personal note for the user.',
65
+ '', '### List Notes',
66
+ '---TOOL:notes_list:all---',
67
+ 'Show user\'s saved notes.',
68
+ '', '### Search Notes',
69
+ '---TOOL:notes_search:query---',
70
+ 'Search through user\'s notes.',
71
+ '', '### Save Contact',
72
+ '---TOOL:contact_add:Name|phone|email---',
73
+ 'Save a contact to the user\'s contact book.',
74
+ '', '### Find Contact',
75
+ '---TOOL:contact_find:name or number---',
76
+ 'Search the user\'s contacts.',
77
+ '', '### Summarize Chat',
78
+ '---TOOL:summarize:conversation---',
79
+ 'Summarize our conversation so far.',
80
+ '', '### Daily Briefing',
81
+ '---TOOL:briefing:now---',
82
+ 'Generate a morning briefing (weather, tasks, news, reminders).',
83
+ '', '### Trivia Game',
84
+ '---TOOL:trivia:start---',
85
+ 'Ask a trivia question.',
86
+ '', '### Riddle',
87
+ '---TOOL:riddle:start---',
88
+ 'Tell a riddle.',
89
+ '', '### Coin Flip',
90
+ '---TOOL:coinflip:flip---',
91
+ 'Flip a coin.',
92
+ '', '### Roll Dice',
93
+ '---TOOL:dice:6---',
94
+ 'Roll a dice. Number is sides (default 6).',
95
+ '', '### Handoff to Human',
96
+ '---TOOL:handoff:reason---',
97
+ 'Transfer the conversation to a human agent. Use when you cannot help further.');
98
+
62
99
  tools.push('', '### Run Command',
63
100
  '---TOOL:exec:ls -la---',
64
101
  'Execute a shell command. Output is returned. Sandboxed for safety.',
@@ -134,6 +171,12 @@ export class ToolRouter {
134
171
  * Process AI response — extract and execute tool calls
135
172
  * Returns: { toolUsed, toolResult, cleanResponse }
136
173
  */
174
+ setContext(engine, aiGateway, model) {
175
+ this._engine = engine;
176
+ this._aiGateway = aiGateway;
177
+ this._model = model;
178
+ }
179
+
137
180
  async processResponse(response, agentId) {
138
181
  const toolMatch = response.match(/---TOOL:(\w+):(.+?)---/);
139
182
  if (!toolMatch) return { toolUsed: false, toolResult: null, cleanResponse: response };
@@ -168,6 +211,84 @@ export class ToolRouter {
168
211
  }
169
212
  break;
170
213
  }
214
+ case 'note': {
215
+ const { NotesManager } = await import('./notes.js');
216
+ const nm = new NotesManager(this.storage);
217
+ nm.add(agentId, this._currentContactId || 'unknown', toolArg);
218
+ toolResult = 'Note saved! 📝';
219
+ break;
220
+ }
221
+ case 'notes_list': {
222
+ const { NotesManager } = await import('./notes.js');
223
+ const nm = new NotesManager(this.storage);
224
+ const notes = nm.list(agentId, this._currentContactId || 'unknown');
225
+ if (notes.length === 0) toolResult = 'No notes yet!';
226
+ else toolResult = notes.map((n, i) => (i+1) + '. ' + (n.title || n.content.slice(0, 50))).join('\n');
227
+ break;
228
+ }
229
+ case 'notes_search': {
230
+ const { NotesManager } = await import('./notes.js');
231
+ const nm = new NotesManager(this.storage);
232
+ const found = nm.search(agentId, this._currentContactId || 'unknown', toolArg);
233
+ if (found.length === 0) toolResult = 'No notes found for: ' + toolArg;
234
+ else toolResult = found.map(n => '• ' + n.content.slice(0, 100)).join('\n');
235
+ break;
236
+ }
237
+ case 'contact_add': {
238
+ const { ContactBook } = await import('./contacts.js');
239
+ const cb = new ContactBook(this.storage);
240
+ const parts = toolArg.split('|').map(p => p.trim());
241
+ cb.add(agentId, this._currentContactId || 'unknown', parts[0], parts[1], parts[2], parts[3]);
242
+ toolResult = 'Contact saved: ' + parts[0] + ' 📇';
243
+ break;
244
+ }
245
+ case 'contact_find': {
246
+ const { ContactBook } = await import('./contacts.js');
247
+ const cb = new ContactBook(this.storage);
248
+ const found = cb.find(agentId, this._currentContactId || 'unknown', toolArg);
249
+ if (found.length === 0) toolResult = 'No contacts found for: ' + toolArg;
250
+ else toolResult = found.map(c => '📇 ' + c.name + (c.phone ? ' — ' + c.phone : '') + (c.email ? ' — ' + c.email : '')).join('\n');
251
+ break;
252
+ }
253
+ case 'summarize': {
254
+ const { summarizeConversation } = await import('../features/conversation-summary.js');
255
+ toolResult = await summarizeConversation(this.storage, this._aiGateway, agentId, this._currentContactId, this._model);
256
+ break;
257
+ }
258
+ case 'briefing': {
259
+ const { generateBriefing } = await import('../features/daily-briefing.js');
260
+ toolResult = await generateBriefing(this._engine, agentId);
261
+ break;
262
+ }
263
+ case 'trivia': {
264
+ const { getTrivia } = await import('./games.js');
265
+ const t = getTrivia();
266
+ toolResult = '🎯 *Trivia!*\n\n' + t.q + '\n\n(Answer: ||' + t.a + '||)';
267
+ break;
268
+ }
269
+ case 'riddle': {
270
+ const { getRiddle } = await import('./games.js');
271
+ const r = getRiddle();
272
+ toolResult = '🧩 *Riddle!*\n\n' + r.q + '\n\n(Answer: ||' + r.a + '||)';
273
+ break;
274
+ }
275
+ case 'coinflip':
276
+ case 'coin': {
277
+ const { coinFlip } = await import('./games.js');
278
+ toolResult = coinFlip();
279
+ break;
280
+ }
281
+ case 'dice':
282
+ case 'roll': {
283
+ const { rollDice } = await import('./games.js');
284
+ const sides = parseInt(toolArg) || 6;
285
+ toolResult = '🎲 Rolled a ' + rollDice(sides) + '! (d' + sides + ')';
286
+ break;
287
+ }
288
+ case 'handoff': {
289
+ toolResult = '🤝 Transferring to a human agent. Reason: ' + toolArg;
290
+ break;
291
+ }
171
292
  case 'exec':
172
293
  case 'shell':
173
294
  case 'run': {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "squidclaw",
3
- "version": "1.5.0",
3
+ "version": "2.0.0",
4
4
  "description": "🦑 AI agent platform — human-like agents for WhatsApp, Telegram & more",
5
5
  "main": "lib/engine.js",
6
6
  "bin": {