@cachly-dev/mcp-server 0.10.56 → 0.10.58

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 (338) 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/{handlers → src/handlers}/advanced.d.ts +1 -1
  184. package/dist/{handlers → src/handlers}/advanced.js +1 -2
  185. package/dist/src/handlers/advanced.js.map +1 -0
  186. package/dist/{handlers → src/handlers}/brain.js +361 -191
  187. package/dist/src/handlers/brain.js.map +1 -0
  188. package/dist/src/handlers/cache.js.map +1 -0
  189. package/dist/{handlers → src/handlers}/context.js +9 -4
  190. package/dist/src/handlers/context.js.map +1 -0
  191. package/dist/{handlers → src/handlers}/fedbrain.d.ts +6 -0
  192. package/dist/{handlers → src/handlers}/fedbrain.js +19 -7
  193. package/dist/src/handlers/fedbrain.js.map +1 -0
  194. package/dist/src/handlers/instances.js.map +1 -0
  195. package/dist/{handlers → src/handlers}/roadmap.d.ts +1 -1
  196. package/dist/{handlers → src/handlers}/roadmap.js +8 -4
  197. package/dist/src/handlers/roadmap.js.map +1 -0
  198. package/dist/src/handlers/syndicate.js.map +1 -0
  199. package/dist/src/handlers/tco.d.ts +16 -0
  200. package/dist/src/handlers/tco.js +303 -0
  201. package/dist/src/handlers/tco.js.map +1 -0
  202. package/dist/{handlers → src/handlers}/team.js +58 -7
  203. package/dist/src/handlers/team.js.map +1 -0
  204. package/dist/{index.js → src/index.js} +207 -429
  205. package/dist/src/index.js.map +1 -0
  206. package/dist/src/namespace.js.map +1 -0
  207. package/dist/src/notifier.d.ts +5 -0
  208. package/dist/src/notifier.js +6 -0
  209. package/dist/src/notifier.js.map +1 -0
  210. package/dist/src/rerank.d.ts +55 -0
  211. package/dist/src/rerank.js +111 -0
  212. package/dist/src/rerank.js.map +1 -0
  213. package/dist/{search.js → src/search.js} +2 -2
  214. package/dist/src/search.js.map +1 -0
  215. package/dist/src/search.test.d.ts +1 -0
  216. package/dist/{search.test.js → src/search.test.js} +1 -1
  217. package/dist/src/search.test.js.map +1 -0
  218. package/dist/{tools.d.ts → src/tools.d.ts} +226 -0
  219. package/dist/{tools.js → src/tools.js} +196 -0
  220. package/dist/src/tools.js.map +1 -0
  221. package/dist/src/utils.js.map +1 -0
  222. package/eslint.config.js +1 -1
  223. package/package.json +8 -7
  224. package/packages/telegram-notify/SPLIT_PLAN.md +106 -0
  225. package/packages/telegram-notify/client.ts +88 -0
  226. package/packages/telegram-notify/emojis.json +22 -0
  227. package/packages/telegram-notify/pyproject.toml +17 -0
  228. package/packages/telegram-notify/telegram_notify/__init__.py +3 -0
  229. package/packages/telegram-notify/telegram_notify/client.py +87 -0
  230. package/packages/telegram-notify/telegram_notify/emojis.json +22 -0
  231. package/scripts/generate-icons.js +28 -0
  232. package/server.json +3 -3
  233. package/smithery.yaml +27 -16
  234. package/src/__tests__/auth.test.ts +1 -1
  235. package/src/__tests__/brain-flow.test.ts +175 -1
  236. package/src/__tests__/e2e/api-health.test.ts +1 -1
  237. package/src/__tests__/handlers-unit.test.ts +1 -2
  238. package/src/__tests__/rerank.test.ts +130 -0
  239. package/src/bench/cachly-bench.ts +217 -0
  240. package/src/bench/fixtures.ts +256 -0
  241. package/src/handlers/advanced.ts +2 -6
  242. package/src/handlers/brain.ts +371 -194
  243. package/src/handlers/cache.ts +1 -2
  244. package/src/handlers/context.ts +10 -6
  245. package/src/handlers/fedbrain.ts +21 -12
  246. package/src/handlers/roadmap.ts +8 -4
  247. package/src/handlers/syndicate.ts +7 -4
  248. package/src/handlers/tco.ts +345 -0
  249. package/src/handlers/team.ts +70 -10
  250. package/src/index.ts +193 -419
  251. package/src/notifier.ts +5 -0
  252. package/src/rerank.ts +148 -0
  253. package/src/search.test.ts +1 -1
  254. package/src/search.ts +2 -2
  255. package/src/tools.ts +210 -0
  256. package/tsconfig.json +2 -3
  257. package/vitest.config.ts +2 -0
  258. package/dist/__tests__/auth.test.js.map +0 -1
  259. package/dist/__tests__/brain-flow.test.js.map +0 -1
  260. package/dist/__tests__/brain-predict-failures.test.d.ts +0 -17
  261. package/dist/__tests__/brain-predict-failures.test.js +0 -339
  262. package/dist/__tests__/brain-predict-failures.test.js.map +0 -1
  263. package/dist/__tests__/confidence.test.js.map +0 -1
  264. package/dist/__tests__/e2e/api-auth.test.js.map +0 -1
  265. package/dist/__tests__/e2e/api-health.test.js.map +0 -1
  266. package/dist/__tests__/e2e/cli.test.js.map +0 -1
  267. package/dist/__tests__/e2e/config.js.map +0 -1
  268. package/dist/__tests__/e2e/funnel.test.js.map +0 -1
  269. package/dist/__tests__/e2e/mcp-tools.test.js.map +0 -1
  270. package/dist/__tests__/e2e/provisioning.test.js.map +0 -1
  271. package/dist/__tests__/handlers-unit.test.js.map +0 -1
  272. package/dist/__tests__/namespace.test.js.map +0 -1
  273. package/dist/auth.js.map +0 -1
  274. package/dist/ckg.js.map +0 -1
  275. package/dist/confidence.js.map +0 -1
  276. package/dist/embeddings.js.map +0 -1
  277. package/dist/handlers/advanced.js.map +0 -1
  278. package/dist/handlers/brain.js.map +0 -1
  279. package/dist/handlers/cache.js.map +0 -1
  280. package/dist/handlers/context.js.map +0 -1
  281. package/dist/handlers/fedbrain.js.map +0 -1
  282. package/dist/handlers/instances.js.map +0 -1
  283. package/dist/handlers/roadmap.js.map +0 -1
  284. package/dist/handlers/syndicate.js.map +0 -1
  285. package/dist/handlers/team.js.map +0 -1
  286. package/dist/index.js.map +0 -1
  287. package/dist/namespace.js.map +0 -1
  288. package/dist/search.js.map +0 -1
  289. package/dist/search.test.js.map +0 -1
  290. package/dist/tools.js.map +0 -1
  291. package/dist/utils.js.map +0 -1
  292. package/src/__tests__/brain-predict-failures.test.ts +0 -430
  293. /package/dist/{__tests__ → src/__tests__}/auth.test.d.ts +0 -0
  294. /package/dist/{__tests__ → src/__tests__}/auth.test.js +0 -0
  295. /package/dist/{__tests__ → src/__tests__}/brain-flow.test.d.ts +0 -0
  296. /package/dist/{__tests__ → src/__tests__}/confidence.test.d.ts +0 -0
  297. /package/dist/{__tests__ → src/__tests__}/confidence.test.js +0 -0
  298. /package/dist/{__tests__ → src/__tests__}/e2e/api-auth.test.d.ts +0 -0
  299. /package/dist/{__tests__ → src/__tests__}/e2e/api-auth.test.js +0 -0
  300. /package/dist/{__tests__ → src/__tests__}/e2e/api-health.test.d.ts +0 -0
  301. /package/dist/{__tests__ → src/__tests__}/e2e/api-health.test.js +0 -0
  302. /package/dist/{__tests__ → src/__tests__}/e2e/cli.test.d.ts +0 -0
  303. /package/dist/{__tests__ → src/__tests__}/e2e/cli.test.js +0 -0
  304. /package/dist/{__tests__ → src/__tests__}/e2e/config.d.ts +0 -0
  305. /package/dist/{__tests__ → src/__tests__}/e2e/config.js +0 -0
  306. /package/dist/{__tests__ → src/__tests__}/e2e/funnel.test.d.ts +0 -0
  307. /package/dist/{__tests__ → src/__tests__}/e2e/funnel.test.js +0 -0
  308. /package/dist/{__tests__ → src/__tests__}/e2e/mcp-tools.test.d.ts +0 -0
  309. /package/dist/{__tests__ → src/__tests__}/e2e/mcp-tools.test.js +0 -0
  310. /package/dist/{__tests__ → src/__tests__}/e2e/provisioning.test.d.ts +0 -0
  311. /package/dist/{__tests__ → src/__tests__}/e2e/provisioning.test.js +0 -0
  312. /package/dist/{__tests__ → src/__tests__}/handlers-unit.test.d.ts +0 -0
  313. /package/dist/{__tests__ → src/__tests__}/namespace.test.d.ts +0 -0
  314. /package/dist/{__tests__ → src/__tests__}/namespace.test.js +0 -0
  315. /package/dist/{search.test.d.ts → src/__tests__/rerank.test.d.ts} +0 -0
  316. /package/dist/{auth.d.ts → src/auth.d.ts} +0 -0
  317. /package/dist/{auth.js → src/auth.js} +0 -0
  318. /package/dist/{ckg.d.ts → src/ckg.d.ts} +0 -0
  319. /package/dist/{ckg.js → src/ckg.js} +0 -0
  320. /package/dist/{confidence.d.ts → src/confidence.d.ts} +0 -0
  321. /package/dist/{confidence.js → src/confidence.js} +0 -0
  322. /package/dist/{embeddings.d.ts → src/embeddings.d.ts} +0 -0
  323. /package/dist/{embeddings.js → src/embeddings.js} +0 -0
  324. /package/dist/{handlers → src/handlers}/brain.d.ts +0 -0
  325. /package/dist/{handlers → src/handlers}/cache.d.ts +0 -0
  326. /package/dist/{handlers → src/handlers}/cache.js +0 -0
  327. /package/dist/{handlers → src/handlers}/context.d.ts +0 -0
  328. /package/dist/{handlers → src/handlers}/instances.d.ts +0 -0
  329. /package/dist/{handlers → src/handlers}/instances.js +0 -0
  330. /package/dist/{handlers → src/handlers}/syndicate.d.ts +0 -0
  331. /package/dist/{handlers → src/handlers}/syndicate.js +0 -0
  332. /package/dist/{handlers → src/handlers}/team.d.ts +0 -0
  333. /package/dist/{index.d.ts → src/index.d.ts} +0 -0
  334. /package/dist/{namespace.d.ts → src/namespace.d.ts} +0 -0
  335. /package/dist/{namespace.js → src/namespace.js} +0 -0
  336. /package/dist/{search.d.ts → src/search.d.ts} +0 -0
  337. /package/dist/{utils.d.ts → src/utils.d.ts} +0 -0
  338. /package/dist/{utils.js → src/utils.js} +0 -0
