@cachly-dev/mcp-server 0.10.49 → 0.10.56

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 (310) hide show
  1. package/.github/workflows/publish.yml +2 -2
  2. package/README.md +33 -5
  3. package/dist/__tests__/auth.test.js.map +1 -0
  4. package/dist/{src/__tests__ → __tests__}/brain-flow.test.js +0 -141
  5. package/dist/__tests__/brain-flow.test.js.map +1 -0
  6. package/dist/__tests__/brain-predict-failures.test.d.ts +17 -0
  7. package/dist/__tests__/brain-predict-failures.test.js +339 -0
  8. package/dist/__tests__/brain-predict-failures.test.js.map +1 -0
  9. package/dist/__tests__/confidence.test.js.map +1 -0
  10. package/dist/__tests__/e2e/api-auth.test.js.map +1 -0
  11. package/dist/__tests__/e2e/api-health.test.js.map +1 -0
  12. package/dist/__tests__/e2e/cli.test.js.map +1 -0
  13. package/dist/__tests__/e2e/config.js.map +1 -0
  14. package/dist/__tests__/e2e/funnel.test.js.map +1 -0
  15. package/dist/__tests__/e2e/mcp-tools.test.js.map +1 -0
  16. package/dist/__tests__/e2e/provisioning.test.js.map +1 -0
  17. package/dist/{src/__tests__ → __tests__}/handlers-unit.test.js +2 -0
  18. package/dist/__tests__/handlers-unit.test.js.map +1 -0
  19. package/dist/__tests__/namespace.test.js.map +1 -0
  20. package/dist/auth.js.map +1 -0
  21. package/dist/ckg.js.map +1 -0
  22. package/dist/confidence.js.map +1 -0
  23. package/dist/embeddings.js.map +1 -0
  24. package/dist/handlers/advanced.js.map +1 -0
  25. package/dist/{src/handlers → handlers}/brain.js +141 -135
  26. package/dist/handlers/brain.js.map +1 -0
  27. package/dist/handlers/cache.js.map +1 -0
  28. package/dist/{src/handlers → handlers}/context.js +4 -9
  29. package/dist/handlers/context.js.map +1 -0
  30. package/dist/{src/handlers → handlers}/fedbrain.d.ts +0 -6
  31. package/dist/{src/handlers → handlers}/fedbrain.js +7 -19
  32. package/dist/handlers/fedbrain.js.map +1 -0
  33. package/dist/handlers/instances.js.map +1 -0
  34. package/dist/{src/handlers → handlers}/roadmap.js +3 -7
  35. package/dist/handlers/roadmap.js.map +1 -0
  36. package/dist/handlers/syndicate.js.map +1 -0
  37. package/dist/{src/handlers → handlers}/team.js +3 -1
  38. package/dist/handlers/team.js.map +1 -0
  39. package/dist/{src/index.js → index.js} +428 -206
  40. package/dist/index.js.map +1 -0
  41. package/dist/namespace.js.map +1 -0
  42. package/dist/search.js.map +1 -0
  43. package/dist/search.test.js.map +1 -0
  44. package/dist/{src/tools.d.ts → tools.d.ts} +0 -196
  45. package/dist/{src/tools.js → tools.js} +0 -178
  46. package/dist/tools.js.map +1 -0
  47. package/dist/utils.js.map +1 -0
  48. package/package.json +3 -3
  49. package/smithery.yaml +16 -27
  50. package/src/__tests__/brain-flow.test.ts +0 -174
  51. package/src/__tests__/brain-predict-failures.test.ts +430 -0
  52. package/src/__tests__/handlers-unit.test.ts +2 -1
  53. package/src/handlers/brain.ts +142 -142
  54. package/src/handlers/context.ts +4 -9
  55. package/src/handlers/fedbrain.ts +7 -21
  56. package/src/handlers/roadmap.ts +3 -7
  57. package/src/handlers/team.ts +3 -1
  58. package/src/index.ts +415 -221
  59. package/src/tools.ts +0 -191
  60. package/tsconfig.json +3 -2
  61. package/.github/workflows/eas-build-android.yml +0 -58
  62. package/.github/workflows/eas-update.yml +0 -55
  63. package/STRATEGY.md +0 -209
  64. package/apps/travel-chaos-organizer/.env.example +0 -49
  65. package/apps/travel-chaos-organizer/DEPLOY.md +0 -328
  66. package/apps/travel-chaos-organizer/Makefile +0 -57
  67. package/apps/travel-chaos-organizer/README.md +0 -159
  68. package/apps/travel-chaos-organizer/backend/.env.example +0 -10
  69. package/apps/travel-chaos-organizer/backend/Dockerfile +0 -25
  70. package/apps/travel-chaos-organizer/backend/alembic/env.py +0 -56
  71. package/apps/travel-chaos-organizer/backend/alembic/script.py.mako +0 -24
  72. package/apps/travel-chaos-organizer/backend/alembic/versions/.gitkeep +0 -0
  73. package/apps/travel-chaos-organizer/backend/alembic/versions/0001_stripe_customer_id.py +0 -24
  74. package/apps/travel-chaos-organizer/backend/alembic/versions/0002_telegram_chat_id.py +0 -24
  75. package/apps/travel-chaos-organizer/backend/alembic.ini +0 -39
  76. package/apps/travel-chaos-organizer/backend/app/__init__.py +0 -0
  77. package/apps/travel-chaos-organizer/backend/app/auth/__init__.py +0 -0
  78. package/apps/travel-chaos-organizer/backend/app/auth/keycloak.py +0 -81
  79. package/apps/travel-chaos-organizer/backend/app/config.py +0 -66
  80. package/apps/travel-chaos-organizer/backend/app/db/__init__.py +0 -0
  81. package/apps/travel-chaos-organizer/backend/app/db/database.py +0 -20
  82. package/apps/travel-chaos-organizer/backend/app/db/migrations/001_init.sql +0 -71
  83. package/apps/travel-chaos-organizer/backend/app/db/migrations/002_telemetry_freemium.sql +0 -31
  84. package/apps/travel-chaos-organizer/backend/app/db/migrations/003_telegram_chat_id.sql +0 -4
  85. package/apps/travel-chaos-organizer/backend/app/db/migrations/004_stripe_and_email.sql +0 -5
  86. package/apps/travel-chaos-organizer/backend/app/limiter.py +0 -4
  87. package/apps/travel-chaos-organizer/backend/app/logging_config.py +0 -25
  88. package/apps/travel-chaos-organizer/backend/app/main.py +0 -77
  89. package/apps/travel-chaos-organizer/backend/app/models/__init__.py +0 -0
  90. package/apps/travel-chaos-organizer/backend/app/models/schemas.py +0 -142
  91. package/apps/travel-chaos-organizer/backend/app/routers/__init__.py +0 -0
  92. package/apps/travel-chaos-organizer/backend/app/routers/admin.py +0 -237
  93. package/apps/travel-chaos-organizer/backend/app/routers/cache.py +0 -13
  94. package/apps/travel-chaos-organizer/backend/app/routers/events.py +0 -29
  95. package/apps/travel-chaos-organizer/backend/app/routers/export.py +0 -122
  96. package/apps/travel-chaos-organizer/backend/app/routers/inbox.py +0 -122
  97. package/apps/travel-chaos-organizer/backend/app/routers/items.py +0 -130
  98. package/apps/travel-chaos-organizer/backend/app/routers/mail.py +0 -108
  99. package/apps/travel-chaos-organizer/backend/app/routers/parse.py +0 -303
  100. package/apps/travel-chaos-organizer/backend/app/routers/payments.py +0 -179
  101. package/apps/travel-chaos-organizer/backend/app/routers/trips.py +0 -134
  102. package/apps/travel-chaos-organizer/backend/app/routers/users.py +0 -169
  103. package/apps/travel-chaos-organizer/backend/app/routers/waitlist.py +0 -37
  104. package/apps/travel-chaos-organizer/backend/app/services/__init__.py +0 -0
  105. package/apps/travel-chaos-organizer/backend/app/services/cache.py +0 -131
  106. package/apps/travel-chaos-organizer/backend/app/services/drip.py +0 -128
  107. package/apps/travel-chaos-organizer/backend/app/services/email.py +0 -61
  108. package/apps/travel-chaos-organizer/backend/app/services/errors.py +0 -44
  109. package/apps/travel-chaos-organizer/backend/app/services/notifier.py +0 -10
  110. package/apps/travel-chaos-organizer/backend/app/services/ollama.py +0 -101
  111. package/apps/travel-chaos-organizer/backend/app/services/parser.py +0 -22
  112. package/apps/travel-chaos-organizer/backend/app/services/quota.py +0 -85
  113. package/apps/travel-chaos-organizer/backend/app/services/sentry.py +0 -11
  114. package/apps/travel-chaos-organizer/backend/app/services/telemetry.py +0 -35
  115. package/apps/travel-chaos-organizer/backend/pytest.ini +0 -4
  116. package/apps/travel-chaos-organizer/backend/requirements-dev.txt +0 -6
  117. package/apps/travel-chaos-organizer/backend/requirements.txt +0 -25
  118. package/apps/travel-chaos-organizer/backend/tests/__init__.py +0 -0
  119. package/apps/travel-chaos-organizer/backend/tests/conftest.py +0 -223
  120. package/apps/travel-chaos-organizer/backend/tests/test_admin.py +0 -99
  121. package/apps/travel-chaos-organizer/backend/tests/test_cache.py +0 -142
  122. package/apps/travel-chaos-organizer/backend/tests/test_drip.py +0 -32
  123. package/apps/travel-chaos-organizer/backend/tests/test_e2e_chain.py +0 -121
  124. package/apps/travel-chaos-organizer/backend/tests/test_export.py +0 -48
  125. package/apps/travel-chaos-organizer/backend/tests/test_inbox.py +0 -107
  126. package/apps/travel-chaos-organizer/backend/tests/test_items.py +0 -138
  127. package/apps/travel-chaos-organizer/backend/tests/test_mail.py +0 -113
  128. package/apps/travel-chaos-organizer/backend/tests/test_ollama.py +0 -145
  129. package/apps/travel-chaos-organizer/backend/tests/test_parse_url.py +0 -98
  130. package/apps/travel-chaos-organizer/backend/tests/test_parser.py +0 -72
  131. package/apps/travel-chaos-organizer/backend/tests/test_payments.py +0 -163
  132. package/apps/travel-chaos-organizer/backend/tests/test_quota.py +0 -46
  133. package/apps/travel-chaos-organizer/backend/tests/test_search.py +0 -67
  134. package/apps/travel-chaos-organizer/backend/tests/test_trips.py +0 -123
  135. package/apps/travel-chaos-organizer/backend/tests/test_users.py +0 -52
  136. package/apps/travel-chaos-organizer/backups/.gitkeep +0 -0
  137. package/apps/travel-chaos-organizer/caddy/Caddyfile +0 -27
  138. package/apps/travel-chaos-organizer/docker-compose.node1.yml +0 -31
  139. package/apps/travel-chaos-organizer/docker-compose.override.yml +0 -14
  140. package/apps/travel-chaos-organizer/docker-compose.yml +0 -128
  141. package/apps/travel-chaos-organizer/frontend/.env.example +0 -13
  142. package/apps/travel-chaos-organizer/frontend/app/(app)/_layout.tsx +0 -80
  143. package/apps/travel-chaos-organizer/frontend/app/(app)/inbox.tsx +0 -166
  144. package/apps/travel-chaos-organizer/frontend/app/(app)/search.tsx +0 -130
  145. package/apps/travel-chaos-organizer/frontend/app/(app)/settings.tsx +0 -381
  146. package/apps/travel-chaos-organizer/frontend/app/(app)/trips/[id].tsx +0 -346
  147. package/apps/travel-chaos-organizer/frontend/app/(app)/trips.tsx +0 -363
  148. package/apps/travel-chaos-organizer/frontend/app/(app)/upgrade/cancel.tsx +0 -34
  149. package/apps/travel-chaos-organizer/frontend/app/(app)/upgrade/success.tsx +0 -35
  150. package/apps/travel-chaos-organizer/frontend/app/(auth)/login.tsx +0 -91
  151. package/apps/travel-chaos-organizer/frontend/app/_layout.tsx +0 -71
  152. package/apps/travel-chaos-organizer/frontend/app/auth.tsx +0 -29
  153. package/apps/travel-chaos-organizer/frontend/app/index.tsx +0 -16
  154. package/apps/travel-chaos-organizer/frontend/app/onboarding.tsx +0 -119
  155. package/apps/travel-chaos-organizer/frontend/app.config.ts +0 -54
  156. package/apps/travel-chaos-organizer/frontend/assets/adaptive-icon.svg +0 -66
  157. package/apps/travel-chaos-organizer/frontend/assets/icon.svg +0 -79
  158. package/apps/travel-chaos-organizer/frontend/assets/splash.svg +0 -15
  159. package/apps/travel-chaos-organizer/frontend/components/BottomSheet.tsx +0 -102
  160. package/apps/travel-chaos-organizer/frontend/components/ConfettiCannon.tsx +0 -107
  161. package/apps/travel-chaos-organizer/frontend/components/ConfirmDialog.tsx +0 -109
  162. package/apps/travel-chaos-organizer/frontend/components/ErrorBoundary.tsx +0 -45
  163. package/apps/travel-chaos-organizer/frontend/components/FileUploadButton.tsx +0 -19
  164. package/apps/travel-chaos-organizer/frontend/components/ImportSheet.tsx +0 -155
  165. package/apps/travel-chaos-organizer/frontend/components/OfflineBadge.tsx +0 -55
  166. package/apps/travel-chaos-organizer/frontend/components/QRCodeViewer.tsx +0 -80
  167. package/apps/travel-chaos-organizer/frontend/components/ScoreRing.tsx +0 -39
  168. package/apps/travel-chaos-organizer/frontend/components/Screen.tsx +0 -39
  169. package/apps/travel-chaos-organizer/frontend/components/Skeleton.tsx +0 -70
  170. package/apps/travel-chaos-organizer/frontend/components/SwipeableInboxItem.tsx +0 -121
  171. package/apps/travel-chaos-organizer/frontend/components/SwipeableTimelineItem.tsx +0 -70
  172. package/apps/travel-chaos-organizer/frontend/components/TimelineItem.tsx +0 -73
  173. package/apps/travel-chaos-organizer/frontend/components/TimelineItemDetail.tsx +0 -130
  174. package/apps/travel-chaos-organizer/frontend/components/Toast.tsx +0 -60
  175. package/apps/travel-chaos-organizer/frontend/components/ToastContext.tsx +0 -42
  176. package/apps/travel-chaos-organizer/frontend/components/TripCard.tsx +0 -146
  177. package/apps/travel-chaos-organizer/frontend/components/TripQRModal.tsx +0 -70
  178. package/apps/travel-chaos-organizer/frontend/components/UpgradeBanner.tsx +0 -44
  179. package/apps/travel-chaos-organizer/frontend/eas.json +0 -22
  180. package/apps/travel-chaos-organizer/frontend/hooks/useInbox.ts +0 -24
  181. package/apps/travel-chaos-organizer/frontend/hooks/useNetworkSync.ts +0 -60
  182. package/apps/travel-chaos-organizer/frontend/hooks/useTrips.ts +0 -112
  183. package/apps/travel-chaos-organizer/frontend/lib/api.ts +0 -223
  184. package/apps/travel-chaos-organizer/frontend/lib/auth.ts +0 -93
  185. package/apps/travel-chaos-organizer/frontend/lib/db.ts +0 -86
  186. package/apps/travel-chaos-organizer/frontend/lib/fileCache.ts +0 -54
  187. package/apps/travel-chaos-organizer/frontend/lib/haptics.ts +0 -13
  188. package/apps/travel-chaos-organizer/frontend/lib/notifications.ts +0 -112
  189. package/apps/travel-chaos-organizer/frontend/lib/offlineQueue.ts +0 -145
  190. package/apps/travel-chaos-organizer/frontend/lib/quota.tsx +0 -43
  191. package/apps/travel-chaos-organizer/frontend/lib/sentry.ts +0 -28
  192. package/apps/travel-chaos-organizer/frontend/lib/shareIntent.ts +0 -44
  193. package/apps/travel-chaos-organizer/frontend/package-lock.json +0 -13037
  194. package/apps/travel-chaos-organizer/frontend/package.json +0 -44
  195. package/apps/travel-chaos-organizer/frontend/tsconfig.json +0 -23
  196. package/apps/travel-chaos-organizer/keycloak/realm-export.json +0 -59
  197. package/apps/travel-chaos-organizer/landing/index.html +0 -773
  198. package/apps/travel-chaos-organizer/scripts/backup.sh +0 -14
  199. package/apps/travel-chaos-organizer/scripts/configure-env.sh +0 -82
  200. package/apps/travel-chaos-organizer/scripts/deploy.sh +0 -54
  201. package/apps/travel-chaos-organizer/scripts/health-check.sh +0 -36
  202. package/apps/travel-chaos-organizer/scripts/migrate.sh +0 -7
  203. package/apps/travel-chaos-organizer/scripts/server-setup.sh +0 -85
  204. package/apps/travel-chaos-organizer/scripts/smoke-test.sh +0 -29
  205. package/apps/travel-chaos-organizer/scripts/verify-tracking.sh +0 -71
  206. package/apps/travel-chaos-organizer/telegram-bot/.env.example +0 -3
  207. package/apps/travel-chaos-organizer/telegram-bot/Dockerfile +0 -6
  208. package/apps/travel-chaos-organizer/telegram-bot/bot.py +0 -504
  209. package/apps/travel-chaos-organizer/telegram-bot/requirements.txt +0 -3
  210. package/dist/packages/telegram-notify/client.d.ts +0 -19
  211. package/dist/packages/telegram-notify/client.js +0 -75
  212. package/dist/packages/telegram-notify/client.js.map +0 -1
  213. package/dist/src/__tests__/auth.test.js.map +0 -1
  214. package/dist/src/__tests__/brain-flow.test.js.map +0 -1
  215. package/dist/src/__tests__/confidence.test.js.map +0 -1
  216. package/dist/src/__tests__/e2e/api-auth.test.js.map +0 -1
  217. package/dist/src/__tests__/e2e/api-health.test.js.map +0 -1
  218. package/dist/src/__tests__/e2e/cli.test.js.map +0 -1
  219. package/dist/src/__tests__/e2e/config.js.map +0 -1
  220. package/dist/src/__tests__/e2e/funnel.test.js.map +0 -1
  221. package/dist/src/__tests__/e2e/mcp-tools.test.js.map +0 -1
  222. package/dist/src/__tests__/e2e/provisioning.test.js.map +0 -1
  223. package/dist/src/__tests__/handlers-unit.test.js.map +0 -1
  224. package/dist/src/__tests__/namespace.test.js.map +0 -1
  225. package/dist/src/auth.js.map +0 -1
  226. package/dist/src/ckg.js.map +0 -1
  227. package/dist/src/confidence.js.map +0 -1
  228. package/dist/src/embeddings.js.map +0 -1
  229. package/dist/src/handlers/advanced.js.map +0 -1
  230. package/dist/src/handlers/brain.js.map +0 -1
  231. package/dist/src/handlers/cache.js.map +0 -1
  232. package/dist/src/handlers/context.js.map +0 -1
  233. package/dist/src/handlers/fedbrain.js.map +0 -1
  234. package/dist/src/handlers/instances.js.map +0 -1
  235. package/dist/src/handlers/roadmap.js.map +0 -1
  236. package/dist/src/handlers/syndicate.js.map +0 -1
  237. package/dist/src/handlers/tco.d.ts +0 -16
  238. package/dist/src/handlers/tco.js +0 -303
  239. package/dist/src/handlers/tco.js.map +0 -1
  240. package/dist/src/handlers/team.js.map +0 -1
  241. package/dist/src/index.js.map +0 -1
  242. package/dist/src/namespace.js.map +0 -1
  243. package/dist/src/notifier.d.ts +0 -5
  244. package/dist/src/notifier.js +0 -6
  245. package/dist/src/notifier.js.map +0 -1
  246. package/dist/src/search.js.map +0 -1
  247. package/dist/src/search.test.js.map +0 -1
  248. package/dist/src/tools.js.map +0 -1
  249. package/dist/src/utils.js.map +0 -1
  250. package/packages/telegram-notify/SPLIT_PLAN.md +0 -106
  251. package/packages/telegram-notify/client.ts +0 -88
  252. package/packages/telegram-notify/emojis.json +0 -22
  253. package/packages/telegram-notify/pyproject.toml +0 -17
  254. package/packages/telegram-notify/telegram_notify/__init__.py +0 -3
  255. package/packages/telegram-notify/telegram_notify/client.py +0 -87
  256. package/packages/telegram-notify/telegram_notify/emojis.json +0 -22
  257. package/scripts/generate-icons.js +0 -28
  258. package/src/handlers/tco.ts +0 -347
  259. package/src/notifier.ts +0 -5
  260. /package/dist/{src/__tests__ → __tests__}/auth.test.d.ts +0 -0
  261. /package/dist/{src/__tests__ → __tests__}/auth.test.js +0 -0
  262. /package/dist/{src/__tests__ → __tests__}/brain-flow.test.d.ts +0 -0
  263. /package/dist/{src/__tests__ → __tests__}/confidence.test.d.ts +0 -0
  264. /package/dist/{src/__tests__ → __tests__}/confidence.test.js +0 -0
  265. /package/dist/{src/__tests__ → __tests__}/e2e/api-auth.test.d.ts +0 -0
  266. /package/dist/{src/__tests__ → __tests__}/e2e/api-auth.test.js +0 -0
  267. /package/dist/{src/__tests__ → __tests__}/e2e/api-health.test.d.ts +0 -0
  268. /package/dist/{src/__tests__ → __tests__}/e2e/api-health.test.js +0 -0
  269. /package/dist/{src/__tests__ → __tests__}/e2e/cli.test.d.ts +0 -0
  270. /package/dist/{src/__tests__ → __tests__}/e2e/cli.test.js +0 -0
  271. /package/dist/{src/__tests__ → __tests__}/e2e/config.d.ts +0 -0
  272. /package/dist/{src/__tests__ → __tests__}/e2e/config.js +0 -0
  273. /package/dist/{src/__tests__ → __tests__}/e2e/funnel.test.d.ts +0 -0
  274. /package/dist/{src/__tests__ → __tests__}/e2e/funnel.test.js +0 -0
  275. /package/dist/{src/__tests__ → __tests__}/e2e/mcp-tools.test.d.ts +0 -0
  276. /package/dist/{src/__tests__ → __tests__}/e2e/mcp-tools.test.js +0 -0
  277. /package/dist/{src/__tests__ → __tests__}/e2e/provisioning.test.d.ts +0 -0
  278. /package/dist/{src/__tests__ → __tests__}/e2e/provisioning.test.js +0 -0
  279. /package/dist/{src/__tests__ → __tests__}/handlers-unit.test.d.ts +0 -0
  280. /package/dist/{src/__tests__ → __tests__}/namespace.test.d.ts +0 -0
  281. /package/dist/{src/__tests__ → __tests__}/namespace.test.js +0 -0
  282. /package/dist/{src/auth.d.ts → auth.d.ts} +0 -0
  283. /package/dist/{src/auth.js → auth.js} +0 -0
  284. /package/dist/{src/ckg.d.ts → ckg.d.ts} +0 -0
  285. /package/dist/{src/ckg.js → ckg.js} +0 -0
  286. /package/dist/{src/confidence.d.ts → confidence.d.ts} +0 -0
  287. /package/dist/{src/confidence.js → confidence.js} +0 -0
  288. /package/dist/{src/embeddings.d.ts → embeddings.d.ts} +0 -0
  289. /package/dist/{src/embeddings.js → embeddings.js} +0 -0
  290. /package/dist/{src/handlers → handlers}/advanced.d.ts +0 -0
  291. /package/dist/{src/handlers → handlers}/advanced.js +0 -0
  292. /package/dist/{src/handlers → handlers}/brain.d.ts +0 -0
  293. /package/dist/{src/handlers → handlers}/cache.d.ts +0 -0
  294. /package/dist/{src/handlers → handlers}/cache.js +0 -0
  295. /package/dist/{src/handlers → handlers}/context.d.ts +0 -0
  296. /package/dist/{src/handlers → handlers}/instances.d.ts +0 -0
  297. /package/dist/{src/handlers → handlers}/instances.js +0 -0
  298. /package/dist/{src/handlers → handlers}/roadmap.d.ts +0 -0
  299. /package/dist/{src/handlers → handlers}/syndicate.d.ts +0 -0
  300. /package/dist/{src/handlers → handlers}/syndicate.js +0 -0
  301. /package/dist/{src/handlers → handlers}/team.d.ts +0 -0
  302. /package/dist/{src/index.d.ts → index.d.ts} +0 -0
  303. /package/dist/{src/namespace.d.ts → namespace.d.ts} +0 -0
  304. /package/dist/{src/namespace.js → namespace.js} +0 -0
  305. /package/dist/{src/search.d.ts → search.d.ts} +0 -0
  306. /package/dist/{src/search.js → search.js} +0 -0
  307. /package/dist/{src/search.test.d.ts → search.test.d.ts} +0 -0
  308. /package/dist/{src/search.test.js → search.test.js} +0 -0
  309. /package/dist/{src/utils.d.ts → utils.d.ts} +0 -0
  310. /package/dist/{src/utils.js → utils.js} +0 -0
