kernelbot 1.0.28 → 1.0.30

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 (56) hide show
  1. package/.env.example +4 -0
  2. package/README.md +0 -0
  3. package/bin/kernel.js +13 -6
  4. package/config.example.yaml +14 -1
  5. package/package.json +1 -1
  6. package/src/agent.js +482 -27
  7. package/src/automation/automation-manager.js +0 -0
  8. package/src/automation/automation.js +0 -0
  9. package/src/automation/index.js +0 -0
  10. package/src/automation/scheduler.js +0 -0
  11. package/src/bot.js +340 -3
  12. package/src/claude-auth.js +93 -0
  13. package/src/coder.js +48 -6
  14. package/src/conversation.js +0 -0
  15. package/src/intents/detector.js +0 -0
  16. package/src/intents/index.js +0 -0
  17. package/src/intents/planner.js +0 -0
  18. package/src/persona.js +0 -0
  19. package/src/prompts/orchestrator.js +53 -5
  20. package/src/prompts/persona.md +0 -0
  21. package/src/prompts/system.js +0 -0
  22. package/src/prompts/workers.js +61 -2
  23. package/src/providers/anthropic.js +0 -0
  24. package/src/providers/base.js +0 -0
  25. package/src/providers/index.js +0 -0
  26. package/src/providers/models.js +0 -0
  27. package/src/providers/openai-compat.js +0 -0
  28. package/src/security/audit.js +0 -0
  29. package/src/security/auth.js +0 -0
  30. package/src/security/confirm.js +0 -0
  31. package/src/self.js +122 -0
  32. package/src/services/stt.js +139 -0
  33. package/src/services/tts.js +124 -0
  34. package/src/skills/catalog.js +0 -0
  35. package/src/skills/custom.js +0 -0
  36. package/src/swarm/job-manager.js +54 -7
  37. package/src/swarm/job.js +19 -1
  38. package/src/swarm/worker-registry.js +5 -0
  39. package/src/tools/browser.js +0 -0
  40. package/src/tools/categories.js +0 -0
  41. package/src/tools/coding.js +5 -0
  42. package/src/tools/docker.js +0 -0
  43. package/src/tools/git.js +0 -0
  44. package/src/tools/github.js +0 -0
  45. package/src/tools/index.js +0 -0
  46. package/src/tools/jira.js +0 -0
  47. package/src/tools/monitor.js +0 -0
  48. package/src/tools/network.js +0 -0
  49. package/src/tools/orchestrator-tools.js +76 -19
  50. package/src/tools/os.js +14 -1
  51. package/src/tools/persona.js +0 -0
  52. package/src/tools/process.js +0 -0
  53. package/src/utils/config.js +105 -2
  54. package/src/utils/display.js +0 -0
  55. package/src/utils/logger.js +0 -0
  56. package/src/worker.js +96 -5
package/src/bot.js CHANGED
@@ -12,6 +12,9 @@ import {
12
12
  deleteCustomSkill,
13
13
  getCustomSkills,
14
14
  } from './skills/custom.js';
15
+ import { TTSService } from './services/tts.js';
16
+ import { STTService } from './services/stt.js';
17
+ import { getClaudeAuthStatus, claudeLogout } from './claude-auth.js';
15
18
 
