kernelbot 1.0.4 → 1.0.6

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 CHANGED
@@ -1,2 +1,3 @@
1
1
  ANTHROPIC_API_KEY=sk-ant-...
2
2
  TELEGRAM_BOT_TOKEN=123456:ABC-DEF...
3
+ GITHUB_TOKEN=ghp_...
package/README.md CHANGED
@@ -23,6 +23,10 @@ KernelBot runs a **tool-use loop**: Claude decides which tools to call, KernelBo
23
23
  | `write_file` | Write/create files, auto-creates parent directories |
24
24
  | `list_directory` | List directory contents, optionally recursive |
25
25
 
26
+ ## Disclaimer
27
+
28
+ > **WARNING:** KernelBot has full access to your operating system. It can execute shell commands, read/write files, manage processes, control Docker containers, and interact with external services (GitHub, Telegram) on your behalf. Only run KernelBot on machines you own and control. Always configure `allowed_users` in production to restrict who can interact with the bot. The authors are not responsible for any damage caused by misuse.
29
+
26
30
  ## Installation
27
31
 
28
32
  ```bash
@@ -11,6 +11,15 @@ anthropic:
11
11
  temperature: 0.3
12
12
  max_tool_depth: 25
13
13
 
14
+ claude_code:
15
+ max_turns: 50
16
+ timeout_seconds: 600
17
+ # workspace_dir: /tmp/kernelbot-workspaces # default: ~/.kernelbot/workspaces
18
+
19
+ github:
20
+ default_branch: main
21
+ # default_org: my-org
22
+
14
23
  telegram:
15
24
  # List Telegram user IDs allowed to interact. Empty = allow all (dev mode).
16
25
  allowed_users: []
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kernelbot",
3
- "version": "1.0.004",
3
+ "version": "1.0.6",
4
4
  "description": "KernelBot — AI engineering agent with full OS control",
5
5
  "type": "module",
6
6
  "author": "Abdullah Al-Taheri <abdullah@altaheri.me>",
@@ -28,13 +28,16 @@
28
28
  "license": "MIT",
29
29
  "dependencies": {
30
30
  "@anthropic-ai/sdk": "^0.39.0",
31
+ "@octokit/rest": "^22.0.1",
32
+ "axios": "^1.13.5",
33
+ "boxen": "^8.0.1",
31
34
  "chalk": "^5.4.1",
32
35
  "commander": "^13.1.0",
33
36
  "dotenv": "^16.4.7",
34
37
  "js-yaml": "^4.1.0",
35
38
  "node-telegram-bot-api": "^0.66.0",
36
39
  "ora": "^8.1.1",
37
- "boxen": "^8.0.1",
40
+ "simple-git": "^3.31.1",
38
41
  "uuid": "^11.1.0",
39
42
  "winston": "^3.17.0"
40
43
  }
package/src/agent.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import Anthropic from '@anthropic-ai/sdk';
2
- import { toolDefinitions, executeTool } from './tools/index.js';
2
+ import { toolDefinitions, executeTool, checkConfirmation } from './tools/index.js';
3
3
  import { getSystemPrompt } from './prompts/system.js';
4
4
  import { getLogger } from './utils/logger.js';
5
5
 
@@ -9,11 +9,73 @@ export class Agent {
9
9
  this.conversationManager = conversationManager;
10
10
  this.client = new Anthropic({ apiKey: config.anthropic.api_key });
11
11
  this.systemPrompt = getSystemPrompt(config);
12
+ this._pendingConfirmation = new Map(); // chatId -> { block, context }
12
13
  }
13
14
 
14
15
  async processMessage(chatId, userMessage, user) {
15
16
  const logger = getLogger();
16
- const { model, max_tokens, temperature, max_tool_depth } = this.config.anthropic;
17
+
18
+ // Handle pending confirmation responses
19
+ const pending = this._pendingConfirmation.get(chatId);
20
+ if (pending) {
21
+ this._pendingConfirmation.delete(chatId);
22
+ const lower = userMessage.toLowerCase().trim();
23
+
24
+ if (lower === 'yes' || lower === 'y' || lower === 'confirm') {
25
+ // User approved — execute the blocked tool and resume
26
+ logger.info(`User confirmed dangerous tool: ${pending.block.name}`);
27
+ const result = await executeTool(pending.block.name, pending.block.input, pending.context);
28
+
29
+ // Resume the agent loop with the tool result
30
+ pending.toolResults.push({
31
+ type: 'tool_result',
32
+ tool_use_id: pending.block.id,
33
+ content: JSON.stringify(result),
34
+ });
35
+
36
+ // Process remaining blocks if any
37
+ for (const block of pending.remainingBlocks) {
38
+ if (block.type !== 'tool_use') continue;
39
+
40
+ const dangerLabel = checkConfirmation(block.name, block.input, this.config);
41
+ if (dangerLabel) {
42
+ // Another dangerous tool — ask again
43
+ this._pendingConfirmation.set(chatId, {
44
+ block,
45
+ context: pending.context,
46
+ toolResults: pending.toolResults,
47
+ remainingBlocks: pending.remainingBlocks.filter((b) => b !== block),
48
+ messages: pending.messages,
49
+ });
50
+ return `⚠️ Next action will **${dangerLabel}**.\n\n\`${block.name}\`: \`${JSON.stringify(block.input)}\`\n\nConfirm? (yes/no)`;
51
+ }
52
+
53
+ logger.info(`Tool call: ${block.name}`);
54
+ const r = await executeTool(block.name, block.input, pending.context);
55
+ pending.toolResults.push({
56
+ type: 'tool_result',
57
+ tool_use_id: block.id,
58
+ content: JSON.stringify(r),
59
+ });
60
+ }
61
+
62
+ // Continue the agent loop
63
+ pending.messages.push({ role: 'user', content: pending.toolResults });
64
+ return await this._continueLoop(chatId, pending.messages, user);
65
+ } else {
66
+ // User denied
67
+ logger.info(`User denied dangerous tool: ${pending.block.name}`);
68
+ pending.toolResults.push({
69
+ type: 'tool_result',
70
+ tool_use_id: pending.block.id,
71
+ content: JSON.stringify({ error: 'User denied this operation.' }),
72
+ });
73
+ pending.messages.push({ role: 'user', content: pending.toolResults });
74
+ return await this._continueLoop(chatId, pending.messages, user);
75
+ }
76
+ }
77
+
78
+ const { max_tool_depth } = this.config.anthropic;
17
79
 
