@cachly-dev/mcp-server 0.10.100 โ†’ 0.10.102

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
@@ -496,9 +496,14 @@ keep talking to your backend on every editor launch. All data stays in your infr
496
496
 
497
497
  ## ๐Ÿ› ๏ธ Ecosystem & Docs
498
498
 
499
+ **One brain, wherever you work.** Start with the MCP server, or drop the same memory
500
+ straight into your editor โ€” your lessons follow you across all of them.
501
+
499
502
  | Package | What it does |
500
503
  |---------|-------------|
501
- | **[`@cachly-dev/mcp-server`](https://www.npmjs.com/package/@cachly-dev/mcp-server)** | โ† you are here |
504
+ | **[`@cachly-dev/mcp-server`](https://www.npmjs.com/package/@cachly-dev/mcp-server)** | โ† you are here ยท works with Claude, Cursor, Copilot, Windsurf, Cline, Zed |
505
+ | **[Cachly Brain for VS Code](https://marketplace.visualstudio.com/items?itemName=cachly-dev.cachly-brain)** | One-click memory in the editor โ€” status bar, lessons view, ambient learning. No terminal needed. |
506
+ | **[Cachly Brain for JetBrains](https://plugins.jetbrains.com/plugin/32059-cachly-brain)** | Same brain for IntelliJ / PyCharm / GoLand / WebStorm / Rider โ€” status bar, brain health, lessons view. |
502
507
  | **[`@cachly-dev/openclaw`](https://www.npmjs.com/package/@cachly-dev/openclaw)** | Cut LLM costs 60โ€“90% in JS/TS apps |
503
508
 
504
509
  - ๐ŸŒ [cachly.dev](https://cachly.dev) โ€” Dashboard & free signup
@@ -0,0 +1,15 @@
1
+ /** Bumped whenever the hook script changes so installers can upgrade old hooks. */
2
+ export declare const CLS_HOOK_VERSION = "v2";
3
+ export type ClsHookResult = 'written' | 'upgraded' | 'appended' | 'unchanged' | 'skipped-no-git';
4
+ /**
5
+ * Idempotently install/upgrade the CLS post-commit hook in `projectDir`.
6
+ * Never throws โ€” returns a status the caller can log. Shared by `init`, `setup`
7
+ * and `autopilot` so all paths stay in lock-step.
8
+ */
9
+ export declare function installClsPostCommitHook(projectDir: string, instanceId: string, apiKey?: string): Promise<ClsHookResult>;
10
+ /**
11
+ * Build the `.git/hooks/post-commit` shell script.
12
+ * @param instanceId Cachly Brain instance to ingest into.
13
+ * @param apiKey Optional cky_โ€ฆ key embedded for authentication.
14
+ */
15
+ export declare function buildClsPostCommitHook(instanceId: string, apiKey?: string): string;
@@ -0,0 +1,94 @@
1
+ // Shared builder for the CLS (Continuous Learning Stream) git post-commit hook.
2
+ //
3
+ // History / why this is centralised:
4
+ // The previous inline versions interpolated the commit message directly into a
5
+ // `node -e "... message:'$MSG' ..."` JS string. Any commit message containing an
6
+ // apostrophe (e.g. "don't crash") produced invalid JavaScript, so the hook
7
+ // silently ingested nothing โ€” and a crafted message could inject shell/JS.
8
+ //
9
+ // This version is robust:
10
+ // โ€ข Commit message / sha / files are passed to node via ENVIRONMENT VARIABLES,
11
+ // never interpolated into JS source โ€” no quoting or injection breakage.
12
+ // โ€ข The JSON payload is handed to the CLI via execFileSync (no shell), so the
13
+ // shell never re-parses quotes, `$`, or backticks from the message.
14
+ // โ€ข CACHLY_JWT is embedded only when provided so the `cls-ingest` CLI command
15
+ // can authenticate; absent it, that command exits 0 silently.
16
+ import { existsSync } from 'node:fs';
17
+ import { readFile, writeFile, chmod, mkdir } from 'node:fs/promises';
18
+ import { resolve } from 'node:path';
19
+ /** Bumped whenever the hook script changes so installers can upgrade old hooks. */
20
+ export const CLS_HOOK_VERSION = 'v2';
21
+ /**
22
+ * Matches any previously-installed cachly CLS block (from its marker comment
23
+ * down to the trailing `exit 0`), optionally including an immediately-preceding
24
+ * standalone shebang. Used to upgrade old hooks in place rather than skipping
25
+ * them (the v1 block was a silent no-op).
26
+ */
27
+ const CLS_BLOCK_RE = /(?:#!\/bin\/sh\n)?# cachly CLS[\s\S]*?\nexit 0\n?/;
28
+ /**
29
+ * Idempotently install/upgrade the CLS post-commit hook in `projectDir`.
30
+ * Never throws โ€” returns a status the caller can log. Shared by `init`, `setup`
31
+ * and `autopilot` so all paths stay in lock-step.
32
+ */
33
+ export async function installClsPostCommitHook(projectDir, instanceId, apiKey) {
34
+ if (!existsSync(resolve(projectDir, '.git')))
35
+ return 'skipped-no-git';
36
+ const hookDir = resolve(projectDir, '.git', 'hooks');
37
+ const hookPath = resolve(hookDir, 'post-commit');
38
+ await mkdir(hookDir, { recursive: true });
39
+ const script = buildClsPostCommitHook(instanceId, apiKey);
40
+ let existing = '';
41
+ try {
42
+ existing = await readFile(hookPath, 'utf-8');
43
+ }
44
+ catch { /* no existing hook */ }
45
+ if (!existing) {
46
+ await writeFile(hookPath, script + '\n', 'utf-8');
47
+ await chmod(hookPath, 0o755).catch(() => { });
48
+ return 'written';
49
+ }
50
+ if (existing.includes(`cachly CLS โ€” Continuous Learning Stream ${CLS_HOOK_VERSION}`)) {
51
+ return 'unchanged';
52
+ }
53
+ if (CLS_BLOCK_RE.test(existing)) {
54
+ // Upgrade an older cachly block in place, preserving any surrounding hook.
55
+ const replaced = existing.replace(CLS_BLOCK_RE, script);
56
+ await writeFile(hookPath, replaced.endsWith('\n') ? replaced : replaced + '\n', 'utf-8');
57
+ await chmod(hookPath, 0o755).catch(() => { });
58
+ return 'upgraded';
59
+ }
60
+ // Foreign hook with no cachly block โ†’ append ours.
61
+ await writeFile(hookPath, existing.trimEnd() + '\n\n' + script + '\n', 'utf-8');
62
+ await chmod(hookPath, 0o755).catch(() => { });
63
+ return 'appended';
64
+ }
65
+ /** The node `-e` program โ€” reads commit data from env, ingests via the CLI. */
66
+ const CLS_NODE_PROGRAM = "try{" +
67
+ "var p={instance_id:process.env.CACHLY_BRAIN_INSTANCE_ID,source:'git_commit'," +
68
+ "payload:{message:process.env.CLS_MSG||'',sha:process.env.CLS_SHA||''," +
69
+ "files:(process.env.CLS_FILES||'').split(',').filter(Boolean)}};" +
70
+ "require('child_process').execFileSync('npx',['@cachly-dev/mcp-server@latest','cls-ingest',JSON.stringify(p)]," +
71
+ "{stdio:'ignore',timeout:8000});" +
72
+ "}catch(e){}";
73
+ /**
74
+ * Build the `.git/hooks/post-commit` shell script.
75
+ * @param instanceId Cachly Brain instance to ingest into.
76
+ * @param apiKey Optional cky_โ€ฆ key embedded for authentication.
77
+ */
78
+ export function buildClsPostCommitHook(instanceId, apiKey) {
79
+ return [
80
+ `#!/bin/sh`,
81
+ `# cachly CLS โ€” Continuous Learning Stream ${CLS_HOOK_VERSION}`,
82
+ `# Runs silently on every commit to keep your brain up to date.`,
83
+ `export CACHLY_BRAIN_INSTANCE_ID="${instanceId}"`,
84
+ ...(apiKey ? [`export CACHLY_JWT="${apiKey}"`] : []),
85
+ `CLS_SHA=$(git rev-parse HEAD 2>/dev/null || echo "")`,
86
+ // No `tr`/quote-mangling โ€” the message is passed verbatim via env.
87
+ `CLS_MSG=$(git log -1 --pretty=%B 2>/dev/null | head -1 | cut -c1-200)`,
88
+ `CLS_FILES=$(git diff-tree --no-commit-id -r --name-only HEAD 2>/dev/null | tr '\\n' ',' | sed 's/,$//')`,
89
+ `export CLS_SHA CLS_MSG CLS_FILES`,
90
+ `node -e "${CLS_NODE_PROGRAM}" 2>/dev/null &`,
91
+ `exit 0`,
92
+ ].join('\n');
93
+ }
94
+ //# sourceMappingURL=cls-hook.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cls-hook.js","sourceRoot":"","sources":["../../src/cls-hook.ts"],"names":[],"mappings":"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"}
@@ -301,6 +301,33 @@ export const BRAIN_TOOL_NAMES = new Set([
301
301
  'skill_gaps', 'brain_coverage', 'brain_metrics', 'brain_service_map',
302
302
  'brain_collab_pairs', 'brain_portability',
303
303
  ]);
304
+ // โ”€โ”€ Free-tier Teaser-Gate โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
305
+ // When a Free-tier Brain crosses its recall limit, smart_recall still delivers
306
+ // the top hits โ€” the magic moment is sacred โ€” but withholds the long tail
307
+ // behind an upgrade teaser ("๐Ÿ”’ N more relevant lessons available"). This is a
308
+ // DEPTH gate (hide the tail), never an access wall: the #1 result is always
309
+ // returned. Usage/limit come from the same authoritative numbers as the Brain
310
+ // Health bar (GET /instances/:id/memory), cached briefly to avoid per-call latency.
311
+ const FREE_TIER_VISIBLE_RECALLS = 3;
312
+ const UPGRADE_URL = 'https://cachly.dev/billing';
313
+ const _recallGateCache = new Map();
314
+ async function getRecallGate(instanceId, apiFetch) {
315
+ const cached = _recallGateCache.get(instanceId);
316
+ if (cached && cached.expiresAt > Date.now())
317
+ return cached.gate;
318
+ try {
319
+ const mem = await apiFetch(`/api/v1/instances/${instanceId}/memory`);
320
+ const limit = mem?.recall_limit ?? -1;
321
+ const used = mem?.total_recall_count ?? 0;
322
+ const gate = { reached: limit > 0 && used >= limit, limit, used };
323
+ _recallGateCache.set(instanceId, { gate, expiresAt: Date.now() + 60_000 });
324
+ return gate;
325
+ }
326
+ catch {
327
+ // Stats unavailable โ†’ never gate (fail open: value delivery beats monetization).
328
+ return { reached: false, limit: -1, used: 0 };
329
+ }
330
+ }
304
331
  export async function handleBrainTool(name, args, getConnection, apiFetch) {
305
332
  switch (name) {
306
333
  case 'learn_from_attempts': {
@@ -838,9 +865,12 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
838
865
  redis.set(`cachly:stats:first_recall_at:${instance_id}`, new Date().toISOString(), 'EX', 365 * 86400, 'NX').catch(() => { });
839
866
  }
840
867
  // โ”€โ”€ Layer 2: Semantic search (parallel, optional) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
868
+ // Semantic recall is a Premium depth layer: free tier keeps full keyword +
869
+ // CKG recall (the magic moment), paid tiers add embedding-based retrieval.
841
870
  const inst = await apiFetch(`/api/v1/instances/${instance_id}`).catch(() => null);
871
+ const tierIsFree = !inst?.tier || inst.tier.toLowerCase() === 'free';
842
872
  const semHits = [];
843
- if (inst?.vector_token && hasEmbedProvider()) {
873
+ if (!tierIsFree && inst?.vector_token && hasEmbedProvider()) {
844
874
  try {
845
875
  const embedding = await computeEmbedding(query);
846
876
  const vectorUrl = `https://api.cachly.dev/v1/sem/${inst.vector_token}`;
@@ -980,6 +1010,10 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
980
1010
  return true;
981
1011
  })
982
1012
  .sort((a, b) => b.hybridScore - a.hybridScore);
1013
+ // โ”€โ”€ Teaser-Gate: free tier over its recall limit hides the long tail โ”€โ”€โ”€โ”€โ”€โ”€
1014
+ const recallGate = await getRecallGate(instance_id, apiFetch);
1015
+ const gateActive = recallGate.reached && hybridResults.length > FREE_TIER_VISIBLE_RECALLS;
1016
+ const visibleCount = gateActive ? FREE_TIER_VISIBLE_RECALLS : 8;
983
1017
  // โ”€โ”€ Build output โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
984
1018
  const lines = [`๐Ÿง  **Smart Recall** for: _"${query}"_\n`];
985
1019
  // "Brain saved you here" banner โ€” surfaces value inline for proven recalled lessons
@@ -1029,7 +1063,7 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
1029
1063
  // Group by sub-query for multi-topic queries
1030
1064
  if (subQueries.length > 1) {
1031
1065
  const grouped = new Map();
1032
- for (const r of hybridResults.slice(0, 12)) {
1066
+ for (const r of hybridResults.slice(0, gateActive ? FREE_TIER_VISIBLE_RECALLS : 12)) {
1033
1067
  const sq = r.subQuery ?? query;
1034
1068
  if (!grouped.has(sq))
1035
1069
  grouped.set(sq, []);
@@ -1060,7 +1094,7 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
1060
1094
  }
1061
1095
  }
1062
1096
  else {
1063
- for (const r of hybridResults.slice(0, 8)) {
1097
+ for (const r of hybridResults.slice(0, visibleCount)) {
1064
1098
  const label = r.key.replace('cachly:ctx:', '๐Ÿ“ ').replace('cachly:lesson:best:', '๐Ÿ’ก ').replace('cachly:idx:', '๐Ÿ“‚ ');
1065
1099
  let authorBadge = '';
1066
1100
  if (r.key.startsWith('cachly:lesson:best:')) {
@@ -1079,6 +1113,13 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
1079
1113
  lines.push(`> ${preview}${r.content.length > 400 ? 'โ€ฆ' : ''}\n`);
1080
1114
  }
1081
1115
  }
1116
+ // โ”€โ”€ Teaser-Gate footer: sell the withheld tail at the moment of need โ”€โ”€
1117
+ if (gateActive) {
1118
+ const withheld = Math.max(0, hybridResults.length - visibleCount);
1119
+ if (withheld > 0) {
1120
+ lines.push(`๐Ÿ”’ **${withheld} more relevant lesson${withheld !== 1 ? 's' : ''} available** โ€” you've reached your Free recall limit (${recallGate.used}/${recallGate.limit}). Unlock your full Brain โ†’ ${UPGRADE_URL}\n`);
1121
+ }
1122
+ }
1082
1123
  }
1083
1124
  else {
1084
1125
  lines.push(`โš ๏ธ No matches found for: "${query}"`);