@cachly-dev/mcp-server 0.10.59 → 0.10.60
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/CHANGELOG.md +20 -0
- package/README.md +1 -1
- package/dist/src/__tests__/brain-flow.test.js +106 -9
- package/dist/src/__tests__/brain-flow.test.js.map +1 -1
- package/dist/src/handlers/brain.js +196 -7
- package/dist/src/handlers/brain.js.map +1 -1
- package/dist/src/index.js +3 -3
- package/dist/src/tools.d.ts +43 -1
- package/dist/src/tools.js +41 -1
- package/dist/src/tools.js.map +1 -1
- package/package.json +2 -2
- package/server.json +3 -3
- package/src/__tests__/brain-flow.test.ts +123 -9
- package/src/handlers/brain.ts +219 -7
- package/src/index.ts +3 -3
- package/src/tools.ts +43 -1
|
@@ -6,14 +6,21 @@ import { keywordSearch, tokenize, splitMultiQuery, levenshtein, indexVocab as _i
|
|
|
6
6
|
import { rerankByQuality } from '../rerank.js';
|
|
7
7
|
import { computeEmbedding, hasEmbedProvider } from '../embeddings.js';
|
|
8
8
|
// ── Changelog (shown once per version in session_start) ──────────────────────
|
|
9
|
-
const MCP_VERSION = '0.10.
|
|
9
|
+
const MCP_VERSION = '0.10.60';
|
|
10
10
|
const WHATS_NEW = {
|
|
11
|
+
'0.10.60': [
|
|
12
|
+
`🆕 **Phase 3B: File map + team overview + visibility**`,
|
|
13
|
+
` 📁 \`brain_file_map(file_paths=[...])\` — experts + lessons per file before you touch it`,
|
|
14
|
+
` 🗺️ \`team_expertise_map()\` — full team skills matrix in one table`,
|
|
15
|
+
` 🔒 \`visibility: "private"\` on \`learn_from_attempts\` — private notes never leak to smart_recall`,
|
|
16
|
+
` 📊 98 MCP tools`,
|
|
17
|
+
],
|
|
11
18
|
'0.10.59': [
|
|
12
19
|
`🆕 **Phase 3A: Org-wide knowledge graph**`,
|
|
13
20
|
` 👥 \`brain_who_knows(topic="...")\` — find your team's experts on any topic instantly`,
|
|
14
21
|
` 🕸️ Person + File nodes auto-built from \`learn_from_attempts(author="...", file_paths=[...])\``,
|
|
15
22
|
` 👤 Author attribution now shown inline in \`smart_recall\` results`,
|
|
16
|
-
` 📊 96 MCP tools ·
|
|
23
|
+
` 📊 96 MCP tools · 386 tests green`,
|
|
17
24
|
],
|
|
18
25
|
'0.10.58': [
|
|
19
26
|
`🧹 Zero lint warnings — all unused imports cleaned across every handler`,
|
|
@@ -55,12 +62,12 @@ const WHATS_NEW = {
|
|
|
55
62
|
export const BRAIN_TOOL_NAMES = new Set([
|
|
56
63
|
'learn_from_attempts', 'recall_best_solution', 'smart_recall',
|
|
57
64
|
'session_start', 'session_end', 'session_ping', 'session_handoff', 'auto_learn_session',
|
|
58
|
-
'brain_who_knows',
|
|
65
|
+
'brain_who_knows', 'brain_file_map', 'team_expertise_map',
|
|
59
66
|
]);
|
|
60
67
|
export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
61
68
|
switch (name) {
|
|
62
69
|
case 'learn_from_attempts': {
|
|
63
|
-
const { instance_id, topic, outcome, what_worked, what_failed = '', context: ctx = '', severity = 'major', file_paths = [], commands = [], tags = [], depends_on = [], author = '', } = args;
|
|
70
|
+
const { instance_id, topic, outcome, what_worked, what_failed = '', context: ctx = '', severity = 'major', file_paths = [], commands = [], tags = [], depends_on = [], author = '', visibility = 'team', } = args;
|
|
64
71
|
const redis = await getConnection(instance_id);
|
|
65
72
|
const ts = new Date().toISOString();
|
|
66
73
|
// ── Structured template hints ──────────────────────────────────────────
|
|
@@ -178,6 +185,7 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
178
185
|
tags,
|
|
179
186
|
depends_on,
|
|
180
187
|
...(author ? { author } : {}),
|
|
188
|
+
visibility,
|
|
181
189
|
recall_count: recallCount,
|
|
182
190
|
ts,
|
|
183
191
|
verified_at: outcome === 'success' || outcome === 'partial' ? ts : undefined,
|
|
@@ -527,9 +535,9 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
527
535
|
}
|
|
528
536
|
}
|
|
529
537
|
// ── Layer 2: Semantic search (parallel, optional) ────────────────────────
|
|
530
|
-
const inst = await apiFetch(`/api/v1/instances/${instance_id}`);
|
|
538
|
+
const inst = await apiFetch(`/api/v1/instances/${instance_id}`).catch(() => null);
|
|
531
539
|
const semHits = [];
|
|
532
|
-
if (inst
|
|
540
|
+
if (inst?.vector_token && hasEmbedProvider()) {
|
|
533
541
|
try {
|
|
534
542
|
const embedding = await computeEmbedding(query);
|
|
535
543
|
const vectorUrl = `https://api.cachly.dev/v1/sem/${inst.vector_token}`;
|
|
@@ -634,7 +642,15 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
634
642
|
}
|
|
635
643
|
}
|
|
636
644
|
catch { /* non-critical — keyword + semantic always available */ }
|
|
637
|
-
|
|
645
|
+
// Filter private lessons — they are only accessible via exact recall_best_solution
|
|
646
|
+
const hybridResults = [...hybridMap.values()]
|
|
647
|
+
.filter(r => {
|
|
648
|
+
if (!r.key.startsWith('cachly:lesson:best:'))
|
|
649
|
+
return true;
|
|
650
|
+
const ld = safeJsonParse(r.content, {});
|
|
651
|
+
return ld.visibility !== 'private';
|
|
652
|
+
})
|
|
653
|
+
.sort((a, b) => b.hybridScore - a.hybridScore);
|
|
638
654
|
// ── Build output ──────────────────────────────────────────────────────────
|
|
639
655
|
const lines = [`🧠 **Smart Recall** for: _"${query}"_\n`];
|
|
640
656
|
// "Brain saved you here" banner — surfaces value inline for proven recalled lessons
|
|
@@ -1911,6 +1927,179 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
1911
1927
|
lines.push(`---`, `_Attribution grows with every \`learn_from_attempts(author="...", ...)\` call._`);
|
|
1912
1928
|
return lines.join('\n');
|
|
1913
1929
|
}
|
|
1930
|
+
// ── brain_file_map ────────────────────────────────────────────────────────
|
|
1931
|
+
// Phase 3B: "What do we know about these files?" — experts + lessons per file.
|
|
1932
|
+
case 'brain_file_map': {
|
|
1933
|
+
const { instance_id, file_paths = [], } = args;
|
|
1934
|
+
if (!file_paths.length)
|
|
1935
|
+
return '⚠️ Pass at least one file path.';
|
|
1936
|
+
const redis = await getConnection(instance_id);
|
|
1937
|
+
const lines = [
|
|
1938
|
+
`## 📁 File Knowledge Map`,
|
|
1939
|
+
``,
|
|
1940
|
+
`_What cachly knows about the files you're about to touch:_`,
|
|
1941
|
+
``,
|
|
1942
|
+
];
|
|
1943
|
+
for (const fp of file_paths.slice(0, 10)) {
|
|
1944
|
+
lines.push(`### \`${fp}\``);
|
|
1945
|
+
// ── Experts via CKG file nodes ─────────────────────────────────────
|
|
1946
|
+
const fileId = `file:${ckgSlug(fp)}`;
|
|
1947
|
+
const inbound = await redis.smembers(`cachly:ckg:idx:to:${fileId}`);
|
|
1948
|
+
const experts = [];
|
|
1949
|
+
for (const ek of inbound) {
|
|
1950
|
+
const er = await redis.get(ek);
|
|
1951
|
+
if (!er)
|
|
1952
|
+
continue;
|
|
1953
|
+
const edge = safeJsonParse(er, null);
|
|
1954
|
+
if (!edge || edge.edgeType !== 'touched')
|
|
1955
|
+
continue;
|
|
1956
|
+
const personRaw = await redis.get(`cachly:ckg:node:${edge.from}`);
|
|
1957
|
+
if (!personRaw)
|
|
1958
|
+
continue;
|
|
1959
|
+
const pn = safeJsonParse(personRaw, null);
|
|
1960
|
+
if (!pn || pn.type !== 'person')
|
|
1961
|
+
continue;
|
|
1962
|
+
experts.push({ handle: pn.handle, touches: edge.trials, lastSeen: edge.last_updated });
|
|
1963
|
+
}
|
|
1964
|
+
experts.sort((a, b) => b.touches - a.touches);
|
|
1965
|
+
if (experts.length > 0) {
|
|
1966
|
+
const expLine = experts.slice(0, 5).map((e, i) => {
|
|
1967
|
+
const medal = i === 0 ? '🥇' : i === 1 ? '🥈' : i === 2 ? '🥉' : `${i + 1}.`;
|
|
1968
|
+
const days = Math.floor((Date.now() - new Date(e.lastSeen).getTime()) / 86400000);
|
|
1969
|
+
const ago = days < 1 ? 'today' : days < 7 ? `${days}d ago` : `${Math.floor(days / 7)}w ago`;
|
|
1970
|
+
return `${medal} **${e.handle}** (${e.touches}× · ${ago})`;
|
|
1971
|
+
}).join(' · ');
|
|
1972
|
+
lines.push(`**Experts:** ${expLine}`);
|
|
1973
|
+
}
|
|
1974
|
+
else {
|
|
1975
|
+
lines.push(`**Experts:** _None yet — add \`author\` to \`learn_from_attempts\` calls_`);
|
|
1976
|
+
}
|
|
1977
|
+
// ── Related lessons via BM25 on filename tokens ────────────────────
|
|
1978
|
+
const tokens = fp.replace(/[^a-z0-9]/gi, ' ').split(/\s+/).filter(t => t.length >= 4);
|
|
1979
|
+
const lessonQuery = tokens.slice(0, 3).join(' ');
|
|
1980
|
+
const relatedLessons = [];
|
|
1981
|
+
if (lessonQuery) {
|
|
1982
|
+
try {
|
|
1983
|
+
const hits = await keywordSearch(redis, ['cachly:lesson:best:*'], lessonQuery, 4);
|
|
1984
|
+
for (const h of hits) {
|
|
1985
|
+
const ld = safeJsonParse(h.content, {});
|
|
1986
|
+
if (ld.visibility === 'private')
|
|
1987
|
+
continue;
|
|
1988
|
+
const emoji = ld.outcome === 'success' ? '✅' : ld.outcome === 'partial' ? '⚠️' : '❌';
|
|
1989
|
+
const sev = ld.severity === 'critical' ? ' 🔴' : ld.severity === 'major' ? ' 🟡' : '';
|
|
1990
|
+
const by = ld.author ? ` · 👤 ${ld.author}` : '';
|
|
1991
|
+
const topic = h.key.replace('cachly:lesson:best:', '');
|
|
1992
|
+
relatedLessons.push(` - ${emoji} \`${topic}\`${sev}${by} — ${(ld.what_worked ?? '').slice(0, 100)}`);
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
catch { /* non-critical */ }
|
|
1996
|
+
}
|
|
1997
|
+
// Also: exact file_paths match via lesson scan
|
|
1998
|
+
if (relatedLessons.length < 2) {
|
|
1999
|
+
const scanKeys = [];
|
|
2000
|
+
const stream = redis.scanStream({ match: 'cachly:lesson:best:*', count: 200 });
|
|
2001
|
+
await new Promise((res, rej) => {
|
|
2002
|
+
stream.on('data', (b) => scanKeys.push(...b));
|
|
2003
|
+
stream.on('end', res);
|
|
2004
|
+
stream.on('error', rej);
|
|
2005
|
+
});
|
|
2006
|
+
for (const k of scanKeys.slice(0, 300)) {
|
|
2007
|
+
const raw = await redis.get(k);
|
|
2008
|
+
if (!raw)
|
|
2009
|
+
continue;
|
|
2010
|
+
const ld = safeJsonParse(raw, {});
|
|
2011
|
+
if (ld.visibility === 'private')
|
|
2012
|
+
continue;
|
|
2013
|
+
if (!(ld.file_paths ?? []).includes(fp))
|
|
2014
|
+
continue;
|
|
2015
|
+
const topic = k.replace('cachly:lesson:best:', '');
|
|
2016
|
+
if (relatedLessons.some(l => l.includes(`\`${topic}\``)))
|
|
2017
|
+
continue;
|
|
2018
|
+
const emoji = ld.outcome === 'success' ? '✅' : ld.outcome === 'partial' ? '⚠️' : '❌';
|
|
2019
|
+
const sev = ld.severity === 'critical' ? ' 🔴' : ld.severity === 'major' ? ' 🟡' : '';
|
|
2020
|
+
const by = ld.author ? ` · 👤 ${ld.author}` : '';
|
|
2021
|
+
relatedLessons.push(` - ${emoji} \`${topic}\`${sev}${by} — ${(ld.what_worked ?? '').slice(0, 100)}`);
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
if (relatedLessons.length > 0) {
|
|
2025
|
+
lines.push(`**Related lessons:**`);
|
|
2026
|
+
lines.push(...relatedLessons.slice(0, 5));
|
|
2027
|
+
}
|
|
2028
|
+
else {
|
|
2029
|
+
lines.push(`**Related lessons:** _None yet_`);
|
|
2030
|
+
}
|
|
2031
|
+
lines.push(``);
|
|
2032
|
+
}
|
|
2033
|
+
lines.push(`---`, `_Attribution builds with \`learn_from_attempts(file_paths=[...], author="...")\`. Run this before committing to track what changed and why._`);
|
|
2034
|
+
return lines.join('\n');
|
|
2035
|
+
}
|
|
2036
|
+
// ── team_expertise_map ────────────────────────────────────────────────────
|
|
2037
|
+
// Phase 3B: Full team expertise overview — who knows what, at a glance.
|
|
2038
|
+
case 'team_expertise_map': {
|
|
2039
|
+
const { instance_id, top_n = 20, } = args;
|
|
2040
|
+
const redis = await getConnection(instance_id);
|
|
2041
|
+
// Scan all person nodes
|
|
2042
|
+
const personKeys = [];
|
|
2043
|
+
const pStream = redis.scanStream({ match: 'cachly:ckg:node:person:*', count: 200 });
|
|
2044
|
+
await new Promise((res, rej) => {
|
|
2045
|
+
pStream.on('data', (b) => personKeys.push(...b));
|
|
2046
|
+
pStream.on('end', res);
|
|
2047
|
+
pStream.on('error', rej);
|
|
2048
|
+
});
|
|
2049
|
+
if (personKeys.length === 0) {
|
|
2050
|
+
return [
|
|
2051
|
+
`## 🗺️ Team Expertise Map`,
|
|
2052
|
+
``,
|
|
2053
|
+
`No contributors yet.`,
|
|
2054
|
+
``,
|
|
2055
|
+
`Add \`author="handle"\` to any \`learn_from_attempts\` call to start building the map.`,
|
|
2056
|
+
].join('\n');
|
|
2057
|
+
}
|
|
2058
|
+
const people = [];
|
|
2059
|
+
for (const pk of personKeys) {
|
|
2060
|
+
const raw = await redis.get(pk);
|
|
2061
|
+
if (!raw)
|
|
2062
|
+
continue;
|
|
2063
|
+
const pn = safeJsonParse(raw, null);
|
|
2064
|
+
if (!pn || pn.type !== 'person')
|
|
2065
|
+
continue;
|
|
2066
|
+
// Count authored lessons + collect domains from authored edges
|
|
2067
|
+
const outEdgeKeys = await redis.smembers(`cachly:ckg:idx:from:${pn.id}`);
|
|
2068
|
+
const domainCounts = new Map();
|
|
2069
|
+
let lessonCount = 0;
|
|
2070
|
+
for (const ek of outEdgeKeys) {
|
|
2071
|
+
const er = await redis.get(ek);
|
|
2072
|
+
if (!er)
|
|
2073
|
+
continue;
|
|
2074
|
+
const edge = safeJsonParse(er, null);
|
|
2075
|
+
if (!edge || edge.edgeType !== 'authored')
|
|
2076
|
+
continue;
|
|
2077
|
+
lessonCount++;
|
|
2078
|
+
const domain = edge.to.split(':')[0] ?? 'unknown';
|
|
2079
|
+
domainCounts.set(domain, (domainCounts.get(domain) ?? 0) + 1);
|
|
2080
|
+
}
|
|
2081
|
+
people.push({ handle: pn.handle, lessonCount, domains: domainCounts, lastActive: pn.last_active });
|
|
2082
|
+
}
|
|
2083
|
+
people.sort((a, b) => b.lessonCount - a.lessonCount);
|
|
2084
|
+
const topPeople = people.slice(0, top_n);
|
|
2085
|
+
const lines = [
|
|
2086
|
+
`## 🗺️ Team Expertise Map`,
|
|
2087
|
+
``,
|
|
2088
|
+
`**${topPeople.length}** contributor${topPeople.length !== 1 ? 's' : ''} tracked:`,
|
|
2089
|
+
``,
|
|
2090
|
+
`| # | Handle | Lessons | Top domains | Last active |`,
|
|
2091
|
+
`|---|--------|---------|-------------|-------------|`,
|
|
2092
|
+
];
|
|
2093
|
+
for (const [i, p] of topPeople.entries()) {
|
|
2094
|
+
const medal = i === 0 ? '🥇' : i === 1 ? '🥈' : i === 2 ? '🥉' : `${i + 1}`;
|
|
2095
|
+
const days = Math.floor((Date.now() - new Date(p.lastActive).getTime()) / 86400000);
|
|
2096
|
+
const ago = days < 1 ? 'today' : days < 7 ? `${days}d ago` : days < 30 ? `${Math.floor(days / 7)}w ago` : `${Math.floor(days / 30)}mo ago`;
|
|
2097
|
+
const domainStr = [...p.domains.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3).map(([d, c]) => `${d}(${c})`).join(', ') || '—';
|
|
2098
|
+
lines.push(`| ${medal} | **${p.handle}** | ${p.lessonCount} | ${domainStr} | ${ago} |`);
|
|
2099
|
+
}
|
|
2100
|
+
lines.push(``, `---`, `_Use \`brain_who_knows(topic="...")\` to find the expert for a specific problem._`);
|
|
2101
|
+
return lines.join('\n');
|
|
2102
|
+
}
|
|
1914
2103
|
// ── sync_file_changes ─────────────────────────────────────────────────────
|
|
1915
2104
|
default:
|
|
1916
2105
|
return null;
|