@@ -1,11 +1,10 @@
1
- import { createHash } from 'node:crypto';
2
1
  import { randomUUID } from 'node:crypto';
3
2
  import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
4
3
  import { readdir, stat, readFile } from 'node:fs/promises';
5
4
  import { join, relative, extname } from 'node:path';
6
5
  import type { Redis } from 'ioredis';
7
6
  import type { Instance } from './brain.js';
8
- import { computeEmbedding, hasEmbedProvider, embedProviderHint, EMBED_PROVIDER } from '../embeddings.js';
7
+ import { computeEmbedding, hasEmbedProvider, embedProviderHint } from '../embeddings.js';
9
8
  import { detectNamespace } from '../namespace.js';
10
9
  import { simpleHash } from '../confidence.js';
11
10
 
@@ -1,6 +1,5 @@
1
1
  import type { Redis } from 'ioredis';
2
- import { computeEmbedding, hasEmbedProvider } from '../embeddings.js';
3
- import { simpleHash } from '../confidence.js';
2
+ import { computeEmbedding } from '../embeddings.js';
4
3
  import type { Instance } from './brain.js';
5
4
 
6
5
  type GetConnection = (instanceId: string) => Promise<Redis>;
@@ -64,13 +63,18 @@ export async function handleContextTool(
64
63
  if (ttl && ttl > 0) {
65
64
  body['expires_at'] = new Date(Date.now() + ttl * 1000).toISOString();
66
65
  }
67
- await fetch(`${vectorUrl}/entries`, {
66
+ const res = await fetch(`${vectorUrl}/entries`, {
68
67
  method: 'POST',
69
68
  headers: { 'Content-Type': 'application/json' },
70
69
  body: JSON.stringify(body),
71
- }).catch(() => undefined);
72
- } catch {
73
- // Embedding optional — continue silently
70
+ signal: AbortSignal.timeout(8000),
71
+ });
72
+ if (!res.ok) {
73
+ process.stderr.write(`[cachly] semantic index skipped (HTTP ${res.status}) — smart_recall on this context will be keyword-only\n`);
74
+ }
75
+ } catch (e) {
76
+ // Embedding is optional — keyword recall still works. Log so it's debuggable.
77
+ process.stderr.write(`[cachly] semantic index failed: ${e instanceof Error ? e.message : String(e)} — smart_recall on this context will be keyword-only\n`);
74
78
  }
