kernelbot 1.0.28 → 1.0.32

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.
@@ -13,6 +13,7 @@ const DEFAULTS = {
13
13
  description: 'AI engineering agent with full OS control',
14
14
  },
15
15
  orchestrator: {
16
+ provider: 'anthropic',
16
17
  model: 'claude-opus-4-6',
17
18
  max_tokens: 2048,
18
19
  temperature: 0.3,
@@ -38,6 +39,7 @@ const DEFAULTS = {
38
39
  max_turns: 50,
39
40
  timeout_seconds: 600,
40
41
  workspace_dir: null, // defaults to ~/.kernelbot/workspaces
42
+ auth_mode: 'system', // system | api_key | oauth_token
41
43
  },
42
44
  github: {
43
45
  default_branch: 'main',
@@ -190,6 +192,84 @@ export function saveProviderToYaml(providerKey, modelId) {
190
192
  return configPath;
191
193
  }
192
194
 
195
+ /**
196
+ * Save orchestrator provider and model to config.yaml.
197
+ */
198
+ 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
+ };
213
+
214
+ writeFileSync(configPath, yaml.dump(existing, { lineWidth: -1 }));
215
+ return configPath;
216
+ }
217
+
218
+ /**
219
+ * Save Claude Code model to config.yaml.
220
+ */
221
+ 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;
238
+ }
239
+
240
+ /**
241
+ * Save Claude Code auth mode + credential to config.yaml and .env.
242
+ */
243
+ 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 }));
259
+
260
+ // Update live config
261
+ config.claude_code.auth_mode = mode;
262
+
263
+ if (mode === 'api_key' && value) {
264
+ saveCredential(config, 'CLAUDE_CODE_API_KEY', value);
265
+ config.claude_code.api_key = value;
266
+ } else if (mode === 'oauth_token' && value) {
267
+ saveCredential(config, 'CLAUDE_CODE_OAUTH_TOKEN', value);
268
+ config.claude_code.oauth_token = value;
269
+ }
270
+ // mode === 'system' — no credentials to save
271
+ }
272
+
193
273
  /**
194
274
  * Full interactive flow: change brain model + optionally enter API key.
195
275
  */
