@cachly-dev/mcp-server 0.10.49 → 0.10.51
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/BENCH.md +80 -0
- package/STRATEGY.md +33 -14
- package/apps/travel-chaos-organizer/frontend/package-lock.json +184 -0
- package/dist/src/__tests__/rerank.test.d.ts +1 -0
- package/dist/src/__tests__/rerank.test.js +115 -0
- package/dist/src/__tests__/rerank.test.js.map +1 -0
- package/dist/src/bench/cachly-bench.d.ts +35 -0
- package/dist/src/bench/cachly-bench.js +182 -0
- package/dist/src/bench/cachly-bench.js.map +1 -0
- package/dist/src/bench/fixtures.d.ts +34 -0
- package/dist/src/bench/fixtures.js +199 -0
- package/dist/src/bench/fixtures.js.map +1 -0
- package/dist/src/handlers/brain.js +141 -65
- package/dist/src/handlers/brain.js.map +1 -1
- package/dist/src/handlers/team.js +56 -4
- package/dist/src/handlers/team.js.map +1 -1
- package/dist/src/rerank.d.ts +55 -0
- package/dist/src/rerank.js +111 -0
- package/dist/src/rerank.js.map +1 -0
- package/dist/src/tools.d.ts +30 -0
- package/dist/src/tools.js +18 -0
- package/dist/src/tools.js.map +1 -1
- package/package.json +2 -1
- package/src/__tests__/rerank.test.ts +130 -0
- package/src/bench/cachly-bench.ts +217 -0
- package/src/bench/fixtures.ts +226 -0
- package/src/handlers/brain.ts +156 -73
- package/src/handlers/team.ts +66 -4
- package/src/rerank.ts +148 -0
- package/src/tools.ts +19 -0
|
@@ -3,20 +3,32 @@ import { calculateConfidence, confidenceBadge, STRUCTURED_TEMPLATES, CONFIDENCE_
|
|
|
3
3
|
import { ckgSlug, extractProblemConcept, ckgUpsertNode, ckgUpdateEdge } from '../ckg.js';
|
|
4
4
|
import { safeJsonParse } from '../utils.js';
|
|
5
5
|
import { keywordSearch, tokenize, splitMultiQuery, levenshtein, indexVocab as _indexVocab } from '../search.js';
|
|
6
|
+
import { rerankByQuality } from '../rerank.js';
|
|
6
7
|
import { computeEmbedding, hasEmbedProvider } from '../embeddings.js';
|
|
7
8
|
// ── Changelog (shown once per version in session_start) ──────────────────────
|
|
8
|
-
const MCP_VERSION = '0.10.
|
|
9
|
+
const MCP_VERSION = '0.10.51';
|
|
9
10
|
const WHATS_NEW = {
|
|
10
|
-
'0.10.
|
|
11
|
+
'0.10.51': [
|
|
11
12
|
`🆕 **What's new in v${MCP_VERSION}:**`,
|
|
13
|
+
` ✅ \`team_confirm\` — endorse a lesson in code/knowledge review; reviewed knowledge ranks higher (🛡️ senior / ✔️ peer)`,
|
|
14
|
+
` ✅ Governance-aware recall — human-confirmed lessons outrank unreviewed auto-learned entries`,
|
|
15
|
+
` ✅ \`team_recall\` — shows reviewer badges and sorts confirmed lessons first`,
|
|
16
|
+
` 💡 Run \`team_confirm(topic, reviewer, level="senior")\` to bless the canonical solution`,
|
|
17
|
+
],
|
|
18
|
+
'0.10.50': [
|
|
19
|
+
` ✅ \`smart_recall\` — unified keyword + semantic hybrid list (one ranked result, not two sections)`,
|
|
20
|
+
` ✅ \`smart_recall\` — "Brain saved you here" banner surfaces time saved inline per proven lesson`,
|
|
21
|
+
` ✅ Quality reranking — proven successes outrank symptom-dense failures (+11% Precision@1)`,
|
|
22
|
+
` ✅ Cachly-Bench — IR benchmark with CI regression guard (see BENCH.md for the proof)`,
|
|
23
|
+
` 🔧 Contradiction resolution now persisted to Redis (90-day TTL)`,
|
|
24
|
+
` 💡 Run \`smart_recall\` to see your Brain's recall quality in action`,
|
|
25
|
+
],
|
|
26
|
+
'0.10.49': [
|
|
12
27
|
` ✅ \`brain_from_git\` — auto-seed your Brain from git history in seconds`,
|
|
13
28
|
` ✅ \`brain_predict_failures\` — predict CI/build failures before they happen`,
|
|
14
|
-
` ✅ \`smart_recall\` — semantic + keyword hybrid recall, now in every CLAUDE.md`,
|
|
15
29
|
` ✅ VS Code extension — Brain status bar, ambient learning, CodeLens hints`,
|
|
16
30
|
` ✅ Team Brain — share lessons across your whole engineering team`,
|
|
17
|
-
` ✅ 89 MCP tools — roadmap, A/B tests, cache, index, predict, syndicate`,
|
|
18
31
|
` 🔧 Stability — auto-expiring memory keys, longer provisioning wait, robust recall`,
|
|
19
|
-
` 💡 Run \`brain_from_git\` to seed your Brain from existing commits instantly`,
|
|
20
32
|
],
|
|
21
33
|
};
|
|
22
34
|
export const BRAIN_TOOL_NAMES = new Set([
|
|
@@ -72,6 +84,19 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
72
84
|
await ckgUpdateEdge(redis, cId, 'contradicts', resId, false);
|
|
73
85
|
}
|
|
74
86
|
catch { /* non-critical */ }
|
|
87
|
+
// Persist a queryable resolution record — so contradiction history survives
|
|
88
|
+
// (CKG edges alone aren't easily auditable). Bounded + TTL'd like other history.
|
|
89
|
+
try {
|
|
90
|
+
const resolutionKey = `cachly:contradictions:${topic}`;
|
|
91
|
+
const direction = prev.outcome === 'success' && outcome === 'failure'
|
|
92
|
+
? 'kept-success' : 'overwrote-failure';
|
|
93
|
+
await redis.rpush(resolutionKey, JSON.stringify({
|
|
94
|
+
ts, prev_outcome: prev.outcome, new_outcome: outcome, direction,
|
|
95
|
+
}));
|
|
96
|
+
await redis.ltrim(resolutionKey, -50, -1);
|
|
97
|
+
await redis.expire(resolutionKey, 180 * 86400);
|
|
98
|
+
}
|
|
99
|
+
catch { /* non-critical */ }
|
|
75
100
|
contradictionWarning.push(`🗳️ Run \`madc_deliberate(topic="${topic}")\` to resolve via expert agent voting.`);
|
|
76
101
|
}
|
|
77
102
|
}
|
|
@@ -411,9 +436,11 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
411
436
|
const { instance_id, query, threshold = 0.78, } = args;
|
|
412
437
|
const redis = await getConnection(instance_id);
|
|
413
438
|
// ── Layer 1: Keyword search across ALL brain data (always works, no embedding) ──
|
|
414
|
-
const
|
|
415
|
-
//
|
|
416
|
-
//
|
|
439
|
+
const rawMatches = await keywordSearch(redis, ['cachly:ctx:*', 'cachly:lesson:best:*', 'cachly:idx:*'], query, 10);
|
|
440
|
+
// ── Layer 1.5: Quality-aware rerank — proven success lessons outrank
|
|
441
|
+
// text-similar failed attempts (the moat; see src/rerank.ts + Cachly-Bench). ──
|
|
442
|
+
const kwMatches = rerankByQuality(rawMatches);
|
|
443
|
+
const savedHere = [];
|
|
417
444
|
const lessonMatches = kwMatches.filter(m => m.key.startsWith('cachly:lesson:best:'));
|
|
418
445
|
for (const m of lessonMatches.slice(0, 5)) {
|
|
419
446
|
const existing = await redis.get(m.key).catch(() => null);
|
|
@@ -422,41 +449,118 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
422
449
|
if (lesson) {
|
|
423
450
|
const updated = { ...lesson, recall_count: (lesson.recall_count ?? 0) + 1, verified_at: new Date().toISOString() };
|
|
424
451
|
redis.set(m.key, JSON.stringify(updated)).catch(() => { });
|
|
425
|
-
const
|
|
452
|
+
const sev = lesson.severity;
|
|
453
|
+
const savedMins = sev === 'critical' ? 240 : sev === 'major' ? 60 : 30;
|
|
426
454
|
redis.incrbyfloat(`cachly:stats:time_saved_mins:${instance_id}`, savedMins).catch(() => { });
|
|
455
|
+
// Surface banner for proven successes (recall_count >= 1 means it's been validated)
|
|
456
|
+
if (lesson.outcome === 'success' && (lesson.recall_count ?? 0) >= 1) {
|
|
457
|
+
savedHere.push({
|
|
458
|
+
topic: m.key.replace('cachly:lesson:best:', ''),
|
|
459
|
+
severity: sev ?? 'major',
|
|
460
|
+
recall_count: (lesson.recall_count ?? 0) + 1,
|
|
461
|
+
savedMins,
|
|
462
|
+
});
|
|
463
|
+
}
|
|
427
464
|
}
|
|
428
465
|
}
|
|
429
466
|
}
|
|
467
|
+
// ── Layer 2: Semantic search (parallel, optional) ────────────────────────
|
|
468
|
+
const inst = await apiFetch(`/api/v1/instances/${instance_id}`);
|
|
469
|
+
const semHits = [];
|
|
470
|
+
if (inst.vector_token && hasEmbedProvider()) {
|
|
471
|
+
try {
|
|
472
|
+
const embedding = await computeEmbedding(query);
|
|
473
|
+
const vectorUrl = `https://api.cachly.dev/v1/sem/${inst.vector_token}`;
|
|
474
|
+
const searchRes = await fetch(`${vectorUrl}/search`, {
|
|
475
|
+
method: 'POST',
|
|
476
|
+
headers: { 'Content-Type': 'application/json' },
|
|
477
|
+
body: JSON.stringify({ embedding, namespace: 'cachly:ctx', threshold, top_k: 5 }),
|
|
478
|
+
});
|
|
479
|
+
if (searchRes.ok) {
|
|
480
|
+
const results = (await searchRes.json());
|
|
481
|
+
for (const hit of results.filter(r => r.found && r.id)) {
|
|
482
|
+
const parts = hit.id.replace('ctx:', '').split(':');
|
|
483
|
+
const category = parts[0];
|
|
484
|
+
const key = parts.slice(1).join(':');
|
|
485
|
+
const redisKey = `cachly:ctx:${category}:${key}`;
|
|
486
|
+
const content = await redis.get(redisKey).catch(() => null);
|
|
487
|
+
semHits.push({ key: redisKey, similarity: hit.similarity ?? 0, content: content ?? '(evicted)' });
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
catch {
|
|
492
|
+
// Semantic search failed silently — keyword results are enough
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
const bm25Scores = kwMatches.map(m => m.score);
|
|
496
|
+
const bm25Min = bm25Scores.length ? Math.min(...bm25Scores) : 0;
|
|
497
|
+
const bm25Range = bm25Scores.length ? (Math.max(...bm25Scores) - bm25Min) || 1 : 1;
|
|
498
|
+
const bm25Norm = (s) => (s - bm25Min) / bm25Range;
|
|
499
|
+
const hybridMap = new Map();
|
|
500
|
+
for (const m of kwMatches) {
|
|
501
|
+
const n = bm25Norm(m.score);
|
|
502
|
+
hybridMap.set(m.key, {
|
|
503
|
+
key: m.key, content: m.content, bm25Score: n,
|
|
504
|
+
hybridScore: n * (semHits.length > 0 ? 0.7 : 1.0),
|
|
505
|
+
matchedWords: m.matchedWords, matchType: 'keyword', subQuery: m.subQuery,
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
for (const hit of semHits) {
|
|
509
|
+
const existing = hybridMap.get(hit.key);
|
|
510
|
+
if (existing) {
|
|
511
|
+
existing.semScore = hit.similarity;
|
|
512
|
+
existing.hybridScore = (existing.bm25Score ?? 0) * 0.6 + hit.similarity * 0.4;
|
|
513
|
+
existing.matchType = 'hybrid';
|
|
514
|
+
}
|
|
515
|
+
else {
|
|
516
|
+
hybridMap.set(hit.key, {
|
|
517
|
+
key: hit.key, content: hit.content, semScore: hit.similarity,
|
|
518
|
+
hybridScore: hit.similarity * 0.5, matchType: 'semantic',
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
const hybridResults = [...hybridMap.values()].sort((a, b) => b.hybridScore - a.hybridScore);
|
|
523
|
+
// ── Build output ──────────────────────────────────────────────────────────
|
|
430
524
|
const lines = [`🧠 **Smart Recall** for: _"${query}"_\n`];
|
|
525
|
+
// "Brain saved you here" banner — surfaces value inline for proven recalled lessons
|
|
526
|
+
const SORDER = { critical: 0, major: 1, minor: 2 };
|
|
527
|
+
const topLesson = savedHere.sort((a, b) => (SORDER[a.severity] ?? 1) - (SORDER[b.severity] ?? 1))[0];
|
|
528
|
+
if (topLesson) {
|
|
529
|
+
lines.push(`> 💡 **Brain saved you ~${topLesson.savedMins}m here** — ${topLesson.severity} issue from \`${topLesson.topic}\` recalled ${topLesson.recall_count}× proven\n`);
|
|
530
|
+
}
|
|
431
531
|
// Show sub-query info if multi-topic was detected
|
|
432
532
|
const subQueries = splitMultiQuery(query);
|
|
433
533
|
if (subQueries.length > 1) {
|
|
434
534
|
lines.push(`_Detected ${subQueries.length} sub-topics:_ ${subQueries.map((s, i) => `${i + 1}. "${s}"`).join(', ')}\n`);
|
|
435
535
|
}
|
|
436
|
-
if (
|
|
437
|
-
|
|
438
|
-
|
|
536
|
+
if (hybridResults.length > 0) {
|
|
537
|
+
const modeLabel = semHits.length > 0
|
|
538
|
+
? `keyword + semantic hybrid`
|
|
539
|
+
: `keyword`;
|
|
540
|
+
lines.push(`### 🔍 Results (${hybridResults.length} — ${modeLabel})\n`);
|
|
541
|
+
// Group by sub-query for multi-topic queries
|
|
439
542
|
if (subQueries.length > 1) {
|
|
440
543
|
const grouped = new Map();
|
|
441
|
-
for (const
|
|
442
|
-
const sq =
|
|
544
|
+
for (const r of hybridResults.slice(0, 12)) {
|
|
545
|
+
const sq = r.subQuery ?? query;
|
|
443
546
|
if (!grouped.has(sq))
|
|
444
547
|
grouped.set(sq, []);
|
|
445
|
-
grouped.get(sq).push(
|
|
548
|
+
grouped.get(sq).push(r);
|
|
446
549
|
}
|
|
447
|
-
for (const [sq,
|
|
448
|
-
lines.push(`**Topic: "${sq}"** (${
|
|
449
|
-
for (const
|
|
450
|
-
const label =
|
|
451
|
-
|
|
452
|
-
.
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
550
|
+
for (const [sq, results] of grouped) {
|
|
551
|
+
lines.push(`**Topic: "${sq}"** (${results.length} results)\n`);
|
|
552
|
+
for (const r of results.slice(0, 4)) {
|
|
553
|
+
const label = r.key.replace('cachly:ctx:', '📝 ').replace('cachly:lesson:best:', '💡 ').replace('cachly:idx:', '📂 ');
|
|
554
|
+
const scorePart = r.matchType === 'hybrid'
|
|
555
|
+
? `BM25: ${(r.bm25Score ?? 0).toFixed(2)}, sem: ${((r.semScore ?? 0) * 100).toFixed(0)}%, 🔀 hybrid`
|
|
556
|
+
: r.matchType === 'semantic'
|
|
557
|
+
? `sem: ${((r.semScore ?? 0) * 100).toFixed(0)}%, 🎯 semantic`
|
|
558
|
+
: `BM25: ${(r.bm25Score ?? 0).toFixed(2)}, matched: ${r.matchedWords?.join(', ')}`;
|
|
559
|
+
const preview = r.content.slice(0, 300).replace(/\n/g, ' ');
|
|
560
|
+
lines.push(` **${label}** _(${scorePart})_`);
|
|
561
|
+
lines.push(` > ${preview}${r.content.length > 300 ? '…' : ''}\n`);
|
|
457
562
|
}
|
|
458
563
|
}
|
|
459
|
-
// Summary: which sub-queries had matches
|
|
460
564
|
const matched = [...grouped.keys()];
|
|
461
565
|
const unmatched = subQueries.filter(sq => !matched.includes(sq));
|
|
462
566
|
if (unmatched.length > 0) {
|
|
@@ -464,48 +568,20 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
464
568
|
}
|
|
465
569
|
}
|
|
466
570
|
else {
|
|
467
|
-
for (const
|
|
468
|
-
const label =
|
|
469
|
-
|
|
470
|
-
.
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
}
|
|
478
|
-
// ── Layer 2: Semantic search (optional, only if embedding provider + vector_token available) ──
|
|
479
|
-
const inst = await apiFetch(`/api/v1/instances/${instance_id}`);
|
|
480
|
-
if (inst.vector_token && hasEmbedProvider()) {
|
|
481
|
-
try {
|
|
482
|
-
const embedding = await computeEmbedding(query);
|
|
483
|
-
const vectorUrl = `https://api.cachly.dev/v1/sem/${inst.vector_token}`;
|
|
484
|
-
const searchRes = await fetch(`${vectorUrl}/search`, {
|
|
485
|
-
method: 'POST',
|
|
486
|
-
headers: { 'Content-Type': 'application/json' },
|
|
487
|
-
body: JSON.stringify({ embedding, namespace: 'cachly:ctx', threshold, top_k: 5 }),
|
|
488
|
-
});
|
|
489
|
-
if (searchRes.ok) {
|
|
490
|
-
const results = (await searchRes.json());
|
|
491
|
-
const semHits = results.filter(r => r.found && r.id);
|
|
492
|
-
if (semHits.length > 0) {
|
|
493
|
-
lines.push(`\n### 🎯 Semantic Matches (${semHits.length})\n`);
|
|
494
|
-
for (const hit of semHits) {
|
|
495
|
-
const parts = hit.id.replace('ctx:', '').split(':');
|
|
496
|
-
const category = parts[0];
|
|
497
|
-
const key = parts.slice(1).join(':');
|
|
498
|
-
const content = await redis.get(`cachly:ctx:${category}:${key}`);
|
|
499
|
-
lines.push(`**${key}** _(${((hit.similarity ?? 0) * 100).toFixed(0)}% similar)_`, `> ${content?.slice(0, 300) ?? '(evicted)'}${(content?.length ?? 0) > 300 ? '…' : ''}\n`);
|
|
500
|
-
}
|
|
501
|
-
}
|
|
571
|
+
for (const r of hybridResults.slice(0, 8)) {
|
|
572
|
+
const label = r.key.replace('cachly:ctx:', '📝 ').replace('cachly:lesson:best:', '💡 ').replace('cachly:idx:', '📂 ');
|
|
573
|
+
const scorePart = r.matchType === 'hybrid'
|
|
574
|
+
? `BM25: ${(r.bm25Score ?? 0).toFixed(2)}, sem: ${((r.semScore ?? 0) * 100).toFixed(0)}%, 🔀 hybrid`
|
|
575
|
+
: r.matchType === 'semantic'
|
|
576
|
+
? `sem: ${((r.semScore ?? 0) * 100).toFixed(0)}%, 🎯 semantic`
|
|
577
|
+
: `BM25: ${(r.bm25Score ?? 0).toFixed(2)}, matched: ${r.matchedWords?.join(', ')}`;
|
|
578
|
+
const preview = r.content.slice(0, 400).replace(/\n/g, ' ');
|
|
579
|
+
lines.push(`**${label}** _(${scorePart})_`);
|
|
580
|
+
lines.push(`> ${preview}${r.content.length > 400 ? '…' : ''}\n`);
|
|
502
581
|
}
|
|
503
582
|
}
|
|
504
|
-
catch {
|
|
505
|
-
// Semantic search failed silently — keyword results are enough
|
|
506
|
-
}
|
|
507
583
|
}
|
|
508
|
-
|
|
584
|
+
else {
|
|
509
585
|
lines.push(`⚠️ No matches found for: "${query}"`);
|
|
510
586
|
// Did-You-Mean: find nearest token in index vocab
|
|
511
587
|
const queryTokens = tokenize(query);
|