@cachly-dev/mcp-server 0.10.111 → 0.10.114

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
@@ -101,6 +101,26 @@ Like what you see? Make it permanent in the next step.
101
101
 
102
102
  ---
103
103
 
104
+ ## Brain-first — Semantic Cache as Proof-Point
105
+
106
+ cachly is not a semantic cache with a brain bolt-on. The Brain is the product. The
107
+ Semantic Cache is the **proof-point** — it shows ROI in dollars from day one, with zero
108
+ trust required. It opens the door. The Brain is why teams never leave.
109
+
110
+ | | Wedge — Land | Moat — Retain |
111
+ |---|---|---|
112
+ | **Feature** | Semantic Cache | AI Brain (Lessons, Recall, Team-Sharing) |
113
+ | **Value** | Measurable cost savings from day one | Compounding team intelligence |
114
+ | **Metric** | Cache-hit rate, $/month saved | Lessons retained, WoW trend, recall quality |
115
+ | **Analogy** | Datadog APM (surfaces the problem) | Stripe (becomes critical infrastructure) |
116
+
117
+ **The org-level advantage:** Brain lessons and cache hits are shared across the whole
118
+ team — one person's fix becomes every agent's reflex. Anthropic Projects Memory is
119
+ per-user and model-locked. cachly is team-wide and model-neutral. That's the structural
120
+ moat no first-party tool can build.
121
+
122
+ ---
123
+
104
124
  ## Setup — one command
105
125
 
106
126
  ```bash
@@ -229,7 +249,7 @@ causal_trace(problem="auth breaks after restart")
229
249
  npx @cachly-dev/mcp-server@latest autopilot # One command — signs in, configures every editor, bootstraps from git
230
250
  npx @cachly-dev/mcp-server@latest demo # Preview your Brain (no account needed)
231
251
  npx @cachly-dev/mcp-server@latest bench # Recall quality vs flat-file memory (no auth required)
232
- npx @cachly-dev/mcp-server@latest setup # Interactive variant — pick editors yourself
252
+ npx @cachly-dev/mcp-server@latest autosetup # Interactive variant — pick editors yourself
233
253
  npx @cachly-dev/mcp-server@latest health # Check token, API, editors, git hook
234
254
  npx @cachly-dev/mcp-server@latest digest # Weekly Brain summary — shareable
235
255
  npx @cachly-dev/mcp-server@latest share # Generate a shareable stats card + tweet
@@ -378,17 +398,17 @@ following. Manual snippets are in the **Manual Setup** section below.
378
398
  | **Cline** | ✅ detected via VS Code | `.vscode/mcp.json` | — | Shares config with Copilot; restart VS Code |
379
399
  | **Continue.dev** | ✅ detected via `.continue/` | `.continue/config.json` | — | Uses `modelContextProtocolServers` key |
380
400
  | **Zed** | ✅ detected via `.zed/` | `.zed/settings.json` | — | Uses `context_servers` key |
381
- | **Windsurf (global)** | `setup --editor windsurf` | `~/.windsurf/mcp.json` | ✅ | Pass `--editor` to target global config |
382
- | **Any other MCP client** | `init --editor claude` | `.mcp.json` | — | Standard `mcpServers` stdio format |
401
+ | **Windsurf (global)** | `autosetup --editor windsurf` | `~/.windsurf/mcp.json` | ✅ | Pass `--editor` to target global config |
402
+ | **Any other MCP client** | `autosetup --editor claude` | `.mcp.json` | — | Standard `mcpServers` stdio format |
383
403
 
384
404
  **Which sign-in path each editor uses:**
385
405
 
386
406
  | Scenario | Path |
387
407
  |---|---|
388
- | `setup` from a real terminal (TTY) | OAuth device-flow → browser click → API key saved automatically |
389
- | `setup` from VSCode task / CI (non-TTY) | Auto-detects non-interactive, opens browser with step-by-step guide, prints `CACHLY_JWT=... setup` instruction |
408
+ | `autosetup` from a real terminal (TTY) | OAuth device-flow → browser click → API key saved automatically |
409
+ | `autosetup` from VSCode task / CI (non-TTY) | Auto-detects non-interactive, opens browser with step-by-step guide, prints `CACHLY_JWT=... autosetup` instruction |
390
410
  | First tool call from Claude Code (no JWT yet) | Inline device-flow: MCP returns URL + code, browser opens automatically, next call proceeds |
391
- | `CACHLY_JWT=cky_live_xxx npx ... setup` | Skips auth step entirely, uses provided key |
411
+ | `CACHLY_JWT=cky_live_xxx npx ... autosetup` | Skips auth step entirely, uses provided key |
392
412
 
393
413
  > **Tip — fastest per-project setup from inside Claude Code:**
394
414
  > ```
