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
@@ -1,11 +1,12 @@
1
1
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
2
- import { join, dirname } from 'path';
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: {
@@ -61,6 +62,12 @@ const DEFAULTS = {
61
62
  max_history: 50,
62
63
  recent_window: 10,
63
64
  },
65
+ dashboard: {
66
+ enabled: false,
67
+ port: 3000,
68
+ },
69
+ linkedin: {},
70
+ x: {},
64
71
  };
65
72
 
66
73
  function deepMerge(target, source) {
@@ -102,10 +109,6 @@ function findConfigFile() {
102
109
  return null;
103
110
  }
104
111
 
105
- function ask(rl, question) {
106
- return new Promise((res) => rl.question(question, res));
107
- }
108
-
109
112
  /**
110
113
  * Migrate legacy `anthropic` config section → `brain` section.
111
114
  */
@@ -126,50 +129,42 @@ function migrateAnthropicConfig(config) {
126
129
  }
127
130
 
128
131
  /**
129
- * Interactive provider → model picker.
132
+ * Interactive provider → model picker using @clack/prompts.
130
133
  */
131
- export async function promptProviderSelection(rl) {
134
+ export async function promptProviderSelection() {
132
135
  const providerKeys = Object.keys(PROVIDERS);
133
136
 
134
- console.log(chalk.bold('\n Select AI provider:\n'));
135
- providerKeys.forEach((key, i) => {
136
- 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
+ })),
137
143
  });
138
- console.log('');
144
+ if (handleCancel(providerKey)) return null;
139
145
 
140
- let providerIdx;
141
- while (true) {
142
- const input = await ask(rl, chalk.cyan(' Provider (number): '));
143
- providerIdx = parseInt(input.trim(), 10) - 1;
144
- if (providerIdx >= 0 && providerIdx < providerKeys.length) break;
145
- console.log(chalk.dim(' Invalid choice, try again.'));
146
- }
147
-
148
- const providerKey = providerKeys[providerIdx];
149
146
  const provider = PROVIDERS[providerKey];
150
147
 
151
- console.log(chalk.bold(`\n Select model for ${provider.name}:\n`));
152
- provider.models.forEach((m, i) => {
153
- 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
+ })),
154
155
  });
155
- console.log('');
156
-
157
- let modelIdx;
158
- while (true) {
159
- const input = await ask(rl, chalk.cyan(' Model (number): '));
160
- modelIdx = parseInt(input.trim(), 10) - 1;
161
- if (modelIdx >= 0 && modelIdx < provider.models.length) break;
162
- console.log(chalk.dim(' Invalid choice, try again.'));
163
- }
156
+ if (handleCancel(modelId)) return null;
164
157
 
165
- const model = provider.models[modelIdx];
166
- return { providerKey, modelId: model.id };
158
+ return { providerKey, modelId };
167
159
  }
168
160
 
169
161
  /**
170
- * Save provider and model to config.yaml.
162
+ * Read config.yaml, merge changes into a top-level section, and write it back.
163
+ * @param {string} section - The top-level YAML key to update (e.g. 'brain', 'orchestrator').
164
+ * @param {object} changes - Key-value pairs to merge into that section.
165
+ * @returns {string} The path to the written config file.
171
166
  */
