@cachly-dev/mcp-server 0.10.4 → 0.10.6
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 +45 -0
- package/LAUNCH_TEXTS.md +182 -0
- package/LICENSE +202 -0
- package/dist/__tests__/auth.test.d.ts +1 -0
- package/dist/__tests__/auth.test.js +107 -0
- package/dist/__tests__/auth.test.js.map +1 -0
- 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/handlers/fedbrain.js +55 -38
- package/dist/handlers/fedbrain.js.map +1 -1
- package/dist/handlers/roadmap.js +8 -2
- package/dist/handlers/roadmap.js.map +1 -1
- package/dist/handlers/syndicate.js +15 -7
- package/dist/handlers/syndicate.js.map +1 -1
- package/dist/handlers/team.js +62 -50
- package/dist/handlers/team.js.map +1 -1
- package/dist/index.js +222 -217
- package/dist/index.js.map +1 -1
- package/dist/tools.d.ts +0 -8
- package/dist/tools.js +5 -15
- 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 +29 -0
- package/llms.txt +155 -0
- package/package.json +29 -13
- package/scripts/postinstall.js +12 -19
- package/server.json +3 -9
- package/smithery.yaml +6 -8
- package/src/__tests__/auth.test.ts +147 -0
- package/src/auth.ts +46 -0
- package/src/handlers/advanced.ts +37 -23
- package/src/handlers/brain.ts +141 -47
- package/src/handlers/fedbrain.ts +43 -31
- package/src/handlers/roadmap.ts +7 -2
- package/src/handlers/syndicate.ts +13 -7
- package/src/handlers/team.ts +57 -33
- package/src/index.ts +224 -234
- package/src/tools.ts +5 -16
- package/src/utils.ts +10 -0
- package/dist/__tests__/brain-predict-failures.test.d.ts +0 -17
- package/dist/__tests__/brain-predict-failures.test.js +0 -339
- package/dist/__tests__/brain-predict-failures.test.js.map +0 -1
- package/src/__tests__/brain-predict-failures.test.ts +0 -430
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/roadmap.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Redis } from 'ioredis';
|
|
2
|
+
import { safeJsonParse } from '../utils.js';
|
|
2
3
|
|
|
3
4
|
type GetConnection = (instanceId: string) => Promise<Redis>;
|
|
4
5
|
type ApiFetch = <T>(path: string, options?: RequestInit) => Promise<T>;
|
|
@@ -67,7 +68,8 @@ export async function handleRoadmapTool(
|
|
|
67
68
|
const redis = await getConnection(rid);
|
|
68
69
|
const raw = await redis.hget(`cachly:roadmap:${rid}`, itemId);
|
|
69
70
|
if (!raw) return `⚠️ **roadmap_update** – Item \`${itemId}\` not found. Use \`roadmap_list\` to see all items.`;
|
|
70
|
-
const item =
|
|
71
|
+
const item = safeJsonParse<Record<string, unknown> | null>(raw, null);
|
|
72
|
+
if (!item) return `⚠️ **roadmap_update** – Item \`${itemId}\` data is corrupted. Re-add with \`roadmap_add\`.`;
|
|
71
73
|
const oldStatus = item.status as string;
|
|
72
74
|
if (newStatus) item.status = newStatus;
|
|
73
75
|
if (newPriority) item.priority = newPriority;
|
|
@@ -106,7 +108,10 @@ export async function handleRoadmapTool(
|
|
|
106
108
|
const PRIORITY_ICON: Record<string, string> = { critical: '🔴', high: '🟠', medium: '🟡', low: '🔵' };
|
|
107
109
|
const STATUS_ICON: Record<string, string> = { planned: '📋', 'in-progress': '⚡', done: '✅', blocked: '🚫', cancelled: '🗑️' };
|
|
108
110
|
const openStatuses = new Set(['planned', 'in-progress', 'blocked']);
|
|
109
|
-
let items = Object.values(all).
|
|
111
|
+
let items = Object.values(all).flatMap(v => {
|
|
112
|
+
const item = safeJsonParse<Record<string, string | string[]> | null>(v as string, null);
|
|
113
|
+
return item ? [item] : [];
|
|
114
|
+
});
|
|
110
115
|
// Filter
|
|
111
116
|
if (filterStatus === 'open') items = items.filter(i => openStatuses.has(i.status as string));
|
|
112
117
|
else if (filterStatus) items = items.filter(i => i.status === filterStatus);
|
|
@@ -4,6 +4,7 @@ import { calculateConfidence } from '../confidence.js';
|
|
|
4
4
|
import { ckgSlug, ckgUpdateEdge } from '../ckg.js';
|
|
5
5
|
import type { CKGEdge, CKGNode } from '../ckg.js';
|
|
6
6
|
import { keywordSearch, tokenize } from '../search.js';
|
|
7
|
+
import { safeJsonParse } from '../utils.js';
|
|
7
8
|
|
|
8
9
|
type GetConnection = (instanceId: string) => Promise<Redis>;
|
|
9
10
|
type ApiFetch = <T>(path: string, options?: RequestInit) => Promise<T>;
|
|
@@ -292,7 +293,8 @@ export async function handleSyndicateTool(
|
|
|
292
293
|
for (const ek of [...fromKeys, ...toKeys].slice(0, 50)) {
|
|
293
294
|
const raw = await redis.get(ek);
|
|
294
295
|
if (!raw) continue;
|
|
295
|
-
const edge
|
|
296
|
+
const edge = safeJsonParse<CKGEdge | null>(raw, null);
|
|
297
|
+
if (!edge) continue;
|
|
296
298
|
allEdges.push(edge);
|
|
297
299
|
if (hop < max_hops) {
|
|
298
300
|
if (!visited.has(edge.from)) queue.push({ id: edge.from, hop: hop + 1 });
|
|
@@ -366,16 +368,18 @@ export async function handleSyndicateTool(
|
|
|
366
368
|
for (const nk of nodeKeys.slice(0, 5)) {
|
|
367
369
|
const nodeRaw = await redis.get(nk);
|
|
368
370
|
if (!nodeRaw) continue;
|
|
369
|
-
const node
|
|
371
|
+
const node = safeJsonParse<CKGNode | null>(nodeRaw, null);
|
|
372
|
+
if (!node) continue;
|
|
370
373
|
const edgeKeys = await redis.smembers(`cachly:ckg:idx:from:${node.id}`);
|
|
371
374
|
for (const ek of edgeKeys.slice(0, 20)) {
|
|
372
375
|
const edgeRaw = await redis.get(ek);
|
|
373
376
|
if (!edgeRaw) continue;
|
|
374
|
-
const edge
|
|
377
|
+
const edge = safeJsonParse<CKGEdge | null>(edgeRaw, null);
|
|
378
|
+
if (!edge) continue;
|
|
375
379
|
// Only interested in fixes and co-occurs for prediction
|
|
376
380
|
if (edge.edgeType !== 'fixes' && edge.edgeType !== 'co-occurs' && edge.edgeType !== 'causes') continue;
|
|
377
381
|
const lessonRaw = await redis.get(`cachly:lesson:best:${edge.from}`);
|
|
378
|
-
const lesson = lessonRaw ?
|
|
382
|
+
const lesson = lessonRaw ? safeJsonParse<{ what_worked?: string; topic: string } | null>(lessonRaw, null) ?? undefined : undefined;
|
|
379
383
|
predictions.push({ concept: node.id, edgeType: edge.edgeType, target: edge.to, confidence: edge.confidence, lesson });
|
|
380
384
|
}
|
|
381
385
|
}
|
|
@@ -389,15 +393,17 @@ export async function handleSyndicateTool(
|
|
|
389
393
|
for (const k of lessonKeys) {
|
|
390
394
|
const raw = await redis.get(k);
|
|
391
395
|
if (!raw) continue;
|
|
392
|
-
|
|
393
|
-
|
|
396
|
+
{
|
|
397
|
+
type PredLesson = { topic: string; what_worked?: string; what_failed?: string; outcome: string; severity?: string; ts: string; verified_at?: string; recall_count?: number };
|
|
398
|
+
const l = safeJsonParse<PredLesson | null>(raw, null);
|
|
399
|
+
if (!l) continue;
|
|
394
400
|
const haystack = [l.topic, l.what_failed ?? '', l.what_worked ?? ''].join(' ').toLowerCase();
|
|
395
401
|
const score = ctxTokens.reduce((s, t) => s + (haystack.includes(t) ? 1 : 0), 0);
|
|
396
402
|
if (score >= 1 && l.outcome !== 'failure') {
|
|
397
403
|
const conf = calculateConfidence(l);
|
|
398
404
|
textPredictions.push({ ...l, confidence: conf });
|
|
399
405
|
}
|
|
400
|
-
}
|
|
406
|
+
}
|
|
401
407
|
}
|
|
402
408
|
textPredictions.sort((a, b) => b.confidence - a.confidence);
|
|
403
409
|
|
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
|
}
|