squidclaw 1.4.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 +14 -1
- package/lib/features/config-manager.js +238 -0
- package/lib/features/conversation-summary.js +15 -0
- package/lib/features/daily-briefing.js +48 -0
- package/lib/features/handoff.js +31 -0
- package/lib/features/scheduled-reports.js +47 -0
- package/lib/features/webhooks.js +55 -0
- package/lib/middleware/commands.js +153 -1
- package/lib/middleware/config-chat.js +35 -0
- package/lib/middleware/learning.js +33 -0
- package/lib/middleware/proactive.js +22 -0
- package/lib/tools/contacts.js +44 -0
- package/lib/tools/games.js +44 -0
- package/lib/tools/notes.js +44 -0
- package/lib/tools/router.js +121 -0
- package/package.json +1 -1
package/lib/engine.js
CHANGED
|
@@ -62,10 +62,16 @@ export class SquidclawEngine {
|
|
|
62
62
|
pipeline.use('media', mediaMiddleware);
|
|
63
63
|
pipeline.use('auto-links', autoLinksMiddleware);
|
|
64
64
|
pipeline.use('auto-memory', autoMemoryMiddleware);
|
|
65
|
+
const { configChatMiddleware } = await import('./middleware/config-chat.js');
|
|
66
|
+
pipeline.use('config-chat', configChatMiddleware);
|
|
65
67
|
pipeline.use('skill-check', skillCheckMiddleware);
|
|
66
68
|
pipeline.use('typing', typingMiddleware); // wraps AI call with typing indicator
|
|
67
69
|
pipeline.use('ai-process', aiProcessMiddleware);
|
|
70
|
+
const { proactiveMiddleware } = await import('./middleware/proactive.js');
|
|
71
|
+
const { learningMiddleware } = await import('./middleware/learning.js');
|
|
68
72
|
pipeline.use('usage-alerts', usageAlertsMiddleware);
|
|
73
|
+
pipeline.use('proactive', proactiveMiddleware);
|
|
74
|
+
pipeline.use('learning', learningMiddleware);
|
|
69
75
|
pipeline.use('response-sender', responseSenderMiddleware);
|
|
70
76
|
|
|
71
77
|
return pipeline;
|
|
@@ -136,7 +142,13 @@ export class SquidclawEngine {
|
|
|
136
142
|
// 5. Features (reminders, auto-memory, usage alerts)
|
|
137
143
|
await this._initFeatures();
|
|
138
144
|
|
|
139
|
-
// 5b.
|
|
145
|
+
// 5b. Config Manager
|
|
146
|
+
try {
|
|
147
|
+
const { ConfigManager } = await import('./features/config-manager.js');
|
|
148
|
+
this.configManager = new ConfigManager(this);
|
|
149
|
+
} catch {}
|
|
150
|
+
|
|
151
|
+
// 5c. Sub-agents
|
|
140
152
|
try {
|
|
141
153
|
const { SubAgentManager } = await import('./features/sub-agents.js');
|
|
142
154
|
this.subAgents = new SubAgentManager(this);
|
|
@@ -183,6 +195,7 @@ export class SquidclawEngine {
|
|
|
183
195
|
this.knowledgeBase = new KnowledgeBase(this.storage, this.config);
|
|
184
196
|
this.toolRouter = new ToolRouter(this.config, this.knowledgeBase);
|
|
185
197
|
|
|
198
|
+
this.toolRouter.setContext(this, this.aiGateway, this.config.ai?.defaultModel);
|
|
186
199
|
for (const agent of agents) {
|
|
187
200
|
addToolSupport(agent, this.toolRouter, this.knowledgeBase);
|
|
188
201
|
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 🦑 Config Manager
|
|
3
|
+
* Hot reload, chat commands, .env support, profiles, validation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { loadConfig, saveConfig, getHome } from '../core/config.js';
|
|
7
|
+
import { readFileSync, writeFileSync, existsSync, copyFileSync } from 'fs';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
import { logger } from '../core/logger.js';
|
|
10
|
+
|
|
11
|
+
export class ConfigManager {
|
|
12
|
+
constructor(engine) {
|
|
13
|
+
this.engine = engine;
|
|
14
|
+
this.home = getHome();
|
|
15
|
+
this._loadEnv();
|
|
16
|
+
this._watchConfig();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// ── .env support ──
|
|
20
|
+
|
|
21
|
+
_loadEnv() {
|
|
22
|
+
const envPath = join(this.home, '.env');
|
|
23
|
+
if (!existsSync(envPath)) return;
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const lines = readFileSync(envPath, 'utf8').split('\n');
|
|
27
|
+
for (const line of lines) {
|
|
28
|
+
const trimmed = line.trim();
|
|
29
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
30
|
+
const eqIdx = trimmed.indexOf('=');
|
|
31
|
+
if (eqIdx === -1) continue;
|
|
32
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
33
|
+
const value = trimmed.slice(eqIdx + 1).trim().replace(/^["']|["']$/g, '');
|
|
34
|
+
process.env[key] = value;
|
|
35
|
+
}
|
|
36
|
+
logger.info('config', 'Loaded .env file');
|
|
37
|
+
} catch {}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ── Hot reload ──
|
|
41
|
+
|
|
42
|
+
_watchConfig() {
|
|
43
|
+
const configPath = join(this.home, 'config.json');
|
|
44
|
+
try {
|
|
45
|
+
const { watch } = require('fs');
|
|
46
|
+
let debounce = null;
|
|
47
|
+
watch(configPath, () => {
|
|
48
|
+
if (debounce) clearTimeout(debounce);
|
|
49
|
+
debounce = setTimeout(() => {
|
|
50
|
+
try {
|
|
51
|
+
this.engine.config = loadConfig();
|
|
52
|
+
logger.info('config', 'Config hot-reloaded');
|
|
53
|
+
} catch (err) {
|
|
54
|
+
logger.error('config', 'Hot reload failed: ' + err.message);
|
|
55
|
+
}
|
|
56
|
+
}, 1000);
|
|
57
|
+
});
|
|
58
|
+
} catch {
|
|
59
|
+
// fs.watch not available — use polling
|
|
60
|
+
this._pollConfig(configPath);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
_pollConfig(configPath) {
|
|
65
|
+
let lastMtime = 0;
|
|
66
|
+
try { lastMtime = readFileSync(configPath).length; } catch {}
|
|
67
|
+
|
|
68
|
+
setInterval(() => {
|
|
69
|
+
try {
|
|
70
|
+
const content = readFileSync(configPath, 'utf8');
|
|
71
|
+
if (content.length !== lastMtime) {
|
|
72
|
+
lastMtime = content.length;
|
|
73
|
+
this.engine.config = loadConfig();
|
|
74
|
+
logger.info('config', 'Config hot-reloaded (poll)');
|
|
75
|
+
}
|
|
76
|
+
} catch {}
|
|
77
|
+
}, 5000);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── Set config value ──
|
|
81
|
+
|
|
82
|
+
set(path, value) {
|
|
83
|
+
const config = loadConfig();
|
|
84
|
+
const parts = path.split('.');
|
|
85
|
+
let obj = config;
|
|
86
|
+
|
|
87
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
88
|
+
if (!obj[parts[i]]) obj[parts[i]] = {};
|
|
89
|
+
obj = obj[parts[i]];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Auto-convert types
|
|
93
|
+
if (value === 'true') value = true;
|
|
94
|
+
else if (value === 'false') value = false;
|
|
95
|
+
else if (!isNaN(value) && value !== '') value = Number(value);
|
|
96
|
+
|
|
97
|
+
obj[parts[parts.length - 1]] = value;
|
|
98
|
+
saveConfig(config);
|
|
99
|
+
this.engine.config = config;
|
|
100
|
+
|
|
101
|
+
logger.info('config', `Set ${path} = ${JSON.stringify(value)}`);
|
|
102
|
+
return { path, value };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ── Get config value ──
|
|
106
|
+
|
|
107
|
+
get(path) {
|
|
108
|
+
const config = loadConfig();
|
|
109
|
+
const parts = path.split('.');
|
|
110
|
+
let obj = config;
|
|
111
|
+
|
|
112
|
+
for (const part of parts) {
|
|
113
|
+
if (obj === undefined) return undefined;
|
|
114
|
+
obj = obj[part];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return obj;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ── Profiles ──
|
|
121
|
+
|
|
122
|
+
saveProfile(name) {
|
|
123
|
+
const configPath = join(this.home, 'config.json');
|
|
124
|
+
const profilePath = join(this.home, `config.${name}.json`);
|
|
125
|
+
copyFileSync(configPath, profilePath);
|
|
126
|
+
return profilePath;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
loadProfile(name) {
|
|
130
|
+
const profilePath = join(this.home, `config.${name}.json`);
|
|
131
|
+
if (!existsSync(profilePath)) throw new Error('Profile not found: ' + name);
|
|
132
|
+
|
|
133
|
+
const configPath = join(this.home, 'config.json');
|
|
134
|
+
copyFileSync(profilePath, configPath);
|
|
135
|
+
this.engine.config = loadConfig();
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
listProfiles() {
|
|
140
|
+
const { readdirSync } = require('fs');
|
|
141
|
+
return readdirSync(this.home)
|
|
142
|
+
.filter(f => f.startsWith('config.') && f.endsWith('.json') && f !== 'config.json')
|
|
143
|
+
.map(f => f.replace('config.', '').replace('.json', ''));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ── Validation ──
|
|
147
|
+
|
|
148
|
+
async validate() {
|
|
149
|
+
const config = loadConfig();
|
|
150
|
+
const results = [];
|
|
151
|
+
|
|
152
|
+
// Check AI providers
|
|
153
|
+
const providers = config.ai?.providers || {};
|
|
154
|
+
for (const [name, prov] of Object.entries(providers)) {
|
|
155
|
+
if (!prov.key || prov.key === 'local') continue;
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
const valid = await this._testProvider(name, prov.key);
|
|
159
|
+
results.push({ name, status: valid ? '✅' : '❌', message: valid ? 'Valid' : 'Invalid key' });
|
|
160
|
+
} catch (err) {
|
|
161
|
+
results.push({ name, status: '❌', message: err.message });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Check Telegram
|
|
166
|
+
if (config.channels?.telegram?.token) {
|
|
167
|
+
try {
|
|
168
|
+
const res = await fetch(`https://api.telegram.org/bot${config.channels.telegram.token}/getMe`);
|
|
169
|
+
const data = await res.json();
|
|
170
|
+
results.push({ name: 'Telegram', status: data.ok ? '✅' : '❌', message: data.ok ? '@' + data.result.username : 'Invalid token' });
|
|
171
|
+
} catch (err) {
|
|
172
|
+
results.push({ name: 'Telegram', status: '❌', message: err.message });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Check agents
|
|
177
|
+
const agentsDir = join(this.home, 'agents');
|
|
178
|
+
if (existsSync(agentsDir)) {
|
|
179
|
+
const { readdirSync } = require('fs');
|
|
180
|
+
const count = readdirSync(agentsDir, { withFileTypes: true }).filter(d => d.isDirectory()).length;
|
|
181
|
+
results.push({ name: 'Agents', status: '✅', message: count + ' configured' });
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return results;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async _testProvider(name, key) {
|
|
188
|
+
const urls = {
|
|
189
|
+
anthropic: 'https://api.anthropic.com/v1/models',
|
|
190
|
+
openai: 'https://api.openai.com/v1/models',
|
|
191
|
+
google: `https://generativelanguage.googleapis.com/v1beta/models?key=${key}`,
|
|
192
|
+
groq: 'https://api.groq.com/openai/v1/models',
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const url = urls[name];
|
|
196
|
+
if (!url) return true; // Can't validate, assume OK
|
|
197
|
+
|
|
198
|
+
const headers = {};
|
|
199
|
+
if (name === 'anthropic') {
|
|
200
|
+
headers['x-api-key'] = key;
|
|
201
|
+
headers['anthropic-version'] = '2023-06-01';
|
|
202
|
+
} else if (name !== 'google') {
|
|
203
|
+
headers['Authorization'] = 'Bearer ' + key;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const res = await fetch(url, { headers });
|
|
207
|
+
return res.ok;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ── Chat command parsing ──
|
|
211
|
+
|
|
212
|
+
static parseConfigCommand(message) {
|
|
213
|
+
const lower = message.toLowerCase().trim();
|
|
214
|
+
|
|
215
|
+
// "switch to gpt-4o" / "use gemini"
|
|
216
|
+
const modelMatch = lower.match(/(?:switch|change|use)\s+(?:to\s+)?(?:model\s+)?(.+)/);
|
|
217
|
+
if (modelMatch) {
|
|
218
|
+
const model = modelMatch[1].trim();
|
|
219
|
+
// Map common names to actual model IDs
|
|
220
|
+
const aliases = {
|
|
221
|
+
'gpt4': 'gpt-4o', 'gpt-4': 'gpt-4o', 'gpt4o': 'gpt-4o',
|
|
222
|
+
'claude': 'claude-sonnet-4-20250514', 'sonnet': 'claude-sonnet-4-20250514',
|
|
223
|
+
'opus': 'claude-opus-4', 'haiku': 'claude-haiku-3-5',
|
|
224
|
+
'gemini': 'gemini-2.5-flash', 'flash': 'gemini-2.5-flash',
|
|
225
|
+
'llama': 'llama-3.3-70b-versatile', 'deepseek': 'deepseek-chat',
|
|
226
|
+
};
|
|
227
|
+
return { action: 'set_model', model: aliases[model] || model };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// "set temperature to 0.8"
|
|
231
|
+
const setMatch = lower.match(/set\s+(\w[\w.]*)\s+(?:to\s+)?(.+)/);
|
|
232
|
+
if (setMatch) {
|
|
233
|
+
return { action: 'set', path: setMatch[1], value: setMatch[2] };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
@@ -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,14 @@ 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
|
-
'/
|
|
19
|
+
'/notes — notes',
|
|
20
|
+
'/contacts — contact book',
|
|
21
|
+
'/briefing — daily briefing',
|
|
22
|
+
'/summary — summarize chat',
|
|
23
|
+
'/trivia — trivia game',
|
|
24
|
+
'/flip — coin flip',
|
|
25
|
+
'/config — manage settings',
|
|
26
|
+
'/exec <cmd> — run a shell command',
|
|
20
27
|
'/files — list sandbox files',
|
|
21
28
|
'/subagents — list background tasks',
|
|
22
29
|
'/help — this message',
|
|
@@ -120,6 +127,151 @@ export async function commandsMiddleware(ctx, next) {
|
|
|
120
127
|
return;
|
|
121
128
|
}
|
|
122
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
|
+
|
|
210
|
+
if (cmd === '/config') {
|
|
211
|
+
const args = msg.slice(8).trim();
|
|
212
|
+
if (!args || args === 'help') {
|
|
213
|
+
await ctx.reply([
|
|
214
|
+
'⚙️ *Config Commands*', '',
|
|
215
|
+
'/config get <path> — show value',
|
|
216
|
+
'/config set <path> <value> — set value',
|
|
217
|
+
'/config test — validate all connections',
|
|
218
|
+
'/config profiles — list saved profiles',
|
|
219
|
+
'/config save <name> — save current config',
|
|
220
|
+
'/config load <name> — switch profile',
|
|
221
|
+
'', 'Or just say: "switch to gemini" 🦑',
|
|
222
|
+
].join('\n'));
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const cm = ctx.engine.configManager;
|
|
227
|
+
if (!cm) { await ctx.reply('❌ Config manager not available'); return; }
|
|
228
|
+
|
|
229
|
+
if (args.startsWith('get ')) {
|
|
230
|
+
const val = cm.get(args.slice(4).trim());
|
|
231
|
+
await ctx.reply('⚙️ ' + args.slice(4).trim() + ' = ' + JSON.stringify(val));
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (args.startsWith('set ')) {
|
|
236
|
+
const parts = args.slice(4).trim().split(/\s+/);
|
|
237
|
+
const path = parts[0];
|
|
238
|
+
const value = parts.slice(1).join(' ');
|
|
239
|
+
cm.set(path, value);
|
|
240
|
+
await ctx.reply('✅ Set *' + path + '* = ' + value);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (args === 'test' || args === 'validate') {
|
|
245
|
+
await ctx.reply('🔍 Validating connections...');
|
|
246
|
+
const results = await cm.validate();
|
|
247
|
+
const lines = results.map(r => r.status + ' *' + r.name + '* — ' + r.message);
|
|
248
|
+
await ctx.reply('⚙️ *Config Validation*\n\n' + lines.join('\n'));
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (args === 'profiles') {
|
|
253
|
+
const profiles = cm.listProfiles();
|
|
254
|
+
await ctx.reply(profiles.length ? '📋 Profiles: ' + profiles.join(', ') : '📋 No saved profiles');
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (args.startsWith('save ')) {
|
|
259
|
+
cm.saveProfile(args.slice(5).trim());
|
|
260
|
+
await ctx.reply('✅ Profile saved: ' + args.slice(5).trim());
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (args.startsWith('load ')) {
|
|
265
|
+
try {
|
|
266
|
+
cm.loadProfile(args.slice(5).trim());
|
|
267
|
+
await ctx.reply('✅ Profile loaded: ' + args.slice(5).trim() + '\n⚠️ Restart recommended');
|
|
268
|
+
} catch (err) {
|
|
269
|
+
await ctx.reply('❌ ' + err.message);
|
|
270
|
+
}
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
123
275
|
if (cmd === '/exec') {
|
|
124
276
|
const command = msg.slice(6).trim();
|
|
125
277
|
if (!command) { await ctx.reply('Usage: /exec <command>'); return; }
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handle config changes via natural language
|
|
3
|
+
* "switch to gemini", "use gpt-4o", "set temperature to 0.8"
|
|
4
|
+
*/
|
|
5
|
+
import { logger } from '../core/logger.js';
|
|
6
|
+
|
|
7
|
+
export async function configChatMiddleware(ctx, next) {
|
|
8
|
+
if (!ctx.engine.configManager) { await next(); return; }
|
|
9
|
+
|
|
10
|
+
const { ConfigManager } = await import('../features/config-manager.js');
|
|
11
|
+
const parsed = ConfigManager.parseConfigCommand(ctx.message);
|
|
12
|
+
|
|
13
|
+
if (!parsed) { await next(); return; }
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
if (parsed.action === 'set_model') {
|
|
17
|
+
ctx.engine.configManager.set('ai.defaultModel', parsed.model);
|
|
18
|
+
// Update agent's model too
|
|
19
|
+
if (ctx.agent) ctx.agent.model = parsed.model;
|
|
20
|
+
await ctx.reply('✅ Model switched to *' + parsed.model + '*\n\nI will use this model from now on 🦑');
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (parsed.action === 'set') {
|
|
25
|
+
ctx.engine.configManager.set(parsed.path, parsed.value);
|
|
26
|
+
await ctx.reply('✅ Set *' + parsed.path + '* = ' + parsed.value);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
} catch (err) {
|
|
30
|
+
await ctx.reply('❌ Config error: ' + err.message);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
await next();
|
|
35
|
+
}
|
|
@@ -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
|
+
}
|
package/lib/tools/router.js
CHANGED
|
@@ -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': {
|