kernelbot 1.0.38 → 1.0.39

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/bin/kernel.js CHANGED
@@ -4,11 +4,11 @@
4
4
  process.removeAllListeners('warning');
5
5
  process.on('warning', (w) => { if (w.name !== 'DeprecationWarning' || !w.message.includes('punycode')) console.warn(w); });
6
6
 
7
- import { createInterface } from 'readline';
8
7
  import { readFileSync, existsSync } from 'fs';
9
8
  import { join } from 'path';
10
9
  import { homedir } from 'os';
11
10
  import chalk from 'chalk';
11
+ import * as p from '@clack/prompts';
12
12
  import { loadConfig, loadConfigInteractive, changeBrainModel, changeOrchestratorModel, saveDashboardToYaml } from '../src/utils/config.js';
13
13
  import { createLogger, getLogger } from '../src/utils/logger.js';
14
14
  import {
@@ -16,8 +16,10 @@ import {
16
16
  showStartupCheck,
17
17
  showStartupComplete,
18
18
  showError,
19
- showCharacterGallery,
20
19
  showCharacterCard,
20
+ showWelcomeScreen,
21
+ handleCancel,
22
+ formatProviderLabel,
21
23
  } from '../src/utils/display.js';
22
24
  import { createAuditLogger } from '../src/security/audit.js';
23
25
  import { CharacterBuilder } from '../src/characters/builder.js';
@@ -38,112 +40,34 @@ import {
38
40
  deleteCustomSkill,
39
41
  } from '../src/skills/custom.js';
40
42
 
41
- function showMenu(config) {
42
- const orchProviderDef = PROVIDERS[config.orchestrator.provider];
43
- const orchProviderName = orchProviderDef ? orchProviderDef.name : config.orchestrator.provider;
44
- const orchModelId = config.orchestrator.model;
45
-
46
- const providerDef = PROVIDERS[config.brain.provider];
47
- const providerName = providerDef ? providerDef.name : config.brain.provider;
48
- const modelId = config.brain.model;
49
-
50
- console.log('');
51
- console.log(chalk.dim(` Current orchestrator: ${orchProviderName} / ${orchModelId}`));
52
- console.log(chalk.dim(` Current brain: ${providerName} / ${modelId}`));
53
- console.log('');
54
- console.log(chalk.bold(' What would you like to do?\n'));
55
- console.log(` ${chalk.cyan('1.')} Start bot`);
56
- console.log(` ${chalk.cyan('2.')} Check connections`);
57
- console.log(` ${chalk.cyan('3.')} View logs`);
58
- console.log(` ${chalk.cyan('4.')} View audit logs`);
59
- console.log(` ${chalk.cyan('5.')} Change brain model`);
60
- console.log(` ${chalk.cyan('6.')} Change orchestrator model`);
61
- console.log(` ${chalk.cyan('7.')} Manage custom skills`);
62
- console.log(` ${chalk.cyan('8.')} Manage automations`);
63
- console.log(` ${chalk.cyan('9.')} Switch character`);
64
- console.log(` ${chalk.cyan('10.')} Link LinkedIn account`);
65
- console.log(` ${chalk.cyan('11.')} Dashboard`);
66
- console.log(` ${chalk.cyan('12.')} Exit`);
67
- console.log('');
68
- }
69
-
70
- function ask(rl, question) {
71
- return new Promise((res) => rl.question(question, res));
72
- }
73
-
74
43
  /**
75
44
  * Register SIGINT/SIGTERM handlers to shut down the bot cleanly.
76
- * Stops polling, cancels running jobs, persists conversations,
77
- * disarms automations, stops the life engine, and clears intervals.
78
45
  */
79
46
  function setupGracefulShutdown({ bot, lifeEngine, automationManager, jobManager, conversationManager, intervals, dashboardHandle }) {
80
47
  let shuttingDown = false;
81
48
 
82
49
  const shutdown = async (signal) => {
83
- if (shuttingDown) return; // prevent double-shutdown
50
+ if (shuttingDown) return;
84
51
  shuttingDown = true;
85
52
 
86
53
  const logger = getLogger();
87
54
  logger.info(`[Shutdown] ${signal} received — shutting down gracefully...`);
88
55
 
89
- // 1. Stop Telegram polling so no new messages arrive
90
- try {
91
- bot.stopPolling();
92
- logger.info('[Shutdown] Telegram polling stopped');
93
- } catch (err) {
94
- logger.error(`[Shutdown] Failed to stop polling: ${err.message}`);
95
- }
96
-
97
- // 2. Stop life engine heartbeat
98
- try {
99
- lifeEngine.stop();
100
- logger.info('[Shutdown] Life engine stopped');
101
- } catch (err) {
102
- logger.error(`[Shutdown] Failed to stop life engine: ${err.message}`);
103
- }
104
-
105
- // 3. Disarm all automation timers
106
- try {
107
- automationManager.shutdown();
108
- logger.info('[Shutdown] Automation timers cancelled');
109
- } catch (err) {
110
- logger.error(`[Shutdown] Failed to shutdown automations: ${err.message}`);
111
- }
56
+ try { bot.stopPolling(); logger.info('[Shutdown] Telegram polling stopped'); } catch (err) { logger.error(`[Shutdown] Failed to stop polling: ${err.message}`); }
57
+ try { lifeEngine.stop(); logger.info('[Shutdown] Life engine stopped'); } catch (err) { logger.error(`[Shutdown] Failed to stop life engine: ${err.message}`); }
58
+ try { automationManager.shutdown(); logger.info('[Shutdown] Automation timers cancelled'); } catch (err) { logger.error(`[Shutdown] Failed to shutdown automations: ${err.message}`); }
112
59
 
113
- // 4. Cancel all running jobs
114
60
  try {
115
61
  const running = [...jobManager.jobs.values()].filter(j => !j.isTerminal);
116
- for (const job of running) {
117
- jobManager.cancelJob(job.id);
118
- }
119
- if (running.length > 0) {
120
- logger.info(`[Shutdown] Cancelled ${running.length} running job(s)`);
121
- }
122
- } catch (err) {
123
- logger.error(`[Shutdown] Failed to cancel jobs: ${err.message}`);
124
- }
62
+ for (const job of running) jobManager.cancelJob(job.id);
63
+ if (running.length > 0) logger.info(`[Shutdown] Cancelled ${running.length} running job(s)`);
64
+ } catch (err) { logger.error(`[Shutdown] Failed to cancel jobs: ${err.message}`); }
125
65
 
126
- // 5. Persist conversations to disk
127
- try {
128
- conversationManager.save();
129
- logger.info('[Shutdown] Conversations saved');
130
- } catch (err) {
131
- logger.error(`[Shutdown] Failed to save conversations: ${err.message}`);
132
- }
66
+ try { conversationManager.save(); logger.info('[Shutdown] Conversations saved'); } catch (err) { logger.error(`[Shutdown] Failed to save conversations: ${err.message}`); }
67
+ try { dashboardHandle?.stop(); } catch (err) { logger.error(`[Shutdown] Failed to stop dashboard: ${err.message}`); }
133
68
 
134
- // 6. Stop dashboard
135
- try {
136
- dashboardHandle?.stop();
137
- } catch (err) {
138
- logger.error(`[Shutdown] Failed to stop dashboard: ${err.message}`);
139
- }
140
-
141
- // 7. Clear periodic intervals
142
- for (const id of intervals) {
143
- clearInterval(id);
144
- }
69
+ for (const id of intervals) clearInterval(id);
145
70
  logger.info('[Shutdown] Periodic timers cleared');
146
-
147
71
  logger.info('[Shutdown] Graceful shutdown complete');
148
72
  process.exit(0);
149
73
  };
@@ -158,33 +82,33 @@ function viewLog(filename) {
158
82
  join(homedir(), '.kernelbot', filename),
159
83
  ];
160
84
 
161
- for (const p of paths) {
162
- if (existsSync(p)) {
163
- const content = readFileSync(p, 'utf-8');
85
+ for (const logPath of paths) {
86
+ if (existsSync(logPath)) {
87
+ const content = readFileSync(logPath, 'utf-8');
164
88
  const lines = content.split('\n').filter(Boolean);
165
89
  const recent = lines.slice(-30);
166
- console.log(chalk.dim(`\n Showing last ${recent.length} entries from ${p}\n`));
167
- for (const line of recent) {
90
+
91
+ const formatted = recent.map(line => {
168
92
  try {
169
93
  const entry = JSON.parse(line);
170
94
  const time = entry.timestamp || '';
171
95
  const level = entry.level || '';
172
96
  const msg = entry.message || '';
173
97
  const color = level === 'error' ? chalk.red : level === 'warn' ? chalk.yellow : chalk.dim;
174
- console.log(` ${chalk.dim(time)} ${color(level)} ${msg}`);
98
+ return `${chalk.dim(time)} ${color(level)} ${msg}`;
175
99
  } catch {
176
- console.log(` ${line}`);
100
+ return line;
177
101
  }
178
- }
179
- console.log('');
102
+ }).join('\n');
103
+
104
+ p.note(formatted, `Last ${recent.length} entries from ${logPath}`);
180
105
  return;
181
106
  }
182
107
  }
183
- console.log(chalk.dim(`\n No ${filename} found yet.\n`));
108
+ p.log.info(`No ${filename} found yet.`);
184
109
  }
185
110
 
186
111
  async function runCheck(config) {
187
- // Orchestrator check
188
112
  const orchProviderKey = config.orchestrator.provider || 'anthropic';
189
113
  const orchProviderDef = PROVIDERS[orchProviderKey];
190
114
  const orchLabel = orchProviderDef ? orchProviderDef.name : orchProviderKey;
@@ -207,7 +131,6 @@ async function runCheck(config) {
207
131
  await provider.ping();
208
132
  });
209
133
 
210
- // Worker brain check
211
134
  const providerDef = PROVIDERS[config.brain.provider];
212
135
  const providerLabel = providerDef ? providerDef.name : config.brain.provider;
213
136
  const envKeyLabel = providerDef ? providerDef.envKey : 'API_KEY';
@@ -226,14 +149,12 @@ async function runCheck(config) {
226
149
  });
227
150
 
228
151
  await showStartupCheck('Telegram Bot API', async () => {
229
- const res = await fetch(
230
- `https://api.telegram.org/bot${config.telegram.bot_token}/getMe`,
231
- );
152
+ const res = await fetch(`https://api.telegram.org/bot${config.telegram.bot_token}/getMe`);
232
153
  const data = await res.json();
233
154
  if (!data.ok) throw new Error(data.description || 'Invalid token');
234
155
  });
235
156
 
236
- console.log(chalk.green('\n All checks passed.\n'));
157
+ p.log.success('All checks passed.');
237
158
  }
