kernelbot 1.0.39 → 1.0.40

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 (80) hide show
  1. package/bin/kernel.js +5 -5
  2. package/config.example.yaml +1 -1
  3. package/package.json +1 -1
  4. package/skills/business/business-analyst.md +32 -0
  5. package/skills/business/product-manager.md +32 -0
  6. package/skills/business/project-manager.md +32 -0
  7. package/skills/business/startup-advisor.md +32 -0
  8. package/skills/creative/music-producer.md +32 -0
  9. package/skills/creative/photographer.md +32 -0
  10. package/skills/creative/video-producer.md +32 -0
  11. package/skills/data/bi-analyst.md +37 -0
  12. package/skills/data/data-scientist.md +38 -0
  13. package/skills/data/ml-engineer.md +38 -0
  14. package/skills/design/graphic-designer.md +38 -0
  15. package/skills/design/product-designer.md +41 -0
  16. package/skills/design/ui-ux.md +38 -0
  17. package/skills/education/curriculum-designer.md +32 -0
  18. package/skills/education/language-teacher.md +32 -0
  19. package/skills/education/tutor.md +32 -0
  20. package/skills/engineering/data-eng.md +55 -0
  21. package/skills/engineering/devops.md +56 -0
  22. package/skills/engineering/mobile-dev.md +55 -0
  23. package/skills/engineering/security-eng.md +55 -0
  24. package/skills/engineering/sr-backend.md +55 -0
  25. package/skills/engineering/sr-frontend.md +55 -0
  26. package/skills/finance/accountant.md +35 -0
  27. package/skills/finance/crypto-defi.md +39 -0
  28. package/skills/finance/financial-analyst.md +35 -0
  29. package/skills/healthcare/health-wellness.md +32 -0
  30. package/skills/healthcare/medical-researcher.md +33 -0
  31. package/skills/legal/contract-reviewer.md +35 -0
  32. package/skills/legal/legal-advisor.md +36 -0
  33. package/skills/marketing/content-marketer.md +38 -0
  34. package/skills/marketing/growth.md +38 -0
  35. package/skills/marketing/seo.md +43 -0
  36. package/skills/marketing/social-media.md +43 -0
  37. package/skills/writing/academic-writer.md +33 -0
  38. package/skills/writing/copywriter.md +32 -0
  39. package/skills/writing/creative-writer.md +32 -0
  40. package/skills/writing/tech-writer.md +33 -0
  41. package/src/agent.js +153 -118
  42. package/src/automation/scheduler.js +36 -3
  43. package/src/bot.js +147 -64
  44. package/src/coder.js +30 -8
  45. package/src/conversation.js +96 -19
  46. package/src/dashboard/dashboard.css +6 -0
  47. package/src/dashboard/dashboard.js +28 -1
  48. package/src/dashboard/index.html +12 -0
  49. package/src/dashboard/server.js +77 -15
  50. package/src/life/codebase.js +2 -1
  51. package/src/life/daydream_engine.js +386 -0
  52. package/src/life/engine.js +1 -0
  53. package/src/life/evolution.js +4 -3
  54. package/src/prompts/orchestrator.js +1 -1
  55. package/src/prompts/system.js +1 -1
  56. package/src/prompts/workers.js +8 -1
  57. package/src/providers/anthropic.js +3 -1
  58. package/src/providers/base.js +33 -0
  59. package/src/providers/index.js +1 -1
  60. package/src/providers/models.js +22 -0
  61. package/src/providers/openai-compat.js +3 -0
  62. package/src/services/x-api.js +14 -3
  63. package/src/skills/loader.js +382 -0
  64. package/src/swarm/worker-registry.js +2 -2
  65. package/src/tools/browser.js +10 -3
  66. package/src/tools/coding.js +16 -0
  67. package/src/tools/docker.js +13 -0
  68. package/src/tools/git.js +31 -29
  69. package/src/tools/jira.js +11 -2
  70. package/src/tools/monitor.js +9 -1
  71. package/src/tools/network.js +34 -0
  72. package/src/tools/orchestrator-tools.js +2 -1
  73. package/src/tools/os.js +20 -6
  74. package/src/utils/config.js +1 -1
  75. package/src/utils/display.js +1 -1
  76. package/src/utils/logger.js +1 -1
  77. package/src/utils/timeAwareness.js +72 -0
  78. package/src/worker.js +26 -33
  79. package/src/skills/catalog.js +0 -506
  80. package/src/skills/custom.js +0 -128
package/src/bot.js CHANGED
@@ -4,14 +4,14 @@ import { isAllowedUser, getUnauthorizedMessage, alertAdmin } from './security/au
4
4
  import { getLogger } from './utils/logger.js';
5
5
  import { PROVIDERS } from './providers/models.js';
