@cachly-dev/mcp-server 0.9.24 → 0.10.0
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/.github/workflows/publish.yml +42 -0
- package/LICENSE +202 -0
- package/dist/handlers/brain.js +93 -33
- package/dist/handlers/brain.js.map +1 -1
- package/dist/handlers/fedbrain.js +55 -38
- package/dist/handlers/fedbrain.js.map +1 -1
- package/dist/handlers/team.js +62 -50
- package/dist/handlers/team.js.map +1 -1
- package/dist/index.js +147 -28
- package/dist/index.js.map +1 -1
- package/dist/tools.js +5 -4
- package/dist/tools.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/glama.json +6 -0
- package/package.json +1 -1
- package/src/handlers/brain.ts +89 -27
- package/src/handlers/fedbrain.ts +43 -31
- package/src/handlers/team.ts +57 -33
- package/src/index.ts +150 -31
- package/src/tools.ts +5 -4
- package/src/utils.ts +10 -0
package/src/handlers/brain.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { Redis } from 'ioredis';
|
|
|
3
3
|
import { calculateConfidence, confidenceBadge, STRUCTURED_TEMPLATES,
|
|
4
4
|
CONFIDENCE_WARN_VALUE, CONFIDENCE_STALE_VALUE, CONFIDENCE_WARN_DAYS } from '../confidence.js';
|
|
5
5
|
import { ckgSlug, extractProblemConcept, ckgUpsertNode, ckgUpdateEdge } from '../ckg.js';
|
|
6
|
+
import { safeJsonParse } from '../utils.js';
|
|
6
7
|
import type { CKGEdge, CKGNode } from '../ckg.js';
|
|
7
8
|
import { keywordSearch, tokenize, splitMultiQuery, levenshtein,
|
|
8
9
|
ZERO_RESULTS_LOG, zeroResultsTotal, indexVocab as _indexVocab } from '../search.js';
|
|
@@ -10,6 +11,19 @@ import type { KeywordMatch } from '../search.js';
|
|
|
10
11
|
import { computeEmbedding, hasEmbedProvider, embedProviderHint, EMBED_PROVIDER } from '../embeddings.js';
|
|
11
12
|
import { detectNamespace } from '../namespace.js';
|
|
12
13
|
|
|
14
|
+
// ── Changelog (shown once per version in session_start) ──────────────────────
|
|
15
|
+
const MCP_VERSION = '0.10.0';
|
|
16
|
+
const WHATS_NEW: Record<string, string[]> = {
|
|
17
|
+
'0.10.0': [
|
|
18
|
+
`🆕 **What's new in v${MCP_VERSION}:**`,
|
|
19
|
+
` ✅ Zero-friction setup — auto-provisions your Brain, no manual config needed`,
|
|
20
|
+
` ✅ 10× faster briefings — batch Redis fetches replace per-key loops`,
|
|
21
|
+
` ✅ Crash-proof data reads — corrupted cache entries no longer break tools`,
|
|
22
|
+
` ✅ \`session_end\` warns when called without an active session`,
|
|
23
|
+
` ✅ \`get_api_status\` now shows live Redis ping + all instance statuses`,
|
|
24
|
+
],
|
|
25
|
+
};
|
|
26
|
+
|
|
13
27
|
// Shared types used in brain handlers
|
|
14
28
|
export interface Instance {
|
|
15
29
|
id: string; name: string; tier: string; status: string; region: string;
|
|
@@ -167,7 +181,7 @@ export async function handleBrainTool(
|
|
|
167
181
|
for (const dep of depends_on) {
|
|
168
182
|
const depKey = `cachly:dep:${dep}`;
|
|
169
183
|
const existing = await redis.get(depKey);
|
|
170
|
-
const depTopics: string[] =
|
|
184
|
+
const depTopics: string[] = safeJsonParse<string[]>(existing, []);
|
|
171
185
|
if (!depTopics.includes(topic)) depTopics.push(topic);
|
|
172
186
|
await redis.set(depKey, JSON.stringify(depTopics));
|
|
173
187
|
}
|
|
@@ -266,7 +280,8 @@ export async function handleBrainTool(
|
|
|
266
280
|
for (const ek of existingEdgeKeys) {
|
|
267
281
|
const er = await redis.get(ek);
|
|
268
282
|
if (!er) continue;
|
|
269
|
-
const existEdge
|
|
283
|
+
const existEdge = safeJsonParse<CKGEdge | null>(er, null);
|
|
284
|
+
if (!existEdge) continue;
|
|
270
285
|
if (existEdge.edgeType === 'fixes' && existEdge.confidence > 0.7 && existEdge.trials >= 3) {
|
|
271
286
|
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.`;
|
|
272
287
|
// Store conflict marker
|
|
@@ -286,14 +301,16 @@ export async function handleBrainTool(
|
|
|
286
301
|
for (const ek of fromEdgeKeys.slice(0, 10)) {
|
|
287
302
|
const er = await redis.get(ek);
|
|
288
303
|
if (!er) continue;
|
|
289
|
-
const e2
|
|
304
|
+
const e2 = safeJsonParse<CKGEdge | null>(er, null);
|
|
305
|
+
if (!e2) continue;
|
|
290
306
|
if (e2.edgeType !== 'fixes') continue;
|
|
291
307
|
// Boost second-degree: edges from e2.to get a small fractional success
|
|
292
308
|
const secondKeys = await redis.smembers(`cachly:ckg:idx:from:${e2.to}`);
|
|
293
309
|
for (const sk of secondKeys.slice(0, 5)) {
|
|
294
310
|
const sr = await redis.get(sk);
|
|
295
311
|
if (!sr) continue;
|
|
296
|
-
const se
|
|
312
|
+
const se = safeJsonParse<CKGEdge | null>(sr, null);
|
|
313
|
+
if (!se) continue;
|
|
297
314
|
if (se.edgeType !== 'co-occurs') continue;
|
|
298
315
|
// Add 0.1 fractional success (second-degree signal)
|
|
299
316
|
se.successes = (se.successes || 0) + 0.1;
|
|
@@ -314,7 +331,8 @@ export async function handleBrainTool(
|
|
|
314
331
|
for (const ek of fromEdgeKeys) {
|
|
315
332
|
const er = await redis.get(ek);
|
|
316
333
|
if (!er) continue;
|
|
317
|
-
const de
|
|
334
|
+
const de = safeJsonParse<CKGEdge | null>(er, null);
|
|
335
|
+
if (!de) continue;
|
|
318
336
|
if (de.trials < 3 && de.last_updated && new Date(de.last_updated).getTime() < decayCutoff) {
|
|
319
337
|
de.confidence = de.confidence * 0.9;
|
|
320
338
|
de.last_updated = new Date().toISOString();
|
|
@@ -356,12 +374,13 @@ export async function handleBrainTool(
|
|
|
356
374
|
// Try exact best-solution key first
|
|
357
375
|
const best = await redis.get(`cachly:lesson:best:${topic}`);
|
|
358
376
|
if (best) {
|
|
359
|
-
const lesson =
|
|
377
|
+
const lesson = safeJsonParse(best, null as null | {
|
|
360
378
|
topic: string; outcome: string; what_worked: string; what_failed?: string;
|
|
361
379
|
context?: string; ts: string; verified_at?: string; severity?: string;
|
|
362
380
|
file_paths?: string[]; commands?: string[]; tags?: string[];
|
|
363
381
|
recall_count?: number; audit_trail?: unknown[];
|
|
364
|
-
};
|
|
382
|
+
});
|
|
383
|
+
if (!lesson) return `⚠️ Lesson data for \`${topic}\` is corrupted. Re-store it with \`learn_from_attempts\`.`;
|
|
365
384
|
|
|
366
385
|
// ── Confidence decay check ───────────────────────────────────────────
|
|
367
386
|
const confidence = calculateConfidence(lesson);
|
|
@@ -424,7 +443,7 @@ export async function handleBrainTool(
|
|
|
424
443
|
const histKey = `cachly:lessons:${topic}`;
|
|
425
444
|
const all = await redis.lrange(histKey, -3, -1);
|
|
426
445
|
if (all.length > 0) {
|
|
427
|
-
const parsed = all.map(e =>
|
|
446
|
+
const parsed = all.map(e => safeJsonParse(e, null as null | { outcome: string; what_worked: string; ts: string })).filter(Boolean) as Array<{ outcome: string; what_worked: string; ts: string }>;
|
|
428
447
|
const lines = parsed.map(p => `- ${p.outcome === 'success' ? '✅' : '❌'} ${p.what_worked.slice(0, 120)} (${new Date(p.ts).toLocaleDateString('de-DE')})`);
|
|
429
448
|
return `⚠️ No successful solution for \`${topic}\` yet. Last attempts:\n\n${lines.join('\n')}`;
|
|
430
449
|
}
|
|
@@ -436,7 +455,8 @@ export async function handleBrainTool(
|
|
|
436
455
|
for (const k of matching.slice(0, 5)) {
|
|
437
456
|
const raw = await redis.get(k);
|
|
438
457
|
if (!raw) continue;
|
|
439
|
-
const lesson =
|
|
458
|
+
const lesson = safeJsonParse(raw, null as null | { topic: string; what_worked: string; context?: string; ts: string });
|
|
459
|
+
if (!lesson) continue;
|
|
440
460
|
results.push(`**\`${lesson.topic}\`** — ${lesson.what_worked.slice(0, 200)}`);
|
|
441
461
|
}
|
|
442
462
|
return `🔍 **Partial matches for \`${topic}\`:**\n\n${results.join('\n\n')}`;
|
|
@@ -597,10 +617,12 @@ export async function handleBrainTool(
|
|
|
597
617
|
tags?: string[]; confidence?: number; audit_trail?: unknown[];
|
|
598
618
|
};
|
|
599
619
|
const lessons: Lesson[] = [];
|
|
600
|
-
|
|
601
|
-
const
|
|
602
|
-
|
|
603
|
-
|
|
620
|
+
if (lessonKeys.length > 0) {
|
|
621
|
+
const raws = await redis.mget(...lessonKeys);
|
|
622
|
+
for (const raw of raws) {
|
|
623
|
+
const l = safeJsonParse<Lesson | null>(raw ?? null, null);
|
|
624
|
+
if (l) lessons.push(l);
|
|
625
|
+
}
|
|
604
626
|
}
|
|
605
627
|
lessons.sort((a, b) => new Date(b.ts).getTime() - new Date(a.ts).getTime());
|
|
606
628
|
|
|
@@ -617,10 +639,8 @@ export async function handleBrainTool(
|
|
|
617
639
|
|
|
618
640
|
// 4. Last session
|
|
619
641
|
const lastSessionRaw = await redis.get('cachly:session:last');
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
try { lastSession = JSON.parse(lastSessionRaw); } catch { /* ignore */ }
|
|
623
|
-
}
|
|
642
|
+
type LastSession = { summary: string; ts: string; files_changed?: string[]; duration_min?: number };
|
|
643
|
+
const lastSession = safeJsonParse<LastSession | null>(lastSessionRaw, null);
|
|
624
644
|
|
|
625
645
|
// 5. Focus filtering
|
|
626
646
|
const focusTerms = focus.toLowerCase().split(/\s+/).filter(Boolean);
|
|
@@ -639,7 +659,7 @@ export async function handleBrainTool(
|
|
|
639
659
|
let streakMessage = '';
|
|
640
660
|
try {
|
|
641
661
|
const streakRaw = await redis.get('cachly:streak:current');
|
|
642
|
-
const streak =
|
|
662
|
+
const streak = safeJsonParse<{ days: number; last_date: string; record: number } | null>(streakRaw, null);
|
|
643
663
|
const today = new Date().toISOString().slice(0, 10);
|
|
644
664
|
if (streak) {
|
|
645
665
|
const lastDate = streak.last_date;
|
|
@@ -681,6 +701,34 @@ export async function handleBrainTool(
|
|
|
681
701
|
const lines: string[] = [`🧠 **Session Briefing**${providerLabel}`, ''];
|
|
682
702
|
if (streakMessage) lines.push(streakMessage, '');
|
|
683
703
|
|
|
704
|
+
// ── What's New (shown once per version update) ──────────────────────────
|
|
705
|
+
try {
|
|
706
|
+
const seenVersion = await redis.get('cachly:mcp:version:last_seen');
|
|
707
|
+
if (seenVersion !== MCP_VERSION) {
|
|
708
|
+
await redis.set('cachly:mcp:version:last_seen', MCP_VERSION, 'EX', 365 * 86400);
|
|
709
|
+
const changelog = WHATS_NEW[MCP_VERSION];
|
|
710
|
+
if (changelog) { lines.push(...changelog, ''); }
|
|
711
|
+
}
|
|
712
|
+
} catch { /* non-critical */ }
|
|
713
|
+
|
|
714
|
+
// ── First-time welcome (empty brain) ───────────────────────────────────
|
|
715
|
+
const isFirstSession = !lastSession && lessons.length === 0 && ctxCount === 0;
|
|
716
|
+
if (isFirstSession) {
|
|
717
|
+
lines.push('🎉 **Welcome! Your AI Brain is live.**', '');
|
|
718
|
+
lines.push('It learns from your work automatically. After your first session it will look like this:', '');
|
|
719
|
+
lines.push(' ✅ `api:auth` — Bearer token in header, not cookie; 401 on missing scope');
|
|
720
|
+
lines.push(' ✅ `database:migrations` — always run migrations before deploy');
|
|
721
|
+
lines.push(' ⚠️ `docker:build` — ARG changes bust all subsequent cache layers');
|
|
722
|
+
lines.push('');
|
|
723
|
+
lines.push('**3 steps to grow your brain:**');
|
|
724
|
+
lines.push(' 1. Fix something → `learn_from_attempts(topic="bug:name", outcome="success", what_worked="...")`');
|
|
725
|
+
lines.push(' 2. Save context → `remember_context(key="arch", value="...")`');
|
|
726
|
+
lines.push(' 3. End session → `session_end(summary="What I did")`');
|
|
727
|
+
lines.push('');
|
|
728
|
+
lines.push('💡 Run `brain_doctor` for a setup health-check and personalised tips.');
|
|
729
|
+
lines.push('');
|
|
730
|
+
}
|
|
731
|
+
|
|
684
732
|
// Handoff from previous window (if any)
|
|
685
733
|
const handoffRaw = await redis.get('cachly:session:handoff');
|
|
686
734
|
if (handoffRaw) {
|
|
@@ -1125,8 +1173,6 @@ export async function handleBrainTool(
|
|
|
1125
1173
|
} catch { /* non-critical */ }
|
|
1126
1174
|
|
|
1127
1175
|
// ── 🌍 Knowledge Commons — community stats banner ───────────────────────
|
|
1128
|
-
// Fetch syndication stats and show a 1-liner: total lessons + confirms + weekly growth.
|
|
1129
|
-
// Non-fatal: never blocks session start if the API call fails.
|
|
1130
1176
|
try {
|
|
1131
1177
|
const commonsStats = await apiFetch<{
|
|
1132
1178
|
total_lessons: number;
|
|
@@ -1143,6 +1189,17 @@ export async function handleBrainTool(
|
|
|
1143
1189
|
}
|
|
1144
1190
|
} catch { /* non-critical — never block session start */ }
|
|
1145
1191
|
|
|
1192
|
+
// ── Brain Doctor hint (Punkt 6) — surface when brain needs attention ───
|
|
1193
|
+
const hasOpenFailures = lessons.filter(l => l.outcome === 'failure' || l.outcome === 'partial').length > 0;
|
|
1194
|
+
const hasStaleLessons = lessons.some(l => l.outcome === 'success' && calculateConfidence(l) < CONFIDENCE_WARN_VALUE);
|
|
1195
|
+
if (lessons.length === 0 || hasOpenFailures || hasStaleLessons) {
|
|
1196
|
+
const reasons: string[] = [];
|
|
1197
|
+
if (lessons.length === 0) reasons.push('brain is empty');
|
|
1198
|
+
if (hasOpenFailures) reasons.push('open failures');
|
|
1199
|
+
if (hasStaleLessons) reasons.push('stale lessons');
|
|
1200
|
+
lines.push(`🩺 _Run \`brain_doctor\` to fix: ${reasons.join(', ')}._`);
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1146
1203
|
return lines.join('\n');
|
|
1147
1204
|
}
|
|
1148
1205
|
|
|
@@ -1168,18 +1225,23 @@ export async function handleBrainTool(
|
|
|
1168
1225
|
// Calculate duration from session_start marker
|
|
1169
1226
|
let durationMin: number | undefined;
|
|
1170
1227
|
const currentRaw = await redis.get('cachly:session:current');
|
|
1171
|
-
if (currentRaw) {
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1228
|
+
if (!currentRaw) {
|
|
1229
|
+
return `⚠️ No active session found.\n\nRun \`session_start\` first to begin tracking a session, then call \`session_end\` when you're done.`;
|
|
1230
|
+
}
|
|
1231
|
+
const current = safeJsonParse<{ started: string } | null>(currentRaw, null);
|
|
1232
|
+
if (current?.started) {
|
|
1233
|
+
durationMin = Math.round((now.getTime() - new Date(current.started).getTime()) / 60000);
|
|
1176
1234
|
}
|
|
1177
1235
|
|
|
1178
1236
|
// ── Session Replay: capture decision log ─────────────────────────────
|
|
1179
|
-
|
|
1237
|
+
type DecisionEntry = { ts: string; topic: string; outcome: string; what_worked: string };
|
|
1238
|
+
let decisionLog: DecisionEntry[] = [];
|
|
1180
1239
|
try {
|
|
1181
1240
|
const dlEntries = await redis.lrange('cachly:session:decision-log', 0, -1);
|
|
1182
|
-
decisionLog = dlEntries.
|
|
1241
|
+
decisionLog = dlEntries.flatMap(e => {
|
|
1242
|
+
const entry = safeJsonParse<DecisionEntry | null>(e, null);
|
|
1243
|
+
return entry ? [entry] : [];
|
|
1244
|
+
});
|
|
1183
1245
|
await redis.del('cachly:session:decision-log');
|
|
1184
1246
|
} catch { /* non-critical */ }
|
|
1185
1247
|
|
package/src/handlers/fedbrain.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { calculateConfidence, confidenceBadge, CONFIDENCE_WARN_VALUE, CONFIDENCE
|
|
|
6
6
|
import { ckgSlug, extractProblemConcept, ckgUpsertNode, ckgUpdateEdge } from '../ckg.js';
|
|
7
7
|
import type { CKGEdge, CKGNode } from '../ckg.js';
|
|
8
8
|
import { keywordSearch, tokenize } from '../search.js';
|
|
9
|
+
import { safeJsonParse } from '../utils.js';
|
|
9
10
|
import { computeEmbedding, hasEmbedProvider } from '../embeddings.js';
|
|
10
11
|
|
|
11
12
|
// Git concurrency semaphore (for brain_from_git parallel workers)
|
|
@@ -397,7 +398,8 @@ export async function handleFedbrainTool(
|
|
|
397
398
|
const raw = await redis.get(`cachly:lesson:best:${lesson_key}`);
|
|
398
399
|
if (!raw) return `❌ Lesson \`${lesson_key}\` not found. Store it first with \`learn_from_attempts\`.`;
|
|
399
400
|
|
|
400
|
-
const lesson =
|
|
401
|
+
const lesson = safeJsonParse(raw, null as null | { topic: string; outcome: string; what_worked: string; what_failed?: string; tags?: string[]; commands?: string[]; severity?: string; ts?: string });
|
|
402
|
+
if (!lesson) return `❌ Lesson \`${lesson_key}\` data is corrupted. Re-store with \`learn_from_attempts\`.`;
|
|
401
403
|
|
|
402
404
|
const domainTokens = [lesson.topic.split(':')[0], ...(lesson.tags ?? [])].filter(Boolean);
|
|
403
405
|
const domainFingerprint = [...new Set(domainTokens)].sort().join(',');
|
|
@@ -747,7 +749,8 @@ export async function handleFedbrainTool(
|
|
|
747
749
|
}
|
|
748
750
|
|
|
749
751
|
type Crystal = { label: string; ts: string; session_count: number; lesson_count: number; top_patterns: Array<{ category: string; insight: string; count: number }>; categories: string[]; created_from: string };
|
|
750
|
-
const crystal
|
|
752
|
+
const crystal = safeJsonParse<Crystal | null>(raw, null);
|
|
753
|
+
if (!crystal) return `❌ Memory Crystal data is corrupted. Re-create with \`memory_crystalize()\`.`;
|
|
751
754
|
const age = Math.floor((Date.now() - new Date(crystal.ts).getTime()) / 86400000);
|
|
752
755
|
const freshEmoji = age <= 7 ? '🟢' : age <= 30 ? '🟡' : '🔴';
|
|
753
756
|
|
|
@@ -784,49 +787,55 @@ export async function handleFedbrainTool(
|
|
|
784
787
|
const crystalRaw = await redis.get('cachly:crystal:latest');
|
|
785
788
|
if (crystalRaw) {
|
|
786
789
|
type Crystal = { label: string; ts: string; session_count: number; lesson_count: number; top_patterns: Array<{ category: string; insight: string; count: number }> };
|
|
787
|
-
const crystal
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
790
|
+
const crystal = safeJsonParse<Crystal | null>(crystalRaw, null);
|
|
791
|
+
if (crystal) {
|
|
792
|
+
lines.push(`### 💎 Memory Crystal: ${crystal.label}`);
|
|
793
|
+
lines.push(`Compressed from ${crystal.session_count} sessions, ${crystal.lesson_count} lessons.`);
|
|
794
|
+
const topN = focus
|
|
795
|
+
? crystal.top_patterns.filter(p => p.category.toLowerCase().includes(focus.toLowerCase()) || p.insight.toLowerCase().includes(focus.toLowerCase())).slice(0, 4)
|
|
796
|
+
: crystal.top_patterns.slice(0, 4);
|
|
797
|
+
for (const p of topN) lines.push(` • **${p.category}**: ${p.insight.slice(0, 100)}`);
|
|
798
|
+
lines.push('');
|
|
799
|
+
}
|
|
795
800
|
}
|
|
796
801
|
|
|
797
802
|
// 2. Last session summary
|
|
798
803
|
const lastSession = await redis.get('cachly:session:last');
|
|
799
804
|
if (lastSession) {
|
|
800
805
|
type Session = { summary?: string; ts?: string; focus?: string };
|
|
801
|
-
const sess
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
+
const sess = safeJsonParse<Session | null>(lastSession, null);
|
|
807
|
+
if (sess) {
|
|
808
|
+
lines.push(`### 🕐 Last Session`);
|
|
809
|
+
if (sess.focus) lines.push(`Focus: _${sess.focus}_`);
|
|
810
|
+
if (sess.summary) lines.push(`Summary: ${sess.summary.slice(0, 300)}`);
|
|
811
|
+
lines.push('');
|
|
812
|
+
}
|
|
806
813
|
}
|
|
807
814
|
|
|
808
815
|
// 3. Session handoff
|
|
809
816
|
const handoff = await redis.get('cachly:session:handoff');
|
|
810
817
|
if (handoff) {
|
|
811
818
|
type Handoff = { remaining_tasks?: string[]; instructions?: string; context_summary?: string; blocked_on?: string };
|
|
812
|
-
const h
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
819
|
+
const h = safeJsonParse<Handoff | null>(handoff, null);
|
|
820
|
+
if (h) {
|
|
821
|
+
lines.push(`### 📋 Handoff (from last window)`);
|
|
822
|
+
if (h.context_summary) lines.push(`Context: ${h.context_summary.slice(0, 200)}`);
|
|
823
|
+
if (h.remaining_tasks?.length) {
|
|
824
|
+
lines.push(`Remaining tasks:`);
|
|
825
|
+
for (const t of h.remaining_tasks.slice(0, 5)) lines.push(` • ${t}`);
|
|
826
|
+
}
|
|
827
|
+
if (h.instructions) lines.push(`⚠️ Instructions: ${h.instructions.slice(0, 200)}`);
|
|
828
|
+
if (h.blocked_on) lines.push(`🚧 Blocked on: ${h.blocked_on}`);
|
|
829
|
+
lines.push('');
|
|
818
830
|
}
|
|
819
|
-
if (h.instructions) lines.push(`⚠️ Instructions: ${h.instructions.slice(0, 200)}`);
|
|
820
|
-
if (h.blocked_on) lines.push(`🚧 Blocked on: ${h.blocked_on}`);
|
|
821
|
-
lines.push('');
|
|
822
831
|
}
|
|
823
832
|
|
|
824
833
|
// 4. WIP registry
|
|
825
834
|
const wipRaw = await redis.get('cachly:ctx:wip-registry');
|
|
826
835
|
if (wipRaw) {
|
|
827
836
|
type Ctx = { content?: string };
|
|
828
|
-
const wip
|
|
829
|
-
if (wip
|
|
837
|
+
const wip = safeJsonParse<Ctx | null>(wipRaw, null);
|
|
838
|
+
if (wip?.content) {
|
|
830
839
|
lines.push(`### 🔧 WIP Registry`);
|
|
831
840
|
lines.push(wip.content.slice(0, 400));
|
|
832
841
|
lines.push('');
|
|
@@ -1026,12 +1035,14 @@ export async function handleFedbrainTool(
|
|
|
1026
1035
|
for (const nk of nodeKeys.slice(0, 5)) {
|
|
1027
1036
|
const nodeRaw = await redis.get(nk);
|
|
1028
1037
|
if (!nodeRaw) continue;
|
|
1029
|
-
const node
|
|
1038
|
+
const node = safeJsonParse<CKGNode | null>(nodeRaw, null);
|
|
1039
|
+
if (!node) continue;
|
|
1030
1040
|
const edgeKeys = await redis.smembers(`cachly:ckg:idx:from:${node.id}`);
|
|
1031
1041
|
for (const ek of edgeKeys.slice(0, 20)) {
|
|
1032
1042
|
const edgeRaw = await redis.get(ek);
|
|
1033
1043
|
if (!edgeRaw) continue;
|
|
1034
|
-
const edge
|
|
1044
|
+
const edge = safeJsonParse<CKGEdge | null>(edgeRaw, null);
|
|
1045
|
+
if (!edge) continue;
|
|
1035
1046
|
if (edge.edgeType !== 'causes' && edge.edgeType !== 'degrades_under') continue;
|
|
1036
1047
|
|
|
1037
1048
|
// Look up fix for this failure from CKG 'fixes' edges
|
|
@@ -1040,12 +1051,13 @@ export async function handleFedbrainTool(
|
|
|
1040
1051
|
for (const fek of fixEdgeKeys.slice(0, 10)) {
|
|
1041
1052
|
const feRaw = await redis.get(fek);
|
|
1042
1053
|
if (!feRaw) continue;
|
|
1043
|
-
const fe
|
|
1054
|
+
const fe = safeJsonParse<CKGEdge | null>(feRaw, null);
|
|
1055
|
+
if (!fe) continue;
|
|
1044
1056
|
if (fe.edgeType === 'fixes') {
|
|
1045
1057
|
const lessonRaw = await redis.get(`cachly:lesson:best:${fe.from}`);
|
|
1046
1058
|
if (lessonRaw) {
|
|
1047
|
-
const lesson =
|
|
1048
|
-
fix = lesson
|
|
1059
|
+
const lesson = safeJsonParse(lessonRaw, null as null | { what_worked?: string });
|
|
1060
|
+
fix = lesson?.what_worked?.slice(0, 120);
|
|
1049
1061
|
break;
|
|
1050
1062
|
}
|
|
1051
1063
|
}
|
package/src/handlers/team.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type { Redis } from 'ioredis';
|
|
|
5
5
|
import { calculateConfidence, confidenceBadge, CONFIDENCE_STALE_VALUE, CONFIDENCE_WARN_VALUE,
|
|
6
6
|
CONFIDENCE_WARN_DAYS, CONFIDENCE_STALE_DAYS, simpleHash } from '../confidence.js';
|
|
7
7
|
import type { Instance } from './brain.js';
|
|
8
|
+
import { safeJsonParse } from '../utils.js';
|
|
8
9
|
|
|
9
10
|
type GetConnection = (instanceId: string) => Promise<Redis>;
|
|
10
11
|
type ApiFetch = <T>(path: string, options?: RequestInit) => Promise<T>;
|
|
@@ -53,10 +54,11 @@ export async function handleTeamTool(
|
|
|
53
54
|
|
|
54
55
|
type Lesson = { topic: string; what_worked: string; outcome: string; file_paths?: string[] };
|
|
55
56
|
const relevant: string[] = [];
|
|
56
|
-
|
|
57
|
-
|
|
57
|
+
const sfRaws = lessonKeys.length > 0 ? await redis.mget(...lessonKeys) : [];
|
|
58
|
+
for (const raw of sfRaws) {
|
|
58
59
|
if (!raw) continue;
|
|
59
|
-
const lesson =
|
|
60
|
+
const lesson = safeJsonParse<Lesson | null>(raw, null);
|
|
61
|
+
if (!lesson) continue;
|
|
60
62
|
// Match by file_paths stored in lesson OR by topic keywords matching file name
|
|
61
63
|
const topicWords = lesson.topic.toLowerCase().split(/[:\-_]/);
|
|
62
64
|
const fileMatches = changed_files.some(f => {
|
|
@@ -146,10 +148,12 @@ export async function handleTeamTool(
|
|
|
146
148
|
author?: string; tags?: string[];
|
|
147
149
|
};
|
|
148
150
|
let lessons: TeamLesson[] = [];
|
|
149
|
-
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
151
|
+
if (lessonKeys.length > 0) {
|
|
152
|
+
const raws = await redis.mget(...lessonKeys);
|
|
153
|
+
for (const raw of raws) {
|
|
154
|
+
const l = safeJsonParse<TeamLesson | null>(raw ?? null, null);
|
|
155
|
+
if (l) lessons.push(l);
|
|
156
|
+
}
|
|
153
157
|
}
|
|
154
158
|
|
|
155
159
|
// Filter
|
|
@@ -278,10 +282,12 @@ export async function handleTeamTool(
|
|
|
278
282
|
|
|
279
283
|
type RawLesson = { topic: string; outcome: string; what_worked: string; severity?: string; ts: string; auto_learned?: boolean };
|
|
280
284
|
const allLessons: RawLesson[] = [];
|
|
281
|
-
|
|
282
|
-
const
|
|
283
|
-
|
|
284
|
-
|
|
285
|
+
if (allLessonKeys.length > 0) {
|
|
286
|
+
const raws = await redis.mget(...allLessonKeys);
|
|
287
|
+
for (const raw of raws) {
|
|
288
|
+
const l = safeJsonParse<RawLesson | null>(raw ?? null, null);
|
|
289
|
+
if (l) allLessons.push(l);
|
|
290
|
+
}
|
|
285
291
|
}
|
|
286
292
|
|
|
287
293
|
// Group lessons by top-level category
|
|
@@ -367,18 +373,17 @@ export async function handleTeamTool(
|
|
|
367
373
|
verified_at?: string; severity?: string; audit_trail?: unknown[];
|
|
368
374
|
};
|
|
369
375
|
const lessons: DrLesson[] = [];
|
|
370
|
-
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
|
|
376
|
+
if (lessonKeys.length > 0) {
|
|
377
|
+
const raws = await redis.mget(...lessonKeys);
|
|
378
|
+
for (const raw of raws) {
|
|
379
|
+
const l = safeJsonParse<DrLesson | null>(raw ?? null, null);
|
|
380
|
+
if (l) lessons.push(l);
|
|
381
|
+
}
|
|
374
382
|
}
|
|
375
383
|
|
|
376
384
|
// Last session
|
|
377
385
|
const lastSessionRaw = await redis.get('cachly:session:last');
|
|
378
|
-
|
|
379
|
-
if (lastSessionRaw) {
|
|
380
|
-
try { lastSession = JSON.parse(lastSessionRaw); } catch { /* ignore */ }
|
|
381
|
-
}
|
|
386
|
+
const lastSession = safeJsonParse<{ ts: string; summary: string } | null>(lastSessionRaw, null);
|
|
382
387
|
|
|
383
388
|
// Open failures
|
|
384
389
|
const openFailures = lessons.filter(l => l.outcome === 'failure' || l.outcome === 'partial');
|
|
@@ -509,13 +514,24 @@ export async function handleTeamTool(
|
|
|
509
514
|
issues.push(`💡 ${unusedLessons.length} lessons never recalled — verify topics match your workflow`);
|
|
510
515
|
}
|
|
511
516
|
|
|
512
|
-
const
|
|
517
|
+
const divider = '─'.repeat(48);
|
|
518
|
+
const lines = [
|
|
519
|
+
`🩺 **Brain Doctor**`,
|
|
520
|
+
`\`${divider}\``,
|
|
521
|
+
'',
|
|
522
|
+
'**📊 Health Checks**',
|
|
523
|
+
...checks.map(c => ' ' + c),
|
|
524
|
+
'',
|
|
525
|
+
];
|
|
513
526
|
if (issues.length > 0) {
|
|
514
|
-
lines.push(
|
|
515
|
-
|
|
527
|
+
lines.push(`**🔧 Action Items** (${issues.length} issue${issues.length > 1 ? 's' : ''})`);
|
|
528
|
+
issues.forEach((item, idx) => lines.push(` ${idx + 1}. ${item}`));
|
|
516
529
|
lines.push('');
|
|
530
|
+
lines.push(`_Fix the action items above, then re-run \`brain_doctor\` to verify._`);
|
|
517
531
|
} else {
|
|
518
|
-
lines.push('
|
|
532
|
+
lines.push('**✅ All checks passed** — Brain is healthy!');
|
|
533
|
+
lines.push('');
|
|
534
|
+
lines.push('_Keep the brain sharp: `session_start` → work → `learn_from_attempts` → `session_end`_');
|
|
519
535
|
}
|
|
520
536
|
return lines.join('\n');
|
|
521
537
|
}
|
|
@@ -536,7 +552,11 @@ export async function handleTeamTool(
|
|
|
536
552
|
.filter((l): l is NonNullable<typeof l> => l !== null && new Date(l.ts).getTime() <= cutoff)
|
|
537
553
|
.sort((a, b) => new Date(a.ts).getTime() - new Date(b.ts).getTime());
|
|
538
554
|
|
|
539
|
-
if (before.length === 0)
|
|
555
|
+
if (before.length === 0) {
|
|
556
|
+
const first = safeJsonParse(all[0] ?? null, null as null | { ts?: string });
|
|
557
|
+
const earliest = first?.ts ? new Date(first.ts).toLocaleDateString('de-DE') : 'unknown';
|
|
558
|
+
return `📭 No entries for \`${topic}\` found before **${date}**. Earliest entry: ${earliest}.`;
|
|
559
|
+
}
|
|
540
560
|
|
|
541
561
|
const lines = [
|
|
542
562
|
`🏺 **Brain Archaeology: \`${topic}\` before ${date}**`,
|
|
@@ -563,7 +583,7 @@ export async function handleTeamTool(
|
|
|
563
583
|
const raw = await redis.get(depKey);
|
|
564
584
|
if (!raw) return `📭 No lessons found that depend on \`${dependency}\`.\n\nAdd dependencies via: \`learn_from_attempts(..., depends_on=["${dependency}"])\``;
|
|
565
585
|
|
|
566
|
-
const topics: string[] =
|
|
586
|
+
const topics: string[] = safeJsonParse<string[]>(raw, []);
|
|
567
587
|
const lines = [
|
|
568
588
|
`🔗 **Causal Chain: \`${dependency}\`** — ${topics.length} dependent lesson${topics.length === 1 ? '' : 's'}`,
|
|
569
589
|
'',
|
|
@@ -572,7 +592,8 @@ export async function handleTeamTool(
|
|
|
572
592
|
for (const t of topics) {
|
|
573
593
|
const lessonRaw = await redis.get(`cachly:lesson:best:${t}`);
|
|
574
594
|
if (!lessonRaw) { lines.push(` • \`${t}\` _(lesson deleted)_`); continue; }
|
|
575
|
-
const lesson =
|
|
595
|
+
const lesson = safeJsonParse(lessonRaw, null as null | { outcome?: string; severity?: string; needs_review?: boolean });
|
|
596
|
+
if (!lesson) { lines.push(` • \`${t}\` _(lesson corrupted)_`); continue; }
|
|
576
597
|
const emoji = lesson.outcome === 'success' ? '✅' : lesson.outcome === 'partial' ? '⚠️' : '❌';
|
|
577
598
|
const reviewBadge = lesson.needs_review ? ' 🔍 **needs_review**' : '';
|
|
578
599
|
lines.push(` ${emoji} \`${t}\` (${lesson.severity ?? 'major'})${reviewBadge}`);
|
|
@@ -614,8 +635,8 @@ export async function handleTeamTool(
|
|
|
614
635
|
// Preserve recall_count on update
|
|
615
636
|
const existing = await redis.get(key);
|
|
616
637
|
if (existing) {
|
|
617
|
-
const prev =
|
|
618
|
-
record.recall_count = prev
|
|
638
|
+
const prev = safeJsonParse(existing, null as null | { recall_count?: number });
|
|
639
|
+
record.recall_count = prev?.recall_count ?? 0;
|
|
619
640
|
}
|
|
620
641
|
await redis.set(key, JSON.stringify(record));
|
|
621
642
|
return `🌐 **Global lesson stored**: \`${topic}\`\n\n${lesson}\n\nRecallable from any project via \`global_recall(topic="${topic}")\`.`;
|
|
@@ -635,10 +656,12 @@ export async function handleTeamTool(
|
|
|
635
656
|
|
|
636
657
|
type GlobalLesson = { topic: string; lesson: string; severity?: string; ts: string; recall_count?: number };
|
|
637
658
|
let lessons: GlobalLesson[] = [];
|
|
638
|
-
|
|
639
|
-
const
|
|
640
|
-
|
|
641
|
-
|
|
659
|
+
if (keys.length > 0) {
|
|
660
|
+
const raws = await redis.mget(...keys);
|
|
661
|
+
for (const raw of raws) {
|
|
662
|
+
const l = safeJsonParse<GlobalLesson | null>(raw ?? null, null);
|
|
663
|
+
if (l) lessons.push(l);
|
|
664
|
+
}
|
|
642
665
|
}
|
|
643
666
|
|
|
644
667
|
if (topic) {
|
|
@@ -655,7 +678,8 @@ export async function handleTeamTool(
|
|
|
655
678
|
const k = `cachly:global:lesson:${l.topic}`;
|
|
656
679
|
const raw = await redis.get(k);
|
|
657
680
|
if (raw) {
|
|
658
|
-
const rec =
|
|
681
|
+
const rec = safeJsonParse(raw, null as null | { recall_count?: number });
|
|
682
|
+
if (!rec) continue;
|
|
659
683
|
rec.recall_count = (rec.recall_count ?? 0) + 1;
|
|
660
684
|
await redis.set(k, JSON.stringify(rec));
|
|
661
685
|
}
|