@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/glama.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "$schema": "https://glama.ai/mcp/schemas/server.json",
3
+ "maintainers": [
4
+ "HeinrichNebula"
5
+ ]
6
+ }
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@cachly-dev/mcp-server",
3
- "version": "0.8.3",
3
+ "version": "0.9.1",
4
4
  "mcpName": "io.github.cachly-dev/cachly-mcp",
5
- "description": "AI memory brain for Claude Code, Cursor, Copilot & Windsurf — persistent sessions, lessons learned, semantic search, team brain. 51 MCP tools.",
5
+ "description": "The world's first Cognitive Cache — AI memory that thinks. 68 MCP tools: sessions, lessons, semantic search, team brain, knowledge syndication, causal tracing, memory consolidation, compact recovery. Claude Code, Cursor, Copilot, Windsurf.",
6
6
  "type": "module",
7
7
  "bin": {
8
- "cachly-mcp": "dist/index.js"
8
+ "cachly-mcp": "./dist/index.js"
9
9
  },
10
10
  "main": "./dist/index.js",
11
11
  "scripts": {
@@ -34,7 +34,7 @@
34
34
  "license": "Apache-2.0",
35
35
  "repository": {
36
36
  "type": "git",
37
- "url": "git+https://github.com/cachly-dev/cachly-mcp.git"
37
+ "url": "https://github.com/cachly-dev/cachly-mcp"
38
38
  },
39
39
  "publishConfig": {
40
40
  "access": "public",
@@ -58,6 +58,12 @@
58
58
  "team-knowledge",
59
59
  "ambient-git",
60
60
  "memory-crystals",
61
+ "cognitive-cache",
62
+ "causal-reasoning",
63
+ "knowledge-graph",
64
+ "autopilot-memory",
65
+ "knowledge-syndication",
66
+ "collective-intelligence",
61
67
  "cachly",
62
68
  "redis",
63
69
  "valkey",
package/server.json CHANGED
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
3
  "name": "io.github.cachly-dev/cachly-mcp",
4
- "description": "AI memory brain for Claude Code, Cursor, Copilot & Windsurf — 51 MCP tools. Persistent sessions, lessons learned, semantic search, Team Telepathy, Ambient Git Learning, Memory Crystals.",
4
+ "description": "The world's first Cognitive Cache — AI memory that thinks. 67 MCP tools: persistent sessions, causal root-cause tracing, knowledge decay scoring, brain diff, memory consolidation, knowledge syndication, zero-config autopilot. Works with Claude, Cursor, Copilot, Windsurf, Gemini. German servers, GDPR-native.",
5
5
  "repository": {
6
6
  "url": "https://github.com/cachly-dev/cachly-mcp",
7
7
  "source": "github"
8
8
  },
9
- "version": "0.8.2",
9
+ "version": "0.9.1",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "name": "@cachly-dev/mcp-server",
14
14
  "identifier": "@cachly-dev/mcp-server",
15
- "version": "0.8.2",
15
+ "version": "0.9.1",
16
16
  "transport": {
17
17
  "type": "stdio"
18
18
  }
package/src/index.ts CHANGED
@@ -3037,6 +3037,22 @@ const TOOLS = [
3037
3037
  required: ['instance_id'],
3038
3038
  },
3039
3039
  },
3040
+ // ── crystal_view — inspect current Memory Crystal ──────────────────────
3041
+ {
3042
+ name: 'crystal_view',
3043
+ description:
3044
+ 'View the current Memory Crystal — the distilled snapshot of everything your AI brain has learned. ' +
3045
+ 'Shows all top patterns grouped by category, lesson count, crystal age, and when to refresh. ' +
3046
+ 'A crystal is created by memory_crystalize and injected into every session_start automatically. ' +
3047
+ 'Call this to review what institutional knowledge is preserved, or to decide if a refresh is due.',
3048
+ inputSchema: {
3049
+ type: 'object',
3050
+ properties: {
3051
+ instance_id: { type: 'string', description: 'UUID of the cache instance' },
3052
+ },
3053
+ required: ['instance_id'],
3054
+ },
3055
+ },
3040
3056
  // ── Roadmap — Persistent project plan tracker ───────────────────────────
3041
3057
  {
3042
3058
  name: 'roadmap_add',
@@ -3556,6 +3572,62 @@ const TOOLS = [
3556
3572
  required: [],
3557
3573
  },
3558
3574
  },
3575
+ // ── v0.9 — compact_recover ─────────────────────────────────────────────────
3576
+ {
3577
+ name: 'compact_recover',
3578
+ description:
3579
+ 'FAST context recovery after Claude/Cursor/Copilot context window compaction. ' +
3580
+ 'Call this as the FIRST tool call after any context limit hit or window restart — before reading any files. ' +
3581
+ 'Returns: last checkpoint (task + files + next_step), session handoff if any, last 5 recalled lessons, open TODOs. ' +
3582
+ 'Unlike session_start, this is instant — no full brain scan, no streak logic, just the critical recovery data. ' +
3583
+ 'Typical use in CLAUDE.md: "If context is compacted, call compact_recover immediately."',
3584
+ inputSchema: {
3585
+ type: 'object',
3586
+ properties: {
3587
+ instance_id: { type: 'string', description: 'UUID of the cache instance' },
3588
+ focus: {
3589
+ type: 'string',
3590
+ description: 'What you are trying to recover context for — used to surface relevant lessons (optional)',
3591
+ },
3592
+ },
3593
+ required: ['instance_id'],
3594
+ },
3595
+ },
3596
+ // ── v0.9 — brain_from_git ─────────────────────────────────────────────────
3597
+ {
3598
+ name: 'brain_from_git',
3599
+ description:
3600
+ 'Bootstrap the AI Brain from git history — reads commit messages and automatically stores lessons. ' +
3601
+ 'Scans the last N days of commits, extracts actionable knowledge (fixes, reverts, deploys, migrations), ' +
3602
+ 'and bulk-loads them into the Brain as lessons. ' +
3603
+ 'Run once to pre-load months of team knowledge before the first session_start. ' +
3604
+ 'Skips commits already in the Brain (safe to re-run). ' +
3605
+ 'Example: brain_from_git(instance_id="...", workspace_path="/home/you/project", days=180) ' +
3606
+ '→ "📊 Scanned 342 commits · ✅ 28 lessons stored · ❌ 9 failure lessons · 0 duplicates skipped"',
3607
+ inputSchema: {
3608
+ type: 'object',
3609
+ properties: {
3610
+ instance_id: { type: 'string', description: 'UUID of the cache instance' },
3611
+ workspace_path: {
3612
+ type: 'string',
3613
+ description: 'Absolute path to the git repository root (e.g. "/home/you/myproject")',
3614
+ },
3615
+ days: {
3616
+ type: 'number',
3617
+ description: 'How many days of history to scan (default: 180, max: 365)',
3618
+ },
3619
+ max_commits: {
3620
+ type: 'number',
3621
+ description: 'Maximum number of commits to process (default: 200, max: 500)',
3622
+ },
3623
+ author: {
3624
+ type: 'string',
3625
+ description: 'Filter to commits by this git author email or name (optional — omit to scan all authors)',
3626
+ },
3627
+ },
3628
+ required: ['instance_id', 'workspace_path'],
3629
+ },
3630
+ },
3559
3631
  ] as const;
