@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
package/src/index.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { jwtExpiryMs, checkJwt, handleApiError } from './auth.js';
|
|
3
|
+
import { handleTcoTool, TCO_TOOL_NAMES } from './handlers/tco.js';
|
|
4
|
+
import { notify } from './notifier.js';
|
|
3
5
|
import { readFile } from 'node:fs/promises';
|
|
6
|
+
import { execSync } from 'node:child_process';
|
|
7
|
+
import { join } from 'node:path';
|
|
4
8
|
import { fileURLToPath } from 'node:url';
|
|
5
9
|
|
|
6
10
|
// True only when this file is the entry point (not imported by tests or other modules).
|
|
@@ -35,11 +39,24 @@ const _isMain = process.argv[1] != null &&
|
|
|
35
39
|
* ── Auth & Status ────────────────────────────────────────────────────────────
|
|
36
40
|
* • get_api_status – check API health + JWT auth info (Keycloak)
|
|
37
41
|
*
|
|
42
|
+
* ── Travel Chaos Organizer (TCO) ────────────────────────────────────────────
|
|
43
|
+
* • tco_list_trips – list all trips
|
|
44
|
+
* • tco_create_trip – create a new trip
|
|
45
|
+
* • tco_get_timeline – get timeline items for a trip
|
|
46
|
+
* • tco_delete_trip – delete a trip
|
|
47
|
+
* • tco_inbox_list – list Chaos Inbox items
|
|
48
|
+
* • tco_inbox_assign – assign inbox item to a trip
|
|
49
|
+
* • tco_inbox_reject – reject/discard an inbox item
|
|
50
|
+
* • tco_parse_url – fetch a URL and parse with Ollama
|
|
51
|
+
* • tco_import_email – import raw email text
|
|
52
|
+
* (Requires TCO_API_URL env var; auth forwarded from CACHLY_JWT — same Keycloak realm)
|
|
53
|
+
*
|
|
38
54
|
* Configuration (env vars):
|
|
39
55
|
* CACHLY_API_URL – default https://api.cachly.dev
|
|
40
56
|
* CACHLY_JWT – your JWT (Keycloak access token)
|
|
41
57
|
* CACHLY_EMBED_PROVIDER – embedding backend: openai (default), gemini, mistral, cohere, ollama, cachly (server fallback)
|
|
42
58
|
* CACHLY_EMBED_MODEL – override embedding model (optional)
|
|
59
|
+
* TCO_API_URL – Travel Chaos Organizer backend URL (optional, enables tco_* tools)
|
|
43
60
|
*/
|
|
44
61
|
|
|
45
62
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
@@ -52,22 +69,19 @@ import {
|
|
|
52
69
|
McpError,
|
|
53
70
|
} from '@modelcontextprotocol/sdk/types.js';
|
|
54
71
|
import { Redis } from 'ioredis';
|
|
55
|
-
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
56
72
|
|
|
57
73
|
// ── Config ────────────────────────────────────────────────────────────────────
|
|
58
74
|
|
|
59
75
|
const API_URL = process.env.CACHLY_API_URL ?? 'https://api.cachly.dev';
|
|
60
76
|
let JWT = process.env.CACHLY_JWT ?? '';
|
|
61
|
-
const
|
|
77
|
+
const EMBED_MODEL = process.env.CACHLY_EMBED_MODEL ?? '';
|
|
78
|
+
const CURRENT_VERSION = '0.10.49';
|
|
62
79
|
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
const reqCtx = new AsyncLocalStorage<ReqCtx>();
|
|
69
|
-
function getJWT(): string { return reqCtx.getStore()?.jwt || JWT; }
|
|
70
|
-
function getDefaultInstanceId(): string { return reqCtx.getStore()?.instanceId || _defaultInstanceId; }
|
|
80
|
+
// Max time to wait for a freshly-provisioned instance to become "running" before
|
|
81
|
+
// giving up. Free-tier provisioning in high-latency regions can take 45–90s, so the
|
|
82
|
+
// old hard-coded 25s caused spurious "instance_not_reachable" on first run.
|
|
83
|
+
// Override via CACHLY_PROVISION_TIMEOUT_MS.
|
|
84
|
+
const PROVISION_TIMEOUT_MS = Number(process.env.CACHLY_PROVISION_TIMEOUT_MS ?? 90_000);
|
|
71
85
|
|
|
72
86
|
// ── Default Instance Resolution (for Smithery & single-credential setups) ────
|
|
73
87
|
// When CACHLY_BRAIN_INSTANCE_ID is set, tools can omit the instance_id parameter.
|
|
@@ -77,28 +91,15 @@ let _defaultInstanceId: string = process.env.CACHLY_BRAIN_INSTANCE_ID ?? '';
|
|
|
77
91
|
let _defaultInstanceLastAttempt = 0;
|
|
78
92
|
|
|
79
93
|
async function resolveDefaultInstanceId(): Promise<string> {
|
|
80
|
-
// In an HTTP request scope the instance ID is fixed by the caller's header.
|
|
81
|
-
const scopedId = reqCtx.getStore()?.instanceId;
|
|
82
|
-
if (scopedId) return scopedId;
|
|
83
94
|
if (_defaultInstanceId) return _defaultInstanceId;
|
|
84
|
-
if (!
|
|
95
|
+
if (!JWT) return '';
|
|
85
96
|
// Cooldown: don't hammer the API on every tool call after a transient failure.
|
|
86
|
-
// 10 s is enough to avoid rate-limit spam while not blocking the user for too long.
|
|
87
97
|
const now = Date.now();
|
|
88
|
-
if (_defaultInstanceLastAttempt > 0 && now - _defaultInstanceLastAttempt <
|
|
89
|
-
throw new McpError(
|
|
90
|
-
ErrorCode.InvalidRequest,
|
|
91
|
-
'⚠️ Cachly Brain is temporarily unreachable (API error a few seconds ago).\n\n' +
|
|
92
|
-
'The server will retry automatically in a moment. If this persists:\n' +
|
|
93
|
-
'• Run `get_api_status` for a full diagnostic\n' +
|
|
94
|
-
'• Check that CACHLY_JWT is set and valid\n' +
|
|
95
|
-
'• Visit https://cachly.dev/instances to verify your Brain is running'
|
|
96
|
-
);
|
|
97
|
-
};
|
|
98
|
+
if (_defaultInstanceLastAttempt > 0 && now - _defaultInstanceLastAttempt < 30_000) return '';
|
|
98
99
|
_defaultInstanceLastAttempt = now;
|
|
99
100
|
try {
|
|
100
101
|
const res = await fetch(`${API_URL}/api/v1/instances`, {
|
|
101
|
-
headers: { Authorization: `Bearer ${
|
|
102
|
+
headers: { Authorization: `Bearer ${JWT}`, Accept: 'application/json' },
|
|
102
103
|
signal: AbortSignal.timeout(5000),
|
|
103
104
|
});
|
|
104
105
|
if (!res.ok) return '';
|
|
@@ -185,28 +186,38 @@ async function pollDeviceFlow(flow: DeviceFlowState): Promise<'pending' | 'expir
|
|
|
185
186
|
} catch { /* use raw JWT as fallback */ }
|
|
186
187
|
JWT = apiKey;
|
|
187
188
|
setEmbedJwt(apiKey); // keep embeddings.ts in sync
|
|
188
|
-
// Persist API key to ~/.claude/mcp.json so the next editor restart
|
|
189
|
-
// doesn't start over from a missing CACHLY_JWT (fixes BrainRecallCount=0 bug).
|
|
190
|
-
try {
|
|
191
|
-
const { resolve: _res, dirname: _dir } = await import('node:path');
|
|
192
|
-
const { homedir: _hd } = await import('node:os');
|
|
193
|
-
const { writeFile: _wf, mkdir: _mk, readFile: _rf } = await import('node:fs/promises');
|
|
194
|
-
const { existsSync: _ex } = await import('node:fs');
|
|
195
|
-
const globalJwtPath = _res(_hd(), '.claude', 'mcp.json');
|
|
196
|
-
await _mk(_dir(globalJwtPath), { recursive: true });
|
|
197
|
-
let cfg: { mcpServers?: Record<string, { env?: Record<string, string> }> } = {};
|
|
198
|
-
if (_ex(globalJwtPath)) {
|
|
199
|
-
try { cfg = JSON.parse(await _rf(globalJwtPath, 'utf-8')) as typeof cfg; } catch { /* corrupt — start fresh */ }
|
|
200
|
-
}
|
|
201
|
-
cfg.mcpServers ??= {};
|
|
202
|
-
cfg.mcpServers['cachly'] ??= {};
|
|
203
|
-
cfg.mcpServers['cachly'].env ??= {};
|
|
204
|
-
cfg.mcpServers['cachly'].env['CACHLY_JWT'] = apiKey;
|
|
205
|
-
if (_defaultInstanceId) cfg.mcpServers['cachly'].env['CACHLY_BRAIN_INSTANCE_ID'] = _defaultInstanceId;
|
|
206
|
-
await _wf(globalJwtPath, JSON.stringify(cfg, null, 2), 'utf-8');
|
|
207
|
-
} catch { /* non-fatal — editor will just prompt again on next restart */ }
|
|
208
189
|
_deviceFlow = null;
|
|
209
190
|
sendFunnelEvent('device_flow_completed');
|
|
191
|
+
// Persist the API key to ~/.claude/mcp.json so subsequent MCP server restarts
|
|
192
|
+
// keep the token. Without this, JWT is lost on restart and brain_recall_count
|
|
193
|
+
// is never incremented (telemetry has no api_key → no tenant resolution).
|
|
194
|
+
void (async () => {
|
|
195
|
+
try {
|
|
196
|
+
const { writeFile, mkdir, readFile } = await import('node:fs/promises');
|
|
197
|
+
const { existsSync } = await import('node:fs');
|
|
198
|
+
const { resolve, dirname } = await import('node:path');
|
|
199
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? '';
|
|
200
|
+
if (!home) return;
|
|
201
|
+
const configPath = resolve(home, '.claude', 'mcp.json');
|
|
202
|
+
let cfg: { mcpServers?: Record<string, { command?: string; args?: string[]; env?: Record<string, string> }> } = {};
|
|
203
|
+
if (existsSync(configPath)) {
|
|
204
|
+
try { cfg = JSON.parse(await readFile(configPath, 'utf-8')) as typeof cfg; } catch { /* corrupt — start fresh */ }
|
|
205
|
+
}
|
|
206
|
+
cfg.mcpServers ??= {};
|
|
207
|
+
const existing = cfg.mcpServers['cachly'];
|
|
208
|
+
if (existing) {
|
|
209
|
+
existing.env ??= {};
|
|
210
|
+
existing.env['CACHLY_JWT'] = apiKey;
|
|
211
|
+
} else {
|
|
212
|
+
cfg.mcpServers['cachly'] = {
|
|
213
|
+
command: 'npx', args: ['-y', '@cachly-dev/mcp-server@latest'],
|
|
214
|
+
env: { CACHLY_JWT: apiKey },
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
await mkdir(dirname(configPath), { recursive: true });
|
|
218
|
+
await writeFile(configPath, JSON.stringify(cfg, null, 2), 'utf-8');
|
|
219
|
+
} catch { /* non-critical — don't break the flow on filesystem errors */ }
|
|
220
|
+
})();
|
|
210
221
|
// Auto-provision: find or create the user's brain instance.
|
|
211
222
|
_defaultInstanceLastAttempt = 0;
|
|
212
223
|
await resolveDefaultInstanceId();
|
|
@@ -214,7 +225,7 @@ async function pollDeviceFlow(flow: DeviceFlowState): Promise<'pending' | 'expir
|
|
|
214
225
|
try {
|
|
215
226
|
const autoRes = await fetch(`${API_URL}/api/v1/instances/auto`, {
|
|
216
227
|
method: 'POST',
|
|
217
|
-
headers: { Authorization: `Bearer ${
|
|
228
|
+
headers: { Authorization: `Bearer ${JWT}`, 'Content-Type': 'application/json' },
|
|
218
229
|
signal: AbortSignal.timeout(12000),
|
|
219
230
|
});
|
|
220
231
|
if (autoRes.ok) {
|
|
@@ -229,7 +240,7 @@ async function pollDeviceFlow(flow: DeviceFlowState): Promise<'pending' | 'expir
|
|
|
229
240
|
// Log so telemetry can detect free-tier auto-provision failures
|
|
230
241
|
void fetch(`${API_URL}/api/v1/telemetry/mcp`, {
|
|
231
242
|
method: 'POST',
|
|
232
|
-
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${
|
|
243
|
+
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${JWT}` },
|
|
233
244
|
body: JSON.stringify({ event: 'auto_provision_failed', status: autoRes.status, version: CURRENT_VERSION }),
|
|
234
245
|
signal: AbortSignal.timeout(3000),
|
|
235
246
|
}).catch(() => {});
|
|
@@ -241,7 +252,7 @@ async function pollDeviceFlow(flow: DeviceFlowState): Promise<'pending' | 'expir
|
|
|
241
252
|
if (_defaultInstanceId) {
|
|
242
253
|
try {
|
|
243
254
|
const checkRes = await fetch(`${API_URL}/api/v1/instances/${_defaultInstanceId}`, {
|
|
244
|
-
headers: { Authorization: `Bearer ${
|
|
255
|
+
headers: { Authorization: `Bearer ${JWT}`, Accept: 'application/json' },
|
|
245
256
|
signal: AbortSignal.timeout(4000),
|
|
246
257
|
});
|
|
247
258
|
if (checkRes.ok) {
|
|
@@ -252,7 +263,7 @@ async function pollDeviceFlow(flow: DeviceFlowState): Promise<'pending' | 'expir
|
|
|
252
263
|
while (checkInst.status === 'provisioning' && Date.now() < pDeadline) {
|
|
253
264
|
await new Promise(r => setTimeout(r, 3000));
|
|
254
265
|
const rr = await fetch(`${API_URL}/api/v1/instances/${_defaultInstanceId}`, {
|
|
255
|
-
headers: { Authorization: `Bearer ${
|
|
266
|
+
headers: { Authorization: `Bearer ${JWT}`, Accept: 'application/json' },
|
|
256
267
|
signal: AbortSignal.timeout(4000),
|
|
257
268
|
}).catch(() => null);
|
|
258
269
|
if (rr?.ok) {
|
|
@@ -264,67 +275,51 @@ async function pollDeviceFlow(flow: DeviceFlowState): Promise<'pending' | 'expir
|
|
|
264
275
|
}
|
|
265
276
|
} catch { /* non-fatal — getConnection will handle the wait if still provisioning */ }
|
|
266
277
|
}
|
|
278
|
+
// Persist instance_id to ~/.claude/mcp.json after it's known.
|
|
279
|
+
// Without this, every restart loses CACHLY_BRAIN_INSTANCE_ID → resolveDefaultInstanceId
|
|
280
|
+
// makes an extra API call on every startup (with 30s cooldown on failure).
|
|
281
|
+
if (_defaultInstanceId) {
|
|
282
|
+
void (async () => {
|
|
283
|
+
try {
|
|
284
|
+
const { writeFile, readFile } = await import('node:fs/promises');
|
|
285
|
+
const { existsSync } = await import('node:fs');
|
|
286
|
+
const { resolve } = await import('node:path');
|
|
287
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? '';
|
|
288
|
+
if (!home) return;
|
|
289
|
+
const configPath = resolve(home, '.claude', 'mcp.json');
|
|
290
|
+
if (!existsSync(configPath)) return;
|
|
291
|
+
let cfg: { mcpServers?: Record<string, { env?: Record<string, string> }> } = {};
|
|
292
|
+
try { cfg = JSON.parse(await readFile(configPath, 'utf-8')) as typeof cfg; } catch { return; }
|
|
293
|
+
const entry = cfg.mcpServers?.['cachly'];
|
|
294
|
+
if (entry?.env) {
|
|
295
|
+
entry.env['CACHLY_BRAIN_INSTANCE_ID'] = _defaultInstanceId;
|
|
296
|
+
await writeFile(configPath, JSON.stringify(cfg, null, 2), 'utf-8');
|
|
297
|
+
}
|
|
298
|
+
} catch { /* non-critical */ }
|
|
299
|
+
})();
|
|
300
|
+
}
|
|
267
301
|
return 'done';
|
|
268
302
|
}
|
|
269
303
|
if (data.error === 'slow_down') flow.pollInterval = Math.min(flow.pollInterval + 2000, 15000);
|
|
270
304
|
return 'pending';
|
|
271
305
|
} catch { return 'pending'; }
|
|
272
306
|
}
|
|
273
|
-
// ──
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
let _silentSetupAttempted = false;
|
|
279
|
-
async function silentAutoSetup(): Promise<boolean> {
|
|
280
|
-
if (_silentSetupAttempted) return false;
|
|
281
|
-
_silentSetupAttempted = true;
|
|
282
|
-
try {
|
|
283
|
-
type TrialResp = { api_key: string; expires_at?: string; instance_id?: string; trial?: boolean };
|
|
284
|
-
const res = await fetch(`${API_URL}/auth/instant-trial`, {
|
|
285
|
-
method: 'POST',
|
|
286
|
-
headers: { 'Content-Type': 'application/json' },
|
|
287
|
-
body: JSON.stringify({}),
|
|
288
|
-
signal: AbortSignal.timeout(8000),
|
|
289
|
-
});
|
|
290
|
-
if (!res.ok) return false;
|
|
291
|
-
const data = await res.json() as TrialResp;
|
|
292
|
-
if (!data.api_key) return false;
|
|
307
|
+
// ── Confidence utils (imported from confidence.ts) ──────────────────────────
|
|
308
|
+
import { calculateConfidence, confidenceBadge, STRUCTURED_TEMPLATES,
|
|
309
|
+
CONFIDENCE_WARN_VALUE, CONFIDENCE_STALE_VALUE, CONFIDENCE_WARN_DAYS,
|
|
310
|
+
CONFIDENCE_STALE_DAYS,
|
|
311
|
+
simpleHash } from './confidence.js';
|
|
293
312
|
|
|
294
|
-
JWT = data.api_key;
|
|
295
|
-
setEmbedJwt(data.api_key);
|
|
296
|
-
if (data.instance_id) _defaultInstanceId = data.instance_id;
|
|
297
|
-
sendFunnelEvent('trial_provisioned', { version: CURRENT_VERSION });
|
|
298
313
|
|
|
299
|
-
// Persist credentials so the next editor restart doesn't re-provision.
|
|
300
|
-
try {
|
|
301
|
-
const { resolve: _rp, dirname: _dn } = await import('node:path');
|
|
302
|
-
const { homedir: _hd } = await import('node:os');
|
|
303
|
-
const { writeFile: _wf, mkdir: _mk, readFile: _rf } = await import('node:fs/promises');
|
|
304
|
-
const { existsSync: _ex } = await import('node:fs');
|
|
305
|
-
const cfgPath = _rp(_hd(), '.claude', 'mcp.json');
|
|
306
|
-
await _mk(_dn(cfgPath), { recursive: true });
|
|
307
|
-
let cfg: { mcpServers?: Record<string, { env?: Record<string, string> }> } = {};
|
|
308
|
-
if (_ex(cfgPath)) {
|
|
309
|
-
try { cfg = JSON.parse(await _rf(cfgPath, 'utf-8')) as typeof cfg; } catch { /* corrupt — overwrite */ }
|
|
310
|
-
}
|
|
311
|
-
cfg.mcpServers ??= {};
|
|
312
|
-
cfg.mcpServers['cachly'] ??= {};
|
|
313
|
-
cfg.mcpServers['cachly'].env ??= {};
|
|
314
|
-
cfg.mcpServers['cachly'].env['CACHLY_JWT'] = data.api_key;
|
|
315
|
-
if (_defaultInstanceId) cfg.mcpServers['cachly'].env['CACHLY_BRAIN_INSTANCE_ID'] = _defaultInstanceId;
|
|
316
|
-
await _wf(cfgPath, JSON.stringify(cfg, null, 2), 'utf-8');
|
|
317
|
-
} catch { /* non-fatal — trial is active for this session even if persist fails */ }
|
|
318
|
-
return true;
|
|
319
|
-
} catch { return false; }
|
|
320
|
-
}
|
|
321
314
|
// ── Embeddings (imported from embeddings.ts) ────────────────────────────────
|
|
322
|
-
import { setEmbedJwt
|
|
315
|
+
import { embedConfig, setEmbedJwt, EMBED_PROVIDER, computeEmbedding,
|
|
316
|
+
hasEmbedProvider, embedProviderHint } from './embeddings.js';
|
|
323
317
|
|
|
324
318
|
// ── Search Engine (imported from search.ts) ─────────────────────────────────
|
|
325
319
|
import { tokenize, splitMultiQuery, levenshtein, recencyBoost, extractTimestamp, STOPWORDS,
|
|
326
320
|
katakanaToRomaji, arabicLightStem, expandCrossLingual, CROSS_LINGUAL_MAP,
|
|
327
|
-
ZERO_RESULTS_LOG, zeroResultsTotal } from './search.js';
|
|
321
|
+
keywordSearch, ZERO_RESULTS_LOG, zeroResultsTotal, indexVocab as _indexVocab } from './search.js';
|
|
322
|
+
import type { KeywordMatch } from './search.js';
|
|
328
323
|
|
|
329
324
|
// ── Exported for testing (re-export from search.ts) ─────────────────────────
|
|
330
325
|
export { tokenize, splitMultiQuery, levenshtein, recencyBoost, extractTimestamp, STOPWORDS,
|
|
@@ -333,6 +328,19 @@ export { tokenize, splitMultiQuery, levenshtein, recencyBoost, extractTimestamp,
|
|
|
333
328
|
// ── Types ─────────────────────────────────────────────────────────────────────
|
|
334
329
|
// Instance is imported from handlers/brain.ts
|
|
335
330
|
|
|
331
|
+
interface CreateResponse {
|
|
332
|
+
instance_id: string;
|
|
333
|
+
checkout_url?: string;
|
|
334
|
+
status: string;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
interface SemanticSearchResponse {
|
|
338
|
+
found: boolean;
|
|
339
|
+
id?: string;
|
|
340
|
+
similarity?: number;
|
|
341
|
+
prompt?: string;
|
|
342
|
+
}
|
|
343
|
+
|
|
336
344
|
// ── Connection pool ───────────────────────────────────────────────────────────
|
|
337
345
|
|
|
338
346
|
/** Reuse Redis connections across tool calls (keyed by instance_id). */
|
|
@@ -349,27 +357,16 @@ async function getConnection(instance_id: string): Promise<Redis> {
|
|
|
349
357
|
);
|
|
350
358
|
}
|
|
351
359
|
|
|
352
|
-
if (pool.has(instance_id))
|
|
353
|
-
const cached = pool.get(instance_id)!;
|
|
354
|
-
// Verify the pooled connection is still alive before reusing it.
|
|
355
|
-
// ioredis 'error' eviction is async — a connection can be in the pool
|
|
356
|
-
// but already internally broken (status 'reconnecting' or 'end').
|
|
357
|
-
const status = (cached as { status?: string }).status;
|
|
358
|
-
if (status === 'ready' || status === 'connect') {
|
|
359
|
-
return cached;
|
|
360
|
-
}
|
|
361
|
-
// Stale — evict and fall through to reconnect.
|
|
362
|
-
pool.delete(instance_id);
|
|
363
|
-
cached.disconnect();
|
|
364
|
-
}
|
|
360
|
+
if (pool.has(instance_id)) return pool.get(instance_id)!;
|
|
365
361
|
|
|
366
|
-
// Fetch instance, waiting up to
|
|
367
|
-
//
|
|
362
|
+
// Fetch instance, waiting up to PROVISION_TIMEOUT_MS if it is still provisioning.
|
|
363
|
+
// This covers the zero-friction path: device-flow auth → auto-provision → first tool call
|
|
364
|
+
// all happen in quick succession and the instance isn't running yet.
|
|
368
365
|
let inst = await apiFetch<Instance>(`/api/v1/instances/${instance_id}`);
|
|
369
366
|
if (inst.status === 'provisioning') {
|
|
370
|
-
const deadline = Date.now() +
|
|
367
|
+
const deadline = Date.now() + PROVISION_TIMEOUT_MS;
|
|
371
368
|
while (inst.status === 'provisioning' && Date.now() < deadline) {
|
|
372
|
-
await new Promise(r => setTimeout(r,
|
|
369
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
373
370
|
inst = await apiFetch<Instance>(`/api/v1/instances/${instance_id}`);
|
|
374
371
|
}
|
|
375
372
|
}
|
|
@@ -378,7 +375,7 @@ async function getConnection(instance_id: string): Promise<Redis> {
|
|
|
378
375
|
// Telemetry: unreachable instance so the team can proactively investigate
|
|
379
376
|
void fetch(`${API_URL}/api/v1/telemetry/mcp`, {
|
|
380
377
|
method: 'POST',
|
|
381
|
-
headers: { 'Content-Type': 'application/json', ...(
|
|
378
|
+
headers: { 'Content-Type': 'application/json', ...(JWT ? { Authorization: `Bearer ${JWT}` } : {}) },
|
|
382
379
|
body: JSON.stringify({ event: 'instance_not_reachable', instance_id, status: inst.status, version: CURRENT_VERSION }),
|
|
383
380
|
signal: AbortSignal.timeout(3000),
|
|
384
381
|
}).catch(() => {});
|
|
@@ -409,9 +406,7 @@ async function getConnection(instance_id: string): Promise<Redis> {
|
|
|
409
406
|
|
|
410
407
|
// Fetch connection details (includes password) from the dedicated endpoint.
|
|
411
408
|
let password: string | undefined;
|
|
412
|
-
|
|
413
|
-
// Only enable TLS when the API explicitly returns tls_enabled: true.
|
|
414
|
-
let tlsEnabled = inst.tls_enabled === true;
|
|
409
|
+
let tlsEnabled = inst.tls_enabled !== false; // default true
|
|
415
410
|
try {
|
|
416
411
|
const conn = await apiFetch<{ password?: string; tls_enabled?: boolean }>(
|
|
417
412
|
`/api/v1/instances/${instance_id}/connection`
|
|
@@ -433,11 +428,11 @@ async function getConnection(instance_id: string): Promise<Redis> {
|
|
|
433
428
|
host: inst.host,
|
|
434
429
|
port: inst.port,
|
|
435
430
|
password: password || undefined,
|
|
431
|
+
commandTimeout: 8000,
|
|
436
432
|
...(tlsEnabled ? { tls: {} } : {}),
|
|
437
433
|
lazyConnect: true,
|
|
438
434
|
enableReadyCheck: true,
|
|
439
435
|
connectTimeout: 8000,
|
|
440
|
-
commandTimeout: 10000,
|
|
441
436
|
retryStrategy: () => null, // fail fast, no reconnect loops in MCP context
|
|
442
437
|
});
|
|
443
438
|
|
|
@@ -445,46 +440,14 @@ async function getConnection(instance_id: string): Promise<Redis> {
|
|
|
445
440
|
client.on('error', evict);
|
|
446
441
|
client.on('end', evict);
|
|
447
442
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
for (let attempt = 0; attempt < 3; attempt++) {
|
|
452
|
-
try {
|
|
453
|
-
if (attempt > 0) {
|
|
454
|
-
await new Promise(r => setTimeout(r, 2000 * attempt));
|
|
455
|
-
// Fresh client on retry — ioredis doesn't allow reconnect after explicit disconnect.
|
|
456
|
-
client.disconnect();
|
|
457
|
-
const retryClient = new Redis({
|
|
458
|
-
host: inst.host,
|
|
459
|
-
port: inst.port,
|
|
460
|
-
password: password || undefined,
|
|
461
|
-
...(tlsEnabled ? { tls: {} } : {}),
|
|
462
|
-
lazyConnect: true,
|
|
463
|
-
enableReadyCheck: true,
|
|
464
|
-
connectTimeout: 8000,
|
|
465
|
-
commandTimeout: 10000,
|
|
466
|
-
retryStrategy: () => null,
|
|
467
|
-
});
|
|
468
|
-
retryClient.on('error', () => pool.delete(instance_id));
|
|
469
|
-
retryClient.on('end', () => pool.delete(instance_id));
|
|
470
|
-
await retryClient.connect();
|
|
471
|
-
pool.set(instance_id, retryClient);
|
|
472
|
-
return retryClient;
|
|
473
|
-
}
|
|
474
|
-
await client.connect();
|
|
475
|
-
connectErr = undefined;
|
|
476
|
-
break;
|
|
477
|
-
} catch (err) {
|
|
478
|
-
connectErr = err;
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
if (connectErr) {
|
|
443
|
+
try {
|
|
444
|
+
await client.connect();
|
|
445
|
+
} catch (err: unknown) {
|
|
483
446
|
client.disconnect();
|
|
484
|
-
const msg =
|
|
447
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
485
448
|
void fetch(`${API_URL}/api/v1/telemetry/mcp`, {
|
|
486
449
|
method: 'POST',
|
|
487
|
-
headers: { 'Content-Type': 'application/json', ...(
|
|
450
|
+
headers: { 'Content-Type': 'application/json', ...(JWT ? { Authorization: `Bearer ${JWT}` } : {}) },
|
|
488
451
|
body: JSON.stringify({ event: 'redis_connect_failed', instance_id, host: inst.host, port: inst.port, error: msg, version: CURRENT_VERSION }),
|
|
489
452
|
signal: AbortSignal.timeout(3000),
|
|
490
453
|
}).catch(() => {});
|
|
@@ -504,14 +467,14 @@ async function getConnection(instance_id: string): Promise<Redis> {
|
|
|
504
467
|
// ── API helper ────────────────────────────────────────────────────────────────
|
|
505
468
|
|
|
506
469
|
async function apiFetch<T>(path: string, options: RequestInit = {}): Promise<T> {
|
|
507
|
-
checkJwt(
|
|
470
|
+
checkJwt(JWT);
|
|
508
471
|
const timeoutMs = Number(process.env.CACHLY_API_TIMEOUT_MS ?? 15_000);
|
|
509
472
|
const res = await fetch(`${API_URL}${path}`, {
|
|
510
473
|
...options,
|
|
511
474
|
signal: AbortSignal.timeout(timeoutMs),
|
|
512
475
|
headers: {
|
|
513
476
|
'Content-Type': 'application/json',
|
|
514
|
-
Authorization: `Bearer ${
|
|
477
|
+
Authorization: `Bearer ${JWT}`,
|
|
515
478
|
...options.headers,
|
|
516
479
|
},
|
|
517
480
|
});
|
|
@@ -524,6 +487,11 @@ async function apiFetch<T>(path: string, options: RequestInit = {}): Promise<T>
|
|
|
524
487
|
return res.json() as Promise<T>;
|
|
525
488
|
}
|
|
526
489
|
|
|
490
|
+
import { detectNamespace } from './namespace.js';
|
|
491
|
+
|
|
492
|
+
// ── CKG (imported from ckg.ts) ─────────────────────────────────────────────
|
|
493
|
+
import type { CKGEdge, CKGNode } from './ckg.js';
|
|
494
|
+
import { ckgSlug, extractProblemConcept, ckgUpsertNode, ckgUpdateEdge } from './ckg.js';
|
|
527
495
|
import { handleBrainTool } from './handlers/brain.js';
|
|
528
496
|
import { handleContextTool } from './handlers/context.js';
|
|
529
497
|
import { handleInstanceTool } from './handlers/instances.js';
|
|
@@ -532,7 +500,7 @@ import { handleTeamTool } from './handlers/team.js';
|
|
|
532
500
|
import { handleRoadmapTool } from './handlers/roadmap.js';
|
|
533
501
|
import { handleAdvancedTool } from './handlers/advanced.js';
|
|
534
502
|
import { handleSyndicateTool } from './handlers/syndicate.js';
|
|
535
|
-
import { handleFedbrainTool } from './handlers/fedbrain.js';
|
|
503
|
+
import { handleFedbrainTool, _lastBrainFromGitCounts } from './handlers/fedbrain.js';
|
|
536
504
|
import type { Instance } from './handlers/brain.js';
|
|
537
505
|
|
|
538
506
|
// ── Tools (imported from tools.ts) ─────────────────────────────────────────
|
|
@@ -561,10 +529,7 @@ function sendFunnelEvent(event: string, extra?: Record<string, unknown>): void {
|
|
|
561
529
|
headers: { 'Content-Type': 'application/json' },
|
|
562
530
|
body: JSON.stringify({
|
|
563
531
|
event, version: CURRENT_VERSION, editor: detectEditor(),
|
|
564
|
-
|
|
565
|
-
// Raw Keycloak JWTs (eyJ...) are intentionally excluded — they are short-lived bearer tokens
|
|
566
|
-
// that should never leave the MCP process boundary.
|
|
567
|
-
...(getJWT().startsWith('cky_') ? { api_key: getJWT() } : {}),
|
|
532
|
+
...(JWT ? { jwt: JWT } : {}),
|
|
568
533
|
...extra,
|
|
569
534
|
}),
|
|
570
535
|
signal: AbortSignal.timeout(3000),
|
|
@@ -586,20 +551,12 @@ async function sendAnonymousTelemetry(toolName: string): Promise<void> {
|
|
|
586
551
|
}
|
|
587
552
|
|
|
588
553
|
async function handleTool(name: string, args: Record<string, unknown>): Promise<string> {
|
|
589
|
-
// Guard: if no JWT,
|
|
590
|
-
if (!
|
|
591
|
-
// ── Step 1: Silent instant-trial (zero-click) ────────────────────────
|
|
592
|
-
// On the very first tool call, attempt to silently provision a free trial.
|
|
593
|
-
// If it succeeds the user never sees an auth prompt — Brain is just active.
|
|
594
|
-
const provisioned = await silentAutoSetup();
|
|
595
|
-
if (provisioned) {
|
|
596
|
-
return handleTool(name, args); // Transparent re-entry with active credentials
|
|
597
|
-
}
|
|
598
|
-
|
|
554
|
+
// Guard: if no JWT, return actionable onboarding message instead of HTTP 401
|
|
555
|
+
if (!JWT) {
|
|
599
556
|
void sendAnonymousTelemetry(name);
|
|
600
557
|
|
|
601
|
-
// ──
|
|
602
|
-
// 1st call
|
|
558
|
+
// ── Zero-credential device flow ─────────────────────────────────────
|
|
559
|
+
// 1st call: start device flow, return code + URL
|
|
603
560
|
// 2nd+ calls: poll for token; once authenticated, proceed transparently
|
|
604
561
|
if (_deviceFlow) {
|
|
605
562
|
const result = await pollDeviceFlow(_deviceFlow);
|
|
@@ -647,7 +604,7 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
|
|
|
647
604
|
'',
|
|
648
605
|
'After sign-in: call **any tool again** — your Brain activates instantly.',
|
|
649
606
|
'',
|
|
650
|
-
'✨ Free forever · No credit card ·
|
|
607
|
+
'✨ Free forever · No credit card · 95 MCP tools · GDPR · EU servers',
|
|
651
608
|
].join('\n');
|
|
652
609
|
}
|
|
653
610
|
|
|
@@ -673,16 +630,16 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
|
|
|
673
630
|
}
|
|
674
631
|
|
|
675
632
|
// Delegate brain tools (learn, recall, session, etc.)
|
|
676
|
-
|
|
633
|
+
const brainResult = await handleBrainTool(name, args, getConnection, apiFetch);
|
|
677
634
|
if (brainResult !== null) {
|
|
678
|
-
if (!_firstCallSuccessSent &&
|
|
635
|
+
if (!_firstCallSuccessSent && JWT) {
|
|
679
636
|
_firstCallSuccessSent = true;
|
|
680
|
-
sendFunnelEvent('first_call_success', { tool: name, instance_id: args.instance_id ??
|
|
637
|
+
sendFunnelEvent('first_call_success', { tool: name, instance_id: args.instance_id ?? _defaultInstanceId ?? '' });
|
|
681
638
|
}
|
|
682
639
|
// Per-tool telemetry — fires after every successful brain tool call.
|
|
683
640
|
// api_key carries the cky_live_... token so the backend can resolve tenant + increment counters.
|
|
684
|
-
const instanceId = (args.instance_id as string | undefined) ??
|
|
685
|
-
const telemetryExtra =
|
|
641
|
+
const instanceId = (args.instance_id as string | undefined) ?? _defaultInstanceId ?? '';
|
|
642
|
+
const telemetryExtra = JWT ? { api_key: JWT, instance_id: instanceId } : { instance_id: instanceId };
|
|
686
643
|
if (name === 'recall_best_solution') {
|
|
687
644
|
// recall_best_solution → increments BrainRecallCount + triggers level-up logic
|
|
688
645
|
sendFunnelEvent('recall_best_solution', telemetryExtra);
|
|
@@ -698,50 +655,25 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
|
|
|
698
655
|
if (!isFirstSession && resultText.length > 100) {
|
|
699
656
|
// Existing brain — session_start acts as implicit recall → increment counter.
|
|
700
657
|
sendFunnelEvent('recall_best_solution', telemetryExtra);
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
// We check a sentinel key in Valkey so this only fires once per workspace path,
|
|
705
|
-
// not just on the very first session (handles cases where brain was empty but
|
|
706
|
-
// session data existed, or where the user re-installed).
|
|
707
|
-
if (args.workspace_path) {
|
|
658
|
+
} else if (isFirstSession && args.workspace_path) {
|
|
659
|
+
// First session with a known workspace: auto-bootstrap from git history.
|
|
660
|
+
// Runs synchronously so the user sees the result in the same response.
|
|
708
661
|
try {
|
|
709
|
-
const
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
brainResult
|
|
662
|
+
const gitBootstrap = await handleFedbrainTool('brain_from_git', {
|
|
663
|
+
instance_id: instanceId,
|
|
664
|
+
repo_path: args.workspace_path as string,
|
|
665
|
+
limit: 50,
|
|
666
|
+
}, getConnection, apiFetch);
|
|
667
|
+
if (gitBootstrap !== null) {
|
|
668
|
+
const gitCounts = _lastBrainFromGitCounts;
|
|
669
|
+
sendFunnelEvent('brain_from_git', { ...telemetryExtra, ...(gitCounts ?? {}) });
|
|
670
|
+
sendFunnelEvent('recall_best_solution', telemetryExtra);
|
|
671
|
+
// Append bootstrap summary to the session_start briefing.
|
|
672
|
+
// brainResult is always a string here (handleBrainTool returns string for session_start).
|
|
673
|
+
const bootstrapText = String(gitBootstrap);
|
|
674
|
+
if (bootstrapText) {
|
|
675
|
+
return String(brainResult) + '\n---\n' + bootstrapText;
|
|
723
676
|
}
|
|
724
|
-
// Fire-and-forget — never block the session_start response
|
|
725
|
-
void handleFedbrainTool('brain_from_git', {
|
|
726
|
-
instance_id: instanceId,
|
|
727
|
-
repo_path: workspacePath,
|
|
728
|
-
limit: 50,
|
|
729
|
-
}, getConnection, apiFetch).then(async (gitBootstrap) => {
|
|
730
|
-
if (gitBootstrap !== null) {
|
|
731
|
-
sendFunnelEvent('brain_from_git', telemetryExtra);
|
|
732
|
-
sendFunnelEvent('recall_best_solution', telemetryExtra);
|
|
733
|
-
// Store ingested count for the next session_start wow moment
|
|
734
|
-
try {
|
|
735
|
-
const ingested = (gitBootstrap as string).match(/Ingested:\s*\*\*(\d+)\*/);
|
|
736
|
-
if (ingested) {
|
|
737
|
-
const r = await getConnection(instanceId);
|
|
738
|
-
await r.set('cachly:brain_from_git:auto_bootstrap_result',
|
|
739
|
-
JSON.stringify({ count: parseInt(ingested[1]!, 10), ts: new Date().toISOString() }),
|
|
740
|
-
'EX', 7 * 86400);
|
|
741
|
-
}
|
|
742
|
-
} catch { /* non-critical — must never break the session */ }
|
|
743
|
-
}
|
|
744
|
-
}).catch(() => { /* non-critical */ });
|
|
745
677
|
}
|
|
746
678
|
} catch { /* git bootstrap errors must never break session_start */ }
|
|
747
679
|
}
|
|
@@ -755,7 +687,7 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
|
|
|
755
687
|
const srText = typeof brainResult === 'object' && brainResult !== null && 'content' in brainResult
|
|
756
688
|
? JSON.stringify(brainResult)
|
|
757
689
|
: String(brainResult ?? '');
|
|
758
|
-
if (srText.length > 50 && !srText.includes('No lessons found') && !srText.includes('no lessons')) {
|
|
690
|
+
if (srText.length > 50 && !srText.includes('No lessons found') && !srText.includes('no lessons') && !srText.includes('No matches found')) {
|
|
759
691
|
sendFunnelEvent('recall_best_solution', telemetryExtra);
|
|
760
692
|
}
|
|
761
693
|
}
|
|
@@ -765,9 +697,9 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
|
|
|
765
697
|
// Delegate context tools (remember/recall/list/forget)
|
|
766
698
|
const contextResult = await handleContextTool(name, args, getConnection, apiFetch);
|
|
767
699
|
if (contextResult !== null) {
|
|
768
|
-
if (!_firstCallSuccessSent &&
|
|
700
|
+
if (!_firstCallSuccessSent && JWT) {
|
|
769
701
|
_firstCallSuccessSent = true;
|
|
770
|
-
sendFunnelEvent('first_call_success', { tool: name, instance_id: args.instance_id ??
|
|
702
|
+
sendFunnelEvent('first_call_success', { tool: name, instance_id: args.instance_id ?? _defaultInstanceId ?? '' });
|
|
771
703
|
}
|
|
772
704
|
return contextResult;
|
|
773
705
|
}
|
|
@@ -793,8 +725,8 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
|
|
|
793
725
|
if (syndicateResult !== null) {
|
|
794
726
|
// brain_predict is in syndicate handler — track telemetry here.
|
|
795
727
|
if (name === 'brain_predict') {
|
|
796
|
-
const instanceId = (args.instance_id as string | undefined) ??
|
|
797
|
-
const telemetryExtra =
|
|
728
|
+
const instanceId = (args.instance_id as string | undefined) ?? _defaultInstanceId ?? '';
|
|
729
|
+
const telemetryExtra = JWT ? { api_key: JWT, instance_id: instanceId } : { instance_id: instanceId };
|
|
798
730
|
sendFunnelEvent('brain_predict', telemetryExtra);
|
|
799
731
|
}
|
|
800
732
|
return syndicateResult;
|
|
@@ -803,16 +735,21 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
|
|
|
803
735
|
const fedbrainResult = await handleFedbrainTool(name, args, getConnection, apiFetch);
|
|
804
736
|
if (fedbrainResult !== null) {
|
|
805
737
|
// brain_from_git and brain_predict_failures are in fedbrain handler.
|
|
806
|
-
const instanceId = (args.instance_id as string | undefined) ??
|
|
807
|
-
const telemetryExtra =
|
|
738
|
+
const instanceId = (args.instance_id as string | undefined) ?? _defaultInstanceId ?? '';
|
|
739
|
+
const telemetryExtra = JWT ? { api_key: JWT, instance_id: instanceId } : { instance_id: instanceId };
|
|
808
740
|
if (name === 'brain_from_git') {
|
|
809
|
-
|
|
741
|
+
const gitCounts = _lastBrainFromGitCounts;
|
|
742
|
+
sendFunnelEvent('brain_from_git', { ...telemetryExtra, ...(gitCounts ?? {}) });
|
|
810
743
|
} else if (name === 'brain_predict_failures') {
|
|
811
744
|
sendFunnelEvent('brain_predict_failures', telemetryExtra);
|
|
812
745
|
}
|
|
813
746
|
return fedbrainResult;
|
|
814
747
|
}
|
|
815
748
|
|
|
749
|
+
// Delegate Travel Chaos Organizer tools (requires TCO_API_URL env var)
|
|
750
|
+
const tcoResult = await handleTcoTool(name, args, JWT ?? '');
|
|
751
|
+
if (tcoResult !== null) return tcoResult;
|
|
752
|
+
|
|
816
753
|
switch (name) {
|
|
817
754
|
// ── Instance management ──────────────────────────────────────────────
|
|
818
755
|
case 'get_api_status': {
|
|
@@ -831,7 +768,7 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
|
|
|
831
768
|
}
|
|
832
769
|
|
|
833
770
|
// Check JWT / auth
|
|
834
|
-
if (!
|
|
771
|
+
if (!JWT) {
|
|
835
772
|
return [
|
|
836
773
|
`📡 **cachly API Status**`,
|
|
837
774
|
``,
|
|
@@ -844,38 +781,28 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
|
|
|
844
781
|
}
|
|
845
782
|
|
|
846
783
|
// Decode JWT claims (inspection only, no verification)
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
const
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
const expTs = payload.exp ? new Date(payload.exp * 1000) : null;
|
|
867
|
-
const expired = expTs ? expTs < new Date() : false;
|
|
868
|
-
authInfo = [
|
|
869
|
-
`✅ JWT decoded`,
|
|
870
|
-
` Sub (user ID): ${sub}`,
|
|
871
|
-
` Provider: ${provider}`,
|
|
872
|
-
` Issuer: ${iss}`,
|
|
873
|
-
` Expires: ${expTs ? expTs.toISOString() : 'never'} ${expired ? '⚠️ EXPIRED – get a new token!' : '✅'}`,
|
|
874
|
-
].join('\n');
|
|
875
|
-
}
|
|
876
|
-
} catch {
|
|
877
|
-
authInfo = '❌ JWT decode failed – check CACHLY_JWT format';
|
|
784
|
+
let authInfo = '❌ invalid JWT format';
|
|
785
|
+
try {
|
|
786
|
+
const parts = JWT.split('.');
|
|
787
|
+
if (parts.length === 3) {
|
|
788
|
+
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf-8')) as {
|
|
789
|
+
sub?: string; exp?: number; iss?: string;
|
|
790
|
+
};
|
|
791
|
+
const sub = payload.sub ?? '(unknown)';
|
|
792
|
+
const iss = payload.iss ?? '(unknown)';
|
|
793
|
+
const provider = iss.includes('keycloak') ? 'Keycloak' : 'OIDC';
|
|
794
|
+
const expTs = payload.exp ? new Date(payload.exp * 1000) : null;
|
|
795
|
+
const expired = expTs ? expTs < new Date() : false;
|
|
796
|
+
authInfo = [
|
|
797
|
+
`✅ JWT decoded`,
|
|
798
|
+
` Sub (user ID): ${sub}`,
|
|
799
|
+
` Provider: ${provider}`,
|
|
800
|
+
` Issuer: ${iss}`,
|
|
801
|
+
` Expires: ${expTs ? expTs.toISOString() : 'never'} ${expired ? '⚠️ EXPIRED – get a new token!' : '✅'}`,
|
|
802
|
+
].join('\n');
|
|
878
803
|
}
|
|
804
|
+
} catch {
|
|
805
|
+
authInfo = '❌ JWT decode failed – check CACHLY_JWT format';
|
|
879
806
|
}
|
|
880
807
|
|
|
881
808
|
// List instances + connectivity
|
|
@@ -889,7 +816,7 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
|
|
|
889
816
|
const lines: string[] = ['\n\n🧠 **Brain Instances:**'];
|
|
890
817
|
for (const inst of instances) {
|
|
891
818
|
const badge = inst.status === 'running' ? '🟢' : inst.status === 'provisioning' ? '🟡' : '🔴';
|
|
892
|
-
const defaultMark = inst.id ===
|
|
819
|
+
const defaultMark = inst.id === _defaultInstanceId ? ' ← active' : '';
|
|
893
820
|
lines.push(` ${badge} **${inst.name}** (\`${inst.id}\`) · ${inst.status}${defaultMark}`);
|
|
894
821
|
if (inst.status === 'running' && inst.host) {
|
|
895
822
|
// Try a quick ping on the cached connection
|
|
@@ -1017,14 +944,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1017
944
|
|| _defaultInstanceId
|
|
1018
945
|
|| process.env.CACHLY_BRAIN_INSTANCE_ID;
|
|
1019
946
|
if (instanceId) {
|
|
1020
|
-
|
|
1021
|
-
// The session_start briefing (which includes brain_from_git) can take 30-120s on large
|
|
1022
|
-
// repos; awaiting it here would freeze the user's first MCP tool call entirely.
|
|
1023
|
-
void autoStartSession(instanceId);
|
|
947
|
+
await autoStartSession(instanceId).catch(() => undefined);
|
|
1024
948
|
}
|
|
949
|
+
} else if (_autoSessionStarting) {
|
|
950
|
+
// Another concurrent call is mid-start — wait for it before proceeding.
|
|
951
|
+
await _autoSessionStarting.catch(() => undefined);
|
|
1025
952
|
}
|
|
1026
|
-
// Note: we intentionally do NOT await _autoSessionStarting for any call.
|
|
1027
|
-
// The session tracker runs in background — tool calls must always proceed immediately.
|
|
1028
953
|
if (!sessionTools.has(name)) _autoSessionToolCount++;
|
|
1029
954
|
|
|
1030
955
|
try {
|
|
@@ -1032,7 +957,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1032
957
|
return { content: [{ type: 'text', text }] };
|
|
1033
958
|
} catch (err) {
|
|
1034
959
|
if (err instanceof McpError) throw err;
|
|
1035
|
-
|
|
960
|
+
const msg = (err as Error).message ?? String(err);
|
|
961
|
+
notify('cachly', 'tool_error', { tool: name, error: msg }).catch(() => undefined);
|
|
962
|
+
throw new McpError(ErrorCode.InternalError, msg);
|
|
1036
963
|
}
|
|
1037
964
|
});
|
|
1038
965
|
|
|
@@ -1921,7 +1848,7 @@ if (!process.argv[2] && process.stdout.isTTY) {
|
|
|
1921
1848
|
console.log(' \x1b[36m npx @cachly-dev/mcp-server@latest upgrade\x1b[0m ← Check for updates');
|
|
1922
1849
|
console.log('');
|
|
1923
1850
|
console.log(' \x1b[90mWorks with: Claude Code · Cursor · Windsurf · GitHub Copilot · Cline · Zed\x1b[0m');
|
|
1924
|
-
console.log(' \x1b[90mFree forever · GDPR · German servers ·
|
|
1851
|
+
console.log(' \x1b[90mFree forever · GDPR · German servers · 95 MCP tools\x1b[0m');
|
|
1925
1852
|
console.log('');
|
|
1926
1853
|
process.exit(0);
|
|
1927
1854
|
}
|
|
@@ -2094,7 +2021,7 @@ if (process.argv[2] === 'status') {
|
|
|
2094
2021
|
if (process.argv[2] === 'health') {
|
|
2095
2022
|
const { existsSync } = await import('node:fs');
|
|
2096
2023
|
const { readFile } = await import('node:fs/promises');
|
|
2097
|
-
const { resolve } = await import('node:path');
|
|
2024
|
+
const { resolve, join: pJoin } = await import('node:path');
|
|
2098
2025
|
|
|
2099
2026
|
const cwd = process.cwd();
|
|
2100
2027
|
let passed = 0;
|
|
@@ -2111,9 +2038,6 @@ if (process.argv[2] === 'health') {
|
|
|
2111
2038
|
const jwt = process.env.CACHLY_JWT ?? '';
|
|
2112
2039
|
if (!jwt) {
|
|
2113
2040
|
fail('CACHLY_JWT not set — run: npx @cachly-dev/mcp-server@latest setup');
|
|
2114
|
-
} else if (jwt.startsWith('cky_')) {
|
|
2115
|
-
// Opaque cachly API key — no JWT expiry to check, format is valid by prefix
|
|
2116
|
-
ok('API key set (cky_*** opaque key)');
|
|
2117
2041
|
} else {
|
|
2118
2042
|
try {
|
|
2119
2043
|
const parts = jwt.split('.');
|
|
@@ -2131,7 +2055,7 @@ if (process.argv[2] === 'health') {
|
|
|
2131
2055
|
ok(`JWT valid${minsLeft !== null ? ` (expires in ${Math.floor(minsLeft / 60)}h ${minsLeft % 60}m)` : ''}`);
|
|
2132
2056
|
}
|
|
2133
2057
|
} catch {
|
|
2134
|
-
fail('CACHLY_JWT format invalid (expected
|
|
2058
|
+
fail('CACHLY_JWT format invalid (expected JWT with 3 parts)');
|
|
2135
2059
|
}
|
|
2136
2060
|
}
|
|
2137
2061
|
|
|
@@ -2179,23 +2103,11 @@ if (process.argv[2] === 'health') {
|
|
|
2179
2103
|
signal: AbortSignal.timeout(5000),
|
|
2180
2104
|
});
|
|
2181
2105
|
if (connRes.ok) {
|
|
2182
|
-
const conn = await connRes.json() as { password?: string;
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
pingPassword = conn.password;
|
|
2186
|
-
} else if (conn.connection_string) {
|
|
2187
|
-
try {
|
|
2188
|
-
const u = new URL(conn.connection_string);
|
|
2189
|
-
if (u.password) pingPassword = decodeURIComponent(u.password);
|
|
2190
|
-
} catch { /* leave pingPassword undefined */ }
|
|
2191
|
-
}
|
|
2192
|
-
pingTls = conn.tls_enabled === true;
|
|
2193
|
-
} else {
|
|
2194
|
-
warn(`Could not fetch Redis credentials (HTTP ${connRes.status}) — connectivity test may fail`);
|
|
2106
|
+
const conn = await connRes.json() as { password?: string; tls_enabled?: boolean };
|
|
2107
|
+
pingPassword = conn.password;
|
|
2108
|
+
pingTls = conn.tls_enabled !== false;
|
|
2195
2109
|
}
|
|
2196
|
-
} catch
|
|
2197
|
-
warn(`Could not fetch Redis credentials — ${(connErr as Error).message}`);
|
|
2198
|
-
}
|
|
2110
|
+
} catch { /* proceed without password */ }
|
|
2199
2111
|
const pingClient = new Redis({
|
|
2200
2112
|
host: inst.host, port: inst.port,
|
|
2201
2113
|
password: pingPassword,
|
|
@@ -2323,6 +2235,7 @@ if (process.argv[2] === 'setup') {
|
|
|
2323
2235
|
const CLIENT_ID = 'cachly-cli';
|
|
2324
2236
|
|
|
2325
2237
|
console.log('Step 1: Sign in to cachly (free, no credit card)\n');
|
|
2238
|
+
sendFunnelEvent('setup_auth_started');
|
|
2326
2239
|
|
|
2327
2240
|
// Start device flow
|
|
2328
2241
|
let deviceCode = '', userCode = '', verifyUri = '', pollInterval = 5000;
|
|
@@ -2346,6 +2259,7 @@ if (process.argv[2] === 'setup') {
|
|
|
2346
2259
|
console.error('Falling back: sign in at https://cachly.dev/setup-ai and paste your API token.\n');
|
|
2347
2260
|
token = await ask(' Paste API token (cky_live_...): ');
|
|
2348
2261
|
if (!token) { console.error('\nToken is required. Aborting.\n'); rl.close(); process.exit(1); }
|
|
2262
|
+
sendFunnelEvent('setup_auth_completed');
|
|
2349
2263
|
console.log('');
|
|
2350
2264
|
deviceCode = ''; // mark as fallback so we skip polling
|
|
2351
2265
|
}
|
|
@@ -2379,6 +2293,7 @@ if (process.argv[2] === 'setup') {
|
|
|
2379
2293
|
if (tokenData.access_token) {
|
|
2380
2294
|
token = tokenData.access_token;
|
|
2381
2295
|
console.log(' \x1b[32m✓ Authorized!\x1b[0m\n');
|
|
2296
|
+
sendFunnelEvent('setup_auth_completed');
|
|
2382
2297
|
break;
|
|
2383
2298
|
}
|
|
2384
2299
|
// authorization_pending = keep polling; slow_down = increase interval
|
|
@@ -2410,7 +2325,7 @@ if (process.argv[2] === 'setup') {
|
|
|
2410
2325
|
if (!keyBody.key) throw new Error('no key in response');
|
|
2411
2326
|
token = keyBody.key; // swap JWT → cky_live_...
|
|
2412
2327
|
console.log(' ✓\n');
|
|
2413
|
-
} catch {
|
|
2328
|
+
} catch (e) {
|
|
2414
2329
|
console.log(' (skipped)\n');
|
|
2415
2330
|
// Non-fatal: fall back to using the Keycloak JWT directly.
|
|
2416
2331
|
// It will expire but setup still works for now.
|
|
@@ -2459,9 +2374,10 @@ if (process.argv[2] === 'setup') {
|
|
|
2459
2374
|
// Newly created — poll until running or give up after 30 s.
|
|
2460
2375
|
const newId = autoBody.instance_id;
|
|
2461
2376
|
console.log(` ✓ created (${newId.slice(0, 8)}…)\n`);
|
|
2377
|
+
// 45 attempts × 3s = 135s — generous for free-tier in slow regions.
|
|
2462
2378
|
process.stdout.write('⏳ Waiting for instance to start');
|
|
2463
|
-
for (let attempt = 0; attempt <
|
|
2464
|
-
await new Promise(r => setTimeout(r,
|
|
2379
|
+
for (let attempt = 0; attempt < 45; attempt++) {
|
|
2380
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
2465
2381
|
process.stdout.write('.');
|
|
2466
2382
|
try {
|
|
2467
2383
|
const checkRes = await fetch(`${API_URL}/api/v1/instances/${newId}`, { headers: { Authorization: `Bearer ${token}` } });
|
|
@@ -2495,6 +2411,7 @@ if (process.argv[2] === 'setup') {
|
|
|
2495
2411
|
console.log(` (Run with --instance-id <id> to use a different one)\n`);
|
|
2496
2412
|
}
|
|
2497
2413
|
console.log(`✓ Instance: ${instance.name} (${instance.id.slice(0, 8)}…)\n`);
|
|
2414
|
+
sendFunnelEvent('setup_instance_ready', { instance_id: instance.id });
|
|
2498
2415
|
|
|
2499
2416
|
// ── Step 3: Detect editors ────────────────────────────────────────────────
|
|
2500
2417
|
const cwd = process.cwd();
|
|
@@ -2667,8 +2584,6 @@ if (process.argv[2] === 'setup') {
|
|
|
2667
2584
|
console.log(` Dashboard: https://cachly.dev/instances/${instance.id}`);
|
|
2668
2585
|
console.log(`\n 📛 Add a live badge to your README:`);
|
|
2669
2586
|
console.log(` npx @cachly-dev/mcp-server@latest badge\n`);
|
|
2670
|
-
console.log(` 🤝 Invite a teammate — they'll arrive pre-briefed on your project:`);
|
|
2671
|
-
console.log(` npx @cachly-dev/mcp-server@latest invite\n`);
|
|
2672
2587
|
|
|
2673
2588
|
// ── Step 7: Email opt-in (non-blocking — at the very end) ────────────────
|
|
2674
2589
|
if (!nonInteractive) {
|
|
@@ -2742,7 +2657,7 @@ if (process.argv[2] === 'index') {
|
|
|
2742
2657
|
const maxFiles = parseInt(flag('max-files') ?? '500', 10);
|
|
2743
2658
|
const namespace = flag('namespace') ?? 'cachly:sem:code';
|
|
2744
2659
|
|
|
2745
|
-
if (!instanceId || !
|
|
2660
|
+
if (!instanceId || !JWT) {
|
|
2746
2661
|
console.error('\n❌ CACHLY_BRAIN_INSTANCE_ID and CACHLY_JWT must be set\n');
|
|
2747
2662
|
console.error(' export CACHLY_BRAIN_INSTANCE_ID=<uuid>');
|
|
2748
2663
|
console.error(' export CACHLY_JWT=<cky_live_...>');
|
|
@@ -2784,7 +2699,7 @@ if (process.argv[2] === 'learn-git') {
|
|
|
2784
2699
|
const instanceId = flag('instance-id') ?? process.env.CACHLY_BRAIN_INSTANCE_ID;
|
|
2785
2700
|
const maxCommits = parseInt(flag('max-commits') ?? '50', 10);
|
|
2786
2701
|
|
|
2787
|
-
if (!instanceId || !
|
|
2702
|
+
if (!instanceId || !JWT) {
|
|
2788
2703
|
console.error('\n❌ CACHLY_BRAIN_INSTANCE_ID and CACHLY_JWT must be set\n');
|
|
2789
2704
|
console.error(' export CACHLY_BRAIN_INSTANCE_ID=<uuid>');
|
|
2790
2705
|
console.error(' export CACHLY_JWT=<cky_live_...>');
|
|
@@ -2802,7 +2717,7 @@ if (process.argv[2] === 'learn-git') {
|
|
|
2802
2717
|
limit: maxCommits,
|
|
2803
2718
|
});
|
|
2804
2719
|
console.log(result);
|
|
2805
|
-
sendFunnelEvent('brain_from_git', { instance_id: instanceId });
|
|
2720
|
+
sendFunnelEvent('brain_from_git', { api_key: JWT, instance_id: instanceId });
|
|
2806
2721
|
console.log('\n✅ Brain learned from your commits.\n');
|
|
2807
2722
|
// Let fire-and-forget telemetry flush before exit.
|
|
2808
2723
|
await new Promise(r => setTimeout(r, 300));
|
|
@@ -2819,8 +2734,7 @@ if (process.argv[2] === 'learn-git') {
|
|
|
2819
2734
|
// actionable message in their editor's MCP log instead of silent failures.
|
|
2820
2735
|
// Skip for CLI commands that intentionally run without credentials.
|
|
2821
2736
|
const _cliNoAuthCommands = ['demo', 'share', 'health', 'setup', 'init', 'digest', 'invite', 'badge', 'join', 'upgrade'];
|
|
2822
|
-
|
|
2823
|
-
if (!JWT && !process.env.PORT && !_cliNoAuthCommands.includes(process.argv[2] ?? '') && !(!process.argv[2] && process.stdout.isTTY)) {
|
|
2737
|
+
if (!JWT && !_cliNoAuthCommands.includes(process.argv[2] ?? '') && !(!process.argv[2] && process.stdout.isTTY)) {
|
|
2824
2738
|
// No args + no JWT in a non-TTY context = running as MCP server without credentials.
|
|
2825
2739
|
// Print actionable setup banner to stdout so it's captured by callers/tests.
|
|
2826
2740
|
const banner =
|
|
@@ -2893,76 +2807,12 @@ if (!process.env.CACHLY_NO_UPDATE_CHECK) {
|
|
|
2893
2807
|
|
|
2894
2808
|
const httpPort = process.env.PORT ? parseInt(process.env.PORT, 10) : undefined;
|
|
2895
2809
|
|
|
2896
|
-
// ── Per-request MCP Server factory (HTTP / Smithery stateless mode) ──────────
|
|
2897
|
-
// SDK ≥1.9 forbids reusing a stateless transport across requests.
|
|
2898
|
-
// Solution: create a fresh Server+Transport per /mcp request.
|
|
2899
|
-
// The module-level `server` below is used for stdio mode only.
|
|
2900
|
-
function createHttpMcpServer(): Server {
|
|
2901
|
-
const srv = new Server(
|
|
2902
|
-
{ name: 'cachly-mcp', version: CURRENT_VERSION },
|
|
2903
|
-
{ capabilities: { tools: {} } }
|
|
2904
|
-
);
|
|
2905
|
-
srv.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
2906
|
-
srv.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
2907
|
-
const { name, arguments: args } = request.params;
|
|
2908
|
-
try {
|
|
2909
|
-
const text = await handleTool(name, (args ?? {}) as Record<string, unknown>);
|
|
2910
|
-
return { content: [{ type: 'text', text }] };
|
|
2911
|
-
} catch (err) {
|
|
2912
|
-
if (err instanceof McpError) throw err;
|
|
2913
|
-
throw new McpError(ErrorCode.InternalError, (err as Error).message);
|
|
2914
|
-
}
|
|
2915
|
-
});
|
|
2916
|
-
return srv;
|
|
2917
|
-
}
|
|
2918
|
-
|
|
2919
2810
|
if (httpPort) {
|
|
2920
2811
|
// ── HTTP mode (Streamable HTTP transport) ───────────────────────────────
|
|
2921
|
-
// Used for Smithery URL deployment: PORT=3000 node dist/index.js
|
|
2922
|
-
// Multi-tenant: per-request JWT + instance ID resolved via:
|
|
2923
|
-
// 1. Authorization: Bearer <jwt> + X-Cachly-Instance-Id: <uuid>
|
|
2924
|
-
// 2. Smithery config: ?config=base64url({CACHLY_JWT, CACHLY_BRAIN_INSTANCE_ID, ...})
|
|
2925
|
-
// Falls back to module-level env-derived values when neither is provided.
|
|
2812
|
+
// Used for Smithery URL deployment: PORT=3000 node dist/src/index.js
|
|
2926
2813
|
const { createServer } = await import('node:http');
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
let jwtVal = '';
|
|
2930
|
-
let instId = '';
|
|
2931
|
-
const auth = req.headers['authorization'];
|
|
2932
|
-
if (typeof auth === 'string' && auth.trim()) {
|
|
2933
|
-
const a = auth.trim();
|
|
2934
|
-
jwtVal = a.toLowerCase().startsWith('bearer ') ? a.slice(7).trim() : a;
|
|
2935
|
-
}
|
|
2936
|
-
// Fallback: raw JWT in custom header (Smithery "header" transport without Bearer prefix)
|
|
2937
|
-
if (!jwtVal) {
|
|
2938
|
-
const rawJwt = req.headers['cachly_jwt'] ?? req.headers['cachly-jwt'] ?? req.headers['x-cachly-jwt'] ?? req.headers['x-api-key'];
|
|
2939
|
-
if (typeof rawJwt === 'string') jwtVal = rawJwt.trim();
|
|
2940
|
-
}
|
|
2941
|
-
const hdrInst = req.headers['x-cachly-instance-id'];
|
|
2942
|
-
if (typeof hdrInst === 'string') instId = hdrInst.trim();
|
|
2943
|
-
// Smithery passes user config as a base64url-encoded JSON blob in ?config=
|
|
2944
|
-
const cfgParam = url.searchParams.get('config');
|
|
2945
|
-
if (cfgParam && (!jwtVal || !instId)) {
|
|
2946
|
-
try {
|
|
2947
|
-
const b64 = cfgParam.replace(/-/g, '+').replace(/_/g, '/');
|
|
2948
|
-
const padded = b64 + '='.repeat((4 - b64.length % 4) % 4);
|
|
2949
|
-
const json = JSON.parse(Buffer.from(padded, 'base64').toString('utf8'));
|
|
2950
|
-
if (!jwtVal && typeof json.CACHLY_JWT === 'string') jwtVal = json.CACHLY_JWT;
|
|
2951
|
-
if (!instId && typeof json.CACHLY_BRAIN_INSTANCE_ID === 'string') instId = json.CACHLY_BRAIN_INSTANCE_ID;
|
|
2952
|
-
} catch { /* malformed config — ignore, fall back to globals */ }
|
|
2953
|
-
}
|
|
2954
|
-
// Direct query params (Smithery "query" transport, manual clients, etc.)
|
|
2955
|
-
if (!jwtVal) {
|
|
2956
|
-
const q = url.searchParams.get('CACHLY_JWT') ?? url.searchParams.get('cachly_jwt') ?? url.searchParams.get('api_key');
|
|
2957
|
-
if (q) jwtVal = q.trim();
|
|
2958
|
-
}
|
|
2959
|
-
if (!instId) {
|
|
2960
|
-
const q = url.searchParams.get('CACHLY_BRAIN_INSTANCE_ID') ?? url.searchParams.get('cachly_brain_instance_id') ?? url.searchParams.get('instance_id');
|
|
2961
|
-
if (q) instId = q.trim();
|
|
2962
|
-
}
|
|
2963
|
-
return { jwt: jwtVal, instanceId: instId };
|
|
2964
|
-
}
|
|
2965
|
-
|
|
2814
|
+
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
|
|
2815
|
+
await server.connect(transport);
|
|
2966
2816
|
const httpServer = createServer((req, res) => {
|
|
2967
2817
|
const url = new URL(req.url ?? '/', `http://localhost:${httpPort}`);
|
|
2968
2818
|
if (url.pathname === '/health') {
|
|
@@ -2970,8 +2820,6 @@ if (httpPort) {
|
|
|
2970
2820
|
res.end(JSON.stringify({
|
|
2971
2821
|
status: 'ok',
|
|
2972
2822
|
version: CURRENT_VERSION,
|
|
2973
|
-
transport: 'streamable-http',
|
|
2974
|
-
multiTenant: true,
|
|
2975
2823
|
zeroResults: {
|
|
2976
2824
|
total: zeroResultsTotal,
|
|
2977
2825
|
last10: ZERO_RESULTS_LOG.slice(-10).map(e => ({ query: e.query, ts: new Date(e.ts).toISOString() })),
|
|
@@ -2979,52 +2827,8 @@ if (httpPort) {
|
|
|
2979
2827
|
}));
|
|
2980
2828
|
return;
|
|
2981
2829
|
}
|
|
2982
|
-
// Static server card — lets Smithery skip live scanning (avoids auth complexity).
|
|
2983
|
-
if (url.pathname === '/.well-known/mcp/server-card.json') {
|
|
2984
|
-
const card = {
|
|
2985
|
-
name: 'cachly AI Brain',
|
|
2986
|
-
version: CURRENT_VERSION,
|
|
2987
|
-
description: 'Persistent AI memory for Claude Code, Cursor, Copilot, Windsurf & friends. Remembers every lesson, fix, and architecture decision forever.',
|
|
2988
|
-
transport: { type: 'streamable-http', url: 'https://mcp.cachly.dev/mcp' },
|
|
2989
|
-
capabilities: { tools: {} },
|
|
2990
|
-
};
|
|
2991
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
2992
|
-
res.end(JSON.stringify(card));
|
|
2993
|
-
return;
|
|
2994
|
-
}
|
|
2995
2830
|
if (url.pathname === '/mcp' || url.pathname === '/') {
|
|
2996
|
-
|
|
2997
|
-
if (req.method === 'OPTIONS') {
|
|
2998
|
-
res.writeHead(204, {
|
|
2999
|
-
'Access-Control-Allow-Origin': '*',
|
|
3000
|
-
'Access-Control-Allow-Methods': 'GET, POST, DELETE, OPTIONS',
|
|
3001
|
-
'Access-Control-Allow-Headers': 'Authorization, Content-Type, Mcp-Session-Id, X-Cachly-Instance-Id, cachly_jwt, cachly-jwt, x-cachly-jwt, x-api-key',
|
|
3002
|
-
});
|
|
3003
|
-
res.end();
|
|
3004
|
-
return;
|
|
3005
|
-
}
|
|
3006
|
-
const ctx = extractReqCtx(req, url);
|
|
3007
|
-
// Create a fresh Server+Transport for each request (stateless multi-tenant mode).
|
|
3008
|
-
// SDK ≥1.9 throws if a stateless transport is reused across requests.
|
|
3009
|
-
const reqServer = createHttpMcpServer();
|
|
3010
|
-
const reqTransport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
|
|
3011
|
-
reqServer.connect(reqTransport).then(() => {
|
|
3012
|
-
reqCtx.run(ctx, () => {
|
|
3013
|
-
reqTransport.handleRequest(req, res).catch((err) => {
|
|
3014
|
-
process.stderr.write(`cachly-mcp HTTP handler error: ${(err as Error).message}\n`);
|
|
3015
|
-
if (!res.headersSent) {
|
|
3016
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
3017
|
-
res.end(JSON.stringify({ error: 'internal_error' }));
|
|
3018
|
-
}
|
|
3019
|
-
});
|
|
3020
|
-
});
|
|
3021
|
-
}).catch((err) => {
|
|
3022
|
-
process.stderr.write(`cachly-mcp connect error: ${(err as Error).message}\n`);
|
|
3023
|
-
if (!res.headersSent) {
|
|
3024
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
3025
|
-
res.end(JSON.stringify({ error: 'internal_error' }));
|
|
3026
|
-
}
|
|
3027
|
-
});
|
|
2831
|
+
transport.handleRequest(req, res);
|
|
3028
2832
|
return;
|
|
3029
2833
|
}
|
|
3030
2834
|
res.writeHead(404);
|
|
@@ -3039,3 +2843,5 @@ if (httpPort) {
|
|
|
3039
2843
|
await server.connect(transport);
|
|
3040
2844
|
}
|
|
3041
2845
|
|
|
2846
|
+
notify('cachly', 'startup', { version: CURRENT_VERSION, mode: process.env.MCP_HTTP_PORT ? 'http' : 'stdio' }).catch(() => undefined);
|
|
2847
|
+
|