kernelbot 1.0.37 → 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.
Files changed (41) hide show
  1. package/bin/kernel.js +499 -249
  2. package/config.example.yaml +17 -0
  3. package/knowledge_base/active_inference_foraging.md +126 -0
  4. package/knowledge_base/index.md +1 -1
  5. package/package.json +3 -1
  6. package/src/agent.js +355 -82
  7. package/src/bot.js +724 -12
  8. package/src/character.js +406 -0
  9. package/src/characters/builder.js +174 -0
  10. package/src/characters/builtins.js +421 -0
  11. package/src/conversation.js +17 -2
  12. package/src/dashboard/agents.css +469 -0
  13. package/src/dashboard/agents.html +184 -0
  14. package/src/dashboard/agents.js +873 -0
  15. package/src/dashboard/dashboard.css +281 -0
  16. package/src/dashboard/dashboard.js +579 -0
  17. package/src/dashboard/index.html +366 -0
  18. package/src/dashboard/server.js +521 -0
  19. package/src/dashboard/shared.css +700 -0
  20. package/src/dashboard/shared.js +218 -0
  21. package/src/life/engine.js +115 -26
  22. package/src/life/evolution.js +7 -5
  23. package/src/life/journal.js +5 -4
  24. package/src/life/memory.js +12 -9
  25. package/src/life/share-queue.js +7 -5
  26. package/src/prompts/orchestrator.js +76 -14
  27. package/src/prompts/workers.js +22 -0
  28. package/src/self.js +17 -5
  29. package/src/services/linkedin-api.js +190 -0
  30. package/src/services/stt.js +8 -2
  31. package/src/services/tts.js +32 -2
  32. package/src/services/x-api.js +141 -0
  33. package/src/swarm/worker-registry.js +7 -0
  34. package/src/tools/categories.js +4 -0
  35. package/src/tools/index.js +6 -0
  36. package/src/tools/linkedin.js +264 -0
  37. package/src/tools/orchestrator-tools.js +337 -2
  38. package/src/tools/x.js +256 -0
  39. package/src/utils/config.js +190 -139
  40. package/src/utils/display.js +165 -52
  41. package/src/utils/temporal-awareness.js +24 -10
package/bin/kernel.js CHANGED
@@ -4,34 +4,35 @@
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';
12
- import { loadConfig, loadConfigInteractive, changeBrainModel, changeOrchestratorModel } from '../src/utils/config.js';
11
+ import * as p from '@clack/prompts';
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 {
15
15
  showLogo,
16
16
  showStartupCheck,
17
17
  showStartupComplete,
18
18
  showError,
19
+ showCharacterCard,
20
+ showWelcomeScreen,
21
+ handleCancel,
22
+ formatProviderLabel,
19
23
  } from '../src/utils/display.js';
20
24
  import { createAuditLogger } from '../src/security/audit.js';
25
+ import { CharacterBuilder } from '../src/characters/builder.js';
21
26
  import { ConversationManager } from '../src/conversation.js';
22
27
  import { UserPersonaManager } from '../src/persona.js';
23
- import { SelfManager } from '../src/self.js';
24
28
  import { Agent } from '../src/agent.js';
25
29
  import { JobManager } from '../src/swarm/job-manager.js';
26
30
  import { startBot } from '../src/bot.js';
27
31
  import { AutomationManager } from '../src/automation/index.js';
28
32
  import { createProvider, PROVIDERS } from '../src/providers/index.js';
29
- import { MemoryManager } from '../src/life/memory.js';
30
- import { JournalManager } from '../src/life/journal.js';
31
- import { ShareQueue } from '../src/life/share-queue.js';
32
- import { EvolutionTracker } from '../src/life/evolution.js';
33
33
  import { CodebaseKnowledge } from '../src/life/codebase.js';
34
34
  import { LifeEngine } from '../src/life/engine.js';
