@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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cachly-dev/mcp-server",
|
|
3
|
-
"version": "0.5.
|
|
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.
|
|
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),
|
|
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(
|
|
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
|
}
|