kernelbot 1.0.25 → 1.0.28

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/README.md +198 -123
  2. package/bin/kernel.js +201 -4
  3. package/package.json +1 -1
  4. package/src/agent.js +447 -174
  5. package/src/automation/automation-manager.js +377 -0
  6. package/src/automation/automation.js +79 -0
  7. package/src/automation/index.js +2 -0
  8. package/src/automation/scheduler.js +141 -0
  9. package/src/bot.js +908 -69
  10. package/src/conversation.js +69 -0
  11. package/src/intents/detector.js +50 -0
  12. package/src/intents/index.js +2 -0
  13. package/src/intents/planner.js +58 -0
  14. package/src/persona.js +68 -0
  15. package/src/prompts/orchestrator.js +76 -0
  16. package/src/prompts/persona.md +21 -0
  17. package/src/prompts/system.js +74 -35
  18. package/src/prompts/workers.js +89 -0
  19. package/src/providers/anthropic.js +23 -16
  20. package/src/providers/base.js +76 -2
  21. package/src/providers/index.js +1 -0
  22. package/src/providers/models.js +2 -1
  23. package/src/providers/openai-compat.js +5 -3
  24. package/src/security/confirm.js +7 -2
  25. package/src/skills/catalog.js +506 -0
  26. package/src/skills/custom.js +128 -0
  27. package/src/swarm/job-manager.js +169 -0
  28. package/src/swarm/job.js +67 -0
  29. package/src/swarm/worker-registry.js +74 -0
  30. package/src/tools/browser.js +458 -335
  31. package/src/tools/categories.js +101 -0
  32. package/src/tools/index.js +3 -0
  33. package/src/tools/orchestrator-tools.js +371 -0
  34. package/src/tools/persona.js +32 -0
  35. package/src/utils/config.js +53 -16
  36. package/src/worker.js +305 -0
  37. package/.agents/skills/interface-design/SKILL.md +0 -391
  38. package/.agents/skills/interface-design/references/critique.md +0 -67
  39. package/.agents/skills/interface-design/references/example.md +0 -86
  40. package/.agents/skills/interface-design/references/principles.md +0 -235
  41. package/.agents/skills/interface-design/references/validation.md +0 -48
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Smart tool filtering — send only relevant tools per request to save tokens.
3
+ */
4
+
5
+ export const TOOL_CATEGORIES = {
6
+ core: ['execute_command', 'read_file', 'write_file', 'list_directory', 'update_user_persona'],
7
+ git: ['git_clone', 'git_checkout', 'git_commit', 'git_push', 'git_diff'],
8
+ github: ['github_create_pr', 'github_get_pr_diff', 'github_post_review', 'github_create_repo', 'github_list_prs'],
9
+ coding: ['spawn_claude_code'],
10
+ docker: ['docker_ps', 'docker_logs', 'docker_exec', 'docker_compose'],
11
+ process: ['process_list', 'kill_process', 'service_control'],
12
+ monitor: ['disk_usage', 'memory_usage', 'cpu_usage', 'system_logs'],
13
+ network: ['check_port', 'curl_url', 'nginx_reload'],
14
+ browser: ['web_search', 'browse_website', 'screenshot_website', 'extract_content', 'send_image', 'interact_with_page'],
15
+ jira: ['jira_get_ticket', 'jira_search_tickets', 'jira_list_my_tickets', 'jira_get_project_tickets'],
16
+ };
17
+
18
+ const CATEGORY_KEYWORDS = {
19
+ coding: ['code', 'fix', 'bug', 'implement', 'refactor', 'build', 'feature', 'develop', 'program', 'write code', 'add feature', 'change', 'update', 'modify', 'create app', 'scaffold', 'debug', 'patch', 'review'],
20
+ git: ['git', 'commit', 'branch', 'merge', 'clone', 'pull', 'push', 'diff', 'stash', 'rebase', 'checkout', 'repo'],
21
+ github: ['pr', 'pull request', 'github', 'review', 'merge request'],
22
+ docker: ['docker', 'container', 'compose', 'image', 'kubernetes', 'k8s'],
23
+ process: ['process', 'kill', 'restart', 'service', 'daemon', 'systemctl', 'pid'],
24
+ monitor: ['disk', 'memory', 'cpu', 'usage', 'monitor', 'logs', 'status', 'health', 'space'],
25
+ network: ['port', 'curl', 'http', 'nginx', 'network', 'api', 'endpoint', 'request', 'url', 'fetch'],
26
+ browser: ['search', 'find', 'look up', 'browse', 'screenshot', 'scrape', 'website', 'web page', 'webpage', 'extract content', 'html', 'css selector'],
27
+ jira: ['jira', 'ticket', 'issue', 'sprint', 'backlog', 'story', 'epic'],
28
+ };
29
+
30
+ // Categories that imply other categories
31
+ const CATEGORY_DEPS = {
32
+ coding: ['git', 'github'],
33
+ github: ['git'],
34
+ };
35
+
36
+ /**
37
+ * Select relevant tools for a user message based on keyword matching.
38
+ * Always includes 'core' tools. Falls back to ALL tools if nothing specific matched.
39
+ */
40
+ export function selectToolsForMessage(userMessage, allTools) {
41
+ const lower = userMessage.toLowerCase();
42
+ const matched = new Set(['core']);
43
+
44
+ for (const [category, keywords] of Object.entries(CATEGORY_KEYWORDS)) {
45
+ for (const kw of keywords) {
46
+ if (lower.includes(kw)) {
47
+ matched.add(category);
48
+ // Add dependencies
49
+ const deps = CATEGORY_DEPS[category];
50
+ if (deps) deps.forEach((d) => matched.add(d));
51
+ break;
52
+ }
53
+ }
54
+ }
55
+
56
+ // Fallback: if only core matched, the request is ambiguous — send all tools
57
+ if (matched.size === 1) {
58
+ return allTools;
59
+ }
60
+
61
+ // Build the filtered tool name set
62
+ const toolNames = new Set();
63
+ for (const cat of matched) {
64
+ const names = TOOL_CATEGORIES[cat];
65
+ if (names) names.forEach((n) => toolNames.add(n));
66
+ }
67
+
68
+ return allTools.filter((t) => toolNames.has(t.name));
69
+ }
70
+
71
+ /**
72
+ * After a tool is used, expand the tool set to include related categories
73
+ * so the model can use follow-up tools it might need.
74
+ */
75
+ export function expandToolsForUsed(usedToolNames, currentTools, allTools) {
76
+ const currentNames = new Set(currentTools.map((t) => t.name));
77
+ const needed = new Set();
78
+
79
+ for (const name of usedToolNames) {
80
+ // Find which category this tool belongs to
81
+ for (const [cat, tools] of Object.entries(TOOL_CATEGORIES)) {
82
+ if (tools.includes(name)) {
83
+ // Add deps for that category
84
+ const deps = CATEGORY_DEPS[cat];
85
+ if (deps) {
86
+ for (const dep of deps) {
87
+ for (const t of TOOL_CATEGORIES[dep]) {
88
+ if (!currentNames.has(t)) needed.add(t);
89
+ }
90
+ }
91
+ }
92
+ break;
93
+ }
94
+ }
95
+ }
96
+
97
+ if (needed.size === 0) return currentTools;
98
+
99
+ const extra = allTools.filter((t) => needed.has(t.name));
100
+ return [...currentTools, ...extra];
101
+ }
@@ -8,6 +8,7 @@ import { definitions as githubDefinitions, handlers as githubHandlers } from './
8
8
  import { definitions as codingDefinitions, handlers as codingHandlers } from './coding.js';