35
+ import { CharacterManager } from '../src/character.js';
35
36
  import {
36
37
  loadCustomSkills,
37
38
  getCustomSkills,
@@ -39,102 +40,34 @@ import {
39
40
  deleteCustomSkill,
40
41
  } from '../src/skills/custom.js';
41
42
 
42
- function showMenu(config) {
43
- const orchProviderDef = PROVIDERS[config.orchestrator.provider];
44
- const orchProviderName = orchProviderDef ? orchProviderDef.name : config.orchestrator.provider;
45
- const orchModelId = config.orchestrator.model;
46
-
47
- const providerDef = PROVIDERS[config.brain.provider];
48
- const providerName = providerDef ? providerDef.name : config.brain.provider;
49
- const modelId = config.brain.model;
50
-
51
- console.log('');
52
- console.log(chalk.dim(` Current orchestrator: ${orchProviderName} / ${orchModelId}`));
53
- console.log(chalk.dim(` Current brain: ${providerName} / ${modelId}`));
54
- console.log('');
55
- console.log(chalk.bold(' What would you like to do?\n'));
56
- console.log(` ${chalk.cyan('1.')} Start bot`);
57
- console.log(` ${chalk.cyan('2.')} Check connections`);
58
- console.log(` ${chalk.cyan('3.')} View logs`);
59
- console.log(` ${chalk.cyan('4.')} View audit logs`);
60
- console.log(` ${chalk.cyan('5.')} Change brain model`);
61
- console.log(` ${chalk.cyan('6.')} Change orchestrator model`);
62
- console.log(` ${chalk.cyan('7.')} Manage custom skills`);
63
- console.log(` ${chalk.cyan('8.')} Manage automations`);
64
- console.log(` ${chalk.cyan('9.')} Exit`);
65
- console.log('');
66
- }
67
-
68
- function ask(rl, question) {
69
- return new Promise((res) => rl.question(question, res));
70
- }
71
-
72
43
  /**
73
44
  * Register SIGINT/SIGTERM handlers to shut down the bot cleanly.
74
- * Stops polling, cancels running jobs, persists conversations,
75
- * disarms automations, stops the life engine, and clears intervals.
76
45
  */
77
- function setupGracefulShutdown({ bot, lifeEngine, automationManager, jobManager, conversationManager, intervals }) {
46
+ function setupGracefulShutdown({ bot, lifeEngine, automationManager, jobManager, conversationManager, intervals, dashboardHandle }) {
78
47
  let shuttingDown = false;
79
48
 
80
49
  const shutdown = async (signal) => {
81
- if (shuttingDown) return; // prevent double-shutdown
50
+ if (shuttingDown) return;
82
51
  shuttingDown = true;
83
52
 
84
53
  const logger = getLogger();
85
54
  logger.info(`[Shutdown] ${signal} received — shutting down gracefully...`);
86
55
 
87
- // 1. Stop Telegram polling so no new messages arrive
88
- try {
89
- bot.stopPolling();
90
- logger.info('[Shutdown] Telegram polling stopped');
91
- } catch (err) {
92
- logger.error(`[Shutdown] Failed to stop polling: ${err.message}`);
93
- }
94
-
95
- // 2. Stop life engine heartbeat
96
- try {
97
- lifeEngine.stop();
98
- logger.info('[Shutdown] Life engine stopped');
99
- } catch (err) {
100
- logger.error(`[Shutdown] Failed to stop life engine: ${err.message}`);
101
- }
102
-
103
- // 3. Disarm all automation timers
104
- try {
105
- automationManager.shutdown();
106
- logger.info('[Shutdown] Automation timers cancelled');
107
- } catch (err) {
108
- logger.error(`[Shutdown] Failed to shutdown automations: ${err.message}`);
109
- }
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}`); }
110
59
 
111
- // 4. Cancel all running jobs
112
60
  try {
113
61
  const running = [...jobManager.jobs.values()].filter(j => !j.isTerminal);
114
- for (const job of running) {
115
- jobManager.cancelJob(job.id);
116
- }
117
- if (running.length > 0) {
118
- logger.info(`[Shutdown] Cancelled ${running.length} running job(s)`);
119
- }
120
- } catch (err) {
121
- logger.error(`[Shutdown] Failed to cancel jobs: ${err.message}`);
122
- }
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}`); }
123
65
 
124
- // 5. Persist conversations to disk
125
- try {
126
- conversationManager.save();
127
- logger.info('[Shutdown] Conversations saved');
128
- } catch (err) {
129
- logger.error(`[Shutdown] Failed to save conversations: ${err.message}`);
130
- }
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}`); }
131
68
 
132
- // 6. Clear periodic intervals
133
- for (const id of intervals) {
134
- clearInterval(id);
135
- }
69
+ for (const id of intervals) clearInterval(id);
136
70
  logger.info('[Shutdown] Periodic timers cleared');
137
-
138
71
  logger.info('[Shutdown] Graceful shutdown complete');
139
72
  process.exit(0);
140
73
  };
@@ -149,33 +82,33 @@ function viewLog(filename) {
149
82
  join(homedir(), '.kernelbot', filename),
150
83
  ];
151
84
 
152
- for (const p of paths) {
153
- if (existsSync(p)) {
154
- const content = readFileSync(p, 'utf-8');
85
+ for (const logPath of paths) {
86
+ if (existsSync(logPath)) {
87
+ const content = readFileSync(logPath, 'utf-8');
155
88
  const lines = content.split('\n').filter(Boolean);
156
89
  const recent = lines.slice(-30);
157
- console.log(chalk.dim(`\n Showing last ${recent.length} entries from ${p}\n`));
158
- for (const line of recent) {
90
+
91
+ const formatted = recent.map(line => {
159
92
  try {
160
93
  const entry = JSON.parse(line);
161
94
  const time = entry.timestamp || '';
162
95
  const level = entry.level || '';
163
96
  const msg = entry.message || '';
164
97
  const color = level === 'error' ? chalk.red : level === 'warn' ? chalk.yellow : chalk.dim;
165
- console.log(` ${chalk.dim(time)} ${color(level)} ${msg}`);
98
+ return `${chalk.dim(time)} ${color(level)} ${msg}`;
166
99
  } catch {
167
- console.log(` ${line}`);
100
+ return line;
168
101
  }
169
- }
170
- console.log('');
102
+ }).join('\n');
103
+
104
+ p.note(formatted, `Last ${recent.length} entries from ${logPath}`);
171
105
  return;
172
106
  }
173
107
  }
174
- console.log(chalk.dim(`\n No ${filename} found yet.\n`));
108
+ p.log.info(`No ${filename} found yet.`);
175
109
  }
176
110
 
177
111
  async function runCheck(config) {
178
- // Orchestrator check
179
112
  const orchProviderKey = config.orchestrator.provider || 'anthropic';
180
113
  const orchProviderDef = PROVIDERS[orchProviderKey];
181
114
  const orchLabel = orchProviderDef ? orchProviderDef.name : orchProviderKey;
@@ -198,7 +131,6 @@ async function runCheck(config) {
198
131
  await provider.ping();
199
132
  });
200
133
 
201
- // Worker brain check
202
134
  const providerDef = PROVIDERS[config.brain.provider];
203
135
  const providerLabel = providerDef ? providerDef.name : config.brain.provider;
204
136
  const envKeyLabel = providerDef ? providerDef.envKey : 'API_KEY';
@@ -217,14 +149,12 @@ async function runCheck(config) {
217
149
  });
218
150
 
219
151
  await showStartupCheck('Telegram Bot API', async () => {
220
- const res = await fetch(
221
- `https://api.telegram.org/bot${config.telegram.bot_token}/getMe`,
222
- );
152
+ const res = await fetch(`https://api.telegram.org/bot${config.telegram.bot_token}/getMe`);
223
153
  const data = await res.json();
