@cachly-dev/mcp-server 0.10.56 → 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 -193
- 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 -431
- 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 +27 -16
- 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 -197
- 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 -418
- 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,48 +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
|
-
|
|
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;
|
|
683
619
|
}
|
|
684
|
-
else if (typeof brainResult === 'string') {
|
|
685
|
-
brainResult = brainResult + bgMsg;
|
|
686
|
-
}
|
|
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(async (gitBootstrap) => {
|
|
693
|
-
if (gitBootstrap !== null) {
|
|
694
|
-
sendFunnelEvent('brain_from_git', telemetryExtra);
|
|
695
|
-
sendFunnelEvent('recall_best_solution', telemetryExtra);
|
|
696
|
-
// Store ingested count for the next session_start wow moment
|
|
697
|
-
try {
|
|
698
|
-
const ingested = gitBootstrap.match(/Ingested:\s*\*\*(\d+)\*/);
|
|
699
|
-
if (ingested) {
|
|
700
|
-
const r = await getConnection(instanceId);
|
|
701
|
-
await r.set('cachly:brain_from_git:auto_bootstrap_result', JSON.stringify({ count: parseInt(ingested[1], 10), ts: new Date().toISOString() }), 'EX', 7 * 86400);
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
catch { /* non-critical — must never break the session */ }
|
|
705
|
-
}
|
|
706
|
-
}).catch(() => { });
|
|
707
620
|
}
|
|
708
621
|
}
|
|
709
622
|
catch { /* git bootstrap errors must never break session_start */ }
|
|
@@ -720,7 +633,7 @@ async function handleTool(name, args) {
|
|
|
720
633
|
const srText = typeof brainResult === 'object' && brainResult !== null && 'content' in brainResult
|
|
721
634
|
? JSON.stringify(brainResult)
|
|
722
635
|
: String(brainResult ?? '');
|
|
723
|
-
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')) {
|
|
724
637
|
sendFunnelEvent('recall_best_solution', telemetryExtra);
|
|
725
638
|
}
|
|
726
639
|
}
|
|
@@ -729,9 +642,9 @@ async function handleTool(name, args) {
|
|
|
729
642
|
// Delegate context tools (remember/recall/list/forget)
|
|
730
643
|
const contextResult = await handleContextTool(name, args, getConnection, apiFetch);
|
|
731
644
|
if (contextResult !== null) {
|
|
732
|
-
if (!_firstCallSuccessSent &&
|
|
645
|
+
if (!_firstCallSuccessSent && JWT) {
|
|
733
646
|
_firstCallSuccessSent = true;
|
|
734
|
-
sendFunnelEvent('first_call_success', { tool: name, instance_id: args.instance_id ??
|
|
647
|
+
sendFunnelEvent('first_call_success', { tool: name, instance_id: args.instance_id ?? _defaultInstanceId ?? '' });
|
|
735
648
|
}
|
|
736
649
|
return contextResult;
|
|
737
650
|
}
|
|
@@ -756,8 +669,8 @@ async function handleTool(name, args) {
|
|
|
756
669
|
if (syndicateResult !== null) {
|
|
757
670
|
// brain_predict is in syndicate handler — track telemetry here.
|
|
758
671
|
if (name === 'brain_predict') {
|
|
759
|
-
const instanceId = args.instance_id ??
|
|
760
|
-
const telemetryExtra =
|
|
672
|
+
const instanceId = args.instance_id ?? _defaultInstanceId ?? '';
|
|
673
|
+
const telemetryExtra = JWT ? { api_key: JWT, instance_id: instanceId } : { instance_id: instanceId };
|
|
761
674
|
sendFunnelEvent('brain_predict', telemetryExtra);
|
|
762
675
|
}
|
|
763
676
|
return syndicateResult;
|
|
@@ -765,16 +678,21 @@ async function handleTool(name, args) {
|
|
|
765
678
|
const fedbrainResult = await handleFedbrainTool(name, args, getConnection, apiFetch);
|
|
766
679
|
if (fedbrainResult !== null) {
|
|
767
680
|
// brain_from_git and brain_predict_failures are in fedbrain handler.
|
|
768
|
-
const instanceId = args.instance_id ??
|
|
769
|
-
const telemetryExtra =
|
|
681
|
+
const instanceId = args.instance_id ?? _defaultInstanceId ?? '';
|
|
682
|
+
const telemetryExtra = JWT ? { api_key: JWT, instance_id: instanceId } : { instance_id: instanceId };
|
|
770
683
|
if (name === 'brain_from_git') {
|
|
771
|
-
|
|
684
|
+
const gitCounts = _lastBrainFromGitCounts;
|
|
685
|
+
sendFunnelEvent('brain_from_git', { ...telemetryExtra, ...(gitCounts ?? {}) });
|
|
772
686
|
}
|
|
773
687
|
else if (name === 'brain_predict_failures') {
|
|
774
688
|
sendFunnelEvent('brain_predict_failures', telemetryExtra);
|
|
775
689
|
}
|
|
776
690
|
return fedbrainResult;
|
|
777
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;
|
|
778
696
|
switch (name) {
|
|
779
697
|
// ── Instance management ──────────────────────────────────────────────
|
|
780
698
|
case 'get_api_status': {
|
|
@@ -794,7 +712,7 @@ async function handleTool(name, args) {
|
|
|
794
712
|
healthStatus = `unreachable: ${e.message}`;
|
|
795
713
|
}
|
|
796
714
|
// Check JWT / auth
|
|
797
|
-
if (!
|
|
715
|
+
if (!JWT) {
|
|
798
716
|
return [
|
|
799
717
|
`📡 **cachly API Status**`,
|
|
800
718
|
``,
|
|
@@ -806,39 +724,28 @@ async function handleTool(name, args) {
|
|
|
806
724
|
].join('\n');
|
|
807
725
|
}
|
|
808
726
|
// Decode JWT claims (inspection only, no verification)
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
const provider = iss.includes('keycloak') ? 'Keycloak' : 'OIDC';
|
|
827
|
-
const expTs = payload.exp ? new Date(payload.exp * 1000) : null;
|
|
828
|
-
const expired = expTs ? expTs < new Date() : false;
|
|
829
|
-
authInfo = [
|
|
830
|
-
`✅ JWT decoded`,
|
|
831
|
-
` Sub (user ID): ${sub}`,
|
|
832
|
-
` Provider: ${provider}`,
|
|
833
|
-
` Issuer: ${iss}`,
|
|
834
|
-
` Expires: ${expTs ? expTs.toISOString() : 'never'} ${expired ? '⚠️ EXPIRED – get a new token!' : '✅'}`,
|
|
835
|
-
].join('\n');
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
catch {
|
|
839
|
-
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');
|
|
840
744
|
}
|
|
841
745
|
}
|
|
746
|
+
catch {
|
|
747
|
+
authInfo = '❌ JWT decode failed – check CACHLY_JWT format';
|
|
748
|
+
}
|
|
842
749
|
// List instances + connectivity
|
|
843
750
|
let instanceInfo = '';
|
|
844
751
|
try {
|
|
@@ -851,7 +758,7 @@ async function handleTool(name, args) {
|
|
|
851
758
|
const lines = ['\n\n🧠 **Brain Instances:**'];
|
|
852
759
|
for (const inst of instances) {
|
|
853
760
|
const badge = inst.status === 'running' ? '🟢' : inst.status === 'provisioning' ? '🟡' : '🔴';
|
|
854
|
-
const defaultMark = inst.id ===
|
|
761
|
+
const defaultMark = inst.id === _defaultInstanceId ? ' ← active' : '';
|
|
855
762
|
lines.push(` ${badge} **${inst.name}** (\`${inst.id}\`) · ${inst.status}${defaultMark}`);
|
|
856
763
|
if (inst.status === 'running' && inst.host) {
|
|
857
764
|
// Try a quick ping on the cached connection
|
|
@@ -979,14 +886,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
979
886
|
|| _defaultInstanceId
|
|
980
887
|
|| process.env.CACHLY_BRAIN_INSTANCE_ID;
|
|
981
888
|
if (instanceId) {
|
|
982
|
-
|
|
983
|
-
// The session_start briefing (which includes brain_from_git) can take 30-120s on large
|
|
984
|
-
// repos; awaiting it here would freeze the user's first MCP tool call entirely.
|
|
985
|
-
void autoStartSession(instanceId);
|
|
889
|
+
await autoStartSession(instanceId).catch(() => undefined);
|
|
986
890
|
}
|
|
987
891
|
}
|
|
988
|
-
|
|
989
|
-
|
|
892
|
+
else if (_autoSessionStarting) {
|
|
893
|
+
// Another concurrent call is mid-start — wait for it before proceeding.
|
|
894
|
+
await _autoSessionStarting.catch(() => undefined);
|
|
895
|
+
}
|
|
990
896
|
if (!sessionTools.has(name))
|
|
991
897
|
_autoSessionToolCount++;
|
|
992
898
|
try {
|
|
@@ -996,7 +902,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
996
902
|
catch (err) {
|
|
997
903
|
if (err instanceof McpError)
|
|
998
904
|
throw err;
|
|
999
|
-
|
|
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);
|
|
1000
908
|
}
|
|
1001
909
|
});
|
|
1002
910
|
// Graceful shutdown – close all Redis connections + auto-end brain session
|
|
@@ -1828,7 +1736,7 @@ if (!process.argv[2] && process.stdout.isTTY) {
|
|
|
1828
1736
|
console.log(' \x1b[36m npx @cachly-dev/mcp-server@latest upgrade\x1b[0m ← Check for updates');
|
|
1829
1737
|
console.log('');
|
|
1830
1738
|
console.log(' \x1b[90mWorks with: Claude Code · Cursor · Windsurf · GitHub Copilot · Cline · Zed\x1b[0m');
|
|
1831
|
-
console.log(' \x1b[90mFree forever · GDPR · German servers ·
|
|
1739
|
+
console.log(' \x1b[90mFree forever · GDPR · German servers · 95 MCP tools\x1b[0m');
|
|
1832
1740
|
console.log('');
|
|
1833
1741
|
process.exit(0);
|
|
1834
1742
|
}
|
|
@@ -1984,7 +1892,7 @@ if (process.argv[2] === 'status') {
|
|
|
1984
1892
|
if (process.argv[2] === 'health') {
|
|
1985
1893
|
const { existsSync } = await import('node:fs');
|
|
1986
1894
|
const { readFile } = await import('node:fs/promises');
|
|
1987
|
-
const { resolve } = await import('node:path');
|
|
1895
|
+
const { resolve, join: pJoin } = await import('node:path');
|
|
1988
1896
|
const cwd = process.cwd();
|
|
1989
1897
|
let passed = 0;
|
|
1990
1898
|
let failed = 0;
|
|
@@ -1998,10 +1906,6 @@ if (process.argv[2] === 'health') {
|
|
|
1998
1906
|
if (!jwt) {
|
|
1999
1907
|
fail('CACHLY_JWT not set — run: npx @cachly-dev/mcp-server@latest setup');
|
|
2000
1908
|
}
|
|
2001
|
-
else if (jwt.startsWith('cky_')) {
|
|
2002
|
-
// Opaque cachly API key — no JWT expiry to check, format is valid by prefix
|
|
2003
|
-
ok('API key set (cky_*** opaque key)');
|
|
2004
|
-
}
|
|
2005
1909
|
else {
|
|
2006
1910
|
try {
|
|
2007
1911
|
const parts = jwt.split('.');
|
|
@@ -2021,7 +1925,7 @@ if (process.argv[2] === 'health') {
|
|
|
2021
1925
|
}
|
|
2022
1926
|
}
|
|
2023
1927
|
catch {
|
|
2024
|
-
fail('CACHLY_JWT format invalid (expected
|
|
1928
|
+
fail('CACHLY_JWT format invalid (expected JWT with 3 parts)');
|
|
2025
1929
|
}
|
|
2026
1930
|
}
|
|
2027
1931
|
// ── 2. Brain API reachability ───────────────────────────────────────────────
|
|
@@ -2072,27 +1976,11 @@ if (process.argv[2] === 'health') {
|
|
|
2072
1976
|
});
|
|
2073
1977
|
if (connRes.ok) {
|
|
2074
1978
|
const conn = await connRes.json();
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
pingPassword = conn.password;
|
|
2078
|
-
}
|
|
2079
|
-
else if (conn.connection_string) {
|
|
2080
|
-
try {
|
|
2081
|
-
const u = new URL(conn.connection_string);
|
|
2082
|
-
if (u.password)
|
|
2083
|
-
pingPassword = decodeURIComponent(u.password);
|
|
2084
|
-
}
|
|
2085
|
-
catch { /* leave pingPassword undefined */ }
|
|
2086
|
-
}
|
|
2087
|
-
pingTls = conn.tls_enabled === true;
|
|
2088
|
-
}
|
|
2089
|
-
else {
|
|
2090
|
-
warn(`Could not fetch Redis credentials (HTTP ${connRes.status}) — connectivity test may fail`);
|
|
1979
|
+
pingPassword = conn.password;
|
|
1980
|
+
pingTls = conn.tls_enabled !== false;
|
|
2091
1981
|
}
|
|
2092
1982
|
}
|
|
2093
|
-
catch
|
|
2094
|
-
warn(`Could not fetch Redis credentials — ${connErr.message}`);
|
|
2095
|
-
}
|
|
1983
|
+
catch { /* proceed without password */ }
|
|
2096
1984
|
const pingClient = new Redis({
|
|
2097
1985
|
host: inst.host, port: inst.port,
|
|
2098
1986
|
password: pingPassword,
|
|
@@ -2226,6 +2114,7 @@ if (process.argv[2] === 'setup') {
|
|
|
2226
2114
|
const AUTH_BASE = 'https://auth.cachly.dev/realms/cachly/protocol/openid-connect';
|
|
2227
2115
|
const CLIENT_ID = 'cachly-cli';
|
|
2228
2116
|
console.log('Step 1: Sign in to cachly (free, no credit card)\n');
|
|
2117
|
+
sendFunnelEvent('setup_auth_started');
|
|
2229
2118
|
// Start device flow
|
|
2230
2119
|
let deviceCode = '', userCode = '', verifyUri = '', pollInterval = 5000;
|
|
2231
2120
|
try {
|
|
@@ -2251,6 +2140,7 @@ if (process.argv[2] === 'setup') {
|
|
|
2251
2140
|
rl.close();
|
|
2252
2141
|
process.exit(1);
|
|
2253
2142
|
}
|
|
2143
|
+
sendFunnelEvent('setup_auth_completed');
|
|
2254
2144
|
console.log('');
|
|
2255
2145
|
deviceCode = ''; // mark as fallback so we skip polling
|
|
2256
2146
|
}
|
|
@@ -2283,6 +2173,7 @@ if (process.argv[2] === 'setup') {
|
|
|
2283
2173
|
if (tokenData.access_token) {
|
|
2284
2174
|
token = tokenData.access_token;
|
|
2285
2175
|
console.log(' \x1b[32m✓ Authorized!\x1b[0m\n');
|
|
2176
|
+
sendFunnelEvent('setup_auth_completed');
|
|
2286
2177
|
break;
|
|
2287
2178
|
}
|
|
2288
2179
|
// authorization_pending = keep polling; slow_down = increase interval
|
|
@@ -2323,7 +2214,7 @@ if (process.argv[2] === 'setup') {
|
|
|
2323
2214
|
token = keyBody.key; // swap JWT → cky_live_...
|
|
2324
2215
|
console.log(' ✓\n');
|
|
2325
2216
|
}
|
|
2326
|
-
catch {
|
|
2217
|
+
catch (e) {
|
|
2327
2218
|
console.log(' (skipped)\n');
|
|
2328
2219
|
// Non-fatal: fall back to using the Keycloak JWT directly.
|
|
2329
2220
|
// It will expire but setup still works for now.
|
|
@@ -2376,9 +2267,10 @@ if (process.argv[2] === 'setup') {
|
|
|
2376
2267
|
// Newly created — poll until running or give up after 30 s.
|
|
2377
2268
|
const newId = autoBody.instance_id;
|
|
2378
2269
|
console.log(` ✓ created (${newId.slice(0, 8)}…)\n`);
|
|
2270
|
+
// 45 attempts × 3s = 135s — generous for free-tier in slow regions.
|
|
2379
2271
|
process.stdout.write('⏳ Waiting for instance to start');
|
|
2380
|
-
for (let attempt = 0; attempt <
|
|
2381
|
-
await new Promise(r => setTimeout(r,
|
|
2272
|
+
for (let attempt = 0; attempt < 45; attempt++) {
|
|
2273
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
2382
2274
|
process.stdout.write('.');
|
|
2383
2275
|
try {
|
|
2384
2276
|
const checkRes = await fetch(`${API_URL}/api/v1/instances/${newId}`, { headers: { Authorization: `Bearer ${token}` } });
|
|
@@ -2417,6 +2309,7 @@ if (process.argv[2] === 'setup') {
|
|
|
2417
2309
|
console.log(` (Run with --instance-id <id> to use a different one)\n`);
|
|
2418
2310
|
}
|
|
2419
2311
|
console.log(`✓ Instance: ${instance.name} (${instance.id.slice(0, 8)}…)\n`);
|
|
2312
|
+
sendFunnelEvent('setup_instance_ready', { instance_id: instance.id });
|
|
2420
2313
|
// ── Step 3: Detect editors ────────────────────────────────────────────────
|
|
2421
2314
|
const cwd = process.cwd();
|
|
2422
2315
|
const detected = [];
|
|
@@ -2594,8 +2487,6 @@ if (process.argv[2] === 'setup') {
|
|
|
2594
2487
|
console.log(` Dashboard: https://cachly.dev/instances/${instance.id}`);
|
|
2595
2488
|
console.log(`\n 📛 Add a live badge to your README:`);
|
|
2596
2489
|
console.log(` npx @cachly-dev/mcp-server@latest badge\n`);
|
|
2597
|
-
console.log(` 🤝 Invite a teammate — they'll arrive pre-briefed on your project:`);
|
|
2598
|
-
console.log(` npx @cachly-dev/mcp-server@latest invite\n`);
|
|
2599
2490
|
// ── Step 7: Email opt-in (non-blocking — at the very end) ────────────────
|
|
2600
2491
|
if (!nonInteractive) {
|
|
2601
2492
|
const email = await ask(' 📬 Stay in the loop? Email for release notes [Enter to skip]: ');
|
|
@@ -2667,7 +2558,7 @@ if (process.argv[2] === 'index') {
|
|
|
2667
2558
|
const instanceId = flag('instance-id') ?? process.env.CACHLY_BRAIN_INSTANCE_ID;
|
|
2668
2559
|
const maxFiles = parseInt(flag('max-files') ?? '500', 10);
|
|
2669
2560
|
const namespace = flag('namespace') ?? 'cachly:sem:code';
|
|
2670
|
-
if (!instanceId || !
|
|
2561
|
+
if (!instanceId || !JWT) {
|
|
2671
2562
|
console.error('\n❌ CACHLY_BRAIN_INSTANCE_ID and CACHLY_JWT must be set\n');
|
|
2672
2563
|
console.error(' export CACHLY_BRAIN_INSTANCE_ID=<uuid>');
|
|
2673
2564
|
console.error(' export CACHLY_JWT=<cky_live_...>');
|
|
@@ -2704,7 +2595,7 @@ if (process.argv[2] === 'learn-git') {
|
|
|
2704
2595
|
const repoDir = resolve(flag('dir') ?? argv.find(a => !a.startsWith('--')) ?? '.');
|
|
2705
2596
|
const instanceId = flag('instance-id') ?? process.env.CACHLY_BRAIN_INSTANCE_ID;
|
|
2706
2597
|
const maxCommits = parseInt(flag('max-commits') ?? '50', 10);
|
|
2707
|
-
if (!instanceId || !
|
|
2598
|
+
if (!instanceId || !JWT) {
|
|
2708
2599
|
console.error('\n❌ CACHLY_BRAIN_INSTANCE_ID and CACHLY_JWT must be set\n');
|
|
2709
2600
|
console.error(' export CACHLY_BRAIN_INSTANCE_ID=<uuid>');
|
|
2710
2601
|
console.error(' export CACHLY_JWT=<cky_live_...>');
|
|
@@ -2720,7 +2611,7 @@ if (process.argv[2] === 'learn-git') {
|
|
|
2720
2611
|
limit: maxCommits,
|
|
2721
2612
|
});
|
|
2722
2613
|
console.log(result);
|
|
2723
|
-
sendFunnelEvent('brain_from_git', { instance_id: instanceId });
|
|
2614
|
+
sendFunnelEvent('brain_from_git', { api_key: JWT, instance_id: instanceId });
|
|
2724
2615
|
console.log('\n✅ Brain learned from your commits.\n');
|
|
2725
2616
|
// Let fire-and-forget telemetry flush before exit.
|
|
2726
2617
|
await new Promise(r => setTimeout(r, 300));
|
|
@@ -2736,8 +2627,7 @@ if (process.argv[2] === 'learn-git') {
|
|
|
2736
2627
|
// actionable message in their editor's MCP log instead of silent failures.
|
|
2737
2628
|
// Skip for CLI commands that intentionally run without credentials.
|
|
2738
2629
|
const _cliNoAuthCommands = ['demo', 'share', 'health', 'setup', 'init', 'digest', 'invite', 'badge', 'join', 'upgrade'];
|
|
2739
|
-
|
|
2740
|
-
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)) {
|
|
2741
2631
|
// No args + no JWT in a non-TTY context = running as MCP server without credentials.
|
|
2742
2632
|
// Print actionable setup banner to stdout so it's captured by callers/tests.
|
|
2743
2633
|
const banner = '\n' +
|
|
@@ -2801,79 +2691,12 @@ if (!process.env.CACHLY_NO_UPDATE_CHECK) {
|
|
|
2801
2691
|
})();
|
|
2802
2692
|
}
|
|
2803
2693
|
const httpPort = process.env.PORT ? parseInt(process.env.PORT, 10) : undefined;
|
|
2804
|
-
// ── Per-request MCP Server factory (HTTP / Smithery stateless mode) ──────────
|
|
2805
|
-
// SDK ≥1.9 forbids reusing a stateless transport across requests.
|
|
2806
|
-
// Solution: create a fresh Server+Transport per /mcp request.
|
|
2807
|
-
// The module-level `server` below is used for stdio mode only.
|
|
2808
|
-
function createHttpMcpServer() {
|
|
2809
|
-
const srv = new Server({ name: 'cachly-mcp', version: CURRENT_VERSION }, { capabilities: { tools: {} } });
|
|
2810
|
-
srv.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
2811
|
-
srv.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
2812
|
-
const { name, arguments: args } = request.params;
|
|
2813
|
-
try {
|
|
2814
|
-
const text = await handleTool(name, (args ?? {}));
|
|
2815
|
-
return { content: [{ type: 'text', text }] };
|
|
2816
|
-
}
|
|
2817
|
-
catch (err) {
|
|
2818
|
-
if (err instanceof McpError)
|
|
2819
|
-
throw err;
|
|
2820
|
-
throw new McpError(ErrorCode.InternalError, err.message);
|
|
2821
|
-
}
|
|
2822
|
-
});
|
|
2823
|
-
return srv;
|
|
2824
|
-
}
|
|
2825
2694
|
if (httpPort) {
|
|
2826
2695
|
// ── HTTP mode (Streamable HTTP transport) ───────────────────────────────
|
|
2827
|
-
// Used for Smithery URL deployment: PORT=3000 node dist/index.js
|
|
2828
|
-
// Multi-tenant: per-request JWT + instance ID resolved via:
|
|
2829
|
-
// 1. Authorization: Bearer <jwt> + X-Cachly-Instance-Id: <uuid>
|
|
2830
|
-
// 2. Smithery config: ?config=base64url({CACHLY_JWT, CACHLY_BRAIN_INSTANCE_ID, ...})
|
|
2831
|
-
// 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
|
|
2832
2697
|
const { createServer } = await import('node:http');
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
let instId = '';
|
|
2836
|
-
const auth = req.headers['authorization'];
|
|
2837
|
-
if (typeof auth === 'string' && auth.trim()) {
|
|
2838
|
-
const a = auth.trim();
|
|
2839
|
-
jwtVal = a.toLowerCase().startsWith('bearer ') ? a.slice(7).trim() : a;
|
|
2840
|
-
}
|
|
2841
|
-
// Fallback: raw JWT in custom header (Smithery "header" transport without Bearer prefix)
|
|
2842
|
-
if (!jwtVal) {
|
|
2843
|
-
const rawJwt = req.headers['cachly_jwt'] ?? req.headers['cachly-jwt'] ?? req.headers['x-cachly-jwt'] ?? req.headers['x-api-key'];
|
|
2844
|
-
if (typeof rawJwt === 'string')
|
|
2845
|
-
jwtVal = rawJwt.trim();
|
|
2846
|
-
}
|
|
2847
|
-
const hdrInst = req.headers['x-cachly-instance-id'];
|
|
2848
|
-
if (typeof hdrInst === 'string')
|
|
2849
|
-
instId = hdrInst.trim();
|
|
2850
|
-
// Smithery passes user config as a base64url-encoded JSON blob in ?config=
|
|
2851
|
-
const cfgParam = url.searchParams.get('config');
|
|
2852
|
-
if (cfgParam && (!jwtVal || !instId)) {
|
|
2853
|
-
try {
|
|
2854
|
-
const b64 = cfgParam.replace(/-/g, '+').replace(/_/g, '/');
|
|
2855
|
-
const padded = b64 + '='.repeat((4 - b64.length % 4) % 4);
|
|
2856
|
-
const json = JSON.parse(Buffer.from(padded, 'base64').toString('utf8'));
|
|
2857
|
-
if (!jwtVal && typeof json.CACHLY_JWT === 'string')
|
|
2858
|
-
jwtVal = json.CACHLY_JWT;
|
|
2859
|
-
if (!instId && typeof json.CACHLY_BRAIN_INSTANCE_ID === 'string')
|
|
2860
|
-
instId = json.CACHLY_BRAIN_INSTANCE_ID;
|
|
2861
|
-
}
|
|
2862
|
-
catch { /* malformed config — ignore, fall back to globals */ }
|
|
2863
|
-
}
|
|
2864
|
-
// Direct query params (Smithery "query" transport, manual clients, etc.)
|
|
2865
|
-
if (!jwtVal) {
|
|
2866
|
-
const q = url.searchParams.get('CACHLY_JWT') ?? url.searchParams.get('cachly_jwt') ?? url.searchParams.get('api_key');
|
|
2867
|
-
if (q)
|
|
2868
|
-
jwtVal = q.trim();
|
|
2869
|
-
}
|
|
2870
|
-
if (!instId) {
|
|
2871
|
-
const q = url.searchParams.get('CACHLY_BRAIN_INSTANCE_ID') ?? url.searchParams.get('cachly_brain_instance_id') ?? url.searchParams.get('instance_id');
|
|
2872
|
-
if (q)
|
|
2873
|
-
instId = q.trim();
|
|
2874
|
-
}
|
|
2875
|
-
return { jwt: jwtVal, instanceId: instId };
|
|
2876
|
-
}
|
|
2698
|
+
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
|
|
2699
|
+
await server.connect(transport);
|
|
2877
2700
|
const httpServer = createServer((req, res) => {
|
|
2878
2701
|
const url = new URL(req.url ?? '/', `http://localhost:${httpPort}`);
|
|
2879
2702
|
if (url.pathname === '/health') {
|
|
@@ -2881,8 +2704,6 @@ if (httpPort) {
|
|
|
2881
2704
|
res.end(JSON.stringify({
|
|
2882
2705
|
status: 'ok',
|
|
2883
2706
|
version: CURRENT_VERSION,
|
|
2884
|
-
transport: 'streamable-http',
|
|
2885
|
-
multiTenant: true,
|
|
2886
2707
|
zeroResults: {
|
|
2887
2708
|
total: zeroResultsTotal,
|
|
2888
2709
|
last10: ZERO_RESULTS_LOG.slice(-10).map(e => ({ query: e.query, ts: new Date(e.ts).toISOString() })),
|
|
@@ -2890,52 +2711,8 @@ if (httpPort) {
|
|
|
2890
2711
|
}));
|
|
2891
2712
|
return;
|
|
2892
2713
|
}
|
|
2893
|
-
// Static server card — lets Smithery skip live scanning (avoids auth complexity).
|
|
2894
|
-
if (url.pathname === '/.well-known/mcp/server-card.json') {
|
|
2895
|
-
const card = {
|
|
2896
|
-
name: 'cachly AI Brain',
|
|
2897
|
-
version: CURRENT_VERSION,
|
|
2898
|
-
description: 'Persistent AI memory for Claude Code, Cursor, Copilot, Windsurf & friends. Remembers every lesson, fix, and architecture decision forever.',
|
|
2899
|
-
transport: { type: 'streamable-http', url: 'https://mcp.cachly.dev/mcp' },
|
|
2900
|
-
capabilities: { tools: {} },
|
|
2901
|
-
};
|
|
2902
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
2903
|
-
res.end(JSON.stringify(card));
|
|
2904
|
-
return;
|
|
2905
|
-
}
|
|
2906
2714
|
if (url.pathname === '/mcp' || url.pathname === '/') {
|
|
2907
|
-
|
|
2908
|
-
if (req.method === 'OPTIONS') {
|
|
2909
|
-
res.writeHead(204, {
|
|
2910
|
-
'Access-Control-Allow-Origin': '*',
|
|
2911
|
-
'Access-Control-Allow-Methods': 'GET, POST, DELETE, OPTIONS',
|
|
2912
|
-
'Access-Control-Allow-Headers': 'Authorization, Content-Type, Mcp-Session-Id, X-Cachly-Instance-Id, cachly_jwt, cachly-jwt, x-cachly-jwt, x-api-key',
|
|
2913
|
-
});
|
|
2914
|
-
res.end();
|
|
2915
|
-
return;
|
|
2916
|
-
}
|
|
2917
|
-
const ctx = extractReqCtx(req, url);
|
|
2918
|
-
// Create a fresh Server+Transport for each request (stateless multi-tenant mode).
|
|
2919
|
-
// SDK ≥1.9 throws if a stateless transport is reused across requests.
|
|
2920
|
-
const reqServer = createHttpMcpServer();
|
|
2921
|
-
const reqTransport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
|
|
2922
|
-
reqServer.connect(reqTransport).then(() => {
|
|
2923
|
-
reqCtx.run(ctx, () => {
|
|
2924
|
-
reqTransport.handleRequest(req, res).catch((err) => {
|
|
2925
|
-
process.stderr.write(`cachly-mcp HTTP handler error: ${err.message}\n`);
|
|
2926
|
-
if (!res.headersSent) {
|
|
2927
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
2928
|
-
res.end(JSON.stringify({ error: 'internal_error' }));
|
|
2929
|
-
}
|
|
2930
|
-
});
|
|
2931
|
-
});
|
|
2932
|
-
}).catch((err) => {
|
|
2933
|
-
process.stderr.write(`cachly-mcp connect error: ${err.message}\n`);
|
|
2934
|
-
if (!res.headersSent) {
|
|
2935
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
2936
|
-
res.end(JSON.stringify({ error: 'internal_error' }));
|
|
2937
|
-
}
|
|
2938
|
-
});
|
|
2715
|
+
transport.handleRequest(req, res);
|
|
2939
2716
|
return;
|
|
2940
2717
|
}
|
|
2941
2718
|
res.writeHead(404);
|
|
@@ -2950,4 +2727,5 @@ else {
|
|
|
2950
2727
|
const transport = new StdioServerTransport();
|
|
2951
2728
|
await server.connect(transport);
|
|
2952
2729
|
}
|
|
2730
|
+
notify('cachly', 'startup', { version: CURRENT_VERSION, mode: process.env.MCP_HTTP_PORT ? 'http' : 'stdio' }).catch(() => undefined);
|
|
2953
2731
|
//# sourceMappingURL=index.js.map
|