@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
@@ -1,7 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
  import { jwtExpiryMs, checkJwt, handleApiError } from './auth.js';
3
- import { handleTcoTool } from './handlers/tco.js';
4
- import { notify } from './notifier.js';
5
3
  import { readFile } from 'node:fs/promises';
6
4
  import { fileURLToPath } from 'node:url';
7
5
  // True only when this file is the entry point (not imported by tests or other modules).
@@ -36,40 +34,25 @@ const _isMain = process.argv[1] != null &&
36
34
  * ── Auth & Status ────────────────────────────────────────────────────────────
37
35
  * • get_api_status – check API health + JWT auth info (Keycloak)
38
36
  *
39
- * ── Travel Chaos Organizer (TCO) ────────────────────────────────────────────
40
- * • tco_list_trips – list all trips
41
- * • tco_create_trip – create a new trip
42
- * • tco_get_timeline – get timeline items for a trip
43
- * • tco_delete_trip – delete a trip
44
- * • tco_inbox_list – list Chaos Inbox items
45
- * • tco_inbox_assign – assign inbox item to a trip
46
- * • tco_inbox_reject – reject/discard an inbox item
47
- * • tco_parse_url – fetch a URL and parse with Ollama
48
- * • tco_import_email – import raw email text
49
- * (Requires TCO_API_URL env var; auth forwarded from CACHLY_JWT — same Keycloak realm)
50
- *
51
37
  * Configuration (env vars):
52
38
  * CACHLY_API_URL – default https://api.cachly.dev
53
39
  * CACHLY_JWT – your JWT (Keycloak access token)
54
40
  * CACHLY_EMBED_PROVIDER – embedding backend: openai (default), gemini, mistral, cohere, ollama, cachly (server fallback)
55
41
  * CACHLY_EMBED_MODEL – override embedding model (optional)
56
- * TCO_API_URL – Travel Chaos Organizer backend URL (optional, enables tco_* tools)
57
42
  */
58
43
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
59
44
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
60
45
  import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
61
46
  import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js';
62
47
  import { Redis } from 'ioredis';
48
+ import { AsyncLocalStorage } from 'node:async_hooks';
63
49
  // ── Config ────────────────────────────────────────────────────────────────────
64
50
  const API_URL = process.env.CACHLY_API_URL ?? 'https://api.cachly.dev';
65
51
  let JWT = process.env.CACHLY_JWT ?? '';
66
- const EMBED_MODEL = process.env.CACHLY_EMBED_MODEL ?? '';
67
- const CURRENT_VERSION = '0.10.49';
68
- // Max time to wait for a freshly-provisioned instance to become "running" before
69
- // giving up. Free-tier provisioning in high-latency regions can take 45–90s, so the
70
- // old hard-coded 25s caused spurious "instance_not_reachable" on first run.
71
- // Override via CACHLY_PROVISION_TIMEOUT_MS.
72
- const PROVISION_TIMEOUT_MS = Number(process.env.CACHLY_PROVISION_TIMEOUT_MS ?? 90_000);
52
+ const CURRENT_VERSION = '0.10.55';
53
+ const reqCtx = new AsyncLocalStorage();
54
+ function getJWT() { return reqCtx.getStore()?.jwt || JWT; }
55
+ function getDefaultInstanceId() { return reqCtx.getStore()?.instanceId || _defaultInstanceId; }
73
56
  // ── Default Instance Resolution (for Smithery & single-credential setups) ────
74
57
  // When CACHLY_BRAIN_INSTANCE_ID is set, tools can omit the instance_id parameter.
75
58
  // When neither is set, we auto-fetch the first running instance once per process.
@@ -77,18 +60,29 @@ let _defaultInstanceId = process.env.CACHLY_BRAIN_INSTANCE_ID ?? '';
77
60
  // Timestamp of last failed fetch — retries after 30 s (not permanently blocked).
78
61
  let _defaultInstanceLastAttempt = 0;
