kernelbot 1.0.34 → 1.0.35
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 +83 -3
- 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 +18 -0
- 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 +7 -0
- 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/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
|
+
}
|