@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
@@ -64,13 +64,18 @@ export async function handleContextTool(
64
64
  if (ttl && ttl > 0) {
65
65
  body['expires_at'] = new Date(Date.now() + ttl * 1000).toISOString();
66
66
  }
67
- await fetch(`${vectorUrl}/entries`, {
67
+ const res = await fetch(`${vectorUrl}/entries`, {
68
68
  method: 'POST',
69
69
  headers: { 'Content-Type': 'application/json' },
70
70
  body: JSON.stringify(body),
71
- }).catch(() => undefined);
72
- } catch {
73
- // Embedding optional β€” continue silently
71
+ signal: AbortSignal.timeout(8000),
72
+ });
73
+ if (!res.ok) {
74
+ process.stderr.write(`[cachly] semantic index skipped (HTTP ${res.status}) β€” smart_recall on this context will be keyword-only\n`);
75
+ }
76
+ } catch (e) {
77
+ // Embedding is optional β€” keyword recall still works. Log so it's debuggable.
78
+ 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
79
  }
75
80
  }
76
81
 
@@ -9,6 +9,9 @@ import { keywordSearch, tokenize } from '../search.js';
9
9
  import { safeJsonParse } from '../utils.js';
10
10
  import { computeEmbedding, hasEmbedProvider } from '../embeddings.js';
11
11
 
12
+ // Last brain_from_git category counts β€” set after each run so index.ts can include them in telemetry
13
+ export let _lastBrainFromGitCounts: { fixes: number; features: number; refactors: number; total: number } | null = null;
14
+
12
15
  // Git concurrency semaphore (for brain_from_git parallel workers)
13
16
  let _gitSemCount = 0;
14
17
  const _GIT_SEM_MAX = 10;
