@cachly-dev/mcp-server 0.5.23 → 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/dist/index.js 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
@@ -44,7 +46,7 @@ import { Redis } from 'ioredis';
44
46
  const API_URL = process.env.CACHLY_API_URL ?? 'https://api.cachly.dev';
45
47
  const JWT = process.env.CACHLY_JWT ?? '';
46
48
  const EMBED_MODEL = process.env.CACHLY_EMBED_MODEL ?? '';
47
- const CURRENT_VERSION = '0.5.23';
49
+ const CURRENT_VERSION = '0.5.25';
48
50
  /**
49
51
  * ┌──────────────────────────────────────────────────────────────────────┐
50
52
  * │ EMBEDDING PROVIDER — pluggable, client-side first │
@@ -1214,6 +1216,10 @@ const TOOLS = [
1214
1216
  items: { type: 'string' },
1215
1217
  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.',
1216
1218
  },
1219
+ author: {
1220
+ type: 'string',
1221
+ 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.',
1222
+ },
1217
1223
  },
1218
1224
  required: ['instance_id', 'topic', 'outcome', 'what_worked'],
1219
1225
  },
@@ -1255,7 +1261,8 @@ const TOOLS = [
1255
1261
  name: 'session_start',
1256
1262
  description: 'Single-call session briefing. Call this at the START of every session INSTEAD of multiple separate smart_recall/recall_best_solution calls. ' +
1257
1263
  'Returns: last session summary, recent lessons sorted by recency, relevant lessons for your focus area, ' +
1258
- 'open failures (topics with only failure outcomes), and brain health stats (lesson count, context count). ' +
1264
+ 'open failures (topics with only failure outcomes), brain health stats, team telepathy (what teammates learned this week), ' +
1265
+ 'predictive pre-warnings (if your focus area has known failure patterns), and memory crystals (compressed wisdom from old sessions). ' +
1259
1266
  'Also saves a session start marker so session_end can compute duration.',
1260
1267
  inputSchema: {
1261
1268
  type: 'object',
@@ -1265,6 +1272,10 @@ const TOOLS = [
1265
1272
  type: 'string',
1266
1273
  description: 'Keywords for what you plan to work on today (e.g. "deploy infra api"). Used to surface relevant lessons at the top.',
1267
1274
  },
1275
+ author: {
1276
+ type: 'string',
1277
+ description: 'Your name or handle (e.g. "alice"). Enables Team Telepathy — filters YOUR lessons vs TEAM lessons from past 7 days.',
1278
+ },
1268
1279
  },
1269
1280
  required: ['instance_id'],
1270
1281
  },
@@ -1274,7 +1285,8 @@ const TOOLS = [
1274
1285
  description: 'Save a session summary when you finish working. ' +
1275
1286
  'Records what was accomplished, files changed, and lesson count. ' +
1276
1287
  'The next session_start will show this summary as "Last session". ' +
1277
- 'Call this when ending a work session, before going idle, or before summarizing.',
1288
+ 'Call this when ending a work session, before going idle, or before summarizing. ' +
1289
+ 'Ambient Learning: if workspace_path is provided, reads git log since session start and auto-learns from commits.',
1278
1290
  inputSchema: {
1279
1291
  type: 'object',
1280
1292
  properties: {
@@ -1292,6 +1304,10 @@ const TOOLS = [
1292
1304
  type: 'number',
1293
1305
  description: 'Number of new lessons stored this session (optional)',
1294
1306
  },
1307
+ workspace_path: {
1308
+ type: 'string',
1309
+ 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.',
1310
+ },
1295
1311
  },
1296
1312
  required: ['instance_id', 'summary'],
1297
1313
  },
@@ -1449,15 +1465,38 @@ const TOOLS = [
1449
1465
  required: ['instance_id', 'topic'],
1450
1466
  },
1451
1467
  },
1468
+ {
1469
+ name: 'memory_crystalize',
1470
+ description: 'Compress the last 30-50 sessions and auto-learned lessons into a dense Memory Crystal. ' +
1471
+ 'A crystal is a compact, structured summary of everything the brain learned — grouped by category (deploy, fix, debug, …). ' +
1472
+ 'Crystals survive session cleanup and appear in session_start once enough sessions have accumulated. ' +
1473
+ 'Run this monthly or after a big milestone to preserve institutional knowledge. ' +
1474
+ 'Returns a digest of what was crystallized.',
1475
+ inputSchema: {
1476
+ type: 'object',
1477
+ properties: {
1478
+ instance_id: { type: 'string', description: 'UUID of the cache instance' },
1479
+ label: {
1480
+ type: 'string',
1481
+ description: 'Optional label for this crystal (e.g. "Q1 2026", "v2 launch"). Auto-generated from date if omitted.',
1482
+ },
1483
+ },
1484
+ required: ['instance_id'],
1485
+ },
1486
+ },
1452
1487
  {
1453
1488
  name: 'brain_doctor',
1454
1489
  description: 'Check the health of your AI Brain and get actionable recommendations. ' +
1455
- 'Reports: lesson count, context entries, last session age, open failures, quality score. ' +
1490
+ 'Reports: lesson count, context entries, last session age, open failures, quality score, effective IQ boost, stale index. ' +
1456
1491
  'Returns a prioritized list of issues with fix instructions.',
1457
1492
  inputSchema: {
1458
1493
  type: 'object',
1459
1494
  properties: {
1460
1495
  instance_id: { type: 'string', description: 'UUID of the cache instance' },
1496
+ workspace_path: {
1497
+ type: 'string',
1498
+ description: 'Absolute path to workspace root — enables package.json analysis for openclaw cross-promo (optional)',
1499
+ },
1461
1500
  },
1462
1501
  required: ['instance_id'],
1463
1502
  },
@@ -2465,7 +2504,7 @@ async function handleTool(name, args) {
2465
2504
  return `🗑️ **Forgot ${deleted} context entries.**\n\nKeys: ${keys.map(k => `\`${k}\``).join(', ')}`;
2466
2505
  }
2467
2506
  case 'learn_from_attempts': {
2468
- const { instance_id, topic, outcome, what_worked, what_failed = '', context: ctx = '', severity = 'major', file_paths = [], commands = [], tags = [], depends_on = [], } = args;
2507
+ const { instance_id, topic, outcome, what_worked, what_failed = '', context: ctx = '', severity = 'major', file_paths = [], commands = [], tags = [], depends_on = [], author = '', } = args;
2469
2508
  const redis = await getConnection(instance_id);
2470
2509
  const ts = new Date().toISOString();
2471
2510
  // ── Structured template hints ──────────────────────────────────────────
@@ -2561,6 +2600,7 @@ async function handleTool(name, args) {
2561
2600
  commands,
2562
2601
  tags,
2563
2602
  depends_on,
2603
+ ...(author ? { author } : {}),
2564
2604
  recall_count: recallCount,
2565
2605
  ts,
2566
2606
  verified_at: outcome === 'success' || outcome === 'partial' ? ts : undefined,
@@ -2844,7 +2884,7 @@ async function handleTool(name, args) {
2844
2884
  }
2845
2885
  // ── session_start ─────────────────────────────────────────────────────────
2846
2886
  case 'session_start': {
2847
- const { instance_id, focus = '' } = args;
2887
+ const { instance_id, focus = '', author = '' } = args;
2848
2888
  const redis = await getConnection(instance_id);
2849
2889
  // 1. Scan all best-solution lessons
2850
2890
  const lessonKeys = [];
@@ -3047,11 +3087,71 @@ async function handleTool(name, args) {
3047
3087
  lines.push(` ❌ Failed: ${failures.slice(0, 3).map(d => `\`${d.topic}\``).join(', ')}`);
3048
3088
  lines.push('');
3049
3089
  }
3090
+ // ── 🔮 Predictive Pre-Warning — intent-based danger detection ────────────
3091
+ // Fires BEFORE work starts. If focus area has known failure patterns → warn loudly.
3092
+ if (focusTerms.length > 0) {
3093
+ const dangerLessons = lessons.filter(l => {
3094
+ if (l.outcome === 'success')
3095
+ return false;
3096
+ const topicCategory = l.topic.split(':')[0];
3097
+ return focusTerms.some(term => l.topic.toLowerCase().includes(term) ||
3098
+ topicCategory === term ||
3099
+ (l.tags ?? []).some((t) => t.toLowerCase() === term));
3100
+ });
3101
+ if (dangerLessons.length >= 1) {
3102
+ lines.unshift(''); // push to top after title
3103
+ lines.unshift(` ❌ ${dangerLessons.slice(0, 3).map(l => `\`${l.topic}\` — ${(l.what_failed ?? l.what_worked).slice(0, 80)}`).join('\n ❌ ')}`);
3104
+ lines.unshift(` Known pitfalls for **"${focus}"** (${dangerLessons.length} past failure${dangerLessons.length > 1 ? 's' : ''}):`);
3105
+ lines.unshift(`🚨 **PRE-WARNING** — Read this BEFORE starting:`);
3106
+ }
3107
+ }
3108
+ // ── 👥 Team Telepathy — what teammates learned this week ─────────────────
3109
+ if (author) {
3110
+ const oneWeekAgo = Date.now() - 7 * 86_400_000;
3111
+ const teamLessons = lessons.filter(l => l.author && l.author !== author && new Date(l.ts).getTime() > oneWeekAgo);
3112
+ if (teamLessons.length > 0) {
3113
+ // Group by author
3114
+ const byAuthor = new Map();
3115
+ for (const l of teamLessons) {
3116
+ const a = l.author;
3117
+ if (!byAuthor.has(a))
3118
+ byAuthor.set(a, []);
3119
+ byAuthor.get(a).push(l);
3120
+ }
3121
+ lines.push(`👥 **Team this week** (${teamLessons.length} lesson${teamLessons.length > 1 ? 's' : ''} from teammates):`);
3122
+ for (const [teamAuthor, tls] of byAuthor) {
3123
+ lines.push(` 👤 **${teamAuthor}**:`);
3124
+ for (const l of tls.slice(0, 3)) {
3125
+ const emoji = l.outcome === 'success' ? '✅' : l.outcome === 'partial' ? '⚠️' : '❌';
3126
+ lines.push(` ${emoji} \`${l.topic}\` — ${l.what_worked.slice(0, 80)}`);
3127
+ }
3128
+ if (tls.length > 3)
3129
+ lines.push(` … and ${tls.length - 3} more`);
3130
+ }
3131
+ lines.push('');
3132
+ }
3133
+ }
3134
+ // ── 💎 Memory Crystal — compressed wisdom from old sessions ──────────────
3135
+ try {
3136
+ const crystalRaw = await redis.get('cachly:crystal:latest');
3137
+ if (crystalRaw) {
3138
+ const crystal = JSON.parse(crystalRaw);
3139
+ const crystalAge = Math.round((Date.now() - new Date(crystal.ts).getTime()) / 86_400_000);
3140
+ if (crystalAge <= 90) {
3141
+ lines.push(`💎 **Memory Crystal** (${crystal.label} · ${crystal.session_count} sessions compressed):`);
3142
+ for (const p of crystal.top_patterns.slice(0, 3)) {
3143
+ lines.push(` • **${p.category}** (${p.count}×): ${p.insight.slice(0, 90)}`);
3144
+ }
3145
+ lines.push('');
3146
+ }
3147
+ }
3148
+ }
3149
+ catch { /* non-critical */ }
3050
3150
  return lines.join('\n');
