@cachly-dev/mcp-server 0.10.56 → 0.10.58

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 (338) hide show
  1. package/.github/workflows/eas-build-android.yml +58 -0
  2. package/.github/workflows/eas-update.yml +55 -0
  3. package/.github/workflows/publish.yml +2 -2
  4. package/BENCH.md +91 -0
  5. package/CHANGELOG.md +44 -0
  6. package/README.md +6 -34
  7. package/STRATEGY.md +228 -0
  8. package/apps/travel-chaos-organizer/.env.example +49 -0
  9. package/apps/travel-chaos-organizer/DEPLOY.md +328 -0
  10. package/apps/travel-chaos-organizer/Makefile +57 -0
  11. package/apps/travel-chaos-organizer/README.md +159 -0
  12. package/apps/travel-chaos-organizer/backend/.env.example +10 -0
  13. package/apps/travel-chaos-organizer/backend/Dockerfile +25 -0
  14. package/apps/travel-chaos-organizer/backend/alembic/env.py +56 -0
  15. package/apps/travel-chaos-organizer/backend/alembic/script.py.mako +24 -0
  16. package/apps/travel-chaos-organizer/backend/alembic/versions/.gitkeep +0 -0
  17. package/apps/travel-chaos-organizer/backend/alembic/versions/0001_stripe_customer_id.py +24 -0
  18. package/apps/travel-chaos-organizer/backend/alembic/versions/0002_telegram_chat_id.py +24 -0
  19. package/apps/travel-chaos-organizer/backend/alembic.ini +39 -0
  20. package/apps/travel-chaos-organizer/backend/app/__init__.py +0 -0
  21. package/apps/travel-chaos-organizer/backend/app/auth/__init__.py +0 -0
  22. package/apps/travel-chaos-organizer/backend/app/auth/keycloak.py +81 -0
  23. package/apps/travel-chaos-organizer/backend/app/config.py +66 -0
  24. package/apps/travel-chaos-organizer/backend/app/db/__init__.py +0 -0
  25. package/apps/travel-chaos-organizer/backend/app/db/database.py +20 -0
  26. package/apps/travel-chaos-organizer/backend/app/db/migrations/001_init.sql +71 -0
  27. package/apps/travel-chaos-organizer/backend/app/db/migrations/002_telemetry_freemium.sql +31 -0
  28. package/apps/travel-chaos-organizer/backend/app/db/migrations/003_telegram_chat_id.sql +4 -0
  29. package/apps/travel-chaos-organizer/backend/app/db/migrations/004_stripe_and_email.sql +5 -0
  30. package/apps/travel-chaos-organizer/backend/app/limiter.py +4 -0
  31. package/apps/travel-chaos-organizer/backend/app/logging_config.py +25 -0
  32. package/apps/travel-chaos-organizer/backend/app/main.py +77 -0
  33. package/apps/travel-chaos-organizer/backend/app/models/__init__.py +0 -0
  34. package/apps/travel-chaos-organizer/backend/app/models/schemas.py +142 -0
  35. package/apps/travel-chaos-organizer/backend/app/routers/__init__.py +0 -0
  36. package/apps/travel-chaos-organizer/backend/app/routers/admin.py +237 -0
  37. package/apps/travel-chaos-organizer/backend/app/routers/cache.py +13 -0
  38. package/apps/travel-chaos-organizer/backend/app/routers/events.py +29 -0
  39. package/apps/travel-chaos-organizer/backend/app/routers/export.py +122 -0
  40. package/apps/travel-chaos-organizer/backend/app/routers/inbox.py +122 -0
  41. package/apps/travel-chaos-organizer/backend/app/routers/items.py +130 -0
  42. package/apps/travel-chaos-organizer/backend/app/routers/mail.py +108 -0
  43. package/apps/travel-chaos-organizer/backend/app/routers/parse.py +303 -0
  44. package/apps/travel-chaos-organizer/backend/app/routers/payments.py +179 -0
  45. package/apps/travel-chaos-organizer/backend/app/routers/trips.py +134 -0
  46. package/apps/travel-chaos-organizer/backend/app/routers/users.py +169 -0
  47. package/apps/travel-chaos-organizer/backend/app/routers/waitlist.py +37 -0
  48. package/apps/travel-chaos-organizer/backend/app/services/__init__.py +0 -0
  49. package/apps/travel-chaos-organizer/backend/app/services/cache.py +131 -0
  50. package/apps/travel-chaos-organizer/backend/app/services/drip.py +128 -0
  51. package/apps/travel-chaos-organizer/backend/app/services/email.py +61 -0
  52. package/apps/travel-chaos-organizer/backend/app/services/errors.py +44 -0
  53. package/apps/travel-chaos-organizer/backend/app/services/notifier.py +10 -0
  54. package/apps/travel-chaos-organizer/backend/app/services/ollama.py +101 -0
  55. package/apps/travel-chaos-organizer/backend/app/services/parser.py +22 -0
  56. package/apps/travel-chaos-organizer/backend/app/services/quota.py +85 -0
  57. package/apps/travel-chaos-organizer/backend/app/services/sentry.py +11 -0
  58. package/apps/travel-chaos-organizer/backend/app/services/telemetry.py +35 -0
  59. package/apps/travel-chaos-organizer/backend/pytest.ini +4 -0
  60. package/apps/travel-chaos-organizer/backend/requirements-dev.txt +6 -0
  61. package/apps/travel-chaos-organizer/backend/requirements.txt +25 -0
  62. package/apps/travel-chaos-organizer/backend/tests/__init__.py +0 -0
  63. package/apps/travel-chaos-organizer/backend/tests/conftest.py +223 -0
  64. package/apps/travel-chaos-organizer/backend/tests/test_admin.py +99 -0
  65. package/apps/travel-chaos-organizer/backend/tests/test_cache.py +142 -0
  66. package/apps/travel-chaos-organizer/backend/tests/test_drip.py +32 -0
  67. package/apps/travel-chaos-organizer/backend/tests/test_e2e_chain.py +121 -0
  68. package/apps/travel-chaos-organizer/backend/tests/test_export.py +48 -0
  69. package/apps/travel-chaos-organizer/backend/tests/test_inbox.py +107 -0
  70. package/apps/travel-chaos-organizer/backend/tests/test_items.py +138 -0
  71. package/apps/travel-chaos-organizer/backend/tests/test_mail.py +113 -0
  72. package/apps/travel-chaos-organizer/backend/tests/test_ollama.py +145 -0
  73. package/apps/travel-chaos-organizer/backend/tests/test_parse_url.py +98 -0
  74. package/apps/travel-chaos-organizer/backend/tests/test_parser.py +72 -0
  75. package/apps/travel-chaos-organizer/backend/tests/test_payments.py +163 -0
  76. package/apps/travel-chaos-organizer/backend/tests/test_quota.py +46 -0
  77. package/apps/travel-chaos-organizer/backend/tests/test_search.py +67 -0
  78. package/apps/travel-chaos-organizer/backend/tests/test_trips.py +123 -0
  79. package/apps/travel-chaos-organizer/backend/tests/test_users.py +52 -0
  80. package/apps/travel-chaos-organizer/backups/.gitkeep +0 -0
  81. package/apps/travel-chaos-organizer/caddy/Caddyfile +27 -0
  82. package/apps/travel-chaos-organizer/docker-compose.node1.yml +31 -0
  83. package/apps/travel-chaos-organizer/docker-compose.override.yml +14 -0
  84. package/apps/travel-chaos-organizer/docker-compose.yml +128 -0
  85. package/apps/travel-chaos-organizer/frontend/.env.example +13 -0
  86. package/apps/travel-chaos-organizer/frontend/app/(app)/_layout.tsx +80 -0
  87. package/apps/travel-chaos-organizer/frontend/app/(app)/inbox.tsx +166 -0
  88. package/apps/travel-chaos-organizer/frontend/app/(app)/search.tsx +130 -0
  89. package/apps/travel-chaos-organizer/frontend/app/(app)/settings.tsx +381 -0
  90. package/apps/travel-chaos-organizer/frontend/app/(app)/trips/[id].tsx +346 -0
  91. package/apps/travel-chaos-organizer/frontend/app/(app)/trips.tsx +363 -0
  92. package/apps/travel-chaos-organizer/frontend/app/(app)/upgrade/cancel.tsx +34 -0
  93. package/apps/travel-chaos-organizer/frontend/app/(app)/upgrade/success.tsx +35 -0
  94. package/apps/travel-chaos-organizer/frontend/app/(auth)/login.tsx +91 -0
  95. package/apps/travel-chaos-organizer/frontend/app/_layout.tsx +71 -0
  96. package/apps/travel-chaos-organizer/frontend/app/auth.tsx +29 -0
  97. package/apps/travel-chaos-organizer/frontend/app/index.tsx +16 -0
  98. package/apps/travel-chaos-organizer/frontend/app/onboarding.tsx +119 -0
  99. package/apps/travel-chaos-organizer/frontend/app.config.ts +54 -0
  100. package/apps/travel-chaos-organizer/frontend/assets/adaptive-icon.svg +66 -0
  101. package/apps/travel-chaos-organizer/frontend/assets/icon.svg +79 -0
  102. package/apps/travel-chaos-organizer/frontend/assets/splash.svg +15 -0
  103. package/apps/travel-chaos-organizer/frontend/components/BottomSheet.tsx +102 -0
  104. package/apps/travel-chaos-organizer/frontend/components/ConfettiCannon.tsx +107 -0
  105. package/apps/travel-chaos-organizer/frontend/components/ConfirmDialog.tsx +109 -0
  106. package/apps/travel-chaos-organizer/frontend/components/ErrorBoundary.tsx +45 -0
  107. package/apps/travel-chaos-organizer/frontend/components/FileUploadButton.tsx +19 -0
  108. package/apps/travel-chaos-organizer/frontend/components/ImportSheet.tsx +155 -0
  109. package/apps/travel-chaos-organizer/frontend/components/OfflineBadge.tsx +55 -0
  110. package/apps/travel-chaos-organizer/frontend/components/QRCodeViewer.tsx +80 -0
  111. package/apps/travel-chaos-organizer/frontend/components/ScoreRing.tsx +39 -0
  112. package/apps/travel-chaos-organizer/frontend/components/Screen.tsx +39 -0
  113. package/apps/travel-chaos-organizer/frontend/components/Skeleton.tsx +70 -0
  114. package/apps/travel-chaos-organizer/frontend/components/SwipeableInboxItem.tsx +121 -0
  115. package/apps/travel-chaos-organizer/frontend/components/SwipeableTimelineItem.tsx +70 -0
  116. package/apps/travel-chaos-organizer/frontend/components/TimelineItem.tsx +73 -0
  117. package/apps/travel-chaos-organizer/frontend/components/TimelineItemDetail.tsx +130 -0
  118. package/apps/travel-chaos-organizer/frontend/components/Toast.tsx +60 -0
  119. package/apps/travel-chaos-organizer/frontend/components/ToastContext.tsx +42 -0
  120. package/apps/travel-chaos-organizer/frontend/components/TripCard.tsx +146 -0
  121. package/apps/travel-chaos-organizer/frontend/components/TripQRModal.tsx +70 -0
  122. package/apps/travel-chaos-organizer/frontend/components/UpgradeBanner.tsx +44 -0
  123. package/apps/travel-chaos-organizer/frontend/eas.json +22 -0
  124. package/apps/travel-chaos-organizer/frontend/hooks/useInbox.ts +24 -0
  125. package/apps/travel-chaos-organizer/frontend/hooks/useNetworkSync.ts +60 -0
  126. package/apps/travel-chaos-organizer/frontend/hooks/useTrips.ts +112 -0
  127. package/apps/travel-chaos-organizer/frontend/lib/api.ts +223 -0
  128. package/apps/travel-chaos-organizer/frontend/lib/auth.ts +93 -0
  129. package/apps/travel-chaos-organizer/frontend/lib/db.ts +86 -0
  130. package/apps/travel-chaos-organizer/frontend/lib/fileCache.ts +54 -0
  131. package/apps/travel-chaos-organizer/frontend/lib/haptics.ts +13 -0
  132. package/apps/travel-chaos-organizer/frontend/lib/notifications.ts +112 -0
  133. package/apps/travel-chaos-organizer/frontend/lib/offlineQueue.ts +145 -0
  134. package/apps/travel-chaos-organizer/frontend/lib/quota.tsx +43 -0
  135. package/apps/travel-chaos-organizer/frontend/lib/sentry.ts +28 -0
  136. package/apps/travel-chaos-organizer/frontend/lib/shareIntent.ts +44 -0
  137. package/apps/travel-chaos-organizer/frontend/package-lock.json +13221 -0
  138. package/apps/travel-chaos-organizer/frontend/package.json +44 -0
  139. package/apps/travel-chaos-organizer/frontend/tsconfig.json +23 -0
  140. package/apps/travel-chaos-organizer/keycloak/realm-export.json +59 -0
  141. package/apps/travel-chaos-organizer/landing/index.html +773 -0
  142. package/apps/travel-chaos-organizer/scripts/backup.sh +14 -0
  143. package/apps/travel-chaos-organizer/scripts/configure-env.sh +82 -0
  144. package/apps/travel-chaos-organizer/scripts/deploy.sh +54 -0
  145. package/apps/travel-chaos-organizer/scripts/health-check.sh +36 -0
  146. package/apps/travel-chaos-organizer/scripts/migrate.sh +7 -0
  147. package/apps/travel-chaos-organizer/scripts/server-setup.sh +85 -0
  148. package/apps/travel-chaos-organizer/scripts/smoke-test.sh +29 -0
  149. package/apps/travel-chaos-organizer/scripts/verify-tracking.sh +71 -0
  150. package/apps/travel-chaos-organizer/telegram-bot/.env.example +3 -0
  151. package/apps/travel-chaos-organizer/telegram-bot/Dockerfile +6 -0
  152. package/apps/travel-chaos-organizer/telegram-bot/bot.py +504 -0
  153. package/apps/travel-chaos-organizer/telegram-bot/requirements.txt +3 -0
  154. package/dist/packages/telegram-notify/client.d.ts +19 -0
  155. package/dist/packages/telegram-notify/client.js +75 -0
  156. package/dist/packages/telegram-notify/client.js.map +1 -0
  157. package/dist/src/__tests__/auth.test.js.map +1 -0
  158. package/dist/{__tests__ → src/__tests__}/brain-flow.test.js +141 -0
  159. package/dist/src/__tests__/brain-flow.test.js.map +1 -0
  160. package/dist/src/__tests__/confidence.test.js.map +1 -0
  161. package/dist/src/__tests__/e2e/api-auth.test.js.map +1 -0
  162. package/dist/src/__tests__/e2e/api-health.test.js.map +1 -0
  163. package/dist/src/__tests__/e2e/cli.test.js.map +1 -0
  164. package/dist/src/__tests__/e2e/config.js.map +1 -0
  165. package/dist/src/__tests__/e2e/funnel.test.js.map +1 -0
  166. package/dist/src/__tests__/e2e/mcp-tools.test.js.map +1 -0
  167. package/dist/src/__tests__/e2e/provisioning.test.js.map +1 -0
  168. package/dist/{__tests__ → src/__tests__}/handlers-unit.test.js +0 -2
  169. package/dist/src/__tests__/handlers-unit.test.js.map +1 -0
  170. package/dist/src/__tests__/namespace.test.js.map +1 -0
  171. package/dist/src/__tests__/rerank.test.js +115 -0
  172. package/dist/src/__tests__/rerank.test.js.map +1 -0
  173. package/dist/src/auth.js.map +1 -0
  174. package/dist/src/bench/cachly-bench.d.ts +35 -0
  175. package/dist/src/bench/cachly-bench.js +182 -0
  176. package/dist/src/bench/cachly-bench.js.map +1 -0
  177. package/dist/src/bench/fixtures.d.ts +37 -0
  178. package/dist/src/bench/fixtures.js +225 -0
  179. package/dist/src/bench/fixtures.js.map +1 -0
  180. package/dist/src/ckg.js.map +1 -0
  181. package/dist/src/confidence.js.map +1 -0
  182. package/dist/src/embeddings.js.map +1 -0
  183. package/dist/{handlers → src/handlers}/advanced.d.ts +1 -1
  184. package/dist/{handlers → src/handlers}/advanced.js +1 -2
  185. package/dist/src/handlers/advanced.js.map +1 -0
  186. package/dist/{handlers → src/handlers}/brain.js +361 -191
  187. package/dist/src/handlers/brain.js.map +1 -0
  188. package/dist/src/handlers/cache.js.map +1 -0
  189. package/dist/{handlers → src/handlers}/context.js +9 -4
  190. package/dist/src/handlers/context.js.map +1 -0
  191. package/dist/{handlers → src/handlers}/fedbrain.d.ts +6 -0
  192. package/dist/{handlers → src/handlers}/fedbrain.js +19 -7
  193. package/dist/src/handlers/fedbrain.js.map +1 -0
  194. package/dist/src/handlers/instances.js.map +1 -0
  195. package/dist/{handlers → src/handlers}/roadmap.d.ts +1 -1
  196. package/dist/{handlers → src/handlers}/roadmap.js +8 -4
  197. package/dist/src/handlers/roadmap.js.map +1 -0
  198. package/dist/src/handlers/syndicate.js.map +1 -0
  199. package/dist/src/handlers/tco.d.ts +16 -0
  200. package/dist/src/handlers/tco.js +303 -0
  201. package/dist/src/handlers/tco.js.map +1 -0
  202. package/dist/{handlers → src/handlers}/team.js +58 -7
  203. package/dist/src/handlers/team.js.map +1 -0
  204. package/dist/{index.js → src/index.js} +207 -429
  205. package/dist/src/index.js.map +1 -0
  206. package/dist/src/namespace.js.map +1 -0
  207. package/dist/src/notifier.d.ts +5 -0
  208. package/dist/src/notifier.js +6 -0
  209. package/dist/src/notifier.js.map +1 -0
  210. package/dist/src/rerank.d.ts +55 -0
  211. package/dist/src/rerank.js +111 -0
  212. package/dist/src/rerank.js.map +1 -0
  213. package/dist/{search.js → src/search.js} +2 -2
  214. package/dist/src/search.js.map +1 -0
  215. package/dist/src/search.test.d.ts +1 -0
  216. package/dist/{search.test.js → src/search.test.js} +1 -1
  217. package/dist/src/search.test.js.map +1 -0
  218. package/dist/{tools.d.ts → src/tools.d.ts} +226 -0
  219. package/dist/{tools.js → src/tools.js} +196 -0
  220. package/dist/src/tools.js.map +1 -0
  221. package/dist/src/utils.js.map +1 -0
  222. package/eslint.config.js +1 -1
  223. package/package.json +8 -7
  224. package/packages/telegram-notify/SPLIT_PLAN.md +106 -0
  225. package/packages/telegram-notify/client.ts +88 -0
  226. package/packages/telegram-notify/emojis.json +22 -0
  227. package/packages/telegram-notify/pyproject.toml +17 -0
  228. package/packages/telegram-notify/telegram_notify/__init__.py +3 -0
  229. package/packages/telegram-notify/telegram_notify/client.py +87 -0
  230. package/packages/telegram-notify/telegram_notify/emojis.json +22 -0
  231. package/scripts/generate-icons.js +28 -0
  232. package/server.json +3 -3
  233. package/smithery.yaml +27 -16
  234. package/src/__tests__/auth.test.ts +1 -1
  235. package/src/__tests__/brain-flow.test.ts +175 -1
  236. package/src/__tests__/e2e/api-health.test.ts +1 -1
  237. package/src/__tests__/handlers-unit.test.ts +1 -2
  238. package/src/__tests__/rerank.test.ts +130 -0
  239. package/src/bench/cachly-bench.ts +217 -0
  240. package/src/bench/fixtures.ts +256 -0
  241. package/src/handlers/advanced.ts +2 -6
  242. package/src/handlers/brain.ts +371 -194
  243. package/src/handlers/cache.ts +1 -2
  244. package/src/handlers/context.ts +10 -6
  245. package/src/handlers/fedbrain.ts +21 -12
  246. package/src/handlers/roadmap.ts +8 -4
  247. package/src/handlers/syndicate.ts +7 -4
  248. package/src/handlers/tco.ts +345 -0
  249. package/src/handlers/team.ts +70 -10
  250. package/src/index.ts +193 -419
  251. package/src/notifier.ts +5 -0
  252. package/src/rerank.ts +148 -0
  253. package/src/search.test.ts +1 -1
  254. package/src/search.ts +2 -2
  255. package/src/tools.ts +210 -0
  256. package/tsconfig.json +2 -3
  257. package/vitest.config.ts +2 -0
  258. package/dist/__tests__/auth.test.js.map +0 -1
  259. package/dist/__tests__/brain-flow.test.js.map +0 -1
  260. package/dist/__tests__/brain-predict-failures.test.d.ts +0 -17
  261. package/dist/__tests__/brain-predict-failures.test.js +0 -339
  262. package/dist/__tests__/brain-predict-failures.test.js.map +0 -1
  263. package/dist/__tests__/confidence.test.js.map +0 -1
  264. package/dist/__tests__/e2e/api-auth.test.js.map +0 -1
  265. package/dist/__tests__/e2e/api-health.test.js.map +0 -1
  266. package/dist/__tests__/e2e/cli.test.js.map +0 -1
  267. package/dist/__tests__/e2e/config.js.map +0 -1
  268. package/dist/__tests__/e2e/funnel.test.js.map +0 -1
  269. package/dist/__tests__/e2e/mcp-tools.test.js.map +0 -1
  270. package/dist/__tests__/e2e/provisioning.test.js.map +0 -1
  271. package/dist/__tests__/handlers-unit.test.js.map +0 -1
  272. package/dist/__tests__/namespace.test.js.map +0 -1
  273. package/dist/auth.js.map +0 -1
  274. package/dist/ckg.js.map +0 -1
  275. package/dist/confidence.js.map +0 -1
  276. package/dist/embeddings.js.map +0 -1
  277. package/dist/handlers/advanced.js.map +0 -1
  278. package/dist/handlers/brain.js.map +0 -1
  279. package/dist/handlers/cache.js.map +0 -1
  280. package/dist/handlers/context.js.map +0 -1
  281. package/dist/handlers/fedbrain.js.map +0 -1
  282. package/dist/handlers/instances.js.map +0 -1
  283. package/dist/handlers/roadmap.js.map +0 -1
  284. package/dist/handlers/syndicate.js.map +0 -1
  285. package/dist/handlers/team.js.map +0 -1
  286. package/dist/index.js.map +0 -1
  287. package/dist/namespace.js.map +0 -1
  288. package/dist/search.js.map +0 -1
  289. package/dist/search.test.js.map +0 -1
  290. package/dist/tools.js.map +0 -1
  291. package/dist/utils.js.map +0 -1
  292. package/src/__tests__/brain-predict-failures.test.ts +0 -430
  293. /package/dist/{__tests__ → src/__tests__}/auth.test.d.ts +0 -0
  294. /package/dist/{__tests__ → src/__tests__}/auth.test.js +0 -0
  295. /package/dist/{__tests__ → src/__tests__}/brain-flow.test.d.ts +0 -0
  296. /package/dist/{__tests__ → src/__tests__}/confidence.test.d.ts +0 -0
  297. /package/dist/{__tests__ → src/__tests__}/confidence.test.js +0 -0
  298. /package/dist/{__tests__ → src/__tests__}/e2e/api-auth.test.d.ts +0 -0
  299. /package/dist/{__tests__ → src/__tests__}/e2e/api-auth.test.js +0 -0
  300. /package/dist/{__tests__ → src/__tests__}/e2e/api-health.test.d.ts +0 -0
  301. /package/dist/{__tests__ → src/__tests__}/e2e/api-health.test.js +0 -0
  302. /package/dist/{__tests__ → src/__tests__}/e2e/cli.test.d.ts +0 -0
  303. /package/dist/{__tests__ → src/__tests__}/e2e/cli.test.js +0 -0
  304. /package/dist/{__tests__ → src/__tests__}/e2e/config.d.ts +0 -0
  305. /package/dist/{__tests__ → src/__tests__}/e2e/config.js +0 -0
  306. /package/dist/{__tests__ → src/__tests__}/e2e/funnel.test.d.ts +0 -0
  307. /package/dist/{__tests__ → src/__tests__}/e2e/funnel.test.js +0 -0
  308. /package/dist/{__tests__ → src/__tests__}/e2e/mcp-tools.test.d.ts +0 -0
  309. /package/dist/{__tests__ → src/__tests__}/e2e/mcp-tools.test.js +0 -0
  310. /package/dist/{__tests__ → src/__tests__}/e2e/provisioning.test.d.ts +0 -0
  311. /package/dist/{__tests__ → src/__tests__}/e2e/provisioning.test.js +0 -0
  312. /package/dist/{__tests__ → src/__tests__}/handlers-unit.test.d.ts +0 -0
  313. /package/dist/{__tests__ → src/__tests__}/namespace.test.d.ts +0 -0
  314. /package/dist/{__tests__ → src/__tests__}/namespace.test.js +0 -0
  315. /package/dist/{search.test.d.ts → src/__tests__/rerank.test.d.ts} +0 -0
  316. /package/dist/{auth.d.ts → src/auth.d.ts} +0 -0
  317. /package/dist/{auth.js → src/auth.js} +0 -0
  318. /package/dist/{ckg.d.ts → src/ckg.d.ts} +0 -0
  319. /package/dist/{ckg.js → src/ckg.js} +0 -0
  320. /package/dist/{confidence.d.ts → src/confidence.d.ts} +0 -0
  321. /package/dist/{confidence.js → src/confidence.js} +0 -0
  322. /package/dist/{embeddings.d.ts → src/embeddings.d.ts} +0 -0
  323. /package/dist/{embeddings.js → src/embeddings.js} +0 -0
  324. /package/dist/{handlers → src/handlers}/brain.d.ts +0 -0
  325. /package/dist/{handlers → src/handlers}/cache.d.ts +0 -0
  326. /package/dist/{handlers → src/handlers}/cache.js +0 -0
  327. /package/dist/{handlers → src/handlers}/context.d.ts +0 -0
  328. /package/dist/{handlers → src/handlers}/instances.d.ts +0 -0
  329. /package/dist/{handlers → src/handlers}/instances.js +0 -0
  330. /package/dist/{handlers → src/handlers}/syndicate.d.ts +0 -0
  331. /package/dist/{handlers → src/handlers}/syndicate.js +0 -0
  332. /package/dist/{handlers → src/handlers}/team.d.ts +0 -0
  333. /package/dist/{index.d.ts → src/index.d.ts} +0 -0
  334. /package/dist/{namespace.d.ts → src/namespace.d.ts} +0 -0
  335. /package/dist/{namespace.js → src/namespace.js} +0 -0
  336. /package/dist/{search.d.ts → src/search.d.ts} +0 -0
  337. /package/dist/{utils.d.ts → src/utils.d.ts} +0 -0
  338. /package/dist/{utils.js → src/utils.js} +0 -0
