@cachly-dev/mcp-server 0.10.55 → 0.10.57

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (329) hide show
  1. package/.github/workflows/eas-build-android.yml +58 -0
  2. package/.github/workflows/eas-update.yml +55 -0
  3. package/.github/workflows/publish.yml +2 -2
  4. package/BENCH.md +91 -0
  5. package/CHANGELOG.md +44 -0
  6. package/README.md +6 -34
  7. package/STRATEGY.md +228 -0
  8. package/apps/travel-chaos-organizer/.env.example +49 -0
  9. package/apps/travel-chaos-organizer/DEPLOY.md +328 -0
  10. package/apps/travel-chaos-organizer/Makefile +57 -0
  11. package/apps/travel-chaos-organizer/README.md +159 -0
  12. package/apps/travel-chaos-organizer/backend/.env.example +10 -0
  13. package/apps/travel-chaos-organizer/backend/Dockerfile +25 -0
  14. package/apps/travel-chaos-organizer/backend/alembic/env.py +56 -0
  15. package/apps/travel-chaos-organizer/backend/alembic/script.py.mako +24 -0
  16. package/apps/travel-chaos-organizer/backend/alembic/versions/.gitkeep +0 -0
  17. package/apps/travel-chaos-organizer/backend/alembic/versions/0001_stripe_customer_id.py +24 -0
  18. package/apps/travel-chaos-organizer/backend/alembic/versions/0002_telegram_chat_id.py +24 -0
  19. package/apps/travel-chaos-organizer/backend/alembic.ini +39 -0
  20. package/apps/travel-chaos-organizer/backend/app/__init__.py +0 -0
  21. package/apps/travel-chaos-organizer/backend/app/auth/__init__.py +0 -0
  22. package/apps/travel-chaos-organizer/backend/app/auth/keycloak.py +81 -0
  23. package/apps/travel-chaos-organizer/backend/app/config.py +66 -0
  24. package/apps/travel-chaos-organizer/backend/app/db/__init__.py +0 -0
  25. package/apps/travel-chaos-organizer/backend/app/db/database.py +20 -0
  26. package/apps/travel-chaos-organizer/backend/app/db/migrations/001_init.sql +71 -0
  27. package/apps/travel-chaos-organizer/backend/app/db/migrations/002_telemetry_freemium.sql +31 -0
  28. package/apps/travel-chaos-organizer/backend/app/db/migrations/003_telegram_chat_id.sql +4 -0
  29. package/apps/travel-chaos-organizer/backend/app/db/migrations/004_stripe_and_email.sql +5 -0
  30. package/apps/travel-chaos-organizer/backend/app/limiter.py +4 -0
  31. package/apps/travel-chaos-organizer/backend/app/logging_config.py +25 -0
  32. package/apps/travel-chaos-organizer/backend/app/main.py +77 -0
  33. package/apps/travel-chaos-organizer/backend/app/models/__init__.py +0 -0
  34. package/apps/travel-chaos-organizer/backend/app/models/schemas.py +142 -0
  35. package/apps/travel-chaos-organizer/backend/app/routers/__init__.py +0 -0
  36. package/apps/travel-chaos-organizer/backend/app/routers/admin.py +237 -0
  37. package/apps/travel-chaos-organizer/backend/app/routers/cache.py +13 -0
  38. package/apps/travel-chaos-organizer/backend/app/routers/events.py +29 -0
  39. package/apps/travel-chaos-organizer/backend/app/routers/export.py +122 -0
  40. package/apps/travel-chaos-organizer/backend/app/routers/inbox.py +122 -0
  41. package/apps/travel-chaos-organizer/backend/app/routers/items.py +130 -0
  42. package/apps/travel-chaos-organizer/backend/app/routers/mail.py +108 -0
  43. package/apps/travel-chaos-organizer/backend/app/routers/parse.py +303 -0
  44. package/apps/travel-chaos-organizer/backend/app/routers/payments.py +179 -0
  45. package/apps/travel-chaos-organizer/backend/app/routers/trips.py +134 -0
  46. package/apps/travel-chaos-organizer/backend/app/routers/users.py +169 -0
  47. package/apps/travel-chaos-organizer/backend/app/routers/waitlist.py +37 -0
  48. package/apps/travel-chaos-organizer/backend/app/services/__init__.py +0 -0
  49. package/apps/travel-chaos-organizer/backend/app/services/cache.py +131 -0
  50. package/apps/travel-chaos-organizer/backend/app/services/drip.py +128 -0
  51. package/apps/travel-chaos-organizer/backend/app/services/email.py +61 -0
  52. package/apps/travel-chaos-organizer/backend/app/services/errors.py +44 -0
  53. package/apps/travel-chaos-organizer/backend/app/services/notifier.py +10 -0
  54. package/apps/travel-chaos-organizer/backend/app/services/ollama.py +101 -0
  55. package/apps/travel-chaos-organizer/backend/app/services/parser.py +22 -0
  56. package/apps/travel-chaos-organizer/backend/app/services/quota.py +85 -0
  57. package/apps/travel-chaos-organizer/backend/app/services/sentry.py +11 -0
  58. package/apps/travel-chaos-organizer/backend/app/services/telemetry.py +35 -0
  59. package/apps/travel-chaos-organizer/backend/pytest.ini +4 -0
  60. package/apps/travel-chaos-organizer/backend/requirements-dev.txt +6 -0
  61. package/apps/travel-chaos-organizer/backend/requirements.txt +25 -0
  62. package/apps/travel-chaos-organizer/backend/tests/__init__.py +0 -0
  63. package/apps/travel-chaos-organizer/backend/tests/conftest.py +223 -0
  64. package/apps/travel-chaos-organizer/backend/tests/test_admin.py +99 -0
  65. package/apps/travel-chaos-organizer/backend/tests/test_cache.py +142 -0
  66. package/apps/travel-chaos-organizer/backend/tests/test_drip.py +32 -0
  67. package/apps/travel-chaos-organizer/backend/tests/test_e2e_chain.py +121 -0
  68. package/apps/travel-chaos-organizer/backend/tests/test_export.py +48 -0
  69. package/apps/travel-chaos-organizer/backend/tests/test_inbox.py +107 -0
  70. package/apps/travel-chaos-organizer/backend/tests/test_items.py +138 -0
  71. package/apps/travel-chaos-organizer/backend/tests/test_mail.py +113 -0
  72. package/apps/travel-chaos-organizer/backend/tests/test_ollama.py +145 -0
  73. package/apps/travel-chaos-organizer/backend/tests/test_parse_url.py +98 -0
  74. package/apps/travel-chaos-organizer/backend/tests/test_parser.py +72 -0
  75. package/apps/travel-chaos-organizer/backend/tests/test_payments.py +163 -0
  76. package/apps/travel-chaos-organizer/backend/tests/test_quota.py +46 -0
  77. package/apps/travel-chaos-organizer/backend/tests/test_search.py +67 -0
  78. package/apps/travel-chaos-organizer/backend/tests/test_trips.py +123 -0
  79. package/apps/travel-chaos-organizer/backend/tests/test_users.py +52 -0
  80. package/apps/travel-chaos-organizer/backups/.gitkeep +0 -0
  81. package/apps/travel-chaos-organizer/caddy/Caddyfile +27 -0
  82. package/apps/travel-chaos-organizer/docker-compose.node1.yml +31 -0
  83. package/apps/travel-chaos-organizer/docker-compose.override.yml +14 -0
  84. package/apps/travel-chaos-organizer/docker-compose.yml +128 -0
  85. package/apps/travel-chaos-organizer/frontend/.env.example +13 -0
  86. package/apps/travel-chaos-organizer/frontend/app/(app)/_layout.tsx +80 -0
  87. package/apps/travel-chaos-organizer/frontend/app/(app)/inbox.tsx +166 -0
  88. package/apps/travel-chaos-organizer/frontend/app/(app)/search.tsx +130 -0
  89. package/apps/travel-chaos-organizer/frontend/app/(app)/settings.tsx +381 -0
  90. package/apps/travel-chaos-organizer/frontend/app/(app)/trips/[id].tsx +346 -0
  91. package/apps/travel-chaos-organizer/frontend/app/(app)/trips.tsx +363 -0
  92. package/apps/travel-chaos-organizer/frontend/app/(app)/upgrade/cancel.tsx +34 -0
  93. package/apps/travel-chaos-organizer/frontend/app/(app)/upgrade/success.tsx +35 -0
  94. package/apps/travel-chaos-organizer/frontend/app/(auth)/login.tsx +91 -0
  95. package/apps/travel-chaos-organizer/frontend/app/_layout.tsx +71 -0
  96. package/apps/travel-chaos-organizer/frontend/app/auth.tsx +29 -0
  97. package/apps/travel-chaos-organizer/frontend/app/index.tsx +16 -0
  98. package/apps/travel-chaos-organizer/frontend/app/onboarding.tsx +119 -0
  99. package/apps/travel-chaos-organizer/frontend/app.config.ts +54 -0
  100. package/apps/travel-chaos-organizer/frontend/assets/adaptive-icon.svg +66 -0
  101. package/apps/travel-chaos-organizer/frontend/assets/icon.svg +79 -0
  102. package/apps/travel-chaos-organizer/frontend/assets/splash.svg +15 -0
  103. package/apps/travel-chaos-organizer/frontend/components/BottomSheet.tsx +102 -0
  104. package/apps/travel-chaos-organizer/frontend/components/ConfettiCannon.tsx +107 -0
  105. package/apps/travel-chaos-organizer/frontend/components/ConfirmDialog.tsx +109 -0
  106. package/apps/travel-chaos-organizer/frontend/components/ErrorBoundary.tsx +45 -0
  107. package/apps/travel-chaos-organizer/frontend/components/FileUploadButton.tsx +19 -0
  108. package/apps/travel-chaos-organizer/frontend/components/ImportSheet.tsx +155 -0
  109. package/apps/travel-chaos-organizer/frontend/components/OfflineBadge.tsx +55 -0
  110. package/apps/travel-chaos-organizer/frontend/components/QRCodeViewer.tsx +80 -0
  111. package/apps/travel-chaos-organizer/frontend/components/ScoreRing.tsx +39 -0
  112. package/apps/travel-chaos-organizer/frontend/components/Screen.tsx +39 -0
  113. package/apps/travel-chaos-organizer/frontend/components/Skeleton.tsx +70 -0
  114. package/apps/travel-chaos-organizer/frontend/components/SwipeableInboxItem.tsx +121 -0
  115. package/apps/travel-chaos-organizer/frontend/components/SwipeableTimelineItem.tsx +70 -0
  116. package/apps/travel-chaos-organizer/frontend/components/TimelineItem.tsx +73 -0
  117. package/apps/travel-chaos-organizer/frontend/components/TimelineItemDetail.tsx +130 -0
  118. package/apps/travel-chaos-organizer/frontend/components/Toast.tsx +60 -0
  119. package/apps/travel-chaos-organizer/frontend/components/ToastContext.tsx +42 -0
  120. package/apps/travel-chaos-organizer/frontend/components/TripCard.tsx +146 -0
  121. package/apps/travel-chaos-organizer/frontend/components/TripQRModal.tsx +70 -0
  122. package/apps/travel-chaos-organizer/frontend/components/UpgradeBanner.tsx +44 -0
  123. package/apps/travel-chaos-organizer/frontend/eas.json +22 -0
  124. package/apps/travel-chaos-organizer/frontend/hooks/useInbox.ts +24 -0
  125. package/apps/travel-chaos-organizer/frontend/hooks/useNetworkSync.ts +60 -0
  126. package/apps/travel-chaos-organizer/frontend/hooks/useTrips.ts +112 -0
  127. package/apps/travel-chaos-organizer/frontend/lib/api.ts +223 -0
  128. package/apps/travel-chaos-organizer/frontend/lib/auth.ts +93 -0
  129. package/apps/travel-chaos-organizer/frontend/lib/db.ts +86 -0
  130. package/apps/travel-chaos-organizer/frontend/lib/fileCache.ts +54 -0
  131. package/apps/travel-chaos-organizer/frontend/lib/haptics.ts +13 -0
  132. package/apps/travel-chaos-organizer/frontend/lib/notifications.ts +112 -0
  133. package/apps/travel-chaos-organizer/frontend/lib/offlineQueue.ts +145 -0
  134. package/apps/travel-chaos-organizer/frontend/lib/quota.tsx +43 -0
  135. package/apps/travel-chaos-organizer/frontend/lib/sentry.ts +28 -0
  136. package/apps/travel-chaos-organizer/frontend/lib/shareIntent.ts +44 -0
  137. package/apps/travel-chaos-organizer/frontend/package-lock.json +13221 -0
  138. package/apps/travel-chaos-organizer/frontend/package.json +44 -0
  139. package/apps/travel-chaos-organizer/frontend/tsconfig.json +23 -0
  140. package/apps/travel-chaos-organizer/keycloak/realm-export.json +59 -0
  141. package/apps/travel-chaos-organizer/landing/index.html +773 -0
  142. package/apps/travel-chaos-organizer/scripts/backup.sh +14 -0
  143. package/apps/travel-chaos-organizer/scripts/configure-env.sh +82 -0
  144. package/apps/travel-chaos-organizer/scripts/deploy.sh +54 -0
  145. package/apps/travel-chaos-organizer/scripts/health-check.sh +36 -0
  146. package/apps/travel-chaos-organizer/scripts/migrate.sh +7 -0
  147. package/apps/travel-chaos-organizer/scripts/server-setup.sh +85 -0
  148. package/apps/travel-chaos-organizer/scripts/smoke-test.sh +29 -0
  149. package/apps/travel-chaos-organizer/scripts/verify-tracking.sh +71 -0
  150. package/apps/travel-chaos-organizer/telegram-bot/.env.example +3 -0
  151. package/apps/travel-chaos-organizer/telegram-bot/Dockerfile +6 -0
  152. package/apps/travel-chaos-organizer/telegram-bot/bot.py +504 -0
  153. package/apps/travel-chaos-organizer/telegram-bot/requirements.txt +3 -0
  154. package/dist/packages/telegram-notify/client.d.ts +19 -0
  155. package/dist/packages/telegram-notify/client.js +75 -0
  156. package/dist/packages/telegram-notify/client.js.map +1 -0
  157. package/dist/src/__tests__/auth.test.js.map +1 -0
  158. package/dist/{__tests__ → src/__tests__}/brain-flow.test.js +141 -0
  159. package/dist/src/__tests__/brain-flow.test.js.map +1 -0
  160. package/dist/src/__tests__/confidence.test.js.map +1 -0
  161. package/dist/src/__tests__/e2e/api-auth.test.js.map +1 -0
  162. package/dist/src/__tests__/e2e/api-health.test.js.map +1 -0
  163. package/dist/src/__tests__/e2e/cli.test.js.map +1 -0
  164. package/dist/src/__tests__/e2e/config.js.map +1 -0
  165. package/dist/src/__tests__/e2e/funnel.test.js.map +1 -0
  166. package/dist/src/__tests__/e2e/mcp-tools.test.js.map +1 -0
  167. package/dist/src/__tests__/e2e/provisioning.test.js.map +1 -0
  168. package/dist/{__tests__ → src/__tests__}/handlers-unit.test.js +0 -2
  169. package/dist/src/__tests__/handlers-unit.test.js.map +1 -0
  170. package/dist/src/__tests__/namespace.test.js.map +1 -0
  171. package/dist/src/__tests__/rerank.test.js +115 -0
  172. package/dist/src/__tests__/rerank.test.js.map +1 -0
  173. package/dist/src/auth.js.map +1 -0
  174. package/dist/src/bench/cachly-bench.d.ts +35 -0
  175. package/dist/src/bench/cachly-bench.js +182 -0
  176. package/dist/src/bench/cachly-bench.js.map +1 -0
  177. package/dist/src/bench/fixtures.d.ts +37 -0
  178. package/dist/src/bench/fixtures.js +225 -0
  179. package/dist/src/bench/fixtures.js.map +1 -0
  180. package/dist/src/ckg.js.map +1 -0
  181. package/dist/src/confidence.js.map +1 -0
  182. package/dist/src/embeddings.js.map +1 -0
  183. package/dist/src/handlers/advanced.js.map +1 -0
  184. package/dist/{handlers → src/handlers}/brain.js +335 -168
  185. package/dist/src/handlers/brain.js.map +1 -0
  186. package/dist/src/handlers/cache.js.map +1 -0
  187. package/dist/{handlers → src/handlers}/context.js +9 -4
  188. package/dist/src/handlers/context.js.map +1 -0
  189. package/dist/{handlers → src/handlers}/fedbrain.d.ts +6 -0
  190. package/dist/{handlers → src/handlers}/fedbrain.js +19 -7
  191. package/dist/src/handlers/fedbrain.js.map +1 -0
  192. package/dist/src/handlers/instances.js.map +1 -0
  193. package/dist/{handlers → src/handlers}/roadmap.js +7 -3
  194. package/dist/src/handlers/roadmap.js.map +1 -0
  195. package/dist/src/handlers/syndicate.js.map +1 -0
  196. package/dist/src/handlers/tco.d.ts +16 -0
  197. package/dist/src/handlers/tco.js +303 -0
  198. package/dist/src/handlers/tco.js.map +1 -0
  199. package/dist/{handlers → src/handlers}/team.js +57 -7
  200. package/dist/src/handlers/team.js.map +1 -0
  201. package/dist/{index.js → src/index.js} +209 -420
  202. package/dist/src/index.js.map +1 -0
  203. package/dist/src/namespace.js.map +1 -0
  204. package/dist/src/notifier.d.ts +5 -0
  205. package/dist/src/notifier.js +6 -0
  206. package/dist/src/notifier.js.map +1 -0
  207. package/dist/src/rerank.d.ts +55 -0
  208. package/dist/src/rerank.js +111 -0
  209. package/dist/src/rerank.js.map +1 -0
  210. package/dist/src/search.js.map +1 -0
  211. package/dist/src/search.test.d.ts +1 -0
  212. package/dist/src/search.test.js.map +1 -0
  213. package/dist/{tools.d.ts → src/tools.d.ts} +226 -0
  214. package/dist/{tools.js → src/tools.js} +196 -0
  215. package/dist/src/tools.js.map +1 -0
  216. package/dist/src/utils.js.map +1 -0
  217. package/package.json +8 -7
  218. package/packages/telegram-notify/SPLIT_PLAN.md +106 -0
  219. package/packages/telegram-notify/client.ts +88 -0
  220. package/packages/telegram-notify/emojis.json +22 -0
  221. package/packages/telegram-notify/pyproject.toml +17 -0
  222. package/packages/telegram-notify/telegram_notify/__init__.py +3 -0
  223. package/packages/telegram-notify/telegram_notify/client.py +87 -0
  224. package/packages/telegram-notify/telegram_notify/emojis.json +22 -0
  225. package/scripts/generate-icons.js +28 -0
  226. package/server.json +3 -3
  227. package/smithery.yaml +21 -8
  228. package/src/__tests__/brain-flow.test.ts +174 -0
  229. package/src/__tests__/handlers-unit.test.ts +1 -2
  230. package/src/__tests__/rerank.test.ts +130 -0
  231. package/src/bench/cachly-bench.ts +217 -0
  232. package/src/bench/fixtures.ts +256 -0
  233. package/src/handlers/brain.ts +345 -172
  234. package/src/handlers/context.ts +9 -4
  235. package/src/handlers/fedbrain.ts +21 -7
  236. package/src/handlers/roadmap.ts +7 -3
  237. package/src/handlers/tco.ts +347 -0
  238. package/src/handlers/team.ts +67 -7
  239. package/src/index.ts +224 -406
  240. package/src/notifier.ts +5 -0
  241. package/src/rerank.ts +148 -0
  242. package/src/tools.ts +210 -0
  243. package/tsconfig.json +2 -3
  244. package/dist/__tests__/auth.test.js.map +0 -1
  245. package/dist/__tests__/brain-flow.test.js.map +0 -1
  246. package/dist/__tests__/brain-predict-failures.test.d.ts +0 -17
  247. package/dist/__tests__/brain-predict-failures.test.js +0 -339
  248. package/dist/__tests__/brain-predict-failures.test.js.map +0 -1
  249. package/dist/__tests__/confidence.test.js.map +0 -1
  250. package/dist/__tests__/e2e/api-auth.test.js.map +0 -1
  251. package/dist/__tests__/e2e/api-health.test.js.map +0 -1
  252. package/dist/__tests__/e2e/cli.test.js.map +0 -1
  253. package/dist/__tests__/e2e/config.js.map +0 -1
  254. package/dist/__tests__/e2e/funnel.test.js.map +0 -1
  255. package/dist/__tests__/e2e/mcp-tools.test.js.map +0 -1
  256. package/dist/__tests__/e2e/provisioning.test.js.map +0 -1
  257. package/dist/__tests__/handlers-unit.test.js.map +0 -1
  258. package/dist/__tests__/namespace.test.js.map +0 -1
  259. package/dist/auth.js.map +0 -1
  260. package/dist/ckg.js.map +0 -1
  261. package/dist/confidence.js.map +0 -1
  262. package/dist/embeddings.js.map +0 -1
  263. package/dist/handlers/advanced.js.map +0 -1
  264. package/dist/handlers/brain.js.map +0 -1
  265. package/dist/handlers/cache.js.map +0 -1
  266. package/dist/handlers/context.js.map +0 -1
  267. package/dist/handlers/fedbrain.js.map +0 -1
  268. package/dist/handlers/instances.js.map +0 -1
  269. package/dist/handlers/roadmap.js.map +0 -1
  270. package/dist/handlers/syndicate.js.map +0 -1
  271. package/dist/handlers/team.js.map +0 -1
  272. package/dist/index.js.map +0 -1
  273. package/dist/namespace.js.map +0 -1
  274. package/dist/search.js.map +0 -1
  275. package/dist/search.test.js.map +0 -1
  276. package/dist/tools.js.map +0 -1
  277. package/dist/utils.js.map +0 -1
  278. package/src/__tests__/brain-predict-failures.test.ts +0 -430
  279. /package/dist/{__tests__ → src/__tests__}/auth.test.d.ts +0 -0
  280. /package/dist/{__tests__ → src/__tests__}/auth.test.js +0 -0
  281. /package/dist/{__tests__ → src/__tests__}/brain-flow.test.d.ts +0 -0
  282. /package/dist/{__tests__ → src/__tests__}/confidence.test.d.ts +0 -0
  283. /package/dist/{__tests__ → src/__tests__}/confidence.test.js +0 -0
  284. /package/dist/{__tests__ → src/__tests__}/e2e/api-auth.test.d.ts +0 -0
  285. /package/dist/{__tests__ → src/__tests__}/e2e/api-auth.test.js +0 -0
  286. /package/dist/{__tests__ → src/__tests__}/e2e/api-health.test.d.ts +0 -0
  287. /package/dist/{__tests__ → src/__tests__}/e2e/api-health.test.js +0 -0
  288. /package/dist/{__tests__ → src/__tests__}/e2e/cli.test.d.ts +0 -0
  289. /package/dist/{__tests__ → src/__tests__}/e2e/cli.test.js +0 -0
  290. /package/dist/{__tests__ → src/__tests__}/e2e/config.d.ts +0 -0
  291. /package/dist/{__tests__ → src/__tests__}/e2e/config.js +0 -0
  292. /package/dist/{__tests__ → src/__tests__}/e2e/funnel.test.d.ts +0 -0
  293. /package/dist/{__tests__ → src/__tests__}/e2e/funnel.test.js +0 -0
  294. /package/dist/{__tests__ → src/__tests__}/e2e/mcp-tools.test.d.ts +0 -0
  295. /package/dist/{__tests__ → src/__tests__}/e2e/mcp-tools.test.js +0 -0
  296. /package/dist/{__tests__ → src/__tests__}/e2e/provisioning.test.d.ts +0 -0
  297. /package/dist/{__tests__ → src/__tests__}/e2e/provisioning.test.js +0 -0
  298. /package/dist/{__tests__ → src/__tests__}/handlers-unit.test.d.ts +0 -0
  299. /package/dist/{__tests__ → src/__tests__}/namespace.test.d.ts +0 -0
  300. /package/dist/{__tests__ → src/__tests__}/namespace.test.js +0 -0
  301. /package/dist/{search.test.d.ts → src/__tests__/rerank.test.d.ts} +0 -0
  302. /package/dist/{auth.d.ts → src/auth.d.ts} +0 -0
  303. /package/dist/{auth.js → src/auth.js} +0 -0
  304. /package/dist/{ckg.d.ts → src/ckg.d.ts} +0 -0
  305. /package/dist/{ckg.js → src/ckg.js} +0 -0
  306. /package/dist/{confidence.d.ts → src/confidence.d.ts} +0 -0
  307. /package/dist/{confidence.js → src/confidence.js} +0 -0
  308. /package/dist/{embeddings.d.ts → src/embeddings.d.ts} +0 -0
  309. /package/dist/{embeddings.js → src/embeddings.js} +0 -0
  310. /package/dist/{handlers → src/handlers}/advanced.d.ts +0 -0
  311. /package/dist/{handlers → src/handlers}/advanced.js +0 -0
  312. /package/dist/{handlers → src/handlers}/brain.d.ts +0 -0
  313. /package/dist/{handlers → src/handlers}/cache.d.ts +0 -0
  314. /package/dist/{handlers → src/handlers}/cache.js +0 -0
  315. /package/dist/{handlers → src/handlers}/context.d.ts +0 -0
  316. /package/dist/{handlers → src/handlers}/instances.d.ts +0 -0
  317. /package/dist/{handlers → src/handlers}/instances.js +0 -0
  318. /package/dist/{handlers → src/handlers}/roadmap.d.ts +0 -0
  319. /package/dist/{handlers → src/handlers}/syndicate.d.ts +0 -0
  320. /package/dist/{handlers → src/handlers}/syndicate.js +0 -0
  321. /package/dist/{handlers → src/handlers}/team.d.ts +0 -0
  322. /package/dist/{index.d.ts → src/index.d.ts} +0 -0
  323. /package/dist/{namespace.d.ts → src/namespace.d.ts} +0 -0
  324. /package/dist/{namespace.js → src/namespace.js} +0 -0
  325. /package/dist/{search.d.ts → src/search.d.ts} +0 -0
  326. /package/dist/{search.js → src/search.js} +0 -0
  327. /package/dist/{search.test.js → src/search.test.js} +0 -0
  328. /package/dist/{utils.d.ts → src/utils.d.ts} +0 -0
  329. /package/dist/{utils.js → src/utils.js} +0 -0
