@cachly-dev/mcp-server 0.10.49 → 0.10.56
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/publish.yml +2 -2
- package/README.md +33 -5
- package/dist/__tests__/auth.test.js.map +1 -0
- package/dist/{src/__tests__ → __tests__}/brain-flow.test.js +0 -141
- package/dist/__tests__/brain-flow.test.js.map +1 -0
- package/dist/__tests__/brain-predict-failures.test.d.ts +17 -0
- package/dist/__tests__/brain-predict-failures.test.js +339 -0
- package/dist/__tests__/brain-predict-failures.test.js.map +1 -0
- package/dist/__tests__/confidence.test.js.map +1 -0
- package/dist/__tests__/e2e/api-auth.test.js.map +1 -0
- package/dist/__tests__/e2e/api-health.test.js.map +1 -0
- package/dist/__tests__/e2e/cli.test.js.map +1 -0
- package/dist/__tests__/e2e/config.js.map +1 -0
- package/dist/__tests__/e2e/funnel.test.js.map +1 -0
- package/dist/__tests__/e2e/mcp-tools.test.js.map +1 -0
- package/dist/__tests__/e2e/provisioning.test.js.map +1 -0
- package/dist/{src/__tests__ → __tests__}/handlers-unit.test.js +2 -0
- package/dist/__tests__/handlers-unit.test.js.map +1 -0
- package/dist/__tests__/namespace.test.js.map +1 -0
- package/dist/auth.js.map +1 -0
- package/dist/ckg.js.map +1 -0
- package/dist/confidence.js.map +1 -0
- package/dist/embeddings.js.map +1 -0
- package/dist/handlers/advanced.js.map +1 -0
- package/dist/{src/handlers → handlers}/brain.js +141 -135
- package/dist/handlers/brain.js.map +1 -0
- package/dist/handlers/cache.js.map +1 -0
- package/dist/{src/handlers → handlers}/context.js +4 -9
- package/dist/handlers/context.js.map +1 -0
- package/dist/{src/handlers → handlers}/fedbrain.d.ts +0 -6
- package/dist/{src/handlers → handlers}/fedbrain.js +7 -19
- package/dist/handlers/fedbrain.js.map +1 -0
- package/dist/handlers/instances.js.map +1 -0
- package/dist/{src/handlers → handlers}/roadmap.js +3 -7
- package/dist/handlers/roadmap.js.map +1 -0
- package/dist/handlers/syndicate.js.map +1 -0
- package/dist/{src/handlers → handlers}/team.js +3 -1
- package/dist/handlers/team.js.map +1 -0
- package/dist/{src/index.js → index.js} +428 -206
- package/dist/index.js.map +1 -0
- package/dist/namespace.js.map +1 -0
- package/dist/search.js.map +1 -0
- package/dist/search.test.js.map +1 -0
- package/dist/{src/tools.d.ts → tools.d.ts} +0 -196
- package/dist/{src/tools.js → tools.js} +0 -178
- package/dist/tools.js.map +1 -0
- package/dist/utils.js.map +1 -0
- package/package.json +3 -3
- package/smithery.yaml +16 -27
- package/src/__tests__/brain-flow.test.ts +0 -174
- package/src/__tests__/brain-predict-failures.test.ts +430 -0
- package/src/__tests__/handlers-unit.test.ts +2 -1
- package/src/handlers/brain.ts +142 -142
- package/src/handlers/context.ts +4 -9
- package/src/handlers/fedbrain.ts +7 -21
- package/src/handlers/roadmap.ts +3 -7
- package/src/handlers/team.ts +3 -1
- package/src/index.ts +415 -221
- package/src/tools.ts +0 -191
- package/tsconfig.json +3 -2
- package/.github/workflows/eas-build-android.yml +0 -58
- package/.github/workflows/eas-update.yml +0 -55
- package/STRATEGY.md +0 -209
- package/apps/travel-chaos-organizer/.env.example +0 -49
- package/apps/travel-chaos-organizer/DEPLOY.md +0 -328
- package/apps/travel-chaos-organizer/Makefile +0 -57
- package/apps/travel-chaos-organizer/README.md +0 -159
- package/apps/travel-chaos-organizer/backend/.env.example +0 -10
- package/apps/travel-chaos-organizer/backend/Dockerfile +0 -25
- package/apps/travel-chaos-organizer/backend/alembic/env.py +0 -56
- package/apps/travel-chaos-organizer/backend/alembic/script.py.mako +0 -24
- package/apps/travel-chaos-organizer/backend/alembic/versions/.gitkeep +0 -0
- package/apps/travel-chaos-organizer/backend/alembic/versions/0001_stripe_customer_id.py +0 -24
- package/apps/travel-chaos-organizer/backend/alembic/versions/0002_telegram_chat_id.py +0 -24
- package/apps/travel-chaos-organizer/backend/alembic.ini +0 -39
- 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 +0 -81
- package/apps/travel-chaos-organizer/backend/app/config.py +0 -66
- package/apps/travel-chaos-organizer/backend/app/db/__init__.py +0 -0
- package/apps/travel-chaos-organizer/backend/app/db/database.py +0 -20
- package/apps/travel-chaos-organizer/backend/app/db/migrations/001_init.sql +0 -71
- package/apps/travel-chaos-organizer/backend/app/db/migrations/002_telemetry_freemium.sql +0 -31
- package/apps/travel-chaos-organizer/backend/app/db/migrations/003_telegram_chat_id.sql +0 -4
- package/apps/travel-chaos-organizer/backend/app/db/migrations/004_stripe_and_email.sql +0 -5
- package/apps/travel-chaos-organizer/backend/app/limiter.py +0 -4
- package/apps/travel-chaos-organizer/backend/app/logging_config.py +0 -25
- package/apps/travel-chaos-organizer/backend/app/main.py +0 -77
- package/apps/travel-chaos-organizer/backend/app/models/__init__.py +0 -0
- package/apps/travel-chaos-organizer/backend/app/models/schemas.py +0 -142
- package/apps/travel-chaos-organizer/backend/app/routers/__init__.py +0 -0
- package/apps/travel-chaos-organizer/backend/app/routers/admin.py +0 -237
- package/apps/travel-chaos-organizer/backend/app/routers/cache.py +0 -13
- package/apps/travel-chaos-organizer/backend/app/routers/events.py +0 -29
- package/apps/travel-chaos-organizer/backend/app/routers/export.py +0 -122
- package/apps/travel-chaos-organizer/backend/app/routers/inbox.py +0 -122
- package/apps/travel-chaos-organizer/backend/app/routers/items.py +0 -130
- package/apps/travel-chaos-organizer/backend/app/routers/mail.py +0 -108
- package/apps/travel-chaos-organizer/backend/app/routers/parse.py +0 -303
- package/apps/travel-chaos-organizer/backend/app/routers/payments.py +0 -179
- package/apps/travel-chaos-organizer/backend/app/routers/trips.py +0 -134
- package/apps/travel-chaos-organizer/backend/app/routers/users.py +0 -169
- package/apps/travel-chaos-organizer/backend/app/routers/waitlist.py +0 -37
- package/apps/travel-chaos-organizer/backend/app/services/__init__.py +0 -0
- package/apps/travel-chaos-organizer/backend/app/services/cache.py +0 -131
- package/apps/travel-chaos-organizer/backend/app/services/drip.py +0 -128
- package/apps/travel-chaos-organizer/backend/app/services/email.py +0 -61
- package/apps/travel-chaos-organizer/backend/app/services/errors.py +0 -44
- package/apps/travel-chaos-organizer/backend/app/services/notifier.py +0 -10
- package/apps/travel-chaos-organizer/backend/app/services/ollama.py +0 -101
- package/apps/travel-chaos-organizer/backend/app/services/parser.py +0 -22
- package/apps/travel-chaos-organizer/backend/app/services/quota.py +0 -85
- package/apps/travel-chaos-organizer/backend/app/services/sentry.py +0 -11
- package/apps/travel-chaos-organizer/backend/app/services/telemetry.py +0 -35
- package/apps/travel-chaos-organizer/backend/pytest.ini +0 -4
- package/apps/travel-chaos-organizer/backend/requirements-dev.txt +0 -6
- package/apps/travel-chaos-organizer/backend/requirements.txt +0 -25
- package/apps/travel-chaos-organizer/backend/tests/__init__.py +0 -0
- package/apps/travel-chaos-organizer/backend/tests/conftest.py +0 -223
- package/apps/travel-chaos-organizer/backend/tests/test_admin.py +0 -99
- package/apps/travel-chaos-organizer/backend/tests/test_cache.py +0 -142
- package/apps/travel-chaos-organizer/backend/tests/test_drip.py +0 -32
- package/apps/travel-chaos-organizer/backend/tests/test_e2e_chain.py +0 -121
- package/apps/travel-chaos-organizer/backend/tests/test_export.py +0 -48
- package/apps/travel-chaos-organizer/backend/tests/test_inbox.py +0 -107
- package/apps/travel-chaos-organizer/backend/tests/test_items.py +0 -138
- package/apps/travel-chaos-organizer/backend/tests/test_mail.py +0 -113
- package/apps/travel-chaos-organizer/backend/tests/test_ollama.py +0 -145
- package/apps/travel-chaos-organizer/backend/tests/test_parse_url.py +0 -98
- package/apps/travel-chaos-organizer/backend/tests/test_parser.py +0 -72
- package/apps/travel-chaos-organizer/backend/tests/test_payments.py +0 -163
- package/apps/travel-chaos-organizer/backend/tests/test_quota.py +0 -46
- package/apps/travel-chaos-organizer/backend/tests/test_search.py +0 -67
- package/apps/travel-chaos-organizer/backend/tests/test_trips.py +0 -123
- package/apps/travel-chaos-organizer/backend/tests/test_users.py +0 -52
- package/apps/travel-chaos-organizer/backups/.gitkeep +0 -0
- package/apps/travel-chaos-organizer/caddy/Caddyfile +0 -27
- package/apps/travel-chaos-organizer/docker-compose.node1.yml +0 -31
- package/apps/travel-chaos-organizer/docker-compose.override.yml +0 -14
- package/apps/travel-chaos-organizer/docker-compose.yml +0 -128
- package/apps/travel-chaos-organizer/frontend/.env.example +0 -13
- package/apps/travel-chaos-organizer/frontend/app/(app)/_layout.tsx +0 -80
- package/apps/travel-chaos-organizer/frontend/app/(app)/inbox.tsx +0 -166
- package/apps/travel-chaos-organizer/frontend/app/(app)/search.tsx +0 -130
- package/apps/travel-chaos-organizer/frontend/app/(app)/settings.tsx +0 -381
- package/apps/travel-chaos-organizer/frontend/app/(app)/trips/[id].tsx +0 -346
- package/apps/travel-chaos-organizer/frontend/app/(app)/trips.tsx +0 -363
- package/apps/travel-chaos-organizer/frontend/app/(app)/upgrade/cancel.tsx +0 -34
- package/apps/travel-chaos-organizer/frontend/app/(app)/upgrade/success.tsx +0 -35
- package/apps/travel-chaos-organizer/frontend/app/(auth)/login.tsx +0 -91
- package/apps/travel-chaos-organizer/frontend/app/_layout.tsx +0 -71
- package/apps/travel-chaos-organizer/frontend/app/auth.tsx +0 -29
- package/apps/travel-chaos-organizer/frontend/app/index.tsx +0 -16
- package/apps/travel-chaos-organizer/frontend/app/onboarding.tsx +0 -119
- package/apps/travel-chaos-organizer/frontend/app.config.ts +0 -54
- package/apps/travel-chaos-organizer/frontend/assets/adaptive-icon.svg +0 -66
- package/apps/travel-chaos-organizer/frontend/assets/icon.svg +0 -79
- package/apps/travel-chaos-organizer/frontend/assets/splash.svg +0 -15
- package/apps/travel-chaos-organizer/frontend/components/BottomSheet.tsx +0 -102
- package/apps/travel-chaos-organizer/frontend/components/ConfettiCannon.tsx +0 -107
- package/apps/travel-chaos-organizer/frontend/components/ConfirmDialog.tsx +0 -109
- package/apps/travel-chaos-organizer/frontend/components/ErrorBoundary.tsx +0 -45
- package/apps/travel-chaos-organizer/frontend/components/FileUploadButton.tsx +0 -19
- package/apps/travel-chaos-organizer/frontend/components/ImportSheet.tsx +0 -155
- package/apps/travel-chaos-organizer/frontend/components/OfflineBadge.tsx +0 -55
- package/apps/travel-chaos-organizer/frontend/components/QRCodeViewer.tsx +0 -80
- package/apps/travel-chaos-organizer/frontend/components/ScoreRing.tsx +0 -39
- package/apps/travel-chaos-organizer/frontend/components/Screen.tsx +0 -39
- package/apps/travel-chaos-organizer/frontend/components/Skeleton.tsx +0 -70
- package/apps/travel-chaos-organizer/frontend/components/SwipeableInboxItem.tsx +0 -121
- package/apps/travel-chaos-organizer/frontend/components/SwipeableTimelineItem.tsx +0 -70
- package/apps/travel-chaos-organizer/frontend/components/TimelineItem.tsx +0 -73
- package/apps/travel-chaos-organizer/frontend/components/TimelineItemDetail.tsx +0 -130
- package/apps/travel-chaos-organizer/frontend/components/Toast.tsx +0 -60
- package/apps/travel-chaos-organizer/frontend/components/ToastContext.tsx +0 -42
- package/apps/travel-chaos-organizer/frontend/components/TripCard.tsx +0 -146
- package/apps/travel-chaos-organizer/frontend/components/TripQRModal.tsx +0 -70
- package/apps/travel-chaos-organizer/frontend/components/UpgradeBanner.tsx +0 -44
- package/apps/travel-chaos-organizer/frontend/eas.json +0 -22
- package/apps/travel-chaos-organizer/frontend/hooks/useInbox.ts +0 -24
- package/apps/travel-chaos-organizer/frontend/hooks/useNetworkSync.ts +0 -60
- package/apps/travel-chaos-organizer/frontend/hooks/useTrips.ts +0 -112
- package/apps/travel-chaos-organizer/frontend/lib/api.ts +0 -223
- package/apps/travel-chaos-organizer/frontend/lib/auth.ts +0 -93
- package/apps/travel-chaos-organizer/frontend/lib/db.ts +0 -86
- package/apps/travel-chaos-organizer/frontend/lib/fileCache.ts +0 -54
- package/apps/travel-chaos-organizer/frontend/lib/haptics.ts +0 -13
- package/apps/travel-chaos-organizer/frontend/lib/notifications.ts +0 -112
- package/apps/travel-chaos-organizer/frontend/lib/offlineQueue.ts +0 -145
- package/apps/travel-chaos-organizer/frontend/lib/quota.tsx +0 -43
- package/apps/travel-chaos-organizer/frontend/lib/sentry.ts +0 -28
- package/apps/travel-chaos-organizer/frontend/lib/shareIntent.ts +0 -44
- package/apps/travel-chaos-organizer/frontend/package-lock.json +0 -13037
- package/apps/travel-chaos-organizer/frontend/package.json +0 -44
- package/apps/travel-chaos-organizer/frontend/tsconfig.json +0 -23
- package/apps/travel-chaos-organizer/keycloak/realm-export.json +0 -59
- package/apps/travel-chaos-organizer/landing/index.html +0 -773
- package/apps/travel-chaos-organizer/scripts/backup.sh +0 -14
- package/apps/travel-chaos-organizer/scripts/configure-env.sh +0 -82
- package/apps/travel-chaos-organizer/scripts/deploy.sh +0 -54
- package/apps/travel-chaos-organizer/scripts/health-check.sh +0 -36
- package/apps/travel-chaos-organizer/scripts/migrate.sh +0 -7
- package/apps/travel-chaos-organizer/scripts/server-setup.sh +0 -85
- package/apps/travel-chaos-organizer/scripts/smoke-test.sh +0 -29
- package/apps/travel-chaos-organizer/scripts/verify-tracking.sh +0 -71
- package/apps/travel-chaos-organizer/telegram-bot/.env.example +0 -3
- package/apps/travel-chaos-organizer/telegram-bot/Dockerfile +0 -6
- package/apps/travel-chaos-organizer/telegram-bot/bot.py +0 -504
- package/apps/travel-chaos-organizer/telegram-bot/requirements.txt +0 -3
- package/dist/packages/telegram-notify/client.d.ts +0 -19
- package/dist/packages/telegram-notify/client.js +0 -75
- package/dist/packages/telegram-notify/client.js.map +0 -1
- package/dist/src/__tests__/auth.test.js.map +0 -1
- package/dist/src/__tests__/brain-flow.test.js.map +0 -1
- package/dist/src/__tests__/confidence.test.js.map +0 -1
- package/dist/src/__tests__/e2e/api-auth.test.js.map +0 -1
- package/dist/src/__tests__/e2e/api-health.test.js.map +0 -1
- package/dist/src/__tests__/e2e/cli.test.js.map +0 -1
- package/dist/src/__tests__/e2e/config.js.map +0 -1
- package/dist/src/__tests__/e2e/funnel.test.js.map +0 -1
- package/dist/src/__tests__/e2e/mcp-tools.test.js.map +0 -1
- package/dist/src/__tests__/e2e/provisioning.test.js.map +0 -1
- package/dist/src/__tests__/handlers-unit.test.js.map +0 -1
- package/dist/src/__tests__/namespace.test.js.map +0 -1
- package/dist/src/auth.js.map +0 -1
- package/dist/src/ckg.js.map +0 -1
- package/dist/src/confidence.js.map +0 -1
- package/dist/src/embeddings.js.map +0 -1
- package/dist/src/handlers/advanced.js.map +0 -1
- package/dist/src/handlers/brain.js.map +0 -1
- package/dist/src/handlers/cache.js.map +0 -1
- package/dist/src/handlers/context.js.map +0 -1
- package/dist/src/handlers/fedbrain.js.map +0 -1
- package/dist/src/handlers/instances.js.map +0 -1
- package/dist/src/handlers/roadmap.js.map +0 -1
- package/dist/src/handlers/syndicate.js.map +0 -1
- package/dist/src/handlers/tco.d.ts +0 -16
- package/dist/src/handlers/tco.js +0 -303
- package/dist/src/handlers/tco.js.map +0 -1
- package/dist/src/handlers/team.js.map +0 -1
- package/dist/src/index.js.map +0 -1
- package/dist/src/namespace.js.map +0 -1
- package/dist/src/notifier.d.ts +0 -5
- package/dist/src/notifier.js +0 -6
- package/dist/src/notifier.js.map +0 -1
- package/dist/src/search.js.map +0 -1
- package/dist/src/search.test.js.map +0 -1
- package/dist/src/tools.js.map +0 -1
- package/dist/src/utils.js.map +0 -1
- package/packages/telegram-notify/SPLIT_PLAN.md +0 -106
- package/packages/telegram-notify/client.ts +0 -88
- package/packages/telegram-notify/emojis.json +0 -22
- package/packages/telegram-notify/pyproject.toml +0 -17
- package/packages/telegram-notify/telegram_notify/__init__.py +0 -3
- package/packages/telegram-notify/telegram_notify/client.py +0 -87
- package/packages/telegram-notify/telegram_notify/emojis.json +0 -22
- package/scripts/generate-icons.js +0 -28
- package/src/handlers/tco.ts +0 -347
- package/src/notifier.ts +0 -5
- /package/dist/{src/__tests__ → __tests__}/auth.test.d.ts +0 -0
- /package/dist/{src/__tests__ → __tests__}/auth.test.js +0 -0
- /package/dist/{src/__tests__ → __tests__}/brain-flow.test.d.ts +0 -0
- /package/dist/{src/__tests__ → __tests__}/confidence.test.d.ts +0 -0
- /package/dist/{src/__tests__ → __tests__}/confidence.test.js +0 -0
- /package/dist/{src/__tests__ → __tests__}/e2e/api-auth.test.d.ts +0 -0
- /package/dist/{src/__tests__ → __tests__}/e2e/api-auth.test.js +0 -0
- /package/dist/{src/__tests__ → __tests__}/e2e/api-health.test.d.ts +0 -0
- /package/dist/{src/__tests__ → __tests__}/e2e/api-health.test.js +0 -0
- /package/dist/{src/__tests__ → __tests__}/e2e/cli.test.d.ts +0 -0
- /package/dist/{src/__tests__ → __tests__}/e2e/cli.test.js +0 -0
- /package/dist/{src/__tests__ → __tests__}/e2e/config.d.ts +0 -0
- /package/dist/{src/__tests__ → __tests__}/e2e/config.js +0 -0
- /package/dist/{src/__tests__ → __tests__}/e2e/funnel.test.d.ts +0 -0
- /package/dist/{src/__tests__ → __tests__}/e2e/funnel.test.js +0 -0
- /package/dist/{src/__tests__ → __tests__}/e2e/mcp-tools.test.d.ts +0 -0
- /package/dist/{src/__tests__ → __tests__}/e2e/mcp-tools.test.js +0 -0
- /package/dist/{src/__tests__ → __tests__}/e2e/provisioning.test.d.ts +0 -0
- /package/dist/{src/__tests__ → __tests__}/e2e/provisioning.test.js +0 -0
- /package/dist/{src/__tests__ → __tests__}/handlers-unit.test.d.ts +0 -0
- /package/dist/{src/__tests__ → __tests__}/namespace.test.d.ts +0 -0
- /package/dist/{src/__tests__ → __tests__}/namespace.test.js +0 -0
- /package/dist/{src/auth.d.ts → auth.d.ts} +0 -0
- /package/dist/{src/auth.js → auth.js} +0 -0
- /package/dist/{src/ckg.d.ts → ckg.d.ts} +0 -0
- /package/dist/{src/ckg.js → ckg.js} +0 -0
- /package/dist/{src/confidence.d.ts → confidence.d.ts} +0 -0
- /package/dist/{src/confidence.js → confidence.js} +0 -0
- /package/dist/{src/embeddings.d.ts → embeddings.d.ts} +0 -0
- /package/dist/{src/embeddings.js → embeddings.js} +0 -0
- /package/dist/{src/handlers → handlers}/advanced.d.ts +0 -0
- /package/dist/{src/handlers → handlers}/advanced.js +0 -0
- /package/dist/{src/handlers → handlers}/brain.d.ts +0 -0
- /package/dist/{src/handlers → handlers}/cache.d.ts +0 -0
- /package/dist/{src/handlers → handlers}/cache.js +0 -0
- /package/dist/{src/handlers → handlers}/context.d.ts +0 -0
- /package/dist/{src/handlers → handlers}/instances.d.ts +0 -0
- /package/dist/{src/handlers → handlers}/instances.js +0 -0
- /package/dist/{src/handlers → handlers}/roadmap.d.ts +0 -0
- /package/dist/{src/handlers → handlers}/syndicate.d.ts +0 -0
- /package/dist/{src/handlers → handlers}/syndicate.js +0 -0
- /package/dist/{src/handlers → handlers}/team.d.ts +0 -0
- /package/dist/{src/index.d.ts → index.d.ts} +0 -0
- /package/dist/{src/namespace.d.ts → namespace.d.ts} +0 -0
- /package/dist/{src/namespace.js → namespace.js} +0 -0
- /package/dist/{src/search.d.ts → search.d.ts} +0 -0
- /package/dist/{src/search.js → search.js} +0 -0
- /package/dist/{src/search.test.d.ts → search.test.d.ts} +0 -0
- /package/dist/{src/search.test.js → search.test.js} +0 -0
- /package/dist/{src/utils.d.ts → utils.d.ts} +0 -0
- /package/dist/{src/utils.js → utils.js} +0 -0
|
@@ -1,7 +1,5 @@
|
|
|
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';
|
|
5
3
|
import { readFile } from 'node:fs/promises';
|
|
6
4
|
import { fileURLToPath } from 'node:url';
|
|
7
5
|
// True only when this file is the entry point (not imported by tests or other modules).
|
|
@@ -36,40 +34,25 @@ const _isMain = process.argv[1] != null &&
|
|
|
36
34
|
* ── Auth & Status ────────────────────────────────────────────────────────────
|
|
37
35
|
* • get_api_status – check API health + JWT auth info (Keycloak)
|
|
38
36
|
*
|
|
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
|
-
*
|
|
51
37
|
* Configuration (env vars):
|
|
52
38
|
* CACHLY_API_URL – default https://api.cachly.dev
|
|
53
39
|
* CACHLY_JWT – your JWT (Keycloak access token)
|
|
54
40
|
* CACHLY_EMBED_PROVIDER – embedding backend: openai (default), gemini, mistral, cohere, ollama, cachly (server fallback)
|
|
55
41
|
* CACHLY_EMBED_MODEL – override embedding model (optional)
|
|
56
|
-
* TCO_API_URL – Travel Chaos Organizer backend URL (optional, enables tco_* tools)
|
|
57
42
|
*/
|
|
58
43
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
59
44
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
60
45
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
61
46
|
import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js';
|
|
62
47
|
import { Redis } from 'ioredis';
|
|
48
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
63
49
|
// ── Config ────────────────────────────────────────────────────────────────────
|
|
64
50
|
const API_URL = process.env.CACHLY_API_URL ?? 'https://api.cachly.dev';
|
|
65
51
|
let JWT = process.env.CACHLY_JWT ?? '';
|
|
66
|
-
const
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
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);
|
|
52
|
+
const CURRENT_VERSION = '0.10.55';
|
|
53
|
+
const reqCtx = new AsyncLocalStorage();
|
|
54
|
+
function getJWT() { return reqCtx.getStore()?.jwt || JWT; }
|
|
55
|
+
function getDefaultInstanceId() { return reqCtx.getStore()?.instanceId || _defaultInstanceId; }
|
|
73
56
|
// ── Default Instance Resolution (for Smithery & single-credential setups) ────
|
|
74
57
|
// When CACHLY_BRAIN_INSTANCE_ID is set, tools can omit the instance_id parameter.
|
|
75
58
|
// When neither is set, we auto-fetch the first running instance once per process.
|
|
@@ -77,18 +60,29 @@ let _defaultInstanceId = process.env.CACHLY_BRAIN_INSTANCE_ID ?? '';
|
|
|
77
60
|
// Timestamp of last failed fetch — retries after 30 s (not permanently blocked).
|
|
78
61
|
let _defaultInstanceLastAttempt = 0;
|
|
79
62
|
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;
|
|
80
67
|
if (_defaultInstanceId)
|
|
81
68
|
return _defaultInstanceId;
|
|
82
|
-
if (!
|
|
69
|
+
if (!getJWT())
|
|
83
70
|
return '';
|
|
84
71
|
// 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.
|
|
85
73
|
const now = Date.now();
|
|
86
|
-
if (_defaultInstanceLastAttempt > 0 && now - _defaultInstanceLastAttempt <
|
|
87
|
-
|
|
74
|
+
if (_defaultInstanceLastAttempt > 0 && now - _defaultInstanceLastAttempt < 10_000) {
|
|
75
|
+
throw new McpError(ErrorCode.InvalidRequest, '⚠️ Cachly Brain is temporarily unreachable (API error a few seconds ago).\n\n' +
|
|
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
|
+
;
|
|
88
82
|
_defaultInstanceLastAttempt = now;
|
|
89
83
|
try {
|
|
90
84
|
const res = await fetch(`${API_URL}/api/v1/instances`, {
|
|
91
|
-
headers: { Authorization: `Bearer ${
|
|
85
|
+
headers: { Authorization: `Bearer ${getJWT()}`, Accept: 'application/json' },
|
|
92
86
|
signal: AbortSignal.timeout(5000),
|
|
93
87
|
});
|
|
94
88
|
if (!res.ok)
|
|
@@ -165,44 +159,33 @@ async function pollDeviceFlow(flow) {
|
|
|
165
159
|
catch { /* use raw JWT as fallback */ }
|
|
166
160
|
JWT = apiKey;
|
|
167
161
|
setEmbedJwt(apiKey); // keep embeddings.ts in sync
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
};
|
|
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'));
|
|
200
175
|
}
|
|
201
|
-
|
|
202
|
-
await writeFile(configPath, JSON.stringify(cfg, null, 2), 'utf-8');
|
|
176
|
+
catch { /* corrupt — start fresh */ }
|
|
203
177
|
}
|
|
204
|
-
|
|
205
|
-
|
|
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
|
+
_deviceFlow = null;
|
|
188
|
+
sendFunnelEvent('device_flow_completed');
|
|
206
189
|
// Auto-provision: find or create the user's brain instance.
|
|
207
190
|
_defaultInstanceLastAttempt = 0;
|
|
208
191
|
await resolveDefaultInstanceId();
|
|
@@ -210,7 +193,7 @@ async function pollDeviceFlow(flow) {
|
|
|
210
193
|
try {
|
|
211
194
|
const autoRes = await fetch(`${API_URL}/api/v1/instances/auto`, {
|
|
212
195
|
method: 'POST',
|
|
213
|
-
headers: { Authorization: `Bearer ${
|
|
196
|
+
headers: { Authorization: `Bearer ${getJWT()}`, 'Content-Type': 'application/json' },
|
|
214
197
|
signal: AbortSignal.timeout(12000),
|
|
215
198
|
});
|
|
216
199
|
if (autoRes.ok) {
|
|
@@ -223,7 +206,7 @@ async function pollDeviceFlow(flow) {
|
|
|
223
206
|
// Log so telemetry can detect free-tier auto-provision failures
|
|
224
207
|
void fetch(`${API_URL}/api/v1/telemetry/mcp`, {
|
|
225
208
|
method: 'POST',
|
|
226
|
-
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${
|
|
209
|
+
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${getJWT()}` },
|
|
227
210
|
body: JSON.stringify({ event: 'auto_provision_failed', status: autoRes.status, version: CURRENT_VERSION }),
|
|
228
211
|
signal: AbortSignal.timeout(3000),
|
|
229
212
|
}).catch(() => { });
|
|
@@ -236,7 +219,7 @@ async function pollDeviceFlow(flow) {
|
|
|
236
219
|
if (_defaultInstanceId) {
|
|
237
220
|
try {
|
|
238
221
|
const checkRes = await fetch(`${API_URL}/api/v1/instances/${_defaultInstanceId}`, {
|
|
239
|
-
headers: { Authorization: `Bearer ${
|
|
222
|
+
headers: { Authorization: `Bearer ${getJWT()}`, Accept: 'application/json' },
|
|
240
223
|
signal: AbortSignal.timeout(4000),
|
|
241
224
|
});
|
|
242
225
|
if (checkRes.ok) {
|
|
@@ -247,7 +230,7 @@ async function pollDeviceFlow(flow) {
|
|
|
247
230
|
while (checkInst.status === 'provisioning' && Date.now() < pDeadline) {
|
|
248
231
|
await new Promise(r => setTimeout(r, 3000));
|
|
249
232
|
const rr = await fetch(`${API_URL}/api/v1/instances/${_defaultInstanceId}`, {
|
|
250
|
-
headers: { Authorization: `Bearer ${
|
|
233
|
+
headers: { Authorization: `Bearer ${getJWT()}`, Accept: 'application/json' },
|
|
251
234
|
signal: AbortSignal.timeout(4000),
|
|
252
235
|
}).catch(() => null);
|
|
253
236
|
if (rr?.ok) {
|
|
@@ -263,37 +246,6 @@ async function pollDeviceFlow(flow) {
|
|
|
263
246
|
}
|
|
264
247
|
catch { /* non-fatal — getConnection will handle the wait if still provisioning */ }
|
|
265
248
|
}
|
|
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
|
-
}
|
|
297
249
|
return 'done';
|
|
298
250
|
}
|
|
299
251
|
if (data.error === 'slow_down')
|
|
@@ -304,12 +256,71 @@ async function pollDeviceFlow(flow) {
|
|
|
304
256
|
return 'pending';
|
|
305
257
|
}
|
|
306
258
|
}
|
|
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
|
+
}
|
|
307
316
|
// ── Embeddings (imported from embeddings.ts) ────────────────────────────────
|
|
308
317
|
import { setEmbedJwt } from './embeddings.js';
|
|
309
318
|
// ── Search Engine (imported from search.ts) ─────────────────────────────────
|
|
310
319
|
import { tokenize, splitMultiQuery, levenshtein, recencyBoost, extractTimestamp, STOPWORDS, katakanaToRomaji, arabicLightStem, expandCrossLingual, CROSS_LINGUAL_MAP, ZERO_RESULTS_LOG, zeroResultsTotal } from './search.js';
|
|
311
320
|
// ── Exported for testing (re-export from search.ts) ─────────────────────────
|
|
312
321
|
export { tokenize, splitMultiQuery, levenshtein, recencyBoost, extractTimestamp, STOPWORDS, katakanaToRomaji, arabicLightStem, expandCrossLingual, CROSS_LINGUAL_MAP };
|
|
322
|
+
// ── Types ─────────────────────────────────────────────────────────────────────
|
|
323
|
+
// Instance is imported from handlers/brain.ts
|
|
313
324
|
// ── Connection pool ───────────────────────────────────────────────────────────
|
|
314
325
|
/** Reuse Redis connections across tool calls (keyed by instance_id). */
|
|
315
326
|
const pool = new Map();
|
|
@@ -320,16 +331,26 @@ async function getConnection(instance_id) {
|
|
|
320
331
|
'• Pass instance_id explicitly to the tool, or\n' +
|
|
321
332
|
'• Run `list_instances` to see your available instances.');
|
|
322
333
|
}
|
|
323
|
-
if (pool.has(instance_id))
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
334
|
+
if (pool.has(instance_id)) {
|
|
335
|
+
const cached = pool.get(instance_id);
|
|
336
|
+
// Verify the pooled connection is still alive before reusing it.
|
|
337
|
+
// ioredis 'error' eviction is async — a connection can be in the pool
|
|
338
|
+
// but already internally broken (status 'reconnecting' or 'end').
|
|
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.)
|
|
328
349
|
let inst = await apiFetch(`/api/v1/instances/${instance_id}`);
|
|
329
350
|
if (inst.status === 'provisioning') {
|
|
330
|
-
const deadline = Date.now() +
|
|
351
|
+
const deadline = Date.now() + 8_000;
|
|
331
352
|
while (inst.status === 'provisioning' && Date.now() < deadline) {
|
|
332
|
-
await new Promise(r => setTimeout(r,
|
|
353
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
333
354
|
inst = await apiFetch(`/api/v1/instances/${instance_id}`);
|
|
334
355
|
}
|
|
335
356
|
}
|
|
@@ -337,7 +358,7 @@ async function getConnection(instance_id) {
|
|
|
337
358
|
// Telemetry: unreachable instance so the team can proactively investigate
|
|
338
359
|
void fetch(`${API_URL}/api/v1/telemetry/mcp`, {
|
|
339
360
|
method: 'POST',
|
|
340
|
-
headers: { 'Content-Type': 'application/json', ...(
|
|
361
|
+
headers: { 'Content-Type': 'application/json', ...(getJWT() ? { Authorization: `Bearer ${getJWT()}` } : {}) },
|
|
341
362
|
body: JSON.stringify({ event: 'instance_not_reachable', instance_id, status: inst.status, version: CURRENT_VERSION }),
|
|
342
363
|
signal: AbortSignal.timeout(3000),
|
|
343
364
|
}).catch(() => { });
|
|
@@ -370,7 +391,9 @@ async function getConnection(instance_id) {
|
|
|
370
391
|
}
|
|
371
392
|
// Fetch connection details (includes password) from the dedicated endpoint.
|
|
372
393
|
let password;
|
|
373
|
-
|
|
394
|
+
// Default to false — most free/dev instances run without TLS.
|
|
395
|
+
// Only enable TLS when the API explicitly returns tls_enabled: true.
|
|
396
|
+
let tlsEnabled = inst.tls_enabled === true;
|
|
374
397
|
try {
|
|
375
398
|
const conn = await apiFetch(`/api/v1/instances/${instance_id}/connection`);
|
|
376
399
|
password = conn.password ?? undefined;
|
|
@@ -389,25 +412,56 @@ async function getConnection(instance_id) {
|
|
|
389
412
|
host: inst.host,
|
|
390
413
|
port: inst.port,
|
|
391
414
|
password: password || undefined,
|
|
392
|
-
commandTimeout: 8000,
|
|
393
415
|
...(tlsEnabled ? { tls: {} } : {}),
|
|
394
416
|
lazyConnect: true,
|
|
395
417
|
enableReadyCheck: true,
|
|
396
418
|
connectTimeout: 8000,
|
|
419
|
+
commandTimeout: 10000,
|
|
397
420
|
retryStrategy: () => null, // fail fast, no reconnect loops in MCP context
|
|
398
421
|
});
|
|
399
422
|
const evict = () => pool.delete(instance_id);
|
|
400
423
|
client.on('error', evict);
|
|
401
424
|
client.on('end', evict);
|
|
402
|
-
|
|
403
|
-
|
|
425
|
+
// Retry up to 3 times with 2s backoff. Covers the window between K8s PodReady
|
|
426
|
+
// and Valkey actually accepting connections (RDB/AOF load, ~1-3s on cold start).
|
|
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
|
+
}
|
|
404
458
|
}
|
|
405
|
-
|
|
459
|
+
if (connectErr) {
|
|
406
460
|
client.disconnect();
|
|
407
|
-
const msg =
|
|
461
|
+
const msg = connectErr instanceof Error ? connectErr.message : String(connectErr);
|
|
408
462
|
void fetch(`${API_URL}/api/v1/telemetry/mcp`, {
|
|
409
463
|
method: 'POST',
|
|
410
|
-
headers: { 'Content-Type': 'application/json', ...(
|
|
464
|
+
headers: { 'Content-Type': 'application/json', ...(getJWT() ? { Authorization: `Bearer ${getJWT()}` } : {}) },
|
|
411
465
|
body: JSON.stringify({ event: 'redis_connect_failed', instance_id, host: inst.host, port: inst.port, error: msg, version: CURRENT_VERSION }),
|
|
412
466
|
signal: AbortSignal.timeout(3000),
|
|
413
467
|
}).catch(() => { });
|
|
@@ -421,14 +475,14 @@ async function getConnection(instance_id) {
|
|
|
421
475
|
}
|
|
422
476
|
// ── API helper ────────────────────────────────────────────────────────────────
|
|
423
477
|
async function apiFetch(path, options = {}) {
|
|
424
|
-
checkJwt(
|
|
478
|
+
checkJwt(getJWT());
|
|
425
479
|
const timeoutMs = Number(process.env.CACHLY_API_TIMEOUT_MS ?? 15_000);
|
|
426
480
|
const res = await fetch(`${API_URL}${path}`, {
|
|
427
481
|
...options,
|
|
428
482
|
signal: AbortSignal.timeout(timeoutMs),
|
|
429
483
|
headers: {
|
|
430
484
|
'Content-Type': 'application/json',
|
|
431
|
-
Authorization: `Bearer ${
|
|
485
|
+
Authorization: `Bearer ${getJWT()}`,
|
|
432
486
|
...options.headers,
|
|
433
487
|
},
|
|
434
488
|
});
|
|
@@ -449,7 +503,7 @@ import { handleTeamTool } from './handlers/team.js';
|
|
|
449
503
|
import { handleRoadmapTool } from './handlers/roadmap.js';
|
|
450
504
|
import { handleAdvancedTool } from './handlers/advanced.js';
|
|
451
505
|
import { handleSyndicateTool } from './handlers/syndicate.js';
|
|
452
|
-
import { handleFedbrainTool
|
|
506
|
+
import { handleFedbrainTool } from './handlers/fedbrain.js';
|
|
453
507
|
// ── Tools (imported from tools.ts) ─────────────────────────────────────────
|
|
454
508
|
import { TOOLS } from './tools.js';
|
|
455
509
|
// ── Handlers ──────────────────────────────────────────────────────────────────
|
|
@@ -472,7 +526,10 @@ function sendFunnelEvent(event, extra) {
|
|
|
472
526
|
headers: { 'Content-Type': 'application/json' },
|
|
473
527
|
body: JSON.stringify({
|
|
474
528
|
event, version: CURRENT_VERSION, editor: detectEditor(),
|
|
475
|
-
|
|
529
|
+
// Send Cachly API keys (cky_*) for server-side tenant resolution (recall counting, emails).
|
|
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() } : {}),
|
|
476
533
|
...extra,
|
|
477
534
|
}),
|
|
478
535
|
signal: AbortSignal.timeout(3000),
|
|
@@ -495,11 +552,18 @@ async function sendAnonymousTelemetry(toolName) {
|
|
|
495
552
|
catch { /* fire-and-forget, never block the user */ }
|
|
496
553
|
}
|
|
497
554
|
async function handleTool(name, args) {
|
|
498
|
-
// Guard: if no JWT,
|
|
499
|
-
if (!
|
|
555
|
+
// Guard: if no JWT, try silent auto-setup first, then fall back to device flow.
|
|
556
|
+
if (!getJWT()) {
|
|
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
|
+
}
|
|
500
564
|
void sendAnonymousTelemetry(name);
|
|
501
|
-
// ── Zero-credential device flow
|
|
502
|
-
// 1st call: start device flow, return code + URL
|
|
565
|
+
// ── Step 2: Zero-credential device flow ─────────────────────────────
|
|
566
|
+
// 1st call after silent setup fails: start device flow, return code + URL
|
|
503
567
|
// 2nd+ calls: poll for token; once authenticated, proceed transparently
|
|
504
568
|
if (_deviceFlow) {
|
|
505
569
|
const result = await pollDeviceFlow(_deviceFlow);
|
|
@@ -571,16 +635,16 @@ async function handleTool(name, args) {
|
|
|
571
635
|
args = { ...args, instance_id: defaultId };
|
|
572
636
|
}
|
|
573
637
|
// Delegate brain tools (learn, recall, session, etc.)
|
|
574
|
-
|
|
638
|
+
let brainResult = await handleBrainTool(name, args, getConnection, apiFetch);
|
|
575
639
|
if (brainResult !== null) {
|
|
576
|
-
if (!_firstCallSuccessSent &&
|
|
640
|
+
if (!_firstCallSuccessSent && getJWT()) {
|
|
577
641
|
_firstCallSuccessSent = true;
|
|
578
|
-
sendFunnelEvent('first_call_success', { tool: name, instance_id: args.instance_id ??
|
|
642
|
+
sendFunnelEvent('first_call_success', { tool: name, instance_id: args.instance_id ?? getDefaultInstanceId() ?? '' });
|
|
579
643
|
}
|
|
580
644
|
// Per-tool telemetry — fires after every successful brain tool call.
|
|
581
645
|
// api_key carries the cky_live_... token so the backend can resolve tenant + increment counters.
|
|
582
|
-
const instanceId = args.instance_id ??
|
|
583
|
-
const telemetryExtra =
|
|
646
|
+
const instanceId = args.instance_id ?? getDefaultInstanceId() ?? '';
|
|
647
|
+
const telemetryExtra = getJWT() ? { api_key: getJWT(), instance_id: instanceId } : { instance_id: instanceId };
|
|
584
648
|
if (name === 'recall_best_solution') {
|
|
585
649
|
// recall_best_solution → increments BrainRecallCount + triggers level-up logic
|
|
586
650
|
sendFunnelEvent('recall_best_solution', telemetryExtra);
|
|
@@ -598,25 +662,48 @@ async function handleTool(name, args) {
|
|
|
598
662
|
// Existing brain — session_start acts as implicit recall → increment counter.
|
|
599
663
|
sendFunnelEvent('recall_best_solution', telemetryExtra);
|
|
600
664
|
}
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
665
|
+
// Auto-bootstrap brain_from_git when workspace_path is provided and it hasn't run yet.
|
|
666
|
+
// We check a sentinel key in Valkey so this only fires once per workspace path,
|
|
667
|
+
// not just on the very first session (handles cases where brain was empty but
|
|
668
|
+
// session data existed, or where the user re-installed).
|
|
669
|
+
if (args.workspace_path) {
|
|
604
670
|
try {
|
|
605
|
-
const
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
if (bootstrapText) {
|
|
618
|
-
return String(brainResult) + '\n---\n' + bootstrapText;
|
|
671
|
+
const workspacePath = args.workspace_path;
|
|
672
|
+
const autoKey = `cachly:brain_from_git:auto_done:${Buffer.from(workspacePath).toString('base64').slice(0, 40)}`;
|
|
673
|
+
const redis = await getConnection(instanceId);
|
|
674
|
+
const alreadyDone = await redis.get(autoKey);
|
|
675
|
+
if (!alreadyDone) {
|
|
676
|
+
// Mark immediately to prevent concurrent runs across multiple MCP tool calls
|
|
677
|
+
await redis.set(autoKey, '1', 'EX', 365 * 86400);
|
|
678
|
+
// Append a placeholder so the user gets immediate feedback, then bootstrap in background.
|
|
679
|
+
// Running brain_from_git synchronously would block session_start for 30-120s on large repos.
|
|
680
|
+
const bgMsg = '\n\n---\n\n🚀 **Auto-indexing your git history in the background** — your Brain will learn from recent commits automatically. No action needed.';
|
|
681
|
+
if (typeof brainResult === 'object' && brainResult !== null && 'content' in brainResult) {
|
|
682
|
+
brainResult.content.push({ type: 'text', text: bgMsg });
|
|
619
683
|
}
|
|
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(() => { });
|
|
620
707
|
}
|
|
621
708
|
}
|
|
622
709
|
catch { /* git bootstrap errors must never break session_start */ }
|
|
@@ -633,7 +720,7 @@ async function handleTool(name, args) {
|
|
|
633
720
|
const srText = typeof brainResult === 'object' && brainResult !== null && 'content' in brainResult
|
|
634
721
|
? JSON.stringify(brainResult)
|
|
635
722
|
: String(brainResult ?? '');
|
|
636
|
-
if (srText.length > 50 && !srText.includes('No lessons found') && !srText.includes('no lessons')
|
|
723
|
+
if (srText.length > 50 && !srText.includes('No lessons found') && !srText.includes('no lessons')) {
|
|
637
724
|
sendFunnelEvent('recall_best_solution', telemetryExtra);
|
|
638
725
|
}
|
|
639
726
|
}
|
|
@@ -642,9 +729,9 @@ async function handleTool(name, args) {
|
|
|
642
729
|
// Delegate context tools (remember/recall/list/forget)
|
|
643
730
|
const contextResult = await handleContextTool(name, args, getConnection, apiFetch);
|
|
644
731
|
if (contextResult !== null) {
|
|
645
|
-
if (!_firstCallSuccessSent &&
|
|
732
|
+
if (!_firstCallSuccessSent && getJWT()) {
|
|
646
733
|
_firstCallSuccessSent = true;
|
|
647
|
-
sendFunnelEvent('first_call_success', { tool: name, instance_id: args.instance_id ??
|
|
734
|
+
sendFunnelEvent('first_call_success', { tool: name, instance_id: args.instance_id ?? getDefaultInstanceId() ?? '' });
|
|
648
735
|
}
|
|
649
736
|
return contextResult;
|
|
650
737
|
}
|
|
@@ -669,8 +756,8 @@ async function handleTool(name, args) {
|
|
|
669
756
|
if (syndicateResult !== null) {
|
|
670
757
|
// brain_predict is in syndicate handler — track telemetry here.
|
|
671
758
|
if (name === 'brain_predict') {
|
|
672
|
-
const instanceId = args.instance_id ??
|
|
673
|
-
const telemetryExtra =
|
|
759
|
+
const instanceId = args.instance_id ?? getDefaultInstanceId() ?? '';
|
|
760
|
+
const telemetryExtra = getJWT() ? { api_key: getJWT(), instance_id: instanceId } : { instance_id: instanceId };
|
|
674
761
|
sendFunnelEvent('brain_predict', telemetryExtra);
|
|
675
762
|
}
|
|
676
763
|
return syndicateResult;
|
|
@@ -678,21 +765,16 @@ async function handleTool(name, args) {
|
|
|
678
765
|
const fedbrainResult = await handleFedbrainTool(name, args, getConnection, apiFetch);
|
|
679
766
|
if (fedbrainResult !== null) {
|
|
680
767
|
// brain_from_git and brain_predict_failures are in fedbrain handler.
|
|
681
|
-
const instanceId = args.instance_id ??
|
|
682
|
-
const telemetryExtra =
|
|
768
|
+
const instanceId = args.instance_id ?? getDefaultInstanceId() ?? '';
|
|
769
|
+
const telemetryExtra = getJWT() ? { api_key: getJWT(), instance_id: instanceId } : { instance_id: instanceId };
|
|
683
770
|
if (name === 'brain_from_git') {
|
|
684
|
-
|
|
685
|
-
sendFunnelEvent('brain_from_git', { ...telemetryExtra, ...(gitCounts ?? {}) });
|
|
771
|
+
sendFunnelEvent('brain_from_git', telemetryExtra);
|
|
686
772
|
}
|
|
687
773
|
else if (name === 'brain_predict_failures') {
|
|
688
774
|
sendFunnelEvent('brain_predict_failures', telemetryExtra);
|
|
689
775
|
}
|
|
690
776
|
return fedbrainResult;
|
|
691
777
|
}
|
|
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;
|
|
696
778
|
switch (name) {
|
|
697
779
|
// ── Instance management ──────────────────────────────────────────────
|
|
698
780
|
case 'get_api_status': {
|
|
@@ -712,7 +794,7 @@ async function handleTool(name, args) {
|
|
|
712
794
|
healthStatus = `unreachable: ${e.message}`;
|
|
713
795
|
}
|
|
714
796
|
// Check JWT / auth
|
|
715
|
-
if (!
|
|
797
|
+
if (!getJWT()) {
|
|
716
798
|
return [
|
|
717
799
|
`📡 **cachly API Status**`,
|
|
718
800
|
``,
|
|
@@ -724,27 +806,38 @@ async function handleTool(name, args) {
|
|
|
724
806
|
].join('\n');
|
|
725
807
|
}
|
|
726
808
|
// Decode JWT claims (inspection only, no verification)
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
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');
|
|
744
|
-
}
|
|
809
|
+
const jwt = getJWT();
|
|
810
|
+
let authInfo;
|
|
811
|
+
// API keys (cky_live_... / cky_trial_...) are valid long-lived credentials.
|
|
812
|
+
// Only try to decode as JWT (3-part base64url) if it looks like one.
|
|
813
|
+
if (jwt.startsWith('cky_')) {
|
|
814
|
+
const keyType = jwt.startsWith('cky_trial_') ? 'Trial' : 'Live';
|
|
815
|
+
const keyId = jwt.slice(0, 20) + '…';
|
|
816
|
+
authInfo = `✅ API key (${keyType})\n Key prefix: ${keyId}`;
|
|
745
817
|
}
|
|
746
|
-
|
|
747
|
-
authInfo = '❌
|
|
818
|
+
else {
|
|
819
|
+
authInfo = '❌ invalid credential format';
|
|
820
|
+
try {
|
|
821
|
+
const parts = jwt.split('.');
|
|
822
|
+
if (parts.length === 3) {
|
|
823
|
+
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf-8'));
|
|
824
|
+
const sub = payload.sub ?? '(unknown)';
|
|
825
|
+
const iss = payload.iss ?? '(unknown)';
|
|
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';
|
|
840
|
+
}
|
|
748
841
|
}
|
|
749
842
|
// List instances + connectivity
|
|
750
843
|
let instanceInfo = '';
|
|
@@ -758,7 +851,7 @@ async function handleTool(name, args) {
|
|
|
758
851
|
const lines = ['\n\n🧠 **Brain Instances:**'];
|
|
759
852
|
for (const inst of instances) {
|
|
760
853
|
const badge = inst.status === 'running' ? '🟢' : inst.status === 'provisioning' ? '🟡' : '🔴';
|
|
761
|
-
const defaultMark = inst.id ===
|
|
854
|
+
const defaultMark = inst.id === getDefaultInstanceId() ? ' ← active' : '';
|
|
762
855
|
lines.push(` ${badge} **${inst.name}** (\`${inst.id}\`) · ${inst.status}${defaultMark}`);
|
|
763
856
|
if (inst.status === 'running' && inst.host) {
|
|
764
857
|
// Try a quick ping on the cached connection
|
|
@@ -886,13 +979,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
886
979
|
|| _defaultInstanceId
|
|
887
980
|
|| process.env.CACHLY_BRAIN_INSTANCE_ID;
|
|
888
981
|
if (instanceId) {
|
|
889
|
-
|
|
982
|
+
// Fire-and-forget — session management is non-critical and must never block tool calls.
|
|
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);
|
|
890
986
|
}
|
|
891
987
|
}
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
await _autoSessionStarting.catch(() => undefined);
|
|
895
|
-
}
|
|
988
|
+
// Note: we intentionally do NOT await _autoSessionStarting for any call.
|
|
989
|
+
// The session tracker runs in background — tool calls must always proceed immediately.
|
|
896
990
|
if (!sessionTools.has(name))
|
|
897
991
|
_autoSessionToolCount++;
|
|
898
992
|
try {
|
|
@@ -902,9 +996,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
902
996
|
catch (err) {
|
|
903
997
|
if (err instanceof McpError)
|
|
904
998
|
throw err;
|
|
905
|
-
|
|
906
|
-
notify('cachly', 'tool_error', { tool: name, error: msg }).catch(() => undefined);
|
|
907
|
-
throw new McpError(ErrorCode.InternalError, msg);
|
|
999
|
+
throw new McpError(ErrorCode.InternalError, err.message);
|
|
908
1000
|
}
|
|
909
1001
|
});
|
|
910
1002
|
// Graceful shutdown – close all Redis connections + auto-end brain session
|
|
@@ -1892,7 +1984,7 @@ if (process.argv[2] === 'status') {
|
|
|
1892
1984
|
if (process.argv[2] === 'health') {
|
|
1893
1985
|
const { existsSync } = await import('node:fs');
|
|
1894
1986
|
const { readFile } = await import('node:fs/promises');
|
|
1895
|
-
const { resolve
|
|
1987
|
+
const { resolve } = await import('node:path');
|
|
1896
1988
|
const cwd = process.cwd();
|
|
1897
1989
|
let passed = 0;
|
|
1898
1990
|
let failed = 0;
|
|
@@ -1906,6 +1998,10 @@ if (process.argv[2] === 'health') {
|
|
|
1906
1998
|
if (!jwt) {
|
|
1907
1999
|
fail('CACHLY_JWT not set — run: npx @cachly-dev/mcp-server@latest setup');
|
|
1908
2000
|
}
|
|
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
|
+
}
|
|
1909
2005
|
else {
|
|
1910
2006
|
try {
|
|
1911
2007
|
const parts = jwt.split('.');
|
|
@@ -1925,7 +2021,7 @@ if (process.argv[2] === 'health') {
|
|
|
1925
2021
|
}
|
|
1926
2022
|
}
|
|
1927
2023
|
catch {
|
|
1928
|
-
fail('CACHLY_JWT format invalid (expected JWT with 3 parts)');
|
|
2024
|
+
fail('CACHLY_JWT format invalid (expected a cky_*** API key or a JWT with 3 parts)');
|
|
1929
2025
|
}
|
|
1930
2026
|
}
|
|
1931
2027
|
// ── 2. Brain API reachability ───────────────────────────────────────────────
|
|
@@ -1976,11 +2072,27 @@ if (process.argv[2] === 'health') {
|
|
|
1976
2072
|
});
|
|
1977
2073
|
if (connRes.ok) {
|
|
1978
2074
|
const conn = await connRes.json();
|
|
1979
|
-
|
|
1980
|
-
|
|
2075
|
+
// password may be a direct field, or embedded in connection_string as redis://:pass@host:port
|
|
2076
|
+
if (conn.password) {
|
|
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`);
|
|
1981
2091
|
}
|
|
1982
2092
|
}
|
|
1983
|
-
catch {
|
|
2093
|
+
catch (connErr) {
|
|
2094
|
+
warn(`Could not fetch Redis credentials — ${connErr.message}`);
|
|
2095
|
+
}
|
|
1984
2096
|
const pingClient = new Redis({
|
|
1985
2097
|
host: inst.host, port: inst.port,
|
|
1986
2098
|
password: pingPassword,
|
|
@@ -2114,7 +2226,6 @@ if (process.argv[2] === 'setup') {
|
|
|
2114
2226
|
const AUTH_BASE = 'https://auth.cachly.dev/realms/cachly/protocol/openid-connect';
|
|
2115
2227
|
const CLIENT_ID = 'cachly-cli';
|
|
2116
2228
|
console.log('Step 1: Sign in to cachly (free, no credit card)\n');
|
|
2117
|
-
sendFunnelEvent('setup_auth_started');
|
|
2118
2229
|
// Start device flow
|
|
2119
2230
|
let deviceCode = '', userCode = '', verifyUri = '', pollInterval = 5000;
|
|
2120
2231
|
try {
|
|
@@ -2140,7 +2251,6 @@ if (process.argv[2] === 'setup') {
|
|
|
2140
2251
|
rl.close();
|
|
2141
2252
|
process.exit(1);
|
|
2142
2253
|
}
|
|
2143
|
-
sendFunnelEvent('setup_auth_completed');
|
|
2144
2254
|
console.log('');
|
|
2145
2255
|
deviceCode = ''; // mark as fallback so we skip polling
|
|
2146
2256
|
}
|
|
@@ -2173,7 +2283,6 @@ if (process.argv[2] === 'setup') {
|
|
|
2173
2283
|
if (tokenData.access_token) {
|
|
2174
2284
|
token = tokenData.access_token;
|
|
2175
2285
|
console.log(' \x1b[32m✓ Authorized!\x1b[0m\n');
|
|
2176
|
-
sendFunnelEvent('setup_auth_completed');
|
|
2177
2286
|
break;
|
|
2178
2287
|
}
|
|
2179
2288
|
// authorization_pending = keep polling; slow_down = increase interval
|
|
@@ -2214,7 +2323,7 @@ if (process.argv[2] === 'setup') {
|
|
|
2214
2323
|
token = keyBody.key; // swap JWT → cky_live_...
|
|
2215
2324
|
console.log(' ✓\n');
|
|
2216
2325
|
}
|
|
2217
|
-
catch
|
|
2326
|
+
catch {
|
|
2218
2327
|
console.log(' (skipped)\n');
|
|
2219
2328
|
// Non-fatal: fall back to using the Keycloak JWT directly.
|
|
2220
2329
|
// It will expire but setup still works for now.
|
|
@@ -2267,10 +2376,9 @@ if (process.argv[2] === 'setup') {
|
|
|
2267
2376
|
// Newly created — poll until running or give up after 30 s.
|
|
2268
2377
|
const newId = autoBody.instance_id;
|
|
2269
2378
|
console.log(` ✓ created (${newId.slice(0, 8)}…)\n`);
|
|
2270
|
-
// 45 attempts × 3s = 135s — generous for free-tier in slow regions.
|
|
2271
2379
|
process.stdout.write('⏳ Waiting for instance to start');
|
|
2272
|
-
for (let attempt = 0; attempt <
|
|
2273
|
-
await new Promise(r => setTimeout(r,
|
|
2380
|
+
for (let attempt = 0; attempt < 15; attempt++) {
|
|
2381
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
2274
2382
|
process.stdout.write('.');
|
|
2275
2383
|
try {
|
|
2276
2384
|
const checkRes = await fetch(`${API_URL}/api/v1/instances/${newId}`, { headers: { Authorization: `Bearer ${token}` } });
|
|
@@ -2309,7 +2417,6 @@ if (process.argv[2] === 'setup') {
|
|
|
2309
2417
|
console.log(` (Run with --instance-id <id> to use a different one)\n`);
|
|
2310
2418
|
}
|
|
2311
2419
|
console.log(`✓ Instance: ${instance.name} (${instance.id.slice(0, 8)}…)\n`);
|
|
2312
|
-
sendFunnelEvent('setup_instance_ready', { instance_id: instance.id });
|
|
2313
2420
|
// ── Step 3: Detect editors ────────────────────────────────────────────────
|
|
2314
2421
|
const cwd = process.cwd();
|
|
2315
2422
|
const detected = [];
|
|
@@ -2487,6 +2594,8 @@ if (process.argv[2] === 'setup') {
|
|
|
2487
2594
|
console.log(` Dashboard: https://cachly.dev/instances/${instance.id}`);
|
|
2488
2595
|
console.log(`\n 📛 Add a live badge to your README:`);
|
|
2489
2596
|
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`);
|
|
2490
2599
|
// ── Step 7: Email opt-in (non-blocking — at the very end) ────────────────
|
|
2491
2600
|
if (!nonInteractive) {
|
|
2492
2601
|
const email = await ask(' 📬 Stay in the loop? Email for release notes [Enter to skip]: ');
|
|
@@ -2558,7 +2667,7 @@ if (process.argv[2] === 'index') {
|
|
|
2558
2667
|
const instanceId = flag('instance-id') ?? process.env.CACHLY_BRAIN_INSTANCE_ID;
|
|
2559
2668
|
const maxFiles = parseInt(flag('max-files') ?? '500', 10);
|
|
2560
2669
|
const namespace = flag('namespace') ?? 'cachly:sem:code';
|
|
2561
|
-
if (!instanceId || !
|
|
2670
|
+
if (!instanceId || !getJWT()) {
|
|
2562
2671
|
console.error('\n❌ CACHLY_BRAIN_INSTANCE_ID and CACHLY_JWT must be set\n');
|
|
2563
2672
|
console.error(' export CACHLY_BRAIN_INSTANCE_ID=<uuid>');
|
|
2564
2673
|
console.error(' export CACHLY_JWT=<cky_live_...>');
|
|
@@ -2595,7 +2704,7 @@ if (process.argv[2] === 'learn-git') {
|
|
|
2595
2704
|
const repoDir = resolve(flag('dir') ?? argv.find(a => !a.startsWith('--')) ?? '.');
|
|
2596
2705
|
const instanceId = flag('instance-id') ?? process.env.CACHLY_BRAIN_INSTANCE_ID;
|
|
2597
2706
|
const maxCommits = parseInt(flag('max-commits') ?? '50', 10);
|
|
2598
|
-
if (!instanceId || !
|
|
2707
|
+
if (!instanceId || !getJWT()) {
|
|
2599
2708
|
console.error('\n❌ CACHLY_BRAIN_INSTANCE_ID and CACHLY_JWT must be set\n');
|
|
2600
2709
|
console.error(' export CACHLY_BRAIN_INSTANCE_ID=<uuid>');
|
|
2601
2710
|
console.error(' export CACHLY_JWT=<cky_live_...>');
|
|
@@ -2611,7 +2720,7 @@ if (process.argv[2] === 'learn-git') {
|
|
|
2611
2720
|
limit: maxCommits,
|
|
2612
2721
|
});
|
|
2613
2722
|
console.log(result);
|
|
2614
|
-
sendFunnelEvent('brain_from_git', {
|
|
2723
|
+
sendFunnelEvent('brain_from_git', { instance_id: instanceId });
|
|
2615
2724
|
console.log('\n✅ Brain learned from your commits.\n');
|
|
2616
2725
|
// Let fire-and-forget telemetry flush before exit.
|
|
2617
2726
|
await new Promise(r => setTimeout(r, 300));
|
|
@@ -2627,7 +2736,8 @@ if (process.argv[2] === 'learn-git') {
|
|
|
2627
2736
|
// actionable message in their editor's MCP log instead of silent failures.
|
|
2628
2737
|
// Skip for CLI commands that intentionally run without credentials.
|
|
2629
2738
|
const _cliNoAuthCommands = ['demo', 'share', 'health', 'setup', 'init', 'digest', 'invite', 'badge', 'join', 'upgrade'];
|
|
2630
|
-
|
|
2739
|
+
// In multi-tenant HTTP mode (PORT set) we never need a startup JWT — per-request auth.
|
|
2740
|
+
if (!JWT && !process.env.PORT && !_cliNoAuthCommands.includes(process.argv[2] ?? '') && !(!process.argv[2] && process.stdout.isTTY)) {
|
|
2631
2741
|
// No args + no JWT in a non-TTY context = running as MCP server without credentials.
|
|
2632
2742
|
// Print actionable setup banner to stdout so it's captured by callers/tests.
|
|
2633
2743
|
const banner = '\n' +
|
|
@@ -2691,12 +2801,79 @@ if (!process.env.CACHLY_NO_UPDATE_CHECK) {
|
|
|
2691
2801
|
})();
|
|
2692
2802
|
}
|
|
2693
2803
|
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
|
+
}
|
|
2694
2825
|
if (httpPort) {
|
|
2695
2826
|
// ── HTTP mode (Streamable HTTP transport) ───────────────────────────────
|
|
2696
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.
|
|
2697
2832
|
const { createServer } = await import('node:http');
|
|
2698
|
-
|
|
2699
|
-
|
|
2833
|
+
function extractReqCtx(req, url) {
|
|
2834
|
+
let jwtVal = '';
|
|
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
|
+
}
|
|
2700
2877
|
const httpServer = createServer((req, res) => {
|
|
2701
2878
|
const url = new URL(req.url ?? '/', `http://localhost:${httpPort}`);
|
|
2702
2879
|
if (url.pathname === '/health') {
|
|
@@ -2704,6 +2881,8 @@ if (httpPort) {
|
|
|
2704
2881
|
res.end(JSON.stringify({
|
|
2705
2882
|
status: 'ok',
|
|
2706
2883
|
version: CURRENT_VERSION,
|
|
2884
|
+
transport: 'streamable-http',
|
|
2885
|
+
multiTenant: true,
|
|
2707
2886
|
zeroResults: {
|
|
2708
2887
|
total: zeroResultsTotal,
|
|
2709
2888
|
last10: ZERO_RESULTS_LOG.slice(-10).map(e => ({ query: e.query, ts: new Date(e.ts).toISOString() })),
|
|
@@ -2711,8 +2890,52 @@ if (httpPort) {
|
|
|
2711
2890
|
}));
|
|
2712
2891
|
return;
|
|
2713
2892
|
}
|
|
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
|
+
}
|
|
2714
2906
|
if (url.pathname === '/mcp' || url.pathname === '/') {
|
|
2715
|
-
|
|
2907
|
+
// Handle CORS preflight
|
|
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
|
+
});
|
|
2716
2939
|
return;
|
|
2717
2940
|
}
|
|
2718
2941
|
res.writeHead(404);
|
|
@@ -2727,5 +2950,4 @@ else {
|
|
|
2727
2950
|
const transport = new StdioServerTransport();
|
|
2728
2951
|
await server.connect(transport);
|
|
2729
2952
|
}
|
|
2730
|
-
notify('cachly', 'startup', { version: CURRENT_VERSION, mode: process.env.MCP_HTTP_PORT ? 'http' : 'stdio' }).catch(() => undefined);
|
|
2731
2953
|
//# sourceMappingURL=index.js.map
|