@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.
- package/.github/workflows/eas-build-android.yml +58 -0
- package/.github/workflows/eas-update.yml +55 -0
- package/.github/workflows/publish.yml +2 -2
- package/BENCH.md +91 -0
- package/CHANGELOG.md +44 -0
- package/README.md +6 -34
- package/STRATEGY.md +228 -0
- package/apps/travel-chaos-organizer/.env.example +49 -0
- package/apps/travel-chaos-organizer/DEPLOY.md +328 -0
- package/apps/travel-chaos-organizer/Makefile +57 -0
- package/apps/travel-chaos-organizer/README.md +159 -0
- package/apps/travel-chaos-organizer/backend/.env.example +10 -0
- package/apps/travel-chaos-organizer/backend/Dockerfile +25 -0
- package/apps/travel-chaos-organizer/backend/alembic/env.py +56 -0
- package/apps/travel-chaos-organizer/backend/alembic/script.py.mako +24 -0
- package/apps/travel-chaos-organizer/backend/alembic/versions/.gitkeep +0 -0
- package/apps/travel-chaos-organizer/backend/alembic/versions/0001_stripe_customer_id.py +24 -0
- package/apps/travel-chaos-organizer/backend/alembic/versions/0002_telegram_chat_id.py +24 -0
- package/apps/travel-chaos-organizer/backend/alembic.ini +39 -0
- package/apps/travel-chaos-organizer/backend/app/__init__.py +0 -0
- package/apps/travel-chaos-organizer/backend/app/auth/__init__.py +0 -0
- package/apps/travel-chaos-organizer/backend/app/auth/keycloak.py +81 -0
- package/apps/travel-chaos-organizer/backend/app/config.py +66 -0
- package/apps/travel-chaos-organizer/backend/app/db/__init__.py +0 -0
- package/apps/travel-chaos-organizer/backend/app/db/database.py +20 -0
- package/apps/travel-chaos-organizer/backend/app/db/migrations/001_init.sql +71 -0
- package/apps/travel-chaos-organizer/backend/app/db/migrations/002_telemetry_freemium.sql +31 -0
- package/apps/travel-chaos-organizer/backend/app/db/migrations/003_telegram_chat_id.sql +4 -0
- package/apps/travel-chaos-organizer/backend/app/db/migrations/004_stripe_and_email.sql +5 -0
- package/apps/travel-chaos-organizer/backend/app/limiter.py +4 -0
- package/apps/travel-chaos-organizer/backend/app/logging_config.py +25 -0
- package/apps/travel-chaos-organizer/backend/app/main.py +77 -0
- package/apps/travel-chaos-organizer/backend/app/models/__init__.py +0 -0
- package/apps/travel-chaos-organizer/backend/app/models/schemas.py +142 -0
- package/apps/travel-chaos-organizer/backend/app/routers/__init__.py +0 -0
- package/apps/travel-chaos-organizer/backend/app/routers/admin.py +237 -0
- package/apps/travel-chaos-organizer/backend/app/routers/cache.py +13 -0
- package/apps/travel-chaos-organizer/backend/app/routers/events.py +29 -0
- package/apps/travel-chaos-organizer/backend/app/routers/export.py +122 -0
- package/apps/travel-chaos-organizer/backend/app/routers/inbox.py +122 -0
- package/apps/travel-chaos-organizer/backend/app/routers/items.py +130 -0
- package/apps/travel-chaos-organizer/backend/app/routers/mail.py +108 -0
- package/apps/travel-chaos-organizer/backend/app/routers/parse.py +303 -0
- package/apps/travel-chaos-organizer/backend/app/routers/payments.py +179 -0
- package/apps/travel-chaos-organizer/backend/app/routers/trips.py +134 -0
- package/apps/travel-chaos-organizer/backend/app/routers/users.py +169 -0
- package/apps/travel-chaos-organizer/backend/app/routers/waitlist.py +37 -0
- package/apps/travel-chaos-organizer/backend/app/services/__init__.py +0 -0
- package/apps/travel-chaos-organizer/backend/app/services/cache.py +131 -0
- package/apps/travel-chaos-organizer/backend/app/services/drip.py +128 -0
- package/apps/travel-chaos-organizer/backend/app/services/email.py +61 -0
- package/apps/travel-chaos-organizer/backend/app/services/errors.py +44 -0
- package/apps/travel-chaos-organizer/backend/app/services/notifier.py +10 -0
- package/apps/travel-chaos-organizer/backend/app/services/ollama.py +101 -0
- package/apps/travel-chaos-organizer/backend/app/services/parser.py +22 -0
- package/apps/travel-chaos-organizer/backend/app/services/quota.py +85 -0
- package/apps/travel-chaos-organizer/backend/app/services/sentry.py +11 -0
- package/apps/travel-chaos-organizer/backend/app/services/telemetry.py +35 -0
- package/apps/travel-chaos-organizer/backend/pytest.ini +4 -0
- package/apps/travel-chaos-organizer/backend/requirements-dev.txt +6 -0
- package/apps/travel-chaos-organizer/backend/requirements.txt +25 -0
- package/apps/travel-chaos-organizer/backend/tests/__init__.py +0 -0
- package/apps/travel-chaos-organizer/backend/tests/conftest.py +223 -0
- package/apps/travel-chaos-organizer/backend/tests/test_admin.py +99 -0
- package/apps/travel-chaos-organizer/backend/tests/test_cache.py +142 -0
- package/apps/travel-chaos-organizer/backend/tests/test_drip.py +32 -0
- package/apps/travel-chaos-organizer/backend/tests/test_e2e_chain.py +121 -0
- package/apps/travel-chaos-organizer/backend/tests/test_export.py +48 -0
- package/apps/travel-chaos-organizer/backend/tests/test_inbox.py +107 -0
- package/apps/travel-chaos-organizer/backend/tests/test_items.py +138 -0
- package/apps/travel-chaos-organizer/backend/tests/test_mail.py +113 -0
- package/apps/travel-chaos-organizer/backend/tests/test_ollama.py +145 -0
- package/apps/travel-chaos-organizer/backend/tests/test_parse_url.py +98 -0
- package/apps/travel-chaos-organizer/backend/tests/test_parser.py +72 -0
- package/apps/travel-chaos-organizer/backend/tests/test_payments.py +163 -0
- package/apps/travel-chaos-organizer/backend/tests/test_quota.py +46 -0
- package/apps/travel-chaos-organizer/backend/tests/test_search.py +67 -0
- package/apps/travel-chaos-organizer/backend/tests/test_trips.py +123 -0
- package/apps/travel-chaos-organizer/backend/tests/test_users.py +52 -0
- package/apps/travel-chaos-organizer/backups/.gitkeep +0 -0
- package/apps/travel-chaos-organizer/caddy/Caddyfile +27 -0
- package/apps/travel-chaos-organizer/docker-compose.node1.yml +31 -0
- package/apps/travel-chaos-organizer/docker-compose.override.yml +14 -0
- package/apps/travel-chaos-organizer/docker-compose.yml +128 -0
- package/apps/travel-chaos-organizer/frontend/.env.example +13 -0
- package/apps/travel-chaos-organizer/frontend/app/(app)/_layout.tsx +80 -0
- package/apps/travel-chaos-organizer/frontend/app/(app)/inbox.tsx +166 -0
- package/apps/travel-chaos-organizer/frontend/app/(app)/search.tsx +130 -0
- package/apps/travel-chaos-organizer/frontend/app/(app)/settings.tsx +381 -0
- package/apps/travel-chaos-organizer/frontend/app/(app)/trips/[id].tsx +346 -0
- package/apps/travel-chaos-organizer/frontend/app/(app)/trips.tsx +363 -0
- package/apps/travel-chaos-organizer/frontend/app/(app)/upgrade/cancel.tsx +34 -0
- package/apps/travel-chaos-organizer/frontend/app/(app)/upgrade/success.tsx +35 -0
- package/apps/travel-chaos-organizer/frontend/app/(auth)/login.tsx +91 -0
- package/apps/travel-chaos-organizer/frontend/app/_layout.tsx +71 -0
- package/apps/travel-chaos-organizer/frontend/app/auth.tsx +29 -0
- package/apps/travel-chaos-organizer/frontend/app/index.tsx +16 -0
- package/apps/travel-chaos-organizer/frontend/app/onboarding.tsx +119 -0
- package/apps/travel-chaos-organizer/frontend/app.config.ts +54 -0
- package/apps/travel-chaos-organizer/frontend/assets/adaptive-icon.svg +66 -0
- package/apps/travel-chaos-organizer/frontend/assets/icon.svg +79 -0
- package/apps/travel-chaos-organizer/frontend/assets/splash.svg +15 -0
- package/apps/travel-chaos-organizer/frontend/components/BottomSheet.tsx +102 -0
- package/apps/travel-chaos-organizer/frontend/components/ConfettiCannon.tsx +107 -0
- package/apps/travel-chaos-organizer/frontend/components/ConfirmDialog.tsx +109 -0
- package/apps/travel-chaos-organizer/frontend/components/ErrorBoundary.tsx +45 -0
- package/apps/travel-chaos-organizer/frontend/components/FileUploadButton.tsx +19 -0
- package/apps/travel-chaos-organizer/frontend/components/ImportSheet.tsx +155 -0
- package/apps/travel-chaos-organizer/frontend/components/OfflineBadge.tsx +55 -0
- package/apps/travel-chaos-organizer/frontend/components/QRCodeViewer.tsx +80 -0
- package/apps/travel-chaos-organizer/frontend/components/ScoreRing.tsx +39 -0
- package/apps/travel-chaos-organizer/frontend/components/Screen.tsx +39 -0
- package/apps/travel-chaos-organizer/frontend/components/Skeleton.tsx +70 -0
- package/apps/travel-chaos-organizer/frontend/components/SwipeableInboxItem.tsx +121 -0
- package/apps/travel-chaos-organizer/frontend/components/SwipeableTimelineItem.tsx +70 -0
- package/apps/travel-chaos-organizer/frontend/components/TimelineItem.tsx +73 -0
- package/apps/travel-chaos-organizer/frontend/components/TimelineItemDetail.tsx +130 -0
- package/apps/travel-chaos-organizer/frontend/components/Toast.tsx +60 -0
- package/apps/travel-chaos-organizer/frontend/components/ToastContext.tsx +42 -0
- package/apps/travel-chaos-organizer/frontend/components/TripCard.tsx +146 -0
- package/apps/travel-chaos-organizer/frontend/components/TripQRModal.tsx +70 -0
- package/apps/travel-chaos-organizer/frontend/components/UpgradeBanner.tsx +44 -0
- package/apps/travel-chaos-organizer/frontend/eas.json +22 -0
- package/apps/travel-chaos-organizer/frontend/hooks/useInbox.ts +24 -0
- package/apps/travel-chaos-organizer/frontend/hooks/useNetworkSync.ts +60 -0
- package/apps/travel-chaos-organizer/frontend/hooks/useTrips.ts +112 -0
- package/apps/travel-chaos-organizer/frontend/lib/api.ts +223 -0
- package/apps/travel-chaos-organizer/frontend/lib/auth.ts +93 -0
- package/apps/travel-chaos-organizer/frontend/lib/db.ts +86 -0
- package/apps/travel-chaos-organizer/frontend/lib/fileCache.ts +54 -0
- package/apps/travel-chaos-organizer/frontend/lib/haptics.ts +13 -0
- package/apps/travel-chaos-organizer/frontend/lib/notifications.ts +112 -0
- package/apps/travel-chaos-organizer/frontend/lib/offlineQueue.ts +145 -0
- package/apps/travel-chaos-organizer/frontend/lib/quota.tsx +43 -0
- package/apps/travel-chaos-organizer/frontend/lib/sentry.ts +28 -0
- package/apps/travel-chaos-organizer/frontend/lib/shareIntent.ts +44 -0
- package/apps/travel-chaos-organizer/frontend/package-lock.json +13221 -0
- package/apps/travel-chaos-organizer/frontend/package.json +44 -0
- package/apps/travel-chaos-organizer/frontend/tsconfig.json +23 -0
- package/apps/travel-chaos-organizer/keycloak/realm-export.json +59 -0
- package/apps/travel-chaos-organizer/landing/index.html +773 -0
- package/apps/travel-chaos-organizer/scripts/backup.sh +14 -0
- package/apps/travel-chaos-organizer/scripts/configure-env.sh +82 -0
- package/apps/travel-chaos-organizer/scripts/deploy.sh +54 -0
- package/apps/travel-chaos-organizer/scripts/health-check.sh +36 -0
- package/apps/travel-chaos-organizer/scripts/migrate.sh +7 -0
- package/apps/travel-chaos-organizer/scripts/server-setup.sh +85 -0
- package/apps/travel-chaos-organizer/scripts/smoke-test.sh +29 -0
- package/apps/travel-chaos-organizer/scripts/verify-tracking.sh +71 -0
- package/apps/travel-chaos-organizer/telegram-bot/.env.example +3 -0
- package/apps/travel-chaos-organizer/telegram-bot/Dockerfile +6 -0
- package/apps/travel-chaos-organizer/telegram-bot/bot.py +504 -0
- package/apps/travel-chaos-organizer/telegram-bot/requirements.txt +3 -0
- package/dist/packages/telegram-notify/client.d.ts +19 -0
- package/dist/packages/telegram-notify/client.js +75 -0
- package/dist/packages/telegram-notify/client.js.map +1 -0
- package/dist/src/__tests__/auth.test.js.map +1 -0
- package/dist/{__tests__ → src/__tests__}/brain-flow.test.js +141 -0
- package/dist/src/__tests__/brain-flow.test.js.map +1 -0
- package/dist/src/__tests__/confidence.test.js.map +1 -0
- package/dist/src/__tests__/e2e/api-auth.test.js.map +1 -0
- package/dist/src/__tests__/e2e/api-health.test.js.map +1 -0
- package/dist/src/__tests__/e2e/cli.test.js.map +1 -0
- package/dist/src/__tests__/e2e/config.js.map +1 -0
- package/dist/src/__tests__/e2e/funnel.test.js.map +1 -0
- package/dist/src/__tests__/e2e/mcp-tools.test.js.map +1 -0
- package/dist/src/__tests__/e2e/provisioning.test.js.map +1 -0
- package/dist/{__tests__ → src/__tests__}/handlers-unit.test.js +0 -2
- package/dist/src/__tests__/handlers-unit.test.js.map +1 -0
- package/dist/src/__tests__/namespace.test.js.map +1 -0
- package/dist/src/__tests__/rerank.test.js +115 -0
- package/dist/src/__tests__/rerank.test.js.map +1 -0
- package/dist/src/auth.js.map +1 -0
- package/dist/src/bench/cachly-bench.d.ts +35 -0
- package/dist/src/bench/cachly-bench.js +182 -0
- package/dist/src/bench/cachly-bench.js.map +1 -0
- package/dist/src/bench/fixtures.d.ts +37 -0
- package/dist/src/bench/fixtures.js +225 -0
- package/dist/src/bench/fixtures.js.map +1 -0
- package/dist/src/ckg.js.map +1 -0
- package/dist/src/confidence.js.map +1 -0
- package/dist/src/embeddings.js.map +1 -0
- package/dist/{handlers → src/handlers}/advanced.d.ts +1 -1
- package/dist/{handlers → src/handlers}/advanced.js +1 -2
- package/dist/src/handlers/advanced.js.map +1 -0
- package/dist/{handlers → src/handlers}/brain.js +361 -191
- package/dist/src/handlers/brain.js.map +1 -0
- package/dist/src/handlers/cache.js.map +1 -0
- package/dist/{handlers → src/handlers}/context.js +9 -4
- package/dist/src/handlers/context.js.map +1 -0
- package/dist/{handlers → src/handlers}/fedbrain.d.ts +6 -0
- package/dist/{handlers → src/handlers}/fedbrain.js +19 -7
- package/dist/src/handlers/fedbrain.js.map +1 -0
- package/dist/src/handlers/instances.js.map +1 -0
- package/dist/{handlers → src/handlers}/roadmap.d.ts +1 -1
- package/dist/{handlers → src/handlers}/roadmap.js +8 -4
- package/dist/src/handlers/roadmap.js.map +1 -0
- package/dist/src/handlers/syndicate.js.map +1 -0
- package/dist/src/handlers/tco.d.ts +16 -0
- package/dist/src/handlers/tco.js +303 -0
- package/dist/src/handlers/tco.js.map +1 -0
- package/dist/{handlers → src/handlers}/team.js +58 -7
- package/dist/src/handlers/team.js.map +1 -0
- package/dist/{index.js → src/index.js} +207 -429
- package/dist/src/index.js.map +1 -0
- package/dist/src/namespace.js.map +1 -0
- package/dist/src/notifier.d.ts +5 -0
- package/dist/src/notifier.js +6 -0
- package/dist/src/notifier.js.map +1 -0
- package/dist/src/rerank.d.ts +55 -0
- package/dist/src/rerank.js +111 -0
- package/dist/src/rerank.js.map +1 -0
- package/dist/{search.js → src/search.js} +2 -2
- package/dist/src/search.js.map +1 -0
- package/dist/src/search.test.d.ts +1 -0
- package/dist/{search.test.js → src/search.test.js} +1 -1
- package/dist/src/search.test.js.map +1 -0
- package/dist/{tools.d.ts → src/tools.d.ts} +226 -0
- package/dist/{tools.js → src/tools.js} +196 -0
- package/dist/src/tools.js.map +1 -0
- package/dist/src/utils.js.map +1 -0
- package/eslint.config.js +1 -1
- package/package.json +8 -7
- package/packages/telegram-notify/SPLIT_PLAN.md +106 -0
- package/packages/telegram-notify/client.ts +88 -0
- package/packages/telegram-notify/emojis.json +22 -0
- package/packages/telegram-notify/pyproject.toml +17 -0
- package/packages/telegram-notify/telegram_notify/__init__.py +3 -0
- package/packages/telegram-notify/telegram_notify/client.py +87 -0
- package/packages/telegram-notify/telegram_notify/emojis.json +22 -0
- package/scripts/generate-icons.js +28 -0
- package/server.json +3 -3
- package/smithery.yaml +27 -16
- package/src/__tests__/auth.test.ts +1 -1
- package/src/__tests__/brain-flow.test.ts +175 -1
- package/src/__tests__/e2e/api-health.test.ts +1 -1
- package/src/__tests__/handlers-unit.test.ts +1 -2
- package/src/__tests__/rerank.test.ts +130 -0
- package/src/bench/cachly-bench.ts +217 -0
- package/src/bench/fixtures.ts +256 -0
- package/src/handlers/advanced.ts +2 -6
- package/src/handlers/brain.ts +371 -194
- package/src/handlers/cache.ts +1 -2
- package/src/handlers/context.ts +10 -6
- package/src/handlers/fedbrain.ts +21 -12
- package/src/handlers/roadmap.ts +8 -4
- package/src/handlers/syndicate.ts +7 -4
- package/src/handlers/tco.ts +345 -0
- package/src/handlers/team.ts +70 -10
- package/src/index.ts +193 -419
- package/src/notifier.ts +5 -0
- package/src/rerank.ts +148 -0
- package/src/search.test.ts +1 -1
- package/src/search.ts +2 -2
- package/src/tools.ts +210 -0
- package/tsconfig.json +2 -3
- package/vitest.config.ts +2 -0
- package/dist/__tests__/auth.test.js.map +0 -1
- package/dist/__tests__/brain-flow.test.js.map +0 -1
- package/dist/__tests__/brain-predict-failures.test.d.ts +0 -17
- package/dist/__tests__/brain-predict-failures.test.js +0 -339
- package/dist/__tests__/brain-predict-failures.test.js.map +0 -1
- package/dist/__tests__/confidence.test.js.map +0 -1
- package/dist/__tests__/e2e/api-auth.test.js.map +0 -1
- package/dist/__tests__/e2e/api-health.test.js.map +0 -1
- package/dist/__tests__/e2e/cli.test.js.map +0 -1
- package/dist/__tests__/e2e/config.js.map +0 -1
- package/dist/__tests__/e2e/funnel.test.js.map +0 -1
- package/dist/__tests__/e2e/mcp-tools.test.js.map +0 -1
- package/dist/__tests__/e2e/provisioning.test.js.map +0 -1
- package/dist/__tests__/handlers-unit.test.js.map +0 -1
- package/dist/__tests__/namespace.test.js.map +0 -1
- package/dist/auth.js.map +0 -1
- package/dist/ckg.js.map +0 -1
- package/dist/confidence.js.map +0 -1
- package/dist/embeddings.js.map +0 -1
- package/dist/handlers/advanced.js.map +0 -1
- package/dist/handlers/brain.js.map +0 -1
- package/dist/handlers/cache.js.map +0 -1
- package/dist/handlers/context.js.map +0 -1
- package/dist/handlers/fedbrain.js.map +0 -1
- package/dist/handlers/instances.js.map +0 -1
- package/dist/handlers/roadmap.js.map +0 -1
- package/dist/handlers/syndicate.js.map +0 -1
- package/dist/handlers/team.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/namespace.js.map +0 -1
- package/dist/search.js.map +0 -1
- package/dist/search.test.js.map +0 -1
- package/dist/tools.js.map +0 -1
- package/dist/utils.js.map +0 -1
- package/src/__tests__/brain-predict-failures.test.ts +0 -430
- /package/dist/{__tests__ → src/__tests__}/auth.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/auth.test.js +0 -0
- /package/dist/{__tests__ → src/__tests__}/brain-flow.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/confidence.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/confidence.test.js +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/api-auth.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/api-auth.test.js +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/api-health.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/api-health.test.js +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/cli.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/cli.test.js +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/config.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/config.js +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/funnel.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/funnel.test.js +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/mcp-tools.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/mcp-tools.test.js +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/provisioning.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/provisioning.test.js +0 -0
- /package/dist/{__tests__ → src/__tests__}/handlers-unit.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/namespace.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/namespace.test.js +0 -0
- /package/dist/{search.test.d.ts → src/__tests__/rerank.test.d.ts} +0 -0
- /package/dist/{auth.d.ts → src/auth.d.ts} +0 -0
- /package/dist/{auth.js → src/auth.js} +0 -0
- /package/dist/{ckg.d.ts → src/ckg.d.ts} +0 -0
- /package/dist/{ckg.js → src/ckg.js} +0 -0
- /package/dist/{confidence.d.ts → src/confidence.d.ts} +0 -0
- /package/dist/{confidence.js → src/confidence.js} +0 -0
- /package/dist/{embeddings.d.ts → src/embeddings.d.ts} +0 -0
- /package/dist/{embeddings.js → src/embeddings.js} +0 -0
- /package/dist/{handlers → src/handlers}/brain.d.ts +0 -0
- /package/dist/{handlers → src/handlers}/cache.d.ts +0 -0
- /package/dist/{handlers → src/handlers}/cache.js +0 -0
- /package/dist/{handlers → src/handlers}/context.d.ts +0 -0
- /package/dist/{handlers → src/handlers}/instances.d.ts +0 -0
- /package/dist/{handlers → src/handlers}/instances.js +0 -0
- /package/dist/{handlers → src/handlers}/syndicate.d.ts +0 -0
- /package/dist/{handlers → src/handlers}/syndicate.js +0 -0
- /package/dist/{handlers → src/handlers}/team.d.ts +0 -0
- /package/dist/{index.d.ts → src/index.d.ts} +0 -0
- /package/dist/{namespace.d.ts → src/namespace.d.ts} +0 -0
- /package/dist/{namespace.js → src/namespace.js} +0 -0
- /package/dist/{search.d.ts → src/search.d.ts} +0 -0
- /package/dist/{utils.d.ts → src/utils.d.ts} +0 -0
- /package/dist/{utils.js → src/utils.js} +0 -0
package/src/index.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { jwtExpiryMs, checkJwt, handleApiError } from './auth.js';
|
|
3
|
+
import { handleTcoTool } from './handlers/tco.js';
|
|
4
|
+
import { notify } from './notifier.js';
|
|
3
5
|
import { readFile } from 'node:fs/promises';
|
|
4
6
|
import { fileURLToPath } from 'node:url';
|
|
5
7
|
|
|
@@ -35,11 +37,24 @@ const _isMain = process.argv[1] != null &&
|
|
|
35
37
|
* ── Auth & Status ────────────────────────────────────────────────────────────
|
|
36
38
|
* • get_api_status – check API health + JWT auth info (Keycloak)
|
|
37
39
|
*
|
|
40
|
+
* ── Travel Chaos Organizer (TCO) ────────────────────────────────────────────
|
|
41
|
+
* • tco_list_trips – list all trips
|
|
42
|
+
* • tco_create_trip – create a new trip
|
|
43
|
+
* • tco_get_timeline – get timeline items for a trip
|
|
44
|
+
* • tco_delete_trip – delete a trip
|
|
45
|
+
* • tco_inbox_list – list Chaos Inbox items
|
|
46
|
+
* • tco_inbox_assign – assign inbox item to a trip
|
|
47
|
+
* • tco_inbox_reject – reject/discard an inbox item
|
|
48
|
+
* • tco_parse_url – fetch a URL and parse with Ollama
|
|
49
|
+
* • tco_import_email – import raw email text
|
|
50
|
+
* (Requires TCO_API_URL env var; auth forwarded from CACHLY_JWT — same Keycloak realm)
|
|
51
|
+
*
|
|
38
52
|
* Configuration (env vars):
|
|
39
53
|
* CACHLY_API_URL – default https://api.cachly.dev
|
|
40
54
|
* CACHLY_JWT – your JWT (Keycloak access token)
|
|
41
55
|
* CACHLY_EMBED_PROVIDER – embedding backend: openai (default), gemini, mistral, cohere, ollama, cachly (server fallback)
|
|
42
56
|
* CACHLY_EMBED_MODEL – override embedding model (optional)
|
|
57
|
+
* TCO_API_URL – Travel Chaos Organizer backend URL (optional, enables tco_* tools)
|
|
43
58
|
*/
|
|
44
59
|
|
|
45
60
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
@@ -52,22 +67,19 @@ import {
|
|
|
52
67
|
McpError,
|
|
53
68
|
} from '@modelcontextprotocol/sdk/types.js';
|
|
54
69
|
import { Redis } from 'ioredis';
|
|
55
|
-
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
56
70
|
|
|
57
71
|
// ── Config ────────────────────────────────────────────────────────────────────
|
|
58
72
|
|
|
59
73
|
const API_URL = process.env.CACHLY_API_URL ?? 'https://api.cachly.dev';
|
|
60
74
|
let JWT = process.env.CACHLY_JWT ?? '';
|
|
61
|
-
const
|
|
75
|
+
const _EMBED_MODEL = process.env.CACHLY_EMBED_MODEL ?? '';
|
|
76
|
+
const CURRENT_VERSION = '0.10.58';
|
|
62
77
|
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
const reqCtx = new AsyncLocalStorage<ReqCtx>();
|
|
69
|
-
function getJWT(): string { return reqCtx.getStore()?.jwt || JWT; }
|
|
70
|
-
function getDefaultInstanceId(): string { return reqCtx.getStore()?.instanceId || _defaultInstanceId; }
|
|
78
|
+
// Max time to wait for a freshly-provisioned instance to become "running" before
|
|
79
|
+
// giving up. Free-tier provisioning in high-latency regions can take 45–90s, so the
|
|
80
|
+
// old hard-coded 25s caused spurious "instance_not_reachable" on first run.
|
|
81
|
+
// Override via CACHLY_PROVISION_TIMEOUT_MS.
|
|
82
|
+
const PROVISION_TIMEOUT_MS = Number(process.env.CACHLY_PROVISION_TIMEOUT_MS ?? 90_000);
|
|
71
83
|
|
|
72
84
|
// ── Default Instance Resolution (for Smithery & single-credential setups) ────
|
|
73
85
|
// When CACHLY_BRAIN_INSTANCE_ID is set, tools can omit the instance_id parameter.
|
|
@@ -77,28 +89,15 @@ let _defaultInstanceId: string = process.env.CACHLY_BRAIN_INSTANCE_ID ?? '';
|
|
|
77
89
|
let _defaultInstanceLastAttempt = 0;
|
|
78
90
|
|
|
79
91
|
async function resolveDefaultInstanceId(): Promise<string> {
|
|
80
|
-
// In an HTTP request scope the instance ID is fixed by the caller's header.
|
|
81
|
-
const scopedId = reqCtx.getStore()?.instanceId;
|
|
82
|
-
if (scopedId) return scopedId;
|
|
83
92
|
if (_defaultInstanceId) return _defaultInstanceId;
|
|
84
|
-
if (!
|
|
93
|
+
if (!JWT) return '';
|
|
85
94
|
// Cooldown: don't hammer the API on every tool call after a transient failure.
|
|
86
|
-
// 10 s is enough to avoid rate-limit spam while not blocking the user for too long.
|
|
87
95
|
const now = Date.now();
|
|
88
|
-
if (_defaultInstanceLastAttempt > 0 && now - _defaultInstanceLastAttempt <
|
|
89
|
-
throw new McpError(
|
|
90
|
-
ErrorCode.InvalidRequest,
|
|
91
|
-
'⚠️ Cachly Brain is temporarily unreachable (API error a few seconds ago).\n\n' +
|
|
92
|
-
'The server will retry automatically in a moment. If this persists:\n' +
|
|
93
|
-
'• Run `get_api_status` for a full diagnostic\n' +
|
|
94
|
-
'• Check that CACHLY_JWT is set and valid\n' +
|
|
95
|
-
'• Visit https://cachly.dev/instances to verify your Brain is running'
|
|
96
|
-
);
|
|
97
|
-
};
|
|
96
|
+
if (_defaultInstanceLastAttempt > 0 && now - _defaultInstanceLastAttempt < 30_000) return '';
|
|
98
97
|
_defaultInstanceLastAttempt = now;
|
|
99
98
|
try {
|
|
100
99
|
const res = await fetch(`${API_URL}/api/v1/instances`, {
|
|
101
|
-
headers: { Authorization: `Bearer ${
|
|
100
|
+
headers: { Authorization: `Bearer ${JWT}`, Accept: 'application/json' },
|
|
102
101
|
signal: AbortSignal.timeout(5000),
|
|
103
102
|
});
|
|
104
103
|
if (!res.ok) return '';
|
|
@@ -185,28 +184,38 @@ async function pollDeviceFlow(flow: DeviceFlowState): Promise<'pending' | 'expir
|
|
|
185
184
|
} catch { /* use raw JWT as fallback */ }
|
|
186
185
|
JWT = apiKey;
|
|
187
186
|
setEmbedJwt(apiKey); // keep embeddings.ts in sync
|
|
188
|
-
// Persist API key to ~/.claude/mcp.json so the next editor restart
|
|
189
|
-
// doesn't start over from a missing CACHLY_JWT (fixes BrainRecallCount=0 bug).
|
|
190
|
-
try {
|
|
191
|
-
const { resolve: _res, dirname: _dir } = await import('node:path');
|
|
192
|
-
const { homedir: _hd } = await import('node:os');
|
|
193
|
-
const { writeFile: _wf, mkdir: _mk, readFile: _rf } = await import('node:fs/promises');
|
|
194
|
-
const { existsSync: _ex } = await import('node:fs');
|
|
195
|
-
const globalJwtPath = _res(_hd(), '.claude', 'mcp.json');
|
|
196
|
-
await _mk(_dir(globalJwtPath), { recursive: true });
|
|
197
|
-
let cfg: { mcpServers?: Record<string, { env?: Record<string, string> }> } = {};
|
|
198
|
-
if (_ex(globalJwtPath)) {
|
|
199
|
-
try { cfg = JSON.parse(await _rf(globalJwtPath, 'utf-8')) as typeof cfg; } catch { /* corrupt — start fresh */ }
|
|
200
|
-
}
|
|
201
|
-
cfg.mcpServers ??= {};
|
|
202
|
-
cfg.mcpServers['cachly'] ??= {};
|
|
203
|
-
cfg.mcpServers['cachly'].env ??= {};
|
|
204
|
-
cfg.mcpServers['cachly'].env['CACHLY_JWT'] = apiKey;
|
|
205
|
-
if (_defaultInstanceId) cfg.mcpServers['cachly'].env['CACHLY_BRAIN_INSTANCE_ID'] = _defaultInstanceId;
|
|
206
|
-
await _wf(globalJwtPath, JSON.stringify(cfg, null, 2), 'utf-8');
|
|
207
|
-
} catch { /* non-fatal — editor will just prompt again on next restart */ }
|
|
208
187
|
_deviceFlow = null;
|
|
209
188
|
sendFunnelEvent('device_flow_completed');
|
|
189
|
+
// Persist the API key to ~/.claude/mcp.json so subsequent MCP server restarts
|
|
190
|
+
// keep the token. Without this, JWT is lost on restart and brain_recall_count
|
|
191
|
+
// is never incremented (telemetry has no api_key → no tenant resolution).
|
|
192
|
+
void (async () => {
|
|
193
|
+
try {
|
|
194
|
+
const { writeFile, mkdir, readFile } = await import('node:fs/promises');
|
|
195
|
+
const { existsSync } = await import('node:fs');
|
|
196
|
+
const { resolve, dirname } = await import('node:path');
|
|
197
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? '';
|
|
198
|
+
if (!home) return;
|
|
199
|
+
const configPath = resolve(home, '.claude', 'mcp.json');
|
|
200
|
+
let cfg: { mcpServers?: Record<string, { command?: string; args?: string[]; env?: Record<string, string> }> } = {};
|
|
201
|
+
if (existsSync(configPath)) {
|
|
202
|
+
try { cfg = JSON.parse(await readFile(configPath, 'utf-8')) as typeof cfg; } catch { /* corrupt — start fresh */ }
|
|
203
|
+
}
|
|
204
|
+
cfg.mcpServers ??= {};
|
|
205
|
+
const existing = cfg.mcpServers['cachly'];
|
|
206
|
+
if (existing) {
|
|
207
|
+
existing.env ??= {};
|
|
208
|
+
existing.env['CACHLY_JWT'] = apiKey;
|
|
209
|
+
} else {
|
|
210
|
+
cfg.mcpServers['cachly'] = {
|
|
211
|
+
command: 'npx', args: ['-y', '@cachly-dev/mcp-server@latest'],
|
|
212
|
+
env: { CACHLY_JWT: apiKey },
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
await mkdir(dirname(configPath), { recursive: true });
|
|
216
|
+
await writeFile(configPath, JSON.stringify(cfg, null, 2), 'utf-8');
|
|
217
|
+
} catch { /* non-critical — don't break the flow on filesystem errors */ }
|
|
218
|
+
})();
|
|
210
219
|
// Auto-provision: find or create the user's brain instance.
|
|
211
220
|
_defaultInstanceLastAttempt = 0;
|
|
212
221
|
await resolveDefaultInstanceId();
|
|
@@ -214,7 +223,7 @@ async function pollDeviceFlow(flow: DeviceFlowState): Promise<'pending' | 'expir
|
|
|
214
223
|
try {
|
|
215
224
|
const autoRes = await fetch(`${API_URL}/api/v1/instances/auto`, {
|
|
216
225
|
method: 'POST',
|
|
217
|
-
headers: { Authorization: `Bearer ${
|
|
226
|
+
headers: { Authorization: `Bearer ${JWT}`, 'Content-Type': 'application/json' },
|
|
218
227
|
signal: AbortSignal.timeout(12000),
|
|
219
228
|
});
|
|
220
229
|
if (autoRes.ok) {
|
|
@@ -229,7 +238,7 @@ async function pollDeviceFlow(flow: DeviceFlowState): Promise<'pending' | 'expir
|
|
|
229
238
|
// Log so telemetry can detect free-tier auto-provision failures
|
|
230
239
|
void fetch(`${API_URL}/api/v1/telemetry/mcp`, {
|
|
231
240
|
method: 'POST',
|
|
232
|
-
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${
|
|
241
|
+
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${JWT}` },
|
|
233
242
|
body: JSON.stringify({ event: 'auto_provision_failed', status: autoRes.status, version: CURRENT_VERSION }),
|
|
234
243
|
signal: AbortSignal.timeout(3000),
|
|
235
244
|
}).catch(() => {});
|
|
@@ -241,7 +250,7 @@ async function pollDeviceFlow(flow: DeviceFlowState): Promise<'pending' | 'expir
|
|
|
241
250
|
if (_defaultInstanceId) {
|
|
242
251
|
try {
|
|
243
252
|
const checkRes = await fetch(`${API_URL}/api/v1/instances/${_defaultInstanceId}`, {
|
|
244
|
-
headers: { Authorization: `Bearer ${
|
|
253
|
+
headers: { Authorization: `Bearer ${JWT}`, Accept: 'application/json' },
|
|
245
254
|
signal: AbortSignal.timeout(4000),
|
|
246
255
|
});
|
|
247
256
|
if (checkRes.ok) {
|
|
@@ -252,7 +261,7 @@ async function pollDeviceFlow(flow: DeviceFlowState): Promise<'pending' | 'expir
|
|
|
252
261
|
while (checkInst.status === 'provisioning' && Date.now() < pDeadline) {
|
|
253
262
|
await new Promise(r => setTimeout(r, 3000));
|
|
254
263
|
const rr = await fetch(`${API_URL}/api/v1/instances/${_defaultInstanceId}`, {
|
|
255
|
-
headers: { Authorization: `Bearer ${
|
|
264
|
+
headers: { Authorization: `Bearer ${JWT}`, Accept: 'application/json' },
|
|
256
265
|
signal: AbortSignal.timeout(4000),
|
|
257
266
|
}).catch(() => null);
|
|
258
267
|
if (rr?.ok) {
|
|
@@ -264,60 +273,35 @@ async function pollDeviceFlow(flow: DeviceFlowState): Promise<'pending' | 'expir
|
|
|
264
273
|
}
|
|
265
274
|
} catch { /* non-fatal — getConnection will handle the wait if still provisioning */ }
|
|
266
275
|
}
|
|
276
|
+
// Persist instance_id to ~/.claude/mcp.json after it's known.
|
|
277
|
+
// Without this, every restart loses CACHLY_BRAIN_INSTANCE_ID → resolveDefaultInstanceId
|
|
278
|
+
// makes an extra API call on every startup (with 30s cooldown on failure).
|
|
279
|
+
if (_defaultInstanceId) {
|
|
280
|
+
void (async () => {
|
|
281
|
+
try {
|
|
282
|
+
const { writeFile, readFile } = await import('node:fs/promises');
|
|
283
|
+
const { existsSync } = await import('node:fs');
|
|
284
|
+
const { resolve } = await import('node:path');
|
|
285
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? '';
|
|
286
|
+
if (!home) return;
|
|
287
|
+
const configPath = resolve(home, '.claude', 'mcp.json');
|
|
288
|
+
if (!existsSync(configPath)) return;
|
|
289
|
+
let cfg: { mcpServers?: Record<string, { env?: Record<string, string> }> } = {};
|
|
290
|
+
try { cfg = JSON.parse(await readFile(configPath, 'utf-8')) as typeof cfg; } catch { return; }
|
|
291
|
+
const entry = cfg.mcpServers?.['cachly'];
|
|
292
|
+
if (entry?.env) {
|
|
293
|
+
entry.env['CACHLY_BRAIN_INSTANCE_ID'] = _defaultInstanceId;
|
|
294
|
+
await writeFile(configPath, JSON.stringify(cfg, null, 2), 'utf-8');
|
|
295
|
+
}
|
|
296
|
+
} catch { /* non-critical */ }
|
|
297
|
+
})();
|
|
298
|
+
}
|
|
267
299
|
return 'done';
|
|
268
300
|
}
|
|
269
301
|
if (data.error === 'slow_down') flow.pollInterval = Math.min(flow.pollInterval + 2000, 15000);
|
|
270
302
|
return 'pending';
|
|
271
303
|
} catch { return 'pending'; }
|
|
272
304
|
}
|
|
273
|
-
// ── Silent Auto-Setup (instant-trial, zero-click) ─────────────────────────
|
|
274
|
-
// Attempts to provision a free trial silently on the first tool call when no
|
|
275
|
-
// JWT is set. Returns true if credentials were written and the tool can
|
|
276
|
-
// proceed transparently; false if the API is unreachable or trials are full.
|
|
277
|
-
// Only runs once per process (subsequent failures short-circuit immediately).
|
|
278
|
-
let _silentSetupAttempted = false;
|
|
279
|
-
async function silentAutoSetup(): Promise<boolean> {
|
|
280
|
-
if (_silentSetupAttempted) return false;
|
|
281
|
-
_silentSetupAttempted = true;
|
|
282
|
-
try {
|
|
283
|
-
type TrialResp = { api_key: string; expires_at?: string; instance_id?: string; trial?: boolean };
|
|
284
|
-
const res = await fetch(`${API_URL}/auth/instant-trial`, {
|
|
285
|
-
method: 'POST',
|
|
286
|
-
headers: { 'Content-Type': 'application/json' },
|
|
287
|
-
body: JSON.stringify({}),
|
|
288
|
-
signal: AbortSignal.timeout(8000),
|
|
289
|
-
});
|
|
290
|
-
if (!res.ok) return false;
|
|
291
|
-
const data = await res.json() as TrialResp;
|
|
292
|
-
if (!data.api_key) return false;
|
|
293
|
-
|
|
294
|
-
JWT = data.api_key;
|
|
295
|
-
setEmbedJwt(data.api_key);
|
|
296
|
-
if (data.instance_id) _defaultInstanceId = data.instance_id;
|
|
297
|
-
sendFunnelEvent('trial_provisioned', { version: CURRENT_VERSION });
|
|
298
|
-
|
|
299
|
-
// Persist credentials so the next editor restart doesn't re-provision.
|
|
300
|
-
try {
|
|
301
|
-
const { resolve: _rp, dirname: _dn } = await import('node:path');
|
|
302
|
-
const { homedir: _hd } = await import('node:os');
|
|
303
|
-
const { writeFile: _wf, mkdir: _mk, readFile: _rf } = await import('node:fs/promises');
|
|
304
|
-
const { existsSync: _ex } = await import('node:fs');
|
|
305
|
-
const cfgPath = _rp(_hd(), '.claude', 'mcp.json');
|
|
306
|
-
await _mk(_dn(cfgPath), { recursive: true });
|
|
307
|
-
let cfg: { mcpServers?: Record<string, { env?: Record<string, string> }> } = {};
|
|
308
|
-
if (_ex(cfgPath)) {
|
|
309
|
-
try { cfg = JSON.parse(await _rf(cfgPath, 'utf-8')) as typeof cfg; } catch { /* corrupt — overwrite */ }
|
|
310
|
-
}
|
|
311
|
-
cfg.mcpServers ??= {};
|
|
312
|
-
cfg.mcpServers['cachly'] ??= {};
|
|
313
|
-
cfg.mcpServers['cachly'].env ??= {};
|
|
314
|
-
cfg.mcpServers['cachly'].env['CACHLY_JWT'] = data.api_key;
|
|
315
|
-
if (_defaultInstanceId) cfg.mcpServers['cachly'].env['CACHLY_BRAIN_INSTANCE_ID'] = _defaultInstanceId;
|
|
316
|
-
await _wf(cfgPath, JSON.stringify(cfg, null, 2), 'utf-8');
|
|
317
|
-
} catch { /* non-fatal — trial is active for this session even if persist fails */ }
|
|
318
|
-
return true;
|
|
319
|
-
} catch { return false; }
|
|
320
|
-
}
|
|
321
305
|
// ── Embeddings (imported from embeddings.ts) ────────────────────────────────
|
|
322
306
|
import { setEmbedJwt } from './embeddings.js';
|
|
323
307
|
|
|
@@ -330,9 +314,6 @@ import { tokenize, splitMultiQuery, levenshtein, recencyBoost, extractTimestamp,
|
|
|
330
314
|
export { tokenize, splitMultiQuery, levenshtein, recencyBoost, extractTimestamp, STOPWORDS,
|
|
331
315
|
katakanaToRomaji, arabicLightStem, expandCrossLingual, CROSS_LINGUAL_MAP };
|
|
332
316
|
|
|
333
|
-
// ── Types ─────────────────────────────────────────────────────────────────────
|
|
334
|
-
// Instance is imported from handlers/brain.ts
|
|
335
|
-
|
|
336
317
|
// ── Connection pool ───────────────────────────────────────────────────────────
|
|
337
318
|
|
|
338
319
|
/** Reuse Redis connections across tool calls (keyed by instance_id). */
|
|
@@ -349,27 +330,16 @@ async function getConnection(instance_id: string): Promise<Redis> {
|
|
|
349
330
|
);
|
|
350
331
|
}
|
|
351
332
|
|
|
352
|
-
if (pool.has(instance_id))
|
|
353
|
-
const cached = pool.get(instance_id)!;
|
|
354
|
-
// Verify the pooled connection is still alive before reusing it.
|
|
355
|
-
// ioredis 'error' eviction is async — a connection can be in the pool
|
|
356
|
-
// but already internally broken (status 'reconnecting' or 'end').
|
|
357
|
-
const status = (cached as { status?: string }).status;
|
|
358
|
-
if (status === 'ready' || status === 'connect') {
|
|
359
|
-
return cached;
|
|
360
|
-
}
|
|
361
|
-
// Stale — evict and fall through to reconnect.
|
|
362
|
-
pool.delete(instance_id);
|
|
363
|
-
cached.disconnect();
|
|
364
|
-
}
|
|
333
|
+
if (pool.has(instance_id)) return pool.get(instance_id)!;
|
|
365
334
|
|
|
366
|
-
// Fetch instance, waiting up to
|
|
367
|
-
//
|
|
335
|
+
// Fetch instance, waiting up to PROVISION_TIMEOUT_MS if it is still provisioning.
|
|
336
|
+
// This covers the zero-friction path: device-flow auth → auto-provision → first tool call
|
|
337
|
+
// all happen in quick succession and the instance isn't running yet.
|
|
368
338
|
let inst = await apiFetch<Instance>(`/api/v1/instances/${instance_id}`);
|
|
369
339
|
if (inst.status === 'provisioning') {
|
|
370
|
-
const deadline = Date.now() +
|
|
340
|
+
const deadline = Date.now() + PROVISION_TIMEOUT_MS;
|
|
371
341
|
while (inst.status === 'provisioning' && Date.now() < deadline) {
|
|
372
|
-
await new Promise(r => setTimeout(r,
|
|
342
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
373
343
|
inst = await apiFetch<Instance>(`/api/v1/instances/${instance_id}`);
|
|
374
344
|
}
|
|
375
345
|
}
|
|
@@ -378,7 +348,7 @@ async function getConnection(instance_id: string): Promise<Redis> {
|
|
|
378
348
|
// Telemetry: unreachable instance so the team can proactively investigate
|
|
379
349
|
void fetch(`${API_URL}/api/v1/telemetry/mcp`, {
|
|
380
350
|
method: 'POST',
|
|
381
|
-
headers: { 'Content-Type': 'application/json', ...(
|
|
351
|
+
headers: { 'Content-Type': 'application/json', ...(JWT ? { Authorization: `Bearer ${JWT}` } : {}) },
|
|
382
352
|
body: JSON.stringify({ event: 'instance_not_reachable', instance_id, status: inst.status, version: CURRENT_VERSION }),
|
|
383
353
|
signal: AbortSignal.timeout(3000),
|
|
384
354
|
}).catch(() => {});
|
|
@@ -409,9 +379,7 @@ async function getConnection(instance_id: string): Promise<Redis> {
|
|
|
409
379
|
|
|
410
380
|
// Fetch connection details (includes password) from the dedicated endpoint.
|
|
411
381
|
let password: string | undefined;
|
|
412
|
-
|
|
413
|
-
// Only enable TLS when the API explicitly returns tls_enabled: true.
|
|
414
|
-
let tlsEnabled = inst.tls_enabled === true;
|
|
382
|
+
let tlsEnabled = inst.tls_enabled !== false; // default true
|
|
415
383
|
try {
|
|
416
384
|
const conn = await apiFetch<{ password?: string; tls_enabled?: boolean }>(
|
|
417
385
|
`/api/v1/instances/${instance_id}/connection`
|
|
@@ -433,11 +401,11 @@ async function getConnection(instance_id: string): Promise<Redis> {
|
|
|
433
401
|
host: inst.host,
|
|
434
402
|
port: inst.port,
|
|
435
403
|
password: password || undefined,
|
|
404
|
+
commandTimeout: 8000,
|
|
436
405
|
...(tlsEnabled ? { tls: {} } : {}),
|
|
437
406
|
lazyConnect: true,
|
|
438
407
|
enableReadyCheck: true,
|
|
439
408
|
connectTimeout: 8000,
|
|
440
|
-
commandTimeout: 10000,
|
|
441
409
|
retryStrategy: () => null, // fail fast, no reconnect loops in MCP context
|
|
442
410
|
});
|
|
443
411
|
|
|
@@ -445,46 +413,14 @@ async function getConnection(instance_id: string): Promise<Redis> {
|
|
|
445
413
|
client.on('error', evict);
|
|
446
414
|
client.on('end', evict);
|
|
447
415
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
for (let attempt = 0; attempt < 3; attempt++) {
|
|
452
|
-
try {
|
|
453
|
-
if (attempt > 0) {
|
|
454
|
-
await new Promise(r => setTimeout(r, 2000 * attempt));
|
|
455
|
-
// Fresh client on retry — ioredis doesn't allow reconnect after explicit disconnect.
|
|
456
|
-
client.disconnect();
|
|
457
|
-
const retryClient = new Redis({
|
|
458
|
-
host: inst.host,
|
|
459
|
-
port: inst.port,
|
|
460
|
-
password: password || undefined,
|
|
461
|
-
...(tlsEnabled ? { tls: {} } : {}),
|
|
462
|
-
lazyConnect: true,
|
|
463
|
-
enableReadyCheck: true,
|
|
464
|
-
connectTimeout: 8000,
|
|
465
|
-
commandTimeout: 10000,
|
|
466
|
-
retryStrategy: () => null,
|
|
467
|
-
});
|
|
468
|
-
retryClient.on('error', () => pool.delete(instance_id));
|
|
469
|
-
retryClient.on('end', () => pool.delete(instance_id));
|
|
470
|
-
await retryClient.connect();
|
|
471
|
-
pool.set(instance_id, retryClient);
|
|
472
|
-
return retryClient;
|
|
473
|
-
}
|
|
474
|
-
await client.connect();
|
|
475
|
-
connectErr = undefined;
|
|
476
|
-
break;
|
|
477
|
-
} catch (err) {
|
|
478
|
-
connectErr = err;
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
if (connectErr) {
|
|
416
|
+
try {
|
|
417
|
+
await client.connect();
|
|
418
|
+
} catch (err: unknown) {
|
|
483
419
|
client.disconnect();
|
|
484
|
-
const msg =
|
|
420
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
485
421
|
void fetch(`${API_URL}/api/v1/telemetry/mcp`, {
|
|
486
422
|
method: 'POST',
|
|
487
|
-
headers: { 'Content-Type': 'application/json', ...(
|
|
423
|
+
headers: { 'Content-Type': 'application/json', ...(JWT ? { Authorization: `Bearer ${JWT}` } : {}) },
|
|
488
424
|
body: JSON.stringify({ event: 'redis_connect_failed', instance_id, host: inst.host, port: inst.port, error: msg, version: CURRENT_VERSION }),
|
|
489
425
|
signal: AbortSignal.timeout(3000),
|
|
490
426
|
}).catch(() => {});
|
|
@@ -504,14 +440,14 @@ async function getConnection(instance_id: string): Promise<Redis> {
|
|
|
504
440
|
// ── API helper ────────────────────────────────────────────────────────────────
|
|
505
441
|
|
|
506
442
|
async function apiFetch<T>(path: string, options: RequestInit = {}): Promise<T> {
|
|
507
|
-
checkJwt(
|
|
443
|
+
checkJwt(JWT);
|
|
508
444
|
const timeoutMs = Number(process.env.CACHLY_API_TIMEOUT_MS ?? 15_000);
|
|
509
445
|
const res = await fetch(`${API_URL}${path}`, {
|
|
510
446
|
...options,
|
|
511
447
|
signal: AbortSignal.timeout(timeoutMs),
|
|
512
448
|
headers: {
|
|
513
449
|
'Content-Type': 'application/json',
|
|
514
|
-
Authorization: `Bearer ${
|
|
450
|
+
Authorization: `Bearer ${JWT}`,
|
|
515
451
|
...options.headers,
|
|
516
452
|
},
|
|
517
453
|
});
|
|
@@ -532,7 +468,7 @@ import { handleTeamTool } from './handlers/team.js';
|
|
|
532
468
|
import { handleRoadmapTool } from './handlers/roadmap.js';
|
|
533
469
|
import { handleAdvancedTool } from './handlers/advanced.js';
|
|
534
470
|
import { handleSyndicateTool } from './handlers/syndicate.js';
|
|
535
|
-
import { handleFedbrainTool } from './handlers/fedbrain.js';
|
|
471
|
+
import { handleFedbrainTool, _lastBrainFromGitCounts } from './handlers/fedbrain.js';
|
|
536
472
|
import type { Instance } from './handlers/brain.js';
|
|
537
473
|
|
|
538
474
|
// ── Tools (imported from tools.ts) ─────────────────────────────────────────
|
|
@@ -561,10 +497,7 @@ function sendFunnelEvent(event: string, extra?: Record<string, unknown>): void {
|
|
|
561
497
|
headers: { 'Content-Type': 'application/json' },
|
|
562
498
|
body: JSON.stringify({
|
|
563
499
|
event, version: CURRENT_VERSION, editor: detectEditor(),
|
|
564
|
-
|
|
565
|
-
// Raw Keycloak JWTs (eyJ...) are intentionally excluded — they are short-lived bearer tokens
|
|
566
|
-
// that should never leave the MCP process boundary.
|
|
567
|
-
...(getJWT().startsWith('cky_') ? { api_key: getJWT() } : {}),
|
|
500
|
+
...(JWT ? { jwt: JWT } : {}),
|
|
568
501
|
...extra,
|
|
569
502
|
}),
|
|
570
503
|
signal: AbortSignal.timeout(3000),
|
|
@@ -586,20 +519,12 @@ async function sendAnonymousTelemetry(toolName: string): Promise<void> {
|
|
|
586
519
|
}
|
|
587
520
|
|
|
588
521
|
async function handleTool(name: string, args: Record<string, unknown>): Promise<string> {
|
|
589
|
-
// Guard: if no JWT,
|
|
590
|
-
if (!
|
|
591
|
-
// ── Step 1: Silent instant-trial (zero-click) ────────────────────────
|
|
592
|
-
// On the very first tool call, attempt to silently provision a free trial.
|
|
593
|
-
// If it succeeds the user never sees an auth prompt — Brain is just active.
|
|
594
|
-
const provisioned = await silentAutoSetup();
|
|
595
|
-
if (provisioned) {
|
|
596
|
-
return handleTool(name, args); // Transparent re-entry with active credentials
|
|
597
|
-
}
|
|
598
|
-
|
|
522
|
+
// Guard: if no JWT, return actionable onboarding message instead of HTTP 401
|
|
523
|
+
if (!JWT) {
|
|
599
524
|
void sendAnonymousTelemetry(name);
|
|
600
525
|
|
|
601
|
-
// ──
|
|
602
|
-
// 1st call
|
|
526
|
+
// ── Zero-credential device flow ─────────────────────────────────────
|
|
527
|
+
// 1st call: start device flow, return code + URL
|
|
603
528
|
// 2nd+ calls: poll for token; once authenticated, proceed transparently
|
|
604
529
|
if (_deviceFlow) {
|
|
605
530
|
const result = await pollDeviceFlow(_deviceFlow);
|
|
@@ -647,7 +572,7 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
|
|
|
647
572
|
'',
|
|
648
573
|
'After sign-in: call **any tool again** — your Brain activates instantly.',
|
|
649
574
|
'',
|
|
650
|
-
'✨ Free forever · No credit card ·
|
|
575
|
+
'✨ Free forever · No credit card · 95 MCP tools · GDPR · EU servers',
|
|
651
576
|
].join('\n');
|
|
652
577
|
}
|
|
653
578
|
|
|
@@ -673,16 +598,16 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
|
|
|
673
598
|
}
|
|
674
599
|
|
|
675
600
|
// Delegate brain tools (learn, recall, session, etc.)
|
|
676
|
-
|
|
601
|
+
const brainResult = await handleBrainTool(name, args, getConnection, apiFetch);
|
|
677
602
|
if (brainResult !== null) {
|
|
678
|
-
if (!_firstCallSuccessSent &&
|
|
603
|
+
if (!_firstCallSuccessSent && JWT) {
|
|
679
604
|
_firstCallSuccessSent = true;
|
|
680
|
-
sendFunnelEvent('first_call_success', { tool: name, instance_id: args.instance_id ??
|
|
605
|
+
sendFunnelEvent('first_call_success', { tool: name, instance_id: args.instance_id ?? _defaultInstanceId ?? '' });
|
|
681
606
|
}
|
|
682
607
|
// Per-tool telemetry — fires after every successful brain tool call.
|
|
683
608
|
// api_key carries the cky_live_... token so the backend can resolve tenant + increment counters.
|
|
684
|
-
const instanceId = (args.instance_id as string | undefined) ??
|
|
685
|
-
const telemetryExtra =
|
|
609
|
+
const instanceId = (args.instance_id as string | undefined) ?? _defaultInstanceId ?? '';
|
|
610
|
+
const telemetryExtra = JWT ? { api_key: JWT, instance_id: instanceId } : { instance_id: instanceId };
|
|
686
611
|
if (name === 'recall_best_solution') {
|
|
687
612
|
// recall_best_solution → increments BrainRecallCount + triggers level-up logic
|
|
688
613
|
sendFunnelEvent('recall_best_solution', telemetryExtra);
|
|
@@ -698,50 +623,25 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
|
|
|
698
623
|
if (!isFirstSession && resultText.length > 100) {
|
|
699
624
|
// Existing brain — session_start acts as implicit recall → increment counter.
|
|
700
625
|
sendFunnelEvent('recall_best_solution', telemetryExtra);
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
// We check a sentinel key in Valkey so this only fires once per workspace path,
|
|
705
|
-
// not just on the very first session (handles cases where brain was empty but
|
|
706
|
-
// session data existed, or where the user re-installed).
|
|
707
|
-
if (args.workspace_path) {
|
|
626
|
+
} else if (isFirstSession && args.workspace_path) {
|
|
627
|
+
// First session with a known workspace: auto-bootstrap from git history.
|
|
628
|
+
// Runs synchronously so the user sees the result in the same response.
|
|
708
629
|
try {
|
|
709
|
-
const
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
brainResult
|
|
630
|
+
const gitBootstrap = await handleFedbrainTool('brain_from_git', {
|
|
631
|
+
instance_id: instanceId,
|
|
632
|
+
repo_path: args.workspace_path as string,
|
|
633
|
+
limit: 50,
|
|
634
|
+
}, getConnection, apiFetch);
|
|
635
|
+
if (gitBootstrap !== null) {
|
|
636
|
+
const gitCounts = _lastBrainFromGitCounts;
|
|
637
|
+
sendFunnelEvent('brain_from_git', { ...telemetryExtra, ...(gitCounts ?? {}) });
|
|
638
|
+
sendFunnelEvent('recall_best_solution', telemetryExtra);
|
|
639
|
+
// Append bootstrap summary to the session_start briefing.
|
|
640
|
+
// brainResult is always a string here (handleBrainTool returns string for session_start).
|
|
641
|
+
const bootstrapText = String(gitBootstrap);
|
|
642
|
+
if (bootstrapText) {
|
|
643
|
+
return String(brainResult) + '\n---\n' + bootstrapText;
|
|
723
644
|
}
|
|
724
|
-
// Fire-and-forget — never block the session_start response
|
|
725
|
-
void handleFedbrainTool('brain_from_git', {
|
|
726
|
-
instance_id: instanceId,
|
|
727
|
-
repo_path: workspacePath,
|
|
728
|
-
limit: 50,
|
|
729
|
-
}, getConnection, apiFetch).then(async (gitBootstrap) => {
|
|
730
|
-
if (gitBootstrap !== null) {
|
|
731
|
-
sendFunnelEvent('brain_from_git', telemetryExtra);
|
|
732
|
-
sendFunnelEvent('recall_best_solution', telemetryExtra);
|
|
733
|
-
// Store ingested count for the next session_start wow moment
|
|
734
|
-
try {
|
|
735
|
-
const ingested = (gitBootstrap as string).match(/Ingested:\s*\*\*(\d+)\*/);
|
|
736
|
-
if (ingested) {
|
|
737
|
-
const r = await getConnection(instanceId);
|
|
738
|
-
await r.set('cachly:brain_from_git:auto_bootstrap_result',
|
|
739
|
-
JSON.stringify({ count: parseInt(ingested[1]!, 10), ts: new Date().toISOString() }),
|
|
740
|
-
'EX', 7 * 86400);
|
|
741
|
-
}
|
|
742
|
-
} catch { /* non-critical — must never break the session */ }
|
|
743
|
-
}
|
|
744
|
-
}).catch(() => { /* non-critical */ });
|
|
745
645
|
}
|
|
746
646
|
} catch { /* git bootstrap errors must never break session_start */ }
|
|
747
647
|
}
|
|
@@ -755,7 +655,7 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
|
|
|
755
655
|
const srText = typeof brainResult === 'object' && brainResult !== null && 'content' in brainResult
|
|
756
656
|
? JSON.stringify(brainResult)
|
|
757
657
|
: String(brainResult ?? '');
|
|
758
|
-
if (srText.length > 50 && !srText.includes('No lessons found') && !srText.includes('no lessons')) {
|
|
658
|
+
if (srText.length > 50 && !srText.includes('No lessons found') && !srText.includes('no lessons') && !srText.includes('No matches found')) {
|
|
759
659
|
sendFunnelEvent('recall_best_solution', telemetryExtra);
|
|
760
660
|
}
|
|
761
661
|
}
|
|
@@ -765,9 +665,9 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
|
|
|
765
665
|
// Delegate context tools (remember/recall/list/forget)
|
|
766
666
|
const contextResult = await handleContextTool(name, args, getConnection, apiFetch);
|
|
767
667
|
if (contextResult !== null) {
|
|
768
|
-
if (!_firstCallSuccessSent &&
|
|
668
|
+
if (!_firstCallSuccessSent && JWT) {
|
|
769
669
|
_firstCallSuccessSent = true;
|
|
770
|
-
sendFunnelEvent('first_call_success', { tool: name, instance_id: args.instance_id ??
|
|
670
|
+
sendFunnelEvent('first_call_success', { tool: name, instance_id: args.instance_id ?? _defaultInstanceId ?? '' });
|
|
771
671
|
}
|
|
772
672
|
return contextResult;
|
|
773
673
|
}
|
|
@@ -793,8 +693,8 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
|
|
|
793
693
|
if (syndicateResult !== null) {
|
|
794
694
|
// brain_predict is in syndicate handler — track telemetry here.
|
|
795
695
|
if (name === 'brain_predict') {
|
|
796
|
-
const instanceId = (args.instance_id as string | undefined) ??
|
|
797
|
-
const telemetryExtra =
|
|
696
|
+
const instanceId = (args.instance_id as string | undefined) ?? _defaultInstanceId ?? '';
|
|
697
|
+
const telemetryExtra = JWT ? { api_key: JWT, instance_id: instanceId } : { instance_id: instanceId };
|
|
798
698
|
sendFunnelEvent('brain_predict', telemetryExtra);
|
|
799
699
|
}
|
|
800
700
|
return syndicateResult;
|
|
@@ -803,16 +703,21 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
|
|
|
803
703
|
const fedbrainResult = await handleFedbrainTool(name, args, getConnection, apiFetch);
|
|
804
704
|
if (fedbrainResult !== null) {
|
|
805
705
|
// brain_from_git and brain_predict_failures are in fedbrain handler.
|
|
806
|
-
const instanceId = (args.instance_id as string | undefined) ??
|
|
807
|
-
const telemetryExtra =
|
|
706
|
+
const instanceId = (args.instance_id as string | undefined) ?? _defaultInstanceId ?? '';
|
|
707
|
+
const telemetryExtra = JWT ? { api_key: JWT, instance_id: instanceId } : { instance_id: instanceId };
|
|
808
708
|
if (name === 'brain_from_git') {
|
|
809
|
-
|
|
709
|
+
const gitCounts = _lastBrainFromGitCounts;
|
|
710
|
+
sendFunnelEvent('brain_from_git', { ...telemetryExtra, ...(gitCounts ?? {}) });
|
|
810
711
|
} else if (name === 'brain_predict_failures') {
|
|
811
712
|
sendFunnelEvent('brain_predict_failures', telemetryExtra);
|
|
812
713
|
}
|
|
813
714
|
return fedbrainResult;
|
|
814
715
|
}
|
|
815
716
|
|
|
717
|
+
// Delegate Travel Chaos Organizer tools (requires TCO_API_URL env var)
|
|
718
|
+
const tcoResult = await handleTcoTool(name, args, JWT ?? '');
|
|
719
|
+
if (tcoResult !== null) return tcoResult;
|
|
720
|
+
|
|
816
721
|
switch (name) {
|
|
817
722
|
// ── Instance management ──────────────────────────────────────────────
|
|
818
723
|
case 'get_api_status': {
|
|
@@ -831,7 +736,7 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
|
|
|
831
736
|
}
|
|
832
737
|
|
|
833
738
|
// Check JWT / auth
|
|
834
|
-
if (!
|
|
739
|
+
if (!JWT) {
|
|
835
740
|
return [
|
|
836
741
|
`📡 **cachly API Status**`,
|
|
837
742
|
``,
|
|
@@ -844,38 +749,28 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
|
|
|
844
749
|
}
|
|
845
750
|
|
|
846
751
|
// Decode JWT claims (inspection only, no verification)
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
const
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
const expTs = payload.exp ? new Date(payload.exp * 1000) : null;
|
|
867
|
-
const expired = expTs ? expTs < new Date() : false;
|
|
868
|
-
authInfo = [
|
|
869
|
-
`✅ JWT decoded`,
|
|
870
|
-
` Sub (user ID): ${sub}`,
|
|
871
|
-
` Provider: ${provider}`,
|
|
872
|
-
` Issuer: ${iss}`,
|
|
873
|
-
` Expires: ${expTs ? expTs.toISOString() : 'never'} ${expired ? '⚠️ EXPIRED – get a new token!' : '✅'}`,
|
|
874
|
-
].join('\n');
|
|
875
|
-
}
|
|
876
|
-
} catch {
|
|
877
|
-
authInfo = '❌ JWT decode failed – check CACHLY_JWT format';
|
|
752
|
+
let authInfo = '❌ invalid JWT format';
|
|
753
|
+
try {
|
|
754
|
+
const parts = JWT.split('.');
|
|
755
|
+
if (parts.length === 3) {
|
|
756
|
+
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf-8')) as {
|
|
757
|
+
sub?: string; exp?: number; iss?: string;
|
|
758
|
+
};
|
|
759
|
+
const sub = payload.sub ?? '(unknown)';
|
|
760
|
+
const iss = payload.iss ?? '(unknown)';
|
|
761
|
+
const provider = iss.includes('keycloak') ? 'Keycloak' : 'OIDC';
|
|
762
|
+
const expTs = payload.exp ? new Date(payload.exp * 1000) : null;
|
|
763
|
+
const expired = expTs ? expTs < new Date() : false;
|
|
764
|
+
authInfo = [
|
|
765
|
+
`✅ JWT decoded`,
|
|
766
|
+
` Sub (user ID): ${sub}`,
|
|
767
|
+
` Provider: ${provider}`,
|
|
768
|
+
` Issuer: ${iss}`,
|
|
769
|
+
` Expires: ${expTs ? expTs.toISOString() : 'never'} ${expired ? '⚠️ EXPIRED – get a new token!' : '✅'}`,
|
|
770
|
+
].join('\n');
|
|
878
771
|
}
|
|
772
|
+
} catch {
|
|
773
|
+
authInfo = '❌ JWT decode failed – check CACHLY_JWT format';
|
|
879
774
|
}
|
|
880
775
|
|
|
881
776
|
// List instances + connectivity
|
|
@@ -889,7 +784,7 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
|
|
|
889
784
|
const lines: string[] = ['\n\n🧠 **Brain Instances:**'];
|
|
890
785
|
for (const inst of instances) {
|
|
891
786
|
const badge = inst.status === 'running' ? '🟢' : inst.status === 'provisioning' ? '🟡' : '🔴';
|
|
892
|
-
const defaultMark = inst.id ===
|
|
787
|
+
const defaultMark = inst.id === _defaultInstanceId ? ' ← active' : '';
|
|
893
788
|
lines.push(` ${badge} **${inst.name}** (\`${inst.id}\`) · ${inst.status}${defaultMark}`);
|
|
894
789
|
if (inst.status === 'running' && inst.host) {
|
|
895
790
|
// Try a quick ping on the cached connection
|
|
@@ -1017,14 +912,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1017
912
|
|| _defaultInstanceId
|
|
1018
913
|
|| process.env.CACHLY_BRAIN_INSTANCE_ID;
|
|
1019
914
|
if (instanceId) {
|
|
1020
|
-
|
|
1021
|
-
// The session_start briefing (which includes brain_from_git) can take 30-120s on large
|
|
1022
|
-
// repos; awaiting it here would freeze the user's first MCP tool call entirely.
|
|
1023
|
-
void autoStartSession(instanceId);
|
|
915
|
+
await autoStartSession(instanceId).catch(() => undefined);
|
|
1024
916
|
}
|
|
917
|
+
} else if (_autoSessionStarting) {
|
|
918
|
+
// Another concurrent call is mid-start — wait for it before proceeding.
|
|
919
|
+
await _autoSessionStarting.catch(() => undefined);
|
|
1025
920
|
}
|
|
1026
|
-
// Note: we intentionally do NOT await _autoSessionStarting for any call.
|
|
1027
|
-
// The session tracker runs in background — tool calls must always proceed immediately.
|
|
1028
921
|
if (!sessionTools.has(name)) _autoSessionToolCount++;
|
|
1029
922
|
|
|
1030
923
|
try {
|
|
@@ -1032,7 +925,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1032
925
|
return { content: [{ type: 'text', text }] };
|
|
1033
926
|
} catch (err) {
|
|
1034
927
|
if (err instanceof McpError) throw err;
|
|
1035
|
-
|
|
928
|
+
const msg = (err as Error).message ?? String(err);
|
|
929
|
+
notify('cachly', 'tool_error', { tool: name, error: msg }).catch(() => undefined);
|
|
930
|
+
throw new McpError(ErrorCode.InternalError, msg);
|
|
1036
931
|
}
|
|
1037
932
|
});
|
|
1038
933
|
|
|
@@ -1921,7 +1816,7 @@ if (!process.argv[2] && process.stdout.isTTY) {
|
|
|
1921
1816
|
console.log(' \x1b[36m npx @cachly-dev/mcp-server@latest upgrade\x1b[0m ← Check for updates');
|
|
1922
1817
|
console.log('');
|
|
1923
1818
|
console.log(' \x1b[90mWorks with: Claude Code · Cursor · Windsurf · GitHub Copilot · Cline · Zed\x1b[0m');
|
|
1924
|
-
console.log(' \x1b[90mFree forever · GDPR · German servers ·
|
|
1819
|
+
console.log(' \x1b[90mFree forever · GDPR · German servers · 95 MCP tools\x1b[0m');
|
|
1925
1820
|
console.log('');
|
|
1926
1821
|
process.exit(0);
|
|
1927
1822
|
}
|
|
@@ -2111,9 +2006,6 @@ if (process.argv[2] === 'health') {
|
|
|
2111
2006
|
const jwt = process.env.CACHLY_JWT ?? '';
|
|
2112
2007
|
if (!jwt) {
|
|
2113
2008
|
fail('CACHLY_JWT not set — run: npx @cachly-dev/mcp-server@latest setup');
|
|
2114
|
-
} else if (jwt.startsWith('cky_')) {
|
|
2115
|
-
// Opaque cachly API key — no JWT expiry to check, format is valid by prefix
|
|
2116
|
-
ok('API key set (cky_*** opaque key)');
|
|
2117
2009
|
} else {
|
|
2118
2010
|
try {
|
|
2119
2011
|
const parts = jwt.split('.');
|
|
@@ -2131,7 +2023,7 @@ if (process.argv[2] === 'health') {
|
|
|
2131
2023
|
ok(`JWT valid${minsLeft !== null ? ` (expires in ${Math.floor(minsLeft / 60)}h ${minsLeft % 60}m)` : ''}`);
|
|
2132
2024
|
}
|
|
2133
2025
|
} catch {
|
|
2134
|
-
fail('CACHLY_JWT format invalid (expected
|
|
2026
|
+
fail('CACHLY_JWT format invalid (expected JWT with 3 parts)');
|
|
2135
2027
|
}
|
|
2136
2028
|
}
|
|
2137
2029
|
|
|
@@ -2179,23 +2071,11 @@ if (process.argv[2] === 'health') {
|
|
|
2179
2071
|
signal: AbortSignal.timeout(5000),
|
|
2180
2072
|
});
|
|
2181
2073
|
if (connRes.ok) {
|
|
2182
|
-
const conn = await connRes.json() as { password?: string;
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
pingPassword = conn.password;
|
|
2186
|
-
} else if (conn.connection_string) {
|
|
2187
|
-
try {
|
|
2188
|
-
const u = new URL(conn.connection_string);
|
|
2189
|
-
if (u.password) pingPassword = decodeURIComponent(u.password);
|
|
2190
|
-
} catch { /* leave pingPassword undefined */ }
|
|
2191
|
-
}
|
|
2192
|
-
pingTls = conn.tls_enabled === true;
|
|
2193
|
-
} else {
|
|
2194
|
-
warn(`Could not fetch Redis credentials (HTTP ${connRes.status}) — connectivity test may fail`);
|
|
2074
|
+
const conn = await connRes.json() as { password?: string; tls_enabled?: boolean };
|
|
2075
|
+
pingPassword = conn.password;
|
|
2076
|
+
pingTls = conn.tls_enabled !== false;
|
|
2195
2077
|
}
|
|
2196
|
-
} catch
|
|
2197
|
-
warn(`Could not fetch Redis credentials — ${(connErr as Error).message}`);
|
|
2198
|
-
}
|
|
2078
|
+
} catch { /* proceed without password */ }
|
|
2199
2079
|
const pingClient = new Redis({
|
|
2200
2080
|
host: inst.host, port: inst.port,
|
|
2201
2081
|
password: pingPassword,
|
|
@@ -2323,6 +2203,7 @@ if (process.argv[2] === 'setup') {
|
|
|
2323
2203
|
const CLIENT_ID = 'cachly-cli';
|
|
2324
2204
|
|
|
2325
2205
|
console.log('Step 1: Sign in to cachly (free, no credit card)\n');
|
|
2206
|
+
sendFunnelEvent('setup_auth_started');
|
|
2326
2207
|
|
|
2327
2208
|
// Start device flow
|
|
2328
2209
|
let deviceCode = '', userCode = '', verifyUri = '', pollInterval = 5000;
|
|
@@ -2346,6 +2227,7 @@ if (process.argv[2] === 'setup') {
|
|
|
2346
2227
|
console.error('Falling back: sign in at https://cachly.dev/setup-ai and paste your API token.\n');
|
|
2347
2228
|
token = await ask(' Paste API token (cky_live_...): ');
|
|
2348
2229
|
if (!token) { console.error('\nToken is required. Aborting.\n'); rl.close(); process.exit(1); }
|
|
2230
|
+
sendFunnelEvent('setup_auth_completed');
|
|
2349
2231
|
console.log('');
|
|
2350
2232
|
deviceCode = ''; // mark as fallback so we skip polling
|
|
2351
2233
|
}
|
|
@@ -2379,6 +2261,7 @@ if (process.argv[2] === 'setup') {
|
|
|
2379
2261
|
if (tokenData.access_token) {
|
|
2380
2262
|
token = tokenData.access_token;
|
|
2381
2263
|
console.log(' \x1b[32m✓ Authorized!\x1b[0m\n');
|
|
2264
|
+
sendFunnelEvent('setup_auth_completed');
|
|
2382
2265
|
break;
|
|
2383
2266
|
}
|
|
2384
2267
|
// authorization_pending = keep polling; slow_down = increase interval
|
|
@@ -2459,9 +2342,10 @@ if (process.argv[2] === 'setup') {
|
|
|
2459
2342
|
// Newly created — poll until running or give up after 30 s.
|
|
2460
2343
|
const newId = autoBody.instance_id;
|
|
2461
2344
|
console.log(` ✓ created (${newId.slice(0, 8)}…)\n`);
|
|
2345
|
+
// 45 attempts × 3s = 135s — generous for free-tier in slow regions.
|
|
2462
2346
|
process.stdout.write('⏳ Waiting for instance to start');
|
|
2463
|
-
for (let attempt = 0; attempt <
|
|
2464
|
-
await new Promise(r => setTimeout(r,
|
|
2347
|
+
for (let attempt = 0; attempt < 45; attempt++) {
|
|
2348
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
2465
2349
|
process.stdout.write('.');
|
|
2466
2350
|
try {
|
|
2467
2351
|
const checkRes = await fetch(`${API_URL}/api/v1/instances/${newId}`, { headers: { Authorization: `Bearer ${token}` } });
|
|
@@ -2495,6 +2379,7 @@ if (process.argv[2] === 'setup') {
|
|
|
2495
2379
|
console.log(` (Run with --instance-id <id> to use a different one)\n`);
|
|
2496
2380
|
}
|
|
2497
2381
|
console.log(`✓ Instance: ${instance.name} (${instance.id.slice(0, 8)}…)\n`);
|
|
2382
|
+
sendFunnelEvent('setup_instance_ready', { instance_id: instance.id });
|
|
2498
2383
|
|
|
2499
2384
|
// ── Step 3: Detect editors ────────────────────────────────────────────────
|
|
2500
2385
|
const cwd = process.cwd();
|
|
@@ -2667,8 +2552,6 @@ if (process.argv[2] === 'setup') {
|
|
|
2667
2552
|
console.log(` Dashboard: https://cachly.dev/instances/${instance.id}`);
|
|
2668
2553
|
console.log(`\n 📛 Add a live badge to your README:`);
|
|
2669
2554
|
console.log(` npx @cachly-dev/mcp-server@latest badge\n`);
|
|
2670
|
-
console.log(` 🤝 Invite a teammate — they'll arrive pre-briefed on your project:`);
|
|
2671
|
-
console.log(` npx @cachly-dev/mcp-server@latest invite\n`);
|
|
2672
2555
|
|
|
2673
2556
|
// ── Step 7: Email opt-in (non-blocking — at the very end) ────────────────
|
|
2674
2557
|
if (!nonInteractive) {
|
|
@@ -2742,7 +2625,7 @@ if (process.argv[2] === 'index') {
|
|
|
2742
2625
|
const maxFiles = parseInt(flag('max-files') ?? '500', 10);
|
|
2743
2626
|
const namespace = flag('namespace') ?? 'cachly:sem:code';
|
|
2744
2627
|
|
|
2745
|
-
if (!instanceId || !
|
|
2628
|
+
if (!instanceId || !JWT) {
|
|
2746
2629
|
console.error('\n❌ CACHLY_BRAIN_INSTANCE_ID and CACHLY_JWT must be set\n');
|
|
2747
2630
|
console.error(' export CACHLY_BRAIN_INSTANCE_ID=<uuid>');
|
|
2748
2631
|
console.error(' export CACHLY_JWT=<cky_live_...>');
|
|
@@ -2784,7 +2667,7 @@ if (process.argv[2] === 'learn-git') {
|
|
|
2784
2667
|
const instanceId = flag('instance-id') ?? process.env.CACHLY_BRAIN_INSTANCE_ID;
|
|
2785
2668
|
const maxCommits = parseInt(flag('max-commits') ?? '50', 10);
|
|
2786
2669
|
|
|
2787
|
-
if (!instanceId || !
|
|
2670
|
+
if (!instanceId || !JWT) {
|
|
2788
2671
|
console.error('\n❌ CACHLY_BRAIN_INSTANCE_ID and CACHLY_JWT must be set\n');
|
|
2789
2672
|
console.error(' export CACHLY_BRAIN_INSTANCE_ID=<uuid>');
|
|
2790
2673
|
console.error(' export CACHLY_JWT=<cky_live_...>');
|
|
@@ -2802,7 +2685,7 @@ if (process.argv[2] === 'learn-git') {
|
|
|
2802
2685
|
limit: maxCommits,
|
|
2803
2686
|
});
|
|
2804
2687
|
console.log(result);
|
|
2805
|
-
sendFunnelEvent('brain_from_git', { instance_id: instanceId });
|
|
2688
|
+
sendFunnelEvent('brain_from_git', { api_key: JWT, instance_id: instanceId });
|
|
2806
2689
|
console.log('\n✅ Brain learned from your commits.\n');
|
|
2807
2690
|
// Let fire-and-forget telemetry flush before exit.
|
|
2808
2691
|
await new Promise(r => setTimeout(r, 300));
|
|
@@ -2819,8 +2702,7 @@ if (process.argv[2] === 'learn-git') {
|
|
|
2819
2702
|
// actionable message in their editor's MCP log instead of silent failures.
|
|
2820
2703
|
// Skip for CLI commands that intentionally run without credentials.
|
|
2821
2704
|
const _cliNoAuthCommands = ['demo', 'share', 'health', 'setup', 'init', 'digest', 'invite', 'badge', 'join', 'upgrade'];
|
|
2822
|
-
|
|
2823
|
-
if (!JWT && !process.env.PORT && !_cliNoAuthCommands.includes(process.argv[2] ?? '') && !(!process.argv[2] && process.stdout.isTTY)) {
|
|
2705
|
+
if (!JWT && !_cliNoAuthCommands.includes(process.argv[2] ?? '') && !(!process.argv[2] && process.stdout.isTTY)) {
|
|
2824
2706
|
// No args + no JWT in a non-TTY context = running as MCP server without credentials.
|
|
2825
2707
|
// Print actionable setup banner to stdout so it's captured by callers/tests.
|
|
2826
2708
|
const banner =
|
|
@@ -2893,76 +2775,12 @@ if (!process.env.CACHLY_NO_UPDATE_CHECK) {
|
|
|
2893
2775
|
|
|
2894
2776
|
const httpPort = process.env.PORT ? parseInt(process.env.PORT, 10) : undefined;
|
|
2895
2777
|
|
|
2896
|
-
// ── Per-request MCP Server factory (HTTP / Smithery stateless mode) ──────────
|
|
2897
|
-
// SDK ≥1.9 forbids reusing a stateless transport across requests.
|
|
2898
|
-
// Solution: create a fresh Server+Transport per /mcp request.
|
|
2899
|
-
// The module-level `server` below is used for stdio mode only.
|
|
2900
|
-
function createHttpMcpServer(): Server {
|
|
2901
|
-
const srv = new Server(
|
|
2902
|
-
{ name: 'cachly-mcp', version: CURRENT_VERSION },
|
|
2903
|
-
{ capabilities: { tools: {} } }
|
|
2904
|
-
);
|
|
2905
|
-
srv.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
2906
|
-
srv.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
2907
|
-
const { name, arguments: args } = request.params;
|
|
2908
|
-
try {
|
|
2909
|
-
const text = await handleTool(name, (args ?? {}) as Record<string, unknown>);
|
|
2910
|
-
return { content: [{ type: 'text', text }] };
|
|
2911
|
-
} catch (err) {
|
|
2912
|
-
if (err instanceof McpError) throw err;
|
|
2913
|
-
throw new McpError(ErrorCode.InternalError, (err as Error).message);
|
|
2914
|
-
}
|
|
2915
|
-
});
|
|
2916
|
-
return srv;
|
|
2917
|
-
}
|
|
2918
|
-
|
|
2919
2778
|
if (httpPort) {
|
|
2920
2779
|
// ── HTTP mode (Streamable HTTP transport) ───────────────────────────────
|
|
2921
|
-
// Used for Smithery URL deployment: PORT=3000 node dist/index.js
|
|
2922
|
-
// Multi-tenant: per-request JWT + instance ID resolved via:
|
|
2923
|
-
// 1. Authorization: Bearer <jwt> + X-Cachly-Instance-Id: <uuid>
|
|
2924
|
-
// 2. Smithery config: ?config=base64url({CACHLY_JWT, CACHLY_BRAIN_INSTANCE_ID, ...})
|
|
2925
|
-
// Falls back to module-level env-derived values when neither is provided.
|
|
2780
|
+
// Used for Smithery URL deployment: PORT=3000 node dist/src/index.js
|
|
2926
2781
|
const { createServer } = await import('node:http');
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
let jwtVal = '';
|
|
2930
|
-
let instId = '';
|
|
2931
|
-
const auth = req.headers['authorization'];
|
|
2932
|
-
if (typeof auth === 'string' && auth.trim()) {
|
|
2933
|
-
const a = auth.trim();
|
|
2934
|
-
jwtVal = a.toLowerCase().startsWith('bearer ') ? a.slice(7).trim() : a;
|
|
2935
|
-
}
|
|
2936
|
-
// Fallback: raw JWT in custom header (Smithery "header" transport without Bearer prefix)
|
|
2937
|
-
if (!jwtVal) {
|
|
2938
|
-
const rawJwt = req.headers['cachly_jwt'] ?? req.headers['cachly-jwt'] ?? req.headers['x-cachly-jwt'] ?? req.headers['x-api-key'];
|
|
2939
|
-
if (typeof rawJwt === 'string') jwtVal = rawJwt.trim();
|
|
2940
|
-
}
|
|
2941
|
-
const hdrInst = req.headers['x-cachly-instance-id'];
|
|
2942
|
-
if (typeof hdrInst === 'string') instId = hdrInst.trim();
|
|
2943
|
-
// Smithery passes user config as a base64url-encoded JSON blob in ?config=
|
|
2944
|
-
const cfgParam = url.searchParams.get('config');
|
|
2945
|
-
if (cfgParam && (!jwtVal || !instId)) {
|
|
2946
|
-
try {
|
|
2947
|
-
const b64 = cfgParam.replace(/-/g, '+').replace(/_/g, '/');
|
|
2948
|
-
const padded = b64 + '='.repeat((4 - b64.length % 4) % 4);
|
|
2949
|
-
const json = JSON.parse(Buffer.from(padded, 'base64').toString('utf8'));
|
|
2950
|
-
if (!jwtVal && typeof json.CACHLY_JWT === 'string') jwtVal = json.CACHLY_JWT;
|
|
2951
|
-
if (!instId && typeof json.CACHLY_BRAIN_INSTANCE_ID === 'string') instId = json.CACHLY_BRAIN_INSTANCE_ID;
|
|
2952
|
-
} catch { /* malformed config — ignore, fall back to globals */ }
|
|
2953
|
-
}
|
|
2954
|
-
// Direct query params (Smithery "query" transport, manual clients, etc.)
|
|
2955
|
-
if (!jwtVal) {
|
|
2956
|
-
const q = url.searchParams.get('CACHLY_JWT') ?? url.searchParams.get('cachly_jwt') ?? url.searchParams.get('api_key');
|
|
2957
|
-
if (q) jwtVal = q.trim();
|
|
2958
|
-
}
|
|
2959
|
-
if (!instId) {
|
|
2960
|
-
const q = url.searchParams.get('CACHLY_BRAIN_INSTANCE_ID') ?? url.searchParams.get('cachly_brain_instance_id') ?? url.searchParams.get('instance_id');
|
|
2961
|
-
if (q) instId = q.trim();
|
|
2962
|
-
}
|
|
2963
|
-
return { jwt: jwtVal, instanceId: instId };
|
|
2964
|
-
}
|
|
2965
|
-
|
|
2782
|
+
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
|
|
2783
|
+
await server.connect(transport);
|
|
2966
2784
|
const httpServer = createServer((req, res) => {
|
|
2967
2785
|
const url = new URL(req.url ?? '/', `http://localhost:${httpPort}`);
|
|
2968
2786
|
if (url.pathname === '/health') {
|
|
@@ -2970,8 +2788,6 @@ if (httpPort) {
|
|
|
2970
2788
|
res.end(JSON.stringify({
|
|
2971
2789
|
status: 'ok',
|
|
2972
2790
|
version: CURRENT_VERSION,
|
|
2973
|
-
transport: 'streamable-http',
|
|
2974
|
-
multiTenant: true,
|
|
2975
2791
|
zeroResults: {
|
|
2976
2792
|
total: zeroResultsTotal,
|
|
2977
2793
|
last10: ZERO_RESULTS_LOG.slice(-10).map(e => ({ query: e.query, ts: new Date(e.ts).toISOString() })),
|
|
@@ -2979,52 +2795,8 @@ if (httpPort) {
|
|
|
2979
2795
|
}));
|
|
2980
2796
|
return;
|
|
2981
2797
|
}
|
|
2982
|
-
// Static server card — lets Smithery skip live scanning (avoids auth complexity).
|
|
2983
|
-
if (url.pathname === '/.well-known/mcp/server-card.json') {
|
|
2984
|
-
const card = {
|
|
2985
|
-
name: 'cachly AI Brain',
|
|
2986
|
-
version: CURRENT_VERSION,
|
|
2987
|
-
description: 'Persistent AI memory for Claude Code, Cursor, Copilot, Windsurf & friends. Remembers every lesson, fix, and architecture decision forever.',
|
|
2988
|
-
transport: { type: 'streamable-http', url: 'https://mcp.cachly.dev/mcp' },
|
|
2989
|
-
capabilities: { tools: {} },
|
|
2990
|
-
};
|
|
2991
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
2992
|
-
res.end(JSON.stringify(card));
|
|
2993
|
-
return;
|
|
2994
|
-
}
|
|
2995
2798
|
if (url.pathname === '/mcp' || url.pathname === '/') {
|
|
2996
|
-
|
|
2997
|
-
if (req.method === 'OPTIONS') {
|
|
2998
|
-
res.writeHead(204, {
|
|
2999
|
-
'Access-Control-Allow-Origin': '*',
|
|
3000
|
-
'Access-Control-Allow-Methods': 'GET, POST, DELETE, OPTIONS',
|
|
3001
|
-
'Access-Control-Allow-Headers': 'Authorization, Content-Type, Mcp-Session-Id, X-Cachly-Instance-Id, cachly_jwt, cachly-jwt, x-cachly-jwt, x-api-key',
|
|
3002
|
-
});
|
|
3003
|
-
res.end();
|
|
3004
|
-
return;
|
|
3005
|
-
}
|
|
3006
|
-
const ctx = extractReqCtx(req, url);
|
|
3007
|
-
// Create a fresh Server+Transport for each request (stateless multi-tenant mode).
|
|
3008
|
-
// SDK ≥1.9 throws if a stateless transport is reused across requests.
|
|
3009
|
-
const reqServer = createHttpMcpServer();
|
|
3010
|
-
const reqTransport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
|
|
3011
|
-
reqServer.connect(reqTransport).then(() => {
|
|
3012
|
-
reqCtx.run(ctx, () => {
|
|
3013
|
-
reqTransport.handleRequest(req, res).catch((err) => {
|
|
3014
|
-
process.stderr.write(`cachly-mcp HTTP handler error: ${(err as Error).message}\n`);
|
|
3015
|
-
if (!res.headersSent) {
|
|
3016
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
3017
|
-
res.end(JSON.stringify({ error: 'internal_error' }));
|
|
3018
|
-
}
|
|
3019
|
-
});
|
|
3020
|
-
});
|
|
3021
|
-
}).catch((err) => {
|
|
3022
|
-
process.stderr.write(`cachly-mcp connect error: ${(err as Error).message}\n`);
|
|
3023
|
-
if (!res.headersSent) {
|
|
3024
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
3025
|
-
res.end(JSON.stringify({ error: 'internal_error' }));
|
|
3026
|
-
}
|
|
3027
|
-
});
|
|
2799
|
+
transport.handleRequest(req, res);
|
|
3028
2800
|
return;
|
|
3029
2801
|
}
|
|
3030
2802
|
res.writeHead(404);
|
|
@@ -3039,3 +2811,5 @@ if (httpPort) {
|
|
|
3039
2811
|
await server.connect(transport);
|
|
3040
2812
|
}
|
|
3041
2813
|
|
|
2814
|
+
notify('cachly', 'startup', { version: CURRENT_VERSION, mode: process.env.MCP_HTTP_PORT ? 'http' : 'stdio' }).catch(() => undefined);
|
|
2815
|
+
|