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.
- package/.env.example +11 -0
- package/README.md +76 -341
- package/bin/kernel.js +134 -15
- package/config.example.yaml +2 -1
- package/goals.md +20 -0
- package/knowledge_base/index.md +11 -0
- package/package.json +2 -1
- package/src/agent.js +166 -19
- package/src/automation/automation-manager.js +16 -0
- package/src/automation/automation.js +6 -2
- package/src/bot.js +295 -163
- package/src/conversation.js +70 -3
- package/src/life/engine.js +87 -68
- package/src/life/evolution.js +4 -8
- package/src/life/improvements.js +2 -6
- package/src/life/journal.js +3 -6
- package/src/life/memory.js +3 -10
- package/src/life/share-queue.js +4 -9
- package/src/prompts/orchestrator.js +21 -12
- package/src/prompts/persona.md +27 -0
- package/src/providers/base.js +51 -8
- package/src/providers/google-genai.js +198 -0
- package/src/providers/index.js +6 -1
- package/src/providers/models.js +6 -2
- package/src/providers/openai-compat.js +25 -11
- package/src/security/auth.js +38 -1
- package/src/services/stt.js +10 -1
- package/src/tools/docker.js +37 -15
- package/src/tools/git.js +6 -0
- package/src/tools/github.js +6 -0
- package/src/tools/jira.js +5 -0
- package/src/tools/monitor.js +13 -15
- package/src/tools/network.js +22 -18
- package/src/tools/os.js +37 -2
- package/src/tools/process.js +21 -14
- package/src/utils/config.js +66 -0
- package/src/utils/date.js +19 -0
- package/src/utils/display.js +1 -1
- package/src/utils/ids.js +12 -0
- package/src/utils/shell.js +31 -0
- package/src/utils/temporal-awareness.js +199 -0
- package/src/utils/timeUtils.js +110 -0
- package/src/utils/truncate.js +42 -0
- package/src/worker.js +2 -18
package/src/providers/base.js
CHANGED
|
@@ -13,16 +13,35 @@ export class BaseProvider {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
*
|
|
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 <=
|
|
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 <
|
|
59
|
-
|
|
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
|
-
|
|
84
|
-
|
|
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
|
+
}
|
package/src/providers/index.js
CHANGED
|
@@ -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
|
-
|
|
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,
|
package/src/providers/models.js
CHANGED
|
@@ -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.
|
|
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
|
|
39
|
-
if (system
|
|
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
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
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 (
|
|
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);
|
package/src/security/auth.js
CHANGED
|
@@ -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
|
|
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
|
+
}
|
package/src/services/stt.js
CHANGED
|
@@ -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',
|
|
52
|
+
writer.on('error', fail);
|
|
44
53
|
});
|
|
45
54
|
}
|
|
46
55
|
|
package/src/tools/docker.js
CHANGED
|
@@ -1,13 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { shellRun, shellEscape } from '../utils/shell.js';
|
|
2
|
+
import { getLogger } from '../utils/logger.js';
|
|
2
3
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
|
69
|
-
|
|
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
|
-
|
|
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
|
|
78
|
-
|
|
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
|
},
|
package/src/tools/github.js
CHANGED
|
@@ -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
|
},
|