package/src/index.ts CHANGED
@@ -1,10 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { jwtExpiryMs, checkJwt, handleApiError } from './auth.js';
3
- import { handleTcoTool, TCO_TOOL_NAMES } from './handlers/tco.js';
4
- import { notify } from './notifier.js';
5
3
  import { readFile } from 'node:fs/promises';
6
- import { execSync } from 'node:child_process';
7
- import { join } from 'node:path';
8
4
  import { fileURLToPath } from 'node:url';
9
5
 
10
6
  // True only when this file is the entry point (not imported by tests or other modules).
@@ -39,24 +35,11 @@ const _isMain = process.argv[1] != null &&
39
35
  * ── Auth & Status ────────────────────────────────────────────────────────────
40
36
  * • get_api_status – check API health + JWT auth info (Keycloak)
41
37
  *
42
- * ── Travel Chaos Organizer (TCO) ────────────────────────────────────────────
43
- * • tco_list_trips – list all trips
44
- * • tco_create_trip – create a new trip
45
- * • tco_get_timeline – get timeline items for a trip
46
- * • tco_delete_trip – delete a trip
47
- * • tco_inbox_list – list Chaos Inbox items
48
- * • tco_inbox_assign – assign inbox item to a trip
49
- * • tco_inbox_reject – reject/discard an inbox item
50
- * • tco_parse_url – fetch a URL and parse with Ollama
51
- * • tco_import_email – import raw email text
52
- * (Requires TCO_API_URL env var; auth forwarded from CACHLY_JWT — same Keycloak realm)
53
- *
54
38
  * Configuration (env vars):
