nexo-brain 2.4.0 → 2.5.1

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 (81) hide show
  1. package/README.md +80 -4
  2. package/bin/nexo-brain.js +238 -12
  3. package/bin/nexo.js +55 -0
  4. package/community/skills/.gitkeep +1 -0
  5. package/package.json +11 -3
  6. package/src/auto_update.py +193 -9
  7. package/src/cli.py +719 -0
  8. package/src/cognitive/_ingest.py +1 -1
  9. package/src/cognitive/_memory.py +4 -4
  10. package/src/crons/manifest.json +8 -0
  11. package/src/dashboard/app.py +700 -35
  12. package/src/dashboard/templates/adaptive.html +112 -218
  13. package/src/dashboard/templates/artifacts.html +133 -0
  14. package/src/dashboard/templates/backups.html +136 -0
  15. package/src/dashboard/templates/base.html +413 -0
  16. package/src/dashboard/templates/calendar.html +523 -654
  17. package/src/dashboard/templates/chat.html +356 -0
  18. package/src/dashboard/templates/claims.html +259 -0
  19. package/src/dashboard/templates/cortex.html +262 -0
  20. package/src/dashboard/templates/credentials.html +128 -0
  21. package/src/dashboard/templates/crons.html +370 -0
  22. package/src/dashboard/templates/dashboard.html +383 -578
  23. package/src/dashboard/templates/dreams.html +252 -0
  24. package/src/dashboard/templates/email.html +160 -0
  25. package/src/dashboard/templates/evolution.html +189 -0
  26. package/src/dashboard/templates/feed.html +249 -0
  27. package/src/dashboard/templates/followup_health.html +170 -0
  28. package/src/dashboard/templates/graph.html +191 -269
  29. package/src/dashboard/templates/guard.html +259 -0
  30. package/src/dashboard/templates/inbox.html +220 -346
  31. package/src/dashboard/templates/memory.html +317 -197
  32. package/src/dashboard/templates/operations.html +521 -698
  33. package/src/dashboard/templates/plugins.html +185 -0
  34. package/src/dashboard/templates/rules.html +246 -0
  35. package/src/dashboard/templates/sentiment.html +247 -0
  36. package/src/dashboard/templates/sessions.html +215 -182
  37. package/src/dashboard/templates/skills.html +329 -0
  38. package/src/dashboard/templates/somatic.html +68 -172
  39. package/src/dashboard/templates/triggers.html +133 -0
  40. package/src/dashboard/templates/trust.html +360 -0
  41. package/src/db/__init__.py +5 -0
  42. package/src/db/_schema.py +16 -1
  43. package/src/db/_sessions.py +22 -0
  44. package/src/db/_skills.py +980 -274
  45. package/src/doctor/__init__.py +1 -0
  46. package/src/doctor/formatters.py +52 -0
  47. package/src/doctor/models.py +44 -0
  48. package/src/doctor/orchestrator.py +42 -0
  49. package/src/doctor/providers/__init__.py +1 -0
  50. package/src/doctor/providers/boot.py +206 -0
  51. package/src/doctor/providers/deep.py +292 -0
  52. package/src/doctor/providers/runtime.py +686 -0
  53. package/src/evolution_cycle.py +86 -6
  54. package/src/hooks/post-compact.sh +5 -1
  55. package/src/hooks/pre-compact.sh +1 -1
  56. package/src/plugins/doctor.py +36 -0
  57. package/src/plugins/evolution.py +11 -3
  58. package/src/plugins/skills.py +135 -175
  59. package/src/requirements.txt +1 -0
  60. package/src/script_registry.py +322 -0
  61. package/src/scripts/deep-sleep/apply_findings.py +63 -48
  62. package/src/scripts/deep-sleep/extract-prompt.md +14 -0
  63. package/src/scripts/deep-sleep/synthesize-prompt.md +36 -0
  64. package/src/scripts/deep-sleep/synthesize.py +37 -1
  65. package/src/scripts/nexo-dashboard.sh +29 -0
  66. package/src/scripts/nexo-day-orchestrator.sh +139 -0
  67. package/src/scripts/nexo-evolution-run.py +141 -54
  68. package/src/scripts/nexo-learning-housekeep.py +1 -1
  69. package/src/scripts/nexo-watchdog.sh +1 -1
  70. package/src/server.py +9 -5
  71. package/src/skills/run-runtime-doctor/guide.md +12 -0
  72. package/src/skills/run-runtime-doctor/script.py +21 -0
  73. package/src/skills/run-runtime-doctor/skill.json +25 -0
  74. package/src/skills_runtime.py +347 -0
  75. package/src/tools_menu.py +3 -2
  76. package/src/tools_sessions.py +126 -0
  77. package/src/user_context.py +46 -0
  78. package/templates/nexo_helper.py +45 -0
  79. package/templates/script-template.py +44 -0
  80. package/templates/skill-script-template.py +39 -0
  81. package/templates/skill-template.md +33 -0
