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,377 @@
1
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { homedir } from 'os';
4
+ import { Automation } from './automation.js';
5
+ import { scheduleNext, cancel } from './scheduler.js';
6
+ import { getLogger } from '../utils/logger.js';
7
+
8
+ const DATA_DIR = join(homedir(), '.kernelbot');
9
+ const DATA_FILE = join(DATA_DIR, 'automations.json');
10
+
11
+ const DEFAULT_MAX_PER_CHAT = 10;
12
+ const DEFAULT_MIN_INTERVAL = 5; // minutes
13
+
14
+ export class AutomationManager {
15
+ constructor() {
16
+ /** @type {Map<string, Automation>} id → Automation */
17
+ this.automations = new Map();
18
+ /** @type {Map<string, any>} id → timer ID */
19
+ this.timers = new Map();
20
+ /** @type {Map<string, Promise>} chatId → execution chain (serialize per chat) */
21
+ this._chatLocks = new Map();
22
+
23
+ // Injected via init()
24
+ this._sendMessage = null;
25
+ this._sendChatAction = null;
26
+ this._agentFactory = null;
27
+ this._config = null;
28
+
29
+ this._load();
30
+ }
31
+
32
+ /**
33
+ * Initialize with bot context. Called after bot is created.
34
+ * @param {object} opts
35
+ * @param {Function} opts.sendMessage - (chatId, text, opts?) => Promise
36
+ * @param {Function} opts.sendChatAction - (chatId, action) => Promise
37
+ * @param {Function} opts.agentFactory - (chatId) => { agent, onUpdate, sendPhoto }
38
+ * @param {object} opts.config
39
+ */
40
+ init({ sendMessage, sendChatAction, agentFactory, config }) {
41
+ this._sendMessage = sendMessage;
42
+ this._sendChatAction = sendChatAction;
43
+ this._agentFactory = agentFactory;
44
+ this._config = config;
45
+ }
46
+
47
+ /** Arm all enabled automations (called on startup). */
48
+ startAll() {
49
+ const logger = getLogger();
50
+ let armed = 0;
51
+ for (const auto of this.automations.values()) {
52
+ if (auto.enabled) {
53
+ this._arm(auto);
54
+ armed++;
55
+ }
56
+ }
57
+ if (armed > 0) {
58
+ logger.info(`[AutomationManager] Armed ${armed} automation(s) on startup`);
59
+ }
60
+ }
61
+
62
+ /** Cancel all timers (called on shutdown). */
63
+ shutdown() {
64
+ for (const timerId of this.timers.values()) {
65
+ cancel(timerId);
66
+ }
67
+ this.timers.clear();
68
+ }
69
+
70
+ // ── CRUD ──────────────────────────────────────────────────────────
71
+
72
+ /**
73
+ * Create a new automation.
74
+ * @param {string} chatId
75
+ * @param {object} data - { name, description, schedule }
76
+ * @returns {Automation}
77
+ */
78
+ create(chatId, data) {
79
+ const logger = getLogger();
80
+
81
+ // Enforce limits
82
+ const chatAutos = this.listForChat(chatId);
83
+ const maxPerChat = this._config?.automation?.max_per_chat || DEFAULT_MAX_PER_CHAT;
84
+ if (chatAutos.length >= maxPerChat) {
85
+ throw new Error(`Maximum automations per chat (${maxPerChat}) reached.`);
86
+ }
87
+
88
+ this._validateSchedule(data.schedule);
89
+
90
+ const auto = new Automation({
91
+ chatId,
92
+ name: data.name,
93
+ description: data.description,
94
+ schedule: data.schedule,
95
+ });
96
+
97
+ this.automations.set(auto.id, auto);
98
+ this._arm(auto);
99
+ this._save();
100
+
101
+ logger.info(`[AutomationManager] Created automation ${auto.id} "${auto.name}" for chat ${chatId}`);
102
+ return auto;
103
+ }
104
+
105
+ /** List all automations for a chat. */
106
+ listForChat(chatId) {
107
+ const id = String(chatId);
108
+ return [...this.automations.values()].filter((a) => a.chatId === id);
109
+ }
110
+
111
+ /** List all automations across all chats. */
112
+ listAll() {
113
+ return [...this.automations.values()];
114
+ }
115
+
116
+ /** Get a single automation by ID. */
117
+ get(id) {
118
+ return this.automations.get(id) || null;
119
+ }
120
+
121
+ /**
122
+ * Update an automation.
123
+ * @param {string} id
124
+ * @param {object} changes - partial fields to update
125
+ * @returns {Automation|null}
126
+ */
127
+ update(id, changes) {
128
+ const logger = getLogger();
129
+ const auto = this.automations.get(id);
130
+ if (!auto) return null;
131
+
132
+ if (changes.name !== undefined) auto.name = changes.name;
133
+ if (changes.description !== undefined) auto.description = changes.description;
134
+
135
+ if (changes.schedule !== undefined) {
136
+ this._validateSchedule(changes.schedule);
137
+ auto.schedule = changes.schedule;
138
+ }
139
+
140
+ if (changes.enabled !== undefined) {
141
+ auto.enabled = changes.enabled;
142
+ if (auto.enabled) {
143
+ this._arm(auto);
144
+ } else {
145
+ this._disarm(auto);
146
+ }
147
+ }
148
+
149
+ // Re-arm if schedule changed while enabled
150
+ if (changes.schedule !== undefined && auto.enabled) {
151
+ this._arm(auto);
152
+ }
153
+
154
+ this._save();
155
+ logger.info(`[AutomationManager] Updated automation ${id}: ${JSON.stringify(changes)}`);
156
+ return auto;
157
+ }
158
+
159
+ /**
160
+ * Delete an automation.
161
+ * @param {string} id
162
+ * @returns {boolean}
163
+ */
164
+ delete(id) {
165
+ const logger = getLogger();
166
+ const auto = this.automations.get(id);
167
+ if (!auto) return false;
168
+
169
+ this._disarm(auto);
170
+ this.automations.delete(id);
171
+ this._save();
172
+
173
+ logger.info(`[AutomationManager] Deleted automation ${id} "${auto.name}"`);
174
+ return true;
175
+ }
176
+
177
+ /**
178
+ * Trigger an automation immediately (manual run).
179
+ * @param {string} id
180
+ */
181
+ async runNow(id) {
182
+ const auto = this.automations.get(id);
183
+ if (!auto) throw new Error(`Automation ${id} not found.`);
184
+ await this._executeAutomation(auto);
185
+ }
186
+
187
+ // ── Timer management ──────────────────────────────────────────────
188
+
189
+ /** Arm (or re-arm) a timer for an automation. */
190
+ _arm(auto) {
191
+ const logger = getLogger();
192
+
193
+ // Cancel existing timer if any
194
+ this._disarm(auto);
195
+
196
+ const { timerId, nextRun } = scheduleNext(auto, () => {
197
+ this._onTimerFire(auto);
198
+ });
199
+
200
+ auto.nextRun = nextRun;
201
+ this.timers.set(auto.id, timerId);
202
+
203
+ logger.debug(`[AutomationManager] Armed ${auto.id} "${auto.name}" — next: ${new Date(nextRun).toLocaleString()}`);
204
+ }
205
+
206
+ /** Cancel an automation's timer. */
207
+ _disarm(auto) {
208
+ const timerId = this.timers.get(auto.id);
209
+ if (timerId != null) {
210
+ cancel(timerId);
211
+ this.timers.delete(auto.id);
212
+ auto.nextRun = null;
213
+ }
214
+ }
215
+
216
+ /** Called when a timer fires. */
217
+ _onTimerFire(auto) {
218
+ const logger = getLogger();
219
+
220
+ // Guard: automation may have been deleted or disabled while timer was pending
221
+ const current = this.automations.get(auto.id);
222
+ if (!current || !current.enabled) {
223
+ logger.debug(`[AutomationManager] Timer fired for ${auto.id} but automation is disabled/deleted — skipping`);
224
+ return;
225
+ }
226
+
227
+ // Serialize execution per chat to prevent conversation history corruption
228
+ this._enqueueExecution(current);
229
+ }
230
+
231
+ /** Enqueue automation execution into per-chat chain. */
232
+ _enqueueExecution(auto) {
233
+ const chatId = auto.chatId;
234
+ const prev = this._chatLocks.get(chatId) || Promise.resolve();
235
+ const next = prev.then(() => this._executeAutomation(auto)).catch(() => {});
236
+ this._chatLocks.set(chatId, next);
237
+ }
238
+
239
+ /** Execute an automation run. */
240
+ async _executeAutomation(auto) {
241
+ const logger = getLogger();
242
+
243
+ // Convert chatId back to number to match Telegram's format
244
+ // (Automation stores as string for JSON safety, but conversationManager uses number keys)
245
+ const chatId = Number(auto.chatId);
246
+
247
+ logger.info(`[AutomationManager] Executing automation ${auto.id} "${auto.name}" for chat ${chatId}`);
248
+
249
+ // Update run stats
250
+ auto.lastRun = Date.now();
251
+ auto.runCount++;
252
+ this._save();
253
+
254
+ try {
255
+ // Notify user
256
+ if (this._sendChatAction) {
257
+ await this._sendChatAction(chatId, 'typing').catch(() => {});
258
+ }
259
+ if (this._sendMessage) {
260
+ await this._sendMessage(chatId, `⏰ Running: **${auto.name}**...`).catch(() => {});
261
+ }
262
+
263
+ // Get agent context for this chat
264
+ if (!this._agentFactory) {
265
+ throw new Error('AutomationManager not initialized — no agentFactory');
266
+ }
267
+
268
+ const { agent, onUpdate, sendPhoto } = this._agentFactory(chatId);
269
+ const prompt = `[AUTOMATION: ${auto.name}] ${auto.description}`;
270
+
271
+ // Run through the orchestrator like a normal user message
272
+ const reply = await agent.processMessage(
273
+ chatId,
274
+ prompt,
275
+ { id: 'automation', username: 'automation' },
276
+ onUpdate,
277
+ sendPhoto,
278
+ );
279
+
280
+ // Send the response
281
+ if (reply && this._sendMessage) {
282
+ await this._sendMessage(chatId, reply).catch(() => {});
283
+ }
284
+
285
+ auto.lastError = null;
286
+ logger.info(`[AutomationManager] Automation ${auto.id} completed — reply: "${(reply || '').slice(0, 150)}"`);
287
+ } catch (err) {
288
+ auto.lastError = err.message;
289
+ logger.error(`[AutomationManager] Automation ${auto.id} failed: ${err.message}`);
290
+
291
+ if (this._sendMessage) {
292
+ await this._sendMessage(
293
+ chatId,
294
+ `⚠️ Automation **${auto.name}** failed: ${err.message}`,
295
+ ).catch(() => {});
296
+ }
297
+ }
298
+
299
+ // Re-arm for next execution (regardless of success/failure)
300
+ const current = this.automations.get(auto.id);
301
+ if (current && current.enabled) {
302
+ this._arm(current);
303
+ this._save();
304
+ }
305
+ }
306
+
307
+ // ── Validation ────────────────────────────────────────────────────
308
+
309
+ _validateSchedule(schedule) {
310
+ if (!schedule || !schedule.type) {
311
+ throw new Error('Schedule must have a type (cron, interval, or random).');
312
+ }
313
+
314
+ const minInterval = this._config?.automation?.min_interval_minutes || DEFAULT_MIN_INTERVAL;
315
+
316
+ switch (schedule.type) {
317
+ case 'cron':
318
+ if (!schedule.expression) {
319
+ throw new Error('Cron schedule requires an expression field.');
320
+ }
321
+ break;
322
+
323
+ case 'interval':
324
+ if (!schedule.minutes || schedule.minutes < minInterval) {
325
+ throw new Error(`Interval must be at least ${minInterval} minutes.`);
326
+ }
327
+ break;
328
+
329
+ case 'random':
330
+ if (!schedule.minMinutes || !schedule.maxMinutes) {
331
+ throw new Error('Random schedule requires minMinutes and maxMinutes.');
332
+ }
333
+ if (schedule.minMinutes < minInterval) {
334
+ throw new Error(`Minimum random interval must be at least ${minInterval} minutes.`);
335
+ }
336
+ if (schedule.maxMinutes <= schedule.minMinutes) {
337
+ throw new Error('maxMinutes must be greater than minMinutes.');
338
+ }
339
+ break;
340
+
341
+ default:
342
+ throw new Error(`Unknown schedule type: ${schedule.type}. Use cron, interval, or random.`);
343
+ }
344
+ }
345
+
346
+ // ── Persistence ───────────────────────────────────────────────────
347
+
348
+ _load() {
349
+ const logger = getLogger();
350
+ try {
351
+ if (existsSync(DATA_FILE)) {
352
+ const raw = readFileSync(DATA_FILE, 'utf-8');
353
+ const data = JSON.parse(raw);
354
+ for (const item of data) {
355
+ const auto = Automation.fromJSON(item);
356
+ this.automations.set(auto.id, auto);
357
+ }
358
+ logger.info(`[AutomationManager] Loaded ${this.automations.size} automation(s) from disk`);
359
+ }
360
+ } catch (err) {
361
+ logger.error(`[AutomationManager] Failed to load automations: ${err.message}`);
362
+ }
363
+ }
364
+
365
+ _save() {
366
+ try {
367
+ if (!existsSync(DATA_DIR)) {
368
+ mkdirSync(DATA_DIR, { recursive: true });
369
+ }
370
+ const data = [...this.automations.values()].map((a) => a.toJSON());
371
+ writeFileSync(DATA_FILE, JSON.stringify(data, null, 2), 'utf-8');
372
+ } catch (err) {
373
+ const logger = getLogger();
374
+ logger.error(`[AutomationManager] Failed to save automations: ${err.message}`);
375
+ }
376
+ }
377
+ }
@@ -0,0 +1,79 @@
1
+ import { randomBytes } from 'crypto';
2
+
3
+ /**
4
+ * A single recurring automation — a scheduled task that the orchestrator runs.
5
+ */
6
+ export class Automation {
7
+ constructor({ chatId, name, description, schedule }) {
8
+ this.id = randomBytes(4).toString('hex');
9
+ this.chatId = String(chatId);
10
+ this.name = name;
11
+ this.description = description; // the task prompt
12
+ this.schedule = schedule; // { type, expression?, minutes?, minMinutes?, maxMinutes? }
13
+ this.enabled = true;
14
+ this.lastRun = null;
15
+ this.nextRun = null;
16
+ this.runCount = 0;
17
+ this.lastError = null;
18
+ this.createdAt = Date.now();
19
+ }
20
+
21
+ /** Human-readable one-line summary. */
22
+ toSummary() {
23
+ const status = this.enabled ? '🟢' : '⏸️';
24
+ const scheduleStr = formatSchedule(this.schedule);
25
+ const nextStr = this.nextRun
26
+ ? `next: ${new Date(this.nextRun).toLocaleString()}`
27
+ : 'not scheduled';
28
+ const runs = this.runCount > 0 ? ` | ${this.runCount} runs` : '';
29
+ return `${status} \`${this.id}\` **${this.name}** — ${scheduleStr} (${nextStr}${runs})`;
30
+ }
31
+
32
+ /** Serialize for persistence. */
33
+ toJSON() {
34
+ return {
35
+ id: this.id,
36
+ chatId: this.chatId,
37
+ name: this.name,
38
+ description: this.description,
39
+ schedule: this.schedule,
40
+ enabled: this.enabled,
41
+ lastRun: this.lastRun,
42
+ nextRun: this.nextRun,
43
+ runCount: this.runCount,
44
+ lastError: this.lastError,
45
+ createdAt: this.createdAt,
46
+ };
47
+ }
48
+
49
+ /** Deserialize from persistence. */
50
+ static fromJSON(data) {
51
+ const auto = Object.create(Automation.prototype);
52
+ auto.id = data.id;
53
+ auto.chatId = String(data.chatId);
54
+ auto.name = data.name;
55
+ auto.description = data.description;
56
+ auto.schedule = data.schedule;
57
+ auto.enabled = data.enabled;
58
+ auto.lastRun = data.lastRun;
59
+ auto.nextRun = data.nextRun;
60
+ auto.runCount = data.runCount;
61
+ auto.lastError = data.lastError;
62
+ auto.createdAt = data.createdAt;
63
+ return auto;
64
+ }
65
+ }
66
+
67
+ /** Format a schedule object for display. */
68
+ function formatSchedule(schedule) {
69
+ switch (schedule.type) {
70
+ case 'cron':
71
+ return `cron: \`${schedule.expression}\``;
72
+ case 'interval':
73
+ return `every ${schedule.minutes}m`;
74
+ case 'random':
75
+ return `random ${schedule.minMinutes}–${schedule.maxMinutes}m`;
76
+ default:
77
+ return schedule.type;
78
+ }
79
+ }
@@ -0,0 +1,2 @@
1
+ export { Automation } from './automation.js';
2
+ export { AutomationManager } from './automation-manager.js';
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Pure timer engine for automations — cron, interval, and random schedules.
3
+ * Uses setTimeout (no new dependencies).
4
+ */
5
+
6
+ /**
7
+ * Calculate the next fire time for an automation.
8
+ * @param {object} schedule - { type, expression?, minutes?, minMinutes?, maxMinutes? }
9
+ * @param {number|null} lastRun - timestamp of last execution
10
+ * @returns {number} next fire timestamp
11
+ */
12
+ export function getNextFireTime(schedule, lastRun) {
13
+ const now = Date.now();
14
+
15
+ switch (schedule.type) {
16
+ case 'cron':
17
+ return nextCronTime(schedule.expression, now);
18
+
19
+ case 'interval': {
20
+ const intervalMs = schedule.minutes * 60 * 1000;
21
+ if (lastRun) {
22
+ const next = lastRun + intervalMs;
23
+ return next > now ? next : now + 1000; // if overdue, fire soon
24
+ }
25
+ return now + intervalMs;
26
+ }
27
+
28
+ case 'random': {
29
+ const minMs = schedule.minMinutes * 60 * 1000;
30
+ const maxMs = schedule.maxMinutes * 60 * 1000;
31
+ const delayMs = minMs + Math.random() * (maxMs - minMs);
32
+ return now + delayMs;
33
+ }
34
+
35
+ default:
36
+ throw new Error(`Unknown schedule type: ${schedule.type}`);
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Schedule a timer for an automation. Returns the timer ID.
42
+ * @param {object} automation - Automation instance
43
+ * @param {Function} callback - called when timer fires
44
+ * @returns {{ timerId: any, nextRun: number }}
45
+ */
46
+ export function scheduleNext(automation, callback) {
47
+ const nextRun = getNextFireTime(automation.schedule, automation.lastRun);
48
+ const delay = Math.max(nextRun - Date.now(), 1000); // at least 1s
49
+
50
+ const timerId = setTimeout(callback, delay);
51
+ return { timerId, nextRun };
52
+ }
53
+
54
+ /**
55
+ * Cancel a scheduled timer.
56
+ * @param {any} timerId
57
+ */
58
+ export function cancel(timerId) {
59
+ if (timerId != null) clearTimeout(timerId);
60
+ }
61
+
62
+ // ── Cron parser ────────────────────────────────────────────────────────
63
+
64
+ /**
65
+ * Parse a 5-field cron expression and compute the next occurrence after `after`.
66
+ * Fields: minute hour dayOfMonth month dayOfWeek
67
+ * Supports: numbers, ranges (1-5), steps (star/5), lists (1,3,5), star
68
+ */
69
+ function nextCronTime(expression, after) {
70
+ const fields = expression.trim().split(/\s+/);
71
+ if (fields.length !== 5) {
72
+ throw new Error(`Invalid cron expression (need 5 fields): ${expression}`);
73
+ }
74
+
75
+ const [minuteF, hourF, domF, monthF, dowF] = fields;
76
+ const minutes = parseField(minuteF, 0, 59);
77
+ const hours = parseField(hourF, 0, 23);
78
+ const doms = parseField(domF, 1, 31);
79
+ const months = parseField(monthF, 1, 12);
80
+ const dows = parseField(dowF, 0, 6); // 0=Sunday
81
+
82
+ const start = new Date(after + 60_000); // start from next minute
83
+ start.setSeconds(0, 0);
84
+
85
+ // Search up to 366 days ahead
86
+ const limit = 366 * 24 * 60;
87
+ for (let i = 0; i < limit; i++) {
88
+ const candidate = new Date(start.getTime() + i * 60_000);
89
+ const m = candidate.getMinutes();
90
+ const h = candidate.getHours();
91
+ const dom = candidate.getDate();
92
+ const mon = candidate.getMonth() + 1; // 1-based
93
+ const dow = candidate.getDay();
94
+
95
+ if (
96
+ minutes.has(m) &&
97
+ hours.has(h) &&
98
+ doms.has(dom) &&
99
+ months.has(mon) &&
100
+ dows.has(dow)
101
+ ) {
102
+ return candidate.getTime();
103
+ }
104
+ }
105
+
106
+ // Fallback: 24h from now
107
+ return after + 24 * 60 * 60 * 1000;
108
+ }
109
+
110
+ /**
111
+ * Parse a single cron field into a Set of valid values.
112
+ */
113
+ function parseField(field, min, max) {
114
+ const values = new Set();
115
+
116
+ for (const part of field.split(',')) {
117
+ if (part === '*') {
118
+ for (let i = min; i <= max; i++) values.add(i);
119
+ } else if (part.includes('/')) {
120
+ const [range, stepStr] = part.split('/');
121
+ const step = parseInt(stepStr, 10);
122
+ let start = min;
123
+ let end = max;
124
+ if (range !== '*') {
125
+ if (range.includes('-')) {
126
+ [start, end] = range.split('-').map(Number);
127
+ } else {
128
+ start = parseInt(range, 10);
129
+ }
130
+ }
131
+ for (let i = start; i <= end; i += step) values.add(i);
132
+ } else if (part.includes('-')) {
133
+ const [s, e] = part.split('-').map(Number);
134
+ for (let i = s; i <= e; i++) values.add(i);
135
+ } else {
136
+ values.add(parseInt(part, 10));
137
+ }
138
+ }
139
+
140
+ return values;
141
+ }