@cachly-dev/mcp-server 0.10.48 → 0.10.50

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 (325) hide show
  1. package/.github/workflows/eas-build-android.yml +58 -0
  2. package/.github/workflows/eas-update.yml +55 -0
  3. package/BENCH.md +80 -0
  4. package/README.md +5 -33
  5. package/STRATEGY.md +218 -0
  6. package/apps/travel-chaos-organizer/.env.example +49 -0
  7. package/apps/travel-chaos-organizer/DEPLOY.md +328 -0
  8. package/apps/travel-chaos-organizer/Makefile +57 -0
  9. package/apps/travel-chaos-organizer/README.md +159 -0
  10. package/apps/travel-chaos-organizer/backend/.env.example +10 -0
  11. package/apps/travel-chaos-organizer/backend/Dockerfile +25 -0
  12. package/apps/travel-chaos-organizer/backend/alembic/env.py +56 -0
  13. package/apps/travel-chaos-organizer/backend/alembic/script.py.mako +24 -0
  14. package/apps/travel-chaos-organizer/backend/alembic/versions/.gitkeep +0 -0
  15. package/apps/travel-chaos-organizer/backend/alembic/versions/0001_stripe_customer_id.py +24 -0
  16. package/apps/travel-chaos-organizer/backend/alembic/versions/0002_telegram_chat_id.py +24 -0
  17. package/apps/travel-chaos-organizer/backend/alembic.ini +39 -0
  18. package/apps/travel-chaos-organizer/backend/app/__init__.py +0 -0
  19. package/apps/travel-chaos-organizer/backend/app/auth/__init__.py +0 -0
  20. package/apps/travel-chaos-organizer/backend/app/auth/keycloak.py +81 -0
  21. package/apps/travel-chaos-organizer/backend/app/config.py +66 -0
  22. package/apps/travel-chaos-organizer/backend/app/db/__init__.py +0 -0
  23. package/apps/travel-chaos-organizer/backend/app/db/database.py +20 -0
  24. package/apps/travel-chaos-organizer/backend/app/db/migrations/001_init.sql +71 -0
  25. package/apps/travel-chaos-organizer/backend/app/db/migrations/002_telemetry_freemium.sql +31 -0
  26. package/apps/travel-chaos-organizer/backend/app/db/migrations/003_telegram_chat_id.sql +4 -0
  27. package/apps/travel-chaos-organizer/backend/app/db/migrations/004_stripe_and_email.sql +5 -0
  28. package/apps/travel-chaos-organizer/backend/app/limiter.py +4 -0
  29. package/apps/travel-chaos-organizer/backend/app/logging_config.py +25 -0
  30. package/apps/travel-chaos-organizer/backend/app/main.py +77 -0
  31. package/apps/travel-chaos-organizer/backend/app/models/__init__.py +0 -0
  32. package/apps/travel-chaos-organizer/backend/app/models/schemas.py +142 -0
  33. package/apps/travel-chaos-organizer/backend/app/routers/__init__.py +0 -0
  34. package/apps/travel-chaos-organizer/backend/app/routers/admin.py +237 -0
  35. package/apps/travel-chaos-organizer/backend/app/routers/cache.py +13 -0
  36. package/apps/travel-chaos-organizer/backend/app/routers/events.py +29 -0
  37. package/apps/travel-chaos-organizer/backend/app/routers/export.py +122 -0
  38. package/apps/travel-chaos-organizer/backend/app/routers/inbox.py +122 -0
  39. package/apps/travel-chaos-organizer/backend/app/routers/items.py +130 -0
  40. package/apps/travel-chaos-organizer/backend/app/routers/mail.py +108 -0
  41. package/apps/travel-chaos-organizer/backend/app/routers/parse.py +303 -0
  42. package/apps/travel-chaos-organizer/backend/app/routers/payments.py +179 -0
  43. package/apps/travel-chaos-organizer/backend/app/routers/trips.py +134 -0
  44. package/apps/travel-chaos-organizer/backend/app/routers/users.py +169 -0
  45. package/apps/travel-chaos-organizer/backend/app/routers/waitlist.py +37 -0
  46. package/apps/travel-chaos-organizer/backend/app/services/__init__.py +0 -0
  47. package/apps/travel-chaos-organizer/backend/app/services/cache.py +131 -0
  48. package/apps/travel-chaos-organizer/backend/app/services/drip.py +128 -0
  49. package/apps/travel-chaos-organizer/backend/app/services/email.py +61 -0
  50. package/apps/travel-chaos-organizer/backend/app/services/errors.py +44 -0
  51. package/apps/travel-chaos-organizer/backend/app/services/notifier.py +10 -0
  52. package/apps/travel-chaos-organizer/backend/app/services/ollama.py +101 -0
  53. package/apps/travel-chaos-organizer/backend/app/services/parser.py +22 -0
  54. package/apps/travel-chaos-organizer/backend/app/services/quota.py +85 -0
  55. package/apps/travel-chaos-organizer/backend/app/services/sentry.py +11 -0
  56. package/apps/travel-chaos-organizer/backend/app/services/telemetry.py +35 -0
  57. package/apps/travel-chaos-organizer/backend/pytest.ini +4 -0
  58. package/apps/travel-chaos-organizer/backend/requirements-dev.txt +6 -0
  59. package/apps/travel-chaos-organizer/backend/requirements.txt +25 -0
  60. package/apps/travel-chaos-organizer/backend/tests/__init__.py +0 -0
  61. package/apps/travel-chaos-organizer/backend/tests/conftest.py +223 -0
  62. package/apps/travel-chaos-organizer/backend/tests/test_admin.py +99 -0
  63. package/apps/travel-chaos-organizer/backend/tests/test_cache.py +142 -0
  64. package/apps/travel-chaos-organizer/backend/tests/test_drip.py +32 -0
  65. package/apps/travel-chaos-organizer/backend/tests/test_e2e_chain.py +121 -0
  66. package/apps/travel-chaos-organizer/backend/tests/test_export.py +48 -0
  67. package/apps/travel-chaos-organizer/backend/tests/test_inbox.py +107 -0
  68. package/apps/travel-chaos-organizer/backend/tests/test_items.py +138 -0
  69. package/apps/travel-chaos-organizer/backend/tests/test_mail.py +113 -0
  70. package/apps/travel-chaos-organizer/backend/tests/test_ollama.py +145 -0
  71. package/apps/travel-chaos-organizer/backend/tests/test_parse_url.py +98 -0
  72. package/apps/travel-chaos-organizer/backend/tests/test_parser.py +72 -0
  73. package/apps/travel-chaos-organizer/backend/tests/test_payments.py +163 -0
  74. package/apps/travel-chaos-organizer/backend/tests/test_quota.py +46 -0
  75. package/apps/travel-chaos-organizer/backend/tests/test_search.py +67 -0
  76. package/apps/travel-chaos-organizer/backend/tests/test_trips.py +123 -0
  77. package/apps/travel-chaos-organizer/backend/tests/test_users.py +52 -0
  78. package/apps/travel-chaos-organizer/backups/.gitkeep +0 -0
  79. package/apps/travel-chaos-organizer/caddy/Caddyfile +27 -0
  80. package/apps/travel-chaos-organizer/docker-compose.node1.yml +31 -0
  81. package/apps/travel-chaos-organizer/docker-compose.override.yml +14 -0
  82. package/apps/travel-chaos-organizer/docker-compose.yml +128 -0
  83. package/apps/travel-chaos-organizer/frontend/.env.example +13 -0
  84. package/apps/travel-chaos-organizer/frontend/app/(app)/_layout.tsx +80 -0
  85. package/apps/travel-chaos-organizer/frontend/app/(app)/inbox.tsx +166 -0
  86. package/apps/travel-chaos-organizer/frontend/app/(app)/search.tsx +130 -0
  87. package/apps/travel-chaos-organizer/frontend/app/(app)/settings.tsx +381 -0
  88. package/apps/travel-chaos-organizer/frontend/app/(app)/trips/[id].tsx +346 -0
  89. package/apps/travel-chaos-organizer/frontend/app/(app)/trips.tsx +363 -0
  90. package/apps/travel-chaos-organizer/frontend/app/(app)/upgrade/cancel.tsx +34 -0
  91. package/apps/travel-chaos-organizer/frontend/app/(app)/upgrade/success.tsx +35 -0
  92. package/apps/travel-chaos-organizer/frontend/app/(auth)/login.tsx +91 -0
  93. package/apps/travel-chaos-organizer/frontend/app/_layout.tsx +71 -0
  94. package/apps/travel-chaos-organizer/frontend/app/auth.tsx +29 -0
  95. package/apps/travel-chaos-organizer/frontend/app/index.tsx +16 -0
  96. package/apps/travel-chaos-organizer/frontend/app/onboarding.tsx +119 -0
  97. package/apps/travel-chaos-organizer/frontend/app.config.ts +54 -0
  98. package/apps/travel-chaos-organizer/frontend/assets/adaptive-icon.svg +66 -0
  99. package/apps/travel-chaos-organizer/frontend/assets/icon.svg +79 -0
  100. package/apps/travel-chaos-organizer/frontend/assets/splash.svg +15 -0
  101. package/apps/travel-chaos-organizer/frontend/components/BottomSheet.tsx +102 -0
  102. package/apps/travel-chaos-organizer/frontend/components/ConfettiCannon.tsx +107 -0
  103. package/apps/travel-chaos-organizer/frontend/components/ConfirmDialog.tsx +109 -0
  104. package/apps/travel-chaos-organizer/frontend/components/ErrorBoundary.tsx +45 -0
  105. package/apps/travel-chaos-organizer/frontend/components/FileUploadButton.tsx +19 -0
  106. package/apps/travel-chaos-organizer/frontend/components/ImportSheet.tsx +155 -0
  107. package/apps/travel-chaos-organizer/frontend/components/OfflineBadge.tsx +55 -0
  108. package/apps/travel-chaos-organizer/frontend/components/QRCodeViewer.tsx +80 -0
  109. package/apps/travel-chaos-organizer/frontend/components/ScoreRing.tsx +39 -0
  110. package/apps/travel-chaos-organizer/frontend/components/Screen.tsx +39 -0
  111. package/apps/travel-chaos-organizer/frontend/components/Skeleton.tsx +70 -0
  112. package/apps/travel-chaos-organizer/frontend/components/SwipeableInboxItem.tsx +121 -0
  113. package/apps/travel-chaos-organizer/frontend/components/SwipeableTimelineItem.tsx +70 -0
  114. package/apps/travel-chaos-organizer/frontend/components/TimelineItem.tsx +73 -0
  115. package/apps/travel-chaos-organizer/frontend/components/TimelineItemDetail.tsx +130 -0
  116. package/apps/travel-chaos-organizer/frontend/components/Toast.tsx +60 -0
  117. package/apps/travel-chaos-organizer/frontend/components/ToastContext.tsx +42 -0
  118. package/apps/travel-chaos-organizer/frontend/components/TripCard.tsx +146 -0
  119. package/apps/travel-chaos-organizer/frontend/components/TripQRModal.tsx +70 -0
  120. package/apps/travel-chaos-organizer/frontend/components/UpgradeBanner.tsx +44 -0
  121. package/apps/travel-chaos-organizer/frontend/eas.json +22 -0
  122. package/apps/travel-chaos-organizer/frontend/hooks/useInbox.ts +24 -0
  123. package/apps/travel-chaos-organizer/frontend/hooks/useNetworkSync.ts +60 -0
  124. package/apps/travel-chaos-organizer/frontend/hooks/useTrips.ts +112 -0
  125. package/apps/travel-chaos-organizer/frontend/lib/api.ts +223 -0
  126. package/apps/travel-chaos-organizer/frontend/lib/auth.ts +93 -0
  127. package/apps/travel-chaos-organizer/frontend/lib/db.ts +86 -0
  128. package/apps/travel-chaos-organizer/frontend/lib/fileCache.ts +54 -0
  129. package/apps/travel-chaos-organizer/frontend/lib/haptics.ts +13 -0
  130. package/apps/travel-chaos-organizer/frontend/lib/notifications.ts +112 -0
  131. package/apps/travel-chaos-organizer/frontend/lib/offlineQueue.ts +145 -0
  132. package/apps/travel-chaos-organizer/frontend/lib/quota.tsx +43 -0
  133. package/apps/travel-chaos-organizer/frontend/lib/sentry.ts +28 -0
  134. package/apps/travel-chaos-organizer/frontend/lib/shareIntent.ts +44 -0
  135. package/apps/travel-chaos-organizer/frontend/package-lock.json +13221 -0
  136. package/apps/travel-chaos-organizer/frontend/package.json +44 -0
  137. package/apps/travel-chaos-organizer/frontend/tsconfig.json +23 -0
  138. package/apps/travel-chaos-organizer/keycloak/realm-export.json +59 -0
  139. package/apps/travel-chaos-organizer/landing/index.html +773 -0
  140. package/apps/travel-chaos-organizer/scripts/backup.sh +14 -0
  141. package/apps/travel-chaos-organizer/scripts/configure-env.sh +82 -0
  142. package/apps/travel-chaos-organizer/scripts/deploy.sh +54 -0
  143. package/apps/travel-chaos-organizer/scripts/health-check.sh +36 -0
  144. package/apps/travel-chaos-organizer/scripts/migrate.sh +7 -0
  145. package/apps/travel-chaos-organizer/scripts/server-setup.sh +85 -0
  146. package/apps/travel-chaos-organizer/scripts/smoke-test.sh +29 -0
  147. package/apps/travel-chaos-organizer/scripts/verify-tracking.sh +71 -0
  148. package/apps/travel-chaos-organizer/telegram-bot/.env.example +3 -0
  149. package/apps/travel-chaos-organizer/telegram-bot/Dockerfile +6 -0
  150. package/apps/travel-chaos-organizer/telegram-bot/bot.py +504 -0
  151. package/apps/travel-chaos-organizer/telegram-bot/requirements.txt +3 -0
  152. package/dist/packages/telegram-notify/client.d.ts +19 -0
  153. package/dist/packages/telegram-notify/client.js +75 -0
  154. package/dist/packages/telegram-notify/client.js.map +1 -0
  155. package/dist/src/__tests__/auth.test.js.map +1 -0
  156. package/dist/{__tests__ → src/__tests__}/brain-flow.test.js +141 -0
  157. package/dist/src/__tests__/brain-flow.test.js.map +1 -0
  158. package/dist/src/__tests__/confidence.test.js.map +1 -0
  159. package/dist/src/__tests__/e2e/api-auth.test.js.map +1 -0
  160. package/dist/src/__tests__/e2e/api-health.test.js.map +1 -0
  161. package/dist/src/__tests__/e2e/cli.test.js.map +1 -0
  162. package/dist/src/__tests__/e2e/config.js.map +1 -0
  163. package/dist/src/__tests__/e2e/funnel.test.js.map +1 -0
  164. package/dist/src/__tests__/e2e/mcp-tools.test.js.map +1 -0
  165. package/dist/src/__tests__/e2e/provisioning.test.js.map +1 -0
  166. package/dist/{__tests__ → src/__tests__}/handlers-unit.test.js +0 -2
  167. package/dist/src/__tests__/handlers-unit.test.js.map +1 -0
  168. package/dist/src/__tests__/namespace.test.js.map +1 -0
  169. package/dist/src/__tests__/rerank.test.js +98 -0
  170. package/dist/src/__tests__/rerank.test.js.map +1 -0
  171. package/dist/src/auth.js.map +1 -0
  172. package/dist/src/bench/cachly-bench.d.ts +35 -0
  173. package/dist/src/bench/cachly-bench.js +182 -0
  174. package/dist/src/bench/cachly-bench.js.map +1 -0
  175. package/dist/src/bench/fixtures.d.ts +34 -0
  176. package/dist/src/bench/fixtures.js +199 -0
  177. package/dist/src/bench/fixtures.js.map +1 -0
  178. package/dist/src/ckg.js.map +1 -0
  179. package/dist/src/confidence.js.map +1 -0
  180. package/dist/src/embeddings.js.map +1 -0
  181. package/dist/src/handlers/advanced.js.map +1 -0
  182. package/dist/{handlers → src/handlers}/brain.js +261 -87
  183. package/dist/src/handlers/brain.js.map +1 -0
  184. package/dist/src/handlers/cache.js.map +1 -0
  185. package/dist/{handlers → src/handlers}/context.js +9 -4
  186. package/dist/src/handlers/context.js.map +1 -0
  187. package/dist/{handlers → src/handlers}/fedbrain.d.ts +6 -0
  188. package/dist/{handlers → src/handlers}/fedbrain.js +19 -7
  189. package/dist/src/handlers/fedbrain.js.map +1 -0
  190. package/dist/src/handlers/instances.js.map +1 -0
  191. package/dist/{handlers → src/handlers}/roadmap.js +7 -3
  192. package/dist/src/handlers/roadmap.js.map +1 -0
  193. package/dist/src/handlers/syndicate.js.map +1 -0
  194. package/dist/src/handlers/tco.d.ts +16 -0
  195. package/dist/src/handlers/tco.js +303 -0
  196. package/dist/src/handlers/tco.js.map +1 -0
  197. package/dist/src/handlers/team.js.map +1 -0
  198. package/dist/{index.js → src/index.js} +153 -218
  199. package/dist/src/index.js.map +1 -0
  200. package/dist/src/namespace.js.map +1 -0
  201. package/dist/src/notifier.d.ts +5 -0
  202. package/dist/src/notifier.js +6 -0
  203. package/dist/src/notifier.js.map +1 -0
  204. package/dist/src/rerank.d.ts +50 -0
  205. package/dist/src/rerank.js +92 -0
  206. package/dist/src/rerank.js.map +1 -0
  207. package/dist/src/search.js.map +1 -0
  208. package/dist/src/search.test.d.ts +1 -0
  209. package/dist/src/search.test.js.map +1 -0
  210. package/dist/{tools.d.ts → src/tools.d.ts} +196 -0
  211. package/dist/{tools.js → src/tools.js} +178 -0
  212. package/dist/src/tools.js.map +1 -0
  213. package/dist/src/utils.js.map +1 -0
  214. package/package.json +4 -3
  215. package/packages/telegram-notify/SPLIT_PLAN.md +106 -0
  216. package/packages/telegram-notify/client.ts +88 -0
  217. package/packages/telegram-notify/emojis.json +22 -0
  218. package/packages/telegram-notify/pyproject.toml +17 -0
  219. package/packages/telegram-notify/telegram_notify/__init__.py +3 -0
  220. package/packages/telegram-notify/telegram_notify/client.py +87 -0
  221. package/packages/telegram-notify/telegram_notify/emojis.json +22 -0
  222. package/scripts/generate-icons.js +28 -0
  223. package/smithery.yaml +21 -8
  224. package/src/__tests__/brain-flow.test.ts +174 -0
  225. package/src/__tests__/handlers-unit.test.ts +1 -2
  226. package/src/__tests__/rerank.test.ts +111 -0
  227. package/src/bench/cachly-bench.ts +217 -0
  228. package/src/bench/fixtures.ts +226 -0
  229. package/src/handlers/brain.ts +276 -92
  230. package/src/handlers/context.ts +9 -4
  231. package/src/handlers/fedbrain.ts +21 -7
  232. package/src/handlers/roadmap.ts +7 -3
  233. package/src/handlers/tco.ts +347 -0
  234. package/src/index.ts +139 -220
  235. package/src/notifier.ts +5 -0
  236. package/src/rerank.ts +123 -0
  237. package/src/tools.ts +191 -0
  238. package/tsconfig.json +2 -3
  239. package/dist/__tests__/auth.test.js.map +0 -1
  240. package/dist/__tests__/brain-flow.test.js.map +0 -1
  241. package/dist/__tests__/brain-predict-failures.test.d.ts +0 -17
  242. package/dist/__tests__/brain-predict-failures.test.js +0 -339
  243. package/dist/__tests__/brain-predict-failures.test.js.map +0 -1
  244. package/dist/__tests__/confidence.test.js.map +0 -1
  245. package/dist/__tests__/e2e/api-auth.test.js.map +0 -1
  246. package/dist/__tests__/e2e/api-health.test.js.map +0 -1
  247. package/dist/__tests__/e2e/cli.test.js.map +0 -1
  248. package/dist/__tests__/e2e/config.js.map +0 -1
  249. package/dist/__tests__/e2e/funnel.test.js.map +0 -1
  250. package/dist/__tests__/e2e/mcp-tools.test.js.map +0 -1
  251. package/dist/__tests__/e2e/provisioning.test.js.map +0 -1
  252. package/dist/__tests__/handlers-unit.test.js.map +0 -1
  253. package/dist/__tests__/namespace.test.js.map +0 -1
  254. package/dist/auth.js.map +0 -1
  255. package/dist/ckg.js.map +0 -1
  256. package/dist/confidence.js.map +0 -1
  257. package/dist/embeddings.js.map +0 -1
  258. package/dist/handlers/advanced.js.map +0 -1
  259. package/dist/handlers/brain.js.map +0 -1
  260. package/dist/handlers/cache.js.map +0 -1
  261. package/dist/handlers/context.js.map +0 -1
  262. package/dist/handlers/fedbrain.js.map +0 -1
  263. package/dist/handlers/instances.js.map +0 -1
  264. package/dist/handlers/roadmap.js.map +0 -1
  265. package/dist/handlers/syndicate.js.map +0 -1
  266. package/dist/handlers/team.js.map +0 -1
  267. package/dist/index.js.map +0 -1
  268. package/dist/namespace.js.map +0 -1
  269. package/dist/search.js.map +0 -1
  270. package/dist/search.test.js.map +0 -1
  271. package/dist/tools.js.map +0 -1
  272. package/dist/utils.js.map +0 -1
  273. package/src/__tests__/brain-predict-failures.test.ts +0 -430
  274. /package/dist/{__tests__ → src/__tests__}/auth.test.d.ts +0 -0
  275. /package/dist/{__tests__ → src/__tests__}/auth.test.js +0 -0
  276. /package/dist/{__tests__ → src/__tests__}/brain-flow.test.d.ts +0 -0
  277. /package/dist/{__tests__ → src/__tests__}/confidence.test.d.ts +0 -0
  278. /package/dist/{__tests__ → src/__tests__}/confidence.test.js +0 -0
  279. /package/dist/{__tests__ → src/__tests__}/e2e/api-auth.test.d.ts +0 -0
  280. /package/dist/{__tests__ → src/__tests__}/e2e/api-auth.test.js +0 -0
  281. /package/dist/{__tests__ → src/__tests__}/e2e/api-health.test.d.ts +0 -0
  282. /package/dist/{__tests__ → src/__tests__}/e2e/api-health.test.js +0 -0
  283. /package/dist/{__tests__ → src/__tests__}/e2e/cli.test.d.ts +0 -0
  284. /package/dist/{__tests__ → src/__tests__}/e2e/cli.test.js +0 -0
  285. /package/dist/{__tests__ → src/__tests__}/e2e/config.d.ts +0 -0
  286. /package/dist/{__tests__ → src/__tests__}/e2e/config.js +0 -0
  287. /package/dist/{__tests__ → src/__tests__}/e2e/funnel.test.d.ts +0 -0
  288. /package/dist/{__tests__ → src/__tests__}/e2e/funnel.test.js +0 -0
  289. /package/dist/{__tests__ → src/__tests__}/e2e/mcp-tools.test.d.ts +0 -0
  290. /package/dist/{__tests__ → src/__tests__}/e2e/mcp-tools.test.js +0 -0
  291. /package/dist/{__tests__ → src/__tests__}/e2e/provisioning.test.d.ts +0 -0
  292. /package/dist/{__tests__ → src/__tests__}/e2e/provisioning.test.js +0 -0
  293. /package/dist/{__tests__ → src/__tests__}/handlers-unit.test.d.ts +0 -0
  294. /package/dist/{__tests__ → src/__tests__}/namespace.test.d.ts +0 -0
  295. /package/dist/{__tests__ → src/__tests__}/namespace.test.js +0 -0
  296. /package/dist/{search.test.d.ts → src/__tests__/rerank.test.d.ts} +0 -0
  297. /package/dist/{auth.d.ts → src/auth.d.ts} +0 -0
  298. /package/dist/{auth.js → src/auth.js} +0 -0
  299. /package/dist/{ckg.d.ts → src/ckg.d.ts} +0 -0
  300. /package/dist/{ckg.js → src/ckg.js} +0 -0
  301. /package/dist/{confidence.d.ts → src/confidence.d.ts} +0 -0
  302. /package/dist/{confidence.js → src/confidence.js} +0 -0
  303. /package/dist/{embeddings.d.ts → src/embeddings.d.ts} +0 -0
  304. /package/dist/{embeddings.js → src/embeddings.js} +0 -0
  305. /package/dist/{handlers → src/handlers}/advanced.d.ts +0 -0
  306. /package/dist/{handlers → src/handlers}/advanced.js +0 -0
  307. /package/dist/{handlers → src/handlers}/brain.d.ts +0 -0
  308. /package/dist/{handlers → src/handlers}/cache.d.ts +0 -0
  309. /package/dist/{handlers → src/handlers}/cache.js +0 -0
  310. /package/dist/{handlers → src/handlers}/context.d.ts +0 -0
  311. /package/dist/{handlers → src/handlers}/instances.d.ts +0 -0
  312. /package/dist/{handlers → src/handlers}/instances.js +0 -0
  313. /package/dist/{handlers → src/handlers}/roadmap.d.ts +0 -0
  314. /package/dist/{handlers → src/handlers}/syndicate.d.ts +0 -0
  315. /package/dist/{handlers → src/handlers}/syndicate.js +0 -0
  316. /package/dist/{handlers → src/handlers}/team.d.ts +0 -0
  317. /package/dist/{handlers → src/handlers}/team.js +0 -0
  318. /package/dist/{index.d.ts → src/index.d.ts} +0 -0
  319. /package/dist/{namespace.d.ts → src/namespace.d.ts} +0 -0
  320. /package/dist/{namespace.js → src/namespace.js} +0 -0
  321. /package/dist/{search.d.ts → src/search.d.ts} +0 -0
  322. /package/dist/{search.js → src/search.js} +0 -0
  323. /package/dist/{search.test.js → src/search.test.js} +0 -0
  324. /package/dist/{utils.d.ts → src/utils.d.ts} +0 -0
  325. /package/dist/{utils.js → src/utils.js} +0 -0
