kernelbot 1.0.33 → 1.0.34

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/src/tools/os.js CHANGED
@@ -2,12 +2,24 @@ import { exec } from 'child_process';
2
2
  import { readFileSync, writeFileSync, mkdirSync, readdirSync, statSync } from 'fs';
3
3
  import { dirname, resolve, join } from 'path';
4
4
  import { homedir } from 'os';
5
+ import { getLogger } from '../utils/logger.js';
5
6
 
7
+ /**
8
+ * Expand a file path, resolving `~` to the user's home directory.
9
+ * @param {string} p - The path to expand.
10
+ * @returns {string} The resolved absolute path.
11
+ */
6
12
  function expandPath(p) {
7
13
  if (p.startsWith('~')) return join(homedir(), p.slice(1));
8
14
  return resolve(p);
9
15
  }
10
16
 
17
+ /**
18
+ * Check whether a file path falls within any blocked path defined in config.
19
+ * @param {string} filePath - The path to check.
20
+ * @param {object} config - The bot configuration object.
21
+ * @returns {boolean} True if the path is blocked.
22
+ */
11
23
  function isBlocked(filePath, config) {
12
24
  const expanded = expandPath(filePath);
13
25
  const blockedPaths = config.security?.blocked_paths || [];
@@ -94,14 +106,25 @@ export const definitions = [
94
106
  },
95
107
  ];
96
108
 
