@cachly-dev/mcp-server 0.10.27 → 0.10.40

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.
Files changed (79) hide show
  1. package/.env.test.example +23 -0
  2. package/.github/ISSUE_TEMPLATE/bug_report.md +50 -0
  3. package/.github/ISSUE_TEMPLATE/config.yml +8 -0
  4. package/.github/ISSUE_TEMPLATE/feature_request.md +25 -0
  5. package/.github/workflows/publish.yml +72 -0
  6. package/CHANGELOG.md +252 -0
  7. package/LAUNCH_TEXTS.md +182 -0
  8. package/LICENSE +202 -0
  9. package/README.md +142 -116
  10. package/dist/__tests__/auth.test.d.ts +1 -0
  11. package/dist/__tests__/auth.test.js +107 -0
  12. package/dist/__tests__/auth.test.js.map +1 -0
  13. package/dist/__tests__/e2e/api-auth.test.d.ts +7 -0
  14. package/dist/__tests__/e2e/api-auth.test.js +162 -0
  15. package/dist/__tests__/e2e/api-auth.test.js.map +1 -0
  16. package/dist/__tests__/e2e/api-health.test.d.ts +7 -0
  17. package/dist/__tests__/e2e/api-health.test.js +95 -0
  18. package/dist/__tests__/e2e/api-health.test.js.map +1 -0
  19. package/dist/__tests__/e2e/cli.test.d.ts +10 -0
  20. package/dist/__tests__/e2e/cli.test.js +204 -0
  21. package/dist/__tests__/e2e/cli.test.js.map +1 -0
  22. package/dist/__tests__/e2e/config.d.ts +35 -0
  23. package/dist/__tests__/e2e/config.js +53 -0
  24. package/dist/__tests__/e2e/config.js.map +1 -0
  25. package/dist/__tests__/e2e/funnel.test.d.ts +13 -0
  26. package/dist/__tests__/e2e/funnel.test.js +90 -0
  27. package/dist/__tests__/e2e/funnel.test.js.map +1 -0
  28. package/dist/__tests__/e2e/mcp-tools.test.d.ts +7 -0
  29. package/dist/__tests__/e2e/mcp-tools.test.js +129 -0
  30. package/dist/__tests__/e2e/mcp-tools.test.js.map +1 -0
  31. package/dist/__tests__/e2e/provisioning.test.d.ts +15 -0
  32. package/dist/__tests__/e2e/provisioning.test.js +123 -0
  33. package/dist/__tests__/e2e/provisioning.test.js.map +1 -0
  34. package/dist/handlers/advanced.js +38 -227
  35. package/dist/handlers/advanced.js.map +1 -1
  36. package/dist/handlers/brain.js +119 -82
  37. package/dist/handlers/brain.js.map +1 -1
  38. package/dist/handlers/fedbrain.js +93 -44
  39. package/dist/handlers/fedbrain.js.map +1 -1
  40. package/dist/handlers/roadmap.js +8 -2
  41. package/dist/handlers/roadmap.js.map +1 -1
  42. package/dist/handlers/syndicate.js +15 -7
  43. package/dist/handlers/syndicate.js.map +1 -1
  44. package/dist/handlers/team.js +62 -50
  45. package/dist/handlers/team.js.map +1 -1
  46. package/dist/index.js +1264 -234
  47. package/dist/index.js.map +1 -1
  48. package/dist/tools.d.ts +3 -54
  49. package/dist/tools.js +8 -62
  50. package/dist/tools.js.map +1 -1
  51. package/eslint.config.js +16 -0
  52. package/glama.json +36 -0
  53. package/llms.txt +155 -0
  54. package/package.json +38 -32
  55. package/scripts/e2e-run.sh +160 -0
  56. package/scripts/postinstall.js +12 -12
  57. package/server.json +3 -9
  58. package/smithery.yaml +15 -8
  59. package/src/__tests__/auth.test.ts +147 -0
  60. package/src/__tests__/e2e/api-auth.test.ts +182 -0
  61. package/src/__tests__/e2e/api-health.test.ts +103 -0
  62. package/src/__tests__/e2e/cli.test.ts +228 -0
  63. package/src/__tests__/e2e/config.ts +53 -0
  64. package/src/__tests__/e2e/funnel.test.ts +99 -0
  65. package/src/__tests__/e2e/mcp-tools.test.ts +191 -0
  66. package/src/__tests__/e2e/provisioning.test.ts +128 -0
  67. package/src/handlers/advanced.ts +37 -217
  68. package/src/handlers/brain.ts +118 -78
  69. package/src/handlers/fedbrain.ts +88 -38
  70. package/src/handlers/roadmap.ts +7 -2
  71. package/src/handlers/syndicate.ts +13 -7
  72. package/src/handlers/team.ts +57 -33
  73. package/src/index.ts +1312 -257
  74. package/src/tools.ts +8 -66
  75. package/vitest.config.ts +12 -0
  76. package/dist/__tests__/brain-predict-failures.test.d.ts +0 -17
  77. package/dist/__tests__/brain-predict-failures.test.js +0 -339
  78. package/dist/__tests__/brain-predict-failures.test.js.map +0 -1
  79. package/src/__tests__/brain-predict-failures.test.ts +0 -430