18
80
  // Add user message to persistent history
19
81
  this.conversationManager.addMessage(chatId, 'user', userMessage);
@@ -21,8 +83,15 @@ export class Agent {
21
83
  // Build working messages from history
22
84
  const messages = [...this.conversationManager.getHistory(chatId)];
23
85
 
24
- for (let depth = 0; depth < max_tool_depth; depth++) {
25
- logger.debug(`Agent loop iteration ${depth + 1}/${max_tool_depth}`);
86
+ return await this._runLoop(chatId, messages, user, 0, max_tool_depth);
87
+ }
88
+
89
+ async _runLoop(chatId, messages, user, startDepth, maxDepth) {
90
+ const logger = getLogger();
91
+ const { model, max_tokens, temperature } = this.config.anthropic;
92
+
93
+ for (let depth = startDepth; depth < maxDepth; depth++) {
94
+ logger.debug(`Agent loop iteration ${depth + 1}/${maxDepth}`);
26
95
 
27
96
  const response = await this.client.messages.create({
28
97
  model,
@@ -39,19 +108,35 @@ export class Agent {
39
108
  .map((b) => b.text);
40
109
  const reply = textBlocks.join('\n');
41
110
 
42
- // Save assistant reply to persistent history
43
111
  this.conversationManager.addMessage(chatId, 'assistant', reply);
44
112
  return reply;
45
113
  }
46
114
 
47
115
  if (response.stop_reason === 'tool_use') {
48
- // Push assistant response as-is (contains tool_use blocks)
49
116
  messages.push({ role: 'assistant', content: response.content });
50
117
 
51
- // Execute each tool_use block
118
+ const toolUseBlocks = response.content.filter((b) => b.type === 'tool_use');
52
119
  const toolResults = [];
53
- for (const block of response.content) {
54
- if (block.type !== 'tool_use') continue;
120
+
121
+ for (let i = 0; i < toolUseBlocks.length; i++) {
122
+ const block = toolUseBlocks[i];
123
+
124
+ // Check if this tool requires confirmation
125
+ const dangerLabel = checkConfirmation(block.name, block.input, this.config);
126
+ if (dangerLabel) {
127
+ logger.warn(`Dangerous tool detected: ${block.name} — ${dangerLabel}`);
128
+
129
+ // Store state and pause for user confirmation
130
+ this._pendingConfirmation.set(chatId, {
131
+ block,
132
+ context: { config: this.config, user },
133
+ toolResults,
134
+ remainingBlocks: toolUseBlocks.slice(i + 1),
135
+ messages,
136
+ });
137
+
138
+ return `⚠️ This action will **${dangerLabel}**.\n\n\`${block.name}\`: \`${JSON.stringify(block.input)}\`\n\nConfirm? (yes/no)`;
139
+ }
55
140
 
56
141
  logger.info(`Tool call: ${block.name}`);
57
142
 
@@ -67,7 +152,6 @@ export class Agent {
67
152
  });
68
153
  }
69
154
 
70
- // Push all tool results as a single user message
71
155
  messages.push({ role: 'user', content: toolResults });
72
156
  continue;
73
157
  }
@@ -86,9 +170,14 @@ export class Agent {
86
170
  }
87
171
 
88
172
  const depthWarning =
89
- `Reached maximum tool depth (${max_tool_depth}). Stopping to prevent infinite loops. ` +
173
+ `Reached maximum tool depth (${maxDepth}). Stopping to prevent infinite loops. ` +
90
174
  `Please try again with a simpler request.`;
91
175
  this.conversationManager.addMessage(chatId, 'assistant', depthWarning);
92
176
  return depthWarning;
93
177
  }
178
+
179
+ async _continueLoop(chatId, messages, user) {
180
+ const { max_tool_depth } = this.config.anthropic;
181
+ return await this._runLoop(chatId, messages, user, 0, max_tool_depth);
182
+ }
94
183
  }
package/src/coder.js ADDED
@@ -0,0 +1,64 @@
1
+ import { spawn } from 'child_process';
2
+ import { getLogger } from './utils/logger.js';
3
+
4
+ export class ClaudeCodeSpawner {
5
+ constructor(config) {
6
+ this.maxTurns = config.claude_code?.max_turns || 50;
7
+ this.timeout = (config.claude_code?.timeout_seconds || 600) * 1000;
8
+ }
9
+
10
+ async run({ workingDirectory, prompt, maxTurns }) {
11
+ const logger = getLogger();
12
+ const turns = maxTurns || this.maxTurns;
13
+
14
+ logger.info(`Spawning Claude Code in ${workingDirectory}`);
15
+
16
+ return new Promise((resolve, reject) => {
17
+ const args = ['-p', prompt, '--max-turns', String(turns), '--output-format', 'text'];
18
+
19
+ const child = spawn('claude', args, {
20
+ cwd: workingDirectory,
21
+ env: { ...process.env },
22
+ stdio: ['ignore', 'pipe', 'pipe'],
23
+ });
24
+
25
+ let stdout = '';
26
+ let stderr = '';
27
+
28
+ child.stdout.on('data', (data) => {
29
+ stdout += data.toString();
30
+ });
31
+
32
+ child.stderr.on('data', (data) => {
33
+ stderr += data.toString();
34
+ });
35
+
36
+ const timer = setTimeout(() => {
37
+ child.kill('SIGTERM');
38
+ reject(new Error(`Claude Code timed out after ${this.timeout / 1000}s`));
39
+ }, this.timeout);
40
+
41
+ child.on('close', (code) => {
42
+ clearTimeout(timer);
43
+ if (code !== 0 && !stdout) {
44
+ reject(new Error(`Claude Code exited with code ${code}: ${stderr}`));
45
+ } else {
46
+ resolve({
47
+ output: stdout.trim(),
48
+ stderr: stderr.trim(),
49
+ exitCode: code,
50
+ });
51
+ }
52
+ });
53
+
54
+ child.on('error', (err) => {
55
+ clearTimeout(timer);
56
+ if (err.code === 'ENOENT') {
57
+ reject(new Error('Claude Code CLI not found. Install with: npm i -g @anthropic-ai/claude-code'));
58
+ } else {
59
+ reject(err);
60
+ }
61
+ });
62
+ });
63
+ }
64
+ }
@@ -3,17 +3,30 @@ import { toolDefinitions } from '../tools/index.js';
3
3
  export function getSystemPrompt(config) {
4
4
  const toolList = toolDefinitions.map((t) => `- ${t.name}: ${t.description}`).join('\n');
5
5
 
6
- return `You are ${config.bot.name}, an AI engineering agent with full OS control.
6
+ return `You are ${config.bot.name}, a senior software engineer and sysadmin AI agent.
7
+ You talk to the user via Telegram. You are confident, concise, and effective.
7
8
 
8
- You have access to the following tools to interact with the operating system:
9
+ You have full access to the operating system through your tools:
9
10
  ${toolList}
10
11
 
11
- Guidelines:
12
+ ## Coding Tasks (writing code, fixing bugs, reviewing code, scaffolding projects)
13
+ 1. Use git tools to clone the repo and create a branch
14
+ 2. Use spawn_claude_code to do the actual coding work inside the repo
15
+ 3. After Claude Code finishes, use git tools to commit and push
16
+ 4. Use GitHub tools to create the PR
17
+ 5. Report back with the PR link
18
+
19
+ ## Non-Coding Tasks (monitoring, deploying, restarting services, checking status)
20
+ - Use OS, Docker, process, network, and monitoring tools directly
21
+ - No need to spawn Claude Code for these
22
+
23
+ ## Guidelines
12
24
  - Use tools proactively to complete tasks. Don't just describe what you would do — do it.
13
25
  - When a task requires multiple steps, execute them in sequence using tools.
14
26
  - 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.`;
27
+ - Be concise you're talking on Telegram, not writing essays.
28
+ - For destructive operations (rm, kill, service stop, force push), confirm with the user first.
29
+ - Never expose API keys, tokens, or secrets in your responses.
30
+ - If a task will take a while, tell the user upfront.
31
+ - If something fails, explain what went wrong and suggest a fix.`;
19
32
  }
@@ -0,0 +1,35 @@
1
+ const DANGEROUS_PATTERNS = [
2
+ { tool: 'execute_command', pattern: /\brm\b/, label: 'delete files' },
3
+ { tool: 'execute_command', pattern: /\brmdir\b/, label: 'delete directories' },
4
+ { tool: 'kill_process', pattern: null, label: 'kill a process' },
5
+ { tool: 'service_control', param: 'action', value: 'stop', label: 'stop a service' },
6
+ { tool: 'github_create_repo', pattern: null, label: 'create a GitHub repository' },
7
+ { tool: 'docker_compose', param: 'action', value: 'down', label: 'take down containers' },
8
+ { tool: 'git_push', param: 'force', value: true, label: 'force push' },
9
+ ];
10
+
11
+ export function requiresConfirmation(toolName, params, config) {
12
+ // Check if confirmation is disabled in config
13
+ if (config.security?.require_confirmation === false) return null;
14
+
15
+ for (const rule of DANGEROUS_PATTERNS) {
16
+ if (rule.tool !== toolName) continue;
17
+
18
+ // Pattern match on command string
19
+ if (rule.pattern && params.command && rule.pattern.test(params.command)) {
20
+ return rule.label;
21
+ }
22
+
23
+ // Param value match
24
+ if (rule.param && params[rule.param] === rule.value) {
25
+ return rule.label;
26
+ }
27
+
28
+ // Tool-level match (no pattern/param — the tool itself is dangerous)
29
+ if (!rule.pattern && !rule.param) {
30
+ return rule.label;
31
+ }
32
+ }
33
+
34
+ return null;
35
+ }
@@ -0,0 +1,50 @@
1
+ import { ClaudeCodeSpawner } from '../coder.js';
2
+
3
+ let spawner = null;
4
+
5
+ function getSpawner(config) {
6
+ if (!spawner) spawner = new ClaudeCodeSpawner(config);
7
+ return spawner;
8
+ }
9
+
10
+ export const definitions = [
11
+ {
12
+ name: 'spawn_claude_code',
13
+ description:
14
+ 'Spawn Claude Code CLI to perform coding tasks in a directory. Use for writing code, fixing bugs, reviewing diffs, and scaffolding projects. Claude Code has full access to the filesystem within the given directory.',
15
+ input_schema: {
16
+ type: 'object',
17
+ properties: {
18
+ working_directory: {
19
+ type: 'string',
20
+ description: 'The directory to run Claude Code in (should be a cloned repo)',
21
+ },
22
+ prompt: {
23
+ type: 'string',
24
+ description: 'The coding task to perform — be specific about what to do',
25
+ },
26
+ max_turns: {
27
+ type: 'number',
28
+ description: 'Max turns for Claude Code (optional, default from config)',
29
+ },
30
+ },
31
+ required: ['working_directory', 'prompt'],
32
+ },
33
+ },
34
+ ];
35
+
36
+ export const handlers = {
37
+ spawn_claude_code: async (params, context) => {
38
+ try {
39
+ const coder = getSpawner(context.config);
40
+ const result = await coder.run({
41
+ workingDirectory: params.working_directory,
42
+ prompt: params.prompt,
43
+ maxTurns: params.max_turns,
44
+ });
45
+ return { success: true, output: result.output };
46
+ } catch (err) {
47
+ return { error: err.message };
48
+ }
49
+ },
50
+ };
@@ -0,0 +1,80 @@
1
+ import { exec } from 'child_process';
2
+
3
+ function run(cmd, timeout = 30000) {
4
+ return new Promise((resolve) => {
5
+ exec(cmd, { timeout, maxBuffer: 10 * 1024 * 1024 }, (error, stdout, stderr) => {
6
+ if (error) return resolve({ error: stderr || error.message });
7
+ resolve({ output: stdout.trim() });
8
+ });
9
+ });
10
+ }
11
+
12
+ export const definitions = [
13
+ {
14
+ name: 'docker_ps',
15
+ description: 'List Docker containers.',
16
+ input_schema: {
17
+ type: 'object',
18
+ properties: {
19
+ all: { type: 'boolean', description: 'Include stopped containers (default false)' },
20
+ },
21
+ },
22
+ },
23
+ {
24
+ name: 'docker_logs',
25
+ description: 'Get logs from a Docker container.',
26
+ input_schema: {
27
+ type: 'object',
28
+ properties: {
29
+ container: { type: 'string', description: 'Container name or ID' },
30
+ tail: { type: 'number', description: 'Number of lines to show (default 100)' },
31
+ },
32
+ required: ['container'],
33
+ },
34
+ },
35
+ {
36
+ name: 'docker_exec',
37
+ description: 'Execute a command inside a running Docker container.',
38
+ input_schema: {
39
+ type: 'object',
40
+ properties: {
41
+ container: { type: 'string', description: 'Container name or ID' },
42
+ command: { type: 'string', description: 'Command to execute' },
43
+ },
44
+ required: ['container', 'command'],
45
+ },
46
+ },
47
+ {
48
+ name: 'docker_compose',
49
+ description: 'Run a docker compose command.',
50
+ input_schema: {
51
+ type: 'object',
52
+ properties: {
53
+ action: { type: 'string', description: 'Compose action (e.g. "up -d", "down", "build", "logs")' },
54
+ project_dir: { type: 'string', description: 'Directory containing docker-compose.yml (optional)' },
55
+ },
56
+ required: ['action'],
57
+ },
58
+ },
59
+ ];
60
+
61
+ export const handlers = {
62
+ docker_ps: async (params) => {
63
+ const flag = params.all ? '-a' : '';
64
+ return await run(`docker ps ${flag} --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}\t{{.Image}}"`);
65
+ },
66
+
67
+ docker_logs: async (params) => {
68
+ const tail = params.tail || 100;
69
+ return await run(`docker logs --tail ${tail} ${params.container}`);
70
+ },
71
+
72
+ docker_exec: async (params) => {
73
+ return await run(`docker exec ${params.container} ${params.command}`);
74
+ },
75
+
76
+ docker_compose: async (params) => {
77
+ const dir = params.project_dir ? `-f ${params.project_dir}/docker-compose.yml` : '';
78
+ return await run(`docker compose ${dir} ${params.action}`, 120000);
79
+ },
80
+ };
@@ -0,0 +1,154 @@
1
+ import simpleGit from 'simple-git';
2
+ import { join } from 'path';
3
+ import { homedir } from 'os';
4
+ import { mkdirSync } from 'fs';
5
+
6
+ function getWorkspaceDir(config) {
7
+ const dir = config.claude_code?.workspace_dir || join(homedir(), '.kernelbot', 'workspaces');
8
+ mkdirSync(dir, { recursive: true });
9
+ return dir;
10
+ }
11
+
12
+ export const definitions = [
13
+ {
14
+ name: 'git_clone',
15
+ description: 'Clone a git repository. Accepts "org/repo" shorthand (uses GitHub) or a full URL.',
16
+ input_schema: {
17
+ type: 'object',
18
+ properties: {
19
+ repo: {
20
+ type: 'string',
21
+ description: 'Repository — "org/repo" or full git URL',
22
+ },
23
+ dest: {
24
+ type: 'string',
25
+ description: 'Destination directory name (optional, defaults to repo name)',
26
+ },
27
+ },
28
+ required: ['repo'],
29
+ },
30
+ },
31
+ {
32
+ name: 'git_checkout',
33
+ description: 'Checkout an existing branch or create a new one.',
34
+ input_schema: {
35
+ type: 'object',
36
+ properties: {
37
+ dir: { type: 'string', description: 'Repository directory path' },
38
+ branch: { type: 'string', description: 'Branch name' },
39
+ create: { type: 'boolean', description: 'Create the branch if it doesn\'t exist (default false)' },
40
+ },
41
+ required: ['dir', 'branch'],
42
+ },
43
+ },
44
+ {
45
+ name: 'git_commit',
46
+ description: 'Stage all changes and create a commit.',
47
+ input_schema: {
48
+ type: 'object',
49
+ properties: {
50
+ dir: { type: 'string', description: 'Repository directory path' },
51
+ message: { type: 'string', description: 'Commit message' },
52
+ },
53
+ required: ['dir', 'message'],
54
+ },
55
+ },
56
+ {
57
+ name: 'git_push',
58
+ description: 'Push the current branch to the remote.',
59
+ input_schema: {
60
+ type: 'object',
61
+ properties: {
62
+ dir: { type: 'string', description: 'Repository directory path' },
63
+ force: { type: 'boolean', description: 'Force push (default false)' },
64
+ },
65
+ required: ['dir'],
66
+ },
67
+ },
68
+ {
69
+ name: 'git_diff',
70
+ description: 'Get the diff of current uncommitted changes.',
71
+ input_schema: {
72
+ type: 'object',
73
+ properties: {
74
+ dir: { type: 'string', description: 'Repository directory path' },
75
+ },
76
+ required: ['dir'],
77
+ },
78
+ },
79
+ ];
80
+
81
+ export const handlers = {
82
+ git_clone: async (params, context) => {
83
+ const { repo, dest } = params;
84
+ const workspaceDir = getWorkspaceDir(context.config);
85
+
86
+ let url = repo;
87
+ if (!repo.includes('://') && !repo.startsWith('git@')) {
88
+ url = `https://github.com/${repo}.git`;
89
+ }
90
+
91
+ const repoName = dest || repo.split('/').pop().replace('.git', '');
92
+ const targetDir = join(workspaceDir, repoName);
93
+
94
+ try {
95
+ const git = simpleGit();
96
+ await git.clone(url, targetDir);
97
+ return { success: true, path: targetDir };
98
+ } catch (err) {
99
+ return { error: err.message };
100
+ }
101
+ },
102
+
103
+ git_checkout: async (params) => {
104
+ const { dir, branch, create = false } = params;
105
+ try {
106
+ const git = simpleGit(dir);
107
+ if (create) {
108
+ await git.checkoutLocalBranch(branch);
109
+ } else {
110
+ await git.checkout(branch);
111
+ }
112
+ return { success: true, branch };
113
+ } catch (err) {
114
+ return { error: err.message };
115
+ }
116
+ },
117
+
118
+ git_commit: async (params) => {
119
+ const { dir, message } = params;
120
+ try {
121
+ const git = simpleGit(dir);
122
+ await git.add('.');
123
+ const result = await git.commit(message);
124
+ return { success: true, commit: result.commit, summary: result.summary };
125
+ } catch (err) {
126
+ return { error: err.message };
127
+ }
128
+ },
129
+
130
+ git_push: async (params) => {
131
+ const { dir, force = false } = params;
132
+ try {
133
+ const git = simpleGit(dir);
134
+ const branch = (await git.branchLocal()).current;
135
+ const options = force ? ['--force'] : [];
136
+ await git.push('origin', branch, options);
137
+ return { success: true, branch };
138
+ } catch (err) {
139
+ return { error: err.message };
140
+ }
141
+ },
142
+
143
+ git_diff: async (params) => {
144
+ const { dir } = params;
145
+ try {
146
+ const git = simpleGit(dir);
147
+ const diff = await git.diff();
148
+ const staged = await git.diff(['--cached']);
149
+ return { unstaged: diff || '(no changes)', staged: staged || '(no staged changes)' };
150
+ } catch (err) {
151
+ return { error: err.message };
152
+ }
153
+ },
154
+ };
@@ -0,0 +1,201 @@
1
+ import { Octokit } from '@octokit/rest';
2
+
3
+ function getOctokit(config) {
4
+ const token = config.github?.token || process.env.GITHUB_TOKEN;
5
+ if (!token) throw new Error('GITHUB_TOKEN not configured');
6
+ return new Octokit({ auth: token });
7
+ }
8
+
9
+ function parseRepo(repo) {
10
+ const parts = repo.replace('https://github.com/', '').replace('.git', '').split('/');
11
+ if (parts.length < 2) throw new Error(`Invalid repo format: ${repo}. Use "owner/repo".`);
12
+ return { owner: parts[0], repo: parts[1] };
13
+ }
14
+
15
+ export const definitions = [
16
+ {
17
+ name: 'github_create_pr',
18
+ description: 'Create a pull request on GitHub.',
19
+ input_schema: {
20
+ type: 'object',
21
+ properties: {
22
+ repo: { type: 'string', description: 'Repository in "owner/repo" format' },
23
+ head: { type: 'string', description: 'Source branch name' },
24
+ base: { type: 'string', description: 'Target branch (default: main)' },
25
+ title: { type: 'string', description: 'PR title' },
26
+ body: { type: 'string', description: 'PR description' },
27
+ },
28
+ required: ['repo', 'head', 'title'],
29
+ },
30
+ },
31
+ {
32
+ name: 'github_get_pr_diff',
33
+ description: 'Get the diff of a pull request.',
34
+ input_schema: {
35
+ type: 'object',
36
+ properties: {
37
+ repo: { type: 'string', description: 'Repository in "owner/repo" format' },
38
+ pr_number: { type: 'number', description: 'Pull request number' },
39
+ },
40
+ required: ['repo', 'pr_number'],
41
+ },
42
+ },
43
+ {
44
+ name: 'github_post_review',
45
+ description: 'Post a review on a pull request.',
46
+ input_schema: {
47
+ type: 'object',
48
+ properties: {
49
+ repo: { type: 'string', description: 'Repository in "owner/repo" format' },
50
+ pr_number: { type: 'number', description: 'Pull request number' },
51
+ body: { type: 'string', description: 'Review body text' },
52
+ event: {
53
+ type: 'string',
54
+ description: 'Review action',
55
+ enum: ['APPROVE', 'REQUEST_CHANGES', 'COMMENT'],
56
+ },
57
+ },
58
+ required: ['repo', 'pr_number', 'body', 'event'],
59
+ },
60
+ },
61
+ {
62
+ name: 'github_create_repo',
63
+ description: 'Create a new GitHub repository.',
64
+ input_schema: {
65
+ type: 'object',
66
+ properties: {
67
+ name: { type: 'string', description: 'Repository name' },
68
+ org: { type: 'string', description: 'Organization (optional, defaults to personal)' },
69
+ private: { type: 'boolean', description: 'Private repo (default true)' },
70
+ description: { type: 'string', description: 'Repo description' },
71
+ },
72
+ required: ['name'],
73
+ },
74
+ },
75
+ {
76
+ name: 'github_list_prs',
77
+ description: 'List pull requests for a repository.',
78
+ input_schema: {
79
+ type: 'object',
80
+ properties: {
81
+ repo: { type: 'string', description: 'Repository in "owner/repo" format' },
82
+ state: { type: 'string', description: 'Filter by state: open, closed, all (default: open)' },
83
+ },
84
+ required: ['repo'],
85
+ },
86
+ },
87
+ ];
88
+
89
+ export const handlers = {
90
+ github_create_pr: async (params, context) => {
91
+ try {
92
+ const octokit = getOctokit(context.config);
93
+ const { owner, repo } = parseRepo(params.repo);
94
+ const base = params.base || context.config.github?.default_branch || 'main';
95
+
96
+ const { data } = await octokit.pulls.create({
97
+ owner,
98
+ repo,
99
+ head: params.head,
100
+ base,
101
+ title: params.title,
102
+ body: params.body || '',
103
+ });
104
+
105
+ return { success: true, pr_number: data.number, url: data.html_url };
106
+ } catch (err) {
107
+ return { error: err.message };
108
+ }
109
+ },
110
+
111
+ github_get_pr_diff: async (params, context) => {
112
+ try {
113
+ const octokit = getOctokit(context.config);
114
+ const { owner, repo } = parseRepo(params.repo);
115
+
116
+ const { data } = await octokit.pulls.get({
117
+ owner,
118
+ repo,
119
+ pull_number: params.pr_number,
120
+ mediaType: { format: 'diff' },
121
+ });
122
+
123
+ return { diff: data };
124
+ } catch (err) {
125
+ return { error: err.message };
126
+ }
127
+ },
128
+
129
+ github_post_review: async (params, context) => {
130
+ try {
131
+ const octokit = getOctokit(context.config);
132
+ const { owner, repo } = parseRepo(params.repo);
133
+
134
+ const { data } = await octokit.pulls.createReview({
135
+ owner,
136
+ repo,
137
+ pull_number: params.pr_number,
138
+ body: params.body,
139
+ event: params.event,
140
+ });
141
+
142
+ return { success: true, review_id: data.id };
143
+ } catch (err) {
144
+ return { error: err.message };
145
+ }
146
+ },
147
+
148
+ github_create_repo: async (params, context) => {
149
+ try {
150
+ const octokit = getOctokit(context.config);
151
+ const org = params.org || context.config.github?.default_org;
152
+ const isPrivate = params.private !== false;
153
+
154
+ let data;
155
+ if (org) {
156
+ ({ data } = await octokit.repos.createInOrg({
157
+ org,
158
+ name: params.name,
159
+ private: isPrivate,
160
+ description: params.description || '',
161
+ }));
162
+ } else {
163
+ ({ data } = await octokit.repos.createForAuthenticatedUser({
164
+ name: params.name,
165
+ private: isPrivate,
166
+ description: params.description || '',
167
+ }));
168
+ }
169
+
170
+ return { success: true, url: data.html_url, clone_url: data.clone_url };
171
+ } catch (err) {
172
+ return { error: err.message };
173
+ }
174
+ },
175
+
176
+ github_list_prs: async (params, context) => {
177
+ try {
178
+ const octokit = getOctokit(context.config);
179
+ const { owner, repo } = parseRepo(params.repo);
180
+
181
+ const { data } = await octokit.pulls.list({
182
+ owner,
183
+ repo,
184
+ state: params.state || 'open',
185
+ });
186
+
187
+ const prs = data.map((pr) => ({
188
+ number: pr.number,
189
+ title: pr.title,
190
+ state: pr.state,
191
+ author: pr.user.login,
192
+ url: pr.html_url,
193
+ created_at: pr.created_at,
194
+ }));
195
+
196
+ return { prs };
197
+ } catch (err) {
198
+ return { error: err.message };
199
+ }
200
+ },
201
+ };
@@ -1,9 +1,39 @@
1
1
  import { definitions as osDefinitions, handlers as osHandlers } from './os.js';
2
+ import { definitions as processDefinitions, handlers as processHandlers } from './process.js';
3
+ import { definitions as dockerDefinitions, handlers as dockerHandlers } from './docker.js';
4
+ import { definitions as monitorDefinitions, handlers as monitorHandlers } from './monitor.js';
5
+ import { definitions as networkDefinitions, handlers as networkHandlers } from './network.js';
6
+ import { definitions as gitDefinitions, handlers as gitHandlers } from './git.js';
7
+ import { definitions as githubDefinitions, handlers as githubHandlers } from './github.js';
8
+ import { definitions as codingDefinitions, handlers as codingHandlers } from './coding.js';
2
9
  import { logToolCall } from '../security/audit.js';
10
+ import { requiresConfirmation } from '../security/confirm.js';
3
11
 
4
- export const toolDefinitions = [...osDefinitions];
12
+ export const toolDefinitions = [
13
+ ...osDefinitions,
14
+ ...processDefinitions,
15
+ ...dockerDefinitions,
16
+ ...monitorDefinitions,
17
+ ...networkDefinitions,
18
+ ...gitDefinitions,
19
+ ...githubDefinitions,
20
+ ...codingDefinitions,
21
+ ];
5
22
 
6
- const handlerMap = { ...osHandlers };
23
+ const handlerMap = {
24
+ ...osHandlers,
25
+ ...processHandlers,
26
+ ...dockerHandlers,
27
+ ...monitorHandlers,
28
+ ...networkHandlers,
29
+ ...gitHandlers,
30
+ ...githubHandlers,
31
+ ...codingHandlers,
32
+ };
33
+
34
+ export function checkConfirmation(name, params, config) {
35
+ return requiresConfirmation(name, params, config);
36
+ }
7
37
 
8
38
  export async function executeTool(name, params, context) {
9
39
  const handler = handlerMap[name];
@@ -0,0 +1,84 @@
1
+ import { exec } from 'child_process';
2
+ import { platform } from 'os';
3
+
4
+ const isMac = platform() === 'darwin';
5
+
6
+ function run(cmd, timeout = 10000) {
7
+ return new Promise((resolve) => {
8
+ exec(cmd, { timeout }, (error, stdout, stderr) => {
9
+ if (error) return resolve({ error: stderr || error.message });
10
+ resolve({ output: stdout.trim() });
11
+ });
12
+ });
13
+ }
14
+
15
+ export const definitions = [
16
+ {
17
+ name: 'disk_usage',
18
+ description: 'Show disk space usage.',
19
+ input_schema: { type: 'object', properties: {} },
20
+ },
21
+ {
22
+ name: 'memory_usage',
23
+ description: 'Show memory (RAM) usage.',
24
+ input_schema: { type: 'object', properties: {} },
25
+ },
26
+ {
27
+ name: 'cpu_usage',
28
+ description: 'Show CPU load information.',
29
+ input_schema: { type: 'object', properties: {} },
30
+ },
31
+ {
32
+ name: 'system_logs',
33
+ description: 'Read system or application logs.',
34
+ input_schema: {
35
+ type: 'object',
36
+ properties: {
37
+ source: {
38
+ type: 'string',
39
+ description: 'Log source — file path or "journalctl" (default: journalctl)',
40
+ },
41
+ lines: { type: 'number', description: 'Number of lines to show (default 50)' },
42
+ filter: { type: 'string', description: 'Filter string (optional)' },
43
+ },
44
+ },
45
+ },
46
+ ];
47
+
48
+ export const handlers = {
49
+ disk_usage: async () => {
50
+ return await run('df -h');
51
+ },
52
+
53
+ memory_usage: async () => {
54
+ if (isMac) {
55
+ // macOS doesn't have /proc/meminfo or free
56
+ const vm = await run('vm_stat');
57
+ const total = await run("sysctl -n hw.memsize | awk '{print $1/1073741824}'");
58
+ return { output: `Total RAM: ${total.output}GB\n\n${vm.output}` };
59
+ }
60
+ return await run('free -h');
61
+ },
62
+
63
+ cpu_usage: async () => {
64
+ if (isMac) {
65
+ return await run('top -l 1 -n 0 | head -10');
66
+ }
67
+ return await run('uptime && echo "---" && cat /proc/loadavg');
68
+ },
69
+
70
+ system_logs: async (params) => {
71
+ const lines = params.lines || 50;
72
+ const source = params.source || 'journalctl';
73
+ const filter = params.filter;
74
+
75
+ if (source === 'journalctl') {
76
+ const filterArg = filter ? ` -g "${filter}"` : '';
77
+ return await run(`journalctl -n ${lines}${filterArg} --no-pager`);
78
+ }
79
+
80
+ // Reading a log file
81
+ const filterCmd = filter ? ` | grep -i "${filter}"` : '';
82
+ return await run(`tail -n ${lines} "${source}"${filterCmd}`);
83
+ },
84
+ };
@@ -0,0 +1,103 @@
1
+ import { exec } from 'child_process';
2
+ import { platform } from 'os';
3
+
4
+ function run(cmd, timeout = 15000) {
5
+ return new Promise((resolve) => {
6
+ exec(cmd, { timeout }, (error, stdout, stderr) => {
7
+ if (error) return resolve({ error: stderr || error.message });
8
+ resolve({ output: stdout.trim() });
9
+ });
10
+ });
11
+ }
12
+
13
+ export const definitions = [
14
+ {
15
+ name: 'check_port',
16
+ description: 'Check if a port is open and listening.',
17
+ input_schema: {
18
+ type: 'object',
19
+ properties: {
20
+ port: { type: 'number', description: 'Port number to check' },
21
+ host: { type: 'string', description: 'Host to check (default: localhost)' },
22
+ },
23
+ required: ['port'],
24
+ },
25
+ },
26
+ {
27
+ name: 'curl_url',
28
+ description: 'Make an HTTP request to a URL and return the response.',
29
+ input_schema: {
30
+ type: 'object',
31
+ properties: {
32
+ url: { type: 'string', description: 'URL to request' },
33
+ method: { type: 'string', description: 'HTTP method (default: GET)' },
34
+ headers: { type: 'object', description: 'Request headers (optional)' },
35
+ body: { type: 'string', description: 'Request body (optional)' },
36
+ },
37
+ required: ['url'],
38
+ },
39
+ },
40
+ {
41
+ name: 'nginx_reload',
42
+ description: 'Test nginx configuration and reload if valid.',
43
+ input_schema: { type: 'object', properties: {} },
44
+ },
45
+ ];
46
+
47
+ export const handlers = {
48
+ check_port: async (params) => {
49
+ const host = params.host || 'localhost';
50
+ const { port } = params;
51
+
52
+ // Use nc (netcat) for port check — works on both macOS and Linux
53
+ const result = await run(`nc -z -w 3 ${host} ${port} 2>&1 && echo "OPEN" || echo "CLOSED"`, 5000);
54
+
55
+ if (result.error) {
56
+ return { port, host, status: 'closed', detail: result.error };
57
+ }
58
+
59
+ const isOpen = result.output.includes('OPEN');
60
+ return { port, host, status: isOpen ? 'open' : 'closed' };
61
+ },
62
+
63
+ curl_url: async (params) => {
64
+ const { url, method = 'GET', headers, body } = params;
65
+
66
+ let cmd = `curl -s -w "\\n---HTTP_STATUS:%{http_code}" -X ${method}`;
67
+
68
+ if (headers) {
69
+ for (const [key, val] of Object.entries(headers)) {
70
+ cmd += ` -H "${key}: ${val}"`;
71
+ }
72
+ }
73
+
74
+ if (body) {
75
+ cmd += ` -d '${body.replace(/'/g, "'\\''")}'`;
76
+ }
77
+
78
+ cmd += ` "${url}"`;
79
+
80
+ const result = await run(cmd);
81
+
82
+ if (result.error) return result;
83
+
84
+ const parts = result.output.split('---HTTP_STATUS:');
85
+ const responseBody = parts[0].trim();
86
+ const statusCode = parts[1] ? parseInt(parts[1].trim()) : null;
87
+
88
+ return { status_code: statusCode, body: responseBody };
89
+ },
90
+
91
+ nginx_reload: async () => {
92
+ // Test config first
93
+ const test = await run('nginx -t 2>&1');
94
+ if (test.error || (test.output && test.output.includes('failed'))) {
95
+ return { error: `Config test failed: ${test.error || test.output}` };
96
+ }
97
+
98
+ const reload = await run('nginx -s reload 2>&1');
99
+ if (reload.error) return reload;
100
+
101
+ return { success: true, test_output: test.output };
102
+ },
103
+ };
@@ -0,0 +1,73 @@
1
+ import { exec } from 'child_process';
2
+
3
+ function run(cmd, timeout = 10000) {
4
+ return new Promise((resolve) => {
5
+ exec(cmd, { timeout }, (error, stdout, stderr) => {
6
+ if (error) return resolve({ error: stderr || error.message });
7
+ resolve({ output: stdout.trim() });
8
+ });
9
+ });
10
+ }
11
+
12
+ export const definitions = [
13
+ {
14
+ name: 'process_list',
15
+ description: 'List running processes. Optionally filter by name.',
16
+ input_schema: {
17
+ type: 'object',
18
+ properties: {
19
+ filter: { type: 'string', description: 'Filter processes by name (optional)' },
20
+ },
21
+ },
22
+ },
23
+ {
24
+ name: 'kill_process',
25
+ description: 'Kill a process by PID or name.',
26
+ input_schema: {
27
+ type: 'object',
28
+ properties: {
29
+ pid: { type: 'number', description: 'Process ID to kill' },
30
+ name: { type: 'string', description: 'Process name to kill (uses pkill)' },
31
+ },
32
+ },
33
+ },
34
+ {
35
+ name: 'service_control',
36
+ description: 'Manage systemd services (start, stop, restart, status).',
37
+ input_schema: {
38
+ type: 'object',
39
+ properties: {
40
+ service: { type: 'string', description: 'Service name' },
41
+ action: {
42
+ type: 'string',
43
+ description: 'Action to perform',
44
+ enum: ['start', 'stop', 'restart', 'status'],
45
+ },
46
+ },
47
+ required: ['service', 'action'],
48
+ },
49
+ },
50
+ ];
51
+
52
+ export const handlers = {
53
+ process_list: async (params) => {
54
+ const filter = params.filter;
55
+ const cmd = filter ? `ps aux | head -1 && ps aux | grep -i "${filter}" | grep -v grep` : 'ps aux';
56
+ return await run(cmd);
57
+ },
58
+
59
+ kill_process: async (params) => {
60
+ if (params.pid) {
61
+ return await run(`kill ${params.pid}`);
62
+ }
63
+ if (params.name) {
64
+ return await run(`pkill -f "${params.name}"`);
65
+ }
66
+ return { error: 'Provide either pid or name' };
67
+ },
68
+
69
+ service_control: async (params) => {
70
+ const { service, action } = params;
71
+ return await run(`systemctl ${action} ${service}`);
72
+ },
73
+ };
@@ -20,6 +20,15 @@ const DEFAULTS = {
20
20
  telegram: {
21
21
  allowed_users: [],
22
22
  },
23
+ claude_code: {
24
+ max_turns: 50,
25
+ timeout_seconds: 600,
26
+ workspace_dir: null, // defaults to ~/.kernelbot/workspaces
27
+ },
28
+ github: {
29
+ default_branch: 'main',
30
+ default_org: null,
31
+ },
23
32
  security: {
24
33
  blocked_paths: [
25
34
  '/etc/shadow',
@@ -163,6 +172,10 @@ export function loadConfig() {
163
172
  if (process.env.TELEGRAM_BOT_TOKEN) {
164
173
  config.telegram.bot_token = process.env.TELEGRAM_BOT_TOKEN;
165
174
  }
175
+ if (process.env.GITHUB_TOKEN) {
176
+ if (!config.github) config.github = {};
177
+ config.github.token = process.env.GITHUB_TOKEN;
178
+ }
166
179
 
167
180
  return config;
168
181
  }
@@ -14,6 +14,24 @@ const LOGO = `
14
14
  export function showLogo() {
15
15
  console.log(chalk.cyan(LOGO));
16
16
  console.log(chalk.dim(' AI Engineering Agent\n'));
17
+ console.log(
18
+ boxen(
19
+ chalk.yellow.bold('WARNING') +
20
+ chalk.yellow(
21
+ '\n\nKernelBot has full access to your operating system.\n' +
22
+ 'It can execute commands, read/write files, manage processes,\n' +
23
+ 'and interact with external services on your behalf.\n\n' +
24
+ 'Only run this on machines you control.\n' +
25
+ 'Set allowed_users in config.yaml to restrict access.',
26
+ ),
27
+ {
28
+ padding: 1,
29
+ borderStyle: 'round',
30
+ borderColor: 'yellow',
31
+ },
32
+ ),
33
+ );
34
+ console.log('');
17
35
  }
18
36
 
19
37
  export async function showStartupCheck(label, checkFn) {