kernelbot 1.0.32 → 1.0.34

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 CHANGED
@@ -9,7 +9,7 @@ import { readFileSync, existsSync } from 'fs';
9
9
  import { join } from 'path';
10
10
  import { homedir } from 'os';
11
11
  import chalk from 'chalk';
12
- import { loadConfig, loadConfigInteractive, changeBrainModel } from '../src/utils/config.js';
12
+ import { loadConfig, loadConfigInteractive, changeBrainModel, changeOrchestratorModel } from '../src/utils/config.js';
13
13
  import { createLogger, getLogger } from '../src/utils/logger.js';
14
14
  import {
15
15
  showLogo,
@@ -40,11 +40,16 @@ import {
40
40
  } from '../src/skills/custom.js';
41
41
 
42
42
  function showMenu(config) {
43
+ const orchProviderDef = PROVIDERS[config.orchestrator.provider];
44
+ const orchProviderName = orchProviderDef ? orchProviderDef.name : config.orchestrator.provider;
45
+ const orchModelId = config.orchestrator.model;
46
+
43
47
  const providerDef = PROVIDERS[config.brain.provider];
44
48
  const providerName = providerDef ? providerDef.name : config.brain.provider;
45
49
  const modelId = config.brain.model;
46
50
 
47
51
  console.log('');
52
+ console.log(chalk.dim(` Current orchestrator: ${orchProviderName} / ${orchModelId}`));
48
53
  console.log(chalk.dim(` Current brain: ${providerName} / ${modelId}`));
49
54
  console.log('');
50
55
  console.log(chalk.bold(' What would you like to do?\n'));
@@ -53,9 +58,10 @@ function showMenu(config) {
53
58
  console.log(` ${chalk.cyan('3.')} View logs`);
54
59
  console.log(` ${chalk.cyan('4.')} View audit logs`);
55
60
  console.log(` ${chalk.cyan('5.')} Change brain model`);
56
- console.log(` ${chalk.cyan('6.')} Manage custom skills`);
57
- console.log(` ${chalk.cyan('7.')} Manage automations`);
58
- console.log(` ${chalk.cyan('8.')} Exit`);
61
+ console.log(` ${chalk.cyan('6.')} Change orchestrator model`);
62
+ console.log(` ${chalk.cyan('7.')} Manage custom skills`);
63
+ console.log(` ${chalk.cyan('8.')} Manage automations`);
64
+ console.log(` ${chalk.cyan('9.')} Exit`);
59
65
  console.log('');
60
66
  }
61
67
 
@@ -95,23 +101,53 @@ function viewLog(filename) {
95
101
  }
96
102
 
97
103
  async function runCheck(config) {
104
+ // Orchestrator check
105
+ const orchProviderKey = config.orchestrator.provider || 'anthropic';
106
+ const orchProviderDef = PROVIDERS[orchProviderKey];
107
+ const orchLabel = orchProviderDef ? orchProviderDef.name : orchProviderKey;
108
+ const orchEnvKey = orchProviderDef ? orchProviderDef.envKey : 'ANTHROPIC_API_KEY';
109
+
110
+ await showStartupCheck(`Orchestrator ${orchEnvKey}`, async () => {
111
+ const orchestratorKey = config.orchestrator.api_key
112
+ || (orchProviderDef && process.env[orchProviderDef.envKey])
113
+ || process.env.ANTHROPIC_API_KEY;
114
+ if (!orchestratorKey) throw new Error('Not set');
115
+ });
116
+
117
+ await showStartupCheck(`Orchestrator (${orchLabel}) API connection`, async () => {
118
+ const orchestratorKey = config.orchestrator.api_key
119
+ || (orchProviderDef && process.env[orchProviderDef.envKey])
120
+ || process.env.ANTHROPIC_API_KEY;
121
+ const provider = createProvider({
122
+ brain: {
123
+ provider: orchProviderKey,
124
+ model: config.orchestrator.model,
125
+ max_tokens: config.orchestrator.max_tokens,
126
+ temperature: config.orchestrator.temperature,
127
+ api_key: orchestratorKey,
128
+ },
129
+ });
130
+ await provider.ping();
131
+ });
132
+
133
+ // Worker brain check
98
134
  const providerDef = PROVIDERS[config.brain.provider];
99
135
  const providerLabel = providerDef ? providerDef.name : config.brain.provider;
100
136
  const envKeyLabel = providerDef ? providerDef.envKey : 'API_KEY';
101
137
 
102
- await showStartupCheck(envKeyLabel, async () => {
138
+ await showStartupCheck(`Worker ${envKeyLabel}`, async () => {
103
139
  if (!config.brain.api_key) throw new Error('Not set');
104
140
  });
105
141
 
106
- await showStartupCheck('TELEGRAM_BOT_TOKEN', async () => {
107
- if (!config.telegram.bot_token) throw new Error('Not set');
108
- });
109
-
110
- await showStartupCheck(`${providerLabel} API connection`, async () => {
142
+ await showStartupCheck(`Worker (${providerLabel}) API connection`, async () => {
111
143
  const provider = createProvider(config);
112
144
  await provider.ping();
113
145
  });
114
146
 
147
+ await showStartupCheck('TELEGRAM_BOT_TOKEN', async () => {
148
+ if (!config.telegram.bot_token) throw new Error('Not set');
149
+ });
150
+
115
151
  await showStartupCheck('Telegram Bot API', async () => {
116
152
  const res = await fetch(
117
153
  `https://api.telegram.org/bot${config.telegram.bot_token}/getMe`,
@@ -429,12 +465,15 @@ async function main() {
429
465
  await changeBrainModel(config, rl);
430
466
  break;
431
467
  case '6':
432
- await manageCustomSkills(rl);
468
+ await changeOrchestratorModel(config, rl);
433
469
  break;
434
470
  case '7':
435
- await manageAutomations(rl);
471
+ await manageCustomSkills(rl);
436
472
  break;
437
473
  case '8':
474
+ await manageAutomations(rl);
475
+ break;
476
+ case '9':
438
477
  running = false;
439
478
  break;
440
479
  default:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kernelbot",
3
- "version": "1.0.32",
3
+ "version": "1.0.34",
4
4
  "description": "KernelBot — AI engineering agent with full OS control",
5
5
  "type": "module",
6
6
  "author": "Abdullah Al-Taheri <abdullah@altaheri.me>",
@@ -31,6 +31,7 @@
31
31
  "license": "MIT",
32
32
  "dependencies": {
33
33
  "@anthropic-ai/sdk": "^0.39.0",
34
+ "@google/genai": "^1.42.0",
34
35
  "@octokit/rest": "^22.0.1",
35
36
  "axios": "^1.13.5",
36
37
  "boxen": "^8.0.1",
package/src/agent.js CHANGED
@@ -9,9 +9,7 @@ import { WorkerAgent } from './worker.js';
9
9
  import { getLogger } from './utils/logger.js';
10
10
  import { getMissingCredential, saveCredential, saveProviderToYaml, saveOrchestratorToYaml, saveClaudeCodeModelToYaml, saveClaudeCodeAuth } from './utils/config.js';
11
11
  import { resetClaudeCodeSpawner, getSpawner } from './tools/coding.js';
12
-
13
- const MAX_RESULT_LENGTH = 3000;
14
- const LARGE_FIELDS = ['stdout', 'stderr', 'content', 'diff', 'output', 'body', 'html', 'text', 'log', 'logs'];
12
+ import { truncateToolResult } from './utils/truncate.js';
15
13
 
16
14
  export class OrchestratorAgent {
17
15
  constructor({ config, conversationManager, personaManager, selfManager, jobManager, automationManager, memoryManager, shareQueue }) {
@@ -49,7 +47,7 @@ export class OrchestratorAgent {
49
47
  }
50
48
 
51
49
  /** Build the orchestrator system prompt. */
52
- _getSystemPrompt(chatId, user) {
50
+ _getSystemPrompt(chatId, user, temporalContext = null) {
53
51
  const logger = getLogger();
54
52
  const skillId = this.conversationManager.getSkill(chatId);
55
53
  const skillPrompt = skillId ? getUnifiedSkillById(skillId)?.systemPrompt : null;
@@ -76,8 +74,8 @@ export class OrchestratorAgent {
76
74
  sharesBlock = this.shareQueue.buildShareBlock(user?.id || null);
77
75
  }
78
76
 
79
- logger.debug(`Orchestrator building system prompt for chat ${chatId} | skill=${skillId || 'none'} | persona=${userPersona ? 'yes' : 'none'} | self=${selfData ? 'yes' : 'none'} | memories=${memoriesBlock ? 'yes' : 'none'} | shares=${sharesBlock ? 'yes' : 'none'}`);
80
- return getOrchestratorPrompt(this.config, skillPrompt || null, userPersona, selfData, memoriesBlock, sharesBlock);
77
+ logger.debug(`Orchestrator building system prompt for chat ${chatId} | skill=${skillId || 'none'} | persona=${userPersona ? 'yes' : 'none'} | self=${selfData ? 'yes' : 'none'} | memories=${memoriesBlock ? 'yes' : 'none'} | shares=${sharesBlock ? 'yes' : 'none'} | temporal=${temporalContext ? 'yes' : 'none'}`);
78
+ return getOrchestratorPrompt(this.config, skillPrompt || null, userPersona, selfData, memoriesBlock, sharesBlock, temporalContext);
81
79
  }
82
80
 
83
81
  setSkill(chatId, skillId) {
@@ -282,32 +280,18 @@ export class OrchestratorAgent {
282
280
  }
283
281
  }
284
282
 
285
- /** Truncate a tool result. */
283
+ /** Truncate a tool result. Delegates to shared utility. */
286
284
  _truncateResult(name, result) {
287
- let str = JSON.stringify(result);
288
- if (str.length <= MAX_RESULT_LENGTH) return str;
289
-
290
- if (result && typeof result === 'object') {
291
- const truncated = { ...result };
292
- for (const field of LARGE_FIELDS) {
293
- if (typeof truncated[field] === 'string' && truncated[field].length > 500) {
294
- truncated[field] = truncated[field].slice(0, 500) + `\n... [truncated ${truncated[field].length - 500} chars]`;
295
- }
296
- }
297
- str = JSON.stringify(truncated);
298
- if (str.length <= MAX_RESULT_LENGTH) return str;
299
- }
300
-
301
- return str.slice(0, MAX_RESULT_LENGTH) + `\n... [truncated, total ${str.length} chars]`;
285
+ return truncateToolResult(name, result);
302
286
  }
303
287
 
304
- async processMessage(chatId, userMessage, user, onUpdate, sendPhoto) {
288
+ async processMessage(chatId, userMessage, user, onUpdate, sendPhoto, opts = {}) {
305
289
  const logger = getLogger();
306
290
 
307
291
  logger.info(`Orchestrator processing message for chat ${chatId} from ${user?.username || user?.id || 'unknown'}: "${userMessage.slice(0, 120)}"`);
308
292
 
309
293
  // Store callbacks so workers can use them later
310
- this._chatCallbacks.set(chatId, { onUpdate, sendPhoto });
294
+ this._chatCallbacks.set(chatId, { onUpdate, sendPhoto, sendReaction: opts.sendReaction, lastUserMessageId: opts.messageId });
311
295
 
312
296
  // Handle pending responses (confirmation or credential)
313
297
  const pending = this._pending.get(chatId);
@@ -322,6 +306,22 @@ export class OrchestratorAgent {
322
306
 
323
307
  const { max_tool_depth } = this.config.orchestrator;
324
308
 
309
+ // Detect time gap before adding the new message
310
+ let temporalContext = null;
311
+ const lastTs = this.conversationManager.getLastMessageTimestamp(chatId);
312
+ if (lastTs) {
313
+ const gapMs = Date.now() - lastTs;
314
+ const gapMinutes = Math.floor(gapMs / 60_000);
315
+ if (gapMinutes >= 30) {
316
+ const gapHours = Math.floor(gapMinutes / 60);
317
+ const gapText = gapHours >= 1
318
+ ? `${gapHours} hour(s)`
319
+ : `${gapMinutes} minute(s)`;
320
+ temporalContext = `[Time gap detected: ${gapText} since last message. User may be starting a new topic.]`;
321
+ logger.info(`Time gap detected for chat ${chatId}: ${gapText}`);
322
+ }
323
+ }
324
+
325
325
  // Add user message to persistent history
326
326
  this.conversationManager.addMessage(chatId, 'user', userMessage);
327
327
 
@@ -329,7 +329,7 @@ export class OrchestratorAgent {
329
329
  const messages = [...this.conversationManager.getSummarizedHistory(chatId)];
330
330
  logger.debug(`Orchestrator conversation context: ${messages.length} messages, max_depth=${max_tool_depth}`);
331
331
 
332
- const reply = await this._runLoop(chatId, messages, user, 0, max_tool_depth);
332
+ const reply = await this._runLoop(chatId, messages, user, 0, max_tool_depth, temporalContext);
333
333
 
334
334
  logger.info(`Orchestrator reply for chat ${chatId}: "${(reply || '').slice(0, 150)}"`);
335
335
 
@@ -738,6 +738,7 @@ export class OrchestratorAgent {
738
738
  callbacks: {
739
739
  onProgress: (text) => addActivity(text),
740
740
  onHeartbeat: (text) => job.addProgress(text),
741
+ onStats: (stats) => job.updateStats(stats),
741
742
  onUpdate, // Real bot onUpdate for tools (coder.js smart output needs message_id)
742
743
  onComplete: (result, parsedResult) => {
743
744
  logger.info(`[Worker ${job.id}] Completed — structured=${!!parsedResult?.structured}, result: "${(result || '').slice(0, 150)}"`);
@@ -840,8 +841,16 @@ export class OrchestratorAgent {
840
841
  for (const job of running) {
841
842
  const workerDef = WORKER_TYPES[job.workerType] || {};
842
843
  const dur = job.startedAt ? Math.round((now - job.startedAt) / 1000) : 0;
843
- const recentActivity = job.progress.slice(-8).join(' → ');
844
- lines.push(`- ${workerDef.label || job.workerType} (${job.id}) running ${dur}s${recentActivity ? `\n Recent: ${recentActivity}` : ''}`);
844
+ const stats = `${job.llmCalls} LLM calls, ${job.toolCalls} tools`;
845
+ const recentActivity = job.progress.slice(-5).join(' ');
846
+ let line = `- ${workerDef.label || job.workerType} (${job.id}) — running ${dur}s [${stats}]`;
847
+ if (job.lastThinking) {
848
+ line += `\n Thinking: "${job.lastThinking.slice(0, 150)}"`;
849
+ }
850
+ if (recentActivity) {
851
+ line += `\n Recent: ${recentActivity}`;
852
+ }
853
+ lines.push(line);
845
854
  }
846
855
 
847
856
  // Queued/waiting jobs
@@ -881,20 +890,28 @@ export class OrchestratorAgent {
881
890
  return `[Active Workers]\n${lines.join('\n')}`;
882
891
  }
883
892
 
884
- async _runLoop(chatId, messages, user, startDepth, maxDepth) {
893
+ async _runLoop(chatId, messages, user, startDepth, maxDepth, temporalContext = null) {
885
894
  const logger = getLogger();
886
895
 
887
896
  for (let depth = startDepth; depth < maxDepth; depth++) {
888
897
  logger.info(`[Orchestrator] LLM call ${depth + 1}/${maxDepth} for chat ${chatId} — sending ${messages.length} messages`);
889
898
 
890
- // Inject worker activity digest (transient — not stored in conversation history)
899
+ // Inject transient context messages (not stored in conversation history)
900
+ let workingMessages = [...messages];
901
+
902
+ // On first iteration, inject temporal context if present
903
+ if (depth === 0 && temporalContext) {
904
+ workingMessages = [{ role: 'user', content: `[Temporal Context]\n${temporalContext}` }, ...workingMessages];
905
+ }
906
+
907
+ // Inject worker activity digest
891
908
  const digest = this._buildWorkerDigest(chatId);
892
- const workingMessages = digest
893
- ? [{ role: 'user', content: `[Worker Status]\n${digest}` }, ...messages]
894
- : messages;
909
+ if (digest) {
910
+ workingMessages = [{ role: 'user', content: `[Worker Status]\n${digest}` }, ...workingMessages];
911
+ }
895
912
 
896
913
  const response = await this.orchestratorProvider.chat({
897
- system: this._getSystemPrompt(chatId, user),
914
+ system: this._getSystemPrompt(chatId, user, temporalContext),
898
915
  messages: workingMessages,
899
916
  tools: orchestratorToolDefinitions,
900
917
  });
@@ -923,6 +940,7 @@ export class OrchestratorAgent {
923
940
  logger.debug(`[Orchestrator] Tool input: ${JSON.stringify(block.input).slice(0, 300)}`);
924
941
  await this._sendUpdate(chatId, `⚡ ${summary}`);
925
942
 
943
+ const chatCallbacks = this._chatCallbacks.get(chatId) || {};
926
944
  const result = await executeOrchestratorTool(block.name, block.input, {
927
945
  chatId,
928
946
  jobManager: this.jobManager,
@@ -930,6 +948,8 @@ export class OrchestratorAgent {
930
948
  spawnWorker: (job) => this._spawnWorker(job),
931
949
  automationManager: this.automationManager,
932
950
  user,
951
+ sendReaction: chatCallbacks.sendReaction || null,
952
+ lastUserMessageId: chatCallbacks.lastUserMessageId || null,
933
953
  });
934
954
 
935
955
  logger.info(`[Orchestrator] Tool result for ${block.name}: ${JSON.stringify(result).slice(0, 200)}`);
@@ -978,11 +998,158 @@ export class OrchestratorAgent {
978
998
  return `Updating automation ${input.automation_id}`;
979
999
  case 'delete_automation':
980
1000
  return `Deleting automation ${input.automation_id}`;
1001
+ case 'send_reaction':
1002
+ return `Reacting with ${input.emoji}`;
981
1003
  default:
982
1004
  return name;
983
1005
  }
984
1006
  }
985
1007
 
1008
+ /**
1009
+ * Resume active chats after a restart.
1010
+ * Checks recent conversations for pending items and sends follow-up messages.
1011
+ * Called once from bot.js after startup.
1012
+ */
1013
+ async resumeActiveChats(sendMessageFn) {
1014
+ const logger = getLogger();
1015
+ const now = Date.now();
1016
+ const MAX_AGE_MS = 24 * 60 * 60_000; // 24 hours
1017
+
1018
+ logger.info('[Orchestrator] Checking for active chats to resume...');
1019
+
1020
+ let resumeCount = 0;
1021
+
1022
+ for (const [chatId, messages] of this.conversationManager.conversations) {
1023
+ // Skip internal life engine chat
1024
+ if (chatId === '__life__') continue;
1025
+
1026
+ try {
1027
+ // Find the last message with a timestamp
1028
+ const lastMsg = [...messages].reverse().find(m => m.timestamp);
1029
+ if (!lastMsg || !lastMsg.timestamp) continue;
1030
+
1031
+ const ageMs = now - lastMsg.timestamp;
1032
+ if (ageMs > MAX_AGE_MS) continue;
1033
+
1034
+ // Calculate time gap for context
1035
+ const gapMinutes = Math.floor(ageMs / 60_000);
1036
+ const gapText = gapMinutes >= 60
1037
+ ? `${Math.floor(gapMinutes / 60)} hour(s)`
1038
+ : `${gapMinutes} minute(s)`;
1039
+
1040
+ // Build summarized history
1041
+ const history = this.conversationManager.getSummarizedHistory(chatId);
1042
+ if (history.length === 0) continue;
1043
+
1044
+ // Build resume prompt
1045
+ const resumePrompt = `[System Restart] You just came back online after being offline for ${gapText}. Review the conversation above.\nIf there's something pending (unfinished task, follow-up, something to share), send a short natural message. If nothing's pending, respond with exactly: NONE`;
1046
+
1047
+ // Use minimal user object (private TG chats: chatId == userId)
1048
+ const user = { id: chatId };
1049
+
1050
+ const response = await this.orchestratorProvider.chat({
1051
+ system: this._getSystemPrompt(chatId, user),
1052
+ messages: [
1053
+ ...history,
1054
+ { role: 'user', content: resumePrompt },
1055
+ ],
1056
+ });
1057
+
1058
+ const reply = (response.text || '').trim();
1059
+
1060
+ if (reply && reply !== 'NONE') {
1061
+ await sendMessageFn(chatId, reply);
1062
+ this.conversationManager.addMessage(chatId, 'assistant', reply);
1063
+ resumeCount++;
1064
+ logger.info(`[Orchestrator] Resume message sent to chat ${chatId}`);
1065
+ } else {
1066
+ logger.debug(`[Orchestrator] No resume needed for chat ${chatId}`);
1067
+ }
1068
+
1069
+ // Small delay between chats to avoid rate limiting
1070
+ await new Promise(r => setTimeout(r, 1000));
1071
+ } catch (err) {
1072
+ logger.error(`[Orchestrator] Resume failed for chat ${chatId}: ${err.message}`);
1073
+ }
1074
+ }
1075
+
1076
+ logger.info(`[Orchestrator] Resume check complete — ${resumeCount} message(s) sent`);
1077
+ }
1078
+
1079
+ /**
1080
+ * Deliver pending shares from the life engine to active chats proactively.
1081
+ * Called periodically from bot.js.
1082
+ */
1083
+ async deliverPendingShares(sendMessageFn) {
1084
+ const logger = getLogger();
1085
+
1086
+ if (!this.shareQueue) return;
1087
+
1088
+ const pending = this.shareQueue.getPending(null, 5);
1089
+ if (pending.length === 0) return;
1090
+
1091
+ const now = Date.now();
1092
+ const MAX_AGE_MS = 24 * 60 * 60_000;
1093
+
1094
+ // Find active chats (last message within 24h)
1095
+ const activeChats = [];
1096
+ for (const [chatId, messages] of this.conversationManager.conversations) {
1097
+ if (chatId === '__life__') continue;
1098
+ const lastMsg = [...messages].reverse().find(m => m.timestamp);
1099
+ if (lastMsg && lastMsg.timestamp && (now - lastMsg.timestamp) < MAX_AGE_MS) {
1100
+ activeChats.push(chatId);
1101
+ }
1102
+ }
1103
+
1104
+ if (activeChats.length === 0) {
1105
+ logger.debug('[Orchestrator] No active chats for share delivery');
1106
+ return;
1107
+ }
1108
+
1109
+ logger.info(`[Orchestrator] Delivering ${pending.length} pending share(s) to ${activeChats.length} active chat(s)`);
1110
+
1111
+ // Cap at 3 chats per cycle to avoid spam
1112
+ const targetChats = activeChats.slice(0, 3);
1113
+
1114
+ for (const chatId of targetChats) {
1115
+ try {
1116
+ const history = this.conversationManager.getSummarizedHistory(chatId);
1117
+ const user = { id: chatId };
1118
+
1119
+ // Build shares into a prompt
1120
+ const sharesText = pending.map((s, i) => `${i + 1}. [${s.source}] ${s.content}`).join('\n');
1121
+
1122
+ const sharePrompt = `[Proactive Share] You have some discoveries and thoughts you'd like to share naturally. Here they are:\n\n${sharesText}\n\nWeave one or more of these into a short, natural message. Don't be forced — pick what feels relevant to this user and conversation. If none feel right for this chat, respond with exactly: NONE`;
1123
+
1124
+ const response = await this.orchestratorProvider.chat({
1125
+ system: this._getSystemPrompt(chatId, user),
1126
+ messages: [
1127
+ ...history,
1128
+ { role: 'user', content: sharePrompt },
1129
+ ],
1130
+ });
1131
+
1132
+ const reply = (response.text || '').trim();
1133
+
1134
+ if (reply && reply !== 'NONE') {
1135
+ await sendMessageFn(chatId, reply);
1136
+ this.conversationManager.addMessage(chatId, 'assistant', reply);
1137
+ logger.info(`[Orchestrator] Proactive share delivered to chat ${chatId}`);
1138
+
1139
+ // Mark shares as delivered for this user
1140
+ for (const item of pending) {
1141
+ this.shareQueue.markShared(item.id, chatId);
1142
+ }
1143
+ }
1144
+
1145
+ // Delay between chats
1146
+ await new Promise(r => setTimeout(r, 2000));
1147
+ } catch (err) {
1148
+ logger.error(`[Orchestrator] Share delivery failed for chat ${chatId}: ${err.message}`);
1149
+ }
1150
+ }
1151
+ }
1152
+
986
1153
  /** Background persona extraction. */
987
1154
  async _extractPersonaBackground(userMessage, reply, user) {
988
1155
  const logger = getLogger();