@@ -0,0 +1,87 @@
1
+ """
2
+ telegram-notify — shared Telegram notification client.
3
+
4
+ Used by TCO backend (Python) and importable as a pip local package.
5
+ Single source of truth for: emoji map, message format, env var convention.
6
+
7
+ Interface: notify(app, event, payload)
8
+ notify_user(db, user_id, message) — direct DM via stored chat_id
9
+
10
+ Env vars:
11
+ TELEGRAM_BOT_TOKEN — bot token from @BotFather
12
+ {APP_UPPER}_TELEGRAM_CHAT_ID — app-specific channel (e.g. TCO_TELEGRAM_CHAT_ID)
13
+ TELEGRAM_CHAT_ID — fallback for all apps
14
+
15
+ No-op when unconfigured. Never throws.
16
+ """
17
+ from __future__ import annotations
18
+
19
+ import json
20
+ import os
21
+ from pathlib import Path
22
+ from typing import Any
23
+
24
+ _EMOJI: dict[str, str] = json.loads(
25
+ (Path(__file__).parent / "emojis.json").read_text()
26
+ )
27
+
28
+
29
+ def _token() -> str:
30
+ return os.getenv("TELEGRAM_BOT_TOKEN", "")
31
+
32
+
33
+ def _chat_id(app: str) -> str:
34
+ key = f"{app.upper()}_TELEGRAM_CHAT_ID"
35
+ return os.getenv(key) or os.getenv("TELEGRAM_CHAT_ID", "")
36
+
37
+
38
+ def format_message(app: str, event: str, payload: dict[str, Any]) -> str:
39
+ icon = _EMOJI.get(event, _EMOJI["default"])
40
+ lines = [f"{icon} *{app.upper()}* · `{event}`"]
41
+ for k, v in payload.items():
42
+ if v is not None and v != "":
43
+ lines.append(f" {k}: `{v}`")
44
+ return "\n".join(lines)
45
+
46
+
47
+ async def notify(app: str, event: str, payload: dict[str, Any] | None = None) -> None:
48
+ """Send event notification to the app's configured Telegram channel."""
49
+ token = _token()
50
+ chat_id = _chat_id(app)
51
+ if not token or not chat_id:
52
+ return
53
+ try:
54
+ import httpx
55
+ text = format_message(app, event, payload or {})
56
+ async with httpx.AsyncClient(timeout=5) as client:
57
+ await client.post(
58
+ f"https://api.telegram.org/bot{token}/sendMessage",
59
+ json={"chat_id": chat_id, "text": text, "parse_mode": "Markdown"},
60
+ )
61
+ except Exception:
62
+ pass # never raises
63
+
64
+
65
+ async def notify_user(db: Any, user_id: str, message: str) -> None:
66
+ """Send a direct Telegram message to a user by their DB user_id.
67
+ Looks up telegram_chat_id from the users table. No-op if not linked."""
68
+ token = _token()
69
+ if not token:
70
+ return
71
+ try:
72
+ from sqlalchemy import text as sql_text
73
+ result = await db.execute(
74
+ sql_text("SELECT telegram_chat_id FROM users WHERE id = :uid"),
75
+ {"uid": user_id},
76
+ )
77
+ row = result.fetchone()
78
+ if not row or not row[0]:
79
+ return
80
+ import httpx
81
+ async with httpx.AsyncClient(timeout=5) as client:
82
+ await client.post(
83
+ f"https://api.telegram.org/bot{token}/sendMessage",
84
+ json={"chat_id": row[0], "text": message, "parse_mode": "Markdown"},
85
+ )
86
+ except Exception:
87
+ pass # never raises
@@ -0,0 +1,22 @@
1
+ {
2
+ "tool_call": "🔧",
3
+ "tool_error": "🔴",
4
+ "cache_hit": "⚡",
5
+ "cache_miss": "💭",
6
+ "auth_ok": "🔑",
7
+ "auth_fail": "🚫",
8
+ "startup": "🚀",
9
+ "waitlist_signup": "📬",
10
+ "parse_file": "📄",
11
+ "parse_text": "📝",
12
+ "parse_url": "🔗",
13
+ "trip_created": "🗺️",
14
+ "trip_deleted": "🗑️",
15
+ "error": "🔴",
16
+ "stripe_payment": "💳",
17
+ "stripe_checkout": "🛒",
18
+ "new_user": "🆕",
19
+ "pdf_export": "📑",
20
+ "trip_search": "🔍",
21
+ "default": "🔔"
22
+ }
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Converts SVG icon assets to PNG for Expo.
4
+ * Run: node scripts/generate-icons.js
5
+ * Requires: npm install -g sharp-cli OR npx sharp-cli
6
+ */
7
+ const { execSync } = require('child_process');
8
+ const path = require('path');
9
+
10
+ const frontend = path.join(__dirname, '..', 'apps', 'travel-chaos-organizer', 'frontend');
11
+
12
+ const conversions = [
13
+ { input: 'assets/icon.svg', output: 'assets/icon.png', width: 1024, height: 1024 },
14
+ { input: 'assets/adaptive-icon.svg', output: 'assets/adaptive-icon.png', width: 1024, height: 1024 },
15
+ { input: 'assets/icon.svg', output: 'assets/splash.png', width: 1284, height: 2778 },
16
+ { input: 'assets/icon.svg', output: 'assets/favicon.png', width: 32, height: 32 },
17
+ ];
18
+
19
+ for (const { input, output, width, height } of conversions) {
20
+ const src = path.join(frontend, input);
21
+ const dst = path.join(frontend, output);
22
+ try {
23
+ execSync(`npx sharp-cli --input "${src}" --output "${dst}" resize ${width} ${height}`, { stdio: 'inherit' });
24
+ console.log(`✓ ${output}`);
25
+ } catch {
26
+ console.error(`✗ ${output} — install sharp-cli: npm i -g sharp-cli`);
27
+ }
28
+ }
package/server.json CHANGED
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
3
  "name": "io.github.cachly-dev/cachly-mcp",
