@cachly-dev/mcp-server 0.10.71 → 0.10.73

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/README.md CHANGED
@@ -22,7 +22,7 @@
22
22
  <img src="https://img.shields.io/badge/GDPR-EU%20servers-green" alt="GDPR: EU servers" />
23
23
  </a>
24
24
  &nbsp;
25
- <img src="https://img.shields.io/badge/102%20MCP%20tools-violet" alt="102 MCP tools" />
25
+ <img src="https://img.shields.io/badge/107%20MCP%20tools-violet" alt="107 MCP tools" />
26
26
  &nbsp;
27
27
  <img src="https://img.shields.io/badge/License-Apache--2.0-yellow" alt="License: Apache-2.0" />
28
28
  </p>
@@ -2,12 +2,29 @@ import { execSync } from 'node:child_process';
2
2
  import { calculateConfidence, confidenceBadge, STRUCTURED_TEMPLATES, CONFIDENCE_WARN_VALUE, CONFIDENCE_STALE_VALUE, CONFIDENCE_WARN_DAYS } from '../confidence.js';
3
3
  import { ckgSlug, extractProblemConcept, ckgUpsertNode, ckgUpdateEdge, ckgUpsertPersonNode, ckgUpsertFileNode, ckgRecordCollaboration, ckgUpsertServiceNode } from '../ckg.js';
4
4
  import { safeJsonParse, scanKeys } from '../utils.js';
5
+ import { getRole, ROLE_BADGE, getScopes, lessonVisibleToScope } from './team.js';
5
6
  import { keywordSearch, tokenize, splitMultiQuery, levenshtein, indexVocab as _indexVocab } from '../search.js';
6
7
  import { rerankByQuality } from '../rerank.js';
7
8
  import { computeEmbedding, hasEmbedProvider } from '../embeddings.js';
8
9
  // ── Changelog (shown once per version in session_start) ──────────────────────
