kernelbot 1.0.28 → 1.0.30
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/.env.example +4 -0
- package/README.md +0 -0
- package/bin/kernel.js +13 -6
- package/config.example.yaml +14 -1
- package/package.json +1 -1
- package/src/agent.js +482 -27
- package/src/automation/automation-manager.js +0 -0
- package/src/automation/automation.js +0 -0
- package/src/automation/index.js +0 -0
- package/src/automation/scheduler.js +0 -0
- package/src/bot.js +340 -3
- package/src/claude-auth.js +93 -0
- package/src/coder.js +48 -6
- package/src/conversation.js +0 -0
- package/src/intents/detector.js +0 -0
- package/src/intents/index.js +0 -0
- package/src/intents/planner.js +0 -0
- package/src/persona.js +0 -0
- package/src/prompts/orchestrator.js +53 -5
- package/src/prompts/persona.md +0 -0
- package/src/prompts/system.js +0 -0
- package/src/prompts/workers.js +61 -2
- package/src/providers/anthropic.js +0 -0
- package/src/providers/base.js +0 -0
- package/src/providers/index.js +0 -0
- package/src/providers/models.js +0 -0
- package/src/providers/openai-compat.js +0 -0
- package/src/security/audit.js +0 -0
- package/src/security/auth.js +0 -0
- package/src/security/confirm.js +0 -0
- package/src/self.js +122 -0
- package/src/services/stt.js +139 -0
- package/src/services/tts.js +124 -0
- package/src/skills/catalog.js +0 -0
- package/src/skills/custom.js +0 -0
- package/src/swarm/job-manager.js +54 -7
- package/src/swarm/job.js +19 -1
- package/src/swarm/worker-registry.js +5 -0
- package/src/tools/browser.js +0 -0
- package/src/tools/categories.js +0 -0
- package/src/tools/coding.js +5 -0
- package/src/tools/docker.js +0 -0
- package/src/tools/git.js +0 -0
- package/src/tools/github.js +0 -0
- package/src/tools/index.js +0 -0
- package/src/tools/jira.js +0 -0
- package/src/tools/monitor.js +0 -0
- package/src/tools/network.js +0 -0
- package/src/tools/orchestrator-tools.js +76 -19
- package/src/tools/os.js +14 -1
- package/src/tools/persona.js +0 -0
- package/src/tools/process.js +0 -0
- package/src/utils/config.js +105 -2
- package/src/utils/display.js +0 -0
- package/src/utils/logger.js +0 -0
- package/src/worker.js +96 -5
package/src/swarm/job-manager.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { EventEmitter } from 'events';
|
|
2
2
|
import { Job } from './job.js';
|
|
3
|
+
import { WORKER_TYPES } from './worker-registry.js';
|
|
3
4
|
import { getLogger } from '../utils/logger.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -10,6 +11,7 @@ import { getLogger } from '../utils/logger.js';
|
|
|
10
11
|
* job:completed (job)
|
|
11
12
|
* job:failed (job)
|
|
12
13
|
* job:cancelled (job)
|
|
14
|
+
* job:ready (job) — emitted when a queued job's dependencies are all met
|
|
13
15
|
*/
|
|
14
16
|
export class JobManager extends EventEmitter {
|
|
15
17
|
constructor({ jobTimeoutSeconds = 300, cleanupIntervalMinutes = 30 } = {}) {
|
|
@@ -22,9 +24,12 @@ export class JobManager extends EventEmitter {
|
|
|
22
24
|
/** Create a new job (status: queued). */
|
|
23
25
|
createJob(chatId, workerType, task) {
|
|
24
26
|
const job = new Job({ chatId, workerType, task });
|
|
27
|
+
// Set per-job timeout from worker type config, fall back to global
|
|
28
|
+
const workerConfig = WORKER_TYPES[workerType];
|
|
29
|
+
job.timeoutMs = workerConfig?.timeout ? workerConfig.timeout * 1000 : this.jobTimeoutMs;
|
|
25
30
|
this.jobs.set(job.id, job);
|
|
26
31
|
const logger = getLogger();
|
|
27
|
-
logger.info(`[JobManager] Job created: ${job.id} [${workerType}] for chat ${chatId} — "${task.slice(0, 100)}"`);
|
|
32
|
+
logger.info(`[JobManager] Job created: ${job.id} [${workerType}] for chat ${chatId} — timeout: ${job.timeoutMs / 1000}s — "${task.slice(0, 100)}"`);
|
|
28
33
|
logger.debug(`[JobManager] Total jobs tracked: ${this.jobs.size}`);
|
|
29
34
|
return job;
|
|
30
35
|
}
|
|
@@ -36,13 +41,17 @@ export class JobManager extends EventEmitter {
|
|
|
36
41
|
getLogger().info(`[JobManager] Job ${job.id} [${job.workerType}] → running`);
|
|
37
42
|
}
|
|
38
43
|
|
|
39
|
-
/** Move a job to completed with a result. */
|
|
40
|
-
completeJob(jobId, result) {
|
|
44
|
+
/** Move a job to completed with a result and optional structured data. */
|
|
45
|
+
completeJob(jobId, result, structuredResult = null) {
|
|
41
46
|
const job = this._get(jobId);
|
|
42
47
|
job.transition('completed');
|
|
43
48
|
job.result = result;
|
|
44
|
-
|
|
49
|
+
if (structuredResult) {
|
|
50
|
+
job.structuredResult = structuredResult;
|
|
51
|
+
}
|
|
52
|
+
getLogger().info(`[JobManager] Job ${job.id} [${job.workerType}] → completed (${job.duration}s) — result: ${(result || '').length} chars, structured: ${!!structuredResult}`);
|
|
45
53
|
this.emit('job:completed', job);
|
|
54
|
+
this._checkDependents(job);
|
|
46
55
|
}
|
|
47
56
|
|
|
48
57
|
/** Move a job to failed with an error message. */
|
|
@@ -52,6 +61,7 @@ export class JobManager extends EventEmitter {
|
|
|
52
61
|
job.error = typeof error === 'string' ? error : error?.message || String(error);
|
|
53
62
|
getLogger().error(`[JobManager] Job ${job.id} [${job.workerType}] → failed (${job.duration}s) — ${job.error}`);
|
|
54
63
|
this.emit('job:failed', job);
|
|
64
|
+
this._failDependents(job);
|
|
55
65
|
}
|
|
56
66
|
|
|
57
67
|
/** Cancel a specific job. Returns the job or null if not found / already terminal. */
|
|
@@ -145,13 +155,14 @@ export class JobManager extends EventEmitter {
|
|
|
145
155
|
if (job.status === 'running' && job.startedAt) {
|
|
146
156
|
checkedCount++;
|
|
147
157
|
const elapsed = now - job.startedAt;
|
|
148
|
-
|
|
149
|
-
|
|
158
|
+
const timeoutMs = job.timeoutMs || this.jobTimeoutMs;
|
|
159
|
+
if (elapsed > timeoutMs) {
|
|
160
|
+
getLogger().warn(`[JobManager] Job ${job.id} [${job.workerType}] timed out — elapsed: ${Math.round(elapsed / 1000)}s, limit: ${timeoutMs / 1000}s`);
|
|
150
161
|
// Cancel the worker first so it stops executing & frees resources
|
|
151
162
|
if (job.worker && typeof job.worker.cancel === 'function') {
|
|
152
163
|
job.worker.cancel();
|
|
153
164
|
}
|
|
154
|
-
this.failJob(job.id, `Timed out after ${
|
|
165
|
+
this.failJob(job.id, `Timed out after ${timeoutMs / 1000}s`);
|
|
155
166
|
}
|
|
156
167
|
}
|
|
157
168
|
}
|
|
@@ -160,6 +171,42 @@ export class JobManager extends EventEmitter {
|
|
|
160
171
|
}
|
|
161
172
|
}
|
|
162
173
|
|
|
174
|
+
/** Check if any queued jobs have all dependencies met after a job completes. */
|
|
175
|
+
_checkDependents(completedJob) {
|
|
176
|
+
const logger = getLogger();
|
|
177
|
+
for (const job of this.jobs.values()) {
|
|
178
|
+
if (job.status !== 'queued' || job.dependsOn.length === 0) continue;
|
|
179
|
+
if (!job.dependsOn.includes(completedJob.id)) continue;
|
|
180
|
+
|
|
181
|
+
// Check if ALL dependencies are completed
|
|
182
|
+
const allMet = job.dependsOn.every(depId => {
|
|
183
|
+
const dep = this.jobs.get(depId);
|
|
184
|
+
return dep && dep.status === 'completed';
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
if (allMet) {
|
|
188
|
+
logger.info(`[JobManager] Job ${job.id} [${job.workerType}] — all dependencies met, emitting job:ready`);
|
|
189
|
+
this.emit('job:ready', job);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Cascade failure to dependent jobs when a job fails. */
|
|
195
|
+
_failDependents(failedJob) {
|
|
196
|
+
const logger = getLogger();
|
|
197
|
+
for (const job of this.jobs.values()) {
|
|
198
|
+
if (job.status !== 'queued' || job.dependsOn.length === 0) continue;
|
|
199
|
+
if (!job.dependsOn.includes(failedJob.id)) continue;
|
|
200
|
+
|
|
201
|
+
logger.warn(`[JobManager] Job ${job.id} [${job.workerType}] — dependency ${failedJob.id} failed, cascading failure`);
|
|
202
|
+
job.transition('failed');
|
|
203
|
+
job.error = `Dependency job ${failedJob.id} failed: ${failedJob.error || 'unknown error'}`;
|
|
204
|
+
this.emit('job:failed', job);
|
|
205
|
+
// Recursively cascade to jobs depending on this one
|
|
206
|
+
this._failDependents(job);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
163
210
|
/** Internal: get job or throw. */
|
|
164
211
|
_get(jobId) {
|
|
165
212
|
const job = this.jobs.get(jobId);
|
package/src/swarm/job.js
CHANGED
|
@@ -20,12 +20,19 @@ export class Job {
|
|
|
20
20
|
this.task = task;
|
|
21
21
|
this.status = 'queued';
|
|
22
22
|
this.result = null;
|
|
23
|
+
this.structuredResult = null; // WorkerResult: { summary, status, details, artifacts, followUp, toolsUsed, errors }
|
|
24
|
+
this.context = null; // Orchestrator-provided context string
|
|
25
|
+
this.dependsOn = []; // Job IDs this job depends on
|
|
26
|
+
this.userId = null; // Telegram user ID for persona loading
|
|
23
27
|
this.error = null;
|
|
24
28
|
this.worker = null; // WorkerAgent ref
|
|
25
29
|
this.statusMessageId = null; // Telegram message for progress edits
|
|
26
30
|
this.createdAt = Date.now();
|
|
27
31
|
this.startedAt = null;
|
|
28
32
|
this.completedAt = null;
|
|
33
|
+
this.timeoutMs = null; // Per-job timeout (set from worker type config)
|
|
34
|
+
this.progress = []; // Recent activity entries
|
|
35
|
+
this.lastActivity = null; // Timestamp of last activity
|
|
29
36
|
}
|
|
30
37
|
|
|
31
38
|
/** Transition to a new status. Throws if the transition is invalid. */
|
|
@@ -46,6 +53,13 @@ export class Job {
|
|
|
46
53
|
return Math.round((end - this.startedAt) / 1000);
|
|
47
54
|
}
|
|
48
55
|
|
|
56
|
+
/** Record a progress heartbeat from the worker. Caps at 20 entries. */
|
|
57
|
+
addProgress(text) {
|
|
58
|
+
this.progress.push(text);
|
|
59
|
+
if (this.progress.length > 20) this.progress.shift();
|
|
60
|
+
this.lastActivity = Date.now();
|
|
61
|
+
}
|
|
62
|
+
|
|
49
63
|
/** Whether this job is in a terminal state. */
|
|
50
64
|
get isTerminal() {
|
|
51
65
|
return ['completed', 'failed', 'cancelled'].includes(this.status);
|
|
@@ -62,6 +76,10 @@ export class Job {
|
|
|
62
76
|
};
|
|
63
77
|
const emoji = statusEmoji[this.status] || '❓';
|
|
64
78
|
const dur = this.duration != null ? ` (${this.duration}s)` : '';
|
|
65
|
-
|
|
79
|
+
const summaryText = this.structuredResult?.summary || null;
|
|
80
|
+
const lastAct = !summaryText && this.progress.length > 0 ? ` | ${this.progress[this.progress.length - 1]}` : '';
|
|
81
|
+
const resultSnippet = summaryText ? ` | ${summaryText.slice(0, 80)}` : '';
|
|
82
|
+
const deps = this.dependsOn.length > 0 ? ` [deps: ${this.dependsOn.join(',')}]` : '';
|
|
83
|
+
return `${emoji} \`${this.id}\` [${this.workerType}] ${this.task.slice(0, 60)}${this.task.length > 60 ? '...' : ''}${dur}${resultSnippet}${lastAct}${deps}`;
|
|
66
84
|
}
|
|
67
85
|
}
|
|
@@ -10,30 +10,35 @@ export const WORKER_TYPES = {
|
|
|
10
10
|
emoji: '💻',
|
|
11
11
|
categories: ['core', 'coding', 'git', 'github'],
|
|
12
12
|
description: 'Write code, fix bugs, create PRs',
|
|
13
|
+
timeout: 86400, // 24 hours — Claude Code can legitimately run for hours
|
|
13
14
|
},
|
|
14
15
|
browser: {
|
|
15
16
|
label: 'Browser Worker',
|
|
16
17
|
emoji: '🌐',
|
|
17
18
|
categories: ['browser'],
|
|
18
19
|
description: 'Web search, scraping, screenshots',
|
|
20
|
+
timeout: 300, // 5 minutes
|
|
19
21
|
},
|
|
20
22
|
system: {
|
|
21
23
|
label: 'System Worker',
|
|
22
24
|
emoji: '🖥️',
|
|
23
25
|
categories: ['core', 'process', 'monitor', 'network'],
|
|
24
26
|
description: 'OS operations, monitoring, network',
|
|
27
|
+
timeout: 600, // 10 minutes
|
|
25
28
|
},
|
|
26
29
|
devops: {
|
|
27
30
|
label: 'DevOps Worker',
|
|
28
31
|
emoji: '🚀',
|
|
29
32
|
categories: ['core', 'docker', 'process', 'monitor', 'network', 'git'],
|
|
30
33
|
description: 'Docker, deploy, infrastructure',
|
|
34
|
+
timeout: 3600, // 1 hour
|
|
31
35
|
},
|
|
32
36
|
research: {
|
|
33
37
|
label: 'Research Worker',
|
|
34
38
|
emoji: '🔍',
|
|
35
39
|
categories: ['browser', 'core'],
|
|
36
40
|
description: 'Deep web research and analysis',
|
|
41
|
+
timeout: 600, // 10 minutes
|
|
37
42
|
},
|
|
38
43
|
};
|
|
39
44
|
|
package/src/tools/browser.js
CHANGED
|
File without changes
|
package/src/tools/categories.js
CHANGED
|
File without changes
|
package/src/tools/coding.js
CHANGED
|
@@ -10,6 +10,10 @@ function getSpawner(config) {
|
|
|
10
10
|
return spawner;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
export function resetClaudeCodeSpawner() {
|
|
14
|
+
spawner = null;
|
|
15
|
+
}
|
|
16
|
+
|
|
13
17
|
export const definitions = [
|
|
14
18
|
{
|
|
15
19
|
name: 'spawn_claude_code',
|
|
@@ -57,6 +61,7 @@ export const handlers = {
|
|
|
57
61
|
prompt: params.prompt,
|
|
58
62
|
maxTurns: params.max_turns,
|
|
59
63
|
onOutput: onUpdate,
|
|
64
|
+
signal: context.signal || null,
|
|
60
65
|
});
|
|
61
66
|
|
|
62
67
|
// Show stderr if any
|
package/src/tools/docker.js
CHANGED
|
File without changes
|
package/src/tools/git.js
CHANGED
|
File without changes
|
package/src/tools/github.js
CHANGED
|
File without changes
|
package/src/tools/index.js
CHANGED
|
File without changes
|
package/src/tools/jira.js
CHANGED
|
File without changes
|
package/src/tools/monitor.js
CHANGED
|
File without changes
|
package/src/tools/network.js
CHANGED
|
File without changes
|
|
@@ -23,6 +23,15 @@ export const orchestratorToolDefinitions = [
|
|
|
23
23
|
type: 'string',
|
|
24
24
|
description: 'A clear, detailed description of what the worker should do.',
|
|
25
25
|
},
|
|
26
|
+
context: {
|
|
27
|
+
type: 'string',
|
|
28
|
+
description: 'Optional background context for the worker — relevant conversation details, goals, constraints. The worker cannot see the chat history, so include anything important here.',
|
|
29
|
+
},
|
|
30
|
+
depends_on: {
|
|
31
|
+
type: 'array',
|
|
32
|
+
items: { type: 'string' },
|
|
33
|
+
description: 'Optional array of job IDs that must complete before this worker starts. The worker will receive dependency results as context.',
|
|
34
|
+
},
|
|
26
35
|
},
|
|
27
36
|
required: ['worker_type', 'task'],
|
|
28
37
|
},
|
|
@@ -158,12 +167,12 @@ export const orchestratorToolDefinitions = [
|
|
|
158
167
|
*/
|
|
159
168
|
export async function executeOrchestratorTool(name, input, context) {
|
|
160
169
|
const logger = getLogger();
|
|
161
|
-
const { chatId, jobManager, config, spawnWorker, automationManager } = context;
|
|
170
|
+
const { chatId, jobManager, config, spawnWorker, automationManager, user } = context;
|
|
162
171
|
|
|
163
172
|
switch (name) {
|
|
164
173
|
case 'dispatch_task': {
|
|
165
|
-
const { worker_type, task } = input;
|
|
166
|
-
logger.info(`[dispatch_task] Request: type=${worker_type}, task="${task.slice(0, 120)}"`);
|
|
174
|
+
const { worker_type, task, context: taskContext, depends_on } = input;
|
|
175
|
+
logger.info(`[dispatch_task] Request: type=${worker_type}, task="${task.slice(0, 120)}", deps=${depends_on?.length || 0}`);
|
|
167
176
|
|
|
168
177
|
// Validate worker type
|
|
169
178
|
if (!WORKER_TYPES[worker_type]) {
|
|
@@ -171,13 +180,34 @@ export async function executeOrchestratorTool(name, input, context) {
|
|
|
171
180
|
return { error: `Unknown worker type: ${worker_type}. Valid: ${workerTypeEnum.join(', ')}` };
|
|
172
181
|
}
|
|
173
182
|
|
|
174
|
-
//
|
|
175
|
-
const
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
183
|
+
// Validate dependency IDs
|
|
184
|
+
const depIds = Array.isArray(depends_on) ? depends_on : [];
|
|
185
|
+
for (const depId of depIds) {
|
|
186
|
+
const depJob = jobManager.getJob(depId);
|
|
187
|
+
if (!depJob) {
|
|
188
|
+
logger.warn(`[dispatch_task] Unknown dependency job: ${depId}`);
|
|
189
|
+
return { error: `Dependency job not found: ${depId}` };
|
|
190
|
+
}
|
|
191
|
+
if (depJob.status === 'failed' || depJob.status === 'cancelled') {
|
|
192
|
+
logger.warn(`[dispatch_task] Dependency job ${depId} already ${depJob.status}`);
|
|
193
|
+
return { error: `Dependency job ${depId} already ${depJob.status}. Cannot dispatch.` };
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Check concurrent job limit (only if we'll spawn immediately)
|
|
198
|
+
const hasUnmetDeps = depIds.some(id => {
|
|
199
|
+
const dep = jobManager.getJob(id);
|
|
200
|
+
return dep && dep.status !== 'completed';
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
if (!hasUnmetDeps) {
|
|
204
|
+
const running = jobManager.getRunningJobsForChat(chatId);
|
|
205
|
+
const maxConcurrent = config.swarm?.max_concurrent_jobs || 3;
|
|
206
|
+
logger.debug(`[dispatch_task] Running jobs: ${running.length}/${maxConcurrent} for chat ${chatId}`);
|
|
207
|
+
if (running.length >= maxConcurrent) {
|
|
208
|
+
logger.warn(`[dispatch_task] Rejected — concurrent limit reached: ${running.length}/${maxConcurrent} jobs for chat ${chatId}`);
|
|
209
|
+
return { error: `Maximum concurrent jobs (${maxConcurrent}) reached. Wait for a job to finish or cancel one.` };
|
|
210
|
+
}
|
|
181
211
|
}
|
|
182
212
|
|
|
183
213
|
// Pre-check credentials for the worker's tools
|
|
@@ -192,10 +222,25 @@ export async function executeOrchestratorTool(name, input, context) {
|
|
|
192
222
|
}
|
|
193
223
|
}
|
|
194
224
|
|
|
195
|
-
// Create
|
|
225
|
+
// Create the job with context and dependencies
|
|
196
226
|
const job = jobManager.createJob(chatId, worker_type, task);
|
|
227
|
+
job.context = taskContext || null;
|
|
228
|
+
job.dependsOn = depIds;
|
|
229
|
+
job.userId = user?.id || null;
|
|
197
230
|
const workerConfig = WORKER_TYPES[worker_type];
|
|
198
231
|
|
|
232
|
+
// If dependencies are not all met, leave job queued (job:ready will spawn it later)
|
|
233
|
+
if (hasUnmetDeps) {
|
|
234
|
+
logger.info(`[dispatch_task] Job ${job.id} queued — waiting for dependencies: ${depIds.join(', ')}`);
|
|
235
|
+
return {
|
|
236
|
+
job_id: job.id,
|
|
237
|
+
worker_type,
|
|
238
|
+
status: 'queued_waiting',
|
|
239
|
+
depends_on: depIds,
|
|
240
|
+
message: `${workerConfig.emoji} ${workerConfig.label} queued — waiting for ${depIds.length} dependency job(s) to complete.`,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
199
244
|
logger.info(`[dispatch_task] Dispatching job ${job.id} — ${workerConfig.emoji} ${workerConfig.label}: "${task.slice(0, 100)}"`);
|
|
200
245
|
|
|
201
246
|
// Fire and forget — spawnWorker handles lifecycle
|
|
@@ -224,14 +269,26 @@ export async function executeOrchestratorTool(name, input, context) {
|
|
|
224
269
|
return { message: 'No jobs for this chat.' };
|
|
225
270
|
}
|
|
226
271
|
return {
|
|
227
|
-
jobs: jobs.slice(0, 20).map((j) =>
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
272
|
+
jobs: jobs.slice(0, 20).map((j) => {
|
|
273
|
+
const entry = {
|
|
274
|
+
id: j.id,
|
|
275
|
+
worker_type: j.workerType,
|
|
276
|
+
status: j.status,
|
|
277
|
+
task: j.task.slice(0, 100),
|
|
278
|
+
duration: j.duration,
|
|
279
|
+
recent_activity: j.progress.slice(-5),
|
|
280
|
+
last_activity_seconds_ago: j.lastActivity ? Math.round((Date.now() - j.lastActivity) / 1000) : null,
|
|
281
|
+
summary: j.toSummary(),
|
|
282
|
+
};
|
|
283
|
+
if (j.dependsOn.length > 0) entry.depends_on = j.dependsOn;
|
|
284
|
+
if (j.structuredResult) {
|
|
285
|
+
entry.result_summary = j.structuredResult.summary;
|
|
286
|
+
entry.result_status = j.structuredResult.status;
|
|
287
|
+
if (j.structuredResult.artifacts?.length > 0) entry.artifacts = j.structuredResult.artifacts;
|
|
288
|
+
if (j.structuredResult.followUp) entry.follow_up = j.structuredResult.followUp;
|
|
289
|
+
}
|
|
290
|
+
return entry;
|
|
291
|
+
}),
|
|
235
292
|
};
|
|
236
293
|
}
|
|
237
294
|
|
package/src/tools/os.js
CHANGED
|
@@ -122,12 +122,15 @@ export const handlers = {
|
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
return new Promise((res) => {
|
|
125
|
+
let abortHandler = null;
|
|
126
|
+
|
|
125
127
|
const child = exec(
|
|
126
128
|
command,
|
|
127
129
|
{ timeout: timeout_seconds * 1000, maxBuffer: 10 * 1024 * 1024 },
|
|
128
130
|
(error, stdout, stderr) => {
|
|
131
|
+
if (abortHandler && context.signal) context.signal.removeEventListener('abort', abortHandler);
|
|
129
132
|
if (error && error.killed) {
|
|
130
|
-
return res({ error: `Command timed out after ${timeout_seconds}s` });
|
|
133
|
+
return res({ error: `Command timed out or was cancelled after ${timeout_seconds}s` });
|
|
131
134
|
}
|
|
132
135
|
const result = {
|
|
133
136
|
stdout: stdout || '',
|
|
@@ -147,6 +150,16 @@ export const handlers = {
|
|
|
147
150
|
res(result);
|
|
148
151
|
},
|
|
149
152
|
);
|
|
153
|
+
|
|
154
|
+
// Wire abort signal to kill the child process
|
|
155
|
+
if (context.signal) {
|
|
156
|
+
if (context.signal.aborted) {
|
|
157
|
+
child.kill('SIGTERM');
|
|
158
|
+
} else {
|
|
159
|
+
abortHandler = () => child.kill('SIGTERM');
|
|
160
|
+
context.signal.addEventListener('abort', abortHandler, { once: true });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
150
163
|
});
|
|
151
164
|
},
|
|
152
165
|
|
package/src/tools/persona.js
CHANGED
|
File without changes
|
package/src/tools/process.js
CHANGED
|
File without changes
|
package/src/utils/config.js
CHANGED
|
@@ -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
|
|
333
|
-
|
|
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/utils/display.js
CHANGED
|
File without changes
|
package/src/utils/logger.js
CHANGED
|
File without changes
|