4
- "description": "Your AI forgets everything between sessions. cachly fixes that permanently. A causal brain that learns WHY things break — built from every bug fix, git commit, and CI run, without any explicit calls. Arrives pre-briefed each session. Predicts failures before they happen. Never re-researches the same problem. 89 MCP tools · Free tier · EU servers · GDPR.",
4
+ "description": "Your AI forgets everything between sessions. cachly fixes that permanently. A causal brain that learns WHY things break — built from every bug fix, git commit, and CI run, without any explicit calls. Arrives pre-briefed each session. Predicts failures before they happen. Never re-researches the same problem. 95 MCP tools · Free tier · EU servers · GDPR.",
5
5
  "repository": {
6
6
  "url": "https://github.com/cachly-dev/cachly-mcp",
7
7
  "source": "github"
8
8
  },
9
- "version": "0.10.23",
9
+ "version": "0.10.58",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "name": "@cachly-dev/mcp-server",
14
14
  "identifier": "@cachly-dev/mcp-server",
15
- "version": "0.10.23",
15
+ "version": "0.10.58",
16
16
  "transport": {
17
17
  "type": "stdio"
18
18
  }
package/smithery.yaml CHANGED
@@ -1,27 +1,21 @@
1
1
  name: cachly AI Brain
2
2
  description: >
