kernelbot 1.0.38 → 1.0.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/bin/kernel.js +335 -451
  2. package/config.example.yaml +1 -1
  3. package/knowledge_base/active_inference_foraging.md +126 -0
  4. package/knowledge_base/index.md +1 -1
  5. package/package.json +2 -1
  6. package/skills/business/business-analyst.md +32 -0
  7. package/skills/business/product-manager.md +32 -0
  8. package/skills/business/project-manager.md +32 -0
  9. package/skills/business/startup-advisor.md +32 -0
  10. package/skills/creative/music-producer.md +32 -0
  11. package/skills/creative/photographer.md +32 -0
  12. package/skills/creative/video-producer.md +32 -0
  13. package/skills/data/bi-analyst.md +37 -0
  14. package/skills/data/data-scientist.md +38 -0
  15. package/skills/data/ml-engineer.md +38 -0
  16. package/skills/design/graphic-designer.md +38 -0
  17. package/skills/design/product-designer.md +41 -0
  18. package/skills/design/ui-ux.md +38 -0
  19. package/skills/education/curriculum-designer.md +32 -0
  20. package/skills/education/language-teacher.md +32 -0
  21. package/skills/education/tutor.md +32 -0
  22. package/skills/engineering/data-eng.md +55 -0
  23. package/skills/engineering/devops.md +56 -0
  24. package/skills/engineering/mobile-dev.md +55 -0
  25. package/skills/engineering/security-eng.md +55 -0
  26. package/skills/engineering/sr-backend.md +55 -0
  27. package/skills/engineering/sr-frontend.md +55 -0
  28. package/skills/finance/accountant.md +35 -0
  29. package/skills/finance/crypto-defi.md +39 -0
  30. package/skills/finance/financial-analyst.md +35 -0
  31. package/skills/healthcare/health-wellness.md +32 -0
  32. package/skills/healthcare/medical-researcher.md +33 -0
  33. package/skills/legal/contract-reviewer.md +35 -0
  34. package/skills/legal/legal-advisor.md +36 -0
  35. package/skills/marketing/content-marketer.md +38 -0
  36. package/skills/marketing/growth.md +38 -0
  37. package/skills/marketing/seo.md +43 -0
  38. package/skills/marketing/social-media.md +43 -0
  39. package/skills/writing/academic-writer.md +33 -0
  40. package/skills/writing/copywriter.md +32 -0
  41. package/skills/writing/creative-writer.md +32 -0
  42. package/skills/writing/tech-writer.md +33 -0
  43. package/src/agent.js +153 -118
  44. package/src/automation/scheduler.js +36 -3
  45. package/src/bot.js +147 -64
  46. package/src/coder.js +30 -8
  47. package/src/conversation.js +96 -19
  48. package/src/dashboard/dashboard.css +6 -0
  49. package/src/dashboard/dashboard.js +28 -1
  50. package/src/dashboard/index.html +12 -0
  51. package/src/dashboard/server.js +77 -15
  52. package/src/dashboard/shared.js +10 -1
  53. package/src/life/codebase.js +2 -1
  54. package/src/life/daydream_engine.js +386 -0
  55. package/src/life/engine.js +88 -6
  56. package/src/life/evolution.js +4 -3
  57. package/src/prompts/orchestrator.js +1 -1
  58. package/src/prompts/system.js +1 -1
  59. package/src/prompts/workers.js +8 -1
  60. package/src/providers/anthropic.js +3 -1
  61. package/src/providers/base.js +33 -0
  62. package/src/providers/index.js +1 -1
  63. package/src/providers/models.js +22 -0
  64. package/src/providers/openai-compat.js +3 -0
  65. package/src/services/x-api.js +14 -3
  66. package/src/skills/loader.js +382 -0
  67. package/src/swarm/worker-registry.js +2 -2
  68. package/src/tools/browser.js +10 -3
  69. package/src/tools/coding.js +16 -0
  70. package/src/tools/docker.js +13 -0
  71. package/src/tools/git.js +31 -29
  72. package/src/tools/jira.js +11 -2
  73. package/src/tools/monitor.js +9 -1
  74. package/src/tools/network.js +34 -0
  75. package/src/tools/orchestrator-tools.js +2 -1
  76. package/src/tools/os.js +20 -6
  77. package/src/utils/config.js +87 -83
  78. package/src/utils/display.js +118 -66
  79. package/src/utils/logger.js +1 -1
  80. package/src/utils/timeAwareness.js +72 -0
  81. package/src/worker.js +26 -33
  82. package/src/skills/catalog.js +0 -506
  83. package/src/skills/custom.js +0 -128
