@cachly-dev/mcp-server 0.10.64 → 0.10.66

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 (270) hide show
  1. package/dist/src/handlers/brain.js +1 -1
  2. package/dist/src/index.js +1 -1
  3. package/package.json +7 -1
  4. package/server.json +2 -2
  5. package/.env.test.example +0 -23
  6. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -50
  7. package/.github/ISSUE_TEMPLATE/config.yml +0 -8
  8. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -25
  9. package/.github/workflows/eas-build-android.yml +0 -58
  10. package/.github/workflows/eas-update.yml +0 -55
  11. package/.github/workflows/publish.yml +0 -72
  12. package/BENCH.md +0 -91
  13. package/CHANGELOG.md +0 -604
  14. package/CHANGELOG.md.tmp +0 -0
  15. package/Dockerfile +0 -16
  16. package/LAUNCH_TEXTS.md +0 -182
  17. package/PROGRESS.md +0 -137
  18. package/STRATEGY.md +0 -228
  19. package/VISION_10X.md +0 -133
  20. package/apps/travel-chaos-organizer/.env.example +0 -49
  21. package/apps/travel-chaos-organizer/DEPLOY.md +0 -328
  22. package/apps/travel-chaos-organizer/Makefile +0 -57
  23. package/apps/travel-chaos-organizer/README.md +0 -159
  24. package/apps/travel-chaos-organizer/backend/.env.example +0 -10
  25. package/apps/travel-chaos-organizer/backend/Dockerfile +0 -25
  26. package/apps/travel-chaos-organizer/backend/alembic/env.py +0 -56
  27. package/apps/travel-chaos-organizer/backend/alembic/script.py.mako +0 -24
  28. package/apps/travel-chaos-organizer/backend/alembic/versions/.gitkeep +0 -0
  29. package/apps/travel-chaos-organizer/backend/alembic/versions/0001_stripe_customer_id.py +0 -24
  30. package/apps/travel-chaos-organizer/backend/alembic/versions/0002_telegram_chat_id.py +0 -24
  31. package/apps/travel-chaos-organizer/backend/alembic.ini +0 -39
  32. package/apps/travel-chaos-organizer/backend/app/__init__.py +0 -0
  33. package/apps/travel-chaos-organizer/backend/app/auth/__init__.py +0 -0
  34. package/apps/travel-chaos-organizer/backend/app/auth/keycloak.py +0 -81
  35. package/apps/travel-chaos-organizer/backend/app/config.py +0 -66
  36. package/apps/travel-chaos-organizer/backend/app/db/__init__.py +0 -0
  37. package/apps/travel-chaos-organizer/backend/app/db/database.py +0 -20
  38. package/apps/travel-chaos-organizer/backend/app/db/migrations/001_init.sql +0 -71
  39. package/apps/travel-chaos-organizer/backend/app/db/migrations/002_telemetry_freemium.sql +0 -31
  40. package/apps/travel-chaos-organizer/backend/app/db/migrations/003_telegram_chat_id.sql +0 -4
  41. package/apps/travel-chaos-organizer/backend/app/db/migrations/004_stripe_and_email.sql +0 -5
  42. package/apps/travel-chaos-organizer/backend/app/limiter.py +0 -4
  43. package/apps/travel-chaos-organizer/backend/app/logging_config.py +0 -25
  44. package/apps/travel-chaos-organizer/backend/app/main.py +0 -77
  45. package/apps/travel-chaos-organizer/backend/app/models/__init__.py +0 -0
  46. package/apps/travel-chaos-organizer/backend/app/models/schemas.py +0 -142
  47. package/apps/travel-chaos-organizer/backend/app/routers/__init__.py +0 -0
  48. package/apps/travel-chaos-organizer/backend/app/routers/admin.py +0 -237
  49. package/apps/travel-chaos-organizer/backend/app/routers/cache.py +0 -13
  50. package/apps/travel-chaos-organizer/backend/app/routers/events.py +0 -29
  51. package/apps/travel-chaos-organizer/backend/app/routers/export.py +0 -122
  52. package/apps/travel-chaos-organizer/backend/app/routers/inbox.py +0 -122
  53. package/apps/travel-chaos-organizer/backend/app/routers/items.py +0 -130
  54. package/apps/travel-chaos-organizer/backend/app/routers/mail.py +0 -108
  55. package/apps/travel-chaos-organizer/backend/app/routers/parse.py +0 -303
  56. package/apps/travel-chaos-organizer/backend/app/routers/payments.py +0 -179
  57. package/apps/travel-chaos-organizer/backend/app/routers/trips.py +0 -134
  58. package/apps/travel-chaos-organizer/backend/app/routers/users.py +0 -169
  59. package/apps/travel-chaos-organizer/backend/app/routers/waitlist.py +0 -37
  60. package/apps/travel-chaos-organizer/backend/app/services/__init__.py +0 -0
  61. package/apps/travel-chaos-organizer/backend/app/services/cache.py +0 -131
  62. package/apps/travel-chaos-organizer/backend/app/services/drip.py +0 -128
  63. package/apps/travel-chaos-organizer/backend/app/services/email.py +0 -61
  64. package/apps/travel-chaos-organizer/backend/app/services/errors.py +0 -44
  65. package/apps/travel-chaos-organizer/backend/app/services/notifier.py +0 -10
  66. package/apps/travel-chaos-organizer/backend/app/services/ollama.py +0 -101
  67. package/apps/travel-chaos-organizer/backend/app/services/parser.py +0 -22
  68. package/apps/travel-chaos-organizer/backend/app/services/quota.py +0 -85
  69. package/apps/travel-chaos-organizer/backend/app/services/sentry.py +0 -11
  70. package/apps/travel-chaos-organizer/backend/app/services/telemetry.py +0 -35
  71. package/apps/travel-chaos-organizer/backend/pytest.ini +0 -4
  72. package/apps/travel-chaos-organizer/backend/requirements-dev.txt +0 -6
  73. package/apps/travel-chaos-organizer/backend/requirements.txt +0 -25
  74. package/apps/travel-chaos-organizer/backend/tests/__init__.py +0 -0
  75. package/apps/travel-chaos-organizer/backend/tests/conftest.py +0 -223
  76. package/apps/travel-chaos-organizer/backend/tests/test_admin.py +0 -99
  77. package/apps/travel-chaos-organizer/backend/tests/test_cache.py +0 -142
  78. package/apps/travel-chaos-organizer/backend/tests/test_drip.py +0 -32
  79. package/apps/travel-chaos-organizer/backend/tests/test_e2e_chain.py +0 -121
  80. package/apps/travel-chaos-organizer/backend/tests/test_export.py +0 -48
  81. package/apps/travel-chaos-organizer/backend/tests/test_inbox.py +0 -107
  82. package/apps/travel-chaos-organizer/backend/tests/test_items.py +0 -138
  83. package/apps/travel-chaos-organizer/backend/tests/test_mail.py +0 -113
  84. package/apps/travel-chaos-organizer/backend/tests/test_ollama.py +0 -145
  85. package/apps/travel-chaos-organizer/backend/tests/test_parse_url.py +0 -98
  86. package/apps/travel-chaos-organizer/backend/tests/test_parser.py +0 -72
  87. package/apps/travel-chaos-organizer/backend/tests/test_payments.py +0 -163
  88. package/apps/travel-chaos-organizer/backend/tests/test_quota.py +0 -46
  89. package/apps/travel-chaos-organizer/backend/tests/test_search.py +0 -67
  90. package/apps/travel-chaos-organizer/backend/tests/test_trips.py +0 -123
  91. package/apps/travel-chaos-organizer/backend/tests/test_users.py +0 -52
  92. package/apps/travel-chaos-organizer/backups/.gitkeep +0 -0
  93. package/apps/travel-chaos-organizer/caddy/Caddyfile +0 -27
  94. package/apps/travel-chaos-organizer/docker-compose.node1.yml +0 -31
  95. package/apps/travel-chaos-organizer/docker-compose.override.yml +0 -14
  96. package/apps/travel-chaos-organizer/docker-compose.yml +0 -128
  97. package/apps/travel-chaos-organizer/frontend/.env.example +0 -13
  98. package/apps/travel-chaos-organizer/frontend/app/(app)/_layout.tsx +0 -80
  99. package/apps/travel-chaos-organizer/frontend/app/(app)/inbox.tsx +0 -166
  100. package/apps/travel-chaos-organizer/frontend/app/(app)/search.tsx +0 -130
  101. package/apps/travel-chaos-organizer/frontend/app/(app)/settings.tsx +0 -381
  102. package/apps/travel-chaos-organizer/frontend/app/(app)/trips/[id].tsx +0 -346
  103. package/apps/travel-chaos-organizer/frontend/app/(app)/trips.tsx +0 -363
  104. package/apps/travel-chaos-organizer/frontend/app/(app)/upgrade/cancel.tsx +0 -34
  105. package/apps/travel-chaos-organizer/frontend/app/(app)/upgrade/success.tsx +0 -35
  106. package/apps/travel-chaos-organizer/frontend/app/(auth)/login.tsx +0 -91
  107. package/apps/travel-chaos-organizer/frontend/app/_layout.tsx +0 -71
  108. package/apps/travel-chaos-organizer/frontend/app/auth.tsx +0 -29
  109. package/apps/travel-chaos-organizer/frontend/app/index.tsx +0 -16
  110. package/apps/travel-chaos-organizer/frontend/app/onboarding.tsx +0 -119
  111. package/apps/travel-chaos-organizer/frontend/app.config.ts +0 -54
  112. package/apps/travel-chaos-organizer/frontend/assets/adaptive-icon.svg +0 -66
  113. package/apps/travel-chaos-organizer/frontend/assets/icon.svg +0 -79
  114. package/apps/travel-chaos-organizer/frontend/assets/splash.svg +0 -15
  115. package/apps/travel-chaos-organizer/frontend/components/BottomSheet.tsx +0 -102
  116. package/apps/travel-chaos-organizer/frontend/components/ConfettiCannon.tsx +0 -107
  117. package/apps/travel-chaos-organizer/frontend/components/ConfirmDialog.tsx +0 -109
  118. package/apps/travel-chaos-organizer/frontend/components/ErrorBoundary.tsx +0 -45
  119. package/apps/travel-chaos-organizer/frontend/components/FileUploadButton.tsx +0 -19
  120. package/apps/travel-chaos-organizer/frontend/components/ImportSheet.tsx +0 -155
  121. package/apps/travel-chaos-organizer/frontend/components/OfflineBadge.tsx +0 -55
  122. package/apps/travel-chaos-organizer/frontend/components/QRCodeViewer.tsx +0 -80
  123. package/apps/travel-chaos-organizer/frontend/components/ScoreRing.tsx +0 -39
  124. package/apps/travel-chaos-organizer/frontend/components/Screen.tsx +0 -39
  125. package/apps/travel-chaos-organizer/frontend/components/Skeleton.tsx +0 -70
  126. package/apps/travel-chaos-organizer/frontend/components/SwipeableInboxItem.tsx +0 -121
  127. package/apps/travel-chaos-organizer/frontend/components/SwipeableTimelineItem.tsx +0 -70
  128. package/apps/travel-chaos-organizer/frontend/components/TimelineItem.tsx +0 -73
  129. package/apps/travel-chaos-organizer/frontend/components/TimelineItemDetail.tsx +0 -130
  130. package/apps/travel-chaos-organizer/frontend/components/Toast.tsx +0 -60
  131. package/apps/travel-chaos-organizer/frontend/components/ToastContext.tsx +0 -42
  132. package/apps/travel-chaos-organizer/frontend/components/TripCard.tsx +0 -146
  133. package/apps/travel-chaos-organizer/frontend/components/TripQRModal.tsx +0 -70
  134. package/apps/travel-chaos-organizer/frontend/components/UpgradeBanner.tsx +0 -44
  135. package/apps/travel-chaos-organizer/frontend/eas.json +0 -22
  136. package/apps/travel-chaos-organizer/frontend/hooks/useInbox.ts +0 -24
  137. package/apps/travel-chaos-organizer/frontend/hooks/useNetworkSync.ts +0 -60
  138. package/apps/travel-chaos-organizer/frontend/hooks/useTrips.ts +0 -112
  139. package/apps/travel-chaos-organizer/frontend/lib/api.ts +0 -223
  140. package/apps/travel-chaos-organizer/frontend/lib/auth.ts +0 -93
  141. package/apps/travel-chaos-organizer/frontend/lib/db.ts +0 -86
  142. package/apps/travel-chaos-organizer/frontend/lib/fileCache.ts +0 -54
  143. package/apps/travel-chaos-organizer/frontend/lib/haptics.ts +0 -13
  144. package/apps/travel-chaos-organizer/frontend/lib/notifications.ts +0 -112
  145. package/apps/travel-chaos-organizer/frontend/lib/offlineQueue.ts +0 -145
  146. package/apps/travel-chaos-organizer/frontend/lib/quota.tsx +0 -43
  147. package/apps/travel-chaos-organizer/frontend/lib/sentry.ts +0 -28
  148. package/apps/travel-chaos-organizer/frontend/lib/shareIntent.ts +0 -44
  149. package/apps/travel-chaos-organizer/frontend/package-lock.json +0 -13221
  150. package/apps/travel-chaos-organizer/frontend/package.json +0 -44
  151. package/apps/travel-chaos-organizer/frontend/tsconfig.json +0 -23
  152. package/apps/travel-chaos-organizer/keycloak/realm-export.json +0 -59
  153. package/apps/travel-chaos-organizer/landing/index.html +0 -773
  154. package/apps/travel-chaos-organizer/scripts/backup.sh +0 -14
  155. package/apps/travel-chaos-organizer/scripts/configure-env.sh +0 -82
  156. package/apps/travel-chaos-organizer/scripts/deploy.sh +0 -54
  157. package/apps/travel-chaos-organizer/scripts/health-check.sh +0 -36
  158. package/apps/travel-chaos-organizer/scripts/migrate.sh +0 -7
  159. package/apps/travel-chaos-organizer/scripts/server-setup.sh +0 -85
  160. package/apps/travel-chaos-organizer/scripts/smoke-test.sh +0 -29
  161. package/apps/travel-chaos-organizer/scripts/verify-tracking.sh +0 -71
  162. package/apps/travel-chaos-organizer/telegram-bot/.env.example +0 -3
  163. package/apps/travel-chaos-organizer/telegram-bot/Dockerfile +0 -6
  164. package/apps/travel-chaos-organizer/telegram-bot/bot.py +0 -504
  165. package/apps/travel-chaos-organizer/telegram-bot/requirements.txt +0 -3
  166. package/dist/src/__tests__/auth.test.d.ts +0 -1
  167. package/dist/src/__tests__/auth.test.js +0 -107
  168. package/dist/src/__tests__/auth.test.js.map +0 -1
  169. package/dist/src/__tests__/brain-flow.test.d.ts +0 -12
  170. package/dist/src/__tests__/brain-flow.test.js +0 -1241
  171. package/dist/src/__tests__/brain-flow.test.js.map +0 -1
  172. package/dist/src/__tests__/confidence.test.d.ts +0 -13
  173. package/dist/src/__tests__/confidence.test.js +0 -233
  174. package/dist/src/__tests__/confidence.test.js.map +0 -1
  175. package/dist/src/__tests__/e2e/api-auth.test.d.ts +0 -7
  176. package/dist/src/__tests__/e2e/api-auth.test.js +0 -162
  177. package/dist/src/__tests__/e2e/api-auth.test.js.map +0 -1
  178. package/dist/src/__tests__/e2e/api-health.test.d.ts +0 -7
  179. package/dist/src/__tests__/e2e/api-health.test.js +0 -95
  180. package/dist/src/__tests__/e2e/api-health.test.js.map +0 -1
  181. package/dist/src/__tests__/e2e/cli.test.d.ts +0 -10
  182. package/dist/src/__tests__/e2e/cli.test.js +0 -204
  183. package/dist/src/__tests__/e2e/cli.test.js.map +0 -1
  184. package/dist/src/__tests__/e2e/config.d.ts +0 -35
  185. package/dist/src/__tests__/e2e/config.js +0 -53
  186. package/dist/src/__tests__/e2e/config.js.map +0 -1
  187. package/dist/src/__tests__/e2e/funnel.test.d.ts +0 -13
  188. package/dist/src/__tests__/e2e/funnel.test.js +0 -90
  189. package/dist/src/__tests__/e2e/funnel.test.js.map +0 -1
  190. package/dist/src/__tests__/e2e/mcp-tools.test.d.ts +0 -7
  191. package/dist/src/__tests__/e2e/mcp-tools.test.js +0 -129
  192. package/dist/src/__tests__/e2e/mcp-tools.test.js.map +0 -1
  193. package/dist/src/__tests__/e2e/provisioning.test.d.ts +0 -15
  194. package/dist/src/__tests__/e2e/provisioning.test.js +0 -123
  195. package/dist/src/__tests__/e2e/provisioning.test.js.map +0 -1
  196. package/dist/src/__tests__/handlers-unit.test.d.ts +0 -12
  197. package/dist/src/__tests__/handlers-unit.test.js +0 -505
  198. package/dist/src/__tests__/handlers-unit.test.js.map +0 -1
  199. package/dist/src/__tests__/namespace.test.d.ts +0 -1
  200. package/dist/src/__tests__/namespace.test.js +0 -114
  201. package/dist/src/__tests__/namespace.test.js.map +0 -1
  202. package/dist/src/__tests__/rerank.test.d.ts +0 -1
  203. package/dist/src/__tests__/rerank.test.js +0 -115
  204. package/dist/src/__tests__/rerank.test.js.map +0 -1
  205. package/dist/src/__tests__/utils.test.d.ts +0 -1
  206. package/dist/src/__tests__/utils.test.js +0 -83
  207. package/dist/src/__tests__/utils.test.js.map +0 -1
  208. package/dist/src/bench/cachly-bench.d.ts +0 -35
  209. package/dist/src/bench/cachly-bench.js +0 -182
  210. package/dist/src/bench/cachly-bench.js.map +0 -1
  211. package/dist/src/bench/fixtures.d.ts +0 -37
  212. package/dist/src/bench/fixtures.js +0 -225
  213. package/dist/src/bench/fixtures.js.map +0 -1
  214. package/dist/src/search.test.d.ts +0 -1
  215. package/dist/src/search.test.js +0 -713
  216. package/dist/src/search.test.js.map +0 -1
  217. package/eslint.config.js +0 -16
  218. package/glama.json +0 -36
  219. package/llms.txt +0 -155
  220. package/packages/telegram-notify/SPLIT_PLAN.md +0 -106
  221. package/packages/telegram-notify/client.ts +0 -88
  222. package/packages/telegram-notify/emojis.json +0 -22
  223. package/packages/telegram-notify/pyproject.toml +0 -17
  224. package/packages/telegram-notify/telegram_notify/__init__.py +0 -3
  225. package/packages/telegram-notify/telegram_notify/client.py +0 -87
  226. package/packages/telegram-notify/telegram_notify/emojis.json +0 -22
  227. package/scripts/e2e-run.sh +0 -160
  228. package/scripts/generate-icons.js +0 -28
  229. package/smithery.yaml +0 -45
  230. package/src/__tests__/auth.test.ts +0 -147
  231. package/src/__tests__/brain-flow.test.ts +0 -1472
  232. package/src/__tests__/confidence.test.ts +0 -285
  233. package/src/__tests__/e2e/api-auth.test.ts +0 -182
  234. package/src/__tests__/e2e/api-health.test.ts +0 -103
  235. package/src/__tests__/e2e/cli.test.ts +0 -228
  236. package/src/__tests__/e2e/config.ts +0 -53
  237. package/src/__tests__/e2e/funnel.test.ts +0 -99
  238. package/src/__tests__/e2e/mcp-tools.test.ts +0 -191
  239. package/src/__tests__/e2e/provisioning.test.ts +0 -128
  240. package/src/__tests__/handlers-unit.test.ts +0 -677
  241. package/src/__tests__/namespace.test.ts +0 -126
  242. package/src/__tests__/rerank.test.ts +0 -130
  243. package/src/__tests__/utils.test.ts +0 -81
  244. package/src/auth.ts +0 -46
  245. package/src/bench/cachly-bench.ts +0 -217
  246. package/src/bench/fixtures.ts +0 -256
  247. package/src/ckg.ts +0 -103
  248. package/src/confidence.ts +0 -38
  249. package/src/embeddings.ts +0 -217
  250. package/src/handlers/advanced.ts +0 -647
  251. package/src/handlers/brain.ts +0 -2622
  252. package/src/handlers/cache.ts +0 -633
  253. package/src/handlers/context.ts +0 -222
  254. package/src/handlers/fedbrain.ts +0 -1232
  255. package/src/handlers/instances.ts +0 -180
  256. package/src/handlers/roadmap.ts +0 -201
  257. package/src/handlers/syndicate.ts +0 -460
  258. package/src/handlers/tco.ts +0 -345
  259. package/src/handlers/team.ts +0 -1249
  260. package/src/index.ts +0 -2815
  261. package/src/namespace.ts +0 -128
  262. package/src/notifier.ts +0 -5
  263. package/src/rerank.ts +0 -148
  264. package/src/search.test.ts +0 -848
  265. package/src/search.ts +0 -1781
  266. package/src/tools.ts +0 -2056
  267. package/src/utils.ts +0 -90
  268. package/test-features.cjs +0 -23
  269. package/tsconfig.json +0 -16
  270. package/vitest.config.ts +0 -14