3
- Your AI forgets everything between sessions. cachly fixes that permanently.
4
- One command, 30 seconds, free forever — your AI arrives pre-briefed from your git history.
5
- brain_from_git: learns 500 commits in 30s — no setup needed.
6
- causal_trace: root-cause any error by tracing what caused what in your project history.
7
- brain_predict: predicts likely failures before you make a change.
8
- team_learn: share what you fixed — teammates' AI knows it immediately.
9
- session_start: every conversation opens with a full briefing, not a blank slate.
10
- 80 tools. Claude Code, Cursor, Copilot, Windsurf, Cline & Zed. GDPR. EU servers. Free tier forever.
3
+ Persistent AI memory for Claude Code, Cursor, Copilot, Windsurf, Cline & Zed.
4
+ Your AI remembers every lesson, fix, and architecture decision — forever.
5
+ Features: causal root-cause analysis (causal_trace), failure prediction (brain_predict),
6
+ git-ambient learning (brain_from_git), team knowledge sharing, Memory Crystals,
7
+ BM25+ search in 11 languages. 80 MCP tools. Free tier. GDPR. German servers.
8
+ Zero-config: sign in once via browser, auto-provisions your Brain instance.
11
9
 
12
- # Streamable HTTP — hosted on our infrastructure (mcp.cachly.dev).
13
- # Multi-tenant: per-request CACHLY_JWT + instance ID resolved from
14
- # Authorization header + X-Cachly-Instance-Id, or Smithery ?config= param.
15
10
  startCommand:
16
- type: http
17
- url: https://mcp.cachly.dev/mcp
11
+ type: stdio
18
12
  configSchema:
19
13
  type: object
20
14
  properties:
21
15
  CACHLY_JWT:
22
16
  type: string
23
- title: "API Key"
24
- description: "Get your free API key at https://cachly.dev/setup-ai (no credit card)."
17
+ title: "API Key (optional — sign in via browser on first use)"
18
+ description: "Leave blank for zero-config: on your first tool call the server opens a browser sign-in (OAuth Device Flow) and auto-provisions your Brain instance. Or pre-fill with your API key from cachly.dev/setup-ai."
25
19
  examples:
26
20
  - "cky_live_abc123..."
27
21
  CACHLY_BRAIN_INSTANCE_ID:
@@ -31,4 +25,21 @@ startCommand:
31
25
  pattern: "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
32
26
  examples:
33
27
  - "9d4077aa-bfa2-468b-89cd-0a8d8f3ec483"
34
-
28
+ CACHLY_API_URL:
29
+ type: string
30
+ title: "API URL (optional)"
31
+ description: "Leave blank unless you self-host Cachly."
32
+ default: "https://api.cachly.dev"
33
+ required: []
34
+ commandFunction: |-
35
+ (config) => ({
36
+ command: "npx",
37
+ args: ["-y", "@cachly-dev/mcp-server@latest"],
38
+ env: Object.fromEntries(
39
+ Object.entries({
40
+ CACHLY_JWT: config.CACHLY_JWT,
41
+ CACHLY_BRAIN_INSTANCE_ID: config.CACHLY_BRAIN_INSTANCE_ID,
42
+ CACHLY_API_URL: config.CACHLY_API_URL ?? "https://api.cachly.dev",
43
+ }).filter(([, v]) => v != null && v !== "")
44
+ )
45
+ })
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect } from 'vitest';
2
- import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
2
+ import { ErrorCode } from '@modelcontextprotocol/sdk/types.js';
3
3
  import { jwtExpiryMs, checkJwt, handleApiError } from '../auth.js';