package/src/index.ts CHANGED
@@ -1,6 +1,10 @@
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';
3
5
  import { readFile } from 'node:fs/promises';
6
+ import { execSync } from 'node:child_process';
7
+ import { join } from 'node:path';
4
8
  import { fileURLToPath } from 'node:url';
5
9
 
6
10
  // True only when this file is the entry point (not imported by tests or other modules).
@@ -35,11 +39,24 @@ const _isMain = process.argv[1] != null &&
35
39
  * ── Auth & Status ────────────────────────────────────────────────────────────
36
40
  * • get_api_status – check API health + JWT auth info (Keycloak)
37
41
  *
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
+ *
38
54
  * Configuration (env vars):
39
55
  * CACHLY_API_URL – default https://api.cachly.dev
40
56
  * CACHLY_JWT – your JWT (Keycloak access token)
41
57
  * CACHLY_EMBED_PROVIDER – embedding backend: openai (default), gemini, mistral, cohere, ollama, cachly (server fallback)
42
58
  * CACHLY_EMBED_MODEL – override embedding model (optional)
59
+ * TCO_API_URL – Travel Chaos Organizer backend URL (optional, enables tco_* tools)
43
60
  */
44
61
 
45
62
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
@@ -52,22 +69,19 @@ import {
52
69
  McpError,
53
70
  } from '@modelcontextprotocol/sdk/types.js';