6
6
  import {
7
- getUnifiedSkillById,
8
- getUnifiedCategoryList,
9
- getUnifiedSkillsByCategory,
10
- loadCustomSkills,
11
- addCustomSkill,
7
+ getSkillById,
8
+ getCategoryList,
9
+ getSkillsByCategory,
10
+ loadAllSkills,
11
+ saveCustomSkill,
12
12
  deleteCustomSkill,
13
13
  getCustomSkills,
14
- } from './skills/custom.js';
14
+ } from './skills/loader.js';
15
15
  import { TTSService } from './services/tts.js';
16
16
  import { STTService } from './services/stt.js';
17
17
  import { getClaudeAuthStatus, claudeLogout } from './claude-auth.js';
@@ -206,6 +206,39 @@ class ChatQueue {
206
206
  }
207
207
  }
208
208
 
209
+ /**
210
+ * Convert raw errors into user-friendly messages.
211
+ * Keeps technical details in logs, shows something helpful in chat.
212
+ */
213
+ function _friendlyError(err) {
214
+ const msg = (err?.message || '').toLowerCase();
215
+
216
+ if (msg.includes('api key') || msg.includes('authentication') || msg.includes('unauthorized')) {
217
+ return '🔑 Authentication issue — please check your API key configuration.';
218
+ }
219
+ if (msg.includes('rate limit') || msg.includes('429') || msg.includes('quota')) {
220
+ return '⏳ Rate limited — too many requests. I\'ll be back in a moment, try again shortly.';
221
+ }
222
+ if (msg.includes('timed out') || msg.includes('timeout')) {
223
+ return '⏱️ The request timed out. Try again or simplify your request.';
224
+ }
225
+ if (msg.includes('context length') || msg.includes('too long') || msg.includes('too large') || msg.includes('token limit')) {
226
+ return '📏 Message too long for the current model. Try a shorter message or switch to a model with a larger context window.';
227
+ }
228
+ if (msg.includes('safety') || msg.includes('blocked') || msg.includes('recitation')) {
229
+ return '🛡️ The model declined this request due to safety filters. Try rephrasing.';
230
+ }
231
+ if (msg.includes('not found') || msg.includes('not available') || msg.includes('does not exist')) {
232
+ return '❓ The configured model is not available. Try switching to a different model with /brain.';
233
+ }
234
+ if (msg.includes('connection') || msg.includes('network') || msg.includes('fetch failed')) {
235
+ return '🌐 Network issue — couldn\'t reach the AI provider. Check your connection and try again.';
236
+ }
237
+
238
+ // Generic fallback — still don't expose raw internals
239
+ return '⚠️ Something went wrong processing your message. Try again, or switch models with /brain if the issue persists.';
240
+ }
241
+
209
242
  export function startBot(config, agent, conversationManager, jobManager, automationManager, lifeDeps = {}) {
210
243
  let { lifeEngine, memoryManager, journalManager, shareQueue, evolutionTracker, codebaseKnowledge, characterManager, dashboardHandle, dashboardDeps } = lifeDeps;
211
244
  const logger = getLogger();
@@ -281,7 +314,7 @@ export function startBot(config, agent, conversationManager, jobManager, automat
281
314
  }
282
315
 
283
316
  // Load custom skills from disk
284
- loadCustomSkills();
317
+ loadAllSkills();
285
318
 
286
319
  // Register commands in Telegram's menu button
287
320
  bot.setMyCommands([
@@ -392,7 +425,10 @@ export function startBot(config, agent, conversationManager, jobManager, automat
392
425
 
393
426
  } else if (data.startsWith('brain_model:')) {
394
427
  // User picked a model — attempt switch
395
- const [, providerKey, modelId] = data.split(':');
428
+ // Use split with limit to avoid truncating model IDs containing colons
429
+ const parts = data.split(':');
430
+ const providerKey = parts[1];
431
+ const modelId = parts.slice(2).join(':');
396
432
  const providerDef = PROVIDERS[providerKey];
397
433
  const modelEntry = providerDef?.models.find((m) => m.id === modelId);
398
434
  const modelLabel = modelEntry ? modelEntry.label : modelId;
@@ -454,29 +490,34 @@ export function startBot(config, agent, conversationManager, jobManager, automat
454
490
  });
455
491
  await bot.answerCallbackQuery(query.id);
456
492
 
457
- // ── Skill callbacks ──────────────────────────────────────────
493
+ // ── Skill callbacks (multi-skill toggle) ─────────────────────
458
494
  } else if (data.startsWith('skill_category:')) {
459
495
  const categoryKey = data.split(':')[1];
460
- const skills = getUnifiedSkillsByCategory(categoryKey);
461
- const categories = getUnifiedCategoryList();
496
+ const skills = getSkillsByCategory(categoryKey);
497
+ const categories = getCategoryList();
462
498
  const cat = categories.find((c) => c.key === categoryKey);
463
499
  if (!skills.length) {
464
500
  await bot.answerCallbackQuery(query.id, { text: 'No skills in this category' });
465
501
  return;
466
502
  }
467
503
 
468
- const activeSkill = agent.getActiveSkill(chatId);
504
+ const activeIds = new Set(agent.getActiveSkillIds(chatId));
469
505
  const buttons = skills.map((s) => ([{
470
- text: `${s.emoji} ${s.name}${activeSkill && activeSkill.id === s.id ? ' ' : ''}`,
471
- callback_data: `skill_select:${s.id}`,
506
+ text: `${activeIds.has(s.id) ? ' ' : ''}${s.emoji} ${s.name}`,
507
+ callback_data: `skill_toggle:${s.id}:${categoryKey}`,
472
508
  }]));
473
509
  buttons.push([
474
510
  { text: '« Back', callback_data: 'skill_back' },
475
- { text: 'Cancel', callback_data: 'skill_cancel' },
511
+ { text: 'Done', callback_data: 'skill_cancel' },
476
512
  ]);
477
513
 
514
+ const activeSkills = agent.getActiveSkills(chatId);
515
+ const activeLine = activeSkills.length > 0
516
+ ? `Active (${activeSkills.length}): ${activeSkills.map(s => `${s.emoji} ${s.name}`).join(', ')}\n\n`
517
+ : '';
518
+
478
519
  await bot.editMessageText(
479
- `${cat ? cat.emoji : ''} *${cat ? cat.name : categoryKey}* — select a skill:`,
520
+ `${activeLine}${cat ? cat.emoji : ''} *${cat ? cat.name : categoryKey}* — tap to toggle:`,
480
521
  {
481
522
  chat_id: chatId,
482
523
  message_id: query.message.message_id,
@@ -486,31 +527,60 @@ export function startBot(config, agent, conversationManager, jobManager, automat
486
527
  );
487
528
  await bot.answerCallbackQuery(query.id);
488
529
 
489
- } else if (data.startsWith('skill_select:')) {
490
- const skillId = data.split(':')[1];
491
- const skill = getUnifiedSkillById(skillId);
530
+ } else if (data.startsWith('skill_toggle:')) {
531
+ const parts = data.split(':');
532
+ const skillId = parts[1];
533
+ const categoryKey = parts[2]; // to refresh the category view
534
+ const skill = getSkillById(skillId);
492
535
  if (!skill) {
493
- logger.warn(`[Bot] Unknown skill selected: ${skillId}`);
494
536
  await bot.answerCallbackQuery(query.id, { text: 'Unknown skill' });
495
537
  return;
496
538
  }
497
539
 
498
- logger.info(`[Bot] Skill activated: ${skill.name} (${skillId}) for chat ${chatId}`);
499
- agent.setSkill(chatId, skillId);
540
+ const { added, skills: currentSkills } = agent.toggleSkill(chatId, skillId);
541
+ if (!added && currentSkills.includes(skillId)) {
542
+ // Wasn't added because at max
543
+ await bot.answerCallbackQuery(query.id, { text: `Max ${5} skills reached. Remove one first.` });
544
+ return;
545
+ }
546
+
547
+ logger.info(`[Bot] Skill ${added ? 'activated' : 'deactivated'}: ${skill.name} (${skillId}) for chat ${chatId} — now ${currentSkills.length} active`);
548
+
549
+ // Refresh the category view with updated toggles
550
+ const catSkills = getSkillsByCategory(categoryKey);
551
+ const categories = getCategoryList();
552
+ const cat = categories.find((c) => c.key === categoryKey);
553
+ const activeIds = new Set(agent.getActiveSkillIds(chatId));
554
+
555
+ const buttons = catSkills.map((s) => ([{
556
+ text: `${activeIds.has(s.id) ? '✅ ' : ''}${s.emoji} ${s.name}`,
557
+ callback_data: `skill_toggle:${s.id}:${categoryKey}`,
558
+ }]));
559
+ buttons.push([
560
+ { text: '« Back', callback_data: 'skill_back' },
561
+ { text: 'Done', callback_data: 'skill_cancel' },
562
+ ]);
563
+
564
+ const activeSkills = agent.getActiveSkills(chatId);
565
+ const activeLine = activeSkills.length > 0
566
+ ? `Active (${activeSkills.length}): ${activeSkills.map(s => `${s.emoji} ${s.name}`).join(', ')}\n\n`
567
+ : '';
568
+
500
569
  await bot.editMessageText(
501
- `${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.`,
570
+ `${activeLine}${cat ? cat.emoji : ''} *${cat ? cat.name : categoryKey}* tap to toggle:`,
502
571
  {
503
572
  chat_id: chatId,
504
573
  message_id: query.message.message_id,
505
574
  parse_mode: 'Markdown',
575
+ reply_markup: { inline_keyboard: buttons },
506
576
  },
507
577
  );
508
- await bot.answerCallbackQuery(query.id);
578
+ await bot.answerCallbackQuery(query.id, { text: added ? `✅ ${skill.name} on` : `❌ ${skill.name} off` });
509
579
 
510
580
  } else if (data === 'skill_reset') {
511
- logger.info(`[Bot] Skill reset for chat ${chatId}`);
581
+ logger.info(`[Bot] Skills cleared for chat ${chatId}`);
512
582
  agent.clearSkill(chatId);
513
- await bot.editMessageText('🔄 Skill cleared — back to default persona.', {
583
+ await bot.editMessageText('🔄 All skills cleared — back to default persona.', {
514
584
  chat_id: chatId,
515
585
  message_id: query.message.message_id,
516
586
  });
@@ -551,10 +621,11 @@ export function startBot(config, agent, conversationManager, jobManager, automat
551
621
  } else if (data.startsWith('skill_custom_delete:')) {
552
622
  const skillId = data.slice('skill_custom_delete:'.length);
553
623
  logger.info(`[Bot] Custom skill delete request: ${skillId} from chat ${chatId}`);
554
- const activeSkill = agent.getActiveSkill(chatId);
555
- if (activeSkill && activeSkill.id === skillId) {
556
- logger.info(`[Bot] Clearing active skill before deletion: ${skillId}`);
557
- agent.clearSkill(chatId);
624
+ // Remove from active skills if present
625
+ const activeIds = agent.getActiveSkillIds(chatId);
626
+ if (activeIds.includes(skillId)) {
627
+ logger.info(`[Bot] Removing deleted skill from active set: ${skillId}`);
628
+ agent.toggleSkill(chatId, skillId);
558
629
  }
559
630
  const deleted = deleteCustomSkill(skillId);
560
631
  const msg = deleted ? '🗑️ Custom skill deleted.' : 'Skill not found.';
@@ -566,8 +637,8 @@ export function startBot(config, agent, conversationManager, jobManager, automat
566
637
 
567
638
  } else if (data === 'skill_back') {
568
639
  // Re-show category list
569
- const categories = getUnifiedCategoryList();
570
- const activeSkill = agent.getActiveSkill(chatId);
640
+ const categories = getCategoryList();
641
+ const activeSkills = agent.getActiveSkills(chatId);
571
642
  const buttons = categories.map((cat) => ([{
572
643
  text: `${cat.emoji} ${cat.name} (${cat.count})`,
573
644
  callback_data: `skill_category:${cat.key}`,
@@ -578,15 +649,16 @@ export function startBot(config, agent, conversationManager, jobManager, automat
578
649
  customRow.push({ text: '🗑️ Manage Custom', callback_data: 'skill_custom_manage' });
579
650
  }
580
651
  buttons.push(customRow);
581
- const footerRow = [{ text: 'Cancel', callback_data: 'skill_cancel' }];
582
- if (activeSkill) {
583
- footerRow.unshift({ text: '🔄 Reset to Default', callback_data: 'skill_reset' });
652
+ const footerRow = [{ text: 'Done', callback_data: 'skill_cancel' }];
653
+ if (activeSkills.length > 0) {
654
+ footerRow.unshift({ text: '🔄 Clear All', callback_data: 'skill_reset' });
584
655
  }
585
656
  buttons.push(footerRow);
586
657
 
587
- const header = activeSkill
588
- ? `🎭 *Active skill:* ${activeSkill.emoji} ${activeSkill.name}\n\nSelect a category:`
589
- : '🎭 *Skills* — select a category:';
658
+ const activeLine = activeSkills.length > 0
659
+ ? `Active (${activeSkills.length}): ${activeSkills.map(s => `${s.emoji} ${s.name}`).join(', ')}\n\n`
660
+ : '';
661
+ const header = `${activeLine}🎭 *Skills* — select a category:`;
590
662
 
591
663
  await bot.editMessageText(header, {
592
664
  chat_id: chatId,
@@ -597,9 +669,14 @@ export function startBot(config, agent, conversationManager, jobManager, automat
597
669
  await bot.answerCallbackQuery(query.id);
598
670
 
599
671
  } else if (data === 'skill_cancel') {
600
- await bot.editMessageText('Skill selection cancelled.', {
672
+ const activeSkills = agent.getActiveSkills(chatId);
673
+ const msg = activeSkills.length > 0
674
+ ? `🎭 Active skills (${activeSkills.length}): ${activeSkills.map(s => `${s.emoji} ${s.name}`).join(', ')}`
675
+ : 'No skills active.';
676
+ await bot.editMessageText(msg, {
601
677
  chat_id: chatId,
602
678
  message_id: query.message.message_id,
679
+ parse_mode: 'Markdown',
603
680
  });
604
681
  await bot.answerCallbackQuery(query.id);
605
682
 
@@ -685,7 +762,10 @@ export function startBot(config, agent, conversationManager, jobManager, automat
685
762
  await bot.answerCallbackQuery(query.id);
686
763
 
687
764
  } else if (data.startsWith('orch_model:')) {
688
- const [, providerKey, modelId] = data.split(':');
765
+ // Use split with limit to avoid truncating model IDs containing colons
766
+ const parts = data.split(':');
767
+ const providerKey = parts[1];
768
+ const modelId = parts.slice(2).join(':');
689
769
  const providerDef = PROVIDERS[providerKey];
690
770
  const modelEntry = providerDef?.models.find((m) => m.id === modelId);
691
771
  const modelLabel = modelEntry ? modelEntry.label : modelId;
@@ -1136,12 +1216,12 @@ export function startBot(config, agent, conversationManager, jobManager, automat
1136
1216
  return;
1137
1217
  }
1138
1218
  pendingCustomSkill.delete(chatId);
1139
- const skill = addCustomSkill({ name: pending.name, systemPrompt: content });
1219
+ const skill = saveCustomSkill({ name: pending.name, body: content });
1140
1220
  logger.info(`[Bot] Custom skill created from file: "${skill.name}" (${skill.id}) — ${content.length} chars, by ${username} in chat ${chatId}`);
1141
- agent.setSkill(chatId, skill.id);
1221
+ agent.toggleSkill(chatId, skill.id);
1142
1222
  await bot.sendMessage(
1143
1223
  chatId,
1144
- `✅ Custom skill *${skill.name}* created and activated!\n\n_Prompt loaded from file (${content.length} chars)_`,
1224
+ `✅ Custom skill *${skill.name}* created and added to active skills!\n\n_Prompt loaded from file (${content.length} chars)_`,
1145
1225
  { parse_mode: 'Markdown' },
1146
1226
  );
1147
1227
  } catch (err) {
@@ -1323,12 +1403,12 @@ export function startBot(config, agent, conversationManager, jobManager, automat
1323
1403
 
1324
1404
  if (pending.step === 'prompt') {
1325
1405
  pendingCustomSkill.delete(chatId);
1326
- const skill = addCustomSkill({ name: pending.name, systemPrompt: text });
1406
+ const skill = saveCustomSkill({ name: pending.name, body: text });
1327
1407
  logger.info(`[Bot] Custom skill created: "${skill.name}" (${skill.id}) by ${username} in chat ${chatId}`);
1328
- agent.setSkill(chatId, skill.id);
1408
+ agent.toggleSkill(chatId, skill.id);
1329
1409
  await bot.sendMessage(
1330
1410
  chatId,
1331
- `✅ Custom skill *${skill.name}* created and activated!`,
1411
+ `✅ Custom skill *${skill.name}* created and added to active skills!`,
1332
1412
  { parse_mode: 'Markdown' },
1333
1413
  );
1334
1414
  return;
@@ -1536,14 +1616,14 @@ export function startBot(config, agent, conversationManager, jobManager, automat
1536
1616
  if (text === '/skills reset' || text === '/skill reset') {
1537
1617
  logger.info(`[Bot] /skills reset from ${username} (${userId}) in chat ${chatId}`);
1538
1618
  agent.clearSkill(chatId);
1539
- await bot.sendMessage(chatId, '🔄 Skill cleared — back to default persona.');
1619
+ await bot.sendMessage(chatId, '🔄 All skills cleared — back to default persona.');
1540
1620
  return;
1541
1621
  }
1542
1622
 
1543
1623
  if (text === '/skills' || text === '/skill') {
1544
1624
  logger.info(`[Bot] /skills command from ${username} (${userId}) in chat ${chatId}`);
1545
- const categories = getUnifiedCategoryList();
1546
- const activeSkill = agent.getActiveSkill(chatId);
1625
+ const categories = getCategoryList();
1626
+ const activeSkills = agent.getActiveSkills(chatId);
1547
1627
  const buttons = categories.map((cat) => ([{
1548
1628
  text: `${cat.emoji} ${cat.name} (${cat.count})`,
1549
1629
  callback_data: `skill_category:${cat.key}`,
@@ -1555,14 +1635,15 @@ export function startBot(config, agent, conversationManager, jobManager, automat
1555
1635
  }
1556
1636
  buttons.push(customRow);
1557
1637
  const footerRow = [{ text: 'Cancel', callback_data: 'skill_cancel' }];
1558
- if (activeSkill) {
1559
- footerRow.unshift({ text: '🔄 Reset to Default', callback_data: 'skill_reset' });
1638
+ if (activeSkills.length > 0) {
1639
+ footerRow.unshift({ text: '🔄 Clear All', callback_data: 'skill_reset' });
1560
1640
  }
1561
1641
  buttons.push(footerRow);
1562
1642
 
1563
- const header = activeSkill
1564
- ? `🎭 *Active skill:* ${activeSkill.emoji} ${activeSkill.name}\n\nSelect a category:`
1565
- : '🎭 *Skills* — select a category:';
1643
+ const activeLine = activeSkills.length > 0
1644
+ ? `Active (${activeSkills.length}): ${activeSkills.map(s => `${s.emoji} ${s.name}`).join(', ')}\n\n`
1645
+ : '';
1646
+ const header = `${activeLine}🎭 *Skills* — select a category:`;
1566
1647
 
1567
1648
  await bot.sendMessage(chatId, header, {
1568
1649
  parse_mode: 'Markdown',
@@ -1589,7 +1670,7 @@ export function startBot(config, agent, conversationManager, jobManager, automat
1589
1670
  const orchInfo = agent.getOrchestratorInfo();
1590
1671
  const ccInfo = agent.getClaudeCodeInfo();
1591
1672
  const authConfig = agent.getClaudeAuthConfig();
1592
- const activeSkill = agent.getActiveSkill(chatId);
1673
+ const activeSkills = agent.getActiveSkills(chatId);
1593
1674
  const msgCount = agent.getMessageCount(chatId);
1594
1675
  const history = agent.getConversationHistory(chatId);
1595
1676
  const maxHistory = conversationManager.maxHistory;
@@ -1615,9 +1696,9 @@ export function startBot(config, agent, conversationManager, jobManager, automat
1615
1696
  `🎛️ *Orchestrator:* ${orchInfo.providerName} / ${orchInfo.modelLabel}`,
1616
1697
  `🧠 *Brain (Workers):* ${info.providerName} / ${info.modelLabel}`,
1617
1698
  `💻 *Claude Code:* ${ccInfo.modelLabel} (auth: ${authConfig.mode})`,
1618
- activeSkill
1619
- ? `🎭 *Skill:* ${activeSkill.emoji} ${activeSkill.name}`
1620
- : '🎭 *Skill:* Default persona',
1699
+ activeSkills.length > 0
1700
+ ? `🎭 *Skills (${activeSkills.length}):* ${activeSkills.map(s => `${s.emoji} ${s.name}`).join(', ')}`
1701
+ : '🎭 *Skills:* None (default persona)',
1621
1702
  `💬 *Messages in memory:* ${msgCount} / ${maxHistory}`,
1622
1703
  `📌 *Recent window:* ${recentWindow} messages`,
1623
1704
  ].filter(Boolean);
@@ -2251,9 +2332,9 @@ export function startBot(config, agent, conversationManager, jobManager, automat
2251
2332
  }
2252
2333
 
2253
2334
  if (text === '/help') {
2254
- const activeSkill = agent.getActiveSkill(chatId);
2255
- const skillLine = activeSkill
2256
- ? `\n🎭 *Active skill:* ${activeSkill.emoji} ${activeSkill.name}\n`
2335
+ const activeSkills = agent.getActiveSkills(chatId);
2336
+ const skillLine = activeSkills.length > 0
2337
+ ? `\n🎭 *Active skills:* ${activeSkills.map(s => `${s.emoji} ${s.name}`).join(', ')}\n`
2257
2338
  : '';
2258
2339
  await bot.sendMessage(chatId, [
2259
2340
  '*KernelBot Commands*',
@@ -2263,8 +2344,8 @@ export function startBot(config, agent, conversationManager, jobManager, automat
2263
2344
  '/orchestrator — Switch orchestrator AI model/provider',
2264
2345
  '/claudemodel — Switch Claude Code model',
2265
2346
  '/claude — Manage Claude Code authentication',
2266
- '/skills — Browse and activate persona skills',
2267
- '/skills reset — Clear active skill back to default',
2347
+ '/skills — Browse and toggle persona skills (multi-skill)',
2348
+ '/skills reset — Clear all active skills',
2268
2349
  '/jobs — List running and recent jobs',
2269
2350
  '/cancel — Cancel running job(s)',
2270
2351
  '/auto — Manage recurring automations',
@@ -2477,7 +2558,9 @@ export function startBot(config, agent, conversationManager, jobManager, automat
2477
2558
  } catch (err) {
2478
2559
  clearInterval(typingInterval);
2479
2560
  logger.error(`[Bot] Error processing message in chat ${chatId}: ${err.message}`);
2480
- await bot.sendMessage(chatId, `Error: ${err.message}`);
2561
+ // Show a friendly message instead of raw error details
2562
+ const friendly = _friendlyError(err);
2563
+ await bot.sendMessage(chatId, friendly);
2481
2564
  }
2482
2565
  });
2483
2566
  });
package/src/coder.js CHANGED
@@ -137,6 +137,8 @@ export class ClaudeCodeSpawner {
137
137
  constructor(config) {
138
138
  this.config = config;
139
139
  this.maxTurns = config.claude_code?.max_turns || 50;
140
+ // Default 24 hours — background workers can legitimately run for extended periods.
141
+ // Stuck process detection is handled separately by idle/loop heuristics.
140
142
  this.timeout = (config.claude_code?.timeout_seconds || 86400) * 1000;
141
143
  }
142
144
 
@@ -160,12 +162,18 @@ export class ClaudeCodeSpawner {
160
162
  }
161
163
  // authMode === 'system' — pass env as-is
162
164
 
163
- return env;
165
+
166
+ // FIX: Ensure system auth uses local credentials, not the bot's API key
167
+ if (authMode === 'system') {
168
+ delete env.ANTHROPIC_API_KEY;
169
+ }
170
+ return env;
164
171
  }
165
172
 
166
- async run({ workingDirectory, prompt, maxTurns, onOutput, signal }) {
173
+ async run({ workingDirectory, prompt, maxTurns, timeoutMs, onOutput, signal }) {
167
174
  const logger = getLogger();
168
175
  const turns = maxTurns || this.maxTurns;
176
+ const effectiveTimeout = timeoutMs || this.timeout;
169
177
 
170
178
  ensureClaudeCodeSetup();
171
179
 
@@ -185,7 +193,7 @@ export class ClaudeCodeSpawner {
185
193
 
186
194
  const cmd = `claude ${args.map((a) => a.includes(' ') ? `"${a}"` : a).join(' ')}`;
187
195
  logger.info(`Spawning: ${cmd.slice(0, 300)}`);
188
- logger.info(`CWD: ${workingDirectory}`);
196
+ logger.info(`CWD: ${workingDirectory} | Timeout: ${effectiveTimeout / 1000}s | Max turns: ${turns}`);
189
197
 
190
198
  // --- Smart output: consolidate tool activity into one editable message ---
191
199
  let statusMsgId = null;
@@ -216,7 +224,9 @@ export class ClaudeCodeSpawner {
216
224
  } else {
217
225
  statusMsgId = await onOutput(buildStatusText());
218
226
  }
219
- } catch {}
227
+ } catch (err) {
228
+ logger.debug(`flushStatus failed: ${err.message}`);
229
+ }
220
230
  };
221
231
 
222
232
  const addActivity = (line) => {
@@ -301,10 +311,20 @@ export class ClaudeCodeSpawner {
301
311
  });
302
312
 
303
313
  const timer = setTimeout(() => {
314
+ logger.warn(`Claude Code timed out after ${effectiveTimeout / 1000}s — sending SIGTERM`);
304
315
  child.kill('SIGTERM');
305
- if (smartOutput) smartOutput(`▸ Claude Code timed out after ${this.timeout / 1000}s`).catch(() => {});
306
- reject(new Error(`Claude Code timed out after ${this.timeout / 1000}s`));
307
- }, this.timeout);
316
+ if (smartOutput) smartOutput(`▸ Claude Code timed out after ${effectiveTimeout / 1000}s`).catch(() => {});
317
+
318
+ // Give it 10s to exit gracefully after SIGTERM, then force-kill
319
+ setTimeout(() => {
320
+ if (!child.killed) {
321
+ logger.warn('Claude Code did not exit after SIGTERM — sending SIGKILL');
322
+ child.kill('SIGKILL');
323
+ }
324
+ }, 10_000);
325
+
326
+ reject(new Error(`Claude Code timed out after ${effectiveTimeout / 1000}s`));
327
+ }, effectiveTimeout);
308
328
 
309
329
  child.on('close', async (code) => {
310
330
  clearTimeout(timer);
@@ -333,7 +353,9 @@ export class ClaudeCodeSpawner {
333
353
  const finalState = code === 0 ? 'done' : 'error';
334
354
  try {
335
355
  await onOutput(buildStatusText(finalState), { editMessageId: statusMsgId });
336
- } catch {}
356
+ } catch (err) {
357
+ logger.debug(`Final status update failed: ${err.message}`);
358
+ }
337
359
  }
338
360
 
339
361
  logger.info(`Claude Code exited with code ${code} | stdout: ${fullOutput.length} chars | stderr: ${stderr.length} chars`);
@@ -43,10 +43,14 @@ export class ConversationManager {
43
43
  const raw = readFileSync(this.filePath, 'utf-8');
44
44
  const data = JSON.parse(raw);
45
45
 
46
- // Restore per-chat skills
46
+ // Restore per-chat skills (backward compat: string → string[])
47
47
  if (data._skills && typeof data._skills === 'object') {
48
- for (const [chatId, skillId] of Object.entries(data._skills)) {
49
- this.activeSkills.set(String(chatId), skillId);
48
+ for (const [chatId, value] of Object.entries(data._skills)) {
49
+ // Old format stored a single string; new format stores an array
50
+ const skills = Array.isArray(value) ? value : (typeof value === 'string' ? [value] : []);
51
+ if (skills.length > 0) {
52
+ this.activeSkills.set(String(chatId), skills);
53
+ }
50
54
  }
51
55
  }
52
56
 
@@ -72,11 +76,11 @@ export class ConversationManager {
72
76
  for (const [chatId, messages] of this.conversations) {
73
77
  data[chatId] = messages;
74
78
  }
75
- // Persist active skills under a reserved key
79
+ // Persist active skills under a reserved key (stored as arrays)
76
80
  if (this.activeSkills.size > 0) {
77
81
  const skills = {};
78
- for (const [chatId, skillId] of this.activeSkills) {
79
- skills[chatId] = skillId;
82
+ for (const [chatId, skillIds] of this.activeSkills) {
83
+ skills[chatId] = skillIds;
80
84
  }
81
85
  data._skills = skills;
82
86
  }
@@ -235,34 +239,107 @@ export class ConversationManager {
235
239
  return history.length;
236
240
  }
237
241
 
242
+ // ── Multi-skill methods ─────────────────────────────────────────────
243
+
244
+ /** Max active skills per chat. */
245
+ static MAX_SKILLS = 5;
246
+
238
247
  /**
239
- * Activate a skill for a specific chat, persisted across restarts.
240
- * @param {string|number} chatId - Telegram chat identifier.
241
- * @param {string} skillId - Skill identifier to activate.
248
+ * Replace all active skills for a chat.
249
+ * @param {string|number} chatId
250
+ * @param {string[]} skillIds
242
251
  */
243
- setSkill(chatId, skillId) {
244
- this.activeSkills.set(String(chatId), skillId);
252
+ setSkills(chatId, skillIds) {
253
+ const ids = skillIds.slice(0, ConversationManager.MAX_SKILLS);
254
+ this.activeSkills.set(String(chatId), ids);
245
255
  this.save();
246
256
  }
247
257
 
248
258
  /**
249
- * Get the currently active skill for a chat.
250
- * @param {string|number} chatId - Telegram chat identifier.
251
- * @returns {string|null} Active skill identifier, or null if none.
259
+ * Add a skill to a chat's active set. No-op if already active or at max.
260
+ * @param {string|number} chatId
261
+ * @param {string} skillId
262
+ * @returns {boolean} true if added, false if already active or at max
252
263
  */
253
- getSkill(chatId) {
254
- return this.activeSkills.get(String(chatId)) || null;
264
+ addSkill(chatId, skillId) {
265
+ const key = String(chatId);
266
+ const current = this.activeSkills.get(key) || [];
267
+ if (current.includes(skillId)) return false;
268
+ if (current.length >= ConversationManager.MAX_SKILLS) return false;
269
+ current.push(skillId);
270
+ this.activeSkills.set(key, current);
271
+ this.save();
272
+ return true;
255
273
  }
256
274
 
257
275
  /**
258
- * Deactivate the active skill for a chat.
259
- * @param {string|number} chatId - Telegram chat identifier.
276
+ * Remove a skill from a chat's active set.
277
+ * @param {string|number} chatId
278
+ * @param {string} skillId
279
+ * @returns {boolean} true if removed
260
280
  */
261
- clearSkill(chatId) {
281
+ removeSkill(chatId, skillId) {
282
+ const key = String(chatId);
283
+ const current = this.activeSkills.get(key) || [];
284
+ const idx = current.indexOf(skillId);
285
+ if (idx === -1) return false;
286
+ current.splice(idx, 1);
287
+ if (current.length === 0) {
288
+ this.activeSkills.delete(key);
289
+ } else {
290
+ this.activeSkills.set(key, current);
291
+ }
292
+ this.save();
293
+ return true;
294
+ }
295
+
296
+ /**
297
+ * Get all active skill IDs for a chat.
298
+ * @param {string|number} chatId
299
+ * @returns {string[]} Array of active skill IDs (empty if none).
300
+ */
301
+ getSkills(chatId) {
302
+ return this.activeSkills.get(String(chatId)) || [];
303
+ }
304
+
305
+ /**
306
+ * Clear all active skills for a chat.
307
+ * @param {string|number} chatId
308
+ */
309
+ clearSkills(chatId) {
262
310
  this.activeSkills.delete(String(chatId));
263
311
  this.save();
264
312
  }
265
313
 
314
+ // ── Backward-compatible single-skill aliases ───────────────────────
315
+
316
+ /**
317
+ * Activate a single skill (replaces all). Backward compat.
318
+ * @param {string|number} chatId
319
+ * @param {string} skillId
320
+ */
321
+ setSkill(chatId, skillId) {
322
+ this.setSkills(chatId, [skillId]);
323
+ }
324
+
325
+ /**
326
+ * Get the first active skill ID (or null). Backward compat.
327
+ * @param {string|number} chatId
328
+ * @returns {string|null}
329
+ */
330
+ getSkill(chatId) {
331
+ const skills = this.getSkills(chatId);
332
+ return skills.length > 0 ? skills[0] : null;
333
+ }
334
+
335
+ /**
336
+ * Deactivate all skills. Backward compat alias for clearSkills.
337
+ * @param {string|number} chatId
338
+ */
339
+ clearSkill(chatId) {
340
+ this.clearSkills(chatId);
341
+ }
342
+
266
343
  /**
267
344
  * Switch the backing file for this manager.
268
345
  * Saves current data, clears in-memory state, then loads from the new file.
@@ -156,6 +156,12 @@
156
156
  .auto-name { color: var(--accent); font-weight: 600; font-family: var(--font-hud); font-size: 9px; letter-spacing: 1px; }
157
157
  .auto-detail { color: var(--dim); font-size: 10px; }
158
158
 
159
+ /* ═══════ SKILLS ═══════ */
160
+ .skill-item { padding: 2px 0; border-bottom: 1px solid rgba(57,255,20,0.03); }
161
+ .skill-emoji { margin-right: 4px; }
162
+ .skill-name { color: var(--accent); font-family: var(--font-hud); font-size: 10px; letter-spacing: 0.5px; }
163
+ .skill-desc { color: var(--dim); font-size: 9px; margin-top: 1px; }
164
+
159
165
  /* ═══════ MARKDOWN CONTENT ═══════ */
160
166
  .md-content { white-space: pre-wrap; word-wrap: break-word; color: var(--text); font-size: 11px; }
161
167
  .md-content .ts-line { color: var(--accent); }