kernelbot 1.0.37 → 1.0.38

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/src/bot.js CHANGED
@@ -16,6 +16,8 @@ import { TTSService } from './services/tts.js';
16
16
  import { STTService } from './services/stt.js';
17
17
  import { getClaudeAuthStatus, claudeLogout } from './claude-auth.js';
18
18
  import { isQuietHours } from './utils/timeUtils.js';
19
+ import { CharacterBuilder } from './characters/builder.js';
20
+ import { LifeEngine } from './life/engine.js';
19
21
 
20
22
  /**
21
23
  * Simulate a human-like typing delay based on response length.
@@ -120,8 +122,12 @@ function createOnUpdate(bot, chatId) {
120
122
  lastMsgId = sent.message_id;
121
123
  } catch (mdErr) {
122
124
  logger.debug(`[Bot] Markdown send failed for chat ${chatId}, falling back to plain: ${mdErr.message}`);
123
- const sent = await bot.sendMessage(chatId, part);
124
- lastMsgId = sent.message_id;
125
+ try {
126
+ const sent = await bot.sendMessage(chatId, part);
127
+ lastMsgId = sent.message_id;
128
+ } catch (plainErr) {
129
+ logger.error(`[Bot] Plain-text send also failed for chat ${chatId}: ${plainErr.message}`);
130
+ }
125
131
  }
126
132
  }
127
133
  return lastMsgId;
@@ -156,11 +162,16 @@ function createSendPhoto(bot, chatId, logger) {
156
162
  * Create a sendReaction callback for reacting to messages with emoji.
157
163
  */
158
164
  function createSendReaction(bot) {
165
+ const logger = getLogger();
159
166
  return async (targetChatId, targetMsgId, emoji, isBig = false) => {
160
- await bot.setMessageReaction(targetChatId, targetMsgId, {
161
- reaction: [{ type: 'emoji', emoji }],
162
- is_big: isBig,
163
- });
167
+ try {
168
+ await bot.setMessageReaction(targetChatId, targetMsgId, {
169
+ reaction: [{ type: 'emoji', emoji }],
170
+ is_big: isBig,
171
+ });
172
+ } catch (err) {
173
+ logger.debug(`[Bot] Failed to set reaction for msg ${targetMsgId} in chat ${targetChatId}: ${err.message}`);
174
+ }
164
175
  };
165
176
  }
166
177
 
@@ -196,7 +207,7 @@ class ChatQueue {
196
207
  }
197
208
 