3051
3151
  }
3052
3152
  // ── session_end ───────────────────────────────────────────────────────────
3053
3153
  case 'session_end': {
3054
- const { instance_id, summary, files_changed = [], lessons_learned, } = args;
3154
+ const { instance_id, summary, files_changed = [], lessons_learned, workspace_path = '', } = args;
3055
3155
  const redis = await getConnection(instance_id);
3056
3156
  const now = new Date();
3057
3157
  // Calculate duration from session_start marker
@@ -3161,6 +3261,58 @@ async function handleTool(name, args) {
3161
3261
  }
3162
3262
  }
3163
3263
  catch { /* auto-learn errors must never break session_end */ }
3264
+ // ── 🌿 Ambient Git Learning ────────────────────────────────────────────────
3265
+ // Read git commits since session start → auto-learn each meaningful commit.
3266
+ const ambientLearned = [];
3267
+ if (workspace_path) {
3268
+ try {
3269
+ // Get the session start time (stored by session_start)
3270
+ const sessionStartTs = currentRaw
3271
+ ? (() => { try {
3272
+ return JSON.parse(currentRaw).started ?? '';
3273
+ }
3274
+ catch {
3275
+ return '';
3276
+ } })()
3277
+ : '';
3278
+ const sinceArg = sessionStartTs ? `--since="${sessionStartTs}"` : '--since="1 hour ago"';
3279
+ const gitOut = execSync(`git -C "${workspace_path}" log ${sinceArg} --oneline --format="%H|||%s|||%ai"`, { timeout: 5000, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
3280
+ if (gitOut) {
3281
+ 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;
3282
+ for (const line of gitOut.split('\n').slice(0, 10)) {
3283
+ const [hash, msg, dateStr] = line.split('|||');
3284
+ if (!msg || !commitActionRe.test(msg))
3285
+ continue;
3286
+ const slug = msg
3287
+ .toLowerCase().replace(/^(fix|feat|chore|docs|test|ci|perf|refactor|build|revert)[:(\s]/i, '')
3288
+ .replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 40);
3289
+ if (!slug)
3290
+ continue;
3291
+ const topic = `git:${slug}`;
3292
+ const key = `cachly:lesson:best:${topic}`;
3293
+ const existing = await redis.get(key);
3294
+ if (existing)
3295
+ continue; // don't overwrite existing
3296
+ const commitLesson = {
3297
+ topic,
3298
+ outcome: 'success',
3299
+ what_worked: msg.slice(0, 200),
3300
+ context: `Auto-learned from git commit ${(hash ?? '').slice(0, 7)} at ${dateStr ?? ''} in ${workspace_path}`,
3301
+ severity: 'minor',
3302
+ ts: now.toISOString(),
3303
+ recall_count: 0,
3304
+ auto_learned: true,
3305
+ source: 'ambient-git',
3306
+ version: 3,
3307
+ };
3308
+ await redis.set(key, JSON.stringify(commitLesson));
3309
+ await redis.expire(key, 60 * 86400); // 60 day TTL for git lessons
3310
+ ambientLearned.push(topic);
3311
+ }
3312
+ }
3313
+ }
3314
+ catch { /* git not available or not a repo — silent skip */ }
3315
+ }
3164
3316
  const durationStr = durationMin !== undefined ? ` · ${durationMin} min` : '';
3165
3317
  return [
3166
3318
  `✅ **Session saved**${durationStr}`,
@@ -3169,6 +3321,7 @@ async function handleTool(name, args) {
3169
3321
  files_changed.length > 0 ? `📁 **Files changed:** ${files_changed.map(f => `\`${f}\``).join(', ')}` : '',
3170
3322
  lessons_learned !== undefined ? `🧠 **Lessons stored:** ${lessons_learned}` : '',
3171
3323
  autoLearned.length > 0 ? `🤖 **Auto-learned:** ${autoLearned.length} lessons extracted from summary (${autoLearned.slice(0, 3).map(t => `\`${t}\``).join(', ')}${autoLearned.length > 3 ? '…' : ''})` : '',
3324
+ 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 ? '…' : ''})` : '',
3172
3325
  ``,
3173
3326
  `💡 Next session: \`session_start(focus="...")\` to see this summary.`,
3174
3327
  ].filter(l => l !== '').join('\n');
@@ -3455,8 +3608,83 @@ async function handleTool(name, args) {
3455
3608
  return lines;
3456
3609
  }
3457
3610
  // ── brain_doctor ──────────────────────────────────────────────────────────
3611
+ // ── memory_crystalize ─────────────────────────────────────────────────────
3612
+ case 'memory_crystalize': {
3613
+ const { instance_id, label: crystalLabel = '' } = args;
3614
+ const redis = await getConnection(instance_id);
3615
+ const now = new Date();
3616
+ const week = `${now.getFullYear()}-W${String(Math.ceil((now.getDate() - now.getDay() + 10) / 7)).padStart(2, '0')}`;
3617
+ const effectiveLabel = crystalLabel || `${now.toISOString().slice(0, 7)} Crystal`;
3618
+ // Read session history
3619
+ const sessionHistory = await redis.lrange('cachly:session:history', 0, 49);
3620
+ // Read all auto-learned lessons
3621
+ const allLessonKeys = [];
3622
+ const ls = redis.scanStream({ match: 'cachly:lesson:best:*', count: 200 });
3623
+ await new Promise((res, rej) => {
3624
+ ls.on('data', (b) => allLessonKeys.push(...b));
3625
+ ls.on('end', res);
3626
+ ls.on('error', rej);
3627
+ });
3628
+ const allLessons = [];
3629
+ for (const k of allLessonKeys) {
3630
+ const raw = await redis.get(k);
3631
+ if (!raw)
3632
+ continue;
3633
+ try {
3634
+ allLessons.push(JSON.parse(raw));
3635
+ }
3636
+ catch { /* skip */ }
3637
+ }
3638
+ // Group lessons by top-level category
3639
+ const categoryMap = new Map();
3640
+ for (const l of allLessons) {
3641
+ const cat = l.topic.split(':')[0] || 'misc';
3642
+ if (!categoryMap.has(cat))
3643
+ categoryMap.set(cat, []);
3644
+ categoryMap.get(cat).push(l);
3645
+ }
3646
+ // Build top patterns (most frequent categories with a representative insight)
3647
+ const topPatterns = [];
3648
+ for (const [cat, catLessons] of [...categoryMap.entries()].sort((a, b) => b[1].length - a[1].length).slice(0, 8)) {
3649
+ const successLessons = catLessons.filter(l => l.outcome === 'success');
3650
+ const best = successLessons[0] ?? catLessons[0];
3651
+ if (!best)
3652
+ continue;
3653
+ topPatterns.push({
3654
+ category: cat,
3655
+ insight: best.what_worked.slice(0, 120),
3656
+ count: catLessons.length,
3657
+ });
3658
+ }
3659
+ const crystal = {
3660
+ label: effectiveLabel,
3661
+ ts: now.toISOString(),
3662
+ session_count: sessionHistory.length,
3663
+ lesson_count: allLessons.length,
3664
+ top_patterns: topPatterns,
3665
+ categories: [...categoryMap.keys()],
3666
+ created_from: `${sessionHistory.length} sessions, ${allLessons.length} lessons`,
3667
+ };
3668
+ const crystalJson = JSON.stringify(crystal);
3669
+ await redis.set('cachly:crystal:latest', crystalJson);
3670
+ await redis.expire('cachly:crystal:latest', 90 * 86400);
3671
+ await redis.set(`cachly:crystal:${week}`, crystalJson);
3672
+ await redis.expire(`cachly:crystal:${week}`, 365 * 86400);
3673
+ const lines = [
3674
+ `💎 **Memory Crystal created: ${effectiveLabel}**`,
3675
+ ``,
3676
+ `📊 Compressed: **${sessionHistory.length} sessions** + **${allLessons.length} lessons** → ${topPatterns.length} top patterns`,
3677
+ ``,
3678
+ `**Top patterns by category:**`,
3679
+ ...topPatterns.slice(0, 6).map(p => ` • **${p.category}** (${p.count}×): ${p.insight.slice(0, 90)}`),
3680
+ ``,
3681
+ `💡 This crystal will appear in every future \`session_start\` briefing.`,
3682
+ `💡 Re-run \`memory_crystalize\` monthly to keep it fresh.`,
3683
+ ];
3684
+ return lines.join('\n');
3685
+ }
3458
3686
  case 'brain_doctor': {
3459
- const { instance_id } = args;
3687
+ const { instance_id, workspace_path: drWorkspacePath = '' } = args;
3460
3688
  const redis = await getConnection(instance_id);
3461
3689
  const issues = [];
3462
3690
  const checks = [];
@@ -3507,6 +3735,11 @@ async function handleTool(name, args) {
3507
3735
  const staleLessons = lessons.filter(l => l.outcome === 'success' && calculateConfidence(l) < CONFIDENCE_STALE_VALUE);
3508
3736
  const warnLessons = lessons.filter(l => l.outcome === 'success' && calculateConfidence(l) >= CONFIDENCE_STALE_VALUE && calculateConfidence(l) < CONFIDENCE_WARN_VALUE);
3509
3737
  const withAudit = lessons.filter(l => (l.audit_trail ?? []).length > 1);
3738
+ const teamLessons = lessons.filter(l => l.author);
3739
+ const uniqueAuthors = new Set(lessons.map(l => l.author).filter(Boolean));
3740
+ // Effective IQ boost: total recalls / lessons (how much the brain actually helped)
3741
+ const totalRecalls = lessons.reduce((sum, l) => sum + (l.recall_count ?? 0), 0);
3742
+ const iqBoostPct = lessons.length > 0 ? Math.min(100, Math.round((totalRecalls / lessons.length) * 10)) : 0;
3510
3743
  // Quality score (0-100)
3511
3744
  let score = 50;
3512
3745
  if (lessonKeys.length >= 5)
@@ -3526,12 +3759,70 @@ async function handleTool(name, args) {
3526
3759
  score += 10;
3527
3760
  if (staleLessons.length === 0)
3528
3761
  score += 5;
3762
+ if (uniqueAuthors.size >= 2)
3763
+ score += 5; // team collaboration bonus
3529
3764
  const scoreEmoji = score >= 80 ? '🟢' : score >= 50 ? '🟡' : '🔴';
3765
+ const iqEmoji = iqBoostPct >= 50 ? '🚀' : iqBoostPct >= 20 ? '📈' : '💤';
3530
3766
  checks.push(`${scoreEmoji} **Brain Quality Score: ${score}/100**`);
3531
- checks.push(`📚 **Lessons:** ${lessonKeys.length} (${criticalLessons.length} critical · ${withAudit.length} with audit trail)`);
3767
+ checks.push(`${iqEmoji} **Effective IQ Boost: ${iqBoostPct}%** (${totalRecalls} recalls across ${lessons.length} lessons)`);
3768
+ checks.push(`📚 **Lessons:** ${lessonKeys.length} (${criticalLessons.length} critical · ${withAudit.length} with audit trail · ${teamLessons.length} from team)`);
3532
3769
  checks.push(`💾 **Context entries:** ${ctxCount}`);
3533
3770
  checks.push(`🎯 **Confidence:** ${lessons.length - staleLessons.length - warnLessons.length} fresh · ${warnLessons.length} warn · ${staleLessons.length} stale`);
3534
3771
  checks.push(`⏱️ **Decay config:** warn after ${CONFIDENCE_WARN_DAYS}d · stale after ${CONFIDENCE_STALE_DAYS}d`);
3772
+ if (uniqueAuthors.size >= 2) {
3773
+ checks.push(`👥 **Team:** ${uniqueAuthors.size} contributors (${[...uniqueAuthors].join(', ')})`);
3774
+ }
3775
+ // Stale index detection
3776
+ try {
3777
+ const lastIndexRaw = await redis.get('cachly:index:last_run');
3778
+ if (lastIndexRaw) {
3779
+ const lastIndexAge = Math.round((Date.now() - new Date(lastIndexRaw).getTime()) / 86_400_000);
3780
+ if (lastIndexAge > 7) {
3781
+ issues.push(`🔄 Index is ${lastIndexAge}d stale — run \`index_project\` to re-sync semantic search`);
3782
+ }
3783
+ else {
3784
+ checks.push(`🗂️ **Semantic index:** ${lastIndexAge}d old (fresh)`);
3785
+ }
3786
+ }
3787
+ else {
3788
+ issues.push(`💡 No semantic index — run \`index_project(dir="<your-src>")\` to enable semantic search`);
3789
+ }
3790
+ }
3791
+ catch { /* non-critical */ }
3792
+ // Memory crystal status
3793
+ try {
3794
+ const crystalRaw = await redis.get('cachly:crystal:latest');
3795
+ if (crystalRaw) {
3796
+ const crystal = JSON.parse(crystalRaw);
3797
+ const crystalAge = Math.round((Date.now() - new Date(crystal.ts).getTime()) / 86_400_000);
3798
+ checks.push(`💎 **Memory Crystal:** ${crystal.label} (${crystalAge}d ago)`);
3799
+ if (crystalAge > 30)
3800
+ issues.push(`💡 Memory Crystal is ${crystalAge}d old — re-run \`memory_crystalize\` to compress new sessions`);
3801
+ }
3802
+ else if (lessonKeys.length >= 10) {
3803
+ issues.push(`💡 ${lessonKeys.length} lessons but no Memory Crystal — run \`memory_crystalize\` to compress wisdom`);
3804
+ }
3805
+ }
3806
+ catch { /* non-critical */ }
3807
+ // openclaw cross-promo (check package.json in workspace)
3808
+ if (drWorkspacePath) {
3809
+ try {
3810
+ const pkgPath = drWorkspacePath.replace(/\/$/, '') + '/package.json';
3811
+ const pkgRaw = readFileSync(pkgPath, 'utf-8');
3812
+ const pkg = JSON.parse(pkgRaw);
3813
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
3814
+ const hasLLMDep = ['openai', '@anthropic-ai/sdk', '@google/generative-ai', 'mistralai', 'cohere-ai'].some(d => d in allDeps);
3815
+ const hasOpenclaw = '@cachly-dev/openclaw' in allDeps;
3816
+ if (hasLLMDep && !hasOpenclaw) {
3817
+ 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\``);
3818
+ issues.push(` → \`npm install @cachly-dev/openclaw\` cuts LLM costs 60–90% with 3 lines of code`);
3819
+ }
3820
+ else if (hasOpenclaw) {
3821
+ checks.push(`✅ **@cachly-dev/openclaw installed** (LLM cost caching active)`);
3822
+ }
3823
+ }
3824
+ catch { /* no package.json or unreadable */ }
3825
+ }
3535
3826
  if (lastSession) {
3536
3827
  const ageMin = Math.round((Date.now() - new Date(lastSession.ts).getTime()) / 60000);
3537
3828
  const ageStr = ageMin < 60 ? `${ageMin}m` : ageMin < 1440 ? `${Math.round(ageMin / 60)}h` : `${Math.round(ageMin / 1440)}d`;
@@ -3546,6 +3837,9 @@ async function handleTool(name, args) {
3546
3837
  else if (lessonKeys.length < 5) {
3547
3838
  issues.push(`💡 Only ${lessonKeys.length} lessons — add more after each problem solved`);
3548
3839
  }
3840
+ if (iqBoostPct === 0 && lessons.length >= 5) {
3841
+ issues.push(`💤 **IQ Boost is 0%** — lessons exist but are never recalled. Use \`recall_best_solution\` BEFORE tasks.`);
3842
+ }
3549
3843
  if (ctxCount === 0) {
3550
3844
  issues.push('💡 No context — use `remember_context` to cache architecture docs, ADRs, etc.');
3551
3845
  }