9
9
  import { definitions as browserDefinitions, handlers as browserHandlers } from './browser.js';
10
10
  import { definitions as jiraDefinitions, handlers as jiraHandlers } from './jira.js';
11
+ import { definitions as personaDefinitions, handlers as personaHandlers } from './persona.js';
11
12
  import { logToolCall } from '../security/audit.js';
12
13
  import { requiresConfirmation } from '../security/confirm.js';
13
14
 
@@ -22,6 +23,7 @@ export const toolDefinitions = [
22
23
  ...codingDefinitions,
23
24
  ...browserDefinitions,
24
25
  ...jiraDefinitions,
26
+ ...personaDefinitions,
25
27
  ];
26
28
 
27
29
  const handlerMap = {
@@ -35,6 +37,7 @@ const handlerMap = {
35
37
  ...codingHandlers,
36
38
  ...browserHandlers,
37
39
  ...jiraHandlers,
40
+ ...personaHandlers,
38
41
  };
39
42
 
40
43
  export function checkConfirmation(name, params, config) {
@@ -0,0 +1,371 @@
1
+ import { WORKER_TYPES, getToolsForWorker, getToolNamesForWorkerType } from '../swarm/worker-registry.js';
2
+ import { getMissingCredential } from '../utils/config.js';
3
+ import { getLogger } from '../utils/logger.js';
4
+
5
+ const workerTypeEnum = Object.keys(WORKER_TYPES);
6
+
7
+ /**
8
+ * Tool definitions for the orchestrator's 3 meta-tools.
9
+ */
10
+ export const orchestratorToolDefinitions = [
11
+ {
12
+ name: 'dispatch_task',
13
+ description: `Dispatch a task to a specialized background worker. Available worker types: ${workerTypeEnum.join(', ')}. The worker runs in the background and you'll be notified when it completes.`,
14
+ input_schema: {
15
+ type: 'object',
16
+ properties: {
17
+ worker_type: {
18
+ type: 'string',
19
+ enum: workerTypeEnum,
20
+ description: 'The type of worker to dispatch the task to.',
21
+ },
22
+ task: {
23
+ type: 'string',
24
+ description: 'A clear, detailed description of what the worker should do.',
25
+ },
26
+ },
27
+ required: ['worker_type', 'task'],
28
+ },
29
+ },
30
+ {
31
+ name: 'list_jobs',
32
+ description: 'List all jobs for the current chat with their status, type, and summary.',
33
+ input_schema: {
34
+ type: 'object',
35
+ properties: {},
36
+ },
37
+ },
38
+ {
39
+ name: 'cancel_job',
40
+ description: 'Cancel a running or queued job by its ID.',
41
+ input_schema: {
42
+ type: 'object',
43
+ properties: {
44
+ job_id: {
45
+ type: 'string',
46
+ description: 'The ID of the job to cancel.',
47
+ },
48
+ },
49
+ required: ['job_id'],
50
+ },
51
+ },
52
+ {
53
+ name: 'create_automation',
54
+ description: 'Create a recurring automation that runs on a schedule. The task description will be executed as a standalone prompt each time it fires.',
55
+ input_schema: {
56
+ type: 'object',
57
+ properties: {
58
+ name: {
59
+ type: 'string',
60
+ description: 'Short name for the automation (e.g. "Server Health Check").',
61
+ },
62
+ description: {
63
+ type: 'string',
64
+ description: 'Detailed task prompt that will be executed each time. Must be standalone and self-contained.',
65
+ },
66
+ schedule_type: {
67
+ type: 'string',
68
+ enum: ['cron', 'interval', 'random'],
69
+ description: 'Schedule type: cron (fixed times), interval (every N minutes), random (human-like random intervals).',
70
+ },
71
+ cron_expression: {
72
+ type: 'string',
73
+ description: 'Cron expression for schedule_type=cron (e.g. "0 9 * * *" for 9am daily). 5 fields: minute hour dayOfMonth month dayOfWeek.',
74
+ },
75
+ interval_minutes: {
76
+ type: 'number',
77
+ description: 'Interval in minutes for schedule_type=interval (minimum 5).',
78
+ },
79
+ min_minutes: {
80
+ type: 'number',
81
+ description: 'Minimum interval in minutes for schedule_type=random.',
82
+ },
83
+ max_minutes: {
84
+ type: 'number',
85
+ description: 'Maximum interval in minutes for schedule_type=random.',
86
+ },
87
+ },
88
+ required: ['name', 'description', 'schedule_type'],
89
+ },
90
+ },
91
+ {
92
+ name: 'list_automations',
93
+ description: 'List all automations for the current chat with their status, schedule, and next run time.',
94
+ input_schema: {
95
+ type: 'object',
96
+ properties: {},
97
+ },
98
+ },
99
+ {
100
+ name: 'update_automation',
101
+ description: 'Update an existing automation — change its name, description, schedule, or enable/disable it.',
102
+ input_schema: {
103
+ type: 'object',
104
+ properties: {
105
+ automation_id: {
106
+ type: 'string',
107
+ description: 'The ID of the automation to update.',
108
+ },
109
+ enabled: {
110
+ type: 'boolean',
111
+ description: 'Enable or disable the automation.',
112
+ },
113
+ name: {
114
+ type: 'string',
115
+ description: 'New name for the automation.',
116
+ },
117
+ description: {
118
+ type: 'string',
119
+ description: 'New task prompt for the automation.',
120
+ },
121
+ schedule_type: {
122
+ type: 'string',
123
+ enum: ['cron', 'interval', 'random'],
124
+ description: 'New schedule type.',
125
+ },
126
+ cron_expression: { type: 'string' },
127
+ interval_minutes: { type: 'number' },
128
+ min_minutes: { type: 'number' },
129
+ max_minutes: { type: 'number' },
130
+ },
131
+ required: ['automation_id'],
132
+ },
133
+ },
134
+ {
135
+ name: 'delete_automation',
136
+ description: 'Permanently delete an automation by its ID.',
137
+ input_schema: {
138
+ type: 'object',
139
+ properties: {
140
+ automation_id: {
141
+ type: 'string',
142
+ description: 'The ID of the automation to delete.',
143
+ },
144
+ },
145
+ required: ['automation_id'],
146
+ },
147
+ },
148
+ ];
149
+
150
+ /**
151
+ * Execute an orchestrator meta-tool.
152
+ * The dispatch_task handler is async and fire-and-forget — it returns immediately.
153
+ *
154
+ * @param {string} name - Tool name
155
+ * @param {object} input - Tool input
156
+ * @param {object} context - { chatId, jobManager, config, spawnWorker }
157
+ * @returns {object} Tool result
158
+ */
159
+ export async function executeOrchestratorTool(name, input, context) {
160
+ const logger = getLogger();
161
+ const { chatId, jobManager, config, spawnWorker, automationManager } = context;
162
+
163
+ switch (name) {
164
+ case 'dispatch_task': {
165
+ const { worker_type, task } = input;
166
+ logger.info(`[dispatch_task] Request: type=${worker_type}, task="${task.slice(0, 120)}"`);
167
+
168
+ // Validate worker type
169
+ if (!WORKER_TYPES[worker_type]) {
170
+ logger.warn(`[dispatch_task] Invalid worker type: ${worker_type}`);
171
+ return { error: `Unknown worker type: ${worker_type}. Valid: ${workerTypeEnum.join(', ')}` };
172
+ }
173
+
174
+ // Check concurrent job limit
175
+ const running = jobManager.getRunningJobsForChat(chatId);
176
+ const maxConcurrent = config.swarm?.max_concurrent_jobs || 3;
177
+ logger.debug(`[dispatch_task] Running jobs: ${running.length}/${maxConcurrent} for chat ${chatId}`);
178
+ if (running.length >= maxConcurrent) {
179
+ logger.warn(`[dispatch_task] Rejected — concurrent limit reached: ${running.length}/${maxConcurrent} jobs for chat ${chatId}`);
180
+ return { error: `Maximum concurrent jobs (${maxConcurrent}) reached. Wait for a job to finish or cancel one.` };
181
+ }
182
+
183
+ // Pre-check credentials for the worker's tools
184
+ const toolNames = getToolNamesForWorkerType(worker_type);
185
+ for (const toolName of toolNames) {
186
+ const missing = getMissingCredential(toolName, config);
187
+ if (missing) {
188
+ logger.warn(`[dispatch_task] Missing credential for ${worker_type}: ${missing.envKey}`);
189
+ return {
190
+ error: `Missing credential for ${worker_type} worker: ${missing.label} (${missing.envKey}). Ask the user to provide it.`,
191
+ };
192
+ }
193
+ }
194
+
195
+ // Create and spawn the job
196
+ const job = jobManager.createJob(chatId, worker_type, task);
197
+ const workerConfig = WORKER_TYPES[worker_type];
198
+
199
+ logger.info(`[dispatch_task] Dispatching job ${job.id} — ${workerConfig.emoji} ${workerConfig.label}: "${task.slice(0, 100)}"`);
200
+
201
+ // Fire and forget — spawnWorker handles lifecycle
202
+ spawnWorker(job).catch((err) => {
203
+ logger.error(`[dispatch_task] Worker spawn error for job ${job.id}: ${err.message}`);
204
+ if (!job.isTerminal) {
205
+ jobManager.failJob(job.id, err.message);
206
+ }
207
+ });
208
+
209
+ return {
210
+ job_id: job.id,
211
+ worker_type,
212
+ status: 'dispatched',
213
+ message: `${workerConfig.emoji} ${workerConfig.label} started.`,
214
+ };
215
+ }
216
+
217
+ case 'list_jobs': {
218
+ const jobs = jobManager.getJobsForChat(chatId);
219
+ logger.info(`[list_jobs] Chat ${chatId} — ${jobs.length} jobs found`);
220
+ if (jobs.length > 0) {
221
+ logger.debug(`[list_jobs] Jobs: ${jobs.slice(0, 5).map(j => `${j.id}[${j.status}]`).join(', ')}${jobs.length > 5 ? '...' : ''}`);
222
+ }
223
+ if (jobs.length === 0) {
224
+ return { message: 'No jobs for this chat.' };
225
+ }
226
+ return {
227
+ jobs: jobs.slice(0, 20).map((j) => ({
228
+ id: j.id,
229
+ worker_type: j.workerType,
230
+ status: j.status,
231
+ task: j.task.slice(0, 100),
232
+ duration: j.duration,
233
+ summary: j.toSummary(),
234
+ })),
235
+ };
236
+ }
237
+
238
+ case 'cancel_job': {
239
+ const { job_id } = input;
240
+ logger.info(`[cancel_job] Request to cancel job ${job_id} in chat ${chatId}`);
241
+ const job = jobManager.cancelJob(job_id);
242
+ if (!job) {
243
+ logger.warn(`[cancel_job] Job ${job_id} not found or already finished`);
244
+ return { error: `Job ${job_id} not found or already finished.` };
245
+ }
246
+ logger.info(`[cancel_job] Successfully cancelled job ${job_id} [${job.workerType}]`);
247
+ return {
248
+ job_id: job.id,
249
+ status: 'cancelled',
250
+ message: `Cancelled ${WORKER_TYPES[job.workerType]?.emoji || ''} ${job.workerType} worker.`,
251
+ };
252
+ }
253
+
254
+ case 'create_automation': {
255
+ if (!automationManager) return { error: 'Automation system not available.' };
256
+
257
+ const { name: autoName, description, schedule_type, cron_expression, interval_minutes, min_minutes, max_minutes } = input;
258
+ logger.info(`[create_automation] Request: name="${autoName}", type=${schedule_type}`);
259
+
260
+ let schedule;
261
+ switch (schedule_type) {
262
+ case 'cron':
263
+ schedule = { type: 'cron', expression: cron_expression };
264
+ break;
265
+ case 'interval':
266
+ schedule = { type: 'interval', minutes: interval_minutes };
267
+ break;
268
+ case 'random':
269
+ schedule = { type: 'random', minMinutes: min_minutes, maxMinutes: max_minutes };
270
+ break;
271
+ default:
272
+ return { error: `Unknown schedule type: ${schedule_type}` };
273
+ }
274
+
275
+ try {
276
+ const auto = automationManager.create(chatId, { name: autoName, description, schedule });
277
+ return {
278
+ automation_id: auto.id,
279
+ name: auto.name,
280
+ schedule: auto.schedule,
281
+ next_run: auto.nextRun ? new Date(auto.nextRun).toLocaleString() : null,
282
+ message: `Automation "${auto.name}" created and armed.`,
283
+ };
284
+ } catch (err) {
285
+ logger.warn(`[create_automation] Failed: ${err.message}`);
286
+ return { error: err.message };
287
+ }
288
+ }
289
+
290
+ case 'list_automations': {
291
+ if (!automationManager) return { error: 'Automation system not available.' };
292
+
293
+ const autos = automationManager.listForChat(chatId);
294
+ logger.info(`[list_automations] Chat ${chatId} — ${autos.length} automation(s)`);
295
+
296
+ if (autos.length === 0) {
297
+ return { message: 'No automations for this chat.' };
298
+ }
299
+
300
+ return {
301
+ automations: autos.map((a) => ({
302
+ id: a.id,
303
+ name: a.name,
304
+ description: a.description.slice(0, 100),
305
+ schedule: a.schedule,
306
+ enabled: a.enabled,
307
+ next_run: a.nextRun ? new Date(a.nextRun).toLocaleString() : null,
308
+ run_count: a.runCount,
309
+ last_error: a.lastError,
310
+ summary: a.toSummary(),
311
+ })),
312
+ };
313
+ }
314
+
315
+ case 'update_automation': {
316
+ if (!automationManager) return { error: 'Automation system not available.' };
317
+
318
+ const { automation_id, enabled, name: newName, description: newDesc, schedule_type: newSchedType, cron_expression: newCron, interval_minutes: newInterval, min_minutes: newMin, max_minutes: newMax } = input;
319
+ logger.info(`[update_automation] Request: id=${automation_id}`);
320
+
321
+ const changes = {};
322
+ if (enabled !== undefined) changes.enabled = enabled;
323
+ if (newName !== undefined) changes.name = newName;
324
+ if (newDesc !== undefined) changes.description = newDesc;
325
+
326
+ if (newSchedType !== undefined) {
327
+ switch (newSchedType) {
328
+ case 'cron':
329
+ changes.schedule = { type: 'cron', expression: newCron };
330
+ break;
331
+ case 'interval':
332
+ changes.schedule = { type: 'interval', minutes: newInterval };
333
+ break;
334
+ case 'random':
335
+ changes.schedule = { type: 'random', minMinutes: newMin, maxMinutes: newMax };
336
+ break;
337
+ }
338
+ }
339
+
340
+ try {
341
+ const auto = automationManager.update(automation_id, changes);
342
+ if (!auto) return { error: `Automation ${automation_id} not found.` };
343
+ return {
344
+ automation_id: auto.id,
345
+ name: auto.name,
346
+ enabled: auto.enabled,
347
+ schedule: auto.schedule,
348
+ next_run: auto.nextRun ? new Date(auto.nextRun).toLocaleString() : null,
349
+ message: `Automation "${auto.name}" updated.`,
350
+ };
351
+ } catch (err) {
352
+ logger.warn(`[update_automation] Failed: ${err.message}`);
353
+ return { error: err.message };
354
+ }
355
+ }
356
+
357
+ case 'delete_automation': {
358
+ if (!automationManager) return { error: 'Automation system not available.' };
359
+
360
+ const { automation_id } = input;
361
+ logger.info(`[delete_automation] Request: id=${automation_id}`);
362
+
363
+ const deleted = automationManager.delete(automation_id);
364
+ if (!deleted) return { error: `Automation ${automation_id} not found.` };
365
+ return { automation_id, status: 'deleted', message: `Automation deleted.` };
366
+ }
367
+
368
+ default:
369
+ return { error: `Unknown orchestrator tool: ${name}` };
370
+ }
371
+ }
@@ -0,0 +1,32 @@
1
+ export const definitions = [
2
+ {
3
+ name: 'update_user_persona',
4
+ description:
5
+ 'Update the stored persona/profile for the current user. ' +
6
+ 'Pass the COMPLETE updated persona document as markdown. ' +
7
+ 'Before calling this, mentally merge any new information into the existing persona — ' +
8
+ 'do not blindly append. Only call when you discover genuinely new, meaningful information ' +
9
+ '(expertise, preferences, projects, communication style).',
10
+ input_schema: {
11
+ type: 'object',
12
+ properties: {
13
+ content: {
14
+ type: 'string',
15
+ description: 'The full updated persona markdown document.',
16
+ },
17
+ },
18
+ required: ['content'],
19
+ },
20
+ },
21
+ ];
22
+
23
+ export const handlers = {
24
+ async update_user_persona({ content }, context) {
25
+ const { personaManager, user } = context;
26
+ if (!personaManager) return { error: 'Persona manager not available.' };
27
+ if (!user?.id) return { error: 'User ID not available.' };
28
+
29
+ personaManager.save(user.id, content);
30
+ return { success: true, message: 'User persona updated.' };
31
+ },
32
+ };
@@ -12,15 +12,26 @@ const DEFAULTS = {
12
12
  name: 'KernelBot',
13
13
  description: 'AI engineering agent with full OS control',
14
14
  },
15
+ orchestrator: {
16
+ model: 'claude-opus-4-6',
17
+ max_tokens: 2048,
18
+ temperature: 0.3,
19
+ max_tool_depth: 5,
20
+ },
15
21
  brain: {
16
22
  provider: 'anthropic',
17
23
  model: 'claude-sonnet-4-20250514',
18
- max_tokens: 8192,
24
+ max_tokens: 4096,
19
25
  temperature: 0.3,
20
- max_tool_depth: 25,
26
+ },
27
+ swarm: {
28
+ max_concurrent_jobs: 3,
29
+ job_timeout_seconds: 300,
30
+ cleanup_interval_minutes: 30,
21
31
  },
22
32
  telegram: {
23
33
  allowed_users: [],
34
+ batch_window_ms: 3000,
24
35
  },
25
36
  claude_code: {
26
37
  model: 'claude-opus-4-6',
@@ -46,6 +57,7 @@ const DEFAULTS = {
46
57
  },
47
58
  conversation: {
48
59
  max_history: 50,
60
+ recent_window: 10,
49
61
  },
50
62
  };
51
63
 
@@ -182,30 +194,50 @@ export function saveProviderToYaml(providerKey, modelId) {
182
194
  * Full interactive flow: change brain model + optionally enter API key.
183
195
  */
184
196
  export async function changeBrainModel(config, rl) {
197
+ const { createProvider } = await import('../providers/index.js');
185
198
  const { providerKey, modelId } = await promptProviderSelection(rl);
186
199
 
187
200
  const providerDef = PROVIDERS[providerKey];
201
+
202
+ // Resolve API key
203
+ const envKey = providerDef.envKey;
204
+ let apiKey = process.env[envKey];
205
+ if (!apiKey) {
206
+ const key = await ask(rl, chalk.cyan(`\n ${providerDef.name} API key (${envKey}): `));
207
+ if (!key.trim()) {
208
+ console.log(chalk.yellow('\n No API key provided. Brain not changed.\n'));
209
+ return config;
210
+ }
211
+ apiKey = key.trim();
212
+ }
213
+
214
+ // Validate the new provider before saving anything
215
+ console.log(chalk.dim(`\n Verifying ${providerDef.name} / ${modelId}...`));
216
+ const testConfig = { ...config, brain: { ...config.brain, provider: providerKey, model: modelId, api_key: apiKey } };
217
+ try {
218
+ const testProvider = createProvider(testConfig);
219
+ await testProvider.ping();
220
+ } catch (err) {
221
+ console.log(chalk.red(`\n ✖ Verification failed: ${err.message}`));
222
+ console.log(chalk.yellow(` Brain not changed. Keeping current model.\n`));
223
+ return config;
224
+ }
225
+
226
+ // Validation passed — save everything
188
227
  const savedPath = saveProviderToYaml(providerKey, modelId);
189
- console.log(chalk.dim(`\n Saved to ${savedPath}`));
228
+ console.log(chalk.dim(` Saved to ${savedPath}`));
190
229
 
191
- // Update live config
192
230
  config.brain.provider = providerKey;
193
231
  config.brain.model = modelId;
232
+ config.brain.api_key = apiKey;
194
233
 
195
- // Check if we have the API key for this provider
196
- const envKey = providerDef.envKey;
197
- const currentKey = process.env[envKey];
198
- if (!currentKey) {
199
- const key = await ask(rl, chalk.cyan(`\n ${providerDef.name} API key (${envKey}): `));
200
- if (key.trim()) {
201
- saveCredential(config, envKey, key.trim());
202
- config.brain.api_key = key.trim();
203
- console.log(chalk.dim(' Saved.\n'));
204
- }
205
- } else {
206
- config.brain.api_key = currentKey;
234
+ // Save the key if it was newly entered
235
+ if (!process.env[envKey]) {
236
+ saveCredential(config, envKey, apiKey);
237
+ console.log(chalk.dim(' API key saved.\n'));
207
238
  }
208
239
 
240
+ console.log(chalk.green(` ✔ Brain switched to ${providerDef.name} / ${modelId}\n`));
209
241
  return config;
210
242
  }
211
243
 
@@ -297,6 +329,11 @@ export function loadConfig() {
297
329
 
298
330
  const config = deepMerge(DEFAULTS, fileConfig);
299
331
 
332
+ // Orchestrator always uses Anthropic — resolve its API key
333
+ if (process.env.ANTHROPIC_API_KEY) {
334
+ config.orchestrator.api_key = process.env.ANTHROPIC_API_KEY;
335
+ }
336
+
300
337
  // Overlay env vars for brain API key based on provider
301
338
  const providerDef = PROVIDERS[config.brain.provider];
302
339
  if (providerDef && process.env[providerDef.envKey]) {