@cachly-dev/mcp-server 0.10.58 → 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 +48 -0
- package/README.md +1 -1
- package/dist/src/__tests__/brain-flow.test.js +198 -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 +322 -7
- package/dist/src/handlers/brain.js.map +1 -1
- package/dist/src/index.js +3 -3
- package/dist/src/tools.d.ts +64 -1
- package/dist/src/tools.js +58 -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 +228 -1
- package/src/ckg.ts +53 -3
- package/src/handlers/brain.ts +370 -8
- package/src/index.ts +3 -3
- package/src/tools.ts +61 -1
|
@@ -1,13 +1,27 @@
|
|
|
1
1
|
import { execSync } from 'node:child_process';
|
|
2
2
|
import { calculateConfidence, confidenceBadge, STRUCTURED_TEMPLATES, CONFIDENCE_WARN_VALUE, CONFIDENCE_STALE_VALUE, CONFIDENCE_WARN_DAYS } from '../confidence.js';
|
|
3
|
-
import { ckgSlug, extractProblemConcept, ckgUpsertNode, ckgUpdateEdge } from '../ckg.js';
|
|
3
|
+
import { ckgSlug, extractProblemConcept, ckgUpsertNode, ckgUpdateEdge, ckgUpsertPersonNode, ckgUpsertFileNode } from '../ckg.js';
|
|
4
4
|
import { safeJsonParse } from '../utils.js';
|
|
5
5
|
import { keywordSearch, tokenize, splitMultiQuery, levenshtein, indexVocab as _indexVocab } from '../search.js';
|
|
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
|
+
],
|
|
18
|
+
'0.10.59': [
|
|
19
|
+
`🆕 **Phase 3A: Org-wide knowledge graph**`,
|
|
20
|
+
` 👥 \`brain_who_knows(topic="...")\` — find your team's experts on any topic instantly`,
|
|
21
|
+
` 🕸️ Person + File nodes auto-built from \`learn_from_attempts(author="...", file_paths=[...])\``,
|
|
22
|
+
` 👤 Author attribution now shown inline in \`smart_recall\` results`,
|
|
23
|
+
` 📊 96 MCP tools · 386 tests green`,
|
|
24
|
+
],
|
|
11
25
|
'0.10.58': [
|
|
12
26
|
`🧹 Zero lint warnings — all unused imports cleaned across every handler`,
|
|
13
27
|
`✅ Build: 0 errors, 0 warnings · 379 tests green`,
|
|
@@ -48,11 +62,12 @@ const WHATS_NEW = {
|
|
|
48
62
|
export const BRAIN_TOOL_NAMES = new Set([
|
|
49
63
|
'learn_from_attempts', 'recall_best_solution', 'smart_recall',
|
|
50
64
|
'session_start', 'session_end', 'session_ping', 'session_handoff', 'auto_learn_session',
|
|
65
|
+
'brain_who_knows', 'brain_file_map', 'team_expertise_map',
|
|
51
66
|
]);
|
|
52
67
|
export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
53
68
|
switch (name) {
|
|
54
69
|
case 'learn_from_attempts': {
|
|
55
|
-
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;
|
|
56
71
|
const redis = await getConnection(instance_id);
|
|
57
72
|
const ts = new Date().toISOString();
|
|
58
73
|
// ── Structured template hints ──────────────────────────────────────────
|
|
@@ -170,6 +185,7 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
170
185
|
tags,
|
|
171
186
|
depends_on,
|
|
172
187
|
...(author ? { author } : {}),
|
|
188
|
+
visibility,
|
|
173
189
|
recall_count: recallCount,
|
|
174
190
|
ts,
|
|
175
191
|
verified_at: outcome === 'success' || outcome === 'partial' ? ts : undefined,
|
|
@@ -316,6 +332,22 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
316
332
|
}
|
|
317
333
|
}
|
|
318
334
|
catch { /* CKG updates are non-critical */ }
|
|
335
|
+
// ── Phase 3A: People + File nodes in knowledge graph ──────────────────────
|
|
336
|
+
// Builds the "who knows what" map automatically from author + file_paths.
|
|
337
|
+
// Person → authored → Concept edges power brain_who_knows queries.
|
|
338
|
+
if (author) {
|
|
339
|
+
try {
|
|
340
|
+
const domain = topic.split(':')[0] ?? 'unknown';
|
|
341
|
+
const personId = await ckgUpsertPersonNode(redis, author, domain);
|
|
342
|
+
const conceptId = ckgSlug(topic);
|
|
343
|
+
await ckgUpdateEdge(redis, personId, 'authored', conceptId, outcome === 'success', outcome === 'partial');
|
|
344
|
+
for (const fp of file_paths.slice(0, 8)) {
|
|
345
|
+
const fileId = await ckgUpsertFileNode(redis, fp);
|
|
346
|
+
await ckgUpdateEdge(redis, personId, 'touched', fileId, true);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
catch { /* non-critical */ }
|
|
350
|
+
}
|
|
319
351
|
// ── Ambient team propagation: if author is set + outcome is success,
|
|
320
352
|
// auto-store as a team lesson so knowledge is shared without a separate
|
|
321
353
|
// team_learn call. Tags gain 'team'; the lesson is attributed to the author.
|
|
@@ -503,9 +535,9 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
503
535
|
}
|
|
504
536
|
}
|
|
505
537
|
// ── Layer 2: Semantic search (parallel, optional) ────────────────────────
|
|
506
|
-
const inst = await apiFetch(`/api/v1/instances/${instance_id}`);
|
|
538
|
+
const inst = await apiFetch(`/api/v1/instances/${instance_id}`).catch(() => null);
|
|
507
539
|
const semHits = [];
|
|
508
|
-
if (inst
|
|
540
|
+
if (inst?.vector_token && hasEmbedProvider()) {
|
|
509
541
|
try {
|
|
510
542
|
const embedding = await computeEmbedding(query);
|
|
511
543
|
const vectorUrl = `https://api.cachly.dev/v1/sem/${inst.vector_token}`;
|
|
@@ -610,7 +642,15 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
610
642
|
}
|
|
611
643
|
}
|
|
612
644
|
catch { /* non-critical — keyword + semantic always available */ }
|
|
613
|
-
|
|
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);
|
|
614
654
|
// ── Build output ──────────────────────────────────────────────────────────
|
|
615
655
|
const lines = [`🧠 **Smart Recall** for: _"${query}"_\n`];
|
|
616
656
|
// "Brain saved you here" banner — surfaces value inline for proven recalled lessons
|
|
@@ -670,13 +710,19 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
670
710
|
else {
|
|
671
711
|
for (const r of hybridResults.slice(0, 8)) {
|
|
672
712
|
const label = r.key.replace('cachly:ctx:', '📝 ').replace('cachly:lesson:best:', '💡 ').replace('cachly:idx:', '📂 ');
|
|
713
|
+
let authorBadge = '';
|
|
714
|
+
if (r.key.startsWith('cachly:lesson:best:')) {
|
|
715
|
+
const ld = safeJsonParse(r.content, {});
|
|
716
|
+
if (ld.author)
|
|
717
|
+
authorBadge = ` · 👤 ${ld.author}`;
|
|
718
|
+
}
|
|
673
719
|
const scorePart = r.matchType === 'hybrid'
|
|
674
720
|
? `BM25: ${(r.bm25Score ?? 0).toFixed(2)}, sem: ${((r.semScore ?? 0) * 100).toFixed(0)}%, 🔀 hybrid`
|
|
675
721
|
: r.matchType === 'semantic'
|
|
676
722
|
? `sem: ${((r.semScore ?? 0) * 100).toFixed(0)}%, 🎯 semantic`
|
|
677
723
|
: `BM25: ${(r.bm25Score ?? 0).toFixed(2)}, matched: ${r.matchedWords?.join(', ')}`;
|
|
678
724
|
const preview = r.content.slice(0, 400).replace(/\n/g, ' ');
|
|
679
|
-
lines.push(`**${label}
|
|
725
|
+
lines.push(`**${label}**${authorBadge} _(${scorePart})_`);
|
|
680
726
|
lines.push(`> ${preview}${r.content.length > 400 ? '…' : ''}\n`);
|
|
681
727
|
}
|
|
682
728
|
}
|
|
@@ -1785,6 +1831,275 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
1785
1831
|
lines.push(`**Skipped** (better lesson already exists): ${skipped.map(t => `\`${t}\``).join(', ')}`);
|
|
1786
1832
|
return lines.join('\n');
|
|
1787
1833
|
}
|
|
1834
|
+
// ── brain_who_knows ───────────────────────────────────────────────────────
|
|
1835
|
+
// Phase 3A: Org-wide people intelligence. Returns ranked list of contributors
|
|
1836
|
+
// whose authored edges in the CKG match the query topic.
|
|
1837
|
+
case 'brain_who_knows': {
|
|
1838
|
+
const { instance_id, topic, limit = 10, } = args;
|
|
1839
|
+
const redis = await getConnection(instance_id);
|
|
1840
|
+
// Step 1: Find candidate concept node IDs matching the query
|
|
1841
|
+
const querySlug = ckgSlug(topic);
|
|
1842
|
+
const qTokens = tokenize(topic).filter((t) => t.length >= 3).slice(0, 5);
|
|
1843
|
+
const candidateConceptIds = new Set([querySlug]);
|
|
1844
|
+
for (const token of qTokens.slice(0, 3)) {
|
|
1845
|
+
const nodeStream = redis.scanStream({ match: `cachly:ckg:node:*${token}*`, count: 30 });
|
|
1846
|
+
await new Promise((res, rej) => {
|
|
1847
|
+
nodeStream.on('data', (batch) => {
|
|
1848
|
+
for (const k of batch.slice(0, 10)) {
|
|
1849
|
+
const nodeId = k.replace('cachly:ckg:node:', '');
|
|
1850
|
+
if (!nodeId.startsWith('person:') && !nodeId.startsWith('file:'))
|
|
1851
|
+
candidateConceptIds.add(nodeId);
|
|
1852
|
+
}
|
|
1853
|
+
});
|
|
1854
|
+
nodeStream.on('end', res);
|
|
1855
|
+
nodeStream.on('error', rej);
|
|
1856
|
+
});
|
|
1857
|
+
}
|
|
1858
|
+
const personScores = new Map();
|
|
1859
|
+
for (const conceptId of [...candidateConceptIds].slice(0, 25)) {
|
|
1860
|
+
const inboundEdgeKeys = await redis.smembers(`cachly:ckg:idx:to:${conceptId}`);
|
|
1861
|
+
for (const ek of inboundEdgeKeys) {
|
|
1862
|
+
const er = await redis.get(ek);
|
|
1863
|
+
if (!er)
|
|
1864
|
+
continue;
|
|
1865
|
+
const edge = safeJsonParse(er, null);
|
|
1866
|
+
if (!edge || edge.edgeType !== 'authored')
|
|
1867
|
+
continue;
|
|
1868
|
+
const personNodeRaw = await redis.get(`cachly:ckg:node:${edge.from}`);
|
|
1869
|
+
if (!personNodeRaw)
|
|
1870
|
+
continue;
|
|
1871
|
+
const personNode = safeJsonParse(personNodeRaw, null);
|
|
1872
|
+
if (!personNode || personNode.type !== 'person')
|
|
1873
|
+
continue;
|
|
1874
|
+
const existing = personScores.get(edge.from);
|
|
1875
|
+
if (existing) {
|
|
1876
|
+
existing.lessonCount++;
|
|
1877
|
+
existing.totalConfidence += edge.confidence;
|
|
1878
|
+
existing.domains.add(personNode.domain);
|
|
1879
|
+
if (edge.last_updated > existing.lastActive)
|
|
1880
|
+
existing.lastActive = edge.last_updated;
|
|
1881
|
+
}
|
|
1882
|
+
else {
|
|
1883
|
+
personScores.set(edge.from, {
|
|
1884
|
+
handle: personNode.handle,
|
|
1885
|
+
lessonCount: 1,
|
|
1886
|
+
totalConfidence: edge.confidence,
|
|
1887
|
+
domains: new Set([personNode.domain]),
|
|
1888
|
+
lastActive: personNode.last_active ?? edge.last_updated,
|
|
1889
|
+
});
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
if (personScores.size === 0) {
|
|
1894
|
+
return [
|
|
1895
|
+
`## 👥 Who Knows About \`${topic}\`?`,
|
|
1896
|
+
``,
|
|
1897
|
+
`No attributed lessons found yet.`,
|
|
1898
|
+
``,
|
|
1899
|
+
`Knowledge attribution builds automatically — add \`author="name"\` to \`learn_from_attempts\` calls:`,
|
|
1900
|
+
`\`learn_from_attempts(topic="${topic}", author="your-handle", ...)\``,
|
|
1901
|
+
].join('\n');
|
|
1902
|
+
}
|
|
1903
|
+
// Step 3: Sort by expertise score (lesson count × avg confidence)
|
|
1904
|
+
const ranked = [...personScores.entries()]
|
|
1905
|
+
.map(([, s]) => ({
|
|
1906
|
+
handle: s.handle,
|
|
1907
|
+
lessonCount: s.lessonCount,
|
|
1908
|
+
avgConfidence: s.totalConfidence / s.lessonCount,
|
|
1909
|
+
domains: [...s.domains].slice(0, 3).join(', '),
|
|
1910
|
+
lastActive: s.lastActive,
|
|
1911
|
+
}))
|
|
1912
|
+
.sort((a, b) => (b.lessonCount * b.avgConfidence) - (a.lessonCount * a.avgConfidence))
|
|
1913
|
+
.slice(0, limit);
|
|
1914
|
+
const lines = [
|
|
1915
|
+
`## 👥 Who Knows About \`${topic}\`?`,
|
|
1916
|
+
``,
|
|
1917
|
+
`**${ranked.length}** contributor${ranked.length !== 1 ? 's' : ''} with relevant lessons:`,
|
|
1918
|
+
``,
|
|
1919
|
+
];
|
|
1920
|
+
for (const [i, p] of ranked.entries()) {
|
|
1921
|
+
const medal = i === 0 ? '🥇' : i === 1 ? '🥈' : i === 2 ? '🥉' : `${i + 1}.`;
|
|
1922
|
+
const conf = (p.avgConfidence * 100).toFixed(0);
|
|
1923
|
+
const daysSince = Math.floor((Date.now() - new Date(p.lastActive).getTime()) / 86400000);
|
|
1924
|
+
const recency = daysSince < 1 ? 'today' : daysSince < 7 ? `${daysSince}d ago` : daysSince < 30 ? `${Math.floor(daysSince / 7)}w ago` : `${Math.floor(daysSince / 30)}mo ago`;
|
|
1925
|
+
lines.push(`${medal} **${p.handle}** — ${p.lessonCount} lesson${p.lessonCount !== 1 ? 's' : ''} · ${conf}% confidence · last active ${recency}`, ` _domains: ${p.domains}_`, ``);
|
|
1926
|
+
}
|
|
1927
|
+
lines.push(`---`, `_Attribution grows with every \`learn_from_attempts(author="...", ...)\` call._`);
|
|
1928
|
+
return lines.join('\n');
|
|
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
|
+
}
|
|
1788
2103
|
// ── sync_file_changes ─────────────────────────────────────────────────────
|
|
1789
2104
|
default:
|
|
1790
2105
|
return null;
|