@cachly-dev/mcp-server 0.10.128 → 0.10.130
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 +2 -2
- package/dist/index.js +1 -7
- package/dist/packages/telegram-notify/client.js +2 -75
- package/dist/src/ambient-cli.js +7 -253
- package/dist/src/ambient-deps.js +1 -55
- package/dist/src/ambient-hooks.js +5 -278
- package/dist/src/ambient-ledger.js +6 -95
- package/dist/src/ambient-memory.js +1 -67
- package/dist/src/ambient-recall.js +1 -221
- package/dist/src/aussetzer.js +1 -82
- package/dist/src/auth.js +9 -140
- package/dist/src/autor-abzeichen.js +1 -56
- package/dist/src/autor.js +1 -136
- package/dist/src/bauabdruck.js +1 -93
- package/dist/src/bedeutung.js +2 -323
- package/dist/src/bench/cachly-bench.js +4 -249
- package/dist/src/bench/external-corpus.js +4 -135
- package/dist/src/bench/fixtures.js +1 -225
- package/dist/src/brain-export.js +16 -119
- package/dist/src/brain-watch-hook.js +8 -86
- package/dist/src/cachly-url.js +1 -21
- package/dist/src/ckg.js +1 -108
- package/dist/src/cls-hook.js +8 -94
- package/dist/src/confidence.js +1 -50
- package/dist/src/credentials.js +1 -85
- package/dist/src/deutsch.js +1 -223
- package/dist/src/doctor.js +2 -248
- package/dist/src/editor.js +1 -63
- package/dist/src/eingaenge-nachruesten.js +1 -290
- package/dist/src/eingaenge.js +2 -228
- package/dist/src/embeddings.js +13 -223
- package/dist/src/feldreparatur.js +3 -0
- package/dist/src/first-contact.js +4 -108
- package/dist/src/first-recall-proof.js +4 -41
- package/dist/src/funnel-milestones.js +2 -48
- package/dist/src/geliefert.js +1 -114
- package/dist/src/handlers/advanced.js +38 -537
- package/dist/src/handlers/brain.js +69 -4181
- package/dist/src/handlers/cache.js +59 -638
- package/dist/src/handlers/context.js +17 -182
- package/dist/src/handlers/fedbrain.js +40 -1436
- package/dist/src/handlers/instances.js +16 -201
- package/dist/src/handlers/roadmap.js +13 -175
- package/dist/src/handlers/share.js +19 -530
- package/dist/src/handlers/syndicate.js +38 -904
- package/dist/src/handlers/team.js +89 -1850
- package/dist/src/handlers/viz.js +3 -148
- package/dist/src/index.js +321 -4281
- package/dist/src/interferenz.js +1 -0
- package/dist/src/kostprobe.js +7 -125
- package/dist/src/lesson-preview.js +2 -133
- package/dist/src/namespace.js +1 -128
- package/dist/src/notifier.js +1 -6
- package/dist/src/rangfolge-stellschrauben.js +1 -0
- package/dist/src/rangfolge.js +1 -214
- package/dist/src/raumkorrektur.js +1 -197
- package/dist/src/recall-tiefe.js +1 -42
- package/dist/src/rerank.js +1 -154
- package/dist/src/resonanz.js +1 -0
- package/dist/src/search.js +1 -2104
- package/dist/src/seltenheitsbestand.js +1 -92
- package/dist/src/speicherlage.js +1 -84
- package/dist/src/spuren.js +1 -150
- package/dist/src/starter-corpus.js +1 -165
- package/dist/src/telemetry-types.js +0 -13
- package/dist/src/tools.js +10 -2465
- package/dist/src/toolspecs.js +1 -121
- package/dist/src/upgrade-nudge.js +1 -29
- package/dist/src/utils.js +1 -89
- package/dist/src/versuch.js +1 -250
- package/dist/src/vorspann.js +1 -146
- package/dist/src/wartung/vektoren-verdichten.js +1 -200
- package/dist/src/werkzeug-nutzung.js +1 -72
- package/dist/src/wertbeitrag.js +1 -73
- package/dist/src/wirkungsspur.js +1 -0
- package/glama.json +1 -1
- package/package.json +8 -7
- package/server.json +3 -3
- package/dist/index.d.ts +0 -1
- package/dist/index.js.map +0 -1
- package/dist/packages/telegram-notify/client.d.ts +0 -19
- package/dist/packages/telegram-notify/client.js.map +0 -1
- package/dist/src/ambient-cli.d.ts +0 -101
- package/dist/src/ambient-cli.js.map +0 -1
- package/dist/src/ambient-deps.d.ts +0 -26
- package/dist/src/ambient-deps.js.map +0 -1
- package/dist/src/ambient-hooks.d.ts +0 -137
- package/dist/src/ambient-hooks.js.map +0 -1
- package/dist/src/ambient-ledger.d.ts +0 -19
- package/dist/src/ambient-ledger.js.map +0 -1
- package/dist/src/ambient-memory.d.ts +0 -12
- package/dist/src/ambient-memory.js.map +0 -1
- package/dist/src/ambient-recall.d.ts +0 -127
- package/dist/src/ambient-recall.js.map +0 -1
- package/dist/src/aussetzer.d.ts +0 -67
- package/dist/src/aussetzer.js.map +0 -1
- package/dist/src/auth.d.ts +0 -43
- package/dist/src/auth.js.map +0 -1
- package/dist/src/autor-abzeichen.d.ts +0 -41
- package/dist/src/autor-abzeichen.js.map +0 -1
- package/dist/src/autor.d.ts +0 -101
- package/dist/src/autor.js.map +0 -1
- package/dist/src/bauabdruck-daten.generiert.json +0 -5
- package/dist/src/bauabdruck.d.ts +0 -38
- package/dist/src/bauabdruck.js.map +0 -1
- package/dist/src/bedeutung.d.ts +0 -168
- package/dist/src/bedeutung.js.map +0 -1
- package/dist/src/bench/cachly-bench.d.ts +0 -61
- package/dist/src/bench/cachly-bench.js.map +0 -1
- package/dist/src/bench/external-corpus.d.ts +0 -44
- package/dist/src/bench/external-corpus.js.map +0 -1
- package/dist/src/bench/fixtures.d.ts +0 -37
- package/dist/src/bench/fixtures.js.map +0 -1
- package/dist/src/brain-export.d.ts +0 -64
- package/dist/src/brain-export.js.map +0 -1
- package/dist/src/brain-watch-hook.d.ts +0 -21
- package/dist/src/brain-watch-hook.js.map +0 -1
- package/dist/src/cachly-url.d.ts +0 -68
- package/dist/src/cachly-url.js.map +0 -1
- package/dist/src/ckg.d.ts +0 -54
- package/dist/src/ckg.js.map +0 -1
- package/dist/src/cls-hook.d.ts +0 -15
- package/dist/src/cls-hook.js.map +0 -1
- package/dist/src/confidence.d.ts +0 -19
- package/dist/src/confidence.js.map +0 -1
- package/dist/src/credentials.d.ts +0 -37
- package/dist/src/credentials.js.map +0 -1
- package/dist/src/deutsch.d.ts +0 -98
- package/dist/src/deutsch.js.map +0 -1
- package/dist/src/doctor.d.ts +0 -47
- package/dist/src/doctor.js.map +0 -1
- package/dist/src/editor.d.ts +0 -9
- package/dist/src/editor.js.map +0 -1
- package/dist/src/eingaenge-nachruesten.d.ts +0 -41
- package/dist/src/eingaenge-nachruesten.js.map +0 -1
- package/dist/src/eingaenge.d.ts +0 -135
- package/dist/src/eingaenge.js.map +0 -1
- package/dist/src/embeddings.d.ts +0 -48
- package/dist/src/embeddings.js.map +0 -1
- package/dist/src/first-contact.d.ts +0 -56
- package/dist/src/first-contact.js.map +0 -1
- package/dist/src/first-recall-proof.d.ts +0 -19
- package/dist/src/first-recall-proof.js.map +0 -1
- package/dist/src/funnel-milestones.d.ts +0 -5
- package/dist/src/funnel-milestones.js.map +0 -1
- package/dist/src/geliefert.d.ts +0 -63
- package/dist/src/geliefert.js.map +0 -1
- package/dist/src/handlers/advanced.d.ts +0 -6
- package/dist/src/handlers/advanced.js.map +0 -1
- package/dist/src/handlers/brain.d.ts +0 -41
- package/dist/src/handlers/brain.js.map +0 -1
- package/dist/src/handlers/cache.d.ts +0 -6
- package/dist/src/handlers/cache.js.map +0 -1
- package/dist/src/handlers/context.d.ts +0 -6
- package/dist/src/handlers/context.js.map +0 -1
- package/dist/src/handlers/fedbrain.d.ts +0 -18
- package/dist/src/handlers/fedbrain.js.map +0 -1
- package/dist/src/handlers/instances.d.ts +0 -33
- package/dist/src/handlers/instances.js.map +0 -1
- package/dist/src/handlers/roadmap.d.ts +0 -6
- package/dist/src/handlers/roadmap.js.map +0 -1
- package/dist/src/handlers/share.d.ts +0 -8
- package/dist/src/handlers/share.js.map +0 -1
- package/dist/src/handlers/syndicate.d.ts +0 -6
- package/dist/src/handlers/syndicate.js.map +0 -1
- package/dist/src/handlers/team.d.ts +0 -45
- package/dist/src/handlers/team.js.map +0 -1
- package/dist/src/handlers/viz.d.ts +0 -6
- package/dist/src/handlers/viz.js.map +0 -1
- package/dist/src/index.d.ts +0 -93
- package/dist/src/index.js.map +0 -1
- package/dist/src/kostprobe.d.ts +0 -25
- package/dist/src/kostprobe.js.map +0 -1
- package/dist/src/lesson-preview.d.ts +0 -81
- package/dist/src/lesson-preview.js.map +0 -1
- package/dist/src/namespace.d.ts +0 -20
- package/dist/src/namespace.js.map +0 -1
- package/dist/src/notifier.d.ts +0 -5
- package/dist/src/notifier.js.map +0 -1
- package/dist/src/rangfolge.d.ts +0 -169
- package/dist/src/rangfolge.js.map +0 -1
- package/dist/src/raumkorrektur.d.ts +0 -100
- package/dist/src/raumkorrektur.js.map +0 -1
- package/dist/src/recall-tiefe.d.ts +0 -48
- package/dist/src/recall-tiefe.js.map +0 -1
- package/dist/src/rerank.d.ts +0 -48
- package/dist/src/rerank.js.map +0 -1
- package/dist/src/search.d.ts +0 -153
- package/dist/src/search.js.map +0 -1
- package/dist/src/seltenheitsbestand.d.ts +0 -53
- package/dist/src/seltenheitsbestand.js.map +0 -1
- package/dist/src/speicherlage.d.ts +0 -51
- package/dist/src/speicherlage.js.map +0 -1
- package/dist/src/spuren.d.ts +0 -97
- package/dist/src/spuren.js.map +0 -1
- package/dist/src/starter-corpus.d.ts +0 -28
- package/dist/src/starter-corpus.js.map +0 -1
- package/dist/src/telemetry-types.d.ts +0 -87
- package/dist/src/telemetry-types.js.map +0 -1
- package/dist/src/tools.d.ts +0 -2933
- package/dist/src/tools.js.map +0 -1
- package/dist/src/toolspecs.d.ts +0 -43
- package/dist/src/toolspecs.js.map +0 -1
- package/dist/src/upgrade-nudge.d.ts +0 -15
- package/dist/src/upgrade-nudge.js.map +0 -1
- package/dist/src/utils.d.ts +0 -34
- package/dist/src/utils.js.map +0 -1
- package/dist/src/versuch.d.ts +0 -179
- package/dist/src/versuch.js.map +0 -1
- package/dist/src/vorspann.d.ts +0 -73
- package/dist/src/vorspann.js.map +0 -1
- package/dist/src/wartung/vektoren-verdichten.d.ts +0 -50
- package/dist/src/wartung/vektoren-verdichten.js.map +0 -1
- package/dist/src/werkzeug-nutzung.d.ts +0 -62
- package/dist/src/werkzeug-nutzung.js.map +0 -1
- package/dist/src/wertbeitrag.d.ts +0 -57
- package/dist/src/wertbeitrag.js.map +0 -1
|
@@ -1,638 +1,59 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const deleted = await redis.del(...keys);
|
|
61
|
-
return `✅ Deleted **${deleted}** of ${keys.length} key(s): ${keys.map((k) => `\`${k}\``).join(', ')}`;
|
|
62
|
-
}
|
|
63
|
-
case 'cache_exists': {
|
|
64
|
-
const { instance_id, keys } = args;
|
|
65
|
-
const redis = await getConnection(instance_id);
|
|
66
|
-
const count = await redis.exists(...keys);
|
|
67
|
-
return `**${count}** of ${keys.length} key(s) exist in cache.`;
|
|
68
|
-
}
|
|
69
|
-
case 'cache_ttl': {
|
|
70
|
-
const { instance_id, key } = args;
|
|
71
|
-
const redis = await getConnection(instance_id);
|
|
72
|
-
const ttl = await redis.ttl(key);
|
|
73
|
-
if (ttl === -2)
|
|
74
|
-
return `Key \`${key}\` → **does not exist**`;
|
|
75
|
-
if (ttl === -1)
|
|
76
|
-
return `Key \`${key}\` → **no expiry** (persists forever)`;
|
|
77
|
-
const mins = Math.floor(ttl / 60);
|
|
78
|
-
const secs = ttl % 60;
|
|
79
|
-
return `Key \`${key}\` → TTL: **${ttl}s** (${mins}m ${secs}s remaining)`;
|
|
80
|
-
}
|
|
81
|
-
case 'cache_keys': {
|
|
82
|
-
const { instance_id, pattern = '*', count = 50 } = args;
|
|
83
|
-
const limit = Math.min(count, 500);
|
|
84
|
-
const redis = await getConnection(instance_id);
|
|
85
|
-
const keys = [];
|
|
86
|
-
const stream = redis.scanStream({ match: pattern, count: 100 });
|
|
87
|
-
await new Promise((resolve, reject) => {
|
|
88
|
-
stream.on('data', (batch) => {
|
|
89
|
-
keys.push(...batch);
|
|
90
|
-
if (keys.length >= limit) {
|
|
91
|
-
stream.destroy();
|
|
92
|
-
resolve();
|
|
93
|
-
}
|
|
94
|
-
});
|
|
95
|
-
stream.on('end', resolve);
|
|
96
|
-
stream.on('error', reject);
|
|
97
|
-
});
|
|
98
|
-
const result = keys.slice(0, limit);
|
|
99
|
-
if (result.length === 0)
|
|
100
|
-
return `No keys found matching pattern \`${pattern}\`.`;
|
|
101
|
-
return [
|
|
102
|
-
`Found **${result.length}** key(s) matching \`${pattern}\`:`,
|
|
103
|
-
...result.map((k) => ` • \`${k}\``),
|
|
104
|
-
result.length === limit ? `\n_(showing first ${limit} — narrow pattern to see more)_` : '',
|
|
105
|
-
]
|
|
106
|
-
.filter(Boolean)
|
|
107
|
-
.join('\n');
|
|
108
|
-
}
|
|
109
|
-
case 'cache_stats': {
|
|
110
|
-
const { instance_id } = args;
|
|
111
|
-
const redis = await getConnection(instance_id);
|
|
112
|
-
const [infoAll, infoStats, infoKeyspace] = await Promise.all([
|
|
113
|
-
redis.info('memory'),
|
|
114
|
-
redis.info('stats'),
|
|
115
|
-
redis.info('keyspace'),
|
|
116
|
-
]);
|
|
117
|
-
const parse = (section, field) => section.match(new RegExp(`${field}:([^\r\n]+)`))?.[1]?.trim() ?? 'n/a';
|
|
118
|
-
const usedMem = parse(infoAll, 'used_memory_human');
|
|
119
|
-
const peakMem = parse(infoAll, 'used_memory_peak_human');
|
|
120
|
-
const hits = parse(infoStats, 'keyspace_hits');
|
|
121
|
-
const misses = parse(infoStats, 'keyspace_misses');
|
|
122
|
-
const opsPerSec = parse(infoStats, 'instantaneous_ops_per_sec');
|
|
123
|
-
const connectedClients = (await redis.info('clients')).match(/connected_clients:(\d+)/)?.[1] ?? 'n/a';
|
|
124
|
-
const hitsN = parseInt(hits) || 0;
|
|
125
|
-
const missesN = parseInt(misses) || 0;
|
|
126
|
-
const total = hitsN + missesN;
|
|
127
|
-
const hitRate = total > 0 ? ((hitsN / total) * 100).toFixed(1) : 'n/a';
|
|
128
|
-
const keyspaceLines = infoKeyspace
|
|
129
|
-
.split('\n')
|
|
130
|
-
.filter((l) => l.startsWith('db'))
|
|
131
|
-
.map((l) => ` ${l.trim()}`);
|
|
132
|
-
const lines = [
|
|
133
|
-
`📊 **Cache Stats for instance \`${instance_id}\`:**`,
|
|
134
|
-
``,
|
|
135
|
-
` 💾 Memory used: ${usedMem} (peak: ${peakMem})`,
|
|
136
|
-
` ⚡ Ops/sec: ${opsPerSec}`,
|
|
137
|
-
` 🎯 Hit rate: ${hitRate}% (${hits} hits / ${misses} misses)`,
|
|
138
|
-
` 🔗 Clients: ${connectedClients}`,
|
|
139
|
-
``,
|
|
140
|
-
keyspaceLines.length > 0
|
|
141
|
-
? ` 🗂️ Keyspace:\n${keyspaceLines.join('\n')}`
|
|
142
|
-
: ` 🗂️ Keyspace: (empty)`,
|
|
143
|
-
];
|
|
144
|
-
try {
|
|
145
|
-
const inst = await apiFetch(`/api/v1/instances/${instance_id}`);
|
|
146
|
-
if (inst.vector_token) {
|
|
147
|
-
const vectorBase = process.env.CACHLY_VECTOR_URL ?? 'https://api.cachly.dev';
|
|
148
|
-
const statsRes = await fetch(`${vectorBase}/v1/sem/${inst.vector_token}/stats`, {
|
|
149
|
-
signal: AbortSignal.timeout(5000),
|
|
150
|
-
});
|
|
151
|
-
if (statsRes.ok) {
|
|
152
|
-
const data = await statsRes.json();
|
|
153
|
-
const fmt = (n, decimals = 2) => n !== undefined ? n.toLocaleString('en-US', { minimumFractionDigits: decimals, maximumFractionDigits: decimals }) : 'n/a';
|
|
154
|
-
const fmtInt = (n) => n !== undefined ? n.toLocaleString('en-US') : 'n/a';
|
|
155
|
-
const totalHits = data.total_hits ?? 0;
|
|
156
|
-
const totalEntries = data.total_entries ?? 0;
|
|
157
|
-
if (totalHits === 0 && totalEntries === 0) {
|
|
158
|
-
const proj = (devs) => (devs * 50 * 0.25 * 0.002 * 30).toFixed(2);
|
|
159
|
-
lines.push(``, `💰 **Tokenmaxxing ROI — Projection:**`, ` 📭 No cache hits yet — you're just getting started.`, ``, ` 📊 Typical savings for your team size:`, ` 5 devs × 50 LLM calls/day × 25% hit-rate → ~$${proj(5)}/month`, ` 10 devs × 50 LLM calls/day × 25% hit-rate → ~$${proj(10)}/month`, ` 20 devs × 50 LLM calls/day × 25% hit-rate → ~$${proj(20)}/month`, ` (Assumes avg $0.002/call saved per cache hit)`, ``, ` 🚀 Next: route LLM calls through cachly to start accumulating hits.`, ` Docs: ${cachlyUrl('/docs/semantic', 'cache-stats')}`);
|
|
160
|
-
}
|
|
161
|
-
else {
|
|
162
|
-
const wow = data.savings?.week_over_week_pct;
|
|
163
|
-
const wowLine = wow !== undefined
|
|
164
|
-
? ` ${wow >= 0 ? '📈' : '📉'} 7d trend: ${wow >= 0 ? '+' : ''}${wow.toFixed(1)}% vs prev week`
|
|
165
|
-
: '';
|
|
166
|
-
lines.push(``, `💰 **Tokenmaxxing ROI:**`, ` 📦 Semantic entries: ${fmtInt(data.total_entries)}`, ` 🎯 Total cache hits: ${fmtInt(data.total_hits)}`, ` ⏱️ Hits last 24h: ${fmtInt(data.savings?.hits_last_24h)}`, ` 📅 Hits last 7d: ${fmtInt(data.savings?.hits_last_7d)}`, ...(wowLine ? [wowLine] : []), ` 💵 Total saved: $${fmt(data.savings?.estimated_total_saved_usd)}`, ` 📈 Projected/month: $${fmt(data.savings?.estimated_monthly_saved_usd)}`, ` 📊 Avg per hit: $${fmt(data.savings?.avg_cost_per_call_usd, 4)}`);
|
|
167
|
-
}
|
|
168
|
-
if (data.top_hits && data.top_hits.length > 0) {
|
|
169
|
-
lines.push(``, `🔝 Top cached prompts:`);
|
|
170
|
-
const topN = data.top_hits.slice(0, 3);
|
|
171
|
-
topN.forEach((hit, i) => {
|
|
172
|
-
const truncated = hit.prompt.length > 50 ? `${hit.prompt.slice(0, 50)}…` : hit.prompt;
|
|
173
|
-
lines.push(` ${i + 1}. "${truncated}" (hit ${hit.hit_count}×)`);
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
catch {
|
|
180
|
-
// API unavailable or free tier — skip silently
|
|
181
|
-
}
|
|
182
|
-
return lines.join('\n');
|
|
183
|
-
}
|
|
184
|
-
case 'semantic_search': {
|
|
185
|
-
const { instance_id, query, threshold = 0.82, namespace: nsArg = 'cachly:sem', top_k = 5, use_hybrid = false, auto_namespace = false, } = args;
|
|
186
|
-
// resolve namespace from query text when requested
|
|
187
|
-
const namespace = auto_namespace ? detectNamespace(query) : nsArg;
|
|
188
|
-
if (!hasEmbedProvider()) {
|
|
189
|
-
return (`❌ semantic_search requires an embedding provider.\n\n` +
|
|
190
|
-
`Current: ${embedProviderHint()}\n\n` +
|
|
191
|
-
`Set one of these in your MCP env config:\n` +
|
|
192
|
-
` OPENAI_API_KEY (provider: openai – default)\n` +
|
|
193
|
-
` MISTRAL_API_KEY (provider: mistral)\n` +
|
|
194
|
-
` COHERE_API_KEY (provider: cohere)\n` +
|
|
195
|
-
` GEMINI_API_KEY (provider: gemini)\n` +
|
|
196
|
-
` OLLAMA_BASE_URL (provider: ollama – local, no key needed)\n` +
|
|
197
|
-
`Also set: CACHLY_EMBED_PROVIDER=<provider>`);
|
|
198
|
-
}
|
|
199
|
-
const inst = await apiFetch(`/api/v1/instances/${instance_id}`);
|
|
200
|
-
if (!inst.vector_token) {
|
|
201
|
-
return (`❌ Semantic search is only available on Speed and Business tiers.\n\n` +
|
|
202
|
-
`Your instance "${inst.name}" is on the **${inst.tier.toUpperCase()}** tier.\n` +
|
|
203
|
-
`Upgrade at ${cachlyUrl(`/instances/${instance_id}`, 'upgrade')}`);
|
|
204
|
-
}
|
|
205
|
-
// Compute embedding via configured provider
|
|
206
|
-
const embedding = await computeEmbedding(query);
|
|
207
|
-
// Query cachly vector API
|
|
208
|
-
const vectorUrl = process.env.CACHLY_VECTOR_URL ?? `https://api.cachly.dev/v1/sem/${inst.vector_token}`;
|
|
209
|
-
const searchPayload = { embedding, namespace, threshold, top_k };
|
|
210
|
-
// hybrid BM25+Vector RRF: include query text when requested.
|
|
211
|
-
if (use_hybrid) {
|
|
212
|
-
searchPayload['hybrid'] = true;
|
|
213
|
-
searchPayload['prompt'] = query;
|
|
214
|
-
}
|
|
215
|
-
const searchRes = await fetch(`${vectorUrl}/search`, {
|
|
216
|
-
method: 'POST',
|
|
217
|
-
headers: { 'Content-Type': 'application/json' },
|
|
218
|
-
body: JSON.stringify(searchPayload),
|
|
219
|
-
signal: AbortSignal.timeout(8000),
|
|
220
|
-
});
|
|
221
|
-
if (!searchRes.ok) {
|
|
222
|
-
throw new McpError(ErrorCode.InternalError, `Vector search failed: ${searchRes.statusText}`);
|
|
223
|
-
}
|
|
224
|
-
const results = (await searchRes.json());
|
|
225
|
-
if (!results.length || (results.length === 1 && !results[0].found)) {
|
|
226
|
-
return (`🔍 No semantically similar entries found for:\n _"${query}"_\n\n` +
|
|
227
|
-
`Try lowering the threshold (current: ${threshold}) or using different keywords.`);
|
|
228
|
-
}
|
|
229
|
-
const redis = await getConnection(instance_id);
|
|
230
|
-
const lines = [
|
|
231
|
-
`🔍 **Semantic search results** for: _"${query}"_`,
|
|
232
|
-
` Threshold: ${threshold} · Namespace: \`${namespace}\``,
|
|
233
|
-
``,
|
|
234
|
-
];
|
|
235
|
-
for (const hit of results) {
|
|
236
|
-
if (!hit.found || !hit.id)
|
|
237
|
-
continue;
|
|
238
|
-
const value = await redis.get(`${namespace}:val:${hit.id}`);
|
|
239
|
-
lines.push(`**Match** (similarity: ${((hit.similarity ?? 0) * 100).toFixed(1)}%)`, ` Prompt: _"${hit.prompt ?? '(unknown)'}"_`, value ? ` Value: \`${value.slice(0, 200)}${value.length > 200 ? '…' : ''}\`` : ` Value: _(evicted from cache)_`, ``);
|
|
240
|
-
}
|
|
241
|
-
return lines.join('\n');
|
|
242
|
-
}
|
|
243
|
-
// ── Namespace Auto-Detection ──────────────────────────────────────────
|
|
244
|
-
case 'detect_namespace': {
|
|
245
|
-
const { prompt } = args;
|
|
246
|
-
const ns = detectNamespace(prompt);
|
|
247
|
-
const typeLabel = ns.split(':').pop();
|
|
248
|
-
const descriptions = {
|
|
249
|
-
code: '💻 Code — contains programming constructs or syntax',
|
|
250
|
-
translation: '🌐 Translation — asks to translate between languages',
|
|
251
|
-
summary: '📝 Summary — requests a summary or key points (TL;DR)',
|
|
252
|
-
qa: '❓ Q&A — a direct question or query',
|
|
253
|
-
creative: '🎨 Creative — general, creative, or conversational prompt',
|
|
254
|
-
};
|
|
255
|
-
return [
|
|
256
|
-
`**Detected namespace:** \`${ns}\``,
|
|
257
|
-
`**Type:** ${descriptions[typeLabel] ?? typeLabel}`,
|
|
258
|
-
``,
|
|
259
|
-
`_Prompt: "${prompt.slice(0, 120)}${prompt.length > 120 ? '…' : ''}"_`,
|
|
260
|
-
``,
|
|
261
|
-
`💡 Use this namespace in \`semantic_search\` or \`cache_warmup\` for better hit rates.`,
|
|
262
|
-
` Set \`auto_namespace: true\` to apply this detection automatically.`,
|
|
263
|
-
].join('\n');
|
|
264
|
-
}
|
|
265
|
-
// ── Cache Warmup ───────────────────────────────────────────────────────
|
|
266
|
-
case 'cache_warmup': {
|
|
267
|
-
const { instance_id, entries: rawEntries, namespace: nsArg = 'cachly:sem', ttl, auto_namespace = false, } = args;
|
|
268
|
-
if (!hasEmbedProvider()) {
|
|
269
|
-
return (`❌ cache_warmup requires an embedding provider.\n\n` +
|
|
270
|
-
`Current: ${embedProviderHint()}\n\n` +
|
|
271
|
-
`Supported: openai (default) · mistral · cohere · ollama (local) · gemini\n` +
|
|
272
|
-
`Set CACHLY_EMBED_PROVIDER and the matching API key env var.`);
|
|
273
|
-
}
|
|
274
|
-
const inst = await apiFetch(`/api/v1/instances/${instance_id}`);
|
|
275
|
-
const vectorUrl = process.env.CACHLY_VECTOR_URL ??
|
|
276
|
-
(inst.vector_token ? `https://api.cachly.dev/v1/sem/${inst.vector_token}` : null);
|
|
277
|
-
const redis = await getConnection(instance_id);
|
|
278
|
-
let warmed = 0;
|
|
279
|
-
let skipped = 0;
|
|
280
|
-
const details = [];
|
|
281
|
-
for (const entry of rawEntries) {
|
|
282
|
-
// resolve namespace per entry
|
|
283
|
-
const ns = entry.namespace ?? (auto_namespace ? detectNamespace(entry.prompt) : nsArg);
|
|
284
|
-
// Compute embedding for this prompt
|
|
285
|
-
const embedding = await computeEmbedding(entry.prompt);
|
|
286
|
-
// Check if a very-similar entry already exists (threshold 0.98 → skip to avoid duplicates)
|
|
287
|
-
let alreadyCached = false;
|
|
288
|
-
if (vectorUrl) {
|
|
289
|
-
const checkRes = await fetch(`${vectorUrl}/search`, {
|
|
290
|
-
method: 'POST',
|
|
291
|
-
headers: { 'Content-Type': 'application/json' },
|
|
292
|
-
body: JSON.stringify({ embedding, namespace: ns, threshold: 0.98 }),
|
|
293
|
-
signal: AbortSignal.timeout(8000),
|
|
294
|
-
}).catch(() => null);
|
|
295
|
-
if (checkRes?.ok) {
|
|
296
|
-
const results = (await checkRes.json());
|
|
297
|
-
alreadyCached = results[0]?.found ?? false;
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
if (alreadyCached) {
|
|
301
|
-
skipped++;
|
|
302
|
-
details.push(` ⏭️ _"${entry.prompt.slice(0, 60)}${entry.prompt.length > 60 ? '…' : ''}"_ → already cached`);
|
|
303
|
-
continue;
|
|
304
|
-
}
|
|
305
|
-
// Write value to Valkey
|
|
306
|
-
const id = randomUUID();
|
|
307
|
-
const vk = `${ns}:val:${id}`;
|
|
308
|
-
if (ttl && ttl > 0) {
|
|
309
|
-
await redis.set(vk, entry.value, 'EX', ttl);
|
|
310
|
-
}
|
|
311
|
-
else {
|
|
312
|
-
await redis.set(vk, entry.value);
|
|
313
|
-
}
|
|
314
|
-
if (vectorUrl) {
|
|
315
|
-
// pgvector path – index embedding in HNSW
|
|
316
|
-
const body = { id, prompt: entry.prompt, namespace: ns, embedding };
|
|
317
|
-
if (ttl && ttl > 0) {
|
|
318
|
-
body['expires_at'] = new Date(Date.now() + ttl * 1000).toISOString();
|
|
319
|
-
}
|
|
320
|
-
await fetch(`${vectorUrl}/entries`, {
|
|
321
|
-
method: 'POST',
|
|
322
|
-
headers: { 'Content-Type': 'application/json' },
|
|
323
|
-
body: JSON.stringify(body),
|
|
324
|
-
signal: AbortSignal.timeout(8000),
|
|
325
|
-
}).catch(() => undefined);
|
|
326
|
-
}
|
|
327
|
-
else {
|
|
328
|
-
// Legacy SCAN path – write emb key to Valkey
|
|
329
|
-
const embKey = `${ns}:emb:${id}`;
|
|
330
|
-
const embPayload = JSON.stringify({ embedding, prompt: entry.prompt });
|
|
331
|
-
if (ttl && ttl > 0) {
|
|
332
|
-
await redis.set(embKey, embPayload, 'EX', ttl);
|
|
333
|
-
}
|
|
334
|
-
else {
|
|
335
|
-
await redis.set(embKey, embPayload);
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
warmed++;
|
|
339
|
-
details.push(` ✅ _"${entry.prompt.slice(0, 60)}${entry.prompt.length > 60 ? '…' : ''}"_ → \`${ns}\``);
|
|
340
|
-
}
|
|
341
|
-
return [
|
|
342
|
-
`🔥 **Cache Warmup Complete**`,
|
|
343
|
-
``,
|
|
344
|
-
` ✅ Warmed: **${warmed}** new entries`,
|
|
345
|
-
` ⏭️ Skipped: **${skipped}** (already cached at ≥ 0.98 similarity)`,
|
|
346
|
-
` 📦 Total: ${rawEntries.length}`,
|
|
347
|
-
auto_namespace
|
|
348
|
-
? ` 🏷️ Namespacing: auto-detected per prompt`
|
|
349
|
-
: ` 🏷️ Namespace: \`${nsArg}\``,
|
|
350
|
-
vectorUrl
|
|
351
|
-
? ` 🔍 Mode: pgvector HNSW (Speed/Business tier)`
|
|
352
|
-
: ` 🔍 Mode: Valkey SCAN (upgrade to Speed tier for scalable search)`,
|
|
353
|
-
``,
|
|
354
|
-
...details,
|
|
355
|
-
].join('\n');
|
|
356
|
-
}
|
|
357
|
-
// ── index_project – Codebase Indexing ─────────────────────────────────────
|
|
358
|
-
case 'index_project': {
|
|
359
|
-
const { instance_id, dir, extensions: extArg, max_files = 100, ttl = 86400, summary_chars = 1200, namespace: nsArg = 'cachly:sem:code', } = args;
|
|
360
|
-
const ALLOWED_EXT = new Set((extArg ?? ['ts', 'js', 'tsx', 'jsx', 'go', 'py', 'java', 'rs', 'md', 'kt', 'swift']).map((e) => (e.startsWith('.') ? e : `.${e}`)));
|
|
361
|
-
// Recursively collect files up to max_files limit
|
|
362
|
-
const files = [];
|
|
363
|
-
async function walk(d) {
|
|
364
|
-
if (files.length >= max_files)
|
|
365
|
-
return;
|
|
366
|
-
const entries = await readdir(d, { withFileTypes: true }).catch(() => null);
|
|
367
|
-
if (!entries)
|
|
368
|
-
return;
|
|
369
|
-
for (const entry of entries) {
|
|
370
|
-
if (files.length >= max_files)
|
|
371
|
-
break;
|
|
372
|
-
const full = join(d, entry.name);
|
|
373
|
-
if (entry.isDirectory()) {
|
|
374
|
-
if (['.git', 'node_modules', 'dist', 'build', '.next', '__pycache__', 'vendor'].includes(entry.name))
|
|
375
|
-
continue;
|
|
376
|
-
await walk(full);
|
|
377
|
-
}
|
|
378
|
-
else if (entry.isFile() && ALLOWED_EXT.has(extname(entry.name).toLowerCase())) {
|
|
379
|
-
files.push(full);
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
await walk(dir);
|
|
384
|
-
if (files.length === 0) {
|
|
385
|
-
return `❌ No matching files found in \`${dir}\`.\nExtensions checked: ${[...ALLOWED_EXT].join(', ')}`;
|
|
386
|
-
}
|
|
387
|
-
const inst = await apiFetch(`/api/v1/instances/${instance_id}`);
|
|
388
|
-
const vectorUrl = process.env.CACHLY_VECTOR_URL ??
|
|
389
|
-
(inst.vector_token ? `https://api.cachly.dev/v1/sem/${inst.vector_token}` : null);
|
|
390
|
-
const canEmbed = vectorUrl && hasEmbedProvider();
|
|
391
|
-
let indexed = 0;
|
|
392
|
-
let skipped = 0;
|
|
393
|
-
let errors = 0;
|
|
394
|
-
let semanticIndexed = 0;
|
|
395
|
-
let unchanged = 0;
|
|
396
|
-
const details = [];
|
|
397
|
-
const expiresAt = new Date(Date.now() + ttl * 1000).toISOString();
|
|
398
|
-
const redis = await getConnection(instance_id);
|
|
399
|
-
for (const filePath of files) {
|
|
400
|
-
const relPath = relative(dir, filePath);
|
|
401
|
-
let content;
|
|
402
|
-
let fileSize;
|
|
403
|
-
try {
|
|
404
|
-
const s = await stat(filePath);
|
|
405
|
-
if (s.size > 200_000) {
|
|
406
|
-
skipped++;
|
|
407
|
-
continue;
|
|
408
|
-
} // skip files >200 KB
|
|
409
|
-
fileSize = s.size;
|
|
410
|
-
content = await readFile(filePath, 'utf-8');
|
|
411
|
-
}
|
|
412
|
-
catch {
|
|
413
|
-
errors++;
|
|
414
|
-
continue;
|
|
415
|
-
}
|
|
416
|
-
// ── Smart invalidation: hash-based change detection ──
|
|
417
|
-
// Compute a simple hash of file content to skip unchanged files
|
|
418
|
-
const hashKey = `cachly:idx:hash:${relPath}`;
|
|
419
|
-
const contentHash = `${fileSize}:${content.length}:${simpleHash(content)}`;
|
|
420
|
-
const existingHash = await redis.get(hashKey);
|
|
421
|
-
if (existingHash === contentHash) {
|
|
422
|
-
// File unchanged — refresh TTL but skip re-indexing
|
|
423
|
-
const idxKey = `cachly:idx:${relPath}`;
|
|
424
|
-
if (ttl > 0)
|
|
425
|
-
await redis.expire(idxKey, ttl);
|
|
426
|
-
if (ttl > 0)
|
|
427
|
-
await redis.expire(hashKey, ttl);
|
|
428
|
-
unchanged++;
|
|
429
|
-
continue;
|
|
430
|
-
}
|
|
431
|
-
const excerpt = content.slice(0, summary_chars).replace(/\s+/g, ' ').trim();
|
|
432
|
-
// ── Layer 1: Keyword index in Valkey (always works, no embedding needed) ──
|
|
433
|
-
const idxKey = `cachly:idx:${relPath}`;
|
|
434
|
-
const idxValue = `File: ${relPath}\n${excerpt}`;
|
|
435
|
-
if (ttl > 0) {
|
|
436
|
-
await redis.set(idxKey, idxValue, 'EX', ttl);
|
|
437
|
-
}
|
|
438
|
-
else {
|
|
439
|
-
await redis.set(idxKey, idxValue);
|
|
440
|
-
}
|
|
441
|
-
// Store content hash for smart invalidation on next run
|
|
442
|
-
if (ttl > 0) {
|
|
443
|
-
await redis.set(hashKey, contentHash, 'EX', ttl);
|
|
444
|
-
}
|
|
445
|
-
else {
|
|
446
|
-
await redis.set(hashKey, contentHash);
|
|
447
|
-
}
|
|
448
|
-
indexed++;
|
|
449
|
-
details.push(` ✅ ${relPath}`);
|
|
450
|
-
// ── Layer 2: Semantic vector index (optional, only if embedding available) ──
|
|
451
|
-
if (canEmbed) {
|
|
452
|
-
try {
|
|
453
|
-
const prompt = `File: ${relPath}\n${excerpt}`;
|
|
454
|
-
const embedding = await computeEmbedding(prompt);
|
|
455
|
-
const id = randomUUID();
|
|
456
|
-
await fetch(`${vectorUrl}/entries`, {
|
|
457
|
-
method: 'POST',
|
|
458
|
-
headers: { 'Content-Type': 'application/json' },
|
|
459
|
-
body: JSON.stringify({ id, prompt, namespace: nsArg, embedding, expires_at: expiresAt }),
|
|
460
|
-
signal: AbortSignal.timeout(8000),
|
|
461
|
-
});
|
|
462
|
-
await redis.set(`${nsArg}:val:${id}`, relPath, 'EX', ttl);
|
|
463
|
-
semanticIndexed++;
|
|
464
|
-
}
|
|
465
|
-
catch {
|
|
466
|
-
// Semantic indexing failed — keyword index is enough
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
const mode = canEmbed ? '🔍 Keyword + 🎯 Semantic' : '🔍 Keyword only (no embedding provider)';
|
|
471
|
-
return [
|
|
472
|
-
`📂 **index_project Complete** — ${mode}`,
|
|
473
|
-
``,
|
|
474
|
-
` 📁 Dir: ${dir}`,
|
|
475
|
-
` ✅ Indexed: **${indexed}** files (new/changed)`,
|
|
476
|
-
` ♻️ Unchanged: ${unchanged} files (hash match — skipped)`,
|
|
477
|
-
...(canEmbed ? [` 🎯 Semantic: **${semanticIndexed}** files (vector-searchable)`] : []),
|
|
478
|
-
` ⏭️ Skipped: ${skipped} (too large or filtered)`,
|
|
479
|
-
` ❌ Errors: ${errors}`,
|
|
480
|
-
` ⏱️ TTL: ${ttl}s (${Math.round(ttl / 3600)}h)`,
|
|
481
|
-
``,
|
|
482
|
-
`💡 **Next steps:**`,
|
|
483
|
-
` 1. Use \`smart_recall("how does auth work")\` to find relevant files.`,
|
|
484
|
-
` 2. Re-run index_project after major refactors.`,
|
|
485
|
-
...(canEmbed ? [] : [` 3. Set OPENAI_API_KEY (or similar) in .env to also enable semantic search.`]),
|
|
486
|
-
``,
|
|
487
|
-
...(details.length <= 20 ? details : [...details.slice(0, 20), ` … and ${details.length - 20} more`]),
|
|
488
|
-
].join('\n');
|
|
489
|
-
}
|
|
490
|
-
// ── Bulk / Lock / Stream tools ──────────────────────────────────────────
|
|
491
|
-
case 'cache_mset': {
|
|
492
|
-
const { instance_id, items } = args;
|
|
493
|
-
if (!Array.isArray(items) || items.length === 0)
|
|
494
|
-
return '⚠️ No items provided.';
|
|
495
|
-
const redis = await getConnection(instance_id);
|
|
496
|
-
const pipe = redis.pipeline();
|
|
497
|
-
for (const item of items) {
|
|
498
|
-
const serialized = typeof item.value === 'string' ? item.value : JSON.stringify(item.value);
|
|
499
|
-
if (item.ttl && item.ttl > 0) {
|
|
500
|
-
pipe.set(item.key, serialized, 'EX', item.ttl);
|
|
501
|
-
}
|
|
502
|
-
else {
|
|
503
|
-
pipe.set(item.key, serialized);
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
await pipe.exec();
|
|
507
|
-
return `✅ **cache_mset** – ${items.length} key(s) written in one pipeline round-trip.\n` +
|
|
508
|
-
items.map(i => ` • \`${i.key}\`${i.ttl ? ` (TTL ${i.ttl}s)` : ''}`).join('\n');
|
|
509
|
-
}
|
|
510
|
-
case 'cache_mget': {
|
|
511
|
-
const { instance_id, keys } = args;
|
|
512
|
-
if (!Array.isArray(keys) || keys.length === 0)
|
|
513
|
-
return '⚠️ No keys provided.';
|
|
514
|
-
const redis = await getConnection(instance_id);
|
|
515
|
-
const raws = await redis.mget(...keys);
|
|
516
|
-
const result = keys.map((k, i) => {
|
|
517
|
-
const raw = raws[i];
|
|
518
|
-
if (raw === null)
|
|
519
|
-
return ` • \`${k}\`: _null (miss)_`;
|
|
520
|
-
return ` • \`${k}\`: ${raw}`;
|
|
521
|
-
});
|
|
522
|
-
return `✅ **cache_mget** – ${keys.length} key(s) fetched in one round-trip.\n` + result.join('\n');
|
|
523
|
-
}
|
|
524
|
-
case 'cache_lock_acquire': {
|
|
525
|
-
const { instance_id, key, ttl_ms = 5000, retries = 3, retry_delay_ms = 50 } = args;
|
|
526
|
-
const redis = await getConnection(instance_id);
|
|
527
|
-
const lockKey = `cachly:lock:${key}`;
|
|
528
|
-
const token = randomUUID();
|
|
529
|
-
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
530
|
-
const result = await redis.set(lockKey, token, 'PX', ttl_ms, 'NX');
|
|
531
|
-
if (result === 'OK') {
|
|
532
|
-
return `🔒 **cache_lock_acquire** – Lock acquired!\n\n Key: \`${key}\`\n Token: \`${token}\`\n TTL: ${ttl_ms} ms\n\n💡 Use **cache_lock_release** with this token to release early.`;
|
|
533
|
-
}
|
|
534
|
-
if (attempt < retries)
|
|
535
|
-
await new Promise(r => setTimeout(r, retry_delay_ms));
|
|
536
|
-
}
|
|
537
|
-
return `❌ **cache_lock_acquire** – Could not acquire lock for \`${key}\` after ${retries + 1} attempts.`;
|
|
538
|
-
}
|
|
539
|
-
case 'cache_lock_release': {
|
|
540
|
-
const { instance_id, key, token } = args;
|
|
541
|
-
const redis = await getConnection(instance_id);
|
|
542
|
-
const script = `if redis.call("get",KEYS[1])==ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end`;
|
|
543
|
-
const released = await redis.eval(script, 1, `cachly:lock:${key}`, token);
|
|
544
|
-
return released === 1
|
|
545
|
-
? `🔓 **cache_lock_release** – Lock \`${key}\` released.`
|
|
546
|
-
: `⚠️ **cache_lock_release** – Lock \`${key}\` expired or token mismatch.`;
|
|
547
|
-
}
|
|
548
|
-
case 'cache_stream_set': {
|
|
549
|
-
const { instance_id, key, chunks, ttl } = args;
|
|
550
|
-
if (!Array.isArray(chunks) || chunks.length === 0)
|
|
551
|
-
return '⚠️ No chunks provided.';
|
|
552
|
-
const redis = await getConnection(instance_id);
|
|
553
|
-
const listKey = `cachly:stream:${key}`;
|
|
554
|
-
await redis.del(listKey);
|
|
555
|
-
const pipe = redis.pipeline();
|
|
556
|
-
for (const chunk of chunks)
|
|
557
|
-
pipe.rpush(listKey, chunk);
|
|
558
|
-
if (ttl && ttl > 0)
|
|
559
|
-
pipe.expire(listKey, ttl);
|
|
560
|
-
await pipe.exec();
|
|
561
|
-
return `✅ **cache_stream_set** – ${chunks.length} chunk(s) stored.\n Key: \`${key}\`\n${ttl ? ` TTL: ${ttl}s\n` : ''} Total size: ${chunks.reduce((a, c) => a + c.length, 0)} chars`;
|
|
562
|
-
}
|
|
563
|
-
case 'cache_stream_get': {
|
|
564
|
-
const { instance_id, key } = args;
|
|
565
|
-
const redis = await getConnection(instance_id);
|
|
566
|
-
const listKey = `cachly:stream:${key}`;
|
|
567
|
-
const len = await redis.llen(listKey);
|
|
568
|
-
if (len === 0)
|
|
569
|
-
return `⚠️ **cache_stream_get** – Cache miss for key \`${key}\`.`;
|
|
570
|
-
const chunks = await redis.lrange(listKey, 0, -1);
|
|
571
|
-
const full = chunks.join('');
|
|
572
|
-
const preview = full.slice(0, 500);
|
|
573
|
-
return `✅ **cache_stream_get** – ${len} chunk(s) retrieved for \`${key}\`.\n\n**Preview** (first 500 chars):\n\`\`\`\n${preview}${preview.length < full.length ? '…' : ''}\n\`\`\``;
|
|
574
|
-
}
|
|
575
|
-
case 'cache_org_stats': {
|
|
576
|
-
const { instance_id, org_id } = args;
|
|
577
|
-
const redis = await getConnection(instance_id);
|
|
578
|
-
const namespace = `org:${org_id}:sem`;
|
|
579
|
-
const keys = await scanKeys(redis, `${namespace}:*`, { max: 10000, timeoutMs: 5000 });
|
|
580
|
-
const entryCount = keys.length;
|
|
581
|
-
const lines = [
|
|
582
|
-
`🏢 Org Cache Stats for org \`${org_id}\`:`,
|
|
583
|
-
` 📦 Shared entries: ${entryCount}`,
|
|
584
|
-
` 🔑 Namespace: ${namespace}`,
|
|
585
|
-
` 👥 Benefit: All instances sharing this org_id serve from the same cache`,
|
|
586
|
-
``,
|
|
587
|
-
`💡 To enable sharing: set org_id="${org_id}" in cache_set calls`,
|
|
588
|
-
];
|
|
589
|
-
// Org-wide savings aggregation (API endpoint may not be deployed yet,
|
|
590
|
-
// or the caller may not be an org member — fail silently in that case).
|
|
591
|
-
try {
|
|
592
|
-
const savings = await apiFetch(`/api/v1/orgs/${org_id}/savings`);
|
|
593
|
-
const usd = (n) => `$${(n ?? 0).toFixed(2)}`;
|
|
594
|
-
lines.push(``, `💰 Org-wide Tokenmaxxing ROI:`, ` 🖥️ Instances: ${savings.instance_count}`, ` 🎯 Total cache hits: ${savings.total_hits.toLocaleString('en-US')}`, ` ⏱️ Hits last 24h: ${savings.hits_last_24h.toLocaleString('en-US')}`, ` 💵 Total saved: ${usd(savings.estimated_total_saved_usd)}`, ` 📈 Projected/month: ${usd(savings.estimated_monthly_saved_usd)}`);
|
|
595
|
-
const top = [...(savings.per_instance ?? [])]
|
|
596
|
-
.sort((a, b) => b.estimated_total_saved_usd - a.estimated_total_saved_usd)
|
|
597
|
-
.slice(0, 5);
|
|
598
|
-
if (top.length > 0) {
|
|
599
|
-
lines.push(``, ` Top instances by savings:`);
|
|
600
|
-
for (const inst of top) {
|
|
601
|
-
lines.push(` • ${inst.name || inst.instance_id}: ${usd(inst.estimated_total_saved_usd)} (${inst.total_hits.toLocaleString('en-US')} hits)`);
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
catch {
|
|
606
|
-
// Endpoint unavailable / not a member / network error → Redis-only output.
|
|
607
|
-
}
|
|
608
|
-
return lines.join('\n');
|
|
609
|
-
}
|
|
610
|
-
case 'set_cost_per_call': {
|
|
611
|
-
const { instance_id, cost_per_call_usd } = args;
|
|
612
|
-
if (!cost_per_call_usd || cost_per_call_usd <= 0 || cost_per_call_usd > 100) {
|
|
613
|
-
throw new McpError(ErrorCode.InvalidParams, 'cost_per_call_usd must be a positive number ≤ 100');
|
|
614
|
-
}
|
|
615
|
-
await apiFetch(`/api/v1/instances/${instance_id}/cost-per-call`, {
|
|
616
|
-
method: 'PUT',
|
|
617
|
-
body: JSON.stringify({ cost_per_call_usd }),
|
|
618
|
-
});
|
|
619
|
-
const examples = {
|
|
620
|
-
'0.02': 'claude-opus-4.8',
|
|
621
|
-
'0.015': 'gpt-5.5',
|
|
622
|
-
'0.009': 'claude-sonnet-4.6',
|
|
623
|
-
'0.002': 'gpt-5.5-mini',
|
|
624
|
-
'0.001': 'claude-haiku-4.5',
|
|
625
|
-
};
|
|
626
|
-
const modelHint = examples[cost_per_call_usd.toString()] ? ` (${examples[cost_per_call_usd.toString()]})` : '';
|
|
627
|
-
return [
|
|
628
|
-
`✅ **Cost per call updated** — $${cost_per_call_usd.toFixed(4)}${modelHint}`,
|
|
629
|
-
``,
|
|
630
|
-
`ROI savings in \`cache_stats\` will now use $${cost_per_call_usd.toFixed(4)} per avoided LLM call.`,
|
|
631
|
-
`Run \`cache_stats\` to see your updated savings estimate.`,
|
|
632
|
-
].join('\n');
|
|
633
|
-
}
|
|
634
|
-
default:
|
|
635
|
-
return null;
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
//# sourceMappingURL=cache.js.map
|
|
1
|
+
import{randomUUID as P}from"node:crypto";import{McpError as D,ErrorCode as Y}from"@modelcontextprotocol/sdk/types.js";import{readdir as J,stat as B,readFile as z}from"node:fs/promises";import{join as W,relative as G,extname as Q}from"node:path";import{computeEmbedding as C,hasEmbedProvider as K,embedProviderHint as q}from"../embeddings.js";import{detectNamespace as I}from"../namespace.js";import{simpleHash as Z}from"../confidence.js";import{scanKeys as ee}from"../utils.js";import{cachlyUrl as V}from"../cachly-url.js";const de=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 he(X,p,u,O){switch(X){case"cache_get":{const{instance_id:n,key:e,org_id:a}=p,t=await u(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
|
+
\`\`\`
|
|
3
|
+
${c}
|
|
4
|
+
\`\`\``}case"cache_set":{const{instance_id:n,key:e,value:a,ttl:t,org_id:s}=p,i=await u(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}=p;return`\u2705 Deleted **${await(await u(n)).del(...e)}** of ${e.length} key(s): ${e.map(s=>`\`${s}\``).join(", ")}`}case"cache_exists":{const{instance_id:n,keys:e}=p;return`**${await(await u(n)).exists(...e)}** of ${e.length} key(s) exist in cache.`}case"cache_ttl":{const{instance_id:n,key:e}=p,t=await(await u(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}=p,t=Math.min(a,500),s=await u(n),i=[],c=s.scanStream({match:e,count:100});await new Promise((r,f)=>{c.on("data",l=>{i.push(...l),i.length>=t&&(c.destroy(),r())}),c.on("end",r),c.on("error",f)});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
|
+
_(showing first ${t} \u2014 narrow pattern to see more)_`:""].filter(Boolean).join(`
|
|
6
|
+
`)}case"cache_stats":{const{instance_id:n}=p,e=await u(n),[a,t,s]=await Promise.all([e.info("memory"),e.info("stats"),e.info("keyspace")]),i=(_,j)=>_.match(new RegExp(`${j}:([^\r
|
|
7
|
+
]+)`))?.[1]?.trim()??"n/a",c=i(a,"used_memory_human"),o=i(a,"used_memory_peak_human"),r=i(t,"keyspace_hits"),f=i(t,"keyspace_misses"),l=i(t,"instantaneous_ops_per_sec"),d=(await e.info("clients")).match(/connected_clients:(\d+)/)?.[1]??"n/a",$=parseInt(r)||0,S=parseInt(f)||0,L=$+S,T=L>0?($/L*100).toFixed(1):"n/a",w=s.split(`
|
|
8
|
+
`).filter(_=>_.startsWith("db")).map(_=>` ${_.trim()}`),h=[`\u{1F4CA} **Cache Stats for instance \`${n}\`:**`,"",` \u{1F4BE} Memory used: ${c} (peak: ${o})`,` \u26A1 Ops/sec: ${l}`,` \u{1F3AF} Hit rate: ${T}% (${r} hits / ${f} misses)`,` \u{1F517} Clients: ${d}`,"",w.length>0?` \u{1F5C2}\uFE0F Keyspace:
|
|
9
|
+
${w.join(`
|
|
10
|
+
`)}`:" \u{1F5C2}\uFE0F Keyspace: (empty)"];try{const _=await O(`/api/v1/instances/${n}`);if(_.vector_token){const j=process.env.CACHLY_VECTOR_URL??"https://api.cachly.dev",E=await fetch(`${j}/v1/sem/${_.vector_token}/stats`,{signal:AbortSignal.timeout(5e3)});if(E.ok){const v=await E.json(),k=(m,y=2)=>m!==void 0?m.toLocaleString("en-US",{minimumFractionDigits:y,maximumFractionDigits:y}):"n/a",g=m=>m!==void 0?m.toLocaleString("en-US"):"n/a",x=v.total_hits??0,A=v.total_entries??0;if(x===0&&A===0){const m=y=>(y*50*.25*.002*30).toFixed(2);h.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 ~$${m(5)}/month`,` 10 devs \xD7 50 LLM calls/day \xD7 25% hit-rate \u2192 ~$${m(10)}/month`,` 20 devs \xD7 50 LLM calls/day \xD7 25% hit-rate \u2192 ~$${m(20)}/month`," (Assumes avg $0.002/call saved per cache hit)",""," \u{1F680} Next: route LLM calls through cachly to start accumulating hits.",` Docs: ${V("/docs/semantic","cache-stats")}`)}else{const m=v.savings?.week_over_week_pct,y=m!==void 0?` ${m>=0?"\u{1F4C8}":"\u{1F4C9}"} 7d trend: ${m>=0?"+":""}${m.toFixed(1)}% vs prev week`:"";h.push("","\u{1F4B0} **Tokenmaxxing ROI:**",` \u{1F4E6} Semantic entries: ${g(v.total_entries)}`,` \u{1F3AF} Total cache hits: ${g(v.total_hits)}`,` \u23F1\uFE0F Hits last 24h: ${g(v.savings?.hits_last_24h)}`,` \u{1F4C5} Hits last 7d: ${g(v.savings?.hits_last_7d)}`,...y?[y]:[],` \u{1F4B5} Total saved: $${k(v.savings?.estimated_total_saved_usd)}`,` \u{1F4C8} Projected/month: $${k(v.savings?.estimated_monthly_saved_usd)}`,` \u{1F4CA} Avg per hit: $${k(v.savings?.avg_cost_per_call_usd,4)}`)}v.top_hits&&v.top_hits.length>0&&(h.push("","\u{1F51D} Top cached prompts:"),v.top_hits.slice(0,3).forEach((y,R)=>{const N=y.prompt.length>50?`${y.prompt.slice(0,50)}\u2026`:y.prompt;h.push(` ${R+1}. "${N}" (hit ${y.hit_count}\xD7)`)}))}}}catch{}return h.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}=p,o=c?I(e):t;if(!K())return`\u274C semantic_search requires an embedding provider.
|
|
12
|
+
|
|
13
|
+
Current: ${q()}
|
|
14
|
+
|
|
15
|
+
Set one of these in your MCP env config:
|
|
16
|
+
OPENAI_API_KEY (provider: openai \u2013 default)
|
|
17
|
+
MISTRAL_API_KEY (provider: mistral)
|
|
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 O(`/api/v1/instances/${n}`);if(!r.vector_token)return`\u274C Semantic search is only available on Speed and Business tiers.
|
|
22
|
+
|
|
23
|
+
Your instance "${r.name}" is on the **${r.tier.toUpperCase()}** tier.
|
|
24
|
+
Upgrade at ${V(`/instances/${n}`,"upgrade")}`;const f=await C(e),l=process.env.CACHLY_VECTOR_URL??`https://api.cachly.dev/v1/sem/${r.vector_token}`,d={embedding:f,namespace:o,threshold:a,top_k:s};i&&(d.hybrid=!0,d.prompt=e);const $=await fetch(`${l}/search`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(d),signal:AbortSignal.timeout(8e3)});if(!$.ok)throw new D(Y.InternalError,`Vector search failed: ${$.statusText}`);const S=await $.json();if(!S.length||S.length===1&&!S[0].found)return`\u{1F50D} No semantically similar entries found for:
|
|
25
|
+
_"${e}"_
|
|
26
|
+
|
|
27
|
+
Try lowering the threshold (current: ${a}) or using different keywords.`;const L=await u(n),T=[`\u{1F50D} **Semantic search results** for: _"${e}"_`,` Threshold: ${a} \xB7 Namespace: \`${o}\``,""];for(const w of S){if(!w.found||!w.id)continue;const h=await L.get(`${o}:val:${w.id}`);T.push(`**Match** (similarity: ${((w.similarity??0)*100).toFixed(1)}%)`,` Prompt: _"${w.prompt??"(unknown)"}"_`,h?` Value: \`${h.slice(0,200)}${h.length>200?"\u2026":""}\``:" Value: _(evicted from cache)_","")}return T.join(`
|
|
28
|
+
`)}case"detect_namespace":{const{prompt:n}=p,e=I(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(`
|
|
29
|
+
`)}case"cache_warmup":{const{instance_id:n,entries:e,namespace:a="cachly:sem",ttl:t,auto_namespace:s=!1}=p;if(!K())return`\u274C cache_warmup requires an embedding provider.
|
|
30
|
+
|
|
31
|
+
Current: ${q()}
|
|
32
|
+
|
|
33
|
+
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 O(`/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 u(n);let r=0,f=0;const l=[];for(const d of e){const $=d.namespace??(s?I(d.prompt):a),S=await C(d.prompt);let L=!1;if(c){const h=await fetch(`${c}/search`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({embedding:S,namespace:$,threshold:.98}),signal:AbortSignal.timeout(8e3)}).catch(()=>null);h?.ok&&(L=(await h.json())[0]?.found??!1)}if(L){f++,l.push(` \u23ED\uFE0F _"${d.prompt.slice(0,60)}${d.prompt.length>60?"\u2026":""}"_ \u2192 already cached`);continue}const T=P(),w=`${$}:val:${T}`;if(t&&t>0?await o.set(w,d.value,"EX",t):await o.set(w,d.value),c){const h={id:T,prompt:d.prompt,namespace:$,embedding:S};t&&t>0&&(h.expires_at=new Date(Date.now()+t*1e3).toISOString()),await fetch(`${c}/entries`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(h),signal:AbortSignal.timeout(8e3)}).catch(()=>{})}else{const h=`${$}:emb:${T}`,_=JSON.stringify({embedding:S,prompt:d.prompt});t&&t>0?await o.set(h,_,"EX",t):await o.set(h,_)}r++,l.push(` \u2705 _"${d.prompt.slice(0,60)}${d.prompt.length>60?"\u2026":""}"_ \u2192 \`${$}\``)}return["\u{1F525} **Cache Warmup Complete**","",` \u2705 Warmed: **${r}** new entries`,` \u23ED\uFE0F Skipped: **${f}** (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)","",...l].join(`
|
|
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"}=p,o=new Set((a??["ts","js","tsx","jsx","go","py","java","rs","md","kt","swift"]).map(k=>k.startsWith(".")?k:`.${k}`)),r=[];async function f(k){if(r.length>=t)return;const g=await J(k,{withFileTypes:!0}).catch(()=>null);if(g)for(const x of g){if(r.length>=t)break;const A=W(k,x.name);if(x.isDirectory()){if([".git","node_modules","dist","build",".next","__pycache__","vendor"].includes(x.name))continue;await f(A)}else x.isFile()&&o.has(Q(x.name).toLowerCase())&&r.push(A)}}if(await f(e),r.length===0)return`\u274C No matching files found in \`${e}\`.
|
|
36
|
+
Extensions checked: ${[...o].join(", ")}`;const l=await O(`/api/v1/instances/${n}`),d=process.env.CACHLY_VECTOR_URL??(l.vector_token?`https://api.cachly.dev/v1/sem/${l.vector_token}`:null),$=d&&K();let S=0,L=0,T=0,w=0,h=0;const _=[],j=new Date(Date.now()+s*1e3).toISOString(),E=await u(n);for(const k of r){const g=G(e,k);let x,A;try{const b=await B(k);if(b.size>2e5){L++;continue}A=b.size,x=await z(k,"utf-8")}catch{T++;continue}const m=`cachly:idx:hash:${g}`,y=`${A}:${x.length}:${Z(x)}`;if(await E.get(m)===y){const b=`cachly:idx:${g}`;s>0&&await E.expire(b,s),s>0&&await E.expire(m,s),h++;continue}const N=x.slice(0,i).replace(/\s+/g," ").trim(),M=`cachly:idx:${g}`,U=`File: ${g}
|
|
37
|
+
${N}`;if(s>0?await E.set(M,U,"EX",s):await E.set(M,U),s>0?await E.set(m,y,"EX",s):await E.set(m,y),S++,_.push(` \u2705 ${g}`),$)try{const b=`File: ${g}
|
|
38
|
+
${N}`,F=await C(b),H=P();await fetch(`${d}/entries`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:H,prompt:b,namespace:c,embedding:F,expires_at:j}),signal:AbortSignal.timeout(8e3)}),await E.set(`${c}:val:${H}`,g,"EX",s),w++}catch{}}return[`\u{1F4C2} **index_project Complete** \u2014 ${$?"\u{1F50D} Keyword + \u{1F3AF} Semantic":"\u{1F50D} Keyword only (no embedding provider)"}`,"",` \u{1F4C1} Dir: ${e}`,` \u2705 Indexed: **${S}** files (new/changed)`,` \u267B\uFE0F Unchanged: ${h} files (hash match \u2014 skipped)`,...$?[` \u{1F3AF} Semantic: **${w}** files (vector-searchable)`]:[],` \u23ED\uFE0F Skipped: ${L} (too large or filtered)`,` \u274C Errors: ${T}`,` \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.",...$?[]:[" 3. Set OPENAI_API_KEY (or similar) in .env to also enable semantic search."],"",..._.length<=20?_:[..._.slice(0,20),` \u2026 and ${_.length-20} more`]].join(`
|
|
39
|
+
`)}case"cache_mset":{const{instance_id:n,items:e}=p;if(!Array.isArray(e)||e.length===0)return"\u26A0\uFE0F No items provided.";const t=(await u(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
|
+
`+e.map(s=>` \u2022 \`${s.key}\`${s.ttl?` (TTL ${s.ttl}s)`:""}`).join(`
|
|
41
|
+
`)}case"cache_mget":{const{instance_id:n,keys:e}=p;if(!Array.isArray(e)||e.length===0)return"\u26A0\uFE0F No keys provided.";const t=await(await u(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
|
+
`+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}=p,i=await u(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
|
+
|
|
45
|
+
Key: \`${e}\`
|
|
46
|
+
Token: \`${o}\`
|
|
47
|
+
TTL: ${a} ms
|
|
48
|
+
|
|
49
|
+
\u{1F4A1} Use **cache_lock_release** with this token to release early.`;r<t&&await new Promise(l=>setTimeout(l,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}=p;return await(await u(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}=p;if(!Array.isArray(a)||a.length===0)return"\u26A0\uFE0F No chunks provided.";const s=await u(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
|
+
Key: \`${e}\`
|
|
51
|
+
${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}=p,a=await u(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
|
+
|
|
54
|
+
**Preview** (first 500 chars):
|
|
55
|
+
\`\`\`
|
|
56
|
+
${o}${o.length<c.length?"\u2026":""}
|
|
57
|
+
\`\`\``}case"cache_org_stats":{const{instance_id:n,org_id:e}=p,a=await u(n),t=`org:${e}:sem`,i=(await ee(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 O(`/api/v1/orgs/${e}/savings`),r=l=>`$${(l??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 f=[...o.per_instance??[]].sort((l,d)=>d.estimated_total_saved_usd-l.estimated_total_saved_usd).slice(0,5);if(f.length>0){c.push(""," Top instances by savings:");for(const l of f)c.push(` \u2022 ${l.name||l.instance_id}: ${r(l.estimated_total_saved_usd)} (${l.total_hits.toLocaleString("en-US")} hits)`)}}catch{}return c.join(`
|
|
58
|
+
`)}case"set_cost_per_call":{const{instance_id:n,cost_per_call_usd:e}=p;if(!e||e<=0||e>100)throw new D(Y.InvalidParams,"cost_per_call_usd must be a positive number \u2264 100");await O(`/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(`
|
|
59
|
+
`)}default:return null}}export{de as CACHE_TOOL_NAMES,he as handleCacheTool};
|