@cachly-dev/mcp-server 0.5.39 → 0.5.41

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/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@cachly-dev/mcp-server",
3
- "version": "0.5.39",
3
+ "version": "0.5.41",
4
4
  "mcpName": "io.github.cachly-dev/mcp-server",
5
- "description": "AI memory brain for Claude Code, Cursor, Copilot & Windsurf — persistent sessions, lessons learned, semantic search, team brain. 51 MCP tools.",
5
+ "description": "Claude forgets. cachly doesn't. Persistent AI memory for Claude Code, Cursor & Copilot — one command setup, zero copy-paste.",
6
6
  "type": "module",
7
7
  "bin": {
8
8
  "cachly-mcp": "./dist/index.js"
package/src/index.ts 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.39
6
+ * cachly MCP Server v0.5.41
7
7
  *
8
8
  * Exposes cachly.dev as MCP tools so any AI assistant
9
9
  * (GitHub Copilot, Claude, Cursor, Windsurf, Continue.dev …) can:
@@ -696,11 +696,26 @@ interface SemanticSearchResponse {
696
696
 
697
697
  // ── Connection pool ───────────────────────────────────────────────────────────
698
698
 
699
- /** Reuse Redis connections across tool calls (keyed by instance_id). */
699
+ /** Reuse Redis connections across tool calls (keyed by instance_id). Max 20 entries, LRU eviction. */
700
700
  const pool = new Map<string, Redis>();
701
+ const POOL_MAX = 20;
701
702
 
702
703
  async function getConnection(instance_id: string): Promise<Redis> {
703
- if (pool.has(instance_id)) return pool.get(instance_id)!;
704
+ if (pool.has(instance_id)) {
705
+ // Move to end (most-recently-used)
706
+ const conn = pool.get(instance_id)!;
707
+ pool.delete(instance_id);
708
+ pool.set(instance_id, conn);
709
+ return conn;
710
+ }
711
+ // Evict oldest entry when pool is full
712
+ if (pool.size >= POOL_MAX) {
713
+ const oldest = pool.keys().next().value;
714
+ if (oldest) {
715
+ pool.get(oldest)?.quit().catch(() => undefined);
716
+ pool.delete(oldest);
717
+ }
718
+ }
704
719
 
705
720
  const inst = await apiFetch<Instance>(`/api/v1/instances/${instance_id}`);
706
721
  if (inst.status !== 'running') {
@@ -2318,7 +2333,8 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
2318
2333
  await redis.set(`${cacheKey}:meta`, meta);
2319
2334
  }
2320
2335
 
2321
- // Also index semantically for smart_recall (if vector available)
2336
+ // Also index semantically for smart_recall (if vector available and embedding is configured)
2337
+ if (EMBED_PROVIDER !== 'none') {
2322
2338
  const inst = await apiFetch<Instance>(`/api/v1/instances/${instance_id}`);
2323
2339
  if (inst.vector_token) {
2324
2340
  try {
@@ -2342,6 +2358,7 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
2342
2358
  // Embedding optional — continue silently
2343
2359
  }
2344
2360
  }
2361
+ } // end EMBED_PROVIDER !== 'none'
2345
2362
 
2346
2363
  return [
2347
2364
  `🧠 **Context Saved**`,
@@ -3004,7 +3021,18 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
3004
3021
  lines.push(`After this session, call \`session_end\` to save a summary.`);
3005
3022
  lines.push('');
3006
3023
  } else {
3007
- lines.push(`📊 **Brain:** ${lessons.length} lessons · ${ctxCount} context entries`, '');
3024
+ const totalRecalls = lessons.reduce((s, l) => s + (l.recall_count ?? 0), 0);
3025
+ const savedMin = totalRecalls * 15;
3026
+ const savedStr = savedMin >= 60
3027
+ ? `~${(savedMin / 60).toFixed(1)}h saved`
3028
+ : savedMin > 0 ? `~${savedMin}min saved` : '';
3029
+ const statsLine = [
3030
+ `${lessons.length} lessons`,
3031
+ ctxCount > 0 ? `${ctxCount} context entries` : '',
3032
+ totalRecalls > 0 ? `${totalRecalls} recalls` : '',
3033
+ savedStr,
3034
+ ].filter(Boolean).join(' · ');
3035
+ lines.push(`📊 **Brain:** ${statsLine}`, '');
3008
3036
  }
3009
3037
 
3010
3038
  // Focus-relevant lessons
@@ -3194,20 +3222,23 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
3194
3222
 
3195
3223
  type Lesson = { topic: string; what_worked: string; outcome: string; file_paths?: string[] };
3196
3224
  const relevant: string[] = [];
3197
- for (const k of lessonKeys) {
3198
- const raw = await redis.get(k);
3199
- if (!raw) continue;
3200
- const lesson = JSON.parse(raw) as Lesson;
3201
- // Match by file_paths stored in lesson OR by topic keywords matching file name
3202
- const topicWords = lesson.topic.toLowerCase().split(/[:\-_]/);
3203
- const fileMatches = changed_files.some(f => {
3204
- const fname = f.split('/').pop()?.replace(/\.[^.]+$/, '').toLowerCase() ?? '';
3205
- return topicWords.some(w => w.length > 3 && fname.includes(w))
3206
- || (lesson.file_paths ?? []).some(lf => f.includes(lf) || lf.includes(f));
3207
- });
3208
- if (fileMatches) {
3209
- const emoji = lesson.outcome === 'success' ? '✅' : '⚠️';
3210
- relevant.push(` ${emoji} \`${lesson.topic}\` — ${lesson.what_worked.slice(0, 80)}`);
3225
+ if (lessonKeys.length > 0) {
3226
+ const values = await redis.mget(...lessonKeys);
3227
+ for (const raw of values) {
3228
+ if (!raw) continue;
3229
+ let lesson: Lesson;
3230
+ try { lesson = JSON.parse(raw) as Lesson; } catch { continue; }
3231
+ // Match by file_paths stored in lesson OR by topic keywords matching file name
3232
+ const topicWords = lesson.topic.toLowerCase().split(/[:\-_]/);
3233
+ const fileMatches = changed_files.some(f => {
3234
+ const fname = f.split('/').pop()?.replace(/\.[^.]+$/, '').toLowerCase() ?? '';
3235
+ return topicWords.some(w => w.length > 3 && fname.includes(w))
3236
+ || (lesson.file_paths ?? []).some(lf => f.includes(lf) || lf.includes(f));
3237
+ });
3238
+ if (fileMatches) {
3239
+ const emoji = lesson.outcome === 'success' ? '✅' : '⚠️';
3240
+ relevant.push(` ${emoji} \`${lesson.topic}\` — ${lesson.what_worked.slice(0, 80)}`);
3241
+ }
3211
3242
  }
3212
3243
  }
3213
3244
 
@@ -3248,7 +3279,7 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
3248
3279
  file_paths: file_paths ?? [],
3249
3280
  commands: commands ?? [],
3250
3281
  tags: [...(tags ?? []), 'team'],
3251
- timestamp: new Date().toISOString(),
3282
+ ts: new Date().toISOString(),
3252
3283
  recall_count: 0,
3253
3284
  version: 2,
3254
3285
  };
@@ -3287,10 +3318,12 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
3287
3318
  author?: string; tags?: string[];
3288
3319
  };
3289
3320
  let lessons: TeamLesson[] = [];
3290
- for (const k of lessonKeys) {
3291
- const raw = await redis.get(k);
3292
- if (!raw) continue;
3293
- try { lessons.push(JSON.parse(raw) as TeamLesson); } catch { /* skip */ }
3321
+ if (lessonKeys.length > 0) {
3322
+ const values = await redis.mget(...lessonKeys);
3323
+ for (const raw of values) {
3324
+ if (!raw) continue;
3325
+ try { lessons.push(JSON.parse(raw) as TeamLesson); } catch { /* skip */ }
3326
+ }
3294
3327
  }
3295
3328
 
3296
3329
  // Filter