@cachly-dev/mcp-server 0.10.65 → 0.10.67

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (277) hide show
  1. package/dist/src/embeddings.js +10 -0
  2. package/dist/src/embeddings.js.map +1 -1
  3. package/dist/src/handlers/brain.js +2 -1
  4. package/dist/src/handlers/brain.js.map +1 -1
  5. package/dist/src/handlers/cache.js +4 -0
  6. package/dist/src/handlers/cache.js.map +1 -1
  7. package/dist/src/handlers/tco.js +1 -0
  8. package/dist/src/handlers/tco.js.map +1 -1
  9. package/dist/src/index.js +2 -2
  10. package/dist/src/index.js.map +1 -1
  11. package/package.json +7 -1
  12. package/server.json +2 -2
  13. package/.env.test.example +0 -23
  14. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -50
  15. package/.github/ISSUE_TEMPLATE/config.yml +0 -8
  16. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -25
  17. package/.github/workflows/eas-build-android.yml +0 -58
  18. package/.github/workflows/eas-update.yml +0 -55
  19. package/.github/workflows/publish.yml +0 -72
  20. package/BENCH.md +0 -113
  21. package/CHANGELOG.md +0 -653
  22. package/Dockerfile +0 -16
  23. package/LAUNCH_TEXTS.md +0 -182
  24. package/PROGRESS.md +0 -141
  25. package/STRATEGY.md +0 -228
  26. package/VISION_10X.md +0 -133
  27. package/apps/travel-chaos-organizer/.env.example +0 -49
  28. package/apps/travel-chaos-organizer/DEPLOY.md +0 -328
  29. package/apps/travel-chaos-organizer/Makefile +0 -57
  30. package/apps/travel-chaos-organizer/README.md +0 -159
  31. package/apps/travel-chaos-organizer/backend/.env.example +0 -10
  32. package/apps/travel-chaos-organizer/backend/Dockerfile +0 -25
  33. package/apps/travel-chaos-organizer/backend/alembic/env.py +0 -56
  34. package/apps/travel-chaos-organizer/backend/alembic/script.py.mako +0 -24
  35. package/apps/travel-chaos-organizer/backend/alembic/versions/.gitkeep +0 -0
  36. package/apps/travel-chaos-organizer/backend/alembic/versions/0001_stripe_customer_id.py +0 -24
  37. package/apps/travel-chaos-organizer/backend/alembic/versions/0002_telegram_chat_id.py +0 -24
  38. package/apps/travel-chaos-organizer/backend/alembic.ini +0 -39
  39. package/apps/travel-chaos-organizer/backend/app/__init__.py +0 -0
  40. package/apps/travel-chaos-organizer/backend/app/auth/__init__.py +0 -0
  41. package/apps/travel-chaos-organizer/backend/app/auth/keycloak.py +0 -81
  42. package/apps/travel-chaos-organizer/backend/app/config.py +0 -66
  43. package/apps/travel-chaos-organizer/backend/app/db/__init__.py +0 -0
  44. package/apps/travel-chaos-organizer/backend/app/db/database.py +0 -20
  45. package/apps/travel-chaos-organizer/backend/app/db/migrations/001_init.sql +0 -71
  46. package/apps/travel-chaos-organizer/backend/app/db/migrations/002_telemetry_freemium.sql +0 -31
  47. package/apps/travel-chaos-organizer/backend/app/db/migrations/003_telegram_chat_id.sql +0 -4
  48. package/apps/travel-chaos-organizer/backend/app/db/migrations/004_stripe_and_email.sql +0 -5
  49. package/apps/travel-chaos-organizer/backend/app/limiter.py +0 -4
  50. package/apps/travel-chaos-organizer/backend/app/logging_config.py +0 -25
  51. package/apps/travel-chaos-organizer/backend/app/main.py +0 -77
  52. package/apps/travel-chaos-organizer/backend/app/models/__init__.py +0 -0
  53. package/apps/travel-chaos-organizer/backend/app/models/schemas.py +0 -142
  54. package/apps/travel-chaos-organizer/backend/app/routers/__init__.py +0 -0
  55. package/apps/travel-chaos-organizer/backend/app/routers/admin.py +0 -237
  56. package/apps/travel-chaos-organizer/backend/app/routers/cache.py +0 -13
  57. package/apps/travel-chaos-organizer/backend/app/routers/events.py +0 -29
  58. package/apps/travel-chaos-organizer/backend/app/routers/export.py +0 -122
  59. package/apps/travel-chaos-organizer/backend/app/routers/inbox.py +0 -122
  60. package/apps/travel-chaos-organizer/backend/app/routers/items.py +0 -130
  61. package/apps/travel-chaos-organizer/backend/app/routers/mail.py +0 -108
  62. package/apps/travel-chaos-organizer/backend/app/routers/parse.py +0 -303
  63. package/apps/travel-chaos-organizer/backend/app/routers/payments.py +0 -179
  64. package/apps/travel-chaos-organizer/backend/app/routers/trips.py +0 -134
  65. package/apps/travel-chaos-organizer/backend/app/routers/users.py +0 -169
  66. package/apps/travel-chaos-organizer/backend/app/routers/waitlist.py +0 -37
  67. package/apps/travel-chaos-organizer/backend/app/services/__init__.py +0 -0
  68. package/apps/travel-chaos-organizer/backend/app/services/cache.py +0 -131
  69. package/apps/travel-chaos-organizer/backend/app/services/drip.py +0 -128
  70. package/apps/travel-chaos-organizer/backend/app/services/email.py +0 -61
  71. package/apps/travel-chaos-organizer/backend/app/services/errors.py +0 -44
  72. package/apps/travel-chaos-organizer/backend/app/services/notifier.py +0 -10
  73. package/apps/travel-chaos-organizer/backend/app/services/ollama.py +0 -101
  74. package/apps/travel-chaos-organizer/backend/app/services/parser.py +0 -22
  75. package/apps/travel-chaos-organizer/backend/app/services/quota.py +0 -85
  76. package/apps/travel-chaos-organizer/backend/app/services/sentry.py +0 -11
  77. package/apps/travel-chaos-organizer/backend/app/services/telemetry.py +0 -35
  78. package/apps/travel-chaos-organizer/backend/pytest.ini +0 -4
  79. package/apps/travel-chaos-organizer/backend/requirements-dev.txt +0 -6
  80. package/apps/travel-chaos-organizer/backend/requirements.txt +0 -25
  81. package/apps/travel-chaos-organizer/backend/tests/__init__.py +0 -0
  82. package/apps/travel-chaos-organizer/backend/tests/conftest.py +0 -223
  83. package/apps/travel-chaos-organizer/backend/tests/test_admin.py +0 -99
  84. package/apps/travel-chaos-organizer/backend/tests/test_cache.py +0 -142
  85. package/apps/travel-chaos-organizer/backend/tests/test_drip.py +0 -32
  86. package/apps/travel-chaos-organizer/backend/tests/test_e2e_chain.py +0 -121
  87. package/apps/travel-chaos-organizer/backend/tests/test_export.py +0 -48
  88. package/apps/travel-chaos-organizer/backend/tests/test_inbox.py +0 -107
  89. package/apps/travel-chaos-organizer/backend/tests/test_items.py +0 -138
  90. package/apps/travel-chaos-organizer/backend/tests/test_mail.py +0 -113
  91. package/apps/travel-chaos-organizer/backend/tests/test_ollama.py +0 -145
  92. package/apps/travel-chaos-organizer/backend/tests/test_parse_url.py +0 -98
  93. package/apps/travel-chaos-organizer/backend/tests/test_parser.py +0 -72
  94. package/apps/travel-chaos-organizer/backend/tests/test_payments.py +0 -163
  95. package/apps/travel-chaos-organizer/backend/tests/test_quota.py +0 -46
  96. package/apps/travel-chaos-organizer/backend/tests/test_search.py +0 -67
  97. package/apps/travel-chaos-organizer/backend/tests/test_trips.py +0 -123
  98. package/apps/travel-chaos-organizer/backend/tests/test_users.py +0 -52
  99. package/apps/travel-chaos-organizer/backups/.gitkeep +0 -0
  100. package/apps/travel-chaos-organizer/caddy/Caddyfile +0 -27
  101. package/apps/travel-chaos-organizer/docker-compose.node1.yml +0 -31
  102. package/apps/travel-chaos-organizer/docker-compose.override.yml +0 -14
  103. package/apps/travel-chaos-organizer/docker-compose.yml +0 -128
  104. package/apps/travel-chaos-organizer/frontend/.env.example +0 -13
  105. package/apps/travel-chaos-organizer/frontend/app/(app)/_layout.tsx +0 -80
  106. package/apps/travel-chaos-organizer/frontend/app/(app)/inbox.tsx +0 -166
  107. package/apps/travel-chaos-organizer/frontend/app/(app)/search.tsx +0 -130
  108. package/apps/travel-chaos-organizer/frontend/app/(app)/settings.tsx +0 -381
  109. package/apps/travel-chaos-organizer/frontend/app/(app)/trips/[id].tsx +0 -346
  110. package/apps/travel-chaos-organizer/frontend/app/(app)/trips.tsx +0 -363
  111. package/apps/travel-chaos-organizer/frontend/app/(app)/upgrade/cancel.tsx +0 -34
  112. package/apps/travel-chaos-organizer/frontend/app/(app)/upgrade/success.tsx +0 -35
  113. package/apps/travel-chaos-organizer/frontend/app/(auth)/login.tsx +0 -91
  114. package/apps/travel-chaos-organizer/frontend/app/_layout.tsx +0 -71
  115. package/apps/travel-chaos-organizer/frontend/app/auth.tsx +0 -29
  116. package/apps/travel-chaos-organizer/frontend/app/index.tsx +0 -16
  117. package/apps/travel-chaos-organizer/frontend/app/onboarding.tsx +0 -119
  118. package/apps/travel-chaos-organizer/frontend/app.config.ts +0 -54
  119. package/apps/travel-chaos-organizer/frontend/assets/adaptive-icon.svg +0 -66
  120. package/apps/travel-chaos-organizer/frontend/assets/icon.svg +0 -79
  121. package/apps/travel-chaos-organizer/frontend/assets/splash.svg +0 -15
  122. package/apps/travel-chaos-organizer/frontend/components/BottomSheet.tsx +0 -102
  123. package/apps/travel-chaos-organizer/frontend/components/ConfettiCannon.tsx +0 -107
  124. package/apps/travel-chaos-organizer/frontend/components/ConfirmDialog.tsx +0 -109
  125. package/apps/travel-chaos-organizer/frontend/components/ErrorBoundary.tsx +0 -45
  126. package/apps/travel-chaos-organizer/frontend/components/FileUploadButton.tsx +0 -19
  127. package/apps/travel-chaos-organizer/frontend/components/ImportSheet.tsx +0 -155
  128. package/apps/travel-chaos-organizer/frontend/components/OfflineBadge.tsx +0 -55
  129. package/apps/travel-chaos-organizer/frontend/components/QRCodeViewer.tsx +0 -80
  130. package/apps/travel-chaos-organizer/frontend/components/ScoreRing.tsx +0 -39
  131. package/apps/travel-chaos-organizer/frontend/components/Screen.tsx +0 -39
  132. package/apps/travel-chaos-organizer/frontend/components/Skeleton.tsx +0 -70
  133. package/apps/travel-chaos-organizer/frontend/components/SwipeableInboxItem.tsx +0 -121
  134. package/apps/travel-chaos-organizer/frontend/components/SwipeableTimelineItem.tsx +0 -70
  135. package/apps/travel-chaos-organizer/frontend/components/TimelineItem.tsx +0 -73
  136. package/apps/travel-chaos-organizer/frontend/components/TimelineItemDetail.tsx +0 -130
  137. package/apps/travel-chaos-organizer/frontend/components/Toast.tsx +0 -60
  138. package/apps/travel-chaos-organizer/frontend/components/ToastContext.tsx +0 -42
  139. package/apps/travel-chaos-organizer/frontend/components/TripCard.tsx +0 -146
  140. package/apps/travel-chaos-organizer/frontend/components/TripQRModal.tsx +0 -70
  141. package/apps/travel-chaos-organizer/frontend/components/UpgradeBanner.tsx +0 -44
  142. package/apps/travel-chaos-organizer/frontend/eas.json +0 -22
  143. package/apps/travel-chaos-organizer/frontend/hooks/useInbox.ts +0 -24
  144. package/apps/travel-chaos-organizer/frontend/hooks/useNetworkSync.ts +0 -60
  145. package/apps/travel-chaos-organizer/frontend/hooks/useTrips.ts +0 -112
  146. package/apps/travel-chaos-organizer/frontend/lib/api.ts +0 -223
  147. package/apps/travel-chaos-organizer/frontend/lib/auth.ts +0 -93
  148. package/apps/travel-chaos-organizer/frontend/lib/db.ts +0 -86
  149. package/apps/travel-chaos-organizer/frontend/lib/fileCache.ts +0 -54
  150. package/apps/travel-chaos-organizer/frontend/lib/haptics.ts +0 -13
  151. package/apps/travel-chaos-organizer/frontend/lib/notifications.ts +0 -112
  152. package/apps/travel-chaos-organizer/frontend/lib/offlineQueue.ts +0 -145
  153. package/apps/travel-chaos-organizer/frontend/lib/quota.tsx +0 -43
  154. package/apps/travel-chaos-organizer/frontend/lib/sentry.ts +0 -28
  155. package/apps/travel-chaos-organizer/frontend/lib/shareIntent.ts +0 -44
  156. package/apps/travel-chaos-organizer/frontend/package-lock.json +0 -13221
  157. package/apps/travel-chaos-organizer/frontend/package.json +0 -44
  158. package/apps/travel-chaos-organizer/frontend/tsconfig.json +0 -23
  159. package/apps/travel-chaos-organizer/keycloak/realm-export.json +0 -59
  160. package/apps/travel-chaos-organizer/landing/index.html +0 -773
  161. package/apps/travel-chaos-organizer/scripts/backup.sh +0 -14
  162. package/apps/travel-chaos-organizer/scripts/configure-env.sh +0 -82
  163. package/apps/travel-chaos-organizer/scripts/deploy.sh +0 -54
  164. package/apps/travel-chaos-organizer/scripts/health-check.sh +0 -36
  165. package/apps/travel-chaos-organizer/scripts/migrate.sh +0 -7
  166. package/apps/travel-chaos-organizer/scripts/server-setup.sh +0 -85
  167. package/apps/travel-chaos-organizer/scripts/smoke-test.sh +0 -29
  168. package/apps/travel-chaos-organizer/scripts/verify-tracking.sh +0 -71
  169. package/apps/travel-chaos-organizer/telegram-bot/.env.example +0 -3
  170. package/apps/travel-chaos-organizer/telegram-bot/Dockerfile +0 -6
  171. package/apps/travel-chaos-organizer/telegram-bot/bot.py +0 -504
  172. package/apps/travel-chaos-organizer/telegram-bot/requirements.txt +0 -3
  173. package/dist/src/__tests__/auth.test.d.ts +0 -1
  174. package/dist/src/__tests__/auth.test.js +0 -107
  175. package/dist/src/__tests__/auth.test.js.map +0 -1
  176. package/dist/src/__tests__/brain-flow.test.d.ts +0 -12
  177. package/dist/src/__tests__/brain-flow.test.js +0 -1241
  178. package/dist/src/__tests__/brain-flow.test.js.map +0 -1
  179. package/dist/src/__tests__/confidence.test.d.ts +0 -13
  180. package/dist/src/__tests__/confidence.test.js +0 -233
  181. package/dist/src/__tests__/confidence.test.js.map +0 -1
  182. package/dist/src/__tests__/e2e/api-auth.test.d.ts +0 -7
  183. package/dist/src/__tests__/e2e/api-auth.test.js +0 -162
  184. package/dist/src/__tests__/e2e/api-auth.test.js.map +0 -1
  185. package/dist/src/__tests__/e2e/api-health.test.d.ts +0 -7
  186. package/dist/src/__tests__/e2e/api-health.test.js +0 -95
  187. package/dist/src/__tests__/e2e/api-health.test.js.map +0 -1
  188. package/dist/src/__tests__/e2e/cli.test.d.ts +0 -10
  189. package/dist/src/__tests__/e2e/cli.test.js +0 -204
  190. package/dist/src/__tests__/e2e/cli.test.js.map +0 -1
  191. package/dist/src/__tests__/e2e/config.d.ts +0 -35
  192. package/dist/src/__tests__/e2e/config.js +0 -53
  193. package/dist/src/__tests__/e2e/config.js.map +0 -1
  194. package/dist/src/__tests__/e2e/funnel.test.d.ts +0 -13
  195. package/dist/src/__tests__/e2e/funnel.test.js +0 -90
  196. package/dist/src/__tests__/e2e/funnel.test.js.map +0 -1
  197. package/dist/src/__tests__/e2e/mcp-tools.test.d.ts +0 -7
  198. package/dist/src/__tests__/e2e/mcp-tools.test.js +0 -129
  199. package/dist/src/__tests__/e2e/mcp-tools.test.js.map +0 -1
  200. package/dist/src/__tests__/e2e/provisioning.test.d.ts +0 -15
  201. package/dist/src/__tests__/e2e/provisioning.test.js +0 -123
  202. package/dist/src/__tests__/e2e/provisioning.test.js.map +0 -1
  203. package/dist/src/__tests__/handlers-unit.test.d.ts +0 -12
  204. package/dist/src/__tests__/handlers-unit.test.js +0 -505
  205. package/dist/src/__tests__/handlers-unit.test.js.map +0 -1
  206. package/dist/src/__tests__/namespace.test.d.ts +0 -1
  207. package/dist/src/__tests__/namespace.test.js +0 -114
  208. package/dist/src/__tests__/namespace.test.js.map +0 -1
  209. package/dist/src/__tests__/rerank.test.d.ts +0 -1
  210. package/dist/src/__tests__/rerank.test.js +0 -125
  211. package/dist/src/__tests__/rerank.test.js.map +0 -1
  212. package/dist/src/__tests__/utils.test.d.ts +0 -1
  213. package/dist/src/__tests__/utils.test.js +0 -83
  214. package/dist/src/__tests__/utils.test.js.map +0 -1
  215. package/dist/src/bench/cachly-bench.d.ts +0 -39
  216. package/dist/src/bench/cachly-bench.js +0 -223
  217. package/dist/src/bench/cachly-bench.js.map +0 -1
  218. package/dist/src/bench/fixtures.d.ts +0 -37
  219. package/dist/src/bench/fixtures.js +0 -225
  220. package/dist/src/bench/fixtures.js.map +0 -1
  221. package/dist/src/search.test.d.ts +0 -1
  222. package/dist/src/search.test.js +0 -713
  223. package/dist/src/search.test.js.map +0 -1
  224. package/eslint.config.js +0 -16
  225. package/glama.json +0 -36
  226. package/llms.txt +0 -155
  227. package/packages/telegram-notify/SPLIT_PLAN.md +0 -106
  228. package/packages/telegram-notify/client.ts +0 -88
  229. package/packages/telegram-notify/emojis.json +0 -22
  230. package/packages/telegram-notify/pyproject.toml +0 -17
  231. package/packages/telegram-notify/telegram_notify/__init__.py +0 -3
  232. package/packages/telegram-notify/telegram_notify/client.py +0 -87
  233. package/packages/telegram-notify/telegram_notify/emojis.json +0 -22
  234. package/scripts/e2e-run.sh +0 -160
  235. package/scripts/generate-icons.js +0 -28
  236. package/smithery.yaml +0 -45
  237. package/src/__tests__/auth.test.ts +0 -147
  238. package/src/__tests__/brain-flow.test.ts +0 -1472
  239. package/src/__tests__/confidence.test.ts +0 -285
  240. package/src/__tests__/e2e/api-auth.test.ts +0 -182
  241. package/src/__tests__/e2e/api-health.test.ts +0 -103
  242. package/src/__tests__/e2e/cli.test.ts +0 -228
  243. package/src/__tests__/e2e/config.ts +0 -53
  244. package/src/__tests__/e2e/funnel.test.ts +0 -99
  245. package/src/__tests__/e2e/mcp-tools.test.ts +0 -191
  246. package/src/__tests__/e2e/provisioning.test.ts +0 -128
  247. package/src/__tests__/handlers-unit.test.ts +0 -677
  248. package/src/__tests__/namespace.test.ts +0 -126
  249. package/src/__tests__/rerank.test.ts +0 -141
  250. package/src/__tests__/utils.test.ts +0 -81
  251. package/src/auth.ts +0 -46
  252. package/src/bench/cachly-bench.ts +0 -263
  253. package/src/bench/fixtures.ts +0 -256
  254. package/src/ckg.ts +0 -103
  255. package/src/confidence.ts +0 -38
  256. package/src/embeddings.ts +0 -217
  257. package/src/handlers/advanced.ts +0 -647
  258. package/src/handlers/brain.ts +0 -2622
  259. package/src/handlers/cache.ts +0 -633
  260. package/src/handlers/context.ts +0 -222
  261. package/src/handlers/fedbrain.ts +0 -1232
  262. package/src/handlers/instances.ts +0 -180
  263. package/src/handlers/roadmap.ts +0 -201
  264. package/src/handlers/syndicate.ts +0 -460
  265. package/src/handlers/tco.ts +0 -345
  266. package/src/handlers/team.ts +0 -1249
  267. package/src/index.ts +0 -2815
  268. package/src/namespace.ts +0 -128
  269. package/src/notifier.ts +0 -5
  270. package/src/rerank.ts +0 -148
  271. package/src/search.test.ts +0 -848
  272. package/src/search.ts +0 -1781
  273. package/src/tools.ts +0 -2056
  274. package/src/utils.ts +0 -90
  275. package/test-features.cjs +0 -23
  276. package/tsconfig.json +0 -16
  277. package/vitest.config.ts +0 -14
