@cachly-dev/mcp-server 0.10.57 → 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.
Files changed (54) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +1 -1
  3. package/dist/src/__tests__/auth.test.js.map +1 -1
  4. package/dist/src/__tests__/brain-flow.test.js +101 -1
  5. package/dist/src/__tests__/brain-flow.test.js.map +1 -1
  6. package/dist/src/__tests__/e2e/api-health.test.js.map +1 -1
  7. package/dist/src/ckg.d.ts +20 -0
  8. package/dist/src/ckg.js +29 -0
  9. package/dist/src/ckg.js.map +1 -1
  10. package/dist/src/handlers/advanced.d.ts +1 -1
  11. package/dist/src/handlers/advanced.js +1 -2
  12. package/dist/src/handlers/advanced.js.map +1 -1
  13. package/dist/src/handlers/brain.js +157 -3
  14. package/dist/src/handlers/brain.js.map +1 -1
  15. package/dist/src/handlers/cache.js.map +1 -1
  16. package/dist/src/handlers/context.js.map +1 -1
  17. package/dist/src/handlers/fedbrain.js.map +1 -1
  18. package/dist/src/handlers/roadmap.d.ts +1 -1
  19. package/dist/src/handlers/roadmap.js +1 -1
  20. package/dist/src/handlers/roadmap.js.map +1 -1
  21. package/dist/src/handlers/syndicate.js.map +1 -1
  22. package/dist/src/handlers/tco.js.map +1 -1
  23. package/dist/src/handlers/team.js +3 -2
  24. package/dist/src/handlers/team.js.map +1 -1
  25. package/dist/src/index.js +6 -6
  26. package/dist/src/index.js.map +1 -1
  27. package/dist/src/search.js +2 -2
  28. package/dist/src/search.js.map +1 -1
  29. package/dist/src/search.test.js +1 -1
  30. package/dist/src/search.test.js.map +1 -1
  31. package/dist/src/tools.d.ts +21 -0
  32. package/dist/src/tools.js +17 -0
  33. package/dist/src/tools.js.map +1 -1
  34. package/eslint.config.js +1 -1
  35. package/package.json +2 -2
  36. package/server.json +3 -3
  37. package/src/__tests__/auth.test.ts +1 -1
  38. package/src/__tests__/brain-flow.test.ts +115 -2
  39. package/src/__tests__/e2e/api-health.test.ts +1 -1
  40. package/src/ckg.ts +53 -3
  41. package/src/handlers/advanced.ts +2 -6
  42. package/src/handlers/brain.ts +183 -4
  43. package/src/handlers/cache.ts +1 -2
  44. package/src/handlers/context.ts +1 -2
  45. package/src/handlers/fedbrain.ts +0 -5
  46. package/src/handlers/roadmap.ts +1 -1
  47. package/src/handlers/syndicate.ts +7 -4
  48. package/src/handlers/tco.ts +0 -2
  49. package/src/handlers/team.ts +5 -5
  50. package/src/index.ts +9 -41
  51. package/src/search.test.ts +1 -1
  52. package/src/search.ts +2 -2
  53. package/src/tools.ts +18 -0
  54. package/vitest.config.ts +2 -0