3560
3632
 
3561
3633
  // ── Handlers ──────────────────────────────────────────────────────────────────
@@ -5533,6 +5605,21 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
5533
5605
  } catch { /* git not available or not a repo — silent skip */ }
5534
5606
  }
5535
5607
 
5608
+ // ── Crystal age check — remind if > 30 days old or missing ──────────────
5609
+ let crystalReminder = '';
5610
+ try {
5611
+ const crystalRaw = await redis.get('cachly:crystal:latest');
5612
+ if (!crystalRaw) {
5613
+ 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.`;
5614
+ } else {
5615
+ const crystal = JSON.parse(crystalRaw) as { ts: string; label?: string; lesson_count?: number };
5616
+ const crystalAgeDays = Math.round((Date.now() - new Date(crystal.ts).getTime()) / 86_400_000);
5617
+ if (crystalAgeDays >= 30) {
5618
+ 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.`;
5619
+ }
5620
+ }
5621
+ } catch { /* non-critical */ }
5622
+
5536
5623
  const durationStr = durationMin !== undefined ? ` · ${durationMin} min` : '';
5537
5624
  return [
5538
5625
  `✅ **Session saved**${durationStr}`,
@@ -5542,6 +5629,7 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
5542
5629
  lessons_learned !== undefined ? `🧠 **Lessons stored:** ${lessons_learned}` : '',
5543
5630
  autoLearned.length > 0 ? `🤖 **Auto-learned:** ${autoLearned.length} lessons extracted from summary (${autoLearned.slice(0, 3).map(t => `\`${t}\``).join(', ')}${autoLearned.length > 3 ? '…' : ''})` : '',
5544
5631
  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 ? '…' : ''})` : '',