@@ -5,13 +5,15 @@ import { safeJsonParse } from '../utils.js';
5
5
  import { keywordSearch, tokenize, splitMultiQuery, levenshtein, indexVocab as _indexVocab } from '../search.js';
6
6
  import { computeEmbedding, hasEmbedProvider } from '../embeddings.js';
7
7
  // ── Changelog (shown once per version in session_start) ──────────────────────
8
- const MCP_VERSION = '0.10.10';
8
+ const MCP_VERSION = '0.10.0';
9
9
  const WHATS_NEW = {
10
- '0.10.10': [
10
+ '0.10.0': [
11
11
  `🆕 **What's new in v${MCP_VERSION}:**`,
12
- ` 📌 **pin_lesson / unpin_lesson** — Pin critical lessons to always show in session_start`,
13
- ` 📊 **brain_stats** — At-a-glance health dashboard (lessons, streak, roadmap, pinned, top topics)`,
14
- ` ✅ Pinned lessons appear at top of every session briefing — never decay, never forgotten`,
12
+ ` ✅ Zero-friction setup — auto-provisions your Brain, no manual config needed`,
13
+ ` ✅ 10× faster briefings — batch Redis fetches replace per-key loops`,
14
+ ` ✅ Crash-proof data reads — corrupted cache entries no longer break tools`,
15
+ ` ✅ \`session_end\` warns when called without an active session`,
16
+ ` ✅ \`get_api_status\` now shows live Redis ping + all instance statuses`,
15
17
  ],
16
18
  };
17
19
  export const BRAIN_TOOL_NAMES = new Set([
@@ -270,20 +272,6 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
270
272
  }
271
273
  }
272
274
  catch { /* CKG updates are non-critical */ }
273
- // ── Auto-pin critical lessons ──────────────────────────────────────────
274
- // severity=critical → always pin automatically so the lesson
275
- // appears in every session_start without the user ever calling pin_lesson.
276
- let autoPinNote = '';
277
- if (severity === 'critical' && (outcome === 'success' || outcome === 'partial')) {
278
- try {
279
- const alreadyPinned = await redis.exists(`cachly:lesson:pin:${topic}`);
280
- if (!alreadyPinned) {
281
- await redis.set(`cachly:lesson:pin:${topic}`, '1');
282
- autoPinNote = `📌 Auto-pinned (critical severity) — will appear in every session_start.`;
283
- }
284
- }
285
- catch { /* non-critical */ }
286
- }
287
275
  const emoji = outcome === 'success' ? '✅' : outcome === 'partial' ? '⚠️' : '❌';
288
276
  const sevEmoji = severity === 'critical' ? '🔴' : severity === 'major' ? '🟡' : '🟢';
289
277
  const action = isUpdate ? 'updated' : 'stored';
@@ -304,7 +292,6 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
304
292
  depends_on.length > 0
305
293
  ? `🔗 Depends on: ${depends_on.map(d => `\`${d}\``).join(', ')} → trace with \`trace_dependency\``
306
294
  : '',
307
- autoPinNote,
308
295
  ...templateWarnings,
309
296
  ...iWasWrongWarning,
310
297
  ].filter(l => l !== '').join('\n');
@@ -636,19 +623,70 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
636
623
  const isFirstSession = !lastSession && lessons.length === 0 && ctxCount === 0;
