@cachly-dev/mcp-server 0.10.5 → 0.10.7
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/dist/auth.d.ts +3 -0
- package/dist/auth.js +32 -0
- package/dist/auth.js.map +1 -0
- package/dist/handlers/advanced.js +38 -31
- package/dist/handlers/advanced.js.map +1 -1
- package/dist/handlers/brain.js +144 -50
- package/dist/handlers/brain.js.map +1 -1
- package/dist/index.js +252 -217
- package/dist/index.js.map +1 -1
- package/dist/utils.d.ts +1 -0
- package/dist/utils.js +13 -0
- package/dist/utils.js.map +1 -0
- package/package.json +1 -1
- package/src/auth.ts +46 -0
- package/src/handlers/advanced.ts +37 -23
- package/src/handlers/brain.ts +141 -47
- package/src/index.ts +251 -234
- package/src/utils.ts +10 -0
package/dist/handlers/brain.js
CHANGED
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
import { execSync } from 'node:child_process';
|
|
2
2
|
import { calculateConfidence, confidenceBadge, STRUCTURED_TEMPLATES, CONFIDENCE_WARN_VALUE, CONFIDENCE_STALE_VALUE, CONFIDENCE_WARN_DAYS } from '../confidence.js';
|
|
3
3
|
import { ckgSlug, extractProblemConcept, ckgUpsertNode, ckgUpdateEdge } from '../ckg.js';
|
|
4
|
+
import { safeJsonParse } from '../utils.js';
|
|
4
5
|
import { keywordSearch, tokenize, splitMultiQuery, levenshtein, indexVocab as _indexVocab } from '../search.js';
|
|
5
6
|
import { computeEmbedding, hasEmbedProvider } from '../embeddings.js';
|
|
7
|
+
// ── Changelog (shown once per version in session_start) ──────────────────────
|
|
8
|
+
const MCP_VERSION = '0.10.0';
|
|
9
|
+
const WHATS_NEW = {
|
|
10
|
+
'0.10.0': [
|
|
11
|
+
`🆕 **What's new in v${MCP_VERSION}:**`,
|
|
12
|
+
` ✅ Zero-friction setup — auto-provisions your Brain, no manual config needed`,
|
|
13
|
+
` ✅ 10× faster briefings — batch Redis fetches replace per-key loops`,
|
|
14
|
+
` ✅ Crash-proof data reads — corrupted cache entries no longer break tools`,
|
|
15
|
+
` ✅ \`session_end\` warns when called without an active session`,
|
|
16
|
+
` ✅ \`get_api_status\` now shows live Redis ping + all instance statuses`,
|
|
17
|
+
],
|
|
18
|
+
};
|
|
6
19
|
export const BRAIN_TOOL_NAMES = new Set([
|
|
7
20
|
'learn_from_attempts', 'recall_best_solution', 'smart_recall',
|
|
8
21
|
'session_start', 'session_end', 'session_ping', 'session_handoff', 'auto_learn_session',
|
|
@@ -98,7 +111,7 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
98
111
|
for (const dep of depends_on) {
|
|
99
112
|
const depKey = `cachly:dep:${dep}`;
|
|
100
113
|
const existing = await redis.get(depKey);
|
|
101
|
-
const depTopics =
|
|
114
|
+
const depTopics = safeJsonParse(existing, []);
|
|
102
115
|
if (!depTopics.includes(topic))
|
|
103
116
|
depTopics.push(topic);
|
|
104
117
|
await redis.set(depKey, JSON.stringify(depTopics));
|
|
@@ -190,7 +203,9 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
190
203
|
const er = await redis.get(ek);
|
|
191
204
|
if (!er)
|
|
192
205
|
continue;
|
|
193
|
-
const existEdge =
|
|
206
|
+
const existEdge = safeJsonParse(er, null);
|
|
207
|
+
if (!existEdge)
|
|
208
|
+
continue;
|
|
194
209
|
if (existEdge.edgeType === 'fixes' && existEdge.confidence > 0.7 && existEdge.trials >= 3) {
|
|
195
210
|
beliefConflict = `⚠️ **belief_conflict** on \`${topic}\`: previously confirmed fix (confidence ${existEdge.confidence.toFixed(2)}, n=${existEdge.trials}) now reports failure. Both beliefs retained as \`contested\`. Use \`ckg_inspect(concept="${conceptId}")\` to review.`;
|
|
196
211
|
// Store conflict marker
|
|
@@ -198,7 +213,7 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
198
213
|
await redis.set(conflictKey, JSON.stringify({ topic, detected_at: new Date().toISOString(), fix_confidence: existEdge.confidence, fix_trials: existEdge.trials, failure_outcome: outcome }), 'EX', 60 * 60 * 24 * 90);
|
|
199
214
|
// Auto-trigger MADC deliberation in background — never blocks learn_from_attempts
|
|
200
215
|
// Note: fire-and-forget; madc_deliberate is handled elsewhere in the switch
|
|
201
|
-
|
|
216
|
+
// (conflict marker stored above — no warm-up call needed)
|
|
202
217
|
}
|
|
203
218
|
}
|
|
204
219
|
}
|
|
@@ -210,7 +225,9 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
210
225
|
const er = await redis.get(ek);
|
|
211
226
|
if (!er)
|
|
212
227
|
continue;
|
|
213
|
-
const e2 =
|
|
228
|
+
const e2 = safeJsonParse(er, null);
|
|
229
|
+
if (!e2)
|
|
230
|
+
continue;
|
|
214
231
|
if (e2.edgeType !== 'fixes')
|
|
215
232
|
continue;
|
|
216
233
|
// Boost second-degree: edges from e2.to get a small fractional success
|
|
@@ -219,7 +236,9 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
219
236
|
const sr = await redis.get(sk);
|
|
220
237
|
if (!sr)
|
|
221
238
|
continue;
|
|
222
|
-
const se =
|
|
239
|
+
const se = safeJsonParse(sr, null);
|
|
240
|
+
if (!se)
|
|
241
|
+
continue;
|
|
223
242
|
if (se.edgeType !== 'co-occurs')
|
|
224
243
|
continue;
|
|
225
244
|
// Add 0.1 fractional success (second-degree signal)
|
|
@@ -241,7 +260,9 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
241
260
|
const er = await redis.get(ek);
|
|
242
261
|
if (!er)
|
|
243
262
|
continue;
|
|
244
|
-
const de =
|
|
263
|
+
const de = safeJsonParse(er, null);
|
|
264
|
+
if (!de)
|
|
265
|
+
continue;
|
|
245
266
|
if (de.trials < 3 && de.last_updated && new Date(de.last_updated).getTime() < decayCutoff) {
|
|
246
267
|
de.confidence = de.confidence * 0.9;
|
|
247
268
|
de.last_updated = new Date().toISOString();
|
|
@@ -281,7 +302,9 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
281
302
|
// Try exact best-solution key first
|
|
282
303
|
const best = await redis.get(`cachly:lesson:best:${topic}`);
|
|
283
304
|
if (best) {
|
|
284
|
-
const lesson =
|
|
305
|
+
const lesson = safeJsonParse(best, null);
|
|
306
|
+
if (!lesson)
|
|
307
|
+
return `⚠️ Lesson data for \`${topic}\` is corrupted. Re-store it with \`learn_from_attempts\`.`;
|
|
285
308
|
// ── Confidence decay check ───────────────────────────────────────────
|
|
286
309
|
const confidence = calculateConfidence(lesson);
|
|
287
310
|
const ref = lesson.verified_at ?? lesson.ts;
|
|
@@ -295,6 +318,9 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
295
318
|
confidence: 1.0,
|
|
296
319
|
};
|
|
297
320
|
await redis.set(`cachly:lesson:best:${topic}`, JSON.stringify(updatedLesson));
|
|
321
|
+
// Track estimated time saved (30m minor · 60m major · 240m critical)
|
|
322
|
+
const savedMins = lesson.severity === 'critical' ? 240 : lesson.severity === 'major' ? 60 : 30;
|
|
323
|
+
redis.incrbyfloat(`cachly:stats:time_saved_mins:${instance_id}`, savedMins).catch(() => { });
|
|
298
324
|
const sevEmoji = lesson.severity === 'critical' ? '🔴' : lesson.severity === 'major' ? '🟡' : lesson.severity ? '🟢' : '';
|
|
299
325
|
const auditSummary = (lesson.audit_trail ?? []).length > 1
|
|
300
326
|
? `_Audit: ${(lesson.audit_trail ?? []).length} changes · stored ${new Date(lesson.ts).toLocaleDateString('de-DE')}_`
|
|
@@ -336,7 +362,7 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
336
362
|
const histKey = `cachly:lessons:${topic}`;
|
|
337
363
|
const all = await redis.lrange(histKey, -3, -1);
|
|
338
364
|
if (all.length > 0) {
|
|
339
|
-
const parsed = all.map(e =>
|
|
365
|
+
const parsed = all.map(e => safeJsonParse(e, null)).filter(Boolean);
|
|
340
366
|
const lines = parsed.map(p => `- ${p.outcome === 'success' ? '✅' : '❌'} ${p.what_worked.slice(0, 120)} (${new Date(p.ts).toLocaleDateString('de-DE')})`);
|
|
341
367
|
return `⚠️ No successful solution for \`${topic}\` yet. Last attempts:\n\n${lines.join('\n')}`;
|
|
342
368
|
}
|
|
@@ -348,7 +374,9 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
348
374
|
const raw = await redis.get(k);
|
|
349
375
|
if (!raw)
|
|
350
376
|
continue;
|
|
351
|
-
const lesson =
|
|
377
|
+
const lesson = safeJsonParse(raw, null);
|
|
378
|
+
if (!lesson)
|
|
379
|
+
continue;
|
|
352
380
|
results.push(`**\`${lesson.topic}\`** — ${lesson.what_worked.slice(0, 200)}`);
|
|
353
381
|
}
|
|
354
382
|
return `🔍 **Partial matches for \`${topic}\`:**\n\n${results.join('\n\n')}`;
|
|
@@ -485,14 +513,13 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
485
513
|
lStream.on('error', reject);
|
|
486
514
|
});
|
|
487
515
|
const lessons = [];
|
|
488
|
-
|
|
489
|
-
const
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
516
|
+
if (lessonKeys.length > 0) {
|
|
517
|
+
const raws = await redis.mget(...lessonKeys);
|
|
518
|
+
for (const raw of raws) {
|
|
519
|
+
const l = safeJsonParse(raw ?? null, null);
|
|
520
|
+
if (l)
|
|
521
|
+
lessons.push(l);
|
|
494
522
|
}
|
|
495
|
-
catch { /* skip corrupt */ }
|
|
496
523
|
}
|
|
497
524
|
lessons.sort((a, b) => new Date(b.ts).getTime() - new Date(a.ts).getTime());
|
|
498
525
|
// 3. Count context entries (filter :meta keys)
|
|
@@ -507,13 +534,7 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
507
534
|
});
|
|
508
535
|
// 4. Last session
|
|
509
536
|
const lastSessionRaw = await redis.get('cachly:session:last');
|
|
510
|
-
|
|
511
|
-
if (lastSessionRaw) {
|
|
512
|
-
try {
|
|
513
|
-
lastSession = JSON.parse(lastSessionRaw);
|
|
514
|
-
}
|
|
515
|
-
catch { /* ignore */ }
|
|
516
|
-
}
|
|
537
|
+
const lastSession = safeJsonParse(lastSessionRaw, null);
|
|
517
538
|
// 5. Focus filtering
|
|
518
539
|
const focusTerms = focus.toLowerCase().split(/\s+/).filter(Boolean);
|
|
519
540
|
const focusLessons = focusTerms.length > 0
|
|
@@ -526,7 +547,7 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
526
547
|
let streakMessage = '';
|
|
527
548
|
try {
|
|
528
549
|
const streakRaw = await redis.get('cachly:streak:current');
|
|
529
|
-
const streak =
|
|
550
|
+
const streak = safeJsonParse(streakRaw, null);
|
|
530
551
|
const today = new Date().toISOString().slice(0, 10);
|
|
531
552
|
if (streak) {
|
|
532
553
|
const lastDate = streak.last_date;
|
|
@@ -567,11 +588,54 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
567
588
|
focus,
|
|
568
589
|
provider,
|
|
569
590
|
}), 'EX', 86400); // auto-expire after 24h if session_end never called
|
|
591
|
+
// 8. Time saved counter
|
|
592
|
+
let timeSavedMins = 0;
|
|
593
|
+
try {
|
|
594
|
+
const raw = await redis.get(`cachly:stats:time_saved_mins:${instance_id}`);
|
|
595
|
+
timeSavedMins = parseFloat(raw ?? '0');
|
|
596
|
+
}
|
|
597
|
+
catch { /* non-critical */ }
|
|
570
598
|
// ── Build briefing ──────────────────────────────────────────────────────
|
|
571
599
|
const providerLabel = provider ? ` · ${provider}` : '';
|
|
572
600
|
const lines = [`🧠 **Session Briefing**${providerLabel}`, ''];
|
|
573
601
|
if (streakMessage)
|
|
574
602
|
lines.push(streakMessage, '');
|
|
603
|
+
// Time saved (only show when meaningful — 30+ minutes)
|
|
604
|
+
if (timeSavedMins >= 30) {
|
|
605
|
+
const h = Math.floor(timeSavedMins / 60);
|
|
606
|
+
const m = Math.round(timeSavedMins % 60);
|
|
607
|
+
const timeStr = h > 0 ? `${h}h${m > 0 ? ` ${m}m` : ''}` : `${m}m`;
|
|
608
|
+
lines.push(`⏱️ **Brain saved you ~${timeStr} total** (time not re-researching known fixes)`, '');
|
|
609
|
+
}
|
|
610
|
+
// ── What's New (shown once per version update) ──────────────────────────
|
|
611
|
+
try {
|
|
612
|
+
const seenVersion = await redis.get('cachly:mcp:version:last_seen');
|
|
613
|
+
if (seenVersion !== MCP_VERSION) {
|
|
614
|
+
await redis.set('cachly:mcp:version:last_seen', MCP_VERSION, 'EX', 365 * 86400);
|
|
615
|
+
const changelog = WHATS_NEW[MCP_VERSION];
|
|
616
|
+
if (changelog) {
|
|
617
|
+
lines.push(...changelog, '');
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
catch { /* non-critical */ }
|
|
622
|
+
// ── First-time welcome (empty brain) ───────────────────────────────────
|
|
623
|
+
const isFirstSession = !lastSession && lessons.length === 0 && ctxCount === 0;
|
|
624
|
+
if (isFirstSession) {
|
|
625
|
+
lines.push('🎉 **Welcome! Your AI Brain is live.**', '');
|
|
626
|
+
lines.push('It learns from your work automatically. After your first session it will look like this:', '');
|
|
627
|
+
lines.push(' ✅ `api:auth` — Bearer token in header, not cookie; 401 on missing scope');
|
|
628
|
+
lines.push(' ✅ `database:migrations` — always run migrations before deploy');
|
|
629
|
+
lines.push(' ⚠️ `docker:build` — ARG changes bust all subsequent cache layers');
|
|
630
|
+
lines.push('');
|
|
631
|
+
lines.push('**3 steps to grow your brain:**');
|
|
632
|
+
lines.push(' 1. Fix something → `learn_from_attempts(topic="bug:name", outcome="success", what_worked="...")`');
|
|
633
|
+
lines.push(' 2. Save context → `remember_context(key="arch", value="...")`');
|
|
634
|
+
lines.push(' 3. End session → `session_end(summary="What I did")`');
|
|
635
|
+
lines.push('');
|
|
636
|
+
lines.push('💡 Run `brain_doctor` for a setup health-check and personalised tips.');
|
|
637
|
+
lines.push('');
|
|
638
|
+
}
|
|
575
639
|
// Handoff from previous window (if any)
|
|
576
640
|
const handoffRaw = await redis.get('cachly:session:handoff');
|
|
577
641
|
if (handoffRaw) {
|
|
@@ -707,7 +771,9 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
707
771
|
const cr = await redis.get(ck);
|
|
708
772
|
if (!cr)
|
|
709
773
|
continue;
|
|
710
|
-
const cf =
|
|
774
|
+
const cf = safeJsonParse(cr, null);
|
|
775
|
+
if (!cf)
|
|
776
|
+
continue;
|
|
711
777
|
lines.push(` ⚠️ \`${cf.topic}\` — previously confirmed fix (${(cf.fix_confidence * 100).toFixed(0)}%, n=${cf.fix_trials}) now contradicted. Use \`ckg_inspect(concept="${ckgSlug(cf.topic)}")\``);
|
|
712
778
|
}
|
|
713
779
|
lines.push('');
|
|
@@ -766,6 +832,14 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
766
832
|
else {
|
|
767
833
|
lines.push('📭 No lessons yet. Use `learn_from_attempts` after solving tasks.', '');
|
|
768
834
|
}
|
|
835
|
+
// Team invite prompt — fires once after 10th lesson if no team use yet
|
|
836
|
+
if (lessons.length >= 10) {
|
|
837
|
+
const hasTeamUse = lessons.some(l => l.author);
|
|
838
|
+
if (!hasTeamUse) {
|
|
839
|
+
lines.push(`🤝 **Your Brain has ${lessons.length} lessons — your team could benefit instantly.**`);
|
|
840
|
+
lines.push(` Share: **cachly.dev/team** · or run \`team_learn\` after the next fix.`, '');
|
|
841
|
+
}
|
|
842
|
+
}
|
|
769
843
|
// Open failures (lessons whose best-key has outcome != success)
|
|
770
844
|
const openFailures = lessons.filter(l => l.outcome === 'failure' || l.outcome === 'partial');
|
|
771
845
|
if (openFailures.length > 0) {
|
|
@@ -857,14 +931,16 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
857
931
|
try {
|
|
858
932
|
const crystalRaw = await redis.get('cachly:crystal:latest');
|
|
859
933
|
if (crystalRaw) {
|
|
860
|
-
const crystal =
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
934
|
+
const crystal = safeJsonParse(crystalRaw, null);
|
|
935
|
+
if (crystal) {
|
|
936
|
+
const crystalAge = Math.round((Date.now() - new Date(crystal.ts).getTime()) / 86_400_000);
|
|
937
|
+
if (crystalAge <= 90) {
|
|
938
|
+
lines.push(`💎 **Memory Crystal** (${crystal.label} · ${crystal.session_count} sessions compressed):`);
|
|
939
|
+
for (const p of crystal.top_patterns.slice(0, 3)) {
|
|
940
|
+
lines.push(` • **${p.category}** (${p.count}×): ${p.insight.slice(0, 90)}`);
|
|
941
|
+
}
|
|
942
|
+
lines.push('');
|
|
866
943
|
}
|
|
867
|
-
lines.push('');
|
|
868
944
|
}
|
|
869
945
|
}
|
|
870
946
|
}
|
|
@@ -876,7 +952,10 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
876
952
|
const PRIORITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
877
953
|
const PRIORITY_ICON = { critical: '🔴', high: '🟠', medium: '🟡', low: '🔵' };
|
|
878
954
|
const openStatuses = new Set(['planned', 'in-progress', 'blocked']);
|
|
879
|
-
const allItems = Object.values(roadmapAll).
|
|
955
|
+
const allItems = Object.values(roadmapAll).flatMap(v => {
|
|
956
|
+
const item = safeJsonParse(v, null);
|
|
957
|
+
return item ? [item] : [];
|
|
958
|
+
});
|
|
880
959
|
const openItems = allItems
|
|
881
960
|
.filter(i => openStatuses.has(i.status))
|
|
882
961
|
.sort((a, b) => {
|
|
@@ -917,23 +996,25 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
917
996
|
const nodeRaw = await redis.get(nk);
|
|
918
997
|
if (!nodeRaw)
|
|
919
998
|
continue;
|
|
920
|
-
const node =
|
|
999
|
+
const node = safeJsonParse(nodeRaw, null);
|
|
1000
|
+
if (!node)
|
|
1001
|
+
continue;
|
|
921
1002
|
const edgeKeys = await redis.smembers(`cachly:ckg:idx:from:${node.id}`);
|
|
922
1003
|
for (const ek of edgeKeys.slice(0, 15)) {
|
|
923
1004
|
const edgeRaw = await redis.get(ek);
|
|
924
1005
|
if (!edgeRaw)
|
|
925
1006
|
continue;
|
|
926
|
-
const edge =
|
|
1007
|
+
const edge = safeJsonParse(edgeRaw, null);
|
|
1008
|
+
if (!edge)
|
|
1009
|
+
continue;
|
|
927
1010
|
if (edge.edgeType !== 'causes' && edge.edgeType !== 'co-occurs' && edge.edgeType !== 'fixes')
|
|
928
1011
|
continue;
|
|
929
1012
|
if (edge.confidence < 0.35)
|
|
930
1013
|
continue;
|
|
931
|
-
// For "fixes" edges, the lesson is on the "from" node (fix for "to")
|
|
932
|
-
// For "causes" edges, target is the failure mode
|
|
933
1014
|
const lessonKey = edge.edgeType === 'fixes' ? `cachly:lesson:best:${edge.from}` : `cachly:lesson:best:${edge.to}`;
|
|
934
1015
|
const lessonRaw = await redis.get(lessonKey);
|
|
935
|
-
const lesson = lessonRaw
|
|
936
|
-
ppePredictions.push({ concept: node.id, edgeType: edge.edgeType, target: edge.to, confidence: edge.confidence, lesson });
|
|
1016
|
+
const lesson = safeJsonParse(lessonRaw, null);
|
|
1017
|
+
ppePredictions.push({ concept: node.id, edgeType: edge.edgeType, target: edge.to, confidence: edge.confidence, lesson: lesson ?? undefined });
|
|
937
1018
|
}
|
|
938
1019
|
}
|
|
939
1020
|
}
|
|
@@ -964,7 +1045,7 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
964
1045
|
// Measures: of recalled lessons with confidence > 0.85, what % had outcome=success?
|
|
965
1046
|
try {
|
|
966
1047
|
const calRaw = await redis.get('cachly:mcm:calibration:last');
|
|
967
|
-
const lastCalMs = calRaw
|
|
1048
|
+
const lastCalMs = safeJsonParse(calRaw, null)?.ts ?? 0;
|
|
968
1049
|
const daysSinceCal = (Date.now() - lastCalMs) / 86_400_000;
|
|
969
1050
|
if (daysSinceCal >= 30 && lessons.length >= 5) {
|
|
970
1051
|
// Quick calibration pass on recalled lessons (recall_count > 0, outcome=success)
|
|
@@ -1011,8 +1092,6 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
1011
1092
|
}
|
|
1012
1093
|
catch { /* non-critical */ }
|
|
1013
1094
|
// ── 🌍 Knowledge Commons — community stats banner ───────────────────────
|
|
1014
|
-
// Fetch syndication stats and show a 1-liner: total lessons + confirms + weekly growth.
|
|
1015
|
-
// Non-fatal: never blocks session start if the API call fails.
|
|
1016
1095
|
try {
|
|
1017
1096
|
const commonsStats = await apiFetch('/api/v1/syndication/stats');
|
|
1018
1097
|
if (commonsStats.total_lessons > 0) {
|
|
@@ -1023,6 +1102,19 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
1023
1102
|
}
|
|
1024
1103
|
}
|
|
1025
1104
|
catch { /* non-critical — never block session start */ }
|
|
1105
|
+
// ── Brain Doctor hint (Punkt 6) — surface when brain needs attention ───
|
|
1106
|
+
const hasOpenFailures = lessons.filter(l => l.outcome === 'failure' || l.outcome === 'partial').length > 0;
|
|
1107
|
+
const hasStaleLessons = lessons.some(l => l.outcome === 'success' && calculateConfidence(l) < CONFIDENCE_WARN_VALUE);
|
|
1108
|
+
if (lessons.length === 0 || hasOpenFailures || hasStaleLessons) {
|
|
1109
|
+
const reasons = [];
|
|
1110
|
+
if (lessons.length === 0)
|
|
1111
|
+
reasons.push('brain is empty');
|
|
1112
|
+
if (hasOpenFailures)
|
|
1113
|
+
reasons.push('open failures');
|
|
1114
|
+
if (hasStaleLessons)
|
|
1115
|
+
reasons.push('stale lessons');
|
|
1116
|
+
lines.push(`🩺 _Run \`brain_doctor\` to fix: ${reasons.join(', ')}._`);
|
|
1117
|
+
}
|
|
1026
1118
|
return lines.join('\n');
|
|
1027
1119
|
}
|
|
1028
1120
|
// ── session_end ───────────────────────────────────────────────────────────
|
|
@@ -1033,18 +1125,20 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
1033
1125
|
// Calculate duration from session_start marker
|
|
1034
1126
|
let durationMin;
|
|
1035
1127
|
const currentRaw = await redis.get('cachly:session:current');
|
|
1036
|
-
if (currentRaw) {
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1128
|
+
if (!currentRaw) {
|
|
1129
|
+
return `⚠️ No active session found.\n\nRun \`session_start\` first to begin tracking a session, then call \`session_end\` when you're done.`;
|
|
1130
|
+
}
|
|
1131
|
+
const current = safeJsonParse(currentRaw, null);
|
|
1132
|
+
if (current?.started) {
|
|
1133
|
+
durationMin = Math.round((now.getTime() - new Date(current.started).getTime()) / 60000);
|
|
1042
1134
|
}
|
|
1043
|
-
// ── Session Replay: capture decision log ─────────────────────────────
|
|
1044
1135
|
let decisionLog = [];
|
|
1045
1136
|
try {
|
|
1046
1137
|
const dlEntries = await redis.lrange('cachly:session:decision-log', 0, -1);
|
|
1047
|
-
decisionLog = dlEntries.
|
|
1138
|
+
decisionLog = dlEntries.flatMap(e => {
|
|
1139
|
+
const entry = safeJsonParse(e, null);
|
|
1140
|
+
return entry ? [entry] : [];
|
|
1141
|
+
});
|
|
1048
1142
|
await redis.del('cachly:session:decision-log');
|
|
1049
1143
|
}
|
|
1050
1144
|
catch { /* non-critical */ }
|