squidclaw 2.1.0 → 2.2.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.
@@ -58,6 +58,8 @@ export function addToolSupport(agent, toolRouter, knowledgeBase) {
58
58
  const fullResponse = result.messages.join('\n');
59
59
  toolRouter._currentContactId = contactId;
60
60
  toolRouter.storage = agent.storage;
61
+ toolRouter._currentContactId = contactId;
62
+ toolRouter._currentPlatform = metadata?.platform;
61
63
  const toolResult = await toolRouter.processResponse(fullResponse, agent.id);
62
64
 
63
65
  if (toolResult.toolUsed && toolResult.toolName === 'remind' && toolResult.reminderTime) {
@@ -71,6 +73,14 @@ export function addToolSupport(agent, toolRouter, knowledgeBase) {
71
73
  return result;
72
74
  }
73
75
 
76
+ // File attachment (pptx, etc)
77
+ if (toolResult.toolUsed && toolResult.filePath) {
78
+ result.filePath = toolResult.filePath;
79
+ result.fileName = toolResult.fileName;
80
+ result.messages = [toolResult.toolResult || 'Here\'s your file! 📎'];
81
+ return result;
82
+ }
83
+
74
84
  if (toolResult.toolUsed && (toolResult.imageBase64 || toolResult.imageUrl)) {
75
85
  // Image generated — pass through directly
76
86
  result.image = { base64: toolResult.imageBase64, url: toolResult.imageUrl, mimeType: toolResult.mimeType };
@@ -12,6 +12,8 @@ export async function aiProcessMiddleware(ctx, next) {
12
12
  messages: result.messages || [],
13
13
  reaction: result.reaction || null,
14
14
  image: result.image || null,
15
+ filePath: result.filePath || null,
16
+ fileName: result.fileName || null,
15
17
  _reminder: result._reminder || null,
16
18
  };
17
19
  ctx.metadata.originalType = ctx.metadata.originalType || null;
@@ -29,6 +29,27 @@ 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, etc)
33
+ if (response.filePath) {
34
+ try {
35
+ const { readFileSync } = await import('fs');
36
+ const { InputFile } = await import('grammy');
37
+ const bot = tm.getBotForAgent?.(agentId) || tm.bots?.values()?.next()?.value;
38
+ if (bot) {
39
+ const buffer = readFileSync(response.filePath);
40
+ await bot.api.sendDocument(contactId.replace('tg_', ''), new InputFile(buffer, response.fileName || 'file'), {
41
+ caption: response.messages?.[0] || '',
42
+ });
43
+ // Skip normal message sending
44
+ await next();
45
+ return;
46
+ }
47
+ } catch (err) {
48
+ logger.error('sender', 'File send failed: ' + err.message);
49
+ // Fall through to text
50
+ }
51
+ }
52
+
32
53
  if (response.image) {
33
54
  const photoData = response.image.url ? { url: response.image.url } : { base64: response.image.base64 };
34
55
  await tm.sendPhoto(agentId, contactId, photoData, response.messages?.[0] || '', metadata);
@@ -0,0 +1,146 @@
1
+ /**
2
+ * 🦑 PowerPoint Generator
3
+ * Create .pptx presentations from AI-generated content
4
+ */
5
+
6
+ import { logger } from '../core/logger.js';
7
+ import { writeFileSync, mkdirSync } from 'fs';
8
+ import { join } from 'path';
9
+
10
+ export class PptxGenerator {
11
+ constructor() {
12
+ this.outputDir = '/tmp/squidclaw-pptx';
13
+ mkdirSync(this.outputDir, { recursive: true });
14
+ }
15
+
16
+ /**
17
+ * Create a presentation from structured content
18
+ * @param {string} title - Presentation title
19
+ * @param {Array} slides - [{title, content, bullets?, notes?}]
20
+ * @param {object} options - {theme, author}
21
+ */
22
+ async create(title, slides, options = {}) {
23
+ const pptxgen = (await import('pptxgenjs')).default;
24
+ const pres = new pptxgen();
25
+
26
+ // Metadata
27
+ pres.author = options.author || 'Squidclaw AI';
28
+ pres.title = title;
29
+ pres.subject = title;
30
+
31
+ // Theme colors
32
+ const themes = {
33
+ dark: { bg: '1a1a2e', title: 'e94560', text: 'eaeaea', accent: '0f3460' },
34
+ light: { bg: 'ffffff', title: '2d3436', text: '636e72', accent: '0984e3' },
35
+ blue: { bg: '0f3460', title: 'e94560', text: 'eaeaea', accent: '16213e' },
36
+ green: { bg: '1b4332', title: 'b7e4c7', text: 'd8f3dc', accent: '2d6a4f' },
37
+ corporate: { bg: 'ffffff', title: '2c3e50', text: '34495e', accent: '3498db' },
38
+ red: { bg: '2d0000', title: 'ff6b6b', text: 'ffe0e0', accent: '4a0000' },
39
+ };
40
+ const theme = themes[options.theme] || themes.corporate;
41
+
42
+ // Title slide
43
+ const titleSlide = pres.addSlide();
44
+ titleSlide.background = { color: theme.bg };
45
+ titleSlide.addText(title, {
46
+ x: 0.5, y: 1.5, w: 9, h: 1.5,
47
+ fontSize: 36, bold: true, color: theme.title,
48
+ align: 'center',
49
+ });
50
+ if (options.subtitle) {
51
+ titleSlide.addText(options.subtitle, {
52
+ x: 0.5, y: 3.2, w: 9, h: 0.8,
53
+ fontSize: 18, color: theme.text, align: 'center',
54
+ });
55
+ }
56
+ titleSlide.addText(new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }), {
57
+ x: 0.5, y: 4.5, w: 9, h: 0.5,
58
+ fontSize: 12, color: theme.text, align: 'center', italic: true,
59
+ });
60
+
61
+ // Content slides
62
+ for (const slide of slides) {
63
+ const s = pres.addSlide();
64
+ s.background = { color: theme.bg };
65
+
66
+ // Slide title
67
+ s.addText(slide.title || '', {
68
+ x: 0.5, y: 0.3, w: 9, h: 0.8,
69
+ fontSize: 24, bold: true, color: theme.title,
70
+ });
71
+
72
+ // Accent line
73
+ s.addShape(pres.ShapeType.rect, {
74
+ x: 0.5, y: 1.1, w: 2, h: 0.04, fill: { color: theme.title },
75
+ });
76
+
77
+ if (slide.bullets && slide.bullets.length > 0) {
78
+ // Bullet points
79
+ const bulletText = slide.bullets.map(b => ({
80
+ text: b,
81
+ options: { fontSize: 16, color: theme.text, bullet: { code: '2022' }, paraSpaceAfter: 8 },
82
+ }));
83
+ s.addText(bulletText, {
84
+ x: 0.5, y: 1.4, w: 9, h: 3.5,
85
+ });
86
+ } else if (slide.content) {
87
+ // Paragraph content
88
+ s.addText(slide.content, {
89
+ x: 0.5, y: 1.4, w: 9, h: 3.5,
90
+ fontSize: 16, color: theme.text,
91
+ valign: 'top', wrap: true,
92
+ });
93
+ }
94
+
95
+ // Slide notes
96
+ if (slide.notes) {
97
+ s.addNotes(slide.notes);
98
+ }
99
+
100
+ // Slide number
101
+ s.addText(String(slides.indexOf(slide) + 2), {
102
+ x: 9, y: 5, w: 0.5, h: 0.3,
103
+ fontSize: 10, color: theme.text, align: 'right',
104
+ });
105
+ }
106
+
107
+ // Save
108
+ const filename = title.replace(/[^a-zA-Z0-9\u0600-\u06FF ]/g, '').replace(/\s+/g, '_').slice(0, 50) + '.pptx';
109
+ const filepath = join(this.outputDir, filename);
110
+
111
+ const data = await pres.write({ outputType: 'nodebuffer' });
112
+ writeFileSync(filepath, data);
113
+
114
+ logger.info('pptx', `Created: ${filepath} (${slides.length} slides)`);
115
+ return { filepath, filename, slideCount: slides.length + 1 }; // +1 for title slide
116
+ }
117
+
118
+ /**
119
+ * Parse AI-generated content into slides
120
+ * Format: "## Slide Title\n- bullet 1\n- bullet 2\n\n## Next Slide..."
121
+ */
122
+ static parseContent(text) {
123
+ const slides = [];
124
+ const sections = text.split(/^## /gm).filter(s => s.trim());
125
+
126
+ for (const section of sections) {
127
+ const lines = section.trim().split('\n');
128
+ const title = lines[0].trim();
129
+ const bullets = [];
130
+ let content = '';
131
+
132
+ for (let i = 1; i < lines.length; i++) {
133
+ const line = lines[i].trim();
134
+ if (line.startsWith('- ') || line.startsWith('• ') || line.startsWith('* ')) {
135
+ bullets.push(line.replace(/^[-•*]\s*/, ''));
136
+ } else if (line) {
137
+ content += (content ? '\n' : '') + line;
138
+ }
139
+ }
140
+
141
+ slides.push({ title, bullets: bullets.length > 0 ? bullets : null, content: content || null });
142
+ }
143
+
144
+ return slides;
145
+ }
146
+ }
@@ -59,6 +59,20 @@ export class ToolRouter {
59
59
  'Send an email.');
60
60
  }
61
61
 
62
+ tools.push('', '### Create PowerPoint',
63
+ '---TOOL:pptx:Title of Presentation---',
64
+ 'Create a .pptx PowerPoint file. After calling this, generate slides using markdown format:',
65
+ '## Slide Title',
66
+ '- Bullet point 1',
67
+ '- Bullet point 2',
68
+ '',
69
+ '## Another Slide',
70
+ '- More content',
71
+ '',
72
+ 'Use ---TOOL:pptx_slides:## Slide 1\n- Point 1\n- Point 2\n\n## Slide 2\n- Point 3--- to generate the actual file.',
73
+ 'Theme options after pipe: dark, light, blue, green, corporate, red',
74
+ 'Example: ---TOOL:pptx_slides:title|theme|## Slide 1\n- point---');
75
+
62
76
  tools.push('', '### Allow User',
63
77
  '---TOOL:allow:user_id_or_phone---',
64
78
  'Add someone to the allowlist so they can message you.',
@@ -221,6 +235,56 @@ export class ToolRouter {
221
235
  }
222
236
  break;
223
237
  }
238
+ case 'pptx':
239
+ case 'pptx_slides':
240
+ case 'powerpoint':
241
+ case 'presentation': {
242
+ try {
243
+ const { PptxGenerator } = await import('./pptx.js');
244
+ const gen = new PptxGenerator();
245
+
246
+ // Parse: title|theme|content or just content
247
+ const parts = toolArg.split('|');
248
+ let title, theme, slideContent;
249
+
250
+ if (parts.length >= 3) {
251
+ title = parts[0].trim();
252
+ theme = parts[1].trim();
253
+ slideContent = parts.slice(2).join('|');
254
+ } else if (parts.length === 2) {
255
+ title = parts[0].trim();
256
+ slideContent = parts[1];
257
+ theme = 'corporate';
258
+ } else {
259
+ // Try to extract title from first ## heading
260
+ const firstH2 = toolArg.match(/^##\s+(.+)/m);
261
+ title = firstH2 ? firstH2[1] : 'Presentation';
262
+ slideContent = toolArg;
263
+ theme = 'corporate';
264
+ }
265
+
266
+ const slides = PptxGenerator.parseContent(slideContent);
267
+ if (slides.length === 0) {
268
+ toolResult = 'No slides found. Use ## headings and - bullet points.';
269
+ break;
270
+ }
271
+
272
+ const result = await gen.create(title, slides, { theme });
273
+
274
+ // Return file path for sending
275
+ return {
276
+ toolUsed: true,
277
+ toolName: 'pptx',
278
+ toolResult: 'PowerPoint created: ' + result.filename + ' (' + result.slideCount + ' slides)',
279
+ filePath: result.filepath,
280
+ fileName: result.filename,
281
+ cleanResponse
282
+ };
283
+ } catch (err) {
284
+ toolResult = 'PowerPoint failed: ' + err.message;
285
+ }
286
+ break;
287
+ }
224
288
  case 'allow': {
225
289
  try {
226
290
  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.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "🦑 AI agent platform — human-like agents for WhatsApp, Telegram & more",
5
5
  "main": "lib/engine.js",
6
6
  "bin": {
@@ -49,6 +49,7 @@
49
49
  "node-edge-tts": "^1.2.10",
50
50
  "pdfjs-dist": "^5.4.624",
51
51
  "pino": "^10.3.1",
52
+ "pptxgenjs": "^4.0.1",
52
53
  "puppeteer-core": "^24.37.5",
53
54
  "qrcode-terminal": "^0.12.0",
54
55
  "sharp": "^0.34.5",