637
624
  if (isFirstSession) {
638
625
  lines.push('🎉 **Welcome! Your AI Brain is live.**', '');
639
- lines.push('It learns from your work automatically. After your first session it will look like this:', '');
640
- lines.push(' ✅ `api:auth` — Bearer token in header, not cookie; 401 on missing scope');
641
- lines.push(' ✅ `database:migrations` — always run migrations before deploy');
642
- lines.push(' ⚠️ `docker:build` — ARG changes bust all subsequent cache layers');
643
- lines.push('');
644
- lines.push('**3 steps to grow your brain:**');
645
- lines.push(' 1. Fix something → `learn_from_attempts(topic="bug:name", outcome="success", what_worked="...")`');
646
- lines.push(' 2. Save context → `remember_context(key="arch", value="...")`');
647
- lines.push(' 3. End session → `session_end(summary="What I did")`');
626
+ if (workspace_path) {
627
+ lines.push(`🚀 **Auto-bootstrapping from your git history...** (this takes a few seconds)`);
628
+ lines.push(` Your brain will learn from your recent commits — no setup needed.`, '');
629
+ }
630
+ else {
631
+ lines.push('It learns from your work automatically. After your first session it will look like this:', '');
632
+ lines.push(' ✅ `api:auth` — Bearer token in header, not cookie; 401 on missing scope');
633
+ lines.push(' ✅ `database:migrations` — always run migrations before deploy');
634
+ lines.push(' ⚠️ `docker:build` — ARG changes bust all subsequent cache layers');
635
+ lines.push('');
636
+ lines.push('**Tip:** Pass `workspace_path` to `session_start` to auto-learn from git history instantly.');
637
+ lines.push('');
638
+ }
639
+ lines.push('**Your brain grows automatically:**');
640
+ lines.push(' • End each session → `session_end(summary="What I did")` — auto-learns from git commits');
641
+ lines.push(' • After fixing bugs → `learn_from_attempts(topic="...", outcome="success", what_worked="...")`');
648
642
  lines.push('');
649
- lines.push('💡 Run `brain_doctor` for a setup health-check and personalised tips.');
643
+ lines.push('💡 Run `brain_doctor` for a health-check and personalised tips.');
650
644
  lines.push('');
651
645
  }
646
+ // ── Team-virality: first-team-briefing wow moment ────────────────────
647
+ // When a user joins a team brain (has team lessons from colleagues but
648
+ // has never been briefed on them), show a dedicated "Welcome to your
649
+ // team's brain" section. Only fires once per user.
650
+ if (author && !isFirstSession) {
651
+ try {
652
+ const briefingKey = `cachly:team:first_briefing:${author}`;
653
+ const alreadyBriefed = await redis.get(briefingKey);
654
+ if (!alreadyBriefed) {
655
+ const teamLessons = lessons.filter(l => l.author && l.author !== author);
656
+ if (teamLessons.length > 0) {
657
+ // Mark briefed so this only fires once
658
+ await redis.set(briefingKey, '1', 'EX', 365 * 86400);
659
+ const byAuthor = new Map();
660
+ for (const l of teamLessons) {
661
+ const a = l.author;
662
+ if (!byAuthor.has(a))
663
+ byAuthor.set(a, []);
664
+ byAuthor.get(a).push(l);
665
+ }
666
+ lines.push('');
667
+ lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
668
+ lines.push('🤝 **Your team\'s AI brain has been briefing you.**');
669
+ lines.push('');
670
+ lines.push(`Your teammates have already solved problems you\'re about to hit:`);
671
+ lines.push('');
672
+ for (const [teamAuthor, tls] of byAuthor) {
673
+ lines.push(` 👤 **${teamAuthor}** fixed ${tls.length} thing${tls.length > 1 ? 's' : ''}:`);
674
+ for (const l of tls.slice(0, 2)) {
675
+ const emoji = l.outcome === 'success' ? '✅' : '⚠️';
676
+ lines.push(` ${emoji} \`${l.topic}\` — ${l.what_worked.slice(0, 90)}`);
677
+ }
678
+ if (tls.length > 2)
679
+ lines.push(` … and ${tls.length - 2} more lessons`);
680
+ }
681
+ lines.push('');
682
+ lines.push(`💡 Use \`team_learn\` after your next fix to pay it forward.`);
683
+ lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
684
+ lines.push('');
685
+ }
686
+ }
687
+ }
688
+ catch { /* team briefing errors must never break session_start */ }
689
+ }
652
690
  // Handoff from previous window (if any)
653
691
  const handoffRaw = await redis.get('cachly:session:handoff');
654
692
  if (handoffRaw) {
@@ -831,47 +869,34 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
831
869
  }