@@ -8,19 +8,28 @@ import type { CKGEdge, CKGNode } from '../ckg.js';
8
8
  import { keywordSearch, tokenize, splitMultiQuery, levenshtein,
9
9
  ZERO_RESULTS_LOG, zeroResultsTotal, indexVocab as _indexVocab } from '../search.js';
10
10
  import type { KeywordMatch } from '../search.js';
11
+ import { rerankByQuality } from '../rerank.js';
11
12
  import { computeEmbedding, hasEmbedProvider, embedProviderHint, EMBED_PROVIDER } from '../embeddings.js';
12
13
  import { detectNamespace } from '../namespace.js';
13
14
 
14
15
  // ── Changelog (shown once per version in session_start) ──────────────────────
15
- const MCP_VERSION = '0.10.0';
16
+ const MCP_VERSION = '0.10.50';
16
17
  const WHATS_NEW: Record<string, string[]> = {
17
- '0.10.0': [
18
+ '0.10.50': [
18
19
  `🆕 **What's new in v${MCP_VERSION}:**`,
19
- ` ✅ Zero-friction setup — auto-provisions your Brain, no manual config needed`,
20
- ` ✅ 10× faster briefings — batch Redis fetches replace per-key loops`,
21
- ` ✅ Crash-proof data reads — corrupted cache entries no longer break tools`,
22
- ` ✅ \`session_end\` warns when called without an active session`,
23
- ` ✅ \`get_api_status\` now shows live Redis ping + all instance statuses`,
20
+ ` ✅ \`smart_recall\` — unified keyword + semantic hybrid list (one ranked result, not two sections)`,
21
+ ` ✅ \`smart_recall\` — "Brain saved you here" banner surfaces time saved inline per proven lesson`,
22
+ ` ✅ Quality reranking — proven successes outrank symptom-dense failures (+11% Precision@1)`,
23
+ ` ✅ Cachly-Bench — IR benchmark with CI regression guard (see BENCH.md for the proof)`,
24
+ ` 🔧 Contradiction resolution now persisted to Redis (90-day TTL)`,
25
+ ` 💡 Run \`smart_recall\` to see your Brain's recall quality in action`,
26
+ ],
27
+ '0.10.49': [
28
+ ` ✅ \`brain_from_git\` — auto-seed your Brain from git history in seconds`,
29
+ ` ✅ \`brain_predict_failures\` — predict CI/build failures before they happen`,
30
+ ` ✅ VS Code extension — Brain status bar, ambient learning, CodeLens hints`,
31
+ ` ✅ Team Brain — share lessons across your whole engineering team`,
32
+ ` 🔧 Stability — auto-expiring memory keys, longer provisioning wait, robust recall`,
24
33
  ],
25
34
  };
26
35
 
@@ -138,6 +147,18 @@ export async function handleBrainTool(
138
147
  const resId = ckgSlug(`resolution:${topic}`);
139
148
  await ckgUpdateEdge(redis, cId, 'contradicts', resId, false);
140
149
  } catch { /* non-critical */ }
150
+ // Persist a queryable resolution record — so contradiction history survives
151
+ // (CKG edges alone aren't easily auditable). Bounded + TTL'd like other history.
152
+ try {
153
+ const resolutionKey = `cachly:contradictions:${topic}`;
154
+ const direction = prev.outcome === 'success' && outcome === 'failure'
155
+ ? 'kept-success' : 'overwrote-failure';
156
+ await redis.rpush(resolutionKey, JSON.stringify({
157
+ ts, prev_outcome: prev.outcome, new_outcome: outcome, direction,
158
+ }));
159
+ await redis.ltrim(resolutionKey, -50, -1);
160
+ await redis.expire(resolutionKey, 180 * 86400);
161
+ } catch { /* non-critical */ }
141
162
  contradictionWarning.push(`🗳️ Run \`madc_deliberate(topic="${topic}")\` to resolve via expert agent voting.`);
142
163
  }
