@cachly-dev/mcp-server 0.5.24 → 0.5.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cachly-dev/mcp-server",
3
- "version": "0.5.24",
3
+ "version": "0.5.25",
4
4
  "mcpName": "io.github.cachly-dev/mcp-server",
5
5
  "description": "AI memory brain for Claude Code, Cursor, Copilot & Windsurf — persistent sessions, lessons learned, semantic search, team brain. 38 MCP tools.",
6
6
  "type": "module",
package/src/index.ts CHANGED
@@ -2,6 +2,8 @@
2
2
  import { randomUUID } from 'node:crypto';
3
3
  import { createHash } from 'node:crypto';
4
4
  import { readdir, readFile, stat } from 'node:fs/promises';
5
+ import { readFileSync } from 'node:fs';
6
+ import { execSync } from 'node:child_process';
5
7
  import { join, relative, extname } from 'node:path';
6
8
  /**
7
9
  * cachly MCP Server v0.4.0
@@ -52,7 +54,7 @@ import { Redis } from 'ioredis';
52
54
  const API_URL = process.env.CACHLY_API_URL ?? 'https://api.cachly.dev';
53
55
  const JWT = process.env.CACHLY_JWT ?? '';
54
56
  const EMBED_MODEL = process.env.CACHLY_EMBED_MODEL ?? '';
55
- const CURRENT_VERSION = '0.5.24';
57
+ const CURRENT_VERSION = '0.5.25';
56
58
  /**
57
59
  * ┌──────────────────────────────────────────────────────────────────────┐
58
60
  * │ EMBEDDING PROVIDER — pluggable, client-side first │
@@ -1349,6 +1351,10 @@ const TOOLS = [
1349
1351
  items: { type: 'string' },
1350
1352
  description: 'Prerequisites this lesson depends on (e.g. ["node:>=20", "docker:running", "wireguard:active"]). When a dependency is marked stale, all dependent lessons get needs_review.',
1351
1353
  },
1354
+ author: {
1355
+ type: 'string',
1356
+ description: 'Name or handle of the person storing this lesson (e.g. "alice", "bob"). Used for Team Telepathy — teammates see each other\'s lessons in session_start.',
1357
+ },
1352
1358
  },
1353
1359
  required: ['instance_id', 'topic', 'outcome', 'what_worked'],
1354
1360
  },
@@ -1393,7 +1399,8 @@ const TOOLS = [
1393
1399
  description:
1394
1400
  'Single-call session briefing. Call this at the START of every session INSTEAD of multiple separate smart_recall/recall_best_solution calls. ' +
1395
1401
  'Returns: last session summary, recent lessons sorted by recency, relevant lessons for your focus area, ' +
1396
- 'open failures (topics with only failure outcomes), and brain health stats (lesson count, context count). ' +
1402
+ 'open failures (topics with only failure outcomes), brain health stats, team telepathy (what teammates learned this week), ' +
1403
+ 'predictive pre-warnings (if your focus area has known failure patterns), and memory crystals (compressed wisdom from old sessions). ' +
1397
1404
  'Also saves a session start marker so session_end can compute duration.',
1398
1405
  inputSchema: {
1399
1406
  type: 'object',
@@ -1403,6 +1410,10 @@ const TOOLS = [
1403
1410
  type: 'string',
1404
1411
  description: 'Keywords for what you plan to work on today (e.g. "deploy infra api"). Used to surface relevant lessons at the top.',
1405
1412
  },
1413
+ author: {
1414
+ type: 'string',
1415
+ description: 'Your name or handle (e.g. "alice"). Enables Team Telepathy — filters YOUR lessons vs TEAM lessons from past 7 days.',
1416
+ },
1406
1417
  },
1407
1418
  required: ['instance_id'],
1408
1419
  },
@@ -1413,7 +1424,8 @@ const TOOLS = [
1413
1424
  'Save a session summary when you finish working. ' +
1414
1425
  'Records what was accomplished, files changed, and lesson count. ' +
1415
1426
  'The next session_start will show this summary as "Last session". ' +
1416
- 'Call this when ending a work session, before going idle, or before summarizing.',
1427
+ 'Call this when ending a work session, before going idle, or before summarizing. ' +
1428
+ 'Ambient Learning: if workspace_path is provided, reads git log since session start and auto-learns from commits.',
1417
1429
  inputSchema: {
1418
1430
  type: 'object',
1419
1431
  properties: {
@@ -1431,6 +1443,10 @@ const TOOLS = [
1431
1443
  type: 'number',
1432
1444
  description: 'Number of new lessons stored this session (optional)',
1433
1445
  },
1446
+ workspace_path: {
1447
+ type: 'string',
1448
+ description: 'Absolute path to the project root (e.g. "/Users/you/myproject"). Enables Ambient Learning — reads git log since session start and auto-learns from commit messages.',
1449
+ },
1434
1450
  },
1435
1451
  required: ['instance_id', 'summary'],
1436
1452
  },
@@ -1594,16 +1610,40 @@ const TOOLS = [
1594
1610
  required: ['instance_id', 'topic'],
1595
1611
  },
1596
1612
  },
1613
+ {
1614
+ name: 'memory_crystalize',
1615
+ description:
1616
+ 'Compress the last 30-50 sessions and auto-learned lessons into a dense Memory Crystal. ' +
1617
+ 'A crystal is a compact, structured summary of everything the brain learned — grouped by category (deploy, fix, debug, …). ' +
1618
+ 'Crystals survive session cleanup and appear in session_start once enough sessions have accumulated. ' +
1619
+ 'Run this monthly or after a big milestone to preserve institutional knowledge. ' +
1620
+ 'Returns a digest of what was crystallized.',
1621
+ inputSchema: {
1622
+ type: 'object',
1623
+ properties: {
1624
+ instance_id: { type: 'string', description: 'UUID of the cache instance' },
1625
+ label: {
1626
+ type: 'string',
1627
+ description: 'Optional label for this crystal (e.g. "Q1 2026", "v2 launch"). Auto-generated from date if omitted.',
1628
+ },
1629
+ },
1630
+ required: ['instance_id'],
1631
+ },
1632
+ },
1597
1633
  {
1598
1634
  name: 'brain_doctor',
1599
1635
  description:
1600
1636
  'Check the health of your AI Brain and get actionable recommendations. ' +
1601
- 'Reports: lesson count, context entries, last session age, open failures, quality score. ' +
1637
+ 'Reports: lesson count, context entries, last session age, open failures, quality score, effective IQ boost, stale index. ' +
1602
1638
  'Returns a prioritized list of issues with fix instructions.',
1603
1639
  inputSchema: {
1604
1640
  type: 'object',
1605
1641
  properties: {
1606
1642
  instance_id: { type: 'string', description: 'UUID of the cache instance' },
1643
+ workspace_path: {
1644
+ type: 'string',
1645
+ description: 'Absolute path to workspace root — enables package.json analysis for openclaw cross-promo (optional)',
1646
+ },
1607
1647
  },
1608
1648
  required: ['instance_id'],
1609
1649
  },
@@ -2781,6 +2821,7 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
2781
2821
  commands = [],
2782
2822
  tags = [],
2783
2823
  depends_on = [],
2824
+ author = '',
2784
2825
  } = args as {
2785
2826
  instance_id: string;
2786
2827
  topic: string;
@@ -2793,6 +2834,7 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
2793
2834
  commands?: string[];
2794
2835
  tags?: string[];
2795
2836
  depends_on?: string[];
2837
+ author?: string;
2796
2838
  };
2797
2839
 
2798
2840
  const redis = await getConnection(instance_id);
@@ -2902,6 +2944,7 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
2902
2944
  commands,
2903
2945
  tags,
2904
2946
  depends_on,
2947
+ ...(author ? { author } : {}),
2905
2948
  recall_count: recallCount,
2906
2949
  ts,
2907
2950
  verified_at: outcome === 'success' || outcome === 'partial' ? ts : undefined,
@@ -3226,7 +3269,7 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
3226
3269
 
3227
3270
  // ── session_start ─────────────────────────────────────────────────────────
3228
3271
  case 'session_start': {
3229
- const { instance_id, focus = '' } = args as { instance_id: string; focus?: string };
3272
+ const { instance_id, focus = '', author = '' } = args as { instance_id: string; focus?: string; author?: string };
3230
3273
  const redis = await getConnection(instance_id);
3231
3274
 
3232
3275
  // 1. Scan all best-solution lessons
@@ -3437,6 +3480,74 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
3437
3480
  lines.push('');
3438
3481
  }
3439
3482
 
3483
+ // ── 🔮 Predictive Pre-Warning — intent-based danger detection ────────────
3484
+ // Fires BEFORE work starts. If focus area has known failure patterns → warn loudly.
3485
+ if (focusTerms.length > 0) {
3486
+ type LessonWithAuthor = typeof lessons[0] & { author?: string };
3487
+ const dangerLessons = (lessons as LessonWithAuthor[]).filter(l => {
3488
+ if (l.outcome === 'success') return false;
3489
+ const topicCategory = l.topic.split(':')[0];
3490
+ return focusTerms.some(term =>
3491
+ l.topic.toLowerCase().includes(term) ||
3492
+ topicCategory === term ||
3493
+ (l.tags ?? []).some((t: string) => t.toLowerCase() === term),
3494
+ );
3495
+ });
3496
+ if (dangerLessons.length >= 1) {
3497
+ lines.unshift(''); // push to top after title
3498
+ lines.unshift(` ❌ ${dangerLessons.slice(0, 3).map(l => `\`${l.topic}\` — ${(l.what_failed ?? l.what_worked).slice(0, 80)}`).join('\n ❌ ')}`);
3499
+ lines.unshift(` Known pitfalls for **"${focus}"** (${dangerLessons.length} past failure${dangerLessons.length > 1 ? 's' : ''}):`);
3500
+ lines.unshift(`🚨 **PRE-WARNING** — Read this BEFORE starting:`);
3501
+ }
3502
+ }
3503
+
3504
+ // ── 👥 Team Telepathy — what teammates learned this week ─────────────────
3505
+ if (author) {
3506
+ type LessonWithAuthor = typeof lessons[0] & { author?: string };
3507
+ const oneWeekAgo = Date.now() - 7 * 86_400_000;
3508
+ const teamLessons = (lessons as LessonWithAuthor[]).filter(l =>
3509
+ l.author && l.author !== author && new Date(l.ts).getTime() > oneWeekAgo,
3510
+ );
3511
+ if (teamLessons.length > 0) {
3512
+ // Group by author
3513
+ const byAuthor = new Map<string, LessonWithAuthor[]>();
3514
+ for (const l of teamLessons) {
3515
+ const a = l.author!;
3516
+ if (!byAuthor.has(a)) byAuthor.set(a, []);
3517
+ byAuthor.get(a)!.push(l);
3518
+ }
3519
+ lines.push(`👥 **Team this week** (${teamLessons.length} lesson${teamLessons.length > 1 ? 's' : ''} from teammates):`);
3520
+ for (const [teamAuthor, tls] of byAuthor) {
3521
+ lines.push(` 👤 **${teamAuthor}**:`);
3522
+ for (const l of tls.slice(0, 3)) {
3523
+ const emoji = l.outcome === 'success' ? '✅' : l.outcome === 'partial' ? '⚠️' : '❌';
3524
+ lines.push(` ${emoji} \`${l.topic}\` — ${l.what_worked.slice(0, 80)}`);
3525
+ }
3526
+ if (tls.length > 3) lines.push(` … and ${tls.length - 3} more`);
3527
+ }
3528
+ lines.push('');
3529
+ }
3530
+ }
3531
+
3532
+ // ── 💎 Memory Crystal — compressed wisdom from old sessions ──────────────
3533
+ try {
3534
+ const crystalRaw = await redis.get('cachly:crystal:latest');
3535
+ if (crystalRaw) {
3536
+ const crystal = JSON.parse(crystalRaw) as {
3537
+ label: string; ts: string; session_count: number;
3538
+ top_patterns: Array<{ category: string; insight: string; count: number }>;
3539
+ };
3540
+ const crystalAge = Math.round((Date.now() - new Date(crystal.ts).getTime()) / 86_400_000);
3541
+ if (crystalAge <= 90) {
3542
+ lines.push(`💎 **Memory Crystal** (${crystal.label} · ${crystal.session_count} sessions compressed):`);
3543
+ for (const p of crystal.top_patterns.slice(0, 3)) {
3544
+ lines.push(` • **${p.category}** (${p.count}×): ${p.insight.slice(0, 90)}`);
3545
+ }
3546
+ lines.push('');
3547
+ }
3548
+ }
3549
+ } catch { /* non-critical */ }
3550
+
3440
3551
  return lines.join('\n');
