@cachly-dev/mcp-server 0.10.0 → 0.10.2

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.
@@ -6,7 +6,6 @@ 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';
10
9
  import { computeEmbedding, hasEmbedProvider } from '../embeddings.js';
11
10
 
12
11
  // Git concurrency semaphore (for brain_from_git parallel workers)
@@ -398,8 +397,7 @@ export async function handleFedbrainTool(
398
397
  const raw = await redis.get(`cachly:lesson:best:${lesson_key}`);
399
398
  if (!raw) return `❌ Lesson \`${lesson_key}\` not found. Store it first with \`learn_from_attempts\`.`;
400
399
 
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\`.`;
400
+ const lesson = JSON.parse(raw) as { topic: string; outcome: string; what_worked: string; what_failed?: string; tags?: string[]; commands?: string[]; severity?: string; ts?: string };
403
401
 
404
402
  const domainTokens = [lesson.topic.split(':')[0], ...(lesson.tags ?? [])].filter(Boolean);
405
403
  const domainFingerprint = [...new Set(domainTokens)].sort().join(',');
@@ -749,8 +747,7 @@ export async function handleFedbrainTool(
749
747
  }
750
748
 
751
749
  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 };
752
- const crystal = safeJsonParse<Crystal | null>(raw, null);
753
- if (!crystal) return `❌ Memory Crystal data is corrupted. Re-create with \`memory_crystalize()\`.`;
750
+ const crystal: Crystal = JSON.parse(raw);
754
751
  const age = Math.floor((Date.now() - new Date(crystal.ts).getTime()) / 86400000);
755
752
  const freshEmoji = age <= 7 ? '🟢' : age <= 30 ? '🟡' : '🔴';
756
753
 
@@ -787,55 +784,49 @@ export async function handleFedbrainTool(
787
784
  const crystalRaw = await redis.get('cachly:crystal:latest');
788
785
  if (crystalRaw) {
789
786
  type Crystal = { label: string; ts: string; session_count: number; lesson_count: number; top_patterns: Array<{ category: string; insight: string; count: number }> };
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
- }
787
+ const crystal: Crystal = JSON.parse(crystalRaw);
788
+ lines.push(`### 💎 Memory Crystal: ${crystal.label}`);
789
+ lines.push(`Compressed from ${crystal.session_count} sessions, ${crystal.lesson_count} lessons.`);
790
+ const topN = focus
791
+ ? crystal.top_patterns.filter(p => p.category.toLowerCase().includes(focus.toLowerCase()) || p.insight.toLowerCase().includes(focus.toLowerCase())).slice(0, 4)
792
+ : crystal.top_patterns.slice(0, 4);
793
+ for (const p of topN) lines.push(` • **${p.category}**: ${p.insight.slice(0, 100)}`);
794
+ lines.push('');
800
795
  }
801
796
 
802
797
  // 2. Last session summary
803
798
  const lastSession = await redis.get('cachly:session:last');
804
799
  if (lastSession) {
805
800
  type Session = { summary?: string; ts?: string; focus?: string };
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
- }
801
+ const sess: Session = JSON.parse(lastSession);
802
+ lines.push(`### 🕐 Last Session`);
803
+ if (sess.focus) lines.push(`Focus: _${sess.focus}_`);
804
+ if (sess.summary) lines.push(`Summary: ${sess.summary.slice(0, 300)}`);
805
+ lines.push('');
813
806
  }
814
807
 
815
808
  // 3. Session handoff
816
809
  const handoff = await redis.get('cachly:session:handoff');
817
810
  if (handoff) {
818
811
  type Handoff = { remaining_tasks?: string[]; instructions?: string; context_summary?: string; blocked_on?: string };
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('');
812
+ const h: Handoff = JSON.parse(handoff);
813
+ lines.push(`### 📋 Handoff (from last window)`);
814
+ if (h.context_summary) lines.push(`Context: ${h.context_summary.slice(0, 200)}`);
815
+ if (h.remaining_tasks?.length) {
816
+ lines.push(`Remaining tasks:`);
817
+ for (const t of h.remaining_tasks.slice(0, 5)) lines.push(` • ${t}`);
830
818
  }
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('');
831
822
  }
