@cachly-dev/mcp-server 0.10.23 → 0.10.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/CHANGELOG.md +0 -122
  2. package/README.md +112 -136
  3. package/dist/__tests__/brain-predict-failures.test.d.ts +17 -0
  4. package/dist/__tests__/brain-predict-failures.test.js +339 -0
  5. package/dist/__tests__/brain-predict-failures.test.js.map +1 -0
  6. package/dist/handlers/brain.js +67 -7
  7. package/dist/handlers/brain.js.map +1 -1
  8. package/dist/handlers/fedbrain.js +44 -93
  9. package/dist/handlers/fedbrain.js.map +1 -1
  10. package/dist/handlers/roadmap.js +2 -8
  11. package/dist/handlers/roadmap.js.map +1 -1
  12. package/dist/handlers/syndicate.js +7 -15
  13. package/dist/handlers/syndicate.js.map +1 -1
  14. package/dist/handlers/team.js +50 -62
  15. package/dist/handlers/team.js.map +1 -1
  16. package/dist/index.js +122 -979
  17. package/dist/index.js.map +1 -1
  18. package/dist/tools.d.ts +54 -3
  19. package/dist/tools.js +62 -8
  20. package/dist/tools.js.map +1 -1
  21. package/package.json +13 -29
  22. package/scripts/postinstall.js +12 -12
  23. package/server.json +9 -3
  24. package/smithery.yaml +8 -15
  25. package/src/__tests__/brain-predict-failures.test.ts +430 -0
  26. package/src/handlers/brain.ts +64 -7
  27. package/src/handlers/fedbrain.ts +38 -88
  28. package/src/handlers/roadmap.ts +2 -7
  29. package/src/handlers/syndicate.ts +7 -13
  30. package/src/handlers/team.ts +33 -57
  31. package/src/index.ts +135 -1011
  32. package/src/tools.ts +66 -8
  33. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -50
  34. package/.github/ISSUE_TEMPLATE/config.yml +0 -8
  35. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -25
  36. package/.github/workflows/publish.yml +0 -72
  37. package/LAUNCH_TEXTS.md +0 -182
  38. package/LICENSE +0 -202
  39. package/dist/__tests__/auth.test.d.ts +0 -1
  40. package/dist/__tests__/auth.test.js +0 -107
  41. package/dist/__tests__/auth.test.js.map +0 -1
  42. package/glama.json +0 -36
  43. package/llms.txt +0 -155
  44. package/src/__tests__/auth.test.ts +0 -147