@@ -2,17 +2,29 @@ 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.57';
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
+ ],
24
+ '0.10.58': [
25
+ `🧹 Zero lint warnings — all unused imports cleaned across every handler`,
26
+ `✅ Build: 0 errors, 0 warnings · 379 tests green`,
27
+ ],
16
28
  '0.10.57': [
17
29
  `🆕 **What's new in v${MCP_VERSION}:**`,
18
30
  ` 🔧 **Critical fix** — corrected the npm bin/main entry path (\`dist/src/index.js\`); 0.10.50–0.10.52 shipped a broken entry point`,
@@ -68,6 +80,7 @@ type ApiFetch = <T>(path: string, options?: RequestInit) => Promise<T>;
68
80
  export const BRAIN_TOOL_NAMES = new Set([
69
81
  'learn_from_attempts', 'recall_best_solution', 'smart_recall',
70
82
  'session_start', 'session_end', 'session_ping', 'session_handoff', 'auto_learn_session',
83
+ 'brain_who_knows',
71
84
  ]);
72
85
 
73
86
  export async function handleBrainTool(
@@ -379,6 +392,44 @@ export async function handleBrainTool(
379
392
  }
380
393
  } catch { /* CKG updates are non-critical */ }
381
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
+
411
+ // ── Ambient team propagation: if author is set + outcome is success,
412
+ // auto-store as a team lesson so knowledge is shared without a separate
413
+ // team_learn call. Tags gain 'team'; the lesson is attributed to the author.
414
+ // This makes team knowledge sharing the default, not the opt-in.
415
+ if (author && (outcome === 'success' || outcome === 'partial')) {
416
+ const teamLesson = {
417
+ topic, outcome, what_worked,
418
+ what_failed: what_failed ?? '',
419
+ severity,
420
+ author,
421
+ file_paths,
422
+ commands,
423
+ tags: [...new Set([...tags, 'team'])],
424
+ timestamp: new Date().toISOString(),
425
+ recall_count: 0,
426
+ version: 2,
427
+ auto_propagated: true,
428
+ };
429
+ const teamListKey = `cachly:lessons:${topic}`;
430
+ redis.rpush(teamListKey, JSON.stringify(teamLesson)).catch(() => {});
431
+ }
432
+
382
433
  const emoji = outcome === 'success' ? '✅' : outcome === 'partial' ? '⚠️' : '❌';
383
434
  const sevEmoji = severity === 'critical' ? '🔴' : severity === 'major' ? '🟡' : '🟢';
384
435
  const action = isUpdate ? 'updated' : 'stored';
@@ -396,6 +447,9 @@ export async function handleBrainTool(
396
447
  isUpdate
397
448
  ? `♻️ Updated (recall count: ${recallCount} · audit entries: ${auditTrail.length})`
398
449
  : `💡 Recall later with \`recall_best_solution(topic="${topic}")\``,
450
+ (author && (outcome === 'success' || outcome === 'partial'))
451
+ ? `👥 Auto-shared with team (by _${author}_) — visible in \`team_recall\``
452
+ : '',
399
453
  depends_on.length > 0
400
454
  ? `🔗 Depends on: ${depends_on.map(d => `\`${d}\``).join(', ')} → trace with \`trace_dependency\``
401
455
  : '',
@@ -745,13 +799,18 @@ export async function handleBrainTool(
745
799
  } else {
746
800
  for (const r of hybridResults.slice(0, 8)) {
747
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
+ }
748
807
  const scorePart = r.matchType === 'hybrid'
749
808
  ? `BM25: ${(r.bm25Score ?? 0).toFixed(2)}, sem: ${((r.semScore ?? 0) * 100).toFixed(0)}%, 🔀 hybrid`
750
809
  : r.matchType === 'semantic'
751
810
  ? `sem: ${((r.semScore ?? 0) * 100).toFixed(0)}%, 🎯 semantic`
752
811
  : `BM25: ${(r.bm25Score ?? 0).toFixed(2)}, matched: ${r.matchedWords?.join(', ')}`;
753
812
  const preview = r.content.slice(0, 400).replace(/\n/g, ' ');
754
- lines.push(`**${label}** _(${scorePart})_`);
813
+ lines.push(`**${label}**${authorBadge} _(${scorePart})_`);
755
814
  lines.push(`> ${preview}${r.content.length > 400 ? '…' : ''}\n`);
756
815
  }
757
816
  }
@@ -1944,6 +2003,126 @@ export async function handleBrainTool(
1944
2003
  return lines.join('\n');
1945
2004
  }
1946
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
+
1947
2126
  // ── sync_file_changes ─────────────────────────────────────────────────────
1948
2127
 
1949
2128
  default:
@@ -1,11 +1,10 @@
1
- import { createHash } from 'node:crypto';
2
1
  import { randomUUID } from 'node:crypto';
3
2
  import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
4
3
  import { readdir, stat, readFile } from 'node:fs/promises';
5
4
  import { join, relative, extname } from 'node:path';
6
5
  import type { Redis } from 'ioredis';
7
6
  import type { Instance } from './brain.js';
8
- import { computeEmbedding, hasEmbedProvider, embedProviderHint, EMBED_PROVIDER } from '../embeddings.js';
7
+ import { computeEmbedding, hasEmbedProvider, embedProviderHint } from '../embeddings.js';
9
8
  import { detectNamespace } from '../namespace.js';