9
- const MCP_VERSION = '0.10.71';
10
+ const MCP_VERSION = '0.10.73';
10
11
  const WHATS_NEW = {
12
+ '0.10.73': [
13
+ `🔐 **Team visibility scopes + one-command init + external bench**`,
14
+ ` 🆕 \`team_grant_scope\` / \`team_scopes\` — scope lessons to a sub-team (group="security"); only members + admins recall them`,
15
+ ` 🏷️ \`learn_from_attempts(group="...")\` — orthogonal to private; smart_recall enforces it per requester`,
16
+ ` ⚡ \`init\` is now zero-arg + idempotent (reuses saved creds, only writes what changed, <60s)`,
17
+ ` 📦 \`npm run bench:external\` — prove recall-lift on a third-party-labeled corpus (portable JSON format)`,
18
+ ` 📊 107 MCP tools`,
19
+ ],
20
+ '0.10.72': [
21
+ `👑 **Role model — admin · reviewer · contributor · viewer (Phase 3)**`,
22
+ ` 🆕 \`team_assign_role\` — establish governance; first call bootstraps, admins manage the rest`,
23
+ ` 🆕 \`team_whoami\` — see your own role and capabilities`,
24
+ ` 🆕 \`team_roster\` — full team role table (👑🛡️✏️👁️)`,
25
+ ` 🛡️ \`team_confirm\` is now role-aware — admin/reviewer auto-get senior weight; contributor gets peer; no self-promotion`,
26
+ ` 👑 Setup CLI now prompts for governance bootstrap (idempotent). 105 MCP tools`,
27
+ ],
11
28
  '0.10.71': [
12
29
  `🛡️ **Self-healing auth** — no more silent "0 recalls because the token quietly died"`,
13
30
  ` ♻️ Near-expiry tokens auto-refresh into a long-lived key while still valid (zero interaction)`,
@@ -105,7 +122,7 @@ export const BRAIN_TOOL_NAMES = new Set([
105
122
  export async function handleBrainTool(name, args, getConnection, apiFetch) {
106
123
  switch (name) {
107
124
  case 'learn_from_attempts': {
108
- const { instance_id, topic, outcome, what_worked, what_failed = '', context: ctx = '', severity = 'major', file_paths = [], commands = [], tags = [], depends_on = [], author = '', visibility = 'team', service = '', service_kind = 'service', } = args;
125
+ const { instance_id, topic, outcome, what_worked, what_failed = '', context: ctx = '', severity = 'major', file_paths = [], commands = [], tags = [], depends_on = [], author = '', visibility = 'team', service = '', service_kind = 'service', group = '', } = args;
109
126
  const redis = await getConnection(instance_id);
110
127
  const ts = new Date().toISOString();
111
128
  // ── Structured template hints ──────────────────────────────────────────
@@ -224,6 +241,7 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
224
241
  depends_on,
225
242
  ...(author ? { author } : {}),
226
243
  ...(service ? { service } : {}),
244
+ ...(group ? { group: String(group).toLowerCase().trim() } : {}),
227
245
  visibility,
228
246
  recall_count: recallCount,
229
247
  ts,
@@ -576,6 +594,15 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
576
594
  ? rawContextFiles.filter((f) => typeof f === 'string')
577
595
  : [];
578
596
  const redis = await getConnection(instance_id);
597
+ // Team-level visibility scopes: resolve the requester's group memberships +
598
+ // admin status once, so group-scoped lessons only surface for members/admins.
599
+ let requesterScopes = new Set();
600
+ let requesterIsAdmin = false;
601
+ if (requester) {
602
+ requesterScopes = await getScopes(redis, instance_id, requester).catch(() => new Set());
603
+ const reqRole = await getRole(redis, instance_id, requester).catch(() => null);
604
+ requesterIsAdmin = reqRole === 'admin';
605
+ }
579
606
  // ── Layer 1: Keyword search across ALL brain data (always works, no embedding) ──
580
607
  const rawMatches = await keywordSearch(redis, ['cachly:ctx:*', 'cachly:lesson:best:*', 'cachly:idx:*'], query, 10);
581
608
  // ── Layer 1.5: Quality-aware rerank — proven success lessons outrank
@@ -749,13 +776,18 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
749
776
  }
750
777
  }
751
778
  }
752
- // Filter private lessons — they are only accessible via exact recall_best_solution
779
+ // Filter private lessons (recall_best_solution only) + enforce team scopes:
780
+ // group-scoped lessons surface only for members of that group (admins see all).
753
781
  const hybridResults = [...hybridMap.values()]
754
782
  .filter(r => {
755
783
  if (!r.key.startsWith('cachly:lesson:best:'))
756
784
  return true;
757
785
  const ld = safeJsonParse(r.content, {});
758
- return ld.visibility !== 'private';
786
+ if (ld.visibility === 'private')
787
+ return false;
788
+ if (!lessonVisibleToScope(ld.group, requesterScopes, requesterIsAdmin))
789
+ return false;
790
+ return true;
759
791
  })
760
792
  .sort((a, b) => b.hybridScore - a.hybridScore);
761
793
  // ── Build output ──────────────────────────────────────────────────────────
@@ -2068,7 +2100,10 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
2068
2100
  const conf = (p.avgConfidence * 100).toFixed(0);
2069
2101
  const daysSince = Math.floor((Date.now() - new Date(p.lastActive).getTime()) / 86400000);
2070
2102
  const recency = daysSince < 1 ? 'today' : daysSince < 7 ? `${daysSince}d ago` : daysSince < 30 ? `${Math.floor(daysSince / 7)}w ago` : `${Math.floor(daysSince / 30)}mo ago`;
2071
- lines.push(`${medal} **${p.handle}** — ${p.lessonCount} lesson${p.lessonCount !== 1 ? 's' : ''} · ${conf}% confidence · last active ${recency}`, ` _domains: ${p.domains}_`, ``);
2103
+ // Show role badge if assigned (non-blocking — role lookup is best-effort)
2104
+ const roleVal = await getRole(redis, instance_id, p.handle).catch(() => null);
2105
+ const roleBadge = roleVal ? ` ${ROLE_BADGE[roleVal]}` : '';
2106
+ lines.push(`${medal} **${p.handle}**${roleBadge} — ${p.lessonCount} lesson${p.lessonCount !== 1 ? 's' : ''} · ${conf}% confidence · last active ${recency}`, ` _domains: ${p.domains}_`, ``);
2072
2107
  }
2073
2108
  if (topCollaborators.length > 0 && topExpert) {
2074
2109
  lines.push(`🤝 **${topExpert.handle}** frequently works with: ${topCollaborators.map(h => `**${h}**`).join(', ')}`, ` _(ask them together — they've solved structurally similar things on shared files)_`, ``);