@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.
- package/CHANGELOG.md +28 -0
- package/README.md +1 -1
- package/dist/src/__tests__/auth.test.js.map +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/__tests__/e2e/api-health.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/advanced.d.ts +1 -1
- package/dist/src/handlers/advanced.js +1 -2
- package/dist/src/handlers/advanced.js.map +1 -1
- package/dist/src/handlers/brain.js +157 -3
- package/dist/src/handlers/brain.js.map +1 -1
- package/dist/src/handlers/cache.js.map +1 -1
- package/dist/src/handlers/context.js.map +1 -1
- package/dist/src/handlers/fedbrain.js.map +1 -1
- package/dist/src/handlers/roadmap.d.ts +1 -1
- package/dist/src/handlers/roadmap.js +1 -1
- package/dist/src/handlers/roadmap.js.map +1 -1
- package/dist/src/handlers/syndicate.js.map +1 -1
- package/dist/src/handlers/tco.js.map +1 -1
- package/dist/src/handlers/team.js +3 -2
- package/dist/src/handlers/team.js.map +1 -1
- package/dist/src/index.js +6 -6
- package/dist/src/index.js.map +1 -1
- package/dist/src/search.js +2 -2
- package/dist/src/search.js.map +1 -1
- package/dist/src/search.test.js +1 -1
- package/dist/src/search.test.js.map +1 -1
- package/dist/src/tools.d.ts +21 -0
- package/dist/src/tools.js +17 -0
- package/dist/src/tools.js.map +1 -1
- package/eslint.config.js +1 -1
- package/package.json +2 -2
- package/server.json +3 -3
- package/src/__tests__/auth.test.ts +1 -1
- package/src/__tests__/brain-flow.test.ts +115 -2
- package/src/__tests__/e2e/api-health.test.ts +1 -1
- package/src/ckg.ts +53 -3
- package/src/handlers/advanced.ts +2 -6
- package/src/handlers/brain.ts +183 -4
- package/src/handlers/cache.ts +1 -2
- package/src/handlers/context.ts +1 -2
- package/src/handlers/fedbrain.ts +0 -5
- package/src/handlers/roadmap.ts +1 -1
- package/src/handlers/syndicate.ts +7 -4
- package/src/handlers/tco.ts +0 -2
- package/src/handlers/team.ts +5 -5
- package/src/index.ts +9 -41
- package/src/search.test.ts +1 -1
- package/src/search.ts +2 -2
- package/src/tools.ts +18 -0
- package/vitest.config.ts +2 -0
package/src/handlers/brain.ts
CHANGED
|
@@ -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
|
|
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
|
+
],
|
|
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}
|
|
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:
|
package/src/handlers/cache.ts
CHANGED
|
@@ -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
|
|
7
|
+
import { computeEmbedding, hasEmbedProvider, embedProviderHint } from '../embeddings.js';
|
|
9
8
|
import { detectNamespace } from '../namespace.js';
|
|
10
9
|
import { simpleHash } from '../confidence.js';
|
|
11
10
|
|
package/src/handlers/context.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { Redis } from 'ioredis';
|
|
2
|
-
import { computeEmbedding
|
|
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>;
|
package/src/handlers/fedbrain.ts
CHANGED
|
@@ -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;
|
package/src/handlers/roadmap.ts
CHANGED
|
@@ -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
|
|
4
|
+
import { ckgSlug } from '../ckg.js';
|
|
5
5
|
import type { CKGEdge, CKGNode } from '../ckg.js';
|
|
6
|
-
import { keywordSearch
|
|
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 (
|
|
169
|
+
if (res.top_contributors?.length) {
|
|
167
170
|
lines.push(``, `### 🏅 Top Contributors (anonymous)`);
|
|
168
|
-
for (const c of
|
|
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
|
}
|
package/src/handlers/tco.ts
CHANGED
|
@@ -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(
|
package/src/handlers/team.ts
CHANGED
|
@@ -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,
|
|
6
|
-
CONFIDENCE_WARN_DAYS, CONFIDENCE_STALE_DAYS
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
78
|
-
const CURRENT_VERSION = '0.10.
|
|
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 {
|
|
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
|
-
|
|
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 ·
|
|
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 ·
|
|
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
|
|
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
|
|
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.
|
package/src/search.test.ts
CHANGED
|
@@ -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 [
|
|
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
|
|
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
|
|
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:
|