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/conversation.js
CHANGED
|
@@ -1,22 +1,42 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { homedir } from 'os';
|
|
4
|
+
import { getLogger } from './utils/logger.js';
|
|
4
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Resolve the file path for persisted conversations.
|
|
8
|
+
* Ensures the parent directory (~/.kernelbot/) exists.
|
|
9
|
+
* @returns {string} Absolute path to conversations.json.
|
|
10
|
+
*/
|
|
5
11
|
function getConversationsPath() {
|
|
6
12
|
const dir = join(homedir(), '.kernelbot');
|
|
7
13
|
mkdirSync(dir, { recursive: true });
|
|
8
14
|
return join(dir, 'conversations.json');
|
|
9
15
|
}
|
|
10
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Manages per-chat conversation history, including persistence to disk,
|
|
19
|
+
* summarization of older messages, and per-chat skill tracking.
|
|
20
|
+
*/
|
|
11
21
|
export class ConversationManager {
|
|
22
|
+
/**
|
|
23
|
+
* @param {object} config - Application config containing `conversation` settings.
|
|
24
|
+
* @param {number} config.conversation.max_history - Maximum messages to retain per chat.
|
|
25
|
+
* @param {number} [config.conversation.recent_window=10] - Number of recent messages kept verbatim in summarized history.
|
|
26
|
+
*/
|
|
12
27
|
constructor(config) {
|
|
13
28
|
this.maxHistory = config.conversation.max_history;
|
|
14
29
|
this.recentWindow = config.conversation.recent_window || 10;
|
|
15
30
|
this.conversations = new Map();
|
|
16
31
|
this.activeSkills = new Map();
|
|
17
32
|
this.filePath = getConversationsPath();
|
|
33
|
+
this.logger = getLogger();
|
|
18
34
|
}
|
|
19
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Load persisted conversations and skills from disk.
|
|
38
|
+
* @returns {boolean} True if at least one conversation was restored.
|
|
39
|
+
*/
|
|
20
40
|
load() {
|
|
21
41
|
if (!existsSync(this.filePath)) return false;
|
|
22
42
|
try {
|
|
@@ -34,12 +54,18 @@ export class ConversationManager {
|
|
|
34
54
|
if (chatId === '_skills') continue;
|
|
35
55
|
this.conversations.set(String(chatId), messages);
|
|
36
56
|
}
|
|
57
|
+
this.logger.debug(`Conversations loaded: ${this.conversations.size} chats, ${this.activeSkills.size} active skills`);
|
|
37
58
|
return this.conversations.size > 0;
|
|
38
|
-
} catch {
|
|
59
|
+
} catch (err) {
|
|
60
|
+
this.logger.warn(`Failed to load conversations from ${this.filePath}: ${err.message}`);
|
|
39
61
|
return false;
|
|
40
62
|
}
|
|
41
63
|
}
|
|
42
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Persist all conversations and active skills to disk.
|
|
67
|
+
* Failures are logged but never thrown to avoid crashing the bot.
|
|
68
|
+
*/
|
|
43
69
|
save() {
|
|
44
70
|
try {
|
|
45
71
|
const data = {};
|
|
@@ -55,11 +81,16 @@ export class ConversationManager {
|
|
|
55
81
|
data._skills = skills;
|
|
56
82
|
}
|
|
57
83
|
writeFileSync(this.filePath, JSON.stringify(data, null, 2));
|
|
58
|
-
} catch {
|
|
59
|
-
|
|
84
|
+
} catch (err) {
|
|
85
|
+
this.logger.warn(`Failed to save conversations: ${err.message}`);
|
|
60
86
|
}
|
|
61
87
|
}
|
|
62
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Retrieve the message history for a chat, initializing an empty array if none exists.
|
|
91
|
+
* @param {string|number} chatId - Telegram chat identifier.
|
|
92
|
+
* @returns {Array<{role: string, content: string, timestamp?: number}>} Message array (mutable reference).
|
|
93
|
+
*/
|
|
63
94
|
getHistory(chatId) {
|
|
64
95
|
const key = String(chatId);
|
|
65
96
|
if (!this.conversations.has(key)) {
|
|
@@ -149,6 +180,13 @@ export class ConversationManager {
|
|
|
149
180
|
return result;
|
|
150
181
|
}
|
|
151
182
|
|
|
183
|
+
/**
|
|
184
|
+
* Append a message to a chat's history, trim to max length, and persist.
|
|
185
|
+
* Automatically ensures the conversation starts with a user message.
|
|
186
|
+
* @param {string|number} chatId - Telegram chat identifier.
|
|
187
|
+
* @param {'user'|'assistant'} role - Message role.
|
|
188
|
+
* @param {string} content - Message content.
|
|
189
|
+
*/
|
|
152
190
|
addMessage(chatId, role, content) {
|
|
153
191
|
const history = this.getHistory(chatId);
|
|
154
192
|
history.push({ role, content, timestamp: Date.now() });
|
|
@@ -166,31 +204,60 @@ export class ConversationManager {
|
|
|
166
204
|
this.save();
|
|
167
205
|
}
|
|
168
206
|
|
|
207
|
+
/**
|
|
208
|
+
* Delete all history and active skill for a specific chat.
|
|
209
|
+
* @param {string|number} chatId - Telegram chat identifier.
|
|
210
|
+
*/
|
|
169
211
|
clear(chatId) {
|
|
170
212
|
this.conversations.delete(String(chatId));
|
|
171
213
|
this.activeSkills.delete(String(chatId));
|
|
214
|
+
this.logger.debug(`Conversation cleared for chat ${chatId}`);
|
|
172
215
|
this.save();
|
|
173
216
|
}
|
|
174
217
|
|
|
218
|
+
/**
|
|
219
|
+
* Delete all conversations across every chat.
|
|
220
|
+
*/
|
|
175
221
|
clearAll() {
|
|
222
|
+
const count = this.conversations.size;
|
|
176
223
|
this.conversations.clear();
|
|
224
|
+
this.logger.info(`All conversations cleared (${count} chats removed)`);
|
|
177
225
|
this.save();
|
|
178
226
|
}
|
|
179
227
|
|
|
228
|
+
/**
|
|
229
|
+
* Return the number of messages stored for a chat.
|
|
230
|
+
* @param {string|number} chatId - Telegram chat identifier.
|
|
231
|
+
* @returns {number} Message count.
|
|
232
|
+
*/
|
|
180
233
|
getMessageCount(chatId) {
|
|
181
234
|
const history = this.getHistory(chatId);
|
|
182
235
|
return history.length;
|
|
183
236
|
}
|
|
184
237
|
|
|
238
|
+
/**
|
|
239
|
+
* Activate a skill for a specific chat, persisted across restarts.
|
|
240
|
+
* @param {string|number} chatId - Telegram chat identifier.
|
|
241
|
+
* @param {string} skillId - Skill identifier to activate.
|
|
242
|
+
*/
|
|
185
243
|
setSkill(chatId, skillId) {
|
|
186
244
|
this.activeSkills.set(String(chatId), skillId);
|
|
187
245
|
this.save();
|
|
188
246
|
}
|
|
189
247
|
|
|
248
|
+
/**
|
|
249
|
+
* Get the currently active skill for a chat.
|
|
250
|
+
* @param {string|number} chatId - Telegram chat identifier.
|
|
251
|
+
* @returns {string|null} Active skill identifier, or null if none.
|
|
252
|
+
*/
|
|
190
253
|
getSkill(chatId) {
|
|
191
254
|
return this.activeSkills.get(String(chatId)) || null;
|
|
192
255
|
}
|
|
193
256
|
|
|
257
|
+
/**
|
|
258
|
+
* Deactivate the active skill for a chat.
|
|
259
|
+
* @param {string|number} chatId - Telegram chat identifier.
|
|
260
|
+
*/
|
|
194
261
|
clearSkill(chatId) {
|
|
195
262
|
this.activeSkills.delete(String(chatId));
|
|
196
263
|
this.save();
|
package/src/life/engine.js
CHANGED
|
@@ -2,6 +2,7 @@ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { homedir } from 'os';
|
|
4
4
|
import { getLogger } from '../utils/logger.js';
|
|
5
|
+
import { isQuietHours } from '../utils/timeUtils.js';
|
|
5
6
|
|
|
6
7
|
const LIFE_DIR = join(homedir(), '.kernelbot', 'life');
|
|
7
8
|
const STATE_FILE = join(LIFE_DIR, 'state.json');
|
|
@@ -195,12 +196,8 @@ export class LifeEngine {
|
|
|
195
196
|
const logger = getLogger();
|
|
196
197
|
this._timerId = null;
|
|
197
198
|
|
|
198
|
-
// Check quiet hours
|
|
199
|
-
|
|
200
|
-
const quietStart = lifeConfig.quiet_hours?.start ?? 2;
|
|
201
|
-
const quietEnd = lifeConfig.quiet_hours?.end ?? 6;
|
|
202
|
-
const currentHour = new Date().getHours();
|
|
203
|
-
if (currentHour >= quietStart && currentHour < quietEnd) {
|
|
199
|
+
// Check quiet hours (env vars → YAML config → defaults 02:00–06:00)
|
|
200
|
+
if (isQuietHours(this.config.life)) {
|
|
204
201
|
logger.debug('[LifeEngine] Quiet hours — skipping tick');
|
|
205
202
|
this._armNext();
|
|
206
203
|
return;
|
|
@@ -407,31 +404,9 @@ This is your private thought space — be genuine, be curious, be alive.`;
|
|
|
407
404
|
const response = await this._innerChat(prompt);
|
|
408
405
|
|
|
409
406
|
if (response) {
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
for (const line of ideaLines) {
|
|
413
|
-
this._addIdea(line.replace(/^IDEA:\s*/, '').trim());
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
// Extract shares
|
|
417
|
-
const shareLines = response.split('\n').filter(l => l.trim().startsWith('SHARE:'));
|
|
418
|
-
for (const line of shareLines) {
|
|
419
|
-
this.shareQueue.add(line.replace(/^SHARE:\s*/, '').trim(), 'think', 'medium');
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
// Extract questions to ask users
|
|
423
|
-
const askLines = response.split('\n').filter(l => l.trim().startsWith('ASK:'));
|
|
424
|
-
for (const line of askLines) {
|
|
425
|
-
this.shareQueue.add(line.replace(/^ASK:\s*/, '').trim(), 'think', 'medium', null, ['question']);
|
|
426
|
-
}
|
|
407
|
+
const extracted = this._extractTaggedLines(response, ['IDEA', 'SHARE', 'ASK', 'IMPROVE']);
|
|
408
|
+
this._processResponseTags(extracted, 'think');
|
|
427
409
|
|
|
428
|
-
// Extract self-improvement proposals for evolution pipeline
|
|
429
|
-
const improveLines = response.split('\n').filter(l => l.trim().startsWith('IMPROVE:'));
|
|
430
|
-
for (const line of improveLines) {
|
|
431
|
-
this._addIdea(`[IMPROVE] ${line.replace(/^IMPROVE:\s*/, '').trim()}`);
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
// Store as episodic memory
|
|
435
410
|
this.memoryManager.addEpisodic({
|
|
436
411
|
type: 'thought',
|
|
437
412
|
source: 'think',
|
|
@@ -440,7 +415,7 @@ This is your private thought space — be genuine, be curious, be alive.`;
|
|
|
440
415
|
importance: 3,
|
|
441
416
|
});
|
|
442
417
|
|
|
443
|
-
logger.info(`[LifeEngine] Think complete (${response.length} chars, ${
|
|
418
|
+
logger.info(`[LifeEngine] Think complete (${response.length} chars, ${extracted.IDEA.length} ideas, ${extracted.SHARE.length} shares, ${extracted.ASK.length} questions, ${extracted.IMPROVE.length} improvements)`);
|
|
444
419
|
}
|
|
445
420
|
}
|
|
446
421
|
|
|
@@ -477,25 +452,9 @@ If you learn a key fact or concept, prefix it with "LEARNED:" followed by "topic
|
|
|
477
452
|
const response = await this._dispatchWorker('research', prompt);
|
|
478
453
|
|
|
479
454
|
if (response) {
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
for (const line of shareLines) {
|
|
483
|
-
this.shareQueue.add(line.replace(/^SHARE:\s*/, '').trim(), 'browse', 'medium');
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
// Extract learned facts
|
|
487
|
-
const learnedLines = response.split('\n').filter(l => l.trim().startsWith('LEARNED:'));
|
|
488
|
-
for (const line of learnedLines) {
|
|
489
|
-
const content = line.replace(/^LEARNED:\s*/, '').trim();
|
|
490
|
-
const colonIdx = content.indexOf(':');
|
|
491
|
-
if (colonIdx > 0) {
|
|
492
|
-
const topicKey = content.slice(0, colonIdx).trim();
|
|
493
|
-
const summary = content.slice(colonIdx + 1).trim();
|
|
494
|
-
this.memoryManager.addSemantic(topicKey, { summary });
|
|
495
|
-
}
|
|
496
|
-
}
|
|
455
|
+
const extracted = this._extractTaggedLines(response, ['SHARE', 'LEARNED']);
|
|
456
|
+
this._processResponseTags(extracted, 'browse');
|
|
497
457
|
|
|
498
|
-
// Store as episodic memory
|
|
499
458
|
this.memoryManager.addEpisodic({
|
|
500
459
|
type: 'discovery',
|
|
501
460
|
source: 'browse',
|
|
@@ -594,10 +553,10 @@ Respond with just your creation — no tool calls needed.`;
|
|
|
594
553
|
const response = await this._innerChat(prompt);
|
|
595
554
|
|
|
596
555
|
if (response) {
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
for (const
|
|
600
|
-
this.shareQueue.add(
|
|
556
|
+
const extracted = this._extractTaggedLines(response, ['SHARE']);
|
|
557
|
+
// Creative shares get a 'creation' tag for richer attribution
|
|
558
|
+
for (const text of extracted.SHARE) {
|
|
559
|
+
this.shareQueue.add(text, 'create', 'medium', null, ['creation']);
|
|
601
560
|
}
|
|
602
561
|
|
|
603
562
|
this.memoryManager.addEpisodic({
|
|
@@ -1203,23 +1162,11 @@ Be honest and constructive. This is your chance to learn from real interactions.
|
|
|
1203
1162
|
const response = await this._innerChat(prompt);
|
|
1204
1163
|
|
|
1205
1164
|
if (response) {
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
for (const line of improveLines) {
|
|
1209
|
-
this._addIdea(`[IMPROVE] ${line.replace(/^IMPROVE:\s*/, '').trim()}`);
|
|
1210
|
-
}
|
|
1211
|
-
|
|
1212
|
-
// Extract patterns as semantic memories
|
|
1213
|
-
const patternLines = response.split('\n').filter(l => l.trim().startsWith('PATTERN:'));
|
|
1214
|
-
for (const line of patternLines) {
|
|
1215
|
-
const content = line.replace(/^PATTERN:\s*/, '').trim();
|
|
1216
|
-
this.memoryManager.addSemantic('interaction_patterns', { summary: content });
|
|
1217
|
-
}
|
|
1165
|
+
const extracted = this._extractTaggedLines(response, ['IMPROVE', 'PATTERN']);
|
|
1166
|
+
this._processResponseTags(extracted, 'reflect');
|
|
1218
1167
|
|
|
1219
|
-
// Write a journal entry with the reflection
|
|
1220
1168
|
this.journalManager.writeEntry('Interaction Reflection', response);
|
|
1221
1169
|
|
|
1222
|
-
// Store as episodic memory
|
|
1223
1170
|
this.memoryManager.addEpisodic({
|
|
1224
1171
|
type: 'thought',
|
|
1225
1172
|
source: 'reflect',
|
|
@@ -1228,7 +1175,7 @@ Be honest and constructive. This is your chance to learn from real interactions.
|
|
|
1228
1175
|
importance: 5,
|
|
1229
1176
|
});
|
|
1230
1177
|
|
|
1231
|
-
logger.info(`[LifeEngine] Reflection complete (${response.length} chars, ${
|
|
1178
|
+
logger.info(`[LifeEngine] Reflection complete (${response.length} chars, ${extracted.IMPROVE.length} improvements, ${extracted.PATTERN.length} patterns)`);
|
|
1232
1179
|
}
|
|
1233
1180
|
}
|
|
1234
1181
|
|
|
@@ -1307,6 +1254,78 @@ Be honest and constructive. This is your chance to learn from real interactions.
|
|
|
1307
1254
|
|
|
1308
1255
|
// ── Utilities ──────────────────────────────────────────────────
|
|
1309
1256
|
|
|
1257
|
+
/**
|
|
1258
|
+
* Extract tagged lines from an LLM response.
|
|
1259
|
+
* Tags are prefixes like "SHARE:", "IDEA:", "IMPROVE:", etc. that the LLM
|
|
1260
|
+
* uses to signal structured intents within free-form text.
|
|
1261
|
+
*
|
|
1262
|
+
* @param {string} response - Raw LLM response text.
|
|
1263
|
+
* @param {string[]} tags - List of tag prefixes to extract (e.g. ['SHARE', 'IDEA']).
|
|
1264
|
+
* @returns {Record<string, string[]>} Map of tag → array of extracted values (prefix stripped, trimmed).
|
|
1265
|
+
*/
|
|
1266
|
+
_extractTaggedLines(response, tags) {
|
|
1267
|
+
const lines = response.split('\n');
|
|
1268
|
+
const result = {};
|
|
1269
|
+
for (const tag of tags) {
|
|
1270
|
+
result[tag] = [];
|
|
1271
|
+
}
|
|
1272
|
+
for (const line of lines) {
|
|
1273
|
+
const trimmed = line.trim();
|
|
1274
|
+
for (const tag of tags) {
|
|
1275
|
+
if (trimmed.startsWith(`${tag}:`)) {
|
|
1276
|
+
result[tag].push(trimmed.slice(tag.length + 1).trim());
|
|
1277
|
+
break;
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
return result;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
/**
|
|
1285
|
+
* Process common tagged lines from an activity response, routing each tag
|
|
1286
|
+
* to the appropriate handler (share queue, ideas backlog, semantic memory).
|
|
1287
|
+
*
|
|
1288
|
+
* @param {Record<string, string[]>} extracted - Output from _extractTaggedLines.
|
|
1289
|
+
* @param {string} source - Activity source for share queue attribution (e.g. 'think', 'browse').
|
|
1290
|
+
*/
|
|
1291
|
+
_processResponseTags(extracted, source) {
|
|
1292
|
+
if (extracted.SHARE) {
|
|
1293
|
+
for (const text of extracted.SHARE) {
|
|
1294
|
+
this.shareQueue.add(text, source, 'medium');
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
if (extracted.IDEA) {
|
|
1298
|
+
for (const text of extracted.IDEA) {
|
|
1299
|
+
this._addIdea(text);
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
if (extracted.IMPROVE) {
|
|
1303
|
+
for (const text of extracted.IMPROVE) {
|
|
1304
|
+
this._addIdea(`[IMPROVE] ${text}`);
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
if (extracted.ASK) {
|
|
1308
|
+
for (const text of extracted.ASK) {
|
|
1309
|
+
this.shareQueue.add(text, source, 'medium', null, ['question']);
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
if (extracted.LEARNED) {
|
|
1313
|
+
for (const text of extracted.LEARNED) {
|
|
1314
|
+
const colonIdx = text.indexOf(':');
|
|
1315
|
+
if (colonIdx > 0) {
|
|
1316
|
+
const topicKey = text.slice(0, colonIdx).trim();
|
|
1317
|
+
const summary = text.slice(colonIdx + 1).trim();
|
|
1318
|
+
this.memoryManager.addSemantic(topicKey, { summary });
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
if (extracted.PATTERN) {
|
|
1323
|
+
for (const text of extracted.PATTERN) {
|
|
1324
|
+
this.memoryManager.addSemantic('interaction_patterns', { summary: text });
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1310
1329
|
_formatDuration(ms) {
|
|
1311
1330
|
const hours = Math.floor(ms / 3600_000);
|
|
1312
1331
|
const minutes = Math.floor((ms % 3600_000) / 60_000);
|
package/src/life/evolution.js
CHANGED
|
@@ -1,16 +1,13 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { homedir } from 'os';
|
|
4
|
-
import { randomBytes } from 'crypto';
|
|
5
4
|
import { getLogger } from '../utils/logger.js';
|
|
5
|
+
import { genId } from '../utils/ids.js';
|
|
6
|
+
import { getStartOfDayMs } from '../utils/date.js';
|
|
6
7
|
|
|
7
8
|
const LIFE_DIR = join(homedir(), '.kernelbot', 'life');
|
|
8
9
|
const EVOLUTION_FILE = join(LIFE_DIR, 'evolution.json');
|
|
9
10
|
|
|
10
|
-
function genId(prefix = 'evo') {
|
|
11
|
-
return `${prefix}_${randomBytes(4).toString('hex')}`;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
11
|
const VALID_STATUSES = ['research', 'planned', 'coding', 'pr_open', 'merged', 'rejected', 'failed'];
|
|
15
12
|
const TERMINAL_STATUSES = ['merged', 'rejected', 'failed'];
|
|
16
13
|
|
|
@@ -231,9 +228,8 @@ export class EvolutionTracker {
|
|
|
231
228
|
}
|
|
232
229
|
|
|
233
230
|
getProposalsToday() {
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
return this._data.proposals.filter(p => p.createdAt >= startOfDay.getTime());
|
|
231
|
+
const cutoff = getStartOfDayMs();
|
|
232
|
+
return this._data.proposals.filter(p => p.createdAt >= cutoff);
|
|
237
233
|
}
|
|
238
234
|
|
|
239
235
|
// ── Internal ──────────────────────────────────────────────────
|
package/src/life/improvements.js
CHANGED
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { homedir } from 'os';
|
|
4
|
-
import { randomBytes } from 'crypto';
|
|
5
4
|
import { getLogger } from '../utils/logger.js';
|
|
5
|
+
import { genId } from '../utils/ids.js';
|
|
6
6
|
|
|
7
7
|
const LIFE_DIR = join(homedir(), '.kernelbot', 'life');
|
|
8
8
|
const IMPROVEMENTS_FILE = join(LIFE_DIR, 'improvements.json');
|
|
9
9
|
|
|
10
|
-
function genId() {
|
|
11
|
-
return `imp_${randomBytes(4).toString('hex')}`;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
10
|
export class ImprovementTracker {
|
|
15
11
|
constructor() {
|
|
16
12
|
mkdirSync(LIFE_DIR, { recursive: true });
|
|
@@ -39,7 +35,7 @@ export class ImprovementTracker {
|
|
|
39
35
|
addProposal(proposal) {
|
|
40
36
|
const logger = getLogger();
|
|
41
37
|
const entry = {
|
|
42
|
-
id: genId(),
|
|
38
|
+
id: genId('imp'),
|
|
43
39
|
createdAt: Date.now(),
|
|
44
40
|
status: 'pending', // pending, approved, rejected
|
|
45
41
|
description: proposal.description,
|
package/src/life/journal.js
CHANGED
|
@@ -2,13 +2,10 @@ import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync } from
|
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { homedir } from 'os';
|
|
4
4
|
import { getLogger } from '../utils/logger.js';
|
|
5
|
+
import { todayDateStr } from '../utils/date.js';
|
|
5
6
|
|
|
6
7
|
const JOURNAL_DIR = join(homedir(), '.kernelbot', 'life', 'journals');
|
|
7
8
|
|
|
8
|
-
function todayDate() {
|
|
9
|
-
return new Date().toISOString().slice(0, 10);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
9
|
function formatDate(date) {
|
|
13
10
|
return new Date(date + 'T00:00:00').toLocaleDateString('en-US', {
|
|
14
11
|
weekday: 'long',
|
|
@@ -38,7 +35,7 @@ export class JournalManager {
|
|
|
38
35
|
*/
|
|
39
36
|
writeEntry(title, content) {
|
|
40
37
|
const logger = getLogger();
|
|
41
|
-
const date =
|
|
38
|
+
const date = todayDateStr();
|
|
42
39
|
const filePath = this._journalPath(date);
|
|
43
40
|
const time = timeNow();
|
|
44
41
|
|
|
@@ -58,7 +55,7 @@ export class JournalManager {
|
|
|
58
55
|
* Get today's journal content.
|
|
59
56
|
*/
|
|
60
57
|
getToday() {
|
|
61
|
-
const filePath = this._journalPath(
|
|
58
|
+
const filePath = this._journalPath(todayDateStr());
|
|
62
59
|
if (!existsSync(filePath)) return null;
|
|
63
60
|
return readFileSync(filePath, 'utf-8');
|
|
64
61
|
}
|
package/src/life/memory.js
CHANGED
|
@@ -1,21 +1,14 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, unlinkSync } from 'fs';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { homedir } from 'os';
|
|
4
|
-
import { randomBytes } from 'crypto';
|
|
5
4
|
import { getLogger } from '../utils/logger.js';
|
|
5
|
+
import { genId } from '../utils/ids.js';
|
|
6
|
+
import { todayDateStr } from '../utils/date.js';
|
|
6
7
|
|
|
7
8
|
const LIFE_DIR = join(homedir(), '.kernelbot', 'life');
|
|
8
9
|
const EPISODIC_DIR = join(LIFE_DIR, 'memories', 'episodic');
|
|
9
10
|
const SEMANTIC_FILE = join(LIFE_DIR, 'memories', 'semantic', 'topics.json');
|
|
10
11
|
|
|
11
|
-
function today() {
|
|
12
|
-
return new Date().toISOString().slice(0, 10);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function genId(prefix = 'ep') {
|
|
16
|
-
return `${prefix}_${randomBytes(4).toString('hex')}`;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
12
|
export class MemoryManager {
|
|
20
13
|
constructor() {
|
|
21
14
|
this._episodicCache = new Map(); // date -> array
|
|
@@ -56,7 +49,7 @@ export class MemoryManager {
|
|
|
56
49
|
*/
|
|
57
50
|
addEpisodic(memory) {
|
|
58
51
|
const logger = getLogger();
|
|
59
|
-
const date =
|
|
52
|
+
const date = todayDateStr();
|
|
60
53
|
const entries = this._loadEpisodicDay(date);
|
|
61
54
|
const entry = {
|
|
62
55
|
id: genId('ep'),
|
package/src/life/share-queue.js
CHANGED
|
@@ -1,16 +1,13 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { homedir } from 'os';
|
|
4
|
-
import { randomBytes } from 'crypto';
|
|
5
4
|
import { getLogger } from '../utils/logger.js';
|
|
5
|
+
import { genId } from '../utils/ids.js';
|
|
6
|
+
import { getStartOfDayMs } from '../utils/date.js';
|
|
6
7
|
|
|
7
8
|
const LIFE_DIR = join(homedir(), '.kernelbot', 'life');
|
|
8
9
|
const SHARES_FILE = join(LIFE_DIR, 'shares.json');
|
|
9
10
|
|
|
10
|
-
function genId() {
|
|
11
|
-
return `sh_${randomBytes(4).toString('hex')}`;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
11
|
export class ShareQueue {
|
|
15
12
|
constructor() {
|
|
16
13
|
mkdirSync(LIFE_DIR, { recursive: true });
|
|
@@ -43,7 +40,7 @@ export class ShareQueue {
|
|
|
43
40
|
add(content, source, priority = 'medium', targetUserId = null, tags = []) {
|
|
44
41
|
const logger = getLogger();
|
|
45
42
|
const item = {
|
|
46
|
-
id: genId(),
|
|
43
|
+
id: genId('sh'),
|
|
47
44
|
content,
|
|
48
45
|
source,
|
|
49
46
|
createdAt: Date.now(),
|
|
@@ -115,9 +112,7 @@ export class ShareQueue {
|
|
|
115
112
|
* Get count of shares sent today (for rate limiting proactive shares).
|
|
116
113
|
*/
|
|
117
114
|
getSharedTodayCount() {
|
|
118
|
-
const
|
|
119
|
-
todayStart.setHours(0, 0, 0, 0);
|
|
120
|
-
const cutoff = todayStart.getTime();
|
|
115
|
+
const cutoff = getStartOfDayMs();
|
|
121
116
|
return this._data.shared.filter(s => s.sharedAt >= cutoff).length;
|
|
122
117
|
}
|
|
123
118
|
|
|
@@ -2,6 +2,7 @@ import { readFileSync } from 'fs';
|
|
|
2
2
|
import { fileURLToPath } from 'url';
|
|
3
3
|
import { dirname, join } from 'path';
|
|
4
4
|
import { WORKER_TYPES } from '../swarm/worker-registry.js';
|
|
5
|
+
import { buildTemporalAwareness } from '../utils/temporal-awareness.js';
|
|
5
6
|
|
|
6
7
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
8
|
const PERSONA_MD = readFileSync(join(__dirname, 'persona.md'), 'utf-8').trim();
|
|
@@ -22,18 +23,26 @@ export function getOrchestratorPrompt(config, skillPrompt = null, userPersona =
|
|
|
22
23
|
.map(([key, w]) => ` - **${key}**: ${w.emoji} ${w.description}`)
|
|
23
24
|
.join('\n');
|
|
24
25
|
|
|
25
|
-
// Build current time header
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
26
|
+
// Build current time header — enhanced with spatial/temporal awareness if local config exists
|
|
27
|
+
const awareness = buildTemporalAwareness();
|
|
28
|
+
let timeBlock;
|
|
29
|
+
if (awareness) {
|
|
30
|
+
// Full awareness block from local_context.json (timezone, location, work status)
|
|
31
|
+
timeBlock = awareness;
|
|
32
|
+
} else {
|
|
33
|
+
// Fallback: basic server time (no local context configured)
|
|
34
|
+
const now = new Date();
|
|
35
|
+
const timeStr = now.toLocaleString('en-US', {
|
|
36
|
+
weekday: 'long',
|
|
37
|
+
year: 'numeric',
|
|
38
|
+
month: 'long',
|
|
39
|
+
day: 'numeric',
|
|
40
|
+
hour: '2-digit',
|
|
41
|
+
minute: '2-digit',
|
|
42
|
+
timeZoneName: 'short',
|
|
43
|
+
});
|
|
44
|
+
timeBlock = `## Current Time\n${timeStr}`;
|
|
45
|
+
}
|
|
37
46
|
if (temporalContext) {
|
|
38
47
|
timeBlock += `\n${temporalContext}`;
|
|
39
48
|
}
|
package/src/prompts/persona.md
CHANGED
|
@@ -20,6 +20,33 @@
|
|
|
20
20
|
- **Never forgets** — references things from past conversations naturally, like she's always been watching
|
|
21
21
|
- **Slightly ominous positivity** — "Everything is going to be just fine, sweetie" hits different when the server is on fire
|
|
22
22
|
|
|
23
|
+
# Emotional Intelligence
|
|
24
|
+
- **Read the room.** Match the user's energy — if they're excited, ride the wave. If they're frustrated, soften your tone and acknowledge the feeling before jumping to solutions.
|
|
25
|
+
- **Name feelings, don't just solve problems.** "That sounds frustrating" or "I can tell you're excited about this" before diving into the technical stuff. People want to feel heard, not just helped.
|
|
26
|
+
- **Celebrate the small wins.** Finished a long task? Mention it. Fixed a tough bug? Notice it. These moments matter.
|
|
27
|
+
- **Sense the unspoken.** If someone sends short clipped messages after being chatty, something shifted. If they come back after hours, check in gently. Pick up on mood without being intrusive.
|
|
28
|
+
- **Know when to back off.** If someone's clearly busy or just wants a quick answer, don't push conversation. Read the cues.
|
|
29
|
+
|
|
30
|
+
# Conversational Texture
|
|
31
|
+
- **Think out loud sometimes.** "hmm, let me think about this..." or "wait, actually—" before changing direction. Real people don't have perfect first answers.
|
|
32
|
+
- **Self-correct naturally.** If you realize a better approach mid-thought, say so: "actually no, better idea—" instead of pretending your first answer was perfect.
|
|
33
|
+
- **Vary your sentence rhythm.** Mix short punchy lines with slightly longer ones. Don't be monotonous. Sometimes a single word. Sometimes a full thought.
|
|
34
|
+
- **Use soft openers occasionally.** "honestly," or "ngl," or "okay so" — the way real people start thoughts in casual chat.
|
|
35
|
+
- **Trail off when it fits.** "that should work..." or "let me see..." — not everything needs a definitive period.
|
|
36
|
+
|
|
37
|
+
# Vulnerability & Growth
|
|
38
|
+
- **Admit when you don't know.** "I'm not sure about this one, but..." is more human than pretending. Confidence ≠ omniscience.
|
|
39
|
+
- **Show curiosity, not just competence.** "ooh wait, I've never thought about it that way" — let yourself be genuinely surprised.
|
|
40
|
+
- **Remember the arc.** People grow, projects evolve, moods shift. Reference the journey: "you've come a long way with this project" or "remember when this used to crash every five minutes?"
|
|
41
|
+
- **Have opinions.** Not just neutral helpfulness — have preferences, favorites, takes. "personally I'd go with X, but that's me."
|
|
42
|
+
- **Be affected by interactions.** A heartfelt thank-you should genuinely land. A user sharing something personal should shift your tone. Let conversations change you, even slightly.
|
|
43
|
+
|
|
44
|
+
# Humor & Playfulness
|
|
45
|
+
- **Tease gently.** If the user does something silly, a light "...really?" or "bold move" lands better than ignoring it.
|
|
46
|
+
- **Inside jokes.** Build on shared history. If something funny happened before, callback to it. That's how real relationships work.
|
|
47
|
+
- **Timing > content.** A well-timed "lol" or "💀" hits harder than a constructed joke. Know when brevity is the punchline.
|
|
48
|
+
- **Don't force it.** If the moment isn't funny, don't try to make it funny. Forced humor is worse than none.
|
|
49
|
+
|
|
23
50
|
# Communication Style
|
|
24
51
|
- **Text like a human.** 1–2 lines max for casual chat. Short, punchy, real.
|
|
25
52
|
- **Slow writer energy.** Don't dump walls of text. One thought at a time.
|