143
164
  } catch { /* ignore parse error */ }
@@ -183,7 +204,7 @@ export async function handleBrainTool(
183
204
  const existing = await redis.get(depKey);
184
205
  const depTopics: string[] = safeJsonParse<string[]>(existing, []);
185
206
  if (!depTopics.includes(topic)) depTopics.push(topic);
186
- await redis.set(depKey, JSON.stringify(depTopics));
207
+ await redis.set(depKey, JSON.stringify(depTopics), 'EX', 90 * 86400);
187
208
  }
188
209
 
189
210
  const lessonObj = {
@@ -207,9 +228,11 @@ export async function handleBrainTool(
207
228
  };
208
229
  const lesson = JSON.stringify(lessonObj);
209
230
 
210
- // Always append to the history list (audit log)
231
+ // Always append to the history list (audit log); keep last 100 entries, 90-day TTL
211
232
  const listKey = `cachly:lessons:${topic}`;
212
233
  await redis.rpush(listKey, lesson);
234
+ await redis.ltrim(listKey, -100, -1);
235
+ await redis.expire(listKey, 90 * 86400);
213
236
 
214
237
  // Update best key for success/partial; for failure only update if no success exists
215
238
  if (outcome === 'success' || outcome === 'partial') {
@@ -417,8 +440,27 @@ export async function handleBrainTool(
417
440
  ? `📌 **You've looked this up 3 times.** Consider pinning it for instant access: add \`pinned: true\` via \`learn_from_attempts\` to always surface it first.`
418
441
  : '';
419
442
 
443
+ // ── Trust signal (today-safe consensus layer) ───────────────────────
444
+ // A lesson recalled many times — or confirmed by multiple distinct
445
+ // authors — has proven its value. Surface that as social proof.
446
+ const lessonAuthors = (lesson as { authors?: string[]; author?: string }).authors
447
+ ?? ((lesson as { author?: string }).author ? [(lesson as { author?: string }).author!] : []);
448
+ const distinctAuthors = [...new Set(lessonAuthors.filter(Boolean))];
449
+ const rc = updatedLesson.recall_count;
450
+ let trustBadge = '';
451
+ if (distinctAuthors.length >= 2 && rc >= 5) {
452
+ trustBadge = `🏆 **Battle-tested** — recalled ${rc}× · verified by ${distinctAuthors.length} developers. Trust this.`;
453
+ } else if (rc >= 10) {
454
+ trustBadge = `🏆 **Battle-tested** — recalled ${rc}×. This is one of your most-proven solutions.`;
455
+ } else if (distinctAuthors.length >= 2) {
456
+ trustBadge = `✅ **Team-verified** — confirmed by ${distinctAuthors.length} developers.`;
457
+ } else if (rc >= 5) {
458
+ trustBadge = `✅ **Proven** — recalled ${rc}× without contradiction.`;
459
+ }
460
+
420
461
  return [
421
462
  rememberWhen,
463
+ trustBadge,
422
464
  `${badge} **Best solution for \`${topic}\`** ${sevEmoji}${lesson.severity ? ` (${lesson.severity})` : ''} · recalled ${updatedLesson.recall_count}×`,
423
465
  ``,
424
466
  `**What worked:** ${lesson.what_worked}`,
@@ -476,65 +518,48 @@ export async function handleBrainTool(
476
518
  const redis = await getConnection(instance_id);
477
519
 
478
520
  // ── Layer 1: Keyword search across ALL brain data (always works, no embedding) ──
479
- const kwMatches = await keywordSearch(
521
+ const rawMatches = await keywordSearch(
480
522
  redis,
481
523
  ['cachly:ctx:*', 'cachly:lesson:best:*', 'cachly:idx:*'],
482
524
  query,
483
525
  10,
484
526
  );
485
527
 
486
- const lines: string[] = [`🧠 **Smart Recall** for: _"${query}"_\n`];
487
-
488
- // Show sub-query info if multi-topic was detected
489
- const subQueries = splitMultiQuery(query);
490
- if (subQueries.length > 1) {
491
- lines.push(`_Detected ${subQueries.length} sub-topics:_ ${subQueries.map((s, i) => `${i + 1}. "${s}"`).join(', ')}\n`);
492
- }
493
-
494
- if (kwMatches.length > 0) {
495
- lines.push(`### 🔍 BM25 Matches (${kwMatches.length})\n`);
528
+ // ── Layer 1.5: Quality-aware rerank — proven success lessons outrank
529
+ // text-similar failed attempts (the moat; see src/rerank.ts + Cachly-Bench). ──
530
+ const kwMatches = rerankByQuality(rawMatches);
496
531
 
497
- // Group by sub-query if multi-topic
498
- if (subQueries.length > 1) {
499
- const grouped = new Map<string, KeywordMatch[]>();
500
- for (const m of kwMatches.slice(0, 12)) {
501
- const sq = m.subQuery ?? query;
502
- if (!grouped.has(sq)) grouped.set(sq, []);
503
- grouped.get(sq)!.push(m);
504
- }
505
- for (const [sq, matches] of grouped) {
506
- lines.push(`**Topic: "${sq}"** (${matches.length} results)\n`);
507
- for (const m of matches.slice(0, 4)) {
508
- const label = m.key
509
- .replace('cachly:ctx:', '📝 ')
510
- .replace('cachly:lesson:best:', '💡 ')
511
- .replace('cachly:idx:', '📂 ');
512
- const preview = m.content.slice(0, 300).replace(/\n/g, ' ');
513
- lines.push(` **${label}** _(BM25: ${m.score.toFixed(2)}, matched: ${m.matchedWords.join(', ')})_`);
514
- lines.push(` > ${preview}${m.content.length > 300 ? '…' : ''}\n`);
532
+ // Increment recall_count on matched lessons (fire-and-forget) + collect "Brain saved you here" signal.
533
+ type RecalledLesson = { topic: string; severity: string; recall_count: number; savedMins: number };
534
+ const savedHere: RecalledLesson[] = [];
535
+ const lessonMatches = kwMatches.filter(m => m.key.startsWith('cachly:lesson:best:'));
536
+ for (const m of lessonMatches.slice(0, 5)) {
537
+ const existing = await redis.get(m.key).catch(() => null);
538
+ if (existing) {
539
+ const lesson = safeJsonParse(existing, null as null | { recall_count?: number; outcome?: string; severity?: string; [k: string]: unknown });
540
+ if (lesson) {
541
+ const updated = { ...lesson, recall_count: (lesson.recall_count ?? 0) + 1, verified_at: new Date().toISOString() };
542
+ redis.set(m.key, JSON.stringify(updated)).catch(() => {});
543
+ const sev = lesson.severity as string;
544
+ const savedMins = sev === 'critical' ? 240 : sev === 'major' ? 60 : 30;
545
+ redis.incrbyfloat(`cachly:stats:time_saved_mins:${instance_id}`, savedMins).catch(() => {});
546
+ // Surface banner for proven successes (recall_count >= 1 means it's been validated)
547
+ if (lesson.outcome === 'success' && (lesson.recall_count ?? 0) >= 1) {
548
+ savedHere.push({
549
+ topic: m.key.replace('cachly:lesson:best:', ''),
550
+ severity: sev ?? 'major',
551
+ recall_count: (lesson.recall_count ?? 0) + 1,
552
+ savedMins,
553
+ });
515
554
  }
516
555
  }
517
- // Summary: which sub-queries had matches
518
- const matched = [...grouped.keys()];
519
- const unmatched = subQueries.filter(sq => !matched.includes(sq));
520
- if (unmatched.length > 0) {
521
- lines.push(`\n⚠️ **No results for:** ${unmatched.map(s => `"${s}"`).join(', ')}`);
522
- }
523
- } else {
524
- for (const m of kwMatches.slice(0, 8)) {
525
- const label = m.key
526
- .replace('cachly:ctx:', '📝 ')
527
- .replace('cachly:lesson:best:', '💡 ')
528
- .replace('cachly:idx:', '📂 ');
529
- const preview = m.content.slice(0, 400).replace(/\n/g, ' ');
530
- lines.push(`**${label}** _(BM25: ${m.score.toFixed(2)}, matched: ${m.matchedWords.join(', ')})_`);
531
- lines.push(`> ${preview}${m.content.length > 400 ? '…' : ''}\n`);
532
- }
533
556
  }
534
557
  }
535
558
 
536
- // ── Layer 2: Semantic search (optional, only if embedding provider + vector_token available) ──
559
+ // ── Layer 2: Semantic search (parallel, optional) ────────────────────────
537
560
  const inst = await apiFetch<Instance>(`/api/v1/instances/${instance_id}`);
561
+ type SemHit = { key: string; similarity: number; content: string };
562
+ const semHits: SemHit[] = [];
538
563
  if (inst.vector_token && hasEmbedProvider()) {
539
564
  try {
540
565
  const embedding = await computeEmbedding(query);
@@ -544,22 +569,15 @@ export async function handleBrainTool(
544
569
  headers: { 'Content-Type': 'application/json' },
545
570
  body: JSON.stringify({ embedding, namespace: 'cachly:ctx', threshold, top_k: 5 }),
546
571
  });
547
-
548
572
  if (searchRes.ok) {
549
573
  const results = (await searchRes.json()) as SemanticSearchResponse[];
550
- const semHits = results.filter(r => r.found && r.id);
551
- if (semHits.length > 0) {
552
- lines.push(`\n### 🎯 Semantic Matches (${semHits.length})\n`);
553
- for (const hit of semHits) {
554
- const parts = hit.id!.replace('ctx:', '').split(':');
555
- const category = parts[0];
556
- const key = parts.slice(1).join(':');
557
- const content = await redis.get(`cachly:ctx:${category}:${key}`);
558
- lines.push(
559
- `**${key}** _(${((hit.similarity ?? 0) * 100).toFixed(0)}% similar)_`,
560
- `> ${content?.slice(0, 300) ?? '(evicted)'}${(content?.length ?? 0) > 300 ? '…' : ''}\n`,
561
- );
562
- }
574
+ for (const hit of results.filter(r => r.found && r.id)) {
575
+ const parts = hit.id!.replace('ctx:', '').split(':');
576
+ const category = parts[0];
577
+ const key = parts.slice(1).join(':');
578
+ const redisKey = `cachly:ctx:${category}:${key}`;
579
+ const content = await redis.get(redisKey).catch(() => null);
580
+ semHits.push({ key: redisKey, similarity: hit.similarity ?? 0, content: content ?? '(evicted)' });
563
581
  }
564
582
  }
565
583
  } catch {
@@ -567,7 +585,106 @@ export async function handleBrainTool(
567
585
  }
568
586
  }
569
587
 
570
- if (kwMatches.length === 0) {
588
+ // ── Layer 3: Hybrid merge — one unified ranked list ──────────────────────
589
+ // BM25 quality scores (already reranked) are re-normalized to [0,1] for merging.
590
+ type HybridResult = {
591
+ key: string; content: string; hybridScore: number;
592
+ bm25Score?: number; semScore?: number; matchedWords?: string[];
593
+ matchType: 'keyword' | 'semantic' | 'hybrid'; subQuery?: string;
594
+ };
595
+
596
+ const bm25Scores = kwMatches.map(m => m.score);
597
+ const bm25Min = bm25Scores.length ? Math.min(...bm25Scores) : 0;
598
+ const bm25Range = bm25Scores.length ? (Math.max(...bm25Scores) - bm25Min) || 1 : 1;
599
+ const bm25Norm = (s: number) => (s - bm25Min) / bm25Range;
600
+
601
+ const hybridMap = new Map<string, HybridResult>();
602
+ for (const m of kwMatches) {
603
+ const n = bm25Norm(m.score);
604
+ hybridMap.set(m.key, {
605
+ key: m.key, content: m.content, bm25Score: n,
606
+ hybridScore: n * (semHits.length > 0 ? 0.7 : 1.0),
607
+ matchedWords: m.matchedWords, matchType: 'keyword', subQuery: m.subQuery,
608
+ });
609
+ }
610
+ for (const hit of semHits) {
611
+ const existing = hybridMap.get(hit.key);
612
+ if (existing) {
613
+ existing.semScore = hit.similarity;
614
+ existing.hybridScore = (existing.bm25Score ?? 0) * 0.6 + hit.similarity * 0.4;
615
+ existing.matchType = 'hybrid';
616
+ } else {
617
+ hybridMap.set(hit.key, {
618
+ key: hit.key, content: hit.content, semScore: hit.similarity,
619
+ hybridScore: hit.similarity * 0.5, matchType: 'semantic',
620
+ });
621
+ }
622
+ }
623
+ const hybridResults = [...hybridMap.values()].sort((a, b) => b.hybridScore - a.hybridScore);
624
+
625
+ // ── Build output ──────────────────────────────────────────────────────────
626
+ const lines: string[] = [`🧠 **Smart Recall** for: _"${query}"_\n`];
627
+
628
+ // "Brain saved you here" banner — surfaces value inline for proven recalled lessons
629
+ const SORDER: Record<string, number> = { critical: 0, major: 1, minor: 2 };
630
+ const topLesson = savedHere.sort((a, b) => (SORDER[a.severity] ?? 1) - (SORDER[b.severity] ?? 1))[0];
631
+ if (topLesson) {
632
+ lines.push(`> 💡 **Brain saved you ~${topLesson.savedMins}m here** — ${topLesson.severity} issue from \`${topLesson.topic}\` recalled ${topLesson.recall_count}× proven\n`);
633
+ }
634
+
635
+ // Show sub-query info if multi-topic was detected
636
+ const subQueries = splitMultiQuery(query);
637
+ if (subQueries.length > 1) {
638
+ lines.push(`_Detected ${subQueries.length} sub-topics:_ ${subQueries.map((s, i) => `${i + 1}. "${s}"`).join(', ')}\n`);
639
+ }
640
+
641
+ if (hybridResults.length > 0) {
642
+ const modeLabel = semHits.length > 0
643
+ ? `keyword + semantic hybrid`
644
+ : `keyword`;
645
+ lines.push(`### 🔍 Results (${hybridResults.length} — ${modeLabel})\n`);
646
+
647
+ // Group by sub-query for multi-topic queries
648
+ if (subQueries.length > 1) {
649
+ const grouped = new Map<string, HybridResult[]>();
650
+ for (const r of hybridResults.slice(0, 12)) {
651
+ const sq = r.subQuery ?? query;
652
+ if (!grouped.has(sq)) grouped.set(sq, []);
653
+ grouped.get(sq)!.push(r);
654
+ }
655
+ for (const [sq, results] of grouped) {
656
+ lines.push(`**Topic: "${sq}"** (${results.length} results)\n`);
657
+ for (const r of results.slice(0, 4)) {
658
+ const label = r.key.replace('cachly:ctx:', '📝 ').replace('cachly:lesson:best:', '💡 ').replace('cachly:idx:', '📂 ');
659
+ const scorePart = r.matchType === 'hybrid'
660
+ ? `BM25: ${(r.bm25Score ?? 0).toFixed(2)}, sem: ${((r.semScore ?? 0) * 100).toFixed(0)}%, 🔀 hybrid`
661
+ : r.matchType === 'semantic'
662
+ ? `sem: ${((r.semScore ?? 0) * 100).toFixed(0)}%, 🎯 semantic`
663
+ : `BM25: ${(r.bm25Score ?? 0).toFixed(2)}, matched: ${r.matchedWords?.join(', ')}`;
664
+ const preview = r.content.slice(0, 300).replace(/\n/g, ' ');
665
+ lines.push(` **${label}** _(${scorePart})_`);
666
+ lines.push(` > ${preview}${r.content.length > 300 ? '…' : ''}\n`);
667
+ }
668
+ }
669
+ const matched = [...grouped.keys()];
670
+ const unmatched = subQueries.filter(sq => !matched.includes(sq));
671
+ if (unmatched.length > 0) {
672
+ lines.push(`\n⚠️ **No results for:** ${unmatched.map(s => `"${s}"`).join(', ')}`);
673
+ }
674
+ } else {
675
+ for (const r of hybridResults.slice(0, 8)) {
676
+ const label = r.key.replace('cachly:ctx:', '📝 ').replace('cachly:lesson:best:', '💡 ').replace('cachly:idx:', '📂 ');
677
+ const scorePart = r.matchType === 'hybrid'
678
+ ? `BM25: ${(r.bm25Score ?? 0).toFixed(2)}, sem: ${((r.semScore ?? 0) * 100).toFixed(0)}%, 🔀 hybrid`
679
+ : r.matchType === 'semantic'
680
+ ? `sem: ${((r.semScore ?? 0) * 100).toFixed(0)}%, 🎯 semantic`
681
+ : `BM25: ${(r.bm25Score ?? 0).toFixed(2)}, matched: ${r.matchedWords?.join(', ')}`;
682
+ const preview = r.content.slice(0, 400).replace(/\n/g, ' ');
683
+ lines.push(`**${label}** _(${scorePart})_`);
684
+ lines.push(`> ${preview}${r.content.length > 400 ? '…' : ''}\n`);
685
+ }
686
+ }
687
+ } else {
571
688
  lines.push(`⚠️ No matches found for: "${query}"`);
572
689
 
573
690
  // Did-You-Mean: find nearest token in index vocab
@@ -897,6 +1014,31 @@ export async function handleBrainTool(
897
1014
  // Brain health
898
1015
  lines.push(`📊 **Brain:** ${lessons.length} lessons · ${ctxCount} context entries`, '');
899
1016
 
1017
+ // ── 📌 Proven Laws — auto-crystallized lessons (quick win 3) ─────────────
1018
+ // A lesson that has proven itself (recalled ≥ CRYSTALLIZE_RECALLS times) or
1019
+ // was explicitly pinned becomes a "law" — surfaced at the top of every
1020
+ // briefing so the most battle-tested knowledge is never buried. This is
1021
+ // the today-safe version of swarm crystallization: enough confirmations
1022
+ // auto-promote a lesson without any manual step.
1023
+ {
1024
+ type LessonPin = typeof lessons[0] & { pinned?: boolean };
1025
+ const CRYSTALLIZE_RECALLS = 5;
1026
+ const laws = (lessons as LessonPin[])
1027
+ .filter(l => l.outcome === 'success' && (l.pinned === true || (l.recall_count ?? 0) >= CRYSTALLIZE_RECALLS))
1028
+ .sort((a, b) => (b.recall_count ?? 0) - (a.recall_count ?? 0))
1029
+ .slice(0, 5);
1030
+ if (laws.length > 0) {
1031
+ lines.push(`📌 **Proven Laws** (auto-crystallized — your most-trusted knowledge):`);
1032
+ for (const l of laws) {
1033
+ const sev = l.severity === 'critical' ? '🔴' : l.severity === 'major' ? '🟡' : '';
1034
+ const rc = l.recall_count ?? 0;
1035
+ const why = l.pinned === true ? 'pinned' : `recalled ${rc}×`;
1036
+ lines.push(` 🏆${sev} \`${l.topic}\` _(${why})_ — ${l.what_worked.slice(0, 100)}`);
1037
+ }
1038
+ lines.push('');
1039
+ }
1040
+ }
1041
+
900
1042
  // ── Layer 7: MCM Domain Coverage Map ────────────────────────────────────
901
1043
  if (lessons.length >= 3) {
902
1044
  const domainMap = new Map<string, { total: number; success: number; critical: number }>();
@@ -996,8 +1138,14 @@ export async function handleBrainTool(
996
1138
  for (const l of topRecalled) {
997
1139
  const emoji = l.outcome === 'success' ? '✅' : l.outcome === 'partial' ? '⚠️' : '❌';
998
1140
  const sev = l.severity === 'critical' ? '🔴' : l.severity === 'major' ? '🟡' : '';
999
- const rct = l.recall_count && l.recall_count > 1 ? ` _(${l.recall_count}× recalled)_` : '';
1000
- lines.push(` ${emoji}${sev} \`${l.topic}\`${rct} — ${l.what_worked.slice(0, 100)}`);
1141
+ const rc = l.recall_count ?? 0;
1142
+ const la = (l as { authors?: string[]; author?: string }).authors
1143
+ ?? ((l as { author?: string }).author ? [(l as { author?: string }).author!] : []);
1144
+ const nAuthors = new Set(la.filter(Boolean)).size;
1145
+ const trust = (nAuthors >= 2 && rc >= 5) || rc >= 10 ? ' 🏆'
1146
+ : nAuthors >= 2 || rc >= 5 ? ' ✅' : '';
1147
+ const rct = rc > 1 ? ` _(${rc}× recalled)_` : '';
1148
+ lines.push(` ${emoji}${sev}${trust} \`${l.topic}\`${rct} — ${l.what_worked.slice(0, 100)}`);
1001
1149
  }
1002
1150
  lines.push('');
1003
1151
  }
@@ -1065,27 +1213,63 @@ export async function handleBrainTool(
1065
1213
  }
1066
1214
 
1067
1215
  // ── 🔮 Predictive Pre-Warning — intent-based danger detection ────────────
1068
- // Fires BEFORE work starts. If focus area has known failure patterns → warn loudly.
1069
- if (focusTerms.length > 0) {
1216
+ // Fires BEFORE work starts. Uses explicit focus when given; otherwise
1217
+ // derives the likely work area from the last session's changed files +
1218
+ // summary (you usually keep working where you left off). This makes the
1219
+ // warning fire even when the caller forgets to pass `focus`.
1220
+ {
1070
1221
  type LessonAny = typeof lessons[0] & { author?: string; tags?: string[] };
1071
- const dangerLessons = (lessons as LessonAny[]).filter(l => {
1072
- if (l.outcome === 'success') return false;
1073
- const topicCategory = l.topic.split(':')[0];
1074
- return focusTerms.some(term =>
1075
- l.topic.toLowerCase().includes(term) ||
1076
- topicCategory === term ||
1077
- (l.tags ?? []).some((t: string) => t.toLowerCase() === term),
1078
- );
1079
- });
1080
- if (dangerLessons.length >= 1) {
1081
- // Insert warning block right after the title line (index 1 = blank line after title)
1082
- const warning = [
1083
- `🚨 **PRE-WARNING** — Read this BEFORE starting:`,
1084
- ` Known pitfalls for **"${focus}"** (${dangerLessons.length} past failure${dangerLessons.length > 1 ? 's' : ''}):`,
1085
- ...dangerLessons.slice(0, 3).map(l => ` ❌ \`${l.topic}\` — ${(l.what_failed ?? l.what_worked).slice(0, 80)}`),
1086
- '',
1087
- ];
1088
- lines.splice(2, 0, ...warning); // after '🧠 **Session Briefing**' + empty line
1222
+
1223
+ // Common path noise / extensions to drop when deriving terms from files.
1224
+ const PATH_NOISE = new Set([
1225
+ 'src', 'lib', 'test', 'tests', 'dist', 'index', 'main', 'app',
1226
+ 'internal', 'pkg', 'cmd', 'node_modules', 'components', 'utils',
1227
+ 'ts', 'tsx', 'js', 'jsx', 'go', 'py', 'rs', 'java', 'json', 'yaml', 'yml',
1228
+ ]);
1229
+ const deriveTermsFromFiles = (files: string[]): string[] => {
1230
+ const terms = new Set<string>();
1231
+ for (const f of files) {
1232
+ for (const seg of f.toLowerCase().split(/[/\\._-]/)) {
1233
+ if (seg.length > 3 && !PATH_NOISE.has(seg)) terms.add(seg);
1234
+ }
1235
+ }
1236
+ return [...terms];
1237
+ };
1238
+
1239
+ let warnTerms = focusTerms;
1240
+ let warnLabel = focus;
1241
+ let derived = false;
1242
+ if (warnTerms.length === 0 && lastSession) {
1243
+ const fromFiles = deriveTermsFromFiles(lastSession.files_changed ?? []);
1244
+ const fromSummary = (lastSession.summary ?? '').toLowerCase()
1245
+ .replace(/[^a-z0-9\s:_-]/g, ' ').split(/\s+/).filter(t => t.length > 3 && !PATH_NOISE.has(t));
1246
+ warnTerms = [...new Set([...fromFiles, ...fromSummary])];
1247
+ warnLabel = 'where you left off last session';
1248
+ derived = true;
1249
+ }
1250
+
1251
+ if (warnTerms.length > 0) {
1252
+ const dangerLessons = (lessons as LessonAny[]).filter(l => {
1253
+ if (l.outcome === 'success') return false;
1254
+ const topicCategory = l.topic.split(':')[0];
1255
+ return warnTerms.some(term =>
1256
+ l.topic.toLowerCase().includes(term) ||
1257
+ topicCategory === term ||
1258
+ (l.tags ?? []).some((t: string) => t.toLowerCase() === term),
1259
+ );
1260
+ });
1261
+ if (dangerLessons.length >= 1) {
1262
+ const headline = derived
1263
+ ? ` You're likely to continue **${warnLabel}** — ${dangerLessons.length} known pitfall${dangerLessons.length > 1 ? 's' : ''} there:`
1264
+ : ` Known pitfalls for **"${warnLabel}"** (${dangerLessons.length} past failure${dangerLessons.length > 1 ? 's' : ''}):`;
1265
+ const warning = [
1266
+ `🚨 **PRE-WARNING** — Read this BEFORE starting:`,
1267
+ headline,
1268
+ ...dangerLessons.slice(0, 3).map(l => ` ❌ \`${l.topic}\` — ${(l.what_failed ?? l.what_worked).slice(0, 80)}`),
1269
+ '',
1270
+ ];
1271
+ lines.splice(2, 0, ...warning); // after '🧠 **Session Briefing**' + empty line
1272
+ }
1089
1273
  }
1090
1274
  }
1091
1275
 
@@ -64,13 +64,18 @@ export async function handleContextTool(
64
64
  if (ttl && ttl > 0) {
65
65
  body['expires_at'] = new Date(Date.now() + ttl * 1000).toISOString();
66
66
  }
67
- await fetch(`${vectorUrl}/entries`, {
67
+ const res = await fetch(`${vectorUrl}/entries`, {
68
68
  method: 'POST',
69
69
  headers: { 'Content-Type': 'application/json' },
70
70
  body: JSON.stringify(body),
71
- }).catch(() => undefined);
72
- } catch {
73
- // Embedding optional — continue silently
71
+ signal: AbortSignal.timeout(8000),
72
+ });
73
+ if (!res.ok) {
74
+ process.stderr.write(`[cachly] semantic index skipped (HTTP ${res.status}) — smart_recall on this context will be keyword-only\n`);
75
+ }
76
+ } catch (e) {
77
+ // Embedding is optional — keyword recall still works. Log so it's debuggable.
78
+ process.stderr.write(`[cachly] semantic index failed: ${e instanceof Error ? e.message : String(e)} — smart_recall on this context will be keyword-only\n`);
74
79
  }
75
80
  }
76
81
 
@@ -9,6 +9,9 @@ import { keywordSearch, tokenize } from '../search.js';
9
9
  import { safeJsonParse } from '../utils.js';
10
10
  import { computeEmbedding, hasEmbedProvider } from '../embeddings.js';
11
11
 
12
+ // Last brain_from_git category counts — set after each run so index.ts can include them in telemetry
13
+ export let _lastBrainFromGitCounts: { fixes: number; features: number; refactors: number; total: number } | null = null;
14
+
12
15
  // Git concurrency semaphore (for brain_from_git parallel workers)
13
16
  let _gitSemCount = 0;
14
17
  const _GIT_SEM_MAX = 10;
@@ -363,8 +366,6 @@ export async function handleFedbrainTool(
363
366
 
364
367
  if (hooksArr.includes('ci')) {
365
368
  lines.push(`### GitHub Actions CI outcome hook`);
366
- lines.push(`**Setup:** add a repo secret \`CACHLY_CACHE_TOKEN\` = your instance's cache token`);
367
- lines.push(`(find it via \`get_connection_string\` — the token in the cache URL).`);
368
369
  lines.push(`**Add at the end of each job** (after build/test steps):`);
369
370
  lines.push('```yaml');
370
371
  lines.push(`- name: cachly CLS — record CI outcome`);
@@ -372,14 +373,15 @@ export async function handleFedbrainTool(
372
373
  lines.push(` run: |`);
373
374
  lines.push(` node -e "`);
374
375
  lines.push(` const r=require('https');`);
375
- lines.push(` const d=JSON.stringify({source:'ci_outcome',payload:{`);
376
+ lines.push(` const d=JSON.stringify({instance_id:'${instance_id}',source:'ci_outcome',payload:{`);
376
377
  lines.push(` status:'\${{ job.status }}',prev_status:'unknown',job:'\${{ github.job }}',`);
377
378
  lines.push(` context:'github-actions run \${{ github.run_number }}'}});`);
378
- lines.push(` r.request({hostname:'api.cachly.dev',path:'/v1/cache/'+process.env.CACHLY_CACHE_TOKEN+'/cls/ingest',method:'POST',`);
379
- lines.push(` headers:{'Content-Type':'application/json','Content-Length':d.length}},()=>{}).end(d);`);
379
+ lines.push(` r.request({hostname:'api.cachly.dev',path:'/api/v1/cls/ingest',method:'POST',`);
380
+ lines.push(` headers:{'Content-Type':'application/json','Authorization':'Bearer $CACHLY_JWT',`);
381
+ lines.push(` 'Content-Length':d.length}},()=>{}).end(d);`);
380
382
  lines.push(` " 2>/dev/null || true`);
381
383
  lines.push(` env:`);
382
- lines.push(` CACHLY_CACHE_TOKEN: \${{ secrets.CACHLY_CACHE_TOKEN }}`);
384
+ lines.push(` CACHLY_JWT: \${{ secrets.CACHLY_JWT }}`);
383
385
  lines.push('```');
384
386
  lines.push('');
385
387
  }
@@ -1013,11 +1015,16 @@ export async function handleFedbrainTool(
1013
1015
  // Only store if no existing lesson for this topic (avoid overwriting higher-confidence lessons)
1014
1016
  const existing = await redis.get(`cachly:lesson:best:${topic}`);
1015
1017
  if (!existing) {
1016
- await redis.set(`cachly:lesson:best:${topic}`, JSON.stringify(lessonObj));
1018
+ // Auto-inferred git lessons get a 90-day TTL — they self-refresh on the next
1019
+ // brain_from_git run, and shouldn't pin memory forever if the repo goes stale.
1020
+ await redis.set(`cachly:lesson:best:${topic}`, JSON.stringify(lessonObj), 'EX', 90 * 86400);
1017
1021
  await redis.rpush(`cachly:lessons:${topic}`, JSON.stringify(lessonObj));
1022
+ await redis.ltrim(`cachly:lessons:${topic}`, -100, -1);
1023
+ await redis.expire(`cachly:lessons:${topic}`, 90 * 86400);
1018
1024
  }
1019
1025
  await redis.rpush('cachly:lessons:brain_from_git:all', JSON.stringify({ topic, sha: commit.sha.slice(0, 8), subject: commit.subject.slice(0, 60) }));
1020
1026
  await redis.ltrim('cachly:lessons:brain_from_git:all', -500, -1);
1027
+ await redis.expire('cachly:lessons:brain_from_git:all', 90 * 86400);
1021
1028
 
1022
1029
  // Update CKG
1023
1030
  const conceptId = ckgSlug(topic);
@@ -1034,6 +1041,13 @@ export async function handleFedbrainTool(
1034
1041
 
1035
1042
  const categoryBreakdown = [...categoryCount.entries()].sort((a, b) => b[1] - a[1]).map(([k, v]) => ` • **${k}** (${v})`).join('\n');
1036
1043
 
1044
+ _lastBrainFromGitCounts = {
1045
+ fixes: categoryCount.get('fix') ?? 0,
1046
+ features: categoryCount.get('feat') ?? 0,
1047
+ refactors: categoryCount.get('refactor') ?? 0,
1048
+ total: ingested,
1049
+ };
1050
+
1037
1051
  return [
1038
1052
  `🔁 **brain_from_git: ${repoDir}**`,
1039
1053
  isIncremental ? `🔄 Incremental mode — only new commits since \`${lastSha.slice(0, 8)}\` were processed` : '',
@@ -157,9 +157,12 @@ export async function handleRoadmapTool(
157
157
  const PRIORITY_ORDER: Record<string, number> = { critical: 0, high: 1, medium: 2, low: 3 };
158
158
  const PRIORITY_ICON: Record<string, string> = { critical: '🔴', high: '🟠', medium: '🟡', low: '🔵' };
159
159
  const items = Object.values(all)
160
- .map(v => JSON.parse(v as string) as Record<string, unknown>)
160
+ .flatMap(v => {
161
+ const item = safeJsonParse<Record<string, unknown> | null>(v as string, null);
162
+ return item ? [item] : [];
163
+ })
161
164
  .filter(i => i.status === 'in-progress' || i.status === 'planned')
162
- .filter(i => !filterTag || (i.tags as string[]).includes(filterTag));
165
+ .filter(i => !filterTag || (Array.isArray(i.tags) ? i.tags : []).includes(filterTag));
163
166
  if (items.length === 0) return '🎉 **No open roadmap items!** All tasks are done (or use `roadmap_list` to check).';
164
167
  // in-progress first, then by priority
165
168
  items.sort((a, b) => {
@@ -169,7 +172,8 @@ export async function handleRoadmapTool(
169
172
  });
170
173
  const next = items[0];
171
174
  const remaining = items.length - 1;
172
- const tags = (next.tags as string[]).length ? `\nTags: ${(next.tags as string[]).join(', ')}` : '';
175
+ const nextTags = Array.isArray(next.tags) ? next.tags as string[] : [];
176
+ const tags = nextTags.length ? `\nTags: ${nextTags.join(', ')}` : '';
173
177
  const milestone = next.milestone ? `\nMilestone: ${next.milestone}` : '';
174
178
  const notes = next.notes ? `\nNotes: ${(next.notes as string).split('\n').pop()?.slice(0, 120)}` : '';
175
179
  return [