kernelbot 1.0.26 → 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 -124
  2. package/bin/kernel.js +201 -4
  3. package/package.json +1 -1
  4. package/src/agent.js +397 -222
  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 +667 -21
  10. package/src/conversation.js +33 -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 +59 -6
  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 +3 -3
  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 +50 -15
  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,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,169 @@
1
+ import { EventEmitter } from 'events';
2
+ import { Job } from './job.js';
3
+ import { getLogger } from '../utils/logger.js';
4
+
5
+ /**
6
+ * Manages all jobs across all chats. Emits lifecycle events
7
+ * so the orchestrator can react when workers finish.
8
+ *
9
+ * Events:
10
+ * job:completed (job)
11
+ * job:failed (job)
12
+ * job:cancelled (job)
13
+ */
14
+ export class JobManager extends EventEmitter {
15
+ constructor({ jobTimeoutSeconds = 300, cleanupIntervalMinutes = 30 } = {}) {
16
+ super();
17
+ this.jobs = new Map(); // id -> Job
18
+ this.jobTimeoutMs = jobTimeoutSeconds * 1000;
19
+ this.cleanupMaxAge = cleanupIntervalMinutes * 60 * 1000;
20
+ }
21
+
22
+ /** Create a new job (status: queued). */
23
+ createJob(chatId, workerType, task) {
24
+ const job = new Job({ chatId, workerType, task });
25
+ this.jobs.set(job.id, job);
26
+ const logger = getLogger();
27
+ logger.info(`[JobManager] Job created: ${job.id} [${workerType}] for chat ${chatId} — "${task.slice(0, 100)}"`);
28
+ logger.debug(`[JobManager] Total jobs tracked: ${this.jobs.size}`);
29
+ return job;
30
+ }
31
+
32
+ /** Move a job to running. */
33
+ startJob(jobId) {
34
+ const job = this._get(jobId);
35
+ job.transition('running');
36
+ getLogger().info(`[JobManager] Job ${job.id} [${job.workerType}] → running`);
37
+ }
38
+
39
+ /** Move a job to completed with a result. */
40
+ completeJob(jobId, result) {
41
+ const job = this._get(jobId);
42
+ job.transition('completed');
43
+ job.result = result;
44
+ getLogger().info(`[JobManager] Job ${job.id} [${job.workerType}] → completed (${job.duration}s) — result: ${(result || '').length} chars`);
45
+ this.emit('job:completed', job);
46
+ }
47
+
48
+ /** Move a job to failed with an error message. */
49
+ failJob(jobId, error) {
50
+ const job = this._get(jobId);
51
+ job.transition('failed');
52
+ job.error = typeof error === 'string' ? error : error?.message || String(error);
53
+ getLogger().error(`[JobManager] Job ${job.id} [${job.workerType}] → failed (${job.duration}s) — ${job.error}`);
54
+ this.emit('job:failed', job);
55
+ }
56
+
57
+ /** Cancel a specific job. Returns the job or null if not found / already terminal. */
58
+ cancelJob(jobId) {
59
+ const logger = getLogger();
60
+ const job = this.jobs.get(jobId);
61
+ if (!job) {
62
+ logger.warn(`[JobManager] Cancel: job ${jobId} not found`);
63
+ return null;
64
+ }
65
+ if (job.isTerminal) {
66
+ logger.warn(`[JobManager] Cancel: job ${jobId} already in terminal state (${job.status})`);
67
+ return null;
68
+ }
69
+
70
+ const prevStatus = job.status;
71
+ job.transition('cancelled');
72
+ // Abort the worker if it's running
73
+ if (job.worker && typeof job.worker.cancel === 'function') {
74
+ logger.info(`[JobManager] Job ${job.id} [${job.workerType}] → cancelled (was: ${prevStatus}) — sending cancel to worker`);
75
+ job.worker.cancel();
76
+ } else {
77
+ logger.info(`[JobManager] Job ${job.id} [${job.workerType}] → cancelled (was: ${prevStatus}) — no active worker to abort`);
78
+ }
79
+ this.emit('job:cancelled', job);
80
+ return job;
81
+ }
82
+
83
+ /** Cancel all non-terminal jobs for a chat. Returns array of cancelled jobs. */
84
+ cancelAllForChat(chatId) {
85
+ const logger = getLogger();
86
+ const allForChat = [...this.jobs.values()].filter(j => j.chatId === chatId && !j.isTerminal);
87
+ logger.info(`[JobManager] Cancel all for chat ${chatId} — ${allForChat.length} active jobs found`);
88
+
89
+ const cancelled = [];
90
+ for (const job of allForChat) {
91
+ const prevStatus = job.status;
92
+ job.transition('cancelled');
93
+ if (job.worker && typeof job.worker.cancel === 'function') {
94
+ job.worker.cancel();
95
+ }
96
+ logger.info(`[JobManager] Job ${job.id} [${job.workerType}] → cancelled (was: ${prevStatus})`);
97
+ this.emit('job:cancelled', job);
98
+ cancelled.push(job);
99
+ }
100
+ return cancelled;
101
+ }
102
+
103
+ /** Get all jobs for a chat (most recent first). */
104
+ getJobsForChat(chatId) {
105
+ const jobs = [...this.jobs.values()]
106
+ .filter((j) => j.chatId === chatId)
107
+ .sort((a, b) => b.createdAt - a.createdAt);
108
+ getLogger().debug(`[JobManager] getJobsForChat(${chatId}): ${jobs.length} jobs`);
109
+ return jobs;
110
+ }
111
+
112
+ /** Get only running jobs for a chat. */
113
+ getRunningJobsForChat(chatId) {
114
+ const running = [...this.jobs.values()]
115
+ .filter((j) => j.chatId === chatId && j.status === 'running');
116
+ getLogger().debug(`[JobManager] getRunningJobsForChat(${chatId}): ${running.length} running`);
117
+ return running;
118
+ }
119
+
120
+ /** Get a job by id. */
121
+ getJob(jobId) {
122
+ return this.jobs.get(jobId) || null;
123
+ }
124
+
125
+ /** Garbage-collect old terminal jobs. */
126
+ cleanup() {
127
+ const now = Date.now();
128
+ let removed = 0;
129
+ for (const [id, job] of this.jobs) {
130
+ if (job.isTerminal && job.completedAt && now - job.completedAt > this.cleanupMaxAge) {
131
+ this.jobs.delete(id);
132
+ removed++;
133
+ }
134
+ }
135
+ if (removed) {
136
+ getLogger().info(`[JobManager] Cleanup: removed ${removed} old jobs — ${this.jobs.size} remaining`);
137
+ }
138
+ }
139
+
140
+ /** Enforce timeout on running jobs. Called periodically. */
141
+ enforceTimeouts() {
142
+ const now = Date.now();
143
+ let checkedCount = 0;
144
+ for (const job of this.jobs.values()) {
145
+ if (job.status === 'running' && job.startedAt) {
146
+ checkedCount++;
147
+ const elapsed = now - job.startedAt;
148
+ if (elapsed > this.jobTimeoutMs) {
149
+ getLogger().warn(`[JobManager] Job ${job.id} [${job.workerType}] timed out — elapsed: ${Math.round(elapsed / 1000)}s, limit: ${this.jobTimeoutMs / 1000}s`);
150
+ // Cancel the worker first so it stops executing & frees resources
151
+ if (job.worker && typeof job.worker.cancel === 'function') {
152
+ job.worker.cancel();
153
+ }
154
+ this.failJob(job.id, `Timed out after ${this.jobTimeoutMs / 1000}s`);
155
+ }
156
+ }
157
+ }
158
+ if (checkedCount > 0) {
159
+ getLogger().debug(`[JobManager] Timeout check: ${checkedCount} running jobs checked`);
160
+ }
161
+ }
162
+
163
+ /** Internal: get job or throw. */
164
+ _get(jobId) {
165
+ const job = this.jobs.get(jobId);
166
+ if (!job) throw new Error(`Job not found: ${jobId}`);
167
+ return job;
168
+ }
169
+ }
@@ -0,0 +1,67 @@
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.error = null;
24
+ this.worker = null; // WorkerAgent ref
25
+ this.statusMessageId = null; // Telegram message for progress edits
26
+ this.createdAt = Date.now();
27
+ this.startedAt = null;
28
+ this.completedAt = null;
29
+ }
30
+
31
+ /** Transition to a new status. Throws if the transition is invalid. */
32
+ transition(newStatus) {
33
+ const allowed = TRANSITIONS[this.status];
34
+ if (!allowed || !allowed.includes(newStatus)) {
35
+ throw new Error(`Invalid job transition: ${this.status} -> ${newStatus}`);
36
+ }
37
+ this.status = newStatus;
38
+ if (newStatus === 'running') this.startedAt = Date.now();
39
+ if (['completed', 'failed', 'cancelled'].includes(newStatus)) this.completedAt = Date.now();
40
+ }
41
+
42
+ /** Duration in seconds (or null if not started). */
43
+ get duration() {
44
+ if (!this.startedAt) return null;
45
+ const end = this.completedAt || Date.now();
46
+ return Math.round((end - this.startedAt) / 1000);
47
+ }
48
+
49
+ /** Whether this job is in a terminal state. */
50
+ get isTerminal() {
51
+ return ['completed', 'failed', 'cancelled'].includes(this.status);
52
+ }
53
+
54
+ /** Human-readable one-line summary. */
55
+ toSummary() {
56
+ const statusEmoji = {
57
+ queued: '🔜',
58
+ running: '⚙️',
59
+ completed: '✅',
60
+ failed: '❌',
61
+ cancelled: '🚫',
62
+ };
63
+ const emoji = statusEmoji[this.status] || '❓';
64
+ const dur = this.duration != null ? ` (${this.duration}s)` : '';
65
+ return `${emoji} \`${this.id}\` [${this.workerType}] ${this.task.slice(0, 60)}${this.task.length > 60 ? '...' : ''}${dur}`;
66
+ }
67
+ }
@@ -0,0 +1,74 @@
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
+ },
14
+ browser: {
15
+ label: 'Browser Worker',
16
+ emoji: '🌐',
17
+ categories: ['browser'],
18
+ description: 'Web search, scraping, screenshots',
19
+ },
20
+ system: {
21
+ label: 'System Worker',
22
+ emoji: '🖥️',
23
+ categories: ['core', 'process', 'monitor', 'network'],
24
+ description: 'OS operations, monitoring, network',
25
+ },
26
+ devops: {
27
+ label: 'DevOps Worker',
28
+ emoji: '🚀',
29
+ categories: ['core', 'docker', 'process', 'monitor', 'network', 'git'],
30
+ description: 'Docker, deploy, infrastructure',
31
+ },
32
+ research: {
33
+ label: 'Research Worker',
34
+ emoji: '🔍',
35
+ categories: ['browser', 'core'],
36
+ description: 'Deep web research and analysis',
37
+ },
38
+ };
39
+
40
+ /**
41
+ * Get the tool name set for a given worker type.
42
+ * @param {string} workerType
43
+ * @returns {Set<string>}
44
+ */
45
+ function getToolNamesForWorker(workerType) {
46
+ const config = WORKER_TYPES[workerType];
47
+ if (!config) throw new Error(`Unknown worker type: ${workerType}`);
48
+
49
+ const names = new Set();
50
+ for (const cat of config.categories) {
51
+ const tools = TOOL_CATEGORIES[cat];
52
+ if (tools) tools.forEach((t) => names.add(t));
53
+ }
54
+ return names;
55
+ }
56
+
57
+ /**
58
+ * Get Anthropic-format tool definitions scoped to a worker type.
59
+ * @param {string} workerType
60
+ * @returns {Array} filtered tool definitions
61
+ */
62
+ export function getToolsForWorker(workerType) {
63
+ const names = getToolNamesForWorker(workerType);
64
+ return toolDefinitions.filter((t) => names.has(t.name));
65
+ }
66
+
67
+ /**
68
+ * Get all tool names for a worker type (for credential checking).
69
+ * @param {string} workerType
70
+ * @returns {string[]}
71
+ */
72
+ export function getToolNamesForWorkerType(workerType) {
73
+ return [...getToolNamesForWorker(workerType)];
74
+ }