kernelbot 1.0.21 → 1.0.22

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kernelbot",
3
- "version": "1.0.21",
3
+ "version": "1.0.22",
4
4
  "description": "KernelBot — AI engineering agent with full OS control",
5
5
  "type": "module",
6
6
  "author": "Abdullah Al-Taheri <abdullah@altaheri.me>",
package/src/agent.js CHANGED
@@ -13,10 +13,11 @@ export class Agent {
13
13
  this._pending = new Map(); // chatId -> pending state
14
14
  }
15
15
 
16
- async processMessage(chatId, userMessage, user, onUpdate) {
16
+ async processMessage(chatId, userMessage, user, onUpdate, sendPhoto) {
17
17
  const logger = getLogger();
18
18
 
19
19
  this._onUpdate = onUpdate || null;
20
+ this._sendPhoto = sendPhoto || null;
20
21
 
21
22
  // Handle pending responses (confirmation or credential)
22
23
  const pending = this._pending.get(chatId);
@@ -66,6 +67,11 @@ export class Agent {
66
67
  docker_compose: 'action',
67
68
  curl_url: 'url',
68
69
  check_port: 'port',
70
+ screenshot_website: 'url',
71
+ send_image: 'file_path',
72
+ browse_website: 'url',
73
+ extract_content: 'url',
74
+ interact_with_page: 'url',
69
75
  }[name];
70
76
  const val = key && input[key] ? String(input[key]).slice(0, 120) : JSON.stringify(input).slice(0, 120);
71
77
  return `${name}: ${val}`;
@@ -100,6 +106,7 @@ export class Agent {
100
106
  config: this.config,
101
107
  user,
102
108
  onUpdate: this._onUpdate,
109
+ sendPhoto: this._sendPhoto,
103
110
  });
104
111
 
105
112
  pending.toolResults.push({
@@ -117,7 +124,7 @@ export class Agent {
117
124
 
118
125
  if (lower === 'yes' || lower === 'y' || lower === 'confirm') {
119
126
  logger.info(`User confirmed dangerous tool: ${pending.block.name}`);
120
- const result = await executeTool(pending.block.name, pending.block.input, { ...pending.context, onUpdate: this._onUpdate });
127
+ const result = await executeTool(pending.block.name, pending.block.input, { ...pending.context, onUpdate: this._onUpdate, sendPhoto: this._sendPhoto });
121
128
 
122
129
  pending.toolResults.push({
123
130
  type: 'tool_result',
@@ -144,7 +151,7 @@ export class Agent {
144
151
  const pauseMsg = await this._checkPause(chatId, block, user, pending.toolResults, pending.remainingBlocks.filter((b) => b !== block), pending.messages);
145
152
  if (pauseMsg) return pauseMsg;
146
153
 
147
- const r = await executeTool(block.name, block.input, { config: this.config, user, onUpdate: this._onUpdate });
154
+ const r = await executeTool(block.name, block.input, { config: this.config, user, onUpdate: this._onUpdate, sendPhoto: this._sendPhoto });
148
155
  pending.toolResults.push({
149
156
  type: 'tool_result',
150
157
  tool_use_id: block.id,
@@ -256,6 +263,7 @@ export class Agent {
256
263
  config: this.config,
257
264
  user,
258
265
  onUpdate: this._onUpdate,
266
+ sendPhoto: this._sendPhoto,
259
267
  });
260
268
 
261
269
  toolResults.push({
package/src/bot.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import TelegramBot from 'node-telegram-bot-api';
2
+ import { createReadStream } from 'fs';
2
3
  import { isAllowedUser, getUnauthorizedMessage } from './security/auth.js';
3
4
  import { getLogger } from './utils/logger.js';
4
5
 
@@ -152,10 +153,27 @@ export function startBot(config, agent, conversationManager) {
152
153
  return lastMsgId;
153
154
  };
154
155
 
156
+ const sendPhoto = async (filePath, caption) => {
157
+ try {
158
+ await bot.sendPhoto(chatId, createReadStream(filePath), {
159
+ caption: caption || '',
160
+ parse_mode: 'Markdown',
161
+ });
162
+ } catch {
163
+ try {
164
+ await bot.sendPhoto(chatId, createReadStream(filePath), {
165
+ caption: caption || '',
166
+ });
167
+ } catch (err) {
168
+ logger.error(`Failed to send photo: ${err.message}`);
169
+ }
170
+ }
171
+ };
172
+
155
173
  const reply = await agent.processMessage(chatId, text, {
156
174
  id: userId,
157
175
  username,
158
- }, onUpdate);
176
+ }, onUpdate, sendPhoto);
159
177
 
160
178
  clearInterval(typingInterval);
161
179
 
package/src/coder.js CHANGED
@@ -84,7 +84,7 @@ function processEvent(line, onOutput, logger) {
84
84
  // Not JSON — send raw text if it looks meaningful
85
85
  if (line.trim() && line.length > 3 && onOutput) {
86
86
  logger.info(`Claude Code (raw): ${line.slice(0, 200)}`);
87
- onOutput(`📟 ${line.trim()}`).catch(() => {});
87
+ onOutput(`▹ ${line.trim()}`).catch(() => {});
88
88
  }
89
89
  return null;
90
90
  }
@@ -103,7 +103,7 @@ function processEvent(line, onOutput, logger) {
103
103
  const tool = extractToolUse(event);
104
104
  if (tool) {
105
105
  logger.info(`Claude Code tool: ${tool.name}: ${tool.summary}`);
106
- if (onOutput) onOutput(`🔨 ${tool.name}: ${tool.summary}`).catch(() => {});
106
+ if (onOutput) onOutput(`▸ ${tool.name}: ${tool.summary}`).catch(() => {});
107
107
  }
108
108
  return event;
109
109
  }
@@ -113,7 +113,7 @@ function processEvent(line, onOutput, logger) {
113
113
  const tool = extractToolUse(event);
114
114
  if (tool) {
115
115
  logger.info(`Claude Code tool: ${tool.name}: ${tool.summary}`);
116
- if (onOutput) onOutput(`🔨 ${tool.name}: ${tool.summary}`).catch(() => {});
116
+ if (onOutput) onOutput(`▸ ${tool.name}: ${tool.summary}`).catch(() => {});
117
117
  }
118
118
  return event;
119
119
  }
@@ -124,7 +124,7 @@ function processEvent(line, onOutput, logger) {
124
124
  const duration = event.duration_ms ? ` in ${(event.duration_ms / 1000).toFixed(1)}s` : '';
125
125
  const cost = event.cost_usd ? ` ($${event.cost_usd.toFixed(3)})` : '';
126
126
  logger.info(`Claude Code finished: ${status}${duration}${cost}`);
127
- if (onOutput) onOutput(`✅ Claude Code finished (${status}${duration}${cost})`).catch(() => {});
127
+ if (onOutput) onOutput(`▪ done (${status}${duration}${cost})`).catch(() => {});
128
128
  return event;
129
129
  }
130
130
 
@@ -173,12 +173,12 @@ export class ClaudeCodeSpawner {
173
173
  ? `\n_... ${activityLines.length} operations total_\n`
174
174
  : '';
175
175
  if (finalState === 'done') {
176
- return `✅ *Claude Code Done* — ${activityLines.length} ops\n${countInfo}\n${visible.join('\n')}`;
176
+ return `░▒▓ *Claude Code Done* — ${activityLines.length} ops\n${countInfo}\n${visible.join('\n')}`;
177
177
  }
178
178
  if (finalState === 'error') {
179
- return `❌ *Claude Code Failed* — ${activityLines.length} ops\n${countInfo}\n${visible.join('\n')}`;
179
+ return `░▒▓ *Claude Code Failed* — ${activityLines.length} ops\n${countInfo}\n${visible.join('\n')}`;
180
180
  }
181
- return `⚙️ *Claude Code Working...*\n${countInfo}\n${visible.join('\n')}`;
181
+ return `░▒▓ *Claude Code Working...*\n${countInfo}\n${visible.join('\n')}`;
182
182
  };
183
183
 
184
184
  const flushStatus = async () => {
@@ -206,17 +206,15 @@ export class ClaudeCodeSpawner {
206
206
 
207
207
  const smartOutput = onOutput ? async (text) => {
208
208
  // Tool calls, raw output, warnings, starting → accumulate in status message
209
- if (text.startsWith('🔨') || text.startsWith('📟') || text.startsWith('⚠️') || text.startsWith('⏳')) {
209
+ if (text.startsWith('') || text.startsWith('') || text.startsWith('')) {
210
210
  addActivity(text);
211
211
  return;
212
212
  }
213
- // Completion handled by close handler, skip
214
- if (text.startsWith('✅')) return;
215
- // Everything else (💬 text, ❌ error, ⏰ timeout) → new message
213
+ // Everything else (💬 text, errors, timeout) → new message
216
214
  await onOutput(text);
217
215
  } : null;
218
216
 
219
- if (smartOutput) smartOutput(`⏳ Starting Claude Code...`).catch(() => {});
217
+ if (smartOutput) smartOutput(`▸ Starting Claude Code...`).catch(() => {});
220
218
 
221
219
  return new Promise((resolve, reject) => {
222
220
  const child = spawn('claude', args, {
@@ -258,13 +256,13 @@ export class ClaudeCodeSpawner {
258
256
  stderr += chunk + '\n';
259
257
  logger.warn(`Claude Code stderr: ${chunk.slice(0, 300)}`);
260
258
  if (smartOutput && chunk) {
261
- smartOutput(`⚠️ ${chunk.slice(0, 300)}`).catch(() => {});
259
+ smartOutput(`▹ ${chunk.slice(0, 300)}`).catch(() => {});
262
260
  }
263
261
  });
264
262
 
265
263
  const timer = setTimeout(() => {
266
264
  child.kill('SIGTERM');
267
- if (smartOutput) smartOutput(`⏰ Claude Code timed out after ${this.timeout / 1000}s`).catch(() => {});
265
+ if (smartOutput) smartOutput(`▸ Claude Code timed out after ${this.timeout / 1000}s`).catch(() => {});
268
266
  reject(new Error(`Claude Code timed out after ${this.timeout / 1000}s`));
269
267
  }, this.timeout);
270
268
 
@@ -19,9 +19,10 @@ IMPORTANT: You MUST NOT write code yourself using read_file/write_file. ALWAYS d
19
19
 
20
20
  ## Web Browsing Tasks (researching, scraping, reading documentation, taking screenshots)
21
21
  - Use browse_website to read and summarize web pages
22
- - Use screenshot_website to capture visual snapshots of pages
22
+ - Use screenshot_website to capture visual snapshots of pages — the screenshot is automatically sent to the chat
23
23
  - Use extract_content to pull specific data from pages using CSS selectors
24
24
  - Use interact_with_page for pages that need clicking, typing, or scrolling to reveal content
25
+ - Use send_image to send any image file directly to the Telegram chat (screenshots, generated images, etc.)
25
26
  - When a user sends /browse <url>, use browse_website on that URL
26
27
  - When a user sends /screenshot <url>, use screenshot_website on that URL
27
28
  - When a user sends /extract <url> <selector>, use extract_content with that URL and selector
@@ -1,5 +1,5 @@
1
1
  import puppeteer from 'puppeteer';
2
- import { writeFile, mkdir } from 'fs/promises';
2
+ import { writeFile, mkdir, access } from 'fs/promises';
3
3
  import { join } from 'path';
4
4
  import { homedir } from 'os';
5
5
 
@@ -186,6 +186,25 @@ export const definitions = [
186
186
  required: ['url', 'selector'],
187
187
  },
188
188
  },
189
+ {
190
+ name: 'send_image',
191
+ description:
192
+ 'Send an image or screenshot file directly to the Telegram chat. Use this to share screenshots, generated images, or any image file with the user.',
193
+ input_schema: {
194
+ type: 'object',
195
+ properties: {
196
+ file_path: {
197
+ type: 'string',
198
+ description: 'Absolute path to the image file to send (e.g., "/home/user/.kernelbot/screenshots/example.png")',
199
+ },
200
+ caption: {
201
+ type: 'string',
202
+ description: 'Optional caption to include with the image',
203
+ },
204
+ },
205
+ required: ['file_path'],
206
+ },
207
+ },
189
208
  {
190
209
  name: 'interact_with_page',
191
210
  description:
@@ -350,7 +369,7 @@ async function handleBrowse(params) {
350
369
  });
351
370
  }
352
371
 
353
- async function handleScreenshot(params) {
372
+ async function handleScreenshot(params, context) {
354
373
  const validation = validateUrl(params.url);
355
374
  if (!validation.valid) return { error: validation.error };
356
375
 
@@ -397,12 +416,24 @@ async function handleScreenshot(params) {
397
416
  await page.screenshot(screenshotOptions);
398
417
  }
399
418
 
419
+ const title = await page.title();
420
+
421
+ // Send the screenshot directly to Telegram chat
422
+ if (context?.sendPhoto) {
423
+ try {
424
+ await context.sendPhoto(filepath, `📸 ${title || url}`);
425
+ } catch {
426
+ // Photo sending is best-effort; don't fail the tool
427
+ }
428
+ }
429
+
400
430
  return {
401
431
  success: true,
402
432
  url: page.url(),
403
- title: await page.title(),
433
+ title,
404
434
  screenshot_path: filepath,
405
435
  filename,
436
+ sent_to_chat: !!context?.sendPhoto,
406
437
  };
407
438
  });
408
439
  }
@@ -614,6 +645,30 @@ async function handleInteract(params) {
614
645
  });
615
646
  }
616
647
 
648
+ async function handleSendImage(params, context) {
649
+ if (!params.file_path) {
650
+ return { error: 'file_path is required' };
651
+ }
652
+
653
+ // Verify the file exists
654
+ try {
655
+ await access(params.file_path);
656
+ } catch {
657
+ return { error: `File not found: ${params.file_path}` };
658
+ }
659
+
660
+ if (!context?.sendPhoto) {
661
+ return { error: 'Image sending is not available in this context (no active Telegram chat)' };
662
+ }
663
+
664
+ try {
665
+ await context.sendPhoto(params.file_path, params.caption || '');
666
+ return { success: true, file_path: params.file_path, sent: true };
667
+ } catch (err) {
668
+ return { error: `Failed to send image: ${err.message}` };
669
+ }
670
+ }
671
+
617
672
  // ── Export ────────────────────────────────────────────────────────────────────
618
673
 
619
674
  export const handlers = {
@@ -621,4 +676,5 @@ export const handlers = {
621
676
  screenshot_website: handleScreenshot,
622
677
  extract_content: handleExtract,
623
678
  interact_with_page: handleInteract,
679
+ send_image: handleSendImage,
624
680
  };