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