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 +2 -0
- package/README.md +138 -0
- package/bin/kernel.js +197 -0
- package/config.example.yaml +30 -0
- package/package.json +33 -0
- package/src/agent.js +94 -0
- package/src/bot.js +81 -0
- package/src/conversation.js +36 -0
- package/src/prompts/system.js +19 -0
- package/src/security/audit.js +58 -0
- package/src/security/auth.js +9 -0
- package/src/tools/index.js +38 -0
- package/src/tools/os.js +201 -0
- package/src/utils/config.js +88 -0
- package/src/utils/display.js +64 -0
- package/src/utils/logger.js +40 -0
package/.env.example
ADDED
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
|
+
}
|
package/src/tools/os.js
ADDED
|
@@ -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
|
+
}
|