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.
@@ -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,
@@ -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 = todayDate();
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(todayDate());
58
+ const filePath = this._journalPath(todayDateStr());
62
59
  if (!existsSync(filePath)) return null;
63
60
  return readFileSync(filePath, 'utf-8');
64
61
  }
@@ -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 = today();
52
+ const date = todayDateStr();
60
53
  const entries = this._loadEpisodicDay(date);
61
54
  const entry = {
62
55
  id: genId('ep'),
@@ -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 todayStart = new Date();
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 now = new Date();
27
- const timeStr = now.toLocaleString('en-US', {
28
- weekday: 'long',
29
- year: 'numeric',
30
- month: 'long',
31
- day: 'numeric',
32
- hour: '2-digit',
33
- minute: '2-digit',
34
- timeZoneName: 'short',
35
- });
36
- let timeBlock = `## Current Time\n${timeStr}`;
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
  }
@@ -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
- await new Promise((r) => setTimeout(r, 1500 * attempt));
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
- return (status >= 500 && status < 600) || status === 429;
126
+ // Anthropic overloaded (529) is also transient
127
+ return (status >= 500 && status < 600) || status === 429 || status === 529;
96
128
  }
97
129
 
98
130
  /**
@@ -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,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
- 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;
58
63
  },
59
64
 
60
65
  docker_logs: async (params) => {
61
- const tail = parseInt(params.tail, 10) || 100;
62
- return await run(`docker logs --tail ${tail} ${shellEscape(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;
63
81
  },
64
82
 
65
83
  docker_exec: async (params) => {
66
- return await run(`docker exec ${shellEscape(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;
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
- return await run(`docker compose ${dir} ${params.action}`, 120000);
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
  },
@@ -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
  },
@@ -59,17 +59,24 @@ export const handlers = {
59
59
  },
60
60
 
61
61
  system_logs: async (params) => {
62
- const lines = parseInt(params.lines, 10) || 50;
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 ${lines}${filterArg} --no-pager`);
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 ${lines} ${shellEscape(source)}${filterCmd}`);
80
+ return await run(`tail -n ${finalLines} ${shellEscape(source)}${filterCmd}`);
74
81
  },
75
82
  };
@@ -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) return reload;
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
  };
@@ -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
- return await run(`kill ${pid}`);
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
- return await run(`pkill -f ${shellEscape(params.name)}`);
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
- return await run(`systemctl ${shellEscape(action)} ${shellEscape(service)}`);
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
  };