55
39
  * CACHLY_API_URL – default https://api.cachly.dev
56
40
  * CACHLY_JWT – your JWT (Keycloak access token)
57
41
  * CACHLY_EMBED_PROVIDER – embedding backend: openai (default), gemini, mistral, cohere, ollama, cachly (server fallback)
58
42
  * CACHLY_EMBED_MODEL – override embedding model (optional)
59
- * TCO_API_URL – Travel Chaos Organizer backend URL (optional, enables tco_* tools)
60
43
  */
61
44
 
62
45
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
@@ -69,19 +52,22 @@ import {
69
52
  McpError,
70
53
  } from '@modelcontextprotocol/sdk/types.js';
71
54
  import { Redis } from 'ioredis';
55
+ import { AsyncLocalStorage } from 'node:async_hooks';
72
56
 
73
57
  // ── Config ────────────────────────────────────────────────────────────────────
74
58
 
75
59
  const API_URL = process.env.CACHLY_API_URL ?? 'https://api.cachly.dev';
76
60
  let JWT = process.env.CACHLY_JWT ?? '';
77
- const EMBED_MODEL = process.env.CACHLY_EMBED_MODEL ?? '';
78
- const CURRENT_VERSION = '0.10.49';
61
+ const CURRENT_VERSION = '0.10.55';
79
62
 
80
- // Max time to wait for a freshly-provisioned instance to become "running" before
81
- // giving up. Free-tier provisioning in high-latency regions can take 45–90s, so the
82
- // old hard-coded 25s caused spurious "instance_not_reachable" on first run.
83
- // Override via CACHLY_PROVISION_TIMEOUT_MS.
84
- const PROVISION_TIMEOUT_MS = Number(process.env.CACHLY_PROVISION_TIMEOUT_MS ?? 90_000);
63
+ // ── Per-request context (multi-tenant HTTP / Smithery) ───────────────────────
64
+ // In stdio mode the module-level JWT/instance globals apply.
65
+ // In HTTP mode, the request handler runs each request in an ALS scope that
66
+ // shadows JWT + instance ID with per-user values extracted from headers.
67
+ interface ReqCtx { jwt: string; instanceId: string; }
68
+ const reqCtx = new AsyncLocalStorage<ReqCtx>();
69
+ function getJWT(): string { return reqCtx.getStore()?.jwt || JWT; }
70
+ function getDefaultInstanceId(): string { return reqCtx.getStore()?.instanceId || _defaultInstanceId; }
85
71
 
86
72
  // ── Default Instance Resolution (for Smithery & single-credential setups) ────
87
73
  // When CACHLY_BRAIN_INSTANCE_ID is set, tools can omit the instance_id parameter.
@@ -91,15 +77,28 @@ let _defaultInstanceId: string = process.env.CACHLY_BRAIN_INSTANCE_ID ?? '';
91
77
  let _defaultInstanceLastAttempt = 0;
92
78
 
