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.
Files changed (44) hide show
  1. package/.env.example +11 -0
  2. package/README.md +76 -341
  3. package/bin/kernel.js +134 -15
  4. package/config.example.yaml +2 -1
  5. package/goals.md +20 -0
  6. package/knowledge_base/index.md +11 -0
  7. package/package.json +2 -1
  8. package/src/agent.js +166 -19
  9. package/src/automation/automation-manager.js +16 -0
  10. package/src/automation/automation.js +6 -2
  11. package/src/bot.js +295 -163
  12. package/src/conversation.js +70 -3
  13. package/src/life/engine.js +87 -68
  14. package/src/life/evolution.js +4 -8
  15. package/src/life/improvements.js +2 -6
  16. package/src/life/journal.js +3 -6
  17. package/src/life/memory.js +3 -10
  18. package/src/life/share-queue.js +4 -9
  19. package/src/prompts/orchestrator.js +21 -12
  20. package/src/prompts/persona.md +27 -0
  21. package/src/providers/base.js +51 -8
  22. package/src/providers/google-genai.js +198 -0
  23. package/src/providers/index.js +6 -1
  24. package/src/providers/models.js +6 -2
  25. package/src/providers/openai-compat.js +25 -11
  26. package/src/security/auth.js +38 -1
  27. package/src/services/stt.js +10 -1
  28. package/src/tools/docker.js +37 -15
  29. package/src/tools/git.js +6 -0
  30. package/src/tools/github.js +6 -0
  31. package/src/tools/jira.js +5 -0
  32. package/src/tools/monitor.js +13 -15
  33. package/src/tools/network.js +22 -18
  34. package/src/tools/os.js +37 -2
  35. package/src/tools/process.js +21 -14
  36. package/src/utils/config.js +66 -0
  37. package/src/utils/date.js +19 -0
  38. package/src/utils/display.js +1 -1
  39. package/src/utils/ids.js +12 -0
  40. package/src/utils/shell.js +31 -0
  41. package/src/utils/temporal-awareness.js +199 -0
  42. package/src/utils/timeUtils.js +110 -0
  43. package/src/utils/truncate.js +42 -0
  44. package/src/worker.js +2 -18
@@ -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
- // Silent fail don't crash the bot over persistence
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();
@@ -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
- const lifeConfig = this.config.life || {};
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
- // Extract ideas
411
- const ideaLines = response.split('\n').filter(l => l.trim().startsWith('IDEA:'));
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, ${ideaLines.length} ideas, ${shareLines.length} shares, ${askLines.length} questions, ${improveLines.length} improvements)`);
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
- // Extract shares
481
- const shareLines = response.split('\n').filter(l => l.trim().startsWith('SHARE:'));
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
- // Extract shares
598
- const shareLines = response.split('\n').filter(l => l.trim().startsWith('SHARE:'));
599
- for (const line of shareLines) {
600
- this.shareQueue.add(line.replace(/^SHARE:\s*/, '').trim(), 'create', 'medium', null, ['creation']);
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
- // Extract improvement ideas
1207
- const improveLines = response.split('\n').filter(l => l.trim().startsWith('IMPROVE:'));
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, ${improveLines.length} improvements, ${patternLines.length} patterns)`);
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);
@@ -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 startOfDay = new Date();
235
- startOfDay.setHours(0, 0, 0, 0);
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 ──────────────────────────────────────────────────
@@ -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,
@@ -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 = todayDate();
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(todayDate());
58
+ const filePath = this._journalPath(todayDateStr());
62
59
  if (!existsSync(filePath)) return null;
63
60
  return readFileSync(filePath, 'utf-8');
64
61
  }
@@ -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 = today();
52
+ const date = todayDateStr();
60
53
  const entries = this._loadEpisodicDay(date);
61
54
  const entry = {
62
55
  id: genId('ep'),
@@ -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 todayStart = new Date();
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 now = new Date();
27
- const timeStr = now.toLocaleString('en-US', {
28
- weekday: 'long',
29
- year: 'numeric',
30
- month: 'long',
31
- day: 'numeric',
32
- hour: '2-digit',
33
- minute: '2-digit',
34
- timeZoneName: 'short',
35
- });
36
- let timeBlock = `## Current Time\n${timeStr}`;
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
  }
@@ -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.