97
- function listRecursive(dirPath, base = '') {
109
+ /** Maximum recursion depth for directory listing to prevent stack overflow. */
110
+ const MAX_RECURSIVE_DEPTH = 10;
111
+
112
+ /**
113
+ * Recursively list all files and directories under a given path.
114
+ * @param {string} dirPath - Absolute path to the directory to list.
115
+ * @param {string} [base=''] - Relative path prefix for nested entries.
116
+ * @param {number} [depth=0] - Current recursion depth (internal).
117
+ * @returns {Array<{name: string, type: string}>} Flat array of entries.
118
+ */
119
+ function listRecursive(dirPath, base = '', depth = 0) {
120
+ if (depth >= MAX_RECURSIVE_DEPTH) return [];
98
121
  const entries = [];
99
122
  for (const entry of readdirSync(dirPath, { withFileTypes: true })) {
100
123
  const rel = base ? `${base}/${entry.name}` : entry.name;
101
124
  const type = entry.isDirectory() ? 'directory' : 'file';
102
125
  entries.push({ name: rel, type });
103
126
  if (entry.isDirectory()) {
104
- entries.push(...listRecursive(join(dirPath, entry.name), rel));
127
+ entries.push(...listRecursive(join(dirPath, entry.name), rel, depth + 1));
105
128
  }
106
129
  }
107
130
  return entries;
@@ -109,6 +132,7 @@ function listRecursive(dirPath, base = '') {
109
132
 
110
133
  export const handlers = {
111
134
  execute_command: async (params, context) => {
135
+ const logger = getLogger();
112
136
  const { command, timeout_seconds = 30 } = params;
113
137
  const { config } = context;
114
138
  const blockedPaths = config.security?.blocked_paths || [];
@@ -117,10 +141,13 @@ export const handlers = {
117
141
  for (const bp of blockedPaths) {
118
142
  const expanded = expandPath(bp);
119
143
  if (command.includes(expanded)) {
144
+ logger.warn(`execute_command blocked: command references restricted path ${bp}`);
120
145
  return { error: `Blocked: command references restricted path ${bp}` };
121
146
  }
122
147
  }
123
148
 
149
+ logger.debug(`execute_command: ${command.slice(0, 120)}`);
150
+
124
151
  return new Promise((res) => {
125
152
  let abortHandler = null;
126
153
 
@@ -164,8 +191,10 @@ export const handlers = {
164
191
  },
165
192
 
166
193
  read_file: async (params, context) => {
194
+ const logger = getLogger();
167
195
  const { path: filePath, max_lines } = params;
168
196
  if (isBlocked(filePath, context.config)) {
197
+ logger.warn(`read_file blocked: access to ${filePath} is restricted`);
169
198
  return { error: `Blocked: access to ${filePath} is restricted` };
170
199
  }
171
200
 
@@ -184,8 +213,10 @@ export const handlers = {
184
213
  },
185
214
 
186
215
  write_file: async (params, context) => {
216
+ const logger = getLogger();
187
217
  const { path: filePath, content } = params;
188
218
  if (isBlocked(filePath, context.config)) {
219
+ logger.warn(`write_file blocked: access to ${filePath} is restricted`);
189
220
  return { error: `Blocked: access to ${filePath} is restricted` };
190
221
  }
191
222
 
@@ -193,15 +224,19 @@ export const handlers = {
193
224
  const expanded = expandPath(filePath);
194
225
  mkdirSync(dirname(expanded), { recursive: true });
195
226
  writeFileSync(expanded, content, 'utf-8');
227
+ logger.debug(`write_file: wrote ${content.length} bytes to ${expanded}`);
196
228
  return { success: true, path: expanded };
197
229
  } catch (err) {
230
+ logger.error(`write_file error for ${filePath}: ${err.message}`);
198
231
  return { error: err.message };
199
232
  }
200
233
  },
201
234
 
202
235
  list_directory: async (params, context) => {
236
+ const logger = getLogger();
203
237
  const { path: dirPath, recursive = false } = params;
204
238
  if (isBlocked(dirPath, context.config)) {
239
+ logger.warn(`list_directory blocked: access to ${dirPath} is restricted`);
205
240
  return { error: `Blocked: access to ${dirPath} is restricted` };
206
241
  }
207
242
 
@@ -1,13 +1,4 @@
1
- import { exec } from 'child_process';
2
-
3
- function run(cmd, timeout = 10000) {
4
- return new Promise((resolve) => {
5
- exec(cmd, { timeout }, (error, stdout, stderr) => {
6
- if (error) return resolve({ error: stderr || error.message });
7
- resolve({ output: stdout.trim() });
8
- });
9
- });
10
- }
1
+ import { shellRun as run, shellEscape } from '../utils/shell.js';
11
2
 
12
3
  export const definitions = [
13
4
  {
@@ -52,22 +43,24 @@ export const definitions = [
52
43
  export const handlers = {
53
44
  process_list: async (params) => {
54
45
  const filter = params.filter;
55
- const cmd = filter ? `ps aux | head -1 && ps aux | grep -i "${filter}" | grep -v grep` : 'ps aux';
46
+ const cmd = filter ? `ps aux | head -1 && ps aux | grep -i ${shellEscape(filter)} | grep -v grep` : 'ps aux';
56
47
  return await run(cmd);
57
48
  },
58
49
 
59
50
  kill_process: async (params) => {
60
51
  if (params.pid) {
61
- return await run(`kill ${params.pid}`);
52
+ const pid = parseInt(params.pid, 10);
53
+ if (!Number.isFinite(pid) || pid <= 0) return { error: 'Invalid PID' };
54
+ return await run(`kill ${pid}`);
62
55
  }
63
56
  if (params.name) {
64
- return await run(`pkill -f "${params.name}"`);
57
+ return await run(`pkill -f ${shellEscape(params.name)}`);
65
58
  }
66
59
  return { error: 'Provide either pid or name' };
67
60
  },
68
61
 
69
62
  service_control: async (params) => {
70
63
  const { service, action } = params;
71
- return await run(`systemctl ${action} ${service}`);
64
+ return await run(`systemctl ${shellEscape(action)} ${shellEscape(service)}`);
72
65
  },
73
66
  };
@@ -270,6 +270,65 @@ export function saveClaudeCodeAuth(config, mode, value) {
270
270
  // mode === 'system' — no credentials to save
271
271
  }
272
272
 
273
+ /**
274
+ * Full interactive flow: change orchestrator model + optionally enter API key.
275
+ */
276
+ export async function changeOrchestratorModel(config, rl) {
277
+ const { createProvider } = await import('../providers/index.js');
278
+ const { providerKey, modelId } = await promptProviderSelection(rl);
279
+
280
+ const providerDef = PROVIDERS[providerKey];
281
+
282
+ // Resolve API key
283
+ const envKey = providerDef.envKey;
284
+ let apiKey = process.env[envKey];
285
+ if (!apiKey) {
286
+ const key = await ask(rl, chalk.cyan(`\n ${providerDef.name} API key (${envKey}): `));
287
+ if (!key.trim()) {
288
+ console.log(chalk.yellow('\n No API key provided. Orchestrator not changed.\n'));
289
+ return config;
290
+ }
291
+ apiKey = key.trim();
292
+ }
293
+
294
+ // Validate the new provider before saving anything
295
+ console.log(chalk.dim(`\n Verifying ${providerDef.name} / ${modelId}...`));
296
+ const testConfig = {
297
+ brain: {
298
+ provider: providerKey,
299
+ model: modelId,
300
+ max_tokens: config.orchestrator.max_tokens,
301
+ temperature: config.orchestrator.temperature,
302
+ api_key: apiKey,
303
+ },
304
+ };
305
+ try {
306
+ const testProvider = createProvider(testConfig);
307
+ await testProvider.ping();
308
+ } catch (err) {
309
+ console.log(chalk.red(`\n ✖ Verification failed: ${err.message}`));
310
+ console.log(chalk.yellow(` Orchestrator not changed. Keeping current model.\n`));
311
+ return config;
312
+ }
313
+
314
+ // Validation passed — save everything
315
+ const savedPath = saveOrchestratorToYaml(providerKey, modelId);
316
+ console.log(chalk.dim(` Saved to ${savedPath}`));
317
+
318
+ config.orchestrator.provider = providerKey;
319
+ config.orchestrator.model = modelId;
320
+ config.orchestrator.api_key = apiKey;
321
+
322
+ // Save the key if it was newly entered
323
+ if (!process.env[envKey]) {
324
+ saveCredential(config, envKey, apiKey);
325
+ console.log(chalk.dim(' API key saved.\n'));
326
+ }
327
+
328
+ console.log(chalk.green(` ✔ Orchestrator switched to ${providerDef.name} / ${modelId}\n`));
329
+ return config;
330
+ }
331
+
273
332
  /**
274
333
  * Full interactive flow: change brain model + optionally enter API key.
275
334
  */
@@ -0,0 +1,31 @@
1
+ import { exec } from 'child_process';
2
+
3
+ /**
4
+ * Escape a string for safe use as a shell argument.
5
+ * Wraps the value in single quotes and escapes any embedded single quotes.
6
+ *
7
+ * @param {string} arg - The value to escape
8
+ * @returns {string} Shell-safe quoted string
9
+ */
10
+ export function shellEscape(arg) {
11
+ if (arg === undefined || arg === null) return "''";
12
+ return "'" + String(arg).replace(/'/g, "'\\''") + "'";
13
+ }
14
+
15
+ /**
16
+ * Run a shell command and return { output } on success or { error } on failure.
17
+ * Resolves (never rejects) so callers can handle errors via the result object.
18
+ *
19
+ * @param {string} cmd - The shell command to execute
20
+ * @param {number} [timeout=10000] - Max execution time in milliseconds
21
+ * @param {{ maxBuffer?: number }} [opts] - Optional exec options
22
+ * @returns {Promise<{ output: string } | { error: string }>}
23
+ */
24
+ export function shellRun(cmd, timeout = 10000, opts = {}) {
25
+ return new Promise((resolve) => {
26
+ exec(cmd, { timeout, maxBuffer: opts.maxBuffer, ...opts }, (error, stdout, stderr) => {
27
+ if (error) return resolve({ error: stderr || error.message });
28
+ resolve({ output: stdout.trim() });
29
+ });
30
+ });
31
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Shared tool-result truncation logic.
3
+ * Used by both OrchestratorAgent and WorkerAgent to cap tool outputs
4
+ * before feeding them back into the LLM context window.
5
+ */
6
+
7
+ const MAX_RESULT_LENGTH = 3000;
8
+
9
+ const LARGE_FIELDS = [
10
+ 'stdout', 'stderr', 'content', 'diff', 'output',
11
+ 'body', 'html', 'text', 'log', 'logs',
12
+ ];
13
+
14
+ /**
15
+ * Truncate a serialized tool result to fit within the context budget.
16
+ *
17
+ * Strategy:
18
+ * 1. If JSON.stringify(result) fits, return it as-is.
19
+ * 2. Otherwise, trim known large string fields to 500 chars each and retry.
20
+ * 3. If still too large, hard-slice the serialized string.
21
+ *
22
+ * @param {string} _name - Tool name (reserved for future per-tool limits)
23
+ * @param {any} result - Raw tool result object
24
+ * @returns {string} JSON string, guaranteed ≤ MAX_RESULT_LENGTH (+tail note)
25
+ */
26
+ export function truncateToolResult(_name, result) {
27
+ let str = JSON.stringify(result);
28
+ if (str.length <= MAX_RESULT_LENGTH) return str;
29
+
30
+ if (result && typeof result === 'object') {
31
+ const truncated = { ...result };
32
+ for (const field of LARGE_FIELDS) {
33
+ if (typeof truncated[field] === 'string' && truncated[field].length > 500) {
34
+ truncated[field] = truncated[field].slice(0, 500) + `\n... [truncated ${truncated[field].length - 500} chars]`;
35
+ }
36
+ }
37
+ str = JSON.stringify(truncated);
38
+ if (str.length <= MAX_RESULT_LENGTH) return str;
39
+ }
40
+
41
+ return str.slice(0, MAX_RESULT_LENGTH) + `\n... [truncated, total ${str.length} chars]`;
42
+ }
package/src/worker.js CHANGED
@@ -5,9 +5,7 @@ import { getMissingCredential } from './utils/config.js';
5
5
  import { getWorkerPrompt } from './prompts/workers.js';
6
6
  import { getUnifiedSkillById } from './skills/custom.js';
7
7
  import { getLogger } from './utils/logger.js';
8
-
9
- const MAX_RESULT_LENGTH = 3000;
10
- const LARGE_FIELDS = ['stdout', 'stderr', 'content', 'diff', 'output', 'body', 'html', 'text', 'log', 'logs'];
8
+ import { truncateToolResult } from './utils/truncate.js';
11
9
 
12
10
  /**
13
11
  * WorkerAgent — runs a scoped agent loop in the background.
@@ -371,21 +369,7 @@ export class WorkerAgent {
371
369
  }
372
370
 
373
371
  _truncateResult(name, result) {
374
- let str = JSON.stringify(result);
375
- if (str.length <= MAX_RESULT_LENGTH) return str;
376
-
377
- if (result && typeof result === 'object') {
378
- const truncated = { ...result };
379
- for (const field of LARGE_FIELDS) {
380
- if (typeof truncated[field] === 'string' && truncated[field].length > 500) {
381
- truncated[field] = truncated[field].slice(0, 500) + `\n... [truncated ${truncated[field].length - 500} chars]`;
382
- }
383
- }
384
- str = JSON.stringify(truncated);
385
- if (str.length <= MAX_RESULT_LENGTH) return str;
386
- }
387
-
388
- return str.slice(0, MAX_RESULT_LENGTH) + `\n... [truncated, total ${str.length} chars]`;
372
+ return truncateToolResult(name, result);
389
373
  }
390
374
 
391
375
  _formatToolSummary(name, input) {