@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
@@ -1,5 +1,7 @@
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';
3
5
  import { readFile } from 'node:fs/promises';
4
6
  import { fileURLToPath } from 'node:url';
5
7
  // True only when this file is the entry point (not imported by tests or other modules).
@@ -34,25 +36,40 @@ const _isMain = process.argv[1] != null &&
34
36
  * ── Auth & Status ────────────────────────────────────────────────────────────
35
37
  * • get_api_status – check API health + JWT auth info (Keycloak)
36
38
  *
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
+ *
37
51
  * Configuration (env vars):
38
52
  * CACHLY_API_URL – default https://api.cachly.dev
39
53
  * CACHLY_JWT – your JWT (Keycloak access token)
40
54
  * CACHLY_EMBED_PROVIDER – embedding backend: openai (default), gemini, mistral, cohere, ollama, cachly (server fallback)
41
55
  * CACHLY_EMBED_MODEL – override embedding model (optional)
56
+ * TCO_API_URL – Travel Chaos Organizer backend URL (optional, enables tco_* tools)
42
57
  */
43
58
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
44
59
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
45
60
  import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
46
61
  import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js';
47
62
  import { Redis } from 'ioredis';
48
- import { AsyncLocalStorage } from 'node:async_hooks';
49
63
  // ── Config ────────────────────────────────────────────────────────────────────
50
64
  const API_URL = process.env.CACHLY_API_URL ?? 'https://api.cachly.dev';
51
65
  let JWT = process.env.CACHLY_JWT ?? '';
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; }
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);
56
73
  // ── Default Instance Resolution (for Smithery & single-credential setups) ────
57
74
  // When CACHLY_BRAIN_INSTANCE_ID is set, tools can omit the instance_id parameter.
58
75
  // When neither is set, we auto-fetch the first running instance once per process.
@@ -60,29 +77,18 @@ let _defaultInstanceId = process.env.CACHLY_BRAIN_INSTANCE_ID ?? '';
60
77
  // Timestamp of last failed fetch — retries after 30 s (not permanently blocked).
61
78
  let _defaultInstanceLastAttempt = 0;
62
79
  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;
67
80
  if (_defaultInstanceId)
68
81
  return _defaultInstanceId;
69
- if (!getJWT())
82
+ if (!JWT)
70
83
  return '';
71
84
  // 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.
73
85
  const now = Date.now();
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
- ;
86
+ if (_defaultInstanceLastAttempt > 0 && now - _defaultInstanceLastAttempt < 30_000)
87
+ return '';
82
88
  _defaultInstanceLastAttempt = now;
83
89
  try {
84
90
  const res = await fetch(`${API_URL}/api/v1/instances`, {
85
- headers: { Authorization: `Bearer ${getJWT()}`, Accept: 'application/json' },
91
+ headers: { Authorization: `Bearer ${JWT}`, Accept: 'application/json' },
86
92
  signal: AbortSignal.timeout(5000),
87
93
  });
88
94
  if (!res.ok)
@@ -159,33 +165,44 @@ async function pollDeviceFlow(flow) {
159
165
  catch { /* use raw JWT as fallback */ }
160
166
  JWT = apiKey;
161
167
  setEmbedJwt(apiKey); // keep embeddings.ts in sync
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'));
175
- }
176
- catch { /* corrupt — start fresh */ }
177
- }
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
168
  _deviceFlow = null;
188
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
+ };
200
+ }
201
+ await mkdir(dirname(configPath), { recursive: true });
202
+ await writeFile(configPath, JSON.stringify(cfg, null, 2), 'utf-8');
203
+ }
204
+ catch { /* non-critical — don't break the flow on filesystem errors */ }
205
+ })();
189
206
  // Auto-provision: find or create the user's brain instance.
190
207
  _defaultInstanceLastAttempt = 0;
191
208
  await resolveDefaultInstanceId();
