kernelbot 1.0.33 → 1.0.35

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.
Files changed (44) hide show
  1. package/.env.example +11 -0
  2. package/README.md +76 -341
  3. package/bin/kernel.js +134 -15
  4. package/config.example.yaml +2 -1
  5. package/goals.md +20 -0
  6. package/knowledge_base/index.md +11 -0
  7. package/package.json +2 -1
  8. package/src/agent.js +166 -19
  9. package/src/automation/automation-manager.js +16 -0
  10. package/src/automation/automation.js +6 -2
  11. package/src/bot.js +295 -163
  12. package/src/conversation.js +70 -3
  13. package/src/life/engine.js +87 -68
  14. package/src/life/evolution.js +4 -8
  15. package/src/life/improvements.js +2 -6
  16. package/src/life/journal.js +3 -6
  17. package/src/life/memory.js +3 -10
  18. package/src/life/share-queue.js +4 -9
  19. package/src/prompts/orchestrator.js +21 -12
  20. package/src/prompts/persona.md +27 -0
  21. package/src/providers/base.js +51 -8
  22. package/src/providers/google-genai.js +198 -0
  23. package/src/providers/index.js +6 -1
  24. package/src/providers/models.js +6 -2
  25. package/src/providers/openai-compat.js +25 -11
  26. package/src/security/auth.js +38 -1
  27. package/src/services/stt.js +10 -1
  28. package/src/tools/docker.js +37 -15
  29. package/src/tools/git.js +6 -0
  30. package/src/tools/github.js +6 -0
  31. package/src/tools/jira.js +5 -0
  32. package/src/tools/monitor.js +13 -15
  33. package/src/tools/network.js +22 -18
  34. package/src/tools/os.js +37 -2
  35. package/src/tools/process.js +21 -14
  36. package/src/utils/config.js +66 -0
  37. package/src/utils/date.js +19 -0
  38. package/src/utils/display.js +1 -1
  39. package/src/utils/ids.js +12 -0
  40. package/src/utils/shell.js +31 -0
  41. package/src/utils/temporal-awareness.js +199 -0
  42. package/src/utils/timeUtils.js +110 -0
  43. package/src/utils/truncate.js +42 -0
  44. package/src/worker.js +2 -18
@@ -13,16 +13,35 @@ export class BaseProvider {
13
13
  }
14
14
 
15
15
  /**
16
- * Wrap an async LLM call with timeout + single retry on transient errors.
16
+ * Compute retry delay using exponential backoff with full jitter.
17
+ * Formula: random(0, min(MAX_BACKOFF, BASE * 2^attempt))
18
+ * This distributes retries across time and avoids thundering-herd
19
+ * when multiple workers retry simultaneously after a service hiccup.
20
+ *
21
+ * @param {number} attempt - Current attempt (1-indexed)
22
+ * @returns {number} Delay in milliseconds
23
+ */
24
+ _retryDelay(attempt) {
25
+ const BASE_MS = 1000;
26
+ const MAX_BACKOFF_MS = 30_000;
27
+ const ceiling = Math.min(MAX_BACKOFF_MS, BASE_MS * Math.pow(2, attempt));
28
+ return Math.round(Math.random() * ceiling);
29
+ }
30
+
31
+ /**
32
+ * Wrap an async LLM call with timeout + retries on transient errors (up to 3 attempts).
17
33
  * Composes an internal timeout AbortController with an optional external signal
18
34
  * (e.g. worker cancellation). Either aborting will cancel the call.
19
35
  *
36
+ * Uses exponential backoff with full jitter between retries to avoid
37
+ * thundering-herd effects when services recover from outages.
38
+ *
20
39
  * @param {(signal: AbortSignal) => Promise<any>} fn - The API call, receives composed signal
21
40
  * @param {AbortSignal} [externalSignal] - Optional external abort signal
22
41
  * @returns {Promise<any>}
23
42
  */
