kernelbot 1.0.15 → 1.0.17

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.15",
3
+ "version": "1.0.17",
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/coder.js CHANGED
@@ -1,6 +1,138 @@
1
1
  import { spawn } from 'child_process';
2
+ import { existsSync, writeFileSync, mkdirSync } from 'fs';
3
+ import { join } from 'path';
4
+ import { homedir } from 'os';
2
5
  import { getLogger } from './utils/logger.js';
3
6
 
7
+ function ensureClaudeCodeSetup() {
8
+ const logger = getLogger();
9
+ const claudeJson = join(homedir(), '.claude.json');
10
+ const claudeDir = join(homedir(), '.claude');
11
+
12
+ if (!existsSync(claudeDir)) {
13
+ mkdirSync(claudeDir, { recursive: true });
14
+ logger.info('Created ~/.claude/ directory');
15
+ }
16
+
17
+ if (!existsSync(claudeJson)) {
18
+ const defaults = {
19
+ hasCompletedOnboarding: true,
20
+ theme: 'dark',
21
+ shiftEnterKeyBindingInstalled: true,
22
+ };
23
+ writeFileSync(claudeJson, JSON.stringify(defaults, null, 2));
24
+ logger.info('Created ~/.claude.json with default settings (skipping setup wizard)');
25
+ }
26
+ }
27
+
28
+ function extractText(event) {
29
+ // Try to find text in various possible event structures
30
+ // Format 1: { type: "message", role: "assistant", content: [{ type: "text", text: "..." }] }
31
+ // Format 2: { type: "assistant", message: { content: [{ type: "text", text: "..." }] } }
32
+ // Format 3: { type: "assistant", content: [{ type: "text", text: "..." }] }
33
+
34
+ const contentSources = [
35
+ event.content,
36
+ event.message?.content,
37
+ ];
38
+
39
+ for (const content of contentSources) {
40
+ if (Array.isArray(content)) {
41
+ const texts = content
42
+ .filter((b) => b.type === 'text' && b.text?.trim())
43
+ .map((b) => b.text.trim());
44
+ if (texts.length > 0) return texts.join('\n');
45
+ }
46
+ }
47
+
48
+ // Direct text field
49
+ if (event.text?.trim()) return event.text.trim();
50
+ if (event.result?.trim()) return event.result.trim();
51
+
52
+ return null;
53
+ }
54
+
55
+ function extractToolUse(event) {
56
+ // Format 1: { type: "tool_use", name: "Bash", input: { command: "..." } }
57
+ // Format 2: content block with type tool_use
58
+ if (event.name && event.input) {
59
+ const input = event.input;
60
+ const summary = input.command || input.file_path || input.pattern || input.query || input.content?.slice(0, 80) || JSON.stringify(input).slice(0, 100);
61
+ return { name: event.name, summary: String(summary).slice(0, 150) };
62
+ }
63
+
64
+ // Check inside content array
65
+ const content = event.content || event.message?.content;
66
+ if (Array.isArray(content)) {
67
+ for (const block of content) {
68
+ if (block.type === 'tool_use' && block.name) {
69
+ const input = block.input || {};
70
+ const summary = input.command || input.file_path || input.pattern || input.query || JSON.stringify(input).slice(0, 100);
71
+ return { name: block.name, summary: String(summary).slice(0, 150) };
72
+ }
73
+ }
74
+ }
75
+
76
+ return null;
77
+ }
78
+
79
+ function processEvent(line, onOutput, logger) {
80
+ let event;
81
+ try {
82
+ event = JSON.parse(line);
83
+ } catch {
84
+ // Not JSON — send raw text if it looks meaningful
85
+ if (line.trim() && line.length > 3 && onOutput) {
86
+ logger.info(`Claude Code (raw): ${line.slice(0, 200)}`);
87
+ onOutput(`📟 ${line.trim()}`).catch(() => {});
88
+ }
89
+ return null;
90
+ }
91
+
92
+ const type = event.type || '';
93
+
94
+ // Assistant thinking/text
95
+ if (type === 'message' || type === 'assistant') {
96
+ const text = extractText(event);
97
+ if (text) {
98
+ logger.info(`Claude Code: ${text.slice(0, 200)}`);
99
+ if (onOutput) onOutput(`💬 *Claude Code:*\n${text}`).catch(() => {});
100
+ }
101
+
102
+ // Also check for tool_use inside the same message
103
+ const tool = extractToolUse(event);
104
+ if (tool) {
105
+ logger.info(`Claude Code tool: ${tool.name}: ${tool.summary}`);
106
+ if (onOutput) onOutput(`🔨 \`${tool.name}: ${tool.summary}\``).catch(() => {});
107
+ }
108
+ return event;
109
+ }
110
+
111
+ // Standalone tool use event
112
+ if (type === 'tool_use') {
113
+ const tool = extractToolUse(event);
114
+ if (tool) {
115
+ logger.info(`Claude Code tool: ${tool.name}: ${tool.summary}`);
116
+ if (onOutput) onOutput(`🔨 \`${tool.name}: ${tool.summary}\``).catch(() => {});
117
+ }
118
+ return event;
119
+ }
120
+
121
+ // Result / completion
122
+ if (type === 'result') {
123
+ const status = event.status || event.subtype || 'done';
124
+ const duration = event.duration_ms ? ` in ${(event.duration_ms / 1000).toFixed(1)}s` : '';
125
+ const cost = event.cost_usd ? ` ($${event.cost_usd.toFixed(3)})` : '';
126
+ logger.info(`Claude Code finished: ${status}${duration}${cost}`);
127
+ if (onOutput) onOutput(`✅ Claude Code finished (${status}${duration}${cost})`).catch(() => {});
128
+ return event;
129
+ }
130
+
131
+ // Log any other event type for debugging
132
+ logger.debug(`Claude Code event [${type}]: ${JSON.stringify(event).slice(0, 300)}`);
133
+ return event;
134
+ }
135
+
4
136
  export class ClaudeCodeSpawner {
5
137
  constructor(config) {
6
138
  this.maxTurns = config.claude_code?.max_turns || 50;
@@ -12,10 +144,18 @@ export class ClaudeCodeSpawner {
12
144
  const logger = getLogger();
13
145
  const turns = maxTurns || this.maxTurns;
14
146
 
15
- logger.info(`Spawning Claude Code in ${workingDirectory}`);
147
+ ensureClaudeCodeSetup();
148
+
149
+ logger.info(`Spawning Claude Code in ${workingDirectory}${this.model ? ` (model: ${this.model})` : ''}`);
150
+ if (onOutput) onOutput(`⏳ Starting Claude Code...`).catch(() => {});
16
151
 
17
152
  return new Promise((resolve, reject) => {
18
- const args = ['-p', prompt, '--max-turns', String(turns), '--output-format', 'text'];
153
+ const args = [
154
+ '-p', prompt,
155
+ '--max-turns', String(turns),
156
+ '--output-format', 'stream-json',
157
+ '--dangerously-skip-permissions',
158
+ ];
19
159
  if (this.model) {
20
160
  args.push('--model', this.model);
21
161
  }
@@ -26,50 +166,67 @@ export class ClaudeCodeSpawner {
26
166
  stdio: ['ignore', 'pipe', 'pipe'],
27
167
  });
28
168
 
29
- let stdout = '';
169
+ let fullOutput = '';
30
170
  let stderr = '';
31
171
  let buffer = '';
172
+ let resultText = '';
32
173
 
33
174
  child.stdout.on('data', (data) => {
34
- const chunk = data.toString();
35
- stdout += chunk;
36
- buffer += chunk;
175
+ buffer += data.toString();
37
176
 
38
- // Stream output in meaningful chunks (split on newlines)
39
177
  const lines = buffer.split('\n');
40
- buffer = lines.pop(); // keep incomplete line in buffer
41
-
42
- if (lines.length > 0 && onOutput) {
43
- const text = lines.join('\n').trim();
44
- if (text) {
45
- logger.info(`Claude Code output: ${text.slice(0, 200)}`);
46
- onOutput(text).catch(() => {});
47
- }
178
+ buffer = lines.pop();
179
+
180
+ for (const line of lines) {
181
+ const trimmed = line.trim();
182
+ if (!trimmed) continue;
183
+
184
+ fullOutput += trimmed + '\n';
185
+
186
+ // Try to extract result text
187
+ try {
188
+ const event = JSON.parse(trimmed);
189
+ if (event.type === 'result') {
190
+ resultText = event.result || resultText;
191
+ }
192
+ } catch {}
193
+
194
+ processEvent(trimmed, onOutput, logger);
48
195
  }
49
196
  });
50
197
 
51
198
  child.stderr.on('data', (data) => {
52
- stderr += data.toString();
199
+ const chunk = data.toString();
200
+ stderr += chunk;
201
+ // Forward stderr too — might contain useful info
202
+ logger.warn(`Claude Code stderr: ${chunk.trim().slice(0, 200)}`);
53
203
  });
54
204
 
55
205
  const timer = setTimeout(() => {
56
206
  child.kill('SIGTERM');
207
+ if (onOutput) onOutput(`⏰ Claude Code timed out after ${this.timeout / 1000}s`).catch(() => {});
57
208
  reject(new Error(`Claude Code timed out after ${this.timeout / 1000}s`));
58
209
  }, this.timeout);
59
210
 
60
211
  child.on('close', (code) => {
61
212
  clearTimeout(timer);
62
213
 
63
- // Flush remaining buffer
64
- if (buffer.trim() && onOutput) {
65
- onOutput(buffer.trim()).catch(() => {});
214
+ if (buffer.trim()) {
215
+ fullOutput += buffer.trim();
216
+ try {
217
+ const event = JSON.parse(buffer.trim());
218
+ if (event.type === 'result') {
219
+ resultText = event.result || resultText;
220
+ }
221
+ } catch {}
222
+ processEvent(buffer.trim(), onOutput, logger);
66
223
  }
67
224
 
68
- if (code !== 0 && !stdout) {
225
+ if (code !== 0 && !fullOutput) {
69
226
  reject(new Error(`Claude Code exited with code ${code}: ${stderr}`));
70
227
  } else {
71
228
  resolve({
72
- output: stdout.trim(),
229
+ output: resultText || fullOutput.trim(),
73
230
  stderr: stderr.trim(),
74
231
  exitCode: code,
75
232
  });
@@ -41,9 +41,7 @@ export const handlers = {
41
41
  workingDirectory: params.working_directory,
42
42
  prompt: params.prompt,
43
43
  maxTurns: params.max_turns,
44
- onOutput: context.onUpdate
45
- ? (text) => context.onUpdate(`📟 \`Claude Code:\`\n${text}`)
46
- : null,
44
+ onOutput: context.onUpdate || null,
47
45
  });
48
46
  return { success: true, output: result.output };
49
47
  } catch (err) {
package/src/tools/os.js CHANGED
@@ -129,11 +129,22 @@ export const handlers = {
129
129
  if (error && error.killed) {
130
130
  return res({ error: `Command timed out after ${timeout_seconds}s` });
131
131
  }
132
- res({
132
+ const result = {
133
133
  stdout: stdout || '',
134
134
  stderr: stderr || '',
135
135
  exit_code: error ? error.code ?? 1 : 0,
136
- });
136
+ };
137
+
138
+ // Send output summary to Telegram
139
+ if (context.onUpdate) {
140
+ const output = (result.stdout || result.stderr || '').trim();
141
+ if (output) {
142
+ const preview = output.length > 300 ? output.slice(0, 300) + '...' : output;
143
+ context.onUpdate(`📋 \`${command.slice(0, 60)}\`\n\`\`\`\n${preview}\n\`\`\``).catch(() => {});
144
+ }
145
+ }
146
+
147
+ res(result);
137
148
  },
138
149
  );
139
150
  });