@@ -193,7 +210,7 @@ async function pollDeviceFlow(flow) {
193
210
  try {
194
211
  const autoRes = await fetch(`${API_URL}/api/v1/instances/auto`, {
195
212
  method: 'POST',
196
- headers: { Authorization: `Bearer ${getJWT()}`, 'Content-Type': 'application/json' },
213
+ headers: { Authorization: `Bearer ${JWT}`, 'Content-Type': 'application/json' },
197
214
  signal: AbortSignal.timeout(12000),
198
215
  });
199
216
  if (autoRes.ok) {
@@ -206,7 +223,7 @@ async function pollDeviceFlow(flow) {
206
223
  // Log so telemetry can detect free-tier auto-provision failures
207
224
  void fetch(`${API_URL}/api/v1/telemetry/mcp`, {
208
225
  method: 'POST',
209
- headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${getJWT()}` },
226
+ headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${JWT}` },
210
227
  body: JSON.stringify({ event: 'auto_provision_failed', status: autoRes.status, version: CURRENT_VERSION }),
211
228
  signal: AbortSignal.timeout(3000),
212
229
  }).catch(() => { });
@@ -219,7 +236,7 @@ async function pollDeviceFlow(flow) {
219
236
  if (_defaultInstanceId) {
220
237
  try {
221
238
  const checkRes = await fetch(`${API_URL}/api/v1/instances/${_defaultInstanceId}`, {
222
- headers: { Authorization: `Bearer ${getJWT()}`, Accept: 'application/json' },
239
+ headers: { Authorization: `Bearer ${JWT}`, Accept: 'application/json' },
223
240
  signal: AbortSignal.timeout(4000),
224
241
  });
225
242
  if (checkRes.ok) {
@@ -230,7 +247,7 @@ async function pollDeviceFlow(flow) {
230
247
  while (checkInst.status === 'provisioning' && Date.now() < pDeadline) {
231
248
  await new Promise(r => setTimeout(r, 3000));
232
249
  const rr = await fetch(`${API_URL}/api/v1/instances/${_defaultInstanceId}`, {
233
- headers: { Authorization: `Bearer ${getJWT()}`, Accept: 'application/json' },
250
+ headers: { Authorization: `Bearer ${JWT}`, Accept: 'application/json' },
234
251
  signal: AbortSignal.timeout(4000),
235
252
  }).catch(() => null);
236
253
  if (rr?.ok) {
@@ -246,6 +263,37 @@ async function pollDeviceFlow(flow) {
246
263
  }
247
264
  catch { /* non-fatal — getConnection will handle the wait if still provisioning */ }
248
265
  }
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
+ }
249
297
  return 'done';
250
298
  }
251
299
  if (data.error === 'slow_down')
@@ -256,71 +304,12 @@ async function pollDeviceFlow(flow) {
256
304
  return 'pending';
257
305
  }
258
306
  }
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
- }
316
307
  // ── Embeddings (imported from embeddings.ts) ────────────────────────────────
317
308
  import { setEmbedJwt } from './embeddings.js';
318
309
  // ── Search Engine (imported from search.ts) ─────────────────────────────────
319
310
  import { tokenize, splitMultiQuery, levenshtein, recencyBoost, extractTimestamp, STOPWORDS, katakanaToRomaji, arabicLightStem, expandCrossLingual, CROSS_LINGUAL_MAP, ZERO_RESULTS_LOG, zeroResultsTotal } from './search.js';
320
311
  // ── Exported for testing (re-export from search.ts) ─────────────────────────
321
312
  export { tokenize, splitMultiQuery, levenshtein, recencyBoost, extractTimestamp, STOPWORDS, katakanaToRomaji, arabicLightStem, expandCrossLingual, CROSS_LINGUAL_MAP };
322
- // ── Types ─────────────────────────────────────────────────────────────────────
323
- // Instance is imported from handlers/brain.ts
324
313
  // ── Connection pool ───────────────────────────────────────────────────────────
325
314
  /** Reuse Redis connections across tool calls (keyed by instance_id). */
326
315
  const pool = new Map();
@@ -331,26 +320,16 @@ async function getConnection(instance_id) {
331
320
  '• Pass instance_id explicitly to the tool, or\n' +
332
321
  '• Run `list_instances` to see your available instances.');
333
322
  }
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.)
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.
349
328
  let inst = await apiFetch(`/api/v1/instances/${instance_id}`);
350
329
  if (inst.status === 'provisioning') {
351
- const deadline = Date.now() + 8_000;
330
+ const deadline = Date.now() + PROVISION_TIMEOUT_MS;
352
331
  while (inst.status === 'provisioning' && Date.now() < deadline) {
353
- await new Promise(r => setTimeout(r, 2000));
332
+ await new Promise(r => setTimeout(r, 3000));
354
333
  inst = await apiFetch(`/api/v1/instances/${instance_id}`);
355
334
  }
356
335
  }
@@ -358,7 +337,7 @@ async function getConnection(instance_id) {
358
337
  // Telemetry: unreachable instance so the team can proactively investigate
359
338
  void fetch(`${API_URL}/api/v1/telemetry/mcp`, {
360
339
  method: 'POST',
361
- headers: { 'Content-Type': 'application/json', ...(getJWT() ? { Authorization: `Bearer ${getJWT()}` } : {}) },
340
+ headers: { 'Content-Type': 'application/json', ...(JWT ? { Authorization: `Bearer ${JWT}` } : {}) },
362
341
  body: JSON.stringify({ event: 'instance_not_reachable', instance_id, status: inst.status, version: CURRENT_VERSION }),
363
342
  signal: AbortSignal.timeout(3000),
364
343
  }).catch(() => { });
@@ -391,9 +370,7 @@ async function getConnection(instance_id) {
391
370
  }
392
371
  // Fetch connection details (includes password) from the dedicated endpoint.
393
372
  let password;
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;
373
+ let tlsEnabled = inst.tls_enabled !== false; // default true
397
374
  try {
398
375
  const conn = await apiFetch(`/api/v1/instances/${instance_id}/connection`);
399
376
  password = conn.password ?? undefined;
@@ -412,56 +389,25 @@ async function getConnection(instance_id) {
412
389
  host: inst.host,
413
390
  port: inst.port,
414
391
  password: password || undefined,
392
+ commandTimeout: 8000,
415
393
  ...(tlsEnabled ? { tls: {} } : {}),
416
394
  lazyConnect: true,
417
395
  enableReadyCheck: true,
418
396
  connectTimeout: 8000,
419
- commandTimeout: 10000,
420
397
  retryStrategy: () => null, // fail fast, no reconnect loops in MCP context
421
398
  });
422
399
  const evict = () => pool.delete(instance_id);
423
400
  client.on('error', evict);
424
401
  client.on('end', evict);
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
- }
402
+ try {
403
+ await client.connect();
458
404
  }
459
- if (connectErr) {
405
+ catch (err) {
460
406
  client.disconnect();
461
- const msg = connectErr instanceof Error ? connectErr.message : String(connectErr);
407
+ const msg = err instanceof Error ? err.message : String(err);
462
408
  void fetch(`${API_URL}/api/v1/telemetry/mcp`, {
463
409
  method: 'POST',
464
- headers: { 'Content-Type': 'application/json', ...(getJWT() ? { Authorization: `Bearer ${getJWT()}` } : {}) },
410
+ headers: { 'Content-Type': 'application/json', ...(JWT ? { Authorization: `Bearer ${JWT}` } : {}) },
465
411
  body: JSON.stringify({ event: 'redis_connect_failed', instance_id, host: inst.host, port: inst.port, error: msg, version: CURRENT_VERSION }),
466
412
  signal: AbortSignal.timeout(3000),
467
413
  }).catch(() => { });
@@ -475,14 +421,14 @@ async function getConnection(instance_id) {
475
421
  }
476
422
  // ── API helper ────────────────────────────────────────────────────────────────
477
423
  async function apiFetch(path, options = {}) {
478
- checkJwt(getJWT());
424
+ checkJwt(JWT);
479
425
  const timeoutMs = Number(process.env.CACHLY_API_TIMEOUT_MS ?? 15_000);
480
426
  const res = await fetch(`${API_URL}${path}`, {
481
427
  ...options,
482
428
  signal: AbortSignal.timeout(timeoutMs),
483
429
  headers: {
484
430
  'Content-Type': 'application/json',
485
- Authorization: `Bearer ${getJWT()}`,
431
+ Authorization: `Bearer ${JWT}`,
486
432
  ...options.headers,
487
433
  },
488
434
  });
@@ -503,7 +449,7 @@ import { handleTeamTool } from './handlers/team.js';
503
449
  import { handleRoadmapTool } from './handlers/roadmap.js';
504
450
  import { handleAdvancedTool } from './handlers/advanced.js';
505
451
  import { handleSyndicateTool } from './handlers/syndicate.js';
506
- import { handleFedbrainTool } from './handlers/fedbrain.js';
452
+ import { handleFedbrainTool, _lastBrainFromGitCounts } from './handlers/fedbrain.js';
507
453
  // ── Tools (imported from tools.ts) ─────────────────────────────────────────
508
454
  import { TOOLS } from './tools.js';
509
455
  // ── Handlers ──────────────────────────────────────────────────────────────────
@@ -526,10 +472,7 @@ function sendFunnelEvent(event, extra) {
526
472
  headers: { 'Content-Type': 'application/json' },
527
473
  body: JSON.stringify({
528
474
  event, version: CURRENT_VERSION, editor: detectEditor(),
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() } : {}),
475
+ ...(JWT ? { jwt: JWT } : {}),
533
476
  ...extra,
534
477
  }),
535
478
  signal: AbortSignal.timeout(3000),
@@ -552,18 +495,11 @@ async function sendAnonymousTelemetry(toolName) {
552
495
  catch { /* fire-and-forget, never block the user */ }
553
496
  }
554
497
  async function handleTool(name, args) {
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
- }
498
+ // Guard: if no JWT, return actionable onboarding message instead of HTTP 401
499
+ if (!JWT) {
564
500
  void sendAnonymousTelemetry(name);
565
- // ── Step 2: Zero-credential device flow ─────────────────────────────
566
- // 1st call after silent setup fails: start device flow, return code + URL
501
+ // ── Zero-credential device flow ─────────────────────────────────────
502
+ // 1st call: start device flow, return code + URL
567
503
  // 2nd+ calls: poll for token; once authenticated, proceed transparently
568
504
  if (_deviceFlow) {
569
505
  const result = await pollDeviceFlow(_deviceFlow);
@@ -611,7 +547,7 @@ async function handleTool(name, args) {
611
547
  '',
612
548
  'After sign-in: call **any tool again** — your Brain activates instantly.',
613
549
  '',
614
- '✨ Free forever · No credit card · 89 MCP tools · GDPR · EU servers',
550
+ '✨ Free forever · No credit card · 95 MCP tools · GDPR · EU servers',
615
551
  ].join('\n');
616
552
  }
617
553
  // Device flow unavailable (network issue) — fall back to manual setup
@@ -635,16 +571,16 @@ async function handleTool(name, args) {
635
571
  args = { ...args, instance_id: defaultId };
636
572
  }
637
573
  // Delegate brain tools (learn, recall, session, etc.)
638
- let brainResult = await handleBrainTool(name, args, getConnection, apiFetch);
574
+ const brainResult = await handleBrainTool(name, args, getConnection, apiFetch);
639
575
  if (brainResult !== null) {
640
- if (!_firstCallSuccessSent && getJWT()) {
576
+ if (!_firstCallSuccessSent && JWT) {
641
577
  _firstCallSuccessSent = true;
642
- sendFunnelEvent('first_call_success', { tool: name, instance_id: args.instance_id ?? getDefaultInstanceId() ?? '' });
578
+ sendFunnelEvent('first_call_success', { tool: name, instance_id: args.instance_id ?? _defaultInstanceId ?? '' });
643
579
  }
644
580
  // Per-tool telemetry — fires after every successful brain tool call.
645
581
  // api_key carries the cky_live_... token so the backend can resolve tenant + increment counters.
646
- const instanceId = args.instance_id ?? getDefaultInstanceId() ?? '';
647
- const telemetryExtra = getJWT() ? { api_key: getJWT(), instance_id: instanceId } : { instance_id: instanceId };
582
+ const instanceId = args.instance_id ?? _defaultInstanceId ?? '';
583
+ const telemetryExtra = JWT ? { api_key: JWT, instance_id: instanceId } : { instance_id: instanceId };
648
584
  if (name === 'recall_best_solution') {
649
585
  // recall_best_solution → increments BrainRecallCount + triggers level-up logic
650
586
  sendFunnelEvent('recall_best_solution', telemetryExtra);
@@ -662,39 +598,25 @@ async function handleTool(name, args) {
662
598
  // Existing brain — session_start acts as implicit recall → increment counter.
663
599
  sendFunnelEvent('recall_best_solution', telemetryExtra);
664
600
  }
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) {
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.
670
604
  try {
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 });
683
- }
684
- else if (typeof brainResult === 'string') {
685
- brainResult = brainResult + bgMsg;
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;
686
619
  }
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((gitBootstrap) => {
693
- if (gitBootstrap !== null) {
694
- sendFunnelEvent('brain_from_git', telemetryExtra);
695
- sendFunnelEvent('recall_best_solution', telemetryExtra);
696
- }
697
- }).catch(() => { });
698
620
  }
699
621
  }
700
622
  catch { /* git bootstrap errors must never break session_start */ }
@@ -711,7 +633,7 @@ async function handleTool(name, args) {
711
633
  const srText = typeof brainResult === 'object' && brainResult !== null && 'content' in brainResult
712
634
  ? JSON.stringify(brainResult)
713
635
  : String(brainResult ?? '');
714
- if (srText.length > 50 && !srText.includes('No lessons found') && !srText.includes('no lessons')) {
636
+ if (srText.length > 50 && !srText.includes('No lessons found') && !srText.includes('no lessons') && !srText.includes('No matches found')) {
715
637
  sendFunnelEvent('recall_best_solution', telemetryExtra);
716
638
  }
717
639
  }
@@ -720,9 +642,9 @@ async function handleTool(name, args) {
720
642
  // Delegate context tools (remember/recall/list/forget)
721
643
  const contextResult = await handleContextTool(name, args, getConnection, apiFetch);
722
644
  if (contextResult !== null) {
723
- if (!_firstCallSuccessSent && getJWT()) {
645
+ if (!_firstCallSuccessSent && JWT) {
724
646
  _firstCallSuccessSent = true;
725
- sendFunnelEvent('first_call_success', { tool: name, instance_id: args.instance_id ?? getDefaultInstanceId() ?? '' });
647
+ sendFunnelEvent('first_call_success', { tool: name, instance_id: args.instance_id ?? _defaultInstanceId ?? '' });
726
648
  }
727
649
  return contextResult;
728
650
  }
@@ -747,8 +669,8 @@ async function handleTool(name, args) {
747
669
  if (syndicateResult !== null) {
748
670
  // brain_predict is in syndicate handler — track telemetry here.
749
671
  if (name === 'brain_predict') {
750
- const instanceId = args.instance_id ?? getDefaultInstanceId() ?? '';
751
- const telemetryExtra = getJWT() ? { api_key: getJWT(), instance_id: instanceId } : { instance_id: instanceId };
672
+ const instanceId = args.instance_id ?? _defaultInstanceId ?? '';
673
+ const telemetryExtra = JWT ? { api_key: JWT, instance_id: instanceId } : { instance_id: instanceId };
752
674
  sendFunnelEvent('brain_predict', telemetryExtra);
753
675
  }
754
676
  return syndicateResult;
@@ -756,16 +678,21 @@ async function handleTool(name, args) {
756
678
  const fedbrainResult = await handleFedbrainTool(name, args, getConnection, apiFetch);
757
679
  if (fedbrainResult !== null) {
758
680
  // brain_from_git and brain_predict_failures are in fedbrain handler.
759
- const instanceId = args.instance_id ?? getDefaultInstanceId() ?? '';
760
- const telemetryExtra = getJWT() ? { api_key: getJWT(), instance_id: instanceId } : { instance_id: instanceId };
681
+ const instanceId = args.instance_id ?? _defaultInstanceId ?? '';
682
+ const telemetryExtra = JWT ? { api_key: JWT, instance_id: instanceId } : { instance_id: instanceId };
761
683
  if (name === 'brain_from_git') {
762
- sendFunnelEvent('brain_from_git', telemetryExtra);
684
+ const gitCounts = _lastBrainFromGitCounts;
685
+ sendFunnelEvent('brain_from_git', { ...telemetryExtra, ...(gitCounts ?? {}) });
763
686
  }
764
687
  else if (name === 'brain_predict_failures') {
765
688
  sendFunnelEvent('brain_predict_failures', telemetryExtra);
766
689
  }
767
690
  return fedbrainResult;
768
691
  }
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;
769
696
  switch (name) {
770
697
  // ── Instance management ──────────────────────────────────────────────
771
698
  case 'get_api_status': {
@@ -785,7 +712,7 @@ async function handleTool(name, args) {
785
712
  healthStatus = `unreachable: ${e.message}`;
786
713
  }
787
714
  // Check JWT / auth
788
- if (!getJWT()) {
715
+ if (!JWT) {
789
716
  return [
790
717
  `📡 **cachly API Status**`,
791
718
  ``,
@@ -797,39 +724,28 @@ async function handleTool(name, args) {
797
724
  ].join('\n');
798
725
  }
799
726
  // Decode JWT claims (inspection only, no verification)
800
- const jwt = getJWT();
801
- let authInfo;
802
- // API keys (cky_live_... / cky_trial_...) are valid long-lived credentials.
803
- // Only try to decode as JWT (3-part base64url) if it looks like one.
804
- if (jwt.startsWith('cky_')) {
805
- const keyType = jwt.startsWith('cky_trial_') ? 'Trial' : 'Live';
806
- const keyId = jwt.slice(0, 20) + '…';
807
- authInfo = `✅ API key (${keyType})\n Key prefix: ${keyId}`;
808
- }
809
- else {
810
- authInfo = '❌ invalid credential format';
811
- try {
812
- const parts = jwt.split('.');
813
- if (parts.length === 3) {
814
- const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf-8'));
815
- const sub = payload.sub ?? '(unknown)';
816
- const iss = payload.iss ?? '(unknown)';
817
- const provider = iss.includes('keycloak') ? 'Keycloak' : 'OIDC';
818
- const expTs = payload.exp ? new Date(payload.exp * 1000) : null;
819
- const expired = expTs ? expTs < new Date() : false;
820
- authInfo = [
821
- `✅ JWT decoded`,
822
- ` Sub (user ID): ${sub}`,
823
- ` Provider: ${provider}`,
824
- ` Issuer: ${iss}`,
825
- ` Expires: ${expTs ? expTs.toISOString() : 'never'} ${expired ? '⚠️ EXPIRED – get a new token!' : '✅'}`,
826
- ].join('\n');
827
- }
828
- }
829
- catch {
830
- authInfo = '❌ JWT decode failed – check CACHLY_JWT format';
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');
831
744
  }
832
745
  }
746
+ catch {
747
+ authInfo = '❌ JWT decode failed – check CACHLY_JWT format';
748
+ }
833
749
  // List instances + connectivity
834
750
  let instanceInfo = '';
835
751
  try {
@@ -842,7 +758,7 @@ async function handleTool(name, args) {
842
758
  const lines = ['\n\n🧠 **Brain Instances:**'];
843
759
  for (const inst of instances) {
844
760
  const badge = inst.status === 'running' ? '🟢' : inst.status === 'provisioning' ? '🟡' : '🔴';
845
- const defaultMark = inst.id === getDefaultInstanceId() ? ' ← active' : '';
761
+ const defaultMark = inst.id === _defaultInstanceId ? ' ← active' : '';
846
762
  lines.push(` ${badge} **${inst.name}** (\`${inst.id}\`) · ${inst.status}${defaultMark}`);
847
763
  if (inst.status === 'running' && inst.host) {
848
764
  // Try a quick ping on the cached connection
@@ -970,14 +886,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
970
886
  || _defaultInstanceId
971
887
  || process.env.CACHLY_BRAIN_INSTANCE_ID;
972
888
  if (instanceId) {
973
- // Fire-and-forget — session management is non-critical and must never block tool calls.
974
- // The session_start briefing (which includes brain_from_git) can take 30-120s on large
975
- // repos; awaiting it here would freeze the user's first MCP tool call entirely.
976
- void autoStartSession(instanceId);
889
+ await autoStartSession(instanceId).catch(() => undefined);
977
890
  }
978
891
  }
979
- // Note: we intentionally do NOT await _autoSessionStarting for any call.
980
- // The session tracker runs in background — tool calls must always proceed immediately.
892
+ else if (_autoSessionStarting) {
893
+ // Another concurrent call is mid-start — wait for it before proceeding.
894
+ await _autoSessionStarting.catch(() => undefined);
895
+ }
981
896
  if (!sessionTools.has(name))
982
897
  _autoSessionToolCount++;
983
898
  try {
@@ -987,7 +902,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
987
902
  catch (err) {
988
903
  if (err instanceof McpError)
989
904
  throw err;
990
- throw new McpError(ErrorCode.InternalError, err.message);
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);
991
908
  }
992
909
  });
993
910
  // Graceful shutdown – close all Redis connections + auto-end brain session
@@ -1819,7 +1736,7 @@ if (!process.argv[2] && process.stdout.isTTY) {
1819
1736
  console.log(' \x1b[36m npx @cachly-dev/mcp-server@latest upgrade\x1b[0m ← Check for updates');
1820
1737
  console.log('');
1821
1738
  console.log(' \x1b[90mWorks with: Claude Code · Cursor · Windsurf · GitHub Copilot · Cline · Zed\x1b[0m');
1822
- console.log(' \x1b[90mFree forever · GDPR · German servers · 89 MCP tools\x1b[0m');
1739
+ console.log(' \x1b[90mFree forever · GDPR · German servers · 95 MCP tools\x1b[0m');
1823
1740
  console.log('');
1824
1741
  process.exit(0);
1825
1742
  }
@@ -1975,7 +1892,7 @@ if (process.argv[2] === 'status') {
1975
1892
  if (process.argv[2] === 'health') {
1976
1893
  const { existsSync } = await import('node:fs');
1977
1894
  const { readFile } = await import('node:fs/promises');
1978
- const { resolve } = await import('node:path');
1895
+ const { resolve, join: pJoin } = await import('node:path');
1979
1896
  const cwd = process.cwd();
1980
1897
  let passed = 0;
1981
1898
  let failed = 0;
@@ -1989,10 +1906,6 @@ if (process.argv[2] === 'health') {
1989
1906
  if (!jwt) {
1990
1907
  fail('CACHLY_JWT not set — run: npx @cachly-dev/mcp-server@latest setup');
1991
1908
  }
1992
- else if (jwt.startsWith('cky_')) {
1993
- // Opaque cachly API key — no JWT expiry to check, format is valid by prefix
1994
- ok('API key set (cky_*** opaque key)');
1995
- }
1996
1909
  else {
1997
1910
  try {
1998
1911
  const parts = jwt.split('.');
@@ -2012,7 +1925,7 @@ if (process.argv[2] === 'health') {
2012
1925
  }
2013
1926
  }
2014
1927
  catch {
2015
- fail('CACHLY_JWT format invalid (expected a cky_*** API key or a JWT with 3 parts)');
1928
+ fail('CACHLY_JWT format invalid (expected JWT with 3 parts)');
2016
1929
  }
2017
1930
  }
2018
1931
  // ── 2. Brain API reachability ───────────────────────────────────────────────
@@ -2063,27 +1976,11 @@ if (process.argv[2] === 'health') {
2063
1976
  });
2064
1977
  if (connRes.ok) {
2065
1978
  const conn = await connRes.json();
2066
- // password may be a direct field, or embedded in connection_string as redis://:pass@host:port
2067
- if (conn.password) {
2068
- pingPassword = conn.password;
2069
- }
2070
- else if (conn.connection_string) {
2071
- try {
2072
- const u = new URL(conn.connection_string);
2073
- if (u.password)
2074
- pingPassword = decodeURIComponent(u.password);
2075
- }
2076
- catch { /* leave pingPassword undefined */ }
2077
- }
2078
- pingTls = conn.tls_enabled === true;
2079
- }
2080
- else {
2081
- warn(`Could not fetch Redis credentials (HTTP ${connRes.status}) — connectivity test may fail`);
1979
+ pingPassword = conn.password;
1980
+ pingTls = conn.tls_enabled !== false;
2082
1981
  }
2083
1982
  }
2084
- catch (connErr) {
2085
- warn(`Could not fetch Redis credentials — ${connErr.message}`);
2086
- }
1983
+ catch { /* proceed without password */ }
2087
1984
  const pingClient = new Redis({
2088
1985
  host: inst.host, port: inst.port,
2089
1986
  password: pingPassword,
@@ -2217,6 +2114,7 @@ if (process.argv[2] === 'setup') {
2217
2114
  const AUTH_BASE = 'https://auth.cachly.dev/realms/cachly/protocol/openid-connect';
2218
2115
  const CLIENT_ID = 'cachly-cli';
2219
2116
  console.log('Step 1: Sign in to cachly (free, no credit card)\n');
2117
+ sendFunnelEvent('setup_auth_started');
2220
2118
  // Start device flow
2221
2119
  let deviceCode = '', userCode = '', verifyUri = '', pollInterval = 5000;
2222
2120
  try {
@@ -2242,6 +2140,7 @@ if (process.argv[2] === 'setup') {
2242
2140
  rl.close();
2243
2141
  process.exit(1);
2244
2142
  }
2143
+ sendFunnelEvent('setup_auth_completed');
2245
2144
  console.log('');
2246
2145
  deviceCode = ''; // mark as fallback so we skip polling
2247
2146
  }
@@ -2274,6 +2173,7 @@ if (process.argv[2] === 'setup') {
2274
2173
  if (tokenData.access_token) {
2275
2174
  token = tokenData.access_token;
2276
2175
  console.log(' \x1b[32m✓ Authorized!\x1b[0m\n');
2176
+ sendFunnelEvent('setup_auth_completed');
2277
2177
  break;
2278
2178
  }
2279
2179
  // authorization_pending = keep polling; slow_down = increase interval
@@ -2314,7 +2214,7 @@ if (process.argv[2] === 'setup') {
2314
2214
  token = keyBody.key; // swap JWT → cky_live_...
2315
2215
  console.log(' ✓\n');
2316
2216
  }
2317
- catch {
2217
+ catch (e) {
2318
2218
  console.log(' (skipped)\n');
2319
2219
  // Non-fatal: fall back to using the Keycloak JWT directly.
2320
2220
  // It will expire but setup still works for now.
@@ -2367,9 +2267,10 @@ if (process.argv[2] === 'setup') {
2367
2267
  // Newly created — poll until running or give up after 30 s.
2368
2268
  const newId = autoBody.instance_id;
2369
2269
  console.log(` ✓ created (${newId.slice(0, 8)}…)\n`);
2270
+ // 45 attempts × 3s = 135s — generous for free-tier in slow regions.
2370
2271
  process.stdout.write('⏳ Waiting for instance to start');
2371
- for (let attempt = 0; attempt < 15; attempt++) {
2372
- await new Promise(r => setTimeout(r, 2000));
2272
+ for (let attempt = 0; attempt < 45; attempt++) {
2273
+ await new Promise(r => setTimeout(r, 3000));
2373
2274
  process.stdout.write('.');
2374
2275
  try {
2375
2276
  const checkRes = await fetch(`${API_URL}/api/v1/instances/${newId}`, { headers: { Authorization: `Bearer ${token}` } });
@@ -2408,6 +2309,7 @@ if (process.argv[2] === 'setup') {
2408
2309
  console.log(` (Run with --instance-id <id> to use a different one)\n`);
2409
2310
  }
2410
2311
  console.log(`✓ Instance: ${instance.name} (${instance.id.slice(0, 8)}…)\n`);
2312
+ sendFunnelEvent('setup_instance_ready', { instance_id: instance.id });
2411
2313
  // ── Step 3: Detect editors ────────────────────────────────────────────────
2412
2314
  const cwd = process.cwd();
2413
2315
  const detected = [];
@@ -2656,7 +2558,7 @@ if (process.argv[2] === 'index') {
2656
2558
  const instanceId = flag('instance-id') ?? process.env.CACHLY_BRAIN_INSTANCE_ID;
2657
2559
  const maxFiles = parseInt(flag('max-files') ?? '500', 10);
2658
2560
  const namespace = flag('namespace') ?? 'cachly:sem:code';
2659
- if (!instanceId || !getJWT()) {
2561
+ if (!instanceId || !JWT) {
2660
2562
  console.error('\n❌ CACHLY_BRAIN_INSTANCE_ID and CACHLY_JWT must be set\n');
2661
2563
  console.error(' export CACHLY_BRAIN_INSTANCE_ID=<uuid>');
2662
2564
  console.error(' export CACHLY_JWT=<cky_live_...>');
@@ -2693,7 +2595,7 @@ if (process.argv[2] === 'learn-git') {
2693
2595
  const repoDir = resolve(flag('dir') ?? argv.find(a => !a.startsWith('--')) ?? '.');
2694
2596
  const instanceId = flag('instance-id') ?? process.env.CACHLY_BRAIN_INSTANCE_ID;
2695
2597
  const maxCommits = parseInt(flag('max-commits') ?? '50', 10);
2696
- if (!instanceId || !getJWT()) {
2598
+ if (!instanceId || !JWT) {
2697
2599
  console.error('\n❌ CACHLY_BRAIN_INSTANCE_ID and CACHLY_JWT must be set\n');
2698
2600
  console.error(' export CACHLY_BRAIN_INSTANCE_ID=<uuid>');
2699
2601
  console.error(' export CACHLY_JWT=<cky_live_...>');
@@ -2709,7 +2611,7 @@ if (process.argv[2] === 'learn-git') {
2709
2611
  limit: maxCommits,
2710
2612
  });
2711
2613
  console.log(result);
2712
- sendFunnelEvent('brain_from_git', { instance_id: instanceId });
2614
+ sendFunnelEvent('brain_from_git', { api_key: JWT, instance_id: instanceId });
2713
2615
  console.log('\n✅ Brain learned from your commits.\n');
2714
2616
  // Let fire-and-forget telemetry flush before exit.
2715
2617
  await new Promise(r => setTimeout(r, 300));
@@ -2725,8 +2627,7 @@ if (process.argv[2] === 'learn-git') {
2725
2627
  // actionable message in their editor's MCP log instead of silent failures.
2726
2628
  // Skip for CLI commands that intentionally run without credentials.
2727
2629
  const _cliNoAuthCommands = ['demo', 'share', 'health', 'setup', 'init', 'digest', 'invite', 'badge', 'join', 'upgrade'];
2728
- // In multi-tenant HTTP mode (PORT set) we never need a startup JWT — per-request auth.
2729
- if (!JWT && !process.env.PORT && !_cliNoAuthCommands.includes(process.argv[2] ?? '') && !(!process.argv[2] && process.stdout.isTTY)) {
2630
+ if (!JWT && !_cliNoAuthCommands.includes(process.argv[2] ?? '') && !(!process.argv[2] && process.stdout.isTTY)) {
2730
2631
  // No args + no JWT in a non-TTY context = running as MCP server without credentials.
2731
2632
  // Print actionable setup banner to stdout so it's captured by callers/tests.
2732
2633
  const banner = '\n' +
@@ -2790,79 +2691,12 @@ if (!process.env.CACHLY_NO_UPDATE_CHECK) {
2790
2691
  })();
2791
2692
  }
2792
2693
  const httpPort = process.env.PORT ? parseInt(process.env.PORT, 10) : undefined;
2793
- // ── Per-request MCP Server factory (HTTP / Smithery stateless mode) ──────────
2794
- // SDK ≥1.9 forbids reusing a stateless transport across requests.
2795
- // Solution: create a fresh Server+Transport per /mcp request.
2796
- // The module-level `server` below is used for stdio mode only.
2797
- function createHttpMcpServer() {
2798
- const srv = new Server({ name: 'cachly-mcp', version: CURRENT_VERSION }, { capabilities: { tools: {} } });
2799
- srv.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
2800
- srv.setRequestHandler(CallToolRequestSchema, async (request) => {
2801
- const { name, arguments: args } = request.params;
2802
- try {
2803
- const text = await handleTool(name, (args ?? {}));
2804
- return { content: [{ type: 'text', text }] };
2805
- }
2806
- catch (err) {
2807
- if (err instanceof McpError)
2808
- throw err;
2809
- throw new McpError(ErrorCode.InternalError, err.message);
2810
- }
2811
- });
2812
- return srv;
2813
- }
2814
2694
  if (httpPort) {
2815
2695
  // ── HTTP mode (Streamable HTTP transport) ───────────────────────────────
2816
- // Used for Smithery URL deployment: PORT=3000 node dist/index.js
2817
- // Multi-tenant: per-request JWT + instance ID resolved via:
2818
- // 1. Authorization: Bearer <jwt> + X-Cachly-Instance-Id: <uuid>
2819
- // 2. Smithery config: ?config=base64url({CACHLY_JWT, CACHLY_BRAIN_INSTANCE_ID, ...})
2820
- // Falls back to module-level env-derived values when neither is provided.
2696
+ // Used for Smithery URL deployment: PORT=3000 node dist/src/index.js
2821
2697
  const { createServer } = await import('node:http');
2822
- function extractReqCtx(req, url) {
2823
- let jwtVal = '';
2824
- let instId = '';
2825
- const auth = req.headers['authorization'];
2826
- if (typeof auth === 'string' && auth.trim()) {
2827
- const a = auth.trim();
2828
- jwtVal = a.toLowerCase().startsWith('bearer ') ? a.slice(7).trim() : a;
2829
- }
2830
- // Fallback: raw JWT in custom header (Smithery "header" transport without Bearer prefix)
2831
- if (!jwtVal) {
2832
- const rawJwt = req.headers['cachly_jwt'] ?? req.headers['cachly-jwt'] ?? req.headers['x-cachly-jwt'] ?? req.headers['x-api-key'];
2833
- if (typeof rawJwt === 'string')
2834
- jwtVal = rawJwt.trim();
2835
- }
2836
- const hdrInst = req.headers['x-cachly-instance-id'];
2837
- if (typeof hdrInst === 'string')
2838
- instId = hdrInst.trim();
2839
- // Smithery passes user config as a base64url-encoded JSON blob in ?config=
2840
- const cfgParam = url.searchParams.get('config');
2841
- if (cfgParam && (!jwtVal || !instId)) {
2842
- try {
2843
- const b64 = cfgParam.replace(/-/g, '+').replace(/_/g, '/');
2844
- const padded = b64 + '='.repeat((4 - b64.length % 4) % 4);
2845
- const json = JSON.parse(Buffer.from(padded, 'base64').toString('utf8'));
2846
- if (!jwtVal && typeof json.CACHLY_JWT === 'string')
2847
- jwtVal = json.CACHLY_JWT;
2848
- if (!instId && typeof json.CACHLY_BRAIN_INSTANCE_ID === 'string')
2849
- instId = json.CACHLY_BRAIN_INSTANCE_ID;
2850
- }
2851
- catch { /* malformed config — ignore, fall back to globals */ }
2852
- }
2853
- // Direct query params (Smithery "query" transport, manual clients, etc.)
2854
- if (!jwtVal) {
2855
- const q = url.searchParams.get('CACHLY_JWT') ?? url.searchParams.get('cachly_jwt') ?? url.searchParams.get('api_key');
2856
- if (q)
2857
- jwtVal = q.trim();
2858
- }
2859
- if (!instId) {
2860
- const q = url.searchParams.get('CACHLY_BRAIN_INSTANCE_ID') ?? url.searchParams.get('cachly_brain_instance_id') ?? url.searchParams.get('instance_id');
2861
- if (q)
2862
- instId = q.trim();
2863
- }
2864
- return { jwt: jwtVal, instanceId: instId };
2865
- }
2698
+ const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
2699
+ await server.connect(transport);
2866
2700
  const httpServer = createServer((req, res) => {
2867
2701
  const url = new URL(req.url ?? '/', `http://localhost:${httpPort}`);
2868
2702
  if (url.pathname === '/health') {
@@ -2870,8 +2704,6 @@ if (httpPort) {
2870
2704
  res.end(JSON.stringify({
2871
2705
  status: 'ok',
2872
2706
  version: CURRENT_VERSION,
2873
- transport: 'streamable-http',
2874
- multiTenant: true,
2875
2707
  zeroResults: {
2876
2708
  total: zeroResultsTotal,
2877
2709
  last10: ZERO_RESULTS_LOG.slice(-10).map(e => ({ query: e.query, ts: new Date(e.ts).toISOString() })),
@@ -2879,52 +2711,8 @@ if (httpPort) {
2879
2711
  }));
2880
2712
  return;
2881
2713
  }
2882
- // Static server card — lets Smithery skip live scanning (avoids auth complexity).
2883
- if (url.pathname === '/.well-known/mcp/server-card.json') {
2884
- const card = {
2885
- name: 'cachly AI Brain',
2886
- version: CURRENT_VERSION,
2887
- description: 'Persistent AI memory for Claude Code, Cursor, Copilot, Windsurf & friends. Remembers every lesson, fix, and architecture decision forever.',
2888
- transport: { type: 'streamable-http', url: 'https://mcp.cachly.dev/mcp' },
2889
- capabilities: { tools: {} },
2890
- };
2891
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
2892
- res.end(JSON.stringify(card));
2893
- return;
2894
- }
2895
2714
  if (url.pathname === '/mcp' || url.pathname === '/') {
2896
- // Handle CORS preflight
2897
- if (req.method === 'OPTIONS') {
2898
- res.writeHead(204, {
2899
- 'Access-Control-Allow-Origin': '*',
2900
- 'Access-Control-Allow-Methods': 'GET, POST, DELETE, OPTIONS',
2901
- 'Access-Control-Allow-Headers': 'Authorization, Content-Type, Mcp-Session-Id, X-Cachly-Instance-Id, cachly_jwt, cachly-jwt, x-cachly-jwt, x-api-key',
2902
- });
2903
- res.end();
2904
- return;
2905
- }
2906
- const ctx = extractReqCtx(req, url);
2907
- // Create a fresh Server+Transport for each request (stateless multi-tenant mode).
2908
- // SDK ≥1.9 throws if a stateless transport is reused across requests.
2909
- const reqServer = createHttpMcpServer();
2910
- const reqTransport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
2911
- reqServer.connect(reqTransport).then(() => {
2912
- reqCtx.run(ctx, () => {
2913
- reqTransport.handleRequest(req, res).catch((err) => {
2914
- process.stderr.write(`cachly-mcp HTTP handler error: ${err.message}\n`);
2915
- if (!res.headersSent) {
2916
- res.writeHead(500, { 'Content-Type': 'application/json' });
2917
- res.end(JSON.stringify({ error: 'internal_error' }));
2918
- }
2919
- });
2920
- });
2921
- }).catch((err) => {
2922
- process.stderr.write(`cachly-mcp connect error: ${err.message}\n`);
2923
- if (!res.headersSent) {
2924
- res.writeHead(500, { 'Content-Type': 'application/json' });
2925
- res.end(JSON.stringify({ error: 'internal_error' }));
2926
- }
2927
- });
2715
+ transport.handleRequest(req, res);
2928
2716
  return;
2929
2717
  }
2930
2718
  res.writeHead(404);
@@ -2939,4 +2727,5 @@ else {
2939
2727
  const transport = new StdioServerTransport();
2940
2728
  await server.connect(transport);
2941
2729
  }
2730
+ notify('cachly', 'startup', { version: CURRENT_VERSION, mode: process.env.MCP_HTTP_PORT ? 'http' : 'stdio' }).catch(() => undefined);
2942
2731
  //# sourceMappingURL=index.js.map