@cachly-dev/mcp-server 0.10.58 → 0.10.59
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 +28 -0
- package/README.md +1 -1
- package/dist/src/__tests__/brain-flow.test.js +101 -1
- package/dist/src/__tests__/brain-flow.test.js.map +1 -1
- package/dist/src/ckg.d.ts +20 -0
- package/dist/src/ckg.js +29 -0
- package/dist/src/ckg.js.map +1 -1
- package/dist/src/handlers/brain.js +129 -3
- package/dist/src/handlers/brain.js.map +1 -1
- package/dist/src/index.js +3 -3
- package/dist/src/tools.d.ts +21 -0
- package/dist/src/tools.js +17 -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 +114 -1
- package/src/ckg.ts +53 -3
- package/src/handlers/brain.ts +154 -4
- package/src/index.ts +3 -3
- package/src/tools.ts +18 -0
package/src/handlers/brain.ts
CHANGED
|
@@ -2,17 +2,25 @@ import { execSync } from 'node:child_process';
|
|
|
2
2
|
import type { Redis } from 'ioredis';
|
|
3
3
|
import { calculateConfidence, confidenceBadge, STRUCTURED_TEMPLATES,
|
|
4
4
|
CONFIDENCE_WARN_VALUE, CONFIDENCE_STALE_VALUE, CONFIDENCE_WARN_DAYS } from '../confidence.js';
|
|
5
|
-
import { ckgSlug, extractProblemConcept, ckgUpsertNode, ckgUpdateEdge
|
|
5
|
+
import { ckgSlug, extractProblemConcept, ckgUpsertNode, ckgUpdateEdge,
|
|
6
|
+
ckgUpsertPersonNode, ckgUpsertFileNode } from '../ckg.js';
|
|
6
7
|
import { safeJsonParse } from '../utils.js';
|
|
7
|
-
import type { CKGEdge, CKGNode } from '../ckg.js';
|
|
8
|
+
import type { CKGEdge, CKGNode, PersonNode } from '../ckg.js';
|
|
8
9
|
import { keywordSearch, tokenize, splitMultiQuery, levenshtein,
|
|
9
10
|
indexVocab as _indexVocab } from '../search.js';
|
|
10
11
|
import { rerankByQuality } from '../rerank.js';
|
|
11
12
|
import { computeEmbedding, hasEmbedProvider } from '../embeddings.js';
|
|
12
13
|
|
|
13
14
|
// ── Changelog (shown once per version in session_start) ──────────────────────
|
|
14
|
-
const MCP_VERSION = '0.10.
|
|
15
|
+
const MCP_VERSION = '0.10.59';
|
|
15
16
|
const WHATS_NEW: Record<string, string[]> = {
|
|
17
|
+
'0.10.59': [
|
|
18
|
+
`🆕 **Phase 3A: Org-wide knowledge graph**`,
|
|
19
|
+
` 👥 \`brain_who_knows(topic="...")\` — find your team's experts on any topic instantly`,
|
|
20
|
+
` 🕸️ Person + File nodes auto-built from \`learn_from_attempts(author="...", file_paths=[...])\``,
|
|
21
|
+
` 👤 Author attribution now shown inline in \`smart_recall\` results`,
|
|
22
|
+
` 📊 96 MCP tools · 379 tests green`,
|
|
23
|
+
],
|
|
16
24
|
'0.10.58': [
|
|
17
25
|
`🧹 Zero lint warnings — all unused imports cleaned across every handler`,
|
|
18
26
|
`✅ Build: 0 errors, 0 warnings · 379 tests green`,
|
|
@@ -72,6 +80,7 @@ type ApiFetch = <T>(path: string, options?: RequestInit) => Promise<T>;
|
|
|
72
80
|
export const BRAIN_TOOL_NAMES = new Set([
|
|
73
81
|
'learn_from_attempts', 'recall_best_solution', 'smart_recall',
|
|
74
82
|
'session_start', 'session_end', 'session_ping', 'session_handoff', 'auto_learn_session',
|
|
83
|
+
'brain_who_knows',
|
|
75
84
|
]);
|
|
76
85
|
|
|
77
86
|
export async function handleBrainTool(
|
|
@@ -383,6 +392,22 @@ export async function handleBrainTool(
|
|
|
383
392
|
}
|
|
384
393
|
} catch { /* CKG updates are non-critical */ }
|
|
385
394
|
|
|
395
|
+
// ── Phase 3A: People + File nodes in knowledge graph ──────────────────────
|
|
396
|
+
// Builds the "who knows what" map automatically from author + file_paths.
|
|
397
|
+
// Person → authored → Concept edges power brain_who_knows queries.
|
|
398
|
+
if (author) {
|
|
399
|
+
try {
|
|
400
|
+
const domain = topic.split(':')[0] ?? 'unknown';
|
|
401
|
+
const personId = await ckgUpsertPersonNode(redis, author, domain);
|
|
402
|
+
const conceptId = ckgSlug(topic);
|
|
403
|
+
await ckgUpdateEdge(redis, personId, 'authored', conceptId, outcome === 'success', outcome === 'partial');
|
|
404
|
+
for (const fp of file_paths.slice(0, 8)) {
|
|
405
|
+
const fileId = await ckgUpsertFileNode(redis, fp);
|
|
406
|
+
await ckgUpdateEdge(redis, personId, 'touched', fileId, true);
|
|
407
|
+
}
|
|
408
|
+
} catch { /* non-critical */ }
|
|
409
|
+
}
|
|
410
|
+
|
|
386
411
|
// ── Ambient team propagation: if author is set + outcome is success,
|
|
387
412
|
// auto-store as a team lesson so knowledge is shared without a separate
|
|
388
413
|
// team_learn call. Tags gain 'team'; the lesson is attributed to the author.
|
|
@@ -774,13 +799,18 @@ export async function handleBrainTool(
|
|
|
774
799
|
} else {
|
|
775
800
|
for (const r of hybridResults.slice(0, 8)) {
|
|
776
801
|
const label = r.key.replace('cachly:ctx:', '📝 ').replace('cachly:lesson:best:', '💡 ').replace('cachly:idx:', '📂 ');
|
|
802
|
+
let authorBadge = '';
|
|
803
|
+
if (r.key.startsWith('cachly:lesson:best:')) {
|
|
804
|
+
const ld = safeJsonParse<{ author?: string }>(r.content, {});
|
|
805
|
+
if (ld.author) authorBadge = ` · 👤 ${ld.author}`;
|
|
806
|
+
}
|
|
777
807
|
const scorePart = r.matchType === 'hybrid'
|
|
778
808
|
? `BM25: ${(r.bm25Score ?? 0).toFixed(2)}, sem: ${((r.semScore ?? 0) * 100).toFixed(0)}%, 🔀 hybrid`
|
|
779
809
|
: r.matchType === 'semantic'
|
|
780
810
|
? `sem: ${((r.semScore ?? 0) * 100).toFixed(0)}%, 🎯 semantic`
|
|
781
811
|
: `BM25: ${(r.bm25Score ?? 0).toFixed(2)}, matched: ${r.matchedWords?.join(', ')}`;
|
|
782
812
|
const preview = r.content.slice(0, 400).replace(/\n/g, ' ');
|
|
783
|
-
lines.push(`**${label}
|
|
813
|
+
lines.push(`**${label}**${authorBadge} _(${scorePart})_`);
|
|
784
814
|
lines.push(`> ${preview}${r.content.length > 400 ? '…' : ''}\n`);
|
|
785
815
|
}
|
|
786
816
|
}
|
|
@@ -1973,6 +2003,126 @@ export async function handleBrainTool(
|
|
|
1973
2003
|
return lines.join('\n');
|
|
1974
2004
|
}
|
|
1975
2005
|
|
|
2006
|
+
// ── brain_who_knows ───────────────────────────────────────────────────────
|
|
2007
|
+
// Phase 3A: Org-wide people intelligence. Returns ranked list of contributors
|
|
2008
|
+
// whose authored edges in the CKG match the query topic.
|
|
2009
|
+
case 'brain_who_knows': {
|
|
2010
|
+
const {
|
|
2011
|
+
instance_id,
|
|
2012
|
+
topic,
|
|
2013
|
+
limit = 10,
|
|
2014
|
+
} = args as { instance_id: string; topic: string; limit?: number };
|
|
2015
|
+
|
|
2016
|
+
const redis = await getConnection(instance_id);
|
|
2017
|
+
|
|
2018
|
+
// Step 1: Find candidate concept node IDs matching the query
|
|
2019
|
+
const querySlug = ckgSlug(topic);
|
|
2020
|
+
const qTokens = tokenize(topic).filter((t: string) => t.length >= 3).slice(0, 5);
|
|
2021
|
+
|
|
2022
|
+
const candidateConceptIds = new Set<string>([querySlug]);
|
|
2023
|
+
for (const token of qTokens.slice(0, 3)) {
|
|
2024
|
+
const nodeStream = redis.scanStream({ match: `cachly:ckg:node:*${token}*`, count: 30 });
|
|
2025
|
+
await new Promise<void>((res, rej) => {
|
|
2026
|
+
nodeStream.on('data', (batch: string[]) => {
|
|
2027
|
+
for (const k of batch.slice(0, 10)) {
|
|
2028
|
+
const nodeId = k.replace('cachly:ckg:node:', '');
|
|
2029
|
+
if (!nodeId.startsWith('person:') && !nodeId.startsWith('file:'))
|
|
2030
|
+
candidateConceptIds.add(nodeId);
|
|
2031
|
+
}
|
|
2032
|
+
});
|
|
2033
|
+
nodeStream.on('end', res);
|
|
2034
|
+
nodeStream.on('error', rej);
|
|
2035
|
+
});
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
// Step 2: Traverse authored edges (person → concept) to find experts
|
|
2039
|
+
type PersonScore = {
|
|
2040
|
+
handle: string; lessonCount: number; totalConfidence: number;
|
|
2041
|
+
domains: Set<string>; lastActive: string;
|
|
2042
|
+
};
|
|
2043
|
+
const personScores = new Map<string, PersonScore>();
|
|
2044
|
+
|
|
2045
|
+
for (const conceptId of [...candidateConceptIds].slice(0, 25)) {
|
|
2046
|
+
const inboundEdgeKeys = await redis.smembers(`cachly:ckg:idx:to:${conceptId}`);
|
|
2047
|
+
for (const ek of inboundEdgeKeys) {
|
|
2048
|
+
const er = await redis.get(ek);
|
|
2049
|
+
if (!er) continue;
|
|
2050
|
+
const edge = safeJsonParse<CKGEdge | null>(er, null);
|
|
2051
|
+
if (!edge || edge.edgeType !== 'authored') continue;
|
|
2052
|
+
|
|
2053
|
+
const personNodeRaw = await redis.get(`cachly:ckg:node:${edge.from}`);
|
|
2054
|
+
if (!personNodeRaw) continue;
|
|
2055
|
+
const personNode = safeJsonParse<PersonNode | null>(personNodeRaw, null);
|
|
2056
|
+
if (!personNode || personNode.type !== 'person') continue;
|
|
2057
|
+
|
|
2058
|
+
const existing = personScores.get(edge.from);
|
|
2059
|
+
if (existing) {
|
|
2060
|
+
existing.lessonCount++;
|
|
2061
|
+
existing.totalConfidence += edge.confidence;
|
|
2062
|
+
existing.domains.add(personNode.domain);
|
|
2063
|
+
if (edge.last_updated > existing.lastActive) existing.lastActive = edge.last_updated;
|
|
2064
|
+
} else {
|
|
2065
|
+
personScores.set(edge.from, {
|
|
2066
|
+
handle: personNode.handle,
|
|
2067
|
+
lessonCount: 1,
|
|
2068
|
+
totalConfidence: edge.confidence,
|
|
2069
|
+
domains: new Set([personNode.domain]),
|
|
2070
|
+
lastActive: personNode.last_active ?? edge.last_updated,
|
|
2071
|
+
});
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
if (personScores.size === 0) {
|
|
2077
|
+
return [
|
|
2078
|
+
`## 👥 Who Knows About \`${topic}\`?`,
|
|
2079
|
+
``,
|
|
2080
|
+
`No attributed lessons found yet.`,
|
|
2081
|
+
``,
|
|
2082
|
+
`Knowledge attribution builds automatically — add \`author="name"\` to \`learn_from_attempts\` calls:`,
|
|
2083
|
+
`\`learn_from_attempts(topic="${topic}", author="your-handle", ...)\``,
|
|
2084
|
+
].join('\n');
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
// Step 3: Sort by expertise score (lesson count × avg confidence)
|
|
2088
|
+
const ranked = [...personScores.entries()]
|
|
2089
|
+
.map(([, s]) => ({
|
|
2090
|
+
handle: s.handle,
|
|
2091
|
+
lessonCount: s.lessonCount,
|
|
2092
|
+
avgConfidence: s.totalConfidence / s.lessonCount,
|
|
2093
|
+
domains: [...s.domains].slice(0, 3).join(', '),
|
|
2094
|
+
lastActive: s.lastActive,
|
|
2095
|
+
}))
|
|
2096
|
+
.sort((a, b) => (b.lessonCount * b.avgConfidence) - (a.lessonCount * a.avgConfidence))
|
|
2097
|
+
.slice(0, limit);
|
|
2098
|
+
|
|
2099
|
+
const lines = [
|
|
2100
|
+
`## 👥 Who Knows About \`${topic}\`?`,
|
|
2101
|
+
``,
|
|
2102
|
+
`**${ranked.length}** contributor${ranked.length !== 1 ? 's' : ''} with relevant lessons:`,
|
|
2103
|
+
``,
|
|
2104
|
+
];
|
|
2105
|
+
|
|
2106
|
+
for (const [i, p] of ranked.entries()) {
|
|
2107
|
+
const medal = i === 0 ? '🥇' : i === 1 ? '🥈' : i === 2 ? '🥉' : `${i + 1}.`;
|
|
2108
|
+
const conf = (p.avgConfidence * 100).toFixed(0);
|
|
2109
|
+
const daysSince = Math.floor((Date.now() - new Date(p.lastActive).getTime()) / 86400000);
|
|
2110
|
+
const recency = daysSince < 1 ? 'today' : daysSince < 7 ? `${daysSince}d ago` : daysSince < 30 ? `${Math.floor(daysSince / 7)}w ago` : `${Math.floor(daysSince / 30)}mo ago`;
|
|
2111
|
+
lines.push(
|
|
2112
|
+
`${medal} **${p.handle}** — ${p.lessonCount} lesson${p.lessonCount !== 1 ? 's' : ''} · ${conf}% confidence · last active ${recency}`,
|
|
2113
|
+
` _domains: ${p.domains}_`,
|
|
2114
|
+
``,
|
|
2115
|
+
);
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
lines.push(
|
|
2119
|
+
`---`,
|
|
2120
|
+
`_Attribution grows with every \`learn_from_attempts(author="...", ...)\` call._`,
|
|
2121
|
+
);
|
|
2122
|
+
|
|
2123
|
+
return lines.join('\n');
|
|
2124
|
+
}
|
|
2125
|
+
|
|
1976
2126
|
// ── sync_file_changes ─────────────────────────────────────────────────────
|
|
1977
2127
|
|
|
1978
2128
|
default:
|
package/src/index.ts
CHANGED
|
@@ -73,7 +73,7 @@ import { Redis } from 'ioredis';
|
|
|
73
73
|
const API_URL = process.env.CACHLY_API_URL ?? 'https://api.cachly.dev';
|
|
74
74
|
let JWT = process.env.CACHLY_JWT ?? '';
|
|
75
75
|
const _EMBED_MODEL = process.env.CACHLY_EMBED_MODEL ?? '';
|
|
76
|
-
const CURRENT_VERSION = '0.10.
|
|
76
|
+
const CURRENT_VERSION = '0.10.59';
|
|
77
77
|
|
|
78
78
|
// Max time to wait for a freshly-provisioned instance to become "running" before
|
|
79
79
|
// giving up. Free-tier provisioning in high-latency regions can take 45–90s, so the
|
|
@@ -572,7 +572,7 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
|
|
|
572
572
|
'',
|
|
573
573
|
'After sign-in: call **any tool again** — your Brain activates instantly.',
|
|
574
574
|
'',
|
|
575
|
-
'✨ Free forever · No credit card ·
|
|
575
|
+
'✨ Free forever · No credit card · 96 MCP tools · GDPR · EU servers',
|
|
576
576
|
].join('\n');
|
|
577
577
|
}
|
|
578
578
|
|
|
@@ -1816,7 +1816,7 @@ if (!process.argv[2] && process.stdout.isTTY) {
|
|
|
1816
1816
|
console.log(' \x1b[36m npx @cachly-dev/mcp-server@latest upgrade\x1b[0m ← Check for updates');
|
|
1817
1817
|
console.log('');
|
|
1818
1818
|
console.log(' \x1b[90mWorks with: Claude Code · Cursor · Windsurf · GitHub Copilot · Cline · Zed\x1b[0m');
|
|
1819
|
-
console.log(' \x1b[90mFree forever · GDPR · German servers ·
|
|
1819
|
+
console.log(' \x1b[90mFree forever · GDPR · German servers · 96 MCP tools\x1b[0m');
|
|
1820
1820
|
console.log('');
|
|
1821
1821
|
process.exit(0);
|
|
1822
1822
|
}
|
package/src/tools.ts
CHANGED
|
@@ -799,6 +799,24 @@ const TOOLS = [
|
|
|
799
799
|
required: ['instance_id', 'observations'],
|
|
800
800
|
},
|
|
801
801
|
},
|
|
802
|
+
{
|
|
803
|
+
name: 'brain_who_knows',
|
|
804
|
+
description:
|
|
805
|
+
'Find who in your team has the most expertise on a given topic. ' +
|
|
806
|
+
'Queries the org-wide knowledge graph (built automatically from learn_from_attempts author fields) ' +
|
|
807
|
+
'and returns a ranked list of contributors whose lessons match the query, ordered by lesson count and confidence. ' +
|
|
808
|
+
'Use to find the right person to ask before starting a task, or to understand knowledge distribution. ' +
|
|
809
|
+
'Example: brain_who_knows(topic="kubernetes deployment") → "🥇 alice — 5 lessons, 94% confidence".',
|
|
810
|
+
inputSchema: {
|
|
811
|
+
type: 'object',
|
|
812
|
+
properties: {
|
|
813
|
+
instance_id: { type: 'string', description: 'UUID of the cache instance' },
|
|
814
|
+
topic: { type: 'string', description: 'Topic or question to find experts for' },
|
|
815
|
+
limit: { type: 'number', description: 'Max number of experts to return (default: 10)' },
|
|
816
|
+
},
|
|
817
|
+
required: ['instance_id', 'topic'],
|
|
818
|
+
},
|
|
819
|
+
},
|
|
802
820
|
{
|
|
803
821
|
name: 'sync_file_changes',
|
|
804
822
|
description:
|