238
159
 
239
160
  async function startBotFlow(config) {
@@ -245,7 +166,6 @@ async function startBotFlow(config) {
245
166
 
246
167
  const checks = [];
247
168
 
248
- // Orchestrator check — dynamic provider
249
169
  const orchProviderKey = config.orchestrator.provider || 'anthropic';
250
170
  const orchProviderDef = PROVIDERS[orchProviderKey];
251
171
  const orchLabel = orchProviderDef ? orchProviderDef.name : orchProviderKey;
@@ -267,7 +187,6 @@ async function startBotFlow(config) {
267
187
  }),
268
188
  );
269
189
 
270
- // Worker brain check
271
190
  checks.push(
272
191
  await showStartupCheck(`Worker (${providerLabel}) API`, async () => {
273
192
  const provider = createProvider(config);
@@ -277,9 +196,7 @@ async function startBotFlow(config) {
277
196
 
278
197
  checks.push(
279
198
  await showStartupCheck('Telegram Bot API', async () => {
280
- const res = await fetch(
281
- `https://api.telegram.org/bot${config.telegram.bot_token}/getMe`,
282
- );
199
+ const res = await fetch(`https://api.telegram.org/bot${config.telegram.bot_token}/getMe`);
283
200
  const data = await res.json();
284
201
  if (!data.ok) throw new Error(data.description || 'Invalid token');
285
202
  }),
@@ -290,11 +207,7 @@ async function startBotFlow(config) {
290
207
  return false;
291
208
  }
292
209
 
293
- // Character system — manages multiple personas with isolated data
294
210
  const characterManager = new CharacterManager();
295
-
296
- // Install built-in characters if needed (fresh install or missing builtins).
297
- // Onboarding flag stays true until user picks a character via Telegram.
298
211
  if (characterManager.needsOnboarding) {
299
212
  characterManager.installAllBuiltins();
300
213
  }
@@ -310,8 +223,6 @@ async function startBotFlow(config) {
310
223
  });
311
224
 
312
225
  const automationManager = new AutomationManager();
313
-
314
- // Life system managers — scoped to active character
315
226
  const codebaseKnowledge = new CodebaseKnowledge({ config });
316
227
 
317
228
  const agent = new Agent({
@@ -323,13 +234,9 @@ async function startBotFlow(config) {
323
234
  characterManager,
324
235
  });
325
236
 
326
- // Load character context into agent (sets persona, name, etc.)
327
237
  agent.loadCharacter(activeCharacterId);
328
-
329
- // Wire codebase knowledge to agent for LLM-powered scanning
330
238
  codebaseKnowledge.setAgent(agent);
331
239
 
332
- // Life Engine — autonomous inner life (scoped to active character)
333
240
  const lifeEngine = new LifeEngine({
334
241
  config, agent,
335
242
  memoryManager: charCtx.memoryManager,
@@ -351,7 +258,6 @@ async function startBotFlow(config) {
351
258
  selfManager: charCtx.selfManager,
352
259
  };
353
260
 
354
- // Optional cyberpunk terminal dashboard (must init before startBot so handle is available)
355
261
  let dashboardHandle = null;
356
262
  if (config.dashboard?.enabled) {
357
263
  const { startDashboard } = await import('../src/dashboard/server.js');
@@ -371,14 +277,12 @@ async function startBotFlow(config) {
371
277
  dashboardDeps,
372
278
  });
373
279
 
374
- // Periodic job cleanup and timeout enforcement
375
280
  const cleanupMs = (config.swarm.cleanup_interval_minutes || 30) * 60 * 1000;
376
281
  const cleanupInterval = setInterval(() => {
377
282
  jobManager.cleanup();
378
283
  jobManager.enforceTimeouts();
379
- }, Math.min(cleanupMs, 60_000)); // enforce timeouts every minute at most
284
+ }, Math.min(cleanupMs, 60_000));
380
285
 
381
- // Periodic memory pruning (daily)
382
286
  const retentionDays = config.life?.memory_retention_days || 90;
383
287
  const pruneInterval = setInterval(() => {
384
288
  charCtx.memoryManager.pruneOld(retentionDays);
@@ -387,7 +291,6 @@ async function startBotFlow(config) {
387
291
 
388
292
  showStartupComplete();
389
293
 
390
- // Start life engine if enabled
391
294
  const lifeEnabled = config.life?.enabled !== false;
392
295
  if (lifeEnabled) {
393
296
  logger.info('[Startup] Life engine enabled — waking up...');
@@ -396,10 +299,9 @@ async function startBotFlow(config) {
396
299
  logger.info('[Startup] Life engine running');
397
300
  }).catch(err => {
398
301
  logger.error(`[Startup] Life engine wake-up failed: ${err.message}`);
399
- lifeEngine.start(); // still start heartbeat even if wake-up fails
302
+ lifeEngine.start();
400
303
  });
401
304
 
402
- // Initial codebase scan (background, non-blocking)
403
305
  if (config.life?.self_coding?.enabled) {
404
306
  codebaseKnowledge.scanChanged().then(count => {
405
307
  if (count > 0) logger.info(`[Startup] Codebase scan: ${count} files indexed`);
@@ -411,7 +313,6 @@ async function startBotFlow(config) {
411
313
  logger.info('[Startup] Life engine disabled');
412
314
  }
413
315
 
414
- // Register graceful shutdown handlers
415
316
  setupGracefulShutdown({
416
317
  bot, lifeEngine, automationManager, jobManager,
417
318
  conversationManager, intervals: [cleanupInterval, pruneInterval],
@@ -421,155 +322,134 @@ async function startBotFlow(config) {
421
322
  return true;
422
323
  }
423
324
 
424
- async function manageCustomSkills(rl) {
325
+ async function manageCustomSkills() {
425
326
  loadCustomSkills();
426
327
 
427
328
  let managing = true;
428
329
  while (managing) {
429
330
  const customs = getCustomSkills();
430
- console.log('');
431
- console.log(chalk.bold(' Custom Skills\n'));
432
- console.log(` ${chalk.cyan('1.')} Create new skill`);
433
- console.log(` ${chalk.cyan('2.')} List skills (${customs.length})`);
434
- console.log(` ${chalk.cyan('3.')} Delete a skill`);
435
- console.log(` ${chalk.cyan('4.')} Back`);
436
- console.log('');
437
-
438
- const choice = await ask(rl, chalk.cyan(' > '));
439
- switch (choice.trim()) {
440
- case '1': {
441
- const name = await ask(rl, chalk.cyan(' Skill name: '));
442
- if (!name.trim()) {
443
- console.log(chalk.dim(' Cancelled.\n'));
444
- break;
445
- }
446
- console.log(chalk.dim(' Enter the system prompt (multi-line). Type END on a blank line to finish:\n'));
447
- const lines = [];
448
- while (true) {
449
- const line = await ask(rl, ' ');
450
- if (line.trim() === 'END') break;
451
- lines.push(line);
452
- }
453
- const prompt = lines.join('\n').trim();
454
- if (!prompt) {
455
- console.log(chalk.dim(' Empty prompt — cancelled.\n'));
456
- break;
457
- }
458
- const skill = addCustomSkill({ name: name.trim(), systemPrompt: prompt });
459
- console.log(chalk.green(`\n ✅ Created: ${skill.name} (${skill.id})\n`));
331
+
332
+ const choice = await p.select({
333
+ message: `Custom Skills (${customs.length})`,
334
+ options: [
335
+ { value: 'create', label: 'Create new skill' },
336
+ { value: 'list', label: `List skills`, hint: `${customs.length} total` },
337
+ { value: 'delete', label: 'Delete a skill' },
338
+ { value: 'back', label: 'Back' },
339
+ ],
340
+ });
341
+ if (handleCancel(choice)) return;
342
+
343
+ switch (choice) {
344
+ case 'create': {
345
+ const name = await p.text({ message: 'Skill name' });
346
+ if (handleCancel(name) || !name.trim()) break;
347
+
348
+ const prompt = await p.text({
349
+ message: 'System prompt',
350
+ placeholder: 'Enter the system prompt for this skill...',
351
+ });
352
+ if (handleCancel(prompt) || !prompt.trim()) break;
353
+
354
+ const skill = addCustomSkill({ name: name.trim(), systemPrompt: prompt.trim() });
355
+ p.log.success(`Created: ${skill.name} (${skill.id})`);
460
356
  break;
461
357
  }
462
- case '2': {
463
- const customs = getCustomSkills();
358
+ case 'list': {
464
359
  if (!customs.length) {
465
- console.log(chalk.dim('\n No custom skills yet.\n'));
360
+ p.log.info('No custom skills yet.');
466
361
  break;
467
362
  }
468
- console.log('');
469
- for (const s of customs) {
363
+ const formatted = customs.map(s => {
470
364
  const preview = s.systemPrompt.slice(0, 60).replace(/\n/g, ' ');
471
- console.log(` 🛠️ ${chalk.bold(s.name)} (${s.id})`);
472
- console.log(chalk.dim(` ${preview}${s.systemPrompt.length > 60 ? '...' : ''}`));
473
- }
474
- console.log('');
365
+ return `${chalk.bold(s.name)} (${s.id})\n${chalk.dim(preview + (s.systemPrompt.length > 60 ? '...' : ''))}`;
366
+ }).join('\n\n');
367
+ p.note(formatted, 'Custom Skills');
475
368
  break;
476
369
  }
477
- case '3': {
478
- const customs = getCustomSkills();
370
+ case 'delete': {
479
371
  if (!customs.length) {
480
- console.log(chalk.dim('\n No custom skills to delete.\n'));
372
+ p.log.info('No custom skills to delete.');
481
373
  break;
482
374
  }
483
- console.log('');
484
- customs.forEach((s, i) => {
485
- console.log(` ${chalk.cyan(`${i + 1}.`)} ${s.name} (${s.id})`);
375
+ const toDelete = await p.select({
376
+ message: 'Select skill to delete',
377
+ options: [
378
+ ...customs.map(s => ({ value: s.id, label: s.name, hint: s.id })),
379
+ { value: '__back', label: 'Cancel' },
380
+ ],
486
381
  });
487
- console.log('');
488
- const pick = await ask(rl, chalk.cyan(' Delete #: '));
489
- const idx = parseInt(pick, 10) - 1;
490
- if (idx >= 0 && idx < customs.length) {
491
- const deleted = deleteCustomSkill(customs[idx].id);
492
- if (deleted) console.log(chalk.green(`\n 🗑️ Deleted: ${customs[idx].name}\n`));
493
- else console.log(chalk.dim(' Not found.\n'));
494
- } else {
495
- console.log(chalk.dim(' Cancelled.\n'));
496
- }
382
+ if (handleCancel(toDelete) || toDelete === '__back') break;
383
+ const deleted = deleteCustomSkill(toDelete);
384
+ if (deleted) p.log.success(`Deleted: ${customs.find(s => s.id === toDelete)?.name}`);
497
385
  break;
498
386
  }
499
- case '4':
387
+ case 'back':
500
388
  managing = false;
501
389
  break;
502
- default:
503
- console.log(chalk.dim(' Invalid choice.\n'));
504
390
  }
505
391
  }
506
392
  }
507
393
 
508
- async function manageAutomations(rl) {
394
+ async function manageAutomations() {
509
395
  const manager = new AutomationManager();
510
396
 
511
397
  let managing = true;
512
398
  while (managing) {
513
399
  const autos = manager.listAll();
514
- console.log('');
515
- console.log(chalk.bold(' Automations\n'));
516
- console.log(` ${chalk.cyan('1.')} List all automations (${autos.length})`);
517
- console.log(` ${chalk.cyan('2.')} Delete an automation`);
518
- console.log(` ${chalk.cyan('3.')} Back`);
519
- console.log('');
520
-
521
- const choice = await ask(rl, chalk.cyan(' > '));
522
- switch (choice.trim()) {
523
- case '1': {
400
+
401
+ const choice = await p.select({
402
+ message: `Automations (${autos.length})`,
403
+ options: [
404
+ { value: 'list', label: 'List all automations', hint: `${autos.length} total` },
405
+ { value: 'delete', label: 'Delete an automation' },
406
+ { value: 'back', label: 'Back' },
407
+ ],
408
+ });
409
+ if (handleCancel(choice)) return;
410
+
411
+ switch (choice) {
412
+ case 'list': {
524
413
  if (!autos.length) {
525
- console.log(chalk.dim('\n No automations found.\n'));
414
+ p.log.info('No automations found.');
526
415
  break;
527
416
  }
528
- console.log('');
529
- for (const a of autos) {
417
+ const formatted = autos.map(a => {
530
418
  const status = a.enabled ? chalk.green('enabled') : chalk.yellow('paused');
531
419
  const next = a.nextRun ? new Date(a.nextRun).toLocaleString() : 'not scheduled';
532
- console.log(` ${chalk.bold(a.name)} (${a.id}) — chat ${a.chatId}`);
533
- console.log(chalk.dim(` Status: ${status} | Runs: ${a.runCount} | Next: ${next}`));
534
- console.log(chalk.dim(` Task: ${a.description.slice(0, 80)}${a.description.length > 80 ? '...' : ''}`));
535
- }
536
- console.log('');
420
+ return `${chalk.bold(a.name)} (${a.id}) — chat ${a.chatId}\n` +
421
+ chalk.dim(`Status: ${status} | Runs: ${a.runCount} | Next: ${next}\n`) +
422
+ chalk.dim(`Task: ${a.description.slice(0, 80)}${a.description.length > 80 ? '...' : ''}`);
423
+ }).join('\n\n');
424
+ p.note(formatted, 'Automations');
537
425
  break;
538
426
  }
539
- case '2': {
427
+ case 'delete': {
540
428
  if (!autos.length) {
541
- console.log(chalk.dim('\n No automations to delete.\n'));
429
+ p.log.info('No automations to delete.');
542
430
  break;
543
431
  }
544
- console.log('');
545
- autos.forEach((a, i) => {
546
- console.log(` ${chalk.cyan(`${i + 1}.`)} ${a.name} (${a.id}) — chat ${a.chatId}`);
432
+ const toDelete = await p.select({
433
+ message: 'Select automation to delete',
434
+ options: [
435
+ ...autos.map(a => ({ value: a.id, label: a.name, hint: `chat ${a.chatId}` })),
436
+ { value: '__back', label: 'Cancel' },
437
+ ],
547
438
  });
548
- console.log('');
549
- const pick = await ask(rl, chalk.cyan(' Delete #: '));
550
- const idx = parseInt(pick, 10) - 1;
551
- if (idx >= 0 && idx < autos.length) {
552
- const deleted = manager.delete(autos[idx].id);
553
- if (deleted) console.log(chalk.green(`\n 🗑️ Deleted: ${autos[idx].name}\n`));
554
- else console.log(chalk.dim(' Not found.\n'));
555
- } else {
556
- console.log(chalk.dim(' Cancelled.\n'));
557
- }
439
+ if (handleCancel(toDelete) || toDelete === '__back') break;
440
+ const deleted = manager.delete(toDelete);
441
+ if (deleted) p.log.success(`Deleted: ${autos.find(a => a.id === toDelete)?.name}`);
558
442
  break;
559
443
  }
560
- case '3':
444
+ case 'back':
561
445
  managing = false;
562
446
  break;
563
- default:
564
- console.log(chalk.dim(' Invalid choice.\n'));
565
447
  }
566
448
  }
567
449
  }
568
450
 
569
- async function manageCharacters(rl, config) {
451
+ async function manageCharacters(config) {
570
452
  const charManager = new CharacterManager();
571
-
572
- // Ensure builtins installed
573
453
  charManager.installAllBuiltins();
574
454
 
575
455
  let managing = true;
@@ -578,49 +458,42 @@ async function manageCharacters(rl, config) {
578
458
  const activeId = charManager.getActiveCharacterId();
579
459
  const active = charManager.getCharacter(activeId);
580
460
 
581
- console.log('');
582
- console.log(chalk.bold(' Character Management'));
583
- console.log(chalk.dim(` Active: ${active?.emoji || ''} ${active?.name || 'None'}`));
584
- console.log('');
585
- console.log(` ${chalk.cyan('1.')} Switch character`);
586
- console.log(` ${chalk.cyan('2.')} Create custom character`);
587
- console.log(` ${chalk.cyan('3.')} View character info`);
588
- console.log(` ${chalk.cyan('4.')} Delete a custom character`);
589
- console.log(` ${chalk.cyan('5.')} Back`);
590
- console.log('');
591
-
592
- const choice = await ask(rl, chalk.cyan(' > '));
593
- switch (choice.trim()) {
594
- case '1': {
595
- showCharacterGallery(characters, activeId);
596
- console.log('');
597
- characters.forEach((c, i) => {
598
- const marker = c.id === activeId ? chalk.green(' ✓') : '';
599
- console.log(` ${chalk.cyan(`${i + 1}.`)} ${c.emoji} ${c.name}${marker}`);
461
+ const choice = await p.select({
462
+ message: `Characters — Active: ${active?.emoji || ''} ${active?.name || 'None'}`,
463
+ options: [
464
+ { value: 'switch', label: 'Switch character' },
465
+ { value: 'create', label: 'Create custom character' },
466
+ { value: 'view', label: 'View character info' },
467
+ { value: 'delete', label: 'Delete a custom character' },
468
+ { value: 'back', label: 'Back' },
469
+ ],
470
+ });
471
+ if (handleCancel(choice)) return;
472
+
473
+ switch (choice) {
474
+ case 'switch': {
475
+ const picked = await p.select({
476
+ message: 'Select character',
477
+ options: characters.map(c => ({
478
+ value: c.id,
479
+ label: `${c.emoji} ${c.name}`,
480
+ hint: c.id === activeId ? 'active' : undefined,
481
+ })),
600
482
  });
601
- console.log('');
602
- const pick = await ask(rl, chalk.cyan(' Select #: '));
603
- const idx = parseInt(pick, 10) - 1;
604
- if (idx >= 0 && idx < characters.length) {
605
- charManager.setActiveCharacter(characters[idx].id);
606
- console.log(chalk.green(`\n ${characters[idx].emoji} Switched to ${characters[idx].name}\n`));
607
- } else {
608
- console.log(chalk.dim(' Cancelled.\n'));
609
- }
483
+ if (handleCancel(picked)) break;
484
+ charManager.setActiveCharacter(picked);
485
+ const char = characters.find(c => c.id === picked);
486
+ p.log.success(`${char.emoji} Switched to ${char.name}`);
610
487
  break;
611
488
  }
612
- case '2': {
613
- // Create custom character via Q&A
614
- console.log('');
615
- console.log(chalk.bold(' Custom Character Builder'));
616
- console.log(chalk.dim(' Answer a few questions to create your character.\n'));
489
+ case 'create': {
490
+ p.log.step('Custom Character Builder');
617
491
 
618
- // Need an LLM provider for generation
619
492
  const orchProviderKey = config.orchestrator.provider || 'anthropic';
620
493
  const orchProviderDef = PROVIDERS[orchProviderKey];
621
494
  const orchApiKey = config.orchestrator.api_key || (orchProviderDef && process.env[orchProviderDef.envKey]);
622
495
  if (!orchApiKey) {
623
- console.log(chalk.red(' No API key configured for character generation.\n'));
496
+ p.log.error('No API key configured for character generation.');
624
497
  break;
625
498
  }
626
499
 
@@ -638,290 +511,301 @@ async function manageCharacters(rl, config) {
638
511
  const answers = {};
639
512
  let cancelled = false;
640
513
 
641
- // Walk through all questions
642
514
  let q = builder.getNextQuestion(answers);
643
515
  while (q) {
644
516
  const progress = builder.getProgress(answers);
645
- console.log(chalk.bold(` Question ${progress.answered + 1}/${progress.total}`));
646
- console.log(` ${q.question}`);
647
- console.log(chalk.dim(` Examples: ${q.examples}`));
648
- const answer = await ask(rl, chalk.cyan(' > '));
649
- if (answer.trim().toLowerCase() === 'cancel') {
650
- cancelled = true;
651
- break;
652
- }
517
+ const answer = await p.text({
518
+ message: `(${progress.answered + 1}/${progress.total}) ${q.question}`,
519
+ placeholder: q.examples,
520
+ });
521
+ if (handleCancel(answer)) { cancelled = true; break; }
653
522
  answers[q.id] = answer.trim();
654
523
  q = builder.getNextQuestion(answers);
655
- console.log('');
656
524
  }
657
525
 
658
- if (cancelled) {
659
- console.log(chalk.dim(' Character creation cancelled.\n'));
660
- break;
661
- }
526
+ if (cancelled) break;
662
527
 
663
- console.log(chalk.dim(' Generating character...'));
528
+ const s = p.spinner();
529
+ s.start('Generating character...');
664
530
  try {
665
531
  const result = await builder.generateCharacter(answers);
532
+ s.stop('Character generated');
666
533
  const id = result.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
667
534
 
668
- // Show preview
669
535
  console.log('');
670
- showCharacterCard({
671
- ...result,
672
- id,
673
- origin: 'Custom',
674
- });
536
+ showCharacterCard({ ...result, id, origin: 'Custom' });
675
537
  console.log('');
676
538
 
677
- const confirm = await ask(rl, chalk.cyan(' Install this character? (y/n): '));
678
- if (confirm.trim().toLowerCase() === 'y') {
679
- charManager.addCharacter(
680
- { id, type: 'custom', name: result.name, origin: 'Custom', age: result.age, emoji: result.emoji, tagline: result.tagline },
681
- result.personaMd,
682
- result.selfDefaults,
683
- );
684
- console.log(chalk.green(`\n ${result.emoji} ${result.name} created!\n`));
685
- } else {
686
- console.log(chalk.dim(' Discarded.\n'));
539
+ const install = await p.confirm({ message: 'Install this character?' });
540
+ if (handleCancel(install) || !install) {
541
+ p.log.info('Discarded.');
542
+ break;
687
543
  }
544
+
545
+ charManager.addCharacter(
546
+ { id, type: 'custom', name: result.name, origin: 'Custom', age: result.age, emoji: result.emoji, tagline: result.tagline },
547
+ result.personaMd,
548
+ result.selfDefaults,
549
+ );
550
+ p.log.success(`${result.emoji} ${result.name} created!`);
688
551
  } catch (err) {
689
- console.log(chalk.red(`\n Character generation failed: ${err.message}\n`));
552
+ s.stop(chalk.red(`Character generation failed: ${err.message}`));
690
553
  }
691
554
  break;
692
555
  }
693
- case '3': {
694
- console.log('');
695
- characters.forEach((c, i) => {
696
- console.log(` ${chalk.cyan(`${i + 1}.`)} ${c.emoji} ${c.name}`);
556
+ case 'view': {
557
+ const picked = await p.select({
558
+ message: 'Select character to view',
559
+ options: characters.map(c => ({
560
+ value: c.id,
561
+ label: `${c.emoji} ${c.name}`,
562
+ })),
697
563
  });
698
- console.log('');
699
- const pick = await ask(rl, chalk.cyan(' View #: '));
700
- const idx = parseInt(pick, 10) - 1;
701
- if (idx >= 0 && idx < characters.length) {
702
- showCharacterCard(characters[idx], characters[idx].id === activeId);
703
- if (characters[idx].evolutionHistory?.length > 0) {
704
- console.log(chalk.dim(` Evolution events: ${characters[idx].evolutionHistory.length}`));
705
- }
706
- console.log('');
707
- } else {
708
- console.log(chalk.dim(' Cancelled.\n'));
564
+ if (handleCancel(picked)) break;
565
+ const char = characters.find(c => c.id === picked);
566
+ showCharacterCard(char, char.id === activeId);
567
+ if (char.evolutionHistory?.length > 0) {
568
+ p.log.info(`Evolution events: ${char.evolutionHistory.length}`);
709
569
  }
710
570
  break;
711
571
  }
712
- case '4': {
572
+ case 'delete': {
713
573
  const customChars = characters.filter(c => c.type === 'custom');
714
574
  if (customChars.length === 0) {
715
- console.log(chalk.dim('\n No custom characters to delete.\n'));
575
+ p.log.info('No custom characters to delete.');
716
576
  break;
717
577
  }
718
- console.log('');
719
- customChars.forEach((c, i) => {
720
- console.log(` ${chalk.cyan(`${i + 1}.`)} ${c.emoji} ${c.name}`);
578
+ const picked = await p.select({
579
+ message: 'Select character to delete',
580
+ options: [
581
+ ...customChars.map(c => ({ value: c.id, label: `${c.emoji} ${c.name}` })),
582
+ { value: '__back', label: 'Cancel' },
583
+ ],
721
584
  });
722
- console.log('');
723
- const pick = await ask(rl, chalk.cyan(' Delete #: '));
724
- const idx = parseInt(pick, 10) - 1;
725
- if (idx >= 0 && idx < customChars.length) {
726
- try {
727
- charManager.removeCharacter(customChars[idx].id);
728
- console.log(chalk.green(`\n Deleted: ${customChars[idx].name}\n`));
729
- } catch (err) {
730
- console.log(chalk.red(`\n ${err.message}\n`));
731
- }
732
- } else {
733
- console.log(chalk.dim(' Cancelled.\n'));
585
+ if (handleCancel(picked) || picked === '__back') break;
586
+ try {
587
+ const char = customChars.find(c => c.id === picked);
588
+ charManager.removeCharacter(picked);
589
+ p.log.success(`Deleted: ${char.name}`);
590
+ } catch (err) {
591
+ p.log.error(err.message);
734
592
  }
735
593
  break;
736
594
  }
737
- case '5':
595
+ case 'back':
738
596
  managing = false;
739
597
  break;
740
- default:
741
- console.log(chalk.dim(' Invalid choice.\n'));
742
598
  }
743
599
  }
744
600
  }
745
601
 
746
- async function linkLinkedInCli(config, rl) {
602
+ async function linkLinkedInCli(config) {
747
603
  const { saveCredential } = await import('../src/utils/config.js');
748
604
 
749
- // Show current status
750
605
  if (config.linkedin?.access_token) {
751
606
  const truncated = `${config.linkedin.access_token.slice(0, 8)}...${config.linkedin.access_token.slice(-4)}`;
752
- console.log(chalk.dim(`\n Currently connected — token: ${truncated}`));
753
- if (config.linkedin.person_urn) console.log(chalk.dim(` URN: ${config.linkedin.person_urn}`));
754
- const relink = (await ask(rl, chalk.cyan('\n Re-link? [y/N]: '))).trim().toLowerCase();
755
- if (relink !== 'y') {
756
- console.log(chalk.dim(' Cancelled.\n'));
757
- return;
758
- }
607
+ p.note(
608
+ `Token: ${truncated}${config.linkedin.person_urn ? `\nURN: ${config.linkedin.person_urn}` : ''}`,
609
+ 'LinkedIn Connected',
610
+ );
611
+ const relink = await p.confirm({ message: 'Re-link account?', initialValue: false });
612
+ if (handleCancel(relink) || !relink) return;
759
613
  }
760
614
 
761
- console.log('');
762
- console.log(chalk.bold(' Link LinkedIn Account\n'));
763
- console.log(chalk.dim(' 1. Go to https://www.linkedin.com/developers/tools/oauth/token-generator'));
764
- console.log(chalk.dim(' 2. Select your app, pick scopes: openid, profile, email, w_member_social'));
765
- console.log(chalk.dim(' 3. Authorize and copy the token'));
766
- console.log('');
615
+ p.note(
616
+ '1. Go to https://www.linkedin.com/developers/tools/oauth/token-generator\n' +
617
+ '2. Select your app, pick scopes: openid, profile, email, w_member_social\n' +
618
+ '3. Authorize and copy the token',
619
+ 'Link LinkedIn Account',
620
+ );
767
621
 
768
- const token = (await ask(rl, chalk.cyan(' Paste token (or "cancel"): '))).trim();
769
- if (!token || token.toLowerCase() === 'cancel') {
770
- console.log(chalk.dim(' Cancelled.\n'));
771
- return;
772
- }
622
+ const token = await p.text({ message: 'Paste access token' });
623
+ if (handleCancel(token) || !token.trim()) return;
773
624
 
774
- console.log(chalk.dim('\n Validating token...'));
625
+ const s = p.spinner();
626
+ s.start('Validating token...');
775
627
 
776
628
  try {
777
- // Try /v2/userinfo (requires "Sign in with LinkedIn" product → openid+profile scopes)
778
629
  const res = await fetch('https://api.linkedin.com/v2/userinfo', {
779
- headers: { 'Authorization': `Bearer ${token}` },
630
+ headers: { 'Authorization': `Bearer ${token.trim()}` },
780
631
  });
781
632
 
782
633
  if (res.ok) {
783
634
  const profile = await res.json();
784
635
  const personUrn = `urn:li:person:${profile.sub}`;
785
636
 
786
- saveCredential(config, 'LINKEDIN_ACCESS_TOKEN', token);
637
+ saveCredential(config, 'LINKEDIN_ACCESS_TOKEN', token.trim());
787
638
  saveCredential(config, 'LINKEDIN_PERSON_URN', personUrn);
788
639
 
789
- console.log(chalk.green(`\n ✔ LinkedIn linked`));
790
- console.log(chalk.dim(` Name: ${profile.name}`));
791
- if (profile.email) console.log(chalk.dim(` Email: ${profile.email}`));
792
- console.log(chalk.dim(` URN: ${personUrn}`));
793
- console.log('');
640
+ s.stop('LinkedIn linked');
641
+ p.log.info(`Name: ${profile.name}${profile.email ? ` | Email: ${profile.email}` : ''}\nURN: ${personUrn}`);
794
642
  } else if (res.status === 401) {
795
643
  throw new Error('Invalid or expired token.');
796
644
  } else {
797
- // 403 = token works but no profile scopes → save token, ask for URN
798
- console.log(chalk.yellow('\n Token accepted but profile scopes missing (openid+profile).'));
799
- console.log(chalk.dim(' To auto-detect your URN, add "Sign in with LinkedIn using OpenID Connect"'));
800
- console.log(chalk.dim(' to your app at https://www.linkedin.com/developers/apps\n'));
801
- console.log(chalk.dim(' For now, enter your person URN manually.'));
802
- console.log(chalk.dim(' Find it: LinkedIn profile → URL contains /in/yourname'));
803
- console.log(chalk.dim(' Or: Developer Portal Your App → Auth → Your member sub value\n'));
804
-
805
- const urn = (await ask(rl, chalk.cyan(' Person URN (urn:li:person:XXXXX): '))).trim();
806
- if (!urn) {
807
- console.log(chalk.yellow(' No URN provided. Token saved but LinkedIn posts will not work without a URN.\n'));
808
- saveCredential(config, 'LINKEDIN_ACCESS_TOKEN', token);
645
+ s.stop('Token accepted (profile scopes missing)');
646
+ p.log.warn(
647
+ 'To auto-detect your URN, add "Sign in with LinkedIn using OpenID Connect"\n' +
648
+ 'to your app at https://www.linkedin.com/developers/apps',
649
+ );
650
+
651
+ const urn = await p.text({
652
+ message: 'Person URN (urn:li:person:XXXXX)',
653
+ placeholder: 'urn:li:person:...',
654
+ });
655
+ if (handleCancel(urn) || !urn.trim()) {
656
+ p.log.warn('No URN provided. Token saved but LinkedIn posts will not work without a URN.');
657
+ saveCredential(config, 'LINKEDIN_ACCESS_TOKEN', token.trim());
809
658
  return;
810
659
  }
811
660
 
812
- const personUrn = urn.startsWith('urn:li:person:') ? urn : `urn:li:person:${urn}`;
813
- saveCredential(config, 'LINKEDIN_ACCESS_TOKEN', token);
661
+ const personUrn = urn.trim().startsWith('urn:li:person:') ? urn.trim() : `urn:li:person:${urn.trim()}`;
662
+ saveCredential(config, 'LINKEDIN_ACCESS_TOKEN', token.trim());
814
663
  saveCredential(config, 'LINKEDIN_PERSON_URN', personUrn);
815
664
 
816
- console.log(chalk.green(`\n ✔ LinkedIn linked`));
817
- console.log(chalk.dim(` URN: ${personUrn}`));
818
- console.log('');
665
+ p.log.success(`LinkedIn linked — URN: ${personUrn}`);
819
666
  }
820
667
  } catch (err) {
821
- console.log(chalk.red(`\n ✖ Token validation failed: ${err.message}\n`));
668
+ s.stop(chalk.red(`Token validation failed: ${err.message}`));
669
+ }
670
+ }
671
+
672
+ async function manageDashboard(config) {
673
+ const dashEnabled = config.dashboard?.enabled;
674
+ const dashPort = config.dashboard?.port || 3000;
675
+
676
+ p.note(
677
+ `Auto-start: ${dashEnabled ? chalk.green('yes') : chalk.yellow('no')}\n` +
678
+ `Port: ${dashPort}\n` +
679
+ `URL: http://localhost:${dashPort}`,
680
+ 'Dashboard',
681
+ );
682
+
683
+ const choice = await p.select({
684
+ message: 'Dashboard settings',
685
+ options: [
686
+ { value: 'toggle', label: `${dashEnabled ? 'Disable' : 'Enable'} auto-start on boot` },
687
+ { value: 'port', label: 'Change port', hint: String(dashPort) },
688
+ { value: 'back', label: 'Back' },
689
+ ],
690
+ });
691
+ if (handleCancel(choice) || choice === 'back') return;
692
+
693
+ if (choice === 'toggle') {
694
+ const newEnabled = !dashEnabled;
695
+ saveDashboardToYaml({ enabled: newEnabled });
696
+ config.dashboard.enabled = newEnabled;
697
+ p.log.success(`Dashboard auto-start ${newEnabled ? 'enabled' : 'disabled'}`);
698
+ if (newEnabled) {
699
+ p.log.info(`Dashboard will start at http://localhost:${dashPort} on next bot launch.`);
700
+ }
701
+ } else if (choice === 'port') {
702
+ const portInput = await p.text({
703
+ message: 'New port',
704
+ placeholder: String(dashPort),
705
+ validate: (v) => {
706
+ const n = parseInt(v.trim(), 10);
707
+ if (!n || n < 1 || n > 65535) return 'Enter a valid port (1-65535)';
708
+ },
709
+ });
710
+ if (handleCancel(portInput)) return;
711
+ const newPort = parseInt(portInput.trim(), 10);
712
+ saveDashboardToYaml({ port: newPort });
713
+ config.dashboard.port = newPort;
714
+ p.log.success(`Dashboard port set to ${newPort}`);
822
715
  }
823
716
  }
824
717
 
825
718
  async function main() {
719
+ const headless = process.argv.includes('--start');
720
+
826
721
  showLogo();
827
722
 
828
- const config = await loadConfigInteractive();
723
+ const config = headless ? loadConfig() : await loadConfigInteractive();
829
724
  createLogger(config);
830
725
 
831
- const rl = createInterface({ input: process.stdin, output: process.stdout });
726
+ // --start flag: skip menu, launch bot directly (for systemd / headless)
727
+ if (headless) {
728
+ const started = await startBotFlow(config);
729
+ if (!started) process.exit(1);
730
+ return;
731
+ }
732
+
733
+ // Show welcome screen with system info
734
+ const characterManager = new CharacterManager();
735
+ characterManager.installAllBuiltins();
736
+ showWelcomeScreen(config, characterManager);
832
737
 
833
738
  let running = true;
834
739
  while (running) {
835
- showMenu(config);
836
- const choice = await ask(rl, chalk.cyan(' > '));
740
+ const brainHint = formatProviderLabel(config, 'brain');
741
+ const orchHint = formatProviderLabel(config, 'orchestrator');
742
+
743
+ const choice = await p.select({
744
+ message: 'What would you like to do?',
745
+ options: [
746
+ { value: 'start', label: 'Start bot' },
747
+ { value: 'check', label: 'Check connections' },
748
+ { value: 'logs', label: 'View logs' },
749
+ { value: 'audit', label: 'View audit logs' },
750
+ { value: 'brain', label: 'Change brain model', hint: brainHint },
751
+ { value: 'orch', label: 'Change orchestrator model', hint: orchHint },
752
+ { value: 'skills', label: 'Manage custom skills' },
753
+ { value: 'automations', label: 'Manage automations' },
754
+ { value: 'characters', label: 'Switch character' },
755
+ { value: 'linkedin', label: 'Link LinkedIn account' },
756
+ { value: 'dashboard', label: 'Dashboard settings' },
757
+ { value: 'exit', label: 'Exit' },
758
+ ],
759
+ });
760
+
761
+ if (handleCancel(choice)) {
762
+ running = false;
763
+ break;
764
+ }
837
765
 
838
- switch (choice.trim()) {
839
- case '1': {
840
- rl.close();
766
+ switch (choice) {
767
+ case 'start': {
841
768
  const started = await startBotFlow(config);
842
769
  if (!started) process.exit(1);
843
- return; // bot is running, don't show menu again
770
+ return;
844
771
  }
845
- case '2':
772
+ case 'check':
846
773
  await runCheck(config);
847
774
  break;
848
- case '3':
775
+ case 'logs':
849
776
  viewLog('kernel.log');
850
777
  break;
851
- case '4':
778
+ case 'audit':
852
779
  viewLog('kernel-audit.log');
853
780
  break;
854
- case '5':
855
- await changeBrainModel(config, rl);
781
+ case 'brain':
782
+ await changeBrainModel(config);
856
783
  break;
857
- case '6':
858
- await changeOrchestratorModel(config, rl);
784
+ case 'orch':
785
+ await changeOrchestratorModel(config);
859
786
  break;
860
- case '7':
861
- await manageCustomSkills(rl);
787
+ case 'skills':
788
+ await manageCustomSkills();
862
789
  break;
863
- case '8':
864
- await manageAutomations(rl);
790
+ case 'automations':
791
+ await manageAutomations();
865
792
  break;
866
- case '9':
867
- await manageCharacters(rl, config);
793
+ case 'characters':
794
+ await manageCharacters(config);
868
795
  break;
869
- case '10':
870
- await linkLinkedInCli(config, rl);
796
+ case 'linkedin':
797
+ await linkLinkedInCli(config);
871
798
  break;
872
- case '11': {
873
- const dashEnabled = config.dashboard?.enabled;
874
- const dashPort = config.dashboard?.port || 3000;
875
- console.log('');
876
- console.log(chalk.bold(' Dashboard'));
877
- console.log(` Auto-start on boot: ${dashEnabled ? chalk.green('yes') : chalk.yellow('no')}`);
878
- console.log(` Port: ${chalk.cyan(dashPort)}`);
879
- console.log(` URL: ${chalk.cyan(`http://localhost:${dashPort}`)}`);
880
- console.log('');
881
- console.log(` ${chalk.cyan('1.')} ${dashEnabled ? 'Disable' : 'Enable'} auto-start on boot`);
882
- console.log(` ${chalk.cyan('2.')} Change port`);
883
- console.log(` ${chalk.cyan('3.')} Back`);
884
- console.log('');
885
- const dashChoice = await ask(rl, chalk.cyan(' > '));
886
- switch (dashChoice.trim()) {
887
- case '1': {
888
- const newEnabled = !dashEnabled;
889
- saveDashboardToYaml({ enabled: newEnabled });
890
- config.dashboard.enabled = newEnabled;
891
- console.log(chalk.green(`\n ✔ Dashboard auto-start ${newEnabled ? 'enabled' : 'disabled'}\n`));
892
- if (newEnabled) {
893
- console.log(chalk.dim(` Dashboard will start at http://localhost:${dashPort} on next bot launch.`));
894
- console.log(chalk.dim(' Or use /dashboard start in Telegram to start now.\n'));
895
- }
896
- break;
897
- }
898
- case '2': {
899
- const portInput = await ask(rl, chalk.cyan(' New port: '));
900
- const newPort = parseInt(portInput.trim(), 10);
901
- if (!newPort || newPort < 1 || newPort > 65535) {
902
- console.log(chalk.dim(' Invalid port.\n'));
903
- break;
904
- }
905
- saveDashboardToYaml({ port: newPort });
906
- config.dashboard.port = newPort;
907
- console.log(chalk.green(`\n ✔ Dashboard port set to ${newPort}\n`));
908
- break;
909
- }
910
- default:
911
- break;
912
- }
799
+ case 'dashboard':
800
+ await manageDashboard(config);
913
801
  break;
914
- }
915
- case '12':
802
+ case 'exit':
916
803
  running = false;
917
804
  break;
918
- default:
919
- console.log(chalk.dim(' Invalid choice.\n'));
920
805
  }
921
806
  }
922
807
 
923
- rl.close();
924
- console.log(chalk.dim(' Goodbye.\n'));
808
+ p.outro('Goodbye.');
925
809
  }
926
810
 
927
811
  main().catch((err) => {