@cachly-dev/mcp-server 0.10.122 → 0.10.123

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 (55) hide show
  1. package/README.md +31 -15
  2. package/dist/src/ambient-hooks.d.ts +11 -2
  3. package/dist/src/ambient-hooks.js +11 -2
  4. package/dist/src/ambient-hooks.js.map +1 -1
  5. package/dist/src/ambient-ledger.js +2 -2
  6. package/dist/src/ambient-ledger.js.map +1 -1
  7. package/dist/src/ambient-memory.js +2 -2
  8. package/dist/src/ambient-memory.js.map +1 -1
  9. package/dist/src/auth.js +2 -1
  10. package/dist/src/auth.js.map +1 -1
  11. package/dist/src/brain-export.d.ts +40 -0
  12. package/dist/src/brain-export.js +89 -0
  13. package/dist/src/brain-export.js.map +1 -0
  14. package/dist/src/cachly-url.d.ts +68 -0
  15. package/dist/src/cachly-url.js +21 -0
  16. package/dist/src/cachly-url.js.map +1 -0
  17. package/dist/src/credentials.d.ts +37 -0
  18. package/dist/src/credentials.js +85 -0
  19. package/dist/src/credentials.js.map +1 -0
  20. package/dist/src/doctor.js +2 -1
  21. package/dist/src/doctor.js.map +1 -1
  22. package/dist/src/first-recall-proof.d.ts +19 -0
  23. package/dist/src/first-recall-proof.js +41 -0
  24. package/dist/src/first-recall-proof.js.map +1 -0
  25. package/dist/src/handlers/advanced.js +13 -29
  26. package/dist/src/handlers/advanced.js.map +1 -1
  27. package/dist/src/handlers/brain.js +132 -41
  28. package/dist/src/handlers/brain.js.map +1 -1
  29. package/dist/src/handlers/cache.js +3 -2
  30. package/dist/src/handlers/cache.js.map +1 -1
  31. package/dist/src/handlers/fedbrain.js +15 -4
  32. package/dist/src/handlers/fedbrain.js.map +1 -1
  33. package/dist/src/handlers/share.js +4 -3
  34. package/dist/src/handlers/share.js.map +1 -1
  35. package/dist/src/handlers/team.js +2 -1
  36. package/dist/src/handlers/team.js.map +1 -1
  37. package/dist/src/index.d.ts +32 -2
  38. package/dist/src/index.js +301 -75
  39. package/dist/src/index.js.map +1 -1
  40. package/dist/src/kostprobe.d.ts +25 -0
  41. package/dist/src/kostprobe.js +125 -0
  42. package/dist/src/kostprobe.js.map +1 -0
  43. package/dist/src/telemetry-types.d.ts +1 -1
  44. package/dist/src/tools.d.ts +0 -99
  45. package/dist/src/tools.js +0 -110
  46. package/dist/src/tools.js.map +1 -1
  47. package/dist/src/toolspecs.d.ts +1 -1
  48. package/dist/src/toolspecs.js +1 -1
  49. package/dist/src/upgrade-nudge.d.ts +15 -0
  50. package/dist/src/upgrade-nudge.js +29 -0
  51. package/dist/src/upgrade-nudge.js.map +1 -0
  52. package/glama.json +1 -1
  53. package/package.json +2 -2
  54. package/scripts/postinstall.js +1 -1
  55. package/server.json +3 -3
@@ -1,4 +1,5 @@
1
1
  import { execSync } from 'node:child_process';
2
+ import { createRequire } from 'node:module';
2
3
  import { calculateConfidence, confidenceBadge, STRUCTURED_TEMPLATES, CONFIDENCE_WARN_VALUE, CONFIDENCE_STALE_VALUE, CONFIDENCE_WARN_DAYS } from '../confidence.js';
3
4
  import { ckgSlug, extractProblemConcept, ckgUpsertNode, ckgUpdateEdge, ckgUpsertPersonNode, ckgUpsertFileNode, ckgRecordCollaboration, ckgUpsertServiceNode } from '../ckg.js';