54
71
  import { Redis } from 'ioredis';
55
- import { AsyncLocalStorage } from 'node:async_hooks';
56
72
 
57
73
  // ── Config ────────────────────────────────────────────────────────────────────
58
74
 
59
75
  const API_URL = process.env.CACHLY_API_URL ?? 'https://api.cachly.dev';
60
76
  let JWT = process.env.CACHLY_JWT ?? '';
61
- const CURRENT_VERSION = '0.10.55';
77
+ const EMBED_MODEL = process.env.CACHLY_EMBED_MODEL ?? '';
78
+ const CURRENT_VERSION = '0.10.49';
62
79
 
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; }
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);
71
85
 
72
86
  // ── Default Instance Resolution (for Smithery & single-credential setups) ────
73
87
  // When CACHLY_BRAIN_INSTANCE_ID is set, tools can omit the instance_id parameter.
@@ -77,28 +91,15 @@ let _defaultInstanceId: string = process.env.CACHLY_BRAIN_INSTANCE_ID ?? '';
77
91
  let _defaultInstanceLastAttempt = 0;
78
92
 
79
93
  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;
83
94
  if (_defaultInstanceId) return _defaultInstanceId;
84
- if (!getJWT()) return '';
95
+ if (!JWT) return '';
85
96
  // 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.