172
- export function saveProviderToYaml(providerKey, modelId) {
167
+ function _patchConfigYaml(section, changes) {
173
168
  const configDir = getConfigDir();
174
169
  mkdirSync(configDir, { recursive: true });
175
170
  const configPath = join(configDir, 'config.yaml');
@@ -179,16 +174,24 @@ export function saveProviderToYaml(providerKey, modelId) {
179
174
  existing = yaml.load(readFileSync(configPath, 'utf-8')) || {};
180
175
  }
181
176
 
182
- existing.brain = {
183
- ...(existing.brain || {}),
184
- provider: providerKey,
185
- model: modelId,
186
- };
177
+ existing[section] = { ...(existing[section] || {}), ...changes };
178
+ writeFileSync(configPath, yaml.dump(existing, { lineWidth: -1 }));
179
+ return configPath;
180
+ }
181
+
182
+ /**
183
+ * Save provider and model to config.yaml.
184
+ */
185
+ export function saveProviderToYaml(providerKey, modelId) {
186
+ const configPath = _patchConfigYaml('brain', { provider: providerKey, model: modelId });
187
187
 
188
188
  // Remove legacy anthropic section if migrating
189
- delete existing.anthropic;
189
+ let existing = yaml.load(readFileSync(configPath, 'utf-8')) || {};
190
+ if (existing.anthropic) {
191
+ delete existing.anthropic;
192
+ writeFileSync(configPath, yaml.dump(existing, { lineWidth: -1 }));
193
+ }
190
194
 
191
- writeFileSync(configPath, yaml.dump(existing, { lineWidth: -1 }));
192
195
  return configPath;
193
196
  }
194
197
 
@@ -196,66 +199,28 @@ export function saveProviderToYaml(providerKey, modelId) {
196
199
  * Save orchestrator provider and model to config.yaml.
197
200
  */
198
201
  export function saveOrchestratorToYaml(providerKey, modelId) {
199
- const configDir = getConfigDir();
200
- mkdirSync(configDir, { recursive: true });
201
- const configPath = join(configDir, 'config.yaml');
202
-
203
- let existing = {};
204
- if (existsSync(configPath)) {
205
- existing = yaml.load(readFileSync(configPath, 'utf-8')) || {};
206
- }
207
-
208
- existing.orchestrator = {
209
- ...(existing.orchestrator || {}),
210
- provider: providerKey,
211
- model: modelId,
212
- };
202
+ return _patchConfigYaml('orchestrator', { provider: providerKey, model: modelId });
203
+ }
213
204
 
214
- writeFileSync(configPath, yaml.dump(existing, { lineWidth: -1 }));
215
- return configPath;
205
+ /**
206
+ * Save dashboard config to config.yaml.
207
+ */
208
+ export function saveDashboardToYaml(changes) {
209
+ return _patchConfigYaml('dashboard', changes);
216
210
  }
217
211
 
218
212
  /**
219
213
  * Save Claude Code model to config.yaml.
220
214
  */
221
215
  export function saveClaudeCodeModelToYaml(modelId) {
222
- const configDir = getConfigDir();
223
- mkdirSync(configDir, { recursive: true });
224
- const configPath = join(configDir, 'config.yaml');
225
-
226
- let existing = {};
227
- if (existsSync(configPath)) {
228
- existing = yaml.load(readFileSync(configPath, 'utf-8')) || {};
229
- }
230
-
231
- existing.claude_code = {
232
- ...(existing.claude_code || {}),
233
- model: modelId,
234
- };
235
-
236
- writeFileSync(configPath, yaml.dump(existing, { lineWidth: -1 }));
237
- return configPath;
216
+ return _patchConfigYaml('claude_code', { model: modelId });
238
217
  }
239
218
 
240
219
  /**
241
220
  * Save Claude Code auth mode + credential to config.yaml and .env.
242
221
  */
243
222
  export function saveClaudeCodeAuth(config, mode, value) {
244
- const configDir = getConfigDir();
245
- mkdirSync(configDir, { recursive: true });
246
- const configPath = join(configDir, 'config.yaml');
247
-
248
- let existing = {};
249
- if (existsSync(configPath)) {
250
- existing = yaml.load(readFileSync(configPath, 'utf-8')) || {};
251
- }
252
-
253
- existing.claude_code = {
254
- ...(existing.claude_code || {}),
255
- auth_mode: mode,
256
- };
257
-
258
- writeFileSync(configPath, yaml.dump(existing, { lineWidth: -1 }));
223
+ _patchConfigYaml('claude_code', { auth_mode: mode });
259
224
 
260
225
  // Update live config
261
226
  config.claude_code.auth_mode = mode;
@@ -273,26 +238,29 @@ export function saveClaudeCodeAuth(config, mode, value) {
273
238
  /**
274
239
  * Full interactive flow: change orchestrator model + optionally enter API key.
275
240
  */
276
- export async function changeOrchestratorModel(config, rl) {
241
+ export async function changeOrchestratorModel(config) {
277
242
  const { createProvider } = await import('../providers/index.js');
278
- const { providerKey, modelId } = await promptProviderSelection(rl);
243
+ const result = await promptProviderSelection();
244
+ if (!result) return config;
279
245
 
246
+ const { providerKey, modelId } = result;
280
247
  const providerDef = PROVIDERS[providerKey];
281
248
 
282
249
  // Resolve API key
283
250
  const envKey = providerDef.envKey;
284
251
  let apiKey = process.env[envKey];
285
252
  if (!apiKey) {
286
- const key = await ask(rl, chalk.cyan(`\n ${providerDef.name} API key (${envKey}): `));
287
- if (!key.trim()) {
288
- console.log(chalk.yellow('\n No API key provided. Orchestrator not changed.\n'));
289
- return config;
290
- }
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;
291
258
  apiKey = key.trim();
292
259
  }
293
260
 
294
261
  // Validate the new provider before saving anything
295
- console.log(chalk.dim(`\n Verifying ${providerDef.name} / ${modelId}...`));
262
+ const s = p.spinner();
263
+ s.start(`Verifying ${providerDef.name} / ${modelId}`);
296
264
  const testConfig = {
297
265
  brain: {
298
266
  provider: providerKey,
@@ -305,16 +273,15 @@ export async function changeOrchestratorModel(config, rl) {
305
273
  try {
306
274
  const testProvider = createProvider(testConfig);
307
275
  await testProvider.ping();
276
+ s.stop(`${providerDef.name} / ${modelId} verified`);
308
277
  } catch (err) {
309
- console.log(chalk.red(`\n ✖ Verification failed: ${err.message}`));
310
- 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.');
311
280
  return config;
312
281
  }
313
282
 
314
283
  // Validation passed — save everything
315
284
  const savedPath = saveOrchestratorToYaml(providerKey, modelId);
316
- console.log(chalk.dim(` Saved to ${savedPath}`));
317
-
318
285
  config.orchestrator.provider = providerKey;
319
286
  config.orchestrator.model = modelId;
320
287
  config.orchestrator.api_key = apiKey;
@@ -322,50 +289,51 @@ export async function changeOrchestratorModel(config, rl) {
322
289
  // Save the key if it was newly entered
323
290
  if (!process.env[envKey]) {
324
291
  saveCredential(config, envKey, apiKey);
325
- console.log(chalk.dim(' API key saved.\n'));
326
292
  }
327
293
 
328
- console.log(chalk.green(`Orchestrator switched to ${providerDef.name} / ${modelId}\n`));
294
+ p.log.success(`Orchestrator switched to ${providerDef.name} / ${modelId}`);
329
295
  return config;
330
296
  }
331
297
 
332
298
  /**
333
299
  * Full interactive flow: change brain model + optionally enter API key.
334
300
  */
335
- export async function changeBrainModel(config, rl) {
301
+ export async function changeBrainModel(config) {
336
302
  const { createProvider } = await import('../providers/index.js');
337
- const { providerKey, modelId } = await promptProviderSelection(rl);
303
+ const result = await promptProviderSelection();
304
+ if (!result) return config;
338
305
 
306
+ const { providerKey, modelId } = result;
339
307
  const providerDef = PROVIDERS[providerKey];
340
308
 
341
309
  // Resolve API key
342
310
  const envKey = providerDef.envKey;
343
311
  let apiKey = process.env[envKey];
344
312
  if (!apiKey) {
345
- const key = await ask(rl, chalk.cyan(`\n ${providerDef.name} API key (${envKey}): `));
346
- if (!key.trim()) {
347
- console.log(chalk.yellow('\n No API key provided. Brain not changed.\n'));
348
- return config;
349
- }
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;
350
318
  apiKey = key.trim();
351
319
  }
352
320
 
353
321
  // Validate the new provider before saving anything
354
- console.log(chalk.dim(`\n Verifying ${providerDef.name} / ${modelId}...`));
322
+ const s = p.spinner();
323
+ s.start(`Verifying ${providerDef.name} / ${modelId}`);
355
324
  const testConfig = { ...config, brain: { ...config.brain, provider: providerKey, model: modelId, api_key: apiKey } };
356
325
  try {
357
326
  const testProvider = createProvider(testConfig);
358
327
  await testProvider.ping();
328
+ s.stop(`${providerDef.name} / ${modelId} verified`);
359
329
  } catch (err) {
360
- console.log(chalk.red(`\n ✖ Verification failed: ${err.message}`));
361
- 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.');
362
332
  return config;
363
333
  }
364
334
 
365
335
  // Validation passed — save everything
366
- const savedPath = saveProviderToYaml(providerKey, modelId);
367
- console.log(chalk.dim(` Saved to ${savedPath}`));
368
-
336
+ saveProviderToYaml(providerKey, modelId);
369
337
  config.brain.provider = providerKey;
370
338
  config.brain.model = modelId;
371
339
  config.brain.api_key = apiKey;
@@ -373,10 +341,9 @@ export async function changeBrainModel(config, rl) {
373
341
  // Save the key if it was newly entered
374
342
  if (!process.env[envKey]) {
375
343
  saveCredential(config, envKey, apiKey);
376
- console.log(chalk.dim(' API key saved.\n'));
377
344
  }
378
345
 
379
- console.log(chalk.green(`Brain switched to ${providerDef.name} / ${modelId}\n`));
346
+ p.log.success(`Brain switched to ${providerDef.name} / ${modelId}`);
380
347
  return config;
381
348
  }
382
349
 
@@ -387,9 +354,8 @@ async function promptForMissing(config) {
387
354
 
388
355
  if (missing.length === 0) return config;
389
356
 
390
- 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.');
391
358
 
392
- const rl = createInterface({ input: process.stdin, output: process.stdout });
393
359
  const mutableConfig = JSON.parse(JSON.stringify(config));
394
360
  const envLines = [];
395
361
 
@@ -402,8 +368,11 @@ async function promptForMissing(config) {
402
368
 
403
369
  if (!mutableConfig.brain.api_key) {
404
370
  // Run brain provider selection flow
405
- console.log(chalk.bold('\n 🧠 Worker Brain'));
406
- 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;
407
376
  mutableConfig.brain.provider = providerKey;
408
377
  mutableConfig.brain.model = modelId;
409
378
  saveProviderToYaml(providerKey, modelId);
@@ -411,36 +380,49 @@ async function promptForMissing(config) {
411
380
  const providerDef = PROVIDERS[providerKey];
412
381
  const envKey = providerDef.envKey;
413
382
 
414
- 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); }
415
388
  mutableConfig.brain.api_key = key.trim();
416
389
  envLines.push(`${envKey}=${key.trim()}`);
417
390
 
418
391
  // Orchestrator provider selection
419
- console.log(chalk.bold('\n 🎛️ Orchestrator'));
420
- const sameChoice = await ask(rl, chalk.cyan(` Use same provider (${providerDef.name} / ${modelId}) for orchestrator? [Y/n]: `));
421
- 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) {
422
400
  mutableConfig.orchestrator.provider = providerKey;
423
401
  mutableConfig.orchestrator.model = modelId;
424
402
  mutableConfig.orchestrator.api_key = key.trim();
425
403
  saveOrchestratorToYaml(providerKey, modelId);
426
404
  } else {
427
- const orch = await promptProviderSelection(rl);
405
+ const orch = await promptProviderSelection();
406
+ if (!orch) { p.cancel('Setup cancelled.'); process.exit(0); }
407
+
428
408
  mutableConfig.orchestrator.provider = orch.providerKey;
429
409
  mutableConfig.orchestrator.model = orch.modelId;
430
410
  saveOrchestratorToYaml(orch.providerKey, orch.modelId);
431
411
 
432
412
  const orchProviderDef = PROVIDERS[orch.providerKey];
433
413
  if (orch.providerKey === providerKey) {
434
- // Same provider — reuse the API key
435
414
  mutableConfig.orchestrator.api_key = key.trim();
436
415
  } else {
437
- // Different provider — need a separate key
438
416
  const orchEnvKey = orchProviderDef.envKey;
439
417
  const orchExisting = process.env[orchEnvKey];
440
418
  if (orchExisting) {
441
419
  mutableConfig.orchestrator.api_key = orchExisting;
442
420
  } else {
443
- 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); }
444
426
  mutableConfig.orchestrator.api_key = orchKey.trim();
445
427
  envLines.push(`${orchEnvKey}=${orchKey.trim()}`);
446
428
  }
@@ -449,13 +431,15 @@ async function promptForMissing(config) {
449
431
  }
450
432
 
451
433
  if (!mutableConfig.telegram.bot_token) {
452
- 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); }
453
439
  mutableConfig.telegram.bot_token = token.trim();
454
440
  envLines.push(`TELEGRAM_BOT_TOKEN=${token.trim()}`);
455
441
  }
456
442
 
457
- rl.close();
458
-
459
443
  // Save to ~/.kernelbot/.env so it persists globally
460
444
  if (envLines.length > 0) {
461
445
  const configDir = getConfigDir();
@@ -465,9 +449,8 @@ async function promptForMissing(config) {
465
449
  // Merge with existing content
466
450
  let content = existingEnv ? existingEnv.trimEnd() + '\n' : '';
467
451
  for (const line of envLines) {
468
- const key = line.split('=')[0];
469
- // Replace if exists, append if not
470
- const regex = new RegExp(`^${key}=.*$`, 'm');
452
+ const envKey = line.split('=')[0];
453
+ const regex = new RegExp(`^${envKey}=.*$`, 'm');
471
454
  if (regex.test(content)) {
472
455
  content = content.replace(regex, line);
473
456
  } else {
@@ -475,7 +458,7 @@ async function promptForMissing(config) {
475
458
  }
476
459
  }
477
460
  writeFileSync(savePath, content);
478
- console.log(chalk.dim(`\n Saved to ${savePath}\n`));
461
+ p.log.info(`Saved to ${savePath}`);
479
462
  }
480
463
 
481
464
  return mutableConfig;
@@ -556,6 +539,34 @@ export function loadConfig() {
556
539
  config.claude_code.oauth_token = process.env.CLAUDE_CODE_OAUTH_TOKEN;
557
540
  }
558
541
 
542
+ // LinkedIn token-based auth from env
543
+ if (process.env.LINKEDIN_ACCESS_TOKEN) {
544
+ if (!config.linkedin) config.linkedin = {};
545
+ config.linkedin.access_token = process.env.LINKEDIN_ACCESS_TOKEN;
546
+ }
547
+ if (process.env.LINKEDIN_PERSON_URN) {
548
+ if (!config.linkedin) config.linkedin = {};
549
+ config.linkedin.person_urn = process.env.LINKEDIN_PERSON_URN;
550
+ }
551
+
552
+ // X (Twitter) OAuth 1.0a credentials from env
553
+ if (process.env.X_CONSUMER_KEY) {
554
+ if (!config.x) config.x = {};
555
+ config.x.consumer_key = process.env.X_CONSUMER_KEY;
556
+ }
557
+ if (process.env.X_CONSUMER_SECRET) {
558
+ if (!config.x) config.x = {};
559
+ config.x.consumer_secret = process.env.X_CONSUMER_SECRET;
560
+ }
561
+ if (process.env.X_ACCESS_TOKEN) {
562
+ if (!config.x) config.x = {};
563
+ config.x.access_token = process.env.X_ACCESS_TOKEN;
564
+ }
565
+ if (process.env.X_ACCESS_TOKEN_SECRET) {
566
+ if (!config.x) config.x = {};
567
+ config.x.access_token_secret = process.env.X_ACCESS_TOKEN_SECRET;
568
+ }
569
+
559
570
  return config;
560
571
  }
561
572
 
@@ -619,6 +630,30 @@ export function saveCredential(config, envKey, value) {
619
630
  if (!config.jira) config.jira = {};
620
631
  config.jira.api_token = value;
621
632
  break;
633
+ case 'LINKEDIN_ACCESS_TOKEN':
634
+ if (!config.linkedin) config.linkedin = {};
635
+ config.linkedin.access_token = value;
636
+ break;
637
+ case 'LINKEDIN_PERSON_URN':
638
+ if (!config.linkedin) config.linkedin = {};
639
+ config.linkedin.person_urn = value;
640
+ break;
641
+ case 'X_CONSUMER_KEY':
642
+ if (!config.x) config.x = {};
643
+ config.x.consumer_key = value;
644
+ break;
645
+ case 'X_CONSUMER_SECRET':
646
+ if (!config.x) config.x = {};
647
+ config.x.consumer_secret = value;
648
+ break;
649
+ case 'X_ACCESS_TOKEN':
650
+ if (!config.x) config.x = {};
651
+ config.x.access_token = value;
652
+ break;
653
+ case 'X_ACCESS_TOKEN_SECRET':
654
+ if (!config.x) config.x = {};
655
+ config.x.access_token_secret = value;
656
+ break;
622
657
  }
623
658
 
624
659
  // Also set in process.env so tools pick it up
@@ -652,5 +687,21 @@ export function getMissingCredential(toolName, config) {
652
687
  }
653
688
  }
654
689
 
690
+ const linkedinTools = ['linkedin_create_post', 'linkedin_get_my_posts', 'linkedin_get_post', 'linkedin_comment_on_post', 'linkedin_get_comments', 'linkedin_like_post', 'linkedin_get_profile', 'linkedin_delete_post'];
691
+
692
+ if (linkedinTools.includes(toolName)) {
693
+ if (!config.linkedin?.access_token && !process.env.LINKEDIN_ACCESS_TOKEN) {
694
+ return { envKey: 'LINKEDIN_ACCESS_TOKEN', label: 'LinkedIn Access Token (from /linkedin link or https://www.linkedin.com/developers/tools/oauth/token-generator)' };
695
+ }
696
+ }
697
+
698
+ const xTools = ['x_post_tweet', 'x_reply_to_tweet', 'x_get_my_tweets', 'x_get_tweet', 'x_search_tweets', 'x_like_tweet', 'x_retweet', 'x_delete_tweet', 'x_get_profile'];
699
+
700
+ if (xTools.includes(toolName)) {
701
+ if (!config.x?.consumer_key && !process.env.X_CONSUMER_KEY) {
702
+ return { envKey: 'X_CONSUMER_KEY', label: 'X (Twitter) Consumer Key (from /x link or X Developer Portal)' };
703
+ }
704
+ }
705
+
655
706
  return null;
656
707
  }