squidclaw 3.2.0 → 3.2.1

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/ai/gateway.js CHANGED
@@ -31,8 +31,10 @@ export class AIGateway {
31
31
  // Smart routing: use best model for the task
32
32
  let routedModel = options.model;
33
33
  if (!routedModel && options.taskHint) {
34
- const route = getRouteForTask(options.taskHint, options.toolName, this.config);
35
- if (route) routedModel = route.model;
34
+ try {
35
+ const route = getRouteForTask(options.taskHint, options.toolName, this.config);
36
+ if (route) routedModel = route.model;
37
+ } catch {}
36
38
  }
37
39
  const model = routedModel || this.config.ai?.defaultModel;
38
40
  const fallbackChain = options.fallbackChain || this.config.ai?.fallbackChain || [];
@@ -74,11 +74,15 @@ export class PromptBuilder {
74
74
 
75
75
  // 4. Contact context
76
76
  const contact = await this.storage.getContact(agent.id, contactId);
77
+ parts.push('\n## About This Person');
77
78
  if (contact) {
78
- parts.push(`\n## About This Person`);
79
- if (contact.name) parts.push(`- Name: ${contact.name}`);
80
- parts.push(`- Messages exchanged: ${contact.message_count || 0}`);
81
- parts.push(`- First seen: ${contact.first_seen || 'just now'}`);
79
+ if (contact.name) parts.push('- Name: ' + contact.name);
80
+ parts.push('- Messages exchanged: ' + (contact.message_count || 0));
81
+ }
82
+ // Also check memories for name (more reliable)
83
+ const memName = memories.find(m => m.key === 'name');
84
+ if (memName && memName.value) {
85
+ parts.push('- Confirmed name: ' + memName.value + ' (USE THIS NAME)');
82
86
  }
83
87
 
84
88
  // 5. Knowledge base (RAG)
@@ -157,6 +157,13 @@ export class TelegramManager {
157
157
  /**
158
158
  * Send a reaction
159
159
  */
160
+ async sendChatAction(agentId, contactId, action, metadata = {}) {
161
+ const botInfo = this.bots.get(agentId);
162
+ if (!botInfo?.bot) return;
163
+ const chatId = metadata.chatId || contactId.replace('tg_', '');
164
+ try { await botInfo.bot.api.sendChatAction(chatId, action); } catch {}
165
+ }
166
+
160
167
  async sendReaction(agentId, contactId, messageId, emoji, metadata = {}) {
161
168
  const botInfo = this.bots.get(agentId);
162
169
  if (!botInfo?.bot) return;
package/lib/core/agent.js CHANGED
@@ -66,12 +66,17 @@ export class Agent {
66
66
  const systemPrompt = await this.promptBuilder.build(this, contactId, message);
67
67
 
68
68
  // Get conversation history
69
- const history = await this.storage.getConversation(this.id, contactId, 50);
69
+ const history = await this.storage.getConversation(this.id, contactId, 20);
70
70
 
71
71
  // Build messages array for AI
72
72
  const messages = [
73
73
  { role: 'system', content: systemPrompt },
74
- ...history.map(h => ({ role: h.role, content: h.content })),
74
+ ...history.map(h => ({
75
+ role: h.role,
76
+ content: h.role === 'assistant'
77
+ ? h.content.replace(/---TOOL:\w+:.{0,200}---/gs, '[tool used]').slice(0, 500)
78
+ : h.content.slice(0, 500),
79
+ })),
75
80
  ];
76
81
 
77
82
  // If the last message in history is already the current message, don't add again
package/lib/engine.js CHANGED
@@ -117,7 +117,7 @@ export class SquidclawEngine {
117
117
  initLogger(this.config.engine?.logLevel || 'info');
118
118
 
119
119
  console.log(`
120
- 🦑 Squidclaw Engine v1.1.0
120
+ 🦑 Squidclaw Engine v${require('./package.json').version}
121
121
  ──────────────────────────`);
122
122
 
123
123
  // 1. Storage
@@ -1,47 +1,73 @@
1
1
  /**
2
- * 🦑 Auto Memory — Extracts facts from conversations automatically
3
- * No AI tags needed scans messages for personal info, preferences, decisions
2
+ * 🦑 Auto Memory v2 Smarter fact extraction
3
+ * Fixes: no more saving "ur" as a name, better filtering
4
4
  */
5
5
 
6
6
  import { logger } from '../core/logger.js';
7
7
 
8
+ // Words that should NEVER be saved as names
9
+ const NAME_BLACKLIST = new Set([
10
+ 'ur', 'u', 'im', 'me', 'my', 'i', 'the', 'a', 'an', 'is', 'am', 'are',
11
+ 'here', 'there', 'this', 'that', 'it', 'he', 'she', 'they', 'we', 'you',
12
+ 'not', 'no', 'yes', 'ok', 'okay', 'hi', 'hey', 'hello', 'bye',
13
+ 'just', 'very', 'really', 'actually', 'basically', 'literally',
14
+ 'new', 'old', 'good', 'bad', 'great', 'nice', 'cool', 'fine',
15
+ 'going', 'doing', 'working', 'looking', 'trying', 'getting',
16
+ 'available', 'interested', 'confused', 'tired', 'busy', 'free',
17
+ 'happy', 'sad', 'angry', 'bored', 'excited', 'worried',
18
+ 'still', 'also', 'too', 'now', 'then', 'so', 'but',
19
+ ]);
20
+
21
+ // Must look like a real name: 2+ chars, starts with letter, no pure numbers
22
+ function isValidName(name) {
23
+ if (!name || name.length < 2 || name.length > 30) return false;
24
+ if (NAME_BLACKLIST.has(name.toLowerCase())) return false;
25
+ if (/^\d+$/.test(name)) return false;
26
+ if (/^[a-z]/.test(name) && !/[\u0600-\u06FF]/.test(name)) return false; // English names must be capitalized
27
+ return true;
28
+ }
29
+
30
+ // Must look like a real location
31
+ function isValidLocation(loc) {
32
+ if (!loc || loc.length < 2 || loc.length > 40) return false;
33
+ if (/^(here|there|home|work|office|bed|room|outside)$/i.test(loc.trim())) return false;
34
+ return true;
35
+ }
36
+
8
37
  const FACT_PATTERNS = [
9
- // Names
10
- { regex: /(?:my name is|call me|i'?m|ana|اسمي|اسم)\s+([A-Z\u0600-\u06FF][a-z\u0600-\u06FF]+)/i, key: 'name', extract: 1 },
38
+ // Names — stricter matching
39
+ { regex: /(?:my name is|call me|اسمي)\s+([A-Z\u0600-\u06FF][\w\u0600-\u06FF]{1,25})/i, key: 'name', extract: 1, validate: isValidName },
40
+
41
+ // "I'm Tamer" — but ONLY if the word after is a capitalized proper noun (not "I'm fine")
42
+ { regex: /^(?:i'?m|ana)\s+([A-Z\u0600-\u06FF][\w\u0600-\u06FF]{1,25})$/im, key: 'name', extract: 1, validate: isValidName },
11
43
 
12
44
  // Location
13
- { regex: /(?:i live in|i'?m from|i'?m in|based in|located in|ساكن في|من)\s+([A-Z\u0600-\u06FF][\w\u0600-\u06FF\s]{2,20})/i, key: 'location', extract: 1 },
45
+ { regex: /(?:i live in|i'?m from|based in|located in|ساكن في|من مدينة)\s+([A-Z\u0600-\u06FF][\w\u0600-\u06FF\s]{2,30})/i, key: 'location', extract: 1, validate: isValidLocation },
14
46
 
15
47
  // Job
16
- { regex: /(?:i work (?:as|at|in|for)|my job is|i'?m a|i'?m an|اشتغل|شغلي)\s+(.{3,40}?)(?:\.|,|!|\?|$)/i, key: 'job', extract: 1 },
48
+ { regex: /(?:i work (?:as|at|in|for)|my job is|اشتغل|شغلي)\s+(.{3,40}?)(?:\.|,|!|\?|$)/i, key: 'job', extract: 1 },
17
49
 
18
- // Age
19
- { regex: /(?:i'?m|i am|عمري)\s+(\d{1,3})\s*(?:years? old|سنة|سنه)?/i, key: 'age', extract: 1 },
50
+ // Age — must be reasonable
51
+ { regex: /(?:i'?m|i am|عمري)\s+(\d{1,3})\s*(?:years? old|سنة|سنه)/i, key: 'age', extract: 1, validate: v => { const n = parseInt(v); return n > 5 && n < 120; } },
20
52
 
21
53
  // Family
22
- { regex: /(?:my (?:wife|husband|son|daughter|brother|sister|mom|dad|father|mother)(?:'s name)? is)\s+(\w+)/i, key: (m) => m[0].match(/wife|husband|son|daughter|brother|sister|mom|dad|father|mother/i)[0], extract: 1 },
54
+ { regex: /(?:my (?:wife|husband|son|daughter|brother|sister|mom|dad|father|mother)(?:'s name)? is)\s+([A-Z\u0600-\u06FF]\w+)/i, key: (m) => m[0].match(/wife|husband|son|daughter|brother|sister|mom|dad|father|mother/i)[0], extract: 1, validate: isValidName },
23
55
 
24
56
  // Favorites
25
- { regex: /(?:my fav(?:orite|ourite)?\s+(\w+)\s+is)\s+(.+?)(?:\.|,|!|$)/i, key: (m) => 'favorite_' + m[1], extract: 2 },
26
- { regex: /(?:i (?:love|like|prefer|enjoy))\s+(.{3,30}?)(?:\.|,|!|\?|$)/i, key: 'likes', extract: 1, append: true },
27
- { regex: /(?:i (?:hate|dislike|don'?t like))\s+(.{3,30}?)(?:\.|,|!|\?|$)/i, key: 'dislikes', extract: 1, append: true },
28
-
29
- // Timezone
30
- { regex: /(?:my time(?:zone)? is|i'?m in)\s+(UTC[+-]\d+|GMT[+-]\d+|[A-Z]{2,4}\/[A-Za-z_]+)/i, key: 'timezone', extract: 1 },
57
+ { regex: /(?:my fav(?:orite|ourite)?\s+(\w+)\s+is)\s+(.{2,30}?)(?:\.|,|!|$)/i, key: (m) => 'favorite_' + m[1], extract: 2 },
31
58
 
32
59
  // Birthday
33
- { regex: /(?:my birthday is|born on|ميلادي)\s+(.{5,20})/i, key: 'birthday', extract: 1 },
60
+ { regex: /(?:my birthday is|born on|ميلادي)\s+(.{5,25})/i, key: 'birthday', extract: 1 },
34
61
 
35
62
  // Language
36
- { regex: /(?:i speak|my language is|لغتي)\s+(\w+)/i, key: 'language', extract: 1 },
63
+ { regex: /(?:i speak|my language is|لغتي)\s+([A-Z\u0600-\u06FF]\w+)/i, key: 'language', extract: 1 },
37
64
 
38
65
  // Pet
39
- { regex: /(?:my (?:cat|dog|pet)(?:'s name)? is)\s+(\w+)/i, key: (m) => m[0].match(/cat|dog|pet/i)[0] + '_name', extract: 1 },
66
+ { regex: /(?:my (?:cat|dog|pet)(?:'s name)? is)\s+([A-Z\u0600-\u06FF]\w+)/i, key: (m) => m[0].match(/cat|dog|pet/i)[0] + '_name', extract: 1, validate: isValidName },
40
67
  ];
41
68
 
42
- // Extract per-contact facts
43
69
  const CONTACT_PATTERNS = [
44
- { regex: /(?:remember|don'?t forget|note|تذكر|لا تنسى)\s+(?:that\s+)?(.{5,100})/i, key: 'noted', extract: 1 },
70
+ { regex: /(?:remember|don'?t forget|note that|تذكر|لا تنسى)\s+(?:that\s+)?(.{5,100})/i, key: 'noted', extract: 1 },
45
71
  ];
46
72
 
47
73
  export class AutoMemory {
@@ -49,10 +75,10 @@ export class AutoMemory {
49
75
  this.storage = storage;
50
76
  }
51
77
 
52
- /**
53
- * Scan a user message for facts and auto-save them
54
- */
55
78
  async extract(agentId, contactId, message) {
79
+ // Skip very short messages or commands
80
+ if (!message || message.length < 4 || message.startsWith('/')) return [];
81
+
56
82
  const extracted = [];
57
83
 
58
84
  for (const pattern of FACT_PATTERNS) {
@@ -62,23 +88,20 @@ export class AutoMemory {
62
88
  const key = typeof pattern.key === 'function' ? pattern.key(match) : pattern.key;
63
89
  const value = match[pattern.extract].trim();
64
90
 
91
+ // Validate
65
92
  if (value.length < 2 || value.length > 100) continue;
93
+ if (pattern.validate && !pattern.validate(value)) continue;
66
94
 
67
- if (pattern.append) {
68
- // Append to existing
69
- const existing = await this._getMemory(agentId, key);
70
- if (existing && existing.includes(value)) continue;
71
- const newValue = existing ? existing + ', ' + value : value;
72
- await this.storage.saveMemory(agentId, key, newValue, 'auto');
73
- } else {
74
- await this.storage.saveMemory(agentId, key, value, 'auto');
75
- }
95
+ // Don't overwrite with worse data
96
+ const existing = await this._getMemory(agentId, key);
97
+ if (existing && key === 'name' && isValidName(existing) && existing.length > value.length) continue;
76
98
 
99
+ await this.storage.saveMemory(agentId, key, value, 'auto');
77
100
  extracted.push({ key, value });
78
101
  logger.info('auto-memory', `Extracted: ${key} = ${value}`);
79
102
  }
80
103
 
81
- // Check "remember this" patterns
104
+ // "Remember this" patterns
82
105
  for (const pattern of CONTACT_PATTERNS) {
83
106
  const match = message.match(pattern.regex);
84
107
  if (!match) continue;
@@ -88,7 +111,6 @@ export class AutoMemory {
88
111
  const key = 'user_note_' + Date.now().toString(36);
89
112
  await this.storage.saveMemory(agentId, key, value, 'noted');
90
113
  extracted.push({ key, value });
91
- logger.info('auto-memory', `User note: ${value}`);
92
114
  }
93
115
 
94
116
  return extracted;
@@ -96,12 +118,7 @@ export class AutoMemory {
96
118
 
97
119
  async _getMemory(agentId, key) {
98
120
  try {
99
- const row = this.storage.db.prepare(
100
- 'SELECT value FROM memories WHERE agent_id = ? AND key = ?'
101
- ).get(agentId, key);
102
- return row?.value;
103
- } catch {
104
- return null;
105
- }
121
+ return this.storage.db.prepare('SELECT value FROM memories WHERE agent_id = ? AND key = ?').get(agentId, key)?.value;
122
+ } catch { return null; }
106
123
  }
107
124
  }
@@ -212,11 +212,12 @@ export class ConfigManager {
212
212
  static parseConfigCommand(message) {
213
213
  const lower = message.toLowerCase().trim();
214
214
 
215
- // "switch to gpt-4o" / "use gemini"
216
- const modelMatch = lower.match(/(?:switch|change|use)\s+(?:to\s+)?(?:model\s+)?(.+)/);
215
+ // "switch to gpt-4o" / "use gemini" — must be the MAIN intent (start of message)
216
+ const modelMatch = lower.match(/^(?:switch|change|use)\s+(?:to\s+)?(?:model\s+)?(\S+(?:\s+\S+){0,3})$/);
217
217
  if (modelMatch) {
218
218
  const model = modelMatch[1].trim();
219
- // Map common names to actual model IDs
219
+ // Only match if it looks like a model name (known alias or contains provider-like patterns)
220
+ const knownPatterns = /^(gpt|claude|sonnet|opus|haiku|gemini|flash|llama|deepseek|mistral|groq|ollama|o[1-4]|\S+-\d)/i;
220
221
  const aliases = {
221
222
  'gpt4': 'gpt-4o', 'gpt-4': 'gpt-4o', 'gpt4o': 'gpt-4o',
222
223
  'claude': 'claude-sonnet-4-20250514', 'sonnet': 'claude-sonnet-4-20250514',
@@ -224,6 +225,8 @@ export class ConfigManager {
224
225
  'gemini': 'gemini-2.5-flash', 'flash': 'gemini-2.5-flash',
225
226
  'llama': 'llama-3.3-70b-versatile', 'deepseek': 'deepseek-chat',
226
227
  };
228
+ if (!knownPatterns.test(model) && !aliases[model]) return null; // Not a model name
229
+ // Map common names to actual model IDs
227
230
  return { action: 'set_model', model: aliases[model] || model };
228
231
  }
229
232
 
@@ -1,8 +1,27 @@
1
1
  /**
2
2
  * Send the response back to the user via the appropriate channel
3
+ * With human-like timing — small delays to feel natural
3
4
  */
4
5
  import { logger } from '../core/logger.js';
5
6
 
7
+ const sleep = ms => new Promise(r => setTimeout(r, ms));
8
+
9
+ // Calculate human-like "typing" delay based on message length
10
+ function typingDelay(text) {
11
+ const len = (text || '').length;
12
+ if (len < 20) return 400 + Math.random() * 300; // Short: 0.4-0.7s
13
+ if (len < 100) return 700 + Math.random() * 500; // Medium: 0.7-1.2s
14
+ if (len < 300) return 1000 + Math.random() * 700; // Long: 1.0-1.7s
15
+ return 1500 + Math.random() * 800; // Very long: 1.5-2.3s
16
+ }
17
+
18
+ // Delay between split messages
19
+ function splitDelay(text) {
20
+ const len = (text || '').length;
21
+ if (len < 50) return 300 + Math.random() * 400; // 0.3-0.7s
22
+ return 500 + Math.random() * 700; // 0.5-1.2s
23
+ }
24
+
6
25
  export async function responseSenderMiddleware(ctx, next) {
7
26
  if (!ctx.response) return;
8
27
 
@@ -15,9 +34,10 @@ export async function responseSenderMiddleware(ctx, next) {
15
34
  ctx.engine.reminders.add(agentId, contactId, response._reminder.message, response._reminder.time, ctx.platform, metadata);
16
35
  }
17
36
 
18
- // Send reaction
37
+ // Send reaction first (with tiny natural delay)
19
38
  if (response.reaction && metadata.messageId) {
20
39
  try {
40
+ await sleep(200 + Math.random() * 400); // 0.2-0.6s to "see" the message
21
41
  if (ctx.platform === 'telegram' && tm) {
22
42
  await tm.sendReaction(agentId, contactId, metadata.messageId, response.reaction, metadata);
23
43
  }
@@ -27,9 +47,18 @@ export async function responseSenderMiddleware(ctx, next) {
27
47
  // No messages to send
28
48
  if (!response.messages?.length && !response.image) return;
29
49
 
50
+ // Human-like delay before first reply (thinking time)
51
+ const firstMsg = response.messages?.[0] || '';
52
+ await sleep(typingDelay(firstMsg));
53
+
54
+ // Send typing indicator if available
55
+ if (ctx.platform === 'telegram' && tm) {
56
+ try { await tm.sendChatAction(agentId, contactId, 'typing', metadata); } catch {}
57
+ }
58
+
30
59
  // Send via appropriate channel
31
60
  if (ctx.platform === 'telegram' && tm) {
32
- // Send file attachment (pptx, excel, pdf, html)
61
+ // Send file attachment
33
62
  if (response.filePath) {
34
63
  logger.info('sender', 'SENDING FILE: ' + response.filePath);
35
64
  try {
@@ -38,7 +67,6 @@ export async function responseSenderMiddleware(ctx, next) {
38
67
  return;
39
68
  } catch (err) {
40
69
  logger.error('sender', 'File send failed: ' + err.message);
41
- // Fall through to text
42
70
  }
43
71
  }
44
72
 
@@ -53,14 +81,28 @@ export async function responseSenderMiddleware(ctx, next) {
53
81
  const audio = await vr.generate(response.messages[0], { language: lang });
54
82
  await tm.sendVoice(agentId, contactId, audio, metadata);
55
83
  } catch {
56
- await tm.sendMessages(agentId, contactId, response.messages, metadata);
84
+ // Fallback: send one by one with timing
85
+ for (let i = 0; i < response.messages.length; i++) {
86
+ if (i > 0) { try { await tm.sendChatAction(agentId, contactId, "typing", metadata); } catch {} await sleep(splitDelay(response.messages[i])); }
87
+ await tm.sendMessage(agentId, contactId, response.messages[i], metadata);
88
+ }
57
89
  }
58
90
  } else {
59
- await tm.sendMessages(agentId, contactId, response.messages, metadata);
91
+ // Send messages with human-like gaps between splits
92
+ for (let i = 0; i < response.messages.length; i++) {
93
+ if (i > 0) {
94
+ // Typing indicator + delay between messages
95
+ try { await tm.sendChatAction(agentId, contactId, 'typing', metadata); } catch {}
96
+ await sleep(splitDelay(response.messages[i]));
97
+ }
98
+ await tm.sendMessage(agentId, contactId, response.messages[i], metadata);
99
+ }
60
100
  }
61
101
  } else if (wm) {
62
- for (const msg of response.messages) {
63
- await wm.sendMessage(agentId, contactId, msg);
102
+ // WhatsApp same human timing
103
+ for (let i = 0; i < response.messages.length; i++) {
104
+ if (i > 0) await sleep(splitDelay(response.messages[i]));
105
+ await wm.sendMessage(agentId, contactId, response.messages[i]);
64
106
  }
65
107
  }
66
108
 
@@ -53,14 +53,14 @@ async function fetchImage(query, width = 1200, height = 800) {
53
53
  // ── Slide Templates (HTML) ──
54
54
 
55
55
  const CSS_BASE = `
56
- @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=Playfair+Display:wght@700;800;900&display=swap');
56
+ /* Fonts: system stack (no external loads) */
57
57
 
58
58
  * { margin: 0; padding: 0; box-sizing: border-box; }
59
59
 
60
60
  .slide {
61
61
  width: 1280px; height: 720px;
62
62
  position: relative; overflow: hidden;
63
- font-family: 'Inter', -apple-system, sans-serif;
63
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif;
64
64
  }
65
65
 
66
66
  /* Dark gradient overlay for text readability on images */
@@ -88,8 +88,8 @@ const CSS_BASE = `
88
88
  .content-right img { width: 100%; height: 100%; object-fit: cover; }
89
89
 
90
90
  /* Typography */
91
- .title-xl { font-family: 'Playfair Display', serif; font-size: 56px; font-weight: 800; color: #fff; line-height: 1.1; letter-spacing: -1px; }
92
- .title-lg { font-family: 'Playfair Display', serif; font-size: 42px; font-weight: 700; color: #fff; line-height: 1.15; }
91
+ .title-xl { font-family: Georgia, 'Times New Roman', serif; font-size: 56px; font-weight: 800; color: #fff; line-height: 1.1; letter-spacing: -1px; }
92
+ .title-lg { font-family: Georgia, 'Times New Roman', serif; font-size: 42px; font-weight: 700; color: #fff; line-height: 1.15; }
93
93
  .title-md { font-size: 28px; font-weight: 700; color: #fff; }
94
94
  .subtitle { font-size: 18px; color: rgba(255,255,255,0.7); margin-top: 16px; line-height: 1.6; font-weight: 300; }
95
95
  .label { font-size: 11px; font-weight: 600; color: rgba(255,255,255,0.5); text-transform: uppercase; letter-spacing: 3px; margin-bottom: 20px; }
@@ -153,8 +153,8 @@ const CSS_BASE = `
153
153
  .timeline-desc { font-size: 13px; color: rgba(255,255,255,0.6); margin-top: 4px; line-height: 1.4; }
154
154
 
155
155
  /* Quote */
156
- .quote-mark { font-family: 'Playfair Display', serif; font-size: 120px; color: rgba(88,166,255,0.2); line-height: 0.8; position: absolute; top: 40px; left: 50px; }
157
- .quote-text { font-family: 'Playfair Display', serif; font-size: 28px; color: #fff; line-height: 1.5; font-style: italic; max-width: 800px; }
156
+ .quote-mark { font-family: Georgia, 'Times New Roman', serif; font-size: 120px; color: rgba(88,166,255,0.2); line-height: 0.8; position: absolute; top: 40px; left: 50px; }
157
+ .quote-text { font-family: Georgia, 'Times New Roman', serif; font-size: 28px; color: #fff; line-height: 1.5; font-style: italic; max-width: 800px; }
158
158
  .quote-author { font-size: 14px; color: rgba(255,255,255,0.5); margin-top: 24px; }
159
159
 
160
160
  /* Progress */
@@ -612,10 +612,10 @@ export async function generateSlides(input) {
612
612
  for (let i = 0; i < slideHtmls.length; i++) {
613
613
  const html = `<!DOCTYPE html><html><head><meta charset="UTF-8"><style>${CSS_BASE}</style></head><body style="margin:0;padding:0;background:#000">${slideHtmls[i]}</body></html>`;
614
614
 
615
- await page.setContent(html, { waitUntil: 'networkidle0', timeout: 15000 });
615
+ await page.setContent(html, { waitUntil: 'domcontentloaded', timeout: 10000 });
616
616
  // Wait for fonts to load
617
- await page.evaluate(() => document.fonts.ready);
618
- await new Promise(r => setTimeout(r, 300));
617
+ // fonts are system, no wait needed
618
+ await new Promise(r => setTimeout(r, 100));
619
619
 
620
620
  const screenshot = await page.screenshot({ type: 'jpeg', quality: 95 });
621
621
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "squidclaw",
3
- "version": "3.2.0",
3
+ "version": "3.2.1",
4
4
  "description": "🦑 AI agent platform — human-like agents for WhatsApp, Telegram & more",
5
5
  "main": "lib/engine.js",
6
6
  "bin": {