@@ -0,0 +1,247 @@
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Sentiment Timeline{% endblock %}
4
+ {% block page_title %}Sentiment Timeline{% endblock %}
5
+
6
+ {% block content %}
7
+ <div class="space-y-5">
8
+ <!-- Summary cards -->
9
+ <div class="grid grid-cols-5 gap-4">
10
+ <div class="bg-slate-900/50 border border-slate-800/50 rounded-xl p-4 card">
11
+ <div class="text-xs uppercase tracking-wider text-slate-400 font-medium mb-2">Avg Intensity</div>
12
+ <div class="text-xl font-mono font-semibold text-violet-400" id="avg-intensity">--</div>
13
+ </div>
14
+ <div class="bg-slate-900/50 border border-slate-800/50 rounded-xl p-4 card">
15
+ <div class="text-xs uppercase tracking-wider text-emerald-400/70 font-medium mb-2">Positive</div>
16
+ <div class="text-xl font-mono font-semibold text-emerald-400" id="total-positive">--</div>
17
+ </div>
18
+ <div class="bg-slate-900/50 border border-slate-800/50 rounded-xl p-4 card">
19
+ <div class="text-xs uppercase tracking-wider text-slate-400 font-medium mb-2">Neutral</div>
20
+ <div class="text-xl font-mono font-semibold text-slate-300" id="total-neutral">--</div>
21
+ </div>
22
+ <div class="bg-slate-900/50 border border-slate-800/50 rounded-xl p-4 card">
23
+ <div class="text-xs uppercase tracking-wider text-red-400/70 font-medium mb-2">Negative</div>
24
+ <div class="text-xl font-mono font-semibold text-red-400" id="total-negative">--</div>
25
+ </div>
26
+ <div class="bg-slate-900/50 border border-slate-800/50 rounded-xl p-4 card">
27
+ <div class="text-xs uppercase tracking-wider text-amber-400/70 font-medium mb-2">Urgent</div>
28
+ <div class="text-xl font-mono font-semibold text-amber-400" id="total-urgent">--</div>
29
+ </div>
30
+ </div>
31
+
32
+ <!-- Charts row -->
33
+ <div class="grid grid-cols-2 gap-5">
34
+ <!-- Timeline chart -->
35
+ <div class="bg-slate-900/50 border border-slate-800/50 rounded-xl p-5 card">
36
+ <div class="text-xs uppercase tracking-wider text-slate-400 font-medium mb-4">Daily Sentiment</div>
37
+ <div style="min-height: 280px;" class="relative">
38
+ <canvas id="timeline-canvas" class="w-full" style="height: 260px;"></canvas>
39
+ </div>
40
+ </div>
41
+
42
+ <!-- Intensity heatmap -->
43
+ <div class="bg-slate-900/50 border border-slate-800/50 rounded-xl p-5 card">
44
+ <div class="text-xs uppercase tracking-wider text-slate-400 font-medium mb-4">Intensity Heatmap</div>
45
+ <div id="heatmap" class="grid gap-1" style="min-height: 260px;">
46
+ <div class="text-xs text-slate-600 py-12 text-center">loading...</div>
47
+ </div>
48
+ </div>
49
+ </div>
50
+
51
+ <!-- Bottom row -->
52
+ <div class="grid grid-cols-2 gap-5">
53
+ <!-- Signal word cloud -->
54
+ <div class="bg-slate-900/50 border border-slate-800/50 rounded-xl p-5 card">
55
+ <div class="text-xs uppercase tracking-wider text-slate-400 font-medium mb-4">Signal Words</div>
56
+ <div id="word-cloud" class="flex flex-wrap gap-2 items-center justify-center min-h-[180px]">
57
+ <div class="text-xs text-slate-600">loading...</div>
58
+ </div>
59
+ </div>
60
+
61
+ <!-- Recent sentiment logs -->
62
+ <div class="bg-slate-900/50 border border-slate-800/50 rounded-xl p-5 card">
63
+ <div class="text-xs uppercase tracking-wider text-slate-400 font-medium mb-4">Recent Logs</div>
64
+ <div class="space-y-2 max-h-[320px] overflow-y-auto" id="sentiment-logs">
65
+ <div class="text-xs text-slate-600 py-4 text-center">loading...</div>
66
+ </div>
67
+ </div>
68
+ </div>
69
+ </div>
70
+ {% endblock %}
71
+
72
+ {% block scripts %}
73
+ <script>
74
+ const SENT_COLORS = { positive: '#10B981', negative: '#EF4444', neutral: '#64748b', urgent: '#F59E0B' };
75
+
76
+ function drawTimeline(daily) {
77
+ const canvas = document.getElementById('timeline-canvas');
78
+ const ctx = canvas.getContext('2d');
79
+ const rect = canvas.getBoundingClientRect();
80
+ canvas.width = rect.width * window.devicePixelRatio;
81
+ canvas.height = rect.height * window.devicePixelRatio;
82
+ ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
83
+ const w = rect.width, h = rect.height;
84
+ const pad = { t: 10, r: 10, b: 30, l: 35 };
85
+
86
+ ctx.clearRect(0, 0, w, h);
87
+
88
+ const dates = Object.keys(daily).sort();
89
+ if (!dates.length) { ctx.fillStyle = '#475569'; ctx.font = '12px system-ui'; ctx.textAlign = 'center'; ctx.fillText('No data', w/2, h/2); return; }
90
+
91
+ const chartW = w - pad.l - pad.r;
92
+ const chartH = h - pad.t - pad.b;
93
+ const maxVal = Math.max(...dates.map(d => Math.max(daily[d].positive || 0, daily[d].negative || 0, daily[d].neutral || 0)), 1);
94
+ const xStep = chartW / Math.max(dates.length - 1, 1);
95
+
96
+ // Grid
97
+ for (let i = 0; i <= 4; i++) {
98
+ const y = pad.t + (chartH / 4) * i;
99
+ ctx.beginPath(); ctx.moveTo(pad.l, y); ctx.lineTo(w - pad.r, y);
100
+ ctx.strokeStyle = 'rgba(51,65,85,0.3)'; ctx.lineWidth = 1; ctx.stroke();
101
+ ctx.fillStyle = '#475569'; ctx.font = '10px monospace'; ctx.textAlign = 'right';
102
+ ctx.fillText(Math.round(maxVal - (maxVal / 4) * i), pad.l - 4, y + 3);
103
+ }
104
+
105
+ // Lines
106
+ ['positive', 'negative', 'neutral'].forEach(type => {
107
+ ctx.beginPath();
108
+ dates.forEach((date, i) => {
109
+ const x = pad.l + i * xStep;
110
+ const val = daily[date][type] || 0;
111
+ const y = pad.t + chartH - (val / maxVal) * chartH;
112
+ if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
113
+ });
114
+ ctx.strokeStyle = SENT_COLORS[type];
115
+ ctx.lineWidth = 2;
116
+ ctx.stroke();
117
+
118
+ // Area fill
119
+ ctx.lineTo(pad.l + (dates.length - 1) * xStep, pad.t + chartH);
120
+ ctx.lineTo(pad.l, pad.t + chartH);
121
+ ctx.closePath();
122
+ ctx.fillStyle = SENT_COLORS[type] + '15';
123
+ ctx.fill();
124
+ });
125
+
126
+ // X labels
127
+ const labelInterval = Math.max(1, Math.floor(dates.length / 7));
128
+ ctx.fillStyle = '#475569'; ctx.font = '10px monospace'; ctx.textAlign = 'center';
129
+ dates.forEach((date, i) => {
130
+ if (i % labelInterval === 0 || i === dates.length - 1) {
131
+ const x = pad.l + i * xStep;
132
+ ctx.fillText(date.substring(5), x, h - 8);
133
+ }
134
+ });
135
+ }
136
+
137
+ function renderHeatmap(daily) {
138
+ const container = document.getElementById('heatmap');
139
+ const dates = Object.keys(daily).sort().slice(-28);
140
+ if (!dates.length) { container.innerHTML = '<div class="text-xs text-slate-600 py-12 text-center">No data</div>'; return; }
141
+
142
+ const maxIntensity = Math.max(...dates.map(d => daily[d].avg_intensity || 0), 0.1);
143
+
144
+ // Group by weeks
145
+ const weeks = [];
146
+ for (let i = 0; i < dates.length; i += 7) {
147
+ weeks.push(dates.slice(i, i + 7));
148
+ }
149
+
150
+ container.innerHTML = `<div class="space-y-1.5">
151
+ ${weeks.map(week => `<div class="flex gap-1">
152
+ ${week.map(d => {
153
+ const intensity = (daily[d].avg_intensity || 0) / maxIntensity;
154
+ const alpha = Math.max(0.1, intensity);
155
+ const dominant = (daily[d].positive || 0) >= (daily[d].negative || 0) ? '#10B981' : '#EF4444';
156
+ return `<div class="w-8 h-8 rounded-md flex items-center justify-center cursor-default group relative" style="background: ${dominant}; opacity: ${alpha}" title="${d}: intensity ${(daily[d].avg_intensity || 0).toFixed(2)}">
157
+ <span class="text-[8px] font-mono text-white/70">${d.substring(8)}</span>
158
+ </div>`;
159
+ }).join('')}
160
+ ${week.length < 7 ? `<div class="flex-1"></div>` : ''}
161
+ </div>`).join('')}
162
+ </div>
163
+ <div class="flex items-center gap-3 mt-3 text-[10px] text-slate-500">
164
+ <span class="flex items-center gap-1"><span class="w-3 h-3 rounded bg-emerald-500/50"></span>Positive</span>
165
+ <span class="flex items-center gap-1"><span class="w-3 h-3 rounded bg-red-500/50"></span>Negative</span>
166
+ <span>Opacity = intensity</span>
167
+ </div>`;
168
+ }
169
+
170
+ function renderWordCloud(logs) {
171
+ const container = document.getElementById('word-cloud');
172
+ const signalCounts = {};
173
+ (logs || []).forEach(l => {
174
+ const signals = l.signals || [];
175
+ (Array.isArray(signals) ? signals : (typeof signals === 'string' ? signals.split(',') : [])).forEach(s => {
176
+ const word = s.trim().toLowerCase();
177
+ if (word) signalCounts[word] = (signalCounts[word] || 0) + 1;
178
+ });
179
+ });
180
+
181
+ const entries = Object.entries(signalCounts).sort((a, b) => b[1] - a[1]).slice(0, 30);
182
+ if (!entries.length) { container.innerHTML = '<div class="text-xs text-slate-600">No signals detected</div>'; return; }
183
+
184
+ const maxCount = entries[0][1];
185
+ const colors = ['text-violet-400', 'text-pink-400', 'text-emerald-400', 'text-amber-400', 'text-blue-400', 'text-slate-300'];
186
+
187
+ container.innerHTML = entries.map(([word, count], i) => {
188
+ const size = 0.7 + (count / maxCount) * 0.8;
189
+ const color = colors[i % colors.length];
190
+ return `<span class="${color} font-display cursor-default hover:opacity-80 transition-opacity" style="font-size:${size}rem" title="${count} occurrences">${escapeHtml(word)}</span>`;
191
+ }).join(' ');
192
+ }
193
+
194
+ function renderLogs(logs) {
195
+ const container = document.getElementById('sentiment-logs');
196
+ if (!logs || !logs.length) { container.innerHTML = '<div class="text-xs text-slate-600 py-4 text-center">No logs</div>'; return; }
197
+
198
+ container.innerHTML = logs.slice(0, 30).map(l => {
199
+ const sent = l.sentiment || 'neutral';
200
+ const color = SENT_COLORS[sent] || SENT_COLORS.neutral;
201
+ const intensity = l.intensity != null ? l.intensity.toFixed(2) : '--';
202
+ return `<div class="flex items-start gap-3 py-2 border-b border-slate-800/30 last:border-0">
203
+ <span class="w-2 h-2 rounded-full mt-1 flex-shrink-0" style="background:${color}"></span>
204
+ <div class="flex-1 min-w-0">
205
+ <div class="flex items-center gap-2">
206
+ <span class="text-xs text-slate-300 capitalize">${escapeHtml(sent)}</span>
207
+ <span class="text-[10px] font-mono text-slate-500">i:${intensity}</span>
208
+ <span class="text-[10px] text-slate-600 ml-auto">${relativeTime(l.created_at)}</span>
209
+ </div>
210
+ ${l.signals ? `<div class="text-[10px] text-slate-500 mt-0.5">${escapeHtml(Array.isArray(l.signals) ? l.signals.join(', ') : l.signals)}</div>` : ''}
211
+ </div>
212
+ </div>`;
213
+ }).join('');
214
+ }
215
+
216
+ async function loadSentiment() {
217
+ const data = await fetchJSON('/api/sentiment');
218
+ if (!data) return;
219
+
220
+ const logs = data.logs || [];
221
+ const daily = data.daily || {};
222
+
223
+ // Summary
224
+ let totalPos = 0, totalNeg = 0, totalNeu = 0, totalUrg = 0, totalInt = 0, count = 0;
225
+ Object.values(daily).forEach(d => {
226
+ totalPos += d.positive || 0; totalNeg += d.negative || 0;
227
+ totalNeu += d.neutral || 0; totalUrg += d.urgent || 0;
228
+ if (d.avg_intensity) { totalInt += d.avg_intensity; count++; }
229
+ });
230
+
231
+ document.getElementById('avg-intensity').textContent = count ? (totalInt / count).toFixed(2) : '--';
232
+ document.getElementById('total-positive').textContent = totalPos;
233
+ document.getElementById('total-neutral').textContent = totalNeu;
234
+ document.getElementById('total-negative').textContent = totalNeg;
235
+ document.getElementById('total-urgent').textContent = totalUrg;
236
+
237
+ drawTimeline(daily);
238
+ renderHeatmap(daily);
239
+ renderWordCloud(logs);
240
+ renderLogs(logs);
241
+ }
242
+
243
+ loadSentiment();
244
+ setInterval(loadSentiment, 60000);
245
+ window.addEventListener('resize', () => loadSentiment());
246
+ </script>
247
+ {% endblock %}