93
79
  async function resolveDefaultInstanceId(): Promise<string> {
80
+ // In an HTTP request scope the instance ID is fixed by the caller's header.
81
+ const scopedId = reqCtx.getStore()?.instanceId;
82
+ if (scopedId) return scopedId;
94
83
  if (_defaultInstanceId) return _defaultInstanceId;
95
- if (!JWT) return '';
84
+ if (!getJWT()) return '';
96
85
  // Cooldown: don't hammer the API on every tool call after a transient failure.
86
+ // 10 s is enough to avoid rate-limit spam while not blocking the user for too long.
97
87
  const now = Date.now();
98
- if (_defaultInstanceLastAttempt > 0 && now - _defaultInstanceLastAttempt < 30_000) return '';
88
+ if (_defaultInstanceLastAttempt > 0 && now - _defaultInstanceLastAttempt < 10_000) {
89
+ throw new McpError(
90
+ ErrorCode.InvalidRequest,
91
+ '⚠️ Cachly Brain is temporarily unreachable (API error a few seconds ago).\n\n' +
92
+ 'The server will retry automatically in a moment. If this persists:\n' +
93
+ '• Run `get_api_status` for a full diagnostic\n' +
94
+ '• Check that CACHLY_JWT is set and valid\n' +
95
+ '• Visit https://cachly.dev/instances to verify your Brain is running'
96
+ );
97
+ };
99
98
  _defaultInstanceLastAttempt = now;
100
99
  try {
101
100
  const res = await fetch(`${API_URL}/api/v1/instances`, {
102
- headers: { Authorization: `Bearer ${JWT}`, Accept: 'application/json' },
101
+ headers: { Authorization: `Bearer ${getJWT()}`, Accept: 'application/json' },
103
102
  signal: AbortSignal.timeout(5000),
104
103
  });
105
104
  if (!res.ok) return '';
@@ -186,38 +185,28 @@ async function pollDeviceFlow(flow: DeviceFlowState): Promise<'pending' | 'expir
186
185
  } catch { /* use raw JWT as fallback */ }
187
186
  JWT = apiKey;
188
187
  setEmbedJwt(apiKey); // keep embeddings.ts in sync
188
+ // Persist API key to ~/.claude/mcp.json so the next editor restart
189
+ // doesn't start over from a missing CACHLY_JWT (fixes BrainRecallCount=0 bug).
190
+ try {
191
+ const { resolve: _res, dirname: _dir } = await import('node:path');
192
+ const { homedir: _hd } = await import('node:os');
193
+ const { writeFile: _wf, mkdir: _mk, readFile: _rf } = await import('node:fs/promises');
194
+ const { existsSync: _ex } = await import('node:fs');
195
+ const globalJwtPath = _res(_hd(), '.claude', 'mcp.json');
196
+ await _mk(_dir(globalJwtPath), { recursive: true });
197
+ let cfg: { mcpServers?: Record<string, { env?: Record<string, string> }> } = {};
198
+ if (_ex(globalJwtPath)) {
199
+ try { cfg = JSON.parse(await _rf(globalJwtPath, 'utf-8')) as typeof cfg; } catch { /* corrupt — start fresh */ }
200
+ }
201
+ cfg.mcpServers ??= {};
202
+ cfg.mcpServers['cachly'] ??= {};
203
+ cfg.mcpServers['cachly'].env ??= {};
204
+ cfg.mcpServers['cachly'].env['CACHLY_JWT'] = apiKey;
205
+ if (_defaultInstanceId) cfg.mcpServers['cachly'].env['CACHLY_BRAIN_INSTANCE_ID'] = _defaultInstanceId;
206
+ await _wf(globalJwtPath, JSON.stringify(cfg, null, 2), 'utf-8');
207
+ } catch { /* non-fatal — editor will just prompt again on next restart */ }
189
208
  _deviceFlow = null;
190
209
  sendFunnelEvent('device_flow_completed');
191
- // Persist the API key to ~/.claude/mcp.json so subsequent MCP server restarts
192
- // keep the token. Without this, JWT is lost on restart and brain_recall_count
193
- // is never incremented (telemetry has no api_key → no tenant resolution).
194
- void (async () => {
195
- try {
196
- const { writeFile, mkdir, readFile } = await import('node:fs/promises');
197
- const { existsSync } = await import('node:fs');
198
- const { resolve, dirname } = await import('node:path');
199
- const home = process.env.HOME ?? process.env.USERPROFILE ?? '';
200
- if (!home) return;
201
- const configPath = resolve(home, '.claude', 'mcp.json');
202
- let cfg: { mcpServers?: Record<string, { command?: string; args?: string[]; env?: Record<string, string> }> } = {};
203
- if (existsSync(configPath)) {
204
- try { cfg = JSON.parse(await readFile(configPath, 'utf-8')) as typeof cfg; } catch { /* corrupt — start fresh */ }
205
- }
206
- cfg.mcpServers ??= {};
207
- const existing = cfg.mcpServers['cachly'];
208
- if (existing) {
209
- existing.env ??= {};
210
- existing.env['CACHLY_JWT'] = apiKey;
211
- } else {
212
- cfg.mcpServers['cachly'] = {
213
- command: 'npx', args: ['-y', '@cachly-dev/mcp-server@latest'],
214
- env: { CACHLY_JWT: apiKey },
215
- };
216
- }
217
- await mkdir(dirname(configPath), { recursive: true });
218
- await writeFile(configPath, JSON.stringify(cfg, null, 2), 'utf-8');
219
- } catch { /* non-critical — don't break the flow on filesystem errors */ }
220
- })();
221
210
  // Auto-provision: find or create the user's brain instance.
222
211
  _defaultInstanceLastAttempt = 0;
223
212
  await resolveDefaultInstanceId();
@@ -225,7 +214,7 @@ async function pollDeviceFlow(flow: DeviceFlowState): Promise<'pending' | 'expir
225
214
  try {
226
215
  const autoRes = await fetch(`${API_URL}/api/v1/instances/auto`, {
227
216
  method: 'POST',
228
- headers: { Authorization: `Bearer ${JWT}`, 'Content-Type': 'application/json' },
217
+ headers: { Authorization: `Bearer ${getJWT()}`, 'Content-Type': 'application/json' },
229
218
  signal: AbortSignal.timeout(12000),
230
219
  });
231
220
  if (autoRes.ok) {
@@ -240,7 +229,7 @@ async function pollDeviceFlow(flow: DeviceFlowState): Promise<'pending' | 'expir
240
229
  // Log so telemetry can detect free-tier auto-provision failures
241
230
  void fetch(`${API_URL}/api/v1/telemetry/mcp`, {
242
231
  method: 'POST',
243
- headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${JWT}` },
232
+ headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${getJWT()}` },
244
233
  body: JSON.stringify({ event: 'auto_provision_failed', status: autoRes.status, version: CURRENT_VERSION }),
245
234
  signal: AbortSignal.timeout(3000),
246
235
  }).catch(() => {});
@@ -252,7 +241,7 @@ async function pollDeviceFlow(flow: DeviceFlowState): Promise<'pending' | 'expir
252
241
  if (_defaultInstanceId) {
253
242
  try {
254
243
  const checkRes = await fetch(`${API_URL}/api/v1/instances/${_defaultInstanceId}`, {
255
- headers: { Authorization: `Bearer ${JWT}`, Accept: 'application/json' },
244
+ headers: { Authorization: `Bearer ${getJWT()}`, Accept: 'application/json' },
256
245
  signal: AbortSignal.timeout(4000),
257
246
  });
258
247
  if (checkRes.ok) {
@@ -263,7 +252,7 @@ async function pollDeviceFlow(flow: DeviceFlowState): Promise<'pending' | 'expir
263
252
  while (checkInst.status === 'provisioning' && Date.now() < pDeadline) {
264
253
  await new Promise(r => setTimeout(r, 3000));
265
254
  const rr = await fetch(`${API_URL}/api/v1/instances/${_defaultInstanceId}`, {
266
- headers: { Authorization: `Bearer ${JWT}`, Accept: 'application/json' },
255
+ headers: { Authorization: `Bearer ${getJWT()}`, Accept: 'application/json' },
267
256
  signal: AbortSignal.timeout(4000),
268
257
  }).catch(() => null);
269
258
  if (rr?.ok) {
@@ -275,51 +264,67 @@ async function pollDeviceFlow(flow: DeviceFlowState): Promise<'pending' | 'expir
275
264
  }
276
265
  } catch { /* non-fatal — getConnection will handle the wait if still provisioning */ }
277
266
  }
278
- // Persist instance_id to ~/.claude/mcp.json after it's known.
279
- // Without this, every restart loses CACHLY_BRAIN_INSTANCE_ID → resolveDefaultInstanceId
280
- // makes an extra API call on every startup (with 30s cooldown on failure).
281
- if (_defaultInstanceId) {
282
- void (async () => {
283
- try {
284
- const { writeFile, readFile } = await import('node:fs/promises');
285
- const { existsSync } = await import('node:fs');
286
- const { resolve } = await import('node:path');
287
- const home = process.env.HOME ?? process.env.USERPROFILE ?? '';
288
- if (!home) return;
289
- const configPath = resolve(home, '.claude', 'mcp.json');
290
- if (!existsSync(configPath)) return;
291
- let cfg: { mcpServers?: Record<string, { env?: Record<string, string> }> } = {};
292
- try { cfg = JSON.parse(await readFile(configPath, 'utf-8')) as typeof cfg; } catch { return; }
293
- const entry = cfg.mcpServers?.['cachly'];
294
- if (entry?.env) {
295
- entry.env['CACHLY_BRAIN_INSTANCE_ID'] = _defaultInstanceId;
296
- await writeFile(configPath, JSON.stringify(cfg, null, 2), 'utf-8');
297
- }
298
- } catch { /* non-critical */ }
299
- })();
300
- }
301
267
  return 'done';
302
268
  }
303
269
  if (data.error === 'slow_down') flow.pollInterval = Math.min(flow.pollInterval + 2000, 15000);
304
270
  return 'pending';
305
271
  } catch { return 'pending'; }
306
272
  }
307
- // ── Confidence utils (imported from confidence.ts) ──────────────────────────
308
- import { calculateConfidence, confidenceBadge, STRUCTURED_TEMPLATES,
309
- CONFIDENCE_WARN_VALUE, CONFIDENCE_STALE_VALUE, CONFIDENCE_WARN_DAYS,
310
- CONFIDENCE_STALE_DAYS,
311
- simpleHash } from './confidence.js';
273
+ // ── Silent Auto-Setup (instant-trial, zero-click) ─────────────────────────
274
+ // Attempts to provision a free trial silently on the first tool call when no
275
+ // JWT is set. Returns true if credentials were written and the tool can
276
+ // proceed transparently; false if the API is unreachable or trials are full.
277
+ // Only runs once per process (subsequent failures short-circuit immediately).
278
+ let _silentSetupAttempted = false;
279
+ async function silentAutoSetup(): Promise<boolean> {
280
+ if (_silentSetupAttempted) return false;
281
+ _silentSetupAttempted = true;
282
+ try {
283
+ type TrialResp = { api_key: string; expires_at?: string; instance_id?: string; trial?: boolean };
284
+ const res = await fetch(`${API_URL}/auth/instant-trial`, {
285
+ method: 'POST',
286
+ headers: { 'Content-Type': 'application/json' },
287
+ body: JSON.stringify({}),
288
+ signal: AbortSignal.timeout(8000),
289
+ });
290
+ if (!res.ok) return false;
291
+ const data = await res.json() as TrialResp;
292
+ if (!data.api_key) return false;
312
293
 
294
+ JWT = data.api_key;
295
+ setEmbedJwt(data.api_key);
296
+ if (data.instance_id) _defaultInstanceId = data.instance_id;
297
+ sendFunnelEvent('trial_provisioned', { version: CURRENT_VERSION });
313
298
 
299
+ // Persist credentials so the next editor restart doesn't re-provision.
300
+ try {
301
+ const { resolve: _rp, dirname: _dn } = await import('node:path');
302
+ const { homedir: _hd } = await import('node:os');
303
+ const { writeFile: _wf, mkdir: _mk, readFile: _rf } = await import('node:fs/promises');
304
+ const { existsSync: _ex } = await import('node:fs');
305
+ const cfgPath = _rp(_hd(), '.claude', 'mcp.json');
306
+ await _mk(_dn(cfgPath), { recursive: true });
307
+ let cfg: { mcpServers?: Record<string, { env?: Record<string, string> }> } = {};
308
+ if (_ex(cfgPath)) {
309
+ try { cfg = JSON.parse(await _rf(cfgPath, 'utf-8')) as typeof cfg; } catch { /* corrupt — overwrite */ }
310
+ }
311
+ cfg.mcpServers ??= {};
312
+ cfg.mcpServers['cachly'] ??= {};
313
+ cfg.mcpServers['cachly'].env ??= {};
314
+ cfg.mcpServers['cachly'].env['CACHLY_JWT'] = data.api_key;
315
+ if (_defaultInstanceId) cfg.mcpServers['cachly'].env['CACHLY_BRAIN_INSTANCE_ID'] = _defaultInstanceId;
316
+ await _wf(cfgPath, JSON.stringify(cfg, null, 2), 'utf-8');
317
+ } catch { /* non-fatal — trial is active for this session even if persist fails */ }
318
+ return true;
319
+ } catch { return false; }
320
+ }
314
321
  // ── Embeddings (imported from embeddings.ts) ────────────────────────────────
315
- import { embedConfig, setEmbedJwt, EMBED_PROVIDER, computeEmbedding,
316
- hasEmbedProvider, embedProviderHint } from './embeddings.js';
322
+ import { setEmbedJwt } from './embeddings.js';
317
323
 
318
324
  // ── Search Engine (imported from search.ts) ─────────────────────────────────
319
325
  import { tokenize, splitMultiQuery, levenshtein, recencyBoost, extractTimestamp, STOPWORDS,
320
326
  katakanaToRomaji, arabicLightStem, expandCrossLingual, CROSS_LINGUAL_MAP,
321
- keywordSearch, ZERO_RESULTS_LOG, zeroResultsTotal, indexVocab as _indexVocab } from './search.js';
322
- import type { KeywordMatch } from './search.js';
327
+ ZERO_RESULTS_LOG, zeroResultsTotal } from './search.js';
323
328
 
324
329
  // ── Exported for testing (re-export from search.ts) ─────────────────────────