4
4
 
5
5
  function makeJwt(payload: Record<string, unknown>): string {
@@ -10,13 +10,14 @@
10
10
  * Run: npx vitest run src/__tests__/brain-flow.test.ts
11
11
  */
12
12
 
13
- import { describe, it, expect, beforeEach, vi } from 'vitest';
13
+ import { describe, it, expect, beforeEach } from 'vitest';
14
14
  import { EventEmitter } from 'node:events';
15
15
 
16
16
  // ── Modules under test ────────────────────────────────────────────────────────
17
17
  import { ckgSlug, extractProblemConcept, ckgUpsertNode, ckgUpdateEdge } from '../ckg.js';
18
18
  import { EMBED_PROVIDER, hasEmbedProvider, embedProviderHint, embedConfig, setEmbedJwt } from '../embeddings.js';
19
19
  import { keywordSearch } from '../search.js';
20
+ import { handleBrainTool } from '../handlers/brain.js';
20
21
  import type { Redis } from 'ioredis';
21
22
 
22
23
  // ── In-memory Redis mock ──────────────────────────────────────────────────────
@@ -67,6 +68,21 @@ class MockRedis {
67
68
  return [...(this.sets.get(key) ?? [])];
68
69
  }
69
70
 
71
+ async mget(...keys: string[]): Promise<(string | null)[]> {
72
+ return keys.map(k => this.store.get(k) ?? null);
73
+ }
74
+
75
+ async exists(key: string): Promise<number> {
76
+ return this.store.has(key) ? 1 : 0;
77
+ }
78
+
79
+ async incrbyfloat(key: string, increment: number): Promise<string> {
80
+ const cur = parseFloat(this.store.get(key) ?? '0');
81
+ const next = cur + increment;
82
+ this.store.set(key, String(next));
83
+ return String(next);
84
+ }
85
+
70
86
  /** Simplified scanStream: returns all matching keys in a single 'data' event. */
71
87
  scanStream(opts: { match: string; count?: number }): EventEmitter {
72
88
  const emitter = new EventEmitter();
@@ -872,3 +888,161 @@ describe('Brain Flow: advanced scenarios', () => {
872
888
  });
873
889
  });
