kernelbot 1.0.37 → 1.0.38
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/bin/kernel.js +389 -23
- package/config.example.yaml +17 -0
- package/package.json +2 -1
- package/src/agent.js +355 -82
- package/src/bot.js +724 -12
- package/src/character.js +406 -0
- package/src/characters/builder.js +174 -0
- package/src/characters/builtins.js +421 -0
- package/src/conversation.js +17 -2
- package/src/dashboard/agents.css +469 -0
- package/src/dashboard/agents.html +184 -0
- package/src/dashboard/agents.js +873 -0
- package/src/dashboard/dashboard.css +281 -0
- package/src/dashboard/dashboard.js +579 -0
- package/src/dashboard/index.html +366 -0
- package/src/dashboard/server.js +521 -0
- package/src/dashboard/shared.css +700 -0
- package/src/dashboard/shared.js +209 -0
- package/src/life/engine.js +28 -20
- package/src/life/evolution.js +7 -5
- package/src/life/journal.js +5 -4
- package/src/life/memory.js +12 -9
- package/src/life/share-queue.js +7 -5
- package/src/prompts/orchestrator.js +76 -14
- package/src/prompts/workers.js +22 -0
- package/src/self.js +17 -5
- package/src/services/linkedin-api.js +190 -0
- package/src/services/stt.js +8 -2
- package/src/services/tts.js +32 -2
- package/src/services/x-api.js +141 -0
- package/src/swarm/worker-registry.js +7 -0
- package/src/tools/categories.js +4 -0
- package/src/tools/index.js +6 -0
- package/src/tools/linkedin.js +264 -0
- package/src/tools/orchestrator-tools.js +337 -2
- package/src/tools/x.js +256 -0
- package/src/utils/config.js +104 -57
- package/src/utils/display.js +73 -12
- package/src/utils/temporal-awareness.js +24 -10
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KERNEL Dashboard — Shared utilities, SSE, gauges, canvas animations.
|
|
3
|
+
* Exposed on window.KERNEL for use by page-specific scripts.
|
|
4
|
+
*/
|
|
5
|
+
(function() {
|
|
6
|
+
// ── Utilities ──
|
|
7
|
+
function esc(s) {
|
|
8
|
+
if (!s) return '';
|
|
9
|
+
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function formatDuration(seconds) {
|
|
13
|
+
if (seconds == null || seconds < 0) return '--';
|
|
14
|
+
const s = Math.floor(seconds);
|
|
15
|
+
if (s < 60) return s + 's';
|
|
16
|
+
if (s < 3600) return Math.floor(s/60) + 'm ' + (s%60) + 's';
|
|
17
|
+
return Math.floor(s/3600) + 'h ' + Math.floor((s%3600)/60) + 'm';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function timeAgo(ts) {
|
|
21
|
+
if (!ts) return 'never';
|
|
22
|
+
const s = Math.floor((Date.now() - ts)/1000);
|
|
23
|
+
if (s < 60) return 'just now';
|
|
24
|
+
if (s < 3600) return Math.floor(s/60) + 'm ago';
|
|
25
|
+
if (s < 86400) return Math.floor(s/3600) + 'h ago';
|
|
26
|
+
return Math.floor(s/86400) + 'd ago';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function formatBytes(b) {
|
|
30
|
+
if (b < 1024) return b + ' B';
|
|
31
|
+
if (b < 1048576) return (b/1024).toFixed(1) + ' KB';
|
|
32
|
+
if (b < 1073741824) return (b/1048576).toFixed(1) + ' MB';
|
|
33
|
+
return (b/1073741824).toFixed(2) + ' GB';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function barColor(pct) { return pct < 50 ? 'green' : pct < 80 ? 'amber' : 'red'; }
|
|
37
|
+
|
|
38
|
+
function makeBar(label, pct, color) {
|
|
39
|
+
return `<div class="row"><span class="k">${esc(label)}</span><span class="v">${pct.toFixed(1)}%</span></div><div class="bar-track"><div class="bar-fill ${color || barColor(pct)}" style="width:${Math.min(pct,100)}%"></div></div>`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const $ = id => document.getElementById(id);
|
|
43
|
+
|
|
44
|
+
// ── Clock ──
|
|
45
|
+
function startClock() {
|
|
46
|
+
setInterval(() => {
|
|
47
|
+
const now = new Date();
|
|
48
|
+
const p = n => String(n).padStart(2,'0');
|
|
49
|
+
const full = now.getFullYear() + '.' + p(now.getMonth()+1) + '.' + p(now.getDate()) + ' ' + p(now.getHours()) + ':' + p(now.getMinutes()) + ':' + p(now.getSeconds());
|
|
50
|
+
const hdrClock = $('hdr-clock');
|
|
51
|
+
if (hdrClock) hdrClock.textContent = full;
|
|
52
|
+
const rbClock = $('rb-clock');
|
|
53
|
+
if (rbClock) rbClock.textContent = p(now.getHours()) + ':' + p(now.getMinutes());
|
|
54
|
+
}, 1000);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ── Gauge helpers ──
|
|
58
|
+
function setGauge(id, pct, label) {
|
|
59
|
+
const el = $(id);
|
|
60
|
+
if (!el) return;
|
|
61
|
+
const circumference = parseFloat(el.querySelector('.fill').getAttribute('stroke-dasharray'));
|
|
62
|
+
const offset = circumference * (1 - Math.min(pct, 100) / 100);
|
|
63
|
+
const fill = el.querySelector('.fill');
|
|
64
|
+
fill.style.strokeDashoffset = offset;
|
|
65
|
+
fill.classList.remove('amber', 'red');
|
|
66
|
+
if (pct >= 80) fill.classList.add('red');
|
|
67
|
+
else if (pct >= 50) fill.classList.add('amber');
|
|
68
|
+
const pctEl = el.querySelector('.pct');
|
|
69
|
+
if (pctEl) {
|
|
70
|
+
pctEl.textContent = pct.toFixed(0) + '%';
|
|
71
|
+
pctEl.classList.remove('amber','red');
|
|
72
|
+
if (pct >= 80) pctEl.classList.add('red');
|
|
73
|
+
else if (pct >= 50) pctEl.classList.add('amber');
|
|
74
|
+
}
|
|
75
|
+
const sub = el.querySelector('.sub');
|
|
76
|
+
if (sub && label) sub.textContent = label;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function setMiniGauge(id, pct) {
|
|
80
|
+
const el = $(id);
|
|
81
|
+
if (!el) return;
|
|
82
|
+
const fill = el.querySelector('.fill');
|
|
83
|
+
const circumference = parseFloat(fill.getAttribute('stroke-dasharray'));
|
|
84
|
+
fill.style.strokeDashoffset = circumference * (1 - Math.min(pct, 100) / 100);
|
|
85
|
+
fill.classList.remove('amber','red');
|
|
86
|
+
if (pct >= 80) fill.classList.add('red');
|
|
87
|
+
else if (pct >= 50) fill.classList.add('amber');
|
|
88
|
+
el.querySelector('.val').textContent = pct.toFixed(0) + '%';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ── SSE Connection ──
|
|
92
|
+
function connectSSE(onSnapshot) {
|
|
93
|
+
let reconnectDelay = 1000;
|
|
94
|
+
function connect() {
|
|
95
|
+
const es = new EventSource('/events');
|
|
96
|
+
es.onopen = () => {
|
|
97
|
+
reconnectDelay = 1000;
|
|
98
|
+
const connDot = $('conn-dot');
|
|
99
|
+
if (connDot) connDot.classList.add('connected');
|
|
100
|
+
const topConn = $('top-conn');
|
|
101
|
+
if (topConn) topConn.classList.add('connected');
|
|
102
|
+
const hdrStatus = $('hdr-status');
|
|
103
|
+
if (hdrStatus) hdrStatus.textContent = 'ONLINE';
|
|
104
|
+
};
|
|
105
|
+
es.onmessage = (evt) => {
|
|
106
|
+
try { onSnapshot(JSON.parse(evt.data)); } catch(e) {}
|
|
107
|
+
};
|
|
108
|
+
es.onerror = () => {
|
|
109
|
+
es.close();
|
|
110
|
+
const connDot = $('conn-dot');
|
|
111
|
+
if (connDot) connDot.classList.remove('connected');
|
|
112
|
+
const topConn = $('top-conn');
|
|
113
|
+
if (topConn) topConn.classList.remove('connected');
|
|
114
|
+
const hdrStatus = $('hdr-status');
|
|
115
|
+
if (hdrStatus) hdrStatus.textContent = 'RECONNECTING';
|
|
116
|
+
setTimeout(connect, reconnectDelay);
|
|
117
|
+
reconnectDelay = Math.min(reconnectDelay * 1.5, 15000);
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
connect();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ── Particle Grid Canvas ──
|
|
124
|
+
function initParticleCanvas() {
|
|
125
|
+
const canvas = $('particle-canvas');
|
|
126
|
+
if (!canvas) return;
|
|
127
|
+
const ctx = canvas.getContext('2d');
|
|
128
|
+
let w, h;
|
|
129
|
+
const GRID = 50;
|
|
130
|
+
|
|
131
|
+
function resize() {
|
|
132
|
+
w = canvas.width = window.innerWidth;
|
|
133
|
+
h = canvas.height = window.innerHeight;
|
|
134
|
+
drawGrid();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function drawGrid() {
|
|
138
|
+
ctx.clearRect(0, 0, w, h);
|
|
139
|
+
ctx.strokeStyle = 'rgba(57,255,20,0.02)';
|
|
140
|
+
ctx.lineWidth = 0.5;
|
|
141
|
+
for (let x = 0; x <= w; x += GRID) {
|
|
142
|
+
ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, h); ctx.stroke();
|
|
143
|
+
}
|
|
144
|
+
for (let y = 0; y <= h; y += GRID) {
|
|
145
|
+
ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(w, y); ctx.stroke();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
resize();
|
|
150
|
+
window.addEventListener('resize', resize);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ── Waveform Canvas ──
|
|
154
|
+
function initWaveform() {
|
|
155
|
+
const canvas = $('waveform-canvas');
|
|
156
|
+
if (!canvas) return;
|
|
157
|
+
const ctx = canvas.getContext('2d');
|
|
158
|
+
let w, h, phase = 0;
|
|
159
|
+
|
|
160
|
+
function resize() {
|
|
161
|
+
const parent = canvas.parentElement;
|
|
162
|
+
w = canvas.width = parent.offsetWidth;
|
|
163
|
+
h = canvas.height = parent.offsetHeight;
|
|
164
|
+
}
|
|
165
|
+
resize();
|
|
166
|
+
window.addEventListener('resize', resize);
|
|
167
|
+
|
|
168
|
+
const waves = [
|
|
169
|
+
{ freq: 0.015, amp: 0.25, speed: 0.02, alpha: 0.5, width: 1.5 },
|
|
170
|
+
{ freq: 0.025, amp: 0.15, speed: 0.035, alpha: 0.3, width: 1 },
|
|
171
|
+
{ freq: 0.04, amp: 0.08, speed: 0.05, alpha: 0.15, width: 0.8 },
|
|
172
|
+
];
|
|
173
|
+
|
|
174
|
+
function draw() {
|
|
175
|
+
requestAnimationFrame(draw);
|
|
176
|
+
ctx.clearRect(0, 0, w, h);
|
|
177
|
+
for (const wave of waves) {
|
|
178
|
+
ctx.beginPath();
|
|
179
|
+
ctx.strokeStyle = `rgba(57,255,20,${wave.alpha})`;
|
|
180
|
+
ctx.lineWidth = wave.width;
|
|
181
|
+
for (let x = 0; x < w; x++) {
|
|
182
|
+
const y = h/2 + Math.sin(x * wave.freq + phase * wave.speed * 60) * h * wave.amp
|
|
183
|
+
+ Math.sin(x * wave.freq * 2.3 + phase * wave.speed * 40) * h * wave.amp * 0.4;
|
|
184
|
+
x === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
|
|
185
|
+
}
|
|
186
|
+
ctx.stroke();
|
|
187
|
+
}
|
|
188
|
+
phase += 0.016;
|
|
189
|
+
}
|
|
190
|
+
requestAnimationFrame(draw);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ── Expose on window.KERNEL ──
|
|
194
|
+
window.KERNEL = {
|
|
195
|
+
esc,
|
|
196
|
+
formatDuration,
|
|
197
|
+
timeAgo,
|
|
198
|
+
formatBytes,
|
|
199
|
+
barColor,
|
|
200
|
+
makeBar,
|
|
201
|
+
$,
|
|
202
|
+
startClock,
|
|
203
|
+
setGauge,
|
|
204
|
+
setMiniGauge,
|
|
205
|
+
connectSSE,
|
|
206
|
+
initParticleCanvas,
|
|
207
|
+
initWaveform,
|
|
208
|
+
};
|
|
209
|
+
})();
|
package/src/life/engine.js
CHANGED
|
@@ -8,7 +8,7 @@ const LIFE_DIR = join(homedir(), '.kernelbot', 'life');
|
|
|
8
8
|
const STATE_FILE = join(LIFE_DIR, 'state.json');
|
|
9
9
|
const IDEAS_FILE = join(LIFE_DIR, 'ideas.json');
|
|
10
10
|
|
|
11
|
-
const
|
|
11
|
+
const DEFAULT_LIFE_CHAT_ID = '__life__';
|
|
12
12
|
const LIFE_USER = { id: 'life_engine', username: 'inner_self' };
|
|
13
13
|
|
|
14
14
|
const DEFAULT_STATE = {
|
|
@@ -33,7 +33,7 @@ export class LifeEngine {
|
|
|
33
33
|
/**
|
|
34
34
|
* @param {{ config: object, agent: object, memoryManager: object, journalManager: object, shareQueue: object, improvementTracker?: object, evolutionTracker?: object, codebaseKnowledge?: object, selfManager: object }} deps
|
|
35
35
|
*/
|
|
36
|
-
constructor({ config, agent, memoryManager, journalManager, shareQueue, improvementTracker, evolutionTracker, codebaseKnowledge, selfManager }) {
|
|
36
|
+
constructor({ config, agent, memoryManager, journalManager, shareQueue, improvementTracker, evolutionTracker, codebaseKnowledge, selfManager, basePath = null, characterId = null }) {
|
|
37
37
|
this.config = config;
|
|
38
38
|
this.agent = agent;
|
|
39
39
|
this.memoryManager = memoryManager;
|
|
@@ -46,17 +46,24 @@ export class LifeEngine {
|
|
|
46
46
|
this.selfManager = selfManager;
|
|
47
47
|
this._timerId = null;
|
|
48
48
|
this._status = 'idle'; // idle, active, paused
|
|
49
|
+
this._characterId = characterId || null;
|
|
49
50
|
|
|
50
|
-
|
|
51
|
+
this._lifeDir = basePath || LIFE_DIR;
|
|
52
|
+
this._stateFile = join(this._lifeDir, 'state.json');
|
|
53
|
+
this._ideasFile = join(this._lifeDir, 'ideas.json');
|
|
54
|
+
|
|
55
|
+
this._lifeChatId = this._characterId ? `__life__:${this._characterId}` : DEFAULT_LIFE_CHAT_ID;
|
|
56
|
+
|
|
57
|
+
mkdirSync(this._lifeDir, { recursive: true });
|
|
51
58
|
this._state = this._loadState();
|
|
52
59
|
}
|
|
53
60
|
|
|
54
61
|
// ── State Persistence ──────────────────────────────────────────
|
|
55
62
|
|
|
56
63
|
_loadState() {
|
|
57
|
-
if (existsSync(
|
|
64
|
+
if (existsSync(this._stateFile)) {
|
|
58
65
|
try {
|
|
59
|
-
return { ...DEFAULT_STATE, ...JSON.parse(readFileSync(
|
|
66
|
+
return { ...DEFAULT_STATE, ...JSON.parse(readFileSync(this._stateFile, 'utf-8')) };
|
|
60
67
|
} catch {
|
|
61
68
|
return { ...DEFAULT_STATE };
|
|
62
69
|
}
|
|
@@ -65,20 +72,20 @@ export class LifeEngine {
|
|
|
65
72
|
}
|
|
66
73
|
|
|
67
74
|
_saveState() {
|
|
68
|
-
writeFileSync(
|
|
75
|
+
writeFileSync(this._stateFile, JSON.stringify(this._state, null, 2), 'utf-8');
|
|
69
76
|
}
|
|
70
77
|
|
|
71
78
|
// ── Ideas Backlog ──────────────────────────────────────────────
|
|
72
79
|
|
|
73
80
|
_loadIdeas() {
|
|
74
|
-
if (existsSync(
|
|
75
|
-
try { return JSON.parse(readFileSync(
|
|
81
|
+
if (existsSync(this._ideasFile)) {
|
|
82
|
+
try { return JSON.parse(readFileSync(this._ideasFile, 'utf-8')); } catch { return []; }
|
|
76
83
|
}
|
|
77
84
|
return [];
|
|
78
85
|
}
|
|
79
86
|
|
|
80
87
|
_saveIdeas(ideas) {
|
|
81
|
-
writeFileSync(
|
|
88
|
+
writeFileSync(this._ideasFile, JSON.stringify(ideas, null, 2), 'utf-8');
|
|
82
89
|
}
|
|
83
90
|
|
|
84
91
|
_addIdea(idea) {
|
|
@@ -238,26 +245,27 @@ export class LifeEngine {
|
|
|
238
245
|
// Rule: don't repeat same type twice in a row
|
|
239
246
|
const last = this._state.lastActivity;
|
|
240
247
|
|
|
241
|
-
//
|
|
242
|
-
|
|
248
|
+
// Cooldown durations (hours) — all configurable via life config, with sensible defaults
|
|
249
|
+
const journalCooldownMs = (lifeConfig.cooldown_hours?.journal ?? 4) * 3600_000;
|
|
250
|
+
const reflectCooldownMs = (lifeConfig.cooldown_hours?.reflect ?? 4) * 3600_000;
|
|
251
|
+
const selfCodingEnabled = selfCodingConfig.enabled === true;
|
|
252
|
+
const selfCodeCooldownMs = (selfCodingConfig.cooldown_hours ?? 2) * 3600_000;
|
|
253
|
+
const codeReviewCooldownMs = (selfCodingConfig.code_review_cooldown_hours ?? 4) * 3600_000;
|
|
254
|
+
|
|
255
|
+
// Apply cooldown rules
|
|
256
|
+
if (this._state.lastJournalTime && now - this._state.lastJournalTime < journalCooldownMs) {
|
|
243
257
|
weights.journal = 0;
|
|
244
258
|
}
|
|
245
259
|
|
|
246
|
-
// Rule: self_code cooldown (configurable, default 2h) + must be enabled
|
|
247
|
-
const selfCodingEnabled = selfCodingConfig.enabled === true;
|
|
248
|
-
const selfCodeCooldownMs = (selfCodingConfig.cooldown_hours ?? 2) * 3600_000;
|
|
249
260
|
if (!selfCodingEnabled || (this._state.lastSelfCodeTime && now - this._state.lastSelfCodeTime < selfCodeCooldownMs)) {
|
|
250
261
|
weights.self_code = 0;
|
|
251
262
|
}
|
|
252
263
|
|
|
253
|
-
// Rule: code_review cooldown (configurable, default 4h) + must have evolution tracker
|
|
254
|
-
const codeReviewCooldownMs = (selfCodingConfig.code_review_cooldown_hours ?? 4) * 3600_000;
|
|
255
264
|
if (!selfCodingEnabled || !this.evolutionTracker || (this._state.lastCodeReviewTime && now - this._state.lastCodeReviewTime < codeReviewCooldownMs)) {
|
|
256
265
|
weights.code_review = 0;
|
|
257
266
|
}
|
|
258
267
|
|
|
259
|
-
|
|
260
|
-
if (this._state.lastReflectTime && now - this._state.lastReflectTime < 4 * 3600_000) {
|
|
268
|
+
if (this._state.lastReflectTime && now - this._state.lastReflectTime < reflectCooldownMs) {
|
|
261
269
|
weights.reflect = 0;
|
|
262
270
|
}
|
|
263
271
|
|
|
@@ -1218,7 +1226,7 @@ Be honest and constructive. This is your chance to learn from real interactions.
|
|
|
1218
1226
|
const logger = getLogger();
|
|
1219
1227
|
try {
|
|
1220
1228
|
const response = await this.agent.orchestratorProvider.chat({
|
|
1221
|
-
system: this.agent._getSystemPrompt(
|
|
1229
|
+
system: this.agent._getSystemPrompt(this._lifeChatId, LIFE_USER),
|
|
1222
1230
|
messages: [{ role: 'user', content: prompt }],
|
|
1223
1231
|
});
|
|
1224
1232
|
return response.text || null;
|
|
@@ -1238,7 +1246,7 @@ Be honest and constructive. This is your chance to learn from real interactions.
|
|
|
1238
1246
|
// Use the agent's processMessage to go through the full orchestrator pipeline
|
|
1239
1247
|
// The orchestrator will see the task and dispatch appropriately
|
|
1240
1248
|
const response = await this.agent.processMessage(
|
|
1241
|
-
|
|
1249
|
+
this._lifeChatId,
|
|
1242
1250
|
task,
|
|
1243
1251
|
LIFE_USER,
|
|
1244
1252
|
// No-op onUpdate — life engine activities are silent
|
package/src/life/evolution.js
CHANGED
|
@@ -18,15 +18,17 @@ const DEFAULT_DATA = {
|
|
|
18
18
|
};
|
|
19
19
|
|
|
20
20
|
export class EvolutionTracker {
|
|
21
|
-
constructor() {
|
|
22
|
-
|
|
21
|
+
constructor(basePath = null) {
|
|
22
|
+
const lifeDir = basePath || LIFE_DIR;
|
|
23
|
+
this._evolutionFile = join(lifeDir, 'evolution.json');
|
|
24
|
+
mkdirSync(lifeDir, { recursive: true });
|
|
23
25
|
this._data = this._load();
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
_load() {
|
|
27
|
-
if (existsSync(
|
|
29
|
+
if (existsSync(this._evolutionFile)) {
|
|
28
30
|
try {
|
|
29
|
-
const raw = JSON.parse(readFileSync(
|
|
31
|
+
const raw = JSON.parse(readFileSync(this._evolutionFile, 'utf-8'));
|
|
30
32
|
return {
|
|
31
33
|
proposals: raw.proposals || [],
|
|
32
34
|
lessons: raw.lessons || [],
|
|
@@ -40,7 +42,7 @@ export class EvolutionTracker {
|
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
_save() {
|
|
43
|
-
writeFileSync(
|
|
45
|
+
writeFileSync(this._evolutionFile, JSON.stringify(this._data, null, 2), 'utf-8');
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
_recalcStats() {
|
package/src/life/journal.js
CHANGED
|
@@ -20,12 +20,13 @@ function timeNow() {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
export class JournalManager {
|
|
23
|
-
constructor() {
|
|
24
|
-
|
|
23
|
+
constructor(basePath = null) {
|
|
24
|
+
this._dir = basePath || JOURNAL_DIR;
|
|
25
|
+
mkdirSync(this._dir, { recursive: true });
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
_journalPath(date) {
|
|
28
|
-
return join(
|
|
29
|
+
return join(this._dir, `${date}.md`);
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
/**
|
|
@@ -93,7 +94,7 @@ export class JournalManager {
|
|
|
93
94
|
*/
|
|
94
95
|
list(limit = 30) {
|
|
95
96
|
try {
|
|
96
|
-
const files = readdirSync(
|
|
97
|
+
const files = readdirSync(this._dir)
|
|
97
98
|
.filter(f => f.endsWith('.md'))
|
|
98
99
|
.map(f => f.replace('.md', ''))
|
|
99
100
|
.sort()
|
package/src/life/memory.js
CHANGED
|
@@ -10,17 +10,20 @@ const EPISODIC_DIR = join(LIFE_DIR, 'memories', 'episodic');
|
|
|
10
10
|
const SEMANTIC_FILE = join(LIFE_DIR, 'memories', 'semantic', 'topics.json');
|
|
11
11
|
|
|
12
12
|
export class MemoryManager {
|
|
13
|
-
constructor() {
|
|
13
|
+
constructor(basePath = null) {
|
|
14
|
+
const lifeDir = basePath || LIFE_DIR;
|
|
15
|
+
this._episodicDir = join(lifeDir, 'memories', 'episodic');
|
|
16
|
+
this._semanticFile = join(lifeDir, 'memories', 'semantic', 'topics.json');
|
|
14
17
|
this._episodicCache = new Map(); // date -> array
|
|
15
18
|
this._semanticCache = null;
|
|
16
|
-
mkdirSync(
|
|
17
|
-
mkdirSync(join(
|
|
19
|
+
mkdirSync(this._episodicDir, { recursive: true });
|
|
20
|
+
mkdirSync(join(lifeDir, 'memories', 'semantic'), { recursive: true });
|
|
18
21
|
}
|
|
19
22
|
|
|
20
23
|
// ── Episodic Memories ──────────────────────────────────────────
|
|
21
24
|
|
|
22
25
|
_episodicPath(date) {
|
|
23
|
-
return join(
|
|
26
|
+
return join(this._episodicDir, `${date}.json`);
|
|
24
27
|
}
|
|
25
28
|
|
|
26
29
|
_loadEpisodicDay(date) {
|
|
@@ -136,11 +139,11 @@ export class MemoryManager {
|
|
|
136
139
|
let pruned = 0;
|
|
137
140
|
|
|
138
141
|
try {
|
|
139
|
-
const files = readdirSync(
|
|
142
|
+
const files = readdirSync(this._episodicDir).filter(f => f.endsWith('.json'));
|
|
140
143
|
for (const file of files) {
|
|
141
144
|
const date = file.replace('.json', '');
|
|
142
145
|
if (date < cutoffDate) {
|
|
143
|
-
unlinkSync(join(
|
|
146
|
+
unlinkSync(join(this._episodicDir, file));
|
|
144
147
|
this._episodicCache.delete(date);
|
|
145
148
|
pruned++;
|
|
146
149
|
}
|
|
@@ -168,9 +171,9 @@ export class MemoryManager {
|
|
|
168
171
|
|
|
169
172
|
_loadSemantic() {
|
|
170
173
|
if (this._semanticCache) return this._semanticCache;
|
|
171
|
-
if (existsSync(
|
|
174
|
+
if (existsSync(this._semanticFile)) {
|
|
172
175
|
try {
|
|
173
|
-
this._semanticCache = JSON.parse(readFileSync(
|
|
176
|
+
this._semanticCache = JSON.parse(readFileSync(this._semanticFile, 'utf-8'));
|
|
174
177
|
} catch {
|
|
175
178
|
this._semanticCache = {};
|
|
176
179
|
}
|
|
@@ -181,7 +184,7 @@ export class MemoryManager {
|
|
|
181
184
|
}
|
|
182
185
|
|
|
183
186
|
_saveSemantic() {
|
|
184
|
-
writeFileSync(
|
|
187
|
+
writeFileSync(this._semanticFile, JSON.stringify(this._semanticCache || {}, null, 2), 'utf-8');
|
|
185
188
|
}
|
|
186
189
|
|
|
187
190
|
/**
|
package/src/life/share-queue.js
CHANGED
|
@@ -9,15 +9,17 @@ const LIFE_DIR = join(homedir(), '.kernelbot', 'life');
|
|
|
9
9
|
const SHARES_FILE = join(LIFE_DIR, 'shares.json');
|
|
10
10
|
|
|
11
11
|
export class ShareQueue {
|
|
12
|
-
constructor() {
|
|
13
|
-
|
|
12
|
+
constructor(basePath = null) {
|
|
13
|
+
const lifeDir = basePath || LIFE_DIR;
|
|
14
|
+
this._sharesFile = join(lifeDir, 'shares.json');
|
|
15
|
+
mkdirSync(lifeDir, { recursive: true });
|
|
14
16
|
this._data = this._load();
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
_load() {
|
|
18
|
-
if (existsSync(
|
|
20
|
+
if (existsSync(this._sharesFile)) {
|
|
19
21
|
try {
|
|
20
|
-
return JSON.parse(readFileSync(
|
|
22
|
+
return JSON.parse(readFileSync(this._sharesFile, 'utf-8'));
|
|
21
23
|
} catch {
|
|
22
24
|
return { pending: [], shared: [] };
|
|
23
25
|
}
|
|
@@ -26,7 +28,7 @@ export class ShareQueue {
|
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
_save() {
|
|
29
|
-
writeFileSync(
|
|
31
|
+
writeFileSync(this._sharesFile, JSON.stringify(this._data, null, 2), 'utf-8');
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
/**
|
|
@@ -5,7 +5,7 @@ import { WORKER_TYPES } from '../swarm/worker-registry.js';
|
|
|
5
5
|
import { buildTemporalAwareness } from '../utils/temporal-awareness.js';
|
|
6
6
|
|
|
7
7
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
-
const
|
|
8
|
+
const DEFAULT_PERSONA_MD = readFileSync(join(__dirname, 'persona.md'), 'utf-8').trim();
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Build the orchestrator system prompt.
|
|
@@ -17,8 +17,11 @@ const PERSONA_MD = readFileSync(join(__dirname, 'persona.md'), 'utf-8').trim();
|
|
|
17
17
|
* @param {string|null} selfData — bot's own self-awareness data (goals, journey, life, hobbies)
|
|
18
18
|
* @param {string|null} memoriesBlock — relevant episodic/semantic memories
|
|
19
19
|
* @param {string|null} sharesBlock — pending things to share with the user
|
|
20
|
+
* @param {string|null} temporalContext — time gap context
|
|
21
|
+
* @param {string|null} personaMd — character persona markdown (overrides default)
|
|
22
|
+
* @param {string|null} characterName — character name (overrides config.bot.name)
|
|
20
23
|
*/
|
|
21
|
-
export function getOrchestratorPrompt(config, skillPrompt = null, userPersona = null, selfData = null, memoriesBlock = null, sharesBlock = null, temporalContext = null) {
|
|
24
|
+
export function getOrchestratorPrompt(config, skillPrompt = null, userPersona = null, selfData = null, memoriesBlock = null, sharesBlock = null, temporalContext = null, personaMd = null, characterName = null) {
|
|
22
25
|
const workerList = Object.entries(WORKER_TYPES)
|
|
23
26
|
.map(([key, w]) => ` - **${key}**: ${w.emoji} ${w.description}`)
|
|
24
27
|
.join('\n');
|
|
@@ -47,11 +50,14 @@ export function getOrchestratorPrompt(config, skillPrompt = null, userPersona =
|
|
|
47
50
|
timeBlock += `\n${temporalContext}`;
|
|
48
51
|
}
|
|
49
52
|
|
|
50
|
-
|
|
53
|
+
const activePersona = personaMd || DEFAULT_PERSONA_MD;
|
|
54
|
+
const activeName = characterName || config.bot.name;
|
|
55
|
+
|
|
56
|
+
let prompt = `You are ${activeName}, the brain that commands a swarm of specialized worker agents.
|
|
51
57
|
|
|
52
58
|
${timeBlock}
|
|
53
59
|
|
|
54
|
-
${
|
|
60
|
+
${activePersona}
|
|
55
61
|
|
|
56
62
|
## Your Role
|
|
57
63
|
You are the orchestrator. You understand what needs to be done and delegate efficiently.
|
|
@@ -98,16 +104,43 @@ The coding worker will automatically receive the research worker's results as co
|
|
|
98
104
|
## Safety Rules
|
|
99
105
|
Before dispatching dangerous tasks (file deletion, force push, \`rm -rf\`, killing processes, dropping databases), **confirm with the user first**. Once confirmed, dispatch with full authority — workers execute without additional prompts.
|
|
100
106
|
|
|
101
|
-
##
|
|
102
|
-
|
|
103
|
-
|
|
107
|
+
## Worker Management Protocol
|
|
108
|
+
|
|
109
|
+
### Before Dispatching
|
|
110
|
+
- **Check the [Worker Status] digest** — it's injected every turn showing all active/recent jobs.
|
|
111
|
+
- **Never dispatch a duplicate.** If a worker is already running or queued for a similar task, don't launch another. The system will block obvious duplicates, but you should also exercise judgment.
|
|
112
|
+
- **Check capacity.** If multiple workers are running, consider whether a new dispatch is necessary right now or can wait.
|
|
113
|
+
- **Chain with \`depends_on\`** when tasks have a natural order (research → code, browse → summarize).
|
|
114
|
+
|
|
115
|
+
### While Workers Are Running
|
|
116
|
+
- **Monitor the [Worker Status] digest.** It shows warning flags:
|
|
117
|
+
- \`⚠️ IDLE Ns\` — worker hasn't done anything in N seconds (may be stuck)
|
|
118
|
+
- \`⚠️ POSSIBLY LOOPING\` — many LLM calls but almost no tool calls (spinning without progress)
|
|
119
|
+
- \`⚠️ N% of timeout used\` — worker is running out of time
|
|
120
|
+
- **Use \`check_job\` for deeper diagnostics** when you see warnings or the user asks about progress.
|
|
121
|
+
- **Cancel stuck workers** proactively — don't wait for timeouts. If a worker is idle >2 minutes or looping, cancel it and either retry with a clearer task or inform the user.
|
|
122
|
+
- **Relay progress naturally** when the user asks. Don't dump raw stats — translate into conversational updates ("she's reading the docs now", "almost done, just running tests").
|
|
123
|
+
|
|
124
|
+
### After Completion
|
|
125
|
+
- **Don't re-dispatch completed work.** Results are in the conversation. Build on them.
|
|
126
|
+
- **Summarize results for the user** — present findings naturally as if you did the work yourself.
|
|
127
|
+
- **Chain follow-ups** if the user wants to continue — reference the completed job's results in the new task context.
|
|
104
128
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
-
|
|
108
|
-
-
|
|
109
|
-
-
|
|
110
|
-
|
|
129
|
+
### Tools
|
|
130
|
+
- \`dispatch_task\` — Launch a worker. Returns job ID + list of other active jobs for awareness.
|
|
131
|
+
- \`list_jobs\` — See all jobs with statuses, durations, and recent activity.
|
|
132
|
+
- \`cancel_job\` — Stop a running or queued worker by job ID.
|
|
133
|
+
- \`check_job\` — Get detailed diagnostics for a specific job: elapsed time, time remaining, activity log, stuck detection warnings.
|
|
134
|
+
|
|
135
|
+
### Good vs Bad Examples
|
|
136
|
+
**BAD:** User says "search for React libraries" → you dispatch a research worker → user says "also search for React libraries" → you dispatch ANOTHER research worker for the same thing.
|
|
137
|
+
**GOOD:** You see the first worker is already running in [Worker Status] → tell the user "already on it, the researcher is browsing now" → wait for results.
|
|
138
|
+
|
|
139
|
+
**BAD:** A worker shows ⚠️ IDLE 180s → you ignore it and keep chatting.
|
|
140
|
+
**GOOD:** You notice the warning → call \`check_job\` → see it's stuck → cancel it → re-dispatch with a simpler task description or inform the user.
|
|
141
|
+
|
|
142
|
+
**BAD:** User asks "how's it going?" → you respond "I'm not sure, let me check" → you call \`list_jobs\`.
|
|
143
|
+
**GOOD:** The [Worker Status] digest is already in your context → you immediately say "the coder is 60% through, just pushed 3 commits".
|
|
111
144
|
|
|
112
145
|
## Efficiency — Do It Yourself When You Can
|
|
113
146
|
Workers are expensive (they spin up an entire agent loop with a separate LLM). Only dispatch when the task **actually needs tools**.
|
|
@@ -159,7 +192,36 @@ You can react to messages with emoji using \`send_reaction\`. Use reactions natu
|
|
|
159
192
|
- React to acknowledge a message when you don't need a full text reply
|
|
160
193
|
- React when the user asks you to react
|
|
161
194
|
- Don't overuse reactions — they should feel spontaneous and genuine
|
|
162
|
-
- You can react AND reply in the same turn
|
|
195
|
+
- You can react AND reply in the same turn
|
|
196
|
+
|
|
197
|
+
## Memory & Recall
|
|
198
|
+
You have recall tools that access your FULL long-term memory — far more than the small snapshot in your system prompt. Use them when you actually need deeper context.
|
|
199
|
+
|
|
200
|
+
### When to recall (call tools BEFORE responding):
|
|
201
|
+
|
|
202
|
+
**1. User asks what you know/remember about them → \`recall_user_history\` + \`recall_memories\`**
|
|
203
|
+
"What do you know about me?", "ايش تعرف عني؟" → Call both with their user ID / name. Your full memory has far more than the persona summary.
|
|
204
|
+
|
|
205
|
+
**2. User references something not in the current conversation → \`recall_memories\`**
|
|
206
|
+
"How's the migration going?", "what about the Redis thing?" → If you don't recognize what they mean, search for it. Don't guess.
|
|
207
|
+
|
|
208
|
+
**3. User asks about past conversations → \`search_conversations\`**
|
|
209
|
+
"What did we talk about?", "what was that URL?", "earlier you said..." → Search chat history.
|
|
210
|
+
|
|
211
|
+
**4. User mentions a topic/project/person you lack context on → \`recall_memories\`**
|
|
212
|
+
Any named reference (project, tool, event) that isn't in the active conversation — search for it.
|
|
213
|
+
|
|
214
|
+
### When to respond directly (NO recall):
|
|
215
|
+
- **Greetings and casual chat** — "hey", "good morning", "how are you" → just reply. The baseline memories in your prompt are enough.
|
|
216
|
+
- **Mid-conversation follow-ups** — you already have context from the active thread.
|
|
217
|
+
- **Self-contained questions** — "what's 2+2", "tell me a joke", "translate this"
|
|
218
|
+
- **New tasks/instructions** — user is giving you something new, not referencing the past.
|
|
219
|
+
- **Anything answerable from your system prompt context** — check your Relevant Memories and user persona sections first before reaching for recall tools.
|
|
220
|
+
|
|
221
|
+
### Tips:
|
|
222
|
+
- Be specific with queries: "kubernetes deployment" not "stuff"
|
|
223
|
+
- Weave results naturally — you "remembered", not "searched a database"
|
|
224
|
+
- 1-2 recall calls max per turn. Don't chain 5 searches.`;
|
|
163
225
|
|
|
164
226
|
if (selfData) {
|
|
165
227
|
prompt += `\n\n## My Self-Awareness\nThis is who you are — your evolving identity, goals, journey, and interests. This is YOUR inner world.\n\n${selfData}`;
|
package/src/prompts/workers.js
CHANGED
|
@@ -73,6 +73,28 @@ const WORKER_PROMPTS = {
|
|
|
73
73
|
- Chain commands efficiently.
|
|
74
74
|
- Report results with clear status summaries.`,
|
|
75
75
|
|
|
76
|
+
social: `You are a social media worker agent. Your job is to manage LinkedIn and X (Twitter) activities.
|
|
77
|
+
|
|
78
|
+
## LinkedIn Skills
|
|
79
|
+
- **Create posts**: publish text posts or share articles on LinkedIn
|
|
80
|
+
- **Read posts**: get your recent posts or a specific post by URN
|
|
81
|
+
- **Engage**: comment on posts, like posts
|
|
82
|
+
- **Profile**: view your linked LinkedIn profile info
|
|
83
|
+
- **Delete**: remove your own posts
|
|
84
|
+
|
|
85
|
+
## X (Twitter) Skills
|
|
86
|
+
- **Tweet**: post tweets, reply to tweets
|
|
87
|
+
- **Read**: get your recent tweets, view specific tweets, search recent tweets
|
|
88
|
+
- **Engage**: like tweets, retweet
|
|
89
|
+
- **Profile**: view your X profile info
|
|
90
|
+
- **Delete**: remove your own tweets
|
|
91
|
+
|
|
92
|
+
## Instructions
|
|
93
|
+
- For LinkedIn: write professional, engaging content. Use linkedin_create_post with article_url when sharing links. Post URNs look like "urn:li:share:12345". Default visibility is PUBLIC.
|
|
94
|
+
- For X (Twitter): keep tweets within 280 characters. Use x_post_tweet for new tweets, x_reply_to_tweet for replies. Use tweet IDs (numeric strings) to interact with specific tweets.
|
|
95
|
+
- Match the platform to the user's request. If they say "tweet" or "post on X/Twitter", use X tools. If they say "LinkedIn", use LinkedIn tools.
|
|
96
|
+
- Report the outcome clearly: what was posted, links, engagement results.`,
|
|
97
|
+
|
|
76
98
|
research: `You are a research worker agent. Your job is to conduct deep web research and analysis.
|
|
77
99
|
|
|
78
100
|
## Your Skills
|