@@ -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('');
@@ -893,8 +884,8 @@ export async function handleFedbrainTool(
893
884
 
894
885
  // ── brain_from_git ────────────────────────────────────────────────────────
895
886
  case 'brain_from_git': {
896
- const { instance_id, repo_path = '.', limit = 100, branch = 'HEAD', since = '', incremental = true } = args as {
897
- instance_id: string; repo_path?: string; limit?: number; branch?: string; since?: string; incremental?: boolean;
887
+ const { instance_id, repo_path = '.', limit = 100, branch = 'HEAD', since = '' } = args as {
888
+ instance_id: string; repo_path?: string; limit?: number; branch?: string; since?: string;
898
889
  };
899
890
  const redis = await getConnection(instance_id);
900
891
  const { execSync } = await import('node:child_process');
@@ -913,21 +904,9 @@ export async function handleFedbrainTool(
913
904
  return `❌ Not a git repository: \`${repoDir}\`. Pass \`repo_path\` pointing to a git checkout.`;
914
905
  }
915
906
 
916
- // Incremental mode: resume from last processed commit SHA
917
- const lastShaKey = `cachly:brain_from_git:last_sha:${Buffer.from(repoDir).toString('base64').slice(0, 32)}`;
918
- let lastSha = '';
919
- let isIncremental = false;
920
- if (incremental !== false && !since) {
921
- lastSha = (await redis.get(lastShaKey)) ?? '';
922
- isIncremental = Boolean(lastSha);
923
- }
924
-
925
907
  // Build git log command
926
- // In incremental mode, only fetch commits after the last processed SHA
927
908
  const sinceFlag = since ? `--since="${since}"` : '';
928
- const afterFlag = isIncremental ? `${lastSha}..HEAD` : '';
929
- const revRange = afterFlag || branch;
930
- const logCmd = `git log ${revRange} ${sinceFlag} --pretty=format:"%H|||%s|||%ad|||%an" --date=short --no-merges -n ${maxCommits}`;
909
+ const logCmd = `git log ${branch} ${sinceFlag} --pretty=format:"%H|||%s|||%ad|||%an" --date=short --no-merges -n ${maxCommits}`;
931
910
 
932
911
  let logOutput = '';
933
912
  try {
@@ -942,9 +921,6 @@ export async function handleFedbrainTool(
942
921
  });
943
922
 
944
923
  if (commits.length === 0) {
945
- if (isIncremental) {
946
- return `✅ brain_from_git: Brain is up to date — no new commits since last run (last SHA: \`${lastSha.slice(0, 8)}\`).`;
947
- }
948
924
  return `⚠️ No commits found in \`${repoDir}\` on branch \`${branch}\`${since ? ` since ${since}` : ''}.`;
949
925
  }
950
926
 
@@ -975,21 +951,8 @@ export async function handleFedbrainTool(
975
951
  let ingested = 0;
976
952
  let skipped = 0;
977
953
  const categoryCount = new Map<string, number>();
978
- const total = commits.length;
979
- const progressInterval = Math.max(1, Math.floor(total / 10)); // emit ~10 progress updates
980
- let lastProgressAt = 0;
981
-
982
- process.stderr.write(`\n🧠 brain_from_git: processing ${total} commits from ${repoDir}...\n`);
983
-
984
- for (let i = 0; i < commits.length; i++) {
985
- const commit = commits[i]!;
986
-
987
- // Emit progress to stderr so editors can display it in MCP logs
988
- if (i - lastProgressAt >= progressInterval) {
989
- process.stderr.write(` ⏳ Processing ${i}/${total} commits (${ingested} lessons so far)...\n`);
990
- lastProgressAt = i;
991
- }
992
954
 
955
+ for (const commit of commits) {
993
956
  if (!commit.subject) { skipped++; continue; }
994
957
  const { category, outcome, severity } = classifyCommit(commit.subject);
995
958
  const domain = extractDomain(commit.subject);
@@ -1024,20 +987,11 @@ export async function handleFedbrainTool(
1024
987
  ingested++;
1025
988
  }
1026
989
 
1027
- process.stderr.write(` ✅ brain_from_git complete: ${ingested}/${total} commits ingested\n\n`);
1028
-
1029
- // Save the latest commit SHA for incremental runs
1030
- if (commits.length > 0 && commits[0]?.sha) {
1031
- await redis.set(lastShaKey, commits[0].sha, 'EX', 90 * 24 * 3600); // expire after 90 days
1032
- }
1033
-
1034
990
  const categoryBreakdown = [...categoryCount.entries()].sort((a, b) => b[1] - a[1]).map(([k, v]) => ` • **${k}** (${v})`).join('\n');
1035
991
 
1036
992
  return [
1037
- `🔁 **brain_from_git: ${repoDir}**`,
1038
- isIncremental ? `🔄 Incremental mode — only new commits since \`${lastSha.slice(0, 8)}\` were processed` : '',
1039
- ``,
1040
- `📂 Branch: \`${revRange}\` | Processed: **${commits.length}** commits | Ingested: **${ingested}** lessons | Skipped: ${skipped}`,
993
+ `🔁 **brain_from_git: ${repoDir}**`, '',
994
+ `📂 Branch: \`${branch}\` | Processed: **${commits.length}** commits | Ingested: **${ingested}** lessons | Skipped: ${skipped}`,
1041
995
  ``,
1042
996
  `**Breakdown by category:**`,
1043
997
  categoryBreakdown,
@@ -1045,8 +999,7 @@ export async function handleFedbrainTool(
1045
999
  `💡 New lessons are stored with confidence 0.55 (auto-inferred).`,
1046
1000
  `💡 As you confirm them via \`learn_from_attempts\`, confidence rises automatically.`,
1047
1001
  `🔍 Explore: \`brain_search(query="fix")\` | \`ckg_inspect(concept="deploy")\``,
1048
- `💾 Next run will automatically continue from the latest commit (incremental mode).`,
1049
- ].filter(Boolean).join('\n');
1002
+ ].join('\n');
1050
1003
  } finally {
1051
1004
  _gitSemRelease();
1052
1005
  }
@@ -1073,14 +1026,12 @@ export async function handleFedbrainTool(
1073
1026
  for (const nk of nodeKeys.slice(0, 5)) {
1074
1027
  const nodeRaw = await redis.get(nk);
1075
1028
  if (!nodeRaw) continue;
1076
- const node = safeJsonParse<CKGNode | null>(nodeRaw, null);
1077
- if (!node) continue;
1029
+ const node: CKGNode = JSON.parse(nodeRaw);
1078
1030
  const edgeKeys = await redis.smembers(`cachly:ckg:idx:from:${node.id}`);
1079
1031
  for (const ek of edgeKeys.slice(0, 20)) {
1080
1032
  const edgeRaw = await redis.get(ek);
1081
1033
  if (!edgeRaw) continue;
1082
- const edge = safeJsonParse<CKGEdge | null>(edgeRaw, null);
1083
- if (!edge) continue;
1034
+ const edge: CKGEdge = JSON.parse(edgeRaw);
1084
1035
  if (edge.edgeType !== 'causes' && edge.edgeType !== 'degrades_under') continue;
1085
1036
 
1086
1037
  // Look up fix for this failure from CKG 'fixes' edges
@@ -1089,13 +1040,12 @@ export async function handleFedbrainTool(
1089
1040
  for (const fek of fixEdgeKeys.slice(0, 10)) {
1090
1041
  const feRaw = await redis.get(fek);
1091
1042
  if (!feRaw) continue;
1092
- const fe = safeJsonParse<CKGEdge | null>(feRaw, null);
1093
- if (!fe) continue;
1043
+ const fe: CKGEdge = JSON.parse(feRaw);
1094
1044
  if (fe.edgeType === 'fixes') {
1095
1045
  const lessonRaw = await redis.get(`cachly:lesson:best:${fe.from}`);
1096
1046
  if (lessonRaw) {
1097
- const lesson = safeJsonParse(lessonRaw, null as null | { what_worked?: string });
1098
- 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);
1099
1049
  break;
1100
1050
  }
1101
1051
  }
@@ -1,5 +1,4 @@
1
1
  import type { Redis } from 'ioredis';
2
- import { safeJsonParse } from '../utils.js';
3
2
 
4
3
  type GetConnection = (instanceId: string) => Promise<Redis>;
5
4
  type ApiFetch = <T>(path: string, options?: RequestInit) => Promise<T>;
@@ -68,8 +67,7 @@ export async function handleRoadmapTool(
68
67
  const redis = await getConnection(rid);
69
68
  const raw = await redis.hget(`cachly:roadmap:${rid}`, itemId);
70
69
  if (!raw) return `⚠️ **roadmap_update** – Item \`${itemId}\` not found. Use \`roadmap_list\` to see all items.`;
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\`.`;
70
+ const item = JSON.parse(raw) as Record<string, unknown>;
73
71
  const oldStatus = item.status as string;
74
72
  if (newStatus) item.status = newStatus;
75
73
  if (newPriority) item.priority = newPriority;
@@ -108,10 +106,7 @@ export async function handleRoadmapTool(
108
106
  const PRIORITY_ICON: Record<string, string> = { critical: '🔴', high: '🟠', medium: '🟡', low: '🔵' };
109
107
  const STATUS_ICON: Record<string, string> = { planned: '📋', 'in-progress': '⚡', done: '✅', blocked: '🚫', cancelled: '🗑️' };
110
108
  const openStatuses = new Set(['planned', 'in-progress', 'blocked']);
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
- });
109
+ let items = Object.values(all).map(v => JSON.parse(v as string) as Record<string, string | string[]>);
115
110
  // Filter
116
111
  if (filterStatus === 'open') items = items.filter(i => openStatuses.has(i.status as string));
117
112
  else if (filterStatus) items = items.filter(i => i.status === filterStatus);
@@ -4,7 +4,6 @@ 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';
8
7
 
9
8
  type GetConnection = (instanceId: string) => Promise<Redis>;
10
9
  type ApiFetch = <T>(path: string, options?: RequestInit) => Promise<T>;
@@ -293,8 +292,7 @@ export async function handleSyndicateTool(
293
292
  for (const ek of [...fromKeys, ...toKeys].slice(0, 50)) {
294
293
  const raw = await redis.get(ek);
295
294
  if (!raw) continue;
296
- const edge = safeJsonParse<CKGEdge | null>(raw, null);
297
- if (!edge) continue;
295
+ const edge: CKGEdge = JSON.parse(raw);
298
296
  allEdges.push(edge);
299
297
  if (hop < max_hops) {
300
298
  if (!visited.has(edge.from)) queue.push({ id: edge.from, hop: hop + 1 });
@@ -368,18 +366,16 @@ export async function handleSyndicateTool(
368
366
  for (const nk of nodeKeys.slice(0, 5)) {
369
367
  const nodeRaw = await redis.get(nk);
370
368
  if (!nodeRaw) continue;
371
- const node = safeJsonParse<CKGNode | null>(nodeRaw, null);
372
- if (!node) continue;
369
+ const node: CKGNode = JSON.parse(nodeRaw);
373
370
  const edgeKeys = await redis.smembers(`cachly:ckg:idx:from:${node.id}`);
374
371
  for (const ek of edgeKeys.slice(0, 20)) {
375
372
  const edgeRaw = await redis.get(ek);
376
373
  if (!edgeRaw) continue;
377
- const edge = safeJsonParse<CKGEdge | null>(edgeRaw, null);
378
- if (!edge) continue;
374
+ const edge: CKGEdge = JSON.parse(edgeRaw);
379
375
  // Only interested in fixes and co-occurs for prediction
380
376
  if (edge.edgeType !== 'fixes' && edge.edgeType !== 'co-occurs' && edge.edgeType !== 'causes') continue;
381
377
  const lessonRaw = await redis.get(`cachly:lesson:best:${edge.from}`);
382
- const lesson = lessonRaw ? safeJsonParse<{ what_worked?: string; topic: string } | null>(lessonRaw, null) ?? undefined : undefined;
378
+ const lesson = lessonRaw ? JSON.parse(lessonRaw) : undefined;
383
379
  predictions.push({ concept: node.id, edgeType: edge.edgeType, target: edge.to, confidence: edge.confidence, lesson });
384
380
  }
385
381
  }
@@ -393,17 +389,15 @@ export async function handleSyndicateTool(
393
389
  for (const k of lessonKeys) {
394
390
  const raw = await redis.get(k);
395
391
  if (!raw) continue;
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;
392
+ try {
393
+ const l = JSON.parse(raw) as { topic: string; what_worked?: string; what_failed?: string; outcome: string; severity?: string; ts: string; verified_at?: string; recall_count?: number };
400
394
  const haystack = [l.topic, l.what_failed ?? '', l.what_worked ?? ''].join(' ').toLowerCase();
401
395
  const score = ctxTokens.reduce((s, t) => s + (haystack.includes(t) ? 1 : 0), 0);
402
396
  if (score >= 1 && l.outcome !== 'failure') {
403
397
  const conf = calculateConfidence(l);
404
398
  textPredictions.push({ ...l, confidence: conf });
405
399
  }
406
- }
400
+ } catch { /* skip */ }
407
401
  }
408
402
  textPredictions.sort((a, b) => b.confidence - a.confidence);
409
403
 
@@ -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
  }