@cachly-dev/mcp-server 0.8.2 → 0.9.0

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/index.js CHANGED
@@ -47,7 +47,7 @@ import { Redis } from 'ioredis';
47
47
  const API_URL = process.env.CACHLY_API_URL ?? 'https://api.cachly.dev';
48
48
  const JWT = process.env.CACHLY_JWT ?? '';
49
49
  const EMBED_MODEL = process.env.CACHLY_EMBED_MODEL ?? '';
50
- const CURRENT_VERSION = '0.8.2';
50
+ const CURRENT_VERSION = '0.8.3';
51
51
  /**
52
52
  * ┌──────────────────────────────────────────────────────────────────────┐
53
53
  * │ EMBEDDING PROVIDER — pluggable, client-side first │
@@ -3465,6 +3465,60 @@ const TOOLS = [
3465
3465
  required: [],
3466
3466
  },
3467
3467
  },
3468
+ // ── v0.9 — compact_recover ─────────────────────────────────────────────────
3469
+ {
3470
+ name: 'compact_recover',
3471
+ description: 'FAST context recovery after Claude/Cursor/Copilot context window compaction. ' +
3472
+ 'Call this as the FIRST tool call after any context limit hit or window restart — before reading any files. ' +
3473
+ 'Returns: last checkpoint (task + files + next_step), session handoff if any, last 5 recalled lessons, open TODOs. ' +
3474
+ 'Unlike session_start, this is instant — no full brain scan, no streak logic, just the critical recovery data. ' +
3475
+ 'Typical use in CLAUDE.md: "If context is compacted, call compact_recover immediately."',
3476
+ inputSchema: {
3477
+ type: 'object',
3478
+ properties: {
3479
+ instance_id: { type: 'string', description: 'UUID of the cache instance' },
3480
+ focus: {
3481
+ type: 'string',
3482
+ description: 'What you are trying to recover context for — used to surface relevant lessons (optional)',
3483
+ },
3484
+ },
3485
+ required: ['instance_id'],
3486
+ },
3487
+ },
3488
+ // ── v0.9 — brain_from_git ─────────────────────────────────────────────────
3489
+ {
3490
+ name: 'brain_from_git',
3491
+ description: 'Bootstrap the AI Brain from git history — reads commit messages and automatically stores lessons. ' +
3492
+ 'Scans the last N days of commits, extracts actionable knowledge (fixes, reverts, deploys, migrations), ' +
3493
+ 'and bulk-loads them into the Brain as lessons. ' +
3494
+ 'Run once to pre-load months of team knowledge before the first session_start. ' +
3495
+ 'Skips commits already in the Brain (safe to re-run). ' +
3496
+ 'Example: brain_from_git(instance_id="...", workspace_path="/home/you/project", days=180) ' +
3497
+ '→ "📊 Scanned 342 commits · ✅ 28 lessons stored · ❌ 9 failure lessons · 0 duplicates skipped"',
3498
+ inputSchema: {
3499
+ type: 'object',
3500
+ properties: {
3501
+ instance_id: { type: 'string', description: 'UUID of the cache instance' },
3502
+ workspace_path: {
3503
+ type: 'string',
3504
+ description: 'Absolute path to the git repository root (e.g. "/home/you/myproject")',
3505
+ },
3506
+ days: {
3507
+ type: 'number',
3508
+ description: 'How many days of history to scan (default: 180, max: 365)',
3509
+ },
3510
+ max_commits: {
3511
+ type: 'number',
3512
+ description: 'Maximum number of commits to process (default: 200, max: 500)',
3513
+ },
3514
+ author: {
3515
+ type: 'string',
3516
+ description: 'Filter to commits by this git author email or name (optional — omit to scan all authors)',
3517
+ },
3518
+ },
3519
+ required: ['instance_id', 'workspace_path'],
3520
+ },
3521
+ },
3468
3522
  ];
3469
3523
  // ── Handlers ──────────────────────────────────────────────────────────────────
3470
3524
  function formatInstance(inst) {
@@ -7214,6 +7268,173 @@ smart_recall(
7214
7268
  lines.push(`---`, `**Confirm** (if this helped you): \`syndicate(topic="${res.results[0]?.topic ?? '...'}", what_worked="...")\` → auto-deduped, trust +1`, `**All trending:** \`syndicate_trending(limit=50)\` | **Search:** \`syndicate_search(q="...")\``);
7215
7269
  return lines.filter(l => l !== '').join('\n');
7216
7270
  }
7271
+ // ── v0.9 compact_recover — fast context recovery ──────────────────────────
7272
+ case 'compact_recover': {
7273
+ const { instance_id, focus = '' } = args;
7274
+ const redis = await getConnection(instance_id);
7275
+ const lines = ['⚡ **Context Recovered**', ''];
7276
+ // 1. Latest checkpoint
7277
+ const checkpointRaw = await redis.get('cachly:session:checkpoint');
7278
+ if (checkpointRaw) {
7279
+ try {
7280
+ const cp = JSON.parse(checkpointRaw);
7281
+ const ago = Math.round((Date.now() - new Date(cp.ts).getTime()) / 60000);
7282
+ const agoStr = ago < 60 ? `${ago}m ago` : `${Math.round(ago / 60)}h ago`;
7283
+ lines.push(`📌 **Last checkpoint** (${agoStr}): ${cp.task}`);
7284
+ if (cp.files_touched && cp.files_touched.length > 0) {
7285
+ lines.push(` Files: ${cp.files_touched.slice(0, 5).join(', ')}${cp.files_touched.length > 5 ? '…' : ''}`);
7286
+ }
7287
+ if (cp.next_step)
7288
+ lines.push(`📍 **Resume with:** ${cp.next_step}`);
7289
+ lines.push('');
7290
+ }
7291
+ catch { /* ignore corrupt */ }
7292
+ }
7293
+ // 2. Handoff (if any, from a different window)
7294
+ const handoffRaw = await redis.get('cachly:session:handoff');
7295
+ if (handoffRaw) {
7296
+ try {
7297
+ const hf = JSON.parse(handoffRaw);
7298
+ const remaining = hf.remaining_tasks ?? [];
7299
+ if (remaining.length > 0) {
7300
+ lines.push(`⏳ **Remaining tasks:**`);
7301
+ remaining.slice(0, 5).forEach(t => lines.push(` - ${t}`));
7302
+ if (remaining.length > 5)
7303
+ lines.push(` …and ${remaining.length - 5} more`);
7304
+ lines.push('');
7305
+ }
7306
+ if (hf.blocked_on) {
7307
+ lines.push(`🚫 **Blocked on:** ${hf.blocked_on}`, '');
7308
+ }
7309
+ }
7310
+ catch { /* ignore */ }
7311
+ }
7312
+ // 3. Top recently recalled lessons (BM25 if focus, else recency)
7313
+ const lessonKeys = [];
7314
+ const lStream = redis.scanStream({ match: 'cachly:lesson:best:*', count: 200 });
7315
+ await new Promise((resolve, reject) => {
7316
+ lStream.on('data', (batch) => lessonKeys.push(...batch));
7317
+ lStream.on('end', resolve);
7318
+ lStream.on('error', reject);
7319
+ });
7320
+ const allLessons = [];
7321
+ for (const k of lessonKeys) {
7322
+ const raw = await redis.get(k);
7323
+ if (!raw)
7324
+ continue;
7325
+ try {
7326
+ allLessons.push(JSON.parse(raw));
7327
+ }
7328
+ catch { /* skip */ }
7329
+ }
7330
+ const focusTerms = focus.toLowerCase().split(/\s+/).filter(Boolean);
7331
+ let topLessons = focusTerms.length > 0
7332
+ ? allLessons.filter(l => focusTerms.some(t => l.topic.toLowerCase().includes(t) || (l.tags ?? []).some((tag) => tag.toLowerCase().includes(t)))).slice(0, 5)
7333
+ : allLessons.sort((a, b) => (b.recall_count ?? 0) - (a.recall_count ?? 0)).slice(0, 5);
7334
+ if (topLessons.length > 0) {
7335
+ lines.push(`🧠 **Top lessons${focus ? ` for "${focus}"` : ''}:**`);
7336
+ for (const l of topLessons) {
7337
+ const icon = l.outcome === 'success' ? '✅' : l.outcome === 'failure' ? '❌' : '⚠️';
7338
+ lines.push(` ${icon} \`${l.topic}\` — ${l.what_worked.slice(0, 120)}${l.what_worked.length > 120 ? '…' : ''}`);
7339
+ }
7340
+ lines.push('');
7341
+ }
7342
+ lines.push(`💡 Call \`session_start\` for the full briefing, or resume work directly.`);
7343
+ return lines.filter(l => l !== '').join('\n');
7344
+ }
7345
+ // ── v0.9 brain_from_git — bulk-load git history as lessons ───────────────
7346
+ case 'brain_from_git': {
7347
+ const { instance_id, workspace_path, days = 180, max_commits = 200, author = '', } = args;
7348
+ const redis = await getConnection(instance_id);
7349
+ const { execSync } = await import('node:child_process');
7350
+ const clampedDays = Math.min(Math.max(1, days), 365);
7351
+ const clampedMax = Math.min(Math.max(1, max_commits), 500);
7352
+ const authorArg = author ? `--author="${author}"` : '';
7353
+ let gitRaw = '';
7354
+ try {
7355
+ gitRaw = execSync(`git -C "${workspace_path}" log --since="${clampedDays} days ago" ${authorArg} --oneline --format="%H|||%s|||%ai|||%ae" -n ${clampedMax}`, { timeout: 15000, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
7356
+ }
7357
+ catch {
7358
+ return `❌ **brain_from_git failed**: Could not read git log from \`${workspace_path}\`.\nMake sure the path exists and is a git repository.`;
7359
+ }
7360
+ if (!gitRaw) {
7361
+ return `📭 **No commits found** in the last ${clampedDays} days${author ? ` by "${author}"` : ''}.\nTry increasing \`days\` or removing the \`author\` filter.`;
7362
+ }
7363
+ const commitLines = gitRaw.split('\n');
7364
+ const now = new Date();
7365
+ const actionRe = /\b(fix|add|remove|refactor|migrate|update|resolve|implement|improve|optimize|configure|create|delete|disable|enable|debug|patch|upgrade|rewrite|deploy|feat|revert|break|merge|bump)\b/i;
7366
+ const failRe = /\b(revert|broken|broke|breaking|rollback|undo|hotfix|crash|oops|wrong|mistake|bad)\b/i;
7367
+ let stored = 0;
7368
+ let skipped = 0;
7369
+ let failures = 0;
7370
+ const topLessons = [];
7371
+ for (const line of commitLines) {
7372
+ const parts = line.split('|||');
7373
+ if (parts.length < 2)
7374
+ continue;
7375
+ const [hash, msg, dateStr] = parts;
7376
+ if (!msg || !actionRe.test(msg))
7377
+ continue;
7378
+ const isFail = failRe.test(msg);
7379
+ const cleanMsg = msg
7380
+ .replace(/^(fix|feat|chore|docs|test|ci|perf|refactor|build|revert|hotfix|bump)[:(\s!]+/i, '')
7381
+ .trim();
7382
+ if (!cleanMsg || cleanMsg.length < 5)
7383
+ continue;
7384
+ const slug = cleanMsg
7385
+ .toLowerCase()
7386
+ .replace(/[^a-z0-9]+/g, '-')
7387
+ .replace(/^-+|-+$/g, '')
7388
+ .slice(0, 40);
7389
+ if (!slug)
7390
+ continue;
7391
+ const topic = `git:${slug}`;
7392
+ const key = `cachly:lesson:best:${topic}`;
7393
+ const existing = await redis.get(key);
7394
+ if (existing) {
7395
+ skipped++;
7396
+ continue;
7397
+ }
7398
+ const lesson = {
7399
+ topic,
7400
+ outcome: isFail ? 'failure' : 'success',
7401
+ what_worked: isFail ? '' : cleanMsg.slice(0, 300),
7402
+ what_failed: isFail ? cleanMsg.slice(0, 300) : undefined,
7403
+ context: `Auto-learned from git commit ${(hash ?? '').slice(0, 7)}${dateStr ? ` at ${dateStr}` : ''} in ${workspace_path}${author ? ` (author: ${author})` : ''}`,
7404
+ severity: 'minor',
7405
+ ts: dateStr ? new Date(dateStr).toISOString() : now.toISOString(),
7406
+ recall_count: 0,
7407
+ auto_learned: true,
7408
+ source: 'brain_from_git',
7409
+ version: 3,
7410
+ };
7411
+ await redis.set(key, JSON.stringify(lesson));
7412
+ await redis.expire(key, 90 * 86400); // 90-day TTL for git-sourced lessons
7413
+ stored++;
7414
+ if (isFail)
7415
+ failures++;
7416
+ if (topLessons.length < 5)
7417
+ topLessons.push(` ${isFail ? '❌' : '✅'} \`${topic}\` — ${cleanMsg.slice(0, 80)}`);
7418
+ }
7419
+ const lines = [
7420
+ `🧠 **Brain bootstrapped from git history**`,
7421
+ ``,
7422
+ `📊 **Scanned:** ${commitLines.filter(l => l.trim()).length} commits over last ${clampedDays} days`,
7423
+ `✅ **Success lessons stored:** ${stored - failures}`,
7424
+ `❌ **Failure/revert lessons:** ${failures}`,
7425
+ skipped > 0 ? `⏭️ **Already in brain (skipped):** ${skipped}` : '',
7426
+ ``,
7427
+ ];
7428
+ if (topLessons.length > 0) {
7429
+ lines.push(`**Top lessons extracted:**`);
7430
+ lines.push(...topLessons);
7431
+ if (stored > 5)
7432
+ lines.push(` …and ${stored - 5} more`);
7433
+ lines.push('');
7434
+ }
7435
+ lines.push(`💡 **Next step:** Call \`session_start(focus="...")\` to see these lessons in your briefing.`, `🔄 **Re-run anytime** — duplicates are safely skipped.`);
7436
+ return lines.filter(l => l !== '').join('\n');
7437
+ }
7217
7438
  default:
7218
7439
  return null;
7219
7440
  }
@@ -7475,9 +7696,17 @@ if (process.argv[2] === 'setup') {
7475
7696
  const { existsSync } = await import('node:fs');
7476
7697
  const { resolve, dirname } = await import('node:path');
7477
7698
  const { createInterface } = await import('node:readline');
7699
+ // --yes / -y → non-interactive mode (skips all prompts, picks defaults)
7700
+ const nonInteractive = process.argv.includes('--yes') || process.argv.includes('-y');
7478
7701
  const rl = createInterface({ input: process.stdin, output: process.stdout });
7479
- const ask = (q) => new Promise(res => rl.question(q, ans => res(ans.trim())));
7480
- console.log('\n🧠 cachly AI Brain — Interactive Setup');
7702
+ const ask = (q, defaultVal = '') => {
7703
+ if (nonInteractive) {
7704
+ console.log(`${q}${defaultVal}`);
7705
+ return Promise.resolve(defaultVal);
7706
+ }
7707
+ return new Promise(res => rl.question(q, ans => res(ans.trim() || defaultVal)));
7708
+ };
7709
+ console.log('\n🧠 cachly AI Brain — Setup');
7481
7710
  console.log('────────────────────────────────────────\n');
7482
7711
  // ── Step 1: Authenticate via OAuth Device Flow ────────────────────────────
7483
7712
  let token = process.env.CACHLY_JWT ?? '';
@@ -7672,13 +7901,11 @@ if (process.argv[2] === 'setup') {
7672
7901
  process.exit(1);
7673
7902
  }
7674
7903
  }
7675
- let instance = instances[0];
7904
+ // Auto-pick the most recently created running instance — no prompt.
7905
+ const instance = instances[0];
7676
7906
  if (instances.length > 1) {
7677
- console.log('Step 2: Pick your Brain instance:\n');
7678
- instances.forEach((inst, idx) => console.log(` ${idx + 1}. ${inst.name.padEnd(24)} ${inst.tier.padEnd(10)} ${inst.region}`));
7679
- const raw = await ask(`\n Pick [1-${instances.length}] (Enter = 1): `);
7680
- const idx = parseInt(raw || '1', 10) - 1;
7681
- instance = instances[Math.max(0, Math.min(instances.length - 1, idx))];
7907
+ console.log(`ℹ️ Multiple instances found — using most recent: ${instance.name}`);
7908
+ console.log(` (Run with --instance-id <id> to use a different one)\n`);
7682
7909
  }
7683
7910
  console.log(`✓ Instance: ${instance.name} (${instance.id.slice(0, 8)}…)\n`);
7684
7911
  // ── Step 3: Detect editors ────────────────────────────────────────────────
@@ -7711,32 +7938,10 @@ if (process.argv[2] === 'setup') {
7711
7938
  if (existsSync(zedDir))
7712
7939
  detected.push('zed');
7713
7940
  const editorLabel = (e) => ({ claude: 'Claude Code', cursor: 'Cursor', windsurf: 'Windsurf', copilot: 'GitHub Copilot', continue: 'Continue.dev', cline: 'Cline (VSCode)', zed: 'Zed' })[e] ?? e;
7714
- console.log(`Step 3: Detected editors: ${detected.map(editorLabel).join(', ')}`);
7715
- const editorsRaw = await ask(` Configure for which? [all / ${detected.join('/')}] (Enter = all): `);
7716
- const editorsToSetup = !editorsRaw || editorsRaw.toLowerCase() === 'all'
7717
- ? detected
7718
- : editorsRaw.split(/[,\s]+/).map(s => s.toLowerCase()).filter(s => EDITOR_FILES[s]);
7719
- console.log('');
7720
- // ── Step 4: Email opt-in ──────────────────────────────────────────────────
7721
- console.log('Step 4: Stay in the loop (optional)');
7722
- const email = await ask(' Email for release notes & tips? [Enter to skip]: ');
7723
- if (email && email.includes('@')) {
7724
- try {
7725
- await fetch(`${API_URL}/api/newsletter/subscribe`, {
7726
- method: 'POST', headers: { 'Content-Type': 'application/json' },
7727
- body: JSON.stringify({ email: email.trim().toLowerCase(), source: 'mcp-setup' }),
7728
- signal: AbortSignal.timeout(5000),
7729
- });
7730
- console.log('✅ Subscribed — you\'ll only hear from us when it matters.\n');
7731
- }
7732
- catch {
7733
- console.log('⚠️ Could not subscribe (no internet?). Continuing...\n');
7734
- }
7735
- }
7736
- else {
7737
- console.log(' Skipped.\n');
7738
- }
7739
- // ── Step 5: Write editor configs ──────────────────────────────────────────
7941
+ // Auto-configure all detected editors — no prompt needed.
7942
+ const editorsToSetup = detected;
7943
+ console.log(`Step 3: Configuring for: ${editorsToSetup.map(editorLabel).join(', ')}\n`);
7944
+ // ── Step 4: Write editor configs ──────────────────────────────────────────
7740
7945
  for (const editor of editorsToSetup) {
7741
7946
  const configFile = EDITOR_FILES[editor] ?? '.mcp.json';
7742
7947
  const configPath = resolve(cwd, configFile);
@@ -7744,14 +7949,11 @@ if (process.argv[2] === 'setup') {
7744
7949
  await writeFile(configPath, buildMcpConfig(token, instance.id, editor), 'utf-8');
7745
7950
  console.log(`✅ Written: ${configFile}`);
7746
7951
  }
7747
- // ── Step 6: CLAUDE.md (always — idempotent) ───────────────────────────────
7952
+ // ── Step 5: CLAUDE.md (always — idempotent) ───────────────────────────────
7748
7953
  const mdResult = await writeClaudeMd(cwd, instance.id);
7749
7954
  const mdLabel = mdResult === 'updated' ? '✅ Updated' : mdResult === 'appended' ? '✅ Appended to' : '✅ Written';
7750
- console.log(`${mdLabel}: CLAUDE.md`);
7751
- rl.close();
7752
- console.log(`\n🧠 Done! Restart your editor — the \`cachly\` MCP tools will appear.`);
7753
- console.log(` Your AI now has persistent memory across every session.\n`);
7754
- // ── Step 7: Show first session_start preview (Aha moment) ────────────────
7955
+ console.log(`${mdLabel}: CLAUDE.md\n`);
7956
+ // ── Step 6: Show Brain health (Aha moment) ────────────────────────────────
7755
7957
  // Fetch brain health from the API to show the user what their agent will see.
7756
7958
  process.stdout.write('\n⏳ Fetching your Brain health preview...');
7757
7959
  try {
@@ -7792,8 +7994,25 @@ if (process.argv[2] === 'setup') {
7792
7994
  catch {
7793
7995
  console.log(' (skipped)');
7794
7996
  }
7795
- console.log(`\n Your AI will call session_start automatically when it uses the first tool.`);
7796
- console.log(` To see your Brain at any time: https://cachly.dev/instances/${instance.id}\n`);
7997
+ console.log(`\n🧠 Done! Restart your editor — the \`cachly\` MCP tools will appear.`);
7998
+ console.log(` Your AI now has persistent memory across every session.\n`);
7999
+ console.log(` Dashboard: https://cachly.dev/instances/${instance.id}\n`);
8000
+ // ── Step 7: Email opt-in (non-blocking — at the very end) ────────────────
8001
+ if (!nonInteractive) {
8002
+ const email = await ask(' 📬 Stay in the loop? Email for release notes [Enter to skip]: ');
8003
+ if (email && email.includes('@')) {
8004
+ try {
8005
+ await fetch(`${API_URL}/api/newsletter/subscribe`, {
8006
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
8007
+ body: JSON.stringify({ email: email.trim().toLowerCase(), source: 'mcp-setup' }),
8008
+ signal: AbortSignal.timeout(5000),
8009
+ });
8010
+ console.log(' ✅ Subscribed — you\'ll only hear from us when it matters.\n');
8011
+ }
8012
+ catch { /* fire and forget */ }
8013
+ }
8014
+ }
8015
+ rl.close();
7797
8016
  process.exit(0);
7798
8017
  }
7799
8018
  // ── CLI: cachly index <dir> ───────────────────────────────────────────────────