@cachly-dev/mcp-server 0.10.55 → 0.10.57

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 (329) 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/src/handlers/advanced.js.map +1 -0
  184. package/dist/{handlers → src/handlers}/brain.js +335 -168
  185. package/dist/src/handlers/brain.js.map +1 -0
  186. package/dist/src/handlers/cache.js.map +1 -0
  187. package/dist/{handlers → src/handlers}/context.js +9 -4
  188. package/dist/src/handlers/context.js.map +1 -0
  189. package/dist/{handlers → src/handlers}/fedbrain.d.ts +6 -0
  190. package/dist/{handlers → src/handlers}/fedbrain.js +19 -7
  191. package/dist/src/handlers/fedbrain.js.map +1 -0
  192. package/dist/src/handlers/instances.js.map +1 -0
  193. package/dist/{handlers → src/handlers}/roadmap.js +7 -3
  194. package/dist/src/handlers/roadmap.js.map +1 -0
  195. package/dist/src/handlers/syndicate.js.map +1 -0
  196. package/dist/src/handlers/tco.d.ts +16 -0
  197. package/dist/src/handlers/tco.js +303 -0
  198. package/dist/src/handlers/tco.js.map +1 -0
  199. package/dist/{handlers → src/handlers}/team.js +57 -7
  200. package/dist/src/handlers/team.js.map +1 -0
  201. package/dist/{index.js → src/index.js} +209 -420
  202. package/dist/src/index.js.map +1 -0
  203. package/dist/src/namespace.js.map +1 -0
  204. package/dist/src/notifier.d.ts +5 -0
  205. package/dist/src/notifier.js +6 -0
  206. package/dist/src/notifier.js.map +1 -0
  207. package/dist/src/rerank.d.ts +55 -0
  208. package/dist/src/rerank.js +111 -0
  209. package/dist/src/rerank.js.map +1 -0
  210. package/dist/src/search.js.map +1 -0
  211. package/dist/src/search.test.d.ts +1 -0
  212. package/dist/src/search.test.js.map +1 -0
  213. package/dist/{tools.d.ts → src/tools.d.ts} +226 -0
  214. package/dist/{tools.js → src/tools.js} +196 -0
  215. package/dist/src/tools.js.map +1 -0
  216. package/dist/src/utils.js.map +1 -0
  217. package/package.json +8 -7
  218. package/packages/telegram-notify/SPLIT_PLAN.md +106 -0
  219. package/packages/telegram-notify/client.ts +88 -0
  220. package/packages/telegram-notify/emojis.json +22 -0
  221. package/packages/telegram-notify/pyproject.toml +17 -0
  222. package/packages/telegram-notify/telegram_notify/__init__.py +3 -0
  223. package/packages/telegram-notify/telegram_notify/client.py +87 -0
  224. package/packages/telegram-notify/telegram_notify/emojis.json +22 -0
  225. package/scripts/generate-icons.js +28 -0
  226. package/server.json +3 -3
  227. package/smithery.yaml +21 -8
  228. package/src/__tests__/brain-flow.test.ts +174 -0
  229. package/src/__tests__/handlers-unit.test.ts +1 -2
  230. package/src/__tests__/rerank.test.ts +130 -0
  231. package/src/bench/cachly-bench.ts +217 -0
  232. package/src/bench/fixtures.ts +256 -0
  233. package/src/handlers/brain.ts +345 -172
  234. package/src/handlers/context.ts +9 -4
  235. package/src/handlers/fedbrain.ts +21 -7
  236. package/src/handlers/roadmap.ts +7 -3
  237. package/src/handlers/tco.ts +347 -0
  238. package/src/handlers/team.ts +67 -7
  239. package/src/index.ts +224 -406
  240. package/src/notifier.ts +5 -0
  241. package/src/rerank.ts +148 -0
  242. package/src/tools.ts +210 -0
  243. package/tsconfig.json +2 -3
  244. package/dist/__tests__/auth.test.js.map +0 -1
  245. package/dist/__tests__/brain-flow.test.js.map +0 -1
  246. package/dist/__tests__/brain-predict-failures.test.d.ts +0 -17
  247. package/dist/__tests__/brain-predict-failures.test.js +0 -339
  248. package/dist/__tests__/brain-predict-failures.test.js.map +0 -1
  249. package/dist/__tests__/confidence.test.js.map +0 -1
  250. package/dist/__tests__/e2e/api-auth.test.js.map +0 -1
  251. package/dist/__tests__/e2e/api-health.test.js.map +0 -1
  252. package/dist/__tests__/e2e/cli.test.js.map +0 -1
  253. package/dist/__tests__/e2e/config.js.map +0 -1
  254. package/dist/__tests__/e2e/funnel.test.js.map +0 -1
  255. package/dist/__tests__/e2e/mcp-tools.test.js.map +0 -1
  256. package/dist/__tests__/e2e/provisioning.test.js.map +0 -1
  257. package/dist/__tests__/handlers-unit.test.js.map +0 -1
  258. package/dist/__tests__/namespace.test.js.map +0 -1
  259. package/dist/auth.js.map +0 -1
  260. package/dist/ckg.js.map +0 -1
  261. package/dist/confidence.js.map +0 -1
  262. package/dist/embeddings.js.map +0 -1
  263. package/dist/handlers/advanced.js.map +0 -1
  264. package/dist/handlers/brain.js.map +0 -1
  265. package/dist/handlers/cache.js.map +0 -1
  266. package/dist/handlers/context.js.map +0 -1
  267. package/dist/handlers/fedbrain.js.map +0 -1
  268. package/dist/handlers/instances.js.map +0 -1
  269. package/dist/handlers/roadmap.js.map +0 -1
  270. package/dist/handlers/syndicate.js.map +0 -1
  271. package/dist/handlers/team.js.map +0 -1
  272. package/dist/index.js.map +0 -1
  273. package/dist/namespace.js.map +0 -1
  274. package/dist/search.js.map +0 -1
  275. package/dist/search.test.js.map +0 -1
  276. package/dist/tools.js.map +0 -1
  277. package/dist/utils.js.map +0 -1
  278. package/src/__tests__/brain-predict-failures.test.ts +0 -430
  279. /package/dist/{__tests__ → src/__tests__}/auth.test.d.ts +0 -0
  280. /package/dist/{__tests__ → src/__tests__}/auth.test.js +0 -0
  281. /package/dist/{__tests__ → src/__tests__}/brain-flow.test.d.ts +0 -0
  282. /package/dist/{__tests__ → src/__tests__}/confidence.test.d.ts +0 -0
  283. /package/dist/{__tests__ → src/__tests__}/confidence.test.js +0 -0
  284. /package/dist/{__tests__ → src/__tests__}/e2e/api-auth.test.d.ts +0 -0
  285. /package/dist/{__tests__ → src/__tests__}/e2e/api-auth.test.js +0 -0
  286. /package/dist/{__tests__ → src/__tests__}/e2e/api-health.test.d.ts +0 -0
  287. /package/dist/{__tests__ → src/__tests__}/e2e/api-health.test.js +0 -0
  288. /package/dist/{__tests__ → src/__tests__}/e2e/cli.test.d.ts +0 -0
  289. /package/dist/{__tests__ → src/__tests__}/e2e/cli.test.js +0 -0
  290. /package/dist/{__tests__ → src/__tests__}/e2e/config.d.ts +0 -0
  291. /package/dist/{__tests__ → src/__tests__}/e2e/config.js +0 -0
  292. /package/dist/{__tests__ → src/__tests__}/e2e/funnel.test.d.ts +0 -0
  293. /package/dist/{__tests__ → src/__tests__}/e2e/funnel.test.js +0 -0
  294. /package/dist/{__tests__ → src/__tests__}/e2e/mcp-tools.test.d.ts +0 -0
  295. /package/dist/{__tests__ → src/__tests__}/e2e/mcp-tools.test.js +0 -0
  296. /package/dist/{__tests__ → src/__tests__}/e2e/provisioning.test.d.ts +0 -0
  297. /package/dist/{__tests__ → src/__tests__}/e2e/provisioning.test.js +0 -0
  298. /package/dist/{__tests__ → src/__tests__}/handlers-unit.test.d.ts +0 -0
  299. /package/dist/{__tests__ → src/__tests__}/namespace.test.d.ts +0 -0
  300. /package/dist/{__tests__ → src/__tests__}/namespace.test.js +0 -0
  301. /package/dist/{search.test.d.ts → src/__tests__/rerank.test.d.ts} +0 -0
  302. /package/dist/{auth.d.ts → src/auth.d.ts} +0 -0
  303. /package/dist/{auth.js → src/auth.js} +0 -0
  304. /package/dist/{ckg.d.ts → src/ckg.d.ts} +0 -0
  305. /package/dist/{ckg.js → src/ckg.js} +0 -0
  306. /package/dist/{confidence.d.ts → src/confidence.d.ts} +0 -0
  307. /package/dist/{confidence.js → src/confidence.js} +0 -0
  308. /package/dist/{embeddings.d.ts → src/embeddings.d.ts} +0 -0
  309. /package/dist/{embeddings.js → src/embeddings.js} +0 -0
  310. /package/dist/{handlers → src/handlers}/advanced.d.ts +0 -0
  311. /package/dist/{handlers → src/handlers}/advanced.js +0 -0
  312. /package/dist/{handlers → src/handlers}/brain.d.ts +0 -0
  313. /package/dist/{handlers → src/handlers}/cache.d.ts +0 -0
  314. /package/dist/{handlers → src/handlers}/cache.js +0 -0
  315. /package/dist/{handlers → src/handlers}/context.d.ts +0 -0
  316. /package/dist/{handlers → src/handlers}/instances.d.ts +0 -0
  317. /package/dist/{handlers → src/handlers}/instances.js +0 -0
  318. /package/dist/{handlers → src/handlers}/roadmap.d.ts +0 -0
  319. /package/dist/{handlers → src/handlers}/syndicate.d.ts +0 -0
  320. /package/dist/{handlers → src/handlers}/syndicate.js +0 -0
  321. /package/dist/{handlers → src/handlers}/team.d.ts +0 -0
  322. /package/dist/{index.d.ts → src/index.d.ts} +0 -0
  323. /package/dist/{namespace.d.ts → src/namespace.d.ts} +0 -0
  324. /package/dist/{namespace.js → src/namespace.js} +0 -0
  325. /package/dist/{search.d.ts → src/search.d.ts} +0 -0
  326. /package/dist/{search.js → src/search.js} +0 -0
  327. /package/dist/{search.test.js → src/search.test.js} +0 -0
  328. /package/dist/{utils.d.ts → src/utils.d.ts} +0 -0
  329. /package/dist/{utils.js → src/utils.js} +0 -0
