@cachly-dev/mcp-server 0.5.40 → 0.5.42
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 +148 -147
- package/dist/index.js +328 -2
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/index.ts +359 -2
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
3
3
|
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
4
4
|
import { join, relative, extname } from 'node:path';
|
|
5
5
|
/**
|
|
6
|
-
* cachly MCP Server v0.5.
|
|
6
|
+
* cachly MCP Server v0.5.42
|
|
7
7
|
*
|
|
8
8
|
* Exposes cachly.dev as MCP tools so any AI assistant
|
|
9
9
|
* (GitHub Copilot, Claude, Cursor, Windsurf, Continue.dev …) can:
|
|
@@ -1391,6 +1391,52 @@ const TOOLS = [
|
|
|
1391
1391
|
required: ['instance_id'],
|
|
1392
1392
|
},
|
|
1393
1393
|
},
|
|
1394
|
+
{
|
|
1395
|
+
name: 'brain_stats',
|
|
1396
|
+
description: 'Complete statistics dashboard for your AI Brain — lessons, recalls, time saved, top topics, team authors, memory usage. ' +
|
|
1397
|
+
'Returns a formatted report designed for sharing (screenshot-worthy). ' +
|
|
1398
|
+
'Use this when you want a full overview or want to share your brain health with others.',
|
|
1399
|
+
inputSchema: {
|
|
1400
|
+
type: 'object',
|
|
1401
|
+
properties: {
|
|
1402
|
+
instance_id: { type: 'string', description: 'UUID of the cache instance' },
|
|
1403
|
+
},
|
|
1404
|
+
required: ['instance_id'],
|
|
1405
|
+
},
|
|
1406
|
+
},
|
|
1407
|
+
{
|
|
1408
|
+
name: 'export_brain',
|
|
1409
|
+
description: 'Export your entire AI Brain as a portable Markdown file. ' +
|
|
1410
|
+
'Includes all lessons (topic, outcome, what worked, severity), context entries (keys + content), ' +
|
|
1411
|
+
'session history, and metadata. Perfect for sharing with teammates, archiving, posting to GitHub, ' +
|
|
1412
|
+
'or importing into a new instance. ' +
|
|
1413
|
+
'Example: export_brain() → returns full markdown string you can save to a .md file.',
|
|
1414
|
+
inputSchema: {
|
|
1415
|
+
type: 'object',
|
|
1416
|
+
properties: {
|
|
1417
|
+
instance_id: { type: 'string', description: 'UUID of the cache instance' },
|
|
1418
|
+
include_context: { type: 'boolean', description: 'Include context entries (default: true)' },
|
|
1419
|
+
max_lessons: { type: 'number', description: 'Max lessons to include (default: all)' },
|
|
1420
|
+
},
|
|
1421
|
+
required: ['instance_id'],
|
|
1422
|
+
},
|
|
1423
|
+
},
|
|
1424
|
+
{
|
|
1425
|
+
name: 'invite_link',
|
|
1426
|
+
description: 'Generate a one-click team invite link for your Brain instance. ' +
|
|
1427
|
+
'Share the link (or the npx join command) with a teammate — they run one command and are instantly ' +
|
|
1428
|
+
'connected to the same shared Brain with all your lessons and context. ' +
|
|
1429
|
+
'No dashboard visit, no copy-paste of instance IDs required. ' +
|
|
1430
|
+
'Example: invite_link() → returns "npx @cachly-dev/mcp-server@latest join <token>"',
|
|
1431
|
+
inputSchema: {
|
|
1432
|
+
type: 'object',
|
|
1433
|
+
properties: {
|
|
1434
|
+
instance_id: { type: 'string', description: 'UUID of the cache instance to share' },
|
|
1435
|
+
label: { type: 'string', description: 'Optional label shown to the invitee (e.g. "my-api project")' },
|
|
1436
|
+
},
|
|
1437
|
+
required: ['instance_id'],
|
|
1438
|
+
},
|
|
1439
|
+
},
|
|
1394
1440
|
{
|
|
1395
1441
|
name: 'brain_doctor',
|
|
1396
1442
|
description: 'Check the health of your AI Brain and get actionable recommendations. ' +
|
|
@@ -2658,7 +2704,18 @@ async function handleTool(name, args) {
|
|
|
2658
2704
|
lines.push('');
|
|
2659
2705
|
}
|
|
2660
2706
|
else {
|
|
2661
|
-
|
|
2707
|
+
const totalRecalls = lessons.reduce((s, l) => s + (l.recall_count ?? 0), 0);
|
|
2708
|
+
const savedMin = totalRecalls * 15;
|
|
2709
|
+
const savedStr = savedMin >= 60
|
|
2710
|
+
? `~${(savedMin / 60).toFixed(1)}h saved`
|
|
2711
|
+
: savedMin > 0 ? `~${savedMin}min saved` : '';
|
|
2712
|
+
const statsLine = [
|
|
2713
|
+
`${lessons.length} lessons`,
|
|
2714
|
+
ctxCount > 0 ? `${ctxCount} context entries` : '',
|
|
2715
|
+
totalRecalls > 0 ? `${totalRecalls} recalls` : '',
|
|
2716
|
+
savedStr,
|
|
2717
|
+
].filter(Boolean).join(' · ');
|
|
2718
|
+
lines.push(`📊 **Brain:** ${statsLine}`, '');
|
|
2662
2719
|
}
|
|
2663
2720
|
// Focus-relevant lessons
|
|
2664
2721
|
if (focusLessons.length > 0) {
|
|
@@ -2941,6 +2998,275 @@ async function handleTool(name, args) {
|
|
|
2941
2998
|
}
|
|
2942
2999
|
return lines.join('\n');
|
|
2943
3000
|
}
|
|
3001
|
+
// ── brain_stats ───────────────────────────────────────────────────────────
|
|
3002
|
+
case 'brain_stats': {
|
|
3003
|
+
const { instance_id } = args;
|
|
3004
|
+
const redis = await getConnection(instance_id);
|
|
3005
|
+
// Scan all lesson keys
|
|
3006
|
+
const lessonKeys = [];
|
|
3007
|
+
const lStream = redis.scanStream({ match: 'cachly:lesson:best:*', count: 200 });
|
|
3008
|
+
await new Promise((resolve, reject) => {
|
|
3009
|
+
lStream.on('data', (batch) => lessonKeys.push(...batch));
|
|
3010
|
+
lStream.on('end', resolve);
|
|
3011
|
+
lStream.on('error', reject);
|
|
3012
|
+
});
|
|
3013
|
+
const lessons = [];
|
|
3014
|
+
if (lessonKeys.length > 0) {
|
|
3015
|
+
const vals = await redis.mget(...lessonKeys);
|
|
3016
|
+
for (const raw of vals) {
|
|
3017
|
+
if (!raw)
|
|
3018
|
+
continue;
|
|
3019
|
+
try {
|
|
3020
|
+
lessons.push(JSON.parse(raw));
|
|
3021
|
+
}
|
|
3022
|
+
catch { /* skip */ }
|
|
3023
|
+
}
|
|
3024
|
+
}
|
|
3025
|
+
// Context count
|
|
3026
|
+
let ctxCount = 0;
|
|
3027
|
+
const ctxStream = redis.scanStream({ match: 'cachly:ctx:*', count: 200 });
|
|
3028
|
+
await new Promise((resolve, reject) => {
|
|
3029
|
+
ctxStream.on('data', (batch) => { ctxCount += batch.filter((k) => !k.endsWith(':meta')).length; });
|
|
3030
|
+
ctxStream.on('end', resolve);
|
|
3031
|
+
ctxStream.on('error', reject);
|
|
3032
|
+
});
|
|
3033
|
+
const lastSessionRaw = await redis.get('cachly:session:last');
|
|
3034
|
+
let lastSession = null;
|
|
3035
|
+
if (lastSessionRaw) {
|
|
3036
|
+
try {
|
|
3037
|
+
lastSession = JSON.parse(lastSessionRaw);
|
|
3038
|
+
}
|
|
3039
|
+
catch { /* ignore */ }
|
|
3040
|
+
}
|
|
3041
|
+
const totalRecalls = lessons.reduce((s, l) => s + (l.recall_count ?? 0), 0);
|
|
3042
|
+
const savedMin = totalRecalls * 15;
|
|
3043
|
+
const savedHours = (savedMin / 60).toFixed(1);
|
|
3044
|
+
const successCount = lessons.filter(l => l.outcome === 'success').length;
|
|
3045
|
+
const failureCount = lessons.filter(l => l.outcome === 'failure' || l.outcome === 'partial').length;
|
|
3046
|
+
const criticalCount = lessons.filter(l => l.severity === 'critical').length;
|
|
3047
|
+
const majorCount = lessons.filter(l => l.severity === 'major').length;
|
|
3048
|
+
const authors = [...new Set(lessons.map(l => l.author).filter(Boolean))];
|
|
3049
|
+
const topByRecall = [...lessons].sort((a, b) => (b.recall_count ?? 0) - (a.recall_count ?? 0)).slice(0, 5);
|
|
3050
|
+
const newestLesson = [...lessons].sort((a, b) => new Date(b.ts).getTime() - new Date(a.ts).getTime())[0];
|
|
3051
|
+
// Age of last session
|
|
3052
|
+
let lastSessionStr = 'none yet';
|
|
3053
|
+
if (lastSession) {
|
|
3054
|
+
const ageMin = Math.round((Date.now() - new Date(lastSession.ts).getTime()) / 60000);
|
|
3055
|
+
lastSessionStr = ageMin < 60 ? `${ageMin}m ago`
|
|
3056
|
+
: ageMin < 1440 ? `${Math.round(ageMin / 60)}h ago`
|
|
3057
|
+
: `${Math.round(ageMin / 1440)}d ago`;
|
|
3058
|
+
}
|
|
3059
|
+
const tokensSaved = (totalRecalls * 1200);
|
|
3060
|
+
const tokenStr = tokensSaved >= 1000 ? `~${(tokensSaved / 1000).toFixed(0)}k tokens` : `~${tokensSaved} tokens`;
|
|
3061
|
+
const costSaved = (tokensSaved * 0.000003).toFixed(2);
|
|
3062
|
+
const lines = [
|
|
3063
|
+
`🧠 **Brain Stats — Full Dashboard**`,
|
|
3064
|
+
``,
|
|
3065
|
+
`╔══════════════════════════════════════╗`,
|
|
3066
|
+
`║ 📚 Lessons: ${String(lessons.length).padEnd(18)}║`,
|
|
3067
|
+
`║ 🔄 Total recalls: ${String(totalRecalls).padEnd(18)}║`,
|
|
3068
|
+
`║ 🕐 Time saved: ~${savedHours}h${' '.repeat(Math.max(0, 16 - savedHours.length))}║`,
|
|
3069
|
+
`║ 💎 Tokens saved: ${tokenStr.padEnd(18)}║`,
|
|
3070
|
+
`║ 💵 Cost saved: ~$${costSaved.padEnd(17)}║`,
|
|
3071
|
+
`║ 📁 Context keys: ${String(ctxCount).padEnd(18)}║`,
|
|
3072
|
+
`╚══════════════════════════════════════╝`,
|
|
3073
|
+
``,
|
|
3074
|
+
`**Lessons breakdown:**`,
|
|
3075
|
+
` ✅ Success: ${successCount} ❌ Failures/partial: ${failureCount}`,
|
|
3076
|
+
` 🔴 Critical: ${criticalCount} 🟡 Major: ${majorCount}`,
|
|
3077
|
+
``,
|
|
3078
|
+
`**Last session:** ${lastSessionStr}${lastSession?.summary ? ` — ${lastSession.summary.slice(0, 80)}` : ''}`,
|
|
3079
|
+
`**Newest lesson:** ${newestLesson ? `\`${newestLesson.topic}\`` : 'none'}`,
|
|
3080
|
+
``,
|
|
3081
|
+
];
|
|
3082
|
+
if (topByRecall.length > 0) {
|
|
3083
|
+
lines.push(`**Top recalled lessons (most reused):**`);
|
|
3084
|
+
for (const l of topByRecall) {
|
|
3085
|
+
const emoji = l.outcome === 'success' ? '✅' : '⚠️';
|
|
3086
|
+
lines.push(` ${emoji} \`${l.topic}\` — recalled ${l.recall_count ?? 0}× · ${l.what_worked.slice(0, 60)}`);
|
|
3087
|
+
}
|
|
3088
|
+
lines.push('');
|
|
3089
|
+
}
|
|
3090
|
+
if (authors.length > 0) {
|
|
3091
|
+
lines.push(`**Team contributors (${authors.length}):** ${authors.join(', ')}`);
|
|
3092
|
+
lines.push('');
|
|
3093
|
+
}
|
|
3094
|
+
lines.push(`💡 Share your brain: \`invite_link()\` — generates a one-command team invite.`);
|
|
3095
|
+
lines.push(`📤 Export to Markdown: \`export_brain()\` — portable snapshot you can save or share.`);
|
|
3096
|
+
return lines.join('\n');
|
|
3097
|
+
}
|
|
3098
|
+
// ── export_brain ──────────────────────────────────────────────────────────
|
|
3099
|
+
case 'export_brain': {
|
|
3100
|
+
const { instance_id, include_context = true, max_lessons } = args;
|
|
3101
|
+
const redis = await getConnection(instance_id);
|
|
3102
|
+
// Scan all lessons
|
|
3103
|
+
const lessonKeys = [];
|
|
3104
|
+
const lStream = redis.scanStream({ match: 'cachly:lesson:best:*', count: 200 });
|
|
3105
|
+
await new Promise((resolve, reject) => {
|
|
3106
|
+
lStream.on('data', (batch) => lessonKeys.push(...batch));
|
|
3107
|
+
lStream.on('end', resolve);
|
|
3108
|
+
lStream.on('error', reject);
|
|
3109
|
+
});
|
|
3110
|
+
let lessons = [];
|
|
3111
|
+
if (lessonKeys.length > 0) {
|
|
3112
|
+
const vals = await redis.mget(...lessonKeys);
|
|
3113
|
+
for (const raw of vals) {
|
|
3114
|
+
if (!raw)
|
|
3115
|
+
continue;
|
|
3116
|
+
try {
|
|
3117
|
+
lessons.push(JSON.parse(raw));
|
|
3118
|
+
}
|
|
3119
|
+
catch { /* skip */ }
|
|
3120
|
+
}
|
|
3121
|
+
}
|
|
3122
|
+
lessons.sort((a, b) => new Date(b.ts).getTime() - new Date(a.ts).getTime());
|
|
3123
|
+
if (max_lessons && max_lessons > 0)
|
|
3124
|
+
lessons = lessons.slice(0, max_lessons);
|
|
3125
|
+
// Last session + crystal
|
|
3126
|
+
const lastSessionRaw = await redis.get('cachly:session:last');
|
|
3127
|
+
let lastSession = null;
|
|
3128
|
+
if (lastSessionRaw) {
|
|
3129
|
+
try {
|
|
3130
|
+
lastSession = JSON.parse(lastSessionRaw);
|
|
3131
|
+
}
|
|
3132
|
+
catch { /* ignore */ }
|
|
3133
|
+
}
|
|
3134
|
+
const crystalRaw = await redis.get('cachly:crystal');
|
|
3135
|
+
let crystal = null;
|
|
3136
|
+
if (crystalRaw) {
|
|
3137
|
+
try {
|
|
3138
|
+
crystal = JSON.parse(crystalRaw);
|
|
3139
|
+
}
|
|
3140
|
+
catch { /* ignore */ }
|
|
3141
|
+
}
|
|
3142
|
+
const ctxEntries = [];
|
|
3143
|
+
if (include_context) {
|
|
3144
|
+
const ctxKeys = [];
|
|
3145
|
+
const ctxStream = redis.scanStream({ match: 'cachly:ctx:*', count: 200 });
|
|
3146
|
+
await new Promise((resolve, reject) => {
|
|
3147
|
+
ctxStream.on('data', (batch) => {
|
|
3148
|
+
ctxKeys.push(...batch.filter((k) => !k.endsWith(':meta')));
|
|
3149
|
+
});
|
|
3150
|
+
ctxStream.on('end', resolve);
|
|
3151
|
+
ctxStream.on('error', reject);
|
|
3152
|
+
});
|
|
3153
|
+
if (ctxKeys.length > 0) {
|
|
3154
|
+
const ctxVals = await redis.mget(...ctxKeys);
|
|
3155
|
+
for (let i = 0; i < ctxKeys.length; i++) {
|
|
3156
|
+
const val = ctxVals[i];
|
|
3157
|
+
if (val)
|
|
3158
|
+
ctxEntries.push({ key: ctxKeys[i].replace('cachly:ctx:', ''), value: val });
|
|
3159
|
+
}
|
|
3160
|
+
}
|
|
3161
|
+
}
|
|
3162
|
+
const exportedAt = new Date().toISOString();
|
|
3163
|
+
const totalRecalls = lessons.reduce((s, l) => s + (l.recall_count ?? 0), 0);
|
|
3164
|
+
const savedHours = (totalRecalls * 15 / 60).toFixed(1);
|
|
3165
|
+
const md = [
|
|
3166
|
+
`# 🧠 cachly Brain Export`,
|
|
3167
|
+
``,
|
|
3168
|
+
`> Exported: ${exportedAt} `,
|
|
3169
|
+
`> Instance: \`${instance_id}\` `,
|
|
3170
|
+
`> Lessons: ${lessons.length} · Recalls: ${totalRecalls} · Time saved: ~${savedHours}h`,
|
|
3171
|
+
``,
|
|
3172
|
+
];
|
|
3173
|
+
if (crystal) {
|
|
3174
|
+
md.push(`## 💎 Memory Crystal`, ``, `> ${crystal.summary}`, ``, `_Generated: ${crystal.created_at}_`, ``);
|
|
3175
|
+
}
|
|
3176
|
+
if (lastSession) {
|
|
3177
|
+
md.push(`## 📅 Last Session`, ``, `**${lastSession.ts.slice(0, 10)}** — ${lastSession.summary}`, ``);
|
|
3178
|
+
}
|
|
3179
|
+
// Group lessons by outcome
|
|
3180
|
+
const successLessons = lessons.filter(l => l.outcome === 'success');
|
|
3181
|
+
const failureLessons = lessons.filter(l => l.outcome !== 'success');
|
|
3182
|
+
const formatLesson = (l) => {
|
|
3183
|
+
const sev = l.severity === 'critical' ? ' 🔴' : l.severity === 'major' ? ' 🟡' : '';
|
|
3184
|
+
const lines = [
|
|
3185
|
+
`### \`${l.topic}\`${sev}`,
|
|
3186
|
+
``,
|
|
3187
|
+
`**Outcome:** ${l.outcome} `,
|
|
3188
|
+
`**Recalls:** ${l.recall_count ?? 0}× `,
|
|
3189
|
+
`**Stored:** ${l.ts.slice(0, 10)}${l.author ? ` \n**Author:** ${l.author}` : ''}`,
|
|
3190
|
+
``,
|
|
3191
|
+
`**What worked:** `,
|
|
3192
|
+
l.what_worked,
|
|
3193
|
+
];
|
|
3194
|
+
if (l.what_failed) {
|
|
3195
|
+
lines.push(``, `**What failed:** `, l.what_failed);
|
|
3196
|
+
}
|
|
3197
|
+
if ((l.commands ?? []).length > 0) {
|
|
3198
|
+
lines.push(``, `**Commands:**`);
|
|
3199
|
+
for (const cmd of l.commands)
|
|
3200
|
+
lines.push(`\`\`\`\n${cmd}\n\`\`\``);
|
|
3201
|
+
}
|
|
3202
|
+
if ((l.file_paths ?? []).length > 0) {
|
|
3203
|
+
lines.push(``, `**Files:** ${l.file_paths.map(f => `\`${f}\``).join(', ')}`);
|
|
3204
|
+
}
|
|
3205
|
+
if ((l.tags ?? []).length > 0) {
|
|
3206
|
+
lines.push(``, `**Tags:** ${l.tags.map(t => `\`${t}\``).join(', ')}`);
|
|
3207
|
+
}
|
|
3208
|
+
lines.push(``);
|
|
3209
|
+
return lines.join('\n');
|
|
3210
|
+
};
|
|
3211
|
+
if (successLessons.length > 0) {
|
|
3212
|
+
md.push(`## ✅ Successful Solutions (${successLessons.length})`, ``);
|
|
3213
|
+
for (const l of successLessons)
|
|
3214
|
+
md.push(formatLesson(l));
|
|
3215
|
+
}
|
|
3216
|
+
if (failureLessons.length > 0) {
|
|
3217
|
+
md.push(`## ⚠️ Open / Partial Issues (${failureLessons.length})`, ``);
|
|
3218
|
+
for (const l of failureLessons)
|
|
3219
|
+
md.push(formatLesson(l));
|
|
3220
|
+
}
|
|
3221
|
+
if (ctxEntries.length > 0) {
|
|
3222
|
+
md.push(`## 📁 Context Entries (${ctxEntries.length})`, ``);
|
|
3223
|
+
for (const e of ctxEntries) {
|
|
3224
|
+
md.push(`### \`${e.key}\``, ``, e.value.slice(0, 500) + (e.value.length > 500 ? '\n\n_[truncated]_' : ''), ``);
|
|
3225
|
+
}
|
|
3226
|
+
}
|
|
3227
|
+
md.push(`---`, ``, `_Exported from [cachly AI Brain](https://cachly.dev) · Re-import with \`npx @cachly-dev/mcp-server@latest setup\`_`);
|
|
3228
|
+
return md.join('\n');
|
|
3229
|
+
}
|
|
3230
|
+
// ── invite_link ───────────────────────────────────────────────────────────
|
|
3231
|
+
case 'invite_link': {
|
|
3232
|
+
const { instance_id, label = '' } = args;
|
|
3233
|
+
// Generate a share token via API
|
|
3234
|
+
let shareToken = '';
|
|
3235
|
+
let inviteUrl = '';
|
|
3236
|
+
try {
|
|
3237
|
+
const res = await apiFetch(`/api/v1/instances/${instance_id}/invite`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ label }) });
|
|
3238
|
+
shareToken = res.token ?? '';
|
|
3239
|
+
inviteUrl = res.url ?? '';
|
|
3240
|
+
}
|
|
3241
|
+
catch {
|
|
3242
|
+
// API doesn't support invite yet — generate a local share snippet instead
|
|
3243
|
+
}
|
|
3244
|
+
// Build the join command
|
|
3245
|
+
const joinCmd = shareToken
|
|
3246
|
+
? `npx @cachly-dev/mcp-server@latest join ${shareToken}`
|
|
3247
|
+
: `npx @cachly-dev/mcp-server@latest setup`;
|
|
3248
|
+
const lines = [
|
|
3249
|
+
`🔗 **Team Invite — ${label || instance_id.slice(0, 8) + '…'}**`,
|
|
3250
|
+
``,
|
|
3251
|
+
`Share this command with your teammate:`,
|
|
3252
|
+
``,
|
|
3253
|
+
`\`\`\`bash`,
|
|
3254
|
+
joinCmd,
|
|
3255
|
+
`\`\`\``,
|
|
3256
|
+
``,
|
|
3257
|
+
`**What happens when they run it:**`,
|
|
3258
|
+
` 1. Browser opens → they sign in (free, 30 seconds)`,
|
|
3259
|
+
` 2. Their Brain is auto-connected to this shared instance`,
|
|
3260
|
+
` 3. All ${label ? `"${label}" ` : ''}lessons and context are instantly available`,
|
|
3261
|
+
` 4. Editor configs written automatically — no copy-paste`,
|
|
3262
|
+
``,
|
|
3263
|
+
];
|
|
3264
|
+
if (inviteUrl) {
|
|
3265
|
+
lines.push(`**Or share the link directly:** ${inviteUrl}`, ``);
|
|
3266
|
+
}
|
|
3267
|
+
lines.push(`💡 Teammates can also run \`session_start\` immediately to see all your lessons.`, ` Use \`team_learn\` with an \`author\` param so everyone knows who learned what.`);
|
|
3268
|
+
return lines.join('\n');
|
|
3269
|
+
}
|
|
2944
3270
|
// ── brain_doctor ──────────────────────────────────────────────────────────
|
|
2945
3271
|
case 'brain_doctor': {
|
|
2946
3272
|
const { instance_id } = args;
|