4
5
  import { safeJsonParse, scanKeys } from '../utils.js';
@@ -6,15 +7,47 @@ import { getRole, ROLE_BADGE, getScopes, lessonVisibleToScope, reviewModeEnabled
6
7
  import { keywordSearch, tokenize, splitMultiQuery, levenshtein, indexVocab as _indexVocab } from '../search.js';
7
8
  import { rerankByQuality, qualityMultiplier, extractLessonQuality } from '../rerank.js';
8
9
  import { computeEmbedding, hasEmbedProvider } from '../embeddings.js';
10
+ import { upgradeNudge } from '../upgrade-nudge.js';
11
+ import { cachlyUrl } from '../cachly-url.js';
9
12
  // ── Changelog (shown once per version in session_start) ──────────────────────
10
- const MCP_VERSION = '0.10.119';
13
+ // Resolve the package version at runtime from package.json so the session briefing
14
+ // `version` field always matches the published npm version. A hardcoded constant
15
+ // silently drifted (0.10.119 vs published 0.10.122), making new-feature adoption
16
+ // impossible to track. Paths cover both prod (dist/handlers/brain.js → ../../../) and
17
+ // dev (src/handlers/brain.ts → ../../../). Falls back to a literal if resolution fails.
18
+ const CURRENT_VERSION = (() => {
19
+ try {
20
+ const req = createRequire(import.meta.url);
21
+ for (const rel of ['../../../package.json', '../../package.json']) {
22
+ try {
23
+ const pkg = req(rel);
24
+ if (pkg.name === '@cachly-dev/mcp-server' && pkg.version)
25
+ return pkg.version;
26
+ }
27
+ catch { /* try next candidate */ }
28
+ }
29
+ }
30
+ catch { /* fall through to literal */ }
31
+ return '0.10.100';
32
+ })();
33
+ // Resolve the total MCP tools count at runtime from generated capabilities.
34
+ // This count is shown in session_start's What's-New briefing for the current version.
35
+ // Falls back silently per CONTRACT: better to skip the count than show a wrong number.
36
+ const getTotalMcpTools = () => {
37
+ try {
38
+ const req = createRequire(import.meta.url);
39
+ const caps = req('../../../CACHLY_CAPABILITIES.json');
40
+ return caps.mcp?.total_tools ?? null;
41
+ }
42
+ catch { /* fail silently */ }
43
+ return null;
44
+ };
11
45
  const WHATS_NEW = {
12
46
  '0.10.119': [
13
47
  `🔍 **Explainable briefings — provenance in every warning**`,
14
48
  ` 📎 \`/briefing\` warnings now carry \`outcome\`, \`author\`, \`learned_at\` and \`matched_on\` (the context tokens that triggered the warning) — editors can show WHY a fix hint fired, not just what it claims.`,
15
49
  ` 🧰 VS Code 0.12.0 uses this: severity + confidence in the toast, full lesson card with provenance, "Not helpful" per-file suppression, reviewable CLS auto-learns.`,
16
50
  ` 🧹 CI docs now describe exactly what ships: scan mode uses the REST \`/scan\` endpoint, the GitLab template covers learn/scan/confirm.`,
17
- ` 📊 126 MCP tools`,
18
51
  ],
19
52
  '0.10.115': [
20
53
  `🔧 **\`brain_get_pref\` + \`brain_set_pref\` — persistent Brain preferences**`,
@@ -332,7 +365,7 @@ const WHATS_NEW = {
332
365
  `✅ Build: 0 errors, 0 warnings · 379 tests green`,
333
366
  ],
334
367
  '0.10.57': [
335
- `🆕 **What's new in v${MCP_VERSION}:**`,
368
+ `🆕 **What's new in v${CURRENT_VERSION}:**`,
336
369
  ` 🔧 **Critical fix** — corrected the npm bin/main entry path (\`dist/src/index.js\`); 0.10.50–0.10.52 shipped a broken entry point`,
337
370
  ` 🔧 Version hygiene — synced package.json, server.json, lockfile, MCP_VERSION; \`latest\` now points forward again`,
338
371
  ` ✅ Accurate tool count (95) across README, server.json, CLI banners`,
@@ -379,8 +412,18 @@ export const BRAIN_TOOL_NAMES = new Set([
379
412
  // DEPTH gate (hide the tail), never an access wall: the #1 result is always
380
413
  // returned. Usage/limit come from the same authoritative numbers as the Brain
381
414
  // Health bar (GET /instances/:id/memory), cached briefly to avoid per-call latency.
382
- const UPGRADE_URL = 'https://cachly.dev/billing';
415
+ const UPGRADE_URL = cachlyUrl('/billing', 'upgrade');
383
416
  const _recallGateCache = new Map();
417
+ /**
418
+ * Instances whose notice has already been shown in this process.
419
+ *
420
+ * Once per PROCESS, not once per call: the MCP server lives for the length of
421
+ * the editor session, so this is exactly "once per session". Without it the
422
+ * notice would repeat on every session_start — and with Ambient Recall that is
423
+ * every prompt. That is the nagging the upgrade nudge was built to avoid, and
424
+ * a line that appears every time stops being read within a day.
425
+ */
426
+ const _noticeGezeigt = new Set();
384
427
  // ── Recall quota: ONE unit per recall CALL ───────────────────────────────────
385
428
  // The gate above reads a monthly counter that this function writes. It counts
386
429
  // calls, not lessons.
@@ -421,6 +464,7 @@ async function getRecallGate(instanceId, apiFetch) {
421
464
  limit,
422
465
  used,
423
466
  goodwillMessage: mem?.goodwill_message,
467
+ notice: mem?.notice,
424
468
  };
425
469
  _recallGateCache.set(instanceId, { gate, expiresAt: Date.now() + 60_000 });
426
470
  return gate;
@@ -1462,6 +1506,24 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
1462
1506
  const timeStr = h > 0 ? `${h}h${m > 0 ? ` ${m}m` : ''}` : `${m}m`;
1463
1507
  lines.push(`⏱️ **Brain saved you ~${timeStr} total** (time not re-researching known fixes)`, '');
1464
1508
  }
1509
+ // Upgrade nudge — surfaces the free-tier quota BEFORE the wall, once
1510
+ // per session and right next to the value it just proved.
1511
+ const recallGate = await getRecallGate(instance_id, apiFetch);
1512
+ const nudge = upgradeNudge({ used: recallGate.used, limit: recallGate.limit, savedMins: timeSavedMins });
1513
+ if (nudge)
1514
+ lines.push(nudge, '');
1515
+ // Notice from the API — the only channel that reaches a WORKING account.
1516
+ // Everything else we can say today requires denying the user something
1517
+ // first (the 402 goodwill text arrived because it was an error), and most
1518
+ // accounts have no email address on file.
1519
+ //
1520
+ // Once per process, so once per editor session. Repeating it would put it
1521
+ // in front of the user on every prompt via Ambient Recall, and a line
1522
+ // that appears every time stops being read within a day.
1523
+ if (recallGate.notice && !_noticeGezeigt.has(instance_id)) {
1524
+ _noticeGezeigt.add(instance_id);
1525
+ lines.push(recallGate.notice, '');
1526
+ }
1465
1527
  // ── Welcome-back digest (gap ≥ 7 days) ──────────────────────────────────
1466
1528
  // When the user has been away a week or more, lead with a short "what
1467
1529
  // happened while you were gone" digest. All data is already loaded above
@@ -1491,11 +1553,16 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
1491
1553
  // ── What's New (shown once per version update) ──────────────────────────
1492
1554
  try {
1493
1555
  const seenVersion = await redis.get('cachly:mcp:version:last_seen');
1494
- if (seenVersion !== MCP_VERSION) {
1495
- await redis.set('cachly:mcp:version:last_seen', MCP_VERSION, 'EX', 365 * 86400);
1496
- const changelog = WHATS_NEW[MCP_VERSION];
1556
+ if (seenVersion !== CURRENT_VERSION) {
1557
+ await redis.set('cachly:mcp:version:last_seen', CURRENT_VERSION, 'EX', 365 * 86400);
1558
+ const changelog = WHATS_NEW[CURRENT_VERSION];
1497
1559
  if (changelog) {
1498
1560
  lines.push(...changelog, '');
1561
+ // For current version, append dynamic tool count if available
1562
+ const toolCount = getTotalMcpTools();
1563
+ if (toolCount) {
1564
+ lines.push(`📊 ${toolCount} MCP tools`, '');
1565
+ }
1499
1566
  }
1500
1567
  }
1501
1568
  }
@@ -2356,33 +2423,12 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
2356
2423
  await redis.expire(key, 90 * 86400);
2357
2424
  autoLearned.push(topic);
2358
2425
  }
2359
- // Also store a lesson per changed file area if files were changed
2360
- if (files_changed.length > 0) {
2361
- const areas = [...new Set(files_changed.map(f => f.split('/').slice(0, 2).join('/')))].slice(0, 3);
2362
- for (const area of areas) {
2363
- const slug = area.replace(/[^a-z0-9]/gi, '-').toLowerCase().replace(/-+/g, '-').slice(0, 30);
2364
- const topic = `auto:changed:${slug}`;
2365
- const key = `cachly:lesson:best:${topic}`;
2366
- const lesson = {
2367
- topic,
2368
- outcome: 'success',
2369
- what_worked: `Files changed in ${area}: ${files_changed.filter(f => f.startsWith(area.split('/')[0])).slice(0, 5).join(', ')}`,
2370
- context: summary.slice(0, 200),
2371
- severity: 'minor',
2372
- ts: now.toISOString(),
2373
- recall_count: 0,
2374
- auto_learned: true,
2375
- version: 2,
2376
- };
2377
- await redis.set(key, JSON.stringify(lesson));
2378
- await redis.expire(key, 90 * 86400);
2379
- autoLearned.push(topic);
2380
- }
2381
- }
2382
2426
  }
2383
2427
  catch { /* auto-learn errors must never break session_end */ }
2384
2428
  // ── 🌿 Ambient Git Learning ────────────────────────────────────────────────
2385
2429
  // Read git commits since session start → auto-learn each meaningful commit.
2430
+ // The commit BODY (%b) carries the reasoning — what was measured, what the
2431
+ // cause was, what trap it avoided. The subject (%s) alone is just a heading.
2386
2432
  const ambientLearned = [];
2387
2433
  if (workspace_path) {
2388
2434
  try {
@@ -2396,13 +2442,56 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
2396
2442
  } })()
2397
2443
  : '';
2398
2444
  const sinceArg = sessionStartTs ? `--since="${sessionStartTs}"` : '--since="1 hour ago"';
2399
- const gitOut = execSync(`git -C "${workspace_path}" log ${sinceArg} --oneline --format="%H|||%s|||%ai"`, { timeout: 5000, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
2445
+ // '|||ENDE' terminates one commit's record. A commit body can contain
2446
+ // blank lines and newlines of its own, so splitting the raw output on
2447
+ // '\n' would tear a single multi-line body apart into several fake
2448
+ // "commits" — splitting on the marker keeps each body intact.
2449
+ const gitOut = execSync(`git -C "${workspace_path}" log ${sinceArg} --oneline --format="%H|||%s|||%b|||%ai|||ENDE"`, { timeout: 5000, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
2400
2450
  if (gitOut) {
2401
2451
  const commitActionRe = /\b(fix|add|remove|refactor|migrate|update|resolve|implement|improve|optimize|configure|create|delete|disable|enable|debug|patch|upgrade|build|rewrite|deploy|feat|chore|docs|test|perf|ci)\b/i;
2402
- for (const line of gitOut.split('\n').slice(0, 10)) {
2403
- const [hash, msg, dateStr] = line.split('|||');
2452
+ const commitBlocks = gitOut.split('|||ENDE').map(b => b.trim()).filter(Boolean);
2453
+ for (const block of commitBlocks.slice(0, 10)) {
2454
+ const [hash, msg, rawBody, dateStr] = block.split('|||');
2455
+ // Files touched together in one commit are recorded as co-occurs
2456
+ // edges between file nodes in the graph ("who touches A usually
2457
+ // touches B") — that is independent of whether the commit message
2458
+ // itself is lesson-worthy, so it runs for every recent commit.
2459
+ if (hash) {
2460
+ try {
2461
+ // '--root' matters for a repo's very first commit: without it,
2462
+ // diff-tree has no parent to compare against and silently
2463
+ // reports zero changed files for that commit.
2464
+ const nameOut = execSync(`git -C "${workspace_path}" diff-tree --no-commit-id --name-only -r --root "${hash}"`, { timeout: 5000, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
2465
+ // Pairing is quadratic, so the cap decides the cost — and this
2466
+ // runs inside session_end, which must stay quick. At 12 files
2467
+ // that is 66 pairs; at 25 it would be 300, times up to ten
2468
+ // commits. Each file node is upserted ONCE up front instead of
2469
+ // once per pair: with 12 files that is 12 upserts rather than
2470
+ // 132, and it turns ~530 round-trips into ~144.
2471
+ const CO_OCCUR_MAX_FILES = 12;
2472
+ const changedFiles = nameOut
2473
+ ? nameOut.split('\n').map(f => f.trim()).filter(Boolean).slice(0, CO_OCCUR_MAX_FILES)
2474
+ : [];
2475
+ const fileIds = [];
2476
+ for (const f of changedFiles)
2477
+ fileIds.push(await ckgUpsertFileNode(redis, f));
2478
+ for (let i = 0; i < fileIds.length; i++) {
2479
+ for (let j = i + 1; j < fileIds.length; j++) {
2480
+ await ckgUpdateEdge(redis, fileIds[i], 'co-occurs', fileIds[j], true);
2481
+ await ckgUpdateEdge(redis, fileIds[j], 'co-occurs', fileIds[i], true);
2482
+ }
2483
+ }
2484
+ }
2485
+ catch { /* file co-occurrence is best-effort */ }
2486
+ }
2404
2487
  if (!msg || !commitActionRe.test(msg))
2405
2488
  continue;
2489
+ // A heading alone is not a lesson. Without a substantial body —
2490
+ // the why, the measurement, the trap — nothing gets stored: better
2491
+ // no entry than one that fills space and says nothing.
2492
+ const body = (rawBody ?? '').trim();
2493
+ if (body.length < 120)
2494
+ continue;
2406
2495
  const slug = msg
2407
2496
  .toLowerCase().replace(/^(fix|feat|chore|docs|test|ci|perf|refactor|build|revert)[:(\s]/i, '')
2408
2497
  .replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 40);
@@ -2416,7 +2505,7 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
2416
2505
  const commitLesson = {
2417
2506
  topic,
2418
2507
  outcome: 'success',
2419
- what_worked: msg.slice(0, 200),
2508
+ what_worked: `${msg}\n\n${body.slice(0, 1200)}`,
2420
2509
  context: `Auto-learned from git commit ${(hash ?? '').slice(0, 7)} at ${dateStr ?? ''} in ${workspace_path}`,
2421
2510
  severity: 'minor',
2422
2511
  ts: now.toISOString(),
@@ -3349,7 +3438,7 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
3349
3438
  cachly: {
3350
3439
  command: 'npx',
3351
3440
  args: ['@cachly-dev/mcp-server@latest'],
3352
- env: { CACHLY_INSTANCE_ID: instance_id },
3441
+ env: { CACHLY_JWT: '<cky_live_...>', CACHLY_BRAIN_INSTANCE_ID: instance_id },
3353
3442
  },
3354
3443
  },
3355
3444
  }, null, 2),
@@ -3364,7 +3453,7 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
3364
3453
  cachly: {
3365
3454
  command: 'npx',
3366
3455
  args: ['@cachly-dev/mcp-server@latest'],
3367
- env: { CACHLY_INSTANCE_ID: instance_id },
3456
+ env: { CACHLY_JWT: '<cky_live_...>', CACHLY_BRAIN_INSTANCE_ID: instance_id },
3368
3457
  },
3369
3458
  },
3370
3459
  }, null, 2),
@@ -3379,7 +3468,7 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
3379
3468
  cachly: {
3380
3469
  command: 'npx',
3381
3470
  args: ['@cachly-dev/mcp-server@latest'],
3382
- env: { CACHLY_INSTANCE_ID: instance_id },
3471
+ env: { CACHLY_JWT: '<cky_live_...>', CACHLY_BRAIN_INSTANCE_ID: instance_id },
3383
3472
  },
3384
3473
  },
3385
3474
  }, null, 2),
@@ -3395,7 +3484,7 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
3395
3484
  type: 'stdio',
3396
3485
  command: 'npx',
3397
3486
  args: ['@cachly-dev/mcp-server@latest'],
3398
- env: { CACHLY_INSTANCE_ID: instance_id },
3487
+ env: { CACHLY_JWT: '<cky_live_...>', CACHLY_BRAIN_INSTANCE_ID: instance_id },
3399
3488
  },
3400
3489
  },
3401
3490
  }, null, 2),
