@cachly-dev/mcp-server 0.10.114 → 0.10.116
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/README.md +7 -2
- package/dist/src/handlers/brain.js +104 -13
- package/dist/src/handlers/brain.js.map +1 -1
- package/dist/src/tools.d.ts +38 -0
- package/dist/src/tools.js +30 -0
- package/dist/src/tools.js.map +1 -1
- package/glama.json +3 -1
- package/package.json +5 -3
- package/server.json +3 -3
package/README.md
CHANGED
|
@@ -22,11 +22,16 @@
|
|
|
22
22
|
<img src="https://img.shields.io/badge/GDPR-EU%20servers-green" alt="GDPR: EU servers" />
|
|
23
23
|
</a>
|
|
24
24
|
|
|
25
|
-
<img src="https://img.shields.io/badge/
|
|
25
|
+
<img src="https://img.shields.io/badge/140%20MCP%20tools-violet" alt="140 MCP tools" />
|
|
26
26
|
|
|
27
27
|
<img src="https://img.shields.io/badge/License-Apache--2.0-yellow" alt="License: Apache-2.0" />
|
|
28
28
|
</p>
|
|
29
29
|
|
|
30
|
+
<p align="center">
|
|
31
|
+
<strong><a href="https://cachly.dev/sign-up">⚡ Get your free Brain → cachly.dev</a></strong><br/>
|
|
32
|
+
<sub>Free forever · no credit card · 1-command setup · German servers · GDPR</sub>
|
|
33
|
+
</p>
|
|
34
|
+
|
|
30
35
|
---
|
|
31
36
|
|
|
32
37
|
## The story you already live every day
|
|
@@ -286,7 +291,7 @@ MCP tool — bulk-ingests past outcomes the same way `brain_from_git` ingests co
|
|
|
286
291
|
|
|
287
292
|
---
|
|
288
293
|
|
|
289
|
-
## MCP Tools (
|
|
294
|
+
## MCP Tools (140 total)
|
|
290
295
|
|
|
291
296
|
### 🧠 Session & Memory (most used)
|
|
292
297
|
|
|
@@ -7,8 +7,15 @@ import { keywordSearch, tokenize, splitMultiQuery, levenshtein, indexVocab as _i
|
|
|
7
7
|
import { rerankByQuality } from '../rerank.js';
|
|
8
8
|
import { computeEmbedding, hasEmbedProvider } from '../embeddings.js';
|
|
9
9
|
// ── Changelog (shown once per version in session_start) ──────────────────────
|
|
10
|
-
const MCP_VERSION = '0.10.
|
|
10
|
+
const MCP_VERSION = '0.10.115';
|
|
11
11
|
const WHATS_NEW = {
|
|
12
|
+
'0.10.115': [
|
|
13
|
+
`🔧 **\`brain_get_pref\` + \`brain_set_pref\` — persistent Brain preferences**`,
|
|
14
|
+
` 📖 \`brain_get_pref\` reads back any stored preference (omit key to list all).`,
|
|
15
|
+
` ✏️ \`brain_set_pref\` saves a preference to Redis — survives restarts, scoped per Brain instance.`,
|
|
16
|
+
` 💡 Known key: \`auto_changelog\` — set to "false" to silence the new-lessons digest at session_start.`,
|
|
17
|
+
` 📊 140 MCP tools`,
|
|
18
|
+
],
|
|
12
19
|
'0.10.114': [
|
|
13
20
|
`📋 **\`brain_changelog\` — weekly knowledge digest in one call**`,
|
|
14
21
|
` 📅 Generates a grouped Markdown changelog of lessons learned in the last N days.`,
|
|
@@ -356,6 +363,7 @@ export const BRAIN_TOOL_NAMES = new Set([
|
|
|
356
363
|
'brain_who_knows', 'brain_file_map', 'team_expertise_map',
|
|
357
364
|
'skill_gaps', 'brain_coverage', 'brain_metrics', 'brain_service_map',
|
|
358
365
|
'brain_collab_pairs', 'brain_portability', 'brain_changelog',
|
|
366
|
+
'brain_set_pref', 'brain_get_pref',
|
|
359
367
|
]);
|
|
360
368
|
// ── Free-tier Teaser-Gate ──────────────────────────────────────────────────
|
|
361
369
|
// When a Free-tier Brain crosses its recall limit, smart_recall still delivers
|
|
@@ -364,7 +372,6 @@ export const BRAIN_TOOL_NAMES = new Set([
|
|
|
364
372
|
// DEPTH gate (hide the tail), never an access wall: the #1 result is always
|
|
365
373
|
// returned. Usage/limit come from the same authoritative numbers as the Brain
|
|
366
374
|
// Health bar (GET /instances/:id/memory), cached briefly to avoid per-call latency.
|
|
367
|
-
const FREE_TIER_VISIBLE_RECALLS = 3;
|
|
368
375
|
const UPGRADE_URL = 'https://cachly.dev/billing';
|
|
369
376
|
const _recallGateCache = new Map();
|
|
370
377
|
async function getRecallGate(instanceId, apiFetch) {
|
|
@@ -375,7 +382,12 @@ async function getRecallGate(instanceId, apiFetch) {
|
|
|
375
382
|
const mem = await apiFetch(`/api/v1/instances/${instanceId}/memory`);
|
|
376
383
|
const limit = mem?.recall_limit ?? -1;
|
|
377
384
|
const used = mem?.total_recall_count ?? 0;
|
|
378
|
-
const gate = {
|
|
385
|
+
const gate = {
|
|
386
|
+
reached: limit > 0 && used >= limit,
|
|
387
|
+
limit,
|
|
388
|
+
used,
|
|
389
|
+
goodwillMessage: mem?.goodwill_message,
|
|
390
|
+
};
|
|
379
391
|
_recallGateCache.set(instanceId, { gate, expiresAt: Date.now() + 60_000 });
|
|
380
392
|
return gate;
|
|
381
393
|
}
|
|
@@ -1089,10 +1101,18 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
1089
1101
|
.sort((a, b) => b.hybridScore - a.hybridScore);
|
|
1090
1102
|
// ── Teaser-Gate: free tier over its recall limit hides the long tail ──────
|
|
1091
1103
|
const recallGate = await getRecallGate(instance_id, apiFetch);
|
|
1092
|
-
|
|
1093
|
-
|
|
1104
|
+
// Hard gate: once a free user is over their (monthly, goodwill-adjusted)
|
|
1105
|
+
// recall limit, the long tail is fully withheld until they upgrade or the
|
|
1106
|
+
// month resets. We still show the single highest-value proven lesson below
|
|
1107
|
+
// as a teaser so the tool isn't dead and the value is obvious.
|
|
1108
|
+
const gateActive = recallGate.reached;
|
|
1109
|
+
const visibleCount = gateActive ? 0 : 8;
|
|
1094
1110
|
// ── Build output ──────────────────────────────────────────────────────────
|
|
1095
1111
|
const lines = [`🧠 **Smart Recall** for: _"${query}"_\n`];
|
|
1112
|
+
// Warm goodwill note — shown the month a free user is gifted +250 recalls.
|
|
1113
|
+
if (recallGate.goodwillMessage) {
|
|
1114
|
+
lines.push(`${recallGate.goodwillMessage}\n`);
|
|
1115
|
+
}
|
|
1096
1116
|
// "Brain saved you here" banner — surfaces value inline for proven recalled lessons
|
|
1097
1117
|
const SORDER = { critical: 0, major: 1, minor: 2 };
|
|
1098
1118
|
const topLesson = savedHere.sort((a, b) => (SORDER[a.severity] ?? 1) - (SORDER[b.severity] ?? 1))[0];
|
|
@@ -1140,7 +1160,7 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
1140
1160
|
// Group by sub-query for multi-topic queries
|
|
1141
1161
|
if (subQueries.length > 1) {
|
|
1142
1162
|
const grouped = new Map();
|
|
1143
|
-
for (const r of hybridResults.slice(0, gateActive ?
|
|
1163
|
+
for (const r of hybridResults.slice(0, gateActive ? 0 : 12)) {
|
|
1144
1164
|
const sq = r.subQuery ?? query;
|
|
1145
1165
|
if (!grouped.has(sq))
|
|
1146
1166
|
grouped.set(sq, []);
|
|
@@ -1190,12 +1210,12 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
1190
1210
|
lines.push(`> ${preview}${r.content.length > 400 ? '…' : ''}\n`);
|
|
1191
1211
|
}
|
|
1192
1212
|
}
|
|
1193
|
-
// ──
|
|
1213
|
+
// ── Hard gate footer: over the recall limit, the tail is withheld ──────
|
|
1194
1214
|
if (gateActive) {
|
|
1195
1215
|
const withheld = Math.max(0, hybridResults.length - visibleCount);
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1216
|
+
lines.push(`🔒 **Free recall limit reached** (${recallGate.used}/${recallGate.limit} this month). ` +
|
|
1217
|
+
`${withheld} relevant lesson${withheld !== 1 ? 's' : ''} withheld. ` +
|
|
1218
|
+
`Your limit resets next month, or unlock your full Brain now → ${UPGRADE_URL}\n`);
|
|
1199
1219
|
}
|
|
1200
1220
|
}
|
|
1201
1221
|
else {
|
|
@@ -2012,6 +2032,49 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
2012
2032
|
reasons.push('stale lessons');
|
|
2013
2033
|
lines.push(`🩺 _Run \`brain_doctor\` to fix: ${reasons.join(', ')}._`);
|
|
2014
2034
|
}
|
|
2035
|
+
// ── Auto-Changelog — new lessons since last session ──────────────────────
|
|
2036
|
+
// Appended at the end so it never competes with the core briefing.
|
|
2037
|
+
// Skipped on: first session, no new lessons, or when disabled via prefs.
|
|
2038
|
+
try {
|
|
2039
|
+
const autoChangelogPref = await redis.get(`cachly:prefs:auto_changelog:${instance_id}`).catch(() => null);
|
|
2040
|
+
const autoChangelogEnabled = autoChangelogPref !== 'false';
|
|
2041
|
+
if (autoChangelogEnabled && lastSession?.ts && !isFirstSession) {
|
|
2042
|
+
const lastTs = new Date(lastSession.ts).getTime();
|
|
2043
|
+
if (!Number.isNaN(lastTs)) {
|
|
2044
|
+
const newLessons = lessons.filter(l => {
|
|
2045
|
+
const t = new Date(l.ts).getTime();
|
|
2046
|
+
return !Number.isNaN(t) && t > lastTs;
|
|
2047
|
+
});
|
|
2048
|
+
if (newLessons.length > 0) {
|
|
2049
|
+
const ago = Math.round((Date.now() - lastTs) / 60000);
|
|
2050
|
+
const agoStr = ago < 60 ? `${ago}m` : ago < 1440 ? `${Math.round(ago / 60)}h` : `${Math.round(ago / 1440)}d`;
|
|
2051
|
+
lines.push('');
|
|
2052
|
+
lines.push(`📋 **${newLessons.length} new lesson${newLessons.length !== 1 ? 's' : ''} since your last session** (${agoStr} ago):`);
|
|
2053
|
+
// Group by category
|
|
2054
|
+
const catMap = new Map();
|
|
2055
|
+
for (const l of newLessons.slice(0, 20)) {
|
|
2056
|
+
const cat = l.topic.includes(':') ? l.topic.split(':')[0] : 'general';
|
|
2057
|
+
const arr = catMap.get(cat) ?? [];
|
|
2058
|
+
arr.push(l);
|
|
2059
|
+
catMap.set(cat, arr);
|
|
2060
|
+
}
|
|
2061
|
+
for (const [cat, catLessons] of catMap) {
|
|
2062
|
+
lines.push(` **${cat}**`);
|
|
2063
|
+
for (const l of catLessons) {
|
|
2064
|
+
const icon = l.outcome === 'failure' ? '❌' : l.severity === 'critical' ? '🚨' : l.severity === 'major' ? '⚠️' : '✅';
|
|
2065
|
+
const body = (l.outcome === 'failure' ? l.what_failed : l.what_worked) ?? '';
|
|
2066
|
+
lines.push(` ${icon} \`${l.topic}\`${body ? ` — ${body.slice(0, 80)}` : ''}`);
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
2069
|
+
if (newLessons.length > 20) {
|
|
2070
|
+
lines.push(` … and ${newLessons.length - 20} more. Call \`brain_changelog\` for the full list.`);
|
|
2071
|
+
}
|
|
2072
|
+
lines.push(` _Disable auto-changelog: \`brain_set_pref(instance_id="${instance_id}", key="auto_changelog", value="false")\`_`);
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
catch { /* changelog must never block session_start */ }
|
|
2015
2078
|
return lines.join('\n');
|
|
2016
2079
|
}
|
|
2017
2080
|
// ── session_start_summary ─────────────────────────────────────────────────
|
|
@@ -3279,7 +3342,6 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
3279
3342
|
const { instance_id, days = 7, max_lessons = 30, include_failures = true, } = args;
|
|
3280
3343
|
const redis = await getConnection(instance_id);
|
|
3281
3344
|
const cutoff = Date.now() - Number(days) * 86_400_000;
|
|
3282
|
-
// Scan all lessons and collect recent ones.
|
|
3283
3345
|
const keys = await scanKeys(redis, 'cachly:lesson:best:*', { max: 4000, timeoutMs: 4000 });
|
|
3284
3346
|
const recent = [];
|
|
3285
3347
|
for (const k of keys) {
|
|
@@ -3295,7 +3357,6 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
3295
3357
|
const topic = k.replace('cachly:lesson:best:', '');
|
|
3296
3358
|
recent.push({ ...lesson, topic });
|
|
3297
3359
|
}
|
|
3298
|
-
// Sort by timestamp desc, cap at max_lessons.
|
|
3299
3360
|
recent.sort((a, b) => {
|
|
3300
3361
|
const ta = a.ts ? new Date(a.ts).getTime() : 0;
|
|
3301
3362
|
const tb = b.ts ? new Date(b.ts).getTime() : 0;
|
|
@@ -3309,7 +3370,6 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
3309
3370
|
`_No new lessons learned in this window. Your Brain already knows everything it knows — add new lessons with \`learn_from_attempts\`._`,
|
|
3310
3371
|
].join('\n');
|
|
3311
3372
|
}
|
|
3312
|
-
// Group by category (topic prefix before first ':').
|
|
3313
3373
|
const groups = new Map();
|
|
3314
3374
|
for (const l of lessons) {
|
|
3315
3375
|
const cat = l.topic.includes(':') ? l.topic.split(':')[0] : 'general';
|
|
@@ -3348,6 +3408,37 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
3348
3408
|
lines.push('---', `_Generated from Brain \`${instance_id}\` · Share this in your standup or Slack_`);
|
|
3349
3409
|
return lines.join('\n');
|
|
3350
3410
|
}
|
|
3411
|
+
// ── brain_set_pref ────────────────────────────────────────────────────────
|
|
3412
|
+
case 'brain_set_pref': {
|
|
3413
|
+
const { instance_id, key, value, } = args;
|
|
3414
|
+
if (!key || !key.trim())
|
|
3415
|
+
throw new Error('key is required');
|
|
3416
|
+
const redis = await getConnection(instance_id);
|
|
3417
|
+
const redisKey = `cachly:prefs:${key.trim()}:${instance_id}`;
|
|
3418
|
+
await redis.set(redisKey, String(value));
|
|
3419
|
+
return `✅ Preference \`${key}\` set to \`${value}\` for this Brain instance.`;
|
|
3420
|
+
}
|
|
3421
|
+
// ── brain_get_pref ────────────────────────────────────────────────────────
|
|
3422
|
+
case 'brain_get_pref': {
|
|
3423
|
+
const { instance_id, key } = args;
|
|
3424
|
+
const redis = await getConnection(instance_id);
|
|
3425
|
+
if (key && key.trim()) {
|
|
3426
|
+
const val = await redis.get(`cachly:prefs:${key.trim()}:${instance_id}`).catch(() => null);
|
|
3427
|
+
return val === null
|
|
3428
|
+
? `ℹ️ Preference \`${key}\` is not set (using default).`
|
|
3429
|
+
: `🔧 Preference \`${key}\` = \`${val}\``;
|
|
3430
|
+
}
|
|
3431
|
+
const prefKeys = await scanKeys(redis, `cachly:prefs:*:${instance_id}`, { max: 200, timeoutMs: 2000 });
|
|
3432
|
+
if (prefKeys.length === 0)
|
|
3433
|
+
return 'ℹ️ No preferences set for this Brain instance.';
|
|
3434
|
+
const vals = await redis.mget(...prefKeys);
|
|
3435
|
+
const lines = ['🔧 **Brain preferences:**', ''];
|
|
3436
|
+
for (let i = 0; i < prefKeys.length; i++) {
|
|
3437
|
+
const k = prefKeys[i].replace('cachly:prefs:', '').replace(`:${instance_id}`, '');
|
|
3438
|
+
lines.push(`- \`${k}\` = \`${vals[i] ?? '(null)'}\``);
|
|
3439
|
+
}
|
|
3440
|
+
return lines.join('\n');
|
|
3441
|
+
}
|
|
3351
3442
|
// ── sync_file_changes ─────────────────────────────────────────────────────
|
|
3352
3443
|
default:
|
|
3353
3444
|
return null;
|