@cachly-dev/mcp-server 0.8.3 → 0.9.1

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
@@ -2976,6 +2976,21 @@ const TOOLS = [
2976
2976
  required: ['instance_id'],
2977
2977
  },
2978
2978
  },
2979
+ // ── crystal_view — inspect current Memory Crystal ──────────────────────
2980
+ {
2981
+ name: 'crystal_view',
2982
+ description: 'View the current Memory Crystal — the distilled snapshot of everything your AI brain has learned. ' +
2983
+ 'Shows all top patterns grouped by category, lesson count, crystal age, and when to refresh. ' +
2984
+ 'A crystal is created by memory_crystalize and injected into every session_start automatically. ' +
2985
+ 'Call this to review what institutional knowledge is preserved, or to decide if a refresh is due.',
2986
+ inputSchema: {
2987
+ type: 'object',
2988
+ properties: {
2989
+ instance_id: { type: 'string', description: 'UUID of the cache instance' },
2990
+ },
2991
+ required: ['instance_id'],
2992
+ },
2993
+ },
2979
2994
  // ── Roadmap — Persistent project plan tracker ───────────────────────────
2980
2995
  {
2981
2996
  name: 'roadmap_add',
@@ -3465,6 +3480,60 @@ const TOOLS = [
3465
3480
  required: [],
3466
3481
  },
3467
3482
  },
3483
+ // ── v0.9 — compact_recover ─────────────────────────────────────────────────
3484
+ {
3485
+ name: 'compact_recover',
3486
+ description: 'FAST context recovery after Claude/Cursor/Copilot context window compaction. ' +
3487
+ 'Call this as the FIRST tool call after any context limit hit or window restart — before reading any files. ' +
3488
+ 'Returns: last checkpoint (task + files + next_step), session handoff if any, last 5 recalled lessons, open TODOs. ' +
3489
+ 'Unlike session_start, this is instant — no full brain scan, no streak logic, just the critical recovery data. ' +
3490
+ 'Typical use in CLAUDE.md: "If context is compacted, call compact_recover immediately."',
3491
+ inputSchema: {
3492
+ type: 'object',
3493
+ properties: {
3494
+ instance_id: { type: 'string', description: 'UUID of the cache instance' },
3495
+ focus: {
3496
+ type: 'string',
3497
+ description: 'What you are trying to recover context for — used to surface relevant lessons (optional)',
3498
+ },
3499
+ },
3500
+ required: ['instance_id'],
3501
+ },
3502
+ },
3503
+ // ── v0.9 — brain_from_git ─────────────────────────────────────────────────
3504
+ {
3505
+ name: 'brain_from_git',
3506
+ description: 'Bootstrap the AI Brain from git history — reads commit messages and automatically stores lessons. ' +
3507
+ 'Scans the last N days of commits, extracts actionable knowledge (fixes, reverts, deploys, migrations), ' +
3508
+ 'and bulk-loads them into the Brain as lessons. ' +
3509
+ 'Run once to pre-load months of team knowledge before the first session_start. ' +
3510
+ 'Skips commits already in the Brain (safe to re-run). ' +
3511
+ 'Example: brain_from_git(instance_id="...", workspace_path="/home/you/project", days=180) ' +
3512
+ '→ "📊 Scanned 342 commits · ✅ 28 lessons stored · ❌ 9 failure lessons · 0 duplicates skipped"',
3513
+ inputSchema: {
3514
+ type: 'object',
3515
+ properties: {
3516
+ instance_id: { type: 'string', description: 'UUID of the cache instance' },
3517
+ workspace_path: {
3518
+ type: 'string',
3519
+ description: 'Absolute path to the git repository root (e.g. "/home/you/myproject")',
3520
+ },
3521
+ days: {
3522
+ type: 'number',
3523
+ description: 'How many days of history to scan (default: 180, max: 365)',
3524
+ },
3525
+ max_commits: {
3526
+ type: 'number',
3527
+ description: 'Maximum number of commits to process (default: 200, max: 500)',
3528
+ },
3529
+ author: {
3530
+ type: 'string',
3531
+ description: 'Filter to commits by this git author email or name (optional — omit to scan all authors)',
3532
+ },
3533
+ },
3534
+ required: ['instance_id', 'workspace_path'],
3535
+ },
3536
+ },
3468
3537
  ];
