@cachly-dev/mcp-server 0.10.60 → 0.10.62
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 +23 -0
- package/README.md +1 -1
- package/dist/src/__tests__/brain-flow.test.js +98 -0
- package/dist/src/__tests__/brain-flow.test.js.map +1 -1
- package/dist/src/handlers/brain.js +203 -5
- package/dist/src/handlers/brain.js.map +1 -1
- package/dist/src/handlers/fedbrain.js +37 -10
- package/dist/src/handlers/fedbrain.js.map +1 -1
- package/dist/src/index.js +3 -3
- package/dist/src/index.js.map +1 -1
- package/dist/src/tools.d.ts +34 -0
- package/dist/src/tools.js +32 -0
- 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 +116 -0
- package/src/handlers/brain.ts +222 -4
- package/src/handlers/fedbrain.ts +38 -10
- package/src/index.ts +3 -3
- package/src/tools.ts +34 -0
|
@@ -6,8 +6,14 @@ 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.62';
|
|
10
10
|
const WHATS_NEW = {
|
|
11
|
+
'0.10.61': [
|
|
12
|
+
`🎯 **Phase 3C: 100 MCP tools milestone — zero-setup knowledge graph**`,
|
|
13
|
+
` 🔄 \`brain_from_git\` now auto-builds Person+File nodes from git history (zero setup!)`,
|
|
14
|
+
` 🔍 \`skill_gaps()\` — find domains with unresolved failures, missing attribution`,
|
|
15
|
+
` 📊 \`brain_coverage()\` — scored 0-100 health report: lessons · attribution · file coverage`,
|
|
16
|
+
],
|
|
11
17
|
'0.10.60': [
|
|
12
18
|
`🆕 **Phase 3B: File map + team overview + visibility**`,
|
|
13
19
|
` 📁 \`brain_file_map(file_paths=[...])\` — experts + lessons per file before you touch it`,
|
|
@@ -63,6 +69,7 @@ export const BRAIN_TOOL_NAMES = new Set([
|
|
|
63
69
|
'learn_from_attempts', 'recall_best_solution', 'smart_recall',
|
|
64
70
|
'session_start', 'session_end', 'session_ping', 'session_handoff', 'auto_learn_session',
|
|
65
71
|
'brain_who_knows', 'brain_file_map', 'team_expertise_map',
|
|
72
|
+
'skill_gaps', 'brain_coverage',
|
|
66
73
|
]);
|
|
67
74
|
export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
68
75
|
switch (name) {
|
|
@@ -1836,6 +1843,10 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
1836
1843
|
// whose authored edges in the CKG match the query topic.
|
|
1837
1844
|
case 'brain_who_knows': {
|
|
1838
1845
|
const { instance_id, topic, limit = 10, } = args;
|
|
1846
|
+
if (!topic || typeof topic !== 'string' || !topic.trim()) {
|
|
1847
|
+
return '⚠️ `brain_who_knows` requires a non-empty `topic`. Example: `brain_who_knows(topic="kubernetes deployment")`.';
|
|
1848
|
+
}
|
|
1849
|
+
const safeLimit = Math.max(1, Math.min(Number(limit) || 10, 50));
|
|
1839
1850
|
const redis = await getConnection(instance_id);
|
|
1840
1851
|
// Step 1: Find candidate concept node IDs matching the query
|
|
1841
1852
|
const querySlug = ckgSlug(topic);
|
|
@@ -1910,7 +1921,7 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
1910
1921
|
lastActive: s.lastActive,
|
|
1911
1922
|
}))
|
|
1912
1923
|
.sort((a, b) => (b.lessonCount * b.avgConfidence) - (a.lessonCount * a.avgConfidence))
|
|
1913
|
-
.slice(0,
|
|
1924
|
+
.slice(0, safeLimit);
|
|
1914
1925
|
const lines = [
|
|
1915
1926
|
`## 👥 Who Knows About \`${topic}\`?`,
|
|
1916
1927
|
``,
|
|
@@ -1931,8 +1942,12 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
1931
1942
|
// Phase 3B: "What do we know about these files?" — experts + lessons per file.
|
|
1932
1943
|
case 'brain_file_map': {
|
|
1933
1944
|
const { instance_id, file_paths = [], } = args;
|
|
1934
|
-
|
|
1935
|
-
|
|
1945
|
+
// Defensive: accept only a non-empty array of non-empty strings.
|
|
1946
|
+
const cleanPaths = (Array.isArray(file_paths) ? file_paths : [])
|
|
1947
|
+
.filter((p) => typeof p === 'string' && p.trim().length > 0)
|
|
1948
|
+
.map(p => p.trim());
|
|
1949
|
+
if (!cleanPaths.length)
|
|
1950
|
+
return '⚠️ Pass at least one file path. Example: `brain_file_map(file_paths=["src/auth/jwt.ts"])`.';
|
|
1936
1951
|
const redis = await getConnection(instance_id);
|
|
1937
1952
|
const lines = [
|
|
1938
1953
|
`## 📁 File Knowledge Map`,
|
|
@@ -1940,7 +1955,7 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
1940
1955
|
`_What cachly knows about the files you're about to touch:_`,
|
|
1941
1956
|
``,
|
|
1942
1957
|
];
|
|
1943
|
-
for (const fp of
|
|
1958
|
+
for (const fp of cleanPaths.slice(0, 10)) {
|
|
1944
1959
|
lines.push(`### \`${fp}\``);
|
|
1945
1960
|
// ── Experts via CKG file nodes ─────────────────────────────────────
|
|
1946
1961
|
const fileId = `file:${ckgSlug(fp)}`;
|
|
@@ -2100,6 +2115,189 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
2100
2115
|
lines.push(``, `---`, `_Use \`brain_who_knows(topic="...")\` to find the expert for a specific problem._`);
|
|
2101
2116
|
return lines.join('\n');
|
|
2102
2117
|
}
|
|
2118
|
+
// ── skill_gaps ────────────────────────────────────────────────────────────
|
|
2119
|
+
// Phase 3C: Show knowledge blind spots — domains/files with no success lessons,
|
|
2120
|
+
// no attribution, or only failures.
|
|
2121
|
+
case 'skill_gaps': {
|
|
2122
|
+
const { instance_id, min_failures = 1, } = args;
|
|
2123
|
+
const redis = await getConnection(instance_id);
|
|
2124
|
+
// Scan all best lessons
|
|
2125
|
+
const lessonKeys = [];
|
|
2126
|
+
const lStream = redis.scanStream({ match: 'cachly:lesson:best:*', count: 500 });
|
|
2127
|
+
await new Promise((res, rej) => {
|
|
2128
|
+
lStream.on('data', (b) => lessonKeys.push(...b));
|
|
2129
|
+
lStream.on('end', res);
|
|
2130
|
+
lStream.on('error', rej);
|
|
2131
|
+
});
|
|
2132
|
+
const domainMap = new Map();
|
|
2133
|
+
let totalAttributed = 0;
|
|
2134
|
+
let totalPrivate = 0;
|
|
2135
|
+
for (const k of lessonKeys) {
|
|
2136
|
+
const raw = await redis.get(k);
|
|
2137
|
+
if (!raw)
|
|
2138
|
+
continue;
|
|
2139
|
+
const l = safeJsonParse(raw, {});
|
|
2140
|
+
const domain = (l.topic ?? '').split(':')[0] ?? 'unknown';
|
|
2141
|
+
const s = domainMap.get(domain) ?? { successes: 0, failures: 0, attributed: 0, topics: [] };
|
|
2142
|
+
if (l.outcome === 'success' || l.outcome === 'partial')
|
|
2143
|
+
s.successes++;
|
|
2144
|
+
else
|
|
2145
|
+
s.failures++;
|
|
2146
|
+
if (l.author) {
|
|
2147
|
+
s.attributed++;
|
|
2148
|
+
totalAttributed++;
|
|
2149
|
+
}
|
|
2150
|
+
if (l.visibility === 'private')
|
|
2151
|
+
totalPrivate++;
|
|
2152
|
+
s.topics.push(l.topic ?? '');
|
|
2153
|
+
domainMap.set(domain, s);
|
|
2154
|
+
}
|
|
2155
|
+
const gaps = [];
|
|
2156
|
+
for (const [domain, s] of domainMap) {
|
|
2157
|
+
if (s.failures >= min_failures && s.successes === 0) {
|
|
2158
|
+
gaps.push({
|
|
2159
|
+
domain, type: 'unresolved failures',
|
|
2160
|
+
severity: s.failures >= 3 ? '🔴 critical' : '🟡 warn',
|
|
2161
|
+
detail: `${s.failures} failure${s.failures !== 1 ? 's' : ''}, 0 solutions — use \`learn_from_attempts\` to store what fixed it`,
|
|
2162
|
+
});
|
|
2163
|
+
}
|
|
2164
|
+
if (s.successes + s.failures >= 3 && s.attributed === 0) {
|
|
2165
|
+
gaps.push({
|
|
2166
|
+
domain, type: 'no attribution',
|
|
2167
|
+
severity: '🔵 info',
|
|
2168
|
+
detail: `${s.successes + s.failures} lessons with no \`author\` field — attribution lost, \`brain_who_knows\` can't help here`,
|
|
2169
|
+
});
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
// Check for domains with person nodes but no lessons (contributors without lessons)
|
|
2173
|
+
const personKeys = [];
|
|
2174
|
+
const pStream = redis.scanStream({ match: 'cachly:ckg:node:person:*', count: 200 });
|
|
2175
|
+
await new Promise((res, rej) => {
|
|
2176
|
+
pStream.on('data', (b) => personKeys.push(...b));
|
|
2177
|
+
pStream.on('end', res);
|
|
2178
|
+
pStream.on('error', rej);
|
|
2179
|
+
});
|
|
2180
|
+
const lines = [
|
|
2181
|
+
`## 🔍 Skill Gaps — Knowledge Blind Spots`,
|
|
2182
|
+
``,
|
|
2183
|
+
`**${lessonKeys.length}** lessons scanned · **${totalAttributed}** attributed · **${gaps.length}** gap${gaps.length !== 1 ? 's' : ''} found`,
|
|
2184
|
+
totalPrivate > 0 ? `_(${totalPrivate} private lessons excluded from gap analysis)_` : '',
|
|
2185
|
+
``,
|
|
2186
|
+
].filter(Boolean);
|
|
2187
|
+
if (gaps.length === 0) {
|
|
2188
|
+
lines.push(`✅ No significant knowledge gaps detected. All domains with failures have at least one success lesson.`);
|
|
2189
|
+
}
|
|
2190
|
+
else {
|
|
2191
|
+
const bySev = [
|
|
2192
|
+
...gaps.filter(g => g.severity.includes('critical')),
|
|
2193
|
+
...gaps.filter(g => g.severity.includes('warn')),
|
|
2194
|
+
...gaps.filter(g => g.severity.includes('info')),
|
|
2195
|
+
];
|
|
2196
|
+
for (const g of bySev.slice(0, 15)) {
|
|
2197
|
+
lines.push(`${g.severity} **\`${g.domain}\`** — ${g.type}`);
|
|
2198
|
+
lines.push(` ${g.detail}`);
|
|
2199
|
+
lines.push(``);
|
|
2200
|
+
}
|
|
2201
|
+
if (gaps.length > 15)
|
|
2202
|
+
lines.push(`_…and ${gaps.length - 15} more gaps. Fix critical ones first._`);
|
|
2203
|
+
}
|
|
2204
|
+
lines.push(`---`, `_Fix gaps with \`learn_from_attempts(topic="${gaps[0]?.domain ?? 'your:topic'}", outcome="success", what_worked="...")\`_`, `_Add \`author="name"\` to every call to enable \`brain_who_knows\` + \`team_expertise_map\`._`);
|
|
2205
|
+
return lines.join('\n');
|
|
2206
|
+
}
|
|
2207
|
+
// ── brain_coverage ────────────────────────────────────────────────────────
|
|
2208
|
+
// Phase 3C: Knowledge-coverage health score for the repository.
|
|
2209
|
+
case 'brain_coverage': {
|
|
2210
|
+
const { instance_id, repo_path = '.', } = args;
|
|
2211
|
+
const redis = await getConnection(instance_id);
|
|
2212
|
+
// Count lessons
|
|
2213
|
+
const lessonKeys = [];
|
|
2214
|
+
const lStream = redis.scanStream({ match: 'cachly:lesson:best:*', count: 500 });
|
|
2215
|
+
await new Promise((res, rej) => {
|
|
2216
|
+
lStream.on('data', (b) => lessonKeys.push(...b));
|
|
2217
|
+
lStream.on('end', res);
|
|
2218
|
+
lStream.on('error', rej);
|
|
2219
|
+
});
|
|
2220
|
+
// Count person nodes
|
|
2221
|
+
const personKeys = [];
|
|
2222
|
+
const pStream = redis.scanStream({ match: 'cachly:ckg:node:person:*', count: 200 });
|
|
2223
|
+
await new Promise((res, rej) => {
|
|
2224
|
+
pStream.on('data', (b) => personKeys.push(...b));
|
|
2225
|
+
pStream.on('end', res);
|
|
2226
|
+
pStream.on('error', rej);
|
|
2227
|
+
});
|
|
2228
|
+
// Count file nodes in CKG
|
|
2229
|
+
const fileKeys = [];
|
|
2230
|
+
const fStream = redis.scanStream({ match: 'cachly:ckg:node:file:*', count: 500 });
|
|
2231
|
+
await new Promise((res, rej) => {
|
|
2232
|
+
fStream.on('data', (b) => fileKeys.push(...b));
|
|
2233
|
+
fStream.on('end', res);
|
|
2234
|
+
fStream.on('error', rej);
|
|
2235
|
+
});
|
|
2236
|
+
// Get git file count for comparison (optional — non-critical if not a git repo)
|
|
2237
|
+
let gitFileCount = 0;
|
|
2238
|
+
let _gitFileCountLabel = 'n/a';
|
|
2239
|
+
try {
|
|
2240
|
+
const { execSync } = await import('node:child_process');
|
|
2241
|
+
const { resolve } = await import('node:path');
|
|
2242
|
+
const repoDir = resolve(repo_path);
|
|
2243
|
+
const output = execSync('git ls-files', { cwd: repoDir, encoding: 'utf-8', stdio: 'pipe' });
|
|
2244
|
+
gitFileCount = output.trim().split('\n').filter(Boolean).length;
|
|
2245
|
+
_gitFileCountLabel = String(gitFileCount);
|
|
2246
|
+
}
|
|
2247
|
+
catch { /* not a git repo — skip */ }
|
|
2248
|
+
// Lesson quality breakdown
|
|
2249
|
+
let successCount = 0;
|
|
2250
|
+
let failureCount = 0;
|
|
2251
|
+
let attributedCount = 0;
|
|
2252
|
+
const domainSet = new Set();
|
|
2253
|
+
for (const k of lessonKeys) {
|
|
2254
|
+
const raw = await redis.get(k);
|
|
2255
|
+
if (!raw)
|
|
2256
|
+
continue;
|
|
2257
|
+
const l = safeJsonParse(raw, {});
|
|
2258
|
+
if (l.outcome === 'success' || l.outcome === 'partial')
|
|
2259
|
+
successCount++;
|
|
2260
|
+
else
|
|
2261
|
+
failureCount++;
|
|
2262
|
+
if (l.author)
|
|
2263
|
+
attributedCount++;
|
|
2264
|
+
domainSet.add((l.topic ?? '').split(':')[0] ?? 'unknown');
|
|
2265
|
+
}
|
|
2266
|
+
const fileCoverage = gitFileCount > 0
|
|
2267
|
+
? `${Math.round((fileKeys.length / gitFileCount) * 100)}% (${fileKeys.length}/${gitFileCount} files)`
|
|
2268
|
+
: `${fileKeys.length} files tracked`;
|
|
2269
|
+
const attributionPct = lessonKeys.length > 0
|
|
2270
|
+
? Math.round((attributedCount / lessonKeys.length) * 100)
|
|
2271
|
+
: 0;
|
|
2272
|
+
const overallScore = Math.round((Math.min(1, lessonKeys.length / 50) * 25) + // lesson volume (25 pts)
|
|
2273
|
+
(Math.min(1, successCount / Math.max(1, lessonKeys.length)) * 25) + // success ratio (25 pts)
|
|
2274
|
+
(Math.min(1, attributionPct / 100) * 25) + // attribution completeness (25 pts)
|
|
2275
|
+
(Math.min(1, personKeys.length / 5) * 25) // team engagement (25 pts)
|
|
2276
|
+
);
|
|
2277
|
+
const scoreEmoji = overallScore >= 80 ? '🟢' : overallScore >= 50 ? '🟡' : '🔴';
|
|
2278
|
+
return [
|
|
2279
|
+
`## 📊 Brain Coverage Report`,
|
|
2280
|
+
``,
|
|
2281
|
+
`${scoreEmoji} **Overall score: ${overallScore}/100**`,
|
|
2282
|
+
``,
|
|
2283
|
+
`| Metric | Value |`,
|
|
2284
|
+
`|--------|-------|`,
|
|
2285
|
+
`| Total lessons | **${lessonKeys.length}** (${successCount} success · ${failureCount} failure) |`,
|
|
2286
|
+
`| Problem domains | **${domainSet.size}** |`,
|
|
2287
|
+
`| Attribution | **${attributedCount}/${lessonKeys.length}** (${attributionPct}%) |`,
|
|
2288
|
+
`| Contributors | **${personKeys.length}** tracked in knowledge graph |`,
|
|
2289
|
+
`| File coverage | **${fileCoverage}** |`,
|
|
2290
|
+
``,
|
|
2291
|
+
`**Score breakdown (each 0–25):**`,
|
|
2292
|
+
`- Lesson volume: **${Math.round(Math.min(1, lessonKeys.length / 50) * 25)}/25** _(target: 50+ lessons)_`,
|
|
2293
|
+
`- Success ratio: **${Math.round(Math.min(1, successCount / Math.max(1, lessonKeys.length)) * 25)}/25** _(target: all lessons resolved)_`,
|
|
2294
|
+
`- Attribution: **${Math.round(Math.min(1, attributionPct / 100) * 25)}/25** _(target: 100% attributed)_`,
|
|
2295
|
+
`- Team engagement: **${Math.round(Math.min(1, personKeys.length / 5) * 25)}/25** _(target: 5+ contributors)_`,
|
|
2296
|
+
``,
|
|
2297
|
+
`---`,
|
|
2298
|
+
`_Boost score: run \`brain_from_git\` · add \`author\` to \`learn_from_attempts\` · fix gaps with \`skill_gaps\`_`,
|
|
2299
|
+
].join('\n');
|
|
2300
|
+
}
|
|
2103
2301
|
// ── sync_file_changes ─────────────────────────────────────────────────────
|
|
2104
2302
|
default:
|
|
2105
2303
|
return null;
|