325
330
  export { tokenize, splitMultiQuery, levenshtein, recencyBoost, extractTimestamp, STOPWORDS,
@@ -328,19 +333,6 @@ export { tokenize, splitMultiQuery, levenshtein, recencyBoost, extractTimestamp,
328
333
  // ── Types ─────────────────────────────────────────────────────────────────────
329
334
  // Instance is imported from handlers/brain.ts
330
335
 
331
- interface CreateResponse {
332
- instance_id: string;
333
- checkout_url?: string;
334
- status: string;
335
- }
336
-
337
- interface SemanticSearchResponse {
338
- found: boolean;
339
- id?: string;
340
- similarity?: number;
341
- prompt?: string;
342
- }
343
-
344
336
  // ── Connection pool ───────────────────────────────────────────────────────────
345
337
 
346
338
  /** Reuse Redis connections across tool calls (keyed by instance_id). */
@@ -357,16 +349,27 @@ async function getConnection(instance_id: string): Promise<Redis> {
357
349
  );
358
350
  }
359
351
 
360
- if (pool.has(instance_id)) return pool.get(instance_id)!;
352
+ if (pool.has(instance_id)) {
353
+ const cached = pool.get(instance_id)!;
354
+ // Verify the pooled connection is still alive before reusing it.
355
+ // ioredis 'error' eviction is async — a connection can be in the pool
356
+ // but already internally broken (status 'reconnecting' or 'end').
357
+ const status = (cached as { status?: string }).status;
358
+ if (status === 'ready' || status === 'connect') {
359
+ return cached;
360
+ }
361
+ // Stale — evict and fall through to reconnect.
362
+ pool.delete(instance_id);
363
+ cached.disconnect();
364
+ }
361
365
 
362
- // Fetch instance, waiting up to PROVISION_TIMEOUT_MS if it is still provisioning.
363
- // This covers the zero-friction path: device-flow auth → auto-provision → first tool call
364
- // all happen in quick succession and the instance isn't running yet.
366
+ // Fetch instance, waiting up to 8 s if it is still provisioning.
367
+ // (autoStartSession already gave the instance a head-start; 8 s covers any remaining gap.)
365
368
  let inst = await apiFetch<Instance>(`/api/v1/instances/${instance_id}`);
366
369
  if (inst.status === 'provisioning') {
367
- const deadline = Date.now() + PROVISION_TIMEOUT_MS;
370
+ const deadline = Date.now() + 8_000;
368
371
  while (inst.status === 'provisioning' && Date.now() < deadline) {
369
- await new Promise(r => setTimeout(r, 3000));
372
+ await new Promise(r => setTimeout(r, 2000));
370
373
  inst = await apiFetch<Instance>(`/api/v1/instances/${instance_id}`);
371
374
  }
372
375
  }
@@ -375,7 +378,7 @@ async function getConnection(instance_id: string): Promise<Redis> {
375
378
  // Telemetry: unreachable instance so the team can proactively investigate
376
379
  void fetch(`${API_URL}/api/v1/telemetry/mcp`, {
377
380
  method: 'POST',
378
- headers: { 'Content-Type': 'application/json', ...(JWT ? { Authorization: `Bearer ${JWT}` } : {}) },
381
+ headers: { 'Content-Type': 'application/json', ...(getJWT() ? { Authorization: `Bearer ${getJWT()}` } : {}) },
379
382
  body: JSON.stringify({ event: 'instance_not_reachable', instance_id, status: inst.status, version: CURRENT_VERSION }),
380
383
  signal: AbortSignal.timeout(3000),
381
384
  }).catch(() => {});
@@ -406,7 +409,9 @@ async function getConnection(instance_id: string): Promise<Redis> {
406
409
 
407
410
  // Fetch connection details (includes password) from the dedicated endpoint.
408
411
  let password: string | undefined;
409
- let tlsEnabled = inst.tls_enabled !== false; // default true
412
+ // Default to false — most free/dev instances run without TLS.
413
+ // Only enable TLS when the API explicitly returns tls_enabled: true.
414
+ let tlsEnabled = inst.tls_enabled === true;
410
415
  try {
411
416
  const conn = await apiFetch<{ password?: string; tls_enabled?: boolean }>(
412
417
  `/api/v1/instances/${instance_id}/connection`
@@ -428,11 +433,11 @@ async function getConnection(instance_id: string): Promise<Redis> {
428
433
  host: inst.host,
429
434
  port: inst.port,
430
435
  password: password || undefined,
431
- commandTimeout: 8000,
432
436
  ...(tlsEnabled ? { tls: {} } : {}),
433
437
  lazyConnect: true,
434
438
  enableReadyCheck: true,
435
439
  connectTimeout: 8000,
440
+ commandTimeout: 10000,
436
441
  retryStrategy: () => null, // fail fast, no reconnect loops in MCP context
437
442
  });
438
443
 
@@ -440,14 +445,46 @@ async function getConnection(instance_id: string): Promise<Redis> {
440
445
  client.on('error', evict);
441
446
  client.on('end', evict);
442
447
 
443
- try {
444
- await client.connect();
445
- } catch (err: unknown) {
448
+ // Retry up to 3 times with 2s backoff. Covers the window between K8s PodReady
449
+ // and Valkey actually accepting connections (RDB/AOF load, ~1-3s on cold start).
450
+ let connectErr: unknown;
451
+ for (let attempt = 0; attempt < 3; attempt++) {
452
+ try {
453
+ if (attempt > 0) {
454
+ await new Promise(r => setTimeout(r, 2000 * attempt));
455
+ // Fresh client on retry — ioredis doesn't allow reconnect after explicit disconnect.
456
+ client.disconnect();
457
+ const retryClient = new Redis({
458
+ host: inst.host,
459
+ port: inst.port,
460
+ password: password || undefined,
461
+ ...(tlsEnabled ? { tls: {} } : {}),
462
+ lazyConnect: true,
463
+ enableReadyCheck: true,
464
+ connectTimeout: 8000,
465
+ commandTimeout: 10000,
466
+ retryStrategy: () => null,
467
+ });
468
+ retryClient.on('error', () => pool.delete(instance_id));
469
+ retryClient.on('end', () => pool.delete(instance_id));
470
+ await retryClient.connect();
471
+ pool.set(instance_id, retryClient);
472
+ return retryClient;
473
+ }
474
+ await client.connect();
475
+ connectErr = undefined;
476
+ break;
477
+ } catch (err) {
478
+ connectErr = err;
479
+ }
480
+ }
481
+
482
+ if (connectErr) {
446
483
  client.disconnect();
447
- const msg = err instanceof Error ? err.message : String(err);
484
+ const msg = connectErr instanceof Error ? connectErr.message : String(connectErr);
448
485
  void fetch(`${API_URL}/api/v1/telemetry/mcp`, {
449
486
  method: 'POST',
450
- headers: { 'Content-Type': 'application/json', ...(JWT ? { Authorization: `Bearer ${JWT}` } : {}) },
487
+ headers: { 'Content-Type': 'application/json', ...(getJWT() ? { Authorization: `Bearer ${getJWT()}` } : {}) },
451
488
  body: JSON.stringify({ event: 'redis_connect_failed', instance_id, host: inst.host, port: inst.port, error: msg, version: CURRENT_VERSION }),
452
489
  signal: AbortSignal.timeout(3000),
453
490
  }).catch(() => {});
@@ -467,14 +504,14 @@ async function getConnection(instance_id: string): Promise<Redis> {
467
504
  // ── API helper ────────────────────────────────────────────────────────────────
468
505
 
469
506
  async function apiFetch<T>(path: string, options: RequestInit = {}): Promise<T> {
470
- checkJwt(JWT);
507
+ checkJwt(getJWT());
471
508
  const timeoutMs = Number(process.env.CACHLY_API_TIMEOUT_MS ?? 15_000);
472
509
  const res = await fetch(`${API_URL}${path}`, {
473
510
  ...options,
474
511
  signal: AbortSignal.timeout(timeoutMs),
475
512
  headers: {
476
513
  'Content-Type': 'application/json',
477
- Authorization: `Bearer ${JWT}`,
514
+ Authorization: `Bearer ${getJWT()}`,
478
515
  ...options.headers,
479
516
  },
480
517
  });
@@ -487,11 +524,6 @@ async function apiFetch<T>(path: string, options: RequestInit = {}): Promise<T>
487
524
  return res.json() as Promise<T>;
488
525
  }
489
526
 
490
- import { detectNamespace } from './namespace.js';
491
-
492
- // ── CKG (imported from ckg.ts) ─────────────────────────────────────────────
493
- import type { CKGEdge, CKGNode } from './ckg.js';
494
- import { ckgSlug, extractProblemConcept, ckgUpsertNode, ckgUpdateEdge } from './ckg.js';
495
527
  import { handleBrainTool } from './handlers/brain.js';
496
528
  import { handleContextTool } from './handlers/context.js';
497
529
  import { handleInstanceTool } from './handlers/instances.js';
@@ -500,7 +532,7 @@ import { handleTeamTool } from './handlers/team.js';
500
532
  import { handleRoadmapTool } from './handlers/roadmap.js';
501
533
  import { handleAdvancedTool } from './handlers/advanced.js';
502
534
  import { handleSyndicateTool } from './handlers/syndicate.js';
503
- import { handleFedbrainTool, _lastBrainFromGitCounts } from './handlers/fedbrain.js';
535
+ import { handleFedbrainTool } from './handlers/fedbrain.js';
504
536
  import type { Instance } from './handlers/brain.js';
505
537
 
506
538
  // ── Tools (imported from tools.ts) ─────────────────────────────────────────
@@ -529,7 +561,10 @@ function sendFunnelEvent(event: string, extra?: Record<string, unknown>): void {
529
561
  headers: { 'Content-Type': 'application/json' },
530
562
  body: JSON.stringify({
531
563
  event, version: CURRENT_VERSION, editor: detectEditor(),
532
- ...(JWT ? { jwt: JWT } : {}),
564
+ // Send Cachly API keys (cky_*) for server-side tenant resolution (recall counting, emails).
565
+ // Raw Keycloak JWTs (eyJ...) are intentionally excluded — they are short-lived bearer tokens
566
+ // that should never leave the MCP process boundary.
567
+ ...(getJWT().startsWith('cky_') ? { api_key: getJWT() } : {}),
533
568
  ...extra,
534
569
  }),
535
570
  signal: AbortSignal.timeout(3000),
@@ -551,12 +586,20 @@ async function sendAnonymousTelemetry(toolName: string): Promise<void> {
551
586
  }
552
587
 
553
588
  async function handleTool(name: string, args: Record<string, unknown>): Promise<string> {
554
- // Guard: if no JWT, return actionable onboarding message instead of HTTP 401
555
- if (!JWT) {
589
+ // Guard: if no JWT, try silent auto-setup first, then fall back to device flow.
590
+ if (!getJWT()) {
591
+ // ── Step 1: Silent instant-trial (zero-click) ────────────────────────
592
+ // On the very first tool call, attempt to silently provision a free trial.
593
+ // If it succeeds the user never sees an auth prompt — Brain is just active.
594
+ const provisioned = await silentAutoSetup();
595
+ if (provisioned) {
596
+ return handleTool(name, args); // Transparent re-entry with active credentials
597
+ }
598
+
556
599
  void sendAnonymousTelemetry(name);
557
600
 
558
- // ── Zero-credential device flow ─────────────────────────────────────
559
- // 1st call: start device flow, return code + URL
601
+ // ── Step 2: Zero-credential device flow ─────────────────────────────
602
+ // 1st call after silent setup fails: start device flow, return code + URL
560
603
  // 2nd+ calls: poll for token; once authenticated, proceed transparently
561
604
  if (_deviceFlow) {
562
605
  const result = await pollDeviceFlow(_deviceFlow);
@@ -630,16 +673,16 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
630
673
  }
631
674
 
632
675
  // Delegate brain tools (learn, recall, session, etc.)
633
- const brainResult = await handleBrainTool(name, args, getConnection, apiFetch);
676
+ let brainResult = await handleBrainTool(name, args, getConnection, apiFetch);
634
677
  if (brainResult !== null) {
635
- if (!_firstCallSuccessSent && JWT) {
678
+ if (!_firstCallSuccessSent && getJWT()) {
636
679
  _firstCallSuccessSent = true;
637
- sendFunnelEvent('first_call_success', { tool: name, instance_id: args.instance_id ?? _defaultInstanceId ?? '' });
680
+ sendFunnelEvent('first_call_success', { tool: name, instance_id: args.instance_id ?? getDefaultInstanceId() ?? '' });
638
681
  }
639
682
  // Per-tool telemetry — fires after every successful brain tool call.
640
683
  // api_key carries the cky_live_... token so the backend can resolve tenant + increment counters.
641
- const instanceId = (args.instance_id as string | undefined) ?? _defaultInstanceId ?? '';
642
- const telemetryExtra = JWT ? { api_key: JWT, instance_id: instanceId } : { instance_id: instanceId };
684
+ const instanceId = (args.instance_id as string | undefined) ?? getDefaultInstanceId() ?? '';
685
+ const telemetryExtra = getJWT() ? { api_key: getJWT(), instance_id: instanceId } : { instance_id: instanceId };
643
686
  if (name === 'recall_best_solution') {
644
687
  // recall_best_solution → increments BrainRecallCount + triggers level-up logic
645
688
  sendFunnelEvent('recall_best_solution', telemetryExtra);
@@ -655,25 +698,50 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
655
698
  if (!isFirstSession && resultText.length > 100) {
656
699
  // Existing brain — session_start acts as implicit recall → increment counter.
657
700
  sendFunnelEvent('recall_best_solution', telemetryExtra);
658
- } else if (isFirstSession && args.workspace_path) {
659
- // First session with a known workspace: auto-bootstrap from git history.
660
- // Runs synchronously so the user sees the result in the same response.
701
+ }
702
+
703
+ // Auto-bootstrap brain_from_git when workspace_path is provided and it hasn't run yet.
704
+ // We check a sentinel key in Valkey so this only fires once per workspace path,
705
+ // not just on the very first session (handles cases where brain was empty but
706
+ // session data existed, or where the user re-installed).
707
+ if (args.workspace_path) {
661
708
  try {
662
- const gitBootstrap = await handleFedbrainTool('brain_from_git', {
663
- instance_id: instanceId,
664
- repo_path: args.workspace_path as string,
665
- limit: 50,
666
- }, getConnection, apiFetch);
667
- if (gitBootstrap !== null) {
668
- const gitCounts = _lastBrainFromGitCounts;
669
- sendFunnelEvent('brain_from_git', { ...telemetryExtra, ...(gitCounts ?? {}) });
670
- sendFunnelEvent('recall_best_solution', telemetryExtra);
671
- // Append bootstrap summary to the session_start briefing.
672
- // brainResult is always a string here (handleBrainTool returns string for session_start).
673
- const bootstrapText = String(gitBootstrap);
674
- if (bootstrapText) {
675
- return String(brainResult) + '\n---\n' + bootstrapText;
709
+ const workspacePath = args.workspace_path as string;
710
+ const autoKey = `cachly:brain_from_git:auto_done:${Buffer.from(workspacePath).toString('base64').slice(0, 40)}`;
711
+ const redis = await getConnection(instanceId);
712
+ const alreadyDone = await redis.get(autoKey);
713
+ if (!alreadyDone) {
714
+ // Mark immediately to prevent concurrent runs across multiple MCP tool calls
715
+ await redis.set(autoKey, '1', 'EX', 365 * 86400);
716
+ // Append a placeholder so the user gets immediate feedback, then bootstrap in background.
717
+ // Running brain_from_git synchronously would block session_start for 30-120s on large repos.
718
+ const bgMsg = '\n\n---\n\n🚀 **Auto-indexing your git history in the background** — your Brain will learn from recent commits automatically. No action needed.';
719
+ if (typeof brainResult === 'object' && brainResult !== null && 'content' in brainResult) {
720
+ (brainResult as { content: { type: string; text: string }[] }).content.push({ type: 'text', text: bgMsg });
721
+ } else if (typeof brainResult === 'string') {
722
+ brainResult = brainResult + bgMsg;
676
723
  }
724
+ // Fire-and-forget — never block the session_start response
725
+ void handleFedbrainTool('brain_from_git', {
726
+ instance_id: instanceId,
727
+ repo_path: workspacePath,
728
+ limit: 50,
729
+ }, getConnection, apiFetch).then(async (gitBootstrap) => {
730
+ if (gitBootstrap !== null) {
731
+ sendFunnelEvent('brain_from_git', telemetryExtra);
732
+ sendFunnelEvent('recall_best_solution', telemetryExtra);
733
+ // Store ingested count for the next session_start wow moment
734
+ try {
735
+ const ingested = (gitBootstrap as string).match(/Ingested:\s*\*\*(\d+)\*/);
736
+ if (ingested) {
737
+ const r = await getConnection(instanceId);
738
+ await r.set('cachly:brain_from_git:auto_bootstrap_result',
739
+ JSON.stringify({ count: parseInt(ingested[1]!, 10), ts: new Date().toISOString() }),
740
+ 'EX', 7 * 86400);
741
+ }
742
+ } catch { /* non-critical — must never break the session */ }
743
+ }
744
+ }).catch(() => { /* non-critical */ });
677
745
  }
678
746
  } catch { /* git bootstrap errors must never break session_start */ }
679
747
  }
@@ -687,7 +755,7 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
687
755
  const srText = typeof brainResult === 'object' && brainResult !== null && 'content' in brainResult
688
756
  ? JSON.stringify(brainResult)
689
757
  : String(brainResult ?? '');
690
- if (srText.length > 50 && !srText.includes('No lessons found') && !srText.includes('no lessons') && !srText.includes('No matches found')) {
758
+ if (srText.length > 50 && !srText.includes('No lessons found') && !srText.includes('no lessons')) {
691
759
  sendFunnelEvent('recall_best_solution', telemetryExtra);
692
760
  }
693
761
  }
@@ -697,9 +765,9 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
697
765
  // Delegate context tools (remember/recall/list/forget)
698
766
  const contextResult = await handleContextTool(name, args, getConnection, apiFetch);
699
767
  if (contextResult !== null) {
700
- if (!_firstCallSuccessSent && JWT) {
768
+ if (!_firstCallSuccessSent && getJWT()) {
701
769
  _firstCallSuccessSent = true;
702
- sendFunnelEvent('first_call_success', { tool: name, instance_id: args.instance_id ?? _defaultInstanceId ?? '' });
770
+ sendFunnelEvent('first_call_success', { tool: name, instance_id: args.instance_id ?? getDefaultInstanceId() ?? '' });
703
771
  }
704
772
  return contextResult;
705
773
  }
@@ -725,8 +793,8 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
725
793
  if (syndicateResult !== null) {
726
794
  // brain_predict is in syndicate handler — track telemetry here.
727
795
  if (name === 'brain_predict') {
728
- const instanceId = (args.instance_id as string | undefined) ?? _defaultInstanceId ?? '';
729
- const telemetryExtra = JWT ? { api_key: JWT, instance_id: instanceId } : { instance_id: instanceId };
796
+ const instanceId = (args.instance_id as string | undefined) ?? getDefaultInstanceId() ?? '';
797
+ const telemetryExtra = getJWT() ? { api_key: getJWT(), instance_id: instanceId } : { instance_id: instanceId };
730
798
  sendFunnelEvent('brain_predict', telemetryExtra);
731
799
  }
732
800
  return syndicateResult;
@@ -735,21 +803,16 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
735
803
  const fedbrainResult = await handleFedbrainTool(name, args, getConnection, apiFetch);
736
804
  if (fedbrainResult !== null) {
737
805
  // brain_from_git and brain_predict_failures are in fedbrain handler.
738
- const instanceId = (args.instance_id as string | undefined) ?? _defaultInstanceId ?? '';
739
- const telemetryExtra = JWT ? { api_key: JWT, instance_id: instanceId } : { instance_id: instanceId };
806
+ const instanceId = (args.instance_id as string | undefined) ?? getDefaultInstanceId() ?? '';
807
+ const telemetryExtra = getJWT() ? { api_key: getJWT(), instance_id: instanceId } : { instance_id: instanceId };
740
808
  if (name === 'brain_from_git') {
741
- const gitCounts = _lastBrainFromGitCounts;
742
- sendFunnelEvent('brain_from_git', { ...telemetryExtra, ...(gitCounts ?? {}) });
809
+ sendFunnelEvent('brain_from_git', telemetryExtra);
743
810
  } else if (name === 'brain_predict_failures') {
744
811
  sendFunnelEvent('brain_predict_failures', telemetryExtra);
745
812
  }
746
813
  return fedbrainResult;
747
814
  }
748
815
 
749
- // Delegate Travel Chaos Organizer tools (requires TCO_API_URL env var)
750
- const tcoResult = await handleTcoTool(name, args, JWT ?? '');
751
- if (tcoResult !== null) return tcoResult;
752
-
753
816
  switch (name) {
754
817
  // ── Instance management ──────────────────────────────────────────────
755
818
  case 'get_api_status': {
@@ -768,7 +831,7 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
768
831
  }
769
832
 
770
833
  // Check JWT / auth
771
- if (!JWT) {
834
+ if (!getJWT()) {
772
835
  return [
773
836
  `📡 **cachly API Status**`,
774
837
  ``,
@@ -781,28 +844,38 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
781
844
  }
782
845
 
783
846
  // Decode JWT claims (inspection only, no verification)
784
- let authInfo = '❌ invalid JWT format';
785
- try {
786
- const parts = JWT.split('.');
787
- if (parts.length === 3) {
788
- const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf-8')) as {
789
- sub?: string; exp?: number; iss?: string;
790
- };
791
- const sub = payload.sub ?? '(unknown)';
792
- const iss = payload.iss ?? '(unknown)';
793
- const provider = iss.includes('keycloak') ? 'Keycloak' : 'OIDC';
794
- const expTs = payload.exp ? new Date(payload.exp * 1000) : null;
795
- const expired = expTs ? expTs < new Date() : false;
796
- authInfo = [
797
- `✅ JWT decoded`,
798
- ` Sub (user ID): ${sub}`,
799
- ` Provider: ${provider}`,
800
- ` Issuer: ${iss}`,
801
- ` Expires: ${expTs ? expTs.toISOString() : 'never'} ${expired ? '⚠️ EXPIRED – get a new token!' : '✅'}`,
802
- ].join('\n');
847
+ const jwt = getJWT();
848
+ let authInfo: string;
849
+ // API keys (cky_live_... / cky_trial_...) are valid long-lived credentials.
850
+ // Only try to decode as JWT (3-part base64url) if it looks like one.
851
+ if (jwt.startsWith('cky_')) {
852
+ const keyType = jwt.startsWith('cky_trial_') ? 'Trial' : 'Live';
853
+ const keyId = jwt.slice(0, 20) + '…';
854
+ authInfo = `✅ API key (${keyType})\n Key prefix: ${keyId}`;
855
+ } else {
856
+ authInfo = '❌ invalid credential format';
857
+ try {
858
+ const parts = jwt.split('.');
859
+ if (parts.length === 3) {
860
+ const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf-8')) as {
861
+ sub?: string; exp?: number; iss?: string;
862
+ };
863
+ const sub = payload.sub ?? '(unknown)';
864
+ const iss = payload.iss ?? '(unknown)';
865
+ const provider = iss.includes('keycloak') ? 'Keycloak' : 'OIDC';
866
+ const expTs = payload.exp ? new Date(payload.exp * 1000) : null;
867
+ const expired = expTs ? expTs < new Date() : false;
868
+ authInfo = [
869
+ `✅ JWT decoded`,
870
+ ` Sub (user ID): ${sub}`,
871
+ ` Provider: ${provider}`,
872
+ ` Issuer: ${iss}`,
873
+ ` Expires: ${expTs ? expTs.toISOString() : 'never'} ${expired ? '⚠️ EXPIRED – get a new token!' : '✅'}`,
874
+ ].join('\n');
875
+ }
876
+ } catch {
877
+ authInfo = '❌ JWT decode failed – check CACHLY_JWT format';
803
878
  }
804
- } catch {
805
- authInfo = '❌ JWT decode failed – check CACHLY_JWT format';
806
879
  }
807
880
 
808
881
  // List instances + connectivity
@@ -816,7 +889,7 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
816
889
  const lines: string[] = ['\n\n🧠 **Brain Instances:**'];
817
890
  for (const inst of instances) {
818
891
  const badge = inst.status === 'running' ? '🟢' : inst.status === 'provisioning' ? '🟡' : '🔴';
819
- const defaultMark = inst.id === _defaultInstanceId ? ' ← active' : '';
892
+ const defaultMark = inst.id === getDefaultInstanceId() ? ' ← active' : '';
820
893
  lines.push(` ${badge} **${inst.name}** (\`${inst.id}\`) · ${inst.status}${defaultMark}`);
821
894
  if (inst.status === 'running' && inst.host) {
822
895
  // Try a quick ping on the cached connection
@@ -944,12 +1017,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
944
1017
  || _defaultInstanceId
945
1018
  || process.env.CACHLY_BRAIN_INSTANCE_ID;
946
1019
  if (instanceId) {
947
- await autoStartSession(instanceId).catch(() => undefined);
1020
+ // Fire-and-forget — session management is non-critical and must never block tool calls.
1021
+ // The session_start briefing (which includes brain_from_git) can take 30-120s on large
1022
+ // repos; awaiting it here would freeze the user's first MCP tool call entirely.
1023
+ void autoStartSession(instanceId);
948
1024
  }
949
- } else if (_autoSessionStarting) {
950
- // Another concurrent call is mid-start — wait for it before proceeding.
951
- await _autoSessionStarting.catch(() => undefined);
952
1025
  }
1026
+ // Note: we intentionally do NOT await _autoSessionStarting for any call.
1027
+ // The session tracker runs in background — tool calls must always proceed immediately.
953
1028
  if (!sessionTools.has(name)) _autoSessionToolCount++;
954
1029
 
955
1030
  try {
@@ -957,9 +1032,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
957
1032
  return { content: [{ type: 'text', text }] };
958
1033
  } catch (err) {
959
1034
  if (err instanceof McpError) throw err;
960
- const msg = (err as Error).message ?? String(err);
961
- notify('cachly', 'tool_error', { tool: name, error: msg }).catch(() => undefined);
962
- throw new McpError(ErrorCode.InternalError, msg);
1035
+ throw new McpError(ErrorCode.InternalError, (err as Error).message);
963
1036
  }
964
1037
  });
965
1038
 
@@ -2021,7 +2094,7 @@ if (process.argv[2] === 'status') {
2021
2094
  if (process.argv[2] === 'health') {
2022
2095
  const { existsSync } = await import('node:fs');
2023
2096
  const { readFile } = await import('node:fs/promises');
2024
- const { resolve, join: pJoin } = await import('node:path');
2097
+ const { resolve } = await import('node:path');
2025
2098
 
2026
2099
  const cwd = process.cwd();
2027
2100
  let passed = 0;
@@ -2038,6 +2111,9 @@ if (process.argv[2] === 'health') {
2038
2111
  const jwt = process.env.CACHLY_JWT ?? '';
2039
2112
  if (!jwt) {
2040
2113
  fail('CACHLY_JWT not set — run: npx @cachly-dev/mcp-server@latest setup');
2114
+ } else if (jwt.startsWith('cky_')) {
2115
+ // Opaque cachly API key — no JWT expiry to check, format is valid by prefix
2116
+ ok('API key set (cky_*** opaque key)');
2041
2117
  } else {
2042
2118
  try {
2043
2119
  const parts = jwt.split('.');
@@ -2055,7 +2131,7 @@ if (process.argv[2] === 'health') {
2055
2131
  ok(`JWT valid${minsLeft !== null ? ` (expires in ${Math.floor(minsLeft / 60)}h ${minsLeft % 60}m)` : ''}`);
2056
2132
  }
2057
2133
  } catch {
2058
- fail('CACHLY_JWT format invalid (expected JWT with 3 parts)');
2134
+ fail('CACHLY_JWT format invalid (expected a cky_*** API key or a JWT with 3 parts)');
2059
2135
  }
2060
2136
  }
2061
2137
 
@@ -2103,11 +2179,23 @@ if (process.argv[2] === 'health') {
2103
2179
  signal: AbortSignal.timeout(5000),
2104
2180
  });
2105
2181
  if (connRes.ok) {
2106
- const conn = await connRes.json() as { password?: string; tls_enabled?: boolean };
2107
- pingPassword = conn.password;
2108
- pingTls = conn.tls_enabled !== false;
2182
+ const conn = await connRes.json() as { password?: string; connection_string?: string; tls_enabled?: boolean };
2183
+ // password may be a direct field, or embedded in connection_string as redis://:pass@host:port
2184
+ if (conn.password) {
2185
+ pingPassword = conn.password;
2186
+ } else if (conn.connection_string) {
2187
+ try {
2188
+ const u = new URL(conn.connection_string);
2189
+ if (u.password) pingPassword = decodeURIComponent(u.password);
2190
+ } catch { /* leave pingPassword undefined */ }
2191
+ }
2192
+ pingTls = conn.tls_enabled === true;
2193
+ } else {
2194
+ warn(`Could not fetch Redis credentials (HTTP ${connRes.status}) — connectivity test may fail`);
2109
2195
  }
2110
- } catch { /* proceed without password */ }
2196
+ } catch (connErr) {
2197
+ warn(`Could not fetch Redis credentials — ${(connErr as Error).message}`);
2198
+ }
2111
2199
  const pingClient = new Redis({
2112
2200
  host: inst.host, port: inst.port,
2113
2201
  password: pingPassword,
@@ -2235,7 +2323,6 @@ if (process.argv[2] === 'setup') {
2235
2323
  const CLIENT_ID = 'cachly-cli';
2236
2324
 
2237
2325
  console.log('Step 1: Sign in to cachly (free, no credit card)\n');
2238
- sendFunnelEvent('setup_auth_started');
2239
2326
 
2240
2327
  // Start device flow
2241
2328
  let deviceCode = '', userCode = '', verifyUri = '', pollInterval = 5000;
@@ -2259,7 +2346,6 @@ if (process.argv[2] === 'setup') {
2259
2346
  console.error('Falling back: sign in at https://cachly.dev/setup-ai and paste your API token.\n');
2260
2347
  token = await ask(' Paste API token (cky_live_...): ');
2261
2348
  if (!token) { console.error('\nToken is required. Aborting.\n'); rl.close(); process.exit(1); }
2262
- sendFunnelEvent('setup_auth_completed');
2263
2349
  console.log('');
2264
2350
  deviceCode = ''; // mark as fallback so we skip polling
2265
2351
  }
@@ -2293,7 +2379,6 @@ if (process.argv[2] === 'setup') {
2293
2379
  if (tokenData.access_token) {
2294
2380
  token = tokenData.access_token;
2295
2381
  console.log(' \x1b[32m✓ Authorized!\x1b[0m\n');
2296
- sendFunnelEvent('setup_auth_completed');
2297
2382
  break;
2298
2383
  }
2299
2384
  // authorization_pending = keep polling; slow_down = increase interval
@@ -2325,7 +2410,7 @@ if (process.argv[2] === 'setup') {
2325
2410
  if (!keyBody.key) throw new Error('no key in response');
2326
2411
  token = keyBody.key; // swap JWT → cky_live_...
2327
2412
  console.log(' ✓\n');
2328
- } catch (e) {
2413
+ } catch {
2329
2414
  console.log(' (skipped)\n');
2330
2415
  // Non-fatal: fall back to using the Keycloak JWT directly.
2331
2416
  // It will expire but setup still works for now.
@@ -2374,10 +2459,9 @@ if (process.argv[2] === 'setup') {
2374
2459
  // Newly created — poll until running or give up after 30 s.
2375
2460
  const newId = autoBody.instance_id;
2376
2461
  console.log(` ✓ created (${newId.slice(0, 8)}…)\n`);
2377
- // 45 attempts × 3s = 135s — generous for free-tier in slow regions.
2378
2462
  process.stdout.write('⏳ Waiting for instance to start');
2379
- for (let attempt = 0; attempt < 45; attempt++) {
2380
- await new Promise(r => setTimeout(r, 3000));
2463
+ for (let attempt = 0; attempt < 15; attempt++) {
2464
+ await new Promise(r => setTimeout(r, 2000));
2381
2465
  process.stdout.write('.');
2382
2466
  try {
2383
2467
  const checkRes = await fetch(`${API_URL}/api/v1/instances/${newId}`, { headers: { Authorization: `Bearer ${token}` } });
@@ -2411,7 +2495,6 @@ if (process.argv[2] === 'setup') {
2411
2495
  console.log(` (Run with --instance-id <id> to use a different one)\n`);
2412
2496
  }
2413
2497
  console.log(`✓ Instance: ${instance.name} (${instance.id.slice(0, 8)}…)\n`);
2414
- sendFunnelEvent('setup_instance_ready', { instance_id: instance.id });
2415
2498
 
2416
2499
  // ── Step 3: Detect editors ────────────────────────────────────────────────
2417
2500
  const cwd = process.cwd();
@@ -2584,6 +2667,8 @@ if (process.argv[2] === 'setup') {
2584
2667
  console.log(` Dashboard: https://cachly.dev/instances/${instance.id}`);
2585
2668
  console.log(`\n 📛 Add a live badge to your README:`);
2586
2669
  console.log(` npx @cachly-dev/mcp-server@latest badge\n`);
2670
+ console.log(` 🤝 Invite a teammate — they'll arrive pre-briefed on your project:`);
2671
+ console.log(` npx @cachly-dev/mcp-server@latest invite\n`);
2587
2672
 
2588
2673
  // ── Step 7: Email opt-in (non-blocking — at the very end) ────────────────
2589
2674
  if (!nonInteractive) {
@@ -2657,7 +2742,7 @@ if (process.argv[2] === 'index') {
2657
2742
  const maxFiles = parseInt(flag('max-files') ?? '500', 10);
2658
2743
  const namespace = flag('namespace') ?? 'cachly:sem:code';
2659
2744
 
2660
- if (!instanceId || !JWT) {
2745
+ if (!instanceId || !getJWT()) {
2661
2746
  console.error('\n❌ CACHLY_BRAIN_INSTANCE_ID and CACHLY_JWT must be set\n');
2662
2747
  console.error(' export CACHLY_BRAIN_INSTANCE_ID=<uuid>');
2663
2748
  console.error(' export CACHLY_JWT=<cky_live_...>');
@@ -2699,7 +2784,7 @@ if (process.argv[2] === 'learn-git') {
2699
2784
  const instanceId = flag('instance-id') ?? process.env.CACHLY_BRAIN_INSTANCE_ID;
2700
2785
  const maxCommits = parseInt(flag('max-commits') ?? '50', 10);
2701
2786
 
2702
- if (!instanceId || !JWT) {
2787
+ if (!instanceId || !getJWT()) {
2703
2788
  console.error('\n❌ CACHLY_BRAIN_INSTANCE_ID and CACHLY_JWT must be set\n');
2704
2789
  console.error(' export CACHLY_BRAIN_INSTANCE_ID=<uuid>');
2705
2790
  console.error(' export CACHLY_JWT=<cky_live_...>');
@@ -2717,7 +2802,7 @@ if (process.argv[2] === 'learn-git') {
2717
2802
  limit: maxCommits,
2718
2803
  });
2719
2804
  console.log(result);
2720
- sendFunnelEvent('brain_from_git', { api_key: JWT, instance_id: instanceId });
2805
+ sendFunnelEvent('brain_from_git', { instance_id: instanceId });
2721
2806
  console.log('\n✅ Brain learned from your commits.\n');
2722
2807
  // Let fire-and-forget telemetry flush before exit.
2723
2808
  await new Promise(r => setTimeout(r, 300));
@@ -2734,7 +2819,8 @@ if (process.argv[2] === 'learn-git') {
2734
2819
  // actionable message in their editor's MCP log instead of silent failures.
2735
2820
  // Skip for CLI commands that intentionally run without credentials.
2736
2821
  const _cliNoAuthCommands = ['demo', 'share', 'health', 'setup', 'init', 'digest', 'invite', 'badge', 'join', 'upgrade'];
2737
- if (!JWT && !_cliNoAuthCommands.includes(process.argv[2] ?? '') && !(!process.argv[2] && process.stdout.isTTY)) {
2822
+ // In multi-tenant HTTP mode (PORT set) we never need a startup JWT — per-request auth.
2823
+ if (!JWT && !process.env.PORT && !_cliNoAuthCommands.includes(process.argv[2] ?? '') && !(!process.argv[2] && process.stdout.isTTY)) {
2738
2824
  // No args + no JWT in a non-TTY context = running as MCP server without credentials.
2739
2825
  // Print actionable setup banner to stdout so it's captured by callers/tests.
2740
2826
  const banner =
@@ -2807,12 +2893,76 @@ if (!process.env.CACHLY_NO_UPDATE_CHECK) {
2807
2893
 
2808
2894
  const httpPort = process.env.PORT ? parseInt(process.env.PORT, 10) : undefined;
2809
2895
 
2896
+ // ── Per-request MCP Server factory (HTTP / Smithery stateless mode) ──────────
2897
+ // SDK ≥1.9 forbids reusing a stateless transport across requests.
2898
+ // Solution: create a fresh Server+Transport per /mcp request.
2899
+ // The module-level `server` below is used for stdio mode only.
2900
+ function createHttpMcpServer(): Server {
2901
+ const srv = new Server(
2902
+ { name: 'cachly-mcp', version: CURRENT_VERSION },
2903
+ { capabilities: { tools: {} } }
2904
+ );
2905
+ srv.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
2906
+ srv.setRequestHandler(CallToolRequestSchema, async (request) => {
2907
+ const { name, arguments: args } = request.params;
2908
+ try {
2909
+ const text = await handleTool(name, (args ?? {}) as Record<string, unknown>);
2910
+ return { content: [{ type: 'text', text }] };
2911
+ } catch (err) {
2912
+ if (err instanceof McpError) throw err;
2913
+ throw new McpError(ErrorCode.InternalError, (err as Error).message);
2914
+ }
2915
+ });
2916
+ return srv;
2917
+ }
2918
+
2810
2919
  if (httpPort) {
2811
2920
  // ── HTTP mode (Streamable HTTP transport) ───────────────────────────────
2812
2921
  // Used for Smithery URL deployment: PORT=3000 node dist/index.js
2922
+ // Multi-tenant: per-request JWT + instance ID resolved via:
2923
+ // 1. Authorization: Bearer <jwt> + X-Cachly-Instance-Id: <uuid>
2924
+ // 2. Smithery config: ?config=base64url({CACHLY_JWT, CACHLY_BRAIN_INSTANCE_ID, ...})
2925
+ // Falls back to module-level env-derived values when neither is provided.
2813
2926
  const { createServer } = await import('node:http');
2814
- const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
2815
- await server.connect(transport);
2927
+
2928
+ function extractReqCtx(req: import('node:http').IncomingMessage, url: URL): ReqCtx {
2929
+ let jwtVal = '';
2930
+ let instId = '';
2931
+ const auth = req.headers['authorization'];
2932
+ if (typeof auth === 'string' && auth.trim()) {
2933
+ const a = auth.trim();
2934
+ jwtVal = a.toLowerCase().startsWith('bearer ') ? a.slice(7).trim() : a;
2935
+ }
2936
+ // Fallback: raw JWT in custom header (Smithery "header" transport without Bearer prefix)
2937
+ if (!jwtVal) {
2938
+ const rawJwt = req.headers['cachly_jwt'] ?? req.headers['cachly-jwt'] ?? req.headers['x-cachly-jwt'] ?? req.headers['x-api-key'];
2939
+ if (typeof rawJwt === 'string') jwtVal = rawJwt.trim();
2940
+ }
2941
+ const hdrInst = req.headers['x-cachly-instance-id'];
2942
+ if (typeof hdrInst === 'string') instId = hdrInst.trim();
2943
+ // Smithery passes user config as a base64url-encoded JSON blob in ?config=
2944
+ const cfgParam = url.searchParams.get('config');
2945
+ if (cfgParam && (!jwtVal || !instId)) {
2946
+ try {
2947
+ const b64 = cfgParam.replace(/-/g, '+').replace(/_/g, '/');
2948
+ const padded = b64 + '='.repeat((4 - b64.length % 4) % 4);
2949
+ const json = JSON.parse(Buffer.from(padded, 'base64').toString('utf8'));
2950
+ if (!jwtVal && typeof json.CACHLY_JWT === 'string') jwtVal = json.CACHLY_JWT;
2951
+ if (!instId && typeof json.CACHLY_BRAIN_INSTANCE_ID === 'string') instId = json.CACHLY_BRAIN_INSTANCE_ID;
2952
+ } catch { /* malformed config — ignore, fall back to globals */ }
2953
+ }
2954
+ // Direct query params (Smithery "query" transport, manual clients, etc.)
2955
+ if (!jwtVal) {
2956
+ const q = url.searchParams.get('CACHLY_JWT') ?? url.searchParams.get('cachly_jwt') ?? url.searchParams.get('api_key');
2957
+ if (q) jwtVal = q.trim();
2958
+ }
2959
+ if (!instId) {
2960
+ const q = url.searchParams.get('CACHLY_BRAIN_INSTANCE_ID') ?? url.searchParams.get('cachly_brain_instance_id') ?? url.searchParams.get('instance_id');
2961
+ if (q) instId = q.trim();
2962
+ }
2963
+ return { jwt: jwtVal, instanceId: instId };
2964
+ }
2965
+
2816
2966
  const httpServer = createServer((req, res) => {
2817
2967
  const url = new URL(req.url ?? '/', `http://localhost:${httpPort}`);
2818
2968
  if (url.pathname === '/health') {
@@ -2820,6 +2970,8 @@ if (httpPort) {
2820
2970
  res.end(JSON.stringify({
2821
2971
  status: 'ok',
2822
2972
  version: CURRENT_VERSION,
2973
+ transport: 'streamable-http',
2974
+ multiTenant: true,
2823
2975
  zeroResults: {
2824
2976
  total: zeroResultsTotal,
2825
2977
  last10: ZERO_RESULTS_LOG.slice(-10).map(e => ({ query: e.query, ts: new Date(e.ts).toISOString() })),
@@ -2827,8 +2979,52 @@ if (httpPort) {
2827
2979
  }));
2828
2980
  return;
2829
2981
  }
2982
+ // Static server card — lets Smithery skip live scanning (avoids auth complexity).
2983
+ if (url.pathname === '/.well-known/mcp/server-card.json') {
2984
+ const card = {
2985
+ name: 'cachly AI Brain',
2986
+ version: CURRENT_VERSION,
2987
+ description: 'Persistent AI memory for Claude Code, Cursor, Copilot, Windsurf & friends. Remembers every lesson, fix, and architecture decision forever.',
2988
+ transport: { type: 'streamable-http', url: 'https://mcp.cachly.dev/mcp' },
2989
+ capabilities: { tools: {} },
2990
+ };
2991
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
2992
+ res.end(JSON.stringify(card));
2993
+ return;
2994
+ }
2830
2995
  if (url.pathname === '/mcp' || url.pathname === '/') {
2831
- transport.handleRequest(req, res);
2996
+ // Handle CORS preflight
2997
+ if (req.method === 'OPTIONS') {
2998
+ res.writeHead(204, {
2999
+ 'Access-Control-Allow-Origin': '*',
3000
+ 'Access-Control-Allow-Methods': 'GET, POST, DELETE, OPTIONS',
3001
+ 'Access-Control-Allow-Headers': 'Authorization, Content-Type, Mcp-Session-Id, X-Cachly-Instance-Id, cachly_jwt, cachly-jwt, x-cachly-jwt, x-api-key',
3002
+ });
3003
+ res.end();
3004
+ return;
3005
+ }
3006
+ const ctx = extractReqCtx(req, url);
3007
+ // Create a fresh Server+Transport for each request (stateless multi-tenant mode).
3008
+ // SDK ≥1.9 throws if a stateless transport is reused across requests.
3009
+ const reqServer = createHttpMcpServer();
3010
+ const reqTransport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
3011
+ reqServer.connect(reqTransport).then(() => {
3012
+ reqCtx.run(ctx, () => {
3013
+ reqTransport.handleRequest(req, res).catch((err) => {
3014
+ process.stderr.write(`cachly-mcp HTTP handler error: ${(err as Error).message}\n`);
3015
+ if (!res.headersSent) {
3016
+ res.writeHead(500, { 'Content-Type': 'application/json' });
3017
+ res.end(JSON.stringify({ error: 'internal_error' }));
3018
+ }
3019
+ });
3020
+ });
3021
+ }).catch((err) => {
3022
+ process.stderr.write(`cachly-mcp connect error: ${(err as Error).message}\n`);
3023
+ if (!res.headersSent) {
3024
+ res.writeHead(500, { 'Content-Type': 'application/json' });
3025
+ res.end(JSON.stringify({ error: 'internal_error' }));
3026
+ }
3027
+ });
2832
3028
  return;
2833
3029
  }
2834
3030
  res.writeHead(404);
@@ -2843,5 +3039,3 @@ if (httpPort) {
2843
3039
  await server.connect(transport);
2844
3040
  }
2845
3041
 
2846
- notify('cachly', 'startup', { version: CURRENT_VERSION, mode: process.env.MCP_HTTP_PORT ? 'http' : 'stdio' }).catch(() => undefined);
2847
-