3469
3538
  // ── Handlers ──────────────────────────────────────────────────────────────────
3470
3539
  function formatInstance(inst) {
@@ -5186,6 +5255,22 @@ async function handleTool(name, args) {
5186
5255
  }
5187
5256
  catch { /* git not available or not a repo — silent skip */ }
5188
5257
  }
5258
+ // ── Crystal age check — remind if > 30 days old or missing ──────────────
5259
+ let crystalReminder = '';
5260
+ try {
5261
+ const crystalRaw = await redis.get('cachly:crystal:latest');
5262
+ if (!crystalRaw) {
5263
+ crystalReminder = `💎 **Memory Crystal:** None exists yet. Run \`memory_crystalize(instance_id="${instance_id}")\` to distill your brain into a permanent snapshot — injected into every future session automatically.`;
5264
+ }
5265
+ else {
5266
+ const crystal = JSON.parse(crystalRaw);
5267
+ const crystalAgeDays = Math.round((Date.now() - new Date(crystal.ts).getTime()) / 86_400_000);
5268
+ if (crystalAgeDays >= 30) {
5269
+ crystalReminder = `💎 **Memory Crystal is ${crystalAgeDays} days old** (${crystal.label ?? 'unnamed'}). Run \`memory_crystalize(instance_id="${instance_id}")\` to refresh it with your latest ${crystal.lesson_count ?? '?'} lessons.`;
5270
+ }
5271
+ }
5272
+ }
5273
+ catch { /* non-critical */ }
5189
5274
  const durationStr = durationMin !== undefined ? ` · ${durationMin} min` : '';
5190
5275
  return [
5191
5276
  `✅ **Session saved**${durationStr}`,
@@ -5195,6 +5280,7 @@ async function handleTool(name, args) {
5195
5280
  lessons_learned !== undefined ? `🧠 **Lessons stored:** ${lessons_learned}` : '',
5196
5281
  autoLearned.length > 0 ? `🤖 **Auto-learned:** ${autoLearned.length} lessons extracted from summary (${autoLearned.slice(0, 3).map(t => `\`${t}\``).join(', ')}${autoLearned.length > 3 ? '…' : ''})` : '',
5197
5282
  ambientLearned.length > 0 ? `🌿 **Ambient git learning:** ${ambientLearned.length} commit${ambientLearned.length > 1 ? 's' : ''} auto-learned (${ambientLearned.slice(0, 3).map(t => `\`${t}\``).join(', ')}${ambientLearned.length > 3 ? '…' : ''})` : '',
5283
+ crystalReminder,
5198
5284
  ``,
5199
5285
  `💡 Next session: \`session_start(focus="...")\` to see this summary.`,
5200
5286
  ].filter(l => l !== '').join('\n');
@@ -5583,6 +5669,52 @@ async function handleTool(name, args) {
5583
5669
  ];
5584
5670
  return lines.join('\n');
5585
5671
  }