@@ -480,12 +500,12 @@ private backend instead of `api.cachly.dev`:
480
500
  npx @cachly-dev/mcp-server@latest autopilot --api-url https://cachly.mycorp.internal
481
501
 
482
502
  # Or non-interactively
483
- npx @cachly-dev/mcp-server@latest init \
503
+ npx @cachly-dev/mcp-server@latest autosetup \
484
504
  --instance-id <uuid> --api-key <cky_live_...> \
485
505
  --api-url https://cachly.mycorp.internal
486
506
  ```
487
507
 
488
- `setup`/`init` bake `CACHLY_API_URL` into the editor config **only** when it differs
508
+ `autosetup` bakes `CACHLY_API_URL` into the editor config **only** when it differs
489
509
  from the default cloud — so default installs stay clean, and self-hosted installs
490
510
  keep talking to your backend on every editor launch. All data stays in your infra.
491
511
 
@@ -0,0 +1,21 @@
1
+ /** Bumped whenever the hook script changes so installers can upgrade old hooks. */
2
+ export declare const BRAIN_WATCH_HOOK_VERSION = "v1";
3
+ export type BrainWatchHookResult = 'written' | 'upgraded' | 'appended' | 'unchanged' | 'skipped-no-git';
4
+ /**
5
+ * Idempotently install/upgrade the brain_watch post-commit hook in `projectDir`.
6
+ * Never throws — returns a status string the caller can report. Safe to run
7
+ * repeatedly; a second call with the same version returns 'unchanged'.
8
+ */
9
+ export declare function installBrainWatchHook(projectDir: string, instanceId: string, apiKey?: string): Promise<{
10
+ result: BrainWatchHookResult;
11
+ hookPath: string;
12
+ }>;
13
+ /**
14
+ * Build the `.git/hooks/post-commit` shell script for brain_watch.
15
+ * Uses curl (not Node) so it works without npx. Runs in the background and
16
+ * always exits 0 — never blocks a commit.
17
+ *
18
+ * @param instanceId Cachly Brain instance to learn into.
19
+ * @param apiKey Optional cky_… key; if absent, the script reads $CACHLY_JWT from env.
20
+ */
21
+ export declare function buildBrainWatchHook(instanceId: string, apiKey?: string): string;
@@ -0,0 +1,86 @@
1
+ // Ambient git post-commit hook builder for Brain auto-learning.
2
+ //
3
+ // Installs a .git/hooks/post-commit script that POSTs the commit data
4
+ // directly to the cachly API via curl — no Node required at hook runtime,
5
+ // so it works even without npx or the MCP server installed locally.
6
+ //
7
+ // Safety contract:
8
+ // • exit 0 is ALWAYS the last line — the hook never blocks a commit.
9
+ // • All network calls run in the background (&) with 2>/dev/null.
10
+ // • Commit message / sha / files are never interpolated into JS source;
11
+ // they're passed as shell variables in the JSON body (curl --data-raw).
12
+ import { existsSync } from 'node:fs';
13
+ import { readFile, writeFile, chmod, mkdir } from 'node:fs/promises';
14
+ import { resolve } from 'node:path';
15
+ /** Bumped whenever the hook script changes so installers can upgrade old hooks. */
16
+ export const BRAIN_WATCH_HOOK_VERSION = 'v1';
17
+ /**
18
+ * Matches any previously-installed cachly brain_watch block (from its marker
19
+ * comment down to the trailing `exit 0`), optionally including a shebang.
20
+ * Used to upgrade old hooks in place rather than duplicating them.
21
+ */
22
+ const BRAIN_WATCH_BLOCK_RE = /(?:#!\/bin\/sh\n)?# cachly brain_watch[\s\S]*?\nexit 0\n?/;
23
+ /**
24
+ * Idempotently install/upgrade the brain_watch post-commit hook in `projectDir`.
25
+ * Never throws — returns a status string the caller can report. Safe to run
26
+ * repeatedly; a second call with the same version returns 'unchanged'.
27
+ */
28
+ export async function installBrainWatchHook(projectDir, instanceId, apiKey) {
29
+ if (!existsSync(resolve(projectDir, '.git'))) {
30
+ return { result: 'skipped-no-git', hookPath: '' };
31
+ }
32
+ const hookDir = resolve(projectDir, '.git', 'hooks');
33
+ const hookPath = resolve(hookDir, 'post-commit');
34
+ await mkdir(hookDir, { recursive: true });
35
+ const script = buildBrainWatchHook(instanceId, apiKey);
36
+ let existing = '';
37
+ try {
38
+ existing = await readFile(hookPath, 'utf-8');
39
+ }
40
+ catch { /* no existing hook */ }
41
+ if (!existing) {
42
+ await writeFile(hookPath, script + '\n', 'utf-8');
43
+ await chmod(hookPath, 0o755).catch(() => { });
44
+ return { result: 'written', hookPath };
45
+ }
46
+ if (existing.includes(`cachly brain_watch — Auto-Learn ${BRAIN_WATCH_HOOK_VERSION}`)) {
47
+ return { result: 'unchanged', hookPath };
48
+ }
49
+ if (BRAIN_WATCH_BLOCK_RE.test(existing)) {
50
+ const replaced = existing.replace(BRAIN_WATCH_BLOCK_RE, script);
51
+ await writeFile(hookPath, replaced.endsWith('\n') ? replaced : replaced + '\n', 'utf-8');
52
+ await chmod(hookPath, 0o755).catch(() => { });
53
+ return { result: 'upgraded', hookPath };
54
+ }
55
+ // Foreign hook with no brain_watch block → append ours.
56
+ await writeFile(hookPath, existing.trimEnd() + '\n\n' + script + '\n', 'utf-8');
57
+ await chmod(hookPath, 0o755).catch(() => { });
58
+ return { result: 'appended', hookPath };
59
+ }
60
+ /**
61
+ * Build the `.git/hooks/post-commit` shell script for brain_watch.
62
+ * Uses curl (not Node) so it works without npx. Runs in the background and
63
+ * always exits 0 — never blocks a commit.
64
+ *
65
+ * @param instanceId Cachly Brain instance to learn into.
66
+ * @param apiKey Optional cky_… key; if absent, the script reads $CACHLY_JWT from env.
67
+ */
68
+ export function buildBrainWatchHook(instanceId, apiKey) {
69
+ return [
70
+ `#!/bin/sh`,
71
+ `# cachly brain_watch — Auto-Learn ${BRAIN_WATCH_HOOK_VERSION}`,
72
+ `# Silently teaches your Brain after every commit — no manual brain_from_git needed.`,
73
+ ...(apiKey ? [`CACHLY_JWT="${apiKey}"`] : []),
74
+ `_BW_SHA=$(git rev-parse HEAD 2>/dev/null || echo "")`,
75
+ `_BW_MSG=$(git log -1 --pretty=%B 2>/dev/null | head -1 | cut -c1-200)`,
76
+ `_BW_FILES=$(git diff-tree --no-commit-id -r --name-only HEAD 2>/dev/null | tr '\\n' '\\t' | sed 's/\\t$//')`,
77
+ `(curl -s -o /dev/null -m 8 \\`,
78
+ ` -X POST "https://api.cachly.dev/api/v1/instances/${instanceId}/learn" \\`,
79
+ ` -H "Authorization: Bearer \${CACHLY_JWT}" \\`,
80
+ ` -H "Content-Type: application/json" \\`,
81
+ ` --data-raw "{\\"message\\":\\"$(echo "$_BW_MSG" | sed 's/"/\\\\"/g')\\",\\"sha\\":\\"$_BW_SHA\\",\\"files\\":[$(echo "$_BW_FILES" | tr '\\t' '\\n' | grep -v '^$' | sed 's/.*/\\"&\\"/' | tr '\\n' ',' | sed 's/,$//')],\\"source\\":\\"brain_watch\\"}" \\`,
82
+ `) 2>/dev/null &`,
83
+ `exit 0`,
84
+ ].join('\n');
85
+ }
86
+ //# sourceMappingURL=brain-watch-hook.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"brain-watch-hook.js","sourceRoot":"","sources":["../../src/brain-watch-hook.ts"],"names":[],"mappings":"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"}
@@ -7,8 +7,28 @@ import { keywordSearch, tokenize, splitMultiQuery, levenshtein, indexVocab as _i
7
7
  import { rerankByQuality } from '../rerank.js';
8
8
  import { computeEmbedding, hasEmbedProvider } from '../embeddings.js';
9
9
  // ── Changelog (shown once per version in session_start) ──────────────────────
10
- const MCP_VERSION = '0.10.111';
10
+ const MCP_VERSION = '0.10.114';
11
11
  const WHATS_NEW = {
12
+ '0.10.114': [
13
+ `📋 **\`brain_changelog\` — weekly knowledge digest in one call**`,
14
+ ` 📅 Generates a grouped Markdown changelog of lessons learned in the last N days.`,
15
+ ` 👥 Grouped by topic category, annotated with author, recall count, and confidence — paste directly into standup or Slack.`,
16
+ ` ⚙️ Params: \`days\` (default 7), \`max_lessons\` (default 30), \`include_failures\` (default true).`,
17
+ ` 📊 138 MCP tools`,
18
+ ],
19
+ '0.10.113': [
20
+ `📈 **Week-over-week savings trend in \`cache_stats\`**`,
21
+ ` 📅 \`cache_stats\` now shows **7-day hits** + **WoW trend %** (e.g. \`+23.0% vs prev week\`) so ROI momentum is visible, not just the cumulative total.`,
22
+ ` ⬇️ Goes red (📉) on drops, green (📈) on growth — first week always shows +100% (new).`,
23
+ ` 🔧 Backed by new \`CountHitsInWindow\` repo method + \`hits_last_7d\`, \`hits_prev_7d\`, \`week_over_week_pct\` fields in \`SavingsEstimate\`.`,
24
+ ` 📊 137 MCP tools`,
25
+ ],
26
+ '0.10.112': [
27
+ `⚙️ **Configurable cost_per_call_usd — accurate ROI for any LLM**`,
28
+ ` 💡 New \`set_cost_per_call\` tool: set your real per-call cost (claude-opus-4.8 → $0.02, gpt-5.5 → $0.015, claude-sonnet-4.6 → $0.009, claude-haiku-4.5 → $0.001) so savings estimates in \`cache_stats\` reflect your actual bill.`,
29
+ ` 🔧 New API: \`PUT /api/v1/instances/:id/cost-per-call\` — persisted per instance, used by \`cache_stats\`, org savings, and SSE analytics stream.`,
30
+ ` 📊 137 MCP tools`,
31
+ ],
12
32
  '0.10.111': [
13
33
  `🏢 **Org-wide ROI + GitLab CI — the CFO number, aggregated**`,
14
34
  ` 💰 \`cache_org_stats\` now shows **org-wide Tokenmaxxing ROI**: total savings summed across every instance in your org, monthly projection, top-5 instances by savings. One number for the whole team.`,
@@ -335,7 +355,7 @@ export const BRAIN_TOOL_NAMES = new Set([
335
355
  'session_start', 'session_start_summary', 'session_end', 'session_ping', 'session_handoff', 'auto_learn_session',
336
356
  'brain_who_knows', 'brain_file_map', 'team_expertise_map',
337
357
  'skill_gaps', 'brain_coverage', 'brain_metrics', 'brain_service_map',
338
- 'brain_collab_pairs', 'brain_portability',
358
+ 'brain_collab_pairs', 'brain_portability', 'brain_changelog',
339
359
  ]);
340
360
  // ── Free-tier Teaser-Gate ──────────────────────────────────────────────────
341
361
  // When a Free-tier Brain crosses its recall limit, smart_recall still delivers
@@ -3252,6 +3272,82 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
3252
3272
  lines.push(`---`, ``, `### What travels with you`, ``, `| Data | Model-neutral? |`, `|------|---------------|`, `| Lessons (learn_from_attempts) | ✅ Full access in all clients |`, `| Memory Crystals (memory_crystalize) | ✅ Included in every session_start |`, `| Causal Knowledge Graph (brain_predict) | ✅ Same graph, same predictions |`, `| Team lessons (team_learn, team_confirm) | ✅ Team is per-Brain, not per-model |`, `| Domain Brain Marketplace installs | ✅ Installed once, available everywhere |`, `| Session handoffs (session_handoff) | ✅ Cross-client continuity |`, ``, `---`, ``, `### One-command setup for any new client`, ``, '```bash', `npx @cachly-dev/mcp-server@latest autopilot`, '```', ``, `Autopilot detects all installed editors and writes the config for each — with your \`${instance_id}\` Brain ID pre-filled.`, ``, `---`, `_Docs: cachly.dev/docs/model-neutral • Your Brain, Any Model_`);
3253
3273
  return lines.join('\n');
3254
3274
  }
3275
+ // ── brain_changelog ───────────────────────────────────────────────────────
3276
+ // Generates a human-readable Markdown changelog of recent lessons.
3277
+ // Useful for weekly standups, sprint reviews, or async team updates.
3278
+ case 'brain_changelog': {
3279
+ const { instance_id, days = 7, max_lessons = 30, include_failures = true, } = args;
3280
+ const redis = await getConnection(instance_id);
3281
+ const cutoff = Date.now() - Number(days) * 86_400_000;
3282
+ // Scan all lessons and collect recent ones.
3283
+ const keys = await scanKeys(redis, 'cachly:lesson:best:*', { max: 4000, timeoutMs: 4000 });
3284
+ const recent = [];
3285
+ for (const k of keys) {
3286
+ const raw = await redis.get(k).catch(() => null);
3287
+ if (!raw)
3288
+ continue;
3289
+ const lesson = safeJsonParse(raw, {});
3290
+ const ts = lesson.ts ? new Date(lesson.ts).getTime() : 0;
3291
+ if (ts < cutoff)
3292
+ continue;
3293
+ if (!include_failures && lesson.outcome === 'failure')
3294
+ continue;
3295
+ const topic = k.replace('cachly:lesson:best:', '');
3296
+ recent.push({ ...lesson, topic });
3297
+ }
3298
+ // Sort by timestamp desc, cap at max_lessons.
3299
+ recent.sort((a, b) => {
3300
+ const ta = a.ts ? new Date(a.ts).getTime() : 0;
3301
+ const tb = b.ts ? new Date(b.ts).getTime() : 0;
3302
+ return tb - ta;
3303
+ });
3304
+ const lessons = recent.slice(0, Number(max_lessons));
3305
+ if (lessons.length === 0) {
3306
+ return [
3307
+ `## 📋 Brain Changelog — last ${days} day${Number(days) !== 1 ? 's' : ''}`,
3308
+ ``,
3309
+ `_No new lessons learned in this window. Your Brain already knows everything it knows — add new lessons with \`learn_from_attempts\`._`,
3310
+ ].join('\n');
3311
+ }
3312
+ // Group by category (topic prefix before first ':').
3313
+ const groups = new Map();
3314
+ for (const l of lessons) {
3315
+ const cat = l.topic.includes(':') ? l.topic.split(':')[0] : 'general';
3316
+ const arr = groups.get(cat) ?? [];
3317
+ arr.push(l);
3318
+ groups.set(cat, arr);
3319
+ }
3320
+ const severityEmoji = (s) => s === 'critical' ? '🚨' : s === 'major' ? '⚠️' : s === 'minor' ? '💡' : '✨';
3321
+ const outcomeEmoji = (o) => o === 'failure' ? '❌' : o === 'partial' ? '🔶' : '✅';
3322
+ const dateStr = new Date().toISOString().slice(0, 10);
3323
+ const lines = [
3324
+ `## 📋 Brain Changelog — ${dateStr} (last ${days} day${Number(days) !== 1 ? 's' : ''})`,
3325
+ ``,
3326
+ `_${lessons.length} lesson${lessons.length !== 1 ? 's' : ''} learned across ${groups.size} topic area${groups.size !== 1 ? 's' : ''}_`,
3327
+ ``,
3328
+ ];
3329
+ for (const [cat, catLessons] of Array.from(groups.entries()).sort((a, b) => b[1].length - a[1].length)) {
3330
+ lines.push(`### ${cat}`);
3331
+ lines.push('');
3332
+ for (const l of catLessons) {
3333
+ const icon = l.outcome === 'failure' ? outcomeEmoji(l.outcome) : severityEmoji(l.severity);
3334
+ const body = l.outcome === 'failure' ? l.what_failed : l.what_worked;
3335
+ const meta = [];
3336
+ if (l.author)
3337
+ meta.push(`by **${l.author}**`);
3338
+ if (l.recall_count)
3339
+ meta.push(`${l.recall_count} recall${l.recall_count !== 1 ? 's' : ''}`);
3340
+ if (l.confidence !== undefined)
3341
+ meta.push(`${Math.round(l.confidence * 100)}% confidence`);
3342
+ lines.push(`- ${icon} **${l.topic}**${body ? ` — ${body}` : ''}`);
3343
+ if (meta.length)
3344
+ lines.push(` _${meta.join(' · ')}_`);
3345
+ }
3346
+ lines.push('');
3347
+ }
3348
+ lines.push('---', `_Generated from Brain \`${instance_id}\` · Share this in your standup or Slack_`);
3349
+ return lines.join('\n');
3350
+ }
3255
3351
  // ── sync_file_changes ─────────────────────────────────────────────────────
3256
3352
  default:
3257
3353
  return null;