874
890
  });
891
+
892
+ // ── New feature coverage (0.10.41 / 0.10.42) ─────────────────────────────────
893
+ // Trust badges, Proven Laws crystallization, and focus-less predictive warning.
894
+ // These exercise the REAL handleBrainTool code path with the MockRedis.
895
+
896
+ const noopApiFetch = (async () => null) as unknown as Parameters<typeof handleBrainTool>[3];
897
+
898
+ /** Seed a best-solution lesson with an explicit recall_count / authors / pinned. */
899
+ async function seedLesson(
900
+ redis: MockRedis,
901
+ topic: string,
902
+ opts: {
903
+ outcome?: 'success' | 'failure' | 'partial';
904
+ what_worked?: string;
905
+ what_failed?: string;
906
+ recall_count?: number;
907
+ severity?: 'critical' | 'major' | 'minor';
908
+ pinned?: boolean;
909
+ author?: string;
910
+ tags?: string[];
911
+ } = {},
912
+ ): Promise<void> {
913
+ const ts = new Date().toISOString();
914
+ const obj = {
915
+ topic,
916
+ outcome: opts.outcome ?? 'success',
917
+ what_worked: opts.what_worked ?? `solution for ${topic}`,
918
+ what_failed: opts.what_failed,
919
+ severity: opts.severity ?? 'minor',
920
+ recall_count: opts.recall_count ?? 0,
921
+ ts,
922
+ verified_at: ts,
923
+ confidence: 1.0,
924
+ tags: opts.tags ?? [],
925
+ ...(opts.pinned ? { pinned: true } : {}),
926
+ ...(opts.author ? { author: opts.author } : {}),
927
+ };
928
+ await redis.set(`cachly:lesson:best:${topic}`, JSON.stringify(obj));
929
+ }
930
+
931
+ describe('Trust badges (recall_best_solution)', () => {
932
+ let redis: MockRedis;
933
+ const getConn = async () => redis as unknown as Redis;
934
+ beforeEach(() => { redis = new MockRedis(); });
935
+
936
+ it('shows "Battle-tested" when recall_count crosses 10', async () => {
937
+ await seedLesson(redis, 'redis:pool', { recall_count: 9, what_worked: 'set max_retries=3' });
938
+ const out = (await handleBrainTool('recall_best_solution', { instance_id: 'i1', topic: 'redis:pool' }, getConn, noopApiFetch))!;
939
+ expect(out).toContain('Battle-tested');
940
+ expect(out).toContain('recalled 10×');
941
+ });
942
+
943
+ it('shows "Proven" when recall_count is between 5 and 9', async () => {
944
+ await seedLesson(redis, 'api:auth', { recall_count: 4 });
945
+ const out = (await handleBrainTool('recall_best_solution', { instance_id: 'i1', topic: 'api:auth' }, getConn, noopApiFetch))!;
946
+ expect(out).toContain('Proven');
947
+ expect(out).toContain('recalled 5×');
948
+ expect(out).not.toContain('Battle-tested');
949
+ });
950
+
951
+ it('shows no trust badge for a fresh low-recall lesson', async () => {
952
+ await seedLesson(redis, 'misc:thing', { recall_count: 0 });
953
+ const out = (await handleBrainTool('recall_best_solution', { instance_id: 'i1', topic: 'misc:thing' }, getConn, noopApiFetch))!;
954
+ expect(out).not.toContain('Battle-tested');
955
+ expect(out).not.toContain('Proven —');
956
+ expect(out).not.toContain('Team-verified');
957
+ });
958
+
959
+ it('increments recall_count and persists it', async () => {
960
+ await seedLesson(redis, 'deploy:x', { recall_count: 2 });
961
+ await handleBrainTool('recall_best_solution', { instance_id: 'i1', topic: 'deploy:x' }, getConn, noopApiFetch);
962
+ const stored = JSON.parse((await redis.get('cachly:lesson:best:deploy:x'))!);
963
+ expect(stored.recall_count).toBe(3);
964
+ });
965
+ });
966
+
967
+ describe('Proven Laws crystallization (session_start)', () => {
968
+ let redis: MockRedis;
969
+ const getConn = async () => redis as unknown as Redis;
970
+ beforeEach(async () => {
971
+ redis = new MockRedis();
972
+ // Provide a last session so session_start does not attempt git reconstruction.
973
+ await redis.set('cachly:session:last', JSON.stringify({ summary: 'prior work', ts: new Date().toISOString(), files_changed: [] }));
974
+ });
975
+
976
+ it('surfaces lessons recalled >= 5 in the Proven Laws section', async () => {
977
+ await seedLesson(redis, 'infra:k3s', { recall_count: 7, what_worked: 'add WireGuard IP to TLS-SAN' });
978
+ await seedLesson(redis, 'random:note', { recall_count: 1 });
979
+ const out = (await handleBrainTool('session_start', { instance_id: 'i1' }, getConn, noopApiFetch))!;
980
+ expect(out).toContain('Proven Laws');
981
+ expect(out).toContain('infra:k3s');
982
+ expect(out).toContain('recalled 7×');
983
+ });
984
+
985
+ it('surfaces an explicitly pinned lesson even with low recall', async () => {
986
+ await seedLesson(redis, 'team:convention', { recall_count: 0, pinned: true, what_worked: 'always snake_case handlers' });
987
+ const out = (await handleBrainTool('session_start', { instance_id: 'i1' }, getConn, noopApiFetch))!;
988
+ expect(out).toContain('Proven Laws');
989
+ expect(out).toContain('team:convention');
990
+ expect(out).toContain('pinned');
991
+ });
992
+
993
+ it('omits the Proven Laws section when nothing qualifies', async () => {
994
+ await seedLesson(redis, 'small:thing', { recall_count: 1 });
995
+ const out = (await handleBrainTool('session_start', { instance_id: 'i1' }, getConn, noopApiFetch))!;
996
+ expect(out).not.toContain('Proven Laws');
997
+ });
998
+
999
+ it('does not crystallize failure lessons', async () => {
1000
+ await seedLesson(redis, 'broken:flow', { recall_count: 9, outcome: 'failure', what_failed: 'never works' });
1001
+ const out = (await handleBrainTool('session_start', { instance_id: 'i1' }, getConn, noopApiFetch))!;
1002
+ expect(out).not.toContain('Proven Laws');
1003
+ });
1004
+ });
1005
+
1006
+ describe('Predictive pre-warning without explicit focus (session_start)', () => {
1007
+ let redis: MockRedis;
1008
+ const getConn = async () => redis as unknown as Redis;
1009
+ beforeEach(() => { redis = new MockRedis(); });
1010
+
1011
+ it('derives danger area from last session files and warns', async () => {
1012
+ // Last session touched payment files → likely to continue there.
1013
+ await redis.set('cachly:session:last', JSON.stringify({
1014
+ summary: 'worked on payment integration',
1015
+ ts: new Date().toISOString(),
1016
+ files_changed: ['api/payment/stripe_webhook.go'],
1017
+ }));
1018
+ // A known failure in the payment area.
1019
+ await seedLesson(redis, 'payment:webhook', { outcome: 'failure', what_failed: 'missed idempotency key → double charge', tags: ['payment'] });
1020
+ const out = (await handleBrainTool('session_start', { instance_id: 'i1' }, getConn, noopApiFetch))!;
1021
+ expect(out).toContain('PRE-WARNING');
1022
+ expect(out).toContain('payment:webhook');
1023
+ });
1024
+
1025
+ it('does not warn when last-session area has no known failures', async () => {
1026
+ await redis.set('cachly:session:last', JSON.stringify({
1027
+ summary: 'wrote docs',
1028
+ ts: new Date().toISOString(),
1029
+ files_changed: ['docs/readme.md'],
1030
+ }));
1031
+ await seedLesson(redis, 'payment:webhook', { outcome: 'failure', what_failed: 'x', tags: ['payment'] });
1032
+ const out = (await handleBrainTool('session_start', { instance_id: 'i1' }, getConn, noopApiFetch))!;
1033
+ expect(out).not.toContain('PRE-WARNING');
1034
+ });
1035
+
1036
+ it('filters path noise so common segments do not trigger false matches', async () => {
1037
+ // Last session only touched generic noise paths (src/lib/index).
1038
+ await redis.set('cachly:session:last', JSON.stringify({
1039
+ summary: 'refactor',
1040
+ ts: new Date().toISOString(),
1041
+ files_changed: ['src/lib/index.ts'],
1042
+ }));
1043
+ // A failure tagged 'src' — should NOT match because 'src' is noise.
1044
+ await seedLesson(redis, 'src:thing', { outcome: 'failure', what_failed: 'noise', tags: ['src'] });
1045
+ const out = (await handleBrainTool('session_start', { instance_id: 'i1' }, getConn, noopApiFetch))!;
1046
+ expect(out).not.toContain('PRE-WARNING');
1047
+ });
1048
+ });
@@ -5,7 +5,7 @@
5
5
  * Run: E2E_API_URL=https://api.dev.cachly.dev npx vitest run src/__tests__/e2e/api-health.test.ts
6
6
  */
7
7
 
8
- import { describe, it, expect, beforeAll } from 'vitest';
8
+ import { describe, it, expect } from 'vitest';
9
9
  import { cfg } from './config.js';
10
10
 
11
11
  describe('API health (no auth required)', () => {
@@ -334,8 +334,7 @@ describe('handleRoadmapTool', () => {
334
334
  // Add and complete one item
335
335
  const addResult = await handleRoadmapTool('roadmap_add', { instance_id: INSTANCE, title: 'Finished task', priority: 'low' }, getConnection, apiFetch);
336
336
  const idMatch = addResult!.match(/`(rm_[a-z0-9_]+)`/);
337
- if (!idMatch) throw new Error('expected roadmap id in add result');
338
- const id = idMatch[1];
337
+ const id = idMatch![1];
339
338
  await handleRoadmapTool('roadmap_update', { instance_id: INSTANCE, id, status: 'done' }, getConnection, apiFetch);
340
339
 
341
340
  const result = await handleRoadmapTool('roadmap_list', { instance_id: INSTANCE, status: 'done' }, getConnection, apiFetch);
@@ -0,0 +1,130 @@
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
+ });