5672
+ // ── crystal_view — show current Memory Crystal ───────────────────────────
5673
+ case 'crystal_view': {
5674
+ const { instance_id } = args;
5675
+ const redis = await getConnection(instance_id);
5676
+ const crystalRaw = await redis.get('cachly:crystal:latest');
5677
+ if (!crystalRaw) {
5678
+ return [
5679
+ `💎 **No Memory Crystal yet**`,
5680
+ ``,
5681
+ `Your brain has no crystal — all knowledge lives in raw lessons only.`,
5682
+ ``,
5683
+ `Run \`memory_crystalize(instance_id="${instance_id}")\` to:`,
5684
+ ` • Distill everything learned into a permanent, compact snapshot`,
5685
+ ` • Inject it into every future \`session_start\` automatically`,
5686
+ ` • Preserve team knowledge even after lesson TTLs expire`,
5687
+ ``,
5688
+ `💡 Recommended: run monthly or after any major milestone.`,
5689
+ ].join('\n');
5690
+ }
5691
+ const crystal = JSON.parse(crystalRaw);
5692
+ const ageDays = Math.round((Date.now() - new Date(crystal.ts).getTime()) / 86_400_000);
5693
+ const ageStr = ageDays === 0 ? 'today' : ageDays === 1 ? '1 day ago' : `${ageDays} days ago`;
5694
+ const freshness = ageDays < 14 ? '🟢 Fresh' : ageDays < 30 ? '🟡 Aging' : '🔴 Stale — refresh recommended';
5695
+ const lines = [
5696
+ `💎 **Memory Crystal: ${crystal.label}**`,
5697
+ ``,
5698
+ `📅 Created: **${ageStr}** · Freshness: ${freshness}`,
5699
+ `📊 Compressed: **${crystal.session_count} sessions** + **${crystal.lesson_count} lessons** → ${crystal.top_patterns.length} patterns`,
5700
+ `🗂️ Categories: ${crystal.categories.slice(0, 10).join(', ')}${crystal.categories.length > 10 ? `… +${crystal.categories.length - 10}` : ''}`,
5701
+ ``,
5702
+ `**Top patterns by category:**`,
5703
+ ``,
5704
+ ];
5705
+ for (const p of crystal.top_patterns) {
5706
+ lines.push(`**${p.category}** (${p.count} lesson${p.count > 1 ? 's' : ''})`);
5707
+ lines.push(` → ${p.insight.slice(0, 150)}${p.insight.length > 150 ? '…' : ''}`);
5708
+ lines.push('');
5709
+ }
5710
+ if (ageDays >= 30) {
5711
+ lines.push(`---`, `⚠️ Crystal is ${ageDays} days old. Run \`memory_crystalize(instance_id="${instance_id}")\` to refresh with your latest lessons.`);
5712
+ }
5713
+ else {
5714
+ lines.push(`---`, `💡 This crystal is injected automatically into every \`session_start\`. Refresh monthly with \`memory_crystalize\`.`);
5715
+ }
5716
+ return lines.filter(l => l !== undefined).join('\n');
5717
+ }
5586
5718
  case 'brain_doctor': {
5587
5719
  const { instance_id, workspace_path: drWorkspacePath = '' } = args;
5588
5720
  const redis = await getConnection(instance_id);
@@ -6982,9 +7114,11 @@ async function handleBulkLockStream(name, args) {
6982
7114
  > Sessions are fully automatic. The Brain starts when you call any tool and saves a summary when you close.
6983
7115
 
6984
7116
  ### MANDATORY hooks (always fire, no exceptions):
7117
+ - **Context compacted** → call \`compact_recover(instance_id="${instance_id}", focus="<current task>")\` FIRST
6985
7118
  - **Window opens** → call \`session_start(instance_id="${instance_id}", focus="<today's goal>")\`
6986
7119
  - **Bug fixed / deployed / discovered** → call \`learn_from_attempts(instance_id="${instance_id}", topic="fix:<area>", outcome="success", what_worked="...")\`
6987
7120
  - **Window closes** → call \`session_end(instance_id="${instance_id}", summary="...", files_changed=[...])\`
7121
+ - **Monthly** → call \`memory_crystalize(instance_id="${instance_id}")\` to distill knowledge into a permanent Crystal
6988
7122
  `;
6989
7123
  const full = `## Cachly AI Brain — Autopilot for ${project_name}
6990
7124
 
@@ -7044,18 +7178,40 @@ smart_recall(
7044
7178
 
7045
7179
  ---
7046
7180
 
7047
- ## ⚡ Cognitive Tools (v0.6)
7181
+ ## ⚡ Context Recovery (MANDATORY after compaction)
7182
+
7183
+ **If your context window was compacted**, call this FIRST — before reading any file:
7184
+ \`\`\`
7185
+ compact_recover(
7186
+ instance_id = "${instance_id}",
7187
+ focus = "<what you were working on>",
7188
+ )
7189
+ \`\`\`
7190
+ Returns: last checkpoint, pending tasks, top lessons. Instant — no full brain scan.
7191
+
7192
+ ---
7193
+
7194
+ ## 🔁 Scheduled Maintenance (run on schedule)
7195
+
7196
+ | When | Call |
7197
+ |---|---|
7198
+ | **Monthly** | \`memory_crystalize(instance_id="${instance_id}")\` — distill brain into a permanent snapshot |
7199
+ | Weekly review | \`brain_diff(instance_id="${instance_id}", since="7d")\` — see what your AI learned this week |
7200
+ | Before big refactor | \`knowledge_decay(instance_id="${instance_id}")\` — see which lessons to still trust |
7201
+ | After major milestone | \`memory_crystalize(instance_id="${instance_id}", label="v2 launch")\` — preserve milestone knowledge |
7202
+
7203
+ ## 🧠 Cognitive Tools (use anytime)
7048
7204
 
7049
7205
  | When | Call |
7050
7206
  |---|---|
7051
7207
  | Brain feeling cluttered | \`memory_consolidate(instance_id="${instance_id}")\` |
7052
- | Weekly review | \`brain_diff(instance_id="${instance_id}", since="7d")\` |
7053
7208
  | Weird bug, no idea why | \`causal_trace(instance_id="${instance_id}", problem="<symptom>")\` |
7054
- | Before big refactor | \`knowledge_decay(instance_id="${instance_id}")\` |
7209
+ | Unknown error | \`syndicate_search(q="<error message>")\` — global community solutions |
7210
+ | Unknown error that you solved | \`syndicate(instance_id="${instance_id}", topic="fix:<area>", what_worked="...")\` |
7055
7211
 
7056
7212
  ---
7057
7213
 
7058
- *Cachly v0.6 · Generated ${new Date().toISOString().slice(0, 10)}*
7214
+ *Cachly v0.9 · Generated ${new Date().toISOString().slice(0, 10)}*
7059
7215
  `;
7060
7216
  const content = style === 'minimal' ? minimal : full;
7061
7217
  const filename = editor === 'copilot'
@@ -7214,6 +7370,173 @@ smart_recall(
7214
7370
  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
7371
  return lines.filter(l => l !== '').join('\n');
7216
7372
  }
7373
+ // ── v0.9 compact_recover — fast context recovery ──────────────────────────
7374
+ case 'compact_recover': {
7375
+ const { instance_id, focus = '' } = args;
7376
+ const redis = await getConnection(instance_id);
7377
+ const lines = ['⚡ **Context Recovered**', ''];
7378
+ // 1. Latest checkpoint
7379
+ const checkpointRaw = await redis.get('cachly:session:checkpoint');
7380
+ if (checkpointRaw) {
7381
+ try {
7382
+ const cp = JSON.parse(checkpointRaw);
7383
+ const ago = Math.round((Date.now() - new Date(cp.ts).getTime()) / 60000);
7384
+ const agoStr = ago < 60 ? `${ago}m ago` : `${Math.round(ago / 60)}h ago`;
7385
+ lines.push(`📌 **Last checkpoint** (${agoStr}): ${cp.task}`);
7386
+ if (cp.files_touched && cp.files_touched.length > 0) {
7387
+ lines.push(` Files: ${cp.files_touched.slice(0, 5).join(', ')}${cp.files_touched.length > 5 ? '…' : ''}`);
7388
+ }
7389
+ if (cp.next_step)
7390
+ lines.push(`📍 **Resume with:** ${cp.next_step}`);
7391
+ lines.push('');
7392
+ }
7393
+ catch { /* ignore corrupt */ }
7394
+ }
7395
+ // 2. Handoff (if any, from a different window)
7396
+ const handoffRaw = await redis.get('cachly:session:handoff');
7397
+ if (handoffRaw) {
7398
+ try {
7399
+ const hf = JSON.parse(handoffRaw);
7400
+ const remaining = hf.remaining_tasks ?? [];
7401
+ if (remaining.length > 0) {
7402
+ lines.push(`⏳ **Remaining tasks:**`);
7403
+ remaining.slice(0, 5).forEach(t => lines.push(` - ${t}`));
7404
+ if (remaining.length > 5)
7405
+ lines.push(` …and ${remaining.length - 5} more`);
7406
+ lines.push('');
7407
+ }
7408
+ if (hf.blocked_on) {
7409
+ lines.push(`🚫 **Blocked on:** ${hf.blocked_on}`, '');
7410
+ }
7411
+ }
7412
+ catch { /* ignore */ }
7413
+ }
7414
+ // 3. Top recently recalled lessons (BM25 if focus, else recency)
7415
+ const lessonKeys = [];
7416
+ const lStream = redis.scanStream({ match: 'cachly:lesson:best:*', count: 200 });
7417
+ await new Promise((resolve, reject) => {
7418
+ lStream.on('data', (batch) => lessonKeys.push(...batch));
7419
+ lStream.on('end', resolve);
7420
+ lStream.on('error', reject);
7421
+ });
7422
+ const allLessons = [];
7423
+ for (const k of lessonKeys) {
7424
+ const raw = await redis.get(k);
7425
+ if (!raw)
7426
+ continue;
7427
+ try {
7428
+ allLessons.push(JSON.parse(raw));
7429
+ }
7430
+ catch { /* skip */ }
7431
+ }
7432
+ const focusTerms = focus.toLowerCase().split(/\s+/).filter(Boolean);
7433
+ let topLessons = focusTerms.length > 0
7434
+ ? allLessons.filter(l => focusTerms.some(t => l.topic.toLowerCase().includes(t) || (l.tags ?? []).some((tag) => tag.toLowerCase().includes(t)))).slice(0, 5)
7435
+ : allLessons.sort((a, b) => (b.recall_count ?? 0) - (a.recall_count ?? 0)).slice(0, 5);
7436
+ if (topLessons.length > 0) {
7437
+ lines.push(`🧠 **Top lessons${focus ? ` for "${focus}"` : ''}:**`);
7438
+ for (const l of topLessons) {
7439
+ const icon = l.outcome === 'success' ? '✅' : l.outcome === 'failure' ? '❌' : '⚠️';
7440
+ lines.push(` ${icon} \`${l.topic}\` — ${l.what_worked.slice(0, 120)}${l.what_worked.length > 120 ? '…' : ''}`);
7441
+ }
7442
+ lines.push('');
7443
+ }
7444
+ lines.push(`💡 Call \`session_start\` for the full briefing, or resume work directly.`);
7445
+ return lines.filter(l => l !== '').join('\n');
7446
+ }
7447
+ // ── v0.9 brain_from_git — bulk-load git history as lessons ───────────────
7448
+ case 'brain_from_git': {
7449
+ const { instance_id, workspace_path, days = 180, max_commits = 200, author = '', } = args;
7450
+ const redis = await getConnection(instance_id);
7451
+ const { execSync } = await import('node:child_process');
7452
+ const clampedDays = Math.min(Math.max(1, days), 365);
7453
+ const clampedMax = Math.min(Math.max(1, max_commits), 500);
7454
+ const authorArg = author ? `--author="${author}"` : '';
7455
+ let gitRaw = '';
7456
+ try {
7457
+ 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();
7458
+ }
7459
+ catch {
7460
+ return `❌ **brain_from_git failed**: Could not read git log from \`${workspace_path}\`.\nMake sure the path exists and is a git repository.`;
7461
+ }
7462
+ if (!gitRaw) {
7463
+ return `📭 **No commits found** in the last ${clampedDays} days${author ? ` by "${author}"` : ''}.\nTry increasing \`days\` or removing the \`author\` filter.`;
7464
+ }
7465
+ const commitLines = gitRaw.split('\n');
7466
+ const now = new Date();
7467
+ 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;
7468
+ const failRe = /\b(revert|broken|broke|breaking|rollback|undo|hotfix|crash|oops|wrong|mistake|bad)\b/i;
7469
+ let stored = 0;
7470
+ let skipped = 0;
7471
+ let failures = 0;
7472
+ const topLessons = [];
7473
+ for (const line of commitLines) {
7474
+ const parts = line.split('|||');
7475
+ if (parts.length < 2)
7476
+ continue;
7477
+ const [hash, msg, dateStr] = parts;
7478
+ if (!msg || !actionRe.test(msg))
7479
+ continue;
7480
+ const isFail = failRe.test(msg);
7481
+ const cleanMsg = msg
7482
+ .replace(/^(fix|feat|chore|docs|test|ci|perf|refactor|build|revert|hotfix|bump)[:(\s!]+/i, '')
7483
+ .trim();
7484
+ if (!cleanMsg || cleanMsg.length < 5)
7485
+ continue;
7486
+ const slug = cleanMsg
7487
+ .toLowerCase()
7488
+ .replace(/[^a-z0-9]+/g, '-')
7489
+ .replace(/^-+|-+$/g, '')
7490
+ .slice(0, 40);
7491
+ if (!slug)
7492
+ continue;
7493
+ const topic = `git:${slug}`;
7494
+ const key = `cachly:lesson:best:${topic}`;
7495
+ const existing = await redis.get(key);
7496
+ if (existing) {
7497
+ skipped++;
7498
+ continue;
7499
+ }
7500
+ const lesson = {
7501
+ topic,
7502
+ outcome: isFail ? 'failure' : 'success',
7503
+ what_worked: isFail ? '' : cleanMsg.slice(0, 300),
7504
+ what_failed: isFail ? cleanMsg.slice(0, 300) : undefined,
7505
+ context: `Auto-learned from git commit ${(hash ?? '').slice(0, 7)}${dateStr ? ` at ${dateStr}` : ''} in ${workspace_path}${author ? ` (author: ${author})` : ''}`,
7506
+ severity: 'minor',
7507
+ ts: dateStr ? new Date(dateStr).toISOString() : now.toISOString(),
7508
+ recall_count: 0,
7509
+ auto_learned: true,
7510
+ source: 'brain_from_git',
7511
+ version: 3,
7512
+ };
7513
+ await redis.set(key, JSON.stringify(lesson));
7514
+ await redis.expire(key, 90 * 86400); // 90-day TTL for git-sourced lessons
7515
+ stored++;
7516
+ if (isFail)
7517
+ failures++;
7518
+ if (topLessons.length < 5)
7519
+ topLessons.push(` ${isFail ? '❌' : '✅'} \`${topic}\` — ${cleanMsg.slice(0, 80)}`);
7520
+ }
7521
+ const lines = [
7522
+ `🧠 **Brain bootstrapped from git history**`,
7523
+ ``,
7524
+ `📊 **Scanned:** ${commitLines.filter(l => l.trim()).length} commits over last ${clampedDays} days`,
7525
+ `✅ **Success lessons stored:** ${stored - failures}`,
7526
+ `❌ **Failure/revert lessons:** ${failures}`,
7527
+ skipped > 0 ? `⏭️ **Already in brain (skipped):** ${skipped}` : '',
7528
+ ``,
7529
+ ];
7530
+ if (topLessons.length > 0) {
7531
+ lines.push(`**Top lessons extracted:**`);
7532
+ lines.push(...topLessons);
7533
+ if (stored > 5)
7534
+ lines.push(` …and ${stored - 5} more`);
7535
+ lines.push('');
7536
+ }
7537
+ lines.push(`💡 **Next step:** Call \`session_start(focus="...")\` to see these lessons in your briefing.`, `🔄 **Re-run anytime** — duplicates are safely skipped.`);
7538
+ return lines.filter(l => l !== '').join('\n');
7539
+ }
7217
7540
  default:
7218
7541
  return null;
7219
7542
  }