@cachly-dev/mcp-server 0.5.80 → 0.6.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
@@ -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.5.80';
50
+ const CURRENT_VERSION = '0.6.1';
51
51
  /**
52
52
  * ┌──────────────────────────────────────────────────────────────────────┐
53
53
  * │ EMBEDDING PROVIDER — pluggable, client-side first │
@@ -181,6 +181,8 @@ async function computeEmbedding(text) {
181
181
  if (!res.ok)
182
182
  throw new Error(`Mistral embedding error: ${res.statusText}`);
183
183
  const json = (await res.json());
184
+ if (!json.data?.[0]?.embedding)
185
+ throw new Error('Mistral returned empty embedding response');
184
186
  return json.data[0].embedding;
185
187
  }
186
188
  case 'cohere': {
@@ -243,6 +245,8 @@ async function computeEmbedding(text) {
243
245
  if (!res.ok)
244
246
  throw new Error(`OpenAI embedding error: ${res.statusText}`);
245
247
  const json = (await res.json());
248
+ if (!json.data?.[0]?.embedding)
249
+ throw new Error('OpenAI returned empty embedding response');
246
250
  return json.data[0].embedding;
247
251
  }
248
252
  default: {
@@ -1212,6 +1216,7 @@ const CROSS_LINGUAL_MAP = new Map([
1212
1216
  ['اتصال', ['connection', 'conn']],
1213
1217
  // ── Hindi (Devanagari) → English ──────────────────────────────────────────
1214
1218
  ['तैनाती', ['deploy', 'deployment']],
1219
+ ['तैना', ['deploy', 'deployment']], // stemmed form
1215
1220
  ['त्रुटि', ['error', 'err']],
1216
1221
  ['गलती', ['error', 'bug', 'issue']],
1217
1222
  ['सुधार', ['fix', 'bugfix']],
@@ -1463,6 +1468,8 @@ const CROSS_LINGUAL_MAP = new Map([
1463
1468
  ['соединение', ['connection', 'conn']],
1464
1469
  ['развёртывание', ['deploy', 'deployment']],
1465
1470
  ['развертывание', ['deploy', 'deployment']],
1471
+ ['развёрт', ['deploy', 'deployment']], // stemmed form
1472
+ ['разверт', ['deploy', 'deployment']], // stemmed form
1466
1473
  ['контейнер', ['container', 'docker']],
1467
1474
  ['образ', ['image']],
1468
1475
  ['кластер', ['cluster']],
@@ -1521,6 +1528,7 @@ const CROSS_LINGUAL_MAP = new Map([
1521
1528
  ['göç', ['migrate', 'migration']],
1522
1529
  // ── Vietnamese (Latin + Latin Extended Additional U+1EA0–U+1EF9) → English ─
1523
1530
  ['triển khai', ['deploy', 'deployment']],
1531
+ ['triển', ['deploy', 'deployment']], // split token form
1524
1532
  ['lỗi', ['error', 'err']],
1525
1533
  ['sửa lỗi', ['fix', 'bugfix', 'debug']],
1526
1534
  ['xây dựng', ['build']],
@@ -3297,6 +3305,98 @@ const TOOLS = [
3297
3305
  required: ['instance_id', 'key'],
3298
3306
  },
3299
3307
  },
3308
+ // ── v0.6 Cognitive Cache Tools ────────────────────────────────────────────
3309
+ {
3310
+ name: 'memory_consolidate',
3311
+ description: 'Cognitive memory consolidation — the weekly garbage collector for your AI Brain. ' +
3312
+ 'Scans all lessons, detects contradictions (same topic with conflicting outcomes), ' +
3313
+ 'merges duplicates, flags stale entries (not recalled in 90+ days), and computes a ' +
3314
+ 'health score. Returns a full consolidation report with conflicts resolved, ' +
3315
+ 'duplicates merged, and a before/after count. ' +
3316
+ 'Run weekly or when brain_doctor reports > 20 lessons. ' +
3317
+ 'Like git gc for knowledge.',
3318
+ inputSchema: {
3319
+ type: 'object',
3320
+ properties: {
3321
+ instance_id: { type: 'string', description: 'UUID of the cache instance' },
3322
+ dry_run: { type: 'boolean', description: 'If true, report what would change without writing (default: false)' },
3323
+ stale_days: { type: 'number', description: 'Lessons not recalled in this many days are flagged stale (default: 90)' },
3324
+ },
3325
+ required: ['instance_id'],
3326
+ },
3327
+ },
3328
+ {
3329
+ name: 'brain_diff',
3330
+ description: 'git log for your AI Brain — see exactly what changed since a point in time. ' +
3331
+ 'Returns a structured changelog: new lessons added, lessons updated (outcome changed), ' +
3332
+ 'lessons recalled (hit count increased), and lessons that decayed. ' +
3333
+ 'Perfect for weekly reviews: "What did my AI learn this week?" ' +
3334
+ 'Example: brain_diff(instance_id="...", since="7d") → "12 new · 4 updated · 2 stale"',
3335
+ inputSchema: {
3336
+ type: 'object',
3337
+ properties: {
3338
+ instance_id: { type: 'string', description: 'UUID of the cache instance' },
3339
+ since: { type: 'string', description: 'Time window: "1d", "7d", "30d", or ISO-8601 date (default: "7d")' },
3340
+ format: { type: 'string', enum: ['summary', 'detailed'], description: 'Output format (default: summary)' },
3341
+ },
3342
+ required: ['instance_id'],
3343
+ },
3344
+ },
3345
+ {
3346
+ name: 'causal_trace',
3347
+ description: 'Root Cause Analysis through memory — the most powerful debugging tool in your AI Brain. ' +
3348
+ 'Given a problem description, traces the causal chain from root cause through intermediate ' +
3349
+ 'failures to the current symptom, then surfaces the exact solution that worked before. ' +
3350
+ 'Example: causal_trace(problem="auth breaks after restart") → ' +
3351
+ '"Root: k8s:namespace-terminating → keycloak:jwks-race → Solution from March 12: PollUntilContextTimeout 3min" ' +
3352
+ 'No other memory system can do this. Replaces 30 minutes of git blame + log archaeology.',
3353
+ inputSchema: {
3354
+ type: 'object',
3355
+ properties: {
3356
+ instance_id: { type: 'string', description: 'UUID of the cache instance' },
3357
+ problem: { type: 'string', description: 'Describe the problem or error you are seeing right now' },
3358
+ max_depth: { type: 'number', description: 'Max causal chain depth to trace (default: 5)' },
3359
+ tags: { type: 'array', items: { type: 'string' }, description: 'Optional: narrow search to these tags' },
3360
+ },
3361
+ required: ['instance_id', 'problem'],
3362
+ },
3363
+ },
3364
+ {
3365
+ name: 'knowledge_decay',
3366
+ description: 'Confidence scoring for every lesson in your Brain — because old knowledge rots. ' +
3367
+ 'Computes a decay score (0–100%) per lesson based on age, recall frequency, and outcome. ' +
3368
+ 'Lessons recalled recently score high. Lessons from 90 days ago never recalled score low. ' +
3369
+ 'Returns a ranked list with visual confidence bars: "████░░░░ 40%". ' +
3370
+ 'Use this before a big refactor to know which lessons to trust and which to re-validate.',
3371
+ inputSchema: {
3372
+ type: 'object',
3373
+ properties: {
3374
+ instance_id: { type: 'string', description: 'UUID of the cache instance' },
3375
+ min_age_days: { type: 'number', description: 'Only include lessons older than N days (default: 0 = all)' },
3376
+ show_top: { type: 'number', description: 'Number of entries to return, sorted by lowest confidence first (default: 20)' },
3377
+ },
3378
+ required: ['instance_id'],
3379
+ },
3380
+ },
3381
+ {
3382
+ name: 'autopilot',
3383
+ description: 'The endgame: generate a CLAUDE.md / copilot-instructions.md that turns any AI into a ' +
3384
+ 'self-managing brain operator. Zero manual session_start / session_end calls forever. ' +
3385
+ 'The generated file instructs Claude, Cursor, Copilot, Windsurf, or Gemini to ' +
3386
+ 'automatically call session_start at window open, learn_from_attempts after every fix, ' +
3387
+ 'and session_end before closing — without being asked. ' +
3388
+ 'One command. Every AI. Always on.',
3389
+ inputSchema: {
3390
+ type: 'object',
3391
+ properties: {
3392
+ instance_id: { type: 'string', description: 'UUID of the cache instance' },
3393
+ editor: { type: 'string', enum: ['claude', 'cursor', 'copilot', 'windsurf', 'gemini', 'continue', 'all'], description: 'Target editor (default: claude)' },
3394
+ project_name: { type: 'string', description: 'Your project name (used in generated instructions)' },
3395
+ style: { type: 'string', enum: ['minimal', 'full'], description: 'minimal = just the hooks, full = full ruleset with examples (default: full)' },
3396
+ },
3397
+ required: ['instance_id'],
3398
+ },
3399
+ },
3300
3400
  ];
3301
3401
  // ── Handlers ──────────────────────────────────────────────────────────────────
3302
3402
  function formatInstance(inst) {
@@ -6439,6 +6539,469 @@ async function handleBulkLockStream(name, args) {
6439
6539
  : `💡 Finish with: \`roadmap_update(id: "${next.id}", status: "done", notes: "...")\``,
6440
6540
  ].filter(s => s !== undefined).join('\n');
6441
6541
  }
6542
+ // ── v0.6 Cognitive Cache: memory_consolidate ─────────────────────────────
6543
+ case 'memory_consolidate': {
6544
+ const { instance_id, dry_run = false, stale_days = 90 } = args;
6545
+ const redis = await getConnection(instance_id);
6546
+ const now = Date.now();
6547
+ const staleMs = stale_days * 86400 * 1000;
6548
+ // Scan all lessons
6549
+ let cursor = 0;
6550
+ const lessonKeys = [];
6551
+ do {
6552
+ const [next, keys] = await redis.scan(cursor, 'MATCH', 'cachly:lesson:best:*', 'COUNT', 200);
6553
+ cursor = parseInt(next);
6554
+ lessonKeys.push(...keys);
6555
+ } while (cursor !== 0);
6556
+ if (lessonKeys.length === 0) {
6557
+ return '🧠 **Brain is empty** — no lessons to consolidate yet. Use `learn_from_attempts` after your next bug fix.';
6558
+ }
6559
+ const lessons = new Map();
6560
+ for (const k of lessonKeys) {
6561
+ const raw = await redis.get(k);
6562
+ if (!raw)
6563
+ continue;
6564
+ try {
6565
+ lessons.set(k, JSON.parse(raw));
6566
+ }
6567
+ catch { /* skip malformed */ }
6568
+ }
6569
+ // Detect duplicates: same topic prefix (e.g. deploy:api vs deploy:api-v2)
6570
+ const duplicates = [];
6571
+ const topicGroups = new Map();
6572
+ for (const [k, l] of lessons) {
6573
+ const prefix = l.topic.split(':')[0];
6574
+ const group = topicGroups.get(prefix) ?? [];
6575
+ group.push(k);
6576
+ topicGroups.set(prefix, group);
6577
+ }
6578
+ // Detect contradictions: same exact topic, different outcomes
6579
+ const contradictions = [];
6580
+ const exactTopics = new Map();
6581
+ for (const [k, l] of lessons) {
6582
+ const arr = exactTopics.get(l.topic) ?? [];
6583
+ arr.push(k);
6584
+ exactTopics.set(l.topic, arr);
6585
+ }
6586
+ for (const [topic, keys] of exactTopics) {
6587
+ const outcomes = new Set(keys.map(k => lessons.get(k)?.outcome));
6588
+ if (outcomes.size > 1)
6589
+ contradictions.push({ topic, keys });
6590
+ }
6591
+ // Detect stale: not recalled in stale_days
6592
+ const stale = [];
6593
+ for (const [k, l] of lessons) {
6594
+ const age = now - new Date(l.ts).getTime();
6595
+ const recalls = l.recall_count ?? 0;
6596
+ if (age > staleMs && recalls === 0)
6597
+ stale.push(k);
6598
+ }
6599
+ // Merge duplicates by prefix: keep the success/highest-severity one
6600
+ let merged = 0;
6601
+ if (!dry_run) {
6602
+ for (const [, keys] of topicGroups) {
6603
+ if (keys.length < 2)
6604
+ continue;
6605
+ const bySuccess = keys.filter(k => lessons.get(k)?.outcome === 'success');
6606
+ const winner = bySuccess[0] ?? keys[0];
6607
+ for (const k of keys) {
6608
+ if (k !== winner) {
6609
+ await redis.del(k);
6610
+ merged++;
6611
+ }
6612
+ }
6613
+ }
6614
+ // Resolve contradictions: keep success, delete failure for same topic
6615
+ for (const { keys } of contradictions) {
6616
+ const success = keys.find(k => lessons.get(k)?.outcome === 'success');
6617
+ if (success) {
6618
+ for (const k of keys) {
6619
+ if (k !== success) {
6620
+ await redis.del(k);
6621
+ }
6622
+ }
6623
+ }
6624
+ }
6625
+ // Flag stale entries with a TTL of 30 days (not deleted, just expiring)
6626
+ for (const k of stale) {
6627
+ await redis.expire(k, 86400 * 30);
6628
+ }
6629
+ }
6630
+ const lines = [
6631
+ `🔬 **Memory Consolidation Report** ${dry_run ? '(dry run — no changes made)' : '✅ Applied'}`,
6632
+ ``,
6633
+ `📊 **Before:** ${lessonKeys.length} lessons`,
6634
+ ``,
6635
+ `🔁 **Contradictions detected:** ${contradictions.length}`,
6636
+ ...contradictions.slice(0, 5).map(c => ` → \`${c.topic}\`: ${c.keys.length} conflicting entries (kept: success)`),
6637
+ contradictions.length > 5 ? ` … and ${contradictions.length - 5} more` : '',
6638
+ ``,
6639
+ `♻️ **Duplicate clusters:** ${Array.from(topicGroups.values()).filter(v => v.length > 1).length}` +
6640
+ (merged > 0 ? ` → ${merged} entries merged` : ''),
6641
+ ``,
6642
+ `🕰️ **Stale entries (${stale_days}d, 0 recalls):** ${stale.length}` +
6643
+ (stale.length > 0 && !dry_run ? ` → set to expire in 30 days` : ''),
6644
+ ``,
6645
+ `📊 **After:** ${dry_run ? lessonKeys.length : lessonKeys.length - merged} lessons`,
6646
+ ``,
6647
+ dry_run
6648
+ ? `💡 Re-run without dry_run=true to apply changes.`
6649
+ : `✨ Brain consolidated. Run \`brain_diff(since="1h")\` to see the delta.`,
6650
+ ].filter(s => s !== '').join('\n');
6651
+ return lines;
6652
+ }
6653
+ // ── v0.6 Cognitive Cache: brain_diff ─────────────────────────────────────
6654
+ case 'brain_diff': {
6655
+ const { instance_id, since = '7d', format = 'summary' } = args;
6656
+ const redis = await getConnection(instance_id);
6657
+ // Parse since
6658
+ let sinceMs;
6659
+ const match = since.match(/^(\d+)([dhm])$/);
6660
+ if (match) {
6661
+ const n = parseInt(match[1]);
6662
+ const unit = match[2];
6663
+ const mult = unit === 'd' ? 86400000 : unit === 'h' ? 3600000 : 60000;
6664
+ sinceMs = Date.now() - n * mult;
6665
+ }
6666
+ else {
6667
+ sinceMs = new Date(since).getTime() || Date.now() - 7 * 86400000;
6668
+ }
6669
+ // Scan all lessons
6670
+ let cursor = 0;
6671
+ const lessonKeys = [];
6672
+ do {
6673
+ const [next, keys] = await redis.scan(cursor, 'MATCH', 'cachly:lesson:best:*', 'COUNT', 200);
6674
+ cursor = parseInt(next);
6675
+ lessonKeys.push(...keys);
6676
+ } while (cursor !== 0);
6677
+ const added = [];
6678
+ const updated = [];
6679
+ const recalled = [];
6680
+ const total = lessonKeys.length;
6681
+ for (const k of lessonKeys) {
6682
+ const raw = await redis.get(k);
6683
+ if (!raw)
6684
+ continue;
6685
+ try {
6686
+ const l = JSON.parse(raw);
6687
+ const ts = new Date(l.ts).getTime();
6688
+ if (ts >= sinceMs) {
6689
+ // Check history to determine add vs update
6690
+ const histKey = `cachly:lesson:history:${l.topic}`;
6691
+ const histLen = await redis.llen(histKey);
6692
+ if (histLen <= 1)
6693
+ added.push(l);
6694
+ else
6695
+ updated.push(l);
6696
+ }
6697
+ if ((l.recall_count ?? 0) > 0) {
6698
+ // We can't easily know when last recalled without extra metadata, so include
6699
+ // lessons with recalls as "active"
6700
+ recalled.push(l);
6701
+ }
6702
+ }
6703
+ catch { /* skip */ }
6704
+ }
6705
+ const sinceLabel = match ? `last ${since}` : new Date(sinceMs).toLocaleDateString('de-DE');
6706
+ const lines = [
6707
+ `📊 **Brain Diff** — ${sinceLabel}`,
6708
+ ``,
6709
+ `Total lessons in brain: **${total}**`,
6710
+ ``,
6711
+ `✅ **New** (${added.length}):`,
6712
+ ...added.slice(0, format === 'detailed' ? 20 : 5).map(l => ` + \`${l.topic}\` — ${l.outcome} ${l.severity === 'critical' ? '🔴' : l.severity === 'major' ? '🟠' : '🟢'}`),
6713
+ added.length > 5 && format === 'summary' ? ` … and ${added.length - 5} more` : '',
6714
+ ``,
6715
+ `🔄 **Updated** (${updated.length}):`,
6716
+ ...updated.slice(0, format === 'detailed' ? 20 : 5).map(l => ` ~ \`${l.topic}\` — now: ${l.outcome}`),
6717
+ updated.length > 5 && format === 'summary' ? ` … and ${updated.length - 5} more` : '',
6718
+ ``,
6719
+ `🔍 **Active** (recalled at least once): ${recalled.length}`,
6720
+ ``,
6721
+ `💡 Run \`memory_consolidate\` to merge duplicates · \`knowledge_decay\` to see confidence scores.`,
6722
+ ].filter(s => s !== '');
6723
+ return lines.join('\n');
6724
+ }
6725
+ // ── v0.6 Cognitive Cache: causal_trace ───────────────────────────────────
6726
+ case 'causal_trace': {
6727
+ const { instance_id, problem, max_depth = 5, tags: filterTags = [] } = args;
6728
+ const redis = await getConnection(instance_id);
6729
+ // Normalize problem to keyword tokens
6730
+ const tokens = problem.toLowerCase()
6731
+ .replace(/[^a-z0-9\s\-_:]/g, ' ')
6732
+ .split(/\s+/)
6733
+ .filter(t => t.length > 2);
6734
+ // Scan all lessons + history entries
6735
+ let cursor = 0;
6736
+ const lessonKeys = [];
6737
+ do {
6738
+ const [next, keys] = await redis.scan(cursor, 'MATCH', 'cachly:lesson:*', 'COUNT', 200);
6739
+ cursor = parseInt(next);
6740
+ lessonKeys.push(...keys);
6741
+ } while (cursor !== 0);
6742
+ // Score each lesson by token overlap with problem description
6743
+ const scored = [];
6744
+ for (const k of lessonKeys) {
6745
+ const raw = await redis.get(k);
6746
+ if (!raw)
6747
+ continue;
6748
+ try {
6749
+ const l = JSON.parse(raw);
6750
+ // Skip if filter tags given and lesson has none of them
6751
+ if (filterTags.length > 0 && !(l.tags ?? []).some((t) => filterTags.includes(t)))
6752
+ continue;
6753
+ const haystack = [l.topic, l.what_failed ?? '', l.what_worked ?? '', l.context ?? '']
6754
+ .join(' ').toLowerCase();
6755
+ const score = tokens.reduce((s, t) => s + (haystack.includes(t) ? 1 : 0), 0);
6756
+ if (score > 0)
6757
+ scored.push({ score, lesson: l, key: k });
6758
+ }
6759
+ catch { /* skip */ }
6760
+ }
6761
+ scored.sort((a, b) => b.score - a.score);
6762
+ const chain = scored.slice(0, max_depth);
6763
+ if (chain.length === 0) {
6764
+ return [
6765
+ `🔍 **Causal Trace: "${problem}"**`,
6766
+ ``,
6767
+ `No matching lessons found in brain.`,
6768
+ ``,
6769
+ `💡 After you solve this, call:`,
6770
+ `\`\`\``,
6771
+ `learn_from_attempts(`,
6772
+ ` instance_id = "${instance_id}",`,
6773
+ ` topic = "fix:${tokens[0] ?? 'issue'}",`,
6774
+ ` outcome = "success",`,
6775
+ ` what_worked = "...",`,
6776
+ ` what_failed = "${problem}",`,
6777
+ `)`,
6778
+ `\`\`\``,
6779
+ ].join('\n');
6780
+ }
6781
+ // Build causal chain narrative: failures first → then successes
6782
+ const failures = chain.filter(c => c.lesson.outcome !== 'success');
6783
+ const solutions = chain.filter(c => c.lesson.outcome === 'success');
6784
+ const SEV_ICON = { critical: '🔴', major: '🟠', minor: '🟡' };
6785
+ const lines = [
6786
+ `🔍 **Causal Trace: "${problem}"**`,
6787
+ ``,
6788
+ `Found **${chain.length}** related lessons. Reconstructed causal chain:`,
6789
+ ``,
6790
+ ];
6791
+ if (failures.length > 0) {
6792
+ lines.push(`**Root causes & failure chain:**`);
6793
+ failures.forEach((c, i) => {
6794
+ const l = c.lesson;
6795
+ const sev = SEV_ICON[l.severity ?? 'minor'] ?? '🟡';
6796
+ lines.push(`${i === 0 ? ' Root:' : ' → :'} ${sev} \`${l.topic}\``);
6797
+ if (l.what_failed)
6798
+ lines.push(` ↳ ${l.what_failed.slice(0, 120)}`);
6799
+ });
6800
+ lines.push('');
6801
+ }
6802
+ if (solutions.length > 0) {
6803
+ lines.push(`**Solutions that worked before:**`);
6804
+ solutions.forEach((c, i) => {
6805
+ const l = c.lesson;
6806
+ const date = new Date(l.ts).toLocaleDateString('de-DE');
6807
+ lines.push(` ${i + 1}. ✅ \`${l.topic}\` — ${date} · recalled ${l.recall_count ?? 0}× `);
6808
+ if (l.what_worked)
6809
+ lines.push(` ${l.what_worked.slice(0, 200)}`);
6810
+ });
6811
+ lines.push('');
6812
+ }
6813
+ const topSolution = solutions[0]?.lesson;
6814
+ if (topSolution?.what_worked) {
6815
+ lines.push(`**⚡ Most likely fix:**`);
6816
+ lines.push(`\`\`\``);
6817
+ lines.push(topSolution.what_worked.slice(0, 500));
6818
+ lines.push(`\`\`\``);
6819
+ lines.push('');
6820
+ }
6821
+ lines.push(`💡 After applying: \`learn_from_attempts(topic="fix:${tokens[0] ?? 'issue'}", outcome="success", ...)\``);
6822
+ return lines.join('\n');
6823
+ }
6824
+ // ── v0.6 Cognitive Cache: knowledge_decay ────────────────────────────────
6825
+ case 'knowledge_decay': {
6826
+ const { instance_id, min_age_days = 0, show_top = 20 } = args;
6827
+ const redis = await getConnection(instance_id);
6828
+ const now = Date.now();
6829
+ const minAgeMs = min_age_days * 86400000;
6830
+ let cursor = 0;
6831
+ const lessonKeys = [];
6832
+ do {
6833
+ const [next, keys] = await redis.scan(cursor, 'MATCH', 'cachly:lesson:best:*', 'COUNT', 200);
6834
+ cursor = parseInt(next);
6835
+ lessonKeys.push(...keys);
6836
+ } while (cursor !== 0);
6837
+ const scores = [];
6838
+ for (const k of lessonKeys) {
6839
+ const raw = await redis.get(k);
6840
+ if (!raw)
6841
+ continue;
6842
+ try {
6843
+ const l = JSON.parse(raw);
6844
+ const ageMs = now - new Date(l.ts).getTime();
6845
+ if (ageMs < minAgeMs)
6846
+ continue;
6847
+ const age_days = Math.floor(ageMs / 86400000);
6848
+ const recalls = l.recall_count ?? 0;
6849
+ // Confidence formula:
6850
+ // base = 100 → decays by 1pt/day after 7 days, floored at 5
6851
+ // boost: +5 per recall, capped at +50
6852
+ // penalty: failure outcome → -20
6853
+ const decayPts = Math.max(0, age_days - 7);
6854
+ const base = Math.max(5, 100 - decayPts);
6855
+ const recallBoost = Math.min(50, recalls * 5);
6856
+ const outcomePenalty = l.outcome === 'failure' ? -20 : 0;
6857
+ const confidence = Math.min(100, Math.max(0, base + recallBoost + outcomePenalty));
6858
+ scores.push({ topic: l.topic, confidence, age_days, recalls, outcome: l.outcome });
6859
+ }
6860
+ catch { /* skip */ }
6861
+ }
6862
+ // Sort by lowest confidence first
6863
+ scores.sort((a, b) => a.confidence - b.confidence);
6864
+ const shown = scores.slice(0, show_top);
6865
+ function bar(pct) {
6866
+ const filled = Math.round(pct / 10);
6867
+ return '█'.repeat(filled) + '░'.repeat(10 - filled) + ` ${pct}%`;
6868
+ }
6869
+ const avgConf = scores.length > 0
6870
+ ? Math.round(scores.reduce((s, e) => s + e.confidence, 0) / scores.length)
6871
+ : 0;
6872
+ const critical = scores.filter(s => s.confidence < 30).length;
6873
+ const healthy = scores.filter(s => s.confidence >= 70).length;
6874
+ const lines = [
6875
+ `🧪 **Knowledge Decay Report** — ${scores.length} lessons`,
6876
+ ``,
6877
+ `Average confidence: **${bar(avgConf)}**`,
6878
+ `🔴 Critical (<30%): ${critical} 🟢 Healthy (≥70%): ${healthy}`,
6879
+ ``,
6880
+ `Lowest confidence lessons (fix these first):`,
6881
+ ``,
6882
+ ...shown.map((s, i) => `${(i + 1).toString().padStart(2)}. \`${s.topic}\`\n ${bar(s.confidence)} · ${s.age_days}d old · ${s.recalls} recalls · ${s.outcome}`),
6883
+ ``,
6884
+ scores.length > show_top ? `… and ${scores.length - show_top} more lessons` : '',
6885
+ ``,
6886
+ critical > 0
6887
+ ? `⚠️ ${critical} lessons are critically stale. Validate them and re-run \`learn_from_attempts\` with updated findings.`
6888
+ : `✅ Brain confidence is healthy overall.`,
6889
+ ``,
6890
+ `💡 Run \`memory_consolidate\` to prune stale entries · \`causal_trace\` to find root causes.`,
6891
+ ].filter(s => s !== '');
6892
+ return lines.join('\n');
6893
+ }
6894
+ // ── v0.6 Cognitive Cache: autopilot ──────────────────────────────────────
6895
+ case 'autopilot': {
6896
+ const { instance_id, editor = 'claude', project_name = 'this project', style = 'full' } = args;
6897
+ const minimal = `## Cachly AI Brain — Autopilot
6898
+
6899
+ **Instance:** \`${instance_id}\`
6900
+
6901
+ > Sessions are fully automatic. The Brain starts when you call any tool and saves a summary when you close.
6902
+
6903
+ ### MANDATORY hooks (always fire, no exceptions):
6904
+ - **Window opens** → call \`session_start(instance_id="${instance_id}", focus="<today's goal>")\`
6905
+ - **Bug fixed / deployed / discovered** → call \`learn_from_attempts(instance_id="${instance_id}", topic="fix:<area>", outcome="success", what_worked="...")\`
6906
+ - **Window closes** → call \`session_end(instance_id="${instance_id}", summary="...", files_changed=[...])\`
6907
+ `;
6908
+ const full = `## Cachly AI Brain — Autopilot for ${project_name}
6909
+
6910
+ **Instance:** \`${instance_id}\`
6911
+
6912
+ ---
6913
+
6914
+ ## 🔴 MANDATORY RULES — No exceptions, no prompting needed
6915
+
6916
+ ### Rule 1 — Session Start (FIRST thing in every window)
6917
+ \`\`\`
6918
+ session_start(
6919
+ instance_id = "${instance_id}",
6920
+ focus = "<what you plan to work on today>",
6921
+ )
6922
+ \`\`\`
6923
+ Returns: last session summary, handoff tasks, recent lessons, open failures.
6924
+
6925
+ ### Rule 2 — Learn After Every Fix/Deploy/Discovery (AUTOMATIC)
6926
+ **After EVERY** bug fix, deployment, or technical discovery — call immediately:
6927
+ \`\`\`
6928
+ learn_from_attempts(
6929
+ instance_id = "${instance_id}",
6930
+ topic = "category:keyword",
6931
+ outcome = "success" | "failure" | "partial",
6932
+ what_worked = "exact command or approach",
6933
+ what_failed = "what failed (if applicable)",
6934
+ severity = "critical" | "major" | "minor",
6935
+ commands = ["the exact command that worked"],
6936
+ file_paths = ["path/to/file"],
6937
+ tags = ["relevant", "tags"],
6938
+ )
6939
+ \`\`\`
6940
+
6941
+ **Triggers:**
6942
+ - ✅ Deployed → \`topic="deploy:web", outcome="success"\`
6943
+ - ✅ Fixed bug → \`topic="fix:auth-race", outcome="success"\`
6944
+ - ✅ Got error → \`topic="debug:docker", outcome="partial"\`
6945
+
6946
+ ### Rule 3 — Session End (LAST thing before closing)
6947
+ \`\`\`
6948
+ session_end(
6949
+ instance_id = "${instance_id}",
6950
+ summary = "What was accomplished (2-3 sentences)",
6951
+ files_changed = ["list", "of", "changed", "files"],
6952
+ lessons_learned = <number of learn_from_attempts calls>,
6953
+ )
6954
+ \`\`\`
6955
+
6956
+ ### Rule 4 — Before any non-trivial task
6957
+ \`\`\`
6958
+ smart_recall(
6959
+ instance_id = "${instance_id}",
6960
+ query = "<describe what you are about to do>",
6961
+ )
6962
+ \`\`\`
6963
+
6964
+ ---
6965
+
6966
+ ## ⚡ Cognitive Tools (v0.6)
6967
+
6968
+ | When | Call |
6969
+ |---|---|
6970
+ | Brain feeling cluttered | \`memory_consolidate(instance_id="${instance_id}")\` |
6971
+ | Weekly review | \`brain_diff(instance_id="${instance_id}", since="7d")\` |
6972
+ | Weird bug, no idea why | \`causal_trace(instance_id="${instance_id}", problem="<symptom>")\` |
6973
+ | Before big refactor | \`knowledge_decay(instance_id="${instance_id}")\` |
6974
+
6975
+ ---
6976
+
6977
+ *Cachly v0.6 · Generated ${new Date().toISOString().slice(0, 10)}*
6978
+ `;
6979
+ const content = style === 'minimal' ? minimal : full;
6980
+ const filename = editor === 'copilot'
6981
+ ? '.github/copilot-instructions.md'
6982
+ : editor === 'continue'
6983
+ ? '.continue/cachly-autopilot.md'
6984
+ : 'CLAUDE.md';
6985
+ return [
6986
+ `🤖 **Autopilot instructions generated** for **${editor === 'all' ? 'all editors' : editor}**`,
6987
+ ``,
6988
+ `**File to create:** \`${filename}\``,
6989
+ ``,
6990
+ `\`\`\`markdown`,
6991
+ content,
6992
+ `\`\`\``,
6993
+ ``,
6994
+ `**How to apply:**`,
6995
+ `\`\`\`bash`,
6996
+ `# Copy to your project root:`,
6997
+ `cat > ${filename} << 'EOF'`,
6998
+ content,
6999
+ `EOF`,
7000
+ `\`\`\``,
7001
+ ``,
7002
+ `✨ Once this file is in place, **${editor === 'copilot' ? 'GitHub Copilot' : editor === 'continue' ? 'Continue.dev' : 'Claude/Cursor/Windsurf'}** will manage the Brain automatically — no manual calls needed, ever.`,
7003
+ ].join('\n');
7004
+ }
6442
7005
  default:
6443
7006
  return null;
6444
7007
  }