@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.
@@ -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 } from '../ckg.js';
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.58';
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}** _(${scorePart})_`);
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.58';
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 · 95 MCP tools · GDPR · EU servers',
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 · 95 MCP tools\x1b[0m');
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: