@cachly-dev/mcp-server 0.10.185 → 0.10.186
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 +6 -6
- package/dist/src/embeddings.js +2 -2
- package/dist/src/handlers/cache.js +28 -32
- package/dist/src/index.js +104 -104
- package/glama.json +1 -1
- package/hooks/cachly-ambient-einblendung.mjs +11 -11
- package/package.json +4 -3
- package/server.json +3 -3
package/README.md
CHANGED
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
> the right moment. You build faster, and your team builds on what you figured out.
|
|
7
7
|
|
|
8
8
|
[](https://www.npmjs.com/package/@cachly-dev/mcp-server)
|
|
9
|
-

|
|
10
9
|

|
|
11
10
|
|
|
12
11
|
`@cachly-dev/mcp-server` connects [cachly](https://cachly.dev) to Claude Code, Cursor,
|
|
13
12
|
GitHub Copilot, Windsurf, Cline, Continue.dev, Zed, Codex CLI and Gemini CLI over
|
|
14
|
-
[MCP](https://modelcontextprotocol.io). It offers 124 MCP tools. Your
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
[MCP](https://modelcontextprotocol.io). It offers 124 MCP tools. Your editor lists 27 entries
|
|
14
|
+
by default and reaches the rest through one of them, `cachly_tool` (details under MCP
|
|
15
|
+
Tools below). Your AI stores a lesson after a fix, and finds it again when the same
|
|
16
|
+
problem returns. Your Brain (the store of your lessons) lives on EU servers.
|
|
17
17
|
|
|
18
18
|
## Start without an account
|
|
19
19
|
|
|
@@ -151,7 +151,7 @@ cachly_tool(tool: "team_roster", describe: true) get its schema first
|
|
|
151
151
|
|
|
152
152
|
Nothing is unreachable. The server dispatches by name, so `team_roster` called directly
|
|
153
153
|
still works. Set `CACHLY_ALLE_WERKZEUGE=1` to list all 124. Set `CACHLY_PROFILE=lean` to
|
|
154
|
-
list 8
|
|
154
|
+
list 8 plus `cachly_tool`, or `CACHLY_PROFILE=recall` to list the 3 read-only tools.
|
|
155
155
|
|
|
156
156
|
### Session and lessons (most used)
|
|
157
157
|
|
|
@@ -441,7 +441,7 @@ The free tier needs no credit card.
|
|
|
441
441
|
| `CACHLY_API_URL` | `https://api.cachly.dev` | Override for a self-hosted backend |
|
|
442
442
|
| `CACHLY_EMBED_PROVIDER` | automatic | Name an embedding provider yourself (see above) |
|
|
443
443
|
| `CACHLY_ALLE_WERKZEUGE` | unset | Set to `1` to list all 124 tools instead of 27 |
|
|
444
|
-
| `CACHLY_PROFILE` | unset | `lean` lists 8 tools
|
|
444
|
+
| `CACHLY_PROFILE` | unset | `lean` lists 8 tools plus `cachly_tool`, `recall` lists the 3 read-only tools |
|
|
445
445
|
| `CACHLY_UPDATE_CHECK` | on | Set to `false` to stop the version check. npm is asked at most once a day, with a 2-second limit |
|
|
446
446
|
| `CACHLY_NO_TELEMETRY` | unset | Set to `1` to disable usage pings. They include your API token and up to 80 characters of `smart_recall` queries |
|
|
447
447
|
|
package/dist/src/embeddings.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{zeitpunkt as u}from"./zeitmessung.js";const s={apiUrl:process.env.CACHLY_API_URL??"https://api.cachly.dev",jwt:process.env.CACHLY_JWT??"",model:process.env.CACHLY_EMBED_MODEL??""};function
|
|
1
|
+
import{zeitpunkt as u}from"./zeitmessung.js";const s={apiUrl:process.env.CACHLY_API_URL??"https://api.cachly.dev",jwt:process.env.CACHLY_JWT??"",model:process.env.CACHLY_EMBED_MODEL??""};function S(n){s.jwt=n,n&&!b&&c==="none"&&(c="cachly")}const A=Number(process.env.CACHLY_EMBED_TIMEOUT_MS??8e3),p=Number(process.env.CACHLY_EMBED_TIMEOUT_MAX_MS??45e3),h=12;function f(n,r){return r==="kurz"?A:Math.min(p,A+n.length*h)}function g(){return process.env.CACHLY_JWT?"cachly":process.env.OLLAMA_BASE_URL?"ollama":"none"}const b=!!process.env.CACHLY_EMBED_PROVIDER;let c=(process.env.CACHLY_EMBED_PROVIDER??g()).toLowerCase();async function I(n,r,a){const d=f(n,r);switch(c){case"cachly":{if(!s.jwt)throw new Error(`CACHLY_JWT not set.
|
|
2
2
|
|
|
3
3
|
The "cachly" provider uses server-side embeddings via the Cachly API.
|
|
4
4
|
Set CACHLY_JWT, or use another provider via CACHLY_EMBED_PROVIDER:
|
|
@@ -10,4 +10,4 @@ Set OPENAI_API_KEY in your .env, or switch provider:
|
|
|
10
10
|
CACHLY_EMBED_PROVIDER=gemini (+ GEMINI_API_KEY)
|
|
11
11
|
CACHLY_EMBED_PROVIDER=ollama (local, free)
|
|
12
12
|
CACHLY_EMBED_PROVIDER=cachly (server-side, no key needed)`);const o=s.model||"text-embedding-3-small",t=await fetch("https://api.openai.com/v1/embeddings",{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${e}`},body:JSON.stringify({model:o,input:n}),signal:AbortSignal.timeout(d)});if(!t.ok)throw new Error(`OpenAI embedding error: ${t.statusText}`);const i=await t.json();if(!i.data?.[0]?.embedding)throw new Error("OpenAI returned empty embedding response");return i.data[0].embedding}default:throw new Error(`Unknown CACHLY_EMBED_PROVIDER="${c}".
|
|
13
|
-
Supported: cachly (default), openai, mistral, cohere, ollama, gemini`)}}const y=3,
|
|
13
|
+
Supported: cachly (default), openai, mistral, cohere, ollama, gemini`)}}const y=3,C=4;function w(n){const r=n;if(typeof r?.status=="number")return r.status===429||r.status>=500;if(r?.name==="TimeoutError"||r?.name==="AbortError")return!1;if(/fetch failed/i.test(String(r?.message??"")))return!0;const a=r?.cause?.code??"";return["ECONNRESET","ECONNREFUSED","ETIMEDOUT","EAI_AGAIN","EPIPE","UND_ERR_SOCKET"].includes(a)}function M(n,r,a){if(r>=(a==="lang"?C:y)||!w(n))return null;const e=n?.retryAfterSek;if(typeof e=="number"&&Number.isFinite(e)&&e>=0){const o=e*1e3;return a==="kurz"?o<=2e3?o:null:Math.min(o,61e3)}return a==="lang"?Math.min(1e3*2**(r-1),8e3):250*2**(r-1)}async function L(n,r){const a=r?.geduld??"kurz";if(r?.modell&&c!=="cachly"&&c!=="ollama")throw new Error(`Anbieter "${c}" kann kein abweichendes Modell anfragen (verlangt: ${r.modell}).`);let d=0;for(;;){d++;try{u(`einbettung: Anfang (${n.length} Zeichen, Versuch ${d})`);const e=await I(n,a,r?.modell);return u("einbettung: fertig"),e}catch(e){const o=M(e,d,a);if(o===null)throw e;await new Promise(t=>setTimeout(t,o))}}}function T(){switch(c){case"cachly":return!!s.jwt;case"mistral":return!!process.env.MISTRAL_API_KEY;case"cohere":return!!process.env.COHERE_API_KEY;case"ollama":return!0;case"gemini":return!!process.env.GEMINI_API_KEY;case"openai":return!!process.env.OPENAI_API_KEY;case"none":return!1;default:return!1}}function v(){const n={cachly:"CACHLY_JWT (server-side, no extra key needed)",openai:"OPENAI_API_KEY",mistral:"MISTRAL_API_KEY",cohere:"COHERE_API_KEY",ollama:"OLLAMA_BASE_URL (optional, default: http://localhost:11434)",gemini:"GEMINI_API_KEY"};if(c==="none")return"No embedding provider configured. Sign in (CACHLY_JWT) to use the EU-hosted cachly embeddings, or set OLLAMA_BASE_URL for a local model. Brain works without embedding.";const r=n[c]??"CACHLY_JWT";return`CACHLY_EMBED_PROVIDER=${c} \u2192 requires ${r}`}export{c as EMBED_PROVIDER,L as computeEmbedding,s as embedConfig,v as embedProviderHint,T as hasEmbedProvider,w as istVoruebergehend,S as setEmbedJwt,M as wiederholungMs,f as zeitlimitMs};
|
|
@@ -1,59 +1,55 @@
|
|
|
1
|
-
import{randomUUID as
|
|
1
|
+
import{randomUUID as P}from"node:crypto";import{McpError as V,ErrorCode as Y}from"@modelcontextprotocol/sdk/types.js";import{readdir as W,stat as z,readFile as G}from"node:fs/promises";import{join as Q,relative as Z,extname as ee}from"node:path";import{computeEmbedding as R,hasEmbedProvider as U,embedProviderHint as J}from"../embeddings.js";import{einbettTakt as te}from"../einbett-takt.js";import{detectNamespace as K}from"../namespace.js";import{simpleHash as se}from"../confidence.js";import{scanKeys as ae}from"../utils.js";import{cachlyUrl as X}from"../cachly-url.js";const _e=new Set(["cache_get","cache_set","cache_delete","cache_exists","cache_ttl","cache_keys","cache_stats","semantic_search","detect_namespace","cache_warmup","index_project","cache_mset","cache_mget","cache_lock_acquire","cache_lock_release","cache_stream_set","cache_stream_get","cache_org_stats","set_cost_per_call"]);async function $e(F,u,f,E){switch(F){case"cache_get":{const{instance_id:n,key:e,org_id:a}=u,t=await f(n);let s=await t.get(e),i="direct";if(s===null&&a){const r=`org:${a}:sem:${e}`;s=await t.get(r),s!==null&&(i=`org:${a}:sem`)}if(s===null)return`Key \`${e}\` \u2192 **not found** (null)`;let c=s;try{c=JSON.stringify(JSON.parse(s),null,2)}catch{}const o=i!=="direct"?` _(from shared org namespace \`${i}\`)_`:"";return`Key \`${e}\`:${o}
|
|
2
2
|
\`\`\`
|
|
3
3
|
${c}
|
|
4
|
-
\`\`\``}case"cache_set":{const{instance_id:n,key:e,value:a,ttl:t,org_id:s}=
|
|
4
|
+
\`\`\``}case"cache_set":{const{instance_id:n,key:e,value:a,ttl:t,org_id:s}=u,i=await f(n);return t&&t>0?(await i.set(e,a,"EX",t),s&&await i.set(`org:${s}:sem:${e}`,a,"EX",t),`\u2705 Set \`${e}\` (TTL: ${t}s)${s?` + shared to \`org:${s}:sem\``:""}`):(await i.set(e,a),s&&await i.set(`org:${s}:sem:${e}`,a),`\u2705 Set \`${e}\` (no expiry)${s?` + shared to \`org:${s}:sem\``:""}`)}case"cache_delete":{const{instance_id:n,keys:e}=u;return`\u2705 Deleted **${await(await f(n)).del(...e)}** of ${e.length} key(s): ${e.map(s=>`\`${s}\``).join(", ")}`}case"cache_exists":{const{instance_id:n,keys:e}=u;return`**${await(await f(n)).exists(...e)}** of ${e.length} key(s) exist in cache.`}case"cache_ttl":{const{instance_id:n,key:e}=u,t=await(await f(n)).ttl(e);if(t===-2)return`Key \`${e}\` \u2192 **does not exist**`;if(t===-1)return`Key \`${e}\` \u2192 **no expiry** (persists forever)`;const s=Math.floor(t/60),i=t%60;return`Key \`${e}\` \u2192 TTL: **${t}s** (${s}m ${i}s remaining)`}case"cache_keys":{const{instance_id:n,pattern:e="*",count:a=50}=u,t=Math.min(a,500),s=await f(n),i=[],c=s.scanStream({match:e,count:100});await new Promise((r,_)=>{c.on("data",d=>{i.push(...d),i.length>=t&&(c.destroy(),r())}),c.on("end",r),c.on("error",_)});const o=i.slice(0,t);return o.length===0?`No keys found matching pattern \`${e}\`.`:[`Found **${o.length}** key(s) matching \`${e}\`:`,...o.map(r=>` \u2022 \`${r}\``),o.length===t?`
|
|
5
5
|
_(showing first ${t} \u2014 narrow pattern to see more)_`:""].filter(Boolean).join(`
|
|
6
|
-
`)}case"cache_stats":{const{instance_id:n}=
|
|
7
|
-
]+)`))?.[1]?.trim()??"n/a",c=i(a,"used_memory_human"),o=i(a,"used_memory_peak_human"),r=i(t,"keyspace_hits"),
|
|
8
|
-
`).filter(w=>w.startsWith("db")).map(w=>` ${w.trim()}`),p=[`\u{1F4CA} **Cache Stats for instance \`${n}\`:**`,"",` \u{1F4BE} Memory used: ${c} (peak: ${o})`,` \u26A1 Ops/sec: ${d}`,` \u{1F3AF} Hit rate: ${x}% (${r} hits / ${
|
|
6
|
+
`)}case"cache_stats":{const{instance_id:n}=u,e=await f(n),[a,t,s]=await Promise.all([e.info("memory"),e.info("stats"),e.info("keyspace")]),i=(w,L)=>w.match(new RegExp(`${L}:([^\r
|
|
7
|
+
]+)`))?.[1]?.trim()??"n/a",c=i(a,"used_memory_human"),o=i(a,"used_memory_peak_human"),r=i(t,"keyspace_hits"),_=i(t,"keyspace_misses"),d=i(t,"instantaneous_ops_per_sec"),h=(await e.info("clients")).match(/connected_clients:(\d+)/)?.[1]??"n/a",y=parseInt(r)||0,g=parseInt(_)||0,T=y+g,x=T>0?(y/T*100).toFixed(1):"n/a",v=s.split(`
|
|
8
|
+
`).filter(w=>w.startsWith("db")).map(w=>` ${w.trim()}`),p=[`\u{1F4CA} **Cache Stats for instance \`${n}\`:**`,"",` \u{1F4BE} Memory used: ${c} (peak: ${o})`,` \u26A1 Ops/sec: ${d}`,` \u{1F3AF} Hit rate: ${x}% (${r} hits / ${_} misses)`,` \u{1F517} Clients: ${h}`,"",v.length>0?` \u{1F5C2}\uFE0F Keyspace:
|
|
9
9
|
${v.join(`
|
|
10
|
-
`)}`:" \u{1F5C2}\uFE0F Keyspace: (empty)"];try{const w=await
|
|
11
|
-
`)}case"semantic_search":{const{instance_id:n,query:e,threshold:a=.82,namespace:t="cachly:sem",top_k:s=5,use_hybrid:i=!1,auto_namespace:c=!1}=
|
|
10
|
+
`)}`:" \u{1F5C2}\uFE0F Keyspace: (empty)"];try{const w=await E(`/api/v1/instances/${n}`);if(w.vector_token){const L=process.env.CACHLY_VECTOR_URL??"https://api.cachly.dev",C=await fetch(`${L}/v1/sem/${w.vector_token}/stats`,{signal:AbortSignal.timeout(5e3)});if(C.ok){const m=await C.json(),b=(l,$=2)=>l!==void 0?l.toLocaleString("en-US",{minimumFractionDigits:$,maximumFractionDigits:$}):"n/a",O=l=>l!==void 0?l.toLocaleString("en-US"):"n/a",k=m.total_hits??0,S=m.total_entries??0;if(k===0&&S===0){const l=$=>($*50*.25*.002*30).toFixed(2);p.push("","\u{1F4B0} **Tokenmaxxing ROI \u2014 Projection:**"," \u{1F4ED} No cache hits yet \u2014 you're just getting started.",""," \u{1F4CA} Typical savings for your team size:",` 5 devs \xD7 50 LLM calls/day \xD7 25% hit-rate \u2192 ~$${l(5)}/month`,` 10 devs \xD7 50 LLM calls/day \xD7 25% hit-rate \u2192 ~$${l(10)}/month`,` 20 devs \xD7 50 LLM calls/day \xD7 25% hit-rate \u2192 ~$${l(20)}/month`," (Assumes avg $0.002/call saved per cache hit)",""," \u{1F680} Next: route LLM calls through cachly to start accumulating hits.",` Docs: ${X("/docs/semantic","cache-stats")}`)}else{const l=m.savings?.week_over_week_pct,$=l!==void 0?` ${l>=0?"\u{1F4C8}":"\u{1F4C9}"} 7d trend: ${l>=0?"+":""}${l.toFixed(1)}% vs prev week`:"";p.push("","\u{1F4B0} **Tokenmaxxing ROI:**",` \u{1F4E6} Semantic entries: ${O(m.total_entries)}`,` \u{1F3AF} Total cache hits: ${O(m.total_hits)}`,` \u23F1\uFE0F Hits last 24h: ${O(m.savings?.hits_last_24h)}`,` \u{1F4C5} Hits last 7d: ${O(m.savings?.hits_last_7d)}`,...$?[$]:[],` \u{1F4B5} Total saved: $${b(m.savings?.estimated_total_saved_usd)}`,` \u{1F4C8} Projected/month: $${b(m.savings?.estimated_monthly_saved_usd)}`,` \u{1F4CA} Avg per hit: $${b(m.savings?.avg_cost_per_call_usd,4)}`)}m.top_hits&&m.top_hits.length>0&&(p.push("","\u{1F51D} Top cached prompts:"),m.top_hits.slice(0,3).forEach(($,j)=>{const N=$.prompt.length>50?`${$.prompt.slice(0,50)}\u2026`:$.prompt;p.push(` ${j+1}. "${N}" (hit ${$.hit_count}\xD7)`)}))}}}catch{}return p.join(`
|
|
11
|
+
`)}case"semantic_search":{const{instance_id:n,query:e,threshold:a=.82,namespace:t="cachly:sem",top_k:s=5,use_hybrid:i=!1,auto_namespace:c=!1}=u,o=c?K(e):t;if(!U())return`\u274C semantic_search requires an embedding provider.
|
|
12
12
|
|
|
13
|
-
Current: ${
|
|
13
|
+
Current: ${J()}
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
COHERE_API_KEY (provider: cohere)
|
|
19
|
-
GEMINI_API_KEY (provider: gemini)
|
|
20
|
-
OLLAMA_BASE_URL (provider: ollama \u2013 local, no key needed)
|
|
21
|
-
Also set: CACHLY_EMBED_PROVIDER=<provider>`;const r=await b(`/api/v1/instances/${n}`);if(!r.vector_token)return`\u274C Semantic search is only available on Speed and Business tiers.
|
|
15
|
+
Easiest: sign in (CACHLY_JWT). Embeddings then run on the cachly servers in the EU, no extra key.
|
|
16
|
+
Or set OLLAMA_BASE_URL to use a local model (no key needed).
|
|
17
|
+
Your own key with a third-party provider works only if you name the provider yourself: CACHLY_EMBED_PROVIDER=<openai|gemini|mistral|cohere> plus its API key. The text is then sent to that provider.`;const r=await E(`/api/v1/instances/${n}`);if(!r.vector_token)return`\u274C Semantic search is only available on Speed and Business tiers.
|
|
22
18
|
|
|
23
19
|
Your instance "${r.name}" is on the **${r.tier.toUpperCase()}** tier.
|
|
24
|
-
Upgrade at ${
|
|
20
|
+
Upgrade at ${X(`/instances/${n}`,"upgrade")}`;const _=await R(e),d=process.env.CACHLY_VECTOR_URL??`https://api.cachly.dev/v1/sem/${r.vector_token}`,h={embedding:_,namespace:o,threshold:a,top_k:s};i&&(h.hybrid=!0,h.prompt=e);const y=await fetch(`${d}/search`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(h),signal:AbortSignal.timeout(8e3)});if(!y.ok)throw new V(Y.InternalError,`Vector search failed: ${y.statusText}`);const g=await y.json();if(!g.length||g.length===1&&!g[0].found)return`\u{1F50D} No semantically similar entries found for:
|
|
25
21
|
_"${e}"_
|
|
26
22
|
|
|
27
23
|
Try lowering the threshold (current: ${a}) or using different keywords.`;const T=await f(n),x=[`\u{1F50D} **Semantic search results** for: _"${e}"_`,` Threshold: ${a} \xB7 Namespace: \`${o}\``,""];for(const v of g){if(!v.found||!v.id)continue;const p=await T.get(`${o}:val:${v.id}`);x.push(`**Match** (similarity: ${((v.similarity??0)*100).toFixed(1)}%)`,` Prompt: _"${v.prompt??"(unknown)"}"_`,p?` Value: \`${p.slice(0,200)}${p.length>200?"\u2026":""}\``:" Value: _(evicted from cache)_","")}return x.join(`
|
|
28
|
-
`)}case"detect_namespace":{const{prompt:n}=
|
|
29
|
-
`)}case"cache_warmup":{const{instance_id:n,entries:e,namespace:a="cachly:sem",ttl:t,auto_namespace:s=!1}=
|
|
24
|
+
`)}case"detect_namespace":{const{prompt:n}=u,e=K(n),a=e.split(":").pop(),t={code:"\u{1F4BB} Code \u2014 contains programming constructs or syntax",translation:"\u{1F310} Translation \u2014 asks to translate between languages",summary:"\u{1F4DD} Summary \u2014 requests a summary or key points (TL;DR)",qa:"\u2753 Q&A \u2014 a direct question or query",creative:"\u{1F3A8} Creative \u2014 general, creative, or conversational prompt"};return[`**Detected namespace:** \`${e}\``,`**Type:** ${t[a]??a}`,"",`_Prompt: "${n.slice(0,120)}${n.length>120?"\u2026":""}"_`,"","\u{1F4A1} Use this namespace in `semantic_search` or `cache_warmup` for better hit rates."," Set `auto_namespace: true` to apply this detection automatically."].join(`
|
|
25
|
+
`)}case"cache_warmup":{const{instance_id:n,entries:e,namespace:a="cachly:sem",ttl:t,auto_namespace:s=!1}=u;if(!U())return`\u274C cache_warmup requires an embedding provider.
|
|
30
26
|
|
|
31
|
-
Current: ${
|
|
27
|
+
Current: ${J()}
|
|
32
28
|
|
|
33
29
|
Supported: openai (default) \xB7 mistral \xB7 cohere \xB7 ollama (local) \xB7 gemini
|
|
34
|
-
Set CACHLY_EMBED_PROVIDER and the matching API key env var.`;const i=await
|
|
35
|
-
`)}case"index_project":{const{instance_id:n,dir:e,extensions:a,max_files:t=100,ttl:s=86400,summary_chars:i=1200,namespace:c="cachly:sem:code",hintergrund:o=!1}=
|
|
36
|
-
Extensions checked: ${[...r].join(", ")}`;const h=await
|
|
37
|
-
${
|
|
38
|
-
${
|
|
39
|
-
`)}case"cache_mset":{const{instance_id:n,items:e}=
|
|
30
|
+
Set CACHLY_EMBED_PROVIDER and the matching API key env var.`;const i=await E(`/api/v1/instances/${n}`),c=process.env.CACHLY_VECTOR_URL??(i.vector_token?`https://api.cachly.dev/v1/sem/${i.vector_token}`:null),o=await f(n);let r=0,_=0;const d=[];for(const h of e){const y=h.namespace??(s?K(h.prompt):a),g=await R(h.prompt);let T=!1;if(c){const p=await fetch(`${c}/search`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({embedding:g,namespace:y,threshold:.98}),signal:AbortSignal.timeout(8e3)}).catch(()=>null);p?.ok&&(T=(await p.json())[0]?.found??!1)}if(T){_++,d.push(` \u23ED\uFE0F _"${h.prompt.slice(0,60)}${h.prompt.length>60?"\u2026":""}"_ \u2192 already cached`);continue}const x=P(),v=`${y}:val:${x}`;if(t&&t>0?await o.set(v,h.value,"EX",t):await o.set(v,h.value),c){const p={id:x,prompt:h.prompt,namespace:y,embedding:g};t&&t>0&&(p.expires_at=new Date(Date.now()+t*1e3).toISOString()),await fetch(`${c}/entries`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(p),signal:AbortSignal.timeout(8e3)}).catch(()=>{})}else{const p=`${y}:emb:${x}`,w=JSON.stringify({embedding:g,prompt:h.prompt});t&&t>0?await o.set(p,w,"EX",t):await o.set(p,w)}r++,d.push(` \u2705 _"${h.prompt.slice(0,60)}${h.prompt.length>60?"\u2026":""}"_ \u2192 \`${y}\``)}return["\u{1F525} **Cache Warmup Complete**","",` \u2705 Warmed: **${r}** new entries`,` \u23ED\uFE0F Skipped: **${_}** (already cached at \u2265 0.98 similarity)`,` \u{1F4E6} Total: ${e.length}`,s?" \u{1F3F7}\uFE0F Namespacing: auto-detected per prompt":` \u{1F3F7}\uFE0F Namespace: \`${a}\``,c?" \u{1F50D} Mode: pgvector HNSW (Speed/Business tier)":" \u{1F50D} Mode: Valkey SCAN (upgrade to Speed tier for scalable search)","",...d].join(`
|
|
31
|
+
`)}case"index_project":{const{instance_id:n,dir:e,extensions:a,max_files:t=100,ttl:s=86400,summary_chars:i=1200,namespace:c="cachly:sem:code",hintergrund:o=!1}=u,r=new Set((a??["ts","js","tsx","jsx","go","py","java","rs","md","kt","swift"]).map(k=>k.startsWith(".")?k:`.${k}`)),_=[];async function d(k){if(_.length>=t)return;const S=await W(k,{withFileTypes:!0}).catch(()=>null);if(S)for(const l of S){if(_.length>=t)break;const $=Q(k,l.name);if(l.isDirectory()){if([".git","node_modules","dist","build",".next","__pycache__","vendor"].includes(l.name))continue;await d($)}else l.isFile()&&r.has(ee(l.name).toLowerCase())&&_.push($)}}if(await d(e),_.length===0)return`\u274C No matching files found in \`${e}\`.
|
|
32
|
+
Extensions checked: ${[...r].join(", ")}`;const h=await E(`/api/v1/instances/${n}`),y=process.env.CACHLY_VECTOR_URL??(h.vector_token?`https://api.cachly.dev/v1/sem/${h.vector_token}`:null),g=y&&U();let T=0,x=0,v=0,p=0,w=0;const L=[],C=new Date(Date.now()+s*1e3).toISOString(),m=await f(n),b=te({hintergrund:o});for(const k of _){const S=Z(e,k);let l,$;try{const A=await z(k);if(A.size>2e5){x++;continue}$=A.size,l=await G(k,"utf-8")}catch{v++;continue}const j=`cachly:idx:hash:${S}`,N=`${$}:${l.length}:${se(l)}`;if(await m.get(j)===N){const A=`cachly:idx:${S}`;s>0&&await m.expire(A,s),s>0&&await m.expire(j,s),w++;continue}const H=l.slice(0,i).replace(/\s+/g," ").trim(),M=`cachly:idx:${S}`,I=`File: ${S}
|
|
33
|
+
${H}`;if(s>0?await m.set(M,I,"EX",s):await m.set(M,I),s>0?await m.set(j,N,"EX",s):await m.set(j,N),T++,L.push(` \u2705 ${S}`),g&&b.darf())try{const A=`File: ${S}
|
|
34
|
+
${H}`;await b.warte();const B=await R(A).catch(q=>{throw b.fehler(q),q}),D=P();await fetch(`${y}/entries`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:D,prompt:A,namespace:c,embedding:B,expires_at:C}),signal:AbortSignal.timeout(8e3)}),await m.set(`${c}:val:${D}`,S,"EX",s),p++}catch{}}return[`\u{1F4C2} **index_project Complete** \u2014 ${g?"\u{1F50D} Keyword + \u{1F3AF} Semantic":"\u{1F50D} Keyword only (no embedding provider)"}`,"",` \u{1F4C1} Dir: ${e}`,` \u2705 Indexed: **${T}** files (new/changed)`,` \u267B\uFE0F Unchanged: ${w} files (hash match \u2014 skipped)`,...g?[` \u{1F3AF} Semantic: **${p}** files (vector-searchable)`]:[],` \u23ED\uFE0F Skipped: ${x} (too large or filtered)`,` \u274C Errors: ${v}`,` \u23F1\uFE0F TTL: ${s}s (${Math.round(s/3600)}h)`,"","\u{1F4A1} **Next steps:**",' 1. Use `smart_recall("how does auth work")` to find relevant files.'," 2. Re-run index_project after major refactors.",...g?[]:[" 3. Sign in (CACHLY_JWT) or set OLLAMA_BASE_URL to also enable semantic search."],"",...L.length<=20?L:[...L.slice(0,20),` \u2026 and ${L.length-20} more`]].join(`
|
|
35
|
+
`)}case"cache_mset":{const{instance_id:n,items:e}=u;if(!Array.isArray(e)||e.length===0)return"\u26A0\uFE0F No items provided.";const t=(await f(n)).pipeline();for(const s of e){const i=typeof s.value=="string"?s.value:JSON.stringify(s.value);s.ttl&&s.ttl>0?t.set(s.key,i,"EX",s.ttl):t.set(s.key,i)}return await t.exec(),`\u2705 **cache_mset** \u2013 ${e.length} key(s) written in one pipeline round-trip.
|
|
40
36
|
`+e.map(s=>` \u2022 \`${s.key}\`${s.ttl?` (TTL ${s.ttl}s)`:""}`).join(`
|
|
41
|
-
`)}case"cache_mget":{const{instance_id:n,keys:e}=
|
|
37
|
+
`)}case"cache_mget":{const{instance_id:n,keys:e}=u;if(!Array.isArray(e)||e.length===0)return"\u26A0\uFE0F No keys provided.";const t=await(await f(n)).mget(...e),s=e.map((i,c)=>{const o=t[c];return o===null?` \u2022 \`${i}\`: _null (miss)_`:` \u2022 \`${i}\`: ${o}`});return`\u2705 **cache_mget** \u2013 ${e.length} key(s) fetched in one round-trip.
|
|
42
38
|
`+s.join(`
|
|
43
|
-
`)}case"cache_lock_acquire":{const{instance_id:n,key:e,ttl_ms:a=5e3,retries:t=3,retry_delay_ms:s=50}=
|
|
39
|
+
`)}case"cache_lock_acquire":{const{instance_id:n,key:e,ttl_ms:a=5e3,retries:t=3,retry_delay_ms:s=50}=u,i=await f(n),c=`cachly:lock:${e}`,o=P();for(let r=0;r<=t;r++){if(await i.set(c,o,"PX",a,"NX")==="OK")return`\u{1F512} **cache_lock_acquire** \u2013 Lock acquired!
|
|
44
40
|
|
|
45
41
|
Key: \`${e}\`
|
|
46
42
|
Token: \`${o}\`
|
|
47
43
|
TTL: ${a} ms
|
|
48
44
|
|
|
49
|
-
\u{1F4A1} Use **cache_lock_release** with this token to release early.`;r<t&&await new Promise(d=>setTimeout(d,s))}return`\u274C **cache_lock_acquire** \u2013 Could not acquire lock for \`${e}\` after ${t+1} attempts.`}case"cache_lock_release":{const{instance_id:n,key:e,token:a}=
|
|
45
|
+
\u{1F4A1} Use **cache_lock_release** with this token to release early.`;r<t&&await new Promise(d=>setTimeout(d,s))}return`\u274C **cache_lock_acquire** \u2013 Could not acquire lock for \`${e}\` after ${t+1} attempts.`}case"cache_lock_release":{const{instance_id:n,key:e,token:a}=u;return await(await f(n)).eval('if redis.call("get",KEYS[1])==ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end',1,`cachly:lock:${e}`,a)===1?`\u{1F513} **cache_lock_release** \u2013 Lock \`${e}\` released.`:`\u26A0\uFE0F **cache_lock_release** \u2013 Lock \`${e}\` expired or token mismatch.`}case"cache_stream_set":{const{instance_id:n,key:e,chunks:a,ttl:t}=u;if(!Array.isArray(a)||a.length===0)return"\u26A0\uFE0F No chunks provided.";const s=await f(n),i=`cachly:stream:${e}`;await s.del(i);const c=s.pipeline();for(const o of a)c.rpush(i,o);return t&&t>0&&c.expire(i,t),await c.exec(),`\u2705 **cache_stream_set** \u2013 ${a.length} chunk(s) stored.
|
|
50
46
|
Key: \`${e}\`
|
|
51
47
|
${t?` TTL: ${t}s
|
|
52
|
-
`:""} Total size: ${a.reduce((o,r)=>o+r.length,0)} chars`}case"cache_stream_get":{const{instance_id:n,key:e}=
|
|
48
|
+
`:""} Total size: ${a.reduce((o,r)=>o+r.length,0)} chars`}case"cache_stream_get":{const{instance_id:n,key:e}=u,a=await f(n),t=`cachly:stream:${e}`,s=await a.llen(t);if(s===0)return`\u26A0\uFE0F **cache_stream_get** \u2013 Cache miss for key \`${e}\`.`;const c=(await a.lrange(t,0,-1)).join(""),o=c.slice(0,500);return`\u2705 **cache_stream_get** \u2013 ${s} chunk(s) retrieved for \`${e}\`.
|
|
53
49
|
|
|
54
50
|
**Preview** (first 500 chars):
|
|
55
51
|
\`\`\`
|
|
56
52
|
${o}${o.length<c.length?"\u2026":""}
|
|
57
|
-
\`\`\``}case"cache_org_stats":{const{instance_id:n,org_id:e}=
|
|
58
|
-
`)}case"set_cost_per_call":{const{instance_id:n,cost_per_call_usd:e}=
|
|
59
|
-
`)}default:return null}}export{
|
|
53
|
+
\`\`\``}case"cache_org_stats":{const{instance_id:n,org_id:e}=u,a=await f(n),t=`org:${e}:sem`,i=(await ae(a,`${t}:*`,{max:1e4,timeoutMs:5e3})).length,c=[`\u{1F3E2} Org Cache Stats for org \`${e}\`:`,` \u{1F4E6} Shared entries: ${i}`,` \u{1F511} Namespace: ${t}`," \u{1F465} Benefit: All instances sharing this org_id serve from the same cache","",`\u{1F4A1} To enable sharing: set org_id="${e}" in cache_set calls`];try{const o=await E(`/api/v1/orgs/${e}/savings`),r=d=>`$${(d??0).toFixed(2)}`;c.push("","\u{1F4B0} Org-wide Tokenmaxxing ROI:",` \u{1F5A5}\uFE0F Instances: ${o.instance_count}`,` \u{1F3AF} Total cache hits: ${o.total_hits.toLocaleString("en-US")}`,` \u23F1\uFE0F Hits last 24h: ${o.hits_last_24h.toLocaleString("en-US")}`,` \u{1F4B5} Total saved: ${r(o.estimated_total_saved_usd)}`,` \u{1F4C8} Projected/month: ${r(o.estimated_monthly_saved_usd)}`);const _=[...o.per_instance??[]].sort((d,h)=>h.estimated_total_saved_usd-d.estimated_total_saved_usd).slice(0,5);if(_.length>0){c.push(""," Top instances by savings:");for(const d of _)c.push(` \u2022 ${d.name||d.instance_id}: ${r(d.estimated_total_saved_usd)} (${d.total_hits.toLocaleString("en-US")} hits)`)}}catch{}return c.join(`
|
|
54
|
+
`)}case"set_cost_per_call":{const{instance_id:n,cost_per_call_usd:e}=u;if(!e||e<=0||e>100)throw new V(Y.InvalidParams,"cost_per_call_usd must be a positive number \u2264 100");await E(`/api/v1/instances/${n}/cost-per-call`,{method:"PUT",body:JSON.stringify({cost_per_call_usd:e})});const a={"0.02":"claude-opus-4.8","0.015":"gpt-5.5","0.009":"claude-sonnet-4.6","0.002":"gpt-5.5-mini","0.001":"claude-haiku-4.5"},t=a[e.toString()]?` (${a[e.toString()]})`:"";return[`\u2705 **Cost per call updated** \u2014 $${e.toFixed(4)}${t}`,"",`ROI savings in \`cache_stats\` will now use $${e.toFixed(4)} per avoided LLM call.`,"Run `cache_stats` to see your updated savings estimate."].join(`
|
|
55
|
+
`)}default:return null}}export{_e as CACHE_TOOL_NAMES,$e as handleCacheTool};
|