@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 +303 -9
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +310 -7
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.
|
|
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),
|
|
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(
|
|
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
|
}
|