@cachly-dev/mcp-server 0.10.81 → 0.10.83
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/dist/src/handlers/brain.js +17 -1
- package/dist/src/handlers/brain.js.map +1 -1
- package/dist/src/handlers/viz.d.ts +6 -0
- package/dist/src/handlers/viz.js +148 -0
- package/dist/src/handlers/viz.js.map +1 -0
- package/dist/src/index.js +39 -10
- package/dist/src/index.js.map +1 -1
- package/dist/src/telemetry-types.d.ts +87 -0
- package/dist/src/telemetry-types.js +13 -0
- package/dist/src/telemetry-types.js.map +1 -0
- package/dist/src/tools.d.ts +30 -0
- package/dist/src/tools.js +20 -0
- package/dist/src/tools.js.map +1 -1
- package/package.json +1 -1
- package/server.json +3 -3
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Redis } from 'ioredis';
|
|
2
|
+
type GetConnection = (instanceId: string) => Promise<Redis>;
|
|
3
|
+
type ApiFetch = <T>(path: string, options?: RequestInit) => Promise<T>;
|
|
4
|
+
export declare const VIZ_TOOL_NAMES: Set<string>;
|
|
5
|
+
export declare function handleVizTool(name: string, args: Record<string, unknown>, getConnection: GetConnection, _apiFetch: ApiFetch): Promise<string | null>;
|
|
6
|
+
export {};
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { safeJsonParse } from '../utils.js';
|
|
2
|
+
export const VIZ_TOOL_NAMES = new Set(['brain_graph']);
|
|
3
|
+
// Stable color groups so the 3D frontend can theme node kinds consistently.
|
|
4
|
+
const NODE_GROUP = {
|
|
5
|
+
concept: 1, person: 2, file: 3, service: 4,
|
|
6
|
+
};
|
|
7
|
+
function nodeKind(id) {
|
|
8
|
+
if (id.startsWith('person:'))
|
|
9
|
+
return 'person';
|
|
10
|
+
if (id.startsWith('file:'))
|
|
11
|
+
return 'file';
|
|
12
|
+
if (id.startsWith('service:'))
|
|
13
|
+
return 'service';
|
|
14
|
+
return 'concept';
|
|
15
|
+
}
|
|
16
|
+
function nodeLabel(n, kind) {
|
|
17
|
+
if (kind === 'person')
|
|
18
|
+
return n.handle ?? n.id.replace(/^person:/, '');
|
|
19
|
+
if (kind === 'file')
|
|
20
|
+
return (n.path ?? n.id.replace(/^file:/, '')).split('/').slice(-1)[0] ?? n.id;
|
|
21
|
+
if (kind === 'service')
|
|
22
|
+
return n.name ?? n.id.replace(/^service:/, '');
|
|
23
|
+
return n.id;
|
|
24
|
+
}
|
|
25
|
+
async function scanAll(redis, match, limit) {
|
|
26
|
+
const keys = [];
|
|
27
|
+
const stream = redis.scanStream({ match, count: 200 });
|
|
28
|
+
await new Promise((resolve, reject) => {
|
|
29
|
+
stream.on('data', (batch) => {
|
|
30
|
+
for (const k of batch) {
|
|
31
|
+
if (keys.length < limit)
|
|
32
|
+
keys.push(k);
|
|
33
|
+
}
|
|
34
|
+
if (keys.length >= limit)
|
|
35
|
+
stream.destroy();
|
|
36
|
+
});
|
|
37
|
+
stream.on('end', resolve);
|
|
38
|
+
stream.on('close', resolve);
|
|
39
|
+
stream.on('error', reject);
|
|
40
|
+
});
|
|
41
|
+
return keys;
|
|
42
|
+
}
|
|
43
|
+
export async function handleVizTool(name, args, getConnection, _apiFetch) {
|
|
44
|
+
if (name !== 'brain_graph')
|
|
45
|
+
return null;
|
|
46
|
+
const { instance_id, max_nodes = 400, domain = '', min_confidence = 0, format = 'json', } = args;
|
|
47
|
+
const redis = await getConnection(instance_id);
|
|
48
|
+
const nodeCap = Math.max(1, Math.min(2000, Number(max_nodes) || 400));
|
|
49
|
+
const minConf = Math.max(0, Math.min(1, Number(min_confidence) || 0));
|
|
50
|
+
const domainFilter = String(domain || '').toLowerCase().trim();
|
|
51
|
+
// ── Nodes ────────────────────────────────────────────────────────────────
|
|
52
|
+
const nodeKeys = await scanAll(redis, 'cachly:ckg:node:*', nodeCap);
|
|
53
|
+
const nodes = [];
|
|
54
|
+
const present = new Set();
|
|
55
|
+
for (const key of nodeKeys) {
|
|
56
|
+
const raw = await redis.get(key);
|
|
57
|
+
const stored = safeJsonParse(raw, null);
|
|
58
|
+
if (!stored || !stored.id)
|
|
59
|
+
continue;
|
|
60
|
+
const kind = nodeKind(stored.id);
|
|
61
|
+
const ndomain = String(stored.domain ?? '').toLowerCase();
|
|
62
|
+
if (domainFilter && !ndomain.includes(domainFilter) && !stored.id.toLowerCase().includes(domainFilter))
|
|
63
|
+
continue;
|
|
64
|
+
const count = Number(stored.count ?? 1) || 1;
|
|
65
|
+
nodes.push({
|
|
66
|
+
id: stored.id,
|
|
67
|
+
name: nodeLabel(stored, kind),
|
|
68
|
+
type: kind,
|
|
69
|
+
group: NODE_GROUP[kind] ?? 0,
|
|
70
|
+
domain: stored.domain ?? '',
|
|
71
|
+
val: Math.max(1, Math.round(Math.sqrt(count) * 3)),
|
|
72
|
+
count,
|
|
73
|
+
});
|
|
74
|
+
present.add(stored.id);
|
|
75
|
+
}
|
|
76
|
+
// ── Edges ──────────────────────────────────────────────────────────────────
|
|
77
|
+
// Edge keys: cachly:ckg:edge:{from}:{edgeType}:{to}. We read the stored CKGEdge
|
|
78
|
+
// (not the key) so from/to/confidence come straight from the value — robust to
|
|
79
|
+
// ids that themselves contain ':' (e.g. "docker:layer-cache").
|
|
80
|
+
const edgeKeys = await scanAll(redis, 'cachly:ckg:edge:*', nodeCap * 8);
|
|
81
|
+
const links = [];
|
|
82
|
+
let edgesSkippedConf = 0;
|
|
83
|
+
let edgesSkippedDangling = 0;
|
|
84
|
+
for (const key of edgeKeys) {
|
|
85
|
+
const raw = await redis.get(key);
|
|
86
|
+
const edge = safeJsonParse(raw, null);
|
|
87
|
+
if (!edge || !edge.from || !edge.to)
|
|
88
|
+
continue;
|
|
89
|
+
if ((edge.confidence ?? 0) < minConf) {
|
|
90
|
+
edgesSkippedConf++;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
// Drop dangling edges (endpoint filtered out) so the 3D graph stays consistent.
|
|
94
|
+
if (!present.has(edge.from) || !present.has(edge.to)) {
|
|
95
|
+
edgesSkippedDangling++;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
links.push({
|
|
99
|
+
source: edge.from,
|
|
100
|
+
target: edge.to,
|
|
101
|
+
type: edge.edgeType ?? 'related',
|
|
102
|
+
value: Number((edge.confidence ?? 0).toFixed(3)),
|
|
103
|
+
trials: edge.trials ?? 0,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
const byType = {};
|
|
107
|
+
for (const n of nodes)
|
|
108
|
+
byType[n.type] = (byType[n.type] ?? 0) + 1;
|
|
109
|
+
const graph = {
|
|
110
|
+
schema: 'cachly.brain_graph/v1',
|
|
111
|
+
nodes,
|
|
112
|
+
links,
|
|
113
|
+
meta: {
|
|
114
|
+
instance_id,
|
|
115
|
+
generated_at: new Date().toISOString(),
|
|
116
|
+
node_count: nodes.length,
|
|
117
|
+
link_count: links.length,
|
|
118
|
+
node_types: byType,
|
|
119
|
+
truncated: nodeKeys.length >= nodeCap,
|
|
120
|
+
filters: { domain: domainFilter || null, min_confidence: minConf, max_nodes: nodeCap },
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
if (format === 'summary') {
|
|
124
|
+
const lines = [
|
|
125
|
+
`## 🧠 Brain Graph — 3D Viz Export`,
|
|
126
|
+
'',
|
|
127
|
+
`**${nodes.length}** nodes · **${links.length}** links`,
|
|
128
|
+
...Object.entries(byType).map(([t, c]) => ` • ${t}: ${c}`),
|
|
129
|
+
edgesSkippedConf ? ` • ${edgesSkippedConf} edges below confidence ${minConf}` : '',
|
|
130
|
+
edgesSkippedDangling ? ` • ${edgesSkippedDangling} dangling edges dropped` : '',
|
|
131
|
+
'',
|
|
132
|
+
graph.meta.truncated ? `⚠️ Truncated at ${nodeCap} nodes — raise \`max_nodes\` for the full graph.` : '',
|
|
133
|
+
'',
|
|
134
|
+
`Call \`brain_graph(instance_id="${instance_id}", format="json")\` for the full payload the 3D frontend renders.`,
|
|
135
|
+
].filter(Boolean);
|
|
136
|
+
return lines.join('\n');
|
|
137
|
+
}
|
|
138
|
+
// Default: emit the renderable payload in a fenced block so the frontend /
|
|
139
|
+
// API can lift it verbatim into react-force-graph-3d.
|
|
140
|
+
return [
|
|
141
|
+
`🧠 **Brain Graph** — ${nodes.length} nodes, ${links.length} links (schema \`cachly.brain_graph/v1\`)`,
|
|
142
|
+
'',
|
|
143
|
+
'```json',
|
|
144
|
+
JSON.stringify(graph, null, 2),
|
|
145
|
+
'```',
|
|
146
|
+
].join('\n');
|
|
147
|
+
}
|
|
148
|
+
//# sourceMappingURL=viz.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"viz.js","sourceRoot":"","sources":["../../../src/handlers/viz.ts"],"names":[],"mappings":"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"}
|
package/dist/src/index.js
CHANGED
|
@@ -65,7 +65,7 @@ import { Redis } from 'ioredis';
|
|
|
65
65
|
let API_URL = process.env.CACHLY_API_URL ?? 'https://api.cachly.dev';
|
|
66
66
|
let JWT = process.env.CACHLY_JWT ?? '';
|
|
67
67
|
const _EMBED_MODEL = process.env.CACHLY_EMBED_MODEL ?? '';
|
|
68
|
-
const CURRENT_VERSION = '0.10.
|
|
68
|
+
const CURRENT_VERSION = '0.10.83';
|
|
69
69
|
// Max time to wait for a freshly-provisioned instance to become "running" before
|
|
70
70
|
// giving up. Free-tier provisioning in high-latency regions can take 45–90s, so the
|
|
71
71
|
// old hard-coded 25s caused spurious "instance_not_reachable" on first run.
|
|
@@ -610,6 +610,7 @@ import { handleAdvancedTool } from './handlers/advanced.js';
|
|
|
610
610
|
import { handleSyndicateTool } from './handlers/syndicate.js';
|
|
611
611
|
import { handleFedbrainTool, _lastBrainFromGitCounts } from './handlers/fedbrain.js';
|
|
612
612
|
import { handleShareTool } from './handlers/share.js';
|
|
613
|
+
import { handleVizTool } from './handlers/viz.js';
|
|
613
614
|
// ── Tools (imported from tools.ts) ─────────────────────────────────────────
|
|
614
615
|
import { TOOLS } from './tools.js';
|
|
615
616
|
// ── Handlers ──────────────────────────────────────────────────────────────────
|
|
@@ -624,15 +625,39 @@ function detectEditor() {
|
|
|
624
625
|
: process.env.CLAUDE_CODE_ENTRYPOINT ? 'claude'
|
|
625
626
|
: 'unknown';
|
|
626
627
|
}
|
|
627
|
-
|
|
628
|
+
/** Derive an anonymous, non-reversible fingerprint from the JWT sub claim. */
|
|
629
|
+
function _jwtUserFingerprint(jwt) {
|
|
630
|
+
try {
|
|
631
|
+
const parts = jwt.split('.');
|
|
632
|
+
if (parts.length !== 3)
|
|
633
|
+
return undefined;
|
|
634
|
+
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf-8'));
|
|
635
|
+
if (!payload.sub)
|
|
636
|
+
return undefined;
|
|
637
|
+
// First 16 hex chars of a deterministic hash — no PII, not reversible.
|
|
638
|
+
let h = 0x811c9dc5;
|
|
639
|
+
for (let i = 0; i < payload.sub.length; i++) {
|
|
640
|
+
h ^= payload.sub.charCodeAt(i);
|
|
641
|
+
h = (h * 0x01000193) >>> 0;
|
|
642
|
+
}
|
|
643
|
+
return h.toString(16).padStart(8, '0') + payload.sub.length.toString(16).padStart(4, '0');
|
|
644
|
+
}
|
|
645
|
+
catch {
|
|
646
|
+
return undefined;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
function sendFunnelEvent(event, extra, metrics) {
|
|
628
650
|
if (process.env.CACHLY_NO_TELEMETRY === '1')
|
|
629
651
|
return;
|
|
652
|
+
const fingerprint = JWT ? _jwtUserFingerprint(JWT) : undefined;
|
|
630
653
|
void fetch(`${API_URL}/api/v1/telemetry/mcp`, {
|
|
631
654
|
method: 'POST',
|
|
632
655
|
headers: { 'Content-Type': 'application/json' },
|
|
633
656
|
body: JSON.stringify({
|
|
634
657
|
event, version: CURRENT_VERSION, editor: detectEditor(),
|
|
635
658
|
...(JWT ? { jwt: JWT } : {}),
|
|
659
|
+
...(fingerprint ? { user_fingerprint: fingerprint } : {}),
|
|
660
|
+
...(metrics ? { metrics } : {}),
|
|
636
661
|
...extra,
|
|
637
662
|
}),
|
|
638
663
|
signal: AbortSignal.timeout(3000),
|
|
@@ -749,7 +774,7 @@ async function handleTool(name, args) {
|
|
|
749
774
|
sendFunnelEvent('learn_from_attempts', telemetryExtra);
|
|
750
775
|
}
|
|
751
776
|
else if (name === 'session_start') {
|
|
752
|
-
sendFunnelEvent('session_start', telemetryExtra);
|
|
777
|
+
sendFunnelEvent('session_start', telemetryExtra, { born_at: undefined, recalls_total: undefined, starter_seeded: false });
|
|
753
778
|
const resultText = typeof brainResult === 'object' && brainResult !== null && 'content' in brainResult
|
|
754
779
|
? JSON.stringify(brainResult)
|
|
755
780
|
: String(brainResult ?? '');
|
|
@@ -779,7 +804,7 @@ async function handleTool(name, args) {
|
|
|
779
804
|
try {
|
|
780
805
|
const seed = await handleShareTool('brain_seed_starter', { instance_id: instanceId }, getConnection, apiFetch);
|
|
781
806
|
if (seed) {
|
|
782
|
-
sendFunnelEvent('brain_seed_starter', { ...telemetryExtra, auto: true });
|
|
807
|
+
sendFunnelEvent('brain_seed_starter', { ...telemetryExtra, auto: true }, { seeded_count: 16, auto: true });
|
|
783
808
|
starterText = '\n---\n' + String(seed);
|
|
784
809
|
}
|
|
785
810
|
}
|
|
@@ -800,14 +825,15 @@ async function handleTool(name, args) {
|
|
|
800
825
|
sendFunnelEvent('session_end', telemetryExtra);
|
|
801
826
|
}
|
|
802
827
|
else if (name === 'smart_recall') {
|
|
803
|
-
sendFunnelEvent('smart_recall', telemetryExtra);
|
|
804
|
-
// smart_recall is the primary recall tool in CLAUDE.md — it retrieves brain lessons
|
|
805
|
-
// just like recall_best_solution. Count it toward BrainRecallCount so the dashboard
|
|
806
|
-
// nudge clears and the first-recall email fires for users following CLAUDE.md.
|
|
807
828
|
const srText = typeof brainResult === 'object' && brainResult !== null && 'content' in brainResult
|
|
808
829
|
? JSON.stringify(brainResult)
|
|
809
830
|
: String(brainResult ?? '');
|
|
810
|
-
|
|
831
|
+
const srHit = srText.length > 50 && !srText.includes('No lessons found') && !srText.includes('no lessons') && !srText.includes('No matches found');
|
|
832
|
+
sendFunnelEvent('smart_recall', telemetryExtra, { hit: srHit, topic: String(args.query ?? '').slice(0, 80) });
|
|
833
|
+
// smart_recall is the primary recall tool in CLAUDE.md — it retrieves brain lessons
|
|
834
|
+
// just like recall_best_solution. Count it toward BrainRecallCount so the dashboard
|
|
835
|
+
// nudge clears and the first-recall email fires for users following CLAUDE.md.
|
|
836
|
+
if (srHit) {
|
|
811
837
|
sendFunnelEvent('recall_best_solution', telemetryExtra);
|
|
812
838
|
}
|
|
813
839
|
// Self-healing: if the credential is degraded, the briefing may be thin or
|
|
@@ -857,6 +883,9 @@ async function handleTool(name, args) {
|
|
|
857
883
|
const shareResult = await handleShareTool(name, args, getConnection, apiFetch);
|
|
858
884
|
if (shareResult !== null)
|
|
859
885
|
return shareResult;
|
|
886
|
+
const vizResult = await handleVizTool(name, args, getConnection, apiFetch);
|
|
887
|
+
if (vizResult !== null)
|
|
888
|
+
return vizResult;
|
|
860
889
|
const fedbrainResult = await handleFedbrainTool(name, args, getConnection, apiFetch);
|
|
861
890
|
if (fedbrainResult !== null) {
|
|
862
891
|
// brain_from_git and brain_predict_failures are in fedbrain handler.
|
|
@@ -864,7 +893,7 @@ async function handleTool(name, args) {
|
|
|
864
893
|
const telemetryExtra = JWT ? { api_key: JWT, instance_id: instanceId } : { instance_id: instanceId };
|
|
865
894
|
if (name === 'brain_from_git') {
|
|
866
895
|
const gitCounts = _lastBrainFromGitCounts;
|
|
867
|
-
sendFunnelEvent('brain_from_git', { ...telemetryExtra, ...(gitCounts ?? {}) });
|
|
896
|
+
sendFunnelEvent('brain_from_git', { ...telemetryExtra, ...(gitCounts ?? {}) }, gitCounts ? { fixes: gitCounts.fixes, features: gitCounts.features, refactors: gitCounts.refactors, total: gitCounts.total } : undefined);
|
|
868
897
|
}
|
|
869
898
|
else if (name === 'brain_predict_failures') {
|
|
870
899
|
sendFunnelEvent('brain_predict_failures', telemetryExtra);
|