87
97
  const now = Date.now();
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
- };
98
+ if (_defaultInstanceLastAttempt > 0 && now - _defaultInstanceLastAttempt < 30_000) return '';
98
99
  _defaultInstanceLastAttempt = now;
99
100
  try {
100
101
  const res = await fetch(`${API_URL}/api/v1/instances`, {
101
- headers: { Authorization: `Bearer ${getJWT()}`, Accept: 'application/json' },
102
+ headers: { Authorization: `Bearer ${JWT}`, Accept: 'application/json' },
102
103
  signal: AbortSignal.timeout(5000),
103
104
  });
104
105
  if (!res.ok) return '';
@@ -185,28 +186,38 @@ async function pollDeviceFlow(flow: DeviceFlowState): Promise<'pending' | 'expir
185
186
  } catch { /* use raw JWT as fallback */ }
186
187
  JWT = apiKey;
187
188
  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 */ }
208
189
  _deviceFlow = null;
209
190
  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
+ })();
210
221
  // Auto-provision: find or create the user's brain instance.
211
222
  _defaultInstanceLastAttempt = 0;
212
223
  await resolveDefaultInstanceId();
@@ -214,7 +225,7 @@ async function pollDeviceFlow(flow: DeviceFlowState): Promise<'pending' | 'expir
214
225
  try {
215
226
  const autoRes = await fetch(`${API_URL}/api/v1/instances/auto`, {
216
227
  method: 'POST',
217
- headers: { Authorization: `Bearer ${getJWT()}`, 'Content-Type': 'application/json' },
228
+ headers: { Authorization: `Bearer ${JWT}`, 'Content-Type': 'application/json' },
218
229
  signal: AbortSignal.timeout(12000),
219
230
  });
220
231
  if (autoRes.ok) {
@@ -229,7 +240,7 @@ async function pollDeviceFlow(flow: DeviceFlowState): Promise<'pending' | 'expir
229
240
  // Log so telemetry can detect free-tier auto-provision failures
230
241
  void fetch(`${API_URL}/api/v1/telemetry/mcp`, {
231
242
  method: 'POST',
232
- headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${getJWT()}` },
243
+ headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${JWT}` },
233
244
  body: JSON.stringify({ event: 'auto_provision_failed', status: autoRes.status, version: CURRENT_VERSION }),
234
245
  signal: AbortSignal.timeout(3000),
235
246
  }).catch(() => {});
@@ -241,7 +252,7 @@ async function pollDeviceFlow(flow: DeviceFlowState): Promise<'pending' | 'expir
241
252
  if (_defaultInstanceId) {
242
253
  try {
243
254
  const checkRes = await fetch(`${API_URL}/api/v1/instances/${_defaultInstanceId}`, {
244
- headers: { Authorization: `Bearer ${getJWT()}`, Accept: 'application/json' },
255
+ headers: { Authorization: `Bearer ${JWT}`, Accept: 'application/json' },
245
256
  signal: AbortSignal.timeout(4000),
246
257
  });
247
258
  if (checkRes.ok) {
@@ -252,7 +263,7 @@ async function pollDeviceFlow(flow: DeviceFlowState): Promise<'pending' | 'expir
252
263
  while (checkInst.status === 'provisioning' && Date.now() < pDeadline) {
253
264
  await new Promise(r => setTimeout(r, 3000));
254
265
  const rr = await fetch(`${API_URL}/api/v1/instances/${_defaultInstanceId}`, {
255
- headers: { Authorization: `Bearer ${getJWT()}`, Accept: 'application/json' },
266
+ headers: { Authorization: `Bearer ${JWT}`, Accept: 'application/json' },
256
267
  signal: AbortSignal.timeout(4000),
257
268
  }).catch(() => null);
258
269
  if (rr?.ok) {
@@ -264,67 +275,51 @@ async function pollDeviceFlow(flow: DeviceFlowState): Promise<'pending' | 'expir
264
275
  }
265
276
  } catch { /* non-fatal — getConnection will handle the wait if still provisioning */ }
266
277
  }
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
+ }
267
301
  return 'done';
268
302
  }
269
303
  if (data.error === 'slow_down') flow.pollInterval = Math.min(flow.pollInterval + 2000, 15000);
270
304
  return 'pending';
271
305
  } catch { return 'pending'; }
272
306
  }
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;
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';
293
312
 
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 });
298
313
 
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
- }
321
314
  // ── Embeddings (imported from embeddings.ts) ────────────────────────────────
322
- import { setEmbedJwt } from './embeddings.js';
315
+ import { embedConfig, setEmbedJwt, EMBED_PROVIDER, computeEmbedding,
316
+ hasEmbedProvider, embedProviderHint } from './embeddings.js';
323
317
 
324
318
  // ── Search Engine (imported from search.ts) ─────────────────────────────────
325
319
  import { tokenize, splitMultiQuery, levenshtein, recencyBoost, extractTimestamp, STOPWORDS,
326
320
  katakanaToRomaji, arabicLightStem, expandCrossLingual, CROSS_LINGUAL_MAP,
327
- ZERO_RESULTS_LOG, zeroResultsTotal } from './search.js';
321
+ keywordSearch, ZERO_RESULTS_LOG, zeroResultsTotal, indexVocab as _indexVocab } from './search.js';
322
+ import type { KeywordMatch } from './search.js';
328
323
 
329
324
  // ── Exported for testing (re-export from search.ts) ─────────────────────────
330
325
  export { tokenize, splitMultiQuery, levenshtein, recencyBoost, extractTimestamp, STOPWORDS,
@@ -333,6 +328,19 @@ export { tokenize, splitMultiQuery, levenshtein, recencyBoost, extractTimestamp,
333
328
  // ── Types ─────────────────────────────────────────────────────────────────────
334
329
  // Instance is imported from handlers/brain.ts
335
330
 
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
+
336
344
  // ── Connection pool ───────────────────────────────────────────────────────────
337
345
 
338
346
  /** Reuse Redis connections across tool calls (keyed by instance_id). */
@@ -349,27 +357,16 @@ async function getConnection(instance_id: string): Promise<Redis> {
349
357
  );
350
358
  }
351
359
 
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
- }
360
+ if (pool.has(instance_id)) return pool.get(instance_id)!;
365
361
 
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.)
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.
368
365
  let inst = await apiFetch<Instance>(`/api/v1/instances/${instance_id}`);
369
366
  if (inst.status === 'provisioning') {
370
- const deadline = Date.now() + 8_000;
367
+ const deadline = Date.now() + PROVISION_TIMEOUT_MS;
371
368
  while (inst.status === 'provisioning' && Date.now() < deadline) {
372
- await new Promise(r => setTimeout(r, 2000));
369
+ await new Promise(r => setTimeout(r, 3000));
373
370
  inst = await apiFetch<Instance>(`/api/v1/instances/${instance_id}`);
374
371
  }
375
372
  }