@@ -1,633 +0,0 @@
1
- import { randomUUID } from 'node:crypto';
2
- import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
3
- import { readdir, stat, readFile } from 'node:fs/promises';
4
- import { join, relative, extname } from 'node:path';
5
- import type { Redis } from 'ioredis';
6
- import type { Instance } from './brain.js';
7
- import { computeEmbedding, hasEmbedProvider, embedProviderHint } from '../embeddings.js';
8
- import { detectNamespace } from '../namespace.js';
9
- import { simpleHash } from '../confidence.js';
10
-
11
- interface SemanticSearchResponse {
12
- found: boolean; id?: string; similarity?: number; prompt?: string;
13
- }
14
-
15
- type GetConnection = (instanceId: string) => Promise<Redis>;
16
- type ApiFetch = <T>(path: string, options?: RequestInit) => Promise<T>;
17
-
18
- export const CACHE_TOOL_NAMES = new Set([
19
- 'cache_get', 'cache_set', 'cache_delete', 'cache_exists', 'cache_ttl', 'cache_keys',
20
- 'cache_stats', 'semantic_search', 'detect_namespace', 'cache_warmup', 'index_project',
21
- 'cache_mset', 'cache_mget', 'cache_lock_acquire', 'cache_lock_release',
22
- 'cache_stream_set', 'cache_stream_get',
23
- ]);
24
-
25
- export async function handleCacheTool(
26
- name: string,
27
- args: Record<string, unknown>,
28
- getConnection: GetConnection,
29
- apiFetch: ApiFetch,
30
- ): Promise<string | null> {
31
- switch (name) {
32
- case 'cache_get': {
33
- const { instance_id, key } = args as { instance_id: string; key: string };
34
- const redis = await getConnection(instance_id);
35
- const value = await redis.get(key);
36
- if (value === null) return `Key \`${key}\` → **not found** (null)`;
37
- let pretty = value;
38
- try {
39
- pretty = JSON.stringify(JSON.parse(value), null, 2);
40
- } catch {
41
- // not JSON — return raw
42
- }
43
- return `Key \`${key}\`:\n\`\`\`\n${pretty}\n\`\`\``;
44
- }
45
-
46
- case 'cache_set': {
47
- const { instance_id, key, value, ttl } = args as {
48
- instance_id: string;
49
- key: string;
50
- value: string;
51
- ttl?: number;
52
- };
53
- const redis = await getConnection(instance_id);
54
- if (ttl && ttl > 0) {
55
- await redis.set(key, value, 'EX', ttl);
56
- return `✅ Set \`${key}\` (TTL: ${ttl}s)`;
57
- }
58
- await redis.set(key, value);
59
- return `✅ Set \`${key}\` (no expiry)`;
60
- }
61
-
62
- case 'cache_delete': {
63
- const { instance_id, keys } = args as { instance_id: string; keys: string[] };
64
- const redis = await getConnection(instance_id);
65
- const deleted = await redis.del(...keys);
66
- return `✅ Deleted **${deleted}** of ${keys.length} key(s): ${keys.map((k) => `\`${k}\``).join(', ')}`;
67
- }
68
-
69
- case 'cache_exists': {
70
- const { instance_id, keys } = args as { instance_id: string; keys: string[] };
71
- const redis = await getConnection(instance_id);
72
- const count = await redis.exists(...keys);
73
- return `**${count}** of ${keys.length} key(s) exist in cache.`;
74
- }
75
-
76
- case 'cache_ttl': {
77
- const { instance_id, key } = args as { instance_id: string; key: string };
78
- const redis = await getConnection(instance_id);
79
- const ttl = await redis.ttl(key);
80
- if (ttl === -2) return `Key \`${key}\` → **does not exist**`;
81
- if (ttl === -1) return `Key \`${key}\` → **no expiry** (persists forever)`;
82
- const mins = Math.floor(ttl / 60);
83
- const secs = ttl % 60;
84
- return `Key \`${key}\` → TTL: **${ttl}s** (${mins}m ${secs}s remaining)`;
85
- }
86
-
87
- case 'cache_keys': {
88
- const { instance_id, pattern = '*', count = 50 } = args as {
89
- instance_id: string;
90
- pattern?: string;
91
- count?: number;
92
- };
93
- const limit = Math.min(count, 500);
94
- const redis = await getConnection(instance_id);
95
- const keys: string[] = [];
96
- const stream = redis.scanStream({ match: pattern, count: 100 });
97
- await new Promise<void>((resolve, reject) => {
98
- stream.on('data', (batch: string[]) => {
99
- keys.push(...batch);
100
- if (keys.length >= limit) {
101
- stream.destroy();
102
- resolve();
103
- }
104
- });
105
- stream.on('end', resolve);
106
- stream.on('error', reject);
107
- });
108
- const result = keys.slice(0, limit);
109
- if (result.length === 0) return `No keys found matching pattern \`${pattern}\`.`;
110
- return [
111
- `Found **${result.length}** key(s) matching \`${pattern}\`:`,
112
- ...result.map((k) => ` • \`${k}\``),
113
- result.length === limit ? `\n_(showing first ${limit} — narrow pattern to see more)_` : '',
114
- ]
115
- .filter(Boolean)
116
- .join('\n');
117
- }
118
-
119
- case 'cache_stats': {
120
- const { instance_id } = args as { instance_id: string };
121
- const redis = await getConnection(instance_id);
122
-
123
- const [infoAll, infoStats, infoKeyspace] = await Promise.all([
124
- redis.info('memory'),
125
- redis.info('stats'),
126
- redis.info('keyspace'),
127
- ]);
128
-
129
- const parse = (section: string, field: string): string =>
130
- section.match(new RegExp(`${field}:([^\r\n]+)`))?.[1]?.trim() ?? 'n/a';
131
-
132
- const usedMem = parse(infoAll, 'used_memory_human');
133
- const peakMem = parse(infoAll, 'used_memory_peak_human');
134
- const hits = parse(infoStats, 'keyspace_hits');
135
- const misses = parse(infoStats, 'keyspace_misses');
136
- const opsPerSec = parse(infoStats, 'instantaneous_ops_per_sec');
137
- const connectedClients = (await redis.info('clients')).match(/connected_clients:(\d+)/)?.[1] ?? 'n/a';
138
-
139
- const hitsN = parseInt(hits) || 0;
140
- const missesN = parseInt(misses) || 0;
141
- const total = hitsN + missesN;
142
- const hitRate = total > 0 ? ((hitsN / total) * 100).toFixed(1) : 'n/a';
143
-
144
- const keyspaceLines = infoKeyspace
145
- .split('\n')
146
- .filter((l: string) => l.startsWith('db'))
147
- .map((l: string) => ` ${l.trim()}`);
148
-
149
- return [
150
- `📊 **Cache Stats for instance \`${instance_id}\`:**`,
151
- ``,
152
- ` 💾 Memory used: ${usedMem} (peak: ${peakMem})`,
153
- ` ⚡ Ops/sec: ${opsPerSec}`,
154
- ` 🎯 Hit rate: ${hitRate}% (${hits} hits / ${misses} misses)`,
155
- ` 🔗 Clients: ${connectedClients}`,
156
- ``,
157
- keyspaceLines.length > 0
158
- ? ` 🗂️ Keyspace:\n${keyspaceLines.join('\n')}`
159
- : ` 🗂️ Keyspace: (empty)`,
160
- ].join('\n');
161
- }
162
-
163
- case 'semantic_search': {
164
- const {
165
- instance_id,
166
- query,
167
- threshold = 0.82,
168
- namespace: nsArg = 'cachly:sem',
169
- top_k = 5,
170
- use_hybrid = false,
171
- auto_namespace = false,
172
- } = args as {
173
- instance_id: string;
174
- query: string;
175
- threshold?: number;
176
- namespace?: string;
177
- top_k?: number;
178
- use_hybrid?: boolean;
179
- auto_namespace?: boolean;
180
- };
181
-
182
- // resolve namespace from query text when requested
183
- const namespace = auto_namespace ? detectNamespace(query) : nsArg;
184
-
185
- if (!hasEmbedProvider()) {
186
- return (
187
- `❌ semantic_search requires an embedding provider.\n\n` +
188
- `Current: ${embedProviderHint()}\n\n` +
189
- `Set one of these in your MCP env config:\n` +
190
- ` OPENAI_API_KEY (provider: openai – default)\n` +
191
- ` MISTRAL_API_KEY (provider: mistral)\n` +
192
- ` COHERE_API_KEY (provider: cohere)\n` +
193
- ` GEMINI_API_KEY (provider: gemini)\n` +
194
- ` OLLAMA_BASE_URL (provider: ollama – local, no key needed)\n` +
195
- `Also set: CACHLY_EMBED_PROVIDER=<provider>`
196
- );
197
- }
198
-
199
- const inst = await apiFetch<Instance>(`/api/v1/instances/${instance_id}`);
200
- if (!inst.vector_token) {
201
- return (
202
- `❌ Semantic search is only available on Speed and Business tiers.\n\n` +
203
- `Your instance "${inst.name}" is on the **${inst.tier.toUpperCase()}** tier.\n` +
204
- `Upgrade at https://cachly.dev/instances/${instance_id}`
205
- );
206
- }
207
-
208
- // Compute embedding via configured provider
209
- const embedding = await computeEmbedding(query);
210
-
211
- // Query cachly vector API
212
- const vectorUrl = process.env.CACHLY_VECTOR_URL ?? `https://api.cachly.dev/v1/sem/${inst.vector_token}`;
213
- const searchPayload: Record<string, unknown> = { embedding, namespace, threshold, top_k };
214
- // hybrid BM25+Vector RRF: include query text when requested.
215
- if (use_hybrid) {
216
- searchPayload['hybrid'] = true;
217
- searchPayload['prompt'] = query;
218
- }
219
- const searchRes = await fetch(`${vectorUrl}/search`, {
220
- method: 'POST',
221
- headers: { 'Content-Type': 'application/json' },
222
- body: JSON.stringify(searchPayload),
223
- });
224
-
225
- if (!searchRes.ok) {
226
- throw new McpError(ErrorCode.InternalError, `Vector search failed: ${searchRes.statusText}`);
227
- }
228
-
229
- const results = (await searchRes.json()) as SemanticSearchResponse[];
230
-
231
- if (!results.length || (results.length === 1 && !results[0].found)) {
232
- return (
233
- `🔍 No semantically similar entries found for:\n _"${query}"_\n\n` +
234
- `Try lowering the threshold (current: ${threshold}) or using different keywords.`
235
- );
236
- }
237
-
238
- const redis = await getConnection(instance_id);
239
- const lines: string[] = [
240
- `🔍 **Semantic search results** for: _"${query}"_`,
241
- ` Threshold: ${threshold} · Namespace: \`${namespace}\``,
242
- ``,
243
- ];
244
-
245
- for (const hit of results) {
246
- if (!hit.found || !hit.id) continue;
247
- const value = await redis.get(`${namespace}:val:${hit.id}`);
248
- lines.push(
249
- `**Match** (similarity: ${((hit.similarity ?? 0) * 100).toFixed(1)}%)`,
250
- ` Prompt: _"${hit.prompt ?? '(unknown)'}"_`,
251
- value ? ` Value: \`${value.slice(0, 200)}${value.length > 200 ? '…' : ''}\`` : ` Value: _(evicted from cache)_`,
252
- ``
253
- );
254
- }
255
-
256
- return lines.join('\n');
257
- }
258
-
259
- // ── Namespace Auto-Detection ──────────────────────────────────────────
260
- case 'detect_namespace': {
261
- const { prompt } = args as { prompt: string };
262
- const ns = detectNamespace(prompt);
263
- const typeLabel = ns.split(':').pop()!;
264
- const descriptions: Record<string, string> = {
265
- code: '💻 Code — contains programming constructs or syntax',
266
- translation: '🌐 Translation — asks to translate between languages',
267
- summary: '📝 Summary — requests a summary or key points (TL;DR)',
268
- qa: '❓ Q&A — a direct question or query',
269
- creative: '🎨 Creative — general, creative, or conversational prompt',
270
- };
271
- return [
272
- `**Detected namespace:** \`${ns}\``,
273
- `**Type:** ${descriptions[typeLabel] ?? typeLabel}`,
274
- ``,
275
- `_Prompt: "${prompt.slice(0, 120)}${prompt.length > 120 ? '…' : ''}"_`,
276
- ``,
277
- `💡 Use this namespace in \`semantic_search\` or \`cache_warmup\` for better hit rates.`,
278
- ` Set \`auto_namespace: true\` to apply this detection automatically.`,
279
- ].join('\n');
280
- }
281
-
282
- // ── Cache Warmup ───────────────────────────────────────────────────────
283
- case 'cache_warmup': {
284
- const {
285
- instance_id,
286
- entries: rawEntries,
287
- namespace: nsArg = 'cachly:sem',
288
- ttl,
289
- auto_namespace = false,
290
- } = args as {
291
- instance_id: string;
292
- entries: Array<{ prompt: string; value: string; namespace?: string }>;
293
- namespace?: string;
294
- ttl?: number;
295
- auto_namespace?: boolean;
296
- };
297
-
298
- if (!hasEmbedProvider()) {
299
- return (
300
- `❌ cache_warmup requires an embedding provider.\n\n` +
301
- `Current: ${embedProviderHint()}\n\n` +
302
- `Supported: openai (default) · mistral · cohere · ollama (local) · gemini\n` +
303
- `Set CACHLY_EMBED_PROVIDER and the matching API key env var.`
304
- );
305
- }
306
-
307
- const inst = await apiFetch<Instance>(`/api/v1/instances/${instance_id}`);
308
- const vectorUrl =
309
- process.env.CACHLY_VECTOR_URL ??
310
- (inst.vector_token ? `https://api.cachly.dev/v1/sem/${inst.vector_token}` : null);
311
-
312
- const redis = await getConnection(instance_id);
313
-
314
- let warmed = 0;
315
- let skipped = 0;
316
- const details: string[] = [];
317
-
318
- for (const entry of rawEntries) {
319
- // resolve namespace per entry
320
- const ns = entry.namespace ?? (auto_namespace ? detectNamespace(entry.prompt) : nsArg);
321
-
322
- // Compute embedding for this prompt
323
- const embedding = await computeEmbedding(entry.prompt);
324
-
325
- // Check if a very-similar entry already exists (threshold 0.98 → skip to avoid duplicates)
326
- let alreadyCached = false;
327
- if (vectorUrl) {
328
- const checkRes = await fetch(`${vectorUrl}/search`, {
329
- method: 'POST',
330
- headers: { 'Content-Type': 'application/json' },
331
- body: JSON.stringify({ embedding, namespace: ns, threshold: 0.98 }),
332
- }).catch(() => null);
333
- if (checkRes?.ok) {
334
- const results = (await checkRes.json()) as SemanticSearchResponse[];
335
- alreadyCached = results[0]?.found ?? false;
336
- }
337
- }
338
-
339
- if (alreadyCached) {
340
- skipped++;
341
- details.push(` ⏭️ _"${entry.prompt.slice(0, 60)}${entry.prompt.length > 60 ? '…' : ''}"_ → already cached`);
342
- continue;
343
- }
344
-
345
- // Write value to Valkey
346
- const id = randomUUID();
347
- const vk = `${ns}:val:${id}`;
348
- if (ttl && ttl > 0) {
349
- await redis.set(vk, entry.value, 'EX', ttl);
350
- } else {
351
- await redis.set(vk, entry.value);
352
- }
353
-
354
- if (vectorUrl) {
355
- // pgvector path – index embedding in HNSW
356
- const body: Record<string, unknown> = { id, prompt: entry.prompt, namespace: ns, embedding };
357
- if (ttl && ttl > 0) {
358
- body['expires_at'] = new Date(Date.now() + ttl * 1000).toISOString();
359
- }
360
- await fetch(`${vectorUrl}/entries`, {
361
- method: 'POST',
362
- headers: { 'Content-Type': 'application/json' },
363
- body: JSON.stringify(body),
364
- }).catch(() => undefined);
365
- } else {
366
- // Legacy SCAN path – write emb key to Valkey
367
- const embKey = `${ns}:emb:${id}`;
368
- const embPayload = JSON.stringify({ embedding, prompt: entry.prompt });
369
- if (ttl && ttl > 0) {
370
- await redis.set(embKey, embPayload, 'EX', ttl);
371
- } else {
372
- await redis.set(embKey, embPayload);
373
- }
374
- }
375
-
376
- warmed++;
377
- details.push(` ✅ _"${entry.prompt.slice(0, 60)}${entry.prompt.length > 60 ? '…' : ''}"_ → \`${ns}\``);
378
- }
379
-
380
- return [
381
- `🔥 **Cache Warmup Complete**`,
382
- ``,
383
- ` ✅ Warmed: **${warmed}** new entries`,
384
- ` ⏭️ Skipped: **${skipped}** (already cached at ≥ 0.98 similarity)`,
385
- ` 📦 Total: ${rawEntries.length}`,
386
- auto_namespace
387
- ? ` 🏷️ Namespacing: auto-detected per prompt`
388
- : ` 🏷️ Namespace: \`${nsArg}\``,
389
- vectorUrl
390
- ? ` 🔍 Mode: pgvector HNSW (Speed/Business tier)`
391
- : ` 🔍 Mode: Valkey SCAN (upgrade to Speed tier for scalable search)`,
392
- ``,
393
- ...details,
394
- ].join('\n');
395
- }
396
-
397
- // ── index_project – Codebase Indexing ─────────────────────────────────────
398
- case 'index_project': {
399
- const {
400
- instance_id,
401
- dir,
402
- extensions: extArg,
403
- max_files = 100,
404
- ttl = 86400,
405
- summary_chars = 1200,
406
- namespace: nsArg = 'cachly:sem:code',
407
- } = args as {
408
- instance_id: string;
409
- dir: string;
410
- extensions?: string[];
411
- max_files?: number;
412
- ttl?: number;
413
- summary_chars?: number;
414
- namespace?: string;
415
- };
416
-
417
- const ALLOWED_EXT = new Set(
418
- (extArg ?? ['ts', 'js', 'tsx', 'jsx', 'go', 'py', 'java', 'rs', 'md', 'kt', 'swift']).map(
419
- (e) => (e.startsWith('.') ? e : `.${e}`),
420
- ),
421
- );
422
-
423
- // Recursively collect files up to max_files limit
424
- const files: string[] = [];
425
- async function walk(d: string): Promise<void> {
426
- if (files.length >= max_files) return;
427
- const entries = await readdir(d, { withFileTypes: true }).catch(() => null);
428
- if (!entries) return;
429
- for (const entry of entries) {
430
- if (files.length >= max_files) break;
431
- const full = join(d, entry.name as unknown as string);
432
- if (entry.isDirectory()) {
433
- if (['.git', 'node_modules', 'dist', 'build', '.next', '__pycache__', 'vendor'].includes(entry.name as unknown as string))
434
- continue;
435
- await walk(full);
436
- } else if (entry.isFile() && ALLOWED_EXT.has(extname(entry.name as unknown as string).toLowerCase())) {
437
- files.push(full);
438
- }
439
- }
440
- }
441
- await walk(dir);
442
-
443
- if (files.length === 0) {
444
- return `❌ No matching files found in \`${dir}\`.\nExtensions checked: ${[...ALLOWED_EXT].join(', ')}`;
445
- }
446
-
447
- const inst = await apiFetch<Instance>(`/api/v1/instances/${instance_id}`);
448
- const vectorUrl =
449
- process.env.CACHLY_VECTOR_URL ??
450
- (inst.vector_token ? `https://api.cachly.dev/v1/sem/${inst.vector_token}` : null);
451
- const canEmbed = vectorUrl && hasEmbedProvider();
452
-
453
- let indexed = 0;
454
- let skipped = 0;
455
- let errors = 0;
456
- let semanticIndexed = 0;
457
- let unchanged = 0;
458
- const details: string[] = [];
459
- const expiresAt = new Date(Date.now() + ttl * 1000).toISOString();
460
- const redis = await getConnection(instance_id);
461
-
462
- for (const filePath of files) {
463
- const relPath = relative(dir, filePath);
464
- let content: string;
465
- let fileSize: number;
466
- try {
467
- const s = await stat(filePath);
468
- if (s.size > 200_000) { skipped++; continue; } // skip files >200 KB
469
- fileSize = s.size;
470
- content = await readFile(filePath, 'utf-8');
471
- } catch {
472
- errors++;
473
- continue;
474
- }
475
-
476
- // ── Smart invalidation: hash-based change detection ──
477
- // Compute a simple hash of file content to skip unchanged files
478
- const hashKey = `cachly:idx:hash:${relPath}`;
479
- const contentHash = `${fileSize}:${content.length}:${simpleHash(content)}`;
480
- const existingHash = await redis.get(hashKey);
481
- if (existingHash === contentHash) {
482
- // File unchanged — refresh TTL but skip re-indexing
483
- const idxKey = `cachly:idx:${relPath}`;
484
- if (ttl > 0) await redis.expire(idxKey, ttl);
485
- if (ttl > 0) await redis.expire(hashKey, ttl);
486
- unchanged++;
487
- continue;
488
- }
489
-
490
- const excerpt = content.slice(0, summary_chars).replace(/\s+/g, ' ').trim();
491
-
492
- // ── Layer 1: Keyword index in Valkey (always works, no embedding needed) ──
493
- const idxKey = `cachly:idx:${relPath}`;
494
- const idxValue = `File: ${relPath}\n${excerpt}`;
495
- if (ttl > 0) {
496
- await redis.set(idxKey, idxValue, 'EX', ttl);
497
- } else {
498
- await redis.set(idxKey, idxValue);
499
- }
500
- // Store content hash for smart invalidation on next run
501
- if (ttl > 0) {
502
- await redis.set(hashKey, contentHash, 'EX', ttl);
503
- } else {
504
- await redis.set(hashKey, contentHash);
505
- }
506
- indexed++;
507
- details.push(` ✅ ${relPath}`);
508
-
509
- // ── Layer 2: Semantic vector index (optional, only if embedding available) ──
510
- if (canEmbed) {
511
- try {
512
- const prompt = `File: ${relPath}\n${excerpt}`;
513
- const embedding = await computeEmbedding(prompt);
514
- const id = randomUUID();
515
- await fetch(`${vectorUrl}/entries`, {
516
- method: 'POST',
517
- headers: { 'Content-Type': 'application/json' },
518
- body: JSON.stringify({ id, prompt, namespace: nsArg, embedding, expires_at: expiresAt }),
519
- });
520
- await redis.set(`${nsArg}:val:${id}`, relPath, 'EX', ttl);
521
- semanticIndexed++;
522
- } catch {
523
- // Semantic indexing failed — keyword index is enough
524
- }
525
- }
526
- }
527
-
528
- const mode = canEmbed ? '🔍 Keyword + 🎯 Semantic' : '🔍 Keyword only (no embedding provider)';
529
-
530
- return [
531
- `📂 **index_project Complete** — ${mode}`,
532
- ``,
533
- ` 📁 Dir: ${dir}`,
534
- ` ✅ Indexed: **${indexed}** files (new/changed)`,
535
- ` ♻️ Unchanged: ${unchanged} files (hash match — skipped)`,
536
- ...(canEmbed ? [` 🎯 Semantic: **${semanticIndexed}** files (vector-searchable)`] : []),
537
- ` ⏭️ Skipped: ${skipped} (too large or filtered)`,
538
- ` ❌ Errors: ${errors}`,
539
- ` ⏱️ TTL: ${ttl}s (${Math.round(ttl / 3600)}h)`,
540
- ``,
541
- `💡 **Next steps:**`,
542
- ` 1. Use \`smart_recall("how does auth work")\` to find relevant files.`,
543
- ` 2. Re-run index_project after major refactors.`,
544
- ...(canEmbed ? [] : [` 3. Set OPENAI_API_KEY (or similar) in .env to also enable semantic search.`]),
545
- ``,
546
- ...(details.length <= 20 ? details : [...details.slice(0, 20), ` … and ${details.length - 20} more`]),
547
- ].join('\n');
548
- }
549
-
550
- // ── Bulk / Lock / Stream tools ──────────────────────────────────────────
551
- case 'cache_mset': {
552
- const { instance_id, items } = args as { instance_id: string; items: Array<{ key: string; value: unknown; ttl?: number }> };
553
- if (!Array.isArray(items) || items.length === 0) return '⚠️ No items provided.';
554
- const redis = await getConnection(instance_id);
555
- const pipe = redis.pipeline();
556
- for (const item of items) {
557
- const serialized = typeof item.value === 'string' ? item.value : JSON.stringify(item.value);
558
- if (item.ttl && item.ttl > 0) { pipe.set(item.key, serialized, 'EX', item.ttl); }
559
- else { pipe.set(item.key, serialized); }
560
- }
561
- await pipe.exec();
562
- return `✅ **cache_mset** – ${items.length} key(s) written in one pipeline round-trip.\n` +
563
- items.map(i => ` • \`${i.key}\`${i.ttl ? ` (TTL ${i.ttl}s)` : ''}`).join('\n');
564
- }
565
-
566
- case 'cache_mget': {
567
- const { instance_id, keys } = args as { instance_id: string; keys: string[] };
568
- if (!Array.isArray(keys) || keys.length === 0) return '⚠️ No keys provided.';
569
- const redis = await getConnection(instance_id);
570
- const raws = await redis.mget(...keys);
571
- const result = keys.map((k, i) => {
572
- const raw = raws[i];
573
- if (raw === null) return ` • \`${k}\`: _null (miss)_`;
574
- return ` • \`${k}\`: ${raw}`;
575
- });
576
- return `✅ **cache_mget** – ${keys.length} key(s) fetched in one round-trip.\n` + result.join('\n');
577
- }
578
-
579
- case 'cache_lock_acquire': {
580
- const { instance_id, key, ttl_ms = 5000, retries = 3, retry_delay_ms = 50 } =
581
- args as { instance_id: string; key: string; ttl_ms?: number; retries?: number; retry_delay_ms?: number };
582
- const redis = await getConnection(instance_id);
583
- const lockKey = `cachly:lock:${key}`;
584
- const token = randomUUID();
585
- for (let attempt = 0; attempt <= retries; attempt++) {
586
- const result = await redis.set(lockKey, token, 'PX', ttl_ms, 'NX');
587
- if (result === 'OK') {
588
- 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.`;
589
- }
590
- if (attempt < retries) await new Promise(r => setTimeout(r, retry_delay_ms));
591
- }
592
- return `❌ **cache_lock_acquire** – Could not acquire lock for \`${key}\` after ${retries + 1} attempts.`;
593
- }
594
-
595
- case 'cache_lock_release': {
596
- const { instance_id, key, token } = args as { instance_id: string; key: string; token: string };
597
- const redis = await getConnection(instance_id);
598
- const script = `if redis.call("get",KEYS[1])==ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end`;
599
- const released = await redis.eval(script, 1, `cachly:lock:${key}`, token);
600
- return released === 1
601
- ? `🔓 **cache_lock_release** – Lock \`${key}\` released.`
602
- : `⚠️ **cache_lock_release** – Lock \`${key}\` expired or token mismatch.`;
603
- }
604
-
605
- case 'cache_stream_set': {
606
- const { instance_id, key, chunks, ttl } = args as { instance_id: string; key: string; chunks: string[]; ttl?: number };
607
- if (!Array.isArray(chunks) || chunks.length === 0) return '⚠️ No chunks provided.';
608
- const redis = await getConnection(instance_id);
609
- const listKey = `cachly:stream:${key}`;
610
- await redis.del(listKey);
611
- const pipe = redis.pipeline();
612
- for (const chunk of chunks) pipe.rpush(listKey, chunk);
613
- if (ttl && ttl > 0) pipe.expire(listKey, ttl);
614
- await pipe.exec();
615
- 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`;
616
- }
617
-
618
- case 'cache_stream_get': {
619
- const { instance_id, key } = args as { instance_id: string; key: string };
620
- const redis = await getConnection(instance_id);
621
- const listKey = `cachly:stream:${key}`;
622
- const len = await redis.llen(listKey);
623
- if (len === 0) return `⚠️ **cache_stream_get** – Cache miss for key \`${key}\`.`;
624
- const chunks = await redis.lrange(listKey, 0, -1);
625
- const full = chunks.join('');
626
- const preview = full.slice(0, 500);
627
- return `✅ **cache_stream_get** – ${len} chunk(s) retrieved for \`${key}\`.\n\n**Preview** (first 500 chars):\n\`\`\`\n${preview}${preview.length < full.length ? '…' : ''}\n\`\`\``;
628
- }
629
-
630
- default:
631
- return null;
632
- }
633
- }