832
823
 
833
824
  // 4. WIP registry
834
825
  const wipRaw = await redis.get('cachly:ctx:wip-registry');
835
826
  if (wipRaw) {
836
827
  type Ctx = { content?: string };
837
- const wip = safeJsonParse<Ctx | null>(wipRaw, null);
838
- if (wip?.content) {
828
+ const wip: Ctx = JSON.parse(wipRaw);
829
+ if (wip.content) {
839
830
  lines.push(`### 🔧 WIP Registry`);
840
831
  lines.push(wip.content.slice(0, 400));
841
832
  lines.push('');
@@ -1035,14 +1026,12 @@ export async function handleFedbrainTool(
1035
1026
  for (const nk of nodeKeys.slice(0, 5)) {
1036
1027
  const nodeRaw = await redis.get(nk);
1037
1028
  if (!nodeRaw) continue;
1038
- const node = safeJsonParse<CKGNode | null>(nodeRaw, null);
1039
- if (!node) continue;
1029
+ const node: CKGNode = JSON.parse(nodeRaw);
1040
1030
  const edgeKeys = await redis.smembers(`cachly:ckg:idx:from:${node.id}`);
1041
1031
  for (const ek of edgeKeys.slice(0, 20)) {
1042
1032
  const edgeRaw = await redis.get(ek);
1043
1033
  if (!edgeRaw) continue;
1044
- const edge = safeJsonParse<CKGEdge | null>(edgeRaw, null);
1045
- if (!edge) continue;
1034
+ const edge: CKGEdge = JSON.parse(edgeRaw);
1046
1035
  if (edge.edgeType !== 'causes' && edge.edgeType !== 'degrades_under') continue;
1047
1036
 
1048
1037
  // Look up fix for this failure from CKG 'fixes' edges
@@ -1051,13 +1040,12 @@ export async function handleFedbrainTool(
1051
1040
  for (const fek of fixEdgeKeys.slice(0, 10)) {
1052
1041
  const feRaw = await redis.get(fek);
1053
1042
  if (!feRaw) continue;
1054
- const fe = safeJsonParse<CKGEdge | null>(feRaw, null);
1055
- if (!fe) continue;
1043
+ const fe: CKGEdge = JSON.parse(feRaw);
1056
1044
  if (fe.edgeType === 'fixes') {
1057
1045
  const lessonRaw = await redis.get(`cachly:lesson:best:${fe.from}`);
1058
1046
  if (lessonRaw) {
1059
- const lesson = safeJsonParse(lessonRaw, null as null | { what_worked?: string });
1060
- fix = lesson?.what_worked?.slice(0, 120);
1047
+ const lesson = JSON.parse(lessonRaw) as { what_worked?: string };
1048
+ fix = lesson.what_worked?.slice(0, 120);
1061
1049
  break;
1062
1050
  }
1063
1051
  }
@@ -5,7 +5,6 @@ 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';
9
8
 
10
9
  type GetConnection = (instanceId: string) => Promise<Redis>;
11
10
  type ApiFetch = <T>(path: string, options?: RequestInit) => Promise<T>;
@@ -54,11 +53,10 @@ export async function handleTeamTool(
54
53
 
55
54
  type Lesson = { topic: string; what_worked: string; outcome: string; file_paths?: string[] };
56
55
  const relevant: string[] = [];
57
- const sfRaws = lessonKeys.length > 0 ? await redis.mget(...lessonKeys) : [];
58
- for (const raw of sfRaws) {
56
+ for (const k of lessonKeys) {
57
+ const raw = await redis.get(k);
59
58
  if (!raw) continue;
60
- const lesson = safeJsonParse<Lesson | null>(raw, null);
61
- if (!lesson) continue;
59
+ const lesson = JSON.parse(raw) as Lesson;
62
60
  // Match by file_paths stored in lesson OR by topic keywords matching file name
63
61
  const topicWords = lesson.topic.toLowerCase().split(/[:\-_]/);
64
62
  const fileMatches = changed_files.some(f => {
@@ -148,12 +146,10 @@ export async function handleTeamTool(
148
146
  author?: string; tags?: string[];
149
147
  };
150
148
  let lessons: TeamLesson[] = [];
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
- }
149
+ for (const k of lessonKeys) {
150
+ const raw = await redis.get(k);
151
+ if (!raw) continue;
152
+ try { lessons.push(JSON.parse(raw) as TeamLesson); } catch { /* skip */ }
157
153
  }
158
154
 
159
155
  // Filter
@@ -282,12 +278,10 @@ export async function handleTeamTool(
282
278
 
283
279
  type RawLesson = { topic: string; outcome: string; what_worked: string; severity?: string; ts: string; auto_learned?: boolean };
284
280
  const allLessons: RawLesson[] = [];
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
- }
281
+ for (const k of allLessonKeys) {
282
+ const raw = await redis.get(k);
283
+ if (!raw) continue;
284
+ try { allLessons.push(JSON.parse(raw) as RawLesson); } catch { /* skip */ }
291
285
  }
292
286
 
293
287
  // Group lessons by top-level category
@@ -373,17 +367,18 @@ export async function handleTeamTool(
373
367
  verified_at?: string; severity?: string; audit_trail?: unknown[];
374
368
  };
375
369
  const lessons: DrLesson[] = [];
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
- }
370
+ for (const k of lessonKeys) {
371
+ const raw = await redis.get(k);
372
+ if (!raw) continue;
373
+ try { lessons.push(JSON.parse(raw) as DrLesson); } catch { /* skip */ }
382
374
  }
383
375
 
384
376
  // Last session
385
377
  const lastSessionRaw = await redis.get('cachly:session:last');
386
- const lastSession = safeJsonParse<{ ts: string; summary: string } | null>(lastSessionRaw, null);
378
+ let lastSession: { ts: string; summary: string } | null = null;
379
+ if (lastSessionRaw) {
380
+ try { lastSession = JSON.parse(lastSessionRaw); } catch { /* ignore */ }
381
+ }
387
382
 
388
383
  // Open failures
389
384
  const openFailures = lessons.filter(l => l.outcome === 'failure' || l.outcome === 'partial');
@@ -514,24 +509,13 @@ export async function handleTeamTool(
514
509
  issues.push(`💡 ${unusedLessons.length} lessons never recalled — verify topics match your workflow`);
515
510
  }
516
511
 
517
- const divider = '─'.repeat(48);
518
- const lines = [
519
- `🩺 **Brain Doctor**`,
520
- `\`${divider}\``,
521
- '',
522
- '**📊 Health Checks**',
523
- ...checks.map(c => ' ' + c),
524
- '',
525
- ];
512
+ const lines = ['🩺 **Brain Doctor Report**', '', ...checks.map(c => ' ' + c), ''];
526
513
  if (issues.length > 0) {
527
- lines.push(`**🔧 Action Items** (${issues.length} issue${issues.length > 1 ? 's' : ''})`);
528
- issues.forEach((item, idx) => lines.push(` ${idx + 1}. ${item}`));
514
+ lines.push('**Issues to fix:**');
515
+ for (const i of issues) lines.push(' ' + i);
529
516
  lines.push('');
530
- lines.push(`_Fix the action items above, then re-run \`brain_doctor\` to verify._`);
531
517
  } else {
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`_');
518
+ lines.push(' 🎉 Brain looks healthy! Keep calling session_start/session_end.');
535
519
  }
536
520
  return lines.join('\n');
537
521
  }
@@ -552,11 +536,7 @@ export async function handleTeamTool(
552
536
  .filter((l): l is NonNullable<typeof l> => l !== null && new Date(l.ts).getTime() <= cutoff)
553
537
  .sort((a, b) => new Date(a.ts).getTime() - new Date(b.ts).getTime());
554
538
 
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
- }
539
+ if (before.length === 0) return `📭 No entries for \`${topic}\` found before **${date}**. Earliest entry: ${new Date(JSON.parse(all[0]).ts).toLocaleDateString('de-DE')}.`;
560
540
 
561
541
  const lines = [
562
542
  `🏺 **Brain Archaeology: \`${topic}\` before ${date}**`,
@@ -583,7 +563,7 @@ export async function handleTeamTool(
583
563
  const raw = await redis.get(depKey);
584
564
  if (!raw) return `📭 No lessons found that depend on \`${dependency}\`.\n\nAdd dependencies via: \`learn_from_attempts(..., depends_on=["${dependency}"])\``;
585
565
 
586
- const topics: string[] = safeJsonParse<string[]>(raw, []);
566
+ const topics: string[] = JSON.parse(raw);
587
567
  const lines = [
588
568
  `🔗 **Causal Chain: \`${dependency}\`** — ${topics.length} dependent lesson${topics.length === 1 ? '' : 's'}`,
589
569
  '',
@@ -592,8 +572,7 @@ export async function handleTeamTool(
592
572
  for (const t of topics) {
593
573
  const lessonRaw = await redis.get(`cachly:lesson:best:${t}`);
594
574
  if (!lessonRaw) { lines.push(` • \`${t}\` _(lesson deleted)_`); continue; }
595
- const lesson = safeJsonParse(lessonRaw, null as null | { outcome?: string; severity?: string; needs_review?: boolean });
596
- if (!lesson) { lines.push(` • \`${t}\` _(lesson corrupted)_`); continue; }
575
+ const lesson = JSON.parse(lessonRaw) as { outcome?: string; severity?: string; needs_review?: boolean };
597
576
  const emoji = lesson.outcome === 'success' ? '✅' : lesson.outcome === 'partial' ? '⚠️' : '❌';
598
577
  const reviewBadge = lesson.needs_review ? ' 🔍 **needs_review**' : '';
599
578
  lines.push(` ${emoji} \`${t}\` (${lesson.severity ?? 'major'})${reviewBadge}`);
@@ -635,8 +614,8 @@ export async function handleTeamTool(
635
614
  // Preserve recall_count on update
636
615
  const existing = await redis.get(key);
637
616
  if (existing) {
638
- const prev = safeJsonParse(existing, null as null | { recall_count?: number });
639
- record.recall_count = prev?.recall_count ?? 0;
617
+ const prev = JSON.parse(existing) as { recall_count?: number };
618
+ record.recall_count = prev.recall_count ?? 0;
640
619
  }
641
620
  await redis.set(key, JSON.stringify(record));
642
621
  return `🌐 **Global lesson stored**: \`${topic}\`\n\n${lesson}\n\nRecallable from any project via \`global_recall(topic="${topic}")\`.`;
@@ -656,12 +635,10 @@ export async function handleTeamTool(
656
635
 
657
636
  type GlobalLesson = { topic: string; lesson: string; severity?: string; ts: string; recall_count?: number };
658
637
  let lessons: GlobalLesson[] = [];
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
- }
638
+ for (const k of keys) {
639
+ const raw = await redis.get(k);
640
+ if (!raw) continue;
641
+ try { lessons.push(JSON.parse(raw) as GlobalLesson); } catch { /* skip */ }
665
642
  }
666
643
 
667
644
  if (topic) {
@@ -678,8 +655,7 @@ export async function handleTeamTool(
678
655
  const k = `cachly:global:lesson:${l.topic}`;
679
656
  const raw = await redis.get(k);
680
657
  if (raw) {
681
- const rec = safeJsonParse(raw, null as null | { recall_count?: number });
682
- if (!rec) continue;
658
+ const rec = JSON.parse(raw) as { recall_count?: number };
683
659
  rec.recall_count = (rec.recall_count ?? 0) + 1;
684
660
  await redis.set(k, JSON.stringify(rec));
685
661
  }