@@ -329,8 +409,13 @@ export function loadConfig() {
329
409
 
330
410
  const config = deepMerge(DEFAULTS, fileConfig);
331
411
 
332
- // Orchestrator always uses Anthropic resolve its API key
333
- if (process.env.ANTHROPIC_API_KEY) {
412
+ // Orchestrator resolve API key based on configured provider
413
+ const orchProvider = PROVIDERS[config.orchestrator.provider];
414
+ if (orchProvider && process.env[orchProvider.envKey]) {
415
+ config.orchestrator.api_key = process.env[orchProvider.envKey];
416
+ }
417
+ // Legacy fallback: ANTHROPIC_API_KEY for anthropic orchestrator
418
+ if (config.orchestrator.provider === 'anthropic' && !config.orchestrator.api_key && process.env.ANTHROPIC_API_KEY) {
334
419
  config.orchestrator.api_key = process.env.ANTHROPIC_API_KEY;
335
420
  }
336
421
 
@@ -351,6 +436,16 @@ export function loadConfig() {
351
436
  if (!config.github) config.github = {};
352
437
  config.github.token = process.env.GITHUB_TOKEN;
353
438
  }
439
+ // ElevenLabs voice credentials
440
+ if (process.env.ELEVENLABS_API_KEY) {
441
+ if (!config.elevenlabs) config.elevenlabs = {};
442
+ config.elevenlabs.api_key = process.env.ELEVENLABS_API_KEY;
443
+ }
444
+ if (process.env.ELEVENLABS_VOICE_ID) {
445
+ if (!config.elevenlabs) config.elevenlabs = {};
446
+ config.elevenlabs.voice_id = process.env.ELEVENLABS_VOICE_ID;
447
+ }
448
+
354
449
  if (process.env.JIRA_BASE_URL || process.env.JIRA_EMAIL || process.env.JIRA_API_TOKEN) {
355
450
  if (!config.jira) config.jira = {};
356
451
  if (process.env.JIRA_BASE_URL) config.jira.base_url = process.env.JIRA_BASE_URL;
@@ -358,6 +453,14 @@ export function loadConfig() {
358
453
  if (process.env.JIRA_API_TOKEN) config.jira.api_token = process.env.JIRA_API_TOKEN;
359
454
  }
360
455
 
456
+ // Claude Code auth credentials from env
457
+ if (process.env.CLAUDE_CODE_API_KEY) {
458
+ config.claude_code.api_key = process.env.CLAUDE_CODE_API_KEY;
459
+ }
460
+ if (process.env.CLAUDE_CODE_OAUTH_TOKEN) {
461
+ config.claude_code.oauth_token = process.env.CLAUDE_CODE_OAUTH_TOKEN;
462
+ }
463
+
361
464
  return config;
362
465
  }
363
466
 
package/src/worker.js CHANGED
@@ -26,18 +26,22 @@ export class WorkerAgent {
26
26
  * @param {string} opts.jobId - Job ID for logging
27
27
  * @param {Array} opts.tools - Scoped tool definitions
28
28
  * @param {string|null} opts.skillId - Active skill ID (for worker prompt)
29
+ * @param {string|null} opts.workerContext - Structured context (conversation history, persona, dependency results)
29
30
  * @param {object} opts.callbacks - { onProgress, onComplete, onError }
30
31
  * @param {AbortController} opts.abortController - For cancellation
31
32
  */
32
- constructor({ config, workerType, jobId, tools, skillId, callbacks, abortController }) {
33
+ constructor({ config, workerType, jobId, tools, skillId, workerContext, callbacks, abortController }) {
33
34
  this.config = config;
34
35
  this.workerType = workerType;
35
36
  this.jobId = jobId;
36
37
  this.tools = tools;
37
38
  this.skillId = skillId;
39
+ this.workerContext = workerContext || null;
38
40
  this.callbacks = callbacks || {};
39
41
  this.abortController = abortController || new AbortController();
40
42
  this._cancelled = false;
43
+ this._toolCallCount = 0;
44
+ this._errors = [];
41
45
 
42
46
  // Create provider from worker brain config
43
47
  this.provider = createProvider(config);
@@ -51,7 +55,7 @@ export class WorkerAgent {
51
55
  this.maxIterations = 200;
52
56
 
53
57
  const logger = getLogger();
54
- logger.info(`[Worker ${jobId}] Created: type=${workerType}, provider=${config.brain.provider}/${config.brain.model}, tools=${tools.length}, skill=${skillId || 'none'}`);
58
+ logger.info(`[Worker ${jobId}] Created: type=${workerType}, provider=${config.brain.provider}/${config.brain.model}, tools=${tools.length}, skill=${skillId || 'none'}, context=${workerContext ? 'yes' : 'none'}`);
55
59
  }
56
60
 
57
61
  /** Cancel this worker. */
@@ -66,7 +70,14 @@ export class WorkerAgent {
66
70
  const logger = getLogger();
67
71
  logger.info(`[Worker ${this.jobId}] Starting task: "${task.slice(0, 150)}"`);
68
72
 
69
- const messages = [{ role: 'user', content: task }];
73
+ // Build first message: context sections + task
74
+ let firstMessage = '';
75
+ if (this.workerContext) {
76
+ firstMessage += this.workerContext + '\n\n---\n\n';
77
+ }
78
+ firstMessage += task;
79
+
80
+ const messages = [{ role: 'user', content: firstMessage }];
70
81
 
71
82
  try {
72
83
  const result = await this._runLoop(messages);
@@ -74,8 +85,9 @@ export class WorkerAgent {
74
85
  logger.info(`[Worker ${this.jobId}] Run completed but worker was cancelled — skipping callbacks`);
75
86
  return;
76
87
  }
77
- logger.info(`[Worker ${this.jobId}] Run finished successfully — result: "${(result || '').slice(0, 150)}"`);
78
- if (this.callbacks.onComplete) this.callbacks.onComplete(result);
88
+ const parsed = this._parseResult(result);
89
+ logger.info(`[Worker ${this.jobId}] Run finished successfully — structured=${!!parsed.structured}, result: "${(result || '').slice(0, 150)}"`);
90
+ if (this.callbacks.onComplete) this.callbacks.onComplete(result, parsed);
79
91
  } catch (err) {
80
92
  if (this._cancelled) {
81
93
  logger.info(`[Worker ${this.jobId}] Run threw error but worker was cancelled — ignoring: ${err.message}`);
@@ -144,6 +156,8 @@ export class WorkerAgent {
144
156
  logger.debug(`[Worker ${this.jobId}] Tool input: ${JSON.stringify(block.input).slice(0, 300)}`);
145
157
  this._reportProgress(`🔧 ${summary}`);
146
158
 
159
+ this._toolCallCount++;
160
+
147
161
  const result = await executeTool(block.name, block.input, {
148
162
  config: this.config,
149
163
  user: null, // workers don't have user context
@@ -151,8 +165,14 @@ export class WorkerAgent {
151
165
  onUpdate: this.callbacks.onUpdate || null, // Real bot onUpdate (returns message_id for coder.js smart output)
152
166
  sendPhoto: this.callbacks.sendPhoto || null,
153
167
  sessionId: this.jobId, // Per-worker browser session isolation
168
+ signal: this.abortController.signal, // For killing child processes on cancellation
154
169
  });
155
170
 
171
+ // Track errors
172
+ if (result && typeof result === 'object' && result.error) {
173
+ this._errors.push({ tool: block.name, error: result.error });
174
+ }
175
+
156
176
  const resultStr = this._truncateResult(block.name, result);
157
177
  logger.info(`[Worker ${this.jobId}] Tool ${block.name} result: ${resultStr.slice(0, 200)}`);
158
178
 
@@ -254,10 +274,83 @@ export class WorkerAgent {
254
274
  return null;
255
275
  }
256
276
 
277
+ /**
278
+ * Parse the worker's final text into a structured WorkerResult.
279
+ * Attempts JSON parse from ```json fences, falls back to wrapping raw text.
280
+ */
281
+ _parseResult(text) {
282
+ if (!text) {
283
+ return {
284
+ structured: false,
285
+ summary: 'Task completed.',
286
+ status: 'success',
287
+ details: '',
288
+ artifacts: [],
289
+ followUp: null,
290
+ toolsUsed: this._toolCallCount,
291
+ errors: this._errors,
292
+ };
293
+ }
294
+
295
+ const _str = (v) => typeof v === 'string' ? v : (v ? JSON.stringify(v, null, 2) : '');
296
+
297
+ // Try to extract JSON from ```json ... ``` fences
298
+ const fenceMatch = text.match(/```json\s*\n?([\s\S]*?)\n?\s*```/);
299
+ if (fenceMatch) {
300
+ try {
301
+ const parsed = JSON.parse(fenceMatch[1]);
302
+ if (parsed.summary && parsed.status) {
303
+ return {
304
+ structured: true,
305
+ summary: String(parsed.summary || ''),
306
+ status: String(parsed.status || 'success'),
307
+ details: _str(parsed.details),
308
+ artifacts: Array.isArray(parsed.artifacts) ? parsed.artifacts : [],
309
+ followUp: parsed.followUp ? String(parsed.followUp) : null,
310
+ toolsUsed: this._toolCallCount,
311
+ errors: this._errors,
312
+ };
313
+ }
314
+ } catch { /* fall through */ }
315
+ }
316
+
317
+ // Try raw JSON parse (no fences)
318
+ try {
319
+ const parsed = JSON.parse(text);
320
+ if (parsed.summary && parsed.status) {
321
+ return {
322
+ structured: true,
323
+ summary: String(parsed.summary || ''),
324
+ status: String(parsed.status || 'success'),
325
+ details: _str(parsed.details),
326
+ artifacts: Array.isArray(parsed.artifacts) ? parsed.artifacts : [],
327
+ followUp: parsed.followUp ? String(parsed.followUp) : null,
328
+ toolsUsed: this._toolCallCount,
329
+ errors: this._errors,
330
+ };
331
+ }
332
+ } catch { /* fall through */ }
333
+
334
+ // Fallback: wrap raw text
335
+ return {
336
+ structured: false,
337
+ summary: text.slice(0, 200),
338
+ status: 'success',
339
+ details: text,
340
+ artifacts: [],
341
+ followUp: null,
342
+ toolsUsed: this._toolCallCount,
343
+ errors: this._errors,
344
+ };
345
+ }
346
+
257
347
  _reportProgress(text) {
258
348
  if (this.callbacks.onProgress) {
259
349
  try { this.callbacks.onProgress(text); } catch {}
260
350
  }
351
+ if (this.callbacks.onHeartbeat) {
352
+ try { this.callbacks.onHeartbeat(text); } catch {}
353
+ }
261
354
  }
262
355
 
263
356
  _truncateResult(name, result) {