kernelbot 1.0.6 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kernelbot",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "description": "KernelBot — AI engineering agent with full OS control",
5
5
  "type": "module",
6
6
  "author": "Abdullah Al-Taheri <abdullah@altaheri.me>",
package/src/agent.js CHANGED
@@ -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._pendingConfirmation = new Map(); // chatId -> { block, context }
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 responses
19
- const pending = this._pendingConfirmation.get(chatId);
19
+ // Handle pending responses (confirmation or credential)
20
+ const pending = this._pending.get(chatId);
20
21
  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
- }
22
+ this._pending.delete(chatId);
52
23
 
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
- }
24
+ if (pending.type === 'credential') {
25
+ return await this._handleCredentialResponse(chatId, userMessage, user, pending);
26
+ }
61
27
 
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);
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 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
- }
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(url, targetDir);
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);
@@ -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
+ }