3441
3552
  }
3442
3553
 
@@ -3447,11 +3558,13 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
3447
3558
  summary,
3448
3559
  files_changed = [],
3449
3560
  lessons_learned,
3561
+ workspace_path = '',
3450
3562
  } = args as {
3451
3563
  instance_id: string;
3452
3564
  summary: string;
3453
3565
  files_changed?: string[];
3454
3566
  lessons_learned?: number;
3567
+ workspace_path?: string;
3455
3568
  };
3456
3569
 
3457
3570
  const redis = await getConnection(instance_id);
@@ -3569,6 +3682,53 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
3569
3682
  }
3570
3683
  } catch { /* auto-learn errors must never break session_end */ }
3571
3684
 
3685
+ // ── 🌿 Ambient Git Learning ────────────────────────────────────────────────
3686
+ // Read git commits since session start → auto-learn each meaningful commit.
3687
+ const ambientLearned: string[] = [];
3688
+ if (workspace_path) {
3689
+ try {
3690
+ // Get the session start time (stored by session_start)
3691
+ const sessionStartTs = currentRaw
3692
+ ? (() => { try { return (JSON.parse(currentRaw) as { started?: string }).started ?? ''; } catch { return ''; } })()
3693
+ : '';
3694
+ const sinceArg = sessionStartTs ? `--since="${sessionStartTs}"` : '--since="1 hour ago"';
3695
+ const gitOut = execSync(
3696
+ `git -C "${workspace_path}" log ${sinceArg} --oneline --format="%H|||%s|||%ai"`,
3697
+ { timeout: 5000, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] },
3698
+ ).trim();
3699
+ if (gitOut) {
3700
+ const commitActionRe = /\b(fix|add|remove|refactor|migrate|update|resolve|implement|improve|optimize|configure|create|delete|disable|enable|debug|patch|upgrade|build|rewrite|deploy|feat|chore|docs|test|perf|ci)\b/i;
3701
+ for (const line of gitOut.split('\n').slice(0, 10)) {
3702
+ const [hash, msg, dateStr] = line.split('|||');
3703
+ if (!msg || !commitActionRe.test(msg)) continue;
3704
+ const slug = msg
3705
+ .toLowerCase().replace(/^(fix|feat|chore|docs|test|ci|perf|refactor|build|revert)[:(\s]/i, '')
3706
+ .replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 40);
3707
+ if (!slug) continue;
3708
+ const topic = `git:${slug}`;
3709
+ const key = `cachly:lesson:best:${topic}`;
3710
+ const existing = await redis.get(key);
3711
+ if (existing) continue; // don't overwrite existing
3712
+ const commitLesson = {
3713
+ topic,
3714
+ outcome: 'success' as const,
3715
+ what_worked: msg.slice(0, 200),
3716
+ context: `Auto-learned from git commit ${(hash ?? '').slice(0, 7)} at ${dateStr ?? ''} in ${workspace_path}`,
3717
+ severity: 'minor' as const,
3718
+ ts: now.toISOString(),
3719
+ recall_count: 0,
3720
+ auto_learned: true,
3721
+ source: 'ambient-git',
3722
+ version: 3,
3723
+ };
3724
+ await redis.set(key, JSON.stringify(commitLesson));
3725
+ await redis.expire(key, 60 * 86400); // 60 day TTL for git lessons
3726
+ ambientLearned.push(topic);
3727
+ }
3728
+ }
3729
+ } catch { /* git not available or not a repo — silent skip */ }
3730
+ }
3731
+
3572
3732
  const durationStr = durationMin !== undefined ? ` · ${durationMin} min` : '';