10
9
  import { simpleHash } from '../confidence.js';
11
10
 
@@ -1,6 +1,5 @@
1
1
  import type { Redis } from 'ioredis';
2
- import { computeEmbedding, hasEmbedProvider } from '../embeddings.js';
3
- import { simpleHash } from '../confidence.js';
2
+ import { computeEmbedding } from '../embeddings.js';
4
3
  import type { Instance } from './brain.js';
5
4
 
6
5
  type GetConnection = (instanceId: string) => Promise<Redis>;
@@ -1,13 +1,8 @@
1
1
  import { createHmac } from 'node:crypto';
2
- import { execSync } from 'node:child_process';
3
2
  import type { Redis } from 'ioredis';
4
- import { calculateConfidence, confidenceBadge, CONFIDENCE_WARN_VALUE, CONFIDENCE_STALE_VALUE,
5
- CONFIDENCE_WARN_DAYS } from '../confidence.js';
6
3
  import { ckgSlug, extractProblemConcept, ckgUpsertNode, ckgUpdateEdge } from '../ckg.js';
7
4
  import type { CKGEdge, CKGNode } from '../ckg.js';
8
- import { keywordSearch, tokenize } from '../search.js';
9
5
  import { safeJsonParse } from '../utils.js';
10
- import { computeEmbedding, hasEmbedProvider } from '../embeddings.js';
11
6
 
12
7
  // Last brain_from_git category counts — set after each run so index.ts can include them in telemetry
13
8
  export let _lastBrainFromGitCounts: { fixes: number; features: number; refactors: number; total: number } | null = null;