5632
+ crystalReminder,
5545
5633
  ``,
5546
5634
  `💡 Next session: \`session_start(focus="...")\` to see this summary.`,
5547
5635
  ].filter(l => l !== '').join('\n');
@@ -6021,6 +6109,63 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
6021
6109
  return lines.join('\n');
6022
6110
  }
6023
6111
 
6112
+ // ── crystal_view — show current Memory Crystal ───────────────────────────
6113
+ case 'crystal_view': {
6114
+ const { instance_id } = args as { instance_id: string };
6115
+ const redis = await getConnection(instance_id);
6116
+
6117
+ const crystalRaw = await redis.get('cachly:crystal:latest');
6118
+ if (!crystalRaw) {
6119
+ return [
6120
+ `💎 **No Memory Crystal yet**`,
6121
+ ``,
6122
+ `Your brain has no crystal — all knowledge lives in raw lessons only.`,
6123
+ ``,
6124
+ `Run \`memory_crystalize(instance_id="${instance_id}")\` to:`,
6125
+ ` • Distill everything learned into a permanent, compact snapshot`,
6126
+ ` • Inject it into every future \`session_start\` automatically`,
6127
+ ` • Preserve team knowledge even after lesson TTLs expire`,
6128
+ ``,
6129
+ `💡 Recommended: run monthly or after any major milestone.`,
6130
+ ].join('\n');
6131
+ }
6132
+
6133
+ const crystal = JSON.parse(crystalRaw) as {
6134
+ label: string; ts: string; session_count: number; lesson_count: number;
6135
+ top_patterns: Array<{ category: string; insight: string; count: number }>;
6136
+ categories: string[]; created_from: string;
6137
+ };
6138
+
6139
+ const ageDays = Math.round((Date.now() - new Date(crystal.ts).getTime()) / 86_400_000);
6140
+ const ageStr = ageDays === 0 ? 'today' : ageDays === 1 ? '1 day ago' : `${ageDays} days ago`;
6141
+ const freshness = ageDays < 14 ? '🟢 Fresh' : ageDays < 30 ? '🟡 Aging' : '🔴 Stale — refresh recommended';
6142
+
6143
+ const lines: string[] = [
6144
+ `💎 **Memory Crystal: ${crystal.label}**`,
6145
+ ``,
6146
+ `📅 Created: **${ageStr}** · Freshness: ${freshness}`,
6147
+ `📊 Compressed: **${crystal.session_count} sessions** + **${crystal.lesson_count} lessons** → ${crystal.top_patterns.length} patterns`,
6148
+ `🗂️ Categories: ${crystal.categories.slice(0, 10).join(', ')}${crystal.categories.length > 10 ? `… +${crystal.categories.length - 10}` : ''}`,
6149
+ ``,
6150
+ `**Top patterns by category:**`,
6151
+ ``,
6152
+ ];
6153
+
6154
+ for (const p of crystal.top_patterns) {
6155
+ lines.push(`**${p.category}** (${p.count} lesson${p.count > 1 ? 's' : ''})`);
6156
+ lines.push(` → ${p.insight.slice(0, 150)}${p.insight.length > 150 ? '…' : ''}`);
6157
+ lines.push('');
6158
+ }
6159
+
6160
+ if (ageDays >= 30) {
6161
+ lines.push(`---`, `⚠️ Crystal is ${ageDays} days old. Run \`memory_crystalize(instance_id="${instance_id}")\` to refresh with your latest lessons.`);
6162
+ } else {
6163
+ lines.push(`---`, `💡 This crystal is injected automatically into every \`session_start\`. Refresh monthly with \`memory_crystalize\`.`);
6164
+ }
6165
+
6166
+ return lines.filter(l => l !== undefined).join('\n');
6167
+ }
6168
+
6024
6169
  case 'brain_doctor': {
6025
6170
  const { instance_id, workspace_path: drWorkspacePath = '' } = args as { instance_id: string; workspace_path?: string };
6026
6171
  const redis = await getConnection(instance_id);
@@ -7537,9 +7682,11 @@ async function handleBulkLockStream(name: string, args: Record<string, unknown>)
7537
7682
  > Sessions are fully automatic. The Brain starts when you call any tool and saves a summary when you close.
7538
7683
 
7539
7684
  ### MANDATORY hooks (always fire, no exceptions):
7685
+ - **Context compacted** → call \`compact_recover(instance_id="${instance_id}", focus="<current task>")\` FIRST
7540
7686
  - **Window opens** → call \`session_start(instance_id="${instance_id}", focus="<today's goal>")\`
7541
7687
  - **Bug fixed / deployed / discovered** → call \`learn_from_attempts(instance_id="${instance_id}", topic="fix:<area>", outcome="success", what_worked="...")\`
7542
7688
  - **Window closes** → call \`session_end(instance_id="${instance_id}", summary="...", files_changed=[...])\`
7689
+ - **Monthly** → call \`memory_crystalize(instance_id="${instance_id}")\` to distill knowledge into a permanent Crystal
7543
7690
  `;
7544
7691
 
7545
7692
  const full = `## Cachly AI Brain — Autopilot for ${project_name}
@@ -7600,18 +7747,40 @@ smart_recall(
7600
7747
 
7601
7748
  ---
7602
7749
 
7603
- ## ⚡ Cognitive Tools (v0.6)
7750
+ ## ⚡ Context Recovery (MANDATORY after compaction)
7751
+
7752
+ **If your context window was compacted**, call this FIRST — before reading any file:
7753
+ \`\`\`
7754
+ compact_recover(
7755
+ instance_id = "${instance_id}",
7756
+ focus = "<what you were working on>",
7757
+ )
7758
+ \`\`\`
7759
+ Returns: last checkpoint, pending tasks, top lessons. Instant — no full brain scan.
7760
+
7761
+ ---
7762
+
7763
+ ## 🔁 Scheduled Maintenance (run on schedule)
7764
+
7765
+ | When | Call |
7766
+ |---|---|
7767
+ | **Monthly** | \`memory_crystalize(instance_id="${instance_id}")\` — distill brain into a permanent snapshot |
7768
+ | Weekly review | \`brain_diff(instance_id="${instance_id}", since="7d")\` — see what your AI learned this week |
7769
+ | Before big refactor | \`knowledge_decay(instance_id="${instance_id}")\` — see which lessons to still trust |
7770
+ | After major milestone | \`memory_crystalize(instance_id="${instance_id}", label="v2 launch")\` — preserve milestone knowledge |
7771
+
7772
+ ## 🧠 Cognitive Tools (use anytime)
7604
7773
 
7605
7774
  | When | Call |
7606
7775
  |---|---|
7607
7776
  | Brain feeling cluttered | \`memory_consolidate(instance_id="${instance_id}")\` |
7608
- | Weekly review | \`brain_diff(instance_id="${instance_id}", since="7d")\` |
7609
7777
  | Weird bug, no idea why | \`causal_trace(instance_id="${instance_id}", problem="<symptom>")\` |
7610
- | Before big refactor | \`knowledge_decay(instance_id="${instance_id}")\` |
7778
+ | Unknown error | \`syndicate_search(q="<error message>")\` — global community solutions |
7779
+ | Unknown error that you solved | \`syndicate(instance_id="${instance_id}", topic="fix:<area>", what_worked="...")\` |
7611
7780
 
7612
7781
  ---
7613
7782
 
7614
- *Cachly v0.6 · Generated ${new Date().toISOString().slice(0, 10)}*
7783
+ *Cachly v0.9 · Generated ${new Date().toISOString().slice(0, 10)}*
7615
7784
  `;
7616
7785
 
7617
7786
  const content = style === 'minimal' ? minimal : full;
@@ -7852,6 +8021,196 @@ smart_recall(
7852
8021
  return lines.filter(l => l !== '').join('\n');
7853
8022
  }
7854
8023
 
8024
+ // ── v0.9 compact_recover — fast context recovery ──────────────────────────
8025
+ case 'compact_recover': {
8026
+ const { instance_id, focus = '' } = args as { instance_id: string; focus?: string };
8027
+ const redis = await getConnection(instance_id);
8028
+
8029
+ const lines: string[] = ['⚡ **Context Recovered**', ''];
8030
+
8031
+ // 1. Latest checkpoint
8032
+ const checkpointRaw = await redis.get('cachly:session:checkpoint');
8033
+ if (checkpointRaw) {
8034
+ try {
8035
+ const cp = JSON.parse(checkpointRaw) as {
8036
+ ts: string; task: string; files_touched?: string[]; next_step?: string; provider?: string;
8037
+ };
8038
+ const ago = Math.round((Date.now() - new Date(cp.ts).getTime()) / 60000);
8039
+ const agoStr = ago < 60 ? `${ago}m ago` : `${Math.round(ago / 60)}h ago`;
8040
+ lines.push(`📌 **Last checkpoint** (${agoStr}): ${cp.task}`);
8041
+ if (cp.files_touched && cp.files_touched.length > 0) {
8042
+ lines.push(` Files: ${cp.files_touched.slice(0, 5).join(', ')}${cp.files_touched.length > 5 ? '…' : ''}`);
8043
+ }
8044
+ if (cp.next_step) lines.push(`📍 **Resume with:** ${cp.next_step}`);
8045
+ lines.push('');
8046
+ } catch { /* ignore corrupt */ }
8047
+ }
8048
+
8049
+ // 2. Handoff (if any, from a different window)
8050
+ const handoffRaw = await redis.get('cachly:session:handoff');
8051
+ if (handoffRaw) {
8052
+ try {
8053
+ const hf = JSON.parse(handoffRaw) as { ts: string; remaining_tasks?: string[]; blocked_on?: string };
8054
+ const remaining = hf.remaining_tasks ?? [];
8055
+ if (remaining.length > 0) {
8056
+ lines.push(`⏳ **Remaining tasks:**`);
8057
+ remaining.slice(0, 5).forEach(t => lines.push(` - ${t}`));
8058
+ if (remaining.length > 5) lines.push(` …and ${remaining.length - 5} more`);
8059
+ lines.push('');
8060
+ }
8061
+ if (hf.blocked_on) { lines.push(`🚫 **Blocked on:** ${hf.blocked_on}`, ''); }
8062
+ } catch { /* ignore */ }
8063
+ }
8064
+
8065
+ // 3. Top recently recalled lessons (BM25 if focus, else recency)
8066
+ const lessonKeys: string[] = [];
8067
+ const lStream = redis.scanStream({ match: 'cachly:lesson:best:*', count: 200 });
8068
+ await new Promise<void>((resolve, reject) => {
8069
+ lStream.on('data', (batch: string[]) => lessonKeys.push(...batch));
8070
+ lStream.on('end', resolve);
8071
+ lStream.on('error', reject);
8072
+ });
8073
+
8074
+ type RecoverLesson = { topic: string; outcome: string; what_worked: string; ts: string; recall_count?: number; tags?: string[] };
8075
+ const allLessons: RecoverLesson[] = [];
8076
+ for (const k of lessonKeys) {
8077
+ const raw = await redis.get(k);
8078
+ if (!raw) continue;
8079
+ try { allLessons.push(JSON.parse(raw) as RecoverLesson); } catch { /* skip */ }
8080
+ }
8081
+
8082
+ const focusTerms = focus.toLowerCase().split(/\s+/).filter(Boolean);
8083
+ let topLessons = focusTerms.length > 0
8084
+ ? allLessons.filter(l =>
8085
+ focusTerms.some(t => l.topic.toLowerCase().includes(t) || (l.tags ?? []).some((tag: string) => tag.toLowerCase().includes(t)))
8086
+ ).slice(0, 5)
8087
+ : allLessons.sort((a, b) => (b.recall_count ?? 0) - (a.recall_count ?? 0)).slice(0, 5);
8088
+
8089
+ if (topLessons.length > 0) {
8090
+ lines.push(`🧠 **Top lessons${focus ? ` for "${focus}"` : ''}:**`);
8091
+ for (const l of topLessons) {
8092
+ const icon = l.outcome === 'success' ? '✅' : l.outcome === 'failure' ? '❌' : '⚠️';
8093
+ lines.push(` ${icon} \`${l.topic}\` — ${l.what_worked.slice(0, 120)}${l.what_worked.length > 120 ? '…' : ''}`);
8094
+ }
8095
+ lines.push('');
8096
+ }
8097
+
8098
+ lines.push(`💡 Call \`session_start\` for the full briefing, or resume work directly.`);
8099
+ return lines.filter(l => l !== '').join('\n');
8100
+ }
8101
+
8102
+ // ── v0.9 brain_from_git — bulk-load git history as lessons ───────────────
8103
+ case 'brain_from_git': {
8104
+ const {
8105
+ instance_id,
8106
+ workspace_path,
8107
+ days = 180,
8108
+ max_commits = 200,
8109
+ author = '',
8110
+ } = args as { instance_id: string; workspace_path: string; days?: number; max_commits?: number; author?: string };
8111
+
8112
+ const redis = await getConnection(instance_id);
8113
+ const { execSync } = await import('node:child_process');
8114
+
8115
+ const clampedDays = Math.min(Math.max(1, days), 365);
8116
+ const clampedMax = Math.min(Math.max(1, max_commits), 500);
8117
+
8118
+ const authorArg = author ? `--author="${author}"` : '';
8119
+ let gitRaw = '';
8120
+ try {
8121
+ gitRaw = execSync(
8122
+ `git -C "${workspace_path}" log --since="${clampedDays} days ago" ${authorArg} --oneline --format="%H|||%s|||%ai|||%ae" -n ${clampedMax}`,
8123
+ { timeout: 15000, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] },
8124
+ ).trim();
8125
+ } catch {
8126
+ return `❌ **brain_from_git failed**: Could not read git log from \`${workspace_path}\`.\nMake sure the path exists and is a git repository.`;
8127
+ }
8128
+
8129
+ if (!gitRaw) {
8130
+ return `📭 **No commits found** in the last ${clampedDays} days${author ? ` by "${author}"` : ''}.\nTry increasing \`days\` or removing the \`author\` filter.`;
8131
+ }
8132
+
8133
+ const commitLines = gitRaw.split('\n');
8134
+ const now = new Date();
8135
+
8136
+ 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;
8137
+ const failRe = /\b(revert|broken|broke|breaking|rollback|undo|hotfix|crash|oops|wrong|mistake|bad)\b/i;
8138
+
8139
+ let stored = 0;
8140
+ let skipped = 0;
8141
+ let failures = 0;
8142
+ const topLessons: string[] = [];
8143
+
8144
+ for (const line of commitLines) {
8145
+ const parts = line.split('|||');
8146
+ if (parts.length < 2) continue;
8147
+ const [hash, msg, dateStr] = parts;
8148
+ if (!msg || !actionRe.test(msg)) continue;
8149
+
8150
+ const isFail = failRe.test(msg);
8151
+ const cleanMsg = msg
8152
+ .replace(/^(fix|feat|chore|docs|test|ci|perf|refactor|build|revert|hotfix|bump)[:(\s!]+/i, '')
8153
+ .trim();
8154
+ if (!cleanMsg || cleanMsg.length < 5) continue;
8155
+
8156
+ const slug = cleanMsg
8157
+ .toLowerCase()
8158
+ .replace(/[^a-z0-9]+/g, '-')
8159
+ .replace(/^-+|-+$/g, '')
8160
+ .slice(0, 40);
8161
+ if (!slug) continue;
8162
+
8163
+ const topic = `git:${slug}`;
8164
+ const key = `cachly:lesson:best:${topic}`;
8165
+ const existing = await redis.get(key);
8166
+ if (existing) { skipped++; continue; }
8167
+
8168
+ const lesson = {
8169
+ topic,
8170
+ outcome: isFail ? ('failure' as const) : ('success' as const),
8171
+ what_worked: isFail ? '' : cleanMsg.slice(0, 300),
8172
+ what_failed: isFail ? cleanMsg.slice(0, 300) : undefined,
8173
+ context: `Auto-learned from git commit ${(hash ?? '').slice(0, 7)}${dateStr ? ` at ${dateStr}` : ''} in ${workspace_path}${author ? ` (author: ${author})` : ''}`,
8174
+ severity: 'minor' as const,
8175
+ ts: dateStr ? new Date(dateStr).toISOString() : now.toISOString(),
8176
+ recall_count: 0,
8177
+ auto_learned: true,
8178
+ source: 'brain_from_git',
8179
+ version: 3,
8180
+ };
8181
+
8182
+ await redis.set(key, JSON.stringify(lesson));
8183
+ await redis.expire(key, 90 * 86400); // 90-day TTL for git-sourced lessons
8184
+ stored++;
8185
+ if (isFail) failures++;
8186
+ if (topLessons.length < 5) topLessons.push(` ${isFail ? '❌' : '✅'} \`${topic}\` — ${cleanMsg.slice(0, 80)}`);
8187
+ }
8188
+
8189
+ const lines: string[] = [
8190
+ `🧠 **Brain bootstrapped from git history**`,
8191
+ ``,
8192
+ `📊 **Scanned:** ${commitLines.filter(l => l.trim()).length} commits over last ${clampedDays} days`,
8193
+ `✅ **Success lessons stored:** ${stored - failures}`,
8194
+ `❌ **Failure/revert lessons:** ${failures}`,
8195
+ skipped > 0 ? `⏭️ **Already in brain (skipped):** ${skipped}` : '',
8196
+ ``,
8197
+ ];
8198
+
8199
+ if (topLessons.length > 0) {
8200
+ lines.push(`**Top lessons extracted:**`);
8201
+ lines.push(...topLessons);
8202
+ if (stored > 5) lines.push(` …and ${stored - 5} more`);
8203
+ lines.push('');
8204
+ }
8205
+
8206
+ lines.push(
8207
+ `💡 **Next step:** Call \`session_start(focus="...")\` to see these lessons in your briefing.`,
8208
+ `🔄 **Re-run anytime** — duplicates are safely skipped.`,
8209
+ );
8210
+
8211
+ return lines.filter(l => l !== '').join('\n');
8212
+ }
8213
+
7855
8214
  default:
7856
8215
  return null;
7857
8216
  }