@@ -1,126 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { detectNamespace } from '../namespace.js';
3
-
4
- // ── Namespace Auto-Detection ───────────────────────────────────────────────
5
-
6
- describe('detectNamespace', () => {
7
- // ── Code ────────────────────────────────────────────────────────────────────
8
- describe('code detection', () => {
9
- it.each([
10
- ['python def', 'def process(data): return data * 2'],
11
- ['js const arrow', 'const handler = () => response.send(200)'],
12
- ['ts class', 'class UserService { constructor(private repo: Repo) {} }'],
13
- ['import statement', 'import { useState } from "react"'],
14
- ['shebang line', '#!/usr/bin/env python3\nprint("hello")'],
15
- ['go func', 'func main() { fmt.Println("hello") }'],
16
- ['cpp include', '#include <iostream>\nint main() { return 0; }'],
17
- ['interface block', 'interface ICache { get(key: string): void; }'],
18
- ['struct block', 'struct Config { host string }'],
19
- ['async def', 'async def fetch_data(url: str) -> dict:'],
20
- ['lambda', 'fn = lambda x: x * 2'],
21
- ['package decl', 'package com.example.service;'],
22
- ])('detects code: %s', (_, prompt) => {
23
- expect(detectNamespace(prompt)).toBe('cachly:sem:code');
24
- });
25
- });
26
-
27
- // ── Translation ─────────────────────────────────────────────────────────────
28
- describe('translation detection', () => {
29
- it.each([
30
- ['translate', 'translate this paragraph to Spanish'],
31
- ['übersetze', 'übersetze diesen Text bitte'],
32
- ['auf deutsch', 'Schreib das auf deutsch'],
33
- ['in english', 'Please write this in english'],
34
- ['ins deutsche', 'Bitte übersetze ins Deutsche'],
35
- ['traduce', 'traduce este texto al alemán'],
36
- ['traduis', 'traduis ce texte en anglais'],
37
- ['vertaal', 'vertaal dit naar het Nederlands'],
38
- ])('detects translation: %s', (_, prompt) => {
39
- expect(detectNamespace(prompt)).toBe('cachly:sem:translation');
40
- });
41
- });
42
-
43
- // ── Summary ──────────────────────────────────────────────────────────────────
44
- describe('summary detection', () => {
45
- it.each([
46
- ['summarize', 'summarize this article for me'],
47
- ['summarise', 'summarise the key points'],
48
- ['tl;dr', 'tl;dr of the following blog post:'],
49
- ['tldr', 'tldr of this document'],
50
- ['key points', 'what are the key points?'],
51
- ['zusammenfass', 'fasse zusammen worum es geht'],
52
- ['give me a brief', 'give me a brief overview'],
53
- ['in a nutshell', 'explain machine learning in a nutshell'],
54
- ['stichpunkte', 'Gib mir die wichtigsten Stichpunkte'],
55
- ])('detects summary: %s', (_, prompt) => {
56
- expect(detectNamespace(prompt)).toBe('cachly:sem:summary');
57
- });
58
- });
59
-
60
- // ── Q&A ──────────────────────────────────────────────────────────────────────
61
- describe('qa detection', () => {
62
- it.each([
63
- ['what is', 'what is the capital of France?'],
64
- ['how does', 'how does photosynthesis work?'],
65
- ['why is', 'why is the sky blue?'],
66
- ['who invented', 'who invented the internet?'],
67
- ['where is', 'where is Mount Everest located?'],
68
- ['when was', 'when was the Eiffel Tower built?'],
69
- ['can you', 'can you explain recursion?'],
70
- ['is redis', 'is Redis single-threaded?'],
71
- ['does postgres', 'does postgres support JSON natively?'],
72
- ['wer ist', 'wer ist der aktuelle Bundeskanzler?'],
73
- ['wie funktioniert', 'wie funktioniert ein JWT Token?'],
74
- ['trailing ?', 'Redis vs Memcached?'],
75
- ])('detects q&a: %s', (_, prompt) => {
76
- expect(detectNamespace(prompt)).toBe('cachly:sem:qa');
77
- });
78
- });
79
-
80
- // ── Creative ─────────────────────────────────────────────────────────────────
81
- describe('creative fallback', () => {
82
- it.each([
83
- ['poem request', 'Write a short poem about autumn leaves'],
84
- ['story request', 'Tell me a fantasy story about dragons'],
85
- ['product copy', 'Generate a product description for running shoes'],
86
- ['greeting', 'Good morning, how are you today'],
87
- ['general task', 'Help me brainstorm names for my startup'],
88
- ])('falls back to creative: %s', (_, prompt) => {
89
- expect(detectNamespace(prompt)).toBe('cachly:sem:creative');
90
- });
91
- });
92
-
93
- // ── Edge cases ────────────────────────────────────────────────────────────────
94
- describe('edge cases', () => {
95
- it('is case-insensitive', () => {
96
- expect(detectNamespace('CONST x = 1')).toBe('cachly:sem:code');
97
- expect(detectNamespace('TRANSLATE THIS')).toBe('cachly:sem:translation');
98
- expect(detectNamespace('SUMMARIZE PLEASE')).toBe('cachly:sem:summary');
99
- expect(detectNamespace('WHAT IS GRAVITY?')).toBe('cachly:sem:qa');
100
- });
101
-
102
- it('trims surrounding whitespace', () => {
103
- expect(detectNamespace(' what is Redis? ')).toBe('cachly:sem:qa');
104
- expect(detectNamespace(' const x = 1 ')).toBe('cachly:sem:code');
105
- });
106
-
107
- it('code keyword takes priority over translation', () => {
108
- // Both "translate" and "def" present – code wins (checked first)
109
- expect(detectNamespace('translate this function: def foo(): pass')).toBe('cachly:sem:code');
110
- });
111
-
112
- it('code keyword takes priority over summary', () => {
113
- expect(detectNamespace('summarize this class MyService { }'))
114
- .toBe('cachly:sem:code');
115
- });
116
-
117
- it('handles empty string as creative', () => {
118
- expect(detectNamespace('')).toBe('cachly:sem:creative');
119
- });
120
-
121
- it('a single ? at end is qa', () => {
122
- expect(detectNamespace('Interesting concept?')).toBe('cachly:sem:qa');
123
- });
124
- });
125
- });
126
-
@@ -1,130 +0,0 @@
1
- /**
2
- * Tests for quality-aware reranking and the Cachly-Bench regression guard.
3
- */
4
- import { describe, it, expect } from 'vitest';
5
- import { qualityMultiplier, extractLessonQuality, rerankByQuality, type LessonQuality } from '../rerank.js';
6
- import type { KeywordMatch } from '../search.js';
7
- import { runBenchmark } from '../bench/cachly-bench.js';
8
-
9
- const lessonMatch = (topic: string, score: number, lesson: Partial<LessonQuality> & Record<string, unknown>): KeywordMatch => ({
10
- key: `cachly:lesson:best:${topic}`,
11
- content: JSON.stringify(lesson),
12
- score,
13
- matchedWords: ['x'],
14
- });
15
-
16
- describe('qualityMultiplier', () => {
17
- it('returns neutral 1.0 for documents with no quality signal', () => {
18
- expect(qualityMultiplier(null)).toBe(1.0);
19
- expect(qualityMultiplier({})).toBe(1.0);
20
- });
21
-
22
- it('ranks a proven success above a fresh failure', () => {
23
- const success = qualityMultiplier({ outcome: 'success', confidence: 0.95, recall_count: 12 });
24
- const failure = qualityMultiplier({ outcome: 'failure', confidence: 0.3, recall_count: 0 });
25
- expect(success).toBeGreaterThan(failure);
26
- });
27
-
28
- it('rewards proven-ness (recall_count) but saturates', () => {
29
- const never = qualityMultiplier({ outcome: 'success', confidence: 0.8, recall_count: 0 });
30
- const some = qualityMultiplier({ outcome: 'success', confidence: 0.8, recall_count: 5 });
31
- const many = qualityMultiplier({ outcome: 'success', confidence: 0.8, recall_count: 500 });
32
- expect(some).toBeGreaterThan(never);
33
- expect(many).toBeGreaterThan(some);
34
- expect(many - some).toBeLessThan(some - never + 0.2); // diminishing returns
35
- });
36
-
37
- it('clamps within [0.5, 2.5]', () => {
38
- const hi = qualityMultiplier({ outcome: 'success', confidence: 1, recall_count: 100000, severity: 'critical', review_level: 'senior', reviewed_by: 'x', endorsements: 99 });
39
- const lo = qualityMultiplier({ outcome: 'failure', confidence: 0, recall_count: 0, severity: 'minor' });
40
- expect(hi).toBeLessThanOrEqual(2.5);
41
- expect(lo).toBeGreaterThanOrEqual(0.5);
42
- });
43
-
44
- it('boosts a human-reviewed lesson above an unreviewed equal', () => {
45
- const base = { outcome: 'success', confidence: 0.8, recall_count: 3 } as const;
46
- const unreviewed = qualityMultiplier({ ...base });
47
- const peer = qualityMultiplier({ ...base, reviewed_by: 'alice', review_level: 'peer', endorsements: 1 });
48
- const senior = qualityMultiplier({ ...base, reviewed_by: 'bob', review_level: 'senior', endorsements: 1 });
49
- expect(peer).toBeGreaterThan(unreviewed);
50
- expect(senior).toBeGreaterThan(peer);
51
- });
52
-
53
- it('rewards additional distinct endorsements but saturates', () => {
54
- const base = { outcome: 'success', confidence: 0.8, recall_count: 3, reviewed_by: 'a', review_level: 'peer' } as const;
55
- const one = qualityMultiplier({ ...base, endorsements: 1 });
56
- const three = qualityMultiplier({ ...base, endorsements: 3 });
57
- const fifty = qualityMultiplier({ ...base, endorsements: 50 });
58
- expect(three).toBeGreaterThan(one);
59
- expect(fifty).toBeGreaterThan(three);
60
- expect(fifty - three).toBeLessThan(three - one + 0.1);
61
- });
62
- });
63
-
64
- describe('extractLessonQuality', () => {
65
- it('parses lesson JSON for lesson keys', () => {
66
- const q = extractLessonQuality({ key: 'cachly:lesson:best:foo', content: '{"outcome":"success"}' });
67
- expect(q?.outcome).toBe('success');
68
- });
69
- it('returns null for non-lesson keys (e.g. context)', () => {
70
- expect(extractLessonQuality({ key: 'cachly:ctx:custom:foo', content: 'plain text' })).toBeNull();
71
- });
72
- it('returns null for corrupt lesson JSON', () => {
73
- expect(extractLessonQuality({ key: 'cachly:lesson:best:foo', content: '{not json' })).toBeNull();
74
- });
75
- });
76
-
77
- describe('rerankByQuality', () => {
78
- it('promotes a proven success over a higher-BM25 failure distractor', () => {
79
- const matches: KeywordMatch[] = [
80
- lessonMatch('symptom-failure', 10, { outcome: 'failure', confidence: 0.2, recall_count: 0 }),
81
- lessonMatch('proven-success', 8, { outcome: 'success', confidence: 0.95, recall_count: 12 }),
82
- ];
83
- const ranked = rerankByQuality(matches);
84
- expect(ranked[0].key).toBe('cachly:lesson:best:proven-success');
85
- });
86
-
87
- it('keeps a strong text match competitive (does not bury it on quality alone)', () => {
88
- const matches: KeywordMatch[] = [
89
- lessonMatch('weak-but-perfect', 1, { outcome: 'success', confidence: 1, recall_count: 100 }),
90
- lessonMatch('strong-text', 100, { outcome: 'partial', confidence: 0.6, recall_count: 0 }),
91
- ];
92
- const ranked = rerankByQuality(matches);
93
- expect(ranked[0].key).toBe('cachly:lesson:best:strong-text');
94
- });
95
-
96
- it('is stable and returns every input', () => {
97
- const matches: KeywordMatch[] = [
98
- lessonMatch('a', 5, { outcome: 'success' }),
99
- lessonMatch('b', 5, { outcome: 'success' }),
100
- ];
101
- const ranked = rerankByQuality(matches);
102
- expect(ranked).toHaveLength(2);
103
- });
104
-
105
- it('handles empty input', () => {
106
- expect(rerankByQuality([])).toEqual([]);
107
- });
108
- });
109
-
110
- describe('Cachly-Bench regression guard', () => {
111
- it('quality rerank never regresses headline metrics vs baseline', async () => {
112
- const r = await runBenchmark();
113
- // Cachly must be >= baseline on the headline metrics (the moat must not hurt).
114
- expect(r.cachly.mrr).toBeGreaterThanOrEqual(r.baseline.mrr - 1e-9);
115
- expect(r.cachly.precisionAt1).toBeGreaterThanOrEqual(r.baseline.precisionAt1 - 1e-9);
116
- expect(r.cachly.ndcgAt5).toBeGreaterThanOrEqual(r.baseline.ndcgAt5 - 1e-9);
117
- });
118
-
119
- it('demonstrates a positive lift on Precision@1 and MRR (the proof)', async () => {
120
- const r = await runBenchmark();
121
- expect(r.lift.precisionAt1).toBeGreaterThan(0);
122
- expect(r.lift.mrr).toBeGreaterThan(0);
123
- });
124
-
125
- it('baseline is already strong (corpus is not rigged to make rerank look good)', async () => {
126
- const r = await runBenchmark();
127
- // A fair benchmark: BM25 alone already gets most queries right.
128
- expect(r.baseline.mrr).toBeGreaterThan(0.7);
129
- });
130
- });
@@ -1,81 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { EventEmitter } from 'node:events';
3
- import { safeJsonParse, withTimeout, normalizeGitPath, scanKeys } from '../utils.js';
4
-
5
- describe('safeJsonParse', () => {
6
- it('parses valid JSON', () => {
7
- expect(safeJsonParse('{"a":1}', {})).toEqual({ a: 1 });
8
- });
9
- it('returns fallback on null/empty/malformed', () => {
10
- expect(safeJsonParse(null, 'fb')).toBe('fb');
11
- expect(safeJsonParse('', 'fb')).toBe('fb');
12
- expect(safeJsonParse('{not json', 'fb')).toBe('fb');
13
- });
14
- });
15
-
16
- describe('withTimeout', () => {
17
- it('resolves with the promise value when it settles in time', async () => {
18
- const out = await withTimeout(Promise.resolve('ok'), 1000, 'fallback');
19
- expect(out).toBe('ok');
20
- });
21
- it('resolves with fallback when the promise is too slow', async () => {
22
- const slow = new Promise<string>((r) => setTimeout(() => r('late'), 100));
23
- const out = await withTimeout(slow, 10, 'fallback');
24
- expect(out).toBe('fallback');
25
- });
26
- it('resolves with fallback when the promise rejects', async () => {
27
- const out = await withTimeout(Promise.reject(new Error('boom')), 1000, 'fallback');
28
- expect(out).toBe('fallback');
29
- });
30
- });
31
-
32
- describe('normalizeGitPath', () => {
33
- it('passes a normal path through', () => {
34
- expect(normalizeGitPath('src/auth/jwt.ts')).toBe('src/auth/jwt.ts');
35
- });
36
- it('resolves brace rename notation to the post-rename path', () => {
37
- expect(normalizeGitPath('src/{old => new}/file.ts')).toBe('src/new/file.ts');
38
- });
39
- it('resolves bare rename notation', () => {
40
- expect(normalizeGitPath('old/path.ts => new/path.ts')).toBe('new/path.ts');
41
- });
42
- it('collapses an empty rename segment cleanly', () => {
43
- // dir/{ => sub}/f.ts → dir/sub/f.ts
44
- expect(normalizeGitPath('dir/{ => sub}/f.ts')).toBe('dir/sub/f.ts');
45
- });
46
- it('trims whitespace', () => {
47
- expect(normalizeGitPath(' src/x.ts ')).toBe('src/x.ts');
48
- });
49
- });
50
-
51
- // Minimal scan-capable mock
52
- class ScanMock {
53
- constructor(private keys: string[], private opts: { hang?: boolean } = {}) {}
54
- scanStream(_o: { match: string; count?: number }) {
55
- const em = new EventEmitter();
56
- if (this.opts.hang) return em; // never emits — exercises the timeout path
57
- setImmediate(() => {
58
- em.emit('data', this.keys);
59
- em.emit('end');
60
- });
61
- return em;
62
- }
63
- }
64
-
65
- describe('scanKeys', () => {
66
- it('collects all matching keys', async () => {
67
- const mock = new ScanMock(['a', 'b', 'c']);
68
- const out = await scanKeys(mock, 'pattern:*');
69
- expect(out).toEqual(['a', 'b', 'c']);
70
- });
71
- it('caps at max keys', async () => {
72
- const mock = new ScanMock(['a', 'b', 'c', 'd', 'e']);
73
- const out = await scanKeys(mock, 'pattern:*', { max: 3 });
74
- expect(out).toHaveLength(3);
75
- });
76
- it('resolves with gathered keys on timeout instead of hanging', async () => {
77
- const mock = new ScanMock([], { hang: true });
78
- const out = await scanKeys(mock, 'pattern:*', { timeoutMs: 20 });
79
- expect(out).toEqual([]);
80
- });
81
- });
package/src/auth.ts DELETED
@@ -1,46 +0,0 @@
1
- import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
2
-
3
- export function jwtExpiryMs(token: string): number | null {
4
- try {
5
- const parts = token.split('.');
6
- if (parts.length !== 3) return null;
7
- const payload = JSON.parse(
8
- Buffer.from(parts[1], 'base64url').toString('utf-8'),
9
- ) as { exp?: number };
10
- return payload.exp ? payload.exp * 1000 : null;
11
- } catch {
12
- return null;
13
- }
14
- }
15
-
16
- export function checkJwt(jwt: string): void {
17
- if (!jwt) {
18
- throw new McpError(
19
- ErrorCode.InvalidRequest,
20
- 'CACHLY_JWT env var not set.\n\nGet your token at https://cachly.dev/setup-ai and add it to your MCP config:\n CACHLY_JWT=<your-token>',
21
- );
22
- }
23
- const expMs = jwtExpiryMs(jwt);
24
- if (expMs !== null && expMs < Date.now()) {
25
- throw new McpError(
26
- ErrorCode.InvalidRequest,
27
- `CACHLY_JWT expired at ${new Date(expMs).toISOString()}.\n\nGet a fresh token at https://cachly.dev/setup-ai and update CACHLY_JWT in your MCP config.`,
28
- );
29
- }
30
- }
31
-
32
- export function handleApiError(status: number, detail: string): never {
33
- if (status === 401) {
34
- throw new McpError(
35
- ErrorCode.InvalidRequest,
36
- `Authentication failed (401): ${detail}\n\nYour CACHLY_JWT may be expired or invalid. Get a fresh token at https://cachly.dev/setup-ai`,
37
- );
38
- }
39
- if (status === 403) {
40
- throw new McpError(
41
- ErrorCode.InvalidRequest,
42
- `Access denied (403): ${detail}\n\nCheck that your CACHLY_JWT belongs to an account with access to this resource.`,
43
- );
44
- }
45
- throw new McpError(ErrorCode.InternalError, `cachly API error ${status}: ${detail}`);
46
- }
@@ -1,217 +0,0 @@
1
- /**
2
- * Cachly-Bench — the moat proof.
3
- *
4
- * Measures recall quality on a fixed, labeled corpus and compares two rankers
5
- * over the *same* BM25 candidate set, so the comparison isolates ranking quality:
6
- *
7
- * baseline — raw BM25+ keyword ranking (what a text-only / flat-file memory does)
8
- * cachly — BM25+ followed by quality-aware reranking (rerank.ts)
9
- *
10
- * It reports standard IR metrics (Precision@k, Recall@k, MRR, nDCG@k) and the
11
- * relative lift. The corpus (fixtures.ts) is fair: gold answers are the lessons
12
- * that actually solve each problem, while text-similar failed attempts act as
13
- * distractors — exactly where ranking by quality should pull ahead.
14
- *
15
- * Run: npm run bench (pretty report)
16
- * npm run bench -- --json (machine-readable)
17
- */
18
-
19
- import { EventEmitter } from 'node:events';
20
- import { keywordSearch, type KeywordMatch } from '../search.js';
21
- import { rerankByQuality } from '../rerank.js';
22
- import { BENCH_LESSONS, BENCH_QUERIES, type BenchLesson } from './fixtures.js';
23
-
24
- const LESSON_PREFIX = 'cachly:lesson:best:';
25
-
26
- // ── Minimal in-memory Redis (only what keywordSearch needs) ────────────────────
27
- class MiniRedis {
28
- private store = new Map<string, string>();
29
-
30
- set(key: string, value: string): void { this.store.set(key, value); }
31
- async get(key: string): Promise<string | null> { return this.store.get(key) ?? null; }
32
-
33
- scanStream(opts: { match: string; count?: number }): EventEmitter {
34
- const emitter = new EventEmitter();
35
- const pattern = '^' + opts.match.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*') + '$';
36
- const regex = new RegExp(pattern);
37
- const matches = [...this.store.keys()].filter(k => regex.test(k));
38
- setImmediate(() => { emitter.emit('data', matches); emitter.emit('end'); });
39
- return emitter;
40
- }
41
-
42
- pipeline() {
43
- const cmds: string[] = [];
44
- const store = this.store;
45
- return {
46
- get(key: string) { cmds.push(key); return this; },
47
- async exec(): Promise<Array<[null, string | null]>> {
48
- return cmds.map(k => [null, store.get(k) ?? null] as [null, string | null]);
49
- },
50
- };
51
- }
52
- }
53
-
54
- function indexCorpus(lessons: BenchLesson[]): MiniRedis {
55
- const redis = new MiniRedis();
56
- for (const l of lessons) {
57
- redis.set(`${LESSON_PREFIX}${l.topic}`, JSON.stringify(l));
58
- }
59
- return redis;
60
- }
61
-
62
- // ── IR metrics ─────────────────────────────────────────────────────────────────
63
- function precisionAtK(ranked: string[], relevant: Set<string>, k: number): number {
64
- const top = ranked.slice(0, k);
65
- if (top.length === 0) return 0;
66
- const hits = top.filter(t => relevant.has(t)).length;
67
- return hits / Math.min(k, top.length);
68
- }
69
-
70
- function recallAtK(ranked: string[], relevant: Set<string>, k: number): number {
71
- if (relevant.size === 0) return 0;
72
- const top = ranked.slice(0, k);
73
- const hits = top.filter(t => relevant.has(t)).length;
74
- return hits / relevant.size;
75
- }
76
-
77
- function reciprocalRank(ranked: string[], relevant: Set<string>): number {
78
- for (let i = 0; i < ranked.length; i++) {
79
- if (relevant.has(ranked[i])) return 1 / (i + 1);
80
- }
81
- return 0;
82
- }
83
-
84
- function ndcgAtK(ranked: string[], relevant: Set<string>, k: number): number {
85
- let dcg = 0;
86
- for (let i = 0; i < Math.min(k, ranked.length); i++) {
87
- if (relevant.has(ranked[i])) dcg += 1 / Math.log2(i + 2);
88
- }
89
- let idcg = 0;
90
- for (let i = 0; i < Math.min(k, relevant.size); i++) {
91
- idcg += 1 / Math.log2(i + 2);
92
- }
93
- return idcg === 0 ? 0 : dcg / idcg;
94
- }
95
-
96
- export interface BenchMetrics {
97
- precisionAt1: number;
98
- precisionAt3: number;
99
- recallAt3: number;
100
- mrr: number;
101
- ndcgAt5: number;
102
- }
103
-
104
- export interface BenchResult {
105
- baseline: BenchMetrics;
106
- cachly: BenchMetrics;
107
- /** Relative lift per metric, e.g. { mrr: 0.23 } = +23%. */
108
- lift: BenchMetrics;
109
- queryCount: number;
110
- corpusSize: number;
111
- }
112
-
113
- const matchTopic = (m: KeywordMatch): string => m.key.replace(LESSON_PREFIX, '');
114
-
115
- function mean(xs: number[]): number {
116
- return xs.length === 0 ? 0 : xs.reduce((a, b) => a + b, 0) / xs.length;
117
- }
118
-
119
- function aggregate(rows: BenchMetrics[]): BenchMetrics {
120
- return {
121
- precisionAt1: mean(rows.map(r => r.precisionAt1)),
122
- precisionAt3: mean(rows.map(r => r.precisionAt3)),
123
- recallAt3: mean(rows.map(r => r.recallAt3)),
124
- mrr: mean(rows.map(r => r.mrr)),
125
- ndcgAt5: mean(rows.map(r => r.ndcgAt5)),
126
- };
127
- }
128
-
129
- function scoreRanking(ranked: string[], relevant: Set<string>): BenchMetrics {
130
- return {
131
- precisionAt1: precisionAtK(ranked, relevant, 1),
132
- precisionAt3: precisionAtK(ranked, relevant, 3),
133
- recallAt3: recallAtK(ranked, relevant, 3),
134
- mrr: reciprocalRank(ranked, relevant),
135
- ndcgAt5: ndcgAtK(ranked, relevant, 5),
136
- };
137
- }
138
-
139
- /** Run the benchmark over the fixture corpus. Pure-ish: no network, no real Redis. */
140
- export async function runBenchmark(): Promise<BenchResult> {
141
- const redis = indexCorpus(BENCH_LESSONS);
142
- const baselineRows: BenchMetrics[] = [];
143
- const cachlyRows: BenchMetrics[] = [];
144
-
145
- for (const q of BENCH_QUERIES) {
146
- const relevant = new Set(q.relevant);
147
-
148
- // Same candidate set for both rankers — isolates the ranking contribution.
149
- const matches = await keywordSearch(redis as never, [`${LESSON_PREFIX}*`], q.query, 10);
150
-
151
- const baselineRanked = matches.map(matchTopic);
152
- const cachlyRanked = rerankByQuality(matches).map(matchTopic);
153
-
154
- baselineRows.push(scoreRanking(baselineRanked, relevant));
155
- cachlyRows.push(scoreRanking(cachlyRanked, relevant));
156
- }
157
-
158
- const baseline = aggregate(baselineRows);
159
- const cachly = aggregate(cachlyRows);
160
- const liftOf = (b: number, c: number) => (b === 0 ? (c > 0 ? 1 : 0) : (c - b) / b);
161
- const lift: BenchMetrics = {
162
- precisionAt1: liftOf(baseline.precisionAt1, cachly.precisionAt1),
163
- precisionAt3: liftOf(baseline.precisionAt3, cachly.precisionAt3),
164
- recallAt3: liftOf(baseline.recallAt3, cachly.recallAt3),
165
- mrr: liftOf(baseline.mrr, cachly.mrr),
166
- ndcgAt5: liftOf(baseline.ndcgAt5, cachly.ndcgAt5),
167
- };
168
-
169
- return { baseline, cachly, lift, queryCount: BENCH_QUERIES.length, corpusSize: BENCH_LESSONS.length };
170
- }
171
-
172
- // ── Reporting ──────────────────────────────────────────────────────────────────
173
- const pct = (x: number) => `${(x * 100).toFixed(1)}%`;
174
- const signedPct = (x: number) => `${x >= 0 ? '+' : ''}${(x * 100).toFixed(1)}%`;
175
-
176
- export function formatReport(r: BenchResult): string {
177
- const row = (label: string, b: number, c: number, l: number) =>
178
- ` ${label.padEnd(14)} ${pct(b).padStart(8)} → ${pct(c).padStart(8)} ${signedPct(l).padStart(8)}`;
179
- return [
180
- '',
181
- '🧠 Cachly-Bench — recall quality (BM25 baseline vs quality-aware rerank)',
182
- '────────────────────────────────────────────────────────────────────────',
183
- ` corpus: ${r.corpusSize} lessons · queries: ${r.queryCount}`,
184
- '',
185
- ` ${'metric'.padEnd(14)} ${'baseline'.padStart(8)} ${'cachly'.padStart(8)} ${'lift'.padStart(8)}`,
186
- row('Precision@1', r.baseline.precisionAt1, r.cachly.precisionAt1, r.lift.precisionAt1),
187
- row('Precision@3', r.baseline.precisionAt3, r.cachly.precisionAt3, r.lift.precisionAt3),
188
- row('Recall@3', r.baseline.recallAt3, r.cachly.recallAt3, r.lift.recallAt3),
189
- row('MRR', r.baseline.mrr, r.cachly.mrr, r.lift.mrr),
190
- row('nDCG@5', r.baseline.ndcgAt5, r.cachly.ndcgAt5, r.lift.ndcgAt5),
191
- '────────────────────────────────────────────────────────────────────────',
192
- ` Headline: MRR ${signedPct(r.lift.mrr)} · Precision@1 ${signedPct(r.lift.precisionAt1)}`,
193
- '',
194
- ].join('\n');
195
- }
196
-
197
- // ── CLI entry ───────────────────────────────────────────────────────────────────
198
- const isMain = (() => {
199
- try {
200
- return import.meta.url === `file://${process.argv[1]}`
201
- || process.argv[1]?.endsWith('cachly-bench.ts')
202
- || process.argv[1]?.endsWith('cachly-bench.js');
203
- } catch { return false; }
204
- })();
205
-
206
- if (isMain) {
207
- runBenchmark().then(result => {
208
- if (process.argv.includes('--json')) {
209
- process.stdout.write(JSON.stringify(result, null, 2) + '\n');
210
- } else {
211
- process.stdout.write(formatReport(result));
212
- }
213
- }).catch(err => {
214
- process.stderr.write(`bench failed: ${err instanceof Error ? err.stack : String(err)}\n`);
215
- process.exit(1);
216
- });
217
- }