kernelbot 1.0.16 → 1.0.18

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.16",
3
+ "version": "1.0.18",
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
@@ -9,13 +9,11 @@ function ensureClaudeCodeSetup() {
9
9
  const claudeJson = join(homedir(), '.claude.json');
10
10
  const claudeDir = join(homedir(), '.claude');
11
11
 
12
- // Create ~/.claude/ directory if missing
13
12
  if (!existsSync(claudeDir)) {
14
13
  mkdirSync(claudeDir, { recursive: true });
15
14
  logger.info('Created ~/.claude/ directory');
16
15
  }
17
16
 
18
- // Create ~/.claude.json with onboarding completed if missing
19
17
  if (!existsSync(claudeJson)) {
20
18
  const defaults = {
21
19
  hasCompletedOnboarding: true,
@@ -27,42 +25,112 @@ function ensureClaudeCodeSetup() {
27
25
  }
28
26
  }
29
27
 
30
- function parseStreamEvent(line, onOutput, logger) {
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) {
31
80
  let event;
32
81
  try {
33
82
  event = JSON.parse(line);
34
83
  } catch {
35
- return; // skip non-JSON lines
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;
36
90
  }
37
91
 
38
- // Assistant text message
39
- if (event.type === 'message' && event.role === 'assistant') {
40
- const texts = (event.content || [])
41
- .filter((b) => b.type === 'text' && b.text?.trim())
42
- .map((b) => b.text.trim());
43
- if (texts.length > 0) {
44
- const msg = texts.join('\n');
45
- logger.info(`Claude Code: ${msg.slice(0, 200)}`);
46
- if (onOutput) onOutput(`💬 *Claude Code:*\n${msg}`).catch(() => {});
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(() => {});
47
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;
48
109
  }
49
110
 
50
- // Tool use
51
- if (event.type === 'tool_use') {
52
- const name = event.name || 'unknown';
53
- const input = event.input || {};
54
- const summary = input.command || input.file_path || input.pattern || input.query || JSON.stringify(input).slice(0, 100);
55
- logger.info(`Claude Code tool: ${name}: ${String(summary).slice(0, 150)}`);
56
- if (onOutput) onOutput(`🔨 \`${name}: ${String(summary).slice(0, 150)}\``).catch(() => {});
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;
57
119
  }
58
120
 
59
- // Result
60
- if (event.type === 'result') {
61
- const status = event.status || 'done';
62
- const duration = event.duration_ms ? `${(event.duration_ms / 1000).toFixed(1)}s` : '';
63
- logger.info(`Claude Code finished: ${status} ${duration}`);
64
- if (onOutput) onOutput(`✅ Claude Code finished (${status}${duration ? ` in ${duration}` : ''})`).catch(() => {});
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;
65
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;
66
134
  }
67
135
 
68
136
  export class ClaudeCodeSpawner {
@@ -76,10 +144,10 @@ export class ClaudeCodeSpawner {
76
144
  const logger = getLogger();
77
145
  const turns = maxTurns || this.maxTurns;
78
146
 
79
- // Auto-setup Claude Code if not configured
80
147
  ensureClaudeCodeSetup();
81
148
 
82
- logger.info(`Spawning Claude Code in ${workingDirectory}`);
149
+ logger.info(`Spawning Claude Code in ${workingDirectory}${this.model ? ` (model: ${this.model})` : ''}`);
150
+ if (onOutput) onOutput(`⏳ Starting Claude Code...`).catch(() => {});
83
151
 
84
152
  return new Promise((resolve, reject) => {
85
153
  const args = [
@@ -106,9 +174,8 @@ export class ClaudeCodeSpawner {
106
174
  child.stdout.on('data', (data) => {
107
175
  buffer += data.toString();
108
176
 
109
- // Process complete JSON lines
110
177
  const lines = buffer.split('\n');
111
- buffer = lines.pop(); // keep incomplete line
178
+ buffer = lines.pop();
112
179
 
113
180
  for (const line of lines) {
114
181
  const trimmed = line.trim();
@@ -116,44 +183,52 @@ export class ClaudeCodeSpawner {
116
183
 
117
184
  fullOutput += trimmed + '\n';
118
185
 
119
- // Extract final result text
186
+ // Try to extract result text
120
187
  try {
121
188
  const event = JSON.parse(trimmed);
122
- if (event.type === 'result' && event.result) {
123
- resultText = event.result;
189
+ if (event.type === 'result') {
190
+ resultText = event.result || resultText;
124
191
  }
125
192
  } catch {}
126
193
 
127
- parseStreamEvent(trimmed, onOutput, logger);
194
+ processEvent(trimmed, onOutput, logger);
128
195
  }
129
196
  });
130
197
 
131
198
  child.stderr.on('data', (data) => {
132
- 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)}`);
133
203
  });
134
204
 
135
205
  const timer = setTimeout(() => {
136
206
  child.kill('SIGTERM');
207
+ if (onOutput) onOutput(`⏰ Claude Code timed out after ${this.timeout / 1000}s`).catch(() => {});
137
208
  reject(new Error(`Claude Code timed out after ${this.timeout / 1000}s`));
138
209
  }, this.timeout);
139
210
 
140
211
  child.on('close', (code) => {
141
212
  clearTimeout(timer);
142
213
 
143
- // Process remaining buffer
144
214
  if (buffer.trim()) {
145
215
  fullOutput += buffer.trim();
146
216
  try {
147
217
  const event = JSON.parse(buffer.trim());
148
- if (event.type === 'result' && event.result) {
149
- resultText = event.result;
218
+ if (event.type === 'result') {
219
+ resultText = event.result || resultText;
150
220
  }
151
221
  } catch {}
152
- parseStreamEvent(buffer.trim(), onOutput, logger);
222
+ processEvent(buffer.trim(), onOutput, logger);
153
223
  }
154
224
 
155
- if (code !== 0 && !fullOutput) {
156
- reject(new Error(`Claude Code exited with code ${code}: ${stderr}`));
225
+ logger.info(`Claude Code exited with code ${code} | stdout: ${fullOutput.length} chars | stderr: ${stderr.length} chars`);
226
+
227
+ if (code !== 0) {
228
+ const errMsg = stderr.trim() || fullOutput.trim() || `exited with code ${code}`;
229
+ logger.error(`Claude Code failed: ${errMsg.slice(0, 500)}`);
230
+ if (onOutput) onOutput(`❌ Claude Code failed (exit ${code}):\n\`\`\`\n${errMsg.slice(0, 400)}\n\`\`\``).catch(() => {});
231
+ reject(new Error(`Claude Code exited with code ${code}: ${errMsg.slice(0, 500)}`));
157
232
  } else {
158
233
  resolve({
159
234
  output: resultText || fullOutput.trim(),
@@ -1,4 +1,7 @@
1
+ import { existsSync } from 'fs';
2
+ import { resolve } from 'path';
1
3
  import { ClaudeCodeSpawner } from '../coder.js';
4
+ import { getLogger } from '../utils/logger.js';
2
5
 
3
6
  let spawner = null;
4
7
 
@@ -35,16 +38,36 @@ export const definitions = [
35
38
 
36
39
  export const handlers = {
37
40
  spawn_claude_code: async (params, context) => {
41
+ const logger = getLogger();
42
+ const onUpdate = context.onUpdate || null;
43
+ const dir = resolve(params.working_directory);
44
+
45
+ // Validate directory exists
46
+ if (!existsSync(dir)) {
47
+ const msg = `Directory not found: ${dir}`;
48
+ logger.error(`spawn_claude_code: ${msg}`);
49
+ if (onUpdate) onUpdate(`❌ ${msg}`).catch(() => {});
50
+ return { error: msg };
51
+ }
52
+
38
53
  try {
39
54
  const coder = getSpawner(context.config);
40
55
  const result = await coder.run({
41
- workingDirectory: params.working_directory,
56
+ workingDirectory: dir,
42
57
  prompt: params.prompt,
43
58
  maxTurns: params.max_turns,
44
- onOutput: context.onUpdate || null,
59
+ onOutput: onUpdate,
45
60
  });
61
+
62
+ // Show stderr if any
63
+ if (result.stderr && onUpdate) {
64
+ onUpdate(`⚠️ Claude Code stderr:\n\`\`\`\n${result.stderr.slice(0, 500)}\n\`\`\``).catch(() => {});
65
+ }
66
+
46
67
  return { success: true, output: result.output };
47
68
  } catch (err) {
69
+ logger.error(`spawn_claude_code failed: ${err.message}`);
70
+ if (onUpdate) onUpdate(`❌ Claude Code error: ${err.message}`).catch(() => {});
48
71
  return { error: err.message };
49
72
  }
50
73
  },
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
  });