@@ -3410,7 +3499,7 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
3410
3499
  cachly: {
3411
3500
  command: 'npx',
3412
3501
  args: ['@cachly-dev/mcp-server@latest'],
3413
- env: { CACHLY_INSTANCE_ID: instance_id },
3502
+ env: { CACHLY_JWT: '<cky_live_...>', CACHLY_BRAIN_INSTANCE_ID: instance_id },
3414
3503
  },
3415
3504
  },
3416
3505
  }, null, 2),
@@ -3424,7 +3513,7 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
3424
3513
  context_servers: {
3425
3514
  cachly: {
3426
3515
  command: { path: 'npx', args: ['@cachly-dev/mcp-server@latest'] },
3427
- env: { CACHLY_INSTANCE_ID: instance_id },
3516
+ env: { CACHLY_JWT: '<cky_live_...>', CACHLY_BRAIN_INSTANCE_ID: instance_id },
3428
3517
  },
3429
3518
  },
3430
3519
  }, null, 2),
@@ -3439,7 +3528,7 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
3439
3528
  {
3440
3529
  name: 'cachly',
3441
3530
  command: 'npx @cachly-dev/mcp-server@latest',
3442
- env: { CACHLY_INSTANCE_ID: instance_id },
3531
+ env: { CACHLY_JWT: '<cky_live_...>', CACHLY_BRAIN_INSTANCE_ID: instance_id },
3443
3532
  },
3444
3533
  ],
3445
3534
  }, null, 2),
@@ -3452,6 +3541,8 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
3452
3541
  ``,
3453
3542
  `**Your Brain ID:** \`${instance_id}\``,
3454
3543
  ``,
3544
+ `Replace \`<cky_live_...>\` below with your own API key (get one at https://cachly.dev/setup-ai) — the Brain ID alone does not authenticate you.`,
3545
+ ``,
3455
3546
  `This Brain ID is model-agnostic. Every lesson you've learned, every Memory Crystal you've distilled,`,
3456
3547
  `and every prediction from your Causal Knowledge Graph is accessible from **any MCP-compatible AI client**.`,
3457
3548
  ``,