kernelbot 1.0.26 → 1.0.28

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.
Files changed (41) hide show
  1. package/README.md +198 -124
  2. package/bin/kernel.js +201 -4
  3. package/package.json +1 -1
  4. package/src/agent.js +397 -222
  5. package/src/automation/automation-manager.js +377 -0
  6. package/src/automation/automation.js +79 -0
  7. package/src/automation/index.js +2 -0
  8. package/src/automation/scheduler.js +141 -0
  9. package/src/bot.js +667 -21
  10. package/src/conversation.js +33 -0
  11. package/src/intents/detector.js +50 -0
  12. package/src/intents/index.js +2 -0
  13. package/src/intents/planner.js +58 -0
  14. package/src/persona.js +68 -0
  15. package/src/prompts/orchestrator.js +76 -0
  16. package/src/prompts/persona.md +21 -0
  17. package/src/prompts/system.js +59 -6
  18. package/src/prompts/workers.js +89 -0
  19. package/src/providers/anthropic.js +23 -16
  20. package/src/providers/base.js +76 -2
  21. package/src/providers/index.js +1 -0
  22. package/src/providers/models.js +2 -1
  23. package/src/providers/openai-compat.js +5 -3
  24. package/src/security/confirm.js +7 -2
  25. package/src/skills/catalog.js +506 -0
  26. package/src/skills/custom.js +128 -0
  27. package/src/swarm/job-manager.js +169 -0
  28. package/src/swarm/job.js +67 -0
  29. package/src/swarm/worker-registry.js +74 -0
  30. package/src/tools/browser.js +458 -335
  31. package/src/tools/categories.js +3 -3
  32. package/src/tools/index.js +3 -0
  33. package/src/tools/orchestrator-tools.js +371 -0
  34. package/src/tools/persona.js +32 -0
  35. package/src/utils/config.js +50 -15
  36. package/src/worker.js +305 -0
  37. package/.agents/skills/interface-design/SKILL.md +0 -391
  38. package/.agents/skills/interface-design/references/critique.md +0 -67
  39. package/.agents/skills/interface-design/references/example.md +0 -86
  40. package/.agents/skills/interface-design/references/principles.md +0 -235
  41. package/.agents/skills/interface-design/references/validation.md +0 -48
package/src/bot.js CHANGED
@@ -1,8 +1,17 @@
1
1
  import TelegramBot from 'node-telegram-bot-api';
2
- import { createReadStream } from 'fs';
2
+ import { createReadStream, readFileSync } from 'fs';
3
3
  import { isAllowedUser, getUnauthorizedMessage } from './security/auth.js';
4
4
  import { getLogger } from './utils/logger.js';
5
5
  import { PROVIDERS } from './providers/models.js';
6
+ import {
7
+ getUnifiedSkillById,
8
+ getUnifiedCategoryList,
9
+ getUnifiedSkillsByCategory,
10
+ loadCustomSkills,
11
+ addCustomSkill,
12
+ deleteCustomSkill,
13
+ getCustomSkills,
14
+ } from './skills/custom.js';
6
15
 
7
16
  function splitMessage(text, maxLength = 4096) {
8
17
  if (text.length <= maxLength) return [text];
@@ -41,7 +50,7 @@ class ChatQueue {
41
50
  }
42
51
  }
43
52
 
