kernelbot 1.0.34 → 1.0.36
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 +48 -318
- package/bin/kernel.js +89 -16
- package/config.example.yaml +2 -1
- package/goals.md +20 -0
- package/knowledge_base/index.md +11 -0
- package/package.json +1 -1
- package/src/agent.js +19 -1
- package/src/automation/automation-manager.js +16 -0
- package/src/automation/automation.js +6 -2
- package/src/bot.js +129 -23
- 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/providers/base.js +36 -4
- package/src/security/auth.js +38 -1
- package/src/services/stt.js +10 -1
- package/src/tools/docker.js +34 -5
- 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 +10 -3
- package/src/tools/network.js +12 -1
- package/src/tools/process.js +17 -3
- package/src/utils/config.js +50 -14
- package/src/utils/date.js +19 -0
- package/src/utils/display.js +1 -1
- package/src/utils/ids.js +12 -0
- package/src/utils/temporal-awareness.js +199 -0
- package/src/utils/timeUtils.js +110 -0
package/src/life/improvements.js
CHANGED
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { homedir } from 'os';
|
|
4
|
-
import { randomBytes } from 'crypto';
|
|
5
4
|
import { getLogger } from '../utils/logger.js';
|
|
5
|
+
import { genId } from '../utils/ids.js';
|
|
6
6
|
|
|
7
7
|
const LIFE_DIR = join(homedir(), '.kernelbot', 'life');
|
|
8
8
|
const IMPROVEMENTS_FILE = join(LIFE_DIR, 'improvements.json');
|
|
9
9
|
|
|
10
|
-
function genId() {
|
|
11
|
-
return `imp_${randomBytes(4).toString('hex')}`;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
10
|
export class ImprovementTracker {
|
|
15
11
|
constructor() {
|
|
16
12
|
mkdirSync(LIFE_DIR, { recursive: true });
|
|
@@ -39,7 +35,7 @@ export class ImprovementTracker {
|
|
|
39
35
|
addProposal(proposal) {
|
|
40
36
|
const logger = getLogger();
|
|
41
37
|
const entry = {
|
|
42
|
-
id: genId(),
|
|
38
|
+
id: genId('imp'),
|
|
43
39
|
createdAt: Date.now(),
|
|
44
40
|
status: 'pending', // pending, approved, rejected
|
|
45
41
|
description: proposal.description,
|
package/src/life/journal.js
CHANGED
|
@@ -2,13 +2,10 @@ import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync } from
|
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { homedir } from 'os';
|
|
4
4
|
import { getLogger } from '../utils/logger.js';
|
|
5
|
+
import { todayDateStr } from '../utils/date.js';
|
|
5
6
|
|
|
6
7
|
const JOURNAL_DIR = join(homedir(), '.kernelbot', 'life', 'journals');
|
|
7
8
|
|
|
8
|
-
function todayDate() {
|
|
9
|
-
return new Date().toISOString().slice(0, 10);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
9
|
function formatDate(date) {
|
|
13
10
|
return new Date(date + 'T00:00:00').toLocaleDateString('en-US', {
|
|
14
11
|
weekday: 'long',
|
|
@@ -38,7 +35,7 @@ export class JournalManager {
|
|
|
38
35
|
*/
|
|
39
36
|
writeEntry(title, content) {
|
|
40
37
|
const logger = getLogger();
|
|
41
|
-
const date =
|
|
38
|
+
const date = todayDateStr();
|
|
42
39
|
const filePath = this._journalPath(date);
|
|
43
40
|
const time = timeNow();
|
|
44
41
|
|
|
@@ -58,7 +55,7 @@ export class JournalManager {
|
|
|
58
55
|
* Get today's journal content.
|
|
59
56
|
*/
|
|
60
57
|
getToday() {
|
|
61
|
-
const filePath = this._journalPath(
|
|
58
|
+
const filePath = this._journalPath(todayDateStr());
|
|
62
59
|
if (!existsSync(filePath)) return null;
|
|
63
60
|
return readFileSync(filePath, 'utf-8');
|
|
64
61
|
}
|
package/src/life/memory.js
CHANGED
|
@@ -1,21 +1,14 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, unlinkSync } from 'fs';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { homedir } from 'os';
|
|
4
|
-
import { randomBytes } from 'crypto';
|
|
5
4
|
import { getLogger } from '../utils/logger.js';
|
|
5
|
+
import { genId } from '../utils/ids.js';
|
|
6
|
+
import { todayDateStr } from '../utils/date.js';
|
|
6
7
|
|
|
7
8
|
const LIFE_DIR = join(homedir(), '.kernelbot', 'life');
|
|
8
9
|
const EPISODIC_DIR = join(LIFE_DIR, 'memories', 'episodic');
|
|
9
10
|
const SEMANTIC_FILE = join(LIFE_DIR, 'memories', 'semantic', 'topics.json');
|
|
10
11
|
|
|
11
|
-
function today() {
|
|
12
|
-
return new Date().toISOString().slice(0, 10);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function genId(prefix = 'ep') {
|
|
16
|
-
return `${prefix}_${randomBytes(4).toString('hex')}`;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
12
|
export class MemoryManager {
|
|
20
13
|
constructor() {
|
|
21
14
|
this._episodicCache = new Map(); // date -> array
|
|
@@ -56,7 +49,7 @@ export class MemoryManager {
|
|
|
56
49
|
*/
|
|
57
50
|
addEpisodic(memory) {
|
|
58
51
|
const logger = getLogger();
|
|
59
|
-
const date =
|
|
52
|
+
const date = todayDateStr();
|
|
60
53
|
const entries = this._loadEpisodicDay(date);
|
|
61
54
|
const entry = {
|
|
62
55
|
id: genId('ep'),
|
package/src/life/share-queue.js
CHANGED
|
@@ -1,16 +1,13 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { homedir } from 'os';
|
|
4
|
-
import { randomBytes } from 'crypto';
|
|
5
4
|
import { getLogger } from '../utils/logger.js';
|
|
5
|
+
import { genId } from '../utils/ids.js';
|
|
6
|
+
import { getStartOfDayMs } from '../utils/date.js';
|
|
6
7
|
|
|
7
8
|
const LIFE_DIR = join(homedir(), '.kernelbot', 'life');
|
|
8
9
|
const SHARES_FILE = join(LIFE_DIR, 'shares.json');
|
|
9
10
|
|
|
10
|
-
function genId() {
|
|
11
|
-
return `sh_${randomBytes(4).toString('hex')}`;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
11
|
export class ShareQueue {
|
|
15
12
|
constructor() {
|
|
16
13
|
mkdirSync(LIFE_DIR, { recursive: true });
|
|
@@ -43,7 +40,7 @@ export class ShareQueue {
|
|
|
43
40
|
add(content, source, priority = 'medium', targetUserId = null, tags = []) {
|
|
44
41
|
const logger = getLogger();
|
|
45
42
|
const item = {
|
|
46
|
-
id: genId(),
|
|
43
|
+
id: genId('sh'),
|
|
47
44
|
content,
|
|
48
45
|
source,
|
|
49
46
|
createdAt: Date.now(),
|
|
@@ -115,9 +112,7 @@ export class ShareQueue {
|
|
|
115
112
|
* Get count of shares sent today (for rate limiting proactive shares).
|
|
116
113
|
*/
|
|
117
114
|
getSharedTodayCount() {
|
|
118
|
-
const
|
|
119
|
-
todayStart.setHours(0, 0, 0, 0);
|
|
120
|
-
const cutoff = todayStart.getTime();
|
|
115
|
+
const cutoff = getStartOfDayMs();
|
|
121
116
|
return this._data.shared.filter(s => s.sharedAt >= cutoff).length;
|
|
122
117
|
}
|
|
123
118
|
|
|
@@ -2,6 +2,7 @@ import { readFileSync } from 'fs';
|
|
|
2
2
|
import { fileURLToPath } from 'url';
|
|
3
3
|
import { dirname, join } from 'path';
|
|
4
4
|
import { WORKER_TYPES } from '../swarm/worker-registry.js';
|
|
5
|
+
import { buildTemporalAwareness } from '../utils/temporal-awareness.js';
|
|
5
6
|
|
|
6
7
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
8
|
const PERSONA_MD = readFileSync(join(__dirname, 'persona.md'), 'utf-8').trim();
|
|
@@ -22,18 +23,26 @@ export function getOrchestratorPrompt(config, skillPrompt = null, userPersona =
|
|
|
22
23
|
.map(([key, w]) => ` - **${key}**: ${w.emoji} ${w.description}`)
|
|
23
24
|
.join('\n');
|
|
24
25
|
|
|
25
|
-
// Build current time header
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
26
|
+
// Build current time header — enhanced with spatial/temporal awareness if local config exists
|
|
27
|
+
const awareness = buildTemporalAwareness();
|
|
28
|
+
let timeBlock;
|
|
29
|
+
if (awareness) {
|
|
30
|
+
// Full awareness block from local_context.json (timezone, location, work status)
|
|
31
|
+
timeBlock = awareness;
|
|
32
|
+
} else {
|
|
33
|
+
// Fallback: basic server time (no local context configured)
|
|
34
|
+
const now = new Date();
|
|
35
|
+
const timeStr = now.toLocaleString('en-US', {
|
|
36
|
+
weekday: 'long',
|
|
37
|
+
year: 'numeric',
|
|
38
|
+
month: 'long',
|
|
39
|
+
day: 'numeric',
|
|
40
|
+
hour: '2-digit',
|
|
41
|
+
minute: '2-digit',
|
|
42
|
+
timeZoneName: 'short',
|
|
43
|
+
});
|
|
44
|
+
timeBlock = `## Current Time\n${timeStr}`;
|
|
45
|
+
}
|
|
37
46
|
if (temporalContext) {
|
|
38
47
|
timeBlock += `\n${temporalContext}`;
|
|
39
48
|
}
|
package/src/providers/base.js
CHANGED
|
@@ -12,11 +12,30 @@ export class BaseProvider {
|
|
|
12
12
|
this.timeout = timeout || 60_000;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
/**
|
|
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
|
+
|
|
15
31
|
/**
|
|
16
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>}
|
|
@@ -56,7 +75,8 @@ export class BaseProvider {
|
|
|
56
75
|
removeListener?.();
|
|
57
76
|
|
|
58
77
|
if (attempt < 3 && this._isTransient(err)) {
|
|
59
|
-
|
|
78
|
+
const delay = this._retryDelay(attempt);
|
|
79
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
60
80
|
continue;
|
|
61
81
|
}
|
|
62
82
|
throw err;
|
|
@@ -66,17 +86,28 @@ 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
|
}
|
|
@@ -92,7 +123,8 @@ export class BaseProvider {
|
|
|
92
123
|
} catch {}
|
|
93
124
|
}
|
|
94
125
|
|
|
95
|
-
|
|
126
|
+
// Anthropic overloaded (529) is also transient
|
|
127
|
+
return (status >= 500 && status < 600) || status === 429 || status === 529;
|
|
96
128
|
}
|
|
97
129
|
|
|
98
130
|
/**
|
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,4 +1,5 @@
|
|
|
1
1
|
import { shellRun, shellEscape } from '../utils/shell.js';
|
|
2
|
+
import { getLogger } from '../utils/logger.js';
|
|
2
3
|
|
|
3
4
|
const run = (cmd, timeout = 30000) => shellRun(cmd, timeout, { maxBuffer: 10 * 1024 * 1024 });
|
|
4
5
|
|
|
@@ -53,21 +54,49 @@ export const definitions = [
|
|
|
53
54
|
|
|
54
55
|
export const handlers = {
|
|
55
56
|
docker_ps: async (params) => {
|
|
57
|
+
const logger = getLogger();
|
|
56
58
|
const flag = params.all ? '-a' : '';
|
|
57
|
-
|
|
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;
|
|
58
63
|
},
|
|
59
64
|
|
|
60
65
|
docker_logs: async (params) => {
|
|
61
|
-
const
|
|
62
|
-
|
|
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;
|
|
63
81
|
},
|
|
64
82
|
|
|
65
83
|
docker_exec: async (params) => {
|
|
66
|
-
|
|
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;
|
|
67
92
|
},
|
|
68
93
|
|
|
69
94
|
docker_compose: async (params) => {
|
|
95
|
+
const logger = getLogger();
|
|
70
96
|
const dir = params.project_dir ? `-f ${shellEscape(params.project_dir + '/docker-compose.yml')}` : '';
|
|
71
|
-
|
|
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;
|
|
72
101
|
},
|
|
73
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
|
},
|
package/src/tools/monitor.js
CHANGED
|
@@ -59,17 +59,24 @@ export const handlers = {
|
|
|
59
59
|
},
|
|
60
60
|
|
|
61
61
|
system_logs: async (params) => {
|
|
62
|
-
|
|
62
|
+
let finalLines = 50;
|
|
63
|
+
if (params.lines != null) {
|
|
64
|
+
const lines = parseInt(params.lines, 10);
|
|
65
|
+
if (!Number.isFinite(lines) || lines <= 0 || lines > 10000) {
|
|
66
|
+
return { error: 'Invalid lines value: must be between 1 and 10000' };
|
|
67
|
+
}
|
|
68
|
+
finalLines = lines;
|
|
69
|
+
}
|
|
63
70
|
const source = params.source || 'journalctl';
|
|
64
71
|
const filter = params.filter;
|
|
65
72
|
|
|
66
73
|
if (source === 'journalctl') {
|
|
67
74
|
const filterArg = filter ? ` -g ${shellEscape(filter)}` : '';
|
|
68
|
-
return await run(`journalctl -n ${
|
|
75
|
+
return await run(`journalctl -n ${finalLines}${filterArg} --no-pager`);
|
|
69
76
|
}
|
|
70
77
|
|
|
71
78
|
// Reading a log file
|
|
72
79
|
const filterCmd = filter ? ` | grep -i ${shellEscape(filter)}` : '';
|
|
73
|
-
return await run(`tail -n ${
|
|
80
|
+
return await run(`tail -n ${finalLines} ${shellEscape(source)}${filterCmd}`);
|
|
74
81
|
},
|
|
75
82
|
};
|
package/src/tools/network.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { shellRun, shellEscape } from '../utils/shell.js';
|
|
2
|
+
import { getLogger } from '../utils/logger.js';
|
|
2
3
|
|
|
3
4
|
const run = (cmd, timeout = 15000) => shellRun(cmd, timeout);
|
|
4
5
|
|
|
@@ -38,14 +39,17 @@ export const definitions = [
|
|
|
38
39
|
|
|
39
40
|
export const handlers = {
|
|
40
41
|
check_port: async (params) => {
|
|
42
|
+
const logger = getLogger();
|
|
41
43
|
const host = params.host || 'localhost';
|
|
42
44
|
const port = parseInt(params.port, 10);
|
|
43
45
|
if (!Number.isFinite(port) || port <= 0 || port > 65535) return { error: 'Invalid port number' };
|
|
44
46
|
|
|
47
|
+
logger.debug(`check_port: checking ${host}:${port}`);
|
|
45
48
|
// Use nc (netcat) for port check — works on both macOS and Linux
|
|
46
49
|
const result = await run(`nc -z -w 3 ${shellEscape(host)} ${port} 2>&1 && echo "OPEN" || echo "CLOSED"`, 5000);
|
|
47
50
|
|
|
48
51
|
if (result.error) {
|
|
52
|
+
logger.error(`check_port failed for ${host}:${port}: ${result.error}`);
|
|
49
53
|
return { port, host, status: 'closed', detail: result.error };
|
|
50
54
|
}
|
|
51
55
|
|
|
@@ -82,15 +86,22 @@ export const handlers = {
|
|
|
82
86
|
},
|
|
83
87
|
|
|
84
88
|
nginx_reload: async () => {
|
|
89
|
+
const logger = getLogger();
|
|
85
90
|
// Test config first
|
|
91
|
+
logger.debug('nginx_reload: testing configuration');
|
|
86
92
|
const test = await run('nginx -t 2>&1');
|
|
87
93
|
if (test.error || (test.output && test.output.includes('failed'))) {
|
|
94
|
+
logger.error(`nginx_reload: config test failed: ${test.error || test.output}`);
|
|
88
95
|
return { error: `Config test failed: ${test.error || test.output}` };
|
|
89
96
|
}
|
|
90
97
|
|
|
91
98
|
const reload = await run('nginx -s reload 2>&1');
|
|
92
|
-
if (reload.error)
|
|
99
|
+
if (reload.error) {
|
|
100
|
+
logger.error(`nginx_reload failed: ${reload.error}`);
|
|
101
|
+
return reload;
|
|
102
|
+
}
|
|
93
103
|
|
|
104
|
+
logger.debug('nginx_reload: successfully reloaded');
|
|
94
105
|
return { success: true, test_output: test.output };
|
|
95
106
|
},
|
|
96
107
|
};
|
package/src/tools/process.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { shellRun as run, shellEscape } from '../utils/shell.js';
|
|
2
|
+
import { getLogger } from '../utils/logger.js';
|
|
2
3
|
|
|
3
4
|
export const definitions = [
|
|
4
5
|
{
|
|
@@ -42,25 +43,38 @@ export const definitions = [
|
|
|
42
43
|
|
|
43
44
|
export const handlers = {
|
|
44
45
|
process_list: async (params) => {
|
|
46
|
+
const logger = getLogger();
|
|
45
47
|
const filter = params.filter;
|
|
48
|
+
logger.debug(`process_list: ${filter ? `filtering by "${filter}"` : 'listing all'}`);
|
|
46
49
|
const cmd = filter ? `ps aux | head -1 && ps aux | grep -i ${shellEscape(filter)} | grep -v grep` : 'ps aux';
|
|
47
50
|
return await run(cmd);
|
|
48
51
|
},
|
|
49
52
|
|
|
50
53
|
kill_process: async (params) => {
|
|
54
|
+
const logger = getLogger();
|
|
51
55
|
if (params.pid) {
|
|
52
56
|
const pid = parseInt(params.pid, 10);
|
|
53
57
|
if (!Number.isFinite(pid) || pid <= 0) return { error: 'Invalid PID' };
|
|
54
|
-
|
|
58
|
+
logger.debug(`kill_process: killing PID ${pid}`);
|
|
59
|
+
const result = await run(`kill ${pid}`);
|
|
60
|
+
if (result.error) logger.error(`kill_process failed for PID ${pid}: ${result.error}`);
|
|
61
|
+
return result;
|
|
55
62
|
}
|
|
56
63
|
if (params.name) {
|
|
57
|
-
|
|
64
|
+
logger.debug(`kill_process: killing processes matching "${params.name}"`);
|
|
65
|
+
const result = await run(`pkill -f ${shellEscape(params.name)}`);
|
|
66
|
+
if (result.error) logger.error(`kill_process failed for name "${params.name}": ${result.error}`);
|
|
67
|
+
return result;
|
|
58
68
|
}
|
|
59
69
|
return { error: 'Provide either pid or name' };
|
|
60
70
|
},
|
|
61
71
|
|
|
62
72
|
service_control: async (params) => {
|
|
73
|
+
const logger = getLogger();
|
|
63
74
|
const { service, action } = params;
|
|
64
|
-
|
|
75
|
+
logger.debug(`service_control: ${action} ${service}`);
|
|
76
|
+
const result = await run(`systemctl ${shellEscape(action)} ${shellEscape(service)}`);
|
|
77
|
+
if (result.error) logger.error(`service_control failed: ${action} ${service}: ${result.error}`);
|
|
78
|
+
return result;
|
|
65
79
|
},
|
|
66
80
|
};
|