squidclaw 2.3.0 β 2.4.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 +6 -0
- package/lib/features/cron.js +217 -0
- package/lib/features/reminders.js +2 -1
- package/lib/middleware/commands.js +25 -0
- 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 +144 -7
- package/package.json +3 -1
package/lib/engine.js
CHANGED
|
@@ -226,6 +226,12 @@ export class SquidclawEngine {
|
|
|
226
226
|
if (pending.c > 0) console.log(` β° Reminders: ${pending.c} pending`);
|
|
227
227
|
} catch {}
|
|
228
228
|
|
|
229
|
+
// Cron jobs
|
|
230
|
+
try {
|
|
231
|
+
const { CronManager } = await import('./features/cron.js');
|
|
232
|
+
this.cron = new CronManager(this.storage, this);
|
|
233
|
+
} catch (err) { logger.error('engine', 'Cron init failed: ' + err.message); }
|
|
234
|
+
|
|
229
235
|
// Auto-memory
|
|
230
236
|
try {
|
|
231
237
|
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
|
|
|
@@ -145,6 +145,31 @@ export async function commandsMiddleware(ctx, next) {
|
|
|
145
145
|
return;
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
if (cmd === '/cron') {
|
|
149
|
+
const args = msg.slice(6).trim();
|
|
150
|
+
if (!ctx.engine.cron) { await ctx.reply('β Cron not available'); return; }
|
|
151
|
+
|
|
152
|
+
if (!args || args === 'list') {
|
|
153
|
+
const jobs = ctx.engine.cron.listAll(ctx.agentId);
|
|
154
|
+
if (jobs.length === 0) { await ctx.reply('β° No scheduled jobs'); return; }
|
|
155
|
+
const lines = jobs.map(j =>
|
|
156
|
+
(j.enabled ? 'β
' : 'βΈοΈ') + ' *' + j.name + '*\n ' + j.schedule + ' β ' + j.action + '\n Next: ' + (j.next_run || '?') + '\n ID: `' + j.id + '`'
|
|
157
|
+
);
|
|
158
|
+
await ctx.reply('β° *Cron Jobs*\n\n' + lines.join('\n\n'));
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (args.startsWith('remove ') || args.startsWith('delete ')) {
|
|
163
|
+
const id = args.split(' ')[1];
|
|
164
|
+
ctx.engine.cron.remove(id);
|
|
165
|
+
await ctx.reply('β
Removed cron job ' + id);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
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"');
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
148
173
|
if (cmd === '/allow') {
|
|
149
174
|
const args = msg.slice(7).trim();
|
|
150
175
|
if (!args) { await ctx.reply('Usage: /allow <user_id or phone>'); return; }
|
|
@@ -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,37 @@ 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('', '### Create Excel Spreadsheet (SENDS AS FILE!)',
|
|
83
|
+
'---TOOL:excel:Title|theme|sheet content---',
|
|
84
|
+
'Creates .xlsx file and sends it. Theme: blue, green, dark, red, saudi, corporate.',
|
|
85
|
+
'Content: use | for columns. First row = headers. Separate sheets with ### SheetName.',
|
|
86
|
+
'Example: ---TOOL:excel:Sales Report|blue|Product|Revenue|Growth\nWidget A|50000|12%\nWidget B|80000|25%---',
|
|
87
|
+
'', '### Create PDF Document (SENDS AS FILE!)',
|
|
88
|
+
'---TOOL:pdf:Title|content in markdown---',
|
|
89
|
+
'Creates .pdf file and sends it. Use ## for headings, - for bullets, > for quotes, --- for dividers.',
|
|
90
|
+
'Example: ---TOOL:pdf:Meeting Notes|## Summary\n- Discussed Q1 results\n- Action items assigned---',
|
|
91
|
+
'', '### Create HTML Page (SENDS AS FILE!)',
|
|
92
|
+
'---TOOL:html:Title|theme|content in markdown---',
|
|
93
|
+
'Creates .html file and sends it. Themes: light, dark, ocean.',
|
|
94
|
+
'Example: ---TOOL:html:Report|dark|## Overview\n- Key findings\n- Recommendations---');
|
|
95
|
+
|
|
96
|
+
tools.push('', '### Schedule Cron Job',
|
|
97
|
+
'---TOOL:cron_add:name|schedule|action|data---',
|
|
98
|
+
'Create a recurring scheduled job. Schedule formats:',
|
|
99
|
+
'- every:5m / every:1h / every:24h (interval)',
|
|
100
|
+
'- daily:09:00 (every day at time UTC)',
|
|
101
|
+
'- weekly:fri:09:00 (every week on day)',
|
|
102
|
+
'- monthly:1:09:00 (every month on date)',
|
|
103
|
+
'Actions: message (send text), briefing (daily brief), remind (reminder), ai (run AI prompt)',
|
|
104
|
+
'Example: ---TOOL:cron_add:Morning Briefing|daily:06:00|briefing|---',
|
|
105
|
+
'Example: ---TOOL:cron_add:Weekly Report Reminder|weekly:fri:09:00|remind|Submit the weekly report!---',
|
|
106
|
+
'', '### List Cron Jobs',
|
|
107
|
+
'---TOOL:cron_list:all---',
|
|
108
|
+
'Show all scheduled jobs.',
|
|
109
|
+
'', '### Remove Cron Job',
|
|
110
|
+
'---TOOL:cron_remove:job_id---',
|
|
111
|
+
'Remove a scheduled job by ID.');
|
|
112
|
+
|
|
82
113
|
tools.push('', '### Allow User',
|
|
83
114
|
'---TOOL:allow:user_id_or_phone---',
|
|
84
115
|
'Add someone to the allowlist so they can message you.',
|
|
@@ -187,9 +218,16 @@ export class ToolRouter {
|
|
|
187
218
|
'You already have vision β use it to read text from screenshots, documents, signs, etc.');
|
|
188
219
|
|
|
189
220
|
tools.push('', '### Set Reminder',
|
|
190
|
-
'---TOOL:remind:
|
|
191
|
-
'Set a reminder
|
|
192
|
-
'
|
|
221
|
+
'---TOOL:remind:TIME|Your reminder message---',
|
|
222
|
+
'Set a reminder. TIME can be:',
|
|
223
|
+
'- Relative: 5m, 30m, 1h, 2h, 1d (minutes, hours, days from now)',
|
|
224
|
+
'- Absolute: 2026-03-03T08:30 (UTC)',
|
|
225
|
+
'- Natural: tomorrow, tonight',
|
|
226
|
+
'Examples:',
|
|
227
|
+
'- ---TOOL:remind:30m|Check the oven!---',
|
|
228
|
+
'- ---TOOL:remind:2h|Call Ahmed---',
|
|
229
|
+
'- ---TOOL:remind:2026-03-03T15:00|Meeting time!---',
|
|
230
|
+
'ALWAYS use this tool when user says "remind me". Do NOT just acknowledge β actually set it.',
|
|
193
231
|
'The user will receive a proactive message at that time even if they are not chatting.');
|
|
194
232
|
|
|
195
233
|
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 +246,7 @@ export class ToolRouter {
|
|
|
208
246
|
}
|
|
209
247
|
|
|
210
248
|
async processResponse(response, agentId) {
|
|
211
|
-
const toolMatch = response.match(/---TOOL:(\w+):(
|
|
249
|
+
const toolMatch = response.match(/---TOOL:(\w+):([\s\S]+?)---/);
|
|
212
250
|
if (!toolMatch) return { toolUsed: false, toolResult: null, cleanResponse: response };
|
|
213
251
|
|
|
214
252
|
const [fullMatch, toolName, toolArg] = toolMatch;
|
|
@@ -229,12 +267,31 @@ export class ToolRouter {
|
|
|
229
267
|
try {
|
|
230
268
|
const pipeIdx = toolArg.indexOf('|');
|
|
231
269
|
if (pipeIdx === -1) {
|
|
232
|
-
toolResult = '
|
|
270
|
+
toolResult = 'Format: time|message. Example: 30m|Check oven';
|
|
233
271
|
break;
|
|
234
272
|
}
|
|
235
|
-
|
|
273
|
+
let timeStr = toolArg.slice(0, pipeIdx).trim();
|
|
236
274
|
const msg = toolArg.slice(pipeIdx + 1).trim();
|
|
237
|
-
|
|
275
|
+
|
|
276
|
+
// Parse relative times
|
|
277
|
+
const relMatch = timeStr.match(/^(\d+)\s*(m|min|minutes?|h|hr|hours?|d|days?)$/i);
|
|
278
|
+
if (relMatch) {
|
|
279
|
+
const num = parseInt(relMatch[1]);
|
|
280
|
+
const unit = relMatch[2][0].toLowerCase();
|
|
281
|
+
const ms = unit === 'm' ? num * 60000 : unit === 'h' ? num * 3600000 : num * 86400000;
|
|
282
|
+
const fireDate = new Date(Date.now() + ms);
|
|
283
|
+
timeStr = fireDate.toISOString().slice(0, 16);
|
|
284
|
+
} else if (timeStr.toLowerCase() === 'tomorrow') {
|
|
285
|
+
const d = new Date(Date.now() + 86400000);
|
|
286
|
+
d.setUTCHours(9, 0, 0, 0);
|
|
287
|
+
timeStr = d.toISOString().slice(0, 16);
|
|
288
|
+
} else if (timeStr.toLowerCase() === 'tonight') {
|
|
289
|
+
const d = new Date();
|
|
290
|
+
d.setUTCHours(18, 0, 0, 0);
|
|
291
|
+
if (d < new Date()) d.setDate(d.getDate() + 1);
|
|
292
|
+
timeStr = d.toISOString().slice(0, 16);
|
|
293
|
+
}
|
|
294
|
+
|
|
238
295
|
return { toolUsed: true, toolName: 'remind', reminderTime: timeStr, reminderMessage: msg, cleanResponse };
|
|
239
296
|
} catch (err) {
|
|
240
297
|
toolResult = 'Failed to set reminder: ' + err.message;
|
|
@@ -291,6 +348,86 @@ export class ToolRouter {
|
|
|
291
348
|
}
|
|
292
349
|
break;
|
|
293
350
|
}
|
|
351
|
+
case 'excel':
|
|
352
|
+
case 'xlsx':
|
|
353
|
+
case 'spreadsheet': {
|
|
354
|
+
try {
|
|
355
|
+
const { ExcelGenerator } = await import('./excel.js');
|
|
356
|
+
const gen = new ExcelGenerator();
|
|
357
|
+
const parts = toolArg.split('|');
|
|
358
|
+
let title, themeName, sheetContent;
|
|
359
|
+
if (parts.length >= 3) {
|
|
360
|
+
title = parts[0].trim();
|
|
361
|
+
themeName = parts[1].trim();
|
|
362
|
+
sheetContent = parts.slice(2).join('|');
|
|
363
|
+
} else {
|
|
364
|
+
title = parts[0]?.trim() || 'Spreadsheet';
|
|
365
|
+
sheetContent = parts.slice(1).join('|');
|
|
366
|
+
themeName = 'corporate';
|
|
367
|
+
}
|
|
368
|
+
const sheets = ExcelGenerator.parseContent(sheetContent);
|
|
369
|
+
const result = await gen.create(title, sheets, { theme: themeName });
|
|
370
|
+
return { toolUsed: true, toolName: 'excel', toolResult: 'Excel created: ' + result.filename, filePath: result.filepath, fileName: result.filename, cleanResponse };
|
|
371
|
+
} catch (err) { toolResult = 'Excel failed: ' + err.message; }
|
|
372
|
+
break;
|
|
373
|
+
}
|
|
374
|
+
case 'pdf':
|
|
375
|
+
case 'document': {
|
|
376
|
+
try {
|
|
377
|
+
const { PdfGenerator } = await import('./pdf.js');
|
|
378
|
+
const gen = new PdfGenerator();
|
|
379
|
+
const pipeIdx = toolArg.indexOf('|');
|
|
380
|
+
const title = pipeIdx > -1 ? toolArg.slice(0, pipeIdx).trim() : 'Document';
|
|
381
|
+
const content = pipeIdx > -1 ? toolArg.slice(pipeIdx + 1) : toolArg;
|
|
382
|
+
const result = await gen.create(title, content);
|
|
383
|
+
return { toolUsed: true, toolName: 'pdf', toolResult: 'PDF created: ' + result.filename, filePath: result.filepath, fileName: result.filename, cleanResponse };
|
|
384
|
+
} catch (err) { toolResult = 'PDF failed: ' + err.message; }
|
|
385
|
+
break;
|
|
386
|
+
}
|
|
387
|
+
case 'html':
|
|
388
|
+
case 'webpage': {
|
|
389
|
+
try {
|
|
390
|
+
const { HtmlGenerator } = await import('./html.js');
|
|
391
|
+
const gen = new HtmlGenerator();
|
|
392
|
+
const parts = toolArg.split('|');
|
|
393
|
+
const title = parts[0]?.trim() || 'Page';
|
|
394
|
+
const themeName = parts.length >= 3 ? parts[1].trim() : 'light';
|
|
395
|
+
const content = parts.length >= 3 ? parts.slice(2).join('|') : parts.slice(1).join('|');
|
|
396
|
+
const result = gen.create(title, content, { theme: themeName });
|
|
397
|
+
return { toolUsed: true, toolName: 'html', toolResult: 'HTML created: ' + result.filename, filePath: result.filepath, fileName: result.filename, cleanResponse };
|
|
398
|
+
} catch (err) { toolResult = 'HTML failed: ' + err.message; }
|
|
399
|
+
break;
|
|
400
|
+
}
|
|
401
|
+
case 'cron_add': {
|
|
402
|
+
try {
|
|
403
|
+
if (!this._engine?.cron) { toolResult = 'Cron not available'; break; }
|
|
404
|
+
const parts = toolArg.split('|').map(p => p.trim());
|
|
405
|
+
const [name, schedule, action, ...dataParts] = parts;
|
|
406
|
+
const data = dataParts.join('|');
|
|
407
|
+
const result = this._engine.cron.add(agentId, this._currentContactId, name, schedule, action || 'message', data, this._currentPlatform);
|
|
408
|
+
toolResult = 'Cron job created! β
\nπ ' + result.name + '\nβ° Schedule: ' + result.schedule + '\nπ Next run: ' + result.nextRun;
|
|
409
|
+
} catch (err) { toolResult = 'Failed: ' + err.message; }
|
|
410
|
+
break;
|
|
411
|
+
}
|
|
412
|
+
case 'cron_list': {
|
|
413
|
+
try {
|
|
414
|
+
if (!this._engine?.cron) { toolResult = 'Cron not available'; break; }
|
|
415
|
+
const jobs = this._engine.cron.listAll(agentId);
|
|
416
|
+
if (jobs.length === 0) { toolResult = 'No scheduled jobs'; break; }
|
|
417
|
+
toolResult = jobs.map(j =>
|
|
418
|
+
(j.enabled ? 'β
' : 'βΈοΈ') + ' ' + j.name + ' (' + j.schedule + ')\n ID: ' + j.id + '\n Next: ' + (j.next_run || 'N/A')
|
|
419
|
+
).join('\n\n');
|
|
420
|
+
} catch (err) { toolResult = 'Failed: ' + err.message; }
|
|
421
|
+
break;
|
|
422
|
+
}
|
|
423
|
+
case 'cron_remove': {
|
|
424
|
+
try {
|
|
425
|
+
if (!this._engine?.cron) { toolResult = 'Cron not available'; break; }
|
|
426
|
+
this._engine.cron.remove(toolArg.trim());
|
|
427
|
+
toolResult = 'Cron job removed β
';
|
|
428
|
+
} catch (err) { toolResult = 'Failed: ' + err.message; }
|
|
429
|
+
break;
|
|
430
|
+
}
|
|
294
431
|
case 'allow': {
|
|
295
432
|
try {
|
|
296
433
|
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.4.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",
|