44
- export function startBot(config, agent, conversationManager) {
53
+ export function startBot(config, agent, conversationManager, jobManager, automationManager) {
45
54
  const logger = getLogger();
46
55
  const bot = new TelegramBot(config.telegram.bot_token, { polling: true });
47
56
  const chatQueue = new ChatQueue();
@@ -56,11 +65,86 @@ export function startBot(config, agent, conversationManager) {
56
65
  logger.info('Loaded previous conversations from disk');
57
66
  }
58
67
 
68
+ // Load custom skills from disk
69
+ loadCustomSkills();
70
+
59
71
  logger.info('Telegram bot started with polling');
60
72
 
73
+ // Initialize automation manager with bot context
74
+ if (automationManager) {
75
+ const sendMsg = async (chatId, text) => {
76
+ try {
77
+ await bot.sendMessage(chatId, text, { parse_mode: 'Markdown' });
78
+ } catch {
79
+ await bot.sendMessage(chatId, text);
80
+ }
81
+ };
82
+
83
+ const sendAction = (chatId, action) => bot.sendChatAction(chatId, action).catch(() => {});
84
+
85
+ const agentFactory = (chatId) => {
86
+ const onUpdate = async (update, opts = {}) => {
87
+ if (opts.editMessageId) {
88
+ try {
89
+ const edited = await bot.editMessageText(update, {
90
+ chat_id: chatId,
91
+ message_id: opts.editMessageId,
92
+ parse_mode: 'Markdown',
93
+ });
94
+ return edited.message_id;
95
+ } catch {
96
+ try {
97
+ const edited = await bot.editMessageText(update, {
98
+ chat_id: chatId,
99
+ message_id: opts.editMessageId,
100
+ });
101
+ return edited.message_id;
102
+ } catch {
103
+ return opts.editMessageId;
104
+ }
105
+ }
106
+ }
107
+ const parts = splitMessage(update);
108
+ let lastMsgId = null;
109
+ for (const part of parts) {
110
+ try {
111
+ const sent = await bot.sendMessage(chatId, part, { parse_mode: 'Markdown' });
112
+ lastMsgId = sent.message_id;
113
+ } catch {
114
+ const sent = await bot.sendMessage(chatId, part);
115
+ lastMsgId = sent.message_id;
116
+ }
117
+ }
118
+ return lastMsgId;
119
+ };
120
+
121
+ const sendPhoto = async (filePath, caption) => {
122
+ const fileOpts = { contentType: 'image/png' };
123
+ try {
124
+ await bot.sendPhoto(chatId, createReadStream(filePath), { caption: caption || '', parse_mode: 'Markdown' }, fileOpts);
125
+ } catch {
126
+ try {
127
+ await bot.sendPhoto(chatId, createReadStream(filePath), { caption: caption || '' }, fileOpts);
128
+ } catch (err) {
129
+ logger.error(`[Automation] Failed to send photo: ${err.message}`);
130
+ }
131
+ }
132
+ };
133
+
134
+ return { agent, onUpdate, sendPhoto };
135
+ };
136
+
137
+ automationManager.init({ sendMessage: sendMsg, sendChatAction: sendAction, agentFactory, config });
138
+ automationManager.startAll();
139
+ logger.info('[Bot] Automation manager initialized and started');
140
+ }
141
+
61
142
  // Track pending brain API key input: chatId -> { providerKey, modelId }
62
143
  const pendingBrainKey = new Map();
63
144
 
145
+ // Track pending custom skill creation: chatId -> { step: 'name' | 'prompt', name?: string }
146
+ const pendingCustomSkill = new Map();
147
+
64
148
  // Handle inline keyboard callbacks for /brain
65
149
  bot.on('callback_query', async (query) => {
66
150
  const chatId = query.message.chat.id;
@@ -72,6 +156,8 @@ export function startBot(config, agent, conversationManager) {
72
156
  }
73
157
 
74
158
  try {
159
+ logger.info(`[Bot] Callback query from chat ${chatId}: ${data}`);
160
+
75
161
  if (data.startsWith('brain_provider:')) {
76
162
  // User picked a provider — show model list
77
163
  const providerKey = data.split(':')[1];
@@ -102,12 +188,35 @@ export function startBot(config, agent, conversationManager) {
102
188
  const modelEntry = providerDef?.models.find((m) => m.id === modelId);
103
189
  const modelLabel = modelEntry ? modelEntry.label : modelId;
104
190
 
105
- const missing = agent.switchBrain(providerKey, modelId);
106
- if (missing) {
191
+ await bot.editMessageText(
192
+ `⏳ Verifying *${providerDef.name}* / *${modelLabel}*...`,
193
+ {
194
+ chat_id: chatId,
195
+ message_id: query.message.message_id,
196
+ parse_mode: 'Markdown',
197
+ },
198
+ );
199
+
200
+ logger.info(`[Bot] Brain switch request: ${providerKey}/${modelId} from chat ${chatId}`);
201
+ const result = await agent.switchBrain(providerKey, modelId);
202
+ if (result && typeof result === 'object' && result.error) {
203
+ // Validation failed — keep current model
204
+ logger.warn(`[Bot] Brain switch failed: ${result.error}`);
205
+ const current = agent.getBrainInfo();
206
+ await bot.editMessageText(
207
+ `❌ Failed to switch: ${result.error}\n\nKeeping *${current.providerName}* / *${current.modelLabel}*`,
208
+ {
209
+ chat_id: chatId,
210
+ message_id: query.message.message_id,
211
+ parse_mode: 'Markdown',
212
+ },
213
+ );
214
+ } else if (result) {
107
215
  // API key missing — ask for it
216
+ logger.info(`[Bot] Brain switch needs API key: ${result} for ${providerKey}/${modelId}`);
108
217
  pendingBrainKey.set(chatId, { providerKey, modelId });
109
218
  await bot.editMessageText(
110
- `🔑 *${providerDef.name}* API key is required.\n\nPlease send your \`${missing}\` now.\n\nOr send *cancel* to abort.`,
219
+ `🔑 *${providerDef.name}* API key is required.\n\nPlease send your \`${result}\` now.\n\nOr send *cancel* to abort.`,
111
220
  {
112
221
  chat_id: chatId,
113
222
  message_id: query.message.message_id,
@@ -116,6 +225,7 @@ export function startBot(config, agent, conversationManager) {
116
225
  );
117
226
  } else {
118
227
  const info = agent.getBrainInfo();
228
+ logger.info(`[Bot] Brain switched successfully to ${info.providerName}/${info.modelLabel}`);
119
229
  await bot.editMessageText(
120
230
  `🧠 Brain switched to *${info.providerName}* / *${info.modelLabel}*`,
121
231
  {
@@ -134,9 +244,216 @@ export function startBot(config, agent, conversationManager) {
134
244
  message_id: query.message.message_id,
135
245
  });
136
246
  await bot.answerCallbackQuery(query.id);
247
+
248
+ // ── Skill callbacks ──────────────────────────────────────────
249
+ } else if (data.startsWith('skill_category:')) {
250
+ const categoryKey = data.split(':')[1];
251
+ const skills = getUnifiedSkillsByCategory(categoryKey);
252
+ const categories = getUnifiedCategoryList();
253
+ const cat = categories.find((c) => c.key === categoryKey);
254
+ if (!skills.length) {
255
+ await bot.answerCallbackQuery(query.id, { text: 'No skills in this category' });
256
+ return;
257
+ }
258
+
259
+ const activeSkill = agent.getActiveSkill(chatId);
260
+ const buttons = skills.map((s) => ([{
261
+ text: `${s.emoji} ${s.name}${activeSkill && activeSkill.id === s.id ? ' ✓' : ''}`,
262
+ callback_data: `skill_select:${s.id}`,
263
+ }]));
264
+ buttons.push([
265
+ { text: '« Back', callback_data: 'skill_back' },
266
+ { text: 'Cancel', callback_data: 'skill_cancel' },
267
+ ]);
268
+
269
+ await bot.editMessageText(
270
+ `${cat ? cat.emoji : ''} *${cat ? cat.name : categoryKey}* — select a skill:`,
271
+ {
272
+ chat_id: chatId,
273
+ message_id: query.message.message_id,
274
+ parse_mode: 'Markdown',
275
+ reply_markup: { inline_keyboard: buttons },
276
+ },
277
+ );
278
+ await bot.answerCallbackQuery(query.id);
279
+
280
+ } else if (data.startsWith('skill_select:')) {
281
+ const skillId = data.split(':')[1];
282
+ const skill = getUnifiedSkillById(skillId);
283
+ if (!skill) {
284
+ logger.warn(`[Bot] Unknown skill selected: ${skillId}`);
285
+ await bot.answerCallbackQuery(query.id, { text: 'Unknown skill' });
286
+ return;
287
+ }
288
+
289
+ logger.info(`[Bot] Skill activated: ${skill.name} (${skillId}) for chat ${chatId}`);
290
+ agent.setSkill(chatId, skillId);
291
+ await bot.editMessageText(
292
+ `${skill.emoji} *${skill.name}* activated!\n\n_${skill.description}_\n\nThe agent will now respond as this persona. Use /skills reset to return to default.`,
293
+ {
294
+ chat_id: chatId,
295
+ message_id: query.message.message_id,
296
+ parse_mode: 'Markdown',
297
+ },
298
+ );
299
+ await bot.answerCallbackQuery(query.id);
300
+
301
+ } else if (data === 'skill_reset') {
302
+ logger.info(`[Bot] Skill reset for chat ${chatId}`);
303
+ agent.clearSkill(chatId);
304
+ await bot.editMessageText('🔄 Skill cleared — back to default persona.', {
305
+ chat_id: chatId,
306
+ message_id: query.message.message_id,
307
+ });
308
+ await bot.answerCallbackQuery(query.id);
309
+
310
+ } else if (data === 'skill_custom_add') {
311
+ pendingCustomSkill.set(chatId, { step: 'name' });
312
+ await bot.editMessageText(
313
+ '✏️ Send me a *name* for your custom skill:',
314
+ {
315
+ chat_id: chatId,
316
+ message_id: query.message.message_id,
317
+ parse_mode: 'Markdown',
318
+ },
319
+ );
320
+ await bot.answerCallbackQuery(query.id);
321
+
322
+ } else if (data === 'skill_custom_manage') {
323
+ const customs = getCustomSkills();
324
+ if (!customs.length) {
325
+ await bot.answerCallbackQuery(query.id, { text: 'No custom skills yet' });
326
+ return;
327
+ }
328
+ const buttons = customs.map((s) => ([{
329
+ text: `🗑️ ${s.name}`,
330
+ callback_data: `skill_custom_delete:${s.id}`,
331
+ }]));
332
+ buttons.push([{ text: '« Back', callback_data: 'skill_back' }]);
333
+
334
+ await bot.editMessageText('🛠️ *Custom Skills* — tap to delete:', {
335
+ chat_id: chatId,
336
+ message_id: query.message.message_id,
337
+ parse_mode: 'Markdown',
338
+ reply_markup: { inline_keyboard: buttons },
339
+ });
340
+ await bot.answerCallbackQuery(query.id);
341
+
342
+ } else if (data.startsWith('skill_custom_delete:')) {
343
+ const skillId = data.slice('skill_custom_delete:'.length);
344
+ logger.info(`[Bot] Custom skill delete request: ${skillId} from chat ${chatId}`);
345
+ const activeSkill = agent.getActiveSkill(chatId);
346
+ if (activeSkill && activeSkill.id === skillId) {
347
+ logger.info(`[Bot] Clearing active skill before deletion: ${skillId}`);
348
+ agent.clearSkill(chatId);
349
+ }
350
+ const deleted = deleteCustomSkill(skillId);
351
+ const msg = deleted ? '🗑️ Custom skill deleted.' : 'Skill not found.';
352
+ await bot.editMessageText(msg, {
353
+ chat_id: chatId,
354
+ message_id: query.message.message_id,
355
+ });
356
+ await bot.answerCallbackQuery(query.id);
357
+
358
+ } else if (data === 'skill_back') {
359
+ // Re-show category list
360
+ const categories = getUnifiedCategoryList();
361
+ const activeSkill = agent.getActiveSkill(chatId);
362
+ const buttons = categories.map((cat) => ([{
363
+ text: `${cat.emoji} ${cat.name} (${cat.count})`,
364
+ callback_data: `skill_category:${cat.key}`,
365
+ }]));
366
+ // Custom skill management row
367
+ const customRow = [{ text: '➕ Add Custom', callback_data: 'skill_custom_add' }];
368
+ if (getCustomSkills().length > 0) {
369
+ customRow.push({ text: '🗑️ Manage Custom', callback_data: 'skill_custom_manage' });
370
+ }
371
+ buttons.push(customRow);
372
+ const footerRow = [{ text: 'Cancel', callback_data: 'skill_cancel' }];
373
+ if (activeSkill) {
374
+ footerRow.unshift({ text: '🔄 Reset to Default', callback_data: 'skill_reset' });
375
+ }
376
+ buttons.push(footerRow);
377
+
378
+ const header = activeSkill
379
+ ? `🎭 *Active skill:* ${activeSkill.emoji} ${activeSkill.name}\n\nSelect a category:`
380
+ : '🎭 *Skills* — select a category:';
381
+
382
+ await bot.editMessageText(header, {
383
+ chat_id: chatId,
384
+ message_id: query.message.message_id,
385
+ parse_mode: 'Markdown',
386
+ reply_markup: { inline_keyboard: buttons },
387
+ });
388
+ await bot.answerCallbackQuery(query.id);
389
+
390
+ } else if (data === 'skill_cancel') {
391
+ await bot.editMessageText('Skill selection cancelled.', {
392
+ chat_id: chatId,
393
+ message_id: query.message.message_id,
394
+ });
395
+ await bot.answerCallbackQuery(query.id);
396
+
397
+ // ── Job cancellation callbacks ────────────────────────────────
398
+ } else if (data.startsWith('cancel_job:')) {
399
+ const jobId = data.slice('cancel_job:'.length);
400
+ logger.info(`[Bot] Job cancel request via callback: ${jobId} from chat ${chatId}`);
401
+ const job = jobManager.cancelJob(jobId);
402
+ if (job) {
403
+ logger.info(`[Bot] Job cancelled via callback: ${jobId} [${job.workerType}]`);
404
+ await bot.editMessageText(`🚫 Cancelled job \`${jobId}\` (${job.workerType})`, {
405
+ chat_id: chatId,
406
+ message_id: query.message.message_id,
407
+ parse_mode: 'Markdown',
408
+ });
409
+ } else {
410
+ await bot.editMessageText(`Job \`${jobId}\` not found or already finished.`, {
411
+ chat_id: chatId,
412
+ message_id: query.message.message_id,
413
+ parse_mode: 'Markdown',
414
+ });
415
+ }
416
+ await bot.answerCallbackQuery(query.id);
417
+
418
+ } else if (data === 'cancel_all_jobs') {
419
+ logger.info(`[Bot] Cancel all jobs request via callback from chat ${chatId}`);
420
+ const cancelled = jobManager.cancelAllForChat(chatId);
421
+ const msg = cancelled.length > 0
422
+ ? `🚫 Cancelled ${cancelled.length} job(s).`
423
+ : 'No running jobs to cancel.';
424
+ await bot.editMessageText(msg, {
425
+ chat_id: chatId,
426
+ message_id: query.message.message_id,
427
+ });
428
+ await bot.answerCallbackQuery(query.id);
429
+
430
+ // ── Automation callbacks ─────────────────────────────────────────
431
+ } else if (data.startsWith('auto_pause:')) {
432
+ const autoId = data.slice('auto_pause:'.length);
433
+ logger.info(`[Bot] Automation pause request: ${autoId} from chat ${chatId}`);
434
+ const auto = automationManager?.update(autoId, { enabled: false });
435
+ const msg = auto ? `⏸️ Paused automation \`${autoId}\` (${auto.name})` : `Automation \`${autoId}\` not found.`;
436
+ await bot.editMessageText(msg, { chat_id: chatId, message_id: query.message.message_id, parse_mode: 'Markdown' });
437
+ await bot.answerCallbackQuery(query.id);
438
+
439
+ } else if (data.startsWith('auto_resume:')) {
440
+ const autoId = data.slice('auto_resume:'.length);
441
+ logger.info(`[Bot] Automation resume request: ${autoId} from chat ${chatId}`);
442
+ const auto = automationManager?.update(autoId, { enabled: true });
443
+ const msg = auto ? `▶️ Resumed automation \`${autoId}\` (${auto.name})` : `Automation \`${autoId}\` not found.`;
444
+ await bot.editMessageText(msg, { chat_id: chatId, message_id: query.message.message_id, parse_mode: 'Markdown' });
445
+ await bot.answerCallbackQuery(query.id);
446
+
447
+ } else if (data.startsWith('auto_delete:')) {
448
+ const autoId = data.slice('auto_delete:'.length);
449
+ logger.info(`[Bot] Automation delete request: ${autoId} from chat ${chatId}`);
450
+ const deleted = automationManager?.delete(autoId);
451
+ const msg = deleted ? `🗑️ Deleted automation \`${autoId}\`` : `Automation \`${autoId}\` not found.`;
452
+ await bot.editMessageText(msg, { chat_id: chatId, message_id: query.message.message_id, parse_mode: 'Markdown' });
453
+ await bot.answerCallbackQuery(query.id);
137
454
  }
138
455
  } catch (err) {
139
- logger.error(`Callback query error: ${err.message}`);
456
+ logger.error(`[Bot] Callback query error for "${data}" in chat ${chatId}: ${err.message}`);
140
457
  await bot.answerCallbackQuery(query.id, { text: 'Error' });
141
458
  }
142
459
  });
@@ -167,6 +484,10 @@ export function startBot(config, agent, conversationManager) {
167
484
  ? batch.messages[0]
168
485
  : batch.messages.map((m, i) => `[${i + 1}]: ${m}`).join('\n\n');
169
486
 
487
+ if (batch.messages.length > 1) {
488
+ logger.info(`[Bot] Batch merged ${batch.messages.length} messages for chat ${key}`);
489
+ }
490
+
170
491
  // First resolver gets the merged text, rest get null (skip)
171
492
  batch.resolvers[0](merged);
172
493
  for (let i = 1; i < batch.resolvers.length; i++) {
@@ -177,19 +498,56 @@ export function startBot(config, agent, conversationManager) {
177
498
  }
178
499
 
179
500
  bot.on('message', async (msg) => {
180
- if (!msg.text) return; // ignore non-text
181
-
182
501
  const chatId = msg.chat.id;
183
502
  const userId = msg.from.id;
184
503
  const username = msg.from.username || msg.from.first_name || 'unknown';
185
504
 
186
505
  // Auth check
187
506
  if (!isAllowedUser(userId, config)) {
188
- logger.warn(`Unauthorized access attempt from ${username} (${userId})`);
189
- await bot.sendMessage(chatId, getUnauthorizedMessage());
507
+ if (msg.text || msg.document) {
508
+ logger.warn(`Unauthorized access attempt from ${username} (${userId})`);
509
+ await bot.sendMessage(chatId, getUnauthorizedMessage());
510
+ }
190
511
  return;
191
512
  }
192
513
 
514
+ // Handle file upload for pending custom skill prompt step
515
+ if (msg.document && pendingCustomSkill.has(chatId)) {
516
+ const pending = pendingCustomSkill.get(chatId);
517
+ if (pending.step === 'prompt') {
518
+ const doc = msg.document;
519
+ const mime = doc.mime_type || '';
520
+ const fname = doc.file_name || '';
521
+ if (!fname.endsWith('.md') && mime !== 'text/markdown' && mime !== 'text/plain') {
522
+ await bot.sendMessage(chatId, 'Please upload a `.md` or plain text file, or type the prompt directly.');
523
+ return;
524
+ }
525
+ try {
526
+ const filePath = await bot.downloadFile(doc.file_id, '/tmp');
527
+ const content = readFileSync(filePath, 'utf-8').trim();
528
+ if (!content) {
529
+ await bot.sendMessage(chatId, 'The file appears to be empty. Please try again.');
530
+ return;
531
+ }
532
+ pendingCustomSkill.delete(chatId);
533
+ const skill = addCustomSkill({ name: pending.name, systemPrompt: content });
534
+ logger.info(`[Bot] Custom skill created from file: "${skill.name}" (${skill.id}) — ${content.length} chars, by ${username} in chat ${chatId}`);
535
+ agent.setSkill(chatId, skill.id);
536
+ await bot.sendMessage(
537
+ chatId,
538
+ `✅ Custom skill *${skill.name}* created and activated!\n\n_Prompt loaded from file (${content.length} chars)_`,
539
+ { parse_mode: 'Markdown' },
540
+ );
541
+ } catch (err) {
542
+ logger.error(`Custom skill file upload error: ${err.message}`);
543
+ await bot.sendMessage(chatId, `Failed to read file: ${err.message}`);
544
+ }
545
+ return;
546
+ }
547
+ }
548
+
549
+ if (!msg.text) return; // ignore non-text (and non-document) messages
550
+
193
551
  let text = msg.text.trim();
194
552
 
195
553
  // Handle pending brain API key input
@@ -198,22 +556,71 @@ export function startBot(config, agent, conversationManager) {
198
556
  pendingBrainKey.delete(chatId);
199
557
 
200
558
  if (text.toLowerCase() === 'cancel') {
559
+ logger.info(`[Bot] Brain key input cancelled by ${username} in chat ${chatId}`);
201
560
  await bot.sendMessage(chatId, 'Brain change cancelled.');
202
561
  return;
203
562
  }
204
563
 
205
- agent.switchBrainWithKey(pending.providerKey, pending.modelId, text);
206
- const info = agent.getBrainInfo();
207
- await bot.sendMessage(
208
- chatId,
209
- `🧠 Brain switched to *${info.providerName}* / *${info.modelLabel}*\n\nAPI key saved.`,
210
- { parse_mode: 'Markdown' },
211
- );
564
+ logger.info(`[Bot] Brain key received for ${pending.providerKey}/${pending.modelId} from ${username} in chat ${chatId}`);
565
+ await bot.sendMessage(chatId, '⏳ Verifying API key...');
566
+ const switchResult = await agent.switchBrainWithKey(pending.providerKey, pending.modelId, text);
567
+ if (switchResult && switchResult.error) {
568
+ const current = agent.getBrainInfo();
569
+ await bot.sendMessage(
570
+ chatId,
571
+ `❌ Failed to switch: ${switchResult.error}\n\nKeeping *${current.providerName}* / *${current.modelLabel}*`,
572
+ { parse_mode: 'Markdown' },
573
+ );
574
+ } else {
575
+ const info = agent.getBrainInfo();
576
+ await bot.sendMessage(
577
+ chatId,
578
+ `🧠 Brain switched to *${info.providerName}* / *${info.modelLabel}*\n\nAPI key saved.`,
579
+ { parse_mode: 'Markdown' },
580
+ );
581
+ }
212
582
  return;
213
583
  }
214
584
 
585
+ // Handle pending custom skill creation (text input for name or prompt)
586
+ if (pendingCustomSkill.has(chatId)) {
587
+ const pending = pendingCustomSkill.get(chatId);
588
+
589
+ if (text.toLowerCase() === 'cancel') {
590
+ pendingCustomSkill.delete(chatId);
591
+ await bot.sendMessage(chatId, 'Custom skill creation cancelled.');
592
+ return;
593
+ }
594
+
595
+ if (pending.step === 'name') {
596
+ pending.name = text;
597
+ pending.step = 'prompt';
598
+ pendingCustomSkill.set(chatId, pending);
599
+ await bot.sendMessage(
600
+ chatId,
601
+ `Got it: *${text}*\n\nNow send the system prompt — type it out or upload a \`.md\` file:`,
602
+ { parse_mode: 'Markdown' },
603
+ );
604
+ return;
605
+ }
606
+
607
+ if (pending.step === 'prompt') {
608
+ pendingCustomSkill.delete(chatId);
609
+ const skill = addCustomSkill({ name: pending.name, systemPrompt: text });
610
+ logger.info(`[Bot] Custom skill created: "${skill.name}" (${skill.id}) by ${username} in chat ${chatId}`);
611
+ agent.setSkill(chatId, skill.id);
612
+ await bot.sendMessage(
613
+ chatId,
614
+ `✅ Custom skill *${skill.name}* created and activated!`,
615
+ { parse_mode: 'Markdown' },
616
+ );
617
+ return;
618
+ }
619
+ }
620
+
215
621
  // Handle commands — these bypass batching entirely
216
622
  if (text === '/brain') {
623
+ logger.info(`[Bot] /brain command from ${username} (${userId}) in chat ${chatId}`);
217
624
  const info = agent.getBrainInfo();
218
625
  const providerKeys = Object.keys(PROVIDERS);
219
626
  const buttons = providerKeys.map((key) => ([{
@@ -233,6 +640,44 @@ export function startBot(config, agent, conversationManager) {
233
640
  return;
234
641
  }
235
642
 
643
+ if (text === '/skills reset' || text === '/skill reset') {
644
+ logger.info(`[Bot] /skills reset from ${username} (${userId}) in chat ${chatId}`);
645
+ agent.clearSkill(chatId);
646
+ await bot.sendMessage(chatId, '🔄 Skill cleared — back to default persona.');
647
+ return;
648
+ }
649
+
650
+ if (text === '/skills' || text === '/skill') {
651
+ logger.info(`[Bot] /skills command from ${username} (${userId}) in chat ${chatId}`);
652
+ const categories = getUnifiedCategoryList();
653
+ const activeSkill = agent.getActiveSkill(chatId);
654
+ const buttons = categories.map((cat) => ([{
655
+ text: `${cat.emoji} ${cat.name} (${cat.count})`,
656
+ callback_data: `skill_category:${cat.key}`,
657
+ }]));
658
+ // Custom skill management row
659
+ const customRow = [{ text: '➕ Add Custom', callback_data: 'skill_custom_add' }];
660
+ if (getCustomSkills().length > 0) {
661
+ customRow.push({ text: '🗑️ Manage Custom', callback_data: 'skill_custom_manage' });
662
+ }
663
+ buttons.push(customRow);
664
+ const footerRow = [{ text: 'Cancel', callback_data: 'skill_cancel' }];
665
+ if (activeSkill) {
666
+ footerRow.unshift({ text: '🔄 Reset to Default', callback_data: 'skill_reset' });
667
+ }
668
+ buttons.push(footerRow);
669
+
670
+ const header = activeSkill
671
+ ? `🎭 *Active skill:* ${activeSkill.emoji} ${activeSkill.name}\n\nSelect a category:`
672
+ : '🎭 *Skills* — select a category:';
673
+
674
+ await bot.sendMessage(chatId, header, {
675
+ parse_mode: 'Markdown',
676
+ reply_markup: { inline_keyboard: buttons },
677
+ });
678
+ return;
679
+ }
680
+
236
681
  if (text === '/clean' || text === '/clear' || text === '/reset') {
237
682
  conversationManager.clear(chatId);
238
683
  logger.info(`Conversation cleared for chat ${chatId} by ${username}`);
@@ -246,11 +691,108 @@ export function startBot(config, agent, conversationManager) {
246
691
  return;
247
692
  }
248
693
 
694
+ if (text === '/context') {
695
+ const info = agent.getBrainInfo();
696
+ const activeSkill = agent.getActiveSkill(chatId);
697
+ const msgCount = conversationManager.getMessageCount(chatId);
698
+ const history = conversationManager.getHistory(chatId);
699
+ const maxHistory = conversationManager.maxHistory;
700
+ const recentWindow = conversationManager.recentWindow;
701
+
702
+ // Build recent topics from last few user messages
703
+ const recentUserMsgs = history
704
+ .filter((m) => m.role === 'user')
705
+ .slice(-5)
706
+ .map((m) => {
707
+ const txt = typeof m.content === 'string' ? m.content : JSON.stringify(m.content);
708
+ return txt.length > 80 ? txt.slice(0, 80) + '…' : txt;
709
+ });
710
+
711
+ const lines = [
712
+ '📋 *Conversation Context*',
713
+ '',
714
+ `🧠 *Brain:* ${info.providerName} / ${info.modelLabel}`,
715
+ activeSkill
716
+ ? `🎭 *Skill:* ${activeSkill.emoji} ${activeSkill.name}`
717
+ : '🎭 *Skill:* Default persona',
718
+ `💬 *Messages in memory:* ${msgCount} / ${maxHistory}`,
719
+ `📌 *Recent window:* ${recentWindow} messages`,
720
+ ];
721
+
722
+ if (recentUserMsgs.length > 0) {
723
+ lines.push('', '🕐 *Recent topics:*');
724
+ recentUserMsgs.forEach((msg) => lines.push(` • ${msg}`));
725
+ } else {
726
+ lines.push('', '_No messages yet — start chatting!_');
727
+ }
728
+
729
+ await bot.sendMessage(chatId, lines.join('\n'), { parse_mode: 'Markdown' });
730
+ return;
731
+ }
732
+
733
+ if (text === '/jobs') {
734
+ logger.info(`[Bot] /jobs command from ${username} (${userId}) in chat ${chatId}`);
735
+ const jobs = jobManager.getJobsForChat(chatId);
736
+ if (jobs.length === 0) {
737
+ await bot.sendMessage(chatId, 'No jobs for this chat.');
738
+ return;
739
+ }
740
+ const lines = ['*Jobs*', ''];
741
+ for (const job of jobs.slice(0, 15)) {
742
+ lines.push(job.toSummary());
743
+ }
744
+ if (jobs.length > 15) {
745
+ lines.push(`\n_... and ${jobs.length - 15} more_`);
746
+ }
747
+ await bot.sendMessage(chatId, lines.join('\n'), { parse_mode: 'Markdown' });
748
+ return;
749
+ }
750
+
751
+ if (text === '/cancel') {
752
+ logger.info(`[Bot] /cancel command from ${username} (${userId}) in chat ${chatId}`);
753
+ const running = jobManager.getRunningJobsForChat(chatId);
754
+ if (running.length === 0) {
755
+ logger.debug(`[Bot] /cancel — no running jobs for chat ${chatId}`);
756
+ await bot.sendMessage(chatId, 'No running jobs to cancel.');
757
+ return;
758
+ }
759
+ if (running.length === 1) {
760
+ logger.info(`[Bot] /cancel — single job ${running[0].id}, cancelling directly`);
761
+ const job = jobManager.cancelJob(running[0].id);
762
+ if (job) {
763
+ await bot.sendMessage(chatId, `🚫 Cancelled \`${job.id}\` (${job.workerType})`, { parse_mode: 'Markdown' });
764
+ }
765
+ return;
766
+ }
767
+ // Multiple running — show inline keyboard
768
+ logger.info(`[Bot] /cancel — ${running.length} running jobs, showing picker`);
769
+ const buttons = running.map((j) => ([{
770
+ text: `🚫 ${j.workerType} (${j.id})`,
771
+ callback_data: `cancel_job:${j.id}`,
772
+ }]));
773
+ buttons.push([{ text: '🚫 Cancel All', callback_data: 'cancel_all_jobs' }]);
774
+ await bot.sendMessage(chatId, `*${running.length} running jobs* — select one to cancel:`, {
775
+ parse_mode: 'Markdown',
776
+ reply_markup: { inline_keyboard: buttons },
777
+ });
778
+ return;
779
+ }
780
+
249
781
  if (text === '/help') {
782
+ const activeSkill = agent.getActiveSkill(chatId);
783
+ const skillLine = activeSkill
784
+ ? `\n🎭 *Active skill:* ${activeSkill.emoji} ${activeSkill.name}\n`
785
+ : '';
250
786
  await bot.sendMessage(chatId, [
251
787
  '*KernelBot Commands*',
252
- '',
788
+ skillLine,
253
789
  '/brain — Show current AI model and switch provider/model',
790
+ '/skills — Browse and activate persona skills',
791
+ '/skills reset — Clear active skill back to default',
792
+ '/jobs — List running and recent jobs',
793
+ '/cancel — Cancel running job(s)',
794
+ '/auto — Manage recurring automations',
795
+ '/context — Show current conversation context and brain info',
254
796
  '/clean — Clear conversation and start fresh',
255
797
  '/history — Show message count in memory',
256
798
  '/browse <url> — Browse a website and get a summary',
@@ -263,6 +805,107 @@ export function startBot(config, agent, conversationManager) {
263
805
  return;
264
806
  }
265
807
 
808
+ // ── /auto command ──────────────────────────────────────────────
809
+ if (text === '/auto' || text.startsWith('/auto ')) {
810
+ logger.info(`[Bot] /auto command from ${username} (${userId}) in chat ${chatId}`);
811
+ const args = text.slice('/auto'.length).trim();
812
+
813
+ if (!automationManager) {
814
+ await bot.sendMessage(chatId, 'Automation system not available.');
815
+ return;
816
+ }
817
+
818
+ // /auto (no args) — list automations
819
+ if (!args) {
820
+ const autos = automationManager.listForChat(chatId);
821
+ if (autos.length === 0) {
822
+ await bot.sendMessage(chatId, [
823
+ '⏰ *No automations set up yet.*',
824
+ '',
825
+ 'Tell me what to automate in natural language, e.g.:',
826
+ ' "check my server health every hour"',
827
+ ' "send me a news summary every morning at 9am"',
828
+ '',
829
+ 'Or use `/auto` subcommands:',
830
+ ' `/auto pause <id>` — pause an automation',
831
+ ' `/auto resume <id>` — resume an automation',
832
+ ' `/auto delete <id>` — delete an automation',
833
+ ' `/auto run <id>` — trigger immediately',
834
+ ].join('\n'), { parse_mode: 'Markdown' });
835
+ return;
836
+ }
837
+
838
+ const lines = ['⏰ *Automations*', ''];
839
+ for (const auto of autos) {
840
+ lines.push(auto.toSummary());
841
+ }
842
+ lines.push('', '_Use `/auto pause|resume|delete|run <id>` to manage._');
843
+
844
+ // Build inline keyboard for quick actions
845
+ const buttons = autos.map((a) => {
846
+ const row = [];
847
+ if (a.enabled) {
848
+ row.push({ text: `⏸️ Pause ${a.id}`, callback_data: `auto_pause:${a.id}` });
849
+ } else {
850
+ row.push({ text: `▶️ Resume ${a.id}`, callback_data: `auto_resume:${a.id}` });
851
+ }
852
+ row.push({ text: `🗑️ Delete ${a.id}`, callback_data: `auto_delete:${a.id}` });
853
+ return row;
854
+ });
855
+
856
+ await bot.sendMessage(chatId, lines.join('\n'), {
857
+ parse_mode: 'Markdown',
858
+ reply_markup: { inline_keyboard: buttons },
859
+ });
860
+ return;
861
+ }
862
+
863
+ // /auto pause <id>
864
+ if (args.startsWith('pause ')) {
865
+ const autoId = args.slice('pause '.length).trim();
866
+ const auto = automationManager.update(autoId, { enabled: false });
867
+ await bot.sendMessage(chatId, auto
868
+ ? `⏸️ Paused automation \`${autoId}\` (${auto.name})`
869
+ : `Automation \`${autoId}\` not found.`, { parse_mode: 'Markdown' });
870
+ return;
871
+ }
872
+
873
+ // /auto resume <id>
874
+ if (args.startsWith('resume ')) {
875
+ const autoId = args.slice('resume '.length).trim();
876
+ const auto = automationManager.update(autoId, { enabled: true });
877
+ await bot.sendMessage(chatId, auto
878
+ ? `▶️ Resumed automation \`${autoId}\` (${auto.name})`
879
+ : `Automation \`${autoId}\` not found.`, { parse_mode: 'Markdown' });
880
+ return;
881
+ }
882
+
883
+ // /auto delete <id>
884
+ if (args.startsWith('delete ')) {
885
+ const autoId = args.slice('delete '.length).trim();
886
+ const deleted = automationManager.delete(autoId);
887
+ await bot.sendMessage(chatId, deleted
888
+ ? `🗑️ Deleted automation \`${autoId}\``
889
+ : `Automation \`${autoId}\` not found.`, { parse_mode: 'Markdown' });
890
+ return;
891
+ }
892
+
893
+ // /auto run <id> — trigger immediately
894
+ if (args.startsWith('run ')) {
895
+ const autoId = args.slice('run '.length).trim();
896
+ try {
897
+ await automationManager.runNow(autoId);
898
+ } catch (err) {
899
+ await bot.sendMessage(chatId, `Failed: ${err.message}`);
900
+ }
901
+ return;
902
+ }
903
+
904
+ // /auto <anything else> — treat as natural language automation request
905
+ text = `Set up an automation: ${args}`;
906
+ // Fall through to normal message processing below
907
+ }
908
+
266
909
  // Web browsing shortcut commands — rewrite as natural language for the agent
267
910
  if (text.startsWith('/browse ')) {
268
911
  const browseUrl = text.slice('/browse '.length).trim();
@@ -346,22 +989,24 @@ export function startBot(config, agent, conversationManager) {
346
989
  };
347
990
 
348
991
  const sendPhoto = async (filePath, caption) => {
992
+ const fileOpts = { contentType: 'image/png' };
349
993
  try {
350
994
  await bot.sendPhoto(chatId, createReadStream(filePath), {
351
995
  caption: caption || '',
352
996
  parse_mode: 'Markdown',
353
- });
997
+ }, fileOpts);
354
998
  } catch {
355
999
  try {
356
1000
  await bot.sendPhoto(chatId, createReadStream(filePath), {
357
1001
  caption: caption || '',
358
- });
1002
+ }, fileOpts);
359
1003
  } catch (err) {
360
1004
  logger.error(`Failed to send photo: ${err.message}`);
361
1005
  }
362
1006
  }
363
1007
  };
364
1008
 
1009
+ logger.debug(`[Bot] Sending to orchestrator: chat ${chatId}, text="${mergedText.slice(0, 80)}"`);
365
1010
  const reply = await agent.processMessage(chatId, mergedText, {
366
1011
  id: userId,
367
1012
  username,
@@ -369,6 +1014,7 @@ export function startBot(config, agent, conversationManager) {
369
1014
 
370
1015
  clearInterval(typingInterval);
371
1016
 
1017
+ logger.info(`[Bot] Reply for chat ${chatId}: ${(reply || '').length} chars`);
372
1018
  const chunks = splitMessage(reply || 'Done.');
373
1019
  for (const chunk of chunks) {
374
1020
  try {
@@ -380,7 +1026,7 @@ export function startBot(config, agent, conversationManager) {
380
1026
  }
381
1027
  } catch (err) {
382
1028
  clearInterval(typingInterval);
383
- logger.error(`Error processing message: ${err.message}`);
1029
+ logger.error(`[Bot] Error processing message in chat ${chatId}: ${err.message}`);
384
1030
  await bot.sendMessage(chatId, `Error: ${err.message}`);
385
1031
  }
386
1032
  });