24
43
  async _callWithResilience(fn, externalSignal) {
25
- for (let attempt = 1; attempt <= 2; attempt++) {
44
+ for (let attempt = 1; attempt <= 3; attempt++) {
26
45
  const ac = new AbortController();
27
46
  const timer = setTimeout(
28
47
  () => ac.abort(new Error(`LLM call timed out after ${this.timeout / 1000}s`)),
@@ -55,8 +74,9 @@ export class BaseProvider {
55
74
  clearTimeout(timer);
56
75
  removeListener?.();
57
76
 
58
- if (attempt < 2 && this._isTransient(err)) {
59
- await new Promise((r) => setTimeout(r, 1500));
77
+ if (attempt < 3 && this._isTransient(err)) {
78
+ const delay = this._retryDelay(attempt);
79
+ await new Promise((r) => setTimeout(r, delay));
60
80
  continue;
61
81
  }
62
82
  throw err;
@@ -66,22 +86,45 @@ export class BaseProvider {
66
86
 
67
87
  /**
68
88
  * Determine if an error is transient and worth retrying.
69
- * Covers connection errors, timeouts, 5xx, and 429 rate limits.
89
+ * Covers connection errors, DNS failures, timeouts, 5xx, and 429 rate limits.
70
90
  */
71
91
  _isTransient(err) {
72
92
  const msg = err?.message || '';
93
+
94
+ // Network-level & connection errors
73
95
  if (
74
96
  msg.includes('Connection error') ||
75
97
  msg.includes('ECONNRESET') ||
98
+ msg.includes('ECONNREFUSED') ||
99
+ msg.includes('ECONNABORTED') ||
100
+ msg.includes('EPIPE') ||
101
+ msg.includes('ENETUNREACH') ||
102
+ msg.includes('EHOSTUNREACH') ||
76
103
  msg.includes('socket hang up') ||
77
104
  msg.includes('ETIMEDOUT') ||
105
+ msg.includes('ENOTFOUND') ||
106
+ msg.includes('EAI_AGAIN') ||
78
107
  msg.includes('fetch failed') ||
79
- msg.includes('timed out')
108
+ msg.includes('timed out') ||
109
+ msg.includes('network socket disconnected') ||
110
+ msg.includes('other side closed')
80
111
  ) {
81
112
  return true;
82
113
  }
83
- const status = err?.status || err?.statusCode;
84
- return (status >= 500 && status < 600) || status === 429;
114
+
115
+ // Check top-level status (Anthropic, OpenAI)
116
+ let status = err?.status || err?.statusCode;
117
+
118
+ // Google SDK nests HTTP status in JSON message — try to extract
119
+ if (!status && msg.startsWith('{')) {
120
+ try {
121
+ const parsed = JSON.parse(msg);
122
+ status = parsed?.error?.code || parsed?.code;
123
+ } catch {}
124
+ }
125
+
126
+ // Anthropic overloaded (529) is also transient
127
+ return (status >= 500 && status < 600) || status === 429 || status === 529;
85
128
  }
86
129
 
87
130
  /**
@@ -0,0 +1,198 @@
1
+ import { GoogleGenAI } from '@google/genai';
2
+ import { BaseProvider } from './base.js';
3
+
4
+ /**
5
+ * Native Google Gemini provider using @google/genai SDK.
6
+ */
7
+ export class GoogleGenaiProvider extends BaseProvider {
8
+ constructor(opts) {
9
+ super(opts);
10
+ this.client = new GoogleGenAI({ apiKey: this.apiKey });
11
+ }
12
+
13
+ // ── Format conversion helpers ──
14
+
15
+ /** Anthropic tool defs → Google functionDeclarations */
16
+ _convertTools(tools) {
17
+ if (!tools || tools.length === 0) return undefined;
18
+ return [
19
+ {
20
+ functionDeclarations: tools.map((t) => ({
21
+ name: t.name,
22
+ description: t.description,
23
+ parameters: t.input_schema,
24
+ })),
25
+ },
26
+ ];
27
+ }
28
+
29
+ /** Anthropic messages → Google contents array */
30
+ _convertMessages(messages) {
31
+ const contents = [];
32
+
33
+ // Build a map of tool_use_id → tool_name from assistant messages
34
+ // so we can resolve function names when converting tool_result blocks
35
+ const toolIdToName = new Map();
36
+ for (const msg of messages) {
37
+ if (msg.role === 'assistant' && Array.isArray(msg.content)) {
38
+ for (const block of msg.content) {
39
+ if (block.type === 'tool_use') {
40
+ toolIdToName.set(block.id, block.name);
41
+ }
42
+ }
43
+ }
44
+ }
45
+
46
+ for (const msg of messages) {
47
+ if (msg.role === 'user') {
48
+ if (typeof msg.content === 'string') {
49
+ contents.push({ role: 'user', parts: [{ text: msg.content }] });
50
+ } else if (Array.isArray(msg.content)) {
51
+ // Check if it's tool results
52
+ if (msg.content[0]?.type === 'tool_result') {
53
+ const parts = msg.content.map((tr) => ({
54
+ functionResponse: {
55
+ name: toolIdToName.get(tr.tool_use_id) || tr.tool_use_id,
56
+ response: {
57
+ result:
58
+ typeof tr.content === 'string' ? tr.content : JSON.stringify(tr.content),
59
+ },
60
+ },
61
+ }));
62
+ contents.push({ role: 'user', parts });
63
+ } else {
64
+ // Text content blocks
65
+ const text = msg.content
66
+ .filter((b) => b.type === 'text')
67
+ .map((b) => b.text)
68
+ .join('\n');
69
+ contents.push({ role: 'user', parts: [{ text: text || '' }] });
70
+ }
71
+ }
72
+ } else if (msg.role === 'assistant') {
73
+ const parts = [];
74
+ if (typeof msg.content === 'string') {
75
+ parts.push({ text: msg.content });
76
+ } else if (Array.isArray(msg.content)) {
77
+ for (const block of msg.content) {
78
+ if (block.type === 'text' && block.text) {
79
+ parts.push({ text: block.text });
80
+ } else if (block.type === 'tool_use') {
81
+ const part = { functionCall: { name: block.name, args: block.input } };
82
+ // Replay thought signature for thinking models
83
+ if (block.thoughtSignature) {
84
+ part.thoughtSignature = block.thoughtSignature;
85
+ }
86
+ parts.push(part);
87
+ }
88
+ }
89
+ }
90
+ if (parts.length > 0) {
91
+ contents.push({ role: 'model', parts });
92
+ }
93
+ }
94
+ }
95
+
96
+ return contents;
97
+ }
98
+
99
+ /** Google response → normalized format with rawContent in Anthropic format */
100
+ _normalizeResponse(response) {
101
+ // Access raw parts to preserve thoughtSignature and avoid SDK warning
102
+ // (response.text logs a warning when there are only functionCall parts)
103
+ const candidate = response.candidates?.[0];
104
+ const parts = candidate?.content?.parts || [];
105
+
106
+ // Extract text from raw parts instead of response.text
107
+ const text = parts
108
+ .filter((p) => p.text)
109
+ .map((p) => p.text)
110
+ .join('\n');
111
+
112
+ const functionCallParts = parts.filter((p) => p.functionCall);
113
+ const toolCalls = functionCallParts.map((p, i) => ({
114
+ id: `toolu_google_${Date.now()}_${i}`,
115
+ name: p.functionCall.name,
116
+ input: p.functionCall.args || {},
117
+ // Preserve thought signature for thinking models (sibling of functionCall)
118
+ ...(p.thoughtSignature && { thoughtSignature: p.thoughtSignature }),
119
+ }));
120
+
121
+ const stopReason = toolCalls.length > 0 ? 'tool_use' : 'end_turn';
122
+
123
+ // Build rawContent in Anthropic format for history consistency
124
+ const rawContent = [];
125
+ if (text) {
126
+ rawContent.push({ type: 'text', text });
127
+ }
128
+ for (const tc of toolCalls) {
129
+ rawContent.push({
130
+ type: 'tool_use',
131
+ id: tc.id,
132
+ name: tc.name,
133
+ input: tc.input,
134
+ ...(tc.thoughtSignature && { thoughtSignature: tc.thoughtSignature }),
135
+ });
136
+ }
137
+
138
+ return { stopReason, text, toolCalls, rawContent };
139
+ }
140
+
141
+ // ── Public API ──
142
+
143
+ async chat({ system, messages, tools, signal }) {
144
+ const config = {
145
+ temperature: this.temperature,
146
+ maxOutputTokens: this.maxTokens,
147
+ };
148
+
149
+ if (system) {
150
+ config.systemInstruction = Array.isArray(system)
151
+ ? system.map((b) => b.text).join('\n')
152
+ : system;
153
+ }
154
+
155
+ const convertedTools = this._convertTools(tools);
156
+ if (convertedTools) {
157
+ config.tools = convertedTools;
158
+ }
159
+
160
+ const contents = this._convertMessages(messages);
161
+
162
+ try {
163
+ return await this._callWithResilience(async (timedSignal) => {
164
+ const response = await this.client.models.generateContent({
165
+ model: this.model,
166
+ contents,
167
+ config: {
168
+ ...config,
169
+ abortSignal: timedSignal,
170
+ httpOptions: { timeout: this.timeout },
171
+ },
172
+ });
173
+ return this._normalizeResponse(response);
174
+ }, signal);
175
+ } catch (err) {
176
+ // Normalize Google SDK error: extract clean message from JSON
177
+ if (err.message?.startsWith('{')) {
178
+ try {
179
+ const parsed = JSON.parse(err.message);
180
+ err.message = parsed?.error?.message || err.message;
181
+ err.status = parsed?.error?.code;
182
+ } catch {}
183
+ }
184
+ throw err;
185
+ }
186
+ }
187
+
188
+ async ping() {
189
+ await this.client.models.generateContent({
190
+ model: this.model,
191
+ contents: 'ping',
192
+ config: {
193
+ maxOutputTokens: 16,
194
+ temperature: 0,
195
+ },
196
+ });
197
+ }
198
+ }
@@ -1,5 +1,6 @@
1
1
  import { AnthropicProvider } from './anthropic.js';
2
2
  import { OpenAICompatProvider } from './openai-compat.js';
3
+ import { GoogleGenaiProvider } from './google-genai.js';
3
4
  import { PROVIDERS } from './models.js';
4
5
 
5
6
  export { PROVIDERS } from './models.js';
@@ -29,7 +30,11 @@ export function createProvider(config) {
29
30
  return new AnthropicProvider(opts);
30
31
  }
31
32
 
32
- // OpenAI, Google, Groq — all use OpenAI-compatible API
33
+ if (provider === 'google') {
34
+ return new GoogleGenaiProvider(opts);
35
+ }
36
+
37
+ // OpenAI, Groq — use OpenAI-compatible API
33
38
  return new OpenAICompatProvider({
34
39
  ...opts,
35
40
  baseUrl: providerDef.baseUrl || undefined,
@@ -32,11 +32,15 @@ export const PROVIDERS = {
32
32
  google: {
33
33
  name: 'Google (Gemini)',
34
34
  envKey: 'GOOGLE_API_KEY',
35
- baseUrl: 'https://generativelanguage.googleapis.com/v1beta/openai/',
36
35
  models: [
36
+ // Gemini 3 series
37
+ { id: 'gemini-3.1-pro-preview', label: 'Gemini 3.1 Pro' },
38
+ { id: 'gemini-3-flash-preview', label: 'Gemini 3 Flash' },
39
+ { id: 'gemini-3-pro-preview', label: 'Gemini 3 Pro' },
40
+ // Gemini 2.5 series
37
41
  { id: 'gemini-2.5-flash', label: 'Gemini 2.5 Flash' },
38
42
  { id: 'gemini-2.5-pro', label: 'Gemini 2.5 Pro' },
39
- { id: 'gemini-2.0-flash', label: 'Gemini 2.0 Flash' },
43
+ { id: 'gemini-2.5-flash-lite', label: 'Gemini 2.5 Flash Lite' },
40
44
  ],
41
45
  },
42
46
  groq: {
@@ -35,12 +35,13 @@ export class OpenAICompatProvider extends BaseProvider {
35
35
  _convertMessages(system, messages) {
36
36
  const out = [];
37
37
 
38
- // System prompt as first message (skip for reasoning models)
39
- if (system && !this.isReasoningModel) {
38
+ // System prompt use 'developer' role for reasoning models, 'system' for others
39
+ if (system) {
40
40
  const systemText = Array.isArray(system)
41
41
  ? system.map((b) => b.text).join('\n')
42
42
  : system;
43
- out.push({ role: 'system', content: systemText });
43
+ const role = this.isReasoningModel ? 'developer' : 'system';
44
+ out.push({ role, content: systemText });
44
45
  }
45
46
 
46
47
  for (const msg of messages) {
@@ -108,11 +109,18 @@ export class OpenAICompatProvider extends BaseProvider {
108
109
 
109
110
  const text = choice.message.content || '';
110
111
 
111
- const toolCalls = (choice.message.tool_calls || []).map((tc) => ({
112
- id: tc.id,
113
- name: tc.function.name,
114
- input: JSON.parse(tc.function.arguments),
115
- }));
112
+ const toolCalls = (choice.message.tool_calls || []).map((tc) => {
113
+ let input = {};
114
+ try {
115
+ input = JSON.parse(tc.function.arguments);
116
+ } catch {
117
+ // LLM returned malformed JSON — use empty object so the tool call
118
+ // still reaches the tool executor (which can surface its own error)
119
+ // rather than crashing the entire chat session.
120
+ input = { _parseError: true, _raw: (tc.function.arguments || '').slice(0, 200) };
121
+ }
122
+ return { id: tc.id, name: tc.function.name, input };
123
+ });
116
124
 
117
125
  // Build rawContent in Anthropic format for message history consistency
118
126
  const rawContent = [];
@@ -138,7 +146,11 @@ export class OpenAICompatProvider extends BaseProvider {
138
146
  params.temperature = this.temperature;
139
147
  }
140
148
 
141
- params.max_tokens = this.maxTokens;
149
+ if (this.isReasoningModel) {
150
+ params.max_completion_tokens = this.maxTokens;
151
+ } else {
152
+ params.max_tokens = this.maxTokens;
153
+ }
142
154
 
143
155
  const convertedTools = this._convertTools(tools);
144
156
  if (convertedTools) {
@@ -154,10 +166,12 @@ export class OpenAICompatProvider extends BaseProvider {
154
166
  async ping() {
155
167
  const params = {
156
168
  model: this.model,
157
- max_tokens: 16,
158
169
  messages: [{ role: 'user', content: 'ping' }],
159
170
  };
160
- if (!this.isReasoningModel) {
171
+ if (this.isReasoningModel) {
172
+ params.max_completion_tokens = 16;
173
+ } else {
174
+ params.max_tokens = 16;
161
175
  params.temperature = 0;
162
176
  }
163
177
  await this.client.chat.completions.create(params);
@@ -1,9 +1,46 @@
1
1
  export function isAllowedUser(userId, config) {
2
2
  const allowed = config.telegram.allowed_users;
3
- if (!allowed || allowed.length === 0) return true; // dev mode
3
+ if (!allowed || allowed.length === 0) return false;
4
4
  return allowed.includes(userId);
5
5
  }
6
6
 
7
7
  export function getUnauthorizedMessage() {
8
8
  return 'Access denied. You are not authorized to use this bot.';
9
9
  }
10
+
11
+ /**
12
+ * Send an alert to the admin when an unauthorized user attempts access.
13
+ */
14
+ export async function alertAdmin(bot, { userId, username, firstName, text, type }) {
15
+ const adminId = Number(process.env.OWNER_TELEGRAM_ID);
16
+ if (!adminId) return;
17
+
18
+ const userTag = username ? `@${username}` : 'بدون معرّف';
19
+ const name = firstName || 'غير معروف';
20
+ const content = text || '—';
21
+ const updateType = type || 'message';
22
+
23
+ const alert =
24
+ `🚨 *محاولة وصول غير مصرح بها\\!*\n\n` +
25
+ `👤 *المستخدم:* ${escapeMarkdown(userTag)} \\(ID: \`${userId}\`\\)\n` +
26
+ `📛 *الاسم:* ${escapeMarkdown(name)}\n` +
27
+ `📩 *النوع:* ${escapeMarkdown(updateType)}\n` +
28
+ `💬 *المحتوى:* ${escapeMarkdown(content)}`;
29
+
30
+ try {
31
+ await bot.sendMessage(adminId, alert, { parse_mode: 'MarkdownV2' });
32
+ } catch {
33
+ // Fallback to plain text if MarkdownV2 fails
34
+ const plain =
35
+ `🚨 محاولة وصول غير مصرح بها!\n\n` +
36
+ `👤 المستخدم: ${userTag} (ID: ${userId})\n` +
37
+ `📛 الاسم: ${name}\n` +
38
+ `📩 النوع: ${updateType}\n` +
39
+ `💬 المحتوى: ${content}`;
40
+ await bot.sendMessage(adminId, plain).catch(() => {});
41
+ }
42
+ }
43
+
44
+ function escapeMarkdown(text) {
45
+ return String(text).replace(/[_*[\]()~`>#+\-=|{}.!\\]/g, '\\$&');
46
+ }
@@ -38,9 +38,18 @@ export class STTService {
38
38
 
39
39
  return new Promise((resolve, reject) => {
40
40
  const writer = createWriteStream(tmpPath);
41
+
42
+ const fail = (err) => {
43
+ writer.destroy();
44
+ // Clean up the partial temp file so it doesn't leak on disk
45
+ try { unlinkSync(tmpPath); } catch {}
46
+ reject(err);
47
+ };
48
+
49
+ response.data.on('error', fail);
41
50
  response.data.pipe(writer);
42
51
  writer.on('finish', () => resolve(tmpPath));
43
- writer.on('error', reject);
52
+ writer.on('error', fail);
44
53
  });
45
54
  }
46
55
 
@@ -1,13 +1,7 @@
1
- import { exec } from 'child_process';
1
+ import { shellRun, shellEscape } from '../utils/shell.js';
2
+ import { getLogger } from '../utils/logger.js';
2
3
 
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
- }
4
+ const run = (cmd, timeout = 30000) => shellRun(cmd, timeout, { maxBuffer: 10 * 1024 * 1024 });
11
5
 
12
6
  export const definitions = [
13
7
  {
@@ -60,21 +54,49 @@ export const definitions = [
60
54
 
61
55
  export const handlers = {
62
56
  docker_ps: async (params) => {
57
+ const logger = getLogger();
63
58
  const flag = params.all ? '-a' : '';
64
- return await run(`docker ps ${flag} --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}\t{{.Image}}"`);
59
+ logger.debug('docker_ps: listing containers');
60
+ const result = await run(`docker ps ${flag} --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}\t{{.Image}}"`);
61
+ if (result.error) logger.error(`docker_ps failed: ${result.error}`);
62
+ return result;
65
63
  },
66
64
 
67
65
  docker_logs: async (params) => {
68
- const tail = params.tail || 100;
69
- return await run(`docker logs --tail ${tail} ${params.container}`);
66
+ const logger = getLogger();
67
+ if (params.tail != null) {
68
+ const tail = parseInt(params.tail, 10);
69
+ if (!Number.isFinite(tail) || tail <= 0 || tail > 10000) {
70
+ return { error: 'Invalid tail value: must be between 1 and 10000' };
71
+ }
72
+ logger.debug(`docker_logs: fetching ${tail} lines from ${params.container}`);
73
+ const result = await run(`docker logs --tail ${tail} ${shellEscape(params.container)}`);
74
+ if (result.error) logger.error(`docker_logs failed for ${params.container}: ${result.error}`);
75
+ return result;
76
+ }
77
+ logger.debug(`docker_logs: fetching 100 lines from ${params.container}`);
78
+ const result = await run(`docker logs --tail 100 ${shellEscape(params.container)}`);
79
+ if (result.error) logger.error(`docker_logs failed for ${params.container}: ${result.error}`);
80
+ return result;
70
81
  },
71
82
 
72
83
  docker_exec: async (params) => {
73
- return await run(`docker exec ${params.container} ${params.command}`);
84
+ const logger = getLogger();
85
+ if (!params.command || !params.command.trim()) {
86
+ return { error: 'Command must not be empty' };
87
+ }
88
+ logger.debug(`docker_exec: running command in ${params.container}`);
89
+ const result = await run(`docker exec ${shellEscape(params.container)} sh -c ${shellEscape(params.command)}`);
90
+ if (result.error) logger.error(`docker_exec failed in ${params.container}: ${result.error}`);
91
+ return result;
74
92
  },
75
93
 
76
94
  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);
95
+ const logger = getLogger();
96
+ const dir = params.project_dir ? `-f ${shellEscape(params.project_dir + '/docker-compose.yml')}` : '';
97
+ logger.debug(`docker_compose: ${params.action}`);
98
+ const result = await run(`docker compose ${dir} ${params.action}`, 120000);
99
+ if (result.error) logger.error(`docker_compose '${params.action}' failed: ${result.error}`);
100
+ return result;
79
101
  },
80
102
  };
package/src/tools/git.js CHANGED
@@ -2,6 +2,7 @@ import simpleGit from 'simple-git';
2
2
  import { join } from 'path';
3
3
  import { homedir } from 'os';
4
4
  import { mkdirSync } from 'fs';
5
+ import { getLogger } from '../utils/logger.js';
5
6
 
6
7
  function getWorkspaceDir(config) {
7
8
  const dir = config.claude_code?.workspace_dir || join(homedir(), '.kernelbot', 'workspaces');
@@ -117,6 +118,7 @@ export const handlers = {
117
118
  await git.clone(authUrl, targetDir);
118
119
  return { success: true, path: targetDir };
119
120
  } catch (err) {
121
+ getLogger().error(`git_clone failed for ${params.repo}: ${err.message}`);
120
122
  return { error: err.message };
121
123
  }
122
124
  },
@@ -132,6 +134,7 @@ export const handlers = {
132
134
  }
133
135
  return { success: true, branch };
134
136
  } catch (err) {
137
+ getLogger().error(`git_checkout failed for branch ${params.branch}: ${err.message}`);
135
138
  return { error: err.message };
136
139
  }
137
140
  },
@@ -144,6 +147,7 @@ export const handlers = {
144
147
  const result = await git.commit(message);
145
148
  return { success: true, commit: result.commit, summary: result.summary };
146
149
  } catch (err) {
150
+ getLogger().error(`git_commit failed: ${err.message}`);
147
151
  return { error: err.message };
148
152
  }
149
153
  },
@@ -169,6 +173,7 @@ export const handlers = {
169
173
  await git.push('origin', branch, options);
170
174
  return { success: true, branch };
171
175
  } catch (err) {
176
+ getLogger().error(`git_push failed: ${err.message}`);
172
177
  return { error: err.message };
173
178
  }
174
179
  },
@@ -181,6 +186,7 @@ export const handlers = {
181
186
  const staged = await git.diff(['--cached']);
182
187
  return { unstaged: diff || '(no changes)', staged: staged || '(no staged changes)' };
183
188
  } catch (err) {
189
+ getLogger().error(`git_diff failed: ${err.message}`);
184
190
  return { error: err.message };
185
191
  }
186
192
  },
@@ -1,4 +1,5 @@
1
1
  import { Octokit } from '@octokit/rest';
2
+ import { getLogger } from '../utils/logger.js';
2
3
 
3
4
  function getOctokit(config) {
4
5
  const token = config.github?.token || process.env.GITHUB_TOKEN;
@@ -104,6 +105,7 @@ export const handlers = {
104
105
 
105
106
  return { success: true, pr_number: data.number, url: data.html_url };
106
107
  } catch (err) {
108
+ getLogger().error(`github_create_pr failed: ${err.message}`);
107
109
  return { error: err.message };
108
110
  }
109
111
  },
@@ -122,6 +124,7 @@ export const handlers = {
122
124
 
123
125
  return { diff: data };
124
126
  } catch (err) {
127
+ getLogger().error(`github_get_pr_diff failed: ${err.message}`);
125
128
  return { error: err.message };
126
129
  }
127
130
  },
@@ -141,6 +144,7 @@ export const handlers = {
141
144
 
142
145
  return { success: true, review_id: data.id };
143
146
  } catch (err) {
147
+ getLogger().error(`github_post_review failed: ${err.message}`);
144
148
  return { error: err.message };
145
149
  }
146
150
  },
@@ -169,6 +173,7 @@ export const handlers = {
169
173
 
170
174
  return { success: true, url: data.html_url, clone_url: data.clone_url };
171
175
  } catch (err) {
176
+ getLogger().error(`github_create_repo failed: ${err.message}`);
172
177
  return { error: err.message };
173
178
  }
174
179
  },
@@ -195,6 +200,7 @@ export const handlers = {
195
200
 
196
201
  return { prs };
197
202
  } catch (err) {
203
+ getLogger().error(`github_list_prs failed: ${err.message}`);
198
204
  return { error: err.message };
199
205
  }
200
206
  },
package/src/tools/jira.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import axios from 'axios';
2
+ import { getLogger } from '../utils/logger.js';
2
3
 
3
4
  /**
4
5
  * Create an axios instance configured for the JIRA REST API.
@@ -142,6 +143,7 @@ export const handlers = {
142
143
  if (err.response?.status === 404) {
143
144
  return { error: `Ticket ${params.ticket_key} not found` };
144
145
  }
146
+ getLogger().error(`jira_get_ticket failed for ${params.ticket_key}: ${err.message}`);
145
147
  return { error: err.response?.data?.errorMessages?.join('; ') || err.message };
146
148
  }
147
149
  },
@@ -169,6 +171,7 @@ export const handlers = {
169
171
  tickets: (data.issues || []).map(formatIssue),
170
172
  };
171
173
  } catch (err) {
174
+ getLogger().error(`jira_search_tickets failed: ${err.message}`);
172
175
  return { error: err.response?.data?.errorMessages?.join('; ') || err.message };
173
176
  }
174
177
  },
@@ -198,6 +201,7 @@ export const handlers = {
198
201
  tickets: (data.issues || []).map(formatIssue),
199
202
  };
200
203
  } catch (err) {
204
+ getLogger().error(`jira_list_my_tickets failed: ${err.message}`);
201
205
  return { error: err.response?.data?.errorMessages?.join('; ') || err.message };
202
206
  }
203
207
  },
@@ -226,6 +230,7 @@ export const handlers = {
226
230
  tickets: (data.issues || []).map(formatIssue),
227
231
  };
228
232
  } catch (err) {
233
+ getLogger().error(`jira_get_project_tickets failed for ${params.project_key}: ${err.message}`);
229
234
  return { error: err.response?.data?.errorMessages?.join('; ') || err.message };
230
235
  }
231
236
  },