squidclaw 2.3.0 β 2.5.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 -1
- package/lib/engine.js +12 -0
- package/lib/features/cron.js +217 -0
- package/lib/features/reminders.js +2 -1
- package/lib/features/sessions.js +269 -0
- package/lib/middleware/commands.js +58 -0
- package/lib/middleware/response-sender.js +2 -1
- package/lib/tools/excel.js +136 -0
- package/lib/tools/html.js +148 -0
- package/lib/tools/pdf.js +133 -0
- package/lib/tools/router.js +207 -7
- package/package.json +3 -1
|
@@ -73,8 +73,9 @@ export function addToolSupport(agent, toolRouter, knowledgeBase) {
|
|
|
73
73
|
return result;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
// File attachment (pptx,
|
|
76
|
+
// File attachment (pptx, excel, pdf, html)
|
|
77
77
|
if (toolResult.toolUsed && toolResult.filePath) {
|
|
78
|
+
logger.info('agent', 'FILE TOOL: ' + toolResult.toolName + ' -> ' + toolResult.filePath);
|
|
78
79
|
result.filePath = toolResult.filePath;
|
|
79
80
|
result.fileName = toolResult.fileName;
|
|
80
81
|
result.messages = [toolResult.toolResult || 'Here\'s your file! π'];
|
package/lib/engine.js
CHANGED
|
@@ -226,6 +226,18 @@ export class SquidclawEngine {
|
|
|
226
226
|
if (pending.c > 0) console.log(` β° Reminders: ${pending.c} pending`);
|
|
227
227
|
} catch {}
|
|
228
228
|
|
|
229
|
+
// Sessions
|
|
230
|
+
try {
|
|
231
|
+
const { SessionManager } = await import('./features/sessions.js');
|
|
232
|
+
this.sessions = new SessionManager(this.storage, this);
|
|
233
|
+
} catch (err) { logger.error('engine', 'Sessions init failed: ' + err.message); }
|
|
234
|
+
|
|
235
|
+
// Cron jobs
|
|
236
|
+
try {
|
|
237
|
+
const { CronManager } = await import('./features/cron.js');
|
|
238
|
+
this.cron = new CronManager(this.storage, this);
|
|
239
|
+
} catch (err) { logger.error('engine', 'Cron init failed: ' + err.message); }
|
|
240
|
+
|
|
229
241
|
// Auto-memory
|
|
230
242
|
try {
|
|
231
243
|
const { AutoMemory } = await import('./features/auto-memory.js');
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* π¦ Cron System
|
|
3
|
+
* Persistent scheduled jobs β survives restarts
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { logger } from '../core/logger.js';
|
|
7
|
+
|
|
8
|
+
export class CronManager {
|
|
9
|
+
constructor(storage, engine) {
|
|
10
|
+
this.storage = storage;
|
|
11
|
+
this.engine = engine;
|
|
12
|
+
this.timers = new Map();
|
|
13
|
+
this._initDb();
|
|
14
|
+
this._loadJobs();
|
|
15
|
+
this._startTicker();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
_initDb() {
|
|
19
|
+
this.storage.db.exec(`
|
|
20
|
+
CREATE TABLE IF NOT EXISTS cron_jobs (
|
|
21
|
+
id TEXT PRIMARY KEY,
|
|
22
|
+
agent_id TEXT NOT NULL,
|
|
23
|
+
contact_id TEXT NOT NULL,
|
|
24
|
+
name TEXT NOT NULL,
|
|
25
|
+
schedule TEXT NOT NULL,
|
|
26
|
+
action TEXT NOT NULL,
|
|
27
|
+
action_data TEXT DEFAULT '',
|
|
28
|
+
platform TEXT DEFAULT 'telegram',
|
|
29
|
+
metadata TEXT DEFAULT '{}',
|
|
30
|
+
enabled INTEGER DEFAULT 1,
|
|
31
|
+
last_run TEXT,
|
|
32
|
+
next_run TEXT,
|
|
33
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
34
|
+
)
|
|
35
|
+
`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Schedule types:
|
|
40
|
+
* - every:5m / every:1h / every:24h β interval
|
|
41
|
+
* - daily:09:00 β every day at HH:MM UTC
|
|
42
|
+
* - weekly:fri:09:00 β every week on day at HH:MM UTC
|
|
43
|
+
* - monthly:1:09:00 β every month on day at HH:MM UTC
|
|
44
|
+
* - cron:* * * * * β standard cron expression (min hour dom mon dow)
|
|
45
|
+
*/
|
|
46
|
+
add(agentId, contactId, name, schedule, action, actionData, platform, metadata) {
|
|
47
|
+
const id = 'cron_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
|
|
48
|
+
const nextRun = this._calcNextRun(schedule);
|
|
49
|
+
|
|
50
|
+
this.storage.db.prepare(
|
|
51
|
+
'INSERT INTO cron_jobs (id, agent_id, contact_id, name, schedule, action, action_data, platform, metadata, next_run) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
|
|
52
|
+
).run(id, agentId, contactId, name, schedule, action, actionData || '', platform || 'telegram', JSON.stringify(metadata || {}), nextRun);
|
|
53
|
+
|
|
54
|
+
logger.info('cron', `Added job ${id}: "${name}" schedule=${schedule} next=${nextRun}`);
|
|
55
|
+
return { id, name, schedule, nextRun };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
list(agentId, contactId) {
|
|
59
|
+
return this.storage.db.prepare(
|
|
60
|
+
'SELECT * FROM cron_jobs WHERE agent_id = ? AND contact_id = ? ORDER BY created_at'
|
|
61
|
+
).all(agentId, contactId);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
listAll(agentId) {
|
|
65
|
+
return this.storage.db.prepare(
|
|
66
|
+
'SELECT * FROM cron_jobs WHERE agent_id = ? ORDER BY created_at'
|
|
67
|
+
).all(agentId);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
remove(id) {
|
|
71
|
+
this.storage.db.prepare('DELETE FROM cron_jobs WHERE id = ?').run(id);
|
|
72
|
+
logger.info('cron', `Removed job ${id}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
toggle(id, enabled) {
|
|
76
|
+
this.storage.db.prepare('UPDATE cron_jobs SET enabled = ? WHERE id = ?').run(enabled ? 1 : 0, id);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ββ Ticker β runs every 60 seconds ββ
|
|
80
|
+
|
|
81
|
+
_startTicker() {
|
|
82
|
+
this._tick(); // run once immediately
|
|
83
|
+
this._interval = setInterval(() => this._tick(), 60000);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
_tick() {
|
|
87
|
+
const now = new Date().toISOString().slice(0, 16);
|
|
88
|
+
const due = this.storage.db.prepare(
|
|
89
|
+
"SELECT * FROM cron_jobs WHERE enabled = 1 AND next_run <= ?"
|
|
90
|
+
).all(now);
|
|
91
|
+
|
|
92
|
+
for (const job of due) {
|
|
93
|
+
this._execute(job);
|
|
94
|
+
// Update next_run
|
|
95
|
+
const nextRun = this._calcNextRun(job.schedule);
|
|
96
|
+
this.storage.db.prepare(
|
|
97
|
+
'UPDATE cron_jobs SET last_run = ?, next_run = ? WHERE id = ?'
|
|
98
|
+
).run(now, nextRun, job.id);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async _execute(job) {
|
|
103
|
+
logger.info('cron', `Executing job ${job.id}: "${job.name}"`);
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
const metadata = JSON.parse(job.metadata || '{}');
|
|
107
|
+
|
|
108
|
+
if (job.action === 'message') {
|
|
109
|
+
// Send a message
|
|
110
|
+
const msg = 'π *Scheduled: ' + job.name + '*\n\n' + (job.action_data || '');
|
|
111
|
+
await this._send(job.agent_id, job.contact_id, msg, job.platform, metadata);
|
|
112
|
+
}
|
|
113
|
+
else if (job.action === 'briefing') {
|
|
114
|
+
// Send daily briefing
|
|
115
|
+
try {
|
|
116
|
+
const { generateBriefing } = await import('./daily-briefing.js');
|
|
117
|
+
const briefing = await generateBriefing(this.engine, job.agent_id);
|
|
118
|
+
await this._send(job.agent_id, job.contact_id, briefing, job.platform, metadata);
|
|
119
|
+
} catch (err) {
|
|
120
|
+
await this._send(job.agent_id, job.contact_id, 'βοΈ Good morning! (Briefing failed: ' + err.message + ')', job.platform, metadata);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
else if (job.action === 'ai') {
|
|
124
|
+
// Run AI prompt and send result
|
|
125
|
+
const messages = [
|
|
126
|
+
{ role: 'system', content: 'You are a helpful assistant. Complete this task concisely.' },
|
|
127
|
+
{ role: 'user', content: job.action_data },
|
|
128
|
+
];
|
|
129
|
+
const response = await this.engine.aiGateway.chat(messages, {
|
|
130
|
+
model: this.engine.config.ai?.defaultModel,
|
|
131
|
+
});
|
|
132
|
+
await this._send(job.agent_id, job.contact_id, response.content, job.platform, metadata);
|
|
133
|
+
}
|
|
134
|
+
else if (job.action === 'remind') {
|
|
135
|
+
await this._send(job.agent_id, job.contact_id, 'β° *Reminder:* ' + (job.action_data || job.name), job.platform, metadata);
|
|
136
|
+
}
|
|
137
|
+
} catch (err) {
|
|
138
|
+
logger.error('cron', `Job ${job.id} failed: ${err.message}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async _send(agentId, contactId, message, platform, metadata) {
|
|
143
|
+
if (platform === 'telegram' && this.engine.telegramManager) {
|
|
144
|
+
await this.engine.telegramManager.sendMessage(agentId, contactId, message, metadata);
|
|
145
|
+
} else if (this.engine.whatsappManager) {
|
|
146
|
+
await this.engine.whatsappManager.sendMessage(agentId, contactId, message);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ββ Schedule Parser ββ
|
|
151
|
+
|
|
152
|
+
_calcNextRun(schedule) {
|
|
153
|
+
const now = new Date();
|
|
154
|
+
|
|
155
|
+
// every:5m / every:1h / every:24h
|
|
156
|
+
const everyMatch = schedule.match(/^every:(\d+)(m|h|d)$/);
|
|
157
|
+
if (everyMatch) {
|
|
158
|
+
const num = parseInt(everyMatch[1]);
|
|
159
|
+
const unit = everyMatch[2];
|
|
160
|
+
const ms = unit === 'm' ? num * 60000 : unit === 'h' ? num * 3600000 : num * 86400000;
|
|
161
|
+
return new Date(now.getTime() + ms).toISOString().slice(0, 16);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// daily:09:00
|
|
165
|
+
const dailyMatch = schedule.match(/^daily:(\d{2}):(\d{2})$/);
|
|
166
|
+
if (dailyMatch) {
|
|
167
|
+
const target = new Date(now);
|
|
168
|
+
target.setUTCHours(parseInt(dailyMatch[1]), parseInt(dailyMatch[2]), 0, 0);
|
|
169
|
+
if (target <= now) target.setDate(target.getDate() + 1);
|
|
170
|
+
return target.toISOString().slice(0, 16);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// weekly:fri:09:00
|
|
174
|
+
const weeklyMatch = schedule.match(/^weekly:(\w{3}):(\d{2}):(\d{2})$/);
|
|
175
|
+
if (weeklyMatch) {
|
|
176
|
+
const days = { sun: 0, mon: 1, tue: 2, wed: 3, thu: 4, fri: 5, sat: 6 };
|
|
177
|
+
const targetDay = days[weeklyMatch[1].toLowerCase()] ?? 0;
|
|
178
|
+
const target = new Date(now);
|
|
179
|
+
target.setUTCHours(parseInt(weeklyMatch[2]), parseInt(weeklyMatch[3]), 0, 0);
|
|
180
|
+
let diff = targetDay - target.getUTCDay();
|
|
181
|
+
if (diff < 0 || (diff === 0 && target <= now)) diff += 7;
|
|
182
|
+
target.setDate(target.getDate() + diff);
|
|
183
|
+
return target.toISOString().slice(0, 16);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// monthly:1:09:00
|
|
187
|
+
const monthlyMatch = schedule.match(/^monthly:(\d+):(\d{2}):(\d{2})$/);
|
|
188
|
+
if (monthlyMatch) {
|
|
189
|
+
const target = new Date(now);
|
|
190
|
+
target.setUTCDate(parseInt(monthlyMatch[1]));
|
|
191
|
+
target.setUTCHours(parseInt(monthlyMatch[2]), parseInt(monthlyMatch[3]), 0, 0);
|
|
192
|
+
if (target <= now) target.setMonth(target.getMonth() + 1);
|
|
193
|
+
return target.toISOString().slice(0, 16);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// cron expression (basic: min hour dom mon dow)
|
|
197
|
+
const cronMatch = schedule.match(/^cron:(.+)$/);
|
|
198
|
+
if (cronMatch) {
|
|
199
|
+
// Simple next-minute for cron β full parser would be heavy
|
|
200
|
+
return new Date(now.getTime() + 60000).toISOString().slice(0, 16);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Default: 1 hour
|
|
204
|
+
return new Date(now.getTime() + 3600000).toISOString().slice(0, 16);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
_loadJobs() {
|
|
208
|
+
const count = this.storage.db.prepare("SELECT COUNT(*) as c FROM cron_jobs WHERE enabled = 1").get()?.c || 0;
|
|
209
|
+
if (count > 0) {
|
|
210
|
+
logger.info('cron', `Loaded ${count} active cron jobs`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
stop() {
|
|
215
|
+
if (this._interval) clearInterval(this._interval);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
@@ -68,7 +68,7 @@ export class ReminderManager {
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
async _fire(reminder) {
|
|
71
|
-
logger.info('reminders',
|
|
71
|
+
logger.info('reminders', `π FIRING reminder ${reminder.id} for ${reminder.contact_id}: ${reminder.message}`);
|
|
72
72
|
|
|
73
73
|
// Mark as fired
|
|
74
74
|
try {
|
|
@@ -95,6 +95,7 @@ export class ReminderManager {
|
|
|
95
95
|
* @param {object} metadata - chat metadata for sending
|
|
96
96
|
*/
|
|
97
97
|
add(agentId, contactId, message, fireAt, platform = 'telegram', metadata = {}) {
|
|
98
|
+
logger.info('reminders', `π ADDING reminder for ${contactId}: ${message} at ${fireAt}`);
|
|
98
99
|
const id = 'rem_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
|
|
99
100
|
const fireAtStr = typeof fireAt === 'string' ? fireAt : fireAt.toISOString().replace('Z', '');
|
|
100
101
|
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* π¦ Session Manager
|
|
3
|
+
* Multiple isolated conversations per agent
|
|
4
|
+
* Types: main (default), isolated (fresh context), thread (linked to parent)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { logger } from '../core/logger.js';
|
|
8
|
+
import crypto from 'crypto';
|
|
9
|
+
|
|
10
|
+
export class SessionManager {
|
|
11
|
+
constructor(storage, engine) {
|
|
12
|
+
this.storage = storage;
|
|
13
|
+
this.engine = engine;
|
|
14
|
+
this.active = new Map(); // sessionKey -> session
|
|
15
|
+
this._initDb();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
_initDb() {
|
|
19
|
+
this.storage.db.exec(`
|
|
20
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
21
|
+
id TEXT PRIMARY KEY,
|
|
22
|
+
agent_id TEXT NOT NULL,
|
|
23
|
+
contact_id TEXT NOT NULL,
|
|
24
|
+
type TEXT DEFAULT 'main',
|
|
25
|
+
label TEXT,
|
|
26
|
+
parent_id TEXT,
|
|
27
|
+
model TEXT,
|
|
28
|
+
system_prompt TEXT,
|
|
29
|
+
status TEXT DEFAULT 'active',
|
|
30
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
31
|
+
updated_at TEXT DEFAULT (datetime('now')),
|
|
32
|
+
metadata TEXT DEFAULT '{}'
|
|
33
|
+
);
|
|
34
|
+
CREATE TABLE IF NOT EXISTS session_messages (
|
|
35
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
36
|
+
session_id TEXT NOT NULL,
|
|
37
|
+
role TEXT NOT NULL,
|
|
38
|
+
content TEXT NOT NULL,
|
|
39
|
+
tokens INTEGER DEFAULT 0,
|
|
40
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
41
|
+
FOREIGN KEY (session_id) REFERENCES sessions(id)
|
|
42
|
+
);
|
|
43
|
+
CREATE INDEX IF NOT EXISTS idx_session_messages_sid ON session_messages(session_id);
|
|
44
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_agent ON sessions(agent_id, contact_id);
|
|
45
|
+
`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ββ Session CRUD ββ
|
|
49
|
+
|
|
50
|
+
create(agentId, contactId, options = {}) {
|
|
51
|
+
const id = 'sess_' + crypto.randomBytes(6).toString('hex');
|
|
52
|
+
const type = options.type || 'main';
|
|
53
|
+
const label = options.label || null;
|
|
54
|
+
const parentId = options.parentId || null;
|
|
55
|
+
const model = options.model || null;
|
|
56
|
+
const systemPrompt = options.systemPrompt || null;
|
|
57
|
+
|
|
58
|
+
this.storage.db.prepare(
|
|
59
|
+
'INSERT INTO sessions (id, agent_id, contact_id, type, label, parent_id, model, system_prompt, metadata) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)'
|
|
60
|
+
).run(id, agentId, contactId, type, label, parentId, model, systemPrompt, JSON.stringify(options.metadata || {}));
|
|
61
|
+
|
|
62
|
+
const session = { id, agentId, contactId, type, label, parentId, model, systemPrompt, status: 'active' };
|
|
63
|
+
this.active.set(id, session);
|
|
64
|
+
|
|
65
|
+
logger.info('sessions', `Created ${type} session ${id}${label ? ' [' + label + ']' : ''}`);
|
|
66
|
+
return session;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
get(sessionId) {
|
|
70
|
+
const cached = this.active.get(sessionId);
|
|
71
|
+
if (cached) return cached;
|
|
72
|
+
|
|
73
|
+
const row = this.storage.db.prepare('SELECT * FROM sessions WHERE id = ?').get(sessionId);
|
|
74
|
+
if (row) {
|
|
75
|
+
this.active.set(row.id, row);
|
|
76
|
+
return row;
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Get or create main session for a contact
|
|
83
|
+
*/
|
|
84
|
+
getMain(agentId, contactId) {
|
|
85
|
+
let session = this.storage.db.prepare(
|
|
86
|
+
"SELECT * FROM sessions WHERE agent_id = ? AND contact_id = ? AND type = 'main' AND status = 'active' LIMIT 1"
|
|
87
|
+
).get(agentId, contactId);
|
|
88
|
+
|
|
89
|
+
if (!session) {
|
|
90
|
+
session = this.create(agentId, contactId, { type: 'main', label: 'Main' });
|
|
91
|
+
}
|
|
92
|
+
return session;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Spawn an isolated session
|
|
97
|
+
*/
|
|
98
|
+
spawn(agentId, contactId, options = {}) {
|
|
99
|
+
return this.create(agentId, contactId, {
|
|
100
|
+
type: options.type || 'isolated',
|
|
101
|
+
label: options.label || options.task?.slice(0, 50),
|
|
102
|
+
parentId: options.parentId,
|
|
103
|
+
model: options.model,
|
|
104
|
+
systemPrompt: options.systemPrompt || (options.task ?
|
|
105
|
+
'You are a focused sub-agent. Complete this task concisely:\n\n' + options.task : null),
|
|
106
|
+
metadata: { task: options.task, timeout: options.timeout },
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* List sessions for a contact
|
|
112
|
+
*/
|
|
113
|
+
list(agentId, contactId, options = {}) {
|
|
114
|
+
let query = 'SELECT * FROM sessions WHERE agent_id = ?';
|
|
115
|
+
const params = [agentId];
|
|
116
|
+
|
|
117
|
+
if (contactId) {
|
|
118
|
+
query += ' AND contact_id = ?';
|
|
119
|
+
params.push(contactId);
|
|
120
|
+
}
|
|
121
|
+
if (options.type) {
|
|
122
|
+
query += ' AND type = ?';
|
|
123
|
+
params.push(options.type);
|
|
124
|
+
}
|
|
125
|
+
if (options.status || !options.includeEnded) {
|
|
126
|
+
query += ' AND status = ?';
|
|
127
|
+
params.push(options.status || 'active');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
query += ' ORDER BY created_at DESC';
|
|
131
|
+
if (options.limit) {
|
|
132
|
+
query += ' LIMIT ?';
|
|
133
|
+
params.push(options.limit);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return this.storage.db.prepare(query).all(...params);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* End a session
|
|
141
|
+
*/
|
|
142
|
+
end(sessionId) {
|
|
143
|
+
this.storage.db.prepare("UPDATE sessions SET status = 'ended', updated_at = datetime('now') WHERE id = ?").run(sessionId);
|
|
144
|
+
this.active.delete(sessionId);
|
|
145
|
+
logger.info('sessions', `Ended session ${sessionId}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ββ Session Messages ββ
|
|
149
|
+
|
|
150
|
+
addMessage(sessionId, role, content, tokens = 0) {
|
|
151
|
+
this.storage.db.prepare(
|
|
152
|
+
'INSERT INTO session_messages (session_id, role, content, tokens) VALUES (?, ?, ?, ?)'
|
|
153
|
+
).run(sessionId, role, content, tokens);
|
|
154
|
+
|
|
155
|
+
this.storage.db.prepare("UPDATE sessions SET updated_at = datetime('now') WHERE id = ?").run(sessionId);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
getHistory(sessionId, limit = 50) {
|
|
159
|
+
return this.storage.db.prepare(
|
|
160
|
+
'SELECT role, content, created_at FROM session_messages WHERE session_id = ? ORDER BY created_at ASC LIMIT ?'
|
|
161
|
+
).all(sessionId, limit);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
clearHistory(sessionId) {
|
|
165
|
+
this.storage.db.prepare('DELETE FROM session_messages WHERE session_id = ?').run(sessionId);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ββ Session AI Processing ββ
|
|
169
|
+
|
|
170
|
+
async process(sessionId, message) {
|
|
171
|
+
const session = this.get(sessionId);
|
|
172
|
+
if (!session) throw new Error('Session not found: ' + sessionId);
|
|
173
|
+
|
|
174
|
+
// Save user message
|
|
175
|
+
this.addMessage(sessionId, 'user', message);
|
|
176
|
+
|
|
177
|
+
// Build messages array
|
|
178
|
+
const history = this.getHistory(sessionId);
|
|
179
|
+
const model = session.model || this.engine.config.ai?.defaultModel;
|
|
180
|
+
|
|
181
|
+
// Get system prompt
|
|
182
|
+
let systemPrompt = session.system_prompt || session.systemPrompt;
|
|
183
|
+
if (!systemPrompt && session.type === 'main') {
|
|
184
|
+
// Use agent's normal prompt for main sessions
|
|
185
|
+
const agent = this.engine.agents?.get(session.agent_id);
|
|
186
|
+
if (agent?.promptBuilder) {
|
|
187
|
+
systemPrompt = await agent.promptBuilder.build(agent, session.contact_id, message);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const messages = [];
|
|
192
|
+
if (systemPrompt) messages.push({ role: 'system', content: systemPrompt });
|
|
193
|
+
messages.push(...history.map(h => ({ role: h.role === 'system' ? 'user' : h.role, content: h.content })));
|
|
194
|
+
|
|
195
|
+
// Call AI
|
|
196
|
+
const response = await this.engine.aiGateway.chat(messages, {
|
|
197
|
+
model,
|
|
198
|
+
fallbackChain: this.engine.config.ai?.fallbackChain,
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// Save assistant response
|
|
202
|
+
this.addMessage(sessionId, 'assistant', response.content, response.outputTokens);
|
|
203
|
+
|
|
204
|
+
// Track usage
|
|
205
|
+
await this.storage.trackUsage(session.agent_id, response.model, response.inputTokens, response.outputTokens, response.costUsd);
|
|
206
|
+
|
|
207
|
+
logger.info('sessions', `Session ${sessionId} processed (${response.model}, ${response.outputTokens} tokens)`);
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
content: response.content,
|
|
211
|
+
model: response.model,
|
|
212
|
+
tokens: response.inputTokens + response.outputTokens,
|
|
213
|
+
cost: response.costUsd,
|
|
214
|
+
sessionId,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ββ Sub-agent spawning with auto-completion ββ
|
|
219
|
+
|
|
220
|
+
async runTask(agentId, contactId, task, options = {}) {
|
|
221
|
+
const session = this.spawn(agentId, contactId, {
|
|
222
|
+
type: 'isolated',
|
|
223
|
+
task,
|
|
224
|
+
model: options.model,
|
|
225
|
+
label: options.label || task.slice(0, 50),
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
const timeout = options.timeout || 60000;
|
|
229
|
+
|
|
230
|
+
try {
|
|
231
|
+
const result = await Promise.race([
|
|
232
|
+
this.process(session.id, task),
|
|
233
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('Task timeout')), timeout)),
|
|
234
|
+
]);
|
|
235
|
+
|
|
236
|
+
this.end(session.id);
|
|
237
|
+
return { ...result, status: 'complete' };
|
|
238
|
+
} catch (err) {
|
|
239
|
+
this.end(session.id);
|
|
240
|
+
return { content: 'Task failed: ' + err.message, status: 'error', sessionId: session.id };
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ββ Send message to another session ββ
|
|
245
|
+
|
|
246
|
+
async send(targetSessionId, message) {
|
|
247
|
+
const session = this.get(targetSessionId);
|
|
248
|
+
if (!session) throw new Error('Session not found');
|
|
249
|
+
return this.process(targetSessionId, message);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ββ Stats ββ
|
|
253
|
+
|
|
254
|
+
getStats(agentId) {
|
|
255
|
+
const active = this.storage.db.prepare(
|
|
256
|
+
"SELECT COUNT(*) as c FROM sessions WHERE agent_id = ? AND status = 'active'"
|
|
257
|
+
).get(agentId)?.c || 0;
|
|
258
|
+
|
|
259
|
+
const total = this.storage.db.prepare(
|
|
260
|
+
'SELECT COUNT(*) as c FROM sessions WHERE agent_id = ?'
|
|
261
|
+
).get(agentId)?.c || 0;
|
|
262
|
+
|
|
263
|
+
const messages = this.storage.db.prepare(
|
|
264
|
+
'SELECT COUNT(*) as c FROM session_messages sm JOIN sessions s ON sm.session_id = s.id WHERE s.agent_id = ?'
|
|
265
|
+
).get(agentId)?.c || 0;
|
|
266
|
+
|
|
267
|
+
return { active, total, messages };
|
|
268
|
+
}
|
|
269
|
+
}
|
|
@@ -145,6 +145,64 @@ export async function commandsMiddleware(ctx, next) {
|
|
|
145
145
|
return;
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
if (cmd === '/sessions') {
|
|
149
|
+
if (!ctx.engine.sessions) { await ctx.reply('β Sessions not available'); return; }
|
|
150
|
+
const args = msg.slice(10).trim();
|
|
151
|
+
|
|
152
|
+
if (args.startsWith('new ')) {
|
|
153
|
+
const task = args.slice(4).trim();
|
|
154
|
+
await ctx.reply('π Spawning session: ' + task.slice(0, 50) + '...');
|
|
155
|
+
const result = await ctx.engine.sessions.runTask(ctx.agentId, ctx.contactId, task);
|
|
156
|
+
await ctx.reply('β
*Session Complete*\n\n' + result.content.slice(0, 2000));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (args === 'stats') {
|
|
161
|
+
const stats = ctx.engine.sessions.getStats(ctx.agentId);
|
|
162
|
+
await ctx.reply('π *Sessions*\n\nπ’ Active: ' + stats.active + '\nπ Total: ' + stats.total + '\nπ¬ Messages: ' + stats.messages);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (args.startsWith('end ')) {
|
|
167
|
+
ctx.engine.sessions.end(args.slice(4).trim());
|
|
168
|
+
await ctx.reply('β
Session ended');
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const sessions = ctx.engine.sessions.list(ctx.agentId, ctx.contactId, { limit: 10 });
|
|
173
|
+
if (sessions.length === 0) { await ctx.reply('π No active sessions'); return; }
|
|
174
|
+
const lines = sessions.map(s =>
|
|
175
|
+
(s.status === 'active' ? 'π’' : 'β«') + ' *' + (s.label || 'Untitled') + '*\n Type: ' + s.type + '\n ID: `' + s.id + '`'
|
|
176
|
+
);
|
|
177
|
+
await ctx.reply('π *Sessions*\n\n' + lines.join('\n\n') + '\n\n/sessions new <task> β spawn\n/sessions end <id> β close\n/sessions stats');
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (cmd === '/cron') {
|
|
182
|
+
const args = msg.slice(6).trim();
|
|
183
|
+
if (!ctx.engine.cron) { await ctx.reply('β Cron not available'); return; }
|
|
184
|
+
|
|
185
|
+
if (!args || args === 'list') {
|
|
186
|
+
const jobs = ctx.engine.cron.listAll(ctx.agentId);
|
|
187
|
+
if (jobs.length === 0) { await ctx.reply('β° No scheduled jobs'); return; }
|
|
188
|
+
const lines = jobs.map(j =>
|
|
189
|
+
(j.enabled ? 'β
' : 'βΈοΈ') + ' *' + j.name + '*\n ' + j.schedule + ' β ' + j.action + '\n Next: ' + (j.next_run || '?') + '\n ID: `' + j.id + '`'
|
|
190
|
+
);
|
|
191
|
+
await ctx.reply('β° *Cron Jobs*\n\n' + lines.join('\n\n'));
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (args.startsWith('remove ') || args.startsWith('delete ')) {
|
|
196
|
+
const id = args.split(' ')[1];
|
|
197
|
+
ctx.engine.cron.remove(id);
|
|
198
|
+
await ctx.reply('β
Removed cron job ' + id);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
await ctx.reply('Usage:\n/cron β list jobs\n/cron remove <id> β delete job\n\nOr tell me naturally: "Every morning at 9am send me a briefing"');
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
148
206
|
if (cmd === '/allow') {
|
|
149
207
|
const args = msg.slice(7).trim();
|
|
150
208
|
if (!args) { await ctx.reply('Usage: /allow <user_id or phone>'); return; }
|
|
@@ -29,8 +29,9 @@ export async function responseSenderMiddleware(ctx, next) {
|
|
|
29
29
|
|
|
30
30
|
// Send via appropriate channel
|
|
31
31
|
if (ctx.platform === 'telegram' && tm) {
|
|
32
|
-
// Send file attachment (pptx,
|
|
32
|
+
// Send file attachment (pptx, excel, pdf, html)
|
|
33
33
|
if (response.filePath) {
|
|
34
|
+
logger.info('sender', 'SENDING FILE: ' + response.filePath);
|
|
34
35
|
try {
|
|
35
36
|
await tm.sendDocument(agentId, contactId, response.filePath, response.fileName, response.messages?.[0] || '');
|
|
36
37
|
await next();
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* π¦ Excel Generator
|
|
3
|
+
* Create .xlsx spreadsheets
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { logger } from '../core/logger.js';
|
|
7
|
+
import { mkdirSync } from 'fs';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
|
|
10
|
+
export class ExcelGenerator {
|
|
11
|
+
constructor() {
|
|
12
|
+
this.outputDir = '/tmp/squidclaw-files';
|
|
13
|
+
mkdirSync(this.outputDir, { recursive: true });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async create(title, sheets, options = {}) {
|
|
17
|
+
const ExcelJS = (await import('exceljs')).default;
|
|
18
|
+
const wb = new ExcelJS.Workbook();
|
|
19
|
+
wb.creator = 'Squidclaw AI π¦';
|
|
20
|
+
wb.created = new Date();
|
|
21
|
+
|
|
22
|
+
const themes = {
|
|
23
|
+
blue: { header: '2563EB', headerFont: 'FFFFFF', alt: 'EFF6FF', border: 'BFDBFE' },
|
|
24
|
+
green: { header: '059669', headerFont: 'FFFFFF', alt: 'ECFDF5', border: 'A7F3D0' },
|
|
25
|
+
dark: { header: '1F2937', headerFont: 'FFFFFF', alt: 'F3F4F6', border: 'D1D5DB' },
|
|
26
|
+
red: { header: 'DC2626', headerFont: 'FFFFFF', alt: 'FEF2F2', border: 'FECACA' },
|
|
27
|
+
saudi: { header: '166534', headerFont: 'FFFFFF', alt: 'F0FDF4', border: 'BBF7D0' },
|
|
28
|
+
corporate: { header: '1E40AF', headerFont: 'FFFFFF', alt: 'F8FAFC', border: 'CBD5E1' },
|
|
29
|
+
};
|
|
30
|
+
const theme = themes[options.theme] || themes.corporate;
|
|
31
|
+
|
|
32
|
+
for (const sheet of sheets) {
|
|
33
|
+
const ws = wb.addWorksheet(sheet.name || 'Sheet');
|
|
34
|
+
|
|
35
|
+
if (!sheet.rows || sheet.rows.length === 0) continue;
|
|
36
|
+
|
|
37
|
+
// Auto-detect columns from first row
|
|
38
|
+
const headers = sheet.rows[0];
|
|
39
|
+
|
|
40
|
+
// Set columns with auto-width
|
|
41
|
+
ws.columns = headers.map((h, i) => ({
|
|
42
|
+
header: String(h),
|
|
43
|
+
key: 'col' + i,
|
|
44
|
+
width: Math.max(String(h).length + 4, 15),
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
// Style header row
|
|
48
|
+
const headerRow = ws.getRow(1);
|
|
49
|
+
headerRow.height = 28;
|
|
50
|
+
headerRow.eachCell((cell) => {
|
|
51
|
+
cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: theme.header } };
|
|
52
|
+
cell.font = { color: { argb: theme.headerFont }, bold: true, size: 12 };
|
|
53
|
+
cell.alignment = { vertical: 'middle', horizontal: 'center' };
|
|
54
|
+
cell.border = {
|
|
55
|
+
bottom: { style: 'medium', color: { argb: theme.border } },
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Data rows
|
|
60
|
+
for (let r = 1; r < sheet.rows.length; r++) {
|
|
61
|
+
const row = sheet.rows[r];
|
|
62
|
+
const wsRow = ws.addRow(row.reduce((obj, val, i) => { obj['col' + i] = val; return obj; }, {}));
|
|
63
|
+
wsRow.height = 22;
|
|
64
|
+
|
|
65
|
+
wsRow.eachCell((cell) => {
|
|
66
|
+
if (r % 2 === 0) {
|
|
67
|
+
cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: theme.alt } };
|
|
68
|
+
}
|
|
69
|
+
cell.alignment = { vertical: 'middle' };
|
|
70
|
+
cell.border = {
|
|
71
|
+
bottom: { style: 'thin', color: { argb: theme.border } },
|
|
72
|
+
};
|
|
73
|
+
// Auto-detect numbers
|
|
74
|
+
const val = cell.value;
|
|
75
|
+
if (typeof val === 'string' && !isNaN(val.replace(/[,$%]/g, ''))) {
|
|
76
|
+
cell.value = parseFloat(val.replace(/[,$]/g, ''));
|
|
77
|
+
if (val.includes('%')) cell.numFmt = '0.0%';
|
|
78
|
+
else if (val.includes('$')) cell.numFmt = '$#,##0.00';
|
|
79
|
+
else cell.numFmt = '#,##0';
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Auto-filter
|
|
85
|
+
if (sheet.rows.length > 1) {
|
|
86
|
+
ws.autoFilter = { from: 'A1', to: String.fromCharCode(64 + headers.length) + '1' };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Freeze header
|
|
90
|
+
ws.views = [{ state: 'frozen', ySplit: 1 }];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const filename = title.replace(/[^a-zA-Z0-9\u0600-\u06FF ]/g, '').replace(/\s+/g, '_').slice(0, 50) + '.xlsx';
|
|
94
|
+
const filepath = join(this.outputDir, filename);
|
|
95
|
+
await wb.xlsx.writeFile(filepath);
|
|
96
|
+
|
|
97
|
+
logger.info('excel', `Created: ${filepath}`);
|
|
98
|
+
return { filepath, filename, sheetCount: sheets.length };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Parse AI content: rows separated by newlines, cells by | or ,
|
|
103
|
+
*/
|
|
104
|
+
static parseContent(text) {
|
|
105
|
+
const sheets = [];
|
|
106
|
+
const sections = text.split(/^### /gm).filter(s => s.trim());
|
|
107
|
+
|
|
108
|
+
if (sections.length === 0) {
|
|
109
|
+
// Single sheet
|
|
110
|
+
sheets.push({ name: 'Data', rows: ExcelGenerator._parseRows(text) });
|
|
111
|
+
} else {
|
|
112
|
+
for (const section of sections) {
|
|
113
|
+
const lines = section.trim().split('\n');
|
|
114
|
+
const name = lines[0].trim();
|
|
115
|
+
sheets.push({ name, rows: ExcelGenerator._parseRows(lines.slice(1).join('\n')) });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return sheets;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
static _parseRows(text) {
|
|
122
|
+
const rows = [];
|
|
123
|
+
for (const line of text.split('\n')) {
|
|
124
|
+
const trimmed = line.trim();
|
|
125
|
+
if (!trimmed || /^[-|:=]+$/.test(trimmed)) continue;
|
|
126
|
+
if (trimmed.includes('|')) {
|
|
127
|
+
rows.push(trimmed.split('|').map(c => c.trim()).filter(Boolean));
|
|
128
|
+
} else if (trimmed.includes(',')) {
|
|
129
|
+
rows.push(trimmed.split(',').map(c => c.trim()));
|
|
130
|
+
} else if (trimmed.includes('\t')) {
|
|
131
|
+
rows.push(trimmed.split('\t').map(c => c.trim()));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return rows;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* π¦ HTML Generator
|
|
3
|
+
* Create styled HTML pages/reports
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { logger } from '../core/logger.js';
|
|
7
|
+
import { writeFileSync, mkdirSync } from 'fs';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
|
|
10
|
+
export class HtmlGenerator {
|
|
11
|
+
constructor() {
|
|
12
|
+
this.outputDir = '/tmp/squidclaw-files';
|
|
13
|
+
mkdirSync(this.outputDir, { recursive: true });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
create(title, content, options = {}) {
|
|
17
|
+
const theme = options.theme || 'light';
|
|
18
|
+
const isDark = ['dark', 'ocean', 'gradient'].includes(theme);
|
|
19
|
+
|
|
20
|
+
const css = isDark ? `
|
|
21
|
+
body { background: #0d1117; color: #c9d1d9; }
|
|
22
|
+
h1 { color: #58a6ff; border-bottom: 2px solid #58a6ff; }
|
|
23
|
+
h2 { color: #58a6ff; }
|
|
24
|
+
h3 { color: #8b949e; }
|
|
25
|
+
a { color: #58a6ff; }
|
|
26
|
+
blockquote { border-left: 3px solid #58a6ff; color: #8b949e; }
|
|
27
|
+
table th { background: #161b22; color: #58a6ff; }
|
|
28
|
+
table td { border-color: #30363d; }
|
|
29
|
+
tr:nth-child(even) { background: #161b22; }
|
|
30
|
+
code { background: #161b22; color: #f0883e; }
|
|
31
|
+
.accent { color: #58a6ff; }
|
|
32
|
+
` : `
|
|
33
|
+
body { background: #ffffff; color: #1f2937; }
|
|
34
|
+
h1 { color: #1e40af; border-bottom: 2px solid #2563eb; }
|
|
35
|
+
h2 { color: #1e40af; }
|
|
36
|
+
h3 { color: #374151; }
|
|
37
|
+
a { color: #2563eb; }
|
|
38
|
+
blockquote { border-left: 3px solid #2563eb; color: #6b7280; }
|
|
39
|
+
table th { background: #2563eb; color: white; }
|
|
40
|
+
table td { border-color: #e5e7eb; }
|
|
41
|
+
tr:nth-child(even) { background: #f8fafc; }
|
|
42
|
+
code { background: #f1f5f9; color: #dc2626; }
|
|
43
|
+
.accent { color: #2563eb; }
|
|
44
|
+
`;
|
|
45
|
+
|
|
46
|
+
// Convert markdown-like content to HTML
|
|
47
|
+
let html = this._markdownToHtml(content);
|
|
48
|
+
|
|
49
|
+
const page = `<!DOCTYPE html>
|
|
50
|
+
<html lang="en">
|
|
51
|
+
<head>
|
|
52
|
+
<meta charset="UTF-8">
|
|
53
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
54
|
+
<title>${this._escape(title)}</title>
|
|
55
|
+
<style>
|
|
56
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
57
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; line-height: 1.7; padding: 40px; max-width: 900px; margin: 0 auto; }
|
|
58
|
+
h1 { font-size: 2.2em; margin-bottom: 8px; padding-bottom: 12px; }
|
|
59
|
+
h2 { font-size: 1.6em; margin-top: 32px; margin-bottom: 12px; }
|
|
60
|
+
h3 { font-size: 1.2em; margin-top: 24px; margin-bottom: 8px; }
|
|
61
|
+
p { margin-bottom: 12px; }
|
|
62
|
+
ul, ol { margin: 12px 0; padding-left: 28px; }
|
|
63
|
+
li { margin-bottom: 6px; }
|
|
64
|
+
blockquote { padding: 12px 20px; margin: 16px 0; font-style: italic; }
|
|
65
|
+
table { width: 100%; border-collapse: collapse; margin: 16px 0; }
|
|
66
|
+
th, td { padding: 10px 14px; text-align: left; }
|
|
67
|
+
td { border-bottom: 1px solid; }
|
|
68
|
+
th { font-weight: 600; text-transform: uppercase; font-size: 0.85em; letter-spacing: 0.5px; }
|
|
69
|
+
code { padding: 2px 6px; border-radius: 4px; font-size: 0.9em; }
|
|
70
|
+
pre { padding: 16px; border-radius: 8px; overflow-x: auto; margin: 16px 0; }
|
|
71
|
+
img { max-width: 100%; border-radius: 8px; }
|
|
72
|
+
hr { border: none; border-top: 1px solid #e5e7eb; margin: 24px 0; }
|
|
73
|
+
.meta { font-size: 0.85em; opacity: 0.6; margin-top: 40px; text-align: center; }
|
|
74
|
+
${css}
|
|
75
|
+
</style>
|
|
76
|
+
</head>
|
|
77
|
+
<body>
|
|
78
|
+
<h1>${this._escape(title)}</h1>
|
|
79
|
+
<p style="opacity:0.5;font-size:0.9em">${new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })}</p>
|
|
80
|
+
${html}
|
|
81
|
+
<p class="meta">Created with Squidclaw AI π¦</p>
|
|
82
|
+
</body>
|
|
83
|
+
</html>`;
|
|
84
|
+
|
|
85
|
+
const filename = title.replace(/[^a-zA-Z0-9\u0600-\u06FF ]/g, '').replace(/\s+/g, '_').slice(0, 50) + '.html';
|
|
86
|
+
const filepath = join(this.outputDir, filename);
|
|
87
|
+
writeFileSync(filepath, page);
|
|
88
|
+
|
|
89
|
+
logger.info('html', `Created: ${filepath}`);
|
|
90
|
+
return { filepath, filename };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
_markdownToHtml(md) {
|
|
94
|
+
let html = md
|
|
95
|
+
.replace(/^## (.+)$/gm, '<h2>$1</h2>')
|
|
96
|
+
.replace(/^### (.+)$/gm, '<h3>$1</h3>')
|
|
97
|
+
.replace(/^\> (.+)$/gm, '<blockquote>$1</blockquote>')
|
|
98
|
+
.replace(/^---$/gm, '<hr>')
|
|
99
|
+
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
|
100
|
+
.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
|
101
|
+
.replace(/`(.+?)`/g, '<code>$1</code>')
|
|
102
|
+
.replace(/\[(.+?)\]\((.+?)\)/g, '<a href="$2">$1</a>');
|
|
103
|
+
|
|
104
|
+
// Bullet lists
|
|
105
|
+
const lines = html.split('\n');
|
|
106
|
+
let result = '';
|
|
107
|
+
let inList = false;
|
|
108
|
+
|
|
109
|
+
for (const line of lines) {
|
|
110
|
+
const trimmed = line.trim();
|
|
111
|
+
if (trimmed.startsWith('- ') || trimmed.startsWith('β’ ') || trimmed.startsWith('* ')) {
|
|
112
|
+
if (!inList) { result += '<ul>'; inList = true; }
|
|
113
|
+
result += '<li>' + trimmed.replace(/^[-β’*]\s*/, '') + '</li>';
|
|
114
|
+
} else {
|
|
115
|
+
if (inList) { result += '</ul>'; inList = false; }
|
|
116
|
+
if (trimmed.startsWith('<h') || trimmed.startsWith('<blockquote') || trimmed.startsWith('<hr') || !trimmed) {
|
|
117
|
+
result += trimmed;
|
|
118
|
+
} else if (trimmed.startsWith('|')) {
|
|
119
|
+
result += this._parseTable(trimmed, lines);
|
|
120
|
+
} else {
|
|
121
|
+
result += '<p>' + trimmed + '</p>';
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (inList) result += '</ul>';
|
|
126
|
+
|
|
127
|
+
return result;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
_parseTable(firstLine, allLines) {
|
|
131
|
+
// Find consecutive | lines
|
|
132
|
+
const tableLines = allLines.filter(l => l.trim().startsWith('|') && !/^[-|:\s]+$/.test(l.trim()));
|
|
133
|
+
if (tableLines.length === 0) return '';
|
|
134
|
+
|
|
135
|
+
let html = '<table>';
|
|
136
|
+
tableLines.forEach((line, i) => {
|
|
137
|
+
const cells = line.split('|').filter(c => c.trim()).map(c => c.trim());
|
|
138
|
+
const tag = i === 0 ? 'th' : 'td';
|
|
139
|
+
html += '<tr>' + cells.map(c => `<${tag}>${c}</${tag}>`).join('') + '</tr>';
|
|
140
|
+
});
|
|
141
|
+
html += '</table>';
|
|
142
|
+
return html;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
_escape(str) {
|
|
146
|
+
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
147
|
+
}
|
|
148
|
+
}
|
package/lib/tools/pdf.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* π¦ PDF Generator
|
|
3
|
+
* Create .pdf documents
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { logger } from '../core/logger.js';
|
|
7
|
+
import { mkdirSync, createWriteStream } from 'fs';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
|
|
10
|
+
export class PdfGenerator {
|
|
11
|
+
constructor() {
|
|
12
|
+
this.outputDir = '/tmp/squidclaw-files';
|
|
13
|
+
mkdirSync(this.outputDir, { recursive: true });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async create(title, content, options = {}) {
|
|
17
|
+
const PDFDocument = (await import('pdfkit')).default;
|
|
18
|
+
|
|
19
|
+
const filename = title.replace(/[^a-zA-Z0-9\u0600-\u06FF ]/g, '').replace(/\s+/g, '_').slice(0, 50) + '.pdf';
|
|
20
|
+
const filepath = join(this.outputDir, filename);
|
|
21
|
+
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
const doc = new PDFDocument({
|
|
24
|
+
size: 'A4',
|
|
25
|
+
margins: { top: 60, bottom: 60, left: 60, right: 60 },
|
|
26
|
+
info: { Title: title, Author: 'Squidclaw AI π¦', Creator: 'Squidclaw' },
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const stream = createWriteStream(filepath);
|
|
30
|
+
doc.pipe(stream);
|
|
31
|
+
|
|
32
|
+
const colors = {
|
|
33
|
+
title: '#1a1a2e',
|
|
34
|
+
heading: '#2563eb',
|
|
35
|
+
text: '#374151',
|
|
36
|
+
accent: '#2563eb',
|
|
37
|
+
light: '#6b7280',
|
|
38
|
+
bg: '#f8fafc',
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// Header accent bar
|
|
42
|
+
doc.rect(0, 0, doc.page.width, 8).fill(colors.accent);
|
|
43
|
+
|
|
44
|
+
// Title
|
|
45
|
+
doc.moveDown(1);
|
|
46
|
+
doc.fontSize(28).fillColor(colors.title).font('Helvetica-Bold').text(title, { align: 'left' });
|
|
47
|
+
|
|
48
|
+
// Subtitle line
|
|
49
|
+
doc.moveDown(0.3);
|
|
50
|
+
doc.moveTo(60, doc.y).lineTo(200, doc.y).strokeColor(colors.accent).lineWidth(2).stroke();
|
|
51
|
+
doc.moveDown(0.5);
|
|
52
|
+
|
|
53
|
+
// Date
|
|
54
|
+
const date = new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' });
|
|
55
|
+
doc.fontSize(10).fillColor(colors.light).font('Helvetica').text(date);
|
|
56
|
+
doc.moveDown(1.5);
|
|
57
|
+
|
|
58
|
+
// Parse and render content
|
|
59
|
+
const lines = content.split('\n');
|
|
60
|
+
for (const line of lines) {
|
|
61
|
+
const trimmed = line.trim();
|
|
62
|
+
if (!trimmed) { doc.moveDown(0.5); continue; }
|
|
63
|
+
|
|
64
|
+
// Check for page overflow
|
|
65
|
+
if (doc.y > doc.page.height - 100) {
|
|
66
|
+
doc.addPage();
|
|
67
|
+
doc.rect(0, 0, doc.page.width, 4).fill(colors.accent);
|
|
68
|
+
doc.moveDown(1);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ## Heading
|
|
72
|
+
if (trimmed.startsWith('## ')) {
|
|
73
|
+
doc.moveDown(0.8);
|
|
74
|
+
doc.fontSize(18).fillColor(colors.heading).font('Helvetica-Bold').text(trimmed.slice(3));
|
|
75
|
+
doc.moveDown(0.2);
|
|
76
|
+
doc.moveTo(60, doc.y).lineTo(160, doc.y).strokeColor(colors.accent).lineWidth(1).stroke();
|
|
77
|
+
doc.moveDown(0.5);
|
|
78
|
+
}
|
|
79
|
+
// ### Subheading
|
|
80
|
+
else if (trimmed.startsWith('### ')) {
|
|
81
|
+
doc.moveDown(0.5);
|
|
82
|
+
doc.fontSize(14).fillColor(colors.title).font('Helvetica-Bold').text(trimmed.slice(4));
|
|
83
|
+
doc.moveDown(0.3);
|
|
84
|
+
}
|
|
85
|
+
// - Bullet
|
|
86
|
+
else if (trimmed.startsWith('- ') || trimmed.startsWith('β’ ') || trimmed.startsWith('* ')) {
|
|
87
|
+
const bullet = trimmed.replace(/^[-β’*]\s*/, '');
|
|
88
|
+
doc.fontSize(11).fillColor(colors.text).font('Helvetica');
|
|
89
|
+
doc.text('β ' + bullet, { indent: 15, lineGap: 4 });
|
|
90
|
+
}
|
|
91
|
+
// **Bold**
|
|
92
|
+
else if (trimmed.startsWith('**') && trimmed.endsWith('**')) {
|
|
93
|
+
doc.fontSize(12).fillColor(colors.title).font('Helvetica-Bold').text(trimmed.replace(/\*\*/g, ''));
|
|
94
|
+
}
|
|
95
|
+
// > Quote
|
|
96
|
+
else if (trimmed.startsWith('> ')) {
|
|
97
|
+
doc.moveDown(0.3);
|
|
98
|
+
const quoteText = trimmed.slice(2);
|
|
99
|
+
const qx = doc.x;
|
|
100
|
+
doc.rect(qx, doc.y, 3, 40).fill(colors.accent);
|
|
101
|
+
doc.fontSize(11).fillColor(colors.light).font('Helvetica-Oblique').text(quoteText, qx + 15, doc.y - 40 + 5, { width: 420 });
|
|
102
|
+
doc.moveDown(0.5);
|
|
103
|
+
}
|
|
104
|
+
// --- Divider
|
|
105
|
+
else if (trimmed === '---' || trimmed === '***') {
|
|
106
|
+
doc.moveDown(0.5);
|
|
107
|
+
doc.moveTo(60, doc.y).lineTo(doc.page.width - 60, doc.y).strokeColor('#e5e7eb').lineWidth(0.5).stroke();
|
|
108
|
+
doc.moveDown(0.5);
|
|
109
|
+
}
|
|
110
|
+
// Normal text
|
|
111
|
+
else {
|
|
112
|
+
doc.fontSize(11).fillColor(colors.text).font('Helvetica').text(trimmed, { lineGap: 4 });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Footer
|
|
117
|
+
const pages = doc.bufferedPageRange();
|
|
118
|
+
for (let i = 0; i < pages.count; i++) {
|
|
119
|
+
doc.switchToPage(i);
|
|
120
|
+
doc.fontSize(8).fillColor(colors.light).font('Helvetica');
|
|
121
|
+
doc.text('Created with Squidclaw AI π¦', 60, doc.page.height - 40, { width: doc.page.width - 120, align: 'center' });
|
|
122
|
+
doc.text(String(i + 1), 60, doc.page.height - 40, { width: doc.page.width - 120, align: 'right' });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
doc.end();
|
|
126
|
+
stream.on('finish', () => {
|
|
127
|
+
logger.info('pdf', `Created: ${filepath}`);
|
|
128
|
+
resolve({ filepath, filename });
|
|
129
|
+
});
|
|
130
|
+
stream.on('error', reject);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
package/lib/tools/router.js
CHANGED
|
@@ -79,6 +79,49 @@ export class ToolRouter {
|
|
|
79
79
|
'Example:',
|
|
80
80
|
'---TOOL:pptx_slides:AI Report|dark|## Introduction\n- AI is transforming industries\n- Revenue growing 40% YoY\n\n## Growth [chart:bar]\n- 2020: 50\n- 2021: 80\n- 2022: 120\n- 2023: 200\n\n## Key Stats [stats]\n- π 195 β Countries using AI\n- π° $500B β Market size\n- π 40% β Annual growth---');
|
|
81
81
|
|
|
82
|
+
tools.push('', '### Spawn Sub-Agent Session',
|
|
83
|
+
'---TOOL:session_spawn:task description---',
|
|
84
|
+
'Spawn an isolated AI session to work on a task in the background. Returns when complete.',
|
|
85
|
+
'Use for: research, analysis, writing, any task that needs focused work.',
|
|
86
|
+
'Example: ---TOOL:session_spawn:Research the top 5 AI companies in Saudi Arabia and summarize their products---',
|
|
87
|
+
'', '### List Sessions',
|
|
88
|
+
'---TOOL:session_list:all---',
|
|
89
|
+
'Show all active sessions.',
|
|
90
|
+
'', '### Send to Session',
|
|
91
|
+
'---TOOL:session_send:session_id|message---',
|
|
92
|
+
'Send a follow-up message to an existing session.');
|
|
93
|
+
|
|
94
|
+
tools.push('', '### Create Excel Spreadsheet (SENDS AS FILE!)',
|
|
95
|
+
'---TOOL:excel:Title|theme|sheet content---',
|
|
96
|
+
'Creates .xlsx file and sends it. Theme: blue, green, dark, red, saudi, corporate.',
|
|
97
|
+
'Content: use | for columns. First row = headers. Separate sheets with ### SheetName.',
|
|
98
|
+
'Example: ---TOOL:excel:Sales Report|blue|Product|Revenue|Growth\nWidget A|50000|12%\nWidget B|80000|25%---',
|
|
99
|
+
'', '### Create PDF Document (SENDS AS FILE!)',
|
|
100
|
+
'---TOOL:pdf:Title|content in markdown---',
|
|
101
|
+
'Creates .pdf file and sends it. Use ## for headings, - for bullets, > for quotes, --- for dividers.',
|
|
102
|
+
'Example: ---TOOL:pdf:Meeting Notes|## Summary\n- Discussed Q1 results\n- Action items assigned---',
|
|
103
|
+
'', '### Create HTML Page (SENDS AS FILE!)',
|
|
104
|
+
'---TOOL:html:Title|theme|content in markdown---',
|
|
105
|
+
'Creates .html file and sends it. Themes: light, dark, ocean.',
|
|
106
|
+
'Example: ---TOOL:html:Report|dark|## Overview\n- Key findings\n- Recommendations---');
|
|
107
|
+
|
|
108
|
+
tools.push('', '### Schedule Cron Job',
|
|
109
|
+
'---TOOL:cron_add:name|schedule|action|data---',
|
|
110
|
+
'Create a recurring scheduled job. Schedule formats:',
|
|
111
|
+
'- every:5m / every:1h / every:24h (interval)',
|
|
112
|
+
'- daily:09:00 (every day at time UTC)',
|
|
113
|
+
'- weekly:fri:09:00 (every week on day)',
|
|
114
|
+
'- monthly:1:09:00 (every month on date)',
|
|
115
|
+
'Actions: message (send text), briefing (daily brief), remind (reminder), ai (run AI prompt)',
|
|
116
|
+
'Example: ---TOOL:cron_add:Morning Briefing|daily:06:00|briefing|---',
|
|
117
|
+
'Example: ---TOOL:cron_add:Weekly Report Reminder|weekly:fri:09:00|remind|Submit the weekly report!---',
|
|
118
|
+
'', '### List Cron Jobs',
|
|
119
|
+
'---TOOL:cron_list:all---',
|
|
120
|
+
'Show all scheduled jobs.',
|
|
121
|
+
'', '### Remove Cron Job',
|
|
122
|
+
'---TOOL:cron_remove:job_id---',
|
|
123
|
+
'Remove a scheduled job by ID.');
|
|
124
|
+
|
|
82
125
|
tools.push('', '### Allow User',
|
|
83
126
|
'---TOOL:allow:user_id_or_phone---',
|
|
84
127
|
'Add someone to the allowlist so they can message you.',
|
|
@@ -187,9 +230,16 @@ export class ToolRouter {
|
|
|
187
230
|
'You already have vision β use it to read text from screenshots, documents, signs, etc.');
|
|
188
231
|
|
|
189
232
|
tools.push('', '### Set Reminder',
|
|
190
|
-
'---TOOL:remind:
|
|
191
|
-
'Set a reminder
|
|
192
|
-
'
|
|
233
|
+
'---TOOL:remind:TIME|Your reminder message---',
|
|
234
|
+
'Set a reminder. TIME can be:',
|
|
235
|
+
'- Relative: 5m, 30m, 1h, 2h, 1d (minutes, hours, days from now)',
|
|
236
|
+
'- Absolute: 2026-03-03T08:30 (UTC)',
|
|
237
|
+
'- Natural: tomorrow, tonight',
|
|
238
|
+
'Examples:',
|
|
239
|
+
'- ---TOOL:remind:30m|Check the oven!---',
|
|
240
|
+
'- ---TOOL:remind:2h|Call Ahmed---',
|
|
241
|
+
'- ---TOOL:remind:2026-03-03T15:00|Meeting time!---',
|
|
242
|
+
'ALWAYS use this tool when user says "remind me". Do NOT just acknowledge β actually set it.',
|
|
193
243
|
'The user will receive a proactive message at that time even if they are not chatting.');
|
|
194
244
|
|
|
195
245
|
tools.push('', '**Important:** Use tools when needed. The tool result will be injected into the conversation automatically. Only use one tool per response.');
|
|
@@ -208,7 +258,21 @@ export class ToolRouter {
|
|
|
208
258
|
}
|
|
209
259
|
|
|
210
260
|
async processResponse(response, agentId) {
|
|
211
|
-
const toolMatch =
|
|
261
|
+
const toolMatch = (() => {
|
|
262
|
+
const startIdx = response.indexOf('---TOOL:');
|
|
263
|
+
if (startIdx === -1) return null;
|
|
264
|
+
const afterTag = response.slice(startIdx + 8);
|
|
265
|
+
const colonIdx = afterTag.indexOf(':');
|
|
266
|
+
if (colonIdx === -1) return null;
|
|
267
|
+
const toolName = afterTag.slice(0, colonIdx);
|
|
268
|
+
const rest = afterTag.slice(colonIdx + 1);
|
|
269
|
+
// Find the closing --- that's NOT part of markdown (look for last --- or ---END---)
|
|
270
|
+
let endIdx = rest.lastIndexOf('---');
|
|
271
|
+
if (endIdx <= 0) return null;
|
|
272
|
+
const toolArg = rest.slice(0, endIdx);
|
|
273
|
+
const fullMatch = response.slice(startIdx, startIdx + 8 + colonIdx + 1 + endIdx + 3);
|
|
274
|
+
return [fullMatch, toolName, toolArg];
|
|
275
|
+
})();
|
|
212
276
|
if (!toolMatch) return { toolUsed: false, toolResult: null, cleanResponse: response };
|
|
213
277
|
|
|
214
278
|
const [fullMatch, toolName, toolArg] = toolMatch;
|
|
@@ -229,12 +293,31 @@ export class ToolRouter {
|
|
|
229
293
|
try {
|
|
230
294
|
const pipeIdx = toolArg.indexOf('|');
|
|
231
295
|
if (pipeIdx === -1) {
|
|
232
|
-
toolResult = '
|
|
296
|
+
toolResult = 'Format: time|message. Example: 30m|Check oven';
|
|
233
297
|
break;
|
|
234
298
|
}
|
|
235
|
-
|
|
299
|
+
let timeStr = toolArg.slice(0, pipeIdx).trim();
|
|
236
300
|
const msg = toolArg.slice(pipeIdx + 1).trim();
|
|
237
|
-
|
|
301
|
+
|
|
302
|
+
// Parse relative times
|
|
303
|
+
const relMatch = timeStr.match(/^(\d+)\s*(m|min|minutes?|h|hr|hours?|d|days?)$/i);
|
|
304
|
+
if (relMatch) {
|
|
305
|
+
const num = parseInt(relMatch[1]);
|
|
306
|
+
const unit = relMatch[2][0].toLowerCase();
|
|
307
|
+
const ms = unit === 'm' ? num * 60000 : unit === 'h' ? num * 3600000 : num * 86400000;
|
|
308
|
+
const fireDate = new Date(Date.now() + ms);
|
|
309
|
+
timeStr = fireDate.toISOString().slice(0, 16);
|
|
310
|
+
} else if (timeStr.toLowerCase() === 'tomorrow') {
|
|
311
|
+
const d = new Date(Date.now() + 86400000);
|
|
312
|
+
d.setUTCHours(9, 0, 0, 0);
|
|
313
|
+
timeStr = d.toISOString().slice(0, 16);
|
|
314
|
+
} else if (timeStr.toLowerCase() === 'tonight') {
|
|
315
|
+
const d = new Date();
|
|
316
|
+
d.setUTCHours(18, 0, 0, 0);
|
|
317
|
+
if (d < new Date()) d.setDate(d.getDate() + 1);
|
|
318
|
+
timeStr = d.toISOString().slice(0, 16);
|
|
319
|
+
}
|
|
320
|
+
|
|
238
321
|
return { toolUsed: true, toolName: 'remind', reminderTime: timeStr, reminderMessage: msg, cleanResponse };
|
|
239
322
|
} catch (err) {
|
|
240
323
|
toolResult = 'Failed to set reminder: ' + err.message;
|
|
@@ -291,6 +374,123 @@ export class ToolRouter {
|
|
|
291
374
|
}
|
|
292
375
|
break;
|
|
293
376
|
}
|
|
377
|
+
case 'session_spawn':
|
|
378
|
+
case 'spawn_session':
|
|
379
|
+
case 'subagent': {
|
|
380
|
+
try {
|
|
381
|
+
if (!this._engine?.sessions) { toolResult = 'Sessions not available'; break; }
|
|
382
|
+
const result = await this._engine.sessions.runTask(
|
|
383
|
+
agentId, this._currentContactId || 'unknown', toolArg, { timeout: 120000 }
|
|
384
|
+
);
|
|
385
|
+
toolResult = result.status === 'complete' ?
|
|
386
|
+
'Sub-agent result:\n\n' + result.content :
|
|
387
|
+
'Sub-agent failed: ' + result.content;
|
|
388
|
+
} catch (err) { toolResult = 'Spawn failed: ' + err.message; }
|
|
389
|
+
break;
|
|
390
|
+
}
|
|
391
|
+
case 'session_list': {
|
|
392
|
+
try {
|
|
393
|
+
if (!this._engine?.sessions) { toolResult = 'Sessions not available'; break; }
|
|
394
|
+
const sessions = this._engine.sessions.list(agentId, this._currentContactId);
|
|
395
|
+
if (sessions.length === 0) { toolResult = 'No active sessions'; break; }
|
|
396
|
+
toolResult = sessions.map(s =>
|
|
397
|
+
(s.status === 'active' ? 'π’' : 'β«') + ' ' + (s.label || s.id) + ' (' + s.type + ')\n ID: ' + s.id
|
|
398
|
+
).join('\n');
|
|
399
|
+
} catch (err) { toolResult = 'Failed: ' + err.message; }
|
|
400
|
+
break;
|
|
401
|
+
}
|
|
402
|
+
case 'session_send': {
|
|
403
|
+
try {
|
|
404
|
+
if (!this._engine?.sessions) { toolResult = 'Sessions not available'; break; }
|
|
405
|
+
const pipeIdx = toolArg.indexOf('|');
|
|
406
|
+
if (pipeIdx === -1) { toolResult = 'Format: session_id|message'; break; }
|
|
407
|
+
const sessId = toolArg.slice(0, pipeIdx).trim();
|
|
408
|
+
const msg = toolArg.slice(pipeIdx + 1).trim();
|
|
409
|
+
const result = await this._engine.sessions.send(sessId, msg);
|
|
410
|
+
toolResult = 'Session response:\n\n' + result.content;
|
|
411
|
+
} catch (err) { toolResult = 'Failed: ' + err.message; }
|
|
412
|
+
break;
|
|
413
|
+
}
|
|
414
|
+
case 'excel':
|
|
415
|
+
case 'xlsx':
|
|
416
|
+
case 'spreadsheet': {
|
|
417
|
+
try {
|
|
418
|
+
const { ExcelGenerator } = await import('./excel.js');
|
|
419
|
+
const gen = new ExcelGenerator();
|
|
420
|
+
const parts = toolArg.split('|');
|
|
421
|
+
let title, themeName, sheetContent;
|
|
422
|
+
if (parts.length >= 3) {
|
|
423
|
+
title = parts[0].trim();
|
|
424
|
+
themeName = parts[1].trim();
|
|
425
|
+
sheetContent = parts.slice(2).join('|');
|
|
426
|
+
} else {
|
|
427
|
+
title = parts[0]?.trim() || 'Spreadsheet';
|
|
428
|
+
sheetContent = parts.slice(1).join('|');
|
|
429
|
+
themeName = 'corporate';
|
|
430
|
+
}
|
|
431
|
+
const sheets = ExcelGenerator.parseContent(sheetContent);
|
|
432
|
+
const result = await gen.create(title, sheets, { theme: themeName });
|
|
433
|
+
return { toolUsed: true, toolName: 'excel', toolResult: 'Excel created: ' + result.filename, filePath: result.filepath, fileName: result.filename, cleanResponse };
|
|
434
|
+
} catch (err) { toolResult = 'Excel failed: ' + err.message; }
|
|
435
|
+
break;
|
|
436
|
+
}
|
|
437
|
+
case 'pdf':
|
|
438
|
+
case 'document': {
|
|
439
|
+
try {
|
|
440
|
+
const { PdfGenerator } = await import('./pdf.js');
|
|
441
|
+
const gen = new PdfGenerator();
|
|
442
|
+
const pipeIdx = toolArg.indexOf('|');
|
|
443
|
+
const title = pipeIdx > -1 ? toolArg.slice(0, pipeIdx).trim() : 'Document';
|
|
444
|
+
const content = pipeIdx > -1 ? toolArg.slice(pipeIdx + 1) : toolArg;
|
|
445
|
+
const result = await gen.create(title, content);
|
|
446
|
+
return { toolUsed: true, toolName: 'pdf', toolResult: 'PDF created: ' + result.filename, filePath: result.filepath, fileName: result.filename, cleanResponse };
|
|
447
|
+
} catch (err) { toolResult = 'PDF failed: ' + err.message; }
|
|
448
|
+
break;
|
|
449
|
+
}
|
|
450
|
+
case 'html':
|
|
451
|
+
case 'webpage': {
|
|
452
|
+
try {
|
|
453
|
+
const { HtmlGenerator } = await import('./html.js');
|
|
454
|
+
const gen = new HtmlGenerator();
|
|
455
|
+
const parts = toolArg.split('|');
|
|
456
|
+
const title = parts[0]?.trim() || 'Page';
|
|
457
|
+
const themeName = parts.length >= 3 ? parts[1].trim() : 'light';
|
|
458
|
+
const content = parts.length >= 3 ? parts.slice(2).join('|') : parts.slice(1).join('|');
|
|
459
|
+
const result = gen.create(title, content, { theme: themeName });
|
|
460
|
+
return { toolUsed: true, toolName: 'html', toolResult: 'HTML created: ' + result.filename, filePath: result.filepath, fileName: result.filename, cleanResponse };
|
|
461
|
+
} catch (err) { toolResult = 'HTML failed: ' + err.message; }
|
|
462
|
+
break;
|
|
463
|
+
}
|
|
464
|
+
case 'cron_add': {
|
|
465
|
+
try {
|
|
466
|
+
if (!this._engine?.cron) { toolResult = 'Cron not available'; break; }
|
|
467
|
+
const parts = toolArg.split('|').map(p => p.trim());
|
|
468
|
+
const [name, schedule, action, ...dataParts] = parts;
|
|
469
|
+
const data = dataParts.join('|');
|
|
470
|
+
const result = this._engine.cron.add(agentId, this._currentContactId, name, schedule, action || 'message', data, this._currentPlatform);
|
|
471
|
+
toolResult = 'Cron job created! β
\nπ ' + result.name + '\nβ° Schedule: ' + result.schedule + '\nπ Next run: ' + result.nextRun;
|
|
472
|
+
} catch (err) { toolResult = 'Failed: ' + err.message; }
|
|
473
|
+
break;
|
|
474
|
+
}
|
|
475
|
+
case 'cron_list': {
|
|
476
|
+
try {
|
|
477
|
+
if (!this._engine?.cron) { toolResult = 'Cron not available'; break; }
|
|
478
|
+
const jobs = this._engine.cron.listAll(agentId);
|
|
479
|
+
if (jobs.length === 0) { toolResult = 'No scheduled jobs'; break; }
|
|
480
|
+
toolResult = jobs.map(j =>
|
|
481
|
+
(j.enabled ? 'β
' : 'βΈοΈ') + ' ' + j.name + ' (' + j.schedule + ')\n ID: ' + j.id + '\n Next: ' + (j.next_run || 'N/A')
|
|
482
|
+
).join('\n\n');
|
|
483
|
+
} catch (err) { toolResult = 'Failed: ' + err.message; }
|
|
484
|
+
break;
|
|
485
|
+
}
|
|
486
|
+
case 'cron_remove': {
|
|
487
|
+
try {
|
|
488
|
+
if (!this._engine?.cron) { toolResult = 'Cron not available'; break; }
|
|
489
|
+
this._engine.cron.remove(toolArg.trim());
|
|
490
|
+
toolResult = 'Cron job removed β
';
|
|
491
|
+
} catch (err) { toolResult = 'Failed: ' + err.message; }
|
|
492
|
+
break;
|
|
493
|
+
}
|
|
294
494
|
case 'allow': {
|
|
295
495
|
try {
|
|
296
496
|
const { AllowlistManager } = await import('../features/allowlist-manager.js');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "squidclaw",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
4
4
|
"description": "π¦ AI agent platform β human-like agents for WhatsApp, Telegram & more",
|
|
5
5
|
"main": "lib/engine.js",
|
|
6
6
|
"bin": {
|
|
@@ -42,12 +42,14 @@
|
|
|
42
42
|
"commander": "^14.0.3",
|
|
43
43
|
"croner": "^10.0.1",
|
|
44
44
|
"dotenv": "^17.3.1",
|
|
45
|
+
"exceljs": "^4.4.0",
|
|
45
46
|
"express": "^5.2.1",
|
|
46
47
|
"file-type": "^21.3.0",
|
|
47
48
|
"grammy": "^1.40.1",
|
|
48
49
|
"linkedom": "^0.18.12",
|
|
49
50
|
"node-edge-tts": "^1.2.10",
|
|
50
51
|
"pdfjs-dist": "^5.4.624",
|
|
52
|
+
"pdfkit": "^0.17.2",
|
|
51
53
|
"pino": "^10.3.1",
|
|
52
54
|
"pptxgenjs": "^4.0.1",
|
|
53
55
|
"puppeteer-core": "^24.37.5",
|