3573
3733
  return [
3574
3734
  `✅ **Session saved**${durationStr}`,
@@ -3577,6 +3737,7 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
3577
3737
  files_changed.length > 0 ? `📁 **Files changed:** ${files_changed.map(f => `\`${f}\``).join(', ')}` : '',
3578
3738
  lessons_learned !== undefined ? `🧠 **Lessons stored:** ${lessons_learned}` : '',
3579
3739
  autoLearned.length > 0 ? `🤖 **Auto-learned:** ${autoLearned.length} lessons extracted from summary (${autoLearned.slice(0, 3).map(t => `\`${t}\``).join(', ')}${autoLearned.length > 3 ? '…' : ''})` : '',
3740
+ ambientLearned.length > 0 ? `🌿 **Ambient git learning:** ${ambientLearned.length} commit${ambientLearned.length > 1 ? 's' : ''} auto-learned (${ambientLearned.slice(0, 3).map(t => `\`${t}\``).join(', ')}${ambientLearned.length > 3 ? '…' : ''})` : '',
3580
3741
  ``,
3581
3742
  `💡 Next session: \`session_start(focus="...")\` to see this summary.`,
3582
3743
  ].filter(l => l !== '').join('\n');
@@ -3932,8 +4093,87 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
3932
4093
  }