@@ -7,61 +7,43 @@ import { safeJsonParse } from '../utils.js';
7
7
  import type { CKGEdge, CKGNode } from '../ckg.js';
8
8
  import { keywordSearch, tokenize, splitMultiQuery, levenshtein,
9
9
  indexVocab as _indexVocab } from '../search.js';
10
- import type { KeywordMatch } from '../search.js';
10
+ import { rerankByQuality } from '../rerank.js';
11
11
  import { computeEmbedding, hasEmbedProvider } from '../embeddings.js';
12
12
 
13
13
  // ── Changelog (shown once per version in session_start) ──────────────────────
14
- const MCP_VERSION = '0.10.55';
14
+ const MCP_VERSION = '0.10.57';
15
15
  const WHATS_NEW: Record<string, string[]> = {
16
- '0.10.55': [
17
- `🆕 **What's new in cachly MCP v0.10.55:**`,
18
- ` ⚡ UX: API failure now shows a clear error instead of silently blocking for 30s`,
19
- ` ⚡ UX: provisioning wait reduced from 25s → 8s (Brain is usually ready sooner)`,
20
- ` 🔧 VS Code: setup spinner is now cancellable — dismiss and MCP auto-connects later`,
21
- ` 🧠 VS Code: Brain Health panel shows offline-queued lessons with pending sync count`,
22
- ],
23
- '0.10.54': [
24
- `🆕 **What's new in cachly MCP v${MCP_VERSION}:**`,
25
- ` 👀 Perf: first tool call no longer blocked by auto-session (was freezing for 30-120s on large repos)`,
26
- ` 👀 Perf: git history bootstrap runs in background — session_start responds instantly`,
27
- ` 🐞 Fixed: VS Code lessons now include author attribution (broken since launch)`,
28
- ],
29
- '0.10.53': [
30
- `🆕 **What's new in cachly MCP v${MCP_VERSION}:**`,
31
- ` 🐛 Fixed: Brain bootstrap summary now visible in session_start (was silently discarded)`,
32
- ` 🐛 Fixed: team lessons now indexed correctly (\`ts\` field was stored as \`timestamp\`)`,
33
- ` ✨ New: proactive follow-up hints after \`learn_from_attempts\` (CKG co-occurrence patterns)`,
34
- ` ✨ New: author name attribution for VS Code lessons (set \`cachly.authorName\` in VS Code settings)`,
16
+ '0.10.57': [
17
+ `🆕 **What's new in v${MCP_VERSION}:**`,
18
+ ` 🔧 **Critical fix** — corrected the npm bin/main entry path (\`dist/src/index.js\`); 0.10.50–0.10.52 shipped a broken entry point`,
19
+ ` 🔧 Version hygiene — synced package.json, server.json, lockfile, MCP_VERSION; \`latest\` now points forward again`,
20
+ ` ✅ Accurate tool count (95) across README, server.json, CLI banners`,
21
+ ` ✅ Includes all Phase 2/3 work: quality reranking, Cachly-Bench, hybrid + CKG recall, team_confirm governance`,
35
22
  ],
36
23
  '0.10.52': [
37
- `🆕 **What's new in cachly MCP v0.10.52:**`,
38
- ` 🚀 Zero-click setup: Brain activates automatically on first call (no auth prompt needed)`,
39
- ` 🐛 Fixed: all Brain connections now use decrypted passwords (resolves AUTH errors for all users)`,
24
+ ` ✅ \`smart_recall\` — CKG traversal as Layer 3: finds lessons that FIXED causal-graph-similar problems (🕸️ causal graph badge)`,
25
+ ` ✅ Bench corpus expanded: governance adversarial pair proves review-boost end-to-end (+22% P@1 headline)`,
26
+ ` 🔧 Import cleanup: unused symbols removed from brain.ts (no functional change)`,
27
+ ` 💡 \`smart_recall\` now shows keyword / semantic / CKG / hybrid match type for every result`,
40
28
  ],
41
- '0.10.50': [
42
- `🆕 **What's new in cachly MCP v0.10.50:**`,
43
- ` 🐛 Fixed: Brain connection now works out of the box (was failing with AUTH error)`,
44
- ` 🐛 Fixed: TLS defaults to off for free-tier instances (no more TLS handshake failure)`,
45
- ` ✅ \`get_api_status\` correctly shows API key type instead of "invalid JWT format"`,
29
+ '0.10.51': [
30
+ ` ✅ \`team_confirm\` — human review raises lesson recall ranking (🛡️ senior / ✔️ peer)`,
31
+ ` ✅ Governance-aware recall — confirmed lessons outrank unreviewed auto-learned entries`,
46
32
  ],
47
- '0.10.46': [
48
- `🆕 **What's new in v0.10.46:**`,
49
- ` ✅ \`brain_from_git\` — auto-learns from your full git history on setup`,
50
- ` ✅ \`smart_recall\` — semantic + keyword hybrid search, 3× more relevant results`,
51
- ` ✅ VS Code Extension — Brain persists across all projects, zero config`,
52
- ` ✅ Device-flow auth — zero-config login, no JWT needed in editor config`,
53
- ` ✅ Auto-provision — Brain instance created automatically after first login`,
54
- ` ✅ JWT now persisted to ~/.claude/mcp.json — survives editor restarts`,
55
- ` ✅ Confidence scoring — every recalled fact shows staleness + reliability`,
56
- ` ✅ CKG (Causal Knowledge Graph) — root-cause chains between bugs + fixes`,
33
+ '0.10.50': [
34
+ ` ✅ \`smart_recall\` — unified keyword + semantic hybrid list (one ranked result, not two sections)`,
35
+ ` ✅ \`smart_recall\` — "Brain saved you here" banner surfaces time saved inline per proven lesson`,
36
+ ` ✅ Quality reranking — proven successes outrank symptom-dense failures (+11% Precision@1)`,
37
+ ` ✅ Cachly-Bench — IR benchmark with CI regression guard (see BENCH.md for the proof)`,
38
+ ` 🔧 Contradiction resolution now persisted to Redis (90-day TTL)`,
39
+ ` 💡 Run \`smart_recall\` to see your Brain's recall quality in action`,
57
40
  ],
58
- '0.10.0': [
59
- `🆕 **What's new in v0.10.0:**`,
60
- ` ✅ Zero-friction setup — auto-provisions your Brain, no manual config needed`,
61
- ` ✅ 10× faster briefings — batch Redis fetches replace per-key loops`,
62
- ` ✅ Crash-proof data reads — corrupted cache entries no longer break tools`,
63
- ` ✅ \`session_end\` warns when called without an active session`,
64
- ` ✅ \`get_api_status\` now shows live Redis ping + all instance statuses`,
41
+ '0.10.49': [
42
+ ` ✅ \`brain_from_git\` — auto-seed your Brain from git history in seconds`,
43
+ ` ✅ \`brain_predict_failures\` — predict CI/build failures before they happen`,
44
+ ` ✅ VS Code extension — Brain status bar, ambient learning, CodeLens hints`,
45
+ ` ✅ Team Brain — share lessons across your whole engineering team`,
46
+ ` 🔧 Stability — auto-expiring memory keys, longer provisioning wait, robust recall`,
65
47
  ],
66
48
  };
67
49
 
@@ -179,6 +161,18 @@ export async function handleBrainTool(
179
161
  const resId = ckgSlug(`resolution:${topic}`);
180
162
  await ckgUpdateEdge(redis, cId, 'contradicts', resId, false);
181
163
  } catch { /* non-critical */ }
164
+ // Persist a queryable resolution record — so contradiction history survives
165
+ // (CKG edges alone aren't easily auditable). Bounded + TTL'd like other history.
166
+ try {
167
+ const resolutionKey = `cachly:contradictions:${topic}`;
168
+ const direction = prev.outcome === 'success' && outcome === 'failure'
169
+ ? 'kept-success' : 'overwrote-failure';
170
+ await redis.rpush(resolutionKey, JSON.stringify({
171
+ ts, prev_outcome: prev.outcome, new_outcome: outcome, direction,
172
+ }));
173
+ await redis.ltrim(resolutionKey, -50, -1);
174
+ await redis.expire(resolutionKey, 180 * 86400);
175
+ } catch { /* non-critical */ }
182
176
  contradictionWarning.push(`🗳️ Run \`madc_deliberate(topic="${topic}")\` to resolve via expert agent voting.`);
183
177
  }
184
178
  } catch { /* ignore parse error */ }
@@ -224,7 +218,7 @@ export async function handleBrainTool(
224
218
  const existing = await redis.get(depKey);
225
219
  const depTopics: string[] = safeJsonParse<string[]>(existing, []);
226
220
  if (!depTopics.includes(topic)) depTopics.push(topic);
227
- await redis.set(depKey, JSON.stringify(depTopics), 'EX', 365 * 86400); // 1-year TTL
221
+ await redis.set(depKey, JSON.stringify(depTopics), 'EX', 90 * 86400);
228
222
  }
229
223
 
230
224
  const lessonObj = {
@@ -248,11 +242,11 @@ export async function handleBrainTool(
248
242
  };
249
243
  const lesson = JSON.stringify(lessonObj);
250
244
 
251
- // Always append to the history list (audit log); cap at 200 entries to prevent unbounded growth
245
+ // Always append to the history list (audit log); keep last 100 entries, 90-day TTL
252
246
  const listKey = `cachly:lessons:${topic}`;
253
247
  await redis.rpush(listKey, lesson);
254
- await redis.ltrim(listKey, -200, -1);
255
- await redis.expire(listKey, 365 * 86400); // 1-year TTL
248
+ await redis.ltrim(listKey, -100, -1);
249
+ await redis.expire(listKey, 90 * 86400);
256
250
 
257
251
  // Update best key for success/partial; for failure only update if no success exists
258
252
  if (outcome === 'success' || outcome === 'partial') {
@@ -385,44 +379,6 @@ export async function handleBrainTool(
385
379
  }
386
380
  } catch { /* CKG updates are non-critical */ }
387
381
 
388
- // ── Proactive follow-up suggestion (CKG co-occurrence patterns) ─────────
389
- // After a successful fix, check if related concepts commonly follow it.
390
- // Uses CKG co-occurs edges with confidence > 0.4 and at least 2 trials.
391
- const proactiveSuggestions: string[] = [];
392
- if (outcome === 'success' || outcome === 'partial') {
393
- try {
394
- const conceptId = ckgSlug(topic);
395
- const fromEdgeKeys = await redis.smembers(`cachly:ckg:idx:from:${conceptId}`);
396
- for (const ek of fromEdgeKeys.slice(0, 20)) {
397
- const er = await redis.get(ek);
398
- if (!er) continue;
399
- const edge = safeJsonParse<CKGEdge | null>(er, null);
400
- if (!edge || edge.confidence < 0.4 || (edge.trials ?? 0) < 2) continue;
401
- if (edge.edgeType !== 'co-occurs' && edge.edgeType !== 'causes') continue;
402
- // Resolve related concept to a lesson key
403
- const relTopic = edge.to
404
- .replace(/^cause:/, '')
405
- .replace(/^tag:/, '')
406
- .replace(/^problem:/, '')
407
- .replace(/-/g, ':')
408
- .replace(/_/g, '-');
409
- const relRaw = await redis.get(`cachly:lesson:best:${relTopic}`);
410
- if (!relRaw) continue;
411
- const relLesson = safeJsonParse<{ topic?: string; what_worked?: string } | null>(relRaw, null);
412
- const displayTopic = relLesson?.topic ?? relTopic;
413
- if (!relLesson || !relLesson.what_worked || displayTopic === topic) continue;
414
- const pct = Math.round(edge.confidence * 100);
415
- if (proactiveSuggestions.length === 0) proactiveSuggestions.push('');
416
- proactiveSuggestions.push(
417
- `🔮 **Pattern detected:** \`${displayTopic}\` tends to follow this (${pct}% co-occurrence across ${edge.trials} sessions)`,
418
- ` → ${relLesson.what_worked.slice(0, 100)}${relLesson.what_worked.length > 100 ? '…' : ''}`,
419
- ` → Pre-empt it now: \`recall_best_solution(topic="${displayTopic}")\``,
420
- );
421
- break; // one proactive hint is enough
422
- }
423
- } catch { /* non-critical */ }
424
- }
425
-
426
382
  const emoji = outcome === 'success' ? '✅' : outcome === 'partial' ? '⚠️' : '❌';
427
383
  const sevEmoji = severity === 'critical' ? '🔴' : severity === 'major' ? '🟡' : '🟢';
428
384
  const action = isUpdate ? 'updated' : 'stored';
@@ -445,7 +401,6 @@ export async function handleBrainTool(
445
401
  : '',
446
402
  ...templateWarnings,
447
403
  ...iWasWrongWarning,
448
- ...proactiveSuggestions,
449
404
  ].filter(l => l !== '').join('\n');
450
405
  }
451
406
 
@@ -499,8 +454,27 @@ export async function handleBrainTool(
499
454
  ? `📌 **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.`
500
455
  : '';
501
456
 
457
+ // ── Trust signal (today-safe consensus layer) ───────────────────────
458
+ // A lesson recalled many times — or confirmed by multiple distinct
459
+ // authors — has proven its value. Surface that as social proof.
460
+ const lessonAuthors = (lesson as { authors?: string[]; author?: string }).authors
461
+ ?? ((lesson as { author?: string }).author ? [(lesson as { author?: string }).author!] : []);
462
+ const distinctAuthors = [...new Set(lessonAuthors.filter(Boolean))];
463
+ const rc = updatedLesson.recall_count;
464
+ let trustBadge = '';
465
+ if (distinctAuthors.length >= 2 && rc >= 5) {
466
+ trustBadge = `🏆 **Battle-tested** — recalled ${rc}× · verified by ${distinctAuthors.length} developers. Trust this.`;
467
+ } else if (rc >= 10) {
468
+ trustBadge = `🏆 **Battle-tested** — recalled ${rc}×. This is one of your most-proven solutions.`;
469
+ } else if (distinctAuthors.length >= 2) {
470
+ trustBadge = `✅ **Team-verified** — confirmed by ${distinctAuthors.length} developers.`;
471
+ } else if (rc >= 5) {
472
+ trustBadge = `✅ **Proven** — recalled ${rc}× without contradiction.`;
473
+ }
474
+
502
475
  return [
503
476
  rememberWhen,
477
+ trustBadge,
504
478
  `${badge} **Best solution for \`${topic}\`** ${sevEmoji}${lesson.severity ? ` (${lesson.severity})` : ''} · recalled ${updatedLesson.recall_count}×`,
505
479
  ``,
506
480
  `**What worked:** ${lesson.what_worked}`,
@@ -558,65 +532,48 @@ export async function handleBrainTool(
558
532
  const redis = await getConnection(instance_id);
559
533
 
560
534
  // ── Layer 1: Keyword search across ALL brain data (always works, no embedding) ──
561
- const kwMatches = await keywordSearch(
535
+ const rawMatches = await keywordSearch(
562
536
  redis,
563
537
  ['cachly:ctx:*', 'cachly:lesson:best:*', 'cachly:idx:*'],
564
538
  query,
565
539
  10,
566
540
  );
567
541
 
568
- const lines: string[] = [`🧠 **Smart Recall** for: _"${query}"_\n`];
569
-
570
- // Show sub-query info if multi-topic was detected
571
- const subQueries = splitMultiQuery(query);
572
- if (subQueries.length > 1) {
573
- lines.push(`_Detected ${subQueries.length} sub-topics:_ ${subQueries.map((s, i) => `${i + 1}. "${s}"`).join(', ')}\n`);
574
- }
575
-
576
- if (kwMatches.length > 0) {
577
- lines.push(`### 🔍 BM25 Matches (${kwMatches.length})\n`);
542
+ // ── Layer 1.5: Quality-aware rerank — proven success lessons outrank
543
+ // text-similar failed attempts (the moat; see src/rerank.ts + Cachly-Bench). ──
544
+ const kwMatches = rerankByQuality(rawMatches);
578
545
 
579
- // Group by sub-query if multi-topic
580
- if (subQueries.length > 1) {
581
- const grouped = new Map<string, KeywordMatch[]>();
582
- for (const m of kwMatches.slice(0, 12)) {
583
- const sq = m.subQuery ?? query;
584
- if (!grouped.has(sq)) grouped.set(sq, []);
585
- grouped.get(sq)!.push(m);
586
- }
587
- for (const [sq, matches] of grouped) {
588
- lines.push(`**Topic: "${sq}"** (${matches.length} results)\n`);
589
- for (const m of matches.slice(0, 4)) {
590
- const label = m.key
591
- .replace('cachly:ctx:', '📝 ')
592
- .replace('cachly:lesson:best:', '💡 ')
593
- .replace('cachly:idx:', '📂 ');
594
- const preview = m.content.slice(0, 300).replace(/\n/g, ' ');
595
- lines.push(` **${label}** _(BM25: ${m.score.toFixed(2)}, matched: ${m.matchedWords.join(', ')})_`);
596
- lines.push(` > ${preview}${m.content.length > 300 ? '…' : ''}\n`);
546
+ // Increment recall_count on matched lessons (fire-and-forget) + collect "Brain saved you here" signal.
547
+ type RecalledLesson = { topic: string; severity: string; recall_count: number; savedMins: number };
548
+ const savedHere: RecalledLesson[] = [];
549
+ const lessonMatches = kwMatches.filter(m => m.key.startsWith('cachly:lesson:best:'));
550
+ for (const m of lessonMatches.slice(0, 5)) {
551
+ const existing = await redis.get(m.key).catch(() => null);
552
+ if (existing) {
553
+ const lesson = safeJsonParse(existing, null as null | { recall_count?: number; outcome?: string; severity?: string; [k: string]: unknown });
554
+ if (lesson) {
555
+ const updated = { ...lesson, recall_count: (lesson.recall_count ?? 0) + 1, verified_at: new Date().toISOString() };
556
+ redis.set(m.key, JSON.stringify(updated)).catch(() => {});
557
+ const sev = lesson.severity as string;
558
+ const savedMins = sev === 'critical' ? 240 : sev === 'major' ? 60 : 30;
559
+ redis.incrbyfloat(`cachly:stats:time_saved_mins:${instance_id}`, savedMins).catch(() => {});
560
+ // Surface banner for proven successes (recall_count >= 1 means it's been validated)
561
+ if (lesson.outcome === 'success' && (lesson.recall_count ?? 0) >= 1) {
562
+ savedHere.push({
563
+ topic: m.key.replace('cachly:lesson:best:', ''),
564
+ severity: sev ?? 'major',
565
+ recall_count: (lesson.recall_count ?? 0) + 1,
566
+ savedMins,
567
+ });
597
568
  }
598
569
  }
599
- // Summary: which sub-queries had matches
600
- const matched = [...grouped.keys()];
601
- const unmatched = subQueries.filter(sq => !matched.includes(sq));
602
- if (unmatched.length > 0) {
603
- lines.push(`\n⚠️ **No results for:** ${unmatched.map(s => `"${s}"`).join(', ')}`);
604
- }
605
- } else {
606
- for (const m of kwMatches.slice(0, 8)) {
607
- const label = m.key
608
- .replace('cachly:ctx:', '📝 ')
609
- .replace('cachly:lesson:best:', '💡 ')
610
- .replace('cachly:idx:', '📂 ');
611
- const preview = m.content.slice(0, 400).replace(/\n/g, ' ');
612
- lines.push(`**${label}** _(BM25: ${m.score.toFixed(2)}, matched: ${m.matchedWords.join(', ')})_`);
613
- lines.push(`> ${preview}${m.content.length > 400 ? '…' : ''}\n`);
614
- }
615
570
  }
616
571
  }
617
572
 
618
- // ── Layer 2: Semantic search (optional, only if embedding provider + vector_token available) ──
573
+ // ── Layer 2: Semantic search (parallel, optional) ────────────────────────
619
574
  const inst = await apiFetch<Instance>(`/api/v1/instances/${instance_id}`);
575
+ type SemHit = { key: string; similarity: number; content: string };
576
+ const semHits: SemHit[] = [];
620
577
  if (inst.vector_token && hasEmbedProvider()) {
621
578
  try {
622
579
  const embedding = await computeEmbedding(query);
@@ -626,22 +583,15 @@ export async function handleBrainTool(
626
583
  headers: { 'Content-Type': 'application/json' },
627
584
  body: JSON.stringify({ embedding, namespace: 'cachly:ctx', threshold, top_k: 5 }),
628
585
  });
629
-
630
586
  if (searchRes.ok) {
631
587
  const results = (await searchRes.json()) as SemanticSearchResponse[];
632
- const semHits = results.filter(r => r.found && r.id);
633
- if (semHits.length > 0) {
634
- lines.push(`\n### 🎯 Semantic Matches (${semHits.length})\n`);
635
- for (const hit of semHits) {
636
- const parts = hit.id!.replace('ctx:', '').split(':');
637
- const category = parts[0];
638
- const key = parts.slice(1).join(':');
639
- const content = await redis.get(`cachly:ctx:${category}:${key}`);
640
- lines.push(
641
- `**${key}** _(${((hit.similarity ?? 0) * 100).toFixed(0)}% similar)_`,
642
- `> ${content?.slice(0, 300) ?? '(evicted)'}${(content?.length ?? 0) > 300 ? '…' : ''}\n`,
643
- );
644
- }
588
+ for (const hit of results.filter(r => r.found && r.id)) {
589
+ const parts = hit.id!.replace('ctx:', '').split(':');
590
+ const category = parts[0];
591
+ const key = parts.slice(1).join(':');
592
+ const redisKey = `cachly:ctx:${category}:${key}`;
593
+ const content = await redis.get(redisKey).catch(() => null);
594
+ semHits.push({ key: redisKey, similarity: hit.similarity ?? 0, content: content ?? '(evicted)' });
645
595
  }
646
596
  }
647
597
  } catch {
@@ -649,7 +599,163 @@ export async function handleBrainTool(
649
599
  }
650
600
  }
651
601
 
652
- if (kwMatches.length === 0) {
602
+ // ── Layer 3: Hybrid merge — one unified ranked list ──────────────────────
603
+ // BM25 quality scores (already reranked) are re-normalized to [0,1] for merging.
604
+ type HybridResult = {
605
+ key: string; content: string; hybridScore: number;
606
+ bm25Score?: number; semScore?: number; ckgScore?: number; matchedWords?: string[];
607
+ matchType: 'keyword' | 'semantic' | 'ckg' | 'hybrid'; subQuery?: string;
608
+ };
609
+
610
+ const bm25Scores = kwMatches.map(m => m.score);
611
+ const bm25Min = bm25Scores.length ? Math.min(...bm25Scores) : 0;
612
+ const bm25Range = bm25Scores.length ? (Math.max(...bm25Scores) - bm25Min) || 1 : 1;
613
+ const bm25Norm = (s: number) => (s - bm25Min) / bm25Range;
614
+
615
+ const hybridMap = new Map<string, HybridResult>();
616
+ for (const m of kwMatches) {
617
+ const n = bm25Norm(m.score);
618
+ hybridMap.set(m.key, {
619
+ key: m.key, content: m.content, bm25Score: n,
620
+ hybridScore: n * (semHits.length > 0 ? 0.7 : 1.0),
621
+ matchedWords: m.matchedWords, matchType: 'keyword', subQuery: m.subQuery,
622
+ });
623
+ }
624
+ for (const hit of semHits) {
625
+ const existing = hybridMap.get(hit.key);
626
+ if (existing) {
627
+ existing.semScore = hit.similarity;
628
+ existing.hybridScore = (existing.bm25Score ?? 0) * 0.6 + hit.similarity * 0.4;
629
+ existing.matchType = 'hybrid';
630
+ } else {
631
+ hybridMap.set(hit.key, {
632
+ key: hit.key, content: hit.content, semScore: hit.similarity,
633
+ hybridScore: hit.similarity * 0.5, matchType: 'semantic',
634
+ });
635
+ }
636
+ }
637
+ // ── Layer 3: CKG traversal — follow "fixes" edges to find lessons that solved
638
+ // structurally similar problems, even when vocabulary differs ───────────────
639
+ try {
640
+ const qTokens = tokenize(query).filter(t => t.length >= 4).slice(0, 4);
641
+ const candidateNodeIds = new Set<string>();
642
+ for (const token of qTokens) {
643
+ const nodeStream = redis.scanStream({ match: `cachly:ckg:node:*${token}*`, count: 20 });
644
+ await new Promise<void>((res, rej) => {
645
+ nodeStream.on('data', (batch: string[]) => {
646
+ for (const k of batch.slice(0, 5)) candidateNodeIds.add(k.replace('cachly:ckg:node:', ''));
647
+ });
648
+ nodeStream.on('end', res);
649
+ nodeStream.on('error', rej);
650
+ });
651
+ }
652
+
653
+ const seenFroms = new Set<string>();
654
+ for (const nodeId of [...candidateNodeIds].slice(0, 12)) {
655
+ const inboundEdgeKeys = await redis.smembers(`cachly:ckg:idx:to:${nodeId}`);
656
+ for (const ek of inboundEdgeKeys.slice(0, 6)) {
657
+ const er = await redis.get(ek);
658
+ if (!er) continue;
659
+ const edge = safeJsonParse<CKGEdge | null>(er, null);
660
+ if (!edge || edge.edgeType !== 'fixes' || edge.confidence < 0.35) continue;
661
+ if (seenFroms.has(edge.from)) continue;
662
+ seenFroms.add(edge.from);
663
+
664
+ const lessonKey = `cachly:lesson:best:${edge.from}`;
665
+ const existing = hybridMap.get(lessonKey);
666
+ if (existing) {
667
+ // Already found by BM25/semantic — boost with causal confirmation
668
+ existing.ckgScore = Math.max(existing.ckgScore ?? 0, edge.confidence);
669
+ existing.hybridScore = existing.hybridScore * 0.88 + edge.confidence * 0.12;
670
+ if (existing.matchType === 'keyword') existing.matchType = 'hybrid';
671
+ } else {
672
+ const content = await redis.get(lessonKey).catch(() => null);
673
+ if (!content) continue;
674
+ hybridMap.set(lessonKey, {
675
+ key: lessonKey, content,
676
+ ckgScore: edge.confidence,
677
+ hybridScore: edge.confidence * 0.30, // CKG-only: lower than text hits
678
+ matchType: 'ckg',
679
+ });
680
+ }
681
+ }
682
+ }
683
+ } catch { /* non-critical — keyword + semantic always available */ }
684
+
685
+ const hybridResults = [...hybridMap.values()].sort((a, b) => b.hybridScore - a.hybridScore);
686
+
687
+ // ── Build output ──────────────────────────────────────────────────────────
688
+ const lines: string[] = [`🧠 **Smart Recall** for: _"${query}"_\n`];
689
+
690
+ // "Brain saved you here" banner — surfaces value inline for proven recalled lessons
691
+ const SORDER: Record<string, number> = { critical: 0, major: 1, minor: 2 };
692
+ const topLesson = savedHere.sort((a, b) => (SORDER[a.severity] ?? 1) - (SORDER[b.severity] ?? 1))[0];
693
+ if (topLesson) {
694
+ lines.push(`> 💡 **Brain saved you ~${topLesson.savedMins}m here** — ${topLesson.severity} issue from \`${topLesson.topic}\` recalled ${topLesson.recall_count}× proven\n`);
695
+ }
696
+
697
+ // Show sub-query info if multi-topic was detected
698
+ const subQueries = splitMultiQuery(query);
699
+ if (subQueries.length > 1) {
700
+ lines.push(`_Detected ${subQueries.length} sub-topics:_ ${subQueries.map((s, i) => `${i + 1}. "${s}"`).join(', ')}\n`);
701
+ }
702
+
703
+ if (hybridResults.length > 0) {
704
+ const hasCKG = hybridResults.some(r => r.ckgScore !== undefined);
705
+ const modeLabel = semHits.length > 0 && hasCKG
706
+ ? `keyword + semantic + CKG hybrid`
707
+ : semHits.length > 0
708
+ ? `keyword + semantic hybrid`
709
+ : hasCKG
710
+ ? `keyword + CKG hybrid`
711
+ : `keyword`;
712
+ lines.push(`### 🔍 Results (${hybridResults.length} — ${modeLabel})\n`);
713
+
714
+ // Group by sub-query for multi-topic queries
715
+ if (subQueries.length > 1) {
716
+ const grouped = new Map<string, HybridResult[]>();
717
+ for (const r of hybridResults.slice(0, 12)) {
718
+ const sq = r.subQuery ?? query;
719
+ if (!grouped.has(sq)) grouped.set(sq, []);
720
+ grouped.get(sq)!.push(r);
721
+ }
722
+ for (const [sq, results] of grouped) {
723
+ lines.push(`**Topic: "${sq}"** (${results.length} results)\n`);
724
+ for (const r of results.slice(0, 4)) {
725
+ const label = r.key.replace('cachly:ctx:', '📝 ').replace('cachly:lesson:best:', '💡 ').replace('cachly:idx:', '📂 ');
726
+ const scorePart = r.matchType === 'ckg'
727
+ ? `CKG: ${(r.ckgScore ?? 0).toFixed(2)}, 🕸️ causal graph`
728
+ : r.ckgScore !== undefined && r.bm25Score !== undefined
729
+ ? `BM25: ${(r.bm25Score).toFixed(2)}, CKG: ${(r.ckgScore).toFixed(2)}, 🔀 hybrid`
730
+ : r.matchType === 'hybrid'
731
+ ? `BM25: ${(r.bm25Score ?? 0).toFixed(2)}, sem: ${((r.semScore ?? 0) * 100).toFixed(0)}%, 🔀 hybrid`
732
+ : r.matchType === 'semantic'
733
+ ? `sem: ${((r.semScore ?? 0) * 100).toFixed(0)}%, 🎯 semantic`
734
+ : `BM25: ${(r.bm25Score ?? 0).toFixed(2)}, matched: ${r.matchedWords?.join(', ')}`;
735
+ const preview = r.content.slice(0, 300).replace(/\n/g, ' ');
736
+ lines.push(` **${label}** _(${scorePart})_`);
737
+ lines.push(` > ${preview}${r.content.length > 300 ? '…' : ''}\n`);
738
+ }
739
+ }
740
+ const matched = [...grouped.keys()];
741
+ const unmatched = subQueries.filter(sq => !matched.includes(sq));
742
+ if (unmatched.length > 0) {
743
+ lines.push(`\n⚠️ **No results for:** ${unmatched.map(s => `"${s}"`).join(', ')}`);
744
+ }
745
+ } else {
746
+ for (const r of hybridResults.slice(0, 8)) {
747
+ const label = r.key.replace('cachly:ctx:', '📝 ').replace('cachly:lesson:best:', '💡 ').replace('cachly:idx:', '📂 ');
748
+ const scorePart = r.matchType === 'hybrid'
749
+ ? `BM25: ${(r.bm25Score ?? 0).toFixed(2)}, sem: ${((r.semScore ?? 0) * 100).toFixed(0)}%, 🔀 hybrid`
750
+ : r.matchType === 'semantic'
751
+ ? `sem: ${((r.semScore ?? 0) * 100).toFixed(0)}%, 🎯 semantic`
752
+ : `BM25: ${(r.bm25Score ?? 0).toFixed(2)}, matched: ${r.matchedWords?.join(', ')}`;
753
+ const preview = r.content.slice(0, 400).replace(/\n/g, ' ');
754
+ lines.push(`**${label}** _(${scorePart})_`);
755
+ lines.push(`> ${preview}${r.content.length > 400 ? '…' : ''}\n`);
756
+ }
757
+ }
758
+ } else {
653
759
  lines.push(`⚠️ No matches found for: "${query}"`);
654
760
 
655
761
  // Did-You-Mean: find nearest token in index vocab
@@ -979,6 +1085,31 @@ export async function handleBrainTool(
979
1085
  // Brain health
980
1086
  lines.push(`📊 **Brain:** ${lessons.length} lessons · ${ctxCount} context entries`, '');
981
1087
 
1088
+ // ── 📌 Proven Laws — auto-crystallized lessons (quick win 3) ─────────────
1089
+ // A lesson that has proven itself (recalled ≥ CRYSTALLIZE_RECALLS times) or
1090
+ // was explicitly pinned becomes a "law" — surfaced at the top of every
1091
+ // briefing so the most battle-tested knowledge is never buried. This is
1092
+ // the today-safe version of swarm crystallization: enough confirmations
1093
+ // auto-promote a lesson without any manual step.
1094
+ {
1095
+ type LessonPin = typeof lessons[0] & { pinned?: boolean };
1096
+ const CRYSTALLIZE_RECALLS = 5;
1097
+ const laws = (lessons as LessonPin[])
1098
+ .filter(l => l.outcome === 'success' && (l.pinned === true || (l.recall_count ?? 0) >= CRYSTALLIZE_RECALLS))
1099
+ .sort((a, b) => (b.recall_count ?? 0) - (a.recall_count ?? 0))
1100
+ .slice(0, 5);
1101
+ if (laws.length > 0) {
1102
+ lines.push(`📌 **Proven Laws** (auto-crystallized — your most-trusted knowledge):`);
1103
+ for (const l of laws) {
1104
+ const sev = l.severity === 'critical' ? '🔴' : l.severity === 'major' ? '🟡' : '';
1105
+ const rc = l.recall_count ?? 0;
1106
+ const why = l.pinned === true ? 'pinned' : `recalled ${rc}×`;
1107
+ lines.push(` 🏆${sev} \`${l.topic}\` _(${why})_ — ${l.what_worked.slice(0, 100)}`);
1108
+ }
1109
+ lines.push('');
1110
+ }
1111
+ }
1112
+
982
1113
  // ── Layer 7: MCM Domain Coverage Map ────────────────────────────────────
983
1114
  if (lessons.length >= 3) {
984
1115
  const domainMap = new Map<string, { total: number; success: number; critical: number }>();
@@ -1078,8 +1209,14 @@ export async function handleBrainTool(
1078
1209
  for (const l of topRecalled) {
1079
1210
  const emoji = l.outcome === 'success' ? '✅' : l.outcome === 'partial' ? '⚠️' : '❌';
1080
1211
  const sev = l.severity === 'critical' ? '🔴' : l.severity === 'major' ? '🟡' : '';
1081
- const rct = l.recall_count && l.recall_count > 1 ? ` _(${l.recall_count}× recalled)_` : '';
1082
- lines.push(` ${emoji}${sev} \`${l.topic}\`${rct} — ${l.what_worked.slice(0, 100)}`);
1212
+ const rc = l.recall_count ?? 0;
1213
+ const la = (l as { authors?: string[]; author?: string }).authors
1214
+ ?? ((l as { author?: string }).author ? [(l as { author?: string }).author!] : []);
1215
+ const nAuthors = new Set(la.filter(Boolean)).size;
1216
+ const trust = (nAuthors >= 2 && rc >= 5) || rc >= 10 ? ' 🏆'
1217
+ : nAuthors >= 2 || rc >= 5 ? ' ✅' : '';
1218
+ const rct = rc > 1 ? ` _(${rc}× recalled)_` : '';
1219
+ lines.push(` ${emoji}${sev}${trust} \`${l.topic}\`${rct} — ${l.what_worked.slice(0, 100)}`);
1083
1220
  }
1084
1221
  lines.push('');
1085
1222
  }
@@ -1147,27 +1284,63 @@ export async function handleBrainTool(
1147
1284
  }
1148
1285
 
1149
1286
  // ── 🔮 Predictive Pre-Warning — intent-based danger detection ────────────
1150
- // Fires BEFORE work starts. If focus area has known failure patterns → warn loudly.
1151
- if (focusTerms.length > 0) {
1287
+ // Fires BEFORE work starts. Uses explicit focus when given; otherwise
1288
+ // derives the likely work area from the last session's changed files +
1289
+ // summary (you usually keep working where you left off). This makes the
1290
+ // warning fire even when the caller forgets to pass `focus`.
1291
+ {
1152
1292
  type LessonAny = typeof lessons[0] & { author?: string; tags?: string[] };
1153
- const dangerLessons = (lessons as LessonAny[]).filter(l => {
1154
- if (l.outcome === 'success') return false;
1155
- const topicCategory = l.topic.split(':')[0];
1156
- return focusTerms.some(term =>
1157
- l.topic.toLowerCase().includes(term) ||
1158
- topicCategory === term ||
1159
- (l.tags ?? []).some((t: string) => t.toLowerCase() === term),
1160
- );
1161
- });
1162
- if (dangerLessons.length >= 1) {
1163
- // Insert warning block right after the title line (index 1 = blank line after title)
1164
- const warning = [
1165
- `🚨 **PRE-WARNING** — Read this BEFORE starting:`,
1166
- ` Known pitfalls for **"${focus}"** (${dangerLessons.length} past failure${dangerLessons.length > 1 ? 's' : ''}):`,
1167
- ...dangerLessons.slice(0, 3).map(l => ` ❌ \`${l.topic}\` — ${(l.what_failed ?? l.what_worked).slice(0, 80)}`),
1168
- '',
1169
- ];
1170
- lines.splice(2, 0, ...warning); // after '🧠 **Session Briefing**' + empty line
1293
+
1294
+ // Common path noise / extensions to drop when deriving terms from files.
1295
+ const PATH_NOISE = new Set([
1296
+ 'src', 'lib', 'test', 'tests', 'dist', 'index', 'main', 'app',
1297
+ 'internal', 'pkg', 'cmd', 'node_modules', 'components', 'utils',
1298
+ 'ts', 'tsx', 'js', 'jsx', 'go', 'py', 'rs', 'java', 'json', 'yaml', 'yml',
1299
+ ]);
1300
+ const deriveTermsFromFiles = (files: string[]): string[] => {
1301
+ const terms = new Set<string>();
1302
+ for (const f of files) {
1303
+ for (const seg of f.toLowerCase().split(/[/\\._-]/)) {
1304
+ if (seg.length > 3 && !PATH_NOISE.has(seg)) terms.add(seg);
1305
+ }
1306
+ }
1307
+ return [...terms];
1308
+ };
1309
+
1310
+ let warnTerms = focusTerms;
1311
+ let warnLabel = focus;
1312
+ let derived = false;
1313
+ if (warnTerms.length === 0 && lastSession) {
1314
+ const fromFiles = deriveTermsFromFiles(lastSession.files_changed ?? []);
1315
+ const fromSummary = (lastSession.summary ?? '').toLowerCase()
1316
+ .replace(/[^a-z0-9\s:_-]/g, ' ').split(/\s+/).filter(t => t.length > 3 && !PATH_NOISE.has(t));
1317
+ warnTerms = [...new Set([...fromFiles, ...fromSummary])];
1318
+ warnLabel = 'where you left off last session';
1319
+ derived = true;
1320
+ }
1321
+
1322
+ if (warnTerms.length > 0) {
1323
+ const dangerLessons = (lessons as LessonAny[]).filter(l => {
1324
+ if (l.outcome === 'success') return false;
1325
+ const topicCategory = l.topic.split(':')[0];
1326
+ return warnTerms.some(term =>
1327
+ l.topic.toLowerCase().includes(term) ||
1328
+ topicCategory === term ||
1329
+ (l.tags ?? []).some((t: string) => t.toLowerCase() === term),
1330
+ );
1331
+ });
1332
+ if (dangerLessons.length >= 1) {
1333
+ const headline = derived
1334
+ ? ` You're likely to continue **${warnLabel}** — ${dangerLessons.length} known pitfall${dangerLessons.length > 1 ? 's' : ''} there:`
1335
+ : ` Known pitfalls for **"${warnLabel}"** (${dangerLessons.length} past failure${dangerLessons.length > 1 ? 's' : ''}):`;
1336
+ const warning = [
1337
+ `🚨 **PRE-WARNING** — Read this BEFORE starting:`,
1338
+ headline,
1339
+ ...dangerLessons.slice(0, 3).map(l => ` ❌ \`${l.topic}\` — ${(l.what_failed ?? l.what_worked).slice(0, 80)}`),
1340
+ '',
1341
+ ];
1342
+ lines.splice(2, 0, ...warning); // after '🧠 **Session Briefing**' + empty line
1343
+ }
1171
1344
  }
1172
1345
  }
1173
1346