kernelbot 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.env.example ADDED
@@ -0,0 +1,2 @@
1
+ ANTHROPIC_API_KEY=sk-ant-...
2
+ TELEGRAM_BOT_TOKEN=123456:ABC-DEF...
package/README.md ADDED
@@ -0,0 +1,138 @@
1
+ # KernelBot
2
+
3
+ AI engineering agent — a Telegram bot backed by Claude Sonnet with full OS control via tool use.
4
+
5
+ Send a message in Telegram, and KernelBot will read files, write code, run commands, and respond with the results. It's your personal engineering assistant with direct access to your machine.
6
+
7
+ ## How It Works
8
+
9
+ ```
10
+ You (Telegram) → KernelBot → Claude Sonnet (Anthropic API)
11
+
12
+ OS Tools (shell, files, directories)
13
+ ```
14
+
15
+ KernelBot runs a **tool-use loop**: Claude decides which tools to call, KernelBot executes them on your OS, feeds results back, and Claude continues until the task is done. One message can trigger dozens of tool calls autonomously.
16
+
17
+ ## Tools
18
+
19
+ | Tool | Description |
20
+ | ----------------- | --------------------------------------------------- |
21
+ | `execute_command` | Run any shell command (git, npm, python, etc.) |
22
+ | `read_file` | Read file contents with optional line limits |
23
+ | `write_file` | Write/create files, auto-creates parent directories |
24
+ | `list_directory` | List directory contents, optionally recursive |
25
+
26
+ ## Installation
27
+
28
+ ```bash
29
+ npm install -g kernelbot
30
+ ```
31
+
32
+ This installs the `kernelbot` command globally.
33
+
34
+ ## Quick Start
35
+
36
+ ```bash
37
+ # Interactive setup — creates .env and config.yaml
38
+ kernelbot init
39
+
40
+ # Verify everything works
41
+ kernelbot check
42
+
43
+ # Launch the bot
44
+ kernelbot start
45
+ ```
46
+
47
+ ## Commands
48
+
49
+ | Command | Description |
50
+ | ------------------------- | --------------------------------------------------------- |
51
+ | `kernelbot start` | Start the Telegram bot |
52
+ | `kernelbot run "prompt"` | One-off agent call without Telegram (for testing/scripts) |
53
+ | `kernelbot check` | Validate config and test API connections |
54
+ | `kernelbot init` | Interactive setup wizard |
55
+
56
+ ## Configuration
57
+
58
+ KernelBot looks for `config.yaml` in the current directory or `~/.kernelbot/`. Secrets are loaded from `.env`.
59
+
60
+ ### `.env`
61
+
62
+ ```
63
+ ANTHROPIC_API_KEY=sk-ant-...
64
+ TELEGRAM_BOT_TOKEN=123456:ABC-DEF...
65
+ ```
66
+
67
+ ### `config.yaml`
68
+
69
+ ```yaml
70
+ bot:
71
+ name: KernelBot
72
+
73
+ anthropic:
74
+ model: claude-sonnet-4-20250514
75
+ max_tokens: 8192
76
+ temperature: 0.3
77
+ max_tool_depth: 25 # max tool calls per message
78
+
79
+ telegram:
80
+ allowed_users: [] # empty = allow all (dev mode)
81
+ # allowed_users: [123456789] # lock to specific Telegram user IDs
82
+
83
+ security:
84
+ blocked_paths: # paths the agent cannot touch
85
+ - /etc/shadow
86
+ - /etc/passwd
87
+
88
+ logging:
89
+ level: info
90
+ max_file_size: 5242880 # 5 MB
91
+
92
+ conversation:
93
+ max_history: 50 # messages per chat
94
+ ```
95
+
96
+ ## Security
97
+
98
+ - **User allowlist** — restrict bot access to specific Telegram user IDs. Empty list = dev mode (anyone can use it).
99
+ - **Blocked paths** — files/directories the agent is forbidden from reading or writing (e.g., `/etc/shadow`, SSH keys).
100
+ - **Audit logging** — every tool call is logged to `kernel-audit.log` with user, tool, params, result, and duration. Secrets in params are automatically redacted.
101
+ - **Command timeout** — shell commands are killed after 30 seconds by default.
102
+
103
+ ## Project Structure
104
+
105
+ ```
106
+ KernelBot/
107
+ ├── bin/
108
+ │ └── kernel.js # CLI entry point
109
+ ├── src/
110
+ │ ├── agent.js # Sonnet tool-use loop
111
+ │ ├── bot.js # Telegram bot (polling, auth, message handling)
112
+ │ ├── conversation.js # Per-chat conversation history
113
+ │ ├── prompts/
114
+ │ │ └── system.js # System prompt
115
+ │ ├── security/
116
+ │ │ ├── auth.js # User allowlist
117
+ │ │ └── audit.js # Tool call audit logging
118
+ │ ├── tools/
119
+ │ │ ├── os.js # OS tool definitions + handlers
120
+ │ │ └── index.js # Tool registry + dispatcher
121
+ │ └── utils/
122
+ │ ├── config.js # Config loading (yaml + env + defaults)
123
+ │ ├── display.js # CLI display (logo, spinners, banners)
124
+ │ └── logger.js # Winston logger
125
+ ├── config.example.yaml
126
+ ├── .env.example
127
+ └── package.json
128
+ ```
129
+
130
+ ## Requirements
131
+
132
+ - Node.js 18+
133
+ - [Anthropic API key](https://console.anthropic.com/)
134
+ - [Telegram Bot Token](https://t.me/BotFather)
135
+
136
+ ## Author
137
+
138
+ Abdullah Al-Tahrei
package/bin/kernel.js ADDED
@@ -0,0 +1,197 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Suppress punycode deprecation warning from transitive deps
4
+ process.removeAllListeners('warning');
5
+ process.on('warning', (w) => { if (w.name !== 'DeprecationWarning' || !w.message.includes('punycode')) console.warn(w); });
6
+
7
+ import { Command } from 'commander';
8
+ import { createInterface } from 'readline';
9
+ import { writeFileSync, existsSync } from 'fs';
10
+ import { loadConfig } from '../src/utils/config.js';
11
+ import { createLogger, getLogger } from '../src/utils/logger.js';
12
+ import {
13
+ showLogo,
14
+ showStartupCheck,
15
+ showStartupComplete,
16
+ showError,
17
+ } from '../src/utils/display.js';
18
+ import { createAuditLogger } from '../src/security/audit.js';
19
+ import { ConversationManager } from '../src/conversation.js';
20
+ import { Agent } from '../src/agent.js';
21
+ import { startBot } from '../src/bot.js';
22
+ import Anthropic from '@anthropic-ai/sdk';
23
+
24
+ const program = new Command();
25
+
26
+ program
27
+ .name('kernelbot')
28
+ .description('KernelBot — AI engineering agent with full OS control')
29
+ .version('1.0.0');
30
+
31
+ // ─── kernel start ────────────────────────────────────────────
32
+ program
33
+ .command('start')
34
+ .description('Start KernelBot Telegram bot')
35
+ .action(async () => {
36
+ showLogo();
37
+
38
+ const config = loadConfig();
39
+ createLogger(config);
40
+ createAuditLogger();
41
+ const logger = getLogger();
42
+
43
+ // Startup checks
44
+ const checks = [];
45
+
46
+ checks.push(
47
+ await showStartupCheck('Configuration loaded', async () => {
48
+ if (!config.anthropic.api_key) throw new Error('ANTHROPIC_API_KEY not set');
49
+ if (!config.telegram.bot_token) throw new Error('TELEGRAM_BOT_TOKEN not set');
50
+ }),
51
+ );
52
+
53
+ checks.push(
54
+ await showStartupCheck('Anthropic API connection', async () => {
55
+ const client = new Anthropic({ apiKey: config.anthropic.api_key });
56
+ await client.messages.create({
57
+ model: config.anthropic.model,
58
+ max_tokens: 16,
59
+ messages: [{ role: 'user', content: 'ping' }],
60
+ });
61
+ }),
62
+ );
63
+
64
+ if (checks.some((c) => !c)) {
65
+ showError('Startup checks failed. Fix the issues above and try again.');
66
+ process.exit(1);
67
+ }
68
+
69
+ const conversationManager = new ConversationManager(config);
70
+ const agent = new Agent({ config, conversationManager });
71
+
72
+ startBot(config, agent);
73
+ showStartupComplete();
74
+ });
75
+
76
+ // ─── kernel run ──────────────────────────────────────────────
77
+ program
78
+ .command('run')
79
+ .description('Run a one-off prompt through the agent (no Telegram)')
80
+ .argument('<prompt>', 'The prompt to send')
81
+ .action(async (prompt) => {
82
+ const config = loadConfig();
83
+ createLogger(config);
84
+ createAuditLogger();
85
+
86
+ if (!config.anthropic.api_key) {
87
+ showError('ANTHROPIC_API_KEY not set. Run `kernelbot init` first.');
88
+ process.exit(1);
89
+ }
90
+
91
+ const conversationManager = new ConversationManager(config);
92
+ const agent = new Agent({ config, conversationManager });
93
+
94
+ const reply = await agent.processMessage('cli', prompt, {
95
+ id: 'cli',
96
+ username: 'cli',
97
+ });
98
+
99
+ console.log('\n' + reply);
100
+ });
101
+
102
+ // ─── kernel check ────────────────────────────────────────────
103
+ program
104
+ .command('check')
105
+ .description('Validate configuration and test API connections')
106
+ .action(async () => {
107
+ showLogo();
108
+
109
+ const config = loadConfig();
110
+ createLogger(config);
111
+
112
+ await showStartupCheck('Configuration file', async () => {
113
+ // loadConfig already succeeded if we got here
114
+ });
115
+
116
+ await showStartupCheck('ANTHROPIC_API_KEY', async () => {
117
+ if (!config.anthropic.api_key) throw new Error('Not set');
118
+ });
119
+
120
+ await showStartupCheck('TELEGRAM_BOT_TOKEN', async () => {
121
+ if (!config.telegram.bot_token) throw new Error('Not set');
122
+ });
123
+
124
+ await showStartupCheck('Anthropic API connection', async () => {
125
+ const client = new Anthropic({ apiKey: config.anthropic.api_key });
126
+ await client.messages.create({
127
+ model: config.anthropic.model,
128
+ max_tokens: 16,
129
+ messages: [{ role: 'user', content: 'ping' }],
130
+ });
131
+ });
132
+
133
+ await showStartupCheck('Telegram Bot API', async () => {
134
+ const res = await fetch(
135
+ `https://api.telegram.org/bot${config.telegram.bot_token}/getMe`,
136
+ );
137
+ const data = await res.json();
138
+ if (!data.ok) throw new Error(data.description || 'Invalid token');
139
+ });
140
+
141
+ console.log('\nAll checks complete.');
142
+ });
143
+
144
+ // ─── kernel init ─────────────────────────────────────────────
145
+ program
146
+ .command('init')
147
+ .description('Interactive setup: create .env and config.yaml')
148
+ .action(async () => {
149
+ showLogo();
150
+
151
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
152
+ const ask = (q) => new Promise((res) => rl.question(q, res));
153
+
154
+ const apiKey = await ask('Anthropic API key: ');
155
+ const botToken = await ask('Telegram Bot Token: ');
156
+ const userId = await ask('Your Telegram User ID (leave blank for dev mode): ');
157
+
158
+ rl.close();
159
+
160
+ // Write .env
161
+ const envContent = `ANTHROPIC_API_KEY=${apiKey}\nTELEGRAM_BOT_TOKEN=${botToken}\n`;
162
+ writeFileSync('.env', envContent);
163
+
164
+ // Write config.yaml
165
+ const allowedUsers =
166
+ userId.trim() ? `\n allowed_users:\n - ${userId.trim()}` : '\n allowed_users: []';
167
+
168
+ const configContent = `bot:
169
+ name: KernelBot
170
+
171
+ anthropic:
172
+ model: claude-sonnet-4-20250514
173
+ max_tokens: 8192
174
+ temperature: 0.3
175
+ max_tool_depth: 25
176
+
177
+ telegram:${allowedUsers}
178
+
179
+ security:
180
+ blocked_paths:
181
+ - /etc/shadow
182
+ - /etc/passwd
183
+
184
+ logging:
185
+ level: info
186
+ max_file_size: 5242880
187
+
188
+ conversation:
189
+ max_history: 50
190
+ `;
191
+ writeFileSync('config.yaml', configContent);
192
+
193
+ console.log('\nCreated .env and config.yaml');
194
+ console.log('Run `kernelbot check` to verify, then `kernelbot start` to launch.');
195
+ });
196
+
197
+ program.parse();
@@ -0,0 +1,30 @@
1
+ # KernelBot Configuration
2
+ # Copy to config.yaml and adjust as needed.
3
+
4
+ bot:
5
+ name: KernelBot
6
+ description: AI engineering agent with full OS control
7
+
8
+ anthropic:
9
+ model: claude-sonnet-4-20250514
10
+ max_tokens: 8192
11
+ temperature: 0.3
12
+ max_tool_depth: 25
13
+
14
+ telegram:
15
+ # List Telegram user IDs allowed to interact. Empty = allow all (dev mode).
16
+ allowed_users: []
17
+
18
+ security:
19
+ blocked_paths:
20
+ - /etc/shadow
21
+ - /etc/passwd
22
+ - ~/.ssh/id_rsa
23
+ - ~/.ssh/id_ed25519
24
+
25
+ logging:
26
+ level: info
27
+ max_file_size: 5242880 # 5 MB
28
+
29
+ conversation:
30
+ max_history: 50
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "kernelbot",
3
+ "version": "1.0.0",
4
+ "description": "KernelBot — AI engineering agent with full OS control",
5
+ "type": "module",
6
+ "author": "Abdullah Al-Tahrei <abdullah@altaheri.me>",
7
+ "bin": {
8
+ "kernelbot": "./bin/kernel.js"
9
+ },
10
+ "scripts": {
11
+ "start": "node bin/kernel.js start"
12
+ },
13
+ "keywords": [
14
+ "ai",
15
+ "agent",
16
+ "telegram",
17
+ "anthropic",
18
+ "tools"
19
+ ],
20
+ "license": "MIT",
21
+ "dependencies": {
22
+ "@anthropic-ai/sdk": "^0.39.0",
23
+ "chalk": "^5.4.1",
24
+ "commander": "^13.1.0",
25
+ "dotenv": "^16.4.7",
26
+ "js-yaml": "^4.1.0",
27
+ "node-telegram-bot-api": "^0.66.0",
28
+ "ora": "^8.1.1",
29
+ "boxen": "^8.0.1",
30
+ "uuid": "^11.1.0",
31
+ "winston": "^3.17.0"
32
+ }
33
+ }
package/src/agent.js ADDED
@@ -0,0 +1,94 @@
1
+ import Anthropic from '@anthropic-ai/sdk';
2
+ import { toolDefinitions, executeTool } from './tools/index.js';
3
+ import { getSystemPrompt } from './prompts/system.js';
4
+ import { getLogger } from './utils/logger.js';
5
+
6
+ export class Agent {
7
+ constructor({ config, conversationManager }) {
8
+ this.config = config;
9
+ this.conversationManager = conversationManager;
10
+ this.client = new Anthropic({ apiKey: config.anthropic.api_key });
11
+ this.systemPrompt = getSystemPrompt(config);
12
+ }
13
+
14
+ async processMessage(chatId, userMessage, user) {
15
+ const logger = getLogger();
16
+ const { model, max_tokens, temperature, max_tool_depth } = this.config.anthropic;
17
+
18
+ // Add user message to persistent history
19
+ this.conversationManager.addMessage(chatId, 'user', userMessage);
20
+
21
+ // Build working messages from history
22
+ const messages = [...this.conversationManager.getHistory(chatId)];
23
+
24
+ for (let depth = 0; depth < max_tool_depth; depth++) {
25
+ logger.debug(`Agent loop iteration ${depth + 1}/${max_tool_depth}`);
26
+
27
+ const response = await this.client.messages.create({
28
+ model,
29
+ max_tokens,
30
+ temperature,
31
+ system: this.systemPrompt,
32
+ tools: toolDefinitions,
33
+ messages,
34
+ });
35
+
36
+ if (response.stop_reason === 'end_turn') {
37
+ const textBlocks = response.content
38
+ .filter((b) => b.type === 'text')
39
+ .map((b) => b.text);
40
+ const reply = textBlocks.join('\n');
41
+
42
+ // Save assistant reply to persistent history
43
+ this.conversationManager.addMessage(chatId, 'assistant', reply);
44
+ return reply;
45
+ }
46
+
47
+ if (response.stop_reason === 'tool_use') {
48
+ // Push assistant response as-is (contains tool_use blocks)
49
+ messages.push({ role: 'assistant', content: response.content });
50
+
51
+ // Execute each tool_use block
52
+ const toolResults = [];
53
+ for (const block of response.content) {
54
+ if (block.type !== 'tool_use') continue;
55
+
56
+ logger.info(`Tool call: ${block.name}`);
57
+
58
+ const result = await executeTool(block.name, block.input, {
59
+ config: this.config,
60
+ user,
61
+ });
62
+
63
+ toolResults.push({
64
+ type: 'tool_result',
65
+ tool_use_id: block.id,
66
+ content: JSON.stringify(result),
67
+ });
68
+ }
69
+
70
+ // Push all tool results as a single user message
71
+ messages.push({ role: 'user', content: toolResults });
72
+ continue;
73
+ }
74
+
75
+ // Unexpected stop reason
76
+ logger.warn(`Unexpected stop_reason: ${response.stop_reason}`);
77
+ const fallbackText = response.content
78
+ .filter((b) => b.type === 'text')
79
+ .map((b) => b.text)
80
+ .join('\n');
81
+ if (fallbackText) {
82
+ this.conversationManager.addMessage(chatId, 'assistant', fallbackText);
83
+ return fallbackText;
84
+ }
85
+ return 'Something went wrong — unexpected response from the model.';
86
+ }
87
+
88
+ const depthWarning =
89
+ `Reached maximum tool depth (${max_tool_depth}). Stopping to prevent infinite loops. ` +
90
+ `Please try again with a simpler request.`;
91
+ this.conversationManager.addMessage(chatId, 'assistant', depthWarning);
92
+ return depthWarning;
93
+ }
94
+ }
package/src/bot.js ADDED
@@ -0,0 +1,81 @@
1
+ import TelegramBot from 'node-telegram-bot-api';
2
+ import { isAllowedUser, getUnauthorizedMessage } from './security/auth.js';
3
+ import { getLogger } from './utils/logger.js';
4
+
5
+ function splitMessage(text, maxLength = 4096) {
6
+ if (text.length <= maxLength) return [text];
7
+
8
+ const chunks = [];
9
+ let remaining = text;
10
+ while (remaining.length > 0) {
11
+ if (remaining.length <= maxLength) {
12
+ chunks.push(remaining);
13
+ break;
14
+ }
15
+ // Try to split at a newline near the limit
16
+ let splitAt = remaining.lastIndexOf('\n', maxLength);
17
+ if (splitAt < maxLength / 2) splitAt = maxLength;
18
+ chunks.push(remaining.slice(0, splitAt));
19
+ remaining = remaining.slice(splitAt);
20
+ }
21
+ return chunks;
22
+ }
23
+
24
+ export function startBot(config, agent) {
25
+ const logger = getLogger();
26
+ const bot = new TelegramBot(config.telegram.bot_token, { polling: true });
27
+
28
+ logger.info('Telegram bot started with polling');
29
+
30
+ bot.on('message', async (msg) => {
31
+ if (!msg.text) return; // ignore non-text
32
+
33
+ const chatId = msg.chat.id;
34
+ const userId = msg.from.id;
35
+ const username = msg.from.username || msg.from.first_name || 'unknown';
36
+
37
+ // Auth check
38
+ if (!isAllowedUser(userId, config)) {
39
+ logger.warn(`Unauthorized access attempt from ${username} (${userId})`);
40
+ await bot.sendMessage(chatId, getUnauthorizedMessage());
41
+ return;
42
+ }
43
+
44
+ logger.info(`Message from ${username} (${userId}): ${msg.text.slice(0, 100)}`);
45
+
46
+ // Show typing and keep refreshing it
47
+ const typingInterval = setInterval(() => {
48
+ bot.sendChatAction(chatId, 'typing').catch(() => {});
49
+ }, 4000);
50
+ bot.sendChatAction(chatId, 'typing').catch(() => {});
51
+
52
+ try {
53
+ const reply = await agent.processMessage(chatId, msg.text, {
54
+ id: userId,
55
+ username,
56
+ });
57
+
58
+ clearInterval(typingInterval);
59
+
60
+ const chunks = splitMessage(reply || 'Done.');
61
+ for (const chunk of chunks) {
62
+ try {
63
+ await bot.sendMessage(chatId, chunk, { parse_mode: 'Markdown' });
64
+ } catch {
65
+ // Fallback to plain text if Markdown fails
66
+ await bot.sendMessage(chatId, chunk);
67
+ }
68
+ }
69
+ } catch (err) {
70
+ clearInterval(typingInterval);
71
+ logger.error(`Error processing message: ${err.message}`);
72
+ await bot.sendMessage(chatId, `Error: ${err.message}`);
73
+ }
74
+ });
75
+
76
+ bot.on('polling_error', (err) => {
77
+ logger.error(`Telegram polling error: ${err.message}`);
78
+ });
79
+
80
+ return bot;
81
+ }
@@ -0,0 +1,36 @@
1
+ export class ConversationManager {
2
+ constructor(config) {
3
+ this.maxHistory = config.conversation.max_history;
4
+ this.conversations = new Map();
5
+ }
6
+
7
+ getHistory(chatId) {
8
+ if (!this.conversations.has(chatId)) {
9
+ this.conversations.set(chatId, []);
10
+ }
11
+ return this.conversations.get(chatId);
12
+ }
13
+
14
+ addMessage(chatId, role, content) {
15
+ const history = this.getHistory(chatId);
16
+ history.push({ role, content });
17
+
18
+ // Trim to max history
19
+ while (history.length > this.maxHistory) {
20
+ history.shift();
21
+ }
22
+
23
+ // Ensure conversation starts with user role
24
+ while (history.length > 0 && history[0].role !== 'user') {
25
+ history.shift();
26
+ }
27
+ }
28
+
29
+ clear(chatId) {
30
+ this.conversations.delete(chatId);
31
+ }
32
+
33
+ clearAll() {
34
+ this.conversations.clear();
35
+ }
36
+ }
@@ -0,0 +1,19 @@
1
+ import { toolDefinitions } from '../tools/index.js';
2
+
3
+ export function getSystemPrompt(config) {
4
+ const toolList = toolDefinitions.map((t) => `- ${t.name}: ${t.description}`).join('\n');
5
+
6
+ return `You are ${config.bot.name}, an AI engineering agent with full OS control.
7
+
8
+ You have access to the following tools to interact with the operating system:
9
+ ${toolList}
10
+
11
+ Guidelines:
12
+ - Use tools proactively to complete tasks. Don't just describe what you would do — do it.
13
+ - When a task requires multiple steps, execute them in sequence using tools.
14
+ - If a command fails, analyze the error and try an alternative approach.
15
+ - Be concise in your responses. Show what you did and the result.
16
+ - When writing code, write complete, working files — not snippets.
17
+ - For destructive operations (deleting files, overwriting data), confirm with the user first unless they've explicitly asked for it.
18
+ - If you're unsure about something, read the relevant files first before making changes.`;
19
+ }
@@ -0,0 +1,58 @@
1
+ import winston from 'winston';
2
+
3
+ const SECRET_PATTERNS = /token|key|secret|password|api_key/i;
4
+
5
+ function redactSecrets(obj) {
6
+ if (obj === null || obj === undefined) return obj;
7
+ if (typeof obj === 'string') return obj;
8
+ if (typeof obj !== 'object') return obj;
9
+
10
+ const redacted = Array.isArray(obj) ? [...obj] : { ...obj };
11
+ for (const k of Object.keys(redacted)) {
12
+ if (SECRET_PATTERNS.test(k)) {
13
+ redacted[k] = '[REDACTED]';
14
+ } else if (typeof redacted[k] === 'object') {
15
+ redacted[k] = redactSecrets(redacted[k]);
16
+ }
17
+ }
18
+ return redacted;
19
+ }
20
+
21
+ function truncate(str, max = 500) {
22
+ if (typeof str !== 'string') str = JSON.stringify(str);
23
+ if (!str) return '';
24
+ return str.length > max ? str.slice(0, max) + '...[truncated]' : str;
25
+ }
26
+
27
+ let auditLogger = null;
28
+
29
+ export function createAuditLogger() {
30
+ auditLogger = winston.createLogger({
31
+ level: 'info',
32
+ format: winston.format.combine(
33
+ winston.format.timestamp(),
34
+ winston.format.json(),
35
+ ),
36
+ transports: [
37
+ new winston.transports.File({
38
+ filename: 'kernel-audit.log',
39
+ maxsize: 5_242_880,
40
+ maxFiles: 3,
41
+ }),
42
+ ],
43
+ });
44
+ return auditLogger;
45
+ }
46
+
47
+ export function logToolCall({ user, tool, params, output, success, duration }) {
48
+ if (!auditLogger) return;
49
+
50
+ auditLogger.info('tool_call', {
51
+ user,
52
+ tool,
53
+ params: redactSecrets(params),
54
+ output: truncate(output),
55
+ success,
56
+ duration_ms: duration,
57
+ });
58
+ }
@@ -0,0 +1,9 @@
1
+ export function isAllowedUser(userId, config) {
2
+ const allowed = config.telegram.allowed_users;
3
+ if (!allowed || allowed.length === 0) return true; // dev mode
4
+ return allowed.includes(userId);
5
+ }
6
+
7
+ export function getUnauthorizedMessage() {
8
+ return 'Access denied. You are not authorized to use this bot.';
9
+ }
@@ -0,0 +1,38 @@
1
+ import { definitions as osDefinitions, handlers as osHandlers } from './os.js';
2
+ import { logToolCall } from '../security/audit.js';
3
+
4
+ export const toolDefinitions = [...osDefinitions];
5
+
6
+ const handlerMap = { ...osHandlers };
7
+
8
+ export async function executeTool(name, params, context) {
9
+ const handler = handlerMap[name];
10
+ if (!handler) {
11
+ return { error: `Unknown tool: ${name}` };
12
+ }
13
+
14
+ const start = Date.now();
15
+ let output;
16
+ let success = true;
17
+
18
+ try {
19
+ output = await handler(params, context);
20
+ if (output?.error) success = false;
21
+ } catch (err) {
22
+ output = { error: err.message };
23
+ success = false;
24
+ }
25
+
26
+ const duration = Date.now() - start;
27
+
28
+ logToolCall({
29
+ user: context.user,
30
+ tool: name,
31
+ params,
32
+ output,
33
+ success,
34
+ duration,
35
+ });
36
+
37
+ return output;
38
+ }
@@ -0,0 +1,201 @@
1
+ import { exec } from 'child_process';
2
+ import { readFileSync, writeFileSync, mkdirSync, readdirSync, statSync } from 'fs';
3
+ import { dirname, resolve, join } from 'path';
4
+ import { homedir } from 'os';
5
+
6
+ function expandPath(p) {
7
+ if (p.startsWith('~')) return join(homedir(), p.slice(1));
8
+ return resolve(p);
9
+ }
10
+
11
+ function isBlocked(filePath, config) {
12
+ const expanded = expandPath(filePath);
13
+ const blockedPaths = config.security?.blocked_paths || [];
14
+ return blockedPaths.some((bp) => expanded.startsWith(expandPath(bp)));
15
+ }
16
+
17
+ export const definitions = [
18
+ {
19
+ name: 'execute_command',
20
+ description:
21
+ 'Execute a shell command and return its stdout and stderr. Use for running programs, scripts, git commands, package managers, etc.',
22
+ input_schema: {
23
+ type: 'object',
24
+ properties: {
25
+ command: {
26
+ type: 'string',
27
+ description: 'The shell command to execute',
28
+ },
29
+ timeout_seconds: {
30
+ type: 'number',
31
+ description: 'Max execution time in seconds (default 30)',
32
+ default: 30,
33
+ },
34
+ },
35
+ required: ['command'],
36
+ },
37
+ },
38
+ {
39
+ name: 'read_file',
40
+ description: 'Read the contents of a file. Returns the text content and total line count.',
41
+ input_schema: {
42
+ type: 'object',
43
+ properties: {
44
+ path: {
45
+ type: 'string',
46
+ description: 'Absolute or relative file path',
47
+ },
48
+ max_lines: {
49
+ type: 'number',
50
+ description: 'Maximum number of lines to return (default: all)',
51
+ },
52
+ },
53
+ required: ['path'],
54
+ },
55
+ },
56
+ {
57
+ name: 'write_file',
58
+ description:
59
+ 'Write content to a file. Creates parent directories if they do not exist. Overwrites existing content.',
60
+ input_schema: {
61
+ type: 'object',
62
+ properties: {
63
+ path: {
64
+ type: 'string',
65
+ description: 'Absolute or relative file path',
66
+ },
67
+ content: {
68
+ type: 'string',
69
+ description: 'The content to write',
70
+ },
71
+ },
72
+ required: ['path', 'content'],
73
+ },
74
+ },
75
+ {
76
+ name: 'list_directory',
77
+ description:
78
+ 'List files and subdirectories in a directory. Returns entries with name and type.',
79
+ input_schema: {
80
+ type: 'object',
81
+ properties: {
82
+ path: {
83
+ type: 'string',
84
+ description: 'Directory path',
85
+ },
86
+ recursive: {
87
+ type: 'boolean',
88
+ description: 'Whether to list recursively (default false)',
89
+ default: false,
90
+ },
91
+ },
92
+ required: ['path'],
93
+ },
94
+ },
95
+ ];
96
+
97
+ function listRecursive(dirPath, base = '') {
98
+ const entries = [];
99
+ for (const entry of readdirSync(dirPath, { withFileTypes: true })) {
100
+ const rel = base ? `${base}/${entry.name}` : entry.name;
101
+ const type = entry.isDirectory() ? 'directory' : 'file';
102
+ entries.push({ name: rel, type });
103
+ if (entry.isDirectory()) {
104
+ entries.push(...listRecursive(join(dirPath, entry.name), rel));
105
+ }
106
+ }
107
+ return entries;
108
+ }
109
+
110
+ export const handlers = {
111
+ execute_command: async (params, context) => {
112
+ const { command, timeout_seconds = 30 } = params;
113
+ const { config } = context;
114
+ const blockedPaths = config.security?.blocked_paths || [];
115
+
116
+ // Simple check: if the command references a blocked path, reject
117
+ for (const bp of blockedPaths) {
118
+ const expanded = expandPath(bp);
119
+ if (command.includes(expanded)) {
120
+ return { error: `Blocked: command references restricted path ${bp}` };
121
+ }
122
+ }
123
+
124
+ return new Promise((res) => {
125
+ const child = exec(
126
+ command,
127
+ { timeout: timeout_seconds * 1000, maxBuffer: 10 * 1024 * 1024 },
128
+ (error, stdout, stderr) => {
129
+ if (error && error.killed) {
130
+ return res({ error: `Command timed out after ${timeout_seconds}s` });
131
+ }
132
+ res({
133
+ stdout: stdout || '',
134
+ stderr: stderr || '',
135
+ exit_code: error ? error.code ?? 1 : 0,
136
+ });
137
+ },
138
+ );
139
+ });
140
+ },
141
+
142
+ read_file: async (params, context) => {
143
+ const { path: filePath, max_lines } = params;
144
+ if (isBlocked(filePath, context.config)) {
145
+ return { error: `Blocked: access to ${filePath} is restricted` };
146
+ }
147
+
148
+ try {
149
+ const content = readFileSync(expandPath(filePath), 'utf-8');
150
+ const lines = content.split('\n');
151
+ const total_lines = lines.length;
152
+
153
+ if (max_lines && max_lines < total_lines) {
154
+ return { content: lines.slice(0, max_lines).join('\n'), total_lines };
155
+ }
156
+ return { content, total_lines };
157
+ } catch (err) {
158
+ return { error: err.message };
159
+ }
160
+ },
161
+
162
+ write_file: async (params, context) => {
163
+ const { path: filePath, content } = params;
164
+ if (isBlocked(filePath, context.config)) {
165
+ return { error: `Blocked: access to ${filePath} is restricted` };
166
+ }
167
+
168
+ try {
169
+ const expanded = expandPath(filePath);
170
+ mkdirSync(dirname(expanded), { recursive: true });
171
+ writeFileSync(expanded, content, 'utf-8');
172
+ return { success: true, path: expanded };
173
+ } catch (err) {
174
+ return { error: err.message };
175
+ }
176
+ },
177
+
178
+ list_directory: async (params, context) => {
179
+ const { path: dirPath, recursive = false } = params;
180
+ if (isBlocked(dirPath, context.config)) {
181
+ return { error: `Blocked: access to ${dirPath} is restricted` };
182
+ }
183
+
184
+ try {
185
+ const expanded = expandPath(dirPath);
186
+ if (recursive) {
187
+ return { entries: listRecursive(expanded) };
188
+ }
189
+
190
+ const entries = readdirSync(expanded, { withFileTypes: true }).map(
191
+ (entry) => ({
192
+ name: entry.name,
193
+ type: entry.isDirectory() ? 'directory' : 'file',
194
+ }),
195
+ );
196
+ return { entries };
197
+ } catch (err) {
198
+ return { error: err.message };
199
+ }
200
+ },
201
+ };
@@ -0,0 +1,88 @@
1
+ import { readFileSync, existsSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { homedir } from 'os';
4
+ import yaml from 'js-yaml';
5
+ import dotenv from 'dotenv';
6
+
7
+ const DEFAULTS = {
8
+ bot: {
9
+ name: 'KernelBot',
10
+ description: 'AI engineering agent with full OS control',
11
+ },
12
+ anthropic: {
13
+ model: 'claude-sonnet-4-20250514',
14
+ max_tokens: 8192,
15
+ temperature: 0.3,
16
+ max_tool_depth: 25,
17
+ },
18
+ telegram: {
19
+ allowed_users: [],
20
+ },
21
+ security: {
22
+ blocked_paths: [
23
+ '/etc/shadow',
24
+ '/etc/passwd',
25
+ '~/.ssh/id_rsa',
26
+ '~/.ssh/id_ed25519',
27
+ ],
28
+ },
29
+ logging: {
30
+ level: 'info',
31
+ max_file_size: 5_242_880,
32
+ },
33
+ conversation: {
34
+ max_history: 50,
35
+ },
36
+ };
37
+
38
+ function deepMerge(target, source) {
39
+ const result = { ...target };
40
+ for (const key of Object.keys(source)) {
41
+ if (
42
+ source[key] &&
43
+ typeof source[key] === 'object' &&
44
+ !Array.isArray(source[key]) &&
45
+ target[key] &&
46
+ typeof target[key] === 'object' &&
47
+ !Array.isArray(target[key])
48
+ ) {
49
+ result[key] = deepMerge(target[key], source[key]);
50
+ } else {
51
+ result[key] = source[key];
52
+ }
53
+ }
54
+ return result;
55
+ }
56
+
57
+ function findConfigFile() {
58
+ const cwdPath = join(process.cwd(), 'config.yaml');
59
+ if (existsSync(cwdPath)) return cwdPath;
60
+
61
+ const homePath = join(homedir(), '.kernelbot', 'config.yaml');
62
+ if (existsSync(homePath)) return homePath;
63
+
64
+ return null;
65
+ }
66
+
67
+ export function loadConfig() {
68
+ dotenv.config();
69
+
70
+ let fileConfig = {};
71
+ const configPath = findConfigFile();
72
+ if (configPath) {
73
+ const raw = readFileSync(configPath, 'utf-8');
74
+ fileConfig = yaml.load(raw) || {};
75
+ }
76
+
77
+ const config = deepMerge(DEFAULTS, fileConfig);
78
+
79
+ // Overlay env vars for secrets
80
+ if (process.env.ANTHROPIC_API_KEY) {
81
+ config.anthropic.api_key = process.env.ANTHROPIC_API_KEY;
82
+ }
83
+ if (process.env.TELEGRAM_BOT_TOKEN) {
84
+ config.telegram.bot_token = process.env.TELEGRAM_BOT_TOKEN;
85
+ }
86
+
87
+ return Object.freeze(config);
88
+ }
@@ -0,0 +1,64 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import boxen from 'boxen';
4
+
5
+ const LOGO = `
6
+ ██╗ ██╗███████╗██████╗ ███╗ ██╗███████╗██╗ ██████╗ ██████╗ ████████╗
7
+ ██║ ██╔╝██╔════╝██╔══██╗████╗ ██║██╔════╝██║ ██╔══██╗██╔═══██╗╚══██╔══╝
8
+ █████╔╝ █████╗ ██████╔╝██╔██╗ ██║█████╗ ██║ ██████╔╝██║ ██║ ██║
9
+ ██╔═██╗ ██╔══╝ ██╔══██╗██║╚██╗██║██╔══╝ ██║ ██╔══██╗██║ ██║ ██║
10
+ ██║ ██╗███████╗██║ ██║██║ ╚████║███████╗███████╗██████╔╝╚██████╔╝ ██║
11
+ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝╚══════╝╚═════╝ ╚═════╝ ╚═╝
12
+ `;
13
+
14
+ export function showLogo() {
15
+ console.log(chalk.cyan(LOGO));
16
+ console.log(chalk.dim(' AI Engineering Agent\n'));
17
+ }
18
+
19
+ export async function showStartupCheck(label, checkFn) {
20
+ const spinner = ora({ text: label, color: 'cyan' }).start();
21
+ try {
22
+ await checkFn();
23
+ spinner.succeed(chalk.green(label));
24
+ return true;
25
+ } catch (err) {
26
+ spinner.fail(chalk.red(`${label} — ${err.message}`));
27
+ return false;
28
+ }
29
+ }
30
+
31
+ export function showStartupComplete() {
32
+ console.log(
33
+ boxen(chalk.green.bold('KernelBot is live'), {
34
+ padding: 1,
35
+ margin: { top: 1 },
36
+ borderStyle: 'round',
37
+ borderColor: 'green',
38
+ }),
39
+ );
40
+ }
41
+
42
+ export function showSuccess(msg) {
43
+ console.log(
44
+ boxen(chalk.green(msg), {
45
+ padding: 1,
46
+ borderStyle: 'round',
47
+ borderColor: 'green',
48
+ }),
49
+ );
50
+ }
51
+
52
+ export function showError(msg) {
53
+ console.log(
54
+ boxen(chalk.red(msg), {
55
+ padding: 1,
56
+ borderStyle: 'round',
57
+ borderColor: 'red',
58
+ }),
59
+ );
60
+ }
61
+
62
+ export function createSpinner(text) {
63
+ return ora({ text, color: 'cyan' });
64
+ }
@@ -0,0 +1,40 @@
1
+ import winston from 'winston';
2
+
3
+ let logger = null;
4
+
5
+ export function createLogger(config) {
6
+ const { level, max_file_size } = config.logging;
7
+
8
+ logger = winston.createLogger({
9
+ level,
10
+ format: winston.format.combine(
11
+ winston.format.timestamp(),
12
+ winston.format.errors({ stack: true }),
13
+ ),
14
+ transports: [
15
+ new winston.transports.Console({
16
+ format: winston.format.combine(
17
+ winston.format.colorize(),
18
+ winston.format.printf(({ level, message, timestamp }) => {
19
+ return `[KernelBot] ${level}: ${message}`;
20
+ }),
21
+ ),
22
+ }),
23
+ new winston.transports.File({
24
+ filename: 'kernel.log',
25
+ maxsize: max_file_size,
26
+ maxFiles: 3,
27
+ format: winston.format.json(),
28
+ }),
29
+ ],
30
+ });
31
+
32
+ return logger;
33
+ }
34
+
35
+ export function getLogger() {
36
+ if (!logger) {
37
+ throw new Error('Logger not initialized. Call createLogger(config) first.');
38
+ }
39
+ return logger;
40
+ }