kernelbot 1.0.5 → 1.0.8
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/README.md +4 -0
- package/package.json +1 -1
- package/src/agent.js +136 -76
- package/src/tools/git.js +34 -2
- package/src/utils/config.js +56 -0
- package/src/utils/display.js +18 -0
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
|
package/package.json
CHANGED
package/src/agent.js
CHANGED
|
@@ -2,6 +2,7 @@ import Anthropic from '@anthropic-ai/sdk';
|
|
|
2
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
|
+
import { getMissingCredential, saveCredential } from './utils/config.js';
|
|
5
6
|
|
|
6
7
|
export class Agent {
|
|
7
8
|
constructor({ config, conversationManager }) {
|
|
@@ -9,69 +10,23 @@ export class Agent {
|
|
|
9
10
|
this.conversationManager = conversationManager;
|
|
10
11
|
this.client = new Anthropic({ apiKey: config.anthropic.api_key });
|
|
11
12
|
this.systemPrompt = getSystemPrompt(config);
|
|
12
|
-
this.
|
|
13
|
+
this._pending = new Map(); // chatId -> pending state
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
async processMessage(chatId, userMessage, user) {
|
|
16
17
|
const logger = getLogger();
|
|
17
18
|
|
|
18
|
-
// Handle pending confirmation
|
|
19
|
-
const pending = this.
|
|
19
|
+
// Handle pending responses (confirmation or credential)
|
|
20
|
+
const pending = this._pending.get(chatId);
|
|
20
21
|
if (pending) {
|
|
21
|
-
this.
|
|
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
|
-
}
|
|
22
|
+
this._pending.delete(chatId);
|
|
52
23
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
type: 'tool_result',
|
|
57
|
-
tool_use_id: block.id,
|
|
58
|
-
content: JSON.stringify(r),
|
|
59
|
-
});
|
|
60
|
-
}
|
|
24
|
+
if (pending.type === 'credential') {
|
|
25
|
+
return await this._handleCredentialResponse(chatId, userMessage, user, pending);
|
|
26
|
+
}
|
|
61
27
|
|
|
62
|
-
|
|
63
|
-
|
|
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);
|
|
28
|
+
if (pending.type === 'confirmation') {
|
|
29
|
+
return await this._handleConfirmationResponse(chatId, userMessage, user, pending);
|
|
75
30
|
}
|
|
76
31
|
}
|
|
77
32
|
|
|
@@ -86,6 +41,122 @@ export class Agent {
|
|
|
86
41
|
return await this._runLoop(chatId, messages, user, 0, max_tool_depth);
|
|
87
42
|
}
|
|
88
43
|
|
|
44
|
+
async _handleCredentialResponse(chatId, userMessage, user, pending) {
|
|
45
|
+
const logger = getLogger();
|
|
46
|
+
const value = userMessage.trim();
|
|
47
|
+
|
|
48
|
+
if (value.toLowerCase() === 'skip' || value.toLowerCase() === 'cancel') {
|
|
49
|
+
logger.info(`User skipped credential: ${pending.credential.envKey}`);
|
|
50
|
+
pending.toolResults.push({
|
|
51
|
+
type: 'tool_result',
|
|
52
|
+
tool_use_id: pending.block.id,
|
|
53
|
+
content: JSON.stringify({ error: `${pending.credential.label} not provided. Operation skipped.` }),
|
|
54
|
+
});
|
|
55
|
+
return await this._resumeAfterPause(chatId, user, pending);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Save the credential
|
|
59
|
+
saveCredential(this.config, pending.credential.envKey, value);
|
|
60
|
+
logger.info(`Saved credential: ${pending.credential.envKey}`);
|
|
61
|
+
|
|
62
|
+
// Now execute the original tool
|
|
63
|
+
const result = await executeTool(pending.block.name, pending.block.input, {
|
|
64
|
+
config: this.config,
|
|
65
|
+
user,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
pending.toolResults.push({
|
|
69
|
+
type: 'tool_result',
|
|
70
|
+
tool_use_id: pending.block.id,
|
|
71
|
+
content: JSON.stringify(result),
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
return await this._resumeAfterPause(chatId, user, pending);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async _handleConfirmationResponse(chatId, userMessage, user, pending) {
|
|
78
|
+
const logger = getLogger();
|
|
79
|
+
const lower = userMessage.toLowerCase().trim();
|
|
80
|
+
|
|
81
|
+
if (lower === 'yes' || lower === 'y' || lower === 'confirm') {
|
|
82
|
+
logger.info(`User confirmed dangerous tool: ${pending.block.name}`);
|
|
83
|
+
const result = await executeTool(pending.block.name, pending.block.input, pending.context);
|
|
84
|
+
|
|
85
|
+
pending.toolResults.push({
|
|
86
|
+
type: 'tool_result',
|
|
87
|
+
tool_use_id: pending.block.id,
|
|
88
|
+
content: JSON.stringify(result),
|
|
89
|
+
});
|
|
90
|
+
} else {
|
|
91
|
+
logger.info(`User denied dangerous tool: ${pending.block.name}`);
|
|
92
|
+
pending.toolResults.push({
|
|
93
|
+
type: 'tool_result',
|
|
94
|
+
tool_use_id: pending.block.id,
|
|
95
|
+
content: JSON.stringify({ error: 'User denied this operation.' }),
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return await this._resumeAfterPause(chatId, user, pending);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async _resumeAfterPause(chatId, user, pending) {
|
|
103
|
+
// Process remaining blocks
|
|
104
|
+
for (const block of pending.remainingBlocks) {
|
|
105
|
+
if (block.type !== 'tool_use') continue;
|
|
106
|
+
|
|
107
|
+
const pauseMsg = await this._checkPause(chatId, block, user, pending.toolResults, pending.remainingBlocks.filter((b) => b !== block), pending.messages);
|
|
108
|
+
if (pauseMsg) return pauseMsg;
|
|
109
|
+
|
|
110
|
+
const r = await executeTool(block.name, block.input, { config: this.config, user });
|
|
111
|
+
pending.toolResults.push({
|
|
112
|
+
type: 'tool_result',
|
|
113
|
+
tool_use_id: block.id,
|
|
114
|
+
content: JSON.stringify(r),
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
pending.messages.push({ role: 'user', content: pending.toolResults });
|
|
119
|
+
const { max_tool_depth } = this.config.anthropic;
|
|
120
|
+
return await this._runLoop(chatId, pending.messages, user, 0, max_tool_depth);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
_checkPause(chatId, block, user, toolResults, remainingBlocks, messages) {
|
|
124
|
+
const logger = getLogger();
|
|
125
|
+
|
|
126
|
+
// Check missing credentials first
|
|
127
|
+
const missing = getMissingCredential(block.name, this.config);
|
|
128
|
+
if (missing) {
|
|
129
|
+
logger.warn(`Missing credential for ${block.name}: ${missing.envKey}`);
|
|
130
|
+
this._pending.set(chatId, {
|
|
131
|
+
type: 'credential',
|
|
132
|
+
block,
|
|
133
|
+
credential: missing,
|
|
134
|
+
context: { config: this.config, user },
|
|
135
|
+
toolResults,
|
|
136
|
+
remainingBlocks,
|
|
137
|
+
messages,
|
|
138
|
+
});
|
|
139
|
+
return `🔑 **${missing.label}** is required for this action.\n\nPlease send your token now (it will be saved to \`~/.kernelbot/.env\`).\n\nOr reply **skip** to cancel.`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Check dangerous operation confirmation
|
|
143
|
+
const dangerLabel = checkConfirmation(block.name, block.input, this.config);
|
|
144
|
+
if (dangerLabel) {
|
|
145
|
+
logger.warn(`Dangerous tool detected: ${block.name} — ${dangerLabel}`);
|
|
146
|
+
this._pending.set(chatId, {
|
|
147
|
+
type: 'confirmation',
|
|
148
|
+
block,
|
|
149
|
+
context: { config: this.config, user },
|
|
150
|
+
toolResults,
|
|
151
|
+
remainingBlocks,
|
|
152
|
+
messages,
|
|
153
|
+
});
|
|
154
|
+
return `⚠️ This action will **${dangerLabel}**.\n\n\`${block.name}\`: \`${JSON.stringify(block.input)}\`\n\nConfirm? (yes/no)`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
|
|
89
160
|
async _runLoop(chatId, messages, user, startDepth, maxDepth) {
|
|
90
161
|
const logger = getLogger();
|
|
91
162
|
const { model, max_tokens, temperature } = this.config.anthropic;
|
|
@@ -121,22 +192,16 @@ export class Agent {
|
|
|
121
192
|
for (let i = 0; i < toolUseBlocks.length; i++) {
|
|
122
193
|
const block = toolUseBlocks[i];
|
|
123
194
|
|
|
124
|
-
// Check if
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
}
|
|
195
|
+
// Check if we need to pause (missing cred or dangerous action)
|
|
196
|
+
const pauseMsg = this._checkPause(
|
|
197
|
+
chatId,
|
|
198
|
+
block,
|
|
199
|
+
user,
|
|
200
|
+
toolResults,
|
|
201
|
+
toolUseBlocks.slice(i + 1),
|
|
202
|
+
messages,
|
|
203
|
+
);
|
|
204
|
+
if (pauseMsg) return pauseMsg;
|
|
140
205
|
|
|
141
206
|
logger.info(`Tool call: ${block.name}`);
|
|
142
207
|
|
|
@@ -175,9 +240,4 @@ export class Agent {
|
|
|
175
240
|
this.conversationManager.addMessage(chatId, 'assistant', depthWarning);
|
|
176
241
|
return depthWarning;
|
|
177
242
|
}
|
|
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
|
-
}
|
|
183
243
|
}
|
package/src/tools/git.js
CHANGED
|
@@ -9,6 +9,24 @@ function getWorkspaceDir(config) {
|
|
|
9
9
|
return dir;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
function injectToken(url, config) {
|
|
13
|
+
const token = config.github?.token || process.env.GITHUB_TOKEN;
|
|
14
|
+
if (!token) return url;
|
|
15
|
+
|
|
16
|
+
// Inject token into HTTPS GitHub URLs for auth-free push/pull
|
|
17
|
+
try {
|
|
18
|
+
const parsed = new URL(url);
|
|
19
|
+
if (parsed.hostname === 'github.com' && parsed.protocol === 'https:') {
|
|
20
|
+
parsed.username = token;
|
|
21
|
+
parsed.password = 'x-oauth-basic';
|
|
22
|
+
return parsed.toString();
|
|
23
|
+
}
|
|
24
|
+
} catch {
|
|
25
|
+
// Not a parseable URL (e.g. org/repo shorthand before expansion)
|
|
26
|
+
}
|
|
27
|
+
return url;
|
|
28
|
+
}
|
|
29
|
+
|
|
12
30
|
export const definitions = [
|
|
13
31
|
{
|
|
14
32
|
name: 'git_clone',
|
|
@@ -88,12 +106,15 @@ export const handlers = {
|
|
|
88
106
|
url = `https://github.com/${repo}.git`;
|
|
89
107
|
}
|
|
90
108
|
|
|
109
|
+
// Inject GitHub token for authenticated clone (enables push later)
|
|
110
|
+
const authUrl = injectToken(url, context.config);
|
|
111
|
+
|
|
91
112
|
const repoName = dest || repo.split('/').pop().replace('.git', '');
|
|
92
113
|
const targetDir = join(workspaceDir, repoName);
|
|
93
114
|
|
|
94
115
|
try {
|
|
95
116
|
const git = simpleGit();
|
|
96
|
-
await git.clone(
|
|
117
|
+
await git.clone(authUrl, targetDir);
|
|
97
118
|
return { success: true, path: targetDir };
|
|
98
119
|
} catch (err) {
|
|
99
120
|
return { error: err.message };
|
|
@@ -127,10 +148,21 @@ export const handlers = {
|
|
|
127
148
|
}
|
|
128
149
|
},
|
|
129
150
|
|
|
130
|
-
git_push: async (params) => {
|
|
151
|
+
git_push: async (params, context) => {
|
|
131
152
|
const { dir, force = false } = params;
|
|
132
153
|
try {
|
|
133
154
|
const git = simpleGit(dir);
|
|
155
|
+
|
|
156
|
+
// Ensure remote URL has auth token for push
|
|
157
|
+
const remotes = await git.getRemotes(true);
|
|
158
|
+
const origin = remotes.find((r) => r.name === 'origin');
|
|
159
|
+
if (origin) {
|
|
160
|
+
const authUrl = injectToken(origin.refs.push || origin.refs.fetch, context.config);
|
|
161
|
+
if (authUrl !== (origin.refs.push || origin.refs.fetch)) {
|
|
162
|
+
await git.remote(['set-url', 'origin', authUrl]);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
134
166
|
const branch = (await git.branchLocal()).current;
|
|
135
167
|
const options = force ? ['--force'] : [];
|
|
136
168
|
await git.push('origin', branch, options);
|
package/src/utils/config.js
CHANGED
|
@@ -184,3 +184,59 @@ export async function loadConfigInteractive() {
|
|
|
184
184
|
const config = loadConfig();
|
|
185
185
|
return await promptForMissing(config);
|
|
186
186
|
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Save a credential to ~/.kernelbot/.env and update the live config object.
|
|
190
|
+
* Called at runtime when a user provides a missing token via Telegram.
|
|
191
|
+
*/
|
|
192
|
+
export function saveCredential(config, envKey, value) {
|
|
193
|
+
const configDir = getConfigDir();
|
|
194
|
+
mkdirSync(configDir, { recursive: true });
|
|
195
|
+
const envPath = join(configDir, '.env');
|
|
196
|
+
|
|
197
|
+
let content = '';
|
|
198
|
+
if (existsSync(envPath)) {
|
|
199
|
+
content = readFileSync(envPath, 'utf-8').trimEnd() + '\n';
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const regex = new RegExp(`^${envKey}=.*$`, 'm');
|
|
203
|
+
if (regex.test(content)) {
|
|
204
|
+
content = content.replace(regex, `${envKey}=${value}`);
|
|
205
|
+
} else {
|
|
206
|
+
content += `${envKey}=${value}\n`;
|
|
207
|
+
}
|
|
208
|
+
writeFileSync(envPath, content);
|
|
209
|
+
|
|
210
|
+
// Update live config
|
|
211
|
+
switch (envKey) {
|
|
212
|
+
case 'GITHUB_TOKEN':
|
|
213
|
+
if (!config.github) config.github = {};
|
|
214
|
+
config.github.token = value;
|
|
215
|
+
break;
|
|
216
|
+
case 'ANTHROPIC_API_KEY':
|
|
217
|
+
config.anthropic.api_key = value;
|
|
218
|
+
break;
|
|
219
|
+
case 'TELEGRAM_BOT_TOKEN':
|
|
220
|
+
config.telegram.bot_token = value;
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Also set in process.env so tools pick it up
|
|
225
|
+
process.env[envKey] = value;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Check which credentials a tool needs and return the missing one, if any.
|
|
230
|
+
*/
|
|
231
|
+
export function getMissingCredential(toolName, config) {
|
|
232
|
+
const githubTools = ['github_create_pr', 'github_get_pr_diff', 'github_post_review', 'github_create_repo', 'github_list_prs', 'git_clone', 'git_push'];
|
|
233
|
+
|
|
234
|
+
if (githubTools.includes(toolName)) {
|
|
235
|
+
const token = config.github?.token || process.env.GITHUB_TOKEN;
|
|
236
|
+
if (!token) {
|
|
237
|
+
return { envKey: 'GITHUB_TOKEN', label: 'GitHub Personal Access Token' };
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return null;
|
|
242
|
+
}
|
package/src/utils/display.js
CHANGED
|
@@ -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) {
|