75
79
  }
76
80
 
@@ -1,13 +1,11 @@
1
1
  import { createHmac } from 'node:crypto';
2
- import { execSync } from 'node:child_process';
3
2
  import type { Redis } from 'ioredis';
4
- import { calculateConfidence, confidenceBadge, CONFIDENCE_WARN_VALUE, CONFIDENCE_STALE_VALUE,
5
- CONFIDENCE_WARN_DAYS } from '../confidence.js';
6
3
  import { ckgSlug, extractProblemConcept, ckgUpsertNode, ckgUpdateEdge } from '../ckg.js';
7
4
  import type { CKGEdge, CKGNode } from '../ckg.js';
8
- import { keywordSearch, tokenize } from '../search.js';
9
5
  import { safeJsonParse } from '../utils.js';
10
- import { computeEmbedding, hasEmbedProvider } from '../embeddings.js';
6
+
7
+ // Last brain_from_git category counts — set after each run so index.ts can include them in telemetry
8
+ export let _lastBrainFromGitCounts: { fixes: number; features: number; refactors: number; total: number } | null = null;
11
9
 
12
10
  // Git concurrency semaphore (for brain_from_git parallel workers)
13
11
  let _gitSemCount = 0;
@@ -363,8 +361,6 @@ export async function handleFedbrainTool(
363
361
 
364
362
  if (hooksArr.includes('ci')) {
365
363
  lines.push(`### GitHub Actions CI outcome hook`);
366
- lines.push(`**Setup:** add a repo secret \`CACHLY_CACHE_TOKEN\` = your instance's cache token`);
367
- lines.push(`(find it via \`get_connection_string\` — the token in the cache URL).`);
368
364
  lines.push(`**Add at the end of each job** (after build/test steps):`);
369
365
  lines.push('```yaml');
370
366
  lines.push(`- name: cachly CLS — record CI outcome`);
@@ -372,14 +368,15 @@ export async function handleFedbrainTool(
372
368
  lines.push(` run: |`);
373
369
  lines.push(` node -e "`);
374
370
  lines.push(` const r=require('https');`);
375
- lines.push(` const d=JSON.stringify({source:'ci_outcome',payload:{`);
371
+ lines.push(` const d=JSON.stringify({instance_id:'${instance_id}',source:'ci_outcome',payload:{`);
376
372
  lines.push(` status:'\${{ job.status }}',prev_status:'unknown',job:'\${{ github.job }}',`);
377
373
  lines.push(` context:'github-actions run \${{ github.run_number }}'}});`);
378
- lines.push(` r.request({hostname:'api.cachly.dev',path:'/v1/cache/'+process.env.CACHLY_CACHE_TOKEN+'/cls/ingest',method:'POST',`);
379
- lines.push(` headers:{'Content-Type':'application/json','Content-Length':d.length}},()=>{}).end(d);`);
374
+ lines.push(` r.request({hostname:'api.cachly.dev',path:'/api/v1/cls/ingest',method:'POST',`);
375
+ lines.push(` headers:{'Content-Type':'application/json','Authorization':'Bearer $CACHLY_JWT',`);
376
+ lines.push(` 'Content-Length':d.length}},()=>{}).end(d);`);
380
377
  lines.push(` " 2>/dev/null || true`);
381
378
  lines.push(` env:`);
382
- lines.push(` CACHLY_CACHE_TOKEN: \${{ secrets.CACHLY_CACHE_TOKEN }}`);
379
+ lines.push(` CACHLY_JWT: \${{ secrets.CACHLY_JWT }}`);
383
380
  lines.push('```');
384
381
  lines.push('');
385
382
  }
@@ -1013,11 +1010,16 @@ export async function handleFedbrainTool(
1013
1010
  // Only store if no existing lesson for this topic (avoid overwriting higher-confidence lessons)
1014
1011
  const existing = await redis.get(`cachly:lesson:best:${topic}`);
1015
1012
  if (!existing) {
1016
- await redis.set(`cachly:lesson:best:${topic}`, JSON.stringify(lessonObj));
1013
+ // Auto-inferred git lessons get a 90-day TTL — they self-refresh on the next
1014
+ // brain_from_git run, and shouldn't pin memory forever if the repo goes stale.
1015
+ await redis.set(`cachly:lesson:best:${topic}`, JSON.stringify(lessonObj), 'EX', 90 * 86400);
1017
1016
  await redis.rpush(`cachly:lessons:${topic}`, JSON.stringify(lessonObj));
1017
+ await redis.ltrim(`cachly:lessons:${topic}`, -100, -1);
1018
+ await redis.expire(`cachly:lessons:${topic}`, 90 * 86400);
1018
1019
  }
1019
1020
  await redis.rpush('cachly:lessons:brain_from_git:all', JSON.stringify({ topic, sha: commit.sha.slice(0, 8), subject: commit.subject.slice(0, 60) }));
1020
1021
  await redis.ltrim('cachly:lessons:brain_from_git:all', -500, -1);
1022
+ await redis.expire('cachly:lessons:brain_from_git:all', 90 * 86400);
1021
1023
 
1022
1024
  // Update CKG
1023
1025
  const conceptId = ckgSlug(topic);
@@ -1034,6 +1036,13 @@ export async function handleFedbrainTool(
1034
1036
 
1035
1037
  const categoryBreakdown = [...categoryCount.entries()].sort((a, b) => b[1] - a[1]).map(([k, v]) => ` • **${k}** (${v})`).join('\n');
1036
1038
 
1039
+ _lastBrainFromGitCounts = {
1040
+ fixes: categoryCount.get('fix') ?? 0,
1041
+ features: categoryCount.get('feat') ?? 0,
1042
+ refactors: categoryCount.get('refactor') ?? 0,
1043
+ total: ingested,
1044
+ };
1045
+
1037
1046
  return [
1038
1047
  `🔁 **brain_from_git: ${repoDir}**`,
1039
1048
  isIncremental ? `🔄 Incremental mode — only new commits since \`${lastSha.slice(0, 8)}\` were processed` : '',
@@ -12,7 +12,7 @@ export async function handleRoadmapTool(
12
12
  name: string,
13
13
  args: Record<string, unknown>,
14
14
  getConnection: GetConnection,
15
- apiFetch: ApiFetch,
15
+ _apiFetch: ApiFetch,
16
16
  ): Promise<string | null> {
17
17
  switch (name) {
18
18
  case 'roadmap_add': {
@@ -157,9 +157,12 @@ export async function handleRoadmapTool(
157
157
  const PRIORITY_ORDER: Record<string, number> = { critical: 0, high: 1, medium: 2, low: 3 };
158
158
  const PRIORITY_ICON: Record<string, string> = { critical: '🔴', high: '🟠', medium: '🟡', low: '🔵' };
159
159
  const items = Object.values(all)
160
- .map(v => JSON.parse(v as string) as Record<string, unknown>)
160
+ .flatMap(v => {
161
+ const item = safeJsonParse<Record<string, unknown> | null>(v as string, null);
162
+ return item ? [item] : [];
163
+ })
161
164
  .filter(i => i.status === 'in-progress' || i.status === 'planned')
162
- .filter(i => !filterTag || (i.tags as string[]).includes(filterTag));
165
+ .filter(i => !filterTag || (Array.isArray(i.tags) ? i.tags : []).includes(filterTag));
163
166
  if (items.length === 0) return '🎉 **No open roadmap items!** All tasks are done (or use `roadmap_list` to check).';
164
167
  // in-progress first, then by priority
165
168
  items.sort((a, b) => {
@@ -169,7 +172,8 @@ export async function handleRoadmapTool(
169
172
  });
170
173
  const next = items[0];
171
174
  const remaining = items.length - 1;
172
- const tags = (next.tags as string[]).length ? `\nTags: ${(next.tags as string[]).join(', ')}` : '';
175
+ const nextTags = Array.isArray(next.tags) ? next.tags as string[] : [];
176
+ const tags = nextTags.length ? `\nTags: ${nextTags.join(', ')}` : '';
173
177
  const milestone = next.milestone ? `\nMilestone: ${next.milestone}` : '';
174
178
  const notes = next.notes ? `\nNotes: ${(next.notes as string).split('\n').pop()?.slice(0, 120)}` : '';
175
179
  return [
@@ -1,9 +1,9 @@
1
1
  import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
2
2
  import type { Redis } from 'ioredis';
3
3
  import { calculateConfidence } from '../confidence.js';
4
- import { ckgSlug, ckgUpdateEdge } from '../ckg.js';
4
+ import { ckgSlug } from '../ckg.js';
5
5
  import type { CKGEdge, CKGNode } from '../ckg.js';
6
- import { keywordSearch, tokenize } from '../search.js';
6
+ import { keywordSearch } from '../search.js';
7
7
  import { safeJsonParse } from '../utils.js';
8
8
 
9
9
  type GetConnection = (instanceId: string) => Promise<Redis>;
@@ -124,6 +124,9 @@ export async function handleSyndicateTool(
124
124
  id: string; topic: string; outcome: string;
125
125
  what_worked: string; confirm_count: number; created_at: string;
126
126
  }>;
127
+ top_contributors?: Array<{
128
+ trust_score: number; lessons_count: number; confirms_received: number;
129
+ }>;
127
130
  }>('/api/v1/syndication/stats');
128
131
 
129
132
  const confirmBar = (n: number) => {
@@ -163,9 +166,9 @@ export async function handleSyndicateTool(
163
166
  );
164
167
 
165
168
  // Top contributors (anonymous scores)
166
- if ((res as any).top_contributors?.length) {
169
+ if (res.top_contributors?.length) {
167
170
  lines.push(``, `### 🏅 Top Contributors (anonymous)`);
168
- for (const c of (res as any).top_contributors) {
171
+ for (const c of res.top_contributors) {
169
172
  lines.push(`- Trust **${c.trust_score}** · ${c.lessons_count} lesson${c.lessons_count === 1 ? '' : 's'} · ${c.confirms_received} confirms received`);
170
173
  }
171
174
  }
@@ -0,0 +1,345 @@
1
+ /**
2
+ * Travel Chaos Organizer — MCP bridge.
3
+ *
4
+ * Exposes the TCO REST API as MCP tools so any Claude instance with cachly-mcp
5
+ * configured can manage travel plans through natural language.
6
+ *
7
+ * Auth: TCO and Cachly share the same Keycloak realm, so the Cachly JWT
8
+ * (CACHLY_JWT env var) is forwarded as-is to the TCO backend — no extra
9
+ * credentials needed.
10
+ *
11
+ * Config:
12
+ * TCO_API_URL — base URL of the TCO FastAPI backend (e.g. http://localhost:8000)
13
+ * If not set, all tools return a configuration error.
14
+ */
15
+
16
+ const TCO_URL = process.env.TCO_API_URL ?? '';
17
+
18
+ export const TCO_TOOL_NAMES = new Set([
19
+ 'tco_list_trips',
20
+ 'tco_create_trip',
21
+ 'tco_get_timeline',
22
+ 'tco_delete_trip',
23
+ 'tco_inbox_list',
24
+ 'tco_inbox_assign',
25
+ 'tco_inbox_reject',
26
+ 'tco_parse_url',
27
+ 'tco_import_email',
28
+ 'tco_parse_text',
29
+ 'tco_cache_stats',
30
+ 'tco_provision_cache',
31
+ 'tco_telegram_status',
32
+ 'tco_telegram_send',
33
+ ]);
34
+
35
+ async function tcoFetch(path: string, jwt: string, init: RequestInit = {}): Promise<unknown> {
36
+ if (!TCO_URL) {
37
+ throw new Error(
38
+ 'TCO_API_URL is not configured. Add `TCO_API_URL=http://<your-tco-host>:8000` to your environment.'
39
+ );
40
+ }
41
+ const res = await fetch(`${TCO_URL}${path}`, {
42
+ ...init,
43
+ headers: {
44
+ Authorization: `Bearer ${jwt}`,
45
+ 'Content-Type': 'application/json',
46
+ ...(init.headers ?? {}),
47
+ },
48
+ signal: AbortSignal.timeout(20_000),
49
+ });
50
+
51
+ if (!res.ok) {
52
+ const text = await res.text().catch(() => '');
53
+ throw new Error(`TCO API ${res.status}: ${text}`);
54
+ }
55
+
56
+ const ct = res.headers.get('content-type') ?? '';
57
+ if (res.status === 204 || !ct.includes('application/json')) return null;
58
+ return res.json();
59
+ }
60
+
61
+ function noConfig(): string {
62
+ return (
63
+ '⚙️ **TCO not configured.**\n\n' +
64
+ 'Set `TCO_API_URL=http://<host>:8000` in your environment and restart cachly-mcp.\n' +
65
+ 'See `apps/travel-chaos-organizer/README.md` for setup instructions.'
66
+ );
67
+ }
68
+
69
+ function fmt(data: unknown): string {
70
+ return JSON.stringify(data, null, 2);
71
+ }
72
+
73
+ export async function handleTcoTool(
74
+ name: string,
75
+ args: Record<string, unknown>,
76
+ jwt: string,
77
+ ): Promise<string | null> {
78
+ if (!TCO_TOOL_NAMES.has(name)) return null;
79
+ if (!TCO_URL) return noConfig();
80
+
81
+ try {
82
+ switch (name) {
83
+
84
+ // ── List all trips ───────────────────────────────────────────────────
85
+ case 'tco_list_trips': {
86
+ const trips = await tcoFetch('/api/v1/trips', jwt);
87
+ const list = trips as Array<Record<string, unknown>>;
88
+ if (!list.length) return '📭 No trips yet. Use `tco_create_trip` to get started.';
89
+ const rows = list.map((t) => {
90
+ const dates = t.start_date
91
+ ? `${t.start_date}${t.end_date ? ` → ${t.end_date}` : ''}`
92
+ : 'no dates';
93
+ return `• **${t.name}** \`${t.id}\` — ${dates}`;
94
+ });
95
+ return `🗺️ **Your trips (${list.length}):**\n\n${rows.join('\n')}`;
96
+ }
97
+
98
+ // ── Create a trip ────────────────────────────────────────────────────
99
+ case 'tco_create_trip': {
100
+ const { name: tripName, description = null, start_date = null, end_date = null } = args as {
101
+ name: string; description?: string | null; start_date?: string | null; end_date?: string | null;
102
+ };
103
+ const trip = await tcoFetch('/api/v1/trips', jwt, {
104
+ method: 'POST',
105
+ body: JSON.stringify({ name: tripName, description, start_date, end_date }),
106
+ });
107
+ const t = trip as Record<string, unknown>;
108
+ return `✅ Trip **${t.name}** created (id: \`${t.id}\`)`;
109
+ }
110
+
111
+ // ── Get timeline for a trip ──────────────────────────────────────────
112
+ case 'tco_get_timeline': {
113
+ const { trip_id } = args as { trip_id: string };
114
+ const items = await tcoFetch(`/api/v1/trips/${trip_id}/items`, jwt);
115
+ const list = items as Array<Record<string, unknown>>;
116
+ if (!list.length) return `📂 Trip \`${trip_id}\` has no timeline items yet. Upload a document to get started.`;
117
+
118
+ const TYPE_ICONS: Record<string, string> = {
119
+ flight: '✈️', train: '🚂', bus: '🚌', hotel: '🏨',
120
+ rental_car: '🚗', activity: '🎡', transfer: '🚕', document: '📄', other: '📋',
121
+ };
122
+ const rows = list.map((i) => {
123
+ const icon = TYPE_ICONS[i.type as string] ?? '📋';
124
+ const time = i.event_at ? ` · ${(i.event_at as string).slice(0, 16).replace('T', ' ')}` : '';
125
+ const ref = i.booking_ref ? ` · #${i.booking_ref}` : '';
126
+ const provider = i.provider ? ` · ${i.provider}` : '';
127
+ return `${icon} **${i.title}**${time}${provider}${ref}`;
128
+ });
129
+ return `📅 **Timeline (${list.length} items):**\n\n${rows.join('\n')}`;
130
+ }
131
+
132
+ // ── Delete a trip ────────────────────────────────────────────────────
133
+ case 'tco_delete_trip': {
134
+ const { trip_id } = args as { trip_id: string };
135
+ await tcoFetch(`/api/v1/trips/${trip_id}`, jwt, { method: 'DELETE' });
136
+ return `🗑️ Trip \`${trip_id}\` deleted.`;
137
+ }
138
+
139
+ // ── Inbox: list pending items ────────────────────────────────────────
140
+ case 'tco_inbox_list': {
141
+ const status = (args.status as string | undefined) ?? 'pending';
142
+ const items = await tcoFetch(`/api/v1/inbox?status_filter=${status}`, jwt);
143
+ const list = items as Array<Record<string, unknown>>;
144
+ if (!list.length) return `🎉 Chaos inbox is empty (status: ${status}).`;
145
+ const rows = list.map((i) => {
146
+ const pd = i.parsed_data as Record<string, unknown> | null;
147
+ const title = (pd?.title as string | undefined) ?? '(no title)';
148
+ const src = (i.source as string | undefined) ?? 'unknown source';
149
+ return `• **${title}** \`${i.id}\` — from ${src}`;
150
+ });
151
+ return `📥 **Inbox (${list.length} ${status}):**\n\n${rows.join('\n')}\n\nUse \`tco_inbox_assign\` to move to a trip.`;
152
+ }
153
+
154
+ // ── Inbox: assign item to trip ───────────────────────────────────────
155
+ case 'tco_inbox_assign': {
156
+ const { inbox_id, trip_id, type = 'other' } = args as {
157
+ inbox_id: string; trip_id: string; type?: string;
158
+ };
159
+ const result = await tcoFetch(`/api/v1/inbox/${inbox_id}/assign`, jwt, {
160
+ method: 'POST',
161
+ body: JSON.stringify({ trip_id, type }),
162
+ });
163
+ const r = result as Record<string, unknown>;
164
+ return `✅ Inbox item assigned to trip. New timeline item id: \`${r.trip_item_id}\``;
165
+ }
166
+
167
+ // ── Inbox: reject / delete item ──────────────────────────────────────
168
+ case 'tco_inbox_reject': {
169
+ const { inbox_id } = args as { inbox_id: string };
170
+ await tcoFetch(`/api/v1/inbox/${inbox_id}`, jwt, { method: 'DELETE' });
171
+ return `🗑️ Inbox item \`${inbox_id}\` rejected and removed.`;
172
+ }
173
+
174
+ // ── Parse a public URL into a trip ───────────────────────────────────
175
+ case 'tco_parse_url': {
176
+ const { url, trip_id } = args as { url: string; trip_id?: string };
177
+ const body: Record<string, string> = { url };
178
+ if (trip_id) body.trip_id = trip_id;
179
+ const result = await tcoFetch('/api/v1/parse/url', jwt, {
180
+ method: 'POST',
181
+ body: JSON.stringify(body),
182
+ });
183
+ const r = result as Record<string, unknown>;
184
+ const confidence = typeof r.confidence === 'number' ? ` (confidence: ${Math.round(r.confidence * 100)}%)` : '';
185
+ const dest = trip_id ? `trip \`${trip_id}\`` : 'chaos inbox';
186
+ return `✅ URL parsed and saved to ${dest}${confidence}.\n\nParsed: ${fmt(r.parsed_data ?? r)}`;
187
+ }
188
+
189
+ // ── Import email text into inbox or trip ─────────────────────────────
190
+ case 'tco_import_email': {
191
+ const { raw_email, trip_id } = args as { raw_email: string; trip_id?: string };
192
+ const body: Record<string, string> = { raw_email };
193
+ if (trip_id) body.trip_id = trip_id;
194
+ const result = await tcoFetch('/api/v1/mail/import', jwt, {
195
+ method: 'POST',
196
+ body: JSON.stringify(body),
197
+ });
198
+ const r = result as Record<string, unknown>;
199
+ const dest = trip_id ? `trip \`${trip_id}\`` : 'chaos inbox';
200
+ return `✅ Email imported to ${dest}. Inbox id: \`${r.inbox_id ?? '?'}\``;
201
+ }
202
+
203
+ // ── Parse raw text via Ollama (Cachly-cached) ────────────────────────
204
+ case 'tco_parse_text': {
205
+ const { text, trip_id } = args as { text: string; trip_id?: string };
206
+ const params = new URLSearchParams();
207
+ params.set('raw_text', text);
208
+ if (trip_id) params.set('trip_id', trip_id);
209
+ const result = await tcoFetch('/api/v1/parse/text', jwt, {
210
+ method: 'POST',
211
+ body: params.toString(),
212
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
213
+ });
214
+ const r = result as Record<string, unknown>;
215
+ const parsed = r.parsed as Record<string, unknown> | undefined;
216
+ const cached = (r as Record<string, unknown>).cached;
217
+ const dest = trip_id ? `trip \`${trip_id}\`` : 'Chaos Inbox';
218
+ const cacheNote = cached ? ' ⚡ **(Cache Hit — kein AI-Call)**' : '';
219
+ const confidence = typeof parsed?.confidence === 'number'
220
+ ? ` (confidence: ${Math.round((parsed.confidence as number) * 100)}%)`
221
+ : '';
222
+ return `✅ Text geparst und in ${dest} gespeichert${confidence}${cacheNote}.\n\nParsed: ${fmt(parsed ?? r)}`;
223
+ }
224
+
225
+ // ── Cachly Redis cache stats ─────────────────────────────────────────
226
+ case 'tco_cache_stats': {
227
+ const stats = await tcoFetch('/api/v1/cache/stats', jwt) as Record<string, unknown>;
228
+ if (!stats.configured) {
229
+ return (
230
+ '📭 **Cachly Redis ist nicht verbunden.**\n\n' +
231
+ 'Setze `CACHLY_REDIS_URL=redis://<host>:6379/0` in der TCO `.env` und deploye neu.\n' +
232
+ 'Nutze `tco_provision_cache` um eine kostenlose Cachly-Instanz zu erstellen.'
233
+ );
234
+ }
235
+ const hits = stats.hits as number;
236
+ const misses = stats.misses as number;
237
+ const total = hits + misses;
238
+ const hitPct = total > 0 ? Math.round((hits / total) * 100) : 0;
239
+ const saved = hits > 0
240
+ ? `\n\n💡 **~${hits} AI-Calls eingespart** (jeder Cache Hit = kein Ollama-Aufruf).`
241
+ : '';
242
+ return (
243
+ `✅ **Cachly Redis — aktiv**\n\n` +
244
+ `| Metric | Wert |\n|--------|------|\n` +
245
+ `| Cache Hits | **${hits}** |\n` +
246
+ `| Cache Misses | ${misses} |\n` +
247
+ `| Hit Rate | **${hitPct}%** |\n` +
248
+ `| Gecachte Dokumente | ${stats.key_count} |\n` +
249
+ `| TTL | ${stats.ttl_days} Tage |` +
250
+ saved
251
+ );
252
+ }
253
+
254
+ // ── Provision a Cachly free instance for TCO ─────────────────────────
255
+ case 'tco_provision_cache': {
256
+ const CACHLY_API = process.env.CACHLY_API_URL ?? 'https://api.cachly.dev';
257
+ const res = await fetch(`${CACHLY_API}/api/v1/instances`, {
258
+ method: 'POST',
259
+ headers: {
260
+ Authorization: `Bearer ${jwt}`,
261
+ 'Content-Type': 'application/json',
262
+ },
263
+ body: JSON.stringify({ name: 'tco-cache', tier: 'free' }),
264
+ signal: AbortSignal.timeout(30_000),
265
+ });
266
+
267
+ if (!res.ok) {
268
+ const txt = await res.text().catch(() => '');
269
+ // Instance may already exist — try to fetch it
270
+ if (res.status === 409 || txt.includes('already')) {
271
+ const listRes = await fetch(`${CACHLY_API}/api/v1/instances`, {
272
+ headers: { Authorization: `Bearer ${jwt}` },
273
+ });
274
+ if (listRes.ok) {
275
+ const list = await listRes.json() as Array<Record<string, unknown>>;
276
+ const existing = list.find((i) => i.name === 'tco-cache');
277
+ if (existing) {
278
+ return (
279
+ `ℹ️ **Instanz "tco-cache" existiert bereits.**\n\n` +
280
+ `Redis URL: \`${existing.redis_url ?? existing.connection_string ?? '(siehe Cachly Dashboard)'}\`\n\n` +
281
+ `Setze in der TCO \`.env\`:\n\`\`\`\nCACHLY_REDIS_URL=${existing.redis_url ?? existing.connection_string}\n\`\`\`\n` +
282
+ `Dann: \`bash scripts/deploy.sh\``
283
+ );
284
+ }
285
+ }
286
+ }
287
+ throw new Error(`Cachly API ${res.status}: ${txt}`);
288
+ }
289
+
290
+ const instance = await res.json() as Record<string, unknown>;
291
+ const redisUrl = instance.redis_url ?? instance.connection_string ?? '';
292
+ return (
293
+ `🎉 **Cachly Redis Instanz erstellt!**\n\n` +
294
+ `Name: \`tco-cache\` (Free Tier, 25 MB)\n` +
295
+ `Redis URL: \`${redisUrl}\`\n\n` +
296
+ `**Setup (2 Schritte):**\n` +
297
+ `1. Füge zur TCO \`.env\` hinzu:\n\`\`\`\nCACHLY_REDIS_URL=${redisUrl}\n\`\`\`\n` +
298
+ `2. Neu deployen:\n\`\`\`bash\nbash scripts/deploy.sh\n\`\`\`\n\n` +
299
+ `Ab sofort werden alle Ollama Parse-Ergebnisse gecacht — kein zweiter AI-Call für dasselbe Dokument.`
300
+ );
301
+ }
302
+
303
+ // ── Telegram: check linked status ────────────────────────────────────
304
+ case 'tco_telegram_status': {
305
+ const me = await tcoFetch('/api/v1/users/me', jwt) as Record<string, unknown>;
306
+ const linked = me.telegram_linked === true;
307
+ if (linked) {
308
+ return (
309
+ `✅ **Telegram ist verknüpft.**\n\n` +
310
+ `Plan: ${me.plan === 'pro' ? '✦ Pro' : 'Free'}\n\n` +
311
+ `Du kannst mit \`tco_telegram_send\` direkt Nachrichten an den User senden.`
312
+ );
313
+ }
314
+ return (
315
+ `🔴 **Telegram nicht verknüpft.**\n\n` +
316
+ `Der User muss in der TCO-App unter *Einstellungen → Mit Telegram verknüpfen* einen PIN generieren ` +
317
+ `und diesen an @TCOBot senden (\`/link <PIN>\`).`
318
+ );
319
+ }
320
+
321
+ // ── Telegram: send message to user ───────────────────────────────────
322
+ case 'tco_telegram_send': {
323
+ const { message } = args as { message: string };
324
+ const me = await tcoFetch('/api/v1/users/me', jwt) as Record<string, unknown>;
325
+ if (!me.telegram_linked) {
326
+ return (
327
+ `🔴 **Telegram nicht verknüpft** — Nachricht wurde nicht gesendet.\n\n` +
328
+ `Nutze \`tco_telegram_status\` für Details zur Verknüpfung.`
329
+ );
330
+ }
331
+ await tcoFetch('/api/v1/users/telegram-notify', jwt, {
332
+ method: 'POST',
333
+ body: JSON.stringify({ message }),
334
+ });
335
+ return `✅ Telegram-Nachricht gesendet:\n\n> ${message.slice(0, 200)}${message.length > 200 ? '…' : ''}`;
336
+ }
337
+
338
+ default:
339
+ return null;
340
+ }
341
+ } catch (err) {
342
+ const msg = err instanceof Error ? err.message : String(err);
343
+ return `❌ TCO error: ${msg}`;
344
+ }
345
+ }