kernelbot 1.0.39 → 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 +5 -5
- package/config.example.yaml +1 -1
- package/package.json +1 -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/life/codebase.js +2 -1
- package/src/life/daydream_engine.js +386 -0
- package/src/life/engine.js +1 -0
- 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 +1 -1
- package/src/utils/display.js +1 -1
- 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/git.js
CHANGED
|
@@ -10,22 +10,29 @@ function getWorkspaceDir(config) {
|
|
|
10
10
|
return dir;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Get the auth header value for GitHub HTTPS operations.
|
|
15
|
+
* Uses extraheader instead of embedding credentials in the URL,
|
|
16
|
+
* preventing token leaks in git remote -v, error messages, and process listings.
|
|
17
|
+
*/
|
|
18
|
+
function getGitAuthEnv(config) {
|
|
14
19
|
const token = config.github?.token || process.env.GITHUB_TOKEN;
|
|
15
|
-
if (!token) return
|
|
20
|
+
if (!token) return null;
|
|
21
|
+
const base64 = Buffer.from(`x-access-token:${token}`).toString('base64');
|
|
22
|
+
return `AUTHORIZATION: basic ${base64}`;
|
|
23
|
+
}
|
|
16
24
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
// Not a parseable URL (e.g. org/repo shorthand before expansion)
|
|
25
|
+
/**
|
|
26
|
+
* Configure a simple-git instance with auth via extraheader (not URL embedding).
|
|
27
|
+
*/
|
|
28
|
+
function configureGitAuth(git, config) {
|
|
29
|
+
const authHeader = getGitAuthEnv(config);
|
|
30
|
+
if (authHeader) {
|
|
31
|
+
git.env('GIT_CONFIG_COUNT', '1');
|
|
32
|
+
git.env('GIT_CONFIG_KEY_0', 'http.extraheader');
|
|
33
|
+
git.env('GIT_CONFIG_VALUE_0', authHeader);
|
|
27
34
|
}
|
|
28
|
-
return
|
|
35
|
+
return git;
|
|
29
36
|
}
|
|
30
37
|
|
|
31
38
|
export const definitions = [
|
|
@@ -107,15 +114,19 @@ export const handlers = {
|
|
|
107
114
|
url = `https://github.com/${repo}.git`;
|
|
108
115
|
}
|
|
109
116
|
|
|
110
|
-
// Inject GitHub token for authenticated clone (enables push later)
|
|
111
|
-
const authUrl = injectToken(url, context.config);
|
|
112
|
-
|
|
113
117
|
const repoName = dest || repo.split('/').pop().replace('.git', '');
|
|
118
|
+
// Prevent path traversal — dest must not escape workspace directory
|
|
119
|
+
if (repoName.includes('..') || repoName.startsWith('/')) {
|
|
120
|
+
return { error: 'Invalid destination: path traversal is not allowed' };
|
|
121
|
+
}
|
|
114
122
|
const targetDir = join(workspaceDir, repoName);
|
|
123
|
+
if (!targetDir.startsWith(workspaceDir)) {
|
|
124
|
+
return { error: 'Invalid destination: path escapes workspace directory' };
|
|
125
|
+
}
|
|
115
126
|
|
|
116
127
|
try {
|
|
117
|
-
const git = simpleGit();
|
|
118
|
-
await git.clone(
|
|
128
|
+
const git = configureGitAuth(simpleGit(), context.config);
|
|
129
|
+
await git.clone(url, targetDir);
|
|
119
130
|
return { success: true, path: targetDir };
|
|
120
131
|
} catch (err) {
|
|
121
132
|
getLogger().error(`git_clone failed for ${params.repo}: ${err.message}`);
|
|
@@ -155,17 +166,8 @@ export const handlers = {
|
|
|
155
166
|
git_push: async (params, context) => {
|
|
156
167
|
const { dir, force = false } = params;
|
|
157
168
|
try {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
// Ensure remote URL has auth token for push
|
|
161
|
-
const remotes = await git.getRemotes(true);
|
|
162
|
-
const origin = remotes.find((r) => r.name === 'origin');
|
|
163
|
-
if (origin) {
|
|
164
|
-
const authUrl = injectToken(origin.refs.push || origin.refs.fetch, context.config);
|
|
165
|
-
if (authUrl !== (origin.refs.push || origin.refs.fetch)) {
|
|
166
|
-
await git.remote(['set-url', 'origin', authUrl]);
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
+
// Use extraheader auth instead of modifying remote URLs
|
|
170
|
+
const git = configureGitAuth(simpleGit(dir), context.config);
|
|
169
171
|
|
|
170
172
|
const branch = (await git.branchLocal()).current;
|
|
171
173
|
const options = ['-u'];
|
package/src/tools/jira.js
CHANGED
|
@@ -186,7 +186,11 @@ export const handlers = {
|
|
|
186
186
|
const client = getJiraClient(context.config);
|
|
187
187
|
const assignee = params.assignee || 'currentUser()';
|
|
188
188
|
const maxResults = params.max_results || 20;
|
|
189
|
-
|
|
189
|
+
// Sanitize assignee to prevent JQL injection — allow currentUser() or quote the value
|
|
190
|
+
const safeAssignee = assignee === 'currentUser()'
|
|
191
|
+
? 'currentUser()'
|
|
192
|
+
: `"${assignee.replace(/["\\]/g, '')}"`;
|
|
193
|
+
const jql = `assignee = ${safeAssignee} ORDER BY updated DESC`;
|
|
190
194
|
|
|
191
195
|
const { data } = await client.get('/search', {
|
|
192
196
|
params: {
|
|
@@ -215,7 +219,12 @@ export const handlers = {
|
|
|
215
219
|
try {
|
|
216
220
|
const client = getJiraClient(context.config);
|
|
217
221
|
const maxResults = params.max_results || 20;
|
|
218
|
-
|
|
222
|
+
// Sanitize project_key to prevent JQL injection — only allow alphanumeric and underscore
|
|
223
|
+
const safeProjectKey = params.project_key.replace(/[^a-zA-Z0-9_]/g, '');
|
|
224
|
+
if (safeProjectKey !== params.project_key) {
|
|
225
|
+
return { error: `Invalid project key: "${params.project_key}". Only alphanumeric characters and underscores are allowed.` };
|
|
226
|
+
}
|
|
227
|
+
const jql = `project = "${safeProjectKey}" ORDER BY updated DESC`;
|
|
219
228
|
|
|
220
229
|
const { data } = await client.get('/search', {
|
|
221
230
|
params: {
|
package/src/tools/monitor.js
CHANGED
|
@@ -75,7 +75,15 @@ export const handlers = {
|
|
|
75
75
|
return await run(`journalctl -n ${finalLines}${filterArg} --no-pager`);
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
// Reading a log file
|
|
78
|
+
// Reading a log file — restrict to known safe directories
|
|
79
|
+
const { resolve: resolvePath } = await import('path');
|
|
80
|
+
const resolvedSource = resolvePath(source);
|
|
81
|
+
const ALLOWED_LOG_DIRS = ['/var/log', process.cwd(), process.env.HOME || ''];
|
|
82
|
+
const isAllowed = ALLOWED_LOG_DIRS.some(dir => dir && resolvedSource.startsWith(dir));
|
|
83
|
+
if (!isAllowed) {
|
|
84
|
+
return { error: `Blocked: log source must be within /var/log, the project directory, or home directory` };
|
|
85
|
+
}
|
|
86
|
+
|
|
79
87
|
const filterCmd = filter ? ` | grep -i ${shellEscape(filter)}` : '';
|
|
80
88
|
return await run(`tail -n ${finalLines} ${shellEscape(source)}${filterCmd}`);
|
|
81
89
|
},
|
package/src/tools/network.js
CHANGED
|
@@ -37,6 +37,25 @@ export const definitions = [
|
|
|
37
37
|
},
|
|
38
38
|
];
|
|
39
39
|
|
|
40
|
+
// SSRF protection: block requests to internal/cloud metadata addresses
|
|
41
|
+
const BLOCKED_HOSTS = [
|
|
42
|
+
/^127\./,
|
|
43
|
+
/^10\./,
|
|
44
|
+
/^172\.(1[6-9]|2\d|3[01])\./,
|
|
45
|
+
/^192\.168\./,
|
|
46
|
+
/^169\.254\./, // AWS/cloud metadata endpoint
|
|
47
|
+
/^0\./,
|
|
48
|
+
/^localhost$/i,
|
|
49
|
+
/^metadata\.google\.internal$/i,
|
|
50
|
+
/^\[::1\]$/,
|
|
51
|
+
/^\[fd/i, // IPv6 private
|
|
52
|
+
/^\[fe80:/i, // IPv6 link-local
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
function isBlockedHost(host) {
|
|
56
|
+
return BLOCKED_HOSTS.some(pattern => pattern.test(host));
|
|
57
|
+
}
|
|
58
|
+
|
|
40
59
|
export const handlers = {
|
|
41
60
|
check_port: async (params) => {
|
|
42
61
|
const logger = getLogger();
|
|
@@ -44,6 +63,11 @@ export const handlers = {
|
|
|
44
63
|
const port = parseInt(params.port, 10);
|
|
45
64
|
if (!Number.isFinite(port) || port <= 0 || port > 65535) return { error: 'Invalid port number' };
|
|
46
65
|
|
|
66
|
+
// SSRF protection: block internal network probing
|
|
67
|
+
if (host !== 'localhost' && isBlockedHost(host)) {
|
|
68
|
+
return { error: 'Blocked: cannot probe internal or cloud metadata addresses' };
|
|
69
|
+
}
|
|
70
|
+
|
|
47
71
|
logger.debug(`check_port: checking ${host}:${port}`);
|
|
48
72
|
// Use nc (netcat) for port check — works on both macOS and Linux
|
|
49
73
|
const result = await run(`nc -z -w 3 ${shellEscape(host)} ${port} 2>&1 && echo "OPEN" || echo "CLOSED"`, 5000);
|
|
@@ -60,6 +84,16 @@ export const handlers = {
|
|
|
60
84
|
curl_url: async (params) => {
|
|
61
85
|
const { url, method = 'GET', headers, body } = params;
|
|
62
86
|
|
|
87
|
+
// SSRF protection: block requests to internal networks and cloud metadata
|
|
88
|
+
try {
|
|
89
|
+
const parsed = new URL(url);
|
|
90
|
+
if (isBlockedHost(parsed.hostname)) {
|
|
91
|
+
return { error: 'Blocked: cannot access internal or cloud metadata addresses' };
|
|
92
|
+
}
|
|
93
|
+
} catch {
|
|
94
|
+
return { error: 'Invalid URL' };
|
|
95
|
+
}
|
|
96
|
+
|
|
63
97
|
let cmd = `curl -s -w "\\n---HTTP_STATUS:%{http_code}" -X ${shellEscape(method)}`;
|
|
64
98
|
|
|
65
99
|
if (headers) {
|
|
@@ -770,7 +770,8 @@ export async function executeOrchestratorTool(name, input, context) {
|
|
|
770
770
|
if (!conversationManager) return { error: 'Conversation system not available.' };
|
|
771
771
|
|
|
772
772
|
const { query, chat_id } = input;
|
|
773
|
-
|
|
773
|
+
// Prevent cross-chat history access — only allow searching own chat
|
|
774
|
+
const targetChatId = chatId;
|
|
774
775
|
logger.info(`[search_conversations] Searching chat ${targetChatId} for: "${query}"`);
|
|
775
776
|
|
|
776
777
|
const history = conversationManager.getHistory(targetChatId);
|
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
|
@@ -38,7 +38,7 @@ const DEFAULTS = {
|
|
|
38
38
|
claude_code: {
|
|
39
39
|
model: 'claude-opus-4-6',
|
|
40
40
|
max_turns: 50,
|
|
41
|
-
timeout_seconds:
|
|
41
|
+
timeout_seconds: 86400,
|
|
42
42
|
workspace_dir: null, // defaults to ~/.kernelbot/workspaces
|
|
43
43
|
auth_mode: 'system', // system | api_key | oauth_token
|
|
44
44
|
},
|
package/src/utils/display.js
CHANGED
|
@@ -121,7 +121,7 @@ export function showCharacterCard(character, isActive = false) {
|
|
|
121
121
|
...(art ? art.split("\n").map((line) => chalk.cyan(line)) : []),
|
|
122
122
|
"",
|
|
123
123
|
chalk.dim(`Origin: ${character.origin || "Unknown"}`),
|
|
124
|
-
chalk.dim(`
|
|
124
|
+
chalk.dim(`Age: ${character.age || "Unknown"}`),
|
|
125
125
|
].join("\n");
|
|
126
126
|
|
|
127
127
|
console.log(
|
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
|
}),
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Time Awareness Utility — Riyadh-local helpers.
|
|
3
|
+
*
|
|
4
|
+
* Provides timezone-aware helpers anchored to Asia/Riyadh so that
|
|
5
|
+
* automated tasks can respect the user's real-world schedule.
|
|
6
|
+
*
|
|
7
|
+
* Quiet hours (02:00 – 10:00 Riyadh time) are intentionally wider than
|
|
8
|
+
* the generic defaults in timeUtils.js because the owner fasts during
|
|
9
|
+
* Ramadan and typically sleeps through the early-morning hours.
|
|
10
|
+
*
|
|
11
|
+
* @module utils/timeAwareness
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const TIMEZONE = 'Asia/Riyadh';
|
|
15
|
+
const QUIET_START_HOUR = 2;
|
|
16
|
+
const QUIET_END_HOUR = 10;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Return the current date/time in the Asia/Riyadh timezone.
|
|
20
|
+
*
|
|
21
|
+
* The returned object contains the full ISO-style formatted string,
|
|
22
|
+
* the numeric hour (0-23), and the raw Date for further processing.
|
|
23
|
+
*
|
|
24
|
+
* @returns {{ formatted: string, hour: number, date: Date }}
|
|
25
|
+
*/
|
|
26
|
+
export function getCurrentRiyadhTime() {
|
|
27
|
+
const now = new Date();
|
|
28
|
+
|
|
29
|
+
const formatted = now.toLocaleString('en-US', {
|
|
30
|
+
weekday: 'long',
|
|
31
|
+
year: 'numeric',
|
|
32
|
+
month: 'long',
|
|
33
|
+
day: 'numeric',
|
|
34
|
+
hour: '2-digit',
|
|
35
|
+
minute: '2-digit',
|
|
36
|
+
second: '2-digit',
|
|
37
|
+
hour12: true,
|
|
38
|
+
timeZone: TIMEZONE,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const hourParts = new Intl.DateTimeFormat('en-US', {
|
|
42
|
+
hour: 'numeric',
|
|
43
|
+
hour12: false,
|
|
44
|
+
timeZone: TIMEZONE,
|
|
45
|
+
}).formatToParts(now);
|
|
46
|
+
|
|
47
|
+
const hour = parseInt(
|
|
48
|
+
hourParts.find((p) => p.type === 'hour')?.value || '0',
|
|
49
|
+
10,
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
return { formatted, hour, date: now };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Check whether the current Riyadh time falls within quiet hours.
|
|
57
|
+
*
|
|
58
|
+
* Quiet hours are defined as **02:00 – 10:00 (Asia/Riyadh)** to
|
|
59
|
+
* respect the user's sleep schedule during Ramadan fasting, when
|
|
60
|
+
* they tend to sleep through the early-morning period.
|
|
61
|
+
*
|
|
62
|
+
* Automated tasks (notifications, self-improvement PRs, noisy jobs)
|
|
63
|
+
* should check this flag and defer non-urgent work until after the
|
|
64
|
+
* quiet window closes.
|
|
65
|
+
*
|
|
66
|
+
* @returns {boolean} `true` when the current Riyadh time is between
|
|
67
|
+
* 02:00 and 10:00 (inclusive start, exclusive end).
|
|
68
|
+
*/
|
|
69
|
+
export function isQuietHours() {
|
|
70
|
+
const { hour } = getCurrentRiyadhTime();
|
|
71
|
+
return hour >= QUIET_START_HOUR && hour < QUIET_END_HOUR;
|
|
72
|
+
}
|
package/src/worker.js
CHANGED
|
@@ -3,7 +3,7 @@ import { executeTool } from './tools/index.js';
|
|
|
3
3
|
import { closeSession } from './tools/browser.js';
|
|
4
4
|
import { getMissingCredential } from './utils/config.js';
|
|
5
5
|
import { getWorkerPrompt } from './prompts/workers.js';
|
|
6
|
-
import {
|
|
6
|
+
import { buildSkillPrompt } from './skills/loader.js';
|
|
7
7
|
import { getLogger } from './utils/logger.js';
|
|
8
8
|
import { truncateToolResult } from './utils/truncate.js';
|
|
9
9
|
|
|
@@ -23,17 +23,17 @@ export class WorkerAgent {
|
|
|
23
23
|
* @param {string} opts.workerType - coding, browser, system, devops, research
|
|
24
24
|
* @param {string} opts.jobId - Job ID for logging
|
|
25
25
|
* @param {Array} opts.tools - Scoped tool definitions
|
|
26
|
-
* @param {string|null} opts.
|
|
26
|
+
* @param {string[]|null} opts.skillIds - Active skill IDs (for worker prompt)
|
|
27
27
|
* @param {string|null} opts.workerContext - Structured context (conversation history, persona, dependency results)
|
|
28
28
|
* @param {object} opts.callbacks - { onProgress, onComplete, onError }
|
|
29
29
|
* @param {AbortController} opts.abortController - For cancellation
|
|
30
30
|
*/
|
|
31
|
-
constructor({ config, workerType, jobId, tools,
|
|
31
|
+
constructor({ config, workerType, jobId, tools, skillIds, workerContext, callbacks, abortController }) {
|
|
32
32
|
this.config = config;
|
|
33
33
|
this.workerType = workerType;
|
|
34
34
|
this.jobId = jobId;
|
|
35
35
|
this.tools = tools;
|
|
36
|
-
this.
|
|
36
|
+
this.skillIds = skillIds || [];
|
|
37
37
|
this.workerContext = workerContext || null;
|
|
38
38
|
this.callbacks = callbacks || {};
|
|
39
39
|
this.abortController = abortController || new AbortController();
|
|
@@ -45,8 +45,8 @@ export class WorkerAgent {
|
|
|
45
45
|
// Create provider from worker brain config
|
|
46
46
|
this.provider = createProvider(config);
|
|
47
47
|
|
|
48
|
-
// Build system prompt
|
|
49
|
-
const skillPrompt =
|
|
48
|
+
// Build system prompt with combined skill expertise
|
|
49
|
+
const skillPrompt = buildSkillPrompt(this.skillIds);
|
|
50
50
|
this.systemPrompt = getWorkerPrompt(workerType, config, skillPrompt);
|
|
51
51
|
|
|
52
52
|
// Safety ceiling — not a real limit, just prevents infinite loops
|
|
@@ -54,7 +54,7 @@ export class WorkerAgent {
|
|
|
54
54
|
this.maxIterations = 200;
|
|
55
55
|
|
|
56
56
|
const logger = getLogger();
|
|
57
|
-
logger.info(`[Worker ${jobId}] Created: type=${workerType}, provider=${config.brain.provider}/${config.brain.model}, tools=${tools.length},
|
|
57
|
+
logger.info(`[Worker ${jobId}] Created: type=${workerType}, provider=${config.brain.provider}/${config.brain.model}, tools=${tools.length}, skills=${this.skillIds.length > 0 ? this.skillIds.join(',') : 'none'}, context=${workerContext ? 'yes' : 'none'}`);
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
/** Cancel this worker. */
|
|
@@ -297,41 +297,34 @@ export class WorkerAgent {
|
|
|
297
297
|
|
|
298
298
|
const _str = (v) => typeof v === 'string' ? v : (v ? JSON.stringify(v, null, 2) : '');
|
|
299
299
|
|
|
300
|
+
/** Try to build a structured result from a parsed JSON object. */
|
|
301
|
+
const _fromParsed = (parsed) => {
|
|
302
|
+
if (!parsed?.summary || !parsed?.status) return null;
|
|
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
|
+
|
|
300
315
|
// Try to extract JSON from ```json ... ``` fences
|
|
301
316
|
const fenceMatch = text.match(/```json\s*\n?([\s\S]*?)\n?\s*```/);
|
|
302
317
|
if (fenceMatch) {
|
|
303
318
|
try {
|
|
304
|
-
const
|
|
305
|
-
if (
|
|
306
|
-
return {
|
|
307
|
-
structured: true,
|
|
308
|
-
summary: String(parsed.summary || ''),
|
|
309
|
-
status: String(parsed.status || 'success'),
|
|
310
|
-
details: _str(parsed.details),
|
|
311
|
-
artifacts: Array.isArray(parsed.artifacts) ? parsed.artifacts : [],
|
|
312
|
-
followUp: parsed.followUp ? String(parsed.followUp) : null,
|
|
313
|
-
toolsUsed: this._toolCallCount,
|
|
314
|
-
errors: this._errors,
|
|
315
|
-
};
|
|
316
|
-
}
|
|
319
|
+
const result = _fromParsed(JSON.parse(fenceMatch[1]));
|
|
320
|
+
if (result) return result;
|
|
317
321
|
} catch { /* fall through */ }
|
|
318
322
|
}
|
|
319
323
|
|
|
320
324
|
// Try raw JSON parse (no fences)
|
|
321
325
|
try {
|
|
322
|
-
const
|
|
323
|
-
if (
|
|
324
|
-
return {
|
|
325
|
-
structured: true,
|
|
326
|
-
summary: String(parsed.summary || ''),
|
|
327
|
-
status: String(parsed.status || 'success'),
|
|
328
|
-
details: _str(parsed.details),
|
|
329
|
-
artifacts: Array.isArray(parsed.artifacts) ? parsed.artifacts : [],
|
|
330
|
-
followUp: parsed.followUp ? String(parsed.followUp) : null,
|
|
331
|
-
toolsUsed: this._toolCallCount,
|
|
332
|
-
errors: this._errors,
|
|
333
|
-
};
|
|
334
|
-
}
|
|
326
|
+
const result = _fromParsed(JSON.parse(text));
|
|
327
|
+
if (result) return result;
|
|
335
328
|
} catch { /* fall through */ }
|
|
336
329
|
|
|
337
330
|
// Fallback: wrap raw text
|