@@ -12,7 +12,7 @@ export async function handleRoadmapTool(
12
12
  name: string,
13
13
  args: Record<string, unknown>,
14
14
  getConnection: GetConnection,
15
- apiFetch: ApiFetch,
15
+ _apiFetch: ApiFetch,
16
16
  ): Promise<string | null> {
17
17
  switch (name) {
18
18
  case 'roadmap_add': {
@@ -1,9 +1,9 @@
1
1
  import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
2
2
  import type { Redis } from 'ioredis';
3
3
  import { calculateConfidence } from '../confidence.js';
4
- import { ckgSlug, ckgUpdateEdge } from '../ckg.js';
4
+ import { ckgSlug } from '../ckg.js';
5
5
  import type { CKGEdge, CKGNode } from '../ckg.js';
6
- import { keywordSearch, tokenize } from '../search.js';
6
+ import { keywordSearch } from '../search.js';
7
7
  import { safeJsonParse } from '../utils.js';
8
8
 
9
9
  type GetConnection = (instanceId: string) => Promise<Redis>;
@@ -124,6 +124,9 @@ export async function handleSyndicateTool(
124
124
  id: string; topic: string; outcome: string;
125
125
  what_worked: string; confirm_count: number; created_at: string;
126
126
  }>;
127
+ top_contributors?: Array<{
128
+ trust_score: number; lessons_count: number; confirms_received: number;
129
+ }>;
127
130
  }>('/api/v1/syndication/stats');
128
131
 
129
132
  const confirmBar = (n: number) => {
@@ -163,9 +166,9 @@ export async function handleSyndicateTool(
163
166
  );
164
167
 
165
168
  // Top contributors (anonymous scores)
166
- if ((res as any).top_contributors?.length) {
169
+ if (res.top_contributors?.length) {
167
170
  lines.push(``, `### 🏅 Top Contributors (anonymous)`);
168
- for (const c of (res as any).top_contributors) {
171
+ for (const c of res.top_contributors) {
169
172
  lines.push(`- Trust **${c.trust_score}** · ${c.lessons_count} lesson${c.lessons_count === 1 ? '' : 's'} · ${c.confirms_received} confirms received`);
170
173
  }
171
174
  }
@@ -32,8 +32,6 @@ export const TCO_TOOL_NAMES = new Set([
32
32
  'tco_telegram_send',
33
33
  ]);
34
34
 
35
- type TcoFetch = (path: string, jwt: string, init?: RequestInit) => Promise<unknown>;
36
-
37
35
  async function tcoFetch(path: string, jwt: string, init: RequestInit = {}): Promise<unknown> {
38
36
  if (!TCO_URL) {
39
37
  throw new Error(
@@ -1,9 +1,8 @@
1
- import { execSync } from 'node:child_process';
2
1
  import { readFileSync } from 'node:fs';
3
2
  import { join } from 'node:path';
4
3
  import type { Redis } from 'ioredis';
5
- import { calculateConfidence, confidenceBadge, CONFIDENCE_STALE_VALUE, CONFIDENCE_WARN_VALUE,
6
- CONFIDENCE_WARN_DAYS, CONFIDENCE_STALE_DAYS, simpleHash } from '../confidence.js';
4
+ import { calculateConfidence, CONFIDENCE_STALE_VALUE, CONFIDENCE_WARN_VALUE,
5
+ CONFIDENCE_WARN_DAYS, CONFIDENCE_STALE_DAYS } from '../confidence.js';
7
6
  import type { Instance } from './brain.js';
8
7
  import { safeJsonParse } from '../utils.js';
9
8
 
@@ -147,7 +146,7 @@ export async function handleTeamTool(
147
146
 
148
147
  // Track distinct reviewers so endorsements can't be inflated by one person.
149
148
  const endorsersKey = `cachly:lesson:endorsers:${topic}`;
150
- const added = await redis.sadd(endorsersKey, reviewer);
149
+ const isNewEndorser = (await redis.sadd(endorsersKey, reviewer)) === 1;
151
150
  await redis.expire(endorsersKey, 365 * 86400);
152
151
  const endorsements = await redis.scard(endorsersKey);
153
152
 
@@ -174,7 +173,8 @@ export async function handleTeamTool(
174
173
  await redis.expire(logKey, 365 * 86400);
175
174
 
176
175
  const badge = effectiveLevel === 'senior' ? '🛡️ senior-reviewed' : '✔️ peer-reviewed';
177
- return `${badge} — \`${topic}\` confirmed by **${reviewer}** (${endorsements} distinct endorsement${endorsements === 1 ? '' : 's'}).\n📈 This lesson now ranks higher in \`smart_recall\` / \`team_recall\`.${note ? `\n📝 ${note.slice(0, 200)}` : ''}`;
176
+ const alreadyNote = isNewEndorser ? '' : ` (already endorsed by ${reviewer} — no change to rank)`;
177
+ return `${badge} — \`${topic}\` confirmed by **${reviewer}** (${endorsements} distinct endorsement${endorsements === 1 ? '' : 's'})${alreadyNote}.\n📈 This lesson now ranks higher in \`smart_recall\` / \`team_recall\`.${note ? `\n📝 ${note.slice(0, 200)}` : ''}`;
178
178
  }
179
179
 
180
180
  // ── team_recall ───────────────────────────────────────────────────────────
package/src/index.ts CHANGED
@@ -1,10 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import { jwtExpiryMs, checkJwt, handleApiError } from './auth.js';
3
- import { handleTcoTool, TCO_TOOL_NAMES } from './handlers/tco.js';
3
+ import { handleTcoTool } from './handlers/tco.js';
4
4
  import { notify } from './notifier.js';
5
5
  import { readFile } from 'node:fs/promises';
6
- import { execSync } from 'node:child_process';
7
- import { join } from 'node:path';
8
6
  import { fileURLToPath } from 'node:url';
9
7
 
10
8
  // True only when this file is the entry point (not imported by tests or other modules).
@@ -74,8 +72,8 @@ import { Redis } from 'ioredis';
74
72
 
75
73
  const API_URL = process.env.CACHLY_API_URL ?? 'https://api.cachly.dev';
76
74
  let JWT = process.env.CACHLY_JWT ?? '';
77
- const EMBED_MODEL = process.env.CACHLY_EMBED_MODEL ?? '';
78
- const CURRENT_VERSION = '0.10.49';
75
+ const _EMBED_MODEL = process.env.CACHLY_EMBED_MODEL ?? '';
76
+ const CURRENT_VERSION = '0.10.59';
79
77
 
80
78
  // Max time to wait for a freshly-provisioned instance to become "running" before
81
79
  // giving up. Free-tier provisioning in high-latency regions can take 45–90s, so the
@@ -304,43 +302,18 @@ async function pollDeviceFlow(flow: DeviceFlowState): Promise<'pending' | 'expir
304
302
  return 'pending';
305
303
  } catch { return 'pending'; }
306
304
  }
307
- // ── Confidence utils (imported from confidence.ts) ──────────────────────────
308
- import { calculateConfidence, confidenceBadge, STRUCTURED_TEMPLATES,
309
- CONFIDENCE_WARN_VALUE, CONFIDENCE_STALE_VALUE, CONFIDENCE_WARN_DAYS,
310
- CONFIDENCE_STALE_DAYS,
311
- simpleHash } from './confidence.js';
312
-
313
-
314
305
  // ── Embeddings (imported from embeddings.ts) ────────────────────────────────
315
- import { embedConfig, setEmbedJwt, EMBED_PROVIDER, computeEmbedding,
316
- hasEmbedProvider, embedProviderHint } from './embeddings.js';
306
+ import { setEmbedJwt } from './embeddings.js';
317
307
 
318
308
  // ── Search Engine (imported from search.ts) ─────────────────────────────────
319
309
  import { tokenize, splitMultiQuery, levenshtein, recencyBoost, extractTimestamp, STOPWORDS,
320
310
  katakanaToRomaji, arabicLightStem, expandCrossLingual, CROSS_LINGUAL_MAP,
321
- keywordSearch, ZERO_RESULTS_LOG, zeroResultsTotal, indexVocab as _indexVocab } from './search.js';
322
- import type { KeywordMatch } from './search.js';
311
+ ZERO_RESULTS_LOG, zeroResultsTotal } from './search.js';
323
312
 
324
313
  // ── Exported for testing (re-export from search.ts) ─────────────────────────
325
314
  export { tokenize, splitMultiQuery, levenshtein, recencyBoost, extractTimestamp, STOPWORDS,
326
315
  katakanaToRomaji, arabicLightStem, expandCrossLingual, CROSS_LINGUAL_MAP };
327
316
 
328
- // ── Types ─────────────────────────────────────────────────────────────────────
329
- // Instance is imported from handlers/brain.ts
330
-
331
- interface CreateResponse {
332
- instance_id: string;
333
- checkout_url?: string;
334
- status: string;
335
- }
336
-
337
- interface SemanticSearchResponse {
338
- found: boolean;
339
- id?: string;
340
- similarity?: number;
341
- prompt?: string;
342
- }
343
-
344
317
  // ── Connection pool ───────────────────────────────────────────────────────────
345
318
 
346
319
  /** Reuse Redis connections across tool calls (keyed by instance_id). */
@@ -487,11 +460,6 @@ async function apiFetch<T>(path: string, options: RequestInit = {}): Promise<T>
487
460
  return res.json() as Promise<T>;
488
461
  }
489
462
 
490
- import { detectNamespace } from './namespace.js';
491
-
492
- // ── CKG (imported from ckg.ts) ─────────────────────────────────────────────
493
- import type { CKGEdge, CKGNode } from './ckg.js';
494
- import { ckgSlug, extractProblemConcept, ckgUpsertNode, ckgUpdateEdge } from './ckg.js';
495
463
  import { handleBrainTool } from './handlers/brain.js';
496
464
  import { handleContextTool } from './handlers/context.js';
497
465
  import { handleInstanceTool } from './handlers/instances.js';
@@ -604,7 +572,7 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
604
572
  '',
605
573
  'After sign-in: call **any tool again** — your Brain activates instantly.',
606
574
  '',
607
- '✨ Free forever · No credit card · 95 MCP tools · GDPR · EU servers',
575
+ '✨ Free forever · No credit card · 96 MCP tools · GDPR · EU servers',
608
576
  ].join('\n');
609
577
  }
610
578
 
@@ -1848,7 +1816,7 @@ if (!process.argv[2] && process.stdout.isTTY) {
1848
1816
  console.log(' \x1b[36m npx @cachly-dev/mcp-server@latest upgrade\x1b[0m ← Check for updates');
1849
1817
  console.log('');
1850
1818
  console.log(' \x1b[90mWorks with: Claude Code · Cursor · Windsurf · GitHub Copilot · Cline · Zed\x1b[0m');
1851
- 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');
1852
1820
  console.log('');
1853
1821
  process.exit(0);
1854
1822
  }
@@ -2021,7 +1989,7 @@ if (process.argv[2] === 'status') {
2021
1989
  if (process.argv[2] === 'health') {
2022
1990
  const { existsSync } = await import('node:fs');
2023
1991
  const { readFile } = await import('node:fs/promises');
2024
- const { resolve, join: pJoin } = await import('node:path');
1992
+ const { resolve } = await import('node:path');
2025
1993
 
2026
1994
  const cwd = process.cwd();
2027
1995
  let passed = 0;
@@ -2325,7 +2293,7 @@ if (process.argv[2] === 'setup') {
2325
2293
  if (!keyBody.key) throw new Error('no key in response');
2326
2294
  token = keyBody.key; // swap JWT → cky_live_...
2327
2295
  console.log(' ✓\n');
2328
- } catch (e) {
2296
+ } catch {
2329
2297
  console.log(' (skipped)\n');
2330
2298
  // Non-fatal: fall back to using the Keycloak JWT directly.
2331
2299
  // It will expire but setup still works for now.
@@ -833,7 +833,7 @@ describe('CROSS_LINGUAL_MAP — coverage', () => {
833
833
  it('bi-directional entries exist (reverse lookups)', () => {
834
834
  // For each forward entry, the reverse should also exist
835
835
  let reverseCount = 0;
836
- for (const [key, values] of CROSS_LINGUAL_MAP.entries()) {
836
+ for (const [, values] of CROSS_LINGUAL_MAP.entries()) {
837
837
  for (const v of values) {
838
838
  if (CROSS_LINGUAL_MAP.has(v)) {
839
839
  reverseCount++;
package/src/search.ts CHANGED
@@ -193,7 +193,7 @@ const SEGMENT_RE = /([\u3040-\u30ff\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff\uac00
193
193
  const RTL_RE = /[\u0590-\u05ff\u0600-\u06ff\u0750-\u077f\u08a0-\u08ff\ufb1d-\ufb4f\ufb50-\ufdff\ufe70-\ufeff]/;
194
194
  // Distinguish Arabic from Hebrew within RTL segments
195
195
  const HEBREW_CHAR_RE = /[\u0590-\u05ff\ufb1d-\ufb4f]/;
196
- const ARABIC_CHAR_RE = /[\u0600-\u06ff\u0750-\u077f\u08a0-\u08ff\ufb50-\ufdff\ufe70-\ufeff]/;
196
+ const _ARABIC_CHAR_RE = /[\u0600-\u06ff\u0750-\u077f\u08a0-\u08ff\ufb50-\ufdff\ufe70-\ufeff]/;
197
197
  // Persian/Farsi: exclusive chars not in Arabic (U+067E=پ, U+0686=چ, U+0698=ژ, U+06AF=گ, U+06CC=ی)
198
198
  const FARSI_CHAR_RE = /[\u067e\u0686\u0698\u06af\u06cc]/;
199
199
  // Devanagari (Hindi, Sanskrit, Marathi …) — U+0900–U+097F + extended
@@ -206,7 +206,7 @@ const TURKISH_CHAR_RE = /[\u011f\u015f\u0131\u0130\u00e7]/; // ğ ş ı İ ç
206
206
  const BENGALI_RE = /[\u0980-\u09ff]/;
207
207
  // Vietnamese uses Latin + Latin Extended Additional (U+1EA0–U+1EF9) for tone marks
208
208
  // e.g. ắ ặ ầ ổ ợ ụ ừ — detected by chars exclusive to Vietnamese diacritics
209
- const VIETNAMESE_CHAR_RE = /[\u1ea0-\u1ef9]/;
209
+ const _VIETNAMESE_CHAR_RE = /[\u1ea0-\u1ef9]/;
210
210
 
211
211
  // ── Katakana → Romaji conversion table (Hepburn system) ──────────────────────
212
212
  // Digraphs must be listed before single chars so they match first.
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:
package/vitest.config.ts CHANGED
@@ -7,6 +7,8 @@ export default defineConfig({
7
7
  exclude: [
8
8
  '**/__tests__/e2e/**',
9
9
  '**/node_modules/**',
10
+ // Compiled dist/ files are identical to src/ — run tests only from source
11
+ '**/dist/**',
10
12
  ],
11
13
  },
12
14
  });