@cachly-dev/mcp-server 0.10.48 → 0.10.49
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/README.md +5 -33
- package/STRATEGY.md +209 -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 +13037 -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/auth.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 +134 -30
- 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/src/handlers/team.js.map +1 -0
- package/dist/{index.js → src/index.js} +153 -218
- 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/search.js.map +1 -0
- package/dist/src/search.test.js.map +1 -0
- package/dist/{tools.d.ts → src/tools.d.ts} +196 -0
- package/dist/{tools.js → src/tools.js} +178 -0
- package/dist/src/tools.js.map +1 -0
- package/dist/src/utils.js.map +1 -0
- package/package.json +3 -3
- 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/smithery.yaml +21 -8
- package/src/__tests__/brain-flow.test.ts +174 -0
- package/src/__tests__/handlers-unit.test.ts +1 -2
- package/src/handlers/brain.ts +138 -31
- 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/index.ts +139 -220
- package/src/notifier.ts +5 -0
- package/src/tools.ts +191 -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/{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/{handlers → src/handlers}/team.js +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.d.ts → src/search.test.d.ts} +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,5 +1,7 @@
|
|
|
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';
|
|
4
6
|
import { execSync } from 'node:child_process';
|
|
5
7
|
import { join } from 'node:path';
|
|
@@ -37,11 +39,24 @@ const _isMain = process.argv[1] != null &&
|
|
|
37
39
|
* ── Auth & Status ────────────────────────────────────────────────────────────
|
|
38
40
|
* • get_api_status – check API health + JWT auth info (Keycloak)
|
|
39
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
|
+
*
|
|
40
54
|
* Configuration (env vars):
|
|
41
55
|
* CACHLY_API_URL – default https://api.cachly.dev
|
|
42
56
|
* CACHLY_JWT – your JWT (Keycloak access token)
|
|
43
57
|
* CACHLY_EMBED_PROVIDER – embedding backend: openai (default), gemini, mistral, cohere, ollama, cachly (server fallback)
|
|
44
58
|
* CACHLY_EMBED_MODEL – override embedding model (optional)
|
|
59
|
+
* TCO_API_URL – Travel Chaos Organizer backend URL (optional, enables tco_* tools)
|
|
45
60
|
*/
|
|
46
61
|
|
|
47
62
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
@@ -54,23 +69,19 @@ import {
|
|
|
54
69
|
McpError,
|
|
55
70
|
} from '@modelcontextprotocol/sdk/types.js';
|
|
56
71
|
import { Redis } from 'ioredis';
|
|
57
|
-
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
58
72
|
|
|
59
73
|
// ── Config ────────────────────────────────────────────────────────────────────
|
|
60
74
|
|
|
61
75
|
const API_URL = process.env.CACHLY_API_URL ?? 'https://api.cachly.dev';
|
|
62
76
|
let JWT = process.env.CACHLY_JWT ?? '';
|
|
63
77
|
const EMBED_MODEL = process.env.CACHLY_EMBED_MODEL ?? '';
|
|
64
|
-
const CURRENT_VERSION = '0.10.
|
|
78
|
+
const CURRENT_VERSION = '0.10.49';
|
|
65
79
|
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
const reqCtx = new AsyncLocalStorage<ReqCtx>();
|
|
72
|
-
function getJWT(): string { return reqCtx.getStore()?.jwt || JWT; }
|
|
73
|
-
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);
|
|
74
85
|
|
|
75
86
|
// ── Default Instance Resolution (for Smithery & single-credential setups) ────
|
|
76
87
|
// When CACHLY_BRAIN_INSTANCE_ID is set, tools can omit the instance_id parameter.
|
|
@@ -80,18 +91,15 @@ let _defaultInstanceId: string = process.env.CACHLY_BRAIN_INSTANCE_ID ?? '';
|
|
|
80
91
|
let _defaultInstanceLastAttempt = 0;
|
|
81
92
|
|
|
82
93
|
async function resolveDefaultInstanceId(): Promise<string> {
|
|
83
|
-
// In an HTTP request scope the instance ID is fixed by the caller's header.
|
|
84
|
-
const scopedId = reqCtx.getStore()?.instanceId;
|
|
85
|
-
if (scopedId) return scopedId;
|
|
86
94
|
if (_defaultInstanceId) return _defaultInstanceId;
|
|
87
|
-
if (!
|
|
95
|
+
if (!JWT) return '';
|
|
88
96
|
// Cooldown: don't hammer the API on every tool call after a transient failure.
|
|
89
97
|
const now = Date.now();
|
|
90
98
|
if (_defaultInstanceLastAttempt > 0 && now - _defaultInstanceLastAttempt < 30_000) return '';
|
|
91
99
|
_defaultInstanceLastAttempt = now;
|
|
92
100
|
try {
|
|
93
101
|
const res = await fetch(`${API_URL}/api/v1/instances`, {
|
|
94
|
-
headers: { Authorization: `Bearer ${
|
|
102
|
+
headers: { Authorization: `Bearer ${JWT}`, Accept: 'application/json' },
|
|
95
103
|
signal: AbortSignal.timeout(5000),
|
|
96
104
|
});
|
|
97
105
|
if (!res.ok) return '';
|
|
@@ -180,6 +188,36 @@ async function pollDeviceFlow(flow: DeviceFlowState): Promise<'pending' | 'expir
|
|
|
180
188
|
setEmbedJwt(apiKey); // keep embeddings.ts in sync
|
|
181
189
|
_deviceFlow = null;
|
|
182
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
|
+
})();
|
|
183
221
|
// Auto-provision: find or create the user's brain instance.
|
|
184
222
|
_defaultInstanceLastAttempt = 0;
|
|
185
223
|
await resolveDefaultInstanceId();
|
|
@@ -187,7 +225,7 @@ async function pollDeviceFlow(flow: DeviceFlowState): Promise<'pending' | 'expir
|
|
|
187
225
|
try {
|
|
188
226
|
const autoRes = await fetch(`${API_URL}/api/v1/instances/auto`, {
|
|
189
227
|
method: 'POST',
|
|
190
|
-
headers: { Authorization: `Bearer ${
|
|
228
|
+
headers: { Authorization: `Bearer ${JWT}`, 'Content-Type': 'application/json' },
|
|
191
229
|
signal: AbortSignal.timeout(12000),
|
|
192
230
|
});
|
|
193
231
|
if (autoRes.ok) {
|
|
@@ -202,7 +240,7 @@ async function pollDeviceFlow(flow: DeviceFlowState): Promise<'pending' | 'expir
|
|
|
202
240
|
// Log so telemetry can detect free-tier auto-provision failures
|
|
203
241
|
void fetch(`${API_URL}/api/v1/telemetry/mcp`, {
|
|
204
242
|
method: 'POST',
|
|
205
|
-
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${
|
|
243
|
+
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${JWT}` },
|
|
206
244
|
body: JSON.stringify({ event: 'auto_provision_failed', status: autoRes.status, version: CURRENT_VERSION }),
|
|
207
245
|
signal: AbortSignal.timeout(3000),
|
|
208
246
|
}).catch(() => {});
|
|
@@ -214,7 +252,7 @@ async function pollDeviceFlow(flow: DeviceFlowState): Promise<'pending' | 'expir
|
|
|
214
252
|
if (_defaultInstanceId) {
|
|
215
253
|
try {
|
|
216
254
|
const checkRes = await fetch(`${API_URL}/api/v1/instances/${_defaultInstanceId}`, {
|
|
217
|
-
headers: { Authorization: `Bearer ${
|
|
255
|
+
headers: { Authorization: `Bearer ${JWT}`, Accept: 'application/json' },
|
|
218
256
|
signal: AbortSignal.timeout(4000),
|
|
219
257
|
});
|
|
220
258
|
if (checkRes.ok) {
|
|
@@ -225,7 +263,7 @@ async function pollDeviceFlow(flow: DeviceFlowState): Promise<'pending' | 'expir
|
|
|
225
263
|
while (checkInst.status === 'provisioning' && Date.now() < pDeadline) {
|
|
226
264
|
await new Promise(r => setTimeout(r, 3000));
|
|
227
265
|
const rr = await fetch(`${API_URL}/api/v1/instances/${_defaultInstanceId}`, {
|
|
228
|
-
headers: { Authorization: `Bearer ${
|
|
266
|
+
headers: { Authorization: `Bearer ${JWT}`, Accept: 'application/json' },
|
|
229
267
|
signal: AbortSignal.timeout(4000),
|
|
230
268
|
}).catch(() => null);
|
|
231
269
|
if (rr?.ok) {
|
|
@@ -237,6 +275,29 @@ async function pollDeviceFlow(flow: DeviceFlowState): Promise<'pending' | 'expir
|
|
|
237
275
|
}
|
|
238
276
|
} catch { /* non-fatal — getConnection will handle the wait if still provisioning */ }
|
|
239
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
|
+
}
|
|
240
301
|
return 'done';
|
|
241
302
|
}
|
|
242
303
|
if (data.error === 'slow_down') flow.pollInterval = Math.min(flow.pollInterval + 2000, 15000);
|
|
@@ -296,26 +357,14 @@ async function getConnection(instance_id: string): Promise<Redis> {
|
|
|
296
357
|
);
|
|
297
358
|
}
|
|
298
359
|
|
|
299
|
-
if (pool.has(instance_id))
|
|
300
|
-
const cached = pool.get(instance_id)!;
|
|
301
|
-
// Verify the pooled connection is still alive before reusing it.
|
|
302
|
-
// ioredis 'error' eviction is async — a connection can be in the pool
|
|
303
|
-
// but already internally broken (status 'reconnecting' or 'end').
|
|
304
|
-
const status = (cached as any).status as string | undefined;
|
|
305
|
-
if (status === 'ready' || status === 'connect') {
|
|
306
|
-
return cached;
|
|
307
|
-
}
|
|
308
|
-
// Stale — evict and fall through to reconnect.
|
|
309
|
-
pool.delete(instance_id);
|
|
310
|
-
cached.disconnect();
|
|
311
|
-
}
|
|
360
|
+
if (pool.has(instance_id)) return pool.get(instance_id)!;
|
|
312
361
|
|
|
313
|
-
// Fetch instance, waiting up to
|
|
362
|
+
// Fetch instance, waiting up to PROVISION_TIMEOUT_MS if it is still provisioning.
|
|
314
363
|
// This covers the zero-friction path: device-flow auth → auto-provision → first tool call
|
|
315
364
|
// all happen in quick succession and the instance isn't running yet.
|
|
316
365
|
let inst = await apiFetch<Instance>(`/api/v1/instances/${instance_id}`);
|
|
317
366
|
if (inst.status === 'provisioning') {
|
|
318
|
-
const deadline = Date.now() +
|
|
367
|
+
const deadline = Date.now() + PROVISION_TIMEOUT_MS;
|
|
319
368
|
while (inst.status === 'provisioning' && Date.now() < deadline) {
|
|
320
369
|
await new Promise(r => setTimeout(r, 3000));
|
|
321
370
|
inst = await apiFetch<Instance>(`/api/v1/instances/${instance_id}`);
|
|
@@ -326,7 +375,7 @@ async function getConnection(instance_id: string): Promise<Redis> {
|
|
|
326
375
|
// Telemetry: unreachable instance so the team can proactively investigate
|
|
327
376
|
void fetch(`${API_URL}/api/v1/telemetry/mcp`, {
|
|
328
377
|
method: 'POST',
|
|
329
|
-
headers: { 'Content-Type': 'application/json', ...(
|
|
378
|
+
headers: { 'Content-Type': 'application/json', ...(JWT ? { Authorization: `Bearer ${JWT}` } : {}) },
|
|
330
379
|
body: JSON.stringify({ event: 'instance_not_reachable', instance_id, status: inst.status, version: CURRENT_VERSION }),
|
|
331
380
|
signal: AbortSignal.timeout(3000),
|
|
332
381
|
}).catch(() => {});
|
|
@@ -379,6 +428,7 @@ async function getConnection(instance_id: string): Promise<Redis> {
|
|
|
379
428
|
host: inst.host,
|
|
380
429
|
port: inst.port,
|
|
381
430
|
password: password || undefined,
|
|
431
|
+
commandTimeout: 8000,
|
|
382
432
|
...(tlsEnabled ? { tls: {} } : {}),
|
|
383
433
|
lazyConnect: true,
|
|
384
434
|
enableReadyCheck: true,
|
|
@@ -390,45 +440,14 @@ async function getConnection(instance_id: string): Promise<Redis> {
|
|
|
390
440
|
client.on('error', evict);
|
|
391
441
|
client.on('end', evict);
|
|
392
442
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
for (let attempt = 0; attempt < 3; attempt++) {
|
|
397
|
-
try {
|
|
398
|
-
if (attempt > 0) {
|
|
399
|
-
await new Promise(r => setTimeout(r, 2000 * attempt));
|
|
400
|
-
// Fresh client on retry — ioredis doesn't allow reconnect after explicit disconnect.
|
|
401
|
-
client.disconnect();
|
|
402
|
-
const retryClient = new Redis({
|
|
403
|
-
host: inst.host,
|
|
404
|
-
port: inst.port,
|
|
405
|
-
password: password || undefined,
|
|
406
|
-
...(tlsEnabled ? { tls: {} } : {}),
|
|
407
|
-
lazyConnect: true,
|
|
408
|
-
enableReadyCheck: true,
|
|
409
|
-
connectTimeout: 8000,
|
|
410
|
-
retryStrategy: () => null,
|
|
411
|
-
});
|
|
412
|
-
retryClient.on('error', () => pool.delete(instance_id));
|
|
413
|
-
retryClient.on('end', () => pool.delete(instance_id));
|
|
414
|
-
await retryClient.connect();
|
|
415
|
-
pool.set(instance_id, retryClient);
|
|
416
|
-
return retryClient;
|
|
417
|
-
}
|
|
418
|
-
await client.connect();
|
|
419
|
-
connectErr = undefined;
|
|
420
|
-
break;
|
|
421
|
-
} catch (err) {
|
|
422
|
-
connectErr = err;
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
if (connectErr) {
|
|
443
|
+
try {
|
|
444
|
+
await client.connect();
|
|
445
|
+
} catch (err: unknown) {
|
|
427
446
|
client.disconnect();
|
|
428
|
-
const msg =
|
|
447
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
429
448
|
void fetch(`${API_URL}/api/v1/telemetry/mcp`, {
|
|
430
449
|
method: 'POST',
|
|
431
|
-
headers: { 'Content-Type': 'application/json', ...(
|
|
450
|
+
headers: { 'Content-Type': 'application/json', ...(JWT ? { Authorization: `Bearer ${JWT}` } : {}) },
|
|
432
451
|
body: JSON.stringify({ event: 'redis_connect_failed', instance_id, host: inst.host, port: inst.port, error: msg, version: CURRENT_VERSION }),
|
|
433
452
|
signal: AbortSignal.timeout(3000),
|
|
434
453
|
}).catch(() => {});
|
|
@@ -448,14 +467,14 @@ async function getConnection(instance_id: string): Promise<Redis> {
|
|
|
448
467
|
// ── API helper ────────────────────────────────────────────────────────────────
|
|
449
468
|
|
|
450
469
|
async function apiFetch<T>(path: string, options: RequestInit = {}): Promise<T> {
|
|
451
|
-
checkJwt(
|
|
470
|
+
checkJwt(JWT);
|
|
452
471
|
const timeoutMs = Number(process.env.CACHLY_API_TIMEOUT_MS ?? 15_000);
|
|
453
472
|
const res = await fetch(`${API_URL}${path}`, {
|
|
454
473
|
...options,
|
|
455
474
|
signal: AbortSignal.timeout(timeoutMs),
|
|
456
475
|
headers: {
|
|
457
476
|
'Content-Type': 'application/json',
|
|
458
|
-
Authorization: `Bearer ${
|
|
477
|
+
Authorization: `Bearer ${JWT}`,
|
|
459
478
|
...options.headers,
|
|
460
479
|
},
|
|
461
480
|
});
|
|
@@ -481,7 +500,7 @@ import { handleTeamTool } from './handlers/team.js';
|
|
|
481
500
|
import { handleRoadmapTool } from './handlers/roadmap.js';
|
|
482
501
|
import { handleAdvancedTool } from './handlers/advanced.js';
|
|
483
502
|
import { handleSyndicateTool } from './handlers/syndicate.js';
|
|
484
|
-
import { handleFedbrainTool } from './handlers/fedbrain.js';
|
|
503
|
+
import { handleFedbrainTool, _lastBrainFromGitCounts } from './handlers/fedbrain.js';
|
|
485
504
|
import type { Instance } from './handlers/brain.js';
|
|
486
505
|
|
|
487
506
|
// ── Tools (imported from tools.ts) ─────────────────────────────────────────
|
|
@@ -510,7 +529,7 @@ function sendFunnelEvent(event: string, extra?: Record<string, unknown>): void {
|
|
|
510
529
|
headers: { 'Content-Type': 'application/json' },
|
|
511
530
|
body: JSON.stringify({
|
|
512
531
|
event, version: CURRENT_VERSION, editor: detectEditor(),
|
|
513
|
-
...(
|
|
532
|
+
...(JWT ? { jwt: JWT } : {}),
|
|
514
533
|
...extra,
|
|
515
534
|
}),
|
|
516
535
|
signal: AbortSignal.timeout(3000),
|
|
@@ -533,7 +552,7 @@ async function sendAnonymousTelemetry(toolName: string): Promise<void> {
|
|
|
533
552
|
|
|
534
553
|
async function handleTool(name: string, args: Record<string, unknown>): Promise<string> {
|
|
535
554
|
// Guard: if no JWT, return actionable onboarding message instead of HTTP 401
|
|
536
|
-
if (!
|
|
555
|
+
if (!JWT) {
|
|
537
556
|
void sendAnonymousTelemetry(name);
|
|
538
557
|
|
|
539
558
|
// ── Zero-credential device flow ─────────────────────────────────────
|
|
@@ -613,14 +632,14 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
|
|
|
613
632
|
// Delegate brain tools (learn, recall, session, etc.)
|
|
614
633
|
const brainResult = await handleBrainTool(name, args, getConnection, apiFetch);
|
|
615
634
|
if (brainResult !== null) {
|
|
616
|
-
if (!_firstCallSuccessSent &&
|
|
635
|
+
if (!_firstCallSuccessSent && JWT) {
|
|
617
636
|
_firstCallSuccessSent = true;
|
|
618
|
-
sendFunnelEvent('first_call_success', { tool: name, instance_id: args.instance_id ??
|
|
637
|
+
sendFunnelEvent('first_call_success', { tool: name, instance_id: args.instance_id ?? _defaultInstanceId ?? '' });
|
|
619
638
|
}
|
|
620
639
|
// Per-tool telemetry — fires after every successful brain tool call.
|
|
621
640
|
// api_key carries the cky_live_... token so the backend can resolve tenant + increment counters.
|
|
622
|
-
const instanceId = (args.instance_id as string | undefined) ??
|
|
623
|
-
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 };
|
|
624
643
|
if (name === 'recall_best_solution') {
|
|
625
644
|
// recall_best_solution → increments BrainRecallCount + triggers level-up logic
|
|
626
645
|
sendFunnelEvent('recall_best_solution', telemetryExtra);
|
|
@@ -646,17 +665,14 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
|
|
|
646
665
|
limit: 50,
|
|
647
666
|
}, getConnection, apiFetch);
|
|
648
667
|
if (gitBootstrap !== null) {
|
|
649
|
-
|
|
668
|
+
const gitCounts = _lastBrainFromGitCounts;
|
|
669
|
+
sendFunnelEvent('brain_from_git', { ...telemetryExtra, ...(gitCounts ?? {}) });
|
|
650
670
|
sendFunnelEvent('recall_best_solution', telemetryExtra);
|
|
651
671
|
// Append bootstrap summary to the session_start briefing.
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
(brainResult as { content: { type: string; text: string }[] }).content.push({
|
|
657
|
-
type: 'text',
|
|
658
|
-
text: '\n---\n' + bootstrapText,
|
|
659
|
-
});
|
|
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;
|
|
660
676
|
}
|
|
661
677
|
}
|
|
662
678
|
} catch { /* git bootstrap errors must never break session_start */ }
|
|
@@ -671,7 +687,7 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
|
|
|
671
687
|
const srText = typeof brainResult === 'object' && brainResult !== null && 'content' in brainResult
|
|
672
688
|
? JSON.stringify(brainResult)
|
|
673
689
|
: String(brainResult ?? '');
|
|
674
|
-
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')) {
|
|
675
691
|
sendFunnelEvent('recall_best_solution', telemetryExtra);
|
|
676
692
|
}
|
|
677
693
|
}
|
|
@@ -681,9 +697,9 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
|
|
|
681
697
|
// Delegate context tools (remember/recall/list/forget)
|
|
682
698
|
const contextResult = await handleContextTool(name, args, getConnection, apiFetch);
|
|
683
699
|
if (contextResult !== null) {
|
|
684
|
-
if (!_firstCallSuccessSent &&
|
|
700
|
+
if (!_firstCallSuccessSent && JWT) {
|
|
685
701
|
_firstCallSuccessSent = true;
|
|
686
|
-
sendFunnelEvent('first_call_success', { tool: name, instance_id: args.instance_id ??
|
|
702
|
+
sendFunnelEvent('first_call_success', { tool: name, instance_id: args.instance_id ?? _defaultInstanceId ?? '' });
|
|
687
703
|
}
|
|
688
704
|
return contextResult;
|
|
689
705
|
}
|
|
@@ -709,8 +725,8 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
|
|
|
709
725
|
if (syndicateResult !== null) {
|
|
710
726
|
// brain_predict is in syndicate handler — track telemetry here.
|
|
711
727
|
if (name === 'brain_predict') {
|
|
712
|
-
const instanceId = (args.instance_id as string | undefined) ??
|
|
713
|
-
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 };
|
|
714
730
|
sendFunnelEvent('brain_predict', telemetryExtra);
|
|
715
731
|
}
|
|
716
732
|
return syndicateResult;
|
|
@@ -719,16 +735,21 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
|
|
|
719
735
|
const fedbrainResult = await handleFedbrainTool(name, args, getConnection, apiFetch);
|
|
720
736
|
if (fedbrainResult !== null) {
|
|
721
737
|
// brain_from_git and brain_predict_failures are in fedbrain handler.
|
|
722
|
-
const instanceId = (args.instance_id as string | undefined) ??
|
|
723
|
-
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 };
|
|
724
740
|
if (name === 'brain_from_git') {
|
|
725
|
-
|
|
741
|
+
const gitCounts = _lastBrainFromGitCounts;
|
|
742
|
+
sendFunnelEvent('brain_from_git', { ...telemetryExtra, ...(gitCounts ?? {}) });
|
|
726
743
|
} else if (name === 'brain_predict_failures') {
|
|
727
744
|
sendFunnelEvent('brain_predict_failures', telemetryExtra);
|
|
728
745
|
}
|
|
729
746
|
return fedbrainResult;
|
|
730
747
|
}
|
|
731
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
|
+
|
|
732
753
|
switch (name) {
|
|
733
754
|
// ── Instance management ──────────────────────────────────────────────
|
|
734
755
|
case 'get_api_status': {
|
|
@@ -747,7 +768,7 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
|
|
|
747
768
|
}
|
|
748
769
|
|
|
749
770
|
// Check JWT / auth
|
|
750
|
-
if (!
|
|
771
|
+
if (!JWT) {
|
|
751
772
|
return [
|
|
752
773
|
`📡 **cachly API Status**`,
|
|
753
774
|
``,
|
|
@@ -762,7 +783,7 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
|
|
|
762
783
|
// Decode JWT claims (inspection only, no verification)
|
|
763
784
|
let authInfo = '❌ invalid JWT format';
|
|
764
785
|
try {
|
|
765
|
-
const parts =
|
|
786
|
+
const parts = JWT.split('.');
|
|
766
787
|
if (parts.length === 3) {
|
|
767
788
|
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf-8')) as {
|
|
768
789
|
sub?: string; exp?: number; iss?: string;
|
|
@@ -795,7 +816,7 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
|
|
|
795
816
|
const lines: string[] = ['\n\n🧠 **Brain Instances:**'];
|
|
796
817
|
for (const inst of instances) {
|
|
797
818
|
const badge = inst.status === 'running' ? '🟢' : inst.status === 'provisioning' ? '🟡' : '🔴';
|
|
798
|
-
const defaultMark = inst.id ===
|
|
819
|
+
const defaultMark = inst.id === _defaultInstanceId ? ' ← active' : '';
|
|
799
820
|
lines.push(` ${badge} **${inst.name}** (\`${inst.id}\`) · ${inst.status}${defaultMark}`);
|
|
800
821
|
if (inst.status === 'running' && inst.host) {
|
|
801
822
|
// Try a quick ping on the cached connection
|
|
@@ -936,7 +957,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
936
957
|
return { content: [{ type: 'text', text }] };
|
|
937
958
|
} catch (err) {
|
|
938
959
|
if (err instanceof McpError) throw err;
|
|
939
|
-
|
|
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);
|
|
940
963
|
}
|
|
941
964
|
});
|
|
942
965
|
|
|
@@ -2212,6 +2235,7 @@ if (process.argv[2] === 'setup') {
|
|
|
2212
2235
|
const CLIENT_ID = 'cachly-cli';
|
|
2213
2236
|
|
|
2214
2237
|
console.log('Step 1: Sign in to cachly (free, no credit card)\n');
|
|
2238
|
+
sendFunnelEvent('setup_auth_started');
|
|
2215
2239
|
|
|
2216
2240
|
// Start device flow
|
|
2217
2241
|
let deviceCode = '', userCode = '', verifyUri = '', pollInterval = 5000;
|
|
@@ -2235,6 +2259,7 @@ if (process.argv[2] === 'setup') {
|
|
|
2235
2259
|
console.error('Falling back: sign in at https://cachly.dev/setup-ai and paste your API token.\n');
|
|
2236
2260
|
token = await ask(' Paste API token (cky_live_...): ');
|
|
2237
2261
|
if (!token) { console.error('\nToken is required. Aborting.\n'); rl.close(); process.exit(1); }
|
|
2262
|
+
sendFunnelEvent('setup_auth_completed');
|
|
2238
2263
|
console.log('');
|
|
2239
2264
|
deviceCode = ''; // mark as fallback so we skip polling
|
|
2240
2265
|
}
|
|
@@ -2268,6 +2293,7 @@ if (process.argv[2] === 'setup') {
|
|
|
2268
2293
|
if (tokenData.access_token) {
|
|
2269
2294
|
token = tokenData.access_token;
|
|
2270
2295
|
console.log(' \x1b[32m✓ Authorized!\x1b[0m\n');
|
|
2296
|
+
sendFunnelEvent('setup_auth_completed');
|
|
2271
2297
|
break;
|
|
2272
2298
|
}
|
|
2273
2299
|
// authorization_pending = keep polling; slow_down = increase interval
|
|
@@ -2348,9 +2374,10 @@ if (process.argv[2] === 'setup') {
|
|
|
2348
2374
|
// Newly created — poll until running or give up after 30 s.
|
|
2349
2375
|
const newId = autoBody.instance_id;
|
|
2350
2376
|
console.log(` ✓ created (${newId.slice(0, 8)}…)\n`);
|
|
2377
|
+
// 45 attempts × 3s = 135s — generous for free-tier in slow regions.
|
|
2351
2378
|
process.stdout.write('⏳ Waiting for instance to start');
|
|
2352
|
-
for (let attempt = 0; attempt <
|
|
2353
|
-
await new Promise(r => setTimeout(r,
|
|
2379
|
+
for (let attempt = 0; attempt < 45; attempt++) {
|
|
2380
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
2354
2381
|
process.stdout.write('.');
|
|
2355
2382
|
try {
|
|
2356
2383
|
const checkRes = await fetch(`${API_URL}/api/v1/instances/${newId}`, { headers: { Authorization: `Bearer ${token}` } });
|
|
@@ -2384,6 +2411,7 @@ if (process.argv[2] === 'setup') {
|
|
|
2384
2411
|
console.log(` (Run with --instance-id <id> to use a different one)\n`);
|
|
2385
2412
|
}
|
|
2386
2413
|
console.log(`✓ Instance: ${instance.name} (${instance.id.slice(0, 8)}…)\n`);
|
|
2414
|
+
sendFunnelEvent('setup_instance_ready', { instance_id: instance.id });
|
|
2387
2415
|
|
|
2388
2416
|
// ── Step 3: Detect editors ────────────────────────────────────────────────
|
|
2389
2417
|
const cwd = process.cwd();
|
|
@@ -2629,7 +2657,7 @@ if (process.argv[2] === 'index') {
|
|
|
2629
2657
|
const maxFiles = parseInt(flag('max-files') ?? '500', 10);
|
|
2630
2658
|
const namespace = flag('namespace') ?? 'cachly:sem:code';
|
|
2631
2659
|
|
|
2632
|
-
if (!instanceId || !
|
|
2660
|
+
if (!instanceId || !JWT) {
|
|
2633
2661
|
console.error('\n❌ CACHLY_BRAIN_INSTANCE_ID and CACHLY_JWT must be set\n');
|
|
2634
2662
|
console.error(' export CACHLY_BRAIN_INSTANCE_ID=<uuid>');
|
|
2635
2663
|
console.error(' export CACHLY_JWT=<cky_live_...>');
|
|
@@ -2671,7 +2699,7 @@ if (process.argv[2] === 'learn-git') {
|
|
|
2671
2699
|
const instanceId = flag('instance-id') ?? process.env.CACHLY_BRAIN_INSTANCE_ID;
|
|
2672
2700
|
const maxCommits = parseInt(flag('max-commits') ?? '50', 10);
|
|
2673
2701
|
|
|
2674
|
-
if (!instanceId || !
|
|
2702
|
+
if (!instanceId || !JWT) {
|
|
2675
2703
|
console.error('\n❌ CACHLY_BRAIN_INSTANCE_ID and CACHLY_JWT must be set\n');
|
|
2676
2704
|
console.error(' export CACHLY_BRAIN_INSTANCE_ID=<uuid>');
|
|
2677
2705
|
console.error(' export CACHLY_JWT=<cky_live_...>');
|
|
@@ -2706,8 +2734,7 @@ if (process.argv[2] === 'learn-git') {
|
|
|
2706
2734
|
// actionable message in their editor's MCP log instead of silent failures.
|
|
2707
2735
|
// Skip for CLI commands that intentionally run without credentials.
|
|
2708
2736
|
const _cliNoAuthCommands = ['demo', 'share', 'health', 'setup', 'init', 'digest', 'invite', 'badge', 'join', 'upgrade'];
|
|
2709
|
-
|
|
2710
|
-
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)) {
|
|
2711
2738
|
// No args + no JWT in a non-TTY context = running as MCP server without credentials.
|
|
2712
2739
|
// Print actionable setup banner to stdout so it's captured by callers/tests.
|
|
2713
2740
|
const banner =
|
|
@@ -2780,76 +2807,12 @@ if (!process.env.CACHLY_NO_UPDATE_CHECK) {
|
|
|
2780
2807
|
|
|
2781
2808
|
const httpPort = process.env.PORT ? parseInt(process.env.PORT, 10) : undefined;
|
|
2782
2809
|
|
|
2783
|
-
// ── Per-request MCP Server factory (HTTP / Smithery stateless mode) ──────────
|
|
2784
|
-
// SDK ≥1.9 forbids reusing a stateless transport across requests.
|
|
2785
|
-
// Solution: create a fresh Server+Transport per /mcp request.
|
|
2786
|
-
// The module-level `server` below is used for stdio mode only.
|
|
2787
|
-
function createHttpMcpServer(): Server {
|
|
2788
|
-
const srv = new Server(
|
|
2789
|
-
{ name: 'cachly-mcp', version: CURRENT_VERSION },
|
|
2790
|
-
{ capabilities: { tools: {} } }
|
|
2791
|
-
);
|
|
2792
|
-
srv.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
2793
|
-
srv.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
2794
|
-
const { name, arguments: args } = request.params;
|
|
2795
|
-
try {
|
|
2796
|
-
const text = await handleTool(name, (args ?? {}) as Record<string, unknown>);
|
|
2797
|
-
return { content: [{ type: 'text', text }] };
|
|
2798
|
-
} catch (err) {
|
|
2799
|
-
if (err instanceof McpError) throw err;
|
|
2800
|
-
throw new McpError(ErrorCode.InternalError, (err as Error).message);
|
|
2801
|
-
}
|
|
2802
|
-
});
|
|
2803
|
-
return srv;
|
|
2804
|
-
}
|
|
2805
|
-
|
|
2806
2810
|
if (httpPort) {
|
|
2807
2811
|
// ── HTTP mode (Streamable HTTP transport) ───────────────────────────────
|
|
2808
2812
|
// Used for Smithery URL deployment: PORT=3000 node dist/index.js
|
|
2809
|
-
// Multi-tenant: per-request JWT + instance ID resolved via:
|
|
2810
|
-
// 1. Authorization: Bearer <jwt> + X-Cachly-Instance-Id: <uuid>
|
|
2811
|
-
// 2. Smithery config: ?config=base64url({CACHLY_JWT, CACHLY_BRAIN_INSTANCE_ID, ...})
|
|
2812
|
-
// Falls back to module-level env-derived values when neither is provided.
|
|
2813
2813
|
const { createServer } = await import('node:http');
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
let jwtVal = '';
|
|
2817
|
-
let instId = '';
|
|
2818
|
-
const auth = req.headers['authorization'];
|
|
2819
|
-
if (typeof auth === 'string' && auth.trim()) {
|
|
2820
|
-
const a = auth.trim();
|
|
2821
|
-
jwtVal = a.toLowerCase().startsWith('bearer ') ? a.slice(7).trim() : a;
|
|
2822
|
-
}
|
|
2823
|
-
// Fallback: raw JWT in custom header (Smithery "header" transport without Bearer prefix)
|
|
2824
|
-
if (!jwtVal) {
|
|
2825
|
-
const rawJwt = req.headers['cachly_jwt'] ?? req.headers['cachly-jwt'] ?? req.headers['x-cachly-jwt'] ?? req.headers['x-api-key'];
|
|
2826
|
-
if (typeof rawJwt === 'string') jwtVal = rawJwt.trim();
|
|
2827
|
-
}
|
|
2828
|
-
const hdrInst = req.headers['x-cachly-instance-id'];
|
|
2829
|
-
if (typeof hdrInst === 'string') instId = hdrInst.trim();
|
|
2830
|
-
// Smithery passes user config as a base64url-encoded JSON blob in ?config=
|
|
2831
|
-
const cfgParam = url.searchParams.get('config');
|
|
2832
|
-
if (cfgParam && (!jwtVal || !instId)) {
|
|
2833
|
-
try {
|
|
2834
|
-
const b64 = cfgParam.replace(/-/g, '+').replace(/_/g, '/');
|
|
2835
|
-
const padded = b64 + '='.repeat((4 - b64.length % 4) % 4);
|
|
2836
|
-
const json = JSON.parse(Buffer.from(padded, 'base64').toString('utf8'));
|
|
2837
|
-
if (!jwtVal && typeof json.CACHLY_JWT === 'string') jwtVal = json.CACHLY_JWT;
|
|
2838
|
-
if (!instId && typeof json.CACHLY_BRAIN_INSTANCE_ID === 'string') instId = json.CACHLY_BRAIN_INSTANCE_ID;
|
|
2839
|
-
} catch { /* malformed config — ignore, fall back to globals */ }
|
|
2840
|
-
}
|
|
2841
|
-
// Direct query params (Smithery "query" transport, manual clients, etc.)
|
|
2842
|
-
if (!jwtVal) {
|
|
2843
|
-
const q = url.searchParams.get('CACHLY_JWT') ?? url.searchParams.get('cachly_jwt') ?? url.searchParams.get('api_key');
|
|
2844
|
-
if (q) jwtVal = q.trim();
|
|
2845
|
-
}
|
|
2846
|
-
if (!instId) {
|
|
2847
|
-
const q = url.searchParams.get('CACHLY_BRAIN_INSTANCE_ID') ?? url.searchParams.get('cachly_brain_instance_id') ?? url.searchParams.get('instance_id');
|
|
2848
|
-
if (q) instId = q.trim();
|
|
2849
|
-
}
|
|
2850
|
-
return { jwt: jwtVal, instanceId: instId };
|
|
2851
|
-
}
|
|
2852
|
-
|
|
2814
|
+
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
|
|
2815
|
+
await server.connect(transport);
|
|
2853
2816
|
const httpServer = createServer((req, res) => {
|
|
2854
2817
|
const url = new URL(req.url ?? '/', `http://localhost:${httpPort}`);
|
|
2855
2818
|
if (url.pathname === '/health') {
|
|
@@ -2857,8 +2820,6 @@ if (httpPort) {
|
|
|
2857
2820
|
res.end(JSON.stringify({
|
|
2858
2821
|
status: 'ok',
|
|
2859
2822
|
version: CURRENT_VERSION,
|
|
2860
|
-
transport: 'streamable-http',
|
|
2861
|
-
multiTenant: true,
|
|
2862
2823
|
zeroResults: {
|
|
2863
2824
|
total: zeroResultsTotal,
|
|
2864
2825
|
last10: ZERO_RESULTS_LOG.slice(-10).map(e => ({ query: e.query, ts: new Date(e.ts).toISOString() })),
|
|
@@ -2866,52 +2827,8 @@ if (httpPort) {
|
|
|
2866
2827
|
}));
|
|
2867
2828
|
return;
|
|
2868
2829
|
}
|
|
2869
|
-
// Static server card — lets Smithery skip live scanning (avoids auth complexity).
|
|
2870
|
-
if (url.pathname === '/.well-known/mcp/server-card.json') {
|
|
2871
|
-
const card = {
|
|
2872
|
-
name: 'cachly AI Brain',
|
|
2873
|
-
version: CURRENT_VERSION,
|
|
2874
|
-
description: 'Persistent AI memory for Claude Code, Cursor, Copilot, Windsurf & friends. Remembers every lesson, fix, and architecture decision forever.',
|
|
2875
|
-
transport: { type: 'streamable-http', url: 'https://mcp.cachly.dev/mcp' },
|
|
2876
|
-
capabilities: { tools: {} },
|
|
2877
|
-
};
|
|
2878
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
2879
|
-
res.end(JSON.stringify(card));
|
|
2880
|
-
return;
|
|
2881
|
-
}
|
|
2882
2830
|
if (url.pathname === '/mcp' || url.pathname === '/') {
|
|
2883
|
-
|
|
2884
|
-
if (req.method === 'OPTIONS') {
|
|
2885
|
-
res.writeHead(204, {
|
|
2886
|
-
'Access-Control-Allow-Origin': '*',
|
|
2887
|
-
'Access-Control-Allow-Methods': 'GET, POST, DELETE, OPTIONS',
|
|
2888
|
-
'Access-Control-Allow-Headers': 'Authorization, Content-Type, Mcp-Session-Id, X-Cachly-Instance-Id, cachly_jwt, cachly-jwt, x-cachly-jwt, x-api-key',
|
|
2889
|
-
});
|
|
2890
|
-
res.end();
|
|
2891
|
-
return;
|
|
2892
|
-
}
|
|
2893
|
-
const ctx = extractReqCtx(req, url);
|
|
2894
|
-
// Create a fresh Server+Transport for each request (stateless multi-tenant mode).
|
|
2895
|
-
// SDK ≥1.9 throws if a stateless transport is reused across requests.
|
|
2896
|
-
const reqServer = createHttpMcpServer();
|
|
2897
|
-
const reqTransport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
|
|
2898
|
-
reqServer.connect(reqTransport).then(() => {
|
|
2899
|
-
reqCtx.run(ctx, () => {
|
|
2900
|
-
reqTransport.handleRequest(req, res).catch((err) => {
|
|
2901
|
-
process.stderr.write(`cachly-mcp HTTP handler error: ${(err as Error).message}\n`);
|
|
2902
|
-
if (!res.headersSent) {
|
|
2903
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
2904
|
-
res.end(JSON.stringify({ error: 'internal_error' }));
|
|
2905
|
-
}
|
|
2906
|
-
});
|
|
2907
|
-
});
|
|
2908
|
-
}).catch((err) => {
|
|
2909
|
-
process.stderr.write(`cachly-mcp connect error: ${(err as Error).message}\n`);
|
|
2910
|
-
if (!res.headersSent) {
|
|
2911
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
2912
|
-
res.end(JSON.stringify({ error: 'internal_error' }));
|
|
2913
|
-
}
|
|
2914
|
-
});
|
|
2831
|
+
transport.handleRequest(req, res);
|
|
2915
2832
|
return;
|
|
2916
2833
|
}
|
|
2917
2834
|
res.writeHead(404);
|
|
@@ -2926,3 +2843,5 @@ if (httpPort) {
|
|
|
2926
2843
|
await server.connect(transport);
|
|
2927
2844
|
}
|
|
2928
2845
|
|
|
2846
|
+
notify('cachly', 'startup', { version: CURRENT_VERSION, mode: process.env.MCP_HTTP_PORT ? 'http' : 'stdio' }).catch(() => undefined);
|
|
2847
|
+
|