@@ -378,7 +375,7 @@ async function getConnection(instance_id: string): Promise<Redis> {
378
375
  // Telemetry: unreachable instance so the team can proactively investigate
379
376
  void fetch(`${API_URL}/api/v1/telemetry/mcp`, {
380
377
  method: 'POST',
381
- headers: { 'Content-Type': 'application/json', ...(getJWT() ? { Authorization: `Bearer ${getJWT()}` } : {}) },
378
+ headers: { 'Content-Type': 'application/json', ...(JWT ? { Authorization: `Bearer ${JWT}` } : {}) },
382
379
  body: JSON.stringify({ event: 'instance_not_reachable', instance_id, status: inst.status, version: CURRENT_VERSION }),
383
380
  signal: AbortSignal.timeout(3000),
384
381
  }).catch(() => {});
@@ -409,9 +406,7 @@ async function getConnection(instance_id: string): Promise<Redis> {
409
406
 
410
407
  // Fetch connection details (includes password) from the dedicated endpoint.
411
408
  let password: string | undefined;
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;
409
+ let tlsEnabled = inst.tls_enabled !== false; // default true
415
410
  try {
416
411
  const conn = await apiFetch<{ password?: string; tls_enabled?: boolean }>(
417
412
  `/api/v1/instances/${instance_id}/connection`
@@ -433,11 +428,11 @@ async function getConnection(instance_id: string): Promise<Redis> {
433
428
  host: inst.host,
434
429
  port: inst.port,
435
430
  password: password || undefined,
431
+ commandTimeout: 8000,
436
432
  ...(tlsEnabled ? { tls: {} } : {}),
437
433
  lazyConnect: true,
438
434
  enableReadyCheck: true,
439
435
  connectTimeout: 8000,
440
- commandTimeout: 10000,
441
436
  retryStrategy: () => null, // fail fast, no reconnect loops in MCP context
442
437
  });
443
438
 
@@ -445,46 +440,14 @@ async function getConnection(instance_id: string): Promise<Redis> {
445
440
  client.on('error', evict);
446
441
  client.on('end', evict);
447
442
 
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) {
443
+ try {
444
+ await client.connect();
445
+ } catch (err: unknown) {
483
446
  client.disconnect();
484
- const msg = connectErr instanceof Error ? connectErr.message : String(connectErr);
447
+ const msg = err instanceof Error ? err.message : String(err);
485
448
  void fetch(`${API_URL}/api/v1/telemetry/mcp`, {
486
449
  method: 'POST',
487
- headers: { 'Content-Type': 'application/json', ...(getJWT() ? { Authorization: `Bearer ${getJWT()}` } : {}) },
450
+ headers: { 'Content-Type': 'application/json', ...(JWT ? { Authorization: `Bearer ${JWT}` } : {}) },
488
451
  body: JSON.stringify({ event: 'redis_connect_failed', instance_id, host: inst.host, port: inst.port, error: msg, version: CURRENT_VERSION }),
489
452
  signal: AbortSignal.timeout(3000),
490
453
  }).catch(() => {});
@@ -504,14 +467,14 @@ async function getConnection(instance_id: string): Promise<Redis> {
504
467
  // ── API helper ────────────────────────────────────────────────────────────────
505
468
 
506
469
  async function apiFetch<T>(path: string, options: RequestInit = {}): Promise<T> {
507
- checkJwt(getJWT());
470
+ checkJwt(JWT);
508
471
  const timeoutMs = Number(process.env.CACHLY_API_TIMEOUT_MS ?? 15_000);
509
472
  const res = await fetch(`${API_URL}${path}`, {
510
473
  ...options,
511
474
  signal: AbortSignal.timeout(timeoutMs),
512
475
  headers: {
513
476
  'Content-Type': 'application/json',
514
- Authorization: `Bearer ${getJWT()}`,
477
+ Authorization: `Bearer ${JWT}`,
515
478
  ...options.headers,
516
479
  },
517
480
  });
@@ -524,6 +487,11 @@ async function apiFetch<T>(path: string, options: RequestInit = {}): Promise<T>
524
487
  return res.json() as Promise<T>;
525
488
  }
526
489
 
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';
527
495
  import { handleBrainTool } from './handlers/brain.js';
528
496
  import { handleContextTool } from './handlers/context.js';
529
497
  import { handleInstanceTool } from './handlers/instances.js';
@@ -532,7 +500,7 @@ import { handleTeamTool } from './handlers/team.js';
532
500
  import { handleRoadmapTool } from './handlers/roadmap.js';
533
501
  import { handleAdvancedTool } from './handlers/advanced.js';
534
502
  import { handleSyndicateTool } from './handlers/syndicate.js';
535
- import { handleFedbrainTool } from './handlers/fedbrain.js';
503
+ import { handleFedbrainTool, _lastBrainFromGitCounts } from './handlers/fedbrain.js';
536
504
  import type { Instance } from './handlers/brain.js';
537
505
 
538
506
  // ── Tools (imported from tools.ts) ─────────────────────────────────────────
@@ -561,10 +529,7 @@ function sendFunnelEvent(event: string, extra?: Record<string, unknown>): void {
561
529
  headers: { 'Content-Type': 'application/json' },
562
530
  body: JSON.stringify({
563
531
  event, version: CURRENT_VERSION, editor: detectEditor(),
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() } : {}),
532
+ ...(JWT ? { jwt: JWT } : {}),
568
533
  ...extra,
569
534
  }),
570
535
  signal: AbortSignal.timeout(3000),
@@ -586,20 +551,12 @@ async function sendAnonymousTelemetry(toolName: string): Promise<void> {
586
551
  }
587
552
 
588
553
  async function handleTool(name: string, args: Record<string, unknown>): Promise<string> {
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
-
554
+ // Guard: if no JWT, return actionable onboarding message instead of HTTP 401
555
+ if (!JWT) {
599
556
  void sendAnonymousTelemetry(name);
600
557
 
601
- // ── Step 2: Zero-credential device flow ─────────────────────────────
602
- // 1st call after silent setup fails: start device flow, return code + URL
558
+ // ── Zero-credential device flow ─────────────────────────────────────
559
+ // 1st call: start device flow, return code + URL
603
560
  // 2nd+ calls: poll for token; once authenticated, proceed transparently
604
561
  if (_deviceFlow) {
605
562
  const result = await pollDeviceFlow(_deviceFlow);
@@ -647,7 +604,7 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
647
604
  '',
648
605
  'After sign-in: call **any tool again** — your Brain activates instantly.',
649
606
  '',
650
- '✨ Free forever · No credit card · 89 MCP tools · GDPR · EU servers',
607
+ '✨ Free forever · No credit card · 95 MCP tools · GDPR · EU servers',
651
608
  ].join('\n');
652
609
  }
653
610
 
@@ -673,16 +630,16 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
673
630
  }
674
631
 
675
632
  // Delegate brain tools (learn, recall, session, etc.)
676
- let brainResult = await handleBrainTool(name, args, getConnection, apiFetch);
633
+ const brainResult = await handleBrainTool(name, args, getConnection, apiFetch);
677
634
  if (brainResult !== null) {
678
- if (!_firstCallSuccessSent && getJWT()) {
635
+ if (!_firstCallSuccessSent && JWT) {
679
636
  _firstCallSuccessSent = true;
680
- sendFunnelEvent('first_call_success', { tool: name, instance_id: args.instance_id ?? getDefaultInstanceId() ?? '' });
637
+ sendFunnelEvent('first_call_success', { tool: name, instance_id: args.instance_id ?? _defaultInstanceId ?? '' });
681
638
  }
682
639
  // Per-tool telemetry — fires after every successful brain tool call.
683
640
  // api_key carries the cky_live_... token so the backend can resolve tenant + increment counters.
684
- const instanceId = (args.instance_id as string | undefined) ?? getDefaultInstanceId() ?? '';
685
- const telemetryExtra = getJWT() ? { api_key: getJWT(), instance_id: instanceId } : { instance_id: instanceId };
641
+ const instanceId = (args.instance_id as string | undefined) ?? _defaultInstanceId ?? '';
642
+ const telemetryExtra = JWT ? { api_key: JWT, instance_id: instanceId } : { instance_id: instanceId };
686
643
  if (name === 'recall_best_solution') {
687
644
  // recall_best_solution → increments BrainRecallCount + triggers level-up logic
688
645
  sendFunnelEvent('recall_best_solution', telemetryExtra);
@@ -698,40 +655,25 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
698
655
  if (!isFirstSession && resultText.length > 100) {
699
656
  // Existing brain — session_start acts as implicit recall → increment counter.
700
657
  sendFunnelEvent('recall_best_solution', telemetryExtra);
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) {
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.
708
661
  try {
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;
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;
723
676
  }
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((gitBootstrap) => {
730
- if (gitBootstrap !== null) {
731
- sendFunnelEvent('brain_from_git', telemetryExtra);
732
- sendFunnelEvent('recall_best_solution', telemetryExtra);
733
- }
734
- }).catch(() => { /* non-critical */ });
735
677
  }
736
678
  } catch { /* git bootstrap errors must never break session_start */ }
737
679
  }
@@ -745,7 +687,7 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
745
687
  const srText = typeof brainResult === 'object' && brainResult !== null && 'content' in brainResult
746
688
  ? JSON.stringify(brainResult)
747
689
  : String(brainResult ?? '');
748
- if (srText.length > 50 && !srText.includes('No lessons found') && !srText.includes('no lessons')) {
690
+ if (srText.length > 50 && !srText.includes('No lessons found') && !srText.includes('no lessons') && !srText.includes('No matches found')) {
749
691
  sendFunnelEvent('recall_best_solution', telemetryExtra);
750
692
  }
751
693
  }
@@ -755,9 +697,9 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
755
697
  // Delegate context tools (remember/recall/list/forget)
756
698
  const contextResult = await handleContextTool(name, args, getConnection, apiFetch);
757
699
  if (contextResult !== null) {
758
- if (!_firstCallSuccessSent && getJWT()) {
700
+ if (!_firstCallSuccessSent && JWT) {
759
701
  _firstCallSuccessSent = true;
760
- sendFunnelEvent('first_call_success', { tool: name, instance_id: args.instance_id ?? getDefaultInstanceId() ?? '' });
702
+ sendFunnelEvent('first_call_success', { tool: name, instance_id: args.instance_id ?? _defaultInstanceId ?? '' });
761
703
  }
762
704
  return contextResult;
763
705
  }
@@ -783,8 +725,8 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
783
725
  if (syndicateResult !== null) {
784
726
  // brain_predict is in syndicate handler — track telemetry here.
785
727
  if (name === 'brain_predict') {
786
- const instanceId = (args.instance_id as string | undefined) ?? getDefaultInstanceId() ?? '';
787
- const telemetryExtra = getJWT() ? { api_key: getJWT(), instance_id: instanceId } : { instance_id: instanceId };
728
+ const instanceId = (args.instance_id as string | undefined) ?? _defaultInstanceId ?? '';
729
+ const telemetryExtra = JWT ? { api_key: JWT, instance_id: instanceId } : { instance_id: instanceId };
788
730
  sendFunnelEvent('brain_predict', telemetryExtra);
789
731
  }
790
732
  return syndicateResult;
@@ -793,16 +735,21 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
793
735
  const fedbrainResult = await handleFedbrainTool(name, args, getConnection, apiFetch);
794
736
  if (fedbrainResult !== null) {
795
737
  // brain_from_git and brain_predict_failures are in fedbrain handler.
796
- const instanceId = (args.instance_id as string | undefined) ?? getDefaultInstanceId() ?? '';
797
- const telemetryExtra = getJWT() ? { api_key: getJWT(), instance_id: instanceId } : { instance_id: instanceId };
738
+ const instanceId = (args.instance_id as string | undefined) ?? _defaultInstanceId ?? '';
739
+ const telemetryExtra = JWT ? { api_key: JWT, instance_id: instanceId } : { instance_id: instanceId };
798
740
  if (name === 'brain_from_git') {
799
- sendFunnelEvent('brain_from_git', telemetryExtra);
741
+ const gitCounts = _lastBrainFromGitCounts;
742
+ sendFunnelEvent('brain_from_git', { ...telemetryExtra, ...(gitCounts ?? {}) });
800
743
  } else if (name === 'brain_predict_failures') {
801
744
  sendFunnelEvent('brain_predict_failures', telemetryExtra);
802
745
  }
803
746
  return fedbrainResult;
804
747
  }
805
748
 
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
+
806
753
  switch (name) {
807
754
  // ── Instance management ──────────────────────────────────────────────
808
755
  case 'get_api_status': {
@@ -821,7 +768,7 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
821
768
  }
822
769
 
823
770
  // Check JWT / auth
824
- if (!getJWT()) {
771
+ if (!JWT) {
825
772
  return [
826
773
  `📡 **cachly API Status**`,
827
774
  ``,
@@ -834,38 +781,28 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
834
781
  }
835
782
 
836
783
  // Decode JWT claims (inspection only, no verification)
837
- const jwt = getJWT();
838
- let authInfo: string;
839
- // API keys (cky_live_... / cky_trial_...) are valid long-lived credentials.
840
- // Only try to decode as JWT (3-part base64url) if it looks like one.
841
- if (jwt.startsWith('cky_')) {
842
- const keyType = jwt.startsWith('cky_trial_') ? 'Trial' : 'Live';
843
- const keyId = jwt.slice(0, 20) + '…';
844
- authInfo = `✅ API key (${keyType})\n Key prefix: ${keyId}`;
845
- } else {
846
- authInfo = '❌ invalid credential format';
847
- try {
848
- const parts = jwt.split('.');
849
- if (parts.length === 3) {
850
- const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf-8')) as {
851
- sub?: string; exp?: number; iss?: string;
852
- };
853
- const sub = payload.sub ?? '(unknown)';
854
- const iss = payload.iss ?? '(unknown)';
855
- const provider = iss.includes('keycloak') ? 'Keycloak' : 'OIDC';
856
- const expTs = payload.exp ? new Date(payload.exp * 1000) : null;
857
- const expired = expTs ? expTs < new Date() : false;
858
- authInfo = [
859
- `✅ JWT decoded`,
860
- ` Sub (user ID): ${sub}`,
861
- ` Provider: ${provider}`,
862
- ` Issuer: ${iss}`,
863
- ` Expires: ${expTs ? expTs.toISOString() : 'never'} ${expired ? '⚠️ EXPIRED – get a new token!' : '✅'}`,
864
- ].join('\n');
865
- }
866
- } catch {
867
- authInfo = '❌ JWT decode failed – check CACHLY_JWT format';
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');
868
803
  }
804
+ } catch {
805
+ authInfo = '❌ JWT decode failed – check CACHLY_JWT format';
869
806
  }
870
807
 
871
808
  // List instances + connectivity
@@ -879,7 +816,7 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
879
816
  const lines: string[] = ['\n\n🧠 **Brain Instances:**'];
880
817
  for (const inst of instances) {
881
818
  const badge = inst.status === 'running' ? '🟢' : inst.status === 'provisioning' ? '🟡' : '🔴';
882
- const defaultMark = inst.id === getDefaultInstanceId() ? ' ← active' : '';
819
+ const defaultMark = inst.id === _defaultInstanceId ? ' ← active' : '';
883
820
  lines.push(` ${badge} **${inst.name}** (\`${inst.id}\`) · ${inst.status}${defaultMark}`);
884
821
  if (inst.status === 'running' && inst.host) {
885
822
  // Try a quick ping on the cached connection
@@ -1007,14 +944,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1007
944
  || _defaultInstanceId
1008
945
  || process.env.CACHLY_BRAIN_INSTANCE_ID;
1009
946
  if (instanceId) {
1010
- // Fire-and-forget — session management is non-critical and must never block tool calls.
1011
- // The session_start briefing (which includes brain_from_git) can take 30-120s on large
1012
- // repos; awaiting it here would freeze the user's first MCP tool call entirely.
1013
- void autoStartSession(instanceId);
947
+ await autoStartSession(instanceId).catch(() => undefined);
1014
948
  }
949
+ } else if (_autoSessionStarting) {
950
+ // Another concurrent call is mid-start — wait for it before proceeding.
951
+ await _autoSessionStarting.catch(() => undefined);
1015
952
  }
1016
- // Note: we intentionally do NOT await _autoSessionStarting for any call.
1017
- // The session tracker runs in background — tool calls must always proceed immediately.
1018
953
  if (!sessionTools.has(name)) _autoSessionToolCount++;
1019
954
 
1020
955
  try {
@@ -1022,7 +957,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1022
957
  return { content: [{ type: 'text', text }] };
1023
958
  } catch (err) {
1024
959
  if (err instanceof McpError) throw err;
1025
- throw new McpError(ErrorCode.InternalError, (err as Error).message);
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);
1026
963
  }
1027
964
  });
1028
965
 
@@ -1911,7 +1848,7 @@ if (!process.argv[2] && process.stdout.isTTY) {
1911
1848
  console.log(' \x1b[36m npx @cachly-dev/mcp-server@latest upgrade\x1b[0m ← Check for updates');
1912
1849
  console.log('');
1913
1850
  console.log(' \x1b[90mWorks with: Claude Code · Cursor · Windsurf · GitHub Copilot · Cline · Zed\x1b[0m');
1914
- console.log(' \x1b[90mFree forever · GDPR · German servers · 89 MCP tools\x1b[0m');
1851
+ console.log(' \x1b[90mFree forever · GDPR · German servers · 95 MCP tools\x1b[0m');
1915
1852
  console.log('');
1916
1853
  process.exit(0);
1917
1854
  }
@@ -2084,7 +2021,7 @@ if (process.argv[2] === 'status') {
2084
2021
  if (process.argv[2] === 'health') {
2085
2022
  const { existsSync } = await import('node:fs');
2086
2023
  const { readFile } = await import('node:fs/promises');
2087
- const { resolve } = await import('node:path');
2024
+ const { resolve, join: pJoin } = await import('node:path');
2088
2025
 
2089
2026
  const cwd = process.cwd();
2090
2027
  let passed = 0;
@@ -2101,9 +2038,6 @@ if (process.argv[2] === 'health') {
2101
2038
  const jwt = process.env.CACHLY_JWT ?? '';
2102
2039
  if (!jwt) {
2103
2040
  fail('CACHLY_JWT not set — run: npx @cachly-dev/mcp-server@latest setup');
2104
- } else if (jwt.startsWith('cky_')) {
2105
- // Opaque cachly API key — no JWT expiry to check, format is valid by prefix
2106
- ok('API key set (cky_*** opaque key)');
2107
2041
  } else {
2108
2042
  try {
2109
2043
  const parts = jwt.split('.');
@@ -2121,7 +2055,7 @@ if (process.argv[2] === 'health') {
2121
2055
  ok(`JWT valid${minsLeft !== null ? ` (expires in ${Math.floor(minsLeft / 60)}h ${minsLeft % 60}m)` : ''}`);
2122
2056
  }
2123
2057
  } catch {
2124
- fail('CACHLY_JWT format invalid (expected a cky_*** API key or a JWT with 3 parts)');
2058
+ fail('CACHLY_JWT format invalid (expected JWT with 3 parts)');
2125
2059
  }
2126
2060
  }
2127
2061
 
@@ -2169,23 +2103,11 @@ if (process.argv[2] === 'health') {
2169
2103
  signal: AbortSignal.timeout(5000),
2170
2104
  });
2171
2105
  if (connRes.ok) {
2172
- const conn = await connRes.json() as { password?: string; connection_string?: string; tls_enabled?: boolean };
2173
- // password may be a direct field, or embedded in connection_string as redis://:pass@host:port
2174
- if (conn.password) {
2175
- pingPassword = conn.password;
2176
- } else if (conn.connection_string) {
2177
- try {
2178
- const u = new URL(conn.connection_string);
2179
- if (u.password) pingPassword = decodeURIComponent(u.password);
2180
- } catch { /* leave pingPassword undefined */ }
2181
- }
2182
- pingTls = conn.tls_enabled === true;
2183
- } else {
2184
- warn(`Could not fetch Redis credentials (HTTP ${connRes.status}) — connectivity test may fail`);
2106
+ const conn = await connRes.json() as { password?: string; tls_enabled?: boolean };
2107
+ pingPassword = conn.password;
2108
+ pingTls = conn.tls_enabled !== false;
2185
2109
  }
2186
- } catch (connErr) {
2187
- warn(`Could not fetch Redis credentials — ${(connErr as Error).message}`);
2188
- }
2110
+ } catch { /* proceed without password */ }
2189
2111
  const pingClient = new Redis({
2190
2112
  host: inst.host, port: inst.port,
2191
2113
  password: pingPassword,
@@ -2313,6 +2235,7 @@ if (process.argv[2] === 'setup') {
2313
2235
  const CLIENT_ID = 'cachly-cli';
2314
2236
 
2315
2237
  console.log('Step 1: Sign in to cachly (free, no credit card)\n');
2238
+ sendFunnelEvent('setup_auth_started');
2316
2239
 
2317
2240
  // Start device flow
2318
2241
  let deviceCode = '', userCode = '', verifyUri = '', pollInterval = 5000;
@@ -2336,6 +2259,7 @@ if (process.argv[2] === 'setup') {
2336
2259
  console.error('Falling back: sign in at https://cachly.dev/setup-ai and paste your API token.\n');
2337
2260
  token = await ask(' Paste API token (cky_live_...): ');
2338
2261
  if (!token) { console.error('\nToken is required. Aborting.\n'); rl.close(); process.exit(1); }
2262
+ sendFunnelEvent('setup_auth_completed');
2339
2263
  console.log('');
2340
2264
  deviceCode = ''; // mark as fallback so we skip polling
2341
2265
  }
@@ -2369,6 +2293,7 @@ if (process.argv[2] === 'setup') {
2369
2293
  if (tokenData.access_token) {
2370
2294
  token = tokenData.access_token;
2371
2295
  console.log(' \x1b[32m✓ Authorized!\x1b[0m\n');
2296
+ sendFunnelEvent('setup_auth_completed');
2372
2297
  break;
2373
2298
  }
2374
2299
  // authorization_pending = keep polling; slow_down = increase interval
@@ -2400,7 +2325,7 @@ if (process.argv[2] === 'setup') {
2400
2325
  if (!keyBody.key) throw new Error('no key in response');
2401
2326
  token = keyBody.key; // swap JWT → cky_live_...
2402
2327
  console.log(' ✓\n');
2403
- } catch {
2328
+ } catch (e) {
2404
2329
  console.log(' (skipped)\n');
2405
2330
  // Non-fatal: fall back to using the Keycloak JWT directly.
2406
2331
  // It will expire but setup still works for now.
@@ -2449,9 +2374,10 @@ if (process.argv[2] === 'setup') {
2449
2374
  // Newly created — poll until running or give up after 30 s.
2450
2375
  const newId = autoBody.instance_id;
2451
2376
  console.log(` ✓ created (${newId.slice(0, 8)}…)\n`);
2377
+ // 45 attempts × 3s = 135s — generous for free-tier in slow regions.
2452
2378
  process.stdout.write('⏳ Waiting for instance to start');
2453
- for (let attempt = 0; attempt < 15; attempt++) {
2454
- await new Promise(r => setTimeout(r, 2000));
2379
+ for (let attempt = 0; attempt < 45; attempt++) {
2380
+ await new Promise(r => setTimeout(r, 3000));
2455
2381
  process.stdout.write('.');
2456
2382
  try {
2457
2383
  const checkRes = await fetch(`${API_URL}/api/v1/instances/${newId}`, { headers: { Authorization: `Bearer ${token}` } });
@@ -2485,6 +2411,7 @@ if (process.argv[2] === 'setup') {
2485
2411
  console.log(` (Run with --instance-id <id> to use a different one)\n`);
2486
2412
  }
2487
2413
  console.log(`✓ Instance: ${instance.name} (${instance.id.slice(0, 8)}…)\n`);
2414
+ sendFunnelEvent('setup_instance_ready', { instance_id: instance.id });
2488
2415
 
2489
2416
  // ── Step 3: Detect editors ────────────────────────────────────────────────
2490
2417
  const cwd = process.cwd();
@@ -2730,7 +2657,7 @@ if (process.argv[2] === 'index') {
2730
2657
  const maxFiles = parseInt(flag('max-files') ?? '500', 10);
2731
2658
  const namespace = flag('namespace') ?? 'cachly:sem:code';
2732
2659
 
2733
- if (!instanceId || !getJWT()) {
2660
+ if (!instanceId || !JWT) {
2734
2661
  console.error('\n❌ CACHLY_BRAIN_INSTANCE_ID and CACHLY_JWT must be set\n');
2735
2662
  console.error(' export CACHLY_BRAIN_INSTANCE_ID=<uuid>');
2736
2663
  console.error(' export CACHLY_JWT=<cky_live_...>');
@@ -2772,7 +2699,7 @@ if (process.argv[2] === 'learn-git') {
2772
2699
  const instanceId = flag('instance-id') ?? process.env.CACHLY_BRAIN_INSTANCE_ID;
2773
2700
  const maxCommits = parseInt(flag('max-commits') ?? '50', 10);
2774
2701
 
2775
- if (!instanceId || !getJWT()) {
2702
+ if (!instanceId || !JWT) {
2776
2703
  console.error('\n❌ CACHLY_BRAIN_INSTANCE_ID and CACHLY_JWT must be set\n');
2777
2704
  console.error(' export CACHLY_BRAIN_INSTANCE_ID=<uuid>');
2778
2705
  console.error(' export CACHLY_JWT=<cky_live_...>');
@@ -2790,7 +2717,7 @@ if (process.argv[2] === 'learn-git') {
2790
2717
  limit: maxCommits,
2791
2718
  });
2792
2719
  console.log(result);
2793
- sendFunnelEvent('brain_from_git', { instance_id: instanceId });
2720
+ sendFunnelEvent('brain_from_git', { api_key: JWT, instance_id: instanceId });
2794
2721
  console.log('\n✅ Brain learned from your commits.\n');
2795
2722
  // Let fire-and-forget telemetry flush before exit.
2796
2723
  await new Promise(r => setTimeout(r, 300));
@@ -2807,8 +2734,7 @@ if (process.argv[2] === 'learn-git') {
2807
2734
  // actionable message in their editor's MCP log instead of silent failures.
2808
2735
  // Skip for CLI commands that intentionally run without credentials.
2809
2736
  const _cliNoAuthCommands = ['demo', 'share', 'health', 'setup', 'init', 'digest', 'invite', 'badge', 'join', 'upgrade'];
2810
- // In multi-tenant HTTP mode (PORT set) we never need a startup JWT — per-request auth.
2811
- if (!JWT && !process.env.PORT && !_cliNoAuthCommands.includes(process.argv[2] ?? '') && !(!process.argv[2] && process.stdout.isTTY)) {
2737
+ if (!JWT && !_cliNoAuthCommands.includes(process.argv[2] ?? '') && !(!process.argv[2] && process.stdout.isTTY)) {
2812
2738
  // No args + no JWT in a non-TTY context = running as MCP server without credentials.
2813
2739
  // Print actionable setup banner to stdout so it's captured by callers/tests.
2814
2740
  const banner =
@@ -2881,76 +2807,12 @@ if (!process.env.CACHLY_NO_UPDATE_CHECK) {
2881
2807
 
2882
2808
  const httpPort = process.env.PORT ? parseInt(process.env.PORT, 10) : undefined;
2883
2809
 
2884
- // ── Per-request MCP Server factory (HTTP / Smithery stateless mode) ──────────
2885
- // SDK ≥1.9 forbids reusing a stateless transport across requests.
2886
- // Solution: create a fresh Server+Transport per /mcp request.
2887
- // The module-level `server` below is used for stdio mode only.
2888
- function createHttpMcpServer(): Server {
2889
- const srv = new Server(
2890
- { name: 'cachly-mcp', version: CURRENT_VERSION },
2891
- { capabilities: { tools: {} } }
2892
- );
2893
- srv.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
2894
- srv.setRequestHandler(CallToolRequestSchema, async (request) => {
2895
- const { name, arguments: args } = request.params;
2896
- try {
2897
- const text = await handleTool(name, (args ?? {}) as Record<string, unknown>);
2898
- return { content: [{ type: 'text', text }] };
2899
- } catch (err) {
2900
- if (err instanceof McpError) throw err;
2901
- throw new McpError(ErrorCode.InternalError, (err as Error).message);
2902
- }
2903
- });
2904
- return srv;
2905
- }
2906
-
2907
2810
  if (httpPort) {
2908
2811
  // ── HTTP mode (Streamable HTTP transport) ───────────────────────────────
2909
- // Used for Smithery URL deployment: PORT=3000 node dist/index.js
2910
- // Multi-tenant: per-request JWT + instance ID resolved via:
2911
- // 1. Authorization: Bearer <jwt> + X-Cachly-Instance-Id: <uuid>
2912
- // 2. Smithery config: ?config=base64url({CACHLY_JWT, CACHLY_BRAIN_INSTANCE_ID, ...})
2913
- // Falls back to module-level env-derived values when neither is provided.
2812
+ // Used for Smithery URL deployment: PORT=3000 node dist/src/index.js
2914
2813
  const { createServer } = await import('node:http');
2915
-
2916
- function extractReqCtx(req: import('node:http').IncomingMessage, url: URL): ReqCtx {
2917
- let jwtVal = '';
2918
- let instId = '';
2919
- const auth = req.headers['authorization'];
2920
- if (typeof auth === 'string' && auth.trim()) {
2921
- const a = auth.trim();
2922
- jwtVal = a.toLowerCase().startsWith('bearer ') ? a.slice(7).trim() : a;
2923
- }
2924
- // Fallback: raw JWT in custom header (Smithery "header" transport without Bearer prefix)
2925
- if (!jwtVal) {
2926
- const rawJwt = req.headers['cachly_jwt'] ?? req.headers['cachly-jwt'] ?? req.headers['x-cachly-jwt'] ?? req.headers['x-api-key'];
2927
- if (typeof rawJwt === 'string') jwtVal = rawJwt.trim();
2928
- }
2929
- const hdrInst = req.headers['x-cachly-instance-id'];
2930
- if (typeof hdrInst === 'string') instId = hdrInst.trim();
2931
- // Smithery passes user config as a base64url-encoded JSON blob in ?config=
2932
- const cfgParam = url.searchParams.get('config');
2933
- if (cfgParam && (!jwtVal || !instId)) {
2934
- try {
2935
- const b64 = cfgParam.replace(/-/g, '+').replace(/_/g, '/');
2936
- const padded = b64 + '='.repeat((4 - b64.length % 4) % 4);
2937
- const json = JSON.parse(Buffer.from(padded, 'base64').toString('utf8'));
2938
- if (!jwtVal && typeof json.CACHLY_JWT === 'string') jwtVal = json.CACHLY_JWT;
2939
- if (!instId && typeof json.CACHLY_BRAIN_INSTANCE_ID === 'string') instId = json.CACHLY_BRAIN_INSTANCE_ID;
2940
- } catch { /* malformed config — ignore, fall back to globals */ }
2941
- }
2942
- // Direct query params (Smithery "query" transport, manual clients, etc.)
2943
- if (!jwtVal) {
2944
- const q = url.searchParams.get('CACHLY_JWT') ?? url.searchParams.get('cachly_jwt') ?? url.searchParams.get('api_key');
2945
- if (q) jwtVal = q.trim();
2946
- }
2947
- if (!instId) {
2948
- const q = url.searchParams.get('CACHLY_BRAIN_INSTANCE_ID') ?? url.searchParams.get('cachly_brain_instance_id') ?? url.searchParams.get('instance_id');
2949
- if (q) instId = q.trim();
2950
- }
2951
- return { jwt: jwtVal, instanceId: instId };
2952
- }
2953
-
2814
+ const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
2815
+ await server.connect(transport);
2954
2816
  const httpServer = createServer((req, res) => {
2955
2817
  const url = new URL(req.url ?? '/', `http://localhost:${httpPort}`);
2956
2818
  if (url.pathname === '/health') {
@@ -2958,8 +2820,6 @@ if (httpPort) {
2958
2820
  res.end(JSON.stringify({
2959
2821
  status: 'ok',
2960
2822
  version: CURRENT_VERSION,
2961
- transport: 'streamable-http',
2962
- multiTenant: true,
2963
2823
  zeroResults: {
2964
2824
  total: zeroResultsTotal,
2965
2825
  last10: ZERO_RESULTS_LOG.slice(-10).map(e => ({ query: e.query, ts: new Date(e.ts).toISOString() })),
@@ -2967,52 +2827,8 @@ if (httpPort) {
2967
2827
  }));
2968
2828
  return;
2969
2829
  }
2970
- // Static server card — lets Smithery skip live scanning (avoids auth complexity).
2971
- if (url.pathname === '/.well-known/mcp/server-card.json') {
2972
- const card = {
2973
- name: 'cachly AI Brain',
2974
- version: CURRENT_VERSION,
2975
- description: 'Persistent AI memory for Claude Code, Cursor, Copilot, Windsurf & friends. Remembers every lesson, fix, and architecture decision forever.',
2976
- transport: { type: 'streamable-http', url: 'https://mcp.cachly.dev/mcp' },
2977
- capabilities: { tools: {} },
2978
- };
2979
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
2980
- res.end(JSON.stringify(card));
2981
- return;
2982
- }
2983
2830
  if (url.pathname === '/mcp' || url.pathname === '/') {
2984
- // Handle CORS preflight
2985
- if (req.method === 'OPTIONS') {
2986
- res.writeHead(204, {
2987
- 'Access-Control-Allow-Origin': '*',
2988
- 'Access-Control-Allow-Methods': 'GET, POST, DELETE, OPTIONS',
2989
- 'Access-Control-Allow-Headers': 'Authorization, Content-Type, Mcp-Session-Id, X-Cachly-Instance-Id, cachly_jwt, cachly-jwt, x-cachly-jwt, x-api-key',
2990
- });
2991
- res.end();
2992
- return;
2993
- }
2994
- const ctx = extractReqCtx(req, url);
2995
- // Create a fresh Server+Transport for each request (stateless multi-tenant mode).
2996
- // SDK ≥1.9 throws if a stateless transport is reused across requests.
2997
- const reqServer = createHttpMcpServer();
2998
- const reqTransport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
2999
- reqServer.connect(reqTransport).then(() => {
3000
- reqCtx.run(ctx, () => {
3001
- reqTransport.handleRequest(req, res).catch((err) => {
3002
- process.stderr.write(`cachly-mcp HTTP handler error: ${(err as Error).message}\n`);
3003
- if (!res.headersSent) {
3004
- res.writeHead(500, { 'Content-Type': 'application/json' });
3005
- res.end(JSON.stringify({ error: 'internal_error' }));
3006
- }
3007
- });
3008
- });
3009
- }).catch((err) => {
3010
- process.stderr.write(`cachly-mcp connect error: ${(err as Error).message}\n`);
3011
- if (!res.headersSent) {
3012
- res.writeHead(500, { 'Content-Type': 'application/json' });
3013
- res.end(JSON.stringify({ error: 'internal_error' }));
3014
- }
3015
- });
2831
+ transport.handleRequest(req, res);
3016
2832
  return;
3017
2833
  }
3018
2834
  res.writeHead(404);
@@ -3027,3 +2843,5 @@ if (httpPort) {
3027
2843
  await server.connect(transport);
3028
2844
  }
3029
2845
 
2846
+ notify('cachly', 'startup', { version: CURRENT_VERSION, mode: process.env.MCP_HTTP_PORT ? 'http' : 'stdio' }).catch(() => undefined);
2847
+