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.
- package/.env.example +11 -0
- package/README.md +48 -318
- package/bin/kernel.js +89 -16
- package/config.example.yaml +2 -1
- package/goals.md +20 -0
- package/knowledge_base/index.md +11 -0
- package/package.json +1 -1
- package/src/agent.js +19 -1
- package/src/automation/automation-manager.js +16 -0
- package/src/automation/automation.js +6 -2
- package/src/bot.js +129 -23
- package/src/life/engine.js +87 -68
- package/src/life/evolution.js +4 -8
- package/src/life/improvements.js +2 -6
- package/src/life/journal.js +3 -6
- package/src/life/memory.js +3 -10
- package/src/life/share-queue.js +4 -9
- package/src/prompts/orchestrator.js +21 -12
- package/src/providers/base.js +36 -4
- package/src/security/auth.js +38 -1
- package/src/services/stt.js +10 -1
- package/src/tools/docker.js +34 -5
- package/src/tools/git.js +6 -0
- package/src/tools/github.js +6 -0
- package/src/tools/jira.js +5 -0
- package/src/tools/monitor.js +10 -3
- package/src/tools/network.js +12 -1
- package/src/tools/process.js +17 -3
- package/src/utils/config.js +50 -14
- package/src/utils/date.js +19 -0
- package/src/utils/display.js +1 -1
- package/src/utils/ids.js +12 -0
- package/src/utils/temporal-awareness.js +199 -0
- package/src/utils/timeUtils.js +110 -0
package/src/utils/config.js
CHANGED
|
@@ -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
|
-
//
|
|
477
|
-
if (config.orchestrator.
|
|
478
|
-
config.orchestrator.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
|
+
}
|
package/src/utils/display.js
CHANGED
|
@@ -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
|
|
49
|
+
'Set OWNER_TELEGRAM_ID in .env or allowed_users in config.yaml.',
|
|
50
50
|
),
|
|
51
51
|
{
|
|
52
52
|
padding: 1,
|
package/src/utils/ids.js
ADDED
|
@@ -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
|
+
}
|