kernelbot 1.0.33 → 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 +76 -341
- package/bin/kernel.js +134 -15
- package/config.example.yaml +2 -1
- package/goals.md +20 -0
- package/knowledge_base/index.md +11 -0
- package/package.json +2 -1
- package/src/agent.js +166 -19
- package/src/automation/automation-manager.js +16 -0
- package/src/automation/automation.js +6 -2
- package/src/bot.js +295 -163
- package/src/conversation.js +70 -3
- 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/prompts/persona.md +27 -0
- package/src/providers/base.js +51 -8
- package/src/providers/google-genai.js +198 -0
- package/src/providers/index.js +6 -1
- package/src/providers/models.js +6 -2
- package/src/providers/openai-compat.js +25 -11
- package/src/security/auth.js +38 -1
- package/src/services/stt.js +10 -1
- package/src/tools/docker.js +37 -15
- 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 +13 -15
- package/src/tools/network.js +22 -18
- package/src/tools/os.js +37 -2
- package/src/tools/process.js +21 -14
- package/src/utils/config.js +66 -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/shell.js +31 -0
- package/src/utils/temporal-awareness.js +199 -0
- package/src/utils/timeUtils.js +110 -0
- package/src/utils/truncate.js +42 -0
- package/src/worker.js +2 -18
package/src/tools/monitor.js
CHANGED
|
@@ -1,17 +1,8 @@
|
|
|
1
|
-
import { exec } from 'child_process';
|
|
2
1
|
import { platform } from 'os';
|
|
2
|
+
import { shellRun as run, shellEscape } from '../utils/shell.js';
|
|
3
3
|
|
|
4
4
|
const isMac = platform() === 'darwin';
|
|
5
5
|
|
|
6
|
-
function run(cmd, timeout = 10000) {
|
|
7
|
-
return new Promise((resolve) => {
|
|
8
|
-
exec(cmd, { timeout }, (error, stdout, stderr) => {
|
|
9
|
-
if (error) return resolve({ error: stderr || error.message });
|
|
10
|
-
resolve({ output: stdout.trim() });
|
|
11
|
-
});
|
|
12
|
-
});
|
|
13
|
-
}
|
|
14
|
-
|
|
15
6
|
export const definitions = [
|
|
16
7
|
{
|
|
17
8
|
name: 'disk_usage',
|
|
@@ -68,17 +59,24 @@ export const handlers = {
|
|
|
68
59
|
},
|
|
69
60
|
|
|
70
61
|
system_logs: async (params) => {
|
|
71
|
-
|
|
62
|
+
let finalLines = 50;
|
|
63
|
+
if (params.lines != null) {
|
|
64
|
+
const lines = parseInt(params.lines, 10);
|
|
65
|
+
if (!Number.isFinite(lines) || lines <= 0 || lines > 10000) {
|
|
66
|
+
return { error: 'Invalid lines value: must be between 1 and 10000' };
|
|
67
|
+
}
|
|
68
|
+
finalLines = lines;
|
|
69
|
+
}
|
|
72
70
|
const source = params.source || 'journalctl';
|
|
73
71
|
const filter = params.filter;
|
|
74
72
|
|
|
75
73
|
if (source === 'journalctl') {
|
|
76
|
-
const filterArg = filter ? ` -g
|
|
77
|
-
return await run(`journalctl -n ${
|
|
74
|
+
const filterArg = filter ? ` -g ${shellEscape(filter)}` : '';
|
|
75
|
+
return await run(`journalctl -n ${finalLines}${filterArg} --no-pager`);
|
|
78
76
|
}
|
|
79
77
|
|
|
80
78
|
// Reading a log file
|
|
81
|
-
const filterCmd = filter ? ` | grep -i
|
|
82
|
-
return await run(`tail -n ${
|
|
79
|
+
const filterCmd = filter ? ` | grep -i ${shellEscape(filter)}` : '';
|
|
80
|
+
return await run(`tail -n ${finalLines} ${shellEscape(source)}${filterCmd}`);
|
|
83
81
|
},
|
|
84
82
|
};
|
package/src/tools/network.js
CHANGED
|
@@ -1,14 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
return new Promise((resolve) => {
|
|
6
|
-
exec(cmd, { timeout }, (error, stdout, stderr) => {
|
|
7
|
-
if (error) return resolve({ error: stderr || error.message });
|
|
8
|
-
resolve({ output: stdout.trim() });
|
|
9
|
-
});
|
|
10
|
-
});
|
|
11
|
-
}
|
|
1
|
+
import { shellRun, shellEscape } from '../utils/shell.js';
|
|
2
|
+
import { getLogger } from '../utils/logger.js';
|
|
3
|
+
|
|
4
|
+
const run = (cmd, timeout = 15000) => shellRun(cmd, timeout);
|
|
12
5
|
|
|
13
6
|
export const definitions = [
|
|
14
7
|
{
|
|
@@ -46,13 +39,17 @@ export const definitions = [
|
|
|
46
39
|
|
|
47
40
|
export const handlers = {
|
|
48
41
|
check_port: async (params) => {
|
|
42
|
+
const logger = getLogger();
|
|
49
43
|
const host = params.host || 'localhost';
|
|
50
|
-
const
|
|
44
|
+
const port = parseInt(params.port, 10);
|
|
45
|
+
if (!Number.isFinite(port) || port <= 0 || port > 65535) return { error: 'Invalid port number' };
|
|
51
46
|
|
|
47
|
+
logger.debug(`check_port: checking ${host}:${port}`);
|
|
52
48
|
// Use nc (netcat) for port check — works on both macOS and Linux
|
|
53
|
-
const result = await run(`nc -z -w 3 ${host} ${port} 2>&1 && echo "OPEN" || echo "CLOSED"`, 5000);
|
|
49
|
+
const result = await run(`nc -z -w 3 ${shellEscape(host)} ${port} 2>&1 && echo "OPEN" || echo "CLOSED"`, 5000);
|
|
54
50
|
|
|
55
51
|
if (result.error) {
|
|
52
|
+
logger.error(`check_port failed for ${host}:${port}: ${result.error}`);
|
|
56
53
|
return { port, host, status: 'closed', detail: result.error };
|
|
57
54
|
}
|
|
58
55
|
|
|
@@ -63,19 +60,19 @@ export const handlers = {
|
|
|
63
60
|
curl_url: async (params) => {
|
|
64
61
|
const { url, method = 'GET', headers, body } = params;
|
|
65
62
|
|
|
66
|
-
let cmd = `curl -s -w "\\n---HTTP_STATUS:%{http_code}" -X ${method}`;
|
|
63
|
+
let cmd = `curl -s -w "\\n---HTTP_STATUS:%{http_code}" -X ${shellEscape(method)}`;
|
|
67
64
|
|
|
68
65
|
if (headers) {
|
|
69
66
|
for (const [key, val] of Object.entries(headers)) {
|
|
70
|
-
cmd += ` -H
|
|
67
|
+
cmd += ` -H ${shellEscape(`${key}: ${val}`)}`;
|
|
71
68
|
}
|
|
72
69
|
}
|
|
73
70
|
|
|
74
71
|
if (body) {
|
|
75
|
-
cmd += ` -d
|
|
72
|
+
cmd += ` -d ${shellEscape(body)}`;
|
|
76
73
|
}
|
|
77
74
|
|
|
78
|
-
cmd += `
|
|
75
|
+
cmd += ` ${shellEscape(url)}`;
|
|
79
76
|
|
|
80
77
|
const result = await run(cmd);
|
|
81
78
|
|
|
@@ -89,15 +86,22 @@ export const handlers = {
|
|
|
89
86
|
},
|
|
90
87
|
|
|
91
88
|
nginx_reload: async () => {
|
|
89
|
+
const logger = getLogger();
|
|
92
90
|
// Test config first
|
|
91
|
+
logger.debug('nginx_reload: testing configuration');
|
|
93
92
|
const test = await run('nginx -t 2>&1');
|
|
94
93
|
if (test.error || (test.output && test.output.includes('failed'))) {
|
|
94
|
+
logger.error(`nginx_reload: config test failed: ${test.error || test.output}`);
|
|
95
95
|
return { error: `Config test failed: ${test.error || test.output}` };
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
const reload = await run('nginx -s reload 2>&1');
|
|
99
|
-
if (reload.error)
|
|
99
|
+
if (reload.error) {
|
|
100
|
+
logger.error(`nginx_reload failed: ${reload.error}`);
|
|
101
|
+
return reload;
|
|
102
|
+
}
|
|
100
103
|
|
|
104
|
+
logger.debug('nginx_reload: successfully reloaded');
|
|
101
105
|
return { success: true, test_output: test.output };
|
|
102
106
|
},
|
|
103
107
|
};
|
package/src/tools/os.js
CHANGED
|
@@ -2,12 +2,24 @@ import { exec } from 'child_process';
|
|
|
2
2
|
import { readFileSync, writeFileSync, mkdirSync, readdirSync, statSync } from 'fs';
|
|
3
3
|
import { dirname, resolve, join } from 'path';
|
|
4
4
|
import { homedir } from 'os';
|
|
5
|
+
import { getLogger } from '../utils/logger.js';
|
|
5
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Expand a file path, resolving `~` to the user's home directory.
|
|
9
|
+
* @param {string} p - The path to expand.
|
|
10
|
+
* @returns {string} The resolved absolute path.
|
|
11
|
+
*/
|
|
6
12
|
function expandPath(p) {
|
|
7
13
|
if (p.startsWith('~')) return join(homedir(), p.slice(1));
|
|
8
14
|
return resolve(p);
|
|
9
15
|
}
|
|
10
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Check whether a file path falls within any blocked path defined in config.
|
|
19
|
+
* @param {string} filePath - The path to check.
|
|
20
|
+
* @param {object} config - The bot configuration object.
|
|
21
|
+
* @returns {boolean} True if the path is blocked.
|
|
22
|
+
*/
|
|
11
23
|
function isBlocked(filePath, config) {
|
|
12
24
|
const expanded = expandPath(filePath);
|
|
13
25
|
const blockedPaths = config.security?.blocked_paths || [];
|
|
@@ -94,14 +106,25 @@ export const definitions = [
|
|
|
94
106
|
},
|
|
95
107
|
];
|
|
96
108
|
|
|
97
|
-
|
|
109
|
+
/** Maximum recursion depth for directory listing to prevent stack overflow. */
|
|
110
|
+
const MAX_RECURSIVE_DEPTH = 10;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Recursively list all files and directories under a given path.
|
|
114
|
+
* @param {string} dirPath - Absolute path to the directory to list.
|
|
115
|
+
* @param {string} [base=''] - Relative path prefix for nested entries.
|
|
116
|
+
* @param {number} [depth=0] - Current recursion depth (internal).
|
|
117
|
+
* @returns {Array<{name: string, type: string}>} Flat array of entries.
|
|
118
|
+
*/
|
|
119
|
+
function listRecursive(dirPath, base = '', depth = 0) {
|
|
120
|
+
if (depth >= MAX_RECURSIVE_DEPTH) return [];
|
|
98
121
|
const entries = [];
|
|
99
122
|
for (const entry of readdirSync(dirPath, { withFileTypes: true })) {
|
|
100
123
|
const rel = base ? `${base}/${entry.name}` : entry.name;
|
|
101
124
|
const type = entry.isDirectory() ? 'directory' : 'file';
|
|
102
125
|
entries.push({ name: rel, type });
|
|
103
126
|
if (entry.isDirectory()) {
|
|
104
|
-
entries.push(...listRecursive(join(dirPath, entry.name), rel));
|
|
127
|
+
entries.push(...listRecursive(join(dirPath, entry.name), rel, depth + 1));
|
|
105
128
|
}
|
|
106
129
|
}
|
|
107
130
|
return entries;
|
|
@@ -109,6 +132,7 @@ function listRecursive(dirPath, base = '') {
|
|
|
109
132
|
|
|
110
133
|
export const handlers = {
|
|
111
134
|
execute_command: async (params, context) => {
|
|
135
|
+
const logger = getLogger();
|
|
112
136
|
const { command, timeout_seconds = 30 } = params;
|
|
113
137
|
const { config } = context;
|
|
114
138
|
const blockedPaths = config.security?.blocked_paths || [];
|
|
@@ -117,10 +141,13 @@ export const handlers = {
|
|
|
117
141
|
for (const bp of blockedPaths) {
|
|
118
142
|
const expanded = expandPath(bp);
|
|
119
143
|
if (command.includes(expanded)) {
|
|
144
|
+
logger.warn(`execute_command blocked: command references restricted path ${bp}`);
|
|
120
145
|
return { error: `Blocked: command references restricted path ${bp}` };
|
|
121
146
|
}
|
|
122
147
|
}
|
|
123
148
|
|
|
149
|
+
logger.debug(`execute_command: ${command.slice(0, 120)}`);
|
|
150
|
+
|
|
124
151
|
return new Promise((res) => {
|
|
125
152
|
let abortHandler = null;
|
|
126
153
|
|
|
@@ -164,8 +191,10 @@ export const handlers = {
|
|
|
164
191
|
},
|
|
165
192
|
|
|
166
193
|
read_file: async (params, context) => {
|
|
194
|
+
const logger = getLogger();
|
|
167
195
|
const { path: filePath, max_lines } = params;
|
|
168
196
|
if (isBlocked(filePath, context.config)) {
|
|
197
|
+
logger.warn(`read_file blocked: access to ${filePath} is restricted`);
|
|
169
198
|
return { error: `Blocked: access to ${filePath} is restricted` };
|
|
170
199
|
}
|
|
171
200
|
|
|
@@ -184,8 +213,10 @@ export const handlers = {
|
|
|
184
213
|
},
|
|
185
214
|
|
|
186
215
|
write_file: async (params, context) => {
|
|
216
|
+
const logger = getLogger();
|
|
187
217
|
const { path: filePath, content } = params;
|
|
188
218
|
if (isBlocked(filePath, context.config)) {
|
|
219
|
+
logger.warn(`write_file blocked: access to ${filePath} is restricted`);
|
|
189
220
|
return { error: `Blocked: access to ${filePath} is restricted` };
|
|
190
221
|
}
|
|
191
222
|
|
|
@@ -193,15 +224,19 @@ export const handlers = {
|
|
|
193
224
|
const expanded = expandPath(filePath);
|
|
194
225
|
mkdirSync(dirname(expanded), { recursive: true });
|
|
195
226
|
writeFileSync(expanded, content, 'utf-8');
|
|
227
|
+
logger.debug(`write_file: wrote ${content.length} bytes to ${expanded}`);
|
|
196
228
|
return { success: true, path: expanded };
|
|
197
229
|
} catch (err) {
|
|
230
|
+
logger.error(`write_file error for ${filePath}: ${err.message}`);
|
|
198
231
|
return { error: err.message };
|
|
199
232
|
}
|
|
200
233
|
},
|
|
201
234
|
|
|
202
235
|
list_directory: async (params, context) => {
|
|
236
|
+
const logger = getLogger();
|
|
203
237
|
const { path: dirPath, recursive = false } = params;
|
|
204
238
|
if (isBlocked(dirPath, context.config)) {
|
|
239
|
+
logger.warn(`list_directory blocked: access to ${dirPath} is restricted`);
|
|
205
240
|
return { error: `Blocked: access to ${dirPath} is restricted` };
|
|
206
241
|
}
|
|
207
242
|
|
package/src/tools/process.js
CHANGED
|
@@ -1,13 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
function run(cmd, timeout = 10000) {
|
|
4
|
-
return new Promise((resolve) => {
|
|
5
|
-
exec(cmd, { timeout }, (error, stdout, stderr) => {
|
|
6
|
-
if (error) return resolve({ error: stderr || error.message });
|
|
7
|
-
resolve({ output: stdout.trim() });
|
|
8
|
-
});
|
|
9
|
-
});
|
|
10
|
-
}
|
|
1
|
+
import { shellRun as run, shellEscape } from '../utils/shell.js';
|
|
2
|
+
import { getLogger } from '../utils/logger.js';
|
|
11
3
|
|
|
12
4
|
export const definitions = [
|
|
13
5
|
{
|
|
@@ -51,23 +43,38 @@ export const definitions = [
|
|
|
51
43
|
|
|
52
44
|
export const handlers = {
|
|
53
45
|
process_list: async (params) => {
|
|
46
|
+
const logger = getLogger();
|
|
54
47
|
const filter = params.filter;
|
|
55
|
-
|
|
48
|
+
logger.debug(`process_list: ${filter ? `filtering by "${filter}"` : 'listing all'}`);
|
|
49
|
+
const cmd = filter ? `ps aux | head -1 && ps aux | grep -i ${shellEscape(filter)} | grep -v grep` : 'ps aux';
|
|
56
50
|
return await run(cmd);
|
|
57
51
|
},
|
|
58
52
|
|
|
59
53
|
kill_process: async (params) => {
|
|
54
|
+
const logger = getLogger();
|
|
60
55
|
if (params.pid) {
|
|
61
|
-
|
|
56
|
+
const pid = parseInt(params.pid, 10);
|
|
57
|
+
if (!Number.isFinite(pid) || pid <= 0) return { error: 'Invalid PID' };
|
|
58
|
+
logger.debug(`kill_process: killing PID ${pid}`);
|
|
59
|
+
const result = await run(`kill ${pid}`);
|
|
60
|
+
if (result.error) logger.error(`kill_process failed for PID ${pid}: ${result.error}`);
|
|
61
|
+
return result;
|
|
62
62
|
}
|
|
63
63
|
if (params.name) {
|
|
64
|
-
|
|
64
|
+
logger.debug(`kill_process: killing processes matching "${params.name}"`);
|
|
65
|
+
const result = await run(`pkill -f ${shellEscape(params.name)}`);
|
|
66
|
+
if (result.error) logger.error(`kill_process failed for name "${params.name}": ${result.error}`);
|
|
67
|
+
return result;
|
|
65
68
|
}
|
|
66
69
|
return { error: 'Provide either pid or name' };
|
|
67
70
|
},
|
|
68
71
|
|
|
69
72
|
service_control: async (params) => {
|
|
73
|
+
const logger = getLogger();
|
|
70
74
|
const { service, action } = params;
|
|
71
|
-
|
|
75
|
+
logger.debug(`service_control: ${action} ${service}`);
|
|
76
|
+
const result = await run(`systemctl ${shellEscape(action)} ${shellEscape(service)}`);
|
|
77
|
+
if (result.error) logger.error(`service_control failed: ${action} ${service}: ${result.error}`);
|
|
78
|
+
return result;
|
|
72
79
|
},
|
|
73
80
|
};
|
package/src/utils/config.js
CHANGED
|
@@ -270,6 +270,65 @@ export function saveClaudeCodeAuth(config, mode, value) {
|
|
|
270
270
|
// mode === 'system' — no credentials to save
|
|
271
271
|
}
|
|
272
272
|
|
|
273
|
+
/**
|
|
274
|
+
* Full interactive flow: change orchestrator model + optionally enter API key.
|
|
275
|
+
*/
|
|
276
|
+
export async function changeOrchestratorModel(config, rl) {
|
|
277
|
+
const { createProvider } = await import('../providers/index.js');
|
|
278
|
+
const { providerKey, modelId } = await promptProviderSelection(rl);
|
|
279
|
+
|
|
280
|
+
const providerDef = PROVIDERS[providerKey];
|
|
281
|
+
|
|
282
|
+
// Resolve API key
|
|
283
|
+
const envKey = providerDef.envKey;
|
|
284
|
+
let apiKey = process.env[envKey];
|
|
285
|
+
if (!apiKey) {
|
|
286
|
+
const key = await ask(rl, chalk.cyan(`\n ${providerDef.name} API key (${envKey}): `));
|
|
287
|
+
if (!key.trim()) {
|
|
288
|
+
console.log(chalk.yellow('\n No API key provided. Orchestrator not changed.\n'));
|
|
289
|
+
return config;
|
|
290
|
+
}
|
|
291
|
+
apiKey = key.trim();
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Validate the new provider before saving anything
|
|
295
|
+
console.log(chalk.dim(`\n Verifying ${providerDef.name} / ${modelId}...`));
|
|
296
|
+
const testConfig = {
|
|
297
|
+
brain: {
|
|
298
|
+
provider: providerKey,
|
|
299
|
+
model: modelId,
|
|
300
|
+
max_tokens: config.orchestrator.max_tokens,
|
|
301
|
+
temperature: config.orchestrator.temperature,
|
|
302
|
+
api_key: apiKey,
|
|
303
|
+
},
|
|
304
|
+
};
|
|
305
|
+
try {
|
|
306
|
+
const testProvider = createProvider(testConfig);
|
|
307
|
+
await testProvider.ping();
|
|
308
|
+
} catch (err) {
|
|
309
|
+
console.log(chalk.red(`\n ✖ Verification failed: ${err.message}`));
|
|
310
|
+
console.log(chalk.yellow(` Orchestrator not changed. Keeping current model.\n`));
|
|
311
|
+
return config;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Validation passed — save everything
|
|
315
|
+
const savedPath = saveOrchestratorToYaml(providerKey, modelId);
|
|
316
|
+
console.log(chalk.dim(` Saved to ${savedPath}`));
|
|
317
|
+
|
|
318
|
+
config.orchestrator.provider = providerKey;
|
|
319
|
+
config.orchestrator.model = modelId;
|
|
320
|
+
config.orchestrator.api_key = apiKey;
|
|
321
|
+
|
|
322
|
+
// Save the key if it was newly entered
|
|
323
|
+
if (!process.env[envKey]) {
|
|
324
|
+
saveCredential(config, envKey, apiKey);
|
|
325
|
+
console.log(chalk.dim(' API key saved.\n'));
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
console.log(chalk.green(` ✔ Orchestrator switched to ${providerDef.name} / ${modelId}\n`));
|
|
329
|
+
return config;
|
|
330
|
+
}
|
|
331
|
+
|
|
273
332
|
/**
|
|
274
333
|
* Full interactive flow: change brain model + optionally enter API key.
|
|
275
334
|
*/
|
|
@@ -432,6 +491,13 @@ export function loadConfig() {
|
|
|
432
491
|
if (process.env.TELEGRAM_BOT_TOKEN) {
|
|
433
492
|
config.telegram.bot_token = process.env.TELEGRAM_BOT_TOKEN;
|
|
434
493
|
}
|
|
494
|
+
// Merge OWNER_TELEGRAM_ID into allowed_users if set
|
|
495
|
+
if (process.env.OWNER_TELEGRAM_ID) {
|
|
496
|
+
const ownerId = Number(process.env.OWNER_TELEGRAM_ID);
|
|
497
|
+
if (!config.telegram.allowed_users.includes(ownerId)) {
|
|
498
|
+
config.telegram.allowed_users.push(ownerId);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
435
501
|
if (process.env.GITHUB_TOKEN) {
|
|
436
502
|
if (!config.github) config.github = {};
|
|
437
503
|
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,31 @@
|
|
|
1
|
+
import { exec } from 'child_process';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Escape a string for safe use as a shell argument.
|
|
5
|
+
* Wraps the value in single quotes and escapes any embedded single quotes.
|
|
6
|
+
*
|
|
7
|
+
* @param {string} arg - The value to escape
|
|
8
|
+
* @returns {string} Shell-safe quoted string
|
|
9
|
+
*/
|
|
10
|
+
export function shellEscape(arg) {
|
|
11
|
+
if (arg === undefined || arg === null) return "''";
|
|
12
|
+
return "'" + String(arg).replace(/'/g, "'\\''") + "'";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Run a shell command and return { output } on success or { error } on failure.
|
|
17
|
+
* Resolves (never rejects) so callers can handle errors via the result object.
|
|
18
|
+
*
|
|
19
|
+
* @param {string} cmd - The shell command to execute
|
|
20
|
+
* @param {number} [timeout=10000] - Max execution time in milliseconds
|
|
21
|
+
* @param {{ maxBuffer?: number }} [opts] - Optional exec options
|
|
22
|
+
* @returns {Promise<{ output: string } | { error: string }>}
|
|
23
|
+
*/
|
|
24
|
+
export function shellRun(cmd, timeout = 10000, opts = {}) {
|
|
25
|
+
return new Promise((resolve) => {
|
|
26
|
+
exec(cmd, { timeout, maxBuffer: opts.maxBuffer, ...opts }, (error, stdout, stderr) => {
|
|
27
|
+
if (error) return resolve({ error: stderr || error.message });
|
|
28
|
+
resolve({ output: stdout.trim() });
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
}
|
|
@@ -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
|
+
}
|