@@ -363,8 +366,6 @@ export async function handleFedbrainTool(
363
366
 
364
367
  if (hooksArr.includes('ci')) {
365
368
  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
369
  lines.push(`**Add at the end of each job** (after build/test steps):`);
369
370
  lines.push('```yaml');
370
371
  lines.push(`- name: cachly CLS β€” record CI outcome`);
@@ -372,14 +373,15 @@ export async function handleFedbrainTool(
372
373
  lines.push(` run: |`);
373
374
  lines.push(` node -e "`);
374
375
  lines.push(` const r=require('https');`);
375
- lines.push(` const d=JSON.stringify({source:'ci_outcome',payload:{`);
376
+ lines.push(` const d=JSON.stringify({instance_id:'${instance_id}',source:'ci_outcome',payload:{`);
376
377
  lines.push(` status:'\${{ job.status }}',prev_status:'unknown',job:'\${{ github.job }}',`);
377
378
  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);`);
379
+ lines.push(` r.request({hostname:'api.cachly.dev',path:'/api/v1/cls/ingest',method:'POST',`);
380
+ lines.push(` headers:{'Content-Type':'application/json','Authorization':'Bearer $CACHLY_JWT',`);
381
+ lines.push(` 'Content-Length':d.length}},()=>{}).end(d);`);
380
382
  lines.push(` " 2>/dev/null || true`);
381
383
  lines.push(` env:`);
382
- lines.push(` CACHLY_CACHE_TOKEN: \${{ secrets.CACHLY_CACHE_TOKEN }}`);
384
+ lines.push(` CACHLY_JWT: \${{ secrets.CACHLY_JWT }}`);
383
385
  lines.push('```');
384
386
  lines.push('');
385
387
  }
@@ -1013,11 +1015,16 @@ export async function handleFedbrainTool(
1013
1015
  // Only store if no existing lesson for this topic (avoid overwriting higher-confidence lessons)
1014
1016
  const existing = await redis.get(`cachly:lesson:best:${topic}`);
1015
1017
  if (!existing) {
1016
- await redis.set(`cachly:lesson:best:${topic}`, JSON.stringify(lessonObj));
1018
+ // Auto-inferred git lessons get a 90-day TTL β€” they self-refresh on the next
1019
+ // brain_from_git run, and shouldn't pin memory forever if the repo goes stale.
1020
+ await redis.set(`cachly:lesson:best:${topic}`, JSON.stringify(lessonObj), 'EX', 90 * 86400);
1017
1021
  await redis.rpush(`cachly:lessons:${topic}`, JSON.stringify(lessonObj));
1022
+ await redis.ltrim(`cachly:lessons:${topic}`, -100, -1);
1023
+ await redis.expire(`cachly:lessons:${topic}`, 90 * 86400);
1018
1024
  }
1019
1025
  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
1026
  await redis.ltrim('cachly:lessons:brain_from_git:all', -500, -1);
1027
+ await redis.expire('cachly:lessons:brain_from_git:all', 90 * 86400);
1021
1028
 
1022
1029
  // Update CKG
1023
1030
  const conceptId = ckgSlug(topic);
@@ -1034,6 +1041,13 @@ export async function handleFedbrainTool(
1034
1041
 
1035
1042
  const categoryBreakdown = [...categoryCount.entries()].sort((a, b) => b[1] - a[1]).map(([k, v]) => ` β€’ **${k}** (${v})`).join('\n');
1036
1043
 
1044
+ _lastBrainFromGitCounts = {
1045
+ fixes: categoryCount.get('fix') ?? 0,
1046
+ features: categoryCount.get('feat') ?? 0,
1047
+ refactors: categoryCount.get('refactor') ?? 0,
1048
+ total: ingested,
1049
+ };
1050
+
1037
1051
  return [
1038
1052
  `πŸ” **brain_from_git: ${repoDir}**`,
1039
1053
  isIncremental ? `πŸ”„ Incremental mode β€” only new commits since \`${lastSha.slice(0, 8)}\` were processed` : '',
@@ -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 [
@@ -0,0 +1,347 @@
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
+ type TcoFetch = (path: string, jwt: string, init?: RequestInit) => Promise<unknown>;
36
+
37
+ async function tcoFetch(path: string, jwt: string, init: RequestInit = {}): Promise<unknown> {
38
+ if (!TCO_URL) {
39
+ throw new Error(
40
+ 'TCO_API_URL is not configured. Add `TCO_API_URL=http://<your-tco-host>:8000` to your environment.'
41
+ );
42
+ }
43
+ const res = await fetch(`${TCO_URL}${path}`, {
44
+ ...init,
45
+ headers: {
46
+ Authorization: `Bearer ${jwt}`,
47
+ 'Content-Type': 'application/json',
48
+ ...(init.headers ?? {}),
49
+ },
50
+ signal: AbortSignal.timeout(20_000),
51
+ });
52
+
53
+ if (!res.ok) {
54
+ const text = await res.text().catch(() => '');
55
+ throw new Error(`TCO API ${res.status}: ${text}`);
56
+ }
57
+
58
+ const ct = res.headers.get('content-type') ?? '';
59
+ if (res.status === 204 || !ct.includes('application/json')) return null;
60
+ return res.json();
61
+ }
62
+
63
+ function noConfig(): string {
64
+ return (
65
+ 'βš™οΈ **TCO not configured.**\n\n' +
66
+ 'Set `TCO_API_URL=http://<host>:8000` in your environment and restart cachly-mcp.\n' +
67
+ 'See `apps/travel-chaos-organizer/README.md` for setup instructions.'
68
+ );
69
+ }
70
+
71
+ function fmt(data: unknown): string {
72
+ return JSON.stringify(data, null, 2);
73
+ }
74
+
75
+ export async function handleTcoTool(
76
+ name: string,
77
+ args: Record<string, unknown>,
78
+ jwt: string,
79
+ ): Promise<string | null> {
80
+ if (!TCO_TOOL_NAMES.has(name)) return null;
81
+ if (!TCO_URL) return noConfig();
82
+
83
+ try {
84
+ switch (name) {
85
+
86
+ // ── List all trips ───────────────────────────────────────────────────
87
+ case 'tco_list_trips': {
88
+ const trips = await tcoFetch('/api/v1/trips', jwt);
89
+ const list = trips as Array<Record<string, unknown>>;
90
+ if (!list.length) return 'πŸ“­ No trips yet. Use `tco_create_trip` to get started.';
91
+ const rows = list.map((t) => {
92
+ const dates = t.start_date
93
+ ? `${t.start_date}${t.end_date ? ` β†’ ${t.end_date}` : ''}`
94
+ : 'no dates';
95
+ return `β€’ **${t.name}** \`${t.id}\` β€” ${dates}`;
96
+ });
97
+ return `πŸ—ΊοΈ **Your trips (${list.length}):**\n\n${rows.join('\n')}`;
98
+ }
99
+
100
+ // ── Create a trip ────────────────────────────────────────────────────
101
+ case 'tco_create_trip': {
102
+ const { name: tripName, description = null, start_date = null, end_date = null } = args as {
103
+ name: string; description?: string | null; start_date?: string | null; end_date?: string | null;
104
+ };
105
+ const trip = await tcoFetch('/api/v1/trips', jwt, {
106
+ method: 'POST',
107
+ body: JSON.stringify({ name: tripName, description, start_date, end_date }),
108
+ });
109
+ const t = trip as Record<string, unknown>;
110
+ return `βœ… Trip **${t.name}** created (id: \`${t.id}\`)`;
111
+ }
112
+
113
+ // ── Get timeline for a trip ──────────────────────────────────────────
114
+ case 'tco_get_timeline': {
115
+ const { trip_id } = args as { trip_id: string };
116
+ const items = await tcoFetch(`/api/v1/trips/${trip_id}/items`, jwt);
117
+ const list = items as Array<Record<string, unknown>>;
118
+ if (!list.length) return `πŸ“‚ Trip \`${trip_id}\` has no timeline items yet. Upload a document to get started.`;
119
+
120
+ const TYPE_ICONS: Record<string, string> = {
121
+ flight: '✈️', train: 'πŸš‚', bus: '🚌', hotel: '🏨',
122
+ rental_car: 'πŸš—', activity: '🎑', transfer: 'πŸš•', document: 'πŸ“„', other: 'πŸ“‹',
123
+ };
124
+ const rows = list.map((i) => {
125
+ const icon = TYPE_ICONS[i.type as string] ?? 'πŸ“‹';
126
+ const time = i.event_at ? ` Β· ${(i.event_at as string).slice(0, 16).replace('T', ' ')}` : '';
127
+ const ref = i.booking_ref ? ` Β· #${i.booking_ref}` : '';
128
+ const provider = i.provider ? ` Β· ${i.provider}` : '';
129
+ return `${icon} **${i.title}**${time}${provider}${ref}`;
130
+ });
131
+ return `πŸ“… **Timeline (${list.length} items):**\n\n${rows.join('\n')}`;
132
+ }
133
+
134
+ // ── Delete a trip ────────────────────────────────────────────────────
135
+ case 'tco_delete_trip': {
136
+ const { trip_id } = args as { trip_id: string };
137
+ await tcoFetch(`/api/v1/trips/${trip_id}`, jwt, { method: 'DELETE' });
138
+ return `πŸ—‘οΈ Trip \`${trip_id}\` deleted.`;
139
+ }
140
+
141
+ // ── Inbox: list pending items ────────────────────────────────────────
142
+ case 'tco_inbox_list': {
143
+ const status = (args.status as string | undefined) ?? 'pending';
144
+ const items = await tcoFetch(`/api/v1/inbox?status_filter=${status}`, jwt);
145
+ const list = items as Array<Record<string, unknown>>;
146
+ if (!list.length) return `πŸŽ‰ Chaos inbox is empty (status: ${status}).`;
147
+ const rows = list.map((i) => {
148
+ const pd = i.parsed_data as Record<string, unknown> | null;
149
+ const title = (pd?.title as string | undefined) ?? '(no title)';
150
+ const src = (i.source as string | undefined) ?? 'unknown source';
151
+ return `β€’ **${title}** \`${i.id}\` β€” from ${src}`;
152
+ });
153
+ return `πŸ“₯ **Inbox (${list.length} ${status}):**\n\n${rows.join('\n')}\n\nUse \`tco_inbox_assign\` to move to a trip.`;
154
+ }
155
+
156
+ // ── Inbox: assign item to trip ───────────────────────────────────────
157
+ case 'tco_inbox_assign': {
158
+ const { inbox_id, trip_id, type = 'other' } = args as {
159
+ inbox_id: string; trip_id: string; type?: string;
160
+ };
161
+ const result = await tcoFetch(`/api/v1/inbox/${inbox_id}/assign`, jwt, {
162
+ method: 'POST',
163
+ body: JSON.stringify({ trip_id, type }),
164
+ });
165
+ const r = result as Record<string, unknown>;
166
+ return `βœ… Inbox item assigned to trip. New timeline item id: \`${r.trip_item_id}\``;
167
+ }
168
+
169
+ // ── Inbox: reject / delete item ──────────────────────────────────────
170
+ case 'tco_inbox_reject': {
171
+ const { inbox_id } = args as { inbox_id: string };
172
+ await tcoFetch(`/api/v1/inbox/${inbox_id}`, jwt, { method: 'DELETE' });
173
+ return `πŸ—‘οΈ Inbox item \`${inbox_id}\` rejected and removed.`;
174
+ }
175
+
176
+ // ── Parse a public URL into a trip ───────────────────────────────────
177
+ case 'tco_parse_url': {
178
+ const { url, trip_id } = args as { url: string; trip_id?: string };
179
+ const body: Record<string, string> = { url };
180
+ if (trip_id) body.trip_id = trip_id;
181
+ const result = await tcoFetch('/api/v1/parse/url', jwt, {
182
+ method: 'POST',
183
+ body: JSON.stringify(body),
184
+ });
185
+ const r = result as Record<string, unknown>;
186
+ const confidence = typeof r.confidence === 'number' ? ` (confidence: ${Math.round(r.confidence * 100)}%)` : '';
187
+ const dest = trip_id ? `trip \`${trip_id}\`` : 'chaos inbox';
188
+ return `βœ… URL parsed and saved to ${dest}${confidence}.\n\nParsed: ${fmt(r.parsed_data ?? r)}`;
189
+ }
190
+
191
+ // ── Import email text into inbox or trip ─────────────────────────────
192
+ case 'tco_import_email': {
193
+ const { raw_email, trip_id } = args as { raw_email: string; trip_id?: string };
194
+ const body: Record<string, string> = { raw_email };
195
+ if (trip_id) body.trip_id = trip_id;
196
+ const result = await tcoFetch('/api/v1/mail/import', jwt, {
197
+ method: 'POST',
198
+ body: JSON.stringify(body),
199
+ });
200
+ const r = result as Record<string, unknown>;
201
+ const dest = trip_id ? `trip \`${trip_id}\`` : 'chaos inbox';
202
+ return `βœ… Email imported to ${dest}. Inbox id: \`${r.inbox_id ?? '?'}\``;
203
+ }
204
+
205
+ // ── Parse raw text via Ollama (Cachly-cached) ────────────────────────
206
+ case 'tco_parse_text': {
207
+ const { text, trip_id } = args as { text: string; trip_id?: string };
208
+ const params = new URLSearchParams();
209
+ params.set('raw_text', text);
210
+ if (trip_id) params.set('trip_id', trip_id);
211
+ const result = await tcoFetch('/api/v1/parse/text', jwt, {
212
+ method: 'POST',
213
+ body: params.toString(),
214
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
215
+ });
216
+ const r = result as Record<string, unknown>;
217
+ const parsed = r.parsed as Record<string, unknown> | undefined;
218
+ const cached = (r as Record<string, unknown>).cached;
219
+ const dest = trip_id ? `trip \`${trip_id}\`` : 'Chaos Inbox';
220
+ const cacheNote = cached ? ' ⚑ **(Cache Hit β€” kein AI-Call)**' : '';
221
+ const confidence = typeof parsed?.confidence === 'number'
222
+ ? ` (confidence: ${Math.round((parsed.confidence as number) * 100)}%)`
223
+ : '';
224
+ return `βœ… Text geparst und in ${dest} gespeichert${confidence}${cacheNote}.\n\nParsed: ${fmt(parsed ?? r)}`;
225
+ }
226
+
227
+ // ── Cachly Redis cache stats ─────────────────────────────────────────
228
+ case 'tco_cache_stats': {
229
+ const stats = await tcoFetch('/api/v1/cache/stats', jwt) as Record<string, unknown>;
230
+ if (!stats.configured) {
231
+ return (
232
+ 'πŸ“­ **Cachly Redis ist nicht verbunden.**\n\n' +
233
+ 'Setze `CACHLY_REDIS_URL=redis://<host>:6379/0` in der TCO `.env` und deploye neu.\n' +
234
+ 'Nutze `tco_provision_cache` um eine kostenlose Cachly-Instanz zu erstellen.'
235
+ );
236
+ }
237
+ const hits = stats.hits as number;
238
+ const misses = stats.misses as number;
239
+ const total = hits + misses;
240
+ const hitPct = total > 0 ? Math.round((hits / total) * 100) : 0;
241
+ const saved = hits > 0
242
+ ? `\n\nπŸ’‘ **~${hits} AI-Calls eingespart** (jeder Cache Hit = kein Ollama-Aufruf).`
243
+ : '';
244
+ return (
245
+ `βœ… **Cachly Redis β€” aktiv**\n\n` +
246
+ `| Metric | Wert |\n|--------|------|\n` +
247
+ `| Cache Hits | **${hits}** |\n` +
248
+ `| Cache Misses | ${misses} |\n` +
249
+ `| Hit Rate | **${hitPct}%** |\n` +
250
+ `| Gecachte Dokumente | ${stats.key_count} |\n` +
251
+ `| TTL | ${stats.ttl_days} Tage |` +
252
+ saved
253
+ );
254
+ }
255
+
256
+ // ── Provision a Cachly free instance for TCO ─────────────────────────
257
+ case 'tco_provision_cache': {
258
+ const CACHLY_API = process.env.CACHLY_API_URL ?? 'https://api.cachly.dev';
259
+ const res = await fetch(`${CACHLY_API}/api/v1/instances`, {
260
+ method: 'POST',
261
+ headers: {
262
+ Authorization: `Bearer ${jwt}`,
263
+ 'Content-Type': 'application/json',
264
+ },
265
+ body: JSON.stringify({ name: 'tco-cache', tier: 'free' }),
266
+ signal: AbortSignal.timeout(30_000),
267
+ });
268
+
269
+ if (!res.ok) {
270
+ const txt = await res.text().catch(() => '');
271
+ // Instance may already exist β€” try to fetch it
272
+ if (res.status === 409 || txt.includes('already')) {
273
+ const listRes = await fetch(`${CACHLY_API}/api/v1/instances`, {
274
+ headers: { Authorization: `Bearer ${jwt}` },
275
+ });
276
+ if (listRes.ok) {
277
+ const list = await listRes.json() as Array<Record<string, unknown>>;
278
+ const existing = list.find((i) => i.name === 'tco-cache');
279
+ if (existing) {
280
+ return (
281
+ `ℹ️ **Instanz "tco-cache" existiert bereits.**\n\n` +
282
+ `Redis URL: \`${existing.redis_url ?? existing.connection_string ?? '(siehe Cachly Dashboard)'}\`\n\n` +
283
+ `Setze in der TCO \`.env\`:\n\`\`\`\nCACHLY_REDIS_URL=${existing.redis_url ?? existing.connection_string}\n\`\`\`\n` +
284
+ `Dann: \`bash scripts/deploy.sh\``
285
+ );
286
+ }
287
+ }
288
+ }
289
+ throw new Error(`Cachly API ${res.status}: ${txt}`);
290
+ }
291
+
292
+ const instance = await res.json() as Record<string, unknown>;
293
+ const redisUrl = instance.redis_url ?? instance.connection_string ?? '';
294
+ return (
295
+ `πŸŽ‰ **Cachly Redis Instanz erstellt!**\n\n` +
296
+ `Name: \`tco-cache\` (Free Tier, 25 MB)\n` +
297
+ `Redis URL: \`${redisUrl}\`\n\n` +
298
+ `**Setup (2 Schritte):**\n` +
299
+ `1. FΓΌge zur TCO \`.env\` hinzu:\n\`\`\`\nCACHLY_REDIS_URL=${redisUrl}\n\`\`\`\n` +
300
+ `2. Neu deployen:\n\`\`\`bash\nbash scripts/deploy.sh\n\`\`\`\n\n` +
301
+ `Ab sofort werden alle Ollama Parse-Ergebnisse gecacht β€” kein zweiter AI-Call fΓΌr dasselbe Dokument.`
302
+ );
303
+ }
304
+
305
+ // ── Telegram: check linked status ────────────────────────────────────
306
+ case 'tco_telegram_status': {
307
+ const me = await tcoFetch('/api/v1/users/me', jwt) as Record<string, unknown>;
308
+ const linked = me.telegram_linked === true;
309
+ if (linked) {
310
+ return (
311
+ `βœ… **Telegram ist verknΓΌpft.**\n\n` +
312
+ `Plan: ${me.plan === 'pro' ? '✦ Pro' : 'Free'}\n\n` +
313
+ `Du kannst mit \`tco_telegram_send\` direkt Nachrichten an den User senden.`
314
+ );
315
+ }
316
+ return (
317
+ `πŸ”΄ **Telegram nicht verknΓΌpft.**\n\n` +
318
+ `Der User muss in der TCO-App unter *Einstellungen β†’ Mit Telegram verknΓΌpfen* einen PIN generieren ` +
319
+ `und diesen an @TCOBot senden (\`/link <PIN>\`).`
320
+ );
321
+ }
322
+
323
+ // ── Telegram: send message to user ───────────────────────────────────
324
+ case 'tco_telegram_send': {
325
+ const { message } = args as { message: string };
326
+ const me = await tcoFetch('/api/v1/users/me', jwt) as Record<string, unknown>;
327
+ if (!me.telegram_linked) {
328
+ return (
329
+ `πŸ”΄ **Telegram nicht verknΓΌpft** β€” Nachricht wurde nicht gesendet.\n\n` +
330
+ `Nutze \`tco_telegram_status\` fΓΌr Details zur VerknΓΌpfung.`
331
+ );
332
+ }
333
+ await tcoFetch('/api/v1/users/telegram-notify', jwt, {
334
+ method: 'POST',
335
+ body: JSON.stringify({ message }),
336
+ });
337
+ return `βœ… Telegram-Nachricht gesendet:\n\n> ${message.slice(0, 200)}${message.length > 200 ? '…' : ''}`;
338
+ }
339
+
340
+ default:
341
+ return null;
342
+ }
343
+ } catch (err) {
344
+ const msg = err instanceof Error ? err.message : String(err);
345
+ return `❌ TCO error: ${msg}`;
346
+ }
347
+ }
@@ -11,7 +11,7 @@ type GetConnection = (instanceId: string) => Promise<Redis>;
11
11
  type ApiFetch = <T>(path: string, options?: RequestInit) => Promise<T>;
12
12
 
13
13
  export const TEAM_TOOL_NAMES = new Set([
14
- 'sync_file_changes', 'team_learn', 'team_recall', 'team_synthesize', 'memory_crystalize',
14
+ 'sync_file_changes', 'team_learn', 'team_confirm', 'team_recall', 'team_synthesize', 'memory_crystalize',
15
15
  'brain_doctor', 'recall_at', 'trace_dependency', 'global_learn', 'global_recall',
16
16
  'publish_lesson', 'import_public_brain', 'setup_ai_memory',
17
17
  ]);
@@ -109,9 +109,7 @@ export async function handleTeamTool(
109
109
  file_paths: file_paths ?? [],
110
110
  commands: commands ?? [],
111
111
  tags: [...(tags ?? []), 'team'],
112
- ts: new Date().toISOString(),
113
- verified_at: outcome === 'success' ? new Date().toISOString() : undefined,
114
- confidence: 1.0,
112
+ timestamp: new Date().toISOString(),
115
113
  recall_count: 0,
116
114
  version: 2,
117
115
  };
@@ -126,6 +124,59 @@ export async function handleTeamTool(
126
124
  return `βœ… Team lesson stored by **${author}**: \`${topic}\` (${outcome})\nπŸ’‘ ${what_worked.slice(0, 120)}`;
127
125
  }
128
126
 
127
+ // ── team_confirm β€” knowledge governance (Phase 3) ─────────────────────────
128
+ // A human reviewer endorses a stored lesson. The endorsement raises the
129
+ // lesson's recall ranking (see src/rerank.ts), so trusted, reviewed knowledge
130
+ // surfaces above unreviewed auto-learned entries.
131
+ case 'team_confirm': {
132
+ const { instance_id, topic, reviewer, level = 'peer', note = '' } = args as {
133
+ instance_id: string; topic: string; reviewer: string;
134
+ level?: 'senior' | 'peer'; note?: string;
135
+ };
136
+ if (!instance_id) return '❌ instance_id required';
137
+ if (!topic || !reviewer) return '❌ Required: topic, reviewer';
138
+ const reviewLevel = level === 'senior' ? 'senior' : 'peer';
139
+
140
+ const redis = await getConnection(instance_id);
141
+ const key = `cachly:lesson:best:${topic}`;
142
+ const raw = await redis.get(key);
143
+ const lesson = safeJsonParse<Record<string, unknown> | null>(raw, null);
144
+ if (!lesson) {
145
+ return `πŸ“­ No best-solution lesson found for \`${topic}\`.\n\nA lesson must exist (store one with \`team_learn\` / \`learn_from_attempts\`) before it can be confirmed.`;
146
+ }
147
+
148
+ // Track distinct reviewers so endorsements can't be inflated by one person.
149
+ const endorsersKey = `cachly:lesson:endorsers:${topic}`;
150
+ const added = await redis.sadd(endorsersKey, reviewer);
151
+ await redis.expire(endorsersKey, 365 * 86400);
152
+ const endorsements = await redis.scard(endorsersKey);
153
+
154
+ // Senior beats peer; never downgrade an existing senior review to peer.
155
+ const prevLevel = (lesson.review_level as string) ?? '';
156
+ const effectiveLevel = prevLevel === 'senior' ? 'senior' : reviewLevel;
157
+
158
+ const updated = {
159
+ ...lesson,
160
+ reviewed_by: reviewer,
161
+ review_level: effectiveLevel,
162
+ endorsements,
163
+ reviewed_at: new Date().toISOString(),
164
+ };
165
+ await redis.set(key, JSON.stringify(updated));
166
+
167
+ // Append an immutable review record for the audit trail.
168
+ const reviewLog = {
169
+ reviewer, level: reviewLevel, note: note.slice(0, 280), ts: new Date().toISOString(),
170
+ };
171
+ const logKey = `cachly:lesson:reviews:${topic}`;
172
+ await redis.rpush(logKey, JSON.stringify(reviewLog));
173
+ await redis.ltrim(logKey, -50, -1);
174
+ await redis.expire(logKey, 365 * 86400);
175
+
176
+ const badge = effectiveLevel === 'senior' ? 'πŸ›‘οΈ senior-reviewed' : 'βœ”οΈ peer-reviewed';
177
+ return `${badge} β€” \`${topic}\` confirmed by **${reviewer}** (${endorsements} distinct endorsement${endorsements === 1 ? '' : 's'}).\nπŸ“ˆ This lesson now ranks higher in \`smart_recall\` / \`team_recall\`.${note ? `\nπŸ“ ${note.slice(0, 200)}` : ''}`;
178
+ }
179
+
129
180
  // ── team_recall ───────────────────────────────────────────────────────────
130
181
  case 'team_recall': {
131
182
  const { instance_id, topic, author, limit = 10 } = args as {
@@ -148,6 +199,7 @@ export async function handleTeamTool(
148
199
  topic: string; outcome: string; what_worked: string;
149
200
  ts: string; severity?: string; recall_count?: number;
150
201
  author?: string; tags?: string[];
202
+ reviewed_by?: string; review_level?: string; endorsements?: number;
151
203
  };
152
204
  let lessons: TeamLesson[] = [];
153
205
  if (lessonKeys.length > 0) {
@@ -171,8 +223,10 @@ export async function handleTeamTool(
171
223
  lessons = lessons.filter(l => l.author?.toLowerCase().includes(a));
172
224
  }
173
225
 
174
- // Sort by recall_count desc
175
- lessons.sort((a, b) => (b.recall_count ?? 0) - (a.recall_count ?? 0));
226
+ // Sort: reviewed lessons first (senior > peer), then by recall_count desc.
227
+ const reviewRank = (l: TeamLesson) => l.reviewed_by ? (l.review_level === 'senior' ? 2 : 1) : 0;
228
+ lessons.sort((a, b) =>
229
+ (reviewRank(b) - reviewRank(a)) || ((b.recall_count ?? 0) - (a.recall_count ?? 0)));
176
230
  lessons = lessons.slice(0, limit);
177
231
 
178
232
  if (lessons.length === 0) {
@@ -185,11 +239,17 @@ export async function handleTeamTool(
185
239
  for (const l of lessons) {
186
240
  const emoji = l.outcome === 'success' ? 'βœ…' : l.outcome === 'partial' ? '⚠️' : '❌';
187
241
  const sev = l.severity === 'critical' ? 'πŸ”΄ ' : l.severity === 'major' ? '🟑 ' : '';
242
+ const reviewBadge = l.reviewed_by
243
+ ? (l.review_level === 'senior' ? ' πŸ›‘οΈ' : ' βœ”οΈ')
244
+ : '';
188
245
  const authorStr = l.author ? ` Β· _by ${l.author}_` : '';
246
+ const reviewStr = l.reviewed_by
247
+ ? ` Β· reviewed by ${l.reviewed_by}${(l.endorsements ?? 0) > 1 ? ` +${(l.endorsements ?? 0) - 1}` : ''}`
248
+ : '';
189
249
  const recallStr = (l.recall_count ?? 0) > 0 ? ` Β· recalled ${l.recall_count}Γ—` : '';
190
250
  const ago = Math.round((Date.now() - new Date(l.ts).getTime()) / 86400000);
191
251
  const agoStr = ago === 0 ? 'today' : ago === 1 ? 'yesterday' : `${ago}d ago`;
192
- lines.push(`${emoji} ${sev}**\`${l.topic}\`**${authorStr}${recallStr} Β· ${agoStr}`);
252
+ lines.push(`${emoji} ${sev}**\`${l.topic}\`**${reviewBadge}${authorStr}${reviewStr}${recallStr} Β· ${agoStr}`);
193
253
  lines.push(` ${l.what_worked.slice(0, 120)}`);
194
254
  lines.push('');
195
255
  }