@cachly-dev/mcp-server 0.10.27 → 0.10.41

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 +145 -115
  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 +146 -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 +143 -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');
@@ -347,8 +334,29 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
347
334
  const suggestPin = updatedLesson.recall_count === 3 && !lesson.pinned
348
335
  ? `📌 **You've looked this up 3 times.** Consider pinning it for instant access: add \`pinned: true\` via \`learn_from_attempts\` to always surface it first.`
349
336
  : '';
337
+ // ── Trust signal (today-safe consensus layer) ───────────────────────
338
+ // A lesson recalled many times — or confirmed by multiple distinct
339
+ // authors — has proven its value. Surface that as social proof.
340
+ const lessonAuthors = lesson.authors
341
+ ?? (lesson.author ? [lesson.author] : []);
342
+ const distinctAuthors = [...new Set(lessonAuthors.filter(Boolean))];
343
+ const rc = updatedLesson.recall_count;
344
+ let trustBadge = '';
345
+ if (distinctAuthors.length >= 2 && rc >= 5) {
346
+ trustBadge = `🏆 **Battle-tested** — recalled ${rc}× · verified by ${distinctAuthors.length} developers. Trust this.`;
347
+ }
348
+ else if (rc >= 10) {
349
+ trustBadge = `🏆 **Battle-tested** — recalled ${rc}×. This is one of your most-proven solutions.`;
350
+ }
351
+ else if (distinctAuthors.length >= 2) {
352
+ trustBadge = `✅ **Team-verified** — confirmed by ${distinctAuthors.length} developers.`;
353
+ }
354
+ else if (rc >= 5) {
355
+ trustBadge = `✅ **Proven** — recalled ${rc}× without contradiction.`;
356
+ }
350
357
  return [
351
358
  rememberWhen,
359
+ trustBadge,
352
360
  `${badge} **Best solution for \`${topic}\`** ${sevEmoji}${lesson.severity ? ` (${lesson.severity})` : ''} · recalled ${updatedLesson.recall_count}×`,
353
361
  ``,
354
362
  `**What worked:** ${lesson.what_worked}`,
@@ -636,19 +644,70 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
636
644
  const isFirstSession = !lastSession && lessons.length === 0 && ctxCount === 0;
637
645
  if (isFirstSession) {
638
646
  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")`');
647
+ if (workspace_path) {
648
+ lines.push(`🚀 **Auto-bootstrapping from your git history...** (this takes a few seconds)`);
649
+ lines.push(` Your brain will learn from your recent commits — no setup needed.`, '');
650
+ }
651
+ else {
652
+ lines.push('It learns from your work automatically. After your first session it will look like this:', '');
653
+ lines.push(' ✅ `api:auth` — Bearer token in header, not cookie; 401 on missing scope');
654
+ lines.push(' ✅ `database:migrations` — always run migrations before deploy');
655
+ lines.push(' ⚠️ `docker:build` — ARG changes bust all subsequent cache layers');
656
+ lines.push('');
657
+ lines.push('**Tip:** Pass `workspace_path` to `session_start` to auto-learn from git history instantly.');
658
+ lines.push('');
659
+ }
660
+ lines.push('**Your brain grows automatically:**');
661
+ lines.push(' • End each session → `session_end(summary="What I did")` — auto-learns from git commits');
662
+ lines.push(' • After fixing bugs → `learn_from_attempts(topic="...", outcome="success", what_worked="...")`');
648
663
  lines.push('');
649
- lines.push('💡 Run `brain_doctor` for a setup health-check and personalised tips.');
664
+ lines.push('💡 Run `brain_doctor` for a health-check and personalised tips.');
650
665
  lines.push('');
651
666
  }
667
+ // ── Team-virality: first-team-briefing wow moment ────────────────────
668
+ // When a user joins a team brain (has team lessons from colleagues but
669
+ // has never been briefed on them), show a dedicated "Welcome to your
670
+ // team's brain" section. Only fires once per user.
671
+ if (author && !isFirstSession) {
672
+ try {
673
+ const briefingKey = `cachly:team:first_briefing:${author}`;
674
+ const alreadyBriefed = await redis.get(briefingKey);
675
+ if (!alreadyBriefed) {
676
+ const teamLessons = lessons.filter(l => l.author && l.author !== author);
677
+ if (teamLessons.length > 0) {
678
+ // Mark briefed so this only fires once
679
+ await redis.set(briefingKey, '1', 'EX', 365 * 86400);
680
+ const byAuthor = new Map();
681
+ for (const l of teamLessons) {
682
+ const a = l.author;
683
+ if (!byAuthor.has(a))
684
+ byAuthor.set(a, []);
685
+ byAuthor.get(a).push(l);
686
+ }
687
+ lines.push('');
688
+ lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
689
+ lines.push('🤝 **Your team\'s AI brain has been briefing you.**');
690
+ lines.push('');
691
+ lines.push(`Your teammates have already solved problems you\'re about to hit:`);
692
+ lines.push('');
693
+ for (const [teamAuthor, tls] of byAuthor) {
694
+ lines.push(` 👤 **${teamAuthor}** fixed ${tls.length} thing${tls.length > 1 ? 's' : ''}:`);
695
+ for (const l of tls.slice(0, 2)) {
696
+ const emoji = l.outcome === 'success' ? '✅' : '⚠️';
697
+ lines.push(` ${emoji} \`${l.topic}\` — ${l.what_worked.slice(0, 90)}`);
698
+ }
699
+ if (tls.length > 2)
700
+ lines.push(` … and ${tls.length - 2} more lessons`);
701
+ }
702
+ lines.push('');
703
+ lines.push(`💡 Use \`team_learn\` after your next fix to pay it forward.`);
704
+ lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
705
+ lines.push('');
706
+ }
707
+ }
708
+ }
709
+ catch { /* team briefing errors must never break session_start */ }
710
+ }
652
711
  // Handoff from previous window (if any)
653
712
  const handoffRaw = await redis.get('cachly:session:handoff');
654
713
  if (handoffRaw) {
@@ -831,47 +890,40 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
831
890
  }
832
891
  lines.push('');
833
892
  }
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;
893
+ // Top lessons — sorted by recall_count desc (most-used = most proven value).
894
+ // If focus is set, focus-matched lessons already shown above; show remaining here.
895
+ if (lessons.length > 0) {
896
+ const byRecall = [...lessons].sort((a, b) => (b.recall_count ?? 0) - (a.recall_count ?? 0));
897
+ const topRecalled = byRecall.filter(l => (l.recall_count ?? 0) > 0).slice(0, 3);
898
+ const remaining = (focusLessons.length > 0 ? lessons.filter(l => !focusLessons.includes(l)) : lessons)
899
+ .filter(l => !topRecalled.includes(l))
900
+ .slice(0, focusLessons.length > 0 ? 3 : 4);
901
+ if (topRecalled.length > 0) {
902
+ lines.push(`🏆 **Most valuable** (recalled ${topRecalled[0].recall_count}× before):`);
903
+ for (const l of topRecalled) {
853
904
  const emoji = l.outcome === 'success' ? '✅' : l.outcome === 'partial' ? '⚠️' : '❌';
854
905
  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('');
906
+ const rc = l.recall_count ?? 0;
907
+ const la = l.authors
908
+ ?? (l.author ? [l.author] : []);
909
+ const nAuthors = new Set(la.filter(Boolean)).size;
910
+ const trust = (nAuthors >= 2 && rc >= 5) || rc >= 10 ? ' 🏆'
911
+ : nAuthors >= 2 || rc >= 5 ? ' ✅' : '';
912
+ const rct = rc > 1 ? ` _(${rc}× recalled)_` : '';
913
+ lines.push(` ${emoji}${sev}${trust} \`${l.topic}\`${rct} — ${l.what_worked.slice(0, 100)}`);
861
914
  }
915
+ lines.push('');
862
916
  }
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)}`);
917
+ if (remaining.length > 0) {
918
+ const header = topRecalled.length > 0 ? `🕐 **Recent:**` : `🕐 **Recent lessons:**`;
919
+ lines.push(header);
920
+ for (const l of remaining) {
921
+ const emoji = l.outcome === 'success' ? '✅' : l.outcome === 'partial' ? '⚠️' : '❌';
922
+ const sev = l.severity === 'critical' ? '🔴' : l.severity === 'major' ? '🟡' : '';
923
+ lines.push(` ${emoji}${sev} \`${l.topic}\` — ${l.what_worked.slice(0, 100)}`);
924
+ }
925
+ lines.push('');
873
926
  }
