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.
- package/.env.example +4 -0
- package/README.md +0 -0
- package/bin/kernel.js +13 -6
- package/config.example.yaml +14 -1
- package/package.json +1 -1
- package/src/agent.js +482 -27
- package/src/automation/automation-manager.js +0 -0
- package/src/automation/automation.js +0 -0
- package/src/automation/index.js +0 -0
- package/src/automation/scheduler.js +0 -0
- package/src/bot.js +340 -3
- package/src/claude-auth.js +93 -0
- package/src/coder.js +48 -6
- package/src/conversation.js +0 -0
- package/src/intents/detector.js +0 -0
- package/src/intents/index.js +0 -0
- package/src/intents/planner.js +0 -0
- package/src/persona.js +0 -0
- package/src/prompts/orchestrator.js +53 -5
- package/src/prompts/persona.md +0 -0
- package/src/prompts/system.js +0 -0
- package/src/prompts/workers.js +61 -2
- package/src/providers/anthropic.js +0 -0
- package/src/providers/base.js +0 -0
- package/src/providers/index.js +0 -0
- package/src/providers/models.js +0 -0
- package/src/providers/openai-compat.js +0 -0
- package/src/security/audit.js +0 -0
- package/src/security/auth.js +0 -0
- package/src/security/confirm.js +0 -0
- package/src/self.js +122 -0
- package/src/services/stt.js +139 -0
- package/src/services/tts.js +124 -0
- package/src/skills/catalog.js +0 -0
- package/src/skills/custom.js +0 -0
- package/src/swarm/job-manager.js +54 -7
- package/src/swarm/job.js +19 -1
- package/src/swarm/worker-registry.js +5 -0
- package/src/tools/browser.js +0 -0
- package/src/tools/categories.js +0 -0
- package/src/tools/coding.js +5 -0
- package/src/tools/docker.js +0 -0
- package/src/tools/git.js +0 -0
- package/src/tools/github.js +0 -0
- package/src/tools/index.js +0 -0
- package/src/tools/jira.js +0 -0
- package/src/tools/monitor.js +0 -0
- package/src/tools/network.js +0 -0
- package/src/tools/orchestrator-tools.js +76 -19
- package/src/tools/os.js +14 -1
- package/src/tools/persona.js +0 -0
- package/src/tools/process.js +0 -0
- package/src/utils/config.js +105 -2
- package/src/utils/display.js +0 -0
- package/src/utils/logger.js +0 -0
- 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
|
-
|
|
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 —
|
|
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
|
|
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 ||
|
|
140
|
-
this.model = config.claude_code?.model || null;
|
|
140
|
+
this.timeout = (config.claude_code?.timeout_seconds || 86400) * 1000;
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
-
|
|
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 (
|
|
157
|
-
args.push('--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:
|
|
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 {
|
package/src/conversation.js
CHANGED
|
File without changes
|
package/src/intents/detector.js
CHANGED
|
File without changes
|
package/src/intents/index.js
CHANGED
|
File without changes
|
package/src/intents/planner.js
CHANGED
|
File without changes
|
package/src/persona.js
CHANGED
|
File without changes
|