16
19
  function splitMessage(text, maxLength = 4096) {
17
20
  if (text.length <= maxLength) return [text];
@@ -56,6 +59,12 @@ export function startBot(config, agent, conversationManager, jobManager, automat
56
59
  const chatQueue = new ChatQueue();
57
60
  const batchWindowMs = config.telegram.batch_window_ms || 3000;
58
61
 
62
+ // Initialize voice services
63
+ const ttsService = new TTSService(config);
64
+ const sttService = new STTService(config);
65
+ if (ttsService.isAvailable()) logger.info('[Bot] TTS service enabled (ElevenLabs)');
66
+ if (sttService.isAvailable()) logger.info('[Bot] STT service enabled');
67
+
59
68
  // Per-chat message batching: chatId -> { messages[], timer, resolve }
60
69
  const chatBatches = new Map();
61
70
 
@@ -68,6 +77,22 @@ export function startBot(config, agent, conversationManager, jobManager, automat
68
77
  // Load custom skills from disk
69
78
  loadCustomSkills();
70
79
 
80
+ // Register commands in Telegram's menu button
81
+ bot.setMyCommands([
82
+ { command: 'brain', description: 'Switch worker AI model/provider' },
83
+ { command: 'orchestrator', description: 'Switch orchestrator AI model/provider' },
84
+ { command: 'claudemodel', description: 'Switch Claude Code model' },
85
+ { command: 'claude', description: 'Manage Claude Code authentication' },
86
+ { command: 'skills', description: 'Browse and activate persona skills' },
87
+ { command: 'jobs', description: 'List running and recent jobs' },
88
+ { command: 'cancel', description: 'Cancel running job(s)' },
89
+ { command: 'auto', description: 'Manage recurring automations' },
90
+ { command: 'context', description: 'Show all models, auth, and context info' },
91
+ { command: 'clean', description: 'Clear conversation and start fresh' },
92
+ { command: 'history', description: 'Show message count in memory' },
93
+ { command: 'help', description: 'Show all available commands' },
94
+ ]).catch((err) => logger.warn(`Failed to set bot commands menu: ${err.message}`));
95
+
71
96
  logger.info('Telegram bot started with polling');
72
97
 
73
98
  // Initialize automation manager with bot context
@@ -142,6 +167,12 @@ export function startBot(config, agent, conversationManager, jobManager, automat
142
167
  // Track pending brain API key input: chatId -> { providerKey, modelId }
143
168
  const pendingBrainKey = new Map();
144
169
 
170
+ // Track pending orchestrator API key input: chatId -> { providerKey, modelId }
171
+ const pendingOrchKey = new Map();
172
+
173
+ // Track pending Claude Code auth input: chatId -> { type: 'api_key' | 'oauth_token' }
174
+ const pendingClaudeAuth = new Map();
175
+
145
176
  // Track pending custom skill creation: chatId -> { step: 'name' | 'prompt', name?: string }
146
177
  const pendingCustomSkill = new Map();
147
178
 
@@ -451,6 +482,140 @@ export function startBot(config, agent, conversationManager, jobManager, automat
451
482
  const msg = deleted ? `🗑️ Deleted automation \`${autoId}\`` : `Automation \`${autoId}\` not found.`;
452
483
  await bot.editMessageText(msg, { chat_id: chatId, message_id: query.message.message_id, parse_mode: 'Markdown' });
453
484
  await bot.answerCallbackQuery(query.id);
485
+
486
+ // ── Orchestrator callbacks ─────────────────────────────────────
487
+ } else if (data.startsWith('orch_provider:')) {
488
+ const providerKey = data.split(':')[1];
489
+ const providerDef = PROVIDERS[providerKey];
490
+ if (!providerDef) {
491
+ await bot.answerCallbackQuery(query.id, { text: 'Unknown provider' });
492
+ return;
493
+ }
494
+
495
+ const modelButtons = providerDef.models.map((m) => ([{
496
+ text: m.label,
497
+ callback_data: `orch_model:${providerKey}:${m.id}`,
498
+ }]));
499
+ modelButtons.push([{ text: 'Cancel', callback_data: 'orch_cancel' }]);
500
+
501
+ await bot.editMessageText(`Select a *${providerDef.name}* model for orchestrator:`, {
502
+ chat_id: chatId,
503
+ message_id: query.message.message_id,
504
+ parse_mode: 'Markdown',
505
+ reply_markup: { inline_keyboard: modelButtons },
506
+ });
507
+ await bot.answerCallbackQuery(query.id);
508
+
509
+ } else if (data.startsWith('orch_model:')) {
510
+ const [, providerKey, modelId] = data.split(':');
511
+ const providerDef = PROVIDERS[providerKey];
512
+ const modelEntry = providerDef?.models.find((m) => m.id === modelId);
513
+ const modelLabel = modelEntry ? modelEntry.label : modelId;
514
+
515
+ await bot.editMessageText(
516
+ `⏳ Verifying *${providerDef.name}* / *${modelLabel}* for orchestrator...`,
517
+ { chat_id: chatId, message_id: query.message.message_id, parse_mode: 'Markdown' },
518
+ );
519
+
520
+ logger.info(`[Bot] Orchestrator switch request: ${providerKey}/${modelId} from chat ${chatId}`);
521
+ const result = await agent.switchOrchestrator(providerKey, modelId);
522
+ if (result && typeof result === 'object' && result.error) {
523
+ const current = agent.getOrchestratorInfo();
524
+ await bot.editMessageText(
525
+ `❌ Failed to switch: ${result.error}\n\nKeeping *${current.providerName}* / *${current.modelLabel}*`,
526
+ { chat_id: chatId, message_id: query.message.message_id, parse_mode: 'Markdown' },
527
+ );
528
+ } else if (result) {
529
+ // API key missing
530
+ logger.info(`[Bot] Orchestrator switch needs API key: ${result} for ${providerKey}/${modelId}`);
531
+ pendingOrchKey.set(chatId, { providerKey, modelId });
532
+ await bot.editMessageText(
533
+ `🔑 *${providerDef.name}* API key is required.\n\nPlease send your \`${result}\` now.\n\nOr send *cancel* to abort.`,
534
+ { chat_id: chatId, message_id: query.message.message_id, parse_mode: 'Markdown' },
535
+ );
536
+ } else {
537
+ const info = agent.getOrchestratorInfo();
538
+ await bot.editMessageText(
539
+ `🎛️ Orchestrator switched to *${info.providerName}* / *${info.modelLabel}*`,
540
+ { chat_id: chatId, message_id: query.message.message_id, parse_mode: 'Markdown' },
541
+ );
542
+ }
543
+ await bot.answerCallbackQuery(query.id);
544
+
545
+ } else if (data === 'orch_cancel') {
546
+ pendingOrchKey.delete(chatId);
547
+ await bot.editMessageText('Orchestrator change cancelled.', {
548
+ chat_id: chatId, message_id: query.message.message_id,
549
+ });
550
+ await bot.answerCallbackQuery(query.id);
551
+
552
+ // ── Claude Code model callbacks ────────────────────────────────
553
+ } else if (data.startsWith('ccmodel:')) {
554
+ const modelId = data.slice('ccmodel:'.length);
555
+ agent.switchClaudeCodeModel(modelId);
556
+ const info = agent.getClaudeCodeInfo();
557
+ logger.info(`[Bot] Claude Code model switched to ${info.modelLabel} from chat ${chatId}`);
558
+ await bot.editMessageText(
559
+ `💻 Claude Code model switched to *${info.modelLabel}*`,
560
+ { chat_id: chatId, message_id: query.message.message_id, parse_mode: 'Markdown' },
561
+ );
562
+ await bot.answerCallbackQuery(query.id);
563
+
564
+ } else if (data === 'ccmodel_cancel') {
565
+ await bot.editMessageText('Claude Code model change cancelled.', {
566
+ chat_id: chatId, message_id: query.message.message_id,
567
+ });
568
+ await bot.answerCallbackQuery(query.id);
569
+
570
+ // ── Claude Code auth callbacks ─────────────────────────────────
571
+ } else if (data === 'claude_apikey') {
572
+ pendingClaudeAuth.set(chatId, { type: 'api_key' });
573
+ await bot.editMessageText(
574
+ '🔑 Send your *Anthropic API key* for Claude Code.\n\nOr send *cancel* to abort.',
575
+ { chat_id: chatId, message_id: query.message.message_id, parse_mode: 'Markdown' },
576
+ );
577
+ await bot.answerCallbackQuery(query.id);
578
+
579
+ } else if (data === 'claude_oauth') {
580
+ pendingClaudeAuth.set(chatId, { type: 'oauth_token' });
581
+ await bot.editMessageText(
582
+ '🔑 Run `claude setup-token` locally and paste the *OAuth token* here.\n\nThis uses your Pro/Max subscription instead of an API key.\n\nOr send *cancel* to abort.',
583
+ { chat_id: chatId, message_id: query.message.message_id, parse_mode: 'Markdown' },
584
+ );
585
+ await bot.answerCallbackQuery(query.id);
586
+
587
+ } else if (data === 'claude_system') {
588
+ agent.setClaudeCodeAuth('system', null);
589
+ logger.info(`[Bot] Claude Code auth set to system from chat ${chatId}`);
590
+ await bot.editMessageText(
591
+ '🔓 Claude Code set to *system auth* — using host machine credentials.',
592
+ { chat_id: chatId, message_id: query.message.message_id, parse_mode: 'Markdown' },
593
+ );
594
+ await bot.answerCallbackQuery(query.id);
595
+
596
+ } else if (data === 'claude_status') {
597
+ await bot.answerCallbackQuery(query.id, { text: 'Checking...' });
598
+ const status = await getClaudeAuthStatus();
599
+ const authConfig = agent.getClaudeAuthConfig();
600
+ await bot.editMessageText(
601
+ `🔐 *Claude Code Auth*\n\n*Mode:* ${authConfig.mode}\n*Credential:* ${authConfig.credential}\n\n*CLI Status:*\n\`\`\`\n${status.output.slice(0, 500)}\n\`\`\``,
602
+ { chat_id: chatId, message_id: query.message.message_id, parse_mode: 'Markdown' },
603
+ );
604
+
605
+ } else if (data === 'claude_logout') {
606
+ await bot.answerCallbackQuery(query.id, { text: 'Logging out...' });
607
+ const result = await claudeLogout();
608
+ await bot.editMessageText(
609
+ `🚪 Claude Code logout: ${result.output || 'Done.'}`,
610
+ { chat_id: chatId, message_id: query.message.message_id, parse_mode: 'Markdown' },
611
+ );
612
+
613
+ } else if (data === 'claude_cancel') {
614
+ pendingClaudeAuth.delete(chatId);
615
+ await bot.editMessageText('Claude Code auth management dismissed.', {
616
+ chat_id: chatId, message_id: query.message.message_id,
617
+ });
618
+ await bot.answerCallbackQuery(query.id);
454
619
  }
455
620
  } catch (err) {
456
621
  logger.error(`[Bot] Callback query error for "${data}" in chat ${chatId}: ${err.message}`);
@@ -546,6 +711,32 @@ export function startBot(config, agent, conversationManager, jobManager, automat
546
711
  }
547
712
  }
548
713
 
714
+ // Handle voice messages — transcribe and process as text
715
+ if (msg.voice && sttService.isAvailable()) {
716
+ logger.info(`[Bot] Voice message from ${username} (${userId}) in chat ${chatId}, duration: ${msg.voice.duration}s`);
717
+ let tmpPath = null;
718
+ try {
719
+ const fileUrl = await bot.getFileLink(msg.voice.file_id);
720
+ tmpPath = await sttService.downloadAudio(fileUrl);
721
+ const transcribed = await sttService.transcribe(tmpPath);
722
+ if (!transcribed) {
723
+ await bot.sendMessage(chatId, 'Could not transcribe the voice message. Please try again or send text.');
724
+ return;
725
+ }
726
+ logger.info(`[Bot] Transcribed voice: "${transcribed.slice(0, 100)}" from ${username} in chat ${chatId}`);
727
+ // Show the user what was heard
728
+ await bot.sendMessage(chatId, `🎤 _"${transcribed}"_`, { parse_mode: 'Markdown' });
729
+ // Process as a normal text message (fall through below)
730
+ msg.text = transcribed;
731
+ } catch (err) {
732
+ logger.error(`[Bot] Voice transcription failed: ${err.message}`);
733
+ await bot.sendMessage(chatId, 'Failed to process voice message. Please try sending text instead.');
734
+ return;
735
+ } finally {
736
+ if (tmpPath) sttService.cleanup(tmpPath);
737
+ }
738
+ }
739
+
549
740
  if (!msg.text) return; // ignore non-text (and non-document) messages
550
741
 
551
742
  let text = msg.text.trim();
@@ -582,6 +773,60 @@ export function startBot(config, agent, conversationManager, jobManager, automat
582
773
  return;
583
774
  }
584
775
 
776
+ // Handle pending orchestrator API key input
777
+ if (pendingOrchKey.has(chatId)) {
778
+ const pending = pendingOrchKey.get(chatId);
779
+ pendingOrchKey.delete(chatId);
780
+
781
+ if (text.toLowerCase() === 'cancel') {
782
+ logger.info(`[Bot] Orchestrator key input cancelled by ${username} in chat ${chatId}`);
783
+ await bot.sendMessage(chatId, 'Orchestrator change cancelled.');
784
+ return;
785
+ }
786
+
787
+ logger.info(`[Bot] Orchestrator key received for ${pending.providerKey}/${pending.modelId} from ${username} in chat ${chatId}`);
788
+ await bot.sendMessage(chatId, '⏳ Verifying API key...');
789
+ const switchResult = await agent.switchOrchestratorWithKey(pending.providerKey, pending.modelId, text);
790
+ if (switchResult && switchResult.error) {
791
+ const current = agent.getOrchestratorInfo();
792
+ await bot.sendMessage(
793
+ chatId,
794
+ `❌ Failed to switch: ${switchResult.error}\n\nKeeping *${current.providerName}* / *${current.modelLabel}*`,
795
+ { parse_mode: 'Markdown' },
796
+ );
797
+ } else {
798
+ const info = agent.getOrchestratorInfo();
799
+ await bot.sendMessage(
800
+ chatId,
801
+ `🎛️ Orchestrator switched to *${info.providerName}* / *${info.modelLabel}*\n\nAPI key saved.`,
802
+ { parse_mode: 'Markdown' },
803
+ );
804
+ }
805
+ return;
806
+ }
807
+
808
+ // Handle pending Claude Code auth input
809
+ if (pendingClaudeAuth.has(chatId)) {
810
+ const pending = pendingClaudeAuth.get(chatId);
811
+ pendingClaudeAuth.delete(chatId);
812
+
813
+ if (text.toLowerCase() === 'cancel') {
814
+ logger.info(`[Bot] Claude Code auth input cancelled by ${username} in chat ${chatId}`);
815
+ await bot.sendMessage(chatId, 'Claude Code auth setup cancelled.');
816
+ return;
817
+ }
818
+
819
+ agent.setClaudeCodeAuth(pending.type, text);
820
+ const label = pending.type === 'api_key' ? 'API Key' : 'OAuth Token';
821
+ logger.info(`[Bot] Claude Code ${label} saved from ${username} in chat ${chatId}`);
822
+ await bot.sendMessage(
823
+ chatId,
824
+ `🔐 Claude Code *${label}* saved and activated.\n\nNext Claude Code spawn will use this credential.`,
825
+ { parse_mode: 'Markdown' },
826
+ );
827
+ return;
828
+ }
829
+
585
830
  // Handle pending custom skill creation (text input for name or prompt)
586
831
  if (pendingCustomSkill.has(chatId)) {
587
832
  const pending = pendingCustomSkill.get(chatId);
@@ -640,6 +885,78 @@ export function startBot(config, agent, conversationManager, jobManager, automat
640
885
  return;
641
886
  }
642
887
 
888
+ if (text === '/orchestrator') {
889
+ logger.info(`[Bot] /orchestrator command from ${username} (${userId}) in chat ${chatId}`);
890
+ const info = agent.getOrchestratorInfo();
891
+ const providerKeys = Object.keys(PROVIDERS);
892
+ const buttons = providerKeys.map((key) => ([{
893
+ text: `${PROVIDERS[key].name}${key === info.provider ? ' ✓' : ''}`,
894
+ callback_data: `orch_provider:${key}`,
895
+ }]));
896
+ buttons.push([{ text: 'Cancel', callback_data: 'orch_cancel' }]);
897
+
898
+ await bot.sendMessage(
899
+ chatId,
900
+ `🎛️ *Current orchestrator:* ${info.providerName} / ${info.modelLabel}\n\nSelect a provider to switch:`,
901
+ {
902
+ parse_mode: 'Markdown',
903
+ reply_markup: { inline_keyboard: buttons },
904
+ },
905
+ );
906
+ return;
907
+ }
908
+
909
+ if (text === '/claudemodel') {
910
+ logger.info(`[Bot] /claudemodel command from ${username} (${userId}) in chat ${chatId}`);
911
+ const info = agent.getClaudeCodeInfo();
912
+ const anthropicModels = PROVIDERS.anthropic.models;
913
+ const buttons = anthropicModels.map((m) => ([{
914
+ text: `${m.label}${m.id === info.model ? ' ✓' : ''}`,
915
+ callback_data: `ccmodel:${m.id}`,
916
+ }]));
917
+ buttons.push([{ text: 'Cancel', callback_data: 'ccmodel_cancel' }]);
918
+
919
+ await bot.sendMessage(
920
+ chatId,
921
+ `💻 *Current Claude Code model:* ${info.modelLabel}\n\nSelect a model:`,
922
+ {
923
+ parse_mode: 'Markdown',
924
+ reply_markup: { inline_keyboard: buttons },
925
+ },
926
+ );
927
+ return;
928
+ }
929
+
930
+ if (text === '/claude') {
931
+ logger.info(`[Bot] /claude command from ${username} (${userId}) in chat ${chatId}`);
932
+ const authConfig = agent.getClaudeAuthConfig();
933
+ const ccInfo = agent.getClaudeCodeInfo();
934
+
935
+ const modeLabels = { system: '🔓 System Login', api_key: '🔑 API Key', oauth_token: '🎫 OAuth Token (Pro/Max)' };
936
+ const modeLabel = modeLabels[authConfig.mode] || authConfig.mode;
937
+
938
+ const buttons = [
939
+ [{ text: '🔑 Set API Key', callback_data: 'claude_apikey' }],
940
+ [{ text: '🎫 Set OAuth Token (Pro/Max)', callback_data: 'claude_oauth' }],
941
+ [{ text: '🔓 Use System Auth', callback_data: 'claude_system' }],
942
+ [
943
+ { text: '🔄 Refresh Status', callback_data: 'claude_status' },
944
+ { text: '🚪 Logout', callback_data: 'claude_logout' },
945
+ ],
946
+ [{ text: 'Cancel', callback_data: 'claude_cancel' }],
947
+ ];
948
+
949
+ await bot.sendMessage(
950
+ chatId,
951
+ `🔐 *Claude Code Auth*\n\n*Auth Mode:* ${modeLabel}\n*Credential:* ${authConfig.credential}\n*Model:* ${ccInfo.modelLabel}\n\nSelect an action:`,
952
+ {
953
+ parse_mode: 'Markdown',
954
+ reply_markup: { inline_keyboard: buttons },
955
+ },
956
+ );
957
+ return;
958
+ }
959
+
643
960
  if (text === '/skills reset' || text === '/skill reset') {
644
961
  logger.info(`[Bot] /skills reset from ${username} (${userId}) in chat ${chatId}`);
645
962
  agent.clearSkill(chatId);
@@ -693,6 +1010,9 @@ export function startBot(config, agent, conversationManager, jobManager, automat
693
1010
 
694
1011
  if (text === '/context') {
695
1012
  const info = agent.getBrainInfo();
1013
+ const orchInfo = agent.getOrchestratorInfo();
1014
+ const ccInfo = agent.getClaudeCodeInfo();
1015
+ const authConfig = agent.getClaudeAuthConfig();
696
1016
  const activeSkill = agent.getActiveSkill(chatId);
697
1017
  const msgCount = conversationManager.getMessageCount(chatId);
698
1018
  const history = conversationManager.getHistory(chatId);
@@ -711,7 +1031,9 @@ export function startBot(config, agent, conversationManager, jobManager, automat
711
1031
  const lines = [
712
1032
  '📋 *Conversation Context*',
713
1033
  '',
714
- `🧠 *Brain:* ${info.providerName} / ${info.modelLabel}`,
1034
+ `🎛️ *Orchestrator:* ${orchInfo.providerName} / ${orchInfo.modelLabel}`,
1035
+ `🧠 *Brain (Workers):* ${info.providerName} / ${info.modelLabel}`,
1036
+ `💻 *Claude Code:* ${ccInfo.modelLabel} (auth: ${authConfig.mode})`,
715
1037
  activeSkill
716
1038
  ? `🎭 *Skill:* ${activeSkill.emoji} ${activeSkill.name}`
717
1039
  : '🎭 *Skill:* Default persona',
@@ -786,13 +1108,16 @@ export function startBot(config, agent, conversationManager, jobManager, automat
786
1108
  await bot.sendMessage(chatId, [
787
1109
  '*KernelBot Commands*',
788
1110
  skillLine,
789
- '/brain — Show current AI model and switch provider/model',
1111
+ '/brain — Switch worker AI model/provider',
1112
+ '/orchestrator — Switch orchestrator AI model/provider',
1113
+ '/claudemodel — Switch Claude Code model',
1114
+ '/claude — Manage Claude Code authentication',
790
1115
  '/skills — Browse and activate persona skills',
791
1116
  '/skills reset — Clear active skill back to default',
792
1117
  '/jobs — List running and recent jobs',
793
1118
  '/cancel — Cancel running job(s)',
794
1119
  '/auto — Manage recurring automations',
795
- '/context — Show current conversation context and brain info',
1120
+ '/context — Show all models, auth, and context info',
796
1121
  '/clean — Clear conversation and start fresh',
797
1122
  '/history — Show message count in memory',
798
1123
  '/browse <url> — Browse a website and get a summary',
@@ -1024,6 +1349,18 @@ export function startBot(config, agent, conversationManager, jobManager, automat
1024
1349
  await bot.sendMessage(chatId, chunk);
1025
1350
  }
1026
1351
  }
1352
+
1353
+ // Send voice reply if TTS is available and the reply isn't too short
1354
+ if (ttsService.isAvailable() && reply && reply.length > 5) {
1355
+ try {
1356
+ const audioPath = await ttsService.synthesize(reply);
1357
+ if (audioPath) {
1358
+ await bot.sendVoice(chatId, createReadStream(audioPath));
1359
+ }
1360
+ } catch (err) {
1361
+ logger.warn(`[Bot] TTS voice reply failed: ${err.message}`);
1362
+ }
1363
+ }
1027
1364
  } catch (err) {
1028
1365
  clearInterval(typingInterval);
1029
1366
  logger.error(`[Bot] Error processing message in chat ${chatId}: ${err.message}`);
@@ -0,0 +1,93 @@
1
+ import { spawn } from 'child_process';
2
+ import { getLogger } from './utils/logger.js';
3
+
4
+ /**
5
+ * Run `claude auth status` and return parsed output.
6
+ */
7
+ export function getClaudeAuthStatus() {
8
+ const logger = getLogger();
9
+ return new Promise((resolve) => {
10
+ const child = spawn('claude', ['auth', 'status'], {
11
+ stdio: ['ignore', 'pipe', 'pipe'],
12
+ });
13
+
14
+ let stdout = '';
15
+ let stderr = '';
16
+
17
+ child.stdout.on('data', (d) => { stdout += d.toString(); });
18
+ child.stderr.on('data', (d) => { stderr += d.toString(); });
19
+
20
+ child.on('close', (code) => {
21
+ const output = (stdout || stderr).trim();
22
+ logger.debug(`claude auth status (code ${code}): ${output.slice(0, 300)}`);
23
+ resolve({ code, output });
24
+ });
25
+
26
+ child.on('error', (err) => {
27
+ if (err.code === 'ENOENT') {
28
+ resolve({ code: -1, output: 'Claude Code CLI not found. Install with: npm i -g @anthropic-ai/claude-code' });
29
+ } else {
30
+ resolve({ code: -1, output: err.message });
31
+ }
32
+ });
33
+
34
+ // Timeout after 10s
35
+ setTimeout(() => {
36
+ child.kill('SIGTERM');
37
+ resolve({ code: -1, output: 'Timed out checking auth status' });
38
+ }, 10_000);
39
+ });
40
+ }
41
+
42
+ /**
43
+ * Run `claude auth logout`.
44
+ */
45
+ export function claudeLogout() {
46
+ const logger = getLogger();
47
+ return new Promise((resolve) => {
48
+ const child = spawn('claude', ['auth', 'logout'], {
49
+ stdio: ['ignore', 'pipe', 'pipe'],
50
+ });
51
+
52
+ let stdout = '';
53
+ let stderr = '';
54
+
55
+ child.stdout.on('data', (d) => { stdout += d.toString(); });
56
+ child.stderr.on('data', (d) => { stderr += d.toString(); });
57
+
58
+ child.on('close', (code) => {
59
+ const output = (stdout || stderr).trim();
60
+ logger.info(`claude auth logout (code ${code}): ${output.slice(0, 300)}`);
61
+ resolve({ code, output });
62
+ });
63
+
64
+ child.on('error', (err) => {
65
+ resolve({ code: -1, output: err.message });
66
+ });
67
+
68
+ setTimeout(() => {
69
+ child.kill('SIGTERM');
70
+ resolve({ code: -1, output: 'Timed out during logout' });
71
+ }, 10_000);
72
+ });
73
+ }
74
+
75
+ /**
76
+ * Return the current Claude Code auth mode from config.
77
+ */
78
+ export function getClaudeCodeAuthMode(config) {
79
+ const mode = config.claude_code?.auth_mode || 'system';
80
+ const info = { mode };
81
+
82
+ if (mode === 'api_key') {
83
+ const key = config.claude_code?.api_key || process.env.CLAUDE_CODE_API_KEY || '';
84
+ info.credential = key ? `${key.slice(0, 8)}...${key.slice(-4)}` : '(not set)';
85
+ } else if (mode === 'oauth_token') {
86
+ const token = config.claude_code?.oauth_token || process.env.CLAUDE_CODE_OAUTH_TOKEN || '';
87
+ info.credential = token ? `${token.slice(0, 8)}...${token.slice(-4)}` : '(not set)';
88
+ } else {
89
+ info.credential = 'Using host system login';
90
+ }
91
+
92
+ return info;
93
+ }
package/src/coder.js CHANGED
@@ -135,17 +135,43 @@ function processEvent(line, onOutput, logger) {
135
135
 
136
136
  export class ClaudeCodeSpawner {
137
137
  constructor(config) {
138
+ this.config = config;
138
139
  this.maxTurns = config.claude_code?.max_turns || 50;
139
- this.timeout = (config.claude_code?.timeout_seconds || 600) * 1000;
140
- this.model = config.claude_code?.model || null;
140
+ this.timeout = (config.claude_code?.timeout_seconds || 86400) * 1000;
141
141
  }
142
142
 
143
- async run({ workingDirectory, prompt, maxTurns, onOutput }) {
143
+ _buildSpawnEnv() {
144
+ const authMode = this.config.claude_code?.auth_mode || 'system';
145
+ const env = { ...process.env, IS_SANDBOX: '1' };
146
+
147
+ if (authMode === 'api_key') {
148
+ const key = this.config.claude_code?.api_key || process.env.CLAUDE_CODE_API_KEY;
149
+ if (key) {
150
+ env.ANTHROPIC_API_KEY = key;
151
+ delete env.CLAUDE_CODE_OAUTH_TOKEN;
152
+ }
153
+ } else if (authMode === 'oauth_token') {
154
+ const token = this.config.claude_code?.oauth_token || process.env.CLAUDE_CODE_OAUTH_TOKEN;
155
+ if (token) {
156
+ env.CLAUDE_CODE_OAUTH_TOKEN = token;
157
+ // Remove ANTHROPIC_API_KEY so it doesn't override subscription auth
158
+ delete env.ANTHROPIC_API_KEY;
159
+ }
160
+ }
161
+ // authMode === 'system' — pass env as-is
162
+
163
+ return env;
164
+ }
165
+
166
+ async run({ workingDirectory, prompt, maxTurns, onOutput, signal }) {
144
167
  const logger = getLogger();
145
168
  const turns = maxTurns || this.maxTurns;
146
169
 
147
170
  ensureClaudeCodeSetup();
148
171
 
172
+ // Read model dynamically from config (supports hot-reload via switchClaudeCodeModel)
173
+ const model = this.config.claude_code?.model || null;
174
+
149
175
  const args = [
150
176
  '-p', prompt,
151
177
  '--max-turns', String(turns),
@@ -153,8 +179,8 @@ export class ClaudeCodeSpawner {
153
179
  '--verbose',
154
180
  '--dangerously-skip-permissions',
155
181
  ];
156
- if (this.model) {
157
- args.push('--model', this.model);
182
+ if (model) {
183
+ args.push('--model', model);
158
184
  }
159
185
 
160
186
  const cmd = `claude ${args.map((a) => a.includes(' ') ? `"${a}"` : a).join(' ')}`;
@@ -219,10 +245,24 @@ export class ClaudeCodeSpawner {
219
245
  return new Promise((resolve, reject) => {
220
246
  const child = spawn('claude', args, {
221
247
  cwd: workingDirectory,
222
- env: { ...process.env, IS_SANDBOX: '1' },
248
+ env: this._buildSpawnEnv(),
223
249
  stdio: ['ignore', 'pipe', 'pipe'],
224
250
  });
225
251
 
252
+ // Wire abort signal to kill the child process
253
+ let abortHandler = null;
254
+ if (signal) {
255
+ if (signal.aborted) {
256
+ child.kill('SIGTERM');
257
+ } else {
258
+ abortHandler = () => {
259
+ logger.info('Claude Code: abort signal received — killing child process');
260
+ child.kill('SIGTERM');
261
+ };
262
+ signal.addEventListener('abort', abortHandler, { once: true });
263
+ }
264
+ }
265
+
226
266
  let fullOutput = '';
227
267
  let stderr = '';
228
268
  let buffer = '';
@@ -268,6 +308,7 @@ export class ClaudeCodeSpawner {
268
308
 
269
309
  child.on('close', async (code) => {
270
310
  clearTimeout(timer);
311
+ if (abortHandler && signal) signal.removeEventListener('abort', abortHandler);
271
312
 
272
313
  if (buffer.trim()) {
273
314
  fullOutput += buffer.trim();
@@ -312,6 +353,7 @@ export class ClaudeCodeSpawner {
312
353
 
313
354
  child.on('error', (err) => {
314
355
  clearTimeout(timer);
356
+ if (abortHandler && signal) signal.removeEventListener('abort', abortHandler);
315
357
  if (err.code === 'ENOENT') {
316
358
  reject(new Error('Claude Code CLI not found. Install with: npm i -g @anthropic-ai/claude-code'));
317
359
  } else {
File without changes
File without changes
File without changes
File without changes
package/src/persona.js CHANGED
File without changes