package/src/tools/os.js CHANGED
@@ -137,12 +137,26 @@ export const handlers = {
137
137
  const { config } = context;
138
138
  const blockedPaths = config.security?.blocked_paths || [];
139
139
 
140
- // Simple check: if the command references a blocked path, reject
141
- for (const bp of blockedPaths) {
142
- const expanded = expandPath(bp);
143
- if (command.includes(expanded)) {
144
- logger.warn(`execute_command blocked: command references restricted path ${bp}`);
145
- return { error: `Blocked: command references restricted path ${bp}` };
140
+ // Tokenize the command to extract all path-like arguments, then resolve
141
+ // each one and check against blocked paths. This prevents bypasses via
142
+ // shell operators (&&, |, ;), quoting, or subshells.
143
+ const shellTokens = command.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) || [];
144
+ for (const token of shellTokens) {
145
+ // Strip surrounding quotes
146
+ const cleaned = token.replace(/^["']|["']$/g, '');
147
+ // Skip tokens that look like flags or shell operators
148
+ if (/^[-|;&<>]/.test(cleaned) || cleaned.length === 0) continue;
149
+ try {
150
+ const resolved = expandPath(cleaned);
151
+ for (const bp of blockedPaths) {
152
+ const expandedBp = expandPath(bp);
153
+ if (resolved.startsWith(expandedBp) || resolved === expandedBp) {
154
+ logger.warn(`execute_command blocked: argument "${cleaned}" references restricted path ${bp}`);
155
+ return { error: `Blocked: command references restricted path ${bp}` };
156
+ }
157
+ }
158
+ } catch {
159
+ // Not a valid path — skip
146
160
  }
147
161
  }
148
162
 
@@ -1,11 +1,12 @@
1
1
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
2
2
  import { join } from 'path';
3
3
  import { homedir } from 'os';
4
- import { createInterface } from 'readline';
5
4
  import yaml from 'js-yaml';
6
5
  import dotenv from 'dotenv';
7
6
  import chalk from 'chalk';
7
+ import * as p from '@clack/prompts';
8
8
  import { PROVIDERS } from '../providers/models.js';
9
+ import { handleCancel } from './display.js';
9
10
 
10
11
  const DEFAULTS = {
11
12
  bot: {
@@ -37,7 +38,7 @@ const DEFAULTS = {
37
38
  claude_code: {
38
39
  model: 'claude-opus-4-6',
39
40
  max_turns: 50,
40
- timeout_seconds: 600,
41
+ timeout_seconds: 86400,
41
42
  workspace_dir: null, // defaults to ~/.kernelbot/workspaces
42
43
  auth_mode: 'system', // system | api_key | oauth_token
43
44
  },
@@ -108,10 +109,6 @@ function findConfigFile() {
108
109
  return null;
109
110
  }
110
111
 
111
- function ask(rl, question) {
112
- return new Promise((res) => rl.question(question, res));
113
- }
114
-
115
112
  /**
116
113
  * Migrate legacy `anthropic` config section → `brain` section.
117
114
  */
@@ -132,44 +129,33 @@ function migrateAnthropicConfig(config) {
132
129
  }
133
130
 
134
131
  /**
135
- * Interactive provider → model picker.
132
+ * Interactive provider → model picker using @clack/prompts.
136
133
  */
137
- export async function promptProviderSelection(rl) {
134
+ export async function promptProviderSelection() {
138
135
  const providerKeys = Object.keys(PROVIDERS);
139
136
 
140
- console.log(chalk.bold('\n Select AI provider:\n'));
141
- providerKeys.forEach((key, i) => {
142
- console.log(` ${chalk.cyan(`${i + 1}.`)} ${PROVIDERS[key].name}`);
137
+ const providerKey = await p.select({
138
+ message: 'Select AI provider',
139
+ options: providerKeys.map(key => ({
140
+ value: key,
141
+ label: PROVIDERS[key].name,
142
+ })),
143
143
  });
144
- console.log('');
145
-
146
- let providerIdx;
147
- while (true) {
148
- const input = await ask(rl, chalk.cyan(' Provider (number): '));
149
- providerIdx = parseInt(input.trim(), 10) - 1;
150
- if (providerIdx >= 0 && providerIdx < providerKeys.length) break;
151
- console.log(chalk.dim(' Invalid choice, try again.'));
152
- }
144
+ if (handleCancel(providerKey)) return null;
153
145
 
154
- const providerKey = providerKeys[providerIdx];
155
146
  const provider = PROVIDERS[providerKey];
156
147
 
157
- console.log(chalk.bold(`\n Select model for ${provider.name}:\n`));
158
- provider.models.forEach((m, i) => {
159
- console.log(` ${chalk.cyan(`${i + 1}.`)} ${m.label} (${m.id})`);
148
+ const modelId = await p.select({
149
+ message: `Select model for ${provider.name}`,
150
+ options: provider.models.map(m => ({
151
+ value: m.id,
152
+ label: m.label,
153
+ hint: m.id,
154
+ })),
160
155
  });
161
- console.log('');
156
+ if (handleCancel(modelId)) return null;
162
157
 
163
- let modelIdx;
164
- while (true) {
165
- const input = await ask(rl, chalk.cyan(' Model (number): '));
166
- modelIdx = parseInt(input.trim(), 10) - 1;
167
- if (modelIdx >= 0 && modelIdx < provider.models.length) break;
168
- console.log(chalk.dim(' Invalid choice, try again.'));
169
- }
170
-
171
- const model = provider.models[modelIdx];
172
- return { providerKey, modelId: model.id };
158
+ return { providerKey, modelId };
173
159
  }
174
160
 
175
161
  /**
@@ -252,26 +238,29 @@ export function saveClaudeCodeAuth(config, mode, value) {
252
238
  /**
253
239
  * Full interactive flow: change orchestrator model + optionally enter API key.
254
240
  */
255
- export async function changeOrchestratorModel(config, rl) {
241
+ export async function changeOrchestratorModel(config) {
256
242
  const { createProvider } = await import('../providers/index.js');
257
- const { providerKey, modelId } = await promptProviderSelection(rl);
243
+ const result = await promptProviderSelection();
244
+ if (!result) return config;
258
245
 
246
+ const { providerKey, modelId } = result;
259
247
  const providerDef = PROVIDERS[providerKey];
260
248
 
261
249
  // Resolve API key
262
250
  const envKey = providerDef.envKey;
263
251
  let apiKey = process.env[envKey];
264
252
  if (!apiKey) {
265
- const key = await ask(rl, chalk.cyan(`\n ${providerDef.name} API key (${envKey}): `));
266
- if (!key.trim()) {
267
- console.log(chalk.yellow('\n No API key provided. Orchestrator not changed.\n'));
268
- return config;
269
- }
253
+ const key = await p.text({
254
+ message: `${providerDef.name} API key (${envKey})`,
255
+ validate: (v) => (!v.trim() ? 'API key is required' : undefined),
256
+ });
257
+ if (handleCancel(key)) return config;
270
258
  apiKey = key.trim();
271
259
  }
272
260
 
273
261
  // Validate the new provider before saving anything
274
- console.log(chalk.dim(`\n Verifying ${providerDef.name} / ${modelId}...`));
262
+ const s = p.spinner();
263
+ s.start(`Verifying ${providerDef.name} / ${modelId}`);
275
264
  const testConfig = {
276
265
  brain: {
277
266
  provider: providerKey,
@@ -284,16 +273,15 @@ export async function changeOrchestratorModel(config, rl) {
284
273
  try {
285
274
  const testProvider = createProvider(testConfig);
286
275
  await testProvider.ping();
276
+ s.stop(`${providerDef.name} / ${modelId} verified`);
287
277
  } catch (err) {
288
- console.log(chalk.red(`\n ✖ Verification failed: ${err.message}`));
289
- console.log(chalk.yellow(` Orchestrator not changed. Keeping current model.\n`));
278
+ s.stop(chalk.red(`Verification failed: ${err.message}`));
279
+ p.log.warn('Orchestrator not changed. Keeping current model.');
290
280
  return config;
291
281
  }
292
282
 
293
283
  // Validation passed — save everything
294
284
  const savedPath = saveOrchestratorToYaml(providerKey, modelId);
295
- console.log(chalk.dim(` Saved to ${savedPath}`));
296
-
297
285
  config.orchestrator.provider = providerKey;
298
286
  config.orchestrator.model = modelId;
299
287
  config.orchestrator.api_key = apiKey;
@@ -301,50 +289,51 @@ export async function changeOrchestratorModel(config, rl) {
301
289
  // Save the key if it was newly entered
302
290
  if (!process.env[envKey]) {
303
291
  saveCredential(config, envKey, apiKey);
304
- console.log(chalk.dim(' API key saved.\n'));
305
292
  }
306
293
 
307
- console.log(chalk.green(`Orchestrator switched to ${providerDef.name} / ${modelId}\n`));
294
+ p.log.success(`Orchestrator switched to ${providerDef.name} / ${modelId}`);
308
295
  return config;
309
296
  }
310
297
 
311
298
  /**
312
299
  * Full interactive flow: change brain model + optionally enter API key.
313
300
  */
314
- export async function changeBrainModel(config, rl) {
301
+ export async function changeBrainModel(config) {
315
302
  const { createProvider } = await import('../providers/index.js');
316
- const { providerKey, modelId } = await promptProviderSelection(rl);
303
+ const result = await promptProviderSelection();
304
+ if (!result) return config;
317
305
 
306
+ const { providerKey, modelId } = result;
318
307
  const providerDef = PROVIDERS[providerKey];
319
308
 
320
309
  // Resolve API key
321
310
  const envKey = providerDef.envKey;
322
311
  let apiKey = process.env[envKey];
323
312
  if (!apiKey) {
324
- const key = await ask(rl, chalk.cyan(`\n ${providerDef.name} API key (${envKey}): `));
325
- if (!key.trim()) {
326
- console.log(chalk.yellow('\n No API key provided. Brain not changed.\n'));
327
- return config;
328
- }
313
+ const key = await p.text({
314
+ message: `${providerDef.name} API key (${envKey})`,
315
+ validate: (v) => (!v.trim() ? 'API key is required' : undefined),
316
+ });
317
+ if (handleCancel(key)) return config;
329
318
  apiKey = key.trim();
330
319
  }
331
320
 
332
321
  // Validate the new provider before saving anything
333
- console.log(chalk.dim(`\n Verifying ${providerDef.name} / ${modelId}...`));
322
+ const s = p.spinner();
323
+ s.start(`Verifying ${providerDef.name} / ${modelId}`);
334
324
  const testConfig = { ...config, brain: { ...config.brain, provider: providerKey, model: modelId, api_key: apiKey } };
335
325
  try {
336
326
  const testProvider = createProvider(testConfig);
337
327
  await testProvider.ping();
328
+ s.stop(`${providerDef.name} / ${modelId} verified`);
338
329
  } catch (err) {
339
- console.log(chalk.red(`\n ✖ Verification failed: ${err.message}`));
340
- console.log(chalk.yellow(` Brain not changed. Keeping current model.\n`));
330
+ s.stop(chalk.red(`Verification failed: ${err.message}`));
331
+ p.log.warn('Brain not changed. Keeping current model.');
341
332
  return config;
342
333
  }
343
334
 
344
335
  // Validation passed — save everything
345
- const savedPath = saveProviderToYaml(providerKey, modelId);
346
- console.log(chalk.dim(` Saved to ${savedPath}`));
347
-
336
+ saveProviderToYaml(providerKey, modelId);
348
337
  config.brain.provider = providerKey;
349
338
  config.brain.model = modelId;
350
339
  config.brain.api_key = apiKey;
@@ -352,10 +341,9 @@ export async function changeBrainModel(config, rl) {
352
341
  // Save the key if it was newly entered
353
342
  if (!process.env[envKey]) {
354
343
  saveCredential(config, envKey, apiKey);
355
- console.log(chalk.dim(' API key saved.\n'));
356
344
  }
357
345
 
358
- console.log(chalk.green(`Brain switched to ${providerDef.name} / ${modelId}\n`));
346
+ p.log.success(`Brain switched to ${providerDef.name} / ${modelId}`);
359
347
  return config;
360
348
  }
361
349
 
@@ -366,9 +354,8 @@ async function promptForMissing(config) {
366
354
 
367
355
  if (missing.length === 0) return config;
368
356
 
369
- console.log(chalk.yellow('\n Missing credentials detected. Let\'s set them up.\n'));
357
+ p.log.warn('Missing credentials detected. Let\'s set them up.');
370
358
 
371
- const rl = createInterface({ input: process.stdin, output: process.stdout });
372
359
  const mutableConfig = JSON.parse(JSON.stringify(config));
373
360
  const envLines = [];
374
361
 
@@ -381,8 +368,11 @@ async function promptForMissing(config) {
381
368
 
382
369
  if (!mutableConfig.brain.api_key) {
383
370
  // Run brain provider selection flow
384
- console.log(chalk.bold('\n 🧠 Worker Brain'));
385
- const { providerKey, modelId } = await promptProviderSelection(rl);
371
+ p.log.step('Worker Brain');
372
+ const brainResult = await promptProviderSelection();
373
+ if (!brainResult) { p.cancel('Setup cancelled.'); process.exit(0); }
374
+
375
+ const { providerKey, modelId } = brainResult;
386
376
  mutableConfig.brain.provider = providerKey;
387
377
  mutableConfig.brain.model = modelId;
388
378
  saveProviderToYaml(providerKey, modelId);
@@ -390,36 +380,49 @@ async function promptForMissing(config) {
390
380
  const providerDef = PROVIDERS[providerKey];
391
381
  const envKey = providerDef.envKey;
392
382
 
393
- const key = await ask(rl, chalk.cyan(`\n ${providerDef.name} API key: `));
383
+ const key = await p.text({
384
+ message: `${providerDef.name} API key`,
385
+ validate: (v) => (!v.trim() ? 'API key is required' : undefined),
386
+ });
387
+ if (handleCancel(key)) { process.exit(0); }
394
388
  mutableConfig.brain.api_key = key.trim();
395
389
  envLines.push(`${envKey}=${key.trim()}`);
396
390
 
397
391
  // Orchestrator provider selection
398
- console.log(chalk.bold('\n 🎛️ Orchestrator'));
399
- const sameChoice = await ask(rl, chalk.cyan(` Use same provider (${providerDef.name} / ${modelId}) for orchestrator? [Y/n]: `));
400
- if (!sameChoice.trim() || sameChoice.trim().toLowerCase() === 'y') {
392
+ p.log.step('Orchestrator');
393
+ const sameChoice = await p.confirm({
394
+ message: `Use same provider (${providerDef.name} / ${modelId}) for orchestrator?`,
395
+ initialValue: true,
396
+ });
397
+ if (handleCancel(sameChoice)) { process.exit(0); }
398
+
399
+ if (sameChoice) {
401
400
  mutableConfig.orchestrator.provider = providerKey;
402
401
  mutableConfig.orchestrator.model = modelId;
403
402
  mutableConfig.orchestrator.api_key = key.trim();
404
403
  saveOrchestratorToYaml(providerKey, modelId);
405
404
  } else {
406
- const orch = await promptProviderSelection(rl);
405
+ const orch = await promptProviderSelection();
406
+ if (!orch) { p.cancel('Setup cancelled.'); process.exit(0); }
407
+
407
408
  mutableConfig.orchestrator.provider = orch.providerKey;
408
409
  mutableConfig.orchestrator.model = orch.modelId;
409
410
  saveOrchestratorToYaml(orch.providerKey, orch.modelId);
410
411
 
411
412
  const orchProviderDef = PROVIDERS[orch.providerKey];
412
413
  if (orch.providerKey === providerKey) {
413
- // Same provider — reuse the API key
414
414
  mutableConfig.orchestrator.api_key = key.trim();
415
415
  } else {
416
- // Different provider — need a separate key
417
416
  const orchEnvKey = orchProviderDef.envKey;
418
417
  const orchExisting = process.env[orchEnvKey];
419
418
  if (orchExisting) {
420
419
  mutableConfig.orchestrator.api_key = orchExisting;
421
420
  } else {
422
- const orchKey = await ask(rl, chalk.cyan(`\n ${orchProviderDef.name} API key: `));
421
+ const orchKey = await p.text({
422
+ message: `${orchProviderDef.name} API key`,
423
+ validate: (v) => (!v.trim() ? 'API key is required' : undefined),
424
+ });
425
+ if (handleCancel(orchKey)) { process.exit(0); }
423
426
  mutableConfig.orchestrator.api_key = orchKey.trim();
424
427
  envLines.push(`${orchEnvKey}=${orchKey.trim()}`);
425
428
  }
@@ -428,13 +431,15 @@ async function promptForMissing(config) {
428
431
  }
429
432
 
430
433
  if (!mutableConfig.telegram.bot_token) {
431
- const token = await ask(rl, chalk.cyan(' Telegram Bot Token: '));
434
+ const token = await p.text({
435
+ message: 'Telegram Bot Token',
436
+ validate: (v) => (!v.trim() ? 'Token is required' : undefined),
437
+ });
438
+ if (handleCancel(token)) { process.exit(0); }
432
439
  mutableConfig.telegram.bot_token = token.trim();
433
440
  envLines.push(`TELEGRAM_BOT_TOKEN=${token.trim()}`);
434
441
  }
435
442
 
436
- rl.close();
437
-
438
443
  // Save to ~/.kernelbot/.env so it persists globally
439
444
  if (envLines.length > 0) {
440
445
  const configDir = getConfigDir();
@@ -444,9 +449,8 @@ async function promptForMissing(config) {
444
449
  // Merge with existing content
445
450
  let content = existingEnv ? existingEnv.trimEnd() + '\n' : '';
446
451
  for (const line of envLines) {
447
- const key = line.split('=')[0];
448
- // Replace if exists, append if not
449
- const regex = new RegExp(`^${key}=.*$`, 'm');
452
+ const envKey = line.split('=')[0];
453
+ const regex = new RegExp(`^${envKey}=.*$`, 'm');
450
454
  if (regex.test(content)) {
451
455
  content = content.replace(regex, line);
452
456
  } else {
@@ -454,7 +458,7 @@ async function promptForMissing(config) {
454
458
  }
455
459
  }
456
460
  writeFileSync(savePath, content);
457
- console.log(chalk.dim(`\n Saved to ${savePath}\n`));
461
+ p.log.info(`Saved to ${savePath}`);
458
462
  }
459
463
 
460
464
  return mutableConfig;
@@ -1,19 +1,34 @@
1
- import { readFileSync } from 'fs';
2
- import { join, dirname } from 'path';
3
- import { fileURLToPath } from 'url';
4
- import chalk from 'chalk';
5
- import ora from 'ora';
6
- import boxen from 'boxen';
7
- import gradient from 'gradient-string';
1
+ import { readFileSync } from "fs";
2
+ import { join, dirname } from "path";
3
+ import { fileURLToPath } from "url";
4
+ import { networkInterfaces } from "os";
5
+ import chalk from "chalk";
6
+ import ora from "ora";
7
+ import boxen from "boxen";
8
+ import gradient from "gradient-string";
9
+ import * as p from "@clack/prompts";
10
+ import { PROVIDERS } from "../providers/models.js";
11
+
12
+ export { p };
13
+
14
+ function getLocalIp() {
15
+ const nets = networkInterfaces();
16
+ for (const iface of Object.values(nets)) {
17
+ for (const info of iface) {
18
+ if (info.family === "IPv4" && !info.internal) return info.address;
19
+ }
20
+ }
21
+ return "localhost";
22
+ }
8
23
 
9
24
  const __dirname = dirname(fileURLToPath(import.meta.url));
10
25
 
11
26
  function getVersion() {
12
27
  try {
13
- const pkg = JSON.parse(readFileSync(join(__dirname, '../../package.json'), 'utf-8'));
28
+ const pkg = JSON.parse(readFileSync(join(__dirname, "../../package.json"), "utf-8"));
14
29
  return pkg.version;
15
30
  } catch {
16
- return 'unknown';
31
+ return "unknown";
17
32
  }
18
33
  }
19
34
 
@@ -38,38 +53,14 @@ const LOGO = `
38
53
  `;
39
54
 
40
55
  // Green terminal gradient
41
- const monoGradient = gradient([
42
- '#00ff41',
43
- '#00cc33',
44
- '#009926',
45
- '#006619',
46
- ]);
56
+ const monoGradient = gradient(["#00ff41", "#00cc33", "#009926", "#006619"]);
47
57
 
48
58
  export function showLogo() {
49
59
  console.log(monoGradient.multiline(LOGO));
50
- console.log(chalk.dim(` AI Engineering Agent — v${getVersion()}\n`));
51
- console.log(
52
- boxen(
53
- chalk.yellow.bold('WARNING') +
54
- chalk.yellow(
55
- '\n\nKernelBot has full access to your operating system.\n' +
56
- 'It can execute commands, read/write files, manage processes,\n' +
57
- 'and interact with external services on your behalf.\n\n' +
58
- 'Only run this on machines you control.\n' +
59
- 'Set OWNER_TELEGRAM_ID in .env or allowed_users in config.yaml.',
60
- ),
61
- {
62
- padding: 1,
63
- borderStyle: 'round',
64
- borderColor: 'yellow',
65
- },
66
- ),
67
- );
68
- console.log('');
69
60
  }
70
61
 
71
62
  export async function showStartupCheck(label, checkFn) {
72
- const spinner = ora({ text: label, color: 'cyan' }).start();
63
+ const spinner = ora({ text: label, color: "cyan" }).start();
73
64
  try {
74
65
  await checkFn();
75
66
  spinner.succeed(chalk.green(label));
@@ -82,11 +73,11 @@ export async function showStartupCheck(label, checkFn) {
82
73
 
83
74
  export function showStartupComplete() {
84
75
  console.log(
85
- boxen(chalk.green.bold('KernelBot is live'), {
76
+ boxen(chalk.green.bold("KernelBot is live"), {
86
77
  padding: 1,
87
78
  margin: { top: 1 },
88
- borderStyle: 'round',
89
- borderColor: 'green',
79
+ borderStyle: "round",
80
+ borderColor: "green",
90
81
  }),
91
82
  );
92
83
  }
@@ -95,8 +86,8 @@ export function showSuccess(msg) {
95
86
  console.log(
96
87
  boxen(chalk.green(msg), {
97
88
  padding: 1,
98
- borderStyle: 'round',
99
- borderColor: 'green',
89
+ borderStyle: "round",
90
+ borderColor: "green",
100
91
  }),
101
92
  );
102
93
  }
@@ -105,14 +96,14 @@ export function showError(msg) {
105
96
  console.log(
106
97
  boxen(chalk.red(msg), {
107
98
  padding: 1,
108
- borderStyle: 'round',
109
- borderColor: 'red',
99
+ borderStyle: "round",
100
+ borderColor: "red",
110
101
  }),
111
102
  );
112
103
  }
113
104
 
114
105
  export function createSpinner(text) {
115
- return ora({ text, color: 'cyan' });
106
+ return ora({ text, color: "cyan" });
116
107
  }
117
108
 
118
109
  /**
@@ -121,47 +112,108 @@ export function createSpinner(text) {
121
112
  * @param {boolean} isActive — whether this is the currently active character
122
113
  */
123
114
  export function showCharacterCard(character, isActive = false) {
124
- const art = character.asciiArt || '';
125
- const activeTag = isActive ? chalk.green(' (active)') : '';
115
+ const art = character.asciiArt || "";
116
+ const activeTag = isActive ? chalk.green(" (active)") : "";
126
117
  const content = [
127
118
  `${character.emoji} ${chalk.bold(character.name)}${activeTag}`,
128
119
  chalk.dim(`"${character.tagline}"`),
129
- '',
130
- ...(art ? art.split('\n').map(line => chalk.cyan(line)) : []),
131
- '',
132
- chalk.dim(`Origin: ${character.origin || 'Unknown'}`),
133
- chalk.dim(`Style: ${character.age || 'Unknown'}`),
134
- ].join('\n');
120
+ "",
121
+ ...(art ? art.split("\n").map((line) => chalk.cyan(line)) : []),
122
+ "",
123
+ chalk.dim(`Origin: ${character.origin || "Unknown"}`),
124
+ chalk.dim(`Age: ${character.age || "Unknown"}`),
125
+ ].join("\n");
135
126
 
136
127
  console.log(
137
128
  boxen(content, {
138
129
  padding: 1,
139
- borderStyle: 'round',
140
- borderColor: isActive ? 'green' : 'cyan',
130
+ borderStyle: "round",
131
+ borderColor: isActive ? "green" : "cyan",
141
132
  }),
142
133
  );
143
134
  }
144
135
 
145
136
  /**
146
- * Display the full character gallery for CLI selection.
147
- * @param {object[]} charactersarray of character profiles
148
- * @param {string|null} activeId — ID of the currently active character
137
+ * Format "Provider / model" label for a config section.
138
+ * @param {object} configfull config
139
+ * @param {'brain'|'orchestrator'} section
140
+ */
141
+ export function formatProviderLabel(config, section) {
142
+ const sec = config[section];
143
+ const providerDef = PROVIDERS[sec.provider];
144
+ const name = providerDef ? providerDef.name : sec.provider;
145
+ return `${name} / ${sec.model}`;
146
+ }
147
+
148
+ /**
149
+ * Centralized cancel handler for @clack/prompts.
150
+ * Call after every prompt — exits gracefully on Ctrl+C.
151
+ */
152
+ export function handleCancel(value) {
153
+ if (p.isCancel(value)) {
154
+ p.cancel("Cancelled.");
155
+ return true;
156
+ }
157
+ return false;
158
+ }
159
+
160
+ /**
161
+ * Claude Code-inspired info box shown after the logo.
149
162
  */
163
+ export function showWelcomeScreen(config, characterManager) {
164
+ const version = getVersion();
165
+
166
+ const orchLabel = formatProviderLabel(config, "orchestrator");
167
+ const brainLabel = formatProviderLabel(config, "brain");
168
+
169
+ let charLabel = "None";
170
+ if (characterManager) {
171
+ const activeId = characterManager.getActiveCharacterId();
172
+ const active = characterManager.getCharacter(activeId);
173
+ if (active) charLabel = `${active.emoji} ${active.name}`;
174
+ }
175
+
176
+ const lifeEnabled = config.life?.enabled !== false;
177
+ const dashPort = config.dashboard?.port || 3000;
178
+ const dashEnabled = config.dashboard?.enabled;
179
+
180
+ const pad = (label, width = 18) => label.padEnd(width);
181
+
182
+ const lines = [
183
+ "",
184
+ ` ${chalk.dim(pad("Orchestrator"))}${orchLabel}`,
185
+ ` ${chalk.dim(pad("Brain"))}${brainLabel}`,
186
+ ` ${chalk.dim(pad("Character"))}${charLabel}`,
187
+ ` ${chalk.dim(pad("Life Engine"))}${lifeEnabled ? chalk.green("enabled") : chalk.yellow("disabled")}`,
188
+ ` ${chalk.dim(pad("Dashboard"))}${dashEnabled ? chalk.green(`http://${getLocalIp()}:${dashPort}/`) : chalk.yellow("off")}`,
189
+ "",
190
+ chalk.dim(" ↑↓ Navigate · Enter Select · Ctrl+C Cancel"),
191
+ ];
192
+
193
+ console.log(
194
+ boxen(lines.join("\n"), {
195
+ title: `KERNEL Bot v${version}`,
196
+ titleAlignment: "left",
197
+ padding: { top: 0, bottom: 0, left: 0, right: 2 },
198
+ borderStyle: "round",
199
+ borderColor: "green",
200
+ }),
201
+ );
202
+ }
203
+
150
204
  export function showCharacterGallery(characters, activeId = null) {
151
- console.log('');
205
+ console.log("");
152
206
  console.log(
153
- gradient(['#ff6b6b', '#feca57', '#48dbfb', '#ff9ff3']).multiline(
154
- ' ═══════════════════════════════\n' +
155
- ' CHOOSE YOUR CHARACTER\n' +
156
- ' ═══════════════════════════════',
207
+ gradient(["#ff6b6b", "#feca57", "#48dbfb", "#ff9ff3"]).multiline(
208
+ " ═══════════════════════════════\n" + " CHOOSE YOUR CHARACTER\n" + " ═══════════════════════════════",
157
209
  ),
158
210
  );
159
- console.log('');
160
- console.log(chalk.dim(' Each character has their own personality,'));
161
- console.log(chalk.dim(' memories, and story that evolves with you.'));
162
- console.log('');
211
+ console.log("");
212
+ console.log(chalk.dim(" Each character has their own personality,"));
213
+ console.log(chalk.dim(" memories, and story that evolves with you."));
214
+ console.log("");
163
215
 
164
216
  for (const c of characters) {
165
217
  showCharacterCard(c, c.id === activeId);
166
218
  }
167
- }
219
+ }
@@ -16,7 +16,7 @@ export function createLogger(config) {
16
16
  format: winston.format.combine(
17
17
  winston.format.colorize(),
18
18
  winston.format.printf(({ level, message, timestamp }) => {
19
- return `[KernelBot] ${level}: ${message}`;
19
+ return `${timestamp} [KernelBot] ${level}: ${message}`;
20
20
  }),
21
21
  ),
22
22
  }),