kernelbot 1.0.34 → 1.0.36

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.
@@ -401,7 +401,8 @@ async function promptForMissing(config) {
401
401
  }
402
402
 
403
403
  if (!mutableConfig.brain.api_key) {
404
- // Run provider selection flow
404
+ // Run brain provider selection flow
405
+ console.log(chalk.bold('\n 🧠 Worker Brain'));
405
406
  const { providerKey, modelId } = await promptProviderSelection(rl);
406
407
  mutableConfig.brain.provider = providerKey;
407
408
  mutableConfig.brain.model = modelId;
@@ -413,6 +414,38 @@ async function promptForMissing(config) {
413
414
  const key = await ask(rl, chalk.cyan(`\n ${providerDef.name} API key: `));
414
415
  mutableConfig.brain.api_key = key.trim();
415
416
  envLines.push(`${envKey}=${key.trim()}`);
417
+
418
+ // Orchestrator provider selection
419
+ console.log(chalk.bold('\n 🎛️ Orchestrator'));
420
+ const sameChoice = await ask(rl, chalk.cyan(` Use same provider (${providerDef.name} / ${modelId}) for orchestrator? [Y/n]: `));
421
+ if (!sameChoice.trim() || sameChoice.trim().toLowerCase() === 'y') {
422
+ mutableConfig.orchestrator.provider = providerKey;
423
+ mutableConfig.orchestrator.model = modelId;
424
+ mutableConfig.orchestrator.api_key = key.trim();
425
+ saveOrchestratorToYaml(providerKey, modelId);
426
+ } else {
427
+ const orch = await promptProviderSelection(rl);
428
+ mutableConfig.orchestrator.provider = orch.providerKey;
429
+ mutableConfig.orchestrator.model = orch.modelId;
430
+ saveOrchestratorToYaml(orch.providerKey, orch.modelId);
431
+
432
+ const orchProviderDef = PROVIDERS[orch.providerKey];
433
+ if (orch.providerKey === providerKey) {
434
+ // Same provider — reuse the API key
435
+ mutableConfig.orchestrator.api_key = key.trim();
436
+ } else {
437
+ // Different provider — need a separate key
438
+ const orchEnvKey = orchProviderDef.envKey;
439
+ const orchExisting = process.env[orchEnvKey];
440
+ if (orchExisting) {
441
+ mutableConfig.orchestrator.api_key = orchExisting;
442
+ } else {
443
+ const orchKey = await ask(rl, chalk.cyan(`\n ${orchProviderDef.name} API key: `));
444
+ mutableConfig.orchestrator.api_key = orchKey.trim();
445
+ envLines.push(`${orchEnvKey}=${orchKey.trim()}`);
446
+ }
447
+ }
448
+ }
416
449
  }
417
450
 
418
451
  if (!mutableConfig.telegram.bot_token) {
@@ -468,29 +501,32 @@ export function loadConfig() {
468
501
 
469
502
  const config = deepMerge(DEFAULTS, fileConfig);
470
503
 
504
+ // Brain — resolve API key from env based on configured provider
505
+ const providerDef = PROVIDERS[config.brain.provider];
506
+ if (providerDef && process.env[providerDef.envKey]) {
507
+ config.brain.api_key = process.env[providerDef.envKey];
508
+ }
509
+
471
510
  // Orchestrator — resolve API key based on configured provider
472
511
  const orchProvider = PROVIDERS[config.orchestrator.provider];
473
512
  if (orchProvider && process.env[orchProvider.envKey]) {
474
513
  config.orchestrator.api_key = process.env[orchProvider.envKey];
475
514
  }
476
- // Legacy fallback: ANTHROPIC_API_KEY for anthropic orchestrator
477
- if (config.orchestrator.provider === 'anthropic' && !config.orchestrator.api_key && process.env.ANTHROPIC_API_KEY) {
478
- config.orchestrator.api_key = process.env.ANTHROPIC_API_KEY;
479
- }
480
-
481
- // Overlay env vars for brain API key based on provider
482
- const providerDef = PROVIDERS[config.brain.provider];
483
- if (providerDef && process.env[providerDef.envKey]) {
484
- config.brain.api_key = process.env[providerDef.envKey];
485
- }
486
- // Legacy fallback: ANTHROPIC_API_KEY for anthropic provider
487
- if (config.brain.provider === 'anthropic' && !config.brain.api_key && process.env.ANTHROPIC_API_KEY) {
488
- config.brain.api_key = process.env.ANTHROPIC_API_KEY;
515
+ // If orchestrator uses the same provider as brain, share the key
516
+ if (!config.orchestrator.api_key && config.orchestrator.provider === config.brain.provider && config.brain.api_key) {
517
+ config.orchestrator.api_key = config.brain.api_key;
489
518
  }
490
519
 
491
520
  if (process.env.TELEGRAM_BOT_TOKEN) {
492
521
  config.telegram.bot_token = process.env.TELEGRAM_BOT_TOKEN;
493
522
  }
523
+ // Merge OWNER_TELEGRAM_ID into allowed_users if set
524
+ if (process.env.OWNER_TELEGRAM_ID) {
525
+ const ownerId = Number(process.env.OWNER_TELEGRAM_ID);
526
+ if (!config.telegram.allowed_users.includes(ownerId)) {
527
+ config.telegram.allowed_users.push(ownerId);
528
+ }
529
+ }
494
530
  if (process.env.GITHUB_TOKEN) {
495
531
  if (!config.github) config.github = {};
496
532
  config.github.token = process.env.GITHUB_TOKEN;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Get the millisecond timestamp for the start of today (midnight 00:00:00.000).
3
+ *
4
+ * @returns {number} Epoch ms at the start of today
5
+ */
6
+ export function getStartOfDayMs() {
7
+ const d = new Date();
8
+ d.setHours(0, 0, 0, 0);
9
+ return d.getTime();
10
+ }
11
+
12
+ /**
13
+ * Get today's date as an ISO date string (YYYY-MM-DD).
14
+ *
15
+ * @returns {string} e.g. "2026-02-21"
16
+ */
17
+ export function todayDateStr() {
18
+ return new Date().toISOString().slice(0, 10);
19
+ }
@@ -46,7 +46,7 @@ export function showLogo() {
46
46
  'It can execute commands, read/write files, manage processes,\n' +
47
47
  'and interact with external services on your behalf.\n\n' +
48
48
  'Only run this on machines you control.\n' +
49
- 'Set allowed_users in config.yaml to restrict access.',
49
+ 'Set OWNER_TELEGRAM_ID in .env or allowed_users in config.yaml.',
50
50
  ),
51
51
  {
52
52
  padding: 1,
@@ -0,0 +1,12 @@
1
+ import { randomBytes } from 'crypto';
2
+
3
+ /**
4
+ * Generate a short, unique ID with a given prefix.
5
+ * Format: `<prefix>_<8-hex-chars>` (e.g. "evo_a3f1b2c4").
6
+ *
7
+ * @param {string} prefix - Short prefix for the ID (e.g. 'evo', 'sh', 'imp', 'ep')
8
+ * @returns {string} Unique identifier
9
+ */
10
+ export function genId(prefix) {
11
+ return `${prefix}_${randomBytes(4).toString('hex')}`;
12
+ }
@@ -0,0 +1,199 @@
1
+ /**
2
+ * Temporal & Spatial Awareness Engine
3
+ *
4
+ * Reads a local (git-ignored) config file to dynamically inject
5
+ * the owner's real-time context into every LLM call:
6
+ * - Current local time in the owner's timezone
7
+ * - Whether they are currently within working hours
8
+ * - Location context
9
+ * - Day-of-week and date context
10
+ *
11
+ * The local config file (local_context.json) is NEVER committed.
12
+ * Only this generic algorithm ships with the repo.
13
+ */
14
+
15
+ import { readFileSync, existsSync, statSync } from 'fs';
16
+ import { join, dirname } from 'path';
17
+ import { fileURLToPath } from 'url';
18
+ import { getLogger } from './logger.js';
19
+
20
+ const __dirname = dirname(fileURLToPath(import.meta.url));
21
+ const LOCAL_CONTEXT_PATH = join(__dirname, '..', '..', 'local_context.json');
22
+
23
+ /** Cache the loaded config (reloaded on file change via mtime check). */
24
+ let _cache = null;
25
+ let _cacheMtime = 0;
26
+
27
+ /**
28
+ * Load the local context config.
29
+ * Returns null if the file doesn't exist (non-personal deployments).
30
+ */
31
+ function loadLocalContext() {
32
+ try {
33
+ if (!existsSync(LOCAL_CONTEXT_PATH)) return null;
34
+
35
+ const stat = statSync(LOCAL_CONTEXT_PATH);
36
+ if (_cache && stat.mtimeMs === _cacheMtime) return _cache;
37
+
38
+ const raw = readFileSync(LOCAL_CONTEXT_PATH, 'utf-8');
39
+ _cache = JSON.parse(raw);
40
+ _cacheMtime = stat.mtimeMs;
41
+ return _cache;
42
+ } catch (err) {
43
+ const logger = getLogger();
44
+ logger.debug(`[TemporalAwareness] Could not load local_context.json: ${err.message}`);
45
+ return null;
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Format a Date in a human-friendly way for a given timezone.
51
+ */
52
+ function formatTime(date, timezone, locale = 'en-US') {
53
+ return date.toLocaleString(locale, {
54
+ weekday: 'long',
55
+ year: 'numeric',
56
+ month: 'long',
57
+ day: 'numeric',
58
+ hour: '2-digit',
59
+ minute: '2-digit',
60
+ second: '2-digit',
61
+ hour12: true,
62
+ timeZone: timezone,
63
+ });
64
+ }
65
+
66
+ /**
67
+ * Get the current hour (0-23) in the given timezone.
68
+ */
69
+ function getCurrentHour(date, timezone) {
70
+ const parts = new Intl.DateTimeFormat('en-US', {
71
+ hour: 'numeric',
72
+ hour12: false,
73
+ timeZone: timezone,
74
+ }).formatToParts(date);
75
+ const hourPart = parts.find(p => p.type === 'hour');
76
+ return parseInt(hourPart?.value || '0', 10);
77
+ }
78
+
79
+ /**
80
+ * Get the current day of week (0=Sun, 1=Mon, ..., 6=Sat) in the given timezone.
81
+ */
82
+ function getCurrentDayOfWeek(date, timezone) {
83
+ const parts = new Intl.DateTimeFormat('en-US', {
84
+ weekday: 'short',
85
+ timeZone: timezone,
86
+ }).formatToParts(date);
87
+ const dayStr = parts.find(p => p.type === 'weekday')?.value || '';
88
+ const dayMap = { Sun: 0, Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6 };
89
+ return dayMap[dayStr] ?? -1;
90
+ }
91
+
92
+ /**
93
+ * Determine the user's current status based on time, day, and working hours.
94
+ */
95
+ function determineStatus(hour, dayOfWeek, workingHours) {
96
+ if (!workingHours) return { status: 'unknown', detail: '' };
97
+
98
+ const { start, end, days } = workingHours;
99
+ const isWorkDay = days ? days.includes(dayOfWeek) : (dayOfWeek >= 0 && dayOfWeek <= 4);
100
+ const isWorkHour = hour >= start && hour < end;
101
+
102
+ if (!isWorkDay) {
103
+ return { status: 'day_off', detail: 'Weekend / day off' };
104
+ }
105
+
106
+ if (isWorkHour) {
107
+ return { status: 'working', detail: `At work (${workingHours.label || `${start}:00–${end}:00`})` };
108
+ }
109
+
110
+ // Outside working hours on a work day
111
+ if (hour < start) {
112
+ const hoursUntilWork = start - hour;
113
+ return { status: 'before_work', detail: `Before work — starts in ~${hoursUntilWork}h` };
114
+ }
115
+
116
+ return { status: 'after_work', detail: 'Off work for the day' };
117
+ }
118
+
119
+ /**
120
+ * Determine the likely activity period for more nuanced awareness.
121
+ */
122
+ function determineActivityPeriod(hour) {
123
+ if (hour >= 0 && hour < 5) return 'late_night';
124
+ if (hour >= 5 && hour < 7) return 'early_morning';
125
+ if (hour >= 7 && hour < 12) return 'morning';
126
+ if (hour >= 12 && hour < 14) return 'midday';
127
+ if (hour >= 14 && hour < 17) return 'afternoon';
128
+ if (hour >= 17 && hour < 20) return 'evening';
129
+ if (hour >= 20 && hour < 23) return 'night';
130
+ return 'late_night';
131
+ }
132
+
133
+ /**
134
+ * Build the temporal & spatial awareness string to inject into the system prompt.
135
+ *
136
+ * Returns a formatted context block, or null if no local config is found.
137
+ *
138
+ * Example output:
139
+ * ## Owner's Real-Time Context
140
+ * - Local Time: Monday, March 10, 2025, 02:15:30 AM (Asia/Riyadh)
141
+ * - Location: Riyadh, Saudi Arabia
142
+ * - Status: Off work — next shift at 10:00 AM
143
+ * - Period: Late night
144
+ *
145
+ * IMPORTANT: Adjust your tone and assumptions to the owner's current time.
146
+ * Do NOT assume they are at work during off-hours or sleeping during work hours.
147
+ */
148
+ export function buildTemporalAwareness() {
149
+ const ctx = loadLocalContext();
150
+ if (!ctx?.owner) return null;
151
+
152
+ const logger = getLogger();
153
+ const { owner } = ctx;
154
+ const now = new Date();
155
+
156
+ const timezone = owner.timezone || 'UTC';
157
+ const locale = owner.locale || 'en-US';
158
+ const location = owner.location
159
+ ? `${owner.location.city}, ${owner.location.country}`
160
+ : null;
161
+
162
+ const formattedTime = formatTime(now, timezone, locale);
163
+ const currentHour = getCurrentHour(now, timezone);
164
+ const currentDay = getCurrentDayOfWeek(now, timezone);
165
+ const { status, detail } = determineStatus(currentHour, currentDay, owner.working_hours);
166
+ const period = determineActivityPeriod(currentHour);
167
+
168
+ const lines = [
169
+ `## Owner's Real-Time Context`,
170
+ `- **Local Time**: ${formattedTime}`,
171
+ ];
172
+
173
+ if (location) {
174
+ lines.push(`- **Location**: ${location}`);
175
+ }
176
+
177
+ if (owner.name) {
178
+ lines.push(`- **Name**: ${owner.name}`);
179
+ }
180
+
181
+ lines.push(`- **Work Status**: ${detail || status}`);
182
+ lines.push(`- **Period**: ${period.replace('_', ' ')}`);
183
+
184
+ if (owner.working_hours) {
185
+ const wh = owner.working_hours;
186
+ lines.push(`- **Working Hours**: ${wh.start}:00–${wh.end}:00${wh.label ? ` (${wh.label})` : ''}`);
187
+ }
188
+
189
+ lines.push('');
190
+ lines.push('IMPORTANT: Be aware of the owner\'s current local time and status.');
191
+ lines.push('Do NOT assume they are at work during off-hours, or sleeping during work hours.');
192
+ lines.push('Adjust greetings, tone, and context to match their real-time situation.');
193
+
194
+ const block = lines.join('\n');
195
+
196
+ logger.debug(`[TemporalAwareness] ${timezone} | hour=${currentHour} day=${currentDay} | status=${status} period=${period}`);
197
+
198
+ return block;
199
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Quiet Hours utility — configurable "Do Not Disturb" schedule.
3
+ *
4
+ * Resolution order (first defined wins):
5
+ * 1. Environment variables QUIET_HOURS_START / QUIET_HOURS_END (HH:mm)
6
+ * 2. YAML config values config.life.quiet_hours.start / .end (integer hour)
7
+ * 3. Built-in defaults 02:00 – 06:00
8
+ *
9
+ * If neither variable is set, quiet hours are disabled (returns false).
10
+ */
11
+
12
+ /** Default quiet-hours window (integer hours). */
13
+ const DEFAULT_START = 2;
14
+ const DEFAULT_END = 6;
15
+
16
+ /**
17
+ * Resolve the active quiet-hours window into a normalised { startMinutes, endMinutes } pair.
18
+ * Both values are in "minutes since midnight" (0 – 1439).
19
+ *
20
+ * @param {object} [lifeConfig] - Optional `config.life` object from YAML.
21
+ * @returns {{ startMinutes: number, endMinutes: number }}
22
+ */
23
+ function resolveWindow(lifeConfig) {
24
+ const envStart = process.env.QUIET_HOURS_START;
25
+ const envEnd = process.env.QUIET_HOURS_END;
26
+
27
+ if (envStart && envEnd) {
28
+ const [startH, startM] = envStart.split(':').map(Number);
29
+ const [endH, endM] = envEnd.split(':').map(Number);
30
+
31
+ if (!isNaN(startH) && !isNaN(startM) && !isNaN(endH) && !isNaN(endM)) {
32
+ return { startMinutes: startH * 60 + startM, endMinutes: endH * 60 + endM };
33
+ }
34
+ }
35
+
36
+ const startHour = lifeConfig?.quiet_hours?.start ?? DEFAULT_START;
37
+ const endHour = lifeConfig?.quiet_hours?.end ?? DEFAULT_END;
38
+ return { startMinutes: startHour * 60, endMinutes: endHour * 60 };
39
+ }
40
+
41
+ /**
42
+ * Check whether a given "minutes since midnight" value falls inside a quiet window.
43
+ *
44
+ * @param {number} current - Current minutes since midnight.
45
+ * @param {number} start - Window start (minutes since midnight).
46
+ * @param {number} end - Window end (minutes since midnight).
47
+ * @returns {boolean}
48
+ */
49
+ function insideWindow(current, start, end) {
50
+ if (start <= end) {
51
+ return current >= start && current < end;
52
+ }
53
+ // Window crosses midnight (e.g. 22:00 – 06:00)
54
+ return current >= start || current < end;
55
+ }
56
+
57
+ /**
58
+ * Check whether the current time falls within "quiet hours".
59
+ *
60
+ * @param {object} [lifeConfig] - Optional `config.life` object from YAML.
61
+ * When provided, `lifeConfig.quiet_hours.start` / `.end` (integer hours)
62
+ * act as the second-priority source before the hardcoded defaults.
63
+ * @returns {boolean} `true` when the current time is inside the quiet window.
64
+ */
65
+ export function isQuietHours(lifeConfig) {
66
+ const { startMinutes, endMinutes } = resolveWindow(lifeConfig);
67
+ const now = new Date();
68
+ const currentMinutes = now.getHours() * 60 + now.getMinutes();
69
+ return insideWindow(currentMinutes, startMinutes, endMinutes);
70
+ }
71
+
72
+ /**
73
+ * Return the resolved quiet-hours configuration for logging / status display.
74
+ *
75
+ * @param {object} [lifeConfig] - Optional `config.life` object from YAML.
76
+ * @returns {{ start: string, end: string, active: boolean }}
77
+ * `start` / `end` are formatted as "HH:MM", `active` reflects the current state.
78
+ */
79
+ export function getQuietHoursConfig(lifeConfig) {
80
+ const { startMinutes, endMinutes } = resolveWindow(lifeConfig);
81
+ const pad = (n) => String(n).padStart(2, '0');
82
+ const fmt = (m) => `${pad(Math.floor(m / 60))}:${pad(m % 60)}`;
83
+ return {
84
+ start: fmt(startMinutes),
85
+ end: fmt(endMinutes),
86
+ active: isQuietHours(lifeConfig),
87
+ };
88
+ }
89
+
90
+ /**
91
+ * Calculate the number of milliseconds remaining until the current quiet-hours
92
+ * window ends. Returns `0` when quiet hours are not active.
93
+ *
94
+ * Useful for deferring non-essential work until the window closes.
95
+ *
96
+ * @param {object} [lifeConfig] - Optional `config.life` object from YAML.
97
+ * @returns {number} Milliseconds until quiet hours end (0 if not currently quiet).
98
+ */
99
+ export function msUntilQuietEnd(lifeConfig) {
100
+ if (!isQuietHours(lifeConfig)) return 0;
101
+
102
+ const { endMinutes } = resolveWindow(lifeConfig);
103
+ const now = new Date();
104
+ const currentMinutes = now.getHours() * 60 + now.getMinutes();
105
+
106
+ let diff = endMinutes - currentMinutes;
107
+ if (diff <= 0) diff += 24 * 60; // crosses midnight
108
+
109
+ return diff * 60_000;
110
+ }