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