832
870
  lines.push('');
833
871
  }
834
- // ── Pinned lessons — always shown regardless of age/confidence ───────────
835
- try {
836
- const pinKeys = [];
837
- const pinStream = redis.scanStream({ match: 'cachly:lesson:pin:*', count: 100 });
838
- await new Promise((res, rej) => {
839
- pinStream.on('data', (b) => pinKeys.push(...b));
840
- pinStream.on('end', res);
841
- pinStream.on('error', rej);
842
- });
843
- if (pinKeys.length > 0) {
844
- const pinnedTopics = pinKeys.map(k => k.replace('cachly:lesson:pin:', ''));
845
- const pinLessonRaws = await redis.mget(...pinnedTopics.map(t => `cachly:lesson:best:${t}`));
846
- const pinLines = [];
847
- for (const raw of pinLessonRaws) {
848
- if (!raw)
849
- continue;
850
- const l = safeJsonParse(raw, null);
851
- if (!l)
852
- continue;
872
+ // Top lessons — sorted by recall_count desc (most-used = most proven value).
873
+ // If focus is set, focus-matched lessons already shown above; show remaining here.
874
+ if (lessons.length > 0) {
875
+ const byRecall = [...lessons].sort((a, b) => (b.recall_count ?? 0) - (a.recall_count ?? 0));
876
+ const topRecalled = byRecall.filter(l => (l.recall_count ?? 0) > 0).slice(0, 3);
877
+ const remaining = (focusLessons.length > 0 ? lessons.filter(l => !focusLessons.includes(l)) : lessons)
878
+ .filter(l => !topRecalled.includes(l))
879
+ .slice(0, focusLessons.length > 0 ? 3 : 4);
880
+ if (topRecalled.length > 0) {
881
+ lines.push(`🏆 **Most valuable** (recalled ${topRecalled[0].recall_count}× before):`);
882
+ for (const l of topRecalled) {
853
883
  const emoji = l.outcome === 'success' ? '✅' : l.outcome === 'partial' ? '⚠️' : '❌';
854
884
  const sev = l.severity === 'critical' ? '🔴' : l.severity === 'major' ? '🟡' : '';
855
- pinLines.push(` 📌${emoji}${sev} \`${l.topic}\` — ${l.what_worked.slice(0, 100)}`);
856
- }
857
- if (pinLines.length > 0) {
858
- lines.push(`📌 **Pinned lessons (always shown):**`);
859
- lines.push(...pinLines);
860
- lines.push('');
885
+ const rct = l.recall_count && l.recall_count > 1 ? ` _(${l.recall_count}× recalled)_` : '';
886
+ lines.push(` ${emoji}${sev} \`${l.topic}\`${rct} — ${l.what_worked.slice(0, 100)}`);
861
887
  }
888
+ lines.push('');
862
889
  }
863
- }
864
- catch { /* non-critical */ }
865
- // Recent lessons
866
- if (lessons.length > 0) {
867
- lines.push(`🕐 **Recent lessons:**`);
868
- const toShow = focusLessons.length > 0 ? lessons.filter(l => !focusLessons.includes(l)).slice(0, 4) : lessons.slice(0, 5);
869
- for (const l of toShow) {
870
- const emoji = l.outcome === 'success' ? '✅' : l.outcome === 'partial' ? '⚠️' : '❌';
871
- const sev = l.severity === 'critical' ? '🔴' : l.severity === 'major' ? '🟡' : '';
872
- lines.push(` ${emoji}${sev} \`${l.topic}\` — ${l.what_worked.slice(0, 90)}`);
890
+ if (remaining.length > 0) {
891
+ const header = topRecalled.length > 0 ? `🕐 **Recent:**` : `🕐 **Recent lessons:**`;
892
+ lines.push(header);
893
+ for (const l of remaining) {
894
+ const emoji = l.outcome === 'success' ? '✅' : l.outcome === 'partial' ? '⚠️' : '❌';
895
+ const sev = l.severity === 'critical' ? '🔴' : l.severity === 'major' ? '🟡' : '';
896
+ lines.push(` ${emoji}${sev} \`${l.topic}\` — ${l.what_worked.slice(0, 100)}`);
897
+ }
898
+ lines.push('');
873
899
  }
874
- lines.push('');
875
900
  }
876
901
  else {
877
902
  lines.push('📭 No lessons yet. Use `learn_from_attempts` after solving tasks.', '');
@@ -1328,21 +1353,33 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
1328
1353
  catch { /* git not available or not a repo — silent skip */ }
1329
1354
  }
1330
1355
  const durationStr = durationMin !== undefined ? ` · ${durationMin} min` : '';
1331
- // ── Mini brain-stats snapshot at session end ───────────────────────────
1332
- let miniStatsLines = [];
1333
- try {
1334
- const totalLessons = await redis.keys('cachly:lesson:best:*').then(k => k.length).catch(() => 0);
1335
- const pinCount = await redis.keys('cachly:lesson:pin:*').then(k => k.length).catch(() => 0);
1336
- const streakRaw = await redis.get('cachly:streak:current');
1337
- const streak = streakRaw ? JSON.parse(streakRaw).days : 0;
1338
- const parts = [`📚 ${totalLessons} lesson${totalLessons !== 1 ? 's' : ''} total`];
1339
- if (pinCount > 0)
1340
- parts.push(`📌 ${pinCount} pinned`);
1341
- if (streak > 1)
1342
- parts.push(`🔥 ${streak}-day streak`);
1343
- miniStatsLines = [`🧠 **Brain:** ${parts.join(' · ')}`];
1344
- }
1345
- catch { /* non-critical */ }
1356
+ const totalAutoLearned = autoLearned.length + ambientLearned.length;
1357
+ // ── Shareable Session Summary Card ────────────────────────────────────────
1358
+ // Generated after each session so the user can share their progress.
1359
+ const tweetLines = [];
1360
+ if (durationMin !== undefined && durationMin > 0)
1361
+ tweetLines.push(`⏱ ${durationMin} min session`);
1362
+ if (totalAutoLearned > 0)
1363
+ tweetLines.push(`🧠 ${totalAutoLearned} lessons saved to Brain`);
1364
+ if (files_changed.length > 0)
1365
+ tweetLines.push(`📁 ${files_changed.length} file${files_changed.length > 1 ? 's' : ''} changed`);
1366
+ const tweetBody = tweetLines.length > 0
1367
+ ? `${tweetLines.join(' · ')}\n\nMy AI Brain remembers this so I never repeat it. @cachlydev\ncachly.dev`
1368
+ : `Session saved to my AI Brain. No more re-explaining this tomorrow. @cachlydev\ncachly.dev`;
1369
+ const tweetURL = `https://twitter.com/intent/tweet?text=${encodeURIComponent(tweetBody)}`;
1370
+ const sessionCard = [
1371
+ ``,
1372
+ `┌─────────────────────────────────────────────┐`,
1373
+ `│ 🧠 Session Summary Card │`,
1374
+ `│ Share your progress — cached forever │`,
1375
+ `├─────────────────────────────────────────────┤`,
1376
+ durationMin !== undefined ? `│ ⏱ Duration : ${String(durationMin + ' min').padEnd(26)}│` : '',
1377
+ totalAutoLearned > 0 ? `│ 📚 Learned : ${String(totalAutoLearned + ' lessons').padEnd(26)}│` : '',
1378
+ files_changed.length > 0 ? `│ 📁 Changed : ${String(files_changed.length + ' file' + (files_changed.length > 1 ? 's' : '')).padEnd(26)}│` : '',
1379
+ `├─────────────────────────────────────────────┤`,
1380
+ `│ 📣 Share: ${tweetURL.slice(0, 34).padEnd(34)}│`,
1381
+ `└─────────────────────────────────────────────┘`,
1382
+ ].filter(l => l !== '').join('\n');
1346
1383
  return [
1347
1384
  `✅ **Session saved**${durationStr}`,
1348
1385
  ``,
@@ -1351,7 +1388,7 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
1351
1388
  lessons_learned !== undefined ? `🧠 **Lessons stored:** ${lessons_learned}` : '',
1352
1389
  autoLearned.length > 0 ? `🤖 **Auto-learned:** ${autoLearned.length} lessons extracted from summary (${autoLearned.slice(0, 3).map(t => `\`${t}\``).join(', ')}${autoLearned.length > 3 ? '…' : ''})` : '',
1353
1390
  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 ? '…' : ''})` : '',
1354
- ...miniStatsLines,
1391
+ sessionCard,
1355
1392
  ``,
1356
1393
  `💡 Next session: \`session_start(focus="...")\` to see this summary.`,
1357
1394
  ].filter(l => l !== '').join('\n');