kernelbot 1.0.26 → 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.
Files changed (61) hide show
  1. package/.env.example +4 -0
  2. package/README.md +198 -124
  3. package/bin/kernel.js +208 -4
  4. package/config.example.yaml +14 -1
  5. package/package.json +1 -1
  6. package/src/agent.js +839 -209
  7. package/src/automation/automation-manager.js +377 -0
  8. package/src/automation/automation.js +79 -0
  9. package/src/automation/index.js +2 -0
  10. package/src/automation/scheduler.js +141 -0
  11. package/src/bot.js +1001 -18
  12. package/src/claude-auth.js +93 -0
  13. package/src/coder.js +48 -6
  14. package/src/conversation.js +33 -0
  15. package/src/intents/detector.js +50 -0
  16. package/src/intents/index.js +2 -0
  17. package/src/intents/planner.js +58 -0
  18. package/src/persona.js +68 -0
  19. package/src/prompts/orchestrator.js +124 -0
  20. package/src/prompts/persona.md +21 -0
  21. package/src/prompts/system.js +59 -6
  22. package/src/prompts/workers.js +148 -0
  23. package/src/providers/anthropic.js +23 -16
  24. package/src/providers/base.js +76 -2
  25. package/src/providers/index.js +1 -0
  26. package/src/providers/models.js +2 -1
  27. package/src/providers/openai-compat.js +5 -3
  28. package/src/security/audit.js +0 -0
  29. package/src/security/auth.js +0 -0
  30. package/src/security/confirm.js +7 -2
  31. package/src/self.js +122 -0
  32. package/src/services/stt.js +139 -0
  33. package/src/services/tts.js +124 -0
  34. package/src/skills/catalog.js +506 -0
  35. package/src/skills/custom.js +128 -0
  36. package/src/swarm/job-manager.js +216 -0
  37. package/src/swarm/job.js +85 -0
  38. package/src/swarm/worker-registry.js +79 -0
  39. package/src/tools/browser.js +458 -335
  40. package/src/tools/categories.js +3 -3
  41. package/src/tools/coding.js +5 -0
  42. package/src/tools/docker.js +0 -0
  43. package/src/tools/git.js +0 -0
  44. package/src/tools/github.js +0 -0
  45. package/src/tools/index.js +3 -0
  46. package/src/tools/jira.js +0 -0
  47. package/src/tools/monitor.js +0 -0
  48. package/src/tools/network.js +0 -0
  49. package/src/tools/orchestrator-tools.js +428 -0
  50. package/src/tools/os.js +14 -1
  51. package/src/tools/persona.js +32 -0
  52. package/src/tools/process.js +0 -0
  53. package/src/utils/config.js +153 -15
  54. package/src/utils/display.js +0 -0
  55. package/src/utils/logger.js +0 -0
  56. package/src/worker.js +396 -0
  57. package/.agents/skills/interface-design/SKILL.md +0 -391
  58. package/.agents/skills/interface-design/references/critique.md +0 -67
  59. package/.agents/skills/interface-design/references/example.md +0 -86
  60. package/.agents/skills/interface-design/references/principles.md +0 -235
  61. package/.agents/skills/interface-design/references/validation.md +0 -48
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Custom user-defined skills — CRUD operations with JSON storage,
3
+ * plus unified lookups that merge custom skills with the built-in catalog.
4
+ */
5
+
6
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
7
+ import { join } from 'path';
8
+ import { homedir } from 'os';
9
+ import { getSkillById, getSkillsByCategory, getCategoryList } from './catalog.js';
10
+
11
+ const STORAGE_DIR = join(homedir(), '.kernelbot');
12
+ const STORAGE_FILE = join(STORAGE_DIR, 'custom_skills.json');
13
+
14
+ let cache = null;
15
+
16
+ /** Slugify a name for use as an ID suffix. */
17
+ function slugify(name) {
18
+ return name
19
+ .toLowerCase()
20
+ .replace(/[^a-z0-9]+/g, '-')
21
+ .replace(/^-|-$/g, '');
22
+ }
23
+
24
+ /** Generate a unique ID with `custom_` prefix. Appends -2, -3, etc. on collision. */
25
+ function generateId(name, existingIds) {
26
+ const base = `custom_${slugify(name)}`;
27
+ if (!existingIds.has(base)) return base;
28
+ let n = 2;
29
+ while (existingIds.has(`${base}-${n}`)) n++;
30
+ return `${base}-${n}`;
31
+ }
32
+
33
+ /** Load custom skills from disk into the in-memory cache. */
34
+ export function loadCustomSkills() {
35
+ if (cache !== null) return cache;
36
+ if (!existsSync(STORAGE_FILE)) {
37
+ cache = [];
38
+ return cache;
39
+ }
40
+ try {
41
+ const raw = readFileSync(STORAGE_FILE, 'utf-8');
42
+ cache = JSON.parse(raw);
43
+ if (!Array.isArray(cache)) cache = [];
44
+ } catch {
45
+ cache = [];
46
+ }
47
+ return cache;
48
+ }
49
+
50
+ /** Write the current cache to disk. */
51
+ export function saveCustomSkills(skills) {
52
+ cache = skills;
53
+ if (!existsSync(STORAGE_DIR)) {
54
+ mkdirSync(STORAGE_DIR, { recursive: true });
55
+ }
56
+ writeFileSync(STORAGE_FILE, JSON.stringify(skills, null, 2), 'utf-8');
57
+ }
58
+
59
+ /** Return the cached array of custom skills. */
60
+ export function getCustomSkills() {
61
+ if (cache === null) loadCustomSkills();
62
+ return cache;
63
+ }
64
+
65
+ /**
66
+ * Create a new custom skill, save, and return it.
67
+ * @param {{ name: string, systemPrompt: string, description?: string }} opts
68
+ */
69
+ export function addCustomSkill({ name, systemPrompt, description }) {
70
+ const skills = getCustomSkills();
71
+ const existingIds = new Set(skills.map((s) => s.id));
72
+ const id = generateId(name, existingIds);
73
+
74
+ const skill = {
75
+ id,
76
+ name,
77
+ emoji: '\u{1F6E0}\uFE0F', // wrench emoji
78
+ description: description || `Custom skill: ${name}`,
79
+ systemPrompt,
80
+ createdAt: new Date().toISOString(),
81
+ };
82
+
83
+ skills.push(skill);
84
+ saveCustomSkills(skills);
85
+ return skill;
86
+ }
87
+
88
+ /** Delete a custom skill by ID. Returns true if found and removed. */
89
+ export function deleteCustomSkill(id) {
90
+ const skills = getCustomSkills();
91
+ const idx = skills.findIndex((s) => s.id === id);
92
+ if (idx === -1) return false;
93
+ skills.splice(idx, 1);
94
+ saveCustomSkills(skills);
95
+ return true;
96
+ }
97
+
98
+ /** Find a custom skill by ID. */
99
+ export function getCustomSkillById(id) {
100
+ const skills = getCustomSkills();
101
+ return skills.find((s) => s.id === id);
102
+ }
103
+
104
+ /** Unified lookup: check custom first, then fall through to built-in catalog. */
105
+ export function getUnifiedSkillById(id) {
106
+ return getCustomSkillById(id) || getSkillById(id);
107
+ }
108
+
109
+ /** Unified category list: built-in categories + custom category (if any exist). */
110
+ export function getUnifiedCategoryList() {
111
+ const categories = getCategoryList();
112
+ const customs = getCustomSkills();
113
+ if (customs.length > 0) {
114
+ categories.push({
115
+ key: 'custom',
116
+ name: 'Custom',
117
+ emoji: '\u{1F6E0}\uFE0F',
118
+ count: customs.length,
119
+ });
120
+ }
121
+ return categories;
122
+ }
123
+
124
+ /** Unified skills-by-category: for 'custom' return custom skills; otherwise delegate. */
125
+ export function getUnifiedSkillsByCategory(key) {
126
+ if (key === 'custom') return getCustomSkills();
127
+ return getSkillsByCategory(key);
128
+ }
@@ -0,0 +1,216 @@
1
+ import { EventEmitter } from 'events';
2
+ import { Job } from './job.js';
3
+ import { WORKER_TYPES } from './worker-registry.js';
4
+ import { getLogger } from '../utils/logger.js';
5
+
6
+ /**
7
+ * Manages all jobs across all chats. Emits lifecycle events
8
+ * so the orchestrator can react when workers finish.
9
+ *
10
+ * Events:
11
+ * job:completed (job)
12
+ * job:failed (job)
13
+ * job:cancelled (job)
14
+ * job:ready (job) — emitted when a queued job's dependencies are all met
15
+ */
16
+ export class JobManager extends EventEmitter {
17
+ constructor({ jobTimeoutSeconds = 300, cleanupIntervalMinutes = 30 } = {}) {
18
+ super();
19
+ this.jobs = new Map(); // id -> Job
20
+ this.jobTimeoutMs = jobTimeoutSeconds * 1000;
21
+ this.cleanupMaxAge = cleanupIntervalMinutes * 60 * 1000;
22
+ }
23
+
24
+ /** Create a new job (status: queued). */
25
+ createJob(chatId, workerType, task) {
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;
30
+ this.jobs.set(job.id, job);
31
+ const logger = getLogger();
32
+ logger.info(`[JobManager] Job created: ${job.id} [${workerType}] for chat ${chatId} — timeout: ${job.timeoutMs / 1000}s — "${task.slice(0, 100)}"`);
33
+ logger.debug(`[JobManager] Total jobs tracked: ${this.jobs.size}`);
34
+ return job;
35
+ }
36
+
37
+ /** Move a job to running. */
38
+ startJob(jobId) {
39
+ const job = this._get(jobId);
40
+ job.transition('running');
41
+ getLogger().info(`[JobManager] Job ${job.id} [${job.workerType}] → running`);
42
+ }
43
+
44
+ /** Move a job to completed with a result and optional structured data. */
45
+ completeJob(jobId, result, structuredResult = null) {
46
+ const job = this._get(jobId);
47
+ job.transition('completed');
48
+ job.result = result;
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}`);
53
+ this.emit('job:completed', job);
54
+ this._checkDependents(job);
55
+ }
56
+
57
+ /** Move a job to failed with an error message. */
58
+ failJob(jobId, error) {
59
+ const job = this._get(jobId);
60
+ job.transition('failed');
61
+ job.error = typeof error === 'string' ? error : error?.message || String(error);
62
+ getLogger().error(`[JobManager] Job ${job.id} [${job.workerType}] → failed (${job.duration}s) — ${job.error}`);
63
+ this.emit('job:failed', job);
64
+ this._failDependents(job);
65
+ }
66
+
67
+ /** Cancel a specific job. Returns the job or null if not found / already terminal. */
68
+ cancelJob(jobId) {
69
+ const logger = getLogger();
70
+ const job = this.jobs.get(jobId);
71
+ if (!job) {
72
+ logger.warn(`[JobManager] Cancel: job ${jobId} not found`);
73
+ return null;
74
+ }
75
+ if (job.isTerminal) {
76
+ logger.warn(`[JobManager] Cancel: job ${jobId} already in terminal state (${job.status})`);
77
+ return null;
78
+ }
79
+
80
+ const prevStatus = job.status;
81
+ job.transition('cancelled');
82
+ // Abort the worker if it's running
83
+ if (job.worker && typeof job.worker.cancel === 'function') {
84
+ logger.info(`[JobManager] Job ${job.id} [${job.workerType}] → cancelled (was: ${prevStatus}) — sending cancel to worker`);
85
+ job.worker.cancel();
86
+ } else {
87
+ logger.info(`[JobManager] Job ${job.id} [${job.workerType}] → cancelled (was: ${prevStatus}) — no active worker to abort`);
88
+ }
89
+ this.emit('job:cancelled', job);
90
+ return job;
91
+ }
92
+
93
+ /** Cancel all non-terminal jobs for a chat. Returns array of cancelled jobs. */
94
+ cancelAllForChat(chatId) {
95
+ const logger = getLogger();
96
+ const allForChat = [...this.jobs.values()].filter(j => j.chatId === chatId && !j.isTerminal);
97
+ logger.info(`[JobManager] Cancel all for chat ${chatId} — ${allForChat.length} active jobs found`);
98
+
99
+ const cancelled = [];
100
+ for (const job of allForChat) {
101
+ const prevStatus = job.status;
102
+ job.transition('cancelled');
103
+ if (job.worker && typeof job.worker.cancel === 'function') {
104
+ job.worker.cancel();
105
+ }
106
+ logger.info(`[JobManager] Job ${job.id} [${job.workerType}] → cancelled (was: ${prevStatus})`);
107
+ this.emit('job:cancelled', job);
108
+ cancelled.push(job);
109
+ }
110
+ return cancelled;
111
+ }
112
+
113
+ /** Get all jobs for a chat (most recent first). */
114
+ getJobsForChat(chatId) {
115
+ const jobs = [...this.jobs.values()]
116
+ .filter((j) => j.chatId === chatId)
117
+ .sort((a, b) => b.createdAt - a.createdAt);
118
+ getLogger().debug(`[JobManager] getJobsForChat(${chatId}): ${jobs.length} jobs`);
119
+ return jobs;
120
+ }
121
+
122
+ /** Get only running jobs for a chat. */
123
+ getRunningJobsForChat(chatId) {
124
+ const running = [...this.jobs.values()]
125
+ .filter((j) => j.chatId === chatId && j.status === 'running');
126
+ getLogger().debug(`[JobManager] getRunningJobsForChat(${chatId}): ${running.length} running`);
127
+ return running;
128
+ }
129
+
130
+ /** Get a job by id. */
131
+ getJob(jobId) {
132
+ return this.jobs.get(jobId) || null;
133
+ }
134
+
135
+ /** Garbage-collect old terminal jobs. */
136
+ cleanup() {
137
+ const now = Date.now();
138
+ let removed = 0;
139
+ for (const [id, job] of this.jobs) {
140
+ if (job.isTerminal && job.completedAt && now - job.completedAt > this.cleanupMaxAge) {
141
+ this.jobs.delete(id);
142
+ removed++;
143
+ }
144
+ }
145
+ if (removed) {
146
+ getLogger().info(`[JobManager] Cleanup: removed ${removed} old jobs — ${this.jobs.size} remaining`);
147
+ }
148
+ }
149
+
150
+ /** Enforce timeout on running jobs. Called periodically. */
151
+ enforceTimeouts() {
152
+ const now = Date.now();
153
+ let checkedCount = 0;
154
+ for (const job of this.jobs.values()) {
155
+ if (job.status === 'running' && job.startedAt) {
156
+ checkedCount++;
157
+ const elapsed = now - job.startedAt;
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`);
161
+ // Cancel the worker first so it stops executing & frees resources
162
+ if (job.worker && typeof job.worker.cancel === 'function') {
163
+ job.worker.cancel();
164
+ }
165
+ this.failJob(job.id, `Timed out after ${timeoutMs / 1000}s`);
166
+ }
167
+ }
168
+ }
169
+ if (checkedCount > 0) {
170
+ getLogger().debug(`[JobManager] Timeout check: ${checkedCount} running jobs checked`);
171
+ }
172
+ }
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
+
210
+ /** Internal: get job or throw. */
211
+ _get(jobId) {
212
+ const job = this.jobs.get(jobId);
213
+ if (!job) throw new Error(`Job not found: ${jobId}`);
214
+ return job;
215
+ }
216
+ }
@@ -0,0 +1,85 @@
1
+ import { randomBytes } from 'crypto';
2
+
3
+ /** Valid job statuses and allowed transitions. */
4
+ const TRANSITIONS = {
5
+ queued: ['running', 'cancelled'],
6
+ running: ['completed', 'failed', 'cancelled'],
7
+ completed: [],
8
+ failed: [],
9
+ cancelled: [],
10
+ };
11
+
12
+ /**
13
+ * A single unit of work dispatched from the orchestrator to a worker.
14
+ */
15
+ export class Job {
16
+ constructor({ chatId, workerType, task }) {
17
+ this.id = randomBytes(4).toString('hex'); // short 8-char hex id
18
+ this.chatId = chatId;
19
+ this.workerType = workerType;
20
+ this.task = task;
21
+ this.status = 'queued';
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
27
+ this.error = null;
28
+ this.worker = null; // WorkerAgent ref
29
+ this.statusMessageId = null; // Telegram message for progress edits
30
+ this.createdAt = Date.now();
31
+ this.startedAt = null;
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
36
+ }
37
+
38
+ /** Transition to a new status. Throws if the transition is invalid. */
39
+ transition(newStatus) {
40
+ const allowed = TRANSITIONS[this.status];
41
+ if (!allowed || !allowed.includes(newStatus)) {
42
+ throw new Error(`Invalid job transition: ${this.status} -> ${newStatus}`);
43
+ }
44
+ this.status = newStatus;
45
+ if (newStatus === 'running') this.startedAt = Date.now();
46
+ if (['completed', 'failed', 'cancelled'].includes(newStatus)) this.completedAt = Date.now();
47
+ }
48
+
49
+ /** Duration in seconds (or null if not started). */
50
+ get duration() {
51
+ if (!this.startedAt) return null;
52
+ const end = this.completedAt || Date.now();
53
+ return Math.round((end - this.startedAt) / 1000);
54
+ }
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
+
63
+ /** Whether this job is in a terminal state. */
64
+ get isTerminal() {
65
+ return ['completed', 'failed', 'cancelled'].includes(this.status);
66
+ }
67
+
68
+ /** Human-readable one-line summary. */
69
+ toSummary() {
70
+ const statusEmoji = {
71
+ queued: '🔜',
72
+ running: '⚙️',
73
+ completed: '✅',
74
+ failed: '❌',
75
+ cancelled: '🚫',
76
+ };
77
+ const emoji = statusEmoji[this.status] || '❓';
78
+ const dur = this.duration != null ? ` (${this.duration}s)` : '';
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}`;
84
+ }
85
+ }
@@ -0,0 +1,79 @@
1
+ import { TOOL_CATEGORIES } from '../tools/categories.js';
2
+ import { toolDefinitions } from '../tools/index.js';
3
+
4
+ /**
5
+ * Worker type definitions — maps each worker type to the tool categories it needs.
6
+ */
7
+ export const WORKER_TYPES = {
8
+ coding: {
9
+ label: 'Coding Worker',
10
+ emoji: '💻',
11
+ categories: ['core', 'coding', 'git', 'github'],
12
+ description: 'Write code, fix bugs, create PRs',
13
+ timeout: 86400, // 24 hours — Claude Code can legitimately run for hours
14
+ },
15
+ browser: {
16
+ label: 'Browser Worker',
17
+ emoji: '🌐',
18
+ categories: ['browser'],
19
+ description: 'Web search, scraping, screenshots',
20
+ timeout: 300, // 5 minutes
21
+ },
22
+ system: {
23
+ label: 'System Worker',
24
+ emoji: '🖥️',
25
+ categories: ['core', 'process', 'monitor', 'network'],
26
+ description: 'OS operations, monitoring, network',
27
+ timeout: 600, // 10 minutes
28
+ },
29
+ devops: {
30
+ label: 'DevOps Worker',
31
+ emoji: '🚀',
32
+ categories: ['core', 'docker', 'process', 'monitor', 'network', 'git'],
33
+ description: 'Docker, deploy, infrastructure',
34
+ timeout: 3600, // 1 hour
35
+ },
36
+ research: {
37
+ label: 'Research Worker',
38
+ emoji: '🔍',
39
+ categories: ['browser', 'core'],
40
+ description: 'Deep web research and analysis',
41
+ timeout: 600, // 10 minutes
42
+ },
43
+ };
44
+
45
+ /**
46
+ * Get the tool name set for a given worker type.
47
+ * @param {string} workerType
48
+ * @returns {Set<string>}
49
+ */
50
+ function getToolNamesForWorker(workerType) {
51
+ const config = WORKER_TYPES[workerType];
52
+ if (!config) throw new Error(`Unknown worker type: ${workerType}`);
53
+
54
+ const names = new Set();
55
+ for (const cat of config.categories) {
56
+ const tools = TOOL_CATEGORIES[cat];
57
+ if (tools) tools.forEach((t) => names.add(t));
58
+ }
59
+ return names;
60
+ }
61
+
62
+ /**
63
+ * Get Anthropic-format tool definitions scoped to a worker type.
64
+ * @param {string} workerType
65
+ * @returns {Array} filtered tool definitions
66
+ */
67
+ export function getToolsForWorker(workerType) {
68
+ const names = getToolNamesForWorker(workerType);
69
+ return toolDefinitions.filter((t) => names.has(t.name));
70
+ }
71
+
72
+ /**
73
+ * Get all tool names for a worker type (for credential checking).
74
+ * @param {string} workerType
75
+ * @returns {string[]}
76
+ */
77
+ export function getToolNamesForWorkerType(workerType) {
78
+ return [...getToolNamesForWorker(workerType)];
79
+ }