198
209
  export function startBot(config, agent, conversationManager, jobManager, automationManager, lifeDeps = {}) {
199
- const { lifeEngine, memoryManager, journalManager, shareQueue, evolutionTracker, codebaseKnowledge } = lifeDeps;
210
+ let { lifeEngine, memoryManager, journalManager, shareQueue, evolutionTracker, codebaseKnowledge, characterManager, dashboardHandle, dashboardDeps } = lifeDeps;
200
211
  const logger = getLogger();
201
212
  const bot = new TelegramBot(config.telegram.bot_token, {
202
213
  polling: {
@@ -214,6 +225,52 @@ export function startBot(config, agent, conversationManager, jobManager, automat
214
225
  if (ttsService.isAvailable()) logger.info('[Bot] TTS service enabled (ElevenLabs)');
215
226
  if (sttService.isAvailable()) logger.info('[Bot] STT service enabled');
216
227
 
228
+ /**
229
+ * Rebuild the life engine for a different character.
230
+ * Stops the current engine, creates a new one with scoped managers, and starts it.
231
+ */
232
+ function rebuildLifeEngine(charCtx) {
233
+ if (lifeEngine) {
234
+ lifeEngine.stop();
235
+ }
236
+
237
+ // Update module-level manager refs so other bot.js code uses the right ones
238
+ memoryManager = charCtx.memoryManager;
239
+ journalManager = charCtx.journalManager;
240
+ shareQueue = charCtx.shareQueue;
241
+ evolutionTracker = charCtx.evolutionTracker;
242
+
243
+ // Switch conversation file to the new character's conversations.json
244
+ conversationManager.switchFile(charCtx.conversationFilePath);
245
+
246
+ const lifeEnabled = config.life?.enabled !== false;
247
+ if (!lifeEnabled) {
248
+ lifeEngine = null;
249
+ return;
250
+ }
251
+
252
+ lifeEngine = new LifeEngine({
253
+ config,
254
+ agent,
255
+ memoryManager: charCtx.memoryManager,
256
+ journalManager: charCtx.journalManager,
257
+ shareQueue: charCtx.shareQueue,
258
+ evolutionTracker: charCtx.evolutionTracker,
259
+ codebaseKnowledge,
260
+ selfManager: charCtx.selfManager,
261
+ basePath: charCtx.lifeBasePath,
262
+ characterId: charCtx.characterId,
263
+ });
264
+
265
+ lifeEngine.wakeUp().then(() => {
266
+ lifeEngine.start();
267
+ logger.info(`[Bot] Life engine rebuilt for character: ${charCtx.characterId}`);
268
+ }).catch(err => {
269
+ logger.error(`[Bot] Life engine wake-up failed: ${err.message}`);
270
+ lifeEngine.start();
271
+ });
272
+ }
273
+
217
274
  // Per-chat message batching: chatId -> { messages[], timer, resolve }
218
275
  const chatBatches = new Map();
219
276
 
@@ -228,6 +285,7 @@ export function startBot(config, agent, conversationManager, jobManager, automat
228
285
 
229
286
  // Register commands in Telegram's menu button
230
287
  bot.setMyCommands([
288
+ { command: 'character', description: 'Switch or manage characters' },
231
289
  { command: 'brain', description: 'Switch worker AI model/provider' },
232
290
  { command: 'orchestrator', description: 'Switch orchestrator AI model/provider' },
233
291
  { command: 'claudemodel', description: 'Switch Claude Code model' },
@@ -240,6 +298,9 @@ export function startBot(config, agent, conversationManager, jobManager, automat
240
298
  { command: 'journal', description: 'View today\'s journal or a past date' },
241
299
  { command: 'memories', description: 'View recent memories or search' },
242
300
  { command: 'evolution', description: 'Self-evolution status, history, and lessons' },
301
+ { command: 'linkedin', description: 'Link/unlink LinkedIn account' },
302
+ { command: 'x', description: 'Link/unlink X (Twitter) account' },
303
+ { command: 'dashboard', description: 'Start/stop the monitoring dashboard' },
243
304
  { command: 'context', description: 'Show all models, auth, and context info' },
244
305
  { command: 'clean', description: 'Clear conversation and start fresh' },
245
306
  { command: 'history', description: 'Show message count in memory' },
@@ -283,6 +344,9 @@ export function startBot(config, agent, conversationManager, jobManager, automat
283
344
  // Track pending custom skill creation: chatId -> { step: 'name' | 'prompt', name?: string }
284
345
  const pendingCustomSkill = new Map();
285
346
 
347
+ // Track pending custom character build: chatId -> { answers: {}, step: number }
348
+ const pendingCharacterBuild = new Map();
349
+
286
350
  // Handle inline keyboard callbacks for /brain
287
351
  bot.on('callback_query', async (query) => {
288
352
  const chatId = query.message.chat.id;
@@ -730,6 +794,230 @@ export function startBot(config, agent, conversationManager, jobManager, automat
730
794
  chat_id: chatId, message_id: query.message.message_id,
731
795
  });
732
796
  await bot.answerCallbackQuery(query.id);
797
+
798
+ // ── Character callbacks ────────────────────────────────────────
799
+ } else if (data.startsWith('char_select:')) {
800
+ const charId = data.slice('char_select:'.length);
801
+ if (!characterManager) {
802
+ await bot.answerCallbackQuery(query.id, { text: 'Character system not available' });
803
+ return;
804
+ }
805
+ const character = characterManager.getCharacter(charId);
806
+ if (!character) {
807
+ await bot.answerCallbackQuery(query.id, { text: 'Character not found' });
808
+ return;
809
+ }
810
+ const activeId = agent.getActiveCharacterInfo()?.id;
811
+ const isActive = activeId === charId;
812
+ const buttons = [];
813
+ if (!isActive) {
814
+ buttons.push([{ text: `Switch to ${character.emoji} ${character.name}`, callback_data: `char_confirm:${charId}` }]);
815
+ }
816
+ buttons.push([
817
+ { text: '« Back', callback_data: 'char_back' },
818
+ { text: 'Cancel', callback_data: 'char_cancel' },
819
+ ]);
820
+
821
+ const artBlock = character.asciiArt ? `\n\`\`\`\n${character.asciiArt}\n\`\`\`\n` : '\n';
822
+ await bot.editMessageText(
823
+ `${character.emoji} *${character.name}*\n_${character.origin || 'Original'}_${artBlock}\n"${character.tagline}"\n\n*Age:* ${character.age}\n${isActive ? '_(Currently active)_' : ''}`,
824
+ {
825
+ chat_id: chatId,
826
+ message_id: query.message.message_id,
827
+ parse_mode: 'Markdown',
828
+ reply_markup: { inline_keyboard: buttons },
829
+ },
830
+ );
831
+ await bot.answerCallbackQuery(query.id);
832
+
833
+ } else if (data.startsWith('char_confirm:')) {
834
+ const charId = data.slice('char_confirm:'.length);
835
+ if (!characterManager) {
836
+ await bot.answerCallbackQuery(query.id, { text: 'Character system not available' });
837
+ return;
838
+ }
839
+
840
+ await bot.editMessageText(
841
+ `Switching character...`,
842
+ { chat_id: chatId, message_id: query.message.message_id },
843
+ );
844
+
845
+ try {
846
+ const charCtx = agent.switchCharacter(charId);
847
+
848
+ // Rebuild life engine with new character's scoped managers
849
+ rebuildLifeEngine(charCtx);
850
+
851
+ const character = characterManager.getCharacter(charId);
852
+ logger.info(`[Bot] Character switched to ${character.name} (${charId})`);
853
+
854
+ await bot.editMessageText(
855
+ `${character.emoji} *${character.name}* is now active!\n\n"${character.tagline}"`,
856
+ { chat_id: chatId, message_id: query.message.message_id, parse_mode: 'Markdown' },
857
+ );
858
+ } catch (err) {
859
+ logger.error(`[Bot] Character switch failed: ${err.message}`);
860
+ await bot.editMessageText(
861
+ `Failed to switch character: ${err.message}`,
862
+ { chat_id: chatId, message_id: query.message.message_id },
863
+ );
864
+ }
865
+ await bot.answerCallbackQuery(query.id);
866
+
867
+ } else if (data === 'char_custom') {
868
+ pendingCharacterBuild.set(chatId, { answers: {}, step: 0 });
869
+ const builder = new CharacterBuilder(agent.orchestratorProvider);
870
+ const nextQ = builder.getNextQuestion({});
871
+ if (nextQ) {
872
+ await bot.editMessageText(
873
+ `*Custom Character Builder* (1/${builder.getTotalQuestions()})\n\n${nextQ.question}\n\n_Examples: ${nextQ.examples}_\n\nSend *cancel* to abort.`,
874
+ { chat_id: chatId, message_id: query.message.message_id, parse_mode: 'Markdown' },
875
+ );
876
+ }
877
+ await bot.answerCallbackQuery(query.id);
878
+
879
+ } else if (data.startsWith('char_delete:')) {
880
+ const charId = data.slice('char_delete:'.length);
881
+ if (!characterManager) {
882
+ await bot.answerCallbackQuery(query.id, { text: 'Character system not available' });
883
+ return;
884
+ }
885
+ const buttons = [
886
+ [{ text: `Yes, delete`, callback_data: `char_delete_confirm:${charId}` }],
887
+ [{ text: 'Cancel', callback_data: 'char_back' }],
888
+ ];
889
+ const character = characterManager.getCharacter(charId);
890
+ await bot.editMessageText(
891
+ `Are you sure you want to delete *${character?.name || charId}*?\n\nThis will remove all their memories, journals, and conversation history.`,
892
+ {
893
+ chat_id: chatId,
894
+ message_id: query.message.message_id,
895
+ parse_mode: 'Markdown',
896
+ reply_markup: { inline_keyboard: buttons },
897
+ },
898
+ );
899
+ await bot.answerCallbackQuery(query.id);
900
+
901
+ } else if (data.startsWith('char_delete_confirm:')) {
902
+ const charId = data.slice('char_delete_confirm:'.length);
903
+ try {
904
+ characterManager.removeCharacter(charId);
905
+ await bot.editMessageText(`Character deleted.`, {
906
+ chat_id: chatId, message_id: query.message.message_id,
907
+ });
908
+ } catch (err) {
909
+ await bot.editMessageText(`Cannot delete: ${err.message}`, {
910
+ chat_id: chatId, message_id: query.message.message_id,
911
+ });
912
+ }
913
+ await bot.answerCallbackQuery(query.id);
914
+
915
+ } else if (data === 'char_back') {
916
+ // Re-show character gallery
917
+ if (!characterManager) {
918
+ await bot.answerCallbackQuery(query.id, { text: 'Character system not available' });
919
+ return;
920
+ }
921
+ const characters = characterManager.listCharacters();
922
+ const activeInfo = agent.getActiveCharacterInfo();
923
+ const buttons = [];
924
+ const row1 = [], row2 = [];
925
+ for (const c of characters) {
926
+ const label = `${c.emoji} ${c.name}${activeInfo?.id === c.id ? ' \u2713' : ''}`;
927
+ const btn = { text: label, callback_data: `char_select:${c.id}` };
928
+ if (row1.length < 3) row1.push(btn);
929
+ else row2.push(btn);
930
+ }
931
+ if (row1.length > 0) buttons.push(row1);
932
+ if (row2.length > 0) buttons.push(row2);
933
+
934
+ // Custom character + delete buttons
935
+ const mgmtRow = [{ text: 'Build Custom', callback_data: 'char_custom' }];
936
+ const customChars = characters.filter(c => c.type === 'custom');
937
+ if (customChars.length > 0) {
938
+ mgmtRow.push({ text: 'Delete Custom', callback_data: 'char_delete_pick' });
939
+ }
940
+ buttons.push(mgmtRow);
941
+ buttons.push([{ text: 'Cancel', callback_data: 'char_cancel' }]);
942
+
943
+ await bot.editMessageText(
944
+ `*Active:* ${activeInfo?.emoji || ''} ${activeInfo?.name || 'None'}\n_"${activeInfo?.tagline || ''}"_\n\nSelect a character:`,
945
+ {
946
+ chat_id: chatId,
947
+ message_id: query.message.message_id,
948
+ parse_mode: 'Markdown',
949
+ reply_markup: { inline_keyboard: buttons },
950
+ },
951
+ );
952
+ await bot.answerCallbackQuery(query.id);
953
+
954
+ } else if (data === 'char_delete_pick') {
955
+ const customChars = characterManager.listCharacters().filter(c => c.type === 'custom');
956
+ if (customChars.length === 0) {
957
+ await bot.answerCallbackQuery(query.id, { text: 'No custom characters' });
958
+ return;
959
+ }
960
+ const buttons = customChars.map(c => ([{
961
+ text: `${c.emoji} ${c.name}`,
962
+ callback_data: `char_delete:${c.id}`,
963
+ }]));
964
+ buttons.push([{ text: '« Back', callback_data: 'char_back' }]);
965
+ await bot.editMessageText('Select a custom character to delete:', {
966
+ chat_id: chatId,
967
+ message_id: query.message.message_id,
968
+ reply_markup: { inline_keyboard: buttons },
969
+ });
970
+ await bot.answerCallbackQuery(query.id);
971
+
972
+ } else if (data === 'char_cancel') {
973
+ pendingCharacterBuild.delete(chatId);
974
+ await bot.editMessageText('Character selection dismissed.', {
975
+ chat_id: chatId, message_id: query.message.message_id,
976
+ });
977
+ await bot.answerCallbackQuery(query.id);
978
+
979
+ // ── Onboarding callbacks ───────────────────────────────────────
980
+ } else if (data.startsWith('onboard_select:')) {
981
+ const charId = data.slice('onboard_select:'.length);
982
+ if (!characterManager) {
983
+ await bot.answerCallbackQuery(query.id, { text: 'Not available' });
984
+ return;
985
+ }
986
+
987
+ characterManager.completeOnboarding(charId);
988
+ const charCtx = agent.loadCharacter(charId);
989
+ const character = characterManager.getCharacter(charId);
990
+
991
+ // Start life engine for the selected character
992
+ rebuildLifeEngine(charCtx);
993
+
994
+ logger.info(`[Bot] Onboarding complete — character: ${character.name} (${charId})`);
995
+
996
+ await bot.editMessageText(
997
+ `${character.emoji} *${character.name}* activated!\n\n"${character.tagline}"\n\nSend me a message to start chatting.`,
998
+ { chat_id: chatId, message_id: query.message.message_id, parse_mode: 'Markdown' },
999
+ );
1000
+ await bot.answerCallbackQuery(query.id);
1001
+
1002
+ } else if (data === 'onboard_custom') {
1003
+ // Start custom builder during onboarding — install builtins first
1004
+ if (characterManager?.needsOnboarding) {
1005
+ // Complete onboarding with kernel as default, then build custom
1006
+ characterManager.completeOnboarding('kernel');
1007
+ const kernelCtx = agent.loadCharacter('kernel');
1008
+ rebuildLifeEngine(kernelCtx);
1009
+ }
1010
+
1011
+ pendingCharacterBuild.set(chatId, { answers: {}, step: 0 });
1012
+ const builder = new CharacterBuilder(agent.orchestratorProvider);
1013
+ const nextQ = builder.getNextQuestion({});
1014
+ if (nextQ) {
1015
+ await bot.editMessageText(
1016
+ `*Custom Character Builder* (1/${builder.getTotalQuestions()})\n\n${nextQ.question}\n\n_Examples: ${nextQ.examples}_\n\nSend *cancel* to abort.`,
1017
+ { chat_id: chatId, message_id: query.message.message_id, parse_mode: 'Markdown' },
1018
+ );
1019
+ }
1020
+ await bot.answerCallbackQuery(query.id);
733
1021
  }
734
1022
  } catch (err) {
735
1023
  logger.error(`[Bot] Callback query error for "${data}" in chat ${chatId}: ${err.message}`);
@@ -797,6 +1085,38 @@ export function startBot(config, agent, conversationManager, jobManager, automat
797
1085
  return;
798
1086
  }
799
1087
 
1088
+ // ── Character onboarding ─────────────────────────────────────
1089
+ // On first-ever message, show character selection gallery
1090
+ if (characterManager?.needsOnboarding) {
1091
+ logger.info(`[Bot] First message from ${username} — showing character onboarding`);
1092
+
1093
+ const characters = characterManager.listCharacters();
1094
+ const buttons = [];
1095
+ const row1 = [], row2 = [];
1096
+ for (const c of characters) {
1097
+ const btn = { text: `${c.emoji} ${c.name}`, callback_data: `onboard_select:${c.id}` };
1098
+ if (row1.length < 3) row1.push(btn);
1099
+ else row2.push(btn);
1100
+ }
1101
+ if (row1.length > 0) buttons.push(row1);
1102
+ if (row2.length > 0) buttons.push(row2);
1103
+ buttons.push([{ text: 'Build Custom', callback_data: 'onboard_custom' }]);
1104
+
1105
+ await bot.sendMessage(chatId, [
1106
+ '*Choose Your Character*',
1107
+ '',
1108
+ 'Pick who you want me to be. Each character has their own personality, memories, and story that evolves with you.',
1109
+ '',
1110
+ ...characters.map(c => `${c.emoji} *${c.name}* — _${c.tagline}_`),
1111
+ '',
1112
+ 'Select below:',
1113
+ ].join('\n'), {
1114
+ parse_mode: 'Markdown',
1115
+ reply_markup: { inline_keyboard: buttons },
1116
+ });
1117
+ return;
1118
+ }
1119
+
800
1120
  // Handle file upload for pending custom skill prompt step
801
1121
  if (msg.document && pendingCustomSkill.has(chatId)) {
802
1122
  const pending = pendingCustomSkill.get(chatId);
@@ -1015,7 +1335,111 @@ export function startBot(config, agent, conversationManager, jobManager, automat
1015
1335
  }
1016
1336
  }
1017
1337
 
1338
+ // Handle pending custom character build
1339
+ if (pendingCharacterBuild.has(chatId)) {
1340
+ const pending = pendingCharacterBuild.get(chatId);
1341
+
1342
+ if (text.toLowerCase() === 'cancel') {
1343
+ pendingCharacterBuild.delete(chatId);
1344
+ await bot.sendMessage(chatId, 'Character creation cancelled.');
1345
+ return;
1346
+ }
1347
+
1348
+ const builder = new CharacterBuilder(agent.orchestratorProvider);
1349
+ const nextQ = builder.getNextQuestion(pending.answers);
1350
+ if (nextQ) {
1351
+ pending.answers[nextQ.id] = text;
1352
+ pending.step++;
1353
+ pendingCharacterBuild.set(chatId, pending);
1354
+
1355
+ const followUp = builder.getNextQuestion(pending.answers);
1356
+ if (followUp) {
1357
+ const { answered, total } = builder.getProgress(pending.answers);
1358
+ await bot.sendMessage(
1359
+ chatId,
1360
+ `*Custom Character Builder* (${answered + 1}/${total})\n\n${followUp.question}\n\n_Examples: ${followUp.examples}_`,
1361
+ { parse_mode: 'Markdown' },
1362
+ );
1363
+ } else {
1364
+ // All questions answered — generate character
1365
+ pendingCharacterBuild.delete(chatId);
1366
+ await bot.sendMessage(chatId, 'Creating your character...');
1367
+
1368
+ try {
1369
+ const result = await builder.generateCharacter(pending.answers);
1370
+ const profile = characterManager.addCharacter(
1371
+ {
1372
+ id: result.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, ''),
1373
+ type: 'custom',
1374
+ name: result.name,
1375
+ origin: 'Custom',
1376
+ age: result.age,
1377
+ emoji: result.emoji,
1378
+ tagline: result.tagline,
1379
+ },
1380
+ result.personaMd,
1381
+ result.selfDefaults,
1382
+ );
1383
+
1384
+ // Auto-switch to the new character and rebuild life engine
1385
+ const charCtx = agent.switchCharacter(profile.id);
1386
+ rebuildLifeEngine(charCtx);
1387
+
1388
+ await bot.sendMessage(
1389
+ chatId,
1390
+ `${profile.emoji} *${profile.name}* has been created and activated!\n\n"${profile.tagline}"\n\n_Use /character to switch between characters._`,
1391
+ { parse_mode: 'Markdown' },
1392
+ );
1393
+ logger.info(`[Bot] Custom character created: ${profile.name} (${profile.id}) by ${username}`);
1394
+ } catch (err) {
1395
+ logger.error(`[Bot] Character generation failed: ${err.message}`);
1396
+ await bot.sendMessage(chatId, `Failed to create character: ${err.message}\n\nUse /character to try again.`);
1397
+ }
1398
+ }
1399
+ }
1400
+ return;
1401
+ }
1402
+
1018
1403
  // Handle commands — these bypass batching entirely
1404
+ if (text === '/character') {
1405
+ logger.info(`[Bot] /character command from ${username} (${userId}) in chat ${chatId}`);
1406
+ if (!characterManager) {
1407
+ await bot.sendMessage(chatId, 'Character system not available.');
1408
+ return;
1409
+ }
1410
+ const characters = characterManager.listCharacters();
1411
+ const activeInfo = agent.getActiveCharacterInfo();
1412
+ const buttons = [];
1413
+ const row1 = [], row2 = [];
1414
+ for (const c of characters) {
1415
+ const label = `${c.emoji} ${c.name}${activeInfo?.id === c.id ? ' \u2713' : ''}`;
1416
+ const btn = { text: label, callback_data: `char_select:${c.id}` };
1417
+ if (row1.length < 3) row1.push(btn);
1418
+ else row2.push(btn);
1419
+ }
1420
+ if (row1.length > 0) buttons.push(row1);
1421
+ if (row2.length > 0) buttons.push(row2);
1422
+
1423
+ // Custom character + delete buttons
1424
+ const mgmtRow = [{ text: 'Build Custom', callback_data: 'char_custom' }];
1425
+ const customChars = characters.filter(c => c.type === 'custom');
1426
+ if (customChars.length > 0) {
1427
+ mgmtRow.push({ text: 'Delete Custom', callback_data: 'char_delete_pick' });
1428
+ }
1429
+ buttons.push(mgmtRow);
1430
+ buttons.push([{ text: 'Cancel', callback_data: 'char_cancel' }]);
1431
+
1432
+ await bot.sendMessage(
1433
+ chatId,
1434
+ `*Active:* ${activeInfo?.emoji || ''} ${activeInfo?.name || 'None'}\n_"${activeInfo?.tagline || ''}"_\n\nSelect a character:`,
1435
+ {
1436
+ parse_mode: 'Markdown',
1437
+ reply_markup: { inline_keyboard: buttons },
1438
+ },
1439
+ );
1440
+ return;
1441
+ }
1442
+
1019
1443
  if (text === '/brain') {
1020
1444
  logger.info(`[Bot] /brain command from ${username} (${userId}) in chat ${chatId}`);
1021
1445
  const info = agent.getBrainInfo();
@@ -1148,14 +1572,14 @@ export function startBot(config, agent, conversationManager, jobManager, automat
1148
1572
  }
1149
1573
 
1150
1574
  if (text === '/clean' || text === '/clear' || text === '/reset') {
1151
- conversationManager.clear(chatId);
1575
+ agent.clearConversation(chatId);
1152
1576
  logger.info(`Conversation cleared for chat ${chatId} by ${username}`);
1153
1577
  await bot.sendMessage(chatId, '🧹 Conversation cleared. Starting fresh.');
1154
1578
  return;
1155
1579
  }
1156
1580
 
1157
1581
  if (text === '/history') {
1158
- const count = conversationManager.getMessageCount(chatId);
1582
+ const count = agent.getMessageCount(chatId);
1159
1583
  await bot.sendMessage(chatId, `📝 This chat has *${count}* messages in memory.`, { parse_mode: 'Markdown' });
1160
1584
  return;
1161
1585
  }
@@ -1166,8 +1590,8 @@ export function startBot(config, agent, conversationManager, jobManager, automat
1166
1590
  const ccInfo = agent.getClaudeCodeInfo();
1167
1591
  const authConfig = agent.getClaudeAuthConfig();
1168
1592
  const activeSkill = agent.getActiveSkill(chatId);
1169
- const msgCount = conversationManager.getMessageCount(chatId);
1170
- const history = conversationManager.getHistory(chatId);
1593
+ const msgCount = agent.getMessageCount(chatId);
1594
+ const history = agent.getConversationHistory(chatId);
1171
1595
  const maxHistory = conversationManager.maxHistory;
1172
1596
  const recentWindow = conversationManager.recentWindow;
1173
1597
 
@@ -1180,9 +1604,14 @@ export function startBot(config, agent, conversationManager, jobManager, automat
1180
1604
  return txt.length > 80 ? txt.slice(0, 80) + '…' : txt;
1181
1605
  });
1182
1606
 
1607
+ const activeChar = agent.getActiveCharacterInfo();
1608
+
1183
1609
  const lines = [
1184
1610
  '📋 *Conversation Context*',
1185
1611
  '',
1612
+ activeChar
1613
+ ? `${activeChar.emoji} *Character:* ${activeChar.name}`
1614
+ : '',
1186
1615
  `🎛️ *Orchestrator:* ${orchInfo.providerName} / ${orchInfo.modelLabel}`,
1187
1616
  `🧠 *Brain (Workers):* ${info.providerName} / ${info.modelLabel}`,
1188
1617
  `💻 *Claude Code:* ${ccInfo.modelLabel} (auth: ${authConfig.mode})`,
@@ -1191,7 +1620,7 @@ export function startBot(config, agent, conversationManager, jobManager, automat
1191
1620
  : '🎭 *Skill:* Default persona',
1192
1621
  `💬 *Messages in memory:* ${msgCount} / ${maxHistory}`,
1193
1622
  `📌 *Recent window:* ${recentWindow} messages`,
1194
- ];
1623
+ ].filter(Boolean);
1195
1624
 
1196
1625
  if (recentUserMsgs.length > 0) {
1197
1626
  lines.push('', '🕐 *Recent topics:*');
@@ -1338,6 +1767,63 @@ export function startBot(config, agent, conversationManager, jobManager, automat
1338
1767
  return;
1339
1768
  }
1340
1769
 
1770
+ // ── /dashboard command ────────────────────────────────────────
1771
+ if (text === '/dashboard' || text.startsWith('/dashboard ')) {
1772
+ logger.info(`[Bot] /dashboard command from ${username} (${userId}) in chat ${chatId}`);
1773
+ const args = text.slice('/dashboard'.length).trim();
1774
+ const port = config.dashboard?.port || 3000;
1775
+
1776
+ if (args === 'start') {
1777
+ if (dashboardHandle) {
1778
+ await bot.sendMessage(chatId, `Dashboard already running at http://localhost:${port}`);
1779
+ return;
1780
+ }
1781
+ try {
1782
+ const { startDashboard } = await import('./dashboard/server.js');
1783
+ dashboardHandle = startDashboard({ port, ...dashboardDeps });
1784
+ logger.info(`[Dashboard] Started via /dashboard command on port ${port}`);
1785
+ await bot.sendMessage(chatId, `🖥️ Dashboard started at http://localhost:${port}`);
1786
+ } catch (err) {
1787
+ logger.error(`[Dashboard] Failed to start: ${err.message}`);
1788
+ await bot.sendMessage(chatId, `Failed to start dashboard: ${err.message}`);
1789
+ }
1790
+ return;
1791
+ }
1792
+
1793
+ if (args === 'stop') {
1794
+ if (!dashboardHandle) {
1795
+ await bot.sendMessage(chatId, 'Dashboard is not running.');
1796
+ return;
1797
+ }
1798
+ try {
1799
+ dashboardHandle.stop();
1800
+ dashboardHandle = null;
1801
+ logger.info('[Dashboard] Stopped via /dashboard command');
1802
+ await bot.sendMessage(chatId, '🛑 Dashboard stopped.');
1803
+ } catch (err) {
1804
+ logger.error(`[Dashboard] Failed to stop: ${err.message}`);
1805
+ await bot.sendMessage(chatId, `Failed to stop dashboard: ${err.message}`);
1806
+ }
1807
+ return;
1808
+ }
1809
+
1810
+ // Default: show status
1811
+ const running = !!dashboardHandle;
1812
+ const lines = [
1813
+ '🖥️ *Dashboard*',
1814
+ '',
1815
+ `*Status:* ${running ? '🟢 Running' : '⚪ Stopped'}`,
1816
+ `*Port:* ${port}`,
1817
+ running ? `*URL:* http://localhost:${port}` : '',
1818
+ '',
1819
+ '_Commands:_',
1820
+ '`/dashboard start` — Start the dashboard',
1821
+ '`/dashboard stop` — Stop the dashboard',
1822
+ ].filter(Boolean);
1823
+ await bot.sendMessage(chatId, lines.join('\n'), { parse_mode: 'Markdown' });
1824
+ return;
1825
+ }
1826
+
1341
1827
  // ── /journal command ─────────────────────────────────────────
1342
1828
  if (text === '/journal' || text.startsWith('/journal ')) {
1343
1829
  logger.info(`[Bot] /journal command from ${username} (${userId}) in chat ${chatId}`);
@@ -1542,6 +2028,228 @@ export function startBot(config, agent, conversationManager, jobManager, automat
1542
2028
  return;
1543
2029
  }
1544
2030
 
2031
+ // ── /linkedin command ───────────────────────────────────────────
2032
+ if (text === '/linkedin' || text.startsWith('/linkedin ')) {
2033
+ logger.info(`[Bot] /linkedin command from ${username} (${userId}) in chat ${chatId}`);
2034
+ const args = text.slice('/linkedin'.length).trim();
2035
+
2036
+ // /linkedin link <token> — validate token and save
2037
+ if (args.startsWith('link')) {
2038
+ const token = args.slice('link'.length).trim();
2039
+ if (!token) {
2040
+ await bot.sendMessage(chatId, [
2041
+ '🔗 *Connect your LinkedIn account*',
2042
+ '',
2043
+ '1. Go to https://www.linkedin.com/developers/tools/oauth/token-generator',
2044
+ '2. Select your app, pick scopes: `openid`, `profile`, `email`, `w_member_social`',
2045
+ '3. Authorize and copy the token',
2046
+ '4. Run: `/linkedin link <your-token>`',
2047
+ ].join('\n'), { parse_mode: 'Markdown' });
2048
+ return;
2049
+ }
2050
+
2051
+ await bot.sendMessage(chatId, '⏳ Validating token...');
2052
+
2053
+ try {
2054
+ // Try /v2/userinfo (requires "Sign in with LinkedIn" product → openid+profile scopes)
2055
+ const res = await fetch('https://api.linkedin.com/v2/userinfo', {
2056
+ headers: { 'Authorization': `Bearer ${token}` },
2057
+ });
2058
+
2059
+ const { saveCredential } = await import('./utils/config.js');
2060
+
2061
+ if (res.ok) {
2062
+ const profile = await res.json();
2063
+ const personUrn = `urn:li:person:${profile.sub}`;
2064
+
2065
+ saveCredential(config, 'LINKEDIN_ACCESS_TOKEN', token);
2066
+ saveCredential(config, 'LINKEDIN_PERSON_URN', personUrn);
2067
+
2068
+ await bot.sendMessage(chatId, [
2069
+ '✅ *LinkedIn connected!*',
2070
+ '',
2071
+ `👤 *${profile.name}*`,
2072
+ profile.email ? `📧 ${profile.email}` : '',
2073
+ '',
2074
+ 'You can now ask me to post on LinkedIn, view your posts, comment, and more.',
2075
+ ].filter(Boolean).join('\n'), { parse_mode: 'Markdown' });
2076
+ } else if (res.status === 401) {
2077
+ throw new Error('Invalid or expired token.');
2078
+ } else {
2079
+ // 403 = token works but missing profile scopes → save token, ask for URN
2080
+ saveCredential(config, 'LINKEDIN_ACCESS_TOKEN', token);
2081
+
2082
+ await bot.sendMessage(chatId, [
2083
+ '⚠️ *Token saved* but profile auto-detect unavailable.',
2084
+ '',
2085
+ 'Your token lacks `openid`+`profile` scopes (only `w_member_social`).',
2086
+ 'To fix: add *"Sign in with LinkedIn using OpenID Connect"* to your app at the Developer Portal, then regenerate the token.',
2087
+ '',
2088
+ 'For now, send your person URN to complete setup:',
2089
+ '`/linkedin urn urn:li:person:XXXXX`',
2090
+ '',
2091
+ 'Find your sub value in your LinkedIn Developer app → Auth tab.',
2092
+ ].join('\n'), { parse_mode: 'Markdown' });
2093
+ }
2094
+ } catch (err) {
2095
+ logger.error(`[Bot] LinkedIn token validation failed: ${err.message}`);
2096
+ await bot.sendMessage(chatId, `❌ Token validation failed: ${err.message}`);
2097
+ }
2098
+ return;
2099
+ }
2100
+
2101
+ // /linkedin urn <value> — manually set person URN
2102
+ if (args.startsWith('urn')) {
2103
+ const urn = args.slice('urn'.length).trim();
2104
+ if (!urn) {
2105
+ await bot.sendMessage(chatId, 'Usage: `/linkedin urn urn:li:person:XXXXX`', { parse_mode: 'Markdown' });
2106
+ return;
2107
+ }
2108
+ const personUrn = urn.startsWith('urn:li:person:') ? urn : `urn:li:person:${urn}`;
2109
+ const { saveCredential } = await import('./utils/config.js');
2110
+ saveCredential(config, 'LINKEDIN_PERSON_URN', personUrn);
2111
+
2112
+ await bot.sendMessage(chatId, `✅ Person URN saved: \`${personUrn}\``, { parse_mode: 'Markdown' });
2113
+ return;
2114
+ }
2115
+
2116
+ // /linkedin unlink — clear saved token
2117
+ if (args === 'unlink') {
2118
+ if (!config.linkedin?.access_token) {
2119
+ await bot.sendMessage(chatId, 'Your LinkedIn account is not connected.');
2120
+ return;
2121
+ }
2122
+
2123
+ const { saveCredential } = await import('./utils/config.js');
2124
+ saveCredential(config, 'LINKEDIN_ACCESS_TOKEN', '');
2125
+ saveCredential(config, 'LINKEDIN_PERSON_URN', '');
2126
+ // Clear from live config
2127
+ config.linkedin.access_token = null;
2128
+ config.linkedin.person_urn = null;
2129
+
2130
+ await bot.sendMessage(chatId, '✅ LinkedIn account disconnected.');
2131
+ return;
2132
+ }
2133
+
2134
+ // /linkedin (status) — show connection status
2135
+ if (!config.linkedin?.access_token) {
2136
+ await bot.sendMessage(chatId, [
2137
+ '📱 *LinkedIn — Not Connected*',
2138
+ '',
2139
+ 'Use `/linkedin link <token>` to connect your account.',
2140
+ '',
2141
+ 'Get a token: https://www.linkedin.com/developers/tools/oauth/token-generator',
2142
+ ].join('\n'), { parse_mode: 'Markdown' });
2143
+ return;
2144
+ }
2145
+
2146
+ await bot.sendMessage(chatId, [
2147
+ '📱 *LinkedIn — Connected*',
2148
+ '',
2149
+ `🔑 Token: \`${config.linkedin.access_token.slice(0, 8)}...${config.linkedin.access_token.slice(-4)}\``,
2150
+ config.linkedin.person_urn ? `👤 URN: \`${config.linkedin.person_urn}\`` : '',
2151
+ '',
2152
+ '`/linkedin unlink` — Disconnect account',
2153
+ ].filter(Boolean).join('\n'), { parse_mode: 'Markdown' });
2154
+ return;
2155
+ }
2156
+
2157
+ // ── /x command ─────────────────────────────────────────────────
2158
+ if (text === '/x' || text.startsWith('/x ')) {
2159
+ logger.info(`[Bot] /x command from ${username} (${userId}) in chat ${chatId}`);
2160
+ const args = text.slice('/x'.length).trim();
2161
+
2162
+ // /x link <consumer_key> <consumer_secret> <access_token> <access_token_secret>
2163
+ if (args.startsWith('link')) {
2164
+ const keys = args.slice('link'.length).trim().split(/\s+/);
2165
+ if (keys.length !== 4) {
2166
+ await bot.sendMessage(chatId, [
2167
+ '🔗 *Connect your X (Twitter) account*',
2168
+ '',
2169
+ 'You need 4 credentials from your X Developer App:',
2170
+ '1. Consumer Key (API Key)',
2171
+ '2. Consumer Secret (API Secret)',
2172
+ '3. Access Token',
2173
+ '4. Access Token Secret',
2174
+ '',
2175
+ 'Run: `/x link <consumer_key> <consumer_secret> <access_token> <access_token_secret>`',
2176
+ '',
2177
+ '⚠️ Make sure your app has *Read and Write* permissions for posting tweets.',
2178
+ ].join('\n'), { parse_mode: 'Markdown' });
2179
+ return;
2180
+ }
2181
+
2182
+ const [consumerKey, consumerSecret, accessToken, accessTokenSecret] = keys;
2183
+ await bot.sendMessage(chatId, '⏳ Validating credentials...');
2184
+
2185
+ try {
2186
+ const { XApi } = await import('./services/x-api.js');
2187
+ const client = new XApi({ consumerKey, consumerSecret, accessToken, accessTokenSecret });
2188
+ const profile = await client.getMe();
2189
+
2190
+ const { saveCredential } = await import('./utils/config.js');
2191
+ saveCredential(config, 'X_CONSUMER_KEY', consumerKey);
2192
+ saveCredential(config, 'X_CONSUMER_SECRET', consumerSecret);
2193
+ saveCredential(config, 'X_ACCESS_TOKEN', accessToken);
2194
+ saveCredential(config, 'X_ACCESS_TOKEN_SECRET', accessTokenSecret);
2195
+
2196
+ await bot.sendMessage(chatId, [
2197
+ '✅ *X (Twitter) connected!*',
2198
+ '',
2199
+ `👤 *${profile.name}* (@${profile.username})`,
2200
+ profile.description ? `📝 ${profile.description}` : '',
2201
+ '',
2202
+ 'You can now ask me to tweet, search tweets, like, retweet, and more.',
2203
+ ].filter(Boolean).join('\n'), { parse_mode: 'Markdown' });
2204
+ } catch (err) {
2205
+ logger.error(`[Bot] X token validation failed: ${err.message}`);
2206
+ const detail = err.response?.data?.detail || err.response?.data?.title || err.message;
2207
+ await bot.sendMessage(chatId, `❌ Validation failed: ${detail}`);
2208
+ }
2209
+ return;
2210
+ }
2211
+
2212
+ // /x unlink — clear saved credentials
2213
+ if (args === 'unlink') {
2214
+ if (!config.x?.consumer_key) {
2215
+ await bot.sendMessage(chatId, 'Your X account is not connected.');
2216
+ return;
2217
+ }
2218
+
2219
+ const { saveCredential } = await import('./utils/config.js');
2220
+ saveCredential(config, 'X_CONSUMER_KEY', '');
2221
+ saveCredential(config, 'X_CONSUMER_SECRET', '');
2222
+ saveCredential(config, 'X_ACCESS_TOKEN', '');
2223
+ saveCredential(config, 'X_ACCESS_TOKEN_SECRET', '');
2224
+ config.x = {};
2225
+
2226
+ await bot.sendMessage(chatId, '✅ X (Twitter) account disconnected.');
2227
+ return;
2228
+ }
2229
+
2230
+ // /x (status) — show connection status
2231
+ if (!config.x?.consumer_key) {
2232
+ await bot.sendMessage(chatId, [
2233
+ '🐦 *X (Twitter) — Not Connected*',
2234
+ '',
2235
+ 'Use `/x link <consumer_key> <consumer_secret> <access_token> <access_token_secret>` to connect.',
2236
+ '',
2237
+ 'Get credentials from https://developer.x.com/en/portal/dashboard',
2238
+ ].join('\n'), { parse_mode: 'Markdown' });
2239
+ return;
2240
+ }
2241
+
2242
+ await bot.sendMessage(chatId, [
2243
+ '🐦 *X (Twitter) — Connected*',
2244
+ '',
2245
+ `🔑 Consumer Key: \`${config.x.consumer_key.slice(0, 6)}...${config.x.consumer_key.slice(-4)}\``,
2246
+ `🔑 Access Token: \`${config.x.access_token.slice(0, 6)}...${config.x.access_token.slice(-4)}\``,
2247
+ '',
2248
+ '`/x unlink` — Disconnect account',
2249
+ ].join('\n'), { parse_mode: 'Markdown' });
2250
+ return;
2251
+ }
2252
+
1545
2253
  if (text === '/help') {
1546
2254
  const activeSkill = agent.getActiveSkill(chatId);
1547
2255
  const skillLine = activeSkill
@@ -1550,6 +2258,7 @@ export function startBot(config, agent, conversationManager, jobManager, automat
1550
2258
  await bot.sendMessage(chatId, [
1551
2259
  '*KernelBot Commands*',
1552
2260
  skillLine,
2261
+ '/character — Switch or manage characters',
1553
2262
  '/brain — Switch worker AI model/provider',
1554
2263
  '/orchestrator — Switch orchestrator AI model/provider',
1555
2264
  '/claudemodel — Switch Claude Code model',
@@ -1563,6 +2272,9 @@ export function startBot(config, agent, conversationManager, jobManager, automat
1563
2272
  '/journal — View today\'s journal or a past date',
1564
2273
  '/memories — View recent memories or search',
1565
2274
  '/evolution — Self-evolution status, history, lessons',
2275
+ '/dashboard — Start/stop the monitoring dashboard',
2276
+ '/linkedin — Link/unlink your LinkedIn account',
2277
+ '/x — Link/unlink your X (Twitter) account',
1566
2278
  '/context — Show all models, auth, and context info',
1567
2279
  '/clean — Clear conversation and start fresh',
1568
2280
  '/history — Show message count in memory',