kernelbot 1.0.36 → 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/bin/kernel.js +389 -23
- package/config.example.yaml +17 -0
- package/package.json +2 -1
- package/src/agent.js +355 -82
- package/src/bot.js +724 -12
- package/src/character.js +406 -0
- package/src/characters/builder.js +174 -0
- package/src/characters/builtins.js +421 -0
- package/src/conversation.js +17 -2
- package/src/dashboard/agents.css +469 -0
- package/src/dashboard/agents.html +184 -0
- package/src/dashboard/agents.js +873 -0
- package/src/dashboard/dashboard.css +281 -0
- package/src/dashboard/dashboard.js +579 -0
- package/src/dashboard/index.html +366 -0
- package/src/dashboard/server.js +521 -0
- package/src/dashboard/shared.css +700 -0
- package/src/dashboard/shared.js +209 -0
- package/src/life/engine.js +28 -20
- package/src/life/evolution.js +7 -5
- package/src/life/journal.js +5 -4
- package/src/life/memory.js +12 -9
- package/src/life/share-queue.js +7 -5
- package/src/prompts/orchestrator.js +76 -14
- package/src/prompts/workers.js +22 -0
- package/src/security/auth.js +42 -1
- package/src/self.js +17 -5
- package/src/services/linkedin-api.js +190 -0
- package/src/services/stt.js +8 -2
- package/src/services/tts.js +32 -2
- package/src/services/x-api.js +141 -0
- package/src/swarm/worker-registry.js +7 -0
- package/src/tools/categories.js +4 -0
- package/src/tools/index.js +6 -0
- package/src/tools/linkedin.js +264 -0
- package/src/tools/orchestrator-tools.js +337 -2
- package/src/tools/x.js +256 -0
- package/src/utils/config.js +104 -57
- package/src/utils/display.js +73 -12
- package/src/utils/temporal-awareness.js +24 -10
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
|
-
|
|
124
|
-
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
1170
|
-
const history =
|
|
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',
|