3933
4094
 
3934
4095
  // ── brain_doctor ──────────────────────────────────────────────────────────
4096
+ // ── memory_crystalize ─────────────────────────────────────────────────────
4097
+ case 'memory_crystalize': {
4098
+ const { instance_id, label: crystalLabel = '' } = args as { instance_id: string; label?: string };
4099
+ const redis = await getConnection(instance_id);
4100
+ const now = new Date();
4101
+ const week = `${now.getFullYear()}-W${String(Math.ceil((now.getDate() - now.getDay() + 10) / 7)).padStart(2, '0')}`;
4102
+ const effectiveLabel = crystalLabel || `${now.toISOString().slice(0, 7)} Crystal`;
4103
+
4104
+ // Read session history
4105
+ const sessionHistory = await redis.lrange('cachly:session:history', 0, 49);
4106
+
4107
+ // Read all auto-learned lessons
4108
+ const allLessonKeys: string[] = [];
4109
+ const ls = redis.scanStream({ match: 'cachly:lesson:best:*', count: 200 });
4110
+ await new Promise<void>((res, rej) => {
4111
+ ls.on('data', (b: string[]) => allLessonKeys.push(...b));
4112
+ ls.on('end', res);
4113
+ ls.on('error', rej);
4114
+ });
4115
+
4116
+ type RawLesson = { topic: string; outcome: string; what_worked: string; severity?: string; ts: string; auto_learned?: boolean };
4117
+ const allLessons: RawLesson[] = [];
4118
+ for (const k of allLessonKeys) {
4119
+ const raw = await redis.get(k);
4120
+ if (!raw) continue;
4121
+ try { allLessons.push(JSON.parse(raw) as RawLesson); } catch { /* skip */ }
4122
+ }
4123
+
4124
+ // Group lessons by top-level category
4125
+ const categoryMap = new Map<string, RawLesson[]>();
4126
+ for (const l of allLessons) {
4127
+ const cat = l.topic.split(':')[0] || 'misc';
4128
+ if (!categoryMap.has(cat)) categoryMap.set(cat, []);
4129
+ categoryMap.get(cat)!.push(l);
4130
+ }
4131
+
4132
+ // Build top patterns (most frequent categories with a representative insight)
4133
+ const topPatterns: Array<{ category: string; insight: string; count: number }> = [];
4134
+ for (const [cat, catLessons] of [...categoryMap.entries()].sort((a, b) => b[1].length - a[1].length).slice(0, 8)) {
4135
+ const successLessons = catLessons.filter(l => l.outcome === 'success');
4136
+ const best = successLessons[0] ?? catLessons[0];
4137
+ if (!best) continue;
4138
+ topPatterns.push({
4139
+ category: cat,
4140
+ insight: best.what_worked.slice(0, 120),
4141
+ count: catLessons.length,
4142
+ });
4143
+ }
4144
+
4145
+ const crystal = {
4146
+ label: effectiveLabel,
4147
+ ts: now.toISOString(),
4148
+ session_count: sessionHistory.length,
4149
+ lesson_count: allLessons.length,
4150
+ top_patterns: topPatterns,
4151
+ categories: [...categoryMap.keys()],
4152
+ created_from: `${sessionHistory.length} sessions, ${allLessons.length} lessons`,
4153
+ };
4154
+
4155
+ const crystalJson = JSON.stringify(crystal);
4156
+ await redis.set('cachly:crystal:latest', crystalJson);
4157
+ await redis.expire('cachly:crystal:latest', 90 * 86400);
4158
+ await redis.set(`cachly:crystal:${week}`, crystalJson);
4159
+ await redis.expire(`cachly:crystal:${week}`, 365 * 86400);
4160
+
4161
+ const lines = [
4162
+ `💎 **Memory Crystal created: ${effectiveLabel}**`,
4163
+ ``,
4164
+ `📊 Compressed: **${sessionHistory.length} sessions** + **${allLessons.length} lessons** → ${topPatterns.length} top patterns`,
4165
+ ``,
4166
+ `**Top patterns by category:**`,
4167
+ ...topPatterns.slice(0, 6).map(p => ` • **${p.category}** (${p.count}×): ${p.insight.slice(0, 90)}`),
4168
+ ``,
4169
+ `💡 This crystal will appear in every future \`session_start\` briefing.`,
4170
+ `💡 Re-run \`memory_crystalize\` monthly to keep it fresh.`,
4171
+ ];
4172
+ return lines.join('\n');
4173
+ }
4174
+
3935
4175
  case 'brain_doctor': {
3936
- const { instance_id } = args as { instance_id: string };
4176
+ const { instance_id, workspace_path: drWorkspacePath = '' } = args as { instance_id: string; workspace_path?: string };
3937
4177
  const redis = await getConnection(instance_id);
3938
4178
  const issues: string[] = [];
3939
4179
  const checks: string[] = [];
@@ -3987,6 +4227,13 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
3987
4227
  const staleLessons = lessons.filter(l => l.outcome === 'success' && calculateConfidence(l) < CONFIDENCE_STALE_VALUE);
3988
4228
  const warnLessons = lessons.filter(l => l.outcome === 'success' && calculateConfidence(l) >= CONFIDENCE_STALE_VALUE && calculateConfidence(l) < CONFIDENCE_WARN_VALUE);
3989
4229
  const withAudit = lessons.filter(l => (l.audit_trail ?? []).length > 1);
4230
+ // Team lessons
4231
+ type DrLessonWithAuthor = DrLesson & { author?: string };
4232
+ const teamLessons = (lessons as DrLessonWithAuthor[]).filter(l => l.author);
4233
+ const uniqueAuthors = new Set((lessons as DrLessonWithAuthor[]).map(l => l.author).filter(Boolean));
4234
+ // Effective IQ boost: total recalls / lessons (how much the brain actually helped)
4235
+ const totalRecalls = lessons.reduce((sum, l) => sum + (l.recall_count ?? 0), 0);
4236
+ const iqBoostPct = lessons.length > 0 ? Math.min(100, Math.round((totalRecalls / lessons.length) * 10)) : 0;
3990
4237
 
3991
4238
  // Quality score (0-100)
3992
4239
  let score = 50;
@@ -3999,14 +4246,66 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
3999
4246
  const unusedRatio = lessons.length > 0 ? unusedLessons.length / lessons.length : 0;
4000
4247
  if (unusedRatio < 0.5) score += 10;
4001
4248
  if (staleLessons.length === 0) score += 5;
4249
+ if (uniqueAuthors.size >= 2) score += 5; // team collaboration bonus
4002
4250
 
4003
4251
  const scoreEmoji = score >= 80 ? '🟢' : score >= 50 ? '🟡' : '🔴';
4252
+ const iqEmoji = iqBoostPct >= 50 ? '🚀' : iqBoostPct >= 20 ? '📈' : '💤';
4004
4253
 
4005
4254
  checks.push(`${scoreEmoji} **Brain Quality Score: ${score}/100**`);
4006
- checks.push(`📚 **Lessons:** ${lessonKeys.length} (${criticalLessons.length} critical · ${withAudit.length} with audit trail)`);
4255
+ checks.push(`${iqEmoji} **Effective IQ Boost: ${iqBoostPct}%** (${totalRecalls} recalls across ${lessons.length} lessons)`);
4256
+ checks.push(`📚 **Lessons:** ${lessonKeys.length} (${criticalLessons.length} critical · ${withAudit.length} with audit trail · ${teamLessons.length} from team)`);
4007
4257
  checks.push(`💾 **Context entries:** ${ctxCount}`);
4008
4258
  checks.push(`🎯 **Confidence:** ${lessons.length - staleLessons.length - warnLessons.length} fresh · ${warnLessons.length} warn · ${staleLessons.length} stale`);
4009
4259
  checks.push(`⏱️ **Decay config:** warn after ${CONFIDENCE_WARN_DAYS}d · stale after ${CONFIDENCE_STALE_DAYS}d`);
4260
+ if (uniqueAuthors.size >= 2) {
4261
+ checks.push(`👥 **Team:** ${uniqueAuthors.size} contributors (${[...uniqueAuthors].join(', ')})`);
4262
+ }
4263
+
4264
+ // Stale index detection
4265
+ try {
4266
+ const lastIndexRaw = await redis.get('cachly:index:last_run');
4267
+ if (lastIndexRaw) {
4268
+ const lastIndexAge = Math.round((Date.now() - new Date(lastIndexRaw).getTime()) / 86_400_000);
4269
+ if (lastIndexAge > 7) {
4270
+ issues.push(`🔄 Index is ${lastIndexAge}d stale — run \`index_project\` to re-sync semantic search`);
4271
+ } else {
4272
+ checks.push(`🗂️ **Semantic index:** ${lastIndexAge}d old (fresh)`);
4273
+ }
4274
+ } else {
4275
+ issues.push(`💡 No semantic index — run \`index_project(dir="<your-src>")\` to enable semantic search`);
4276
+ }
4277
+ } catch { /* non-critical */ }
4278
+
4279
+ // Memory crystal status
4280
+ try {
4281
+ const crystalRaw = await redis.get('cachly:crystal:latest');
4282
+ if (crystalRaw) {
4283
+ const crystal = JSON.parse(crystalRaw) as { ts: string; label: string };
4284
+ const crystalAge = Math.round((Date.now() - new Date(crystal.ts).getTime()) / 86_400_000);
4285
+ checks.push(`💎 **Memory Crystal:** ${crystal.label} (${crystalAge}d ago)`);
4286
+ if (crystalAge > 30) issues.push(`💡 Memory Crystal is ${crystalAge}d old — re-run \`memory_crystalize\` to compress new sessions`);
4287
+ } else if (lessonKeys.length >= 10) {
4288
+ issues.push(`💡 ${lessonKeys.length} lessons but no Memory Crystal — run \`memory_crystalize\` to compress wisdom`);
4289
+ }
4290
+ } catch { /* non-critical */ }
4291
+
4292
+ // openclaw cross-promo (check package.json in workspace)
4293
+ if (drWorkspacePath) {
4294
+ try {
4295
+ const pkgPath = drWorkspacePath.replace(/\/$/, '') + '/package.json';
4296
+ const pkgRaw = readFileSync(pkgPath, 'utf-8');
4297
+ const pkg = JSON.parse(pkgRaw) as { dependencies?: Record<string, string>; devDependencies?: Record<string, string> };
4298
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
4299
+ const hasLLMDep = ['openai', '@anthropic-ai/sdk', '@google/generative-ai', 'mistralai', 'cohere-ai'].some(d => d in allDeps);
4300
+ const hasOpenclaw = '@cachly-dev/openclaw' in allDeps;
4301
+ if (hasLLMDep && !hasOpenclaw) {
4302
+ issues.push(`💡 **openclaw missing:** you use LLM APIs (${Object.keys(allDeps).filter(d => ['openai','@anthropic-ai/sdk'].includes(d)).join(', ')}) but not \`@cachly-dev/openclaw\``);
4303
+ issues.push(` → \`npm install @cachly-dev/openclaw\` cuts LLM costs 60–90% with 3 lines of code`);
4304
+ } else if (hasOpenclaw) {
4305
+ checks.push(`✅ **@cachly-dev/openclaw installed** (LLM cost caching active)`);
4306
+ }
4307
+ } catch { /* no package.json or unreadable */ }
4308
+ }
4010
4309
 
4011
4310
  if (lastSession) {
4012
4311
  const ageMin = Math.round((Date.now() - new Date(lastSession.ts).getTime()) / 60000);
@@ -4022,6 +4321,10 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
4022
4321
  issues.push(`💡 Only ${lessonKeys.length} lessons — add more after each problem solved`);
4023
4322
  }
4024
4323
 
4324
+ if (iqBoostPct === 0 && lessons.length >= 5) {
4325
+ issues.push(`💤 **IQ Boost is 0%** — lessons exist but are never recalled. Use \`recall_best_solution\` BEFORE tasks.`);
4326
+ }
4327
+
4025
4328
  if (ctxCount === 0) {
4026
4329
  issues.push('💡 No context — use `remember_context` to cache architecture docs, ADRs, etc.');
4027
4330
  }