79
62
  async function resolveDefaultInstanceId() {
63
+ // In an HTTP request scope the instance ID is fixed by the caller's header.
64
+ const scopedId = reqCtx.getStore()?.instanceId;
65
+ if (scopedId)
66
+ return scopedId;
80
67
  if (_defaultInstanceId)
81
68
  return _defaultInstanceId;
82
- if (!JWT)
69
+ if (!getJWT())
83
70
  return '';
84
71
  // Cooldown: don't hammer the API on every tool call after a transient failure.
72
+ // 10 s is enough to avoid rate-limit spam while not blocking the user for too long.
85
73
  const now = Date.now();
86
- if (_defaultInstanceLastAttempt > 0 && now - _defaultInstanceLastAttempt < 30_000)
87
- return '';
74
+ if (_defaultInstanceLastAttempt > 0 && now - _defaultInstanceLastAttempt < 10_000) {
75
+ throw new McpError(ErrorCode.InvalidRequest, '⚠️ Cachly Brain is temporarily unreachable (API error a few seconds ago).\n\n' +
76
+ 'The server will retry automatically in a moment. If this persists:\n' +
77
+ '• Run `get_api_status` for a full diagnostic\n' +
78
+ '• Check that CACHLY_JWT is set and valid\n' +
79
+ '• Visit https://cachly.dev/instances to verify your Brain is running');
80
+ }
81
+ ;
88
82
  _defaultInstanceLastAttempt = now;
89
83
  try {
90
84
  const res = await fetch(`${API_URL}/api/v1/instances`, {
91
- headers: { Authorization: `Bearer ${JWT}`, Accept: 'application/json' },
85
+ headers: { Authorization: `Bearer ${getJWT()}`, Accept: 'application/json' },
92
86
  signal: AbortSignal.timeout(5000),
93
87
  });
94
88
  if (!res.ok)
@@ -165,44 +159,33 @@ async function pollDeviceFlow(flow) {
165
159
  catch { /* use raw JWT as fallback */ }
166
160
  JWT = apiKey;
167
161
  setEmbedJwt(apiKey); // keep embeddings.ts in sync
168
- _deviceFlow = null;
169
- sendFunnelEvent('device_flow_completed');
170
- // Persist the API key to ~/.claude/mcp.json so subsequent MCP server restarts
171
- // keep the token. Without this, JWT is lost on restart and brain_recall_count
172
- // is never incremented (telemetry has no api_key → no tenant resolution).
173
- void (async () => {
174
- try {
175
- const { writeFile, mkdir, readFile } = await import('node:fs/promises');
176
- const { existsSync } = await import('node:fs');
177
- const { resolve, dirname } = await import('node:path');
178
- const home = process.env.HOME ?? process.env.USERPROFILE ?? '';
179
- if (!home)
180
- return;
181
- const configPath = resolve(home, '.claude', 'mcp.json');
182
- let cfg = {};
183
- if (existsSync(configPath)) {
184
- try {
185
- cfg = JSON.parse(await readFile(configPath, 'utf-8'));
186
- }
187
- catch { /* corrupt — start fresh */ }
188
- }
189
- cfg.mcpServers ??= {};
190
- const existing = cfg.mcpServers['cachly'];
191
- if (existing) {
192
- existing.env ??= {};
193
- existing.env['CACHLY_JWT'] = apiKey;
194
- }
195
- else {
196
- cfg.mcpServers['cachly'] = {
197
- command: 'npx', args: ['-y', '@cachly-dev/mcp-server@latest'],
198
- env: { CACHLY_JWT: apiKey },
199
- };
162
+ // Persist API key to ~/.claude/mcp.json so the next editor restart
163
+ // doesn't start over from a missing CACHLY_JWT (fixes BrainRecallCount=0 bug).
164
+ try {
165
+ const { resolve: _res, dirname: _dir } = await import('node:path');
166
+ const { homedir: _hd } = await import('node:os');
167
+ const { writeFile: _wf, mkdir: _mk, readFile: _rf } = await import('node:fs/promises');
168
+ const { existsSync: _ex } = await import('node:fs');
169
+ const globalJwtPath = _res(_hd(), '.claude', 'mcp.json');
170
+ await _mk(_dir(globalJwtPath), { recursive: true });
171
+ let cfg = {};
172
+ if (_ex(globalJwtPath)) {
173
+ try {
174
+ cfg = JSON.parse(await _rf(globalJwtPath, 'utf-8'));
200
175
  }
201
- await mkdir(dirname(configPath), { recursive: true });
202
- await writeFile(configPath, JSON.stringify(cfg, null, 2), 'utf-8');
176
+ catch { /* corrupt — start fresh */ }
203
177
  }
204
- catch { /* non-critical — don't break the flow on filesystem errors */ }
205
- })();
178
+ cfg.mcpServers ??= {};
179
+ cfg.mcpServers['cachly'] ??= {};
180
+ cfg.mcpServers['cachly'].env ??= {};
181
+ cfg.mcpServers['cachly'].env['CACHLY_JWT'] = apiKey;
182
+ if (_defaultInstanceId)
183
+ cfg.mcpServers['cachly'].env['CACHLY_BRAIN_INSTANCE_ID'] = _defaultInstanceId;
184
+ await _wf(globalJwtPath, JSON.stringify(cfg, null, 2), 'utf-8');
185
+ }
186
+ catch { /* non-fatal — editor will just prompt again on next restart */ }
187
+ _deviceFlow = null;
188
+ sendFunnelEvent('device_flow_completed');
206
189
  // Auto-provision: find or create the user's brain instance.
207
190
  _defaultInstanceLastAttempt = 0;
208
191
  await resolveDefaultInstanceId();
@@ -210,7 +193,7 @@ async function pollDeviceFlow(flow) {
210
193
  try {
211
194
  const autoRes = await fetch(`${API_URL}/api/v1/instances/auto`, {
212
195
  method: 'POST',
213
- headers: { Authorization: `Bearer ${JWT}`, 'Content-Type': 'application/json' },
196
+ headers: { Authorization: `Bearer ${getJWT()}`, 'Content-Type': 'application/json' },
214
197
  signal: AbortSignal.timeout(12000),
215
198
  });
216
199
  if (autoRes.ok) {
@@ -223,7 +206,7 @@ async function pollDeviceFlow(flow) {
223
206
  // Log so telemetry can detect free-tier auto-provision failures
224
207
  void fetch(`${API_URL}/api/v1/telemetry/mcp`, {
225
208
  method: 'POST',
226
- headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${JWT}` },
209
+ headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${getJWT()}` },
227
210
  body: JSON.stringify({ event: 'auto_provision_failed', status: autoRes.status, version: CURRENT_VERSION }),
228
211
  signal: AbortSignal.timeout(3000),
229
212
  }).catch(() => { });
@@ -236,7 +219,7 @@ async function pollDeviceFlow(flow) {
236
219
  if (_defaultInstanceId) {
237
220
  try {
238
221
  const checkRes = await fetch(`${API_URL}/api/v1/instances/${_defaultInstanceId}`, {
239
- headers: { Authorization: `Bearer ${JWT}`, Accept: 'application/json' },
222
+ headers: { Authorization: `Bearer ${getJWT()}`, Accept: 'application/json' },
240
223
  signal: AbortSignal.timeout(4000),
241
224
  });
242
225
  if (checkRes.ok) {
@@ -247,7 +230,7 @@ async function pollDeviceFlow(flow) {
247
230
  while (checkInst.status === 'provisioning' && Date.now() < pDeadline) {
248
231
  await new Promise(r => setTimeout(r, 3000));
249
232
  const rr = await fetch(`${API_URL}/api/v1/instances/${_defaultInstanceId}`, {
250
- headers: { Authorization: `Bearer ${JWT}`, Accept: 'application/json' },
233
+ headers: { Authorization: `Bearer ${getJWT()}`, Accept: 'application/json' },
251
234
  signal: AbortSignal.timeout(4000),
252
235
  }).catch(() => null);
253
236
  if (rr?.ok) {
@@ -263,37 +246,6 @@ async function pollDeviceFlow(flow) {
263
246
  }
264
247
  catch { /* non-fatal — getConnection will handle the wait if still provisioning */ }
265
248
  }
266
- // Persist instance_id to ~/.claude/mcp.json after it's known.
267
- // Without this, every restart loses CACHLY_BRAIN_INSTANCE_ID → resolveDefaultInstanceId
268
- // makes an extra API call on every startup (with 30s cooldown on failure).
269
- if (_defaultInstanceId) {
270
- void (async () => {
271
- try {
272
- const { writeFile, readFile } = await import('node:fs/promises');
273
- const { existsSync } = await import('node:fs');
274
- const { resolve } = await import('node:path');
275
- const home = process.env.HOME ?? process.env.USERPROFILE ?? '';
276
- if (!home)
277
- return;
278
- const configPath = resolve(home, '.claude', 'mcp.json');
279
- if (!existsSync(configPath))
280
- return;
281
- let cfg = {};
282
- try {
283
- cfg = JSON.parse(await readFile(configPath, 'utf-8'));
284
- }
285
- catch {
286
- return;
287
- }
288
- const entry = cfg.mcpServers?.['cachly'];
289
- if (entry?.env) {
290
- entry.env['CACHLY_BRAIN_INSTANCE_ID'] = _defaultInstanceId;
291
- await writeFile(configPath, JSON.stringify(cfg, null, 2), 'utf-8');
292
- }
293
- }
294
- catch { /* non-critical */ }
295
- })();
296
- }
297
249
  return 'done';
298
250
  }
299
251
  if (data.error === 'slow_down')
@@ -304,12 +256,71 @@ async function pollDeviceFlow(flow) {
304
256
  return 'pending';
305
257
  }
306
258
  }
259
+ // ── Silent Auto-Setup (instant-trial, zero-click) ─────────────────────────
260
+ // Attempts to provision a free trial silently on the first tool call when no
261
+ // JWT is set. Returns true if credentials were written and the tool can
262
+ // proceed transparently; false if the API is unreachable or trials are full.
263
+ // Only runs once per process (subsequent failures short-circuit immediately).
264
+ let _silentSetupAttempted = false;
265
+ async function silentAutoSetup() {
266
+ if (_silentSetupAttempted)
267
+ return false;
268
+ _silentSetupAttempted = true;
269
+ try {
270
+ const res = await fetch(`${API_URL}/auth/instant-trial`, {
271
+ method: 'POST',
272
+ headers: { 'Content-Type': 'application/json' },
273
+ body: JSON.stringify({}),
274
+ signal: AbortSignal.timeout(8000),
275
+ });
276
+ if (!res.ok)
277
+ return false;
278
+ const data = await res.json();
279
+ if (!data.api_key)
280
+ return false;
281
+ JWT = data.api_key;
282
+ setEmbedJwt(data.api_key);
283
+ if (data.instance_id)
284
+ _defaultInstanceId = data.instance_id;
285
+ sendFunnelEvent('trial_provisioned', { version: CURRENT_VERSION });
286
+ // Persist credentials so the next editor restart doesn't re-provision.
287
+ try {
288
+ const { resolve: _rp, dirname: _dn } = await import('node:path');
289
+ const { homedir: _hd } = await import('node:os');
290
+ const { writeFile: _wf, mkdir: _mk, readFile: _rf } = await import('node:fs/promises');
291
+ const { existsSync: _ex } = await import('node:fs');
292
+ const cfgPath = _rp(_hd(), '.claude', 'mcp.json');
293
+ await _mk(_dn(cfgPath), { recursive: true });
294
+ let cfg = {};
295
+ if (_ex(cfgPath)) {
296
+ try {
297
+ cfg = JSON.parse(await _rf(cfgPath, 'utf-8'));
298
+ }
299
+ catch { /* corrupt — overwrite */ }
300
+ }
301
+ cfg.mcpServers ??= {};
302
+ cfg.mcpServers['cachly'] ??= {};
303
+ cfg.mcpServers['cachly'].env ??= {};
304
+ cfg.mcpServers['cachly'].env['CACHLY_JWT'] = data.api_key;
305
+ if (_defaultInstanceId)
306
+ cfg.mcpServers['cachly'].env['CACHLY_BRAIN_INSTANCE_ID'] = _defaultInstanceId;
307
+ await _wf(cfgPath, JSON.stringify(cfg, null, 2), 'utf-8');
308
+ }
309
+ catch { /* non-fatal — trial is active for this session even if persist fails */ }
310
+ return true;
311
+ }
312
+ catch {
313
+ return false;
314
+ }
315
+ }
307
316
  // ── Embeddings (imported from embeddings.ts) ────────────────────────────────
308
317
  import { setEmbedJwt } from './embeddings.js';
309
318
  // ── Search Engine (imported from search.ts) ─────────────────────────────────
310
319
  import { tokenize, splitMultiQuery, levenshtein, recencyBoost, extractTimestamp, STOPWORDS, katakanaToRomaji, arabicLightStem, expandCrossLingual, CROSS_LINGUAL_MAP, ZERO_RESULTS_LOG, zeroResultsTotal } from './search.js';
311
320
  // ── Exported for testing (re-export from search.ts) ─────────────────────────
312
321
  export { tokenize, splitMultiQuery, levenshtein, recencyBoost, extractTimestamp, STOPWORDS, katakanaToRomaji, arabicLightStem, expandCrossLingual, CROSS_LINGUAL_MAP };
322
+ // ── Types ─────────────────────────────────────────────────────────────────────
323
+ // Instance is imported from handlers/brain.ts
313
324
  // ── Connection pool ───────────────────────────────────────────────────────────
314
325
  /** Reuse Redis connections across tool calls (keyed by instance_id). */
315
326
  const pool = new Map();
@@ -320,16 +331,26 @@ async function getConnection(instance_id) {
320
331
  '• Pass instance_id explicitly to the tool, or\n' +
321
332
  '• Run `list_instances` to see your available instances.');
322
333
  }
323
- if (pool.has(instance_id))
324
- return pool.get(instance_id);
325
- // Fetch instance, waiting up to PROVISION_TIMEOUT_MS if it is still provisioning.
326
- // This covers the zero-friction path: device-flow auth → auto-provision → first tool call
327
- // all happen in quick succession and the instance isn't running yet.
334
+ if (pool.has(instance_id)) {
335
+ const cached = pool.get(instance_id);
336
+ // Verify the pooled connection is still alive before reusing it.
337
+ // ioredis 'error' eviction is async — a connection can be in the pool
338
+ // but already internally broken (status 'reconnecting' or 'end').
339
+ const status = cached.status;
340
+ if (status === 'ready' || status === 'connect') {
341
+ return cached;
342
+ }
343
+ // Stale — evict and fall through to reconnect.
344
+ pool.delete(instance_id);
345
+ cached.disconnect();
346
+ }
347
+ // Fetch instance, waiting up to 8 s if it is still provisioning.
348
+ // (autoStartSession already gave the instance a head-start; 8 s covers any remaining gap.)
328
349
  let inst = await apiFetch(`/api/v1/instances/${instance_id}`);
329
350
  if (inst.status === 'provisioning') {
330
- const deadline = Date.now() + PROVISION_TIMEOUT_MS;
351
+ const deadline = Date.now() + 8_000;
331
352
  while (inst.status === 'provisioning' && Date.now() < deadline) {
332
- await new Promise(r => setTimeout(r, 3000));
353
+ await new Promise(r => setTimeout(r, 2000));
333
354
  inst = await apiFetch(`/api/v1/instances/${instance_id}`);
334
355
  }
335
356
  }
@@ -337,7 +358,7 @@ async function getConnection(instance_id) {
337
358
  // Telemetry: unreachable instance so the team can proactively investigate
338
359
  void fetch(`${API_URL}/api/v1/telemetry/mcp`, {
339
360
  method: 'POST',
340
- headers: { 'Content-Type': 'application/json', ...(JWT ? { Authorization: `Bearer ${JWT}` } : {}) },
361
+ headers: { 'Content-Type': 'application/json', ...(getJWT() ? { Authorization: `Bearer ${getJWT()}` } : {}) },
341
362
  body: JSON.stringify({ event: 'instance_not_reachable', instance_id, status: inst.status, version: CURRENT_VERSION }),
342
363
  signal: AbortSignal.timeout(3000),
343
364
  }).catch(() => { });
@@ -370,7 +391,9 @@ async function getConnection(instance_id) {
370
391
  }
371
392
  // Fetch connection details (includes password) from the dedicated endpoint.
372
393
  let password;
373
- let tlsEnabled = inst.tls_enabled !== false; // default true
394
+ // Default to false — most free/dev instances run without TLS.
395
+ // Only enable TLS when the API explicitly returns tls_enabled: true.
396
+ let tlsEnabled = inst.tls_enabled === true;
374
397
  try {
375
398
  const conn = await apiFetch(`/api/v1/instances/${instance_id}/connection`);
376
399
  password = conn.password ?? undefined;
@@ -389,25 +412,56 @@ async function getConnection(instance_id) {
389
412
  host: inst.host,
390
413
  port: inst.port,
391
414
  password: password || undefined,
392
- commandTimeout: 8000,
393
415
  ...(tlsEnabled ? { tls: {} } : {}),
394
416
  lazyConnect: true,
395
417
  enableReadyCheck: true,
396
418
  connectTimeout: 8000,
419
+ commandTimeout: 10000,
397
420
  retryStrategy: () => null, // fail fast, no reconnect loops in MCP context
398
421
  });
399
422
  const evict = () => pool.delete(instance_id);
400
423
  client.on('error', evict);
401
424
  client.on('end', evict);
402
- try {
403
- await client.connect();
425
+ // Retry up to 3 times with 2s backoff. Covers the window between K8s PodReady
426
+ // and Valkey actually accepting connections (RDB/AOF load, ~1-3s on cold start).
427
+ let connectErr;
428
+ for (let attempt = 0; attempt < 3; attempt++) {
429
+ try {
430
+ if (attempt > 0) {
431
+ await new Promise(r => setTimeout(r, 2000 * attempt));
432
+ // Fresh client on retry — ioredis doesn't allow reconnect after explicit disconnect.
433
+ client.disconnect();
434
+ const retryClient = new Redis({
435
+ host: inst.host,
436
+ port: inst.port,
437
+ password: password || undefined,
438
+ ...(tlsEnabled ? { tls: {} } : {}),
439
+ lazyConnect: true,
440
+ enableReadyCheck: true,
441
+ connectTimeout: 8000,
442
+ commandTimeout: 10000,
443
+ retryStrategy: () => null,
444
+ });
445
+ retryClient.on('error', () => pool.delete(instance_id));
446
+ retryClient.on('end', () => pool.delete(instance_id));
447
+ await retryClient.connect();
448
+ pool.set(instance_id, retryClient);
449
+ return retryClient;
450
+ }
451
+ await client.connect();
452
+ connectErr = undefined;
453
+ break;
454
+ }
455
+ catch (err) {
456
+ connectErr = err;
457
+ }
404
458
  }
405
- catch (err) {
459
+ if (connectErr) {
406
460
  client.disconnect();
407
- const msg = err instanceof Error ? err.message : String(err);
461
+ const msg = connectErr instanceof Error ? connectErr.message : String(connectErr);
408
462
  void fetch(`${API_URL}/api/v1/telemetry/mcp`, {
409
463
  method: 'POST',
410
- headers: { 'Content-Type': 'application/json', ...(JWT ? { Authorization: `Bearer ${JWT}` } : {}) },
464
+ headers: { 'Content-Type': 'application/json', ...(getJWT() ? { Authorization: `Bearer ${getJWT()}` } : {}) },
411
465
  body: JSON.stringify({ event: 'redis_connect_failed', instance_id, host: inst.host, port: inst.port, error: msg, version: CURRENT_VERSION }),
412
466
  signal: AbortSignal.timeout(3000),
413
467
  }).catch(() => { });
@@ -421,14 +475,14 @@ async function getConnection(instance_id) {
421
475
  }
422
476
  // ── API helper ────────────────────────────────────────────────────────────────
423
477
  async function apiFetch(path, options = {}) {
424
- checkJwt(JWT);
478
+ checkJwt(getJWT());
425
479
  const timeoutMs = Number(process.env.CACHLY_API_TIMEOUT_MS ?? 15_000);
426
480
  const res = await fetch(`${API_URL}${path}`, {
427
481
  ...options,
428
482
  signal: AbortSignal.timeout(timeoutMs),
429
483
  headers: {
430
484
  'Content-Type': 'application/json',
431
- Authorization: `Bearer ${JWT}`,
485
+ Authorization: `Bearer ${getJWT()}`,
432
486
  ...options.headers,
433
487
  },
434
488
  });
@@ -449,7 +503,7 @@ import { handleTeamTool } from './handlers/team.js';
449
503
  import { handleRoadmapTool } from './handlers/roadmap.js';
450
504
  import { handleAdvancedTool } from './handlers/advanced.js';
451
505
  import { handleSyndicateTool } from './handlers/syndicate.js';
452
- import { handleFedbrainTool, _lastBrainFromGitCounts } from './handlers/fedbrain.js';
506
+ import { handleFedbrainTool } from './handlers/fedbrain.js';
453
507
  // ── Tools (imported from tools.ts) ─────────────────────────────────────────
454
508
  import { TOOLS } from './tools.js';
455
509
  // ── Handlers ──────────────────────────────────────────────────────────────────
@@ -472,7 +526,10 @@ function sendFunnelEvent(event, extra) {
472
526
  headers: { 'Content-Type': 'application/json' },
473
527
  body: JSON.stringify({
474
528
  event, version: CURRENT_VERSION, editor: detectEditor(),
475
- ...(JWT ? { jwt: JWT } : {}),
529
+ // Send Cachly API keys (cky_*) for server-side tenant resolution (recall counting, emails).
530
+ // Raw Keycloak JWTs (eyJ...) are intentionally excluded — they are short-lived bearer tokens
531
+ // that should never leave the MCP process boundary.
532
+ ...(getJWT().startsWith('cky_') ? { api_key: getJWT() } : {}),
476
533
  ...extra,
477
534
  }),
478
535
  signal: AbortSignal.timeout(3000),
@@ -495,11 +552,18 @@ async function sendAnonymousTelemetry(toolName) {
495
552
  catch { /* fire-and-forget, never block the user */ }
496
553
  }
497
554
  async function handleTool(name, args) {
498
- // Guard: if no JWT, return actionable onboarding message instead of HTTP 401
499
- if (!JWT) {
555
+ // Guard: if no JWT, try silent auto-setup first, then fall back to device flow.
556
+ if (!getJWT()) {
557
+ // ── Step 1: Silent instant-trial (zero-click) ────────────────────────
558
+ // On the very first tool call, attempt to silently provision a free trial.
559
+ // If it succeeds the user never sees an auth prompt — Brain is just active.
560
+ const provisioned = await silentAutoSetup();
561
+ if (provisioned) {
562
+ return handleTool(name, args); // Transparent re-entry with active credentials
563
+ }
500
564
  void sendAnonymousTelemetry(name);
501
- // ── Zero-credential device flow ─────────────────────────────────────
502
- // 1st call: start device flow, return code + URL
565
+ // ── Step 2: Zero-credential device flow ─────────────────────────────
566
+ // 1st call after silent setup fails: start device flow, return code + URL
503
567
  // 2nd+ calls: poll for token; once authenticated, proceed transparently
504
568
  if (_deviceFlow) {
505
569
  const result = await pollDeviceFlow(_deviceFlow);
@@ -571,16 +635,16 @@ async function handleTool(name, args) {
571
635
  args = { ...args, instance_id: defaultId };
572
636
  }
573
637
  // Delegate brain tools (learn, recall, session, etc.)
574
- const brainResult = await handleBrainTool(name, args, getConnection, apiFetch);
638
+ let brainResult = await handleBrainTool(name, args, getConnection, apiFetch);
575
639
  if (brainResult !== null) {
576
- if (!_firstCallSuccessSent && JWT) {
640
+ if (!_firstCallSuccessSent && getJWT()) {
577
641
  _firstCallSuccessSent = true;
578
- sendFunnelEvent('first_call_success', { tool: name, instance_id: args.instance_id ?? _defaultInstanceId ?? '' });
642
+ sendFunnelEvent('first_call_success', { tool: name, instance_id: args.instance_id ?? getDefaultInstanceId() ?? '' });
579
643
  }
580
644
  // Per-tool telemetry — fires after every successful brain tool call.
581
645
  // api_key carries the cky_live_... token so the backend can resolve tenant + increment counters.
582
- const instanceId = args.instance_id ?? _defaultInstanceId ?? '';
583
- const telemetryExtra = JWT ? { api_key: JWT, instance_id: instanceId } : { instance_id: instanceId };
646
+ const instanceId = args.instance_id ?? getDefaultInstanceId() ?? '';
647
+ const telemetryExtra = getJWT() ? { api_key: getJWT(), instance_id: instanceId } : { instance_id: instanceId };
584
648
  if (name === 'recall_best_solution') {
585
649
  // recall_best_solution → increments BrainRecallCount + triggers level-up logic
586
650
  sendFunnelEvent('recall_best_solution', telemetryExtra);
@@ -598,25 +662,48 @@ async function handleTool(name, args) {
598
662
  // Existing brain — session_start acts as implicit recall → increment counter.
599
663
  sendFunnelEvent('recall_best_solution', telemetryExtra);
600
664
  }
601
- else if (isFirstSession && args.workspace_path) {
602
- // First session with a known workspace: auto-bootstrap from git history.
603
- // Runs synchronously so the user sees the result in the same response.
665
+ // Auto-bootstrap brain_from_git when workspace_path is provided and it hasn't run yet.
666
+ // We check a sentinel key in Valkey so this only fires once per workspace path,
667
+ // not just on the very first session (handles cases where brain was empty but
668
+ // session data existed, or where the user re-installed).
669
+ if (args.workspace_path) {
604
670
  try {
605
- const gitBootstrap = await handleFedbrainTool('brain_from_git', {
606
- instance_id: instanceId,
607
- repo_path: args.workspace_path,
608
- limit: 50,
609
- }, getConnection, apiFetch);
610
- if (gitBootstrap !== null) {
611
- const gitCounts = _lastBrainFromGitCounts;
612
- sendFunnelEvent('brain_from_git', { ...telemetryExtra, ...(gitCounts ?? {}) });
613
- sendFunnelEvent('recall_best_solution', telemetryExtra);
614
- // Append bootstrap summary to the session_start briefing.
615
- // brainResult is always a string here (handleBrainTool returns string for session_start).
616
- const bootstrapText = String(gitBootstrap);
617
- if (bootstrapText) {
618
- return String(brainResult) + '\n---\n' + bootstrapText;
671
+ const workspacePath = args.workspace_path;
672
+ const autoKey = `cachly:brain_from_git:auto_done:${Buffer.from(workspacePath).toString('base64').slice(0, 40)}`;
673
+ const redis = await getConnection(instanceId);
674
+ const alreadyDone = await redis.get(autoKey);
675
+ if (!alreadyDone) {
676
+ // Mark immediately to prevent concurrent runs across multiple MCP tool calls
677
+ await redis.set(autoKey, '1', 'EX', 365 * 86400);
678
+ // Append a placeholder so the user gets immediate feedback, then bootstrap in background.
679
+ // Running brain_from_git synchronously would block session_start for 30-120s on large repos.
680
+ 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.';
681
+ if (typeof brainResult === 'object' && brainResult !== null && 'content' in brainResult) {
682
+ brainResult.content.push({ type: 'text', text: bgMsg });
619
683
  }
684
+ else if (typeof brainResult === 'string') {
685
+ brainResult = brainResult + bgMsg;
686
+ }
687
+ // Fire-and-forget — never block the session_start response
688
+ void handleFedbrainTool('brain_from_git', {
689
+ instance_id: instanceId,
690
+ repo_path: workspacePath,
691
+ limit: 50,
692
+ }, getConnection, apiFetch).then(async (gitBootstrap) => {
693
+ if (gitBootstrap !== null) {
694
+ sendFunnelEvent('brain_from_git', telemetryExtra);
695
+ sendFunnelEvent('recall_best_solution', telemetryExtra);
696
+ // Store ingested count for the next session_start wow moment
697
+ try {
698
+ const ingested = gitBootstrap.match(/Ingested:\s*\*\*(\d+)\*/);
699
+ if (ingested) {
700
+ const r = await getConnection(instanceId);
701
+ await r.set('cachly:brain_from_git:auto_bootstrap_result', JSON.stringify({ count: parseInt(ingested[1], 10), ts: new Date().toISOString() }), 'EX', 7 * 86400);
702
+ }
703
+ }
704
+ catch { /* non-critical — must never break the session */ }
705
+ }
706
+ }).catch(() => { });
620
707
  }
621
708
  }
622
709
  catch { /* git bootstrap errors must never break session_start */ }
@@ -633,7 +720,7 @@ async function handleTool(name, args) {
633
720
  const srText = typeof brainResult === 'object' && brainResult !== null && 'content' in brainResult
634
721
  ? JSON.stringify(brainResult)
635
722
  : String(brainResult ?? '');
636
- if (srText.length > 50 && !srText.includes('No lessons found') && !srText.includes('no lessons') && !srText.includes('No matches found')) {
723
+ if (srText.length > 50 && !srText.includes('No lessons found') && !srText.includes('no lessons')) {
637
724
  sendFunnelEvent('recall_best_solution', telemetryExtra);
638
725
  }
639
726
  }
@@ -642,9 +729,9 @@ async function handleTool(name, args) {
642
729
  // Delegate context tools (remember/recall/list/forget)
643
730
  const contextResult = await handleContextTool(name, args, getConnection, apiFetch);
644
731
  if (contextResult !== null) {
645
- if (!_firstCallSuccessSent && JWT) {
732
+ if (!_firstCallSuccessSent && getJWT()) {
646
733
  _firstCallSuccessSent = true;
647
- sendFunnelEvent('first_call_success', { tool: name, instance_id: args.instance_id ?? _defaultInstanceId ?? '' });
734
+ sendFunnelEvent('first_call_success', { tool: name, instance_id: args.instance_id ?? getDefaultInstanceId() ?? '' });
648
735
  }
649
736
  return contextResult;
650
737
  }
@@ -669,8 +756,8 @@ async function handleTool(name, args) {
669
756
  if (syndicateResult !== null) {
670
757
  // brain_predict is in syndicate handler — track telemetry here.
671
758
  if (name === 'brain_predict') {
672
- const instanceId = args.instance_id ?? _defaultInstanceId ?? '';
673
- const telemetryExtra = JWT ? { api_key: JWT, instance_id: instanceId } : { instance_id: instanceId };
759
+ const instanceId = args.instance_id ?? getDefaultInstanceId() ?? '';
760
+ const telemetryExtra = getJWT() ? { api_key: getJWT(), instance_id: instanceId } : { instance_id: instanceId };
674
761
  sendFunnelEvent('brain_predict', telemetryExtra);
675
762
  }
676
763
  return syndicateResult;
@@ -678,21 +765,16 @@ async function handleTool(name, args) {
678
765
  const fedbrainResult = await handleFedbrainTool(name, args, getConnection, apiFetch);
679
766
  if (fedbrainResult !== null) {
680
767
  // brain_from_git and brain_predict_failures are in fedbrain handler.
681
- const instanceId = args.instance_id ?? _defaultInstanceId ?? '';
682
- const telemetryExtra = JWT ? { api_key: JWT, instance_id: instanceId } : { instance_id: instanceId };
768
+ const instanceId = args.instance_id ?? getDefaultInstanceId() ?? '';
769
+ const telemetryExtra = getJWT() ? { api_key: getJWT(), instance_id: instanceId } : { instance_id: instanceId };
683
770
  if (name === 'brain_from_git') {
684
- const gitCounts = _lastBrainFromGitCounts;
685
- sendFunnelEvent('brain_from_git', { ...telemetryExtra, ...(gitCounts ?? {}) });
771
+ sendFunnelEvent('brain_from_git', telemetryExtra);
686
772
  }
687
773
  else if (name === 'brain_predict_failures') {
688
774
  sendFunnelEvent('brain_predict_failures', telemetryExtra);
689
775
  }
690
776
  return fedbrainResult;
691
777
  }
692
- // Delegate Travel Chaos Organizer tools (requires TCO_API_URL env var)
693
- const tcoResult = await handleTcoTool(name, args, JWT ?? '');
694
- if (tcoResult !== null)
695
- return tcoResult;
696
778
  switch (name) {
697
779
  // ── Instance management ──────────────────────────────────────────────
698
780
  case 'get_api_status': {
@@ -712,7 +794,7 @@ async function handleTool(name, args) {
712
794
  healthStatus = `unreachable: ${e.message}`;
713
795
  }
714
796
  // Check JWT / auth
715
- if (!JWT) {
797
+ if (!getJWT()) {
716
798
  return [
717
799
  `📡 **cachly API Status**`,
718
800
  ``,
@@ -724,27 +806,38 @@ async function handleTool(name, args) {
724
806
  ].join('\n');
725
807
  }
726
808
  // Decode JWT claims (inspection only, no verification)
727
- let authInfo = '❌ invalid JWT format';
728
- try {
729
- const parts = JWT.split('.');
730
- if (parts.length === 3) {
731
- const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf-8'));
732
- const sub = payload.sub ?? '(unknown)';
733
- const iss = payload.iss ?? '(unknown)';
734
- const provider = iss.includes('keycloak') ? 'Keycloak' : 'OIDC';
735
- const expTs = payload.exp ? new Date(payload.exp * 1000) : null;
736
- const expired = expTs ? expTs < new Date() : false;
737
- authInfo = [
738
- `✅ JWT decoded`,
739
- ` Sub (user ID): ${sub}`,
740
- ` Provider: ${provider}`,
741
- ` Issuer: ${iss}`,
742
- ` Expires: ${expTs ? expTs.toISOString() : 'never'} ${expired ? '⚠️ EXPIRED – get a new token!' : '✅'}`,
743
- ].join('\n');
744
- }
809
+ const jwt = getJWT();
810
+ let authInfo;
811
+ // API keys (cky_live_... / cky_trial_...) are valid long-lived credentials.
812
+ // Only try to decode as JWT (3-part base64url) if it looks like one.
813
+ if (jwt.startsWith('cky_')) {
814
+ const keyType = jwt.startsWith('cky_trial_') ? 'Trial' : 'Live';
815
+ const keyId = jwt.slice(0, 20) + '…';
816
+ authInfo = `✅ API key (${keyType})\n Key prefix: ${keyId}`;
745
817
  }
746
- catch {
747
- authInfo = '❌ JWT decode failed – check CACHLY_JWT format';
818
+ else {
819
+ authInfo = '❌ invalid credential format';
820
+ try {
821
+ const parts = jwt.split('.');
822
+ if (parts.length === 3) {
823
+ const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf-8'));
824
+ const sub = payload.sub ?? '(unknown)';
825
+ const iss = payload.iss ?? '(unknown)';
826
+ const provider = iss.includes('keycloak') ? 'Keycloak' : 'OIDC';
827
+ const expTs = payload.exp ? new Date(payload.exp * 1000) : null;
828
+ const expired = expTs ? expTs < new Date() : false;
829
+ authInfo = [
830
+ `✅ JWT decoded`,
831
+ ` Sub (user ID): ${sub}`,
832
+ ` Provider: ${provider}`,
833
+ ` Issuer: ${iss}`,
834
+ ` Expires: ${expTs ? expTs.toISOString() : 'never'} ${expired ? '⚠️ EXPIRED – get a new token!' : '✅'}`,
835
+ ].join('\n');
836
+ }
837
+ }
838
+ catch {
839
+ authInfo = '❌ JWT decode failed – check CACHLY_JWT format';
840
+ }
748
841
  }
749
842
  // List instances + connectivity
750
843
  let instanceInfo = '';
@@ -758,7 +851,7 @@ async function handleTool(name, args) {
758
851
  const lines = ['\n\n🧠 **Brain Instances:**'];
759
852
  for (const inst of instances) {
760
853
  const badge = inst.status === 'running' ? '🟢' : inst.status === 'provisioning' ? '🟡' : '🔴';
761
- const defaultMark = inst.id === _defaultInstanceId ? ' ← active' : '';
854
+ const defaultMark = inst.id === getDefaultInstanceId() ? ' ← active' : '';
762
855
  lines.push(` ${badge} **${inst.name}** (\`${inst.id}\`) · ${inst.status}${defaultMark}`);
763
856
  if (inst.status === 'running' && inst.host) {
764
857
  // Try a quick ping on the cached connection
@@ -886,13 +979,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
886
979
  || _defaultInstanceId
887
980
  || process.env.CACHLY_BRAIN_INSTANCE_ID;
888
981
  if (instanceId) {
889
- await autoStartSession(instanceId).catch(() => undefined);
982
+ // Fire-and-forget — session management is non-critical and must never block tool calls.
983
+ // The session_start briefing (which includes brain_from_git) can take 30-120s on large
984
+ // repos; awaiting it here would freeze the user's first MCP tool call entirely.
985
+ void autoStartSession(instanceId);
890
986
  }
891
987
  }
892
- else if (_autoSessionStarting) {
893
- // Another concurrent call is mid-start — wait for it before proceeding.
894
- await _autoSessionStarting.catch(() => undefined);
895
- }
988
+ // Note: we intentionally do NOT await _autoSessionStarting for any call.
989
+ // The session tracker runs in background — tool calls must always proceed immediately.
896
990
  if (!sessionTools.has(name))
897
991
  _autoSessionToolCount++;
898
992
  try {
@@ -902,9 +996,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
902
996
  catch (err) {
903
997
  if (err instanceof McpError)
904
998
  throw err;
905
- const msg = err.message ?? String(err);
906
- notify('cachly', 'tool_error', { tool: name, error: msg }).catch(() => undefined);
907
- throw new McpError(ErrorCode.InternalError, msg);
999
+ throw new McpError(ErrorCode.InternalError, err.message);
908
1000
  }
909
1001
  });
910
1002
  // Graceful shutdown – close all Redis connections + auto-end brain session
@@ -1892,7 +1984,7 @@ if (process.argv[2] === 'status') {
1892
1984
  if (process.argv[2] === 'health') {
1893
1985
  const { existsSync } = await import('node:fs');
1894
1986
  const { readFile } = await import('node:fs/promises');
1895
- const { resolve, join: pJoin } = await import('node:path');
1987
+ const { resolve } = await import('node:path');
1896
1988
  const cwd = process.cwd();
1897
1989
  let passed = 0;
1898
1990
  let failed = 0;
@@ -1906,6 +1998,10 @@ if (process.argv[2] === 'health') {
1906
1998
  if (!jwt) {
1907
1999
  fail('CACHLY_JWT not set — run: npx @cachly-dev/mcp-server@latest setup');
1908
2000
  }
2001
+ else if (jwt.startsWith('cky_')) {
2002
+ // Opaque cachly API key — no JWT expiry to check, format is valid by prefix
2003
+ ok('API key set (cky_*** opaque key)');
2004
+ }
1909
2005
  else {
1910
2006
  try {
1911
2007
  const parts = jwt.split('.');
@@ -1925,7 +2021,7 @@ if (process.argv[2] === 'health') {
1925
2021
  }
1926
2022
  }
1927
2023
  catch {
1928
- fail('CACHLY_JWT format invalid (expected JWT with 3 parts)');
2024
+ fail('CACHLY_JWT format invalid (expected a cky_*** API key or a JWT with 3 parts)');
1929
2025
  }
1930
2026
  }
1931
2027
  // ── 2. Brain API reachability ───────────────────────────────────────────────
@@ -1976,11 +2072,27 @@ if (process.argv[2] === 'health') {
1976
2072
  });
1977
2073
  if (connRes.ok) {
1978
2074
  const conn = await connRes.json();
1979
- pingPassword = conn.password;
1980
- pingTls = conn.tls_enabled !== false;
2075
+ // password may be a direct field, or embedded in connection_string as redis://:pass@host:port
2076
+ if (conn.password) {
2077
+ pingPassword = conn.password;
2078
+ }
2079
+ else if (conn.connection_string) {
2080
+ try {
2081
+ const u = new URL(conn.connection_string);
2082
+ if (u.password)
2083
+ pingPassword = decodeURIComponent(u.password);
2084
+ }
2085
+ catch { /* leave pingPassword undefined */ }
2086
+ }
2087
+ pingTls = conn.tls_enabled === true;
2088
+ }
2089
+ else {
2090
+ warn(`Could not fetch Redis credentials (HTTP ${connRes.status}) — connectivity test may fail`);
1981
2091
  }
1982
2092
  }
1983
- catch { /* proceed without password */ }
2093
+ catch (connErr) {
2094
+ warn(`Could not fetch Redis credentials — ${connErr.message}`);
2095
+ }
1984
2096
  const pingClient = new Redis({
1985
2097
  host: inst.host, port: inst.port,
1986
2098
  password: pingPassword,
@@ -2114,7 +2226,6 @@ if (process.argv[2] === 'setup') {
2114
2226
  const AUTH_BASE = 'https://auth.cachly.dev/realms/cachly/protocol/openid-connect';
2115
2227
  const CLIENT_ID = 'cachly-cli';
2116
2228
  console.log('Step 1: Sign in to cachly (free, no credit card)\n');
2117
- sendFunnelEvent('setup_auth_started');
2118
2229
  // Start device flow
2119
2230
  let deviceCode = '', userCode = '', verifyUri = '', pollInterval = 5000;
2120
2231
  try {
@@ -2140,7 +2251,6 @@ if (process.argv[2] === 'setup') {
2140
2251
  rl.close();
2141
2252
  process.exit(1);
2142
2253
  }
2143
- sendFunnelEvent('setup_auth_completed');
2144
2254
  console.log('');
2145
2255
  deviceCode = ''; // mark as fallback so we skip polling
2146
2256
  }
@@ -2173,7 +2283,6 @@ if (process.argv[2] === 'setup') {
2173
2283
  if (tokenData.access_token) {
2174
2284
  token = tokenData.access_token;
2175
2285
  console.log(' \x1b[32m✓ Authorized!\x1b[0m\n');
2176
- sendFunnelEvent('setup_auth_completed');
2177
2286
  break;
2178
2287
  }
2179
2288
  // authorization_pending = keep polling; slow_down = increase interval
@@ -2214,7 +2323,7 @@ if (process.argv[2] === 'setup') {
2214
2323
  token = keyBody.key; // swap JWT → cky_live_...
2215
2324
  console.log(' ✓\n');
2216
2325
  }
2217
- catch (e) {
2326
+ catch {
2218
2327
  console.log(' (skipped)\n');
2219
2328
  // Non-fatal: fall back to using the Keycloak JWT directly.
2220
2329
  // It will expire but setup still works for now.
@@ -2267,10 +2376,9 @@ if (process.argv[2] === 'setup') {
2267
2376
  // Newly created — poll until running or give up after 30 s.
2268
2377
  const newId = autoBody.instance_id;
2269
2378
  console.log(` ✓ created (${newId.slice(0, 8)}…)\n`);
2270
- // 45 attempts × 3s = 135s — generous for free-tier in slow regions.
2271
2379
  process.stdout.write('⏳ Waiting for instance to start');
2272
- for (let attempt = 0; attempt < 45; attempt++) {
2273
- await new Promise(r => setTimeout(r, 3000));
2380
+ for (let attempt = 0; attempt < 15; attempt++) {
2381
+ await new Promise(r => setTimeout(r, 2000));
2274
2382
  process.stdout.write('.');
2275
2383
  try {
2276
2384
  const checkRes = await fetch(`${API_URL}/api/v1/instances/${newId}`, { headers: { Authorization: `Bearer ${token}` } });
@@ -2309,7 +2417,6 @@ if (process.argv[2] === 'setup') {
2309
2417
  console.log(` (Run with --instance-id <id> to use a different one)\n`);
2310
2418
  }
2311
2419
  console.log(`✓ Instance: ${instance.name} (${instance.id.slice(0, 8)}…)\n`);
2312
- sendFunnelEvent('setup_instance_ready', { instance_id: instance.id });
2313
2420
  // ── Step 3: Detect editors ────────────────────────────────────────────────
2314
2421
  const cwd = process.cwd();
2315
2422
  const detected = [];
@@ -2487,6 +2594,8 @@ if (process.argv[2] === 'setup') {
2487
2594
  console.log(` Dashboard: https://cachly.dev/instances/${instance.id}`);
2488
2595
  console.log(`\n 📛 Add a live badge to your README:`);
2489
2596
  console.log(` npx @cachly-dev/mcp-server@latest badge\n`);
2597
+ console.log(` 🤝 Invite a teammate — they'll arrive pre-briefed on your project:`);
2598
+ console.log(` npx @cachly-dev/mcp-server@latest invite\n`);
2490
2599
  // ── Step 7: Email opt-in (non-blocking — at the very end) ────────────────
2491
2600
  if (!nonInteractive) {
2492
2601
  const email = await ask(' 📬 Stay in the loop? Email for release notes [Enter to skip]: ');
@@ -2558,7 +2667,7 @@ if (process.argv[2] === 'index') {
2558
2667
  const instanceId = flag('instance-id') ?? process.env.CACHLY_BRAIN_INSTANCE_ID;
2559
2668
  const maxFiles = parseInt(flag('max-files') ?? '500', 10);
2560
2669
  const namespace = flag('namespace') ?? 'cachly:sem:code';
2561
- if (!instanceId || !JWT) {
2670
+ if (!instanceId || !getJWT()) {
2562
2671
  console.error('\n❌ CACHLY_BRAIN_INSTANCE_ID and CACHLY_JWT must be set\n');
2563
2672
  console.error(' export CACHLY_BRAIN_INSTANCE_ID=<uuid>');
2564
2673
  console.error(' export CACHLY_JWT=<cky_live_...>');
@@ -2595,7 +2704,7 @@ if (process.argv[2] === 'learn-git') {
2595
2704
  const repoDir = resolve(flag('dir') ?? argv.find(a => !a.startsWith('--')) ?? '.');
2596
2705
  const instanceId = flag('instance-id') ?? process.env.CACHLY_BRAIN_INSTANCE_ID;
2597
2706
  const maxCommits = parseInt(flag('max-commits') ?? '50', 10);
2598
- if (!instanceId || !JWT) {
2707
+ if (!instanceId || !getJWT()) {
2599
2708
  console.error('\n❌ CACHLY_BRAIN_INSTANCE_ID and CACHLY_JWT must be set\n');
2600
2709
  console.error(' export CACHLY_BRAIN_INSTANCE_ID=<uuid>');
2601
2710
  console.error(' export CACHLY_JWT=<cky_live_...>');
@@ -2611,7 +2720,7 @@ if (process.argv[2] === 'learn-git') {
2611
2720
  limit: maxCommits,
2612
2721
  });
2613
2722
  console.log(result);
2614
- sendFunnelEvent('brain_from_git', { api_key: JWT, instance_id: instanceId });
2723
+ sendFunnelEvent('brain_from_git', { instance_id: instanceId });
2615
2724
  console.log('\n✅ Brain learned from your commits.\n');
2616
2725
  // Let fire-and-forget telemetry flush before exit.
2617
2726
  await new Promise(r => setTimeout(r, 300));
@@ -2627,7 +2736,8 @@ if (process.argv[2] === 'learn-git') {
2627
2736
  // actionable message in their editor's MCP log instead of silent failures.
2628
2737
  // Skip for CLI commands that intentionally run without credentials.
2629
2738
  const _cliNoAuthCommands = ['demo', 'share', 'health', 'setup', 'init', 'digest', 'invite', 'badge', 'join', 'upgrade'];
2630
- if (!JWT && !_cliNoAuthCommands.includes(process.argv[2] ?? '') && !(!process.argv[2] && process.stdout.isTTY)) {
2739
+ // In multi-tenant HTTP mode (PORT set) we never need a startup JWT — per-request auth.
2740
+ if (!JWT && !process.env.PORT && !_cliNoAuthCommands.includes(process.argv[2] ?? '') && !(!process.argv[2] && process.stdout.isTTY)) {
2631
2741
  // No args + no JWT in a non-TTY context = running as MCP server without credentials.
2632
2742
  // Print actionable setup banner to stdout so it's captured by callers/tests.
2633
2743
  const banner = '\n' +
@@ -2691,12 +2801,79 @@ if (!process.env.CACHLY_NO_UPDATE_CHECK) {
2691
2801
  })();
2692
2802
  }
2693
2803
  const httpPort = process.env.PORT ? parseInt(process.env.PORT, 10) : undefined;
2804
+ // ── Per-request MCP Server factory (HTTP / Smithery stateless mode) ──────────
2805
+ // SDK ≥1.9 forbids reusing a stateless transport across requests.
2806
+ // Solution: create a fresh Server+Transport per /mcp request.
2807
+ // The module-level `server` below is used for stdio mode only.
2808
+ function createHttpMcpServer() {
2809
+ const srv = new Server({ name: 'cachly-mcp', version: CURRENT_VERSION }, { capabilities: { tools: {} } });
2810
+ srv.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
2811
+ srv.setRequestHandler(CallToolRequestSchema, async (request) => {
2812
+ const { name, arguments: args } = request.params;
2813
+ try {
2814
+ const text = await handleTool(name, (args ?? {}));
2815
+ return { content: [{ type: 'text', text }] };
2816
+ }
2817
+ catch (err) {
2818
+ if (err instanceof McpError)
2819
+ throw err;
2820
+ throw new McpError(ErrorCode.InternalError, err.message);
2821
+ }
2822
+ });
2823
+ return srv;
2824
+ }
2694
2825
  if (httpPort) {
2695
2826
  // ── HTTP mode (Streamable HTTP transport) ───────────────────────────────
2696
2827
  // Used for Smithery URL deployment: PORT=3000 node dist/index.js
2828
+ // Multi-tenant: per-request JWT + instance ID resolved via:
2829
+ // 1. Authorization: Bearer <jwt> + X-Cachly-Instance-Id: <uuid>
2830
+ // 2. Smithery config: ?config=base64url({CACHLY_JWT, CACHLY_BRAIN_INSTANCE_ID, ...})
2831
+ // Falls back to module-level env-derived values when neither is provided.
2697
2832
  const { createServer } = await import('node:http');
2698
- const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
2699
- await server.connect(transport);
2833
+ function extractReqCtx(req, url) {
2834
+ let jwtVal = '';
2835
+ let instId = '';
2836
+ const auth = req.headers['authorization'];
2837
+ if (typeof auth === 'string' && auth.trim()) {
2838
+ const a = auth.trim();
2839
+ jwtVal = a.toLowerCase().startsWith('bearer ') ? a.slice(7).trim() : a;
2840
+ }
2841
+ // Fallback: raw JWT in custom header (Smithery "header" transport without Bearer prefix)
2842
+ if (!jwtVal) {
2843
+ const rawJwt = req.headers['cachly_jwt'] ?? req.headers['cachly-jwt'] ?? req.headers['x-cachly-jwt'] ?? req.headers['x-api-key'];
2844
+ if (typeof rawJwt === 'string')
2845
+ jwtVal = rawJwt.trim();
2846
+ }
2847
+ const hdrInst = req.headers['x-cachly-instance-id'];
2848
+ if (typeof hdrInst === 'string')
2849
+ instId = hdrInst.trim();
2850
+ // Smithery passes user config as a base64url-encoded JSON blob in ?config=
2851
+ const cfgParam = url.searchParams.get('config');
2852
+ if (cfgParam && (!jwtVal || !instId)) {
2853
+ try {
2854
+ const b64 = cfgParam.replace(/-/g, '+').replace(/_/g, '/');
2855
+ const padded = b64 + '='.repeat((4 - b64.length % 4) % 4);
2856
+ const json = JSON.parse(Buffer.from(padded, 'base64').toString('utf8'));
2857
+ if (!jwtVal && typeof json.CACHLY_JWT === 'string')
2858
+ jwtVal = json.CACHLY_JWT;
2859
+ if (!instId && typeof json.CACHLY_BRAIN_INSTANCE_ID === 'string')
2860
+ instId = json.CACHLY_BRAIN_INSTANCE_ID;
2861
+ }
2862
+ catch { /* malformed config — ignore, fall back to globals */ }
2863
+ }
2864
+ // Direct query params (Smithery "query" transport, manual clients, etc.)
2865
+ if (!jwtVal) {
2866
+ const q = url.searchParams.get('CACHLY_JWT') ?? url.searchParams.get('cachly_jwt') ?? url.searchParams.get('api_key');
2867
+ if (q)
2868
+ jwtVal = q.trim();
2869
+ }
2870
+ if (!instId) {
2871
+ const q = url.searchParams.get('CACHLY_BRAIN_INSTANCE_ID') ?? url.searchParams.get('cachly_brain_instance_id') ?? url.searchParams.get('instance_id');
2872
+ if (q)
2873
+ instId = q.trim();
2874
+ }
2875
+ return { jwt: jwtVal, instanceId: instId };
2876
+ }
2700
2877
  const httpServer = createServer((req, res) => {
2701
2878
  const url = new URL(req.url ?? '/', `http://localhost:${httpPort}`);
2702
2879
  if (url.pathname === '/health') {
@@ -2704,6 +2881,8 @@ if (httpPort) {
2704
2881
  res.end(JSON.stringify({
2705
2882
  status: 'ok',
2706
2883
  version: CURRENT_VERSION,
2884
+ transport: 'streamable-http',
2885
+ multiTenant: true,
2707
2886
  zeroResults: {
2708
2887
  total: zeroResultsTotal,
2709
2888
  last10: ZERO_RESULTS_LOG.slice(-10).map(e => ({ query: e.query, ts: new Date(e.ts).toISOString() })),
@@ -2711,8 +2890,52 @@ if (httpPort) {
2711
2890
  }));
2712
2891
  return;
2713
2892
  }
2893
+ // Static server card — lets Smithery skip live scanning (avoids auth complexity).
2894
+ if (url.pathname === '/.well-known/mcp/server-card.json') {
2895
+ const card = {
2896
+ name: 'cachly AI Brain',
2897
+ version: CURRENT_VERSION,
2898
+ description: 'Persistent AI memory for Claude Code, Cursor, Copilot, Windsurf & friends. Remembers every lesson, fix, and architecture decision forever.',
2899
+ transport: { type: 'streamable-http', url: 'https://mcp.cachly.dev/mcp' },
2900
+ capabilities: { tools: {} },
2901
+ };
2902
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
2903
+ res.end(JSON.stringify(card));
2904
+ return;
2905
+ }
2714
2906
  if (url.pathname === '/mcp' || url.pathname === '/') {
2715
- transport.handleRequest(req, res);
2907
+ // Handle CORS preflight
2908
+ if (req.method === 'OPTIONS') {
2909
+ res.writeHead(204, {
2910
+ 'Access-Control-Allow-Origin': '*',
2911
+ 'Access-Control-Allow-Methods': 'GET, POST, DELETE, OPTIONS',
2912
+ 'Access-Control-Allow-Headers': 'Authorization, Content-Type, Mcp-Session-Id, X-Cachly-Instance-Id, cachly_jwt, cachly-jwt, x-cachly-jwt, x-api-key',
2913
+ });
2914
+ res.end();
2915
+ return;
2916
+ }
2917
+ const ctx = extractReqCtx(req, url);
2918
+ // Create a fresh Server+Transport for each request (stateless multi-tenant mode).
2919
+ // SDK ≥1.9 throws if a stateless transport is reused across requests.
2920
+ const reqServer = createHttpMcpServer();
2921
+ const reqTransport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
2922
+ reqServer.connect(reqTransport).then(() => {
2923
+ reqCtx.run(ctx, () => {
2924
+ reqTransport.handleRequest(req, res).catch((err) => {
2925
+ process.stderr.write(`cachly-mcp HTTP handler error: ${err.message}\n`);
2926
+ if (!res.headersSent) {
2927
+ res.writeHead(500, { 'Content-Type': 'application/json' });
2928
+ res.end(JSON.stringify({ error: 'internal_error' }));
2929
+ }
2930
+ });
2931
+ });
2932
+ }).catch((err) => {
2933
+ process.stderr.write(`cachly-mcp connect error: ${err.message}\n`);
2934
+ if (!res.headersSent) {
2935
+ res.writeHead(500, { 'Content-Type': 'application/json' });
2936
+ res.end(JSON.stringify({ error: 'internal_error' }));
2937
+ }
2938
+ });
2716
2939
  return;
2717
2940
  }
2718
2941
  res.writeHead(404);
@@ -2727,5 +2950,4 @@ else {
2727
2950
  const transport = new StdioServerTransport();
2728
2951
  await server.connect(transport);
2729
2952
  }
2730
- notify('cachly', 'startup', { version: CURRENT_VERSION, mode: process.env.MCP_HTTP_PORT ? 'http' : 'stdio' }).catch(() => undefined);
2731
2953
  //# sourceMappingURL=index.js.map