package/test-features.cjs DELETED
@@ -1,23 +0,0 @@
1
- const fs = require('fs');
2
-
3
- const src = fs.readFileSync('src/index.ts', 'utf8');
4
- const initSrc = fs.readFileSync('../init/index.js', 'utf8');
5
- const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
6
-
7
- let pass = 0, fail = 0;
8
- function check(name, cond) {
9
- if (cond) { console.log('PASS:', name); pass++; }
10
- else { console.log('FAIL:', name); fail++; }
11
- }
12
-
13
- check('Test 1 - Ambient git learning in session_end', src.includes('git log --since') || src.includes('Ambient git'));
14
- check('Test 2a - author param in learn_from_attempts schema', src.includes('"author"') && src.includes('learn_from_attempts'));
15
- check('Test 2b - Team Telepathy in session_start', src.includes('Team Telepathy') || src.includes('team telepathy'));
16
- check('Test 3 - memory_crystalize tool', src.includes('memory_crystalize'));
17
- check('Test 4 - init team detection (~/.cachly-teams.json)', initSrc.includes('cachly-teams.json'));
18
- check('Test 5 - brain_doctor workspace_path param', src.includes('workspace_path') && src.includes('brain_doctor'));
19
- check('Bugfix - Pre-Warning uses splice(2,0)', src.includes('splice(2, 0'));
20
- check('Bugfix - No broken unshift in pre-warning', !src.includes('lines.unshift('));
21
- check('MCP version is 0.5.26', pkg.version === '0.5.26');
22
-
23
- console.log(`\n${pass}/${pass+fail} tests passed`);