874
- lines.push('');
875
927
  }
876
928
  else {
877
929
  lines.push('📭 No lessons yet. Use `learn_from_attempts` after solving tasks.', '');
@@ -1328,21 +1380,33 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
1328
1380
  catch { /* git not available or not a repo — silent skip */ }
1329
1381
  }
1330
1382
  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 */ }
1383
+ const totalAutoLearned = autoLearned.length + ambientLearned.length;
1384
+ // ── Shareable Session Summary Card ────────────────────────────────────────
1385
+ // Generated after each session so the user can share their progress.
1386
+ const tweetLines = [];
1387
+ if (durationMin !== undefined && durationMin > 0)
1388
+ tweetLines.push(`⏱ ${durationMin} min session`);
1389
+ if (totalAutoLearned > 0)
1390
+ tweetLines.push(`🧠 ${totalAutoLearned} lessons saved to Brain`);
1391
+ if (files_changed.length > 0)
1392
+ tweetLines.push(`📁 ${files_changed.length} file${files_changed.length > 1 ? 's' : ''} changed`);
1393
+ const tweetBody = tweetLines.length > 0
1394
+ ? `${tweetLines.join(' · ')}\n\nMy AI Brain remembers this so I never repeat it. @cachlydev\ncachly.dev`
1395
+ : `Session saved to my AI Brain. No more re-explaining this tomorrow. @cachlydev\ncachly.dev`;
1396
+ const tweetURL = `https://twitter.com/intent/tweet?text=${encodeURIComponent(tweetBody)}`;
1397
+ const sessionCard = [
1398
+ ``,
1399
+ `┌─────────────────────────────────────────────┐`,
1400
+ `│ 🧠 Session Summary Card │`,
1401
+ `│ Share your progress — cached forever │`,
1402
+ `├─────────────────────────────────────────────┤`,
1403
+ durationMin !== undefined ? `│ ⏱ Duration : ${String(durationMin + ' min').padEnd(26)}│` : '',
1404
+ totalAutoLearned > 0 ? `│ 📚 Learned : ${String(totalAutoLearned + ' lessons').padEnd(26)}│` : '',
1405
+ files_changed.length > 0 ? `│ 📁 Changed : ${String(files_changed.length + ' file' + (files_changed.length > 1 ? 's' : '')).padEnd(26)}│` : '',
1406
+ `├─────────────────────────────────────────────┤`,
1407
+ `│ 📣 Share: ${tweetURL.slice(0, 34).padEnd(34)}│`,
1408
+ `└─────────────────────────────────────────────┘`,
1409
+ ].filter(l => l !== '').join('\n');
1346
1410
  return [
1347
1411
  `✅ **Session saved**${durationStr}`,
1348
1412
  ``,
@@ -1351,7 +1415,7 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
1351
1415
  lessons_learned !== undefined ? `🧠 **Lessons stored:** ${lessons_learned}` : '',
1352
1416
  autoLearned.length > 0 ? `🤖 **Auto-learned:** ${autoLearned.length} lessons extracted from summary (${autoLearned.slice(0, 3).map(t => `\`${t}\``).join(', ')}${autoLearned.length > 3 ? '…' : ''})` : '',
1353
1417
  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,
1418
+ sessionCard,
1355
1419
  ``,
1356
1420
  `💡 Next session: \`session_start(focus="...")\` to see this summary.`,
1357
1421
  ].filter(l => l !== '').join('\n');