224
154
  if (!data.ok) throw new Error(data.description || 'Invalid token');
225
155
  });
226
156
 
227
- console.log(chalk.green('\n All checks passed.\n'));
157
+ p.log.success('All checks passed.');
228
158
  }
229
159
 
230
160
  async function startBotFlow(config) {
@@ -236,7 +166,6 @@ async function startBotFlow(config) {
236
166
 
237
167
  const checks = [];
238
168
 
239
- // Orchestrator check — dynamic provider
240
169
  const orchProviderKey = config.orchestrator.provider || 'anthropic';
241
170
  const orchProviderDef = PROVIDERS[orchProviderKey];
242
171
  const orchLabel = orchProviderDef ? orchProviderDef.name : orchProviderKey;
@@ -258,7 +187,6 @@ async function startBotFlow(config) {
258
187
  }),
259
188
  );
260
189
 
261
- // Worker brain check
262
190
  checks.push(
263
191
  await showStartupCheck(`Worker (${providerLabel}) API`, async () => {
264
192
  const provider = createProvider(config);
@@ -268,9 +196,7 @@ async function startBotFlow(config) {
268
196
 
269
197
  checks.push(
270
198
  await showStartupCheck('Telegram Bot API', async () => {
271
- const res = await fetch(
272
- `https://api.telegram.org/bot${config.telegram.bot_token}/getMe`,
273
- );
199
+ const res = await fetch(`https://api.telegram.org/bot${config.telegram.bot_token}/getMe`);
274
200
  const data = await res.json();
275
201
  if (!data.ok) throw new Error(data.description || 'Invalid token');
276
202
  }),
@@ -281,53 +207,90 @@ async function startBotFlow(config) {
281
207
  return false;
282
208
  }
283
209
 
284
- const conversationManager = new ConversationManager(config);
210
+ const characterManager = new CharacterManager();
211
+ if (characterManager.needsOnboarding) {
212
+ characterManager.installAllBuiltins();
213
+ }
214
+
215
+ const activeCharacterId = characterManager.getActiveCharacterId();
216
+ const charCtx = characterManager.buildContext(activeCharacterId);
217
+
218
+ const conversationManager = new ConversationManager(config, charCtx.conversationFilePath);
285
219
  const personaManager = new UserPersonaManager();
286
- const selfManager = new SelfManager();
287
220
  const jobManager = new JobManager({
288
221
  jobTimeoutSeconds: config.swarm.job_timeout_seconds,
289
222
  cleanupIntervalMinutes: config.swarm.cleanup_interval_minutes,
290
223
  });
291
224
 
292
225
  const automationManager = new AutomationManager();
293
-
294
- // Life system managers
295
- const memoryManager = new MemoryManager();
296
- const journalManager = new JournalManager();
297
- const shareQueue = new ShareQueue();
298
- const evolutionTracker = new EvolutionTracker();
299
226
  const codebaseKnowledge = new CodebaseKnowledge({ config });
300
227
 
301
- const agent = new Agent({ config, conversationManager, personaManager, selfManager, jobManager, automationManager, memoryManager, shareQueue });
228
+ const agent = new Agent({
229
+ config, conversationManager, personaManager,
230
+ selfManager: charCtx.selfManager,
231
+ jobManager, automationManager,
232
+ memoryManager: charCtx.memoryManager,
233
+ shareQueue: charCtx.shareQueue,
234
+ characterManager,
235
+ });
302
236
 
303
- // Wire codebase knowledge to agent for LLM-powered scanning
237
+ agent.loadCharacter(activeCharacterId);
304
238
  codebaseKnowledge.setAgent(agent);
305
239
 
306
- // Life Engine — autonomous inner life
307
240
  const lifeEngine = new LifeEngine({
308
- config, agent, memoryManager, journalManager, shareQueue,
309
- evolutionTracker, codebaseKnowledge, selfManager,
241
+ config, agent,
242
+ memoryManager: charCtx.memoryManager,
243
+ journalManager: charCtx.journalManager,
244
+ shareQueue: charCtx.shareQueue,
245
+ evolutionTracker: charCtx.evolutionTracker,
246
+ codebaseKnowledge,
247
+ selfManager: charCtx.selfManager,
248
+ basePath: charCtx.lifeBasePath,
249
+ characterId: activeCharacterId,
310
250
  });
311
251
 
312
- const bot = startBot(config, agent, conversationManager, jobManager, automationManager, { lifeEngine, memoryManager, journalManager, shareQueue, evolutionTracker, codebaseKnowledge });
252
+ const dashboardDeps = {
253
+ config, jobManager, automationManager, lifeEngine, conversationManager, characterManager,
254
+ memoryManager: charCtx.memoryManager,
255
+ journalManager: charCtx.journalManager,
256
+ shareQueue: charCtx.shareQueue,
257
+ evolutionTracker: charCtx.evolutionTracker,
258
+ selfManager: charCtx.selfManager,
259
+ };
260
+
261
+ let dashboardHandle = null;
262
+ if (config.dashboard?.enabled) {
263
+ const { startDashboard } = await import('../src/dashboard/server.js');
264
+ dashboardHandle = startDashboard({ port: config.dashboard.port, ...dashboardDeps });
265
+ logger.info(`[Dashboard] Running on http://localhost:${config.dashboard.port}`);
266
+ }
267
+
268
+ const bot = startBot(config, agent, conversationManager, jobManager, automationManager, {
269
+ lifeEngine,
270
+ memoryManager: charCtx.memoryManager,
271
+ journalManager: charCtx.journalManager,
272
+ shareQueue: charCtx.shareQueue,
273
+ evolutionTracker: charCtx.evolutionTracker,
274
+ codebaseKnowledge,
275
+ characterManager,
276
+ dashboardHandle,
277
+ dashboardDeps,
278
+ });
313
279
 
314
- // Periodic job cleanup and timeout enforcement
315
280
  const cleanupMs = (config.swarm.cleanup_interval_minutes || 30) * 60 * 1000;
316
281
  const cleanupInterval = setInterval(() => {
317
282
  jobManager.cleanup();
318
283
  jobManager.enforceTimeouts();
319
- }, Math.min(cleanupMs, 60_000)); // enforce timeouts every minute at most
284
+ }, Math.min(cleanupMs, 60_000));
320
285
 
321
- // Periodic memory pruning (daily)
322
286
  const retentionDays = config.life?.memory_retention_days || 90;
323
287
  const pruneInterval = setInterval(() => {
324
- memoryManager.pruneOld(retentionDays);
325
- shareQueue.prune(7);
288
+ charCtx.memoryManager.pruneOld(retentionDays);
289
+ charCtx.shareQueue.prune(7);
326
290
  }, 24 * 3600_000);
327
291
 
328
292
  showStartupComplete();
329
293
 
330
- // Start life engine if enabled
331
294
  const lifeEnabled = config.life?.enabled !== false;
332
295
  if (lifeEnabled) {
333
296
  logger.info('[Startup] Life engine enabled — waking up...');
@@ -336,10 +299,9 @@ async function startBotFlow(config) {
336
299
  logger.info('[Startup] Life engine running');
337
300
  }).catch(err => {
338
301
  logger.error(`[Startup] Life engine wake-up failed: ${err.message}`);
339
- lifeEngine.start(); // still start heartbeat even if wake-up fails
302
+ lifeEngine.start();
340
303
  });
341
304
 
342
- // Initial codebase scan (background, non-blocking)
343
305
  if (config.life?.self_coding?.enabled) {
344
306
  codebaseKnowledge.scanChanged().then(count => {
345
307
  if (count > 0) logger.info(`[Startup] Codebase scan: ${count} files indexed`);
@@ -351,211 +313,499 @@ async function startBotFlow(config) {
351
313
  logger.info('[Startup] Life engine disabled');
352
314
  }
353
315
 
354
- // Register graceful shutdown handlers
355
316
  setupGracefulShutdown({
356
317
  bot, lifeEngine, automationManager, jobManager,
357
318
  conversationManager, intervals: [cleanupInterval, pruneInterval],
319
+ dashboardHandle,
358
320
  });
359
321
 
360
322
  return true;
361
323
  }
362
324
 
363
- async function manageCustomSkills(rl) {
325
+ async function manageCustomSkills() {
364
326
  loadCustomSkills();
365
327
 
366
328
  let managing = true;
367
329
  while (managing) {
368
330
  const customs = getCustomSkills();
369
- console.log('');
370
- console.log(chalk.bold(' Custom Skills\n'));
371
- console.log(` ${chalk.cyan('1.')} Create new skill`);
372
- console.log(` ${chalk.cyan('2.')} List skills (${customs.length})`);
373
- console.log(` ${chalk.cyan('3.')} Delete a skill`);
374
- console.log(` ${chalk.cyan('4.')} Back`);
375
- console.log('');
376
-
377
- const choice = await ask(rl, chalk.cyan(' > '));
378
- switch (choice.trim()) {
379
- case '1': {
380
- const name = await ask(rl, chalk.cyan(' Skill name: '));
381
- if (!name.trim()) {
382
- console.log(chalk.dim(' Cancelled.\n'));
383
- break;
384
- }
385
- console.log(chalk.dim(' Enter the system prompt (multi-line). Type END on a blank line to finish:\n'));
386
- const lines = [];
387
- while (true) {
388
- const line = await ask(rl, ' ');
389
- if (line.trim() === 'END') break;
390
- lines.push(line);
391
- }
392
- const prompt = lines.join('\n').trim();
393
- if (!prompt) {
394
- console.log(chalk.dim(' Empty prompt — cancelled.\n'));
395
- break;
396
- }
397
- const skill = addCustomSkill({ name: name.trim(), systemPrompt: prompt });
398
- 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})`);
399
356
  break;
400
357
  }
401
- case '2': {
402
- const customs = getCustomSkills();
358
+ case 'list': {
403
359
  if (!customs.length) {
404
- console.log(chalk.dim('\n No custom skills yet.\n'));
360
+ p.log.info('No custom skills yet.');
405
361
  break;
406
362
  }
407
- console.log('');
408
- for (const s of customs) {
363
+ const formatted = customs.map(s => {
409
364
  const preview = s.systemPrompt.slice(0, 60).replace(/\n/g, ' ');
410
- console.log(` 🛠️ ${chalk.bold(s.name)} (${s.id})`);
411
- console.log(chalk.dim(` ${preview}${s.systemPrompt.length > 60 ? '...' : ''}`));
412
- }
413
- 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');
414
368
  break;
415
369
  }
416
- case '3': {
417
- const customs = getCustomSkills();
370
+ case 'delete': {
418
371
  if (!customs.length) {
419
- console.log(chalk.dim('\n No custom skills to delete.\n'));
372
+ p.log.info('No custom skills to delete.');
420
373
  break;
421
374
  }
422
- console.log('');
423
- customs.forEach((s, i) => {
424
- 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
+ ],
425
381
  });
426
- console.log('');
427
- const pick = await ask(rl, chalk.cyan(' Delete #: '));
428
- const idx = parseInt(pick, 10) - 1;
429
- if (idx >= 0 && idx < customs.length) {
430
- const deleted = deleteCustomSkill(customs[idx].id);
431
- if (deleted) console.log(chalk.green(`\n 🗑️ Deleted: ${customs[idx].name}\n`));
432
- else console.log(chalk.dim(' Not found.\n'));
433
- } else {
434
- console.log(chalk.dim(' Cancelled.\n'));
435
- }
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}`);
436
385
  break;
437
386
  }
438
- case '4':
387
+ case 'back':
439
388
  managing = false;
440
389
  break;
441
- default:
442
- console.log(chalk.dim(' Invalid choice.\n'));
443
390
  }
444
391
  }
445
392
  }
446
393
 
447
- async function manageAutomations(rl) {
394
+ async function manageAutomations() {
448
395
  const manager = new AutomationManager();
449
396
 
450
397
  let managing = true;
451
398
  while (managing) {
452
399
  const autos = manager.listAll();
453
- console.log('');
454
- console.log(chalk.bold(' Automations\n'));
455
- console.log(` ${chalk.cyan('1.')} List all automations (${autos.length})`);
456
- console.log(` ${chalk.cyan('2.')} Delete an automation`);
457
- console.log(` ${chalk.cyan('3.')} Back`);
458
- console.log('');
459
-
460
- const choice = await ask(rl, chalk.cyan(' > '));
461
- switch (choice.trim()) {
462
- 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': {
463
413
  if (!autos.length) {
464
- console.log(chalk.dim('\n No automations found.\n'));
414
+ p.log.info('No automations found.');
465
415
  break;
466
416
  }
467
- console.log('');
468
- for (const a of autos) {
417
+ const formatted = autos.map(a => {
469
418
  const status = a.enabled ? chalk.green('enabled') : chalk.yellow('paused');
470
419
  const next = a.nextRun ? new Date(a.nextRun).toLocaleString() : 'not scheduled';
471
- console.log(` ${chalk.bold(a.name)} (${a.id}) — chat ${a.chatId}`);
472
- console.log(chalk.dim(` Status: ${status} | Runs: ${a.runCount} | Next: ${next}`));
473
- console.log(chalk.dim(` Task: ${a.description.slice(0, 80)}${a.description.length > 80 ? '...' : ''}`));
474
- }
475
- 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');
476
425
  break;
477
426
  }
478
- case '2': {
427
+ case 'delete': {
479
428
  if (!autos.length) {
480
- console.log(chalk.dim('\n No automations to delete.\n'));
429
+ p.log.info('No automations to delete.');
430
+ break;
431
+ }
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
+ ],
438
+ });
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}`);
442
+ break;
443
+ }
444
+ case 'back':
445
+ managing = false;
446
+ break;
447
+ }
448
+ }
449
+ }
450
+
451
+ async function manageCharacters(config) {
452
+ const charManager = new CharacterManager();
453
+ charManager.installAllBuiltins();
454
+
455
+ let managing = true;
456
+ while (managing) {
457
+ const characters = charManager.listCharacters();
458
+ const activeId = charManager.getActiveCharacterId();
459
+ const active = charManager.getCharacter(activeId);
460
+
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
+ })),
482
+ });
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}`);
487
+ break;
488
+ }
489
+ case 'create': {
490
+ p.log.step('Custom Character Builder');
491
+
492
+ const orchProviderKey = config.orchestrator.provider || 'anthropic';
493
+ const orchProviderDef = PROVIDERS[orchProviderKey];
494
+ const orchApiKey = config.orchestrator.api_key || (orchProviderDef && process.env[orchProviderDef.envKey]);
495
+ if (!orchApiKey) {
496
+ p.log.error('No API key configured for character generation.');
497
+ break;
498
+ }
499
+
500
+ const provider = createProvider({
501
+ brain: {
502
+ provider: orchProviderKey,
503
+ model: config.orchestrator.model,
504
+ max_tokens: config.orchestrator.max_tokens,
505
+ temperature: config.orchestrator.temperature,
506
+ api_key: orchApiKey,
507
+ },
508
+ });
509
+
510
+ const builder = new CharacterBuilder(provider);
511
+ const answers = {};
512
+ let cancelled = false;
513
+
514
+ let q = builder.getNextQuestion(answers);
515
+ while (q) {
516
+ const progress = builder.getProgress(answers);
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; }
522
+ answers[q.id] = answer.trim();
523
+ q = builder.getNextQuestion(answers);
524
+ }
525
+
526
+ if (cancelled) break;
527
+
528
+ const s = p.spinner();
529
+ s.start('Generating character...');
530
+ try {
531
+ const result = await builder.generateCharacter(answers);
532
+ s.stop('Character generated');
533
+ const id = result.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
534
+
535
+ console.log('');
536
+ showCharacterCard({ ...result, id, origin: 'Custom' });
537
+ console.log('');
538
+
539
+ const install = await p.confirm({ message: 'Install this character?' });
540
+ if (handleCancel(install) || !install) {
541
+ p.log.info('Discarded.');
542
+ break;
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!`);
551
+ } catch (err) {
552
+ s.stop(chalk.red(`Character generation failed: ${err.message}`));
553
+ }
554
+ break;
555
+ }
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
+ })),
563
+ });
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}`);
569
+ }
570
+ break;
571
+ }
572
+ case 'delete': {
573
+ const customChars = characters.filter(c => c.type === 'custom');
574
+ if (customChars.length === 0) {
575
+ p.log.info('No custom characters to delete.');
481
576
  break;
482
577
  }
483
- console.log('');
484
- autos.forEach((a, i) => {
485
- console.log(` ${chalk.cyan(`${i + 1}.`)} ${a.name} (${a.id}) — chat ${a.chatId}`);
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
+ ],
486
584
  });
487
- console.log('');
488
- const pick = await ask(rl, chalk.cyan(' Delete #: '));
489
- const idx = parseInt(pick, 10) - 1;
490
- if (idx >= 0 && idx < autos.length) {
491
- const deleted = manager.delete(autos[idx].id);
492
- if (deleted) console.log(chalk.green(`\n 🗑️ Deleted: ${autos[idx].name}\n`));
493
- else console.log(chalk.dim(' Not found.\n'));
494
- } else {
495
- 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);
496
592
  }
497
593
  break;
498
594
  }
499
- case '3':
595
+ case 'back':
500
596
  managing = false;
501
597
  break;
502
- default:
503
- console.log(chalk.dim(' Invalid choice.\n'));
504
598
  }
505
599
  }
506
600
  }
507
601
 
602
+ async function linkLinkedInCli(config) {
603
+ const { saveCredential } = await import('../src/utils/config.js');
604
+
605
+ if (config.linkedin?.access_token) {
606
+ const truncated = `${config.linkedin.access_token.slice(0, 8)}...${config.linkedin.access_token.slice(-4)}`;
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;
613
+ }
614
+
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
+ );
621
+
622
+ const token = await p.text({ message: 'Paste access token' });
623
+ if (handleCancel(token) || !token.trim()) return;
624
+
625
+ const s = p.spinner();
626
+ s.start('Validating token...');
627
+
628
+ try {
629
+ const res = await fetch('https://api.linkedin.com/v2/userinfo', {
630
+ headers: { 'Authorization': `Bearer ${token.trim()}` },
631
+ });
632
+
633
+ if (res.ok) {
634
+ const profile = await res.json();
635
+ const personUrn = `urn:li:person:${profile.sub}`;
636
+
637
+ saveCredential(config, 'LINKEDIN_ACCESS_TOKEN', token.trim());
638
+ saveCredential(config, 'LINKEDIN_PERSON_URN', personUrn);
639
+
640
+ s.stop('LinkedIn linked');
641
+ p.log.info(`Name: ${profile.name}${profile.email ? ` | Email: ${profile.email}` : ''}\nURN: ${personUrn}`);
642
+ } else if (res.status === 401) {
643
+ throw new Error('Invalid or expired token.');
644
+ } else {
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());
658
+ return;
659
+ }
660
+
661
+ const personUrn = urn.trim().startsWith('urn:li:person:') ? urn.trim() : `urn:li:person:${urn.trim()}`;
662
+ saveCredential(config, 'LINKEDIN_ACCESS_TOKEN', token.trim());
663
+ saveCredential(config, 'LINKEDIN_PERSON_URN', personUrn);
664
+
665
+ p.log.success(`LinkedIn linked — URN: ${personUrn}`);
666
+ }
667
+ } catch (err) {
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}`);
715
+ }
716
+ }
717
+
508
718
  async function main() {
719
+ const headless = process.argv.includes('--start');
720
+
509
721
  showLogo();
510
722
 
511
- const config = await loadConfigInteractive();
723
+ const config = headless ? loadConfig() : await loadConfigInteractive();
512
724
  createLogger(config);
513
725
 
514
- 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);
515
737
 
516
738
  let running = true;
517
739
  while (running) {
518
- showMenu(config);
519
- 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
+ }
520
765
 
521
- switch (choice.trim()) {
522
- case '1': {
523
- rl.close();
766
+ switch (choice) {
767
+ case 'start': {
524
768
  const started = await startBotFlow(config);
525
769
  if (!started) process.exit(1);
526
- return; // bot is running, don't show menu again
770
+ return;
527
771
  }
528
- case '2':
772
+ case 'check':
529
773
  await runCheck(config);
530
774
  break;
531
- case '3':
775
+ case 'logs':
532
776
  viewLog('kernel.log');
533
777
  break;
534
- case '4':
778
+ case 'audit':
535
779
  viewLog('kernel-audit.log');
536
780
  break;
537
- case '5':
538
- await changeBrainModel(config, rl);
781
+ case 'brain':
782
+ await changeBrainModel(config);
783
+ break;
784
+ case 'orch':
785
+ await changeOrchestratorModel(config);
786
+ break;
787
+ case 'skills':
788
+ await manageCustomSkills();
789
+ break;
790
+ case 'automations':
791
+ await manageAutomations();
539
792
  break;
540
- case '6':
541
- await changeOrchestratorModel(config, rl);
793
+ case 'characters':
794
+ await manageCharacters(config);
542
795
  break;
543
- case '7':
544
- await manageCustomSkills(rl);
796
+ case 'linkedin':
797
+ await linkLinkedInCli(config);
545
798
  break;
546
- case '8':
547
- await manageAutomations(rl);
799
+ case 'dashboard':
800
+ await manageDashboard(config);
548
801
  break;
549
- case '9':
802
+ case 'exit':
550
803
  running = false;
551
804
  break;
552
- default:
553
- console.log(chalk.dim(' Invalid choice.\n'));
554
805
  }
555
806
  }
556
807
 
557
- rl.close();
558
- console.log(chalk.dim(' Goodbye.\n'));
808
+ p.outro('Goodbye.');
559
809
  }
560
810
 
561
811
  main().catch((err) => {