@cachly-dev/mcp-server 0.10.64 ā 0.10.66
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/dist/src/handlers/brain.js +1 -1
- package/dist/src/index.js +1 -1
- package/package.json +7 -1
- package/server.json +2 -2
- package/.env.test.example +0 -23
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -50
- package/.github/ISSUE_TEMPLATE/config.yml +0 -8
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -25
- package/.github/workflows/eas-build-android.yml +0 -58
- package/.github/workflows/eas-update.yml +0 -55
- package/.github/workflows/publish.yml +0 -72
- package/BENCH.md +0 -91
- package/CHANGELOG.md +0 -604
- package/CHANGELOG.md.tmp +0 -0
- package/Dockerfile +0 -16
- package/LAUNCH_TEXTS.md +0 -182
- package/PROGRESS.md +0 -137
- package/STRATEGY.md +0 -228
- package/VISION_10X.md +0 -133
- 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 -13221
- 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/src/__tests__/auth.test.d.ts +0 -1
- package/dist/src/__tests__/auth.test.js +0 -107
- package/dist/src/__tests__/auth.test.js.map +0 -1
- package/dist/src/__tests__/brain-flow.test.d.ts +0 -12
- package/dist/src/__tests__/brain-flow.test.js +0 -1241
- package/dist/src/__tests__/brain-flow.test.js.map +0 -1
- package/dist/src/__tests__/confidence.test.d.ts +0 -13
- package/dist/src/__tests__/confidence.test.js +0 -233
- package/dist/src/__tests__/confidence.test.js.map +0 -1
- package/dist/src/__tests__/e2e/api-auth.test.d.ts +0 -7
- package/dist/src/__tests__/e2e/api-auth.test.js +0 -162
- package/dist/src/__tests__/e2e/api-auth.test.js.map +0 -1
- package/dist/src/__tests__/e2e/api-health.test.d.ts +0 -7
- package/dist/src/__tests__/e2e/api-health.test.js +0 -95
- package/dist/src/__tests__/e2e/api-health.test.js.map +0 -1
- package/dist/src/__tests__/e2e/cli.test.d.ts +0 -10
- package/dist/src/__tests__/e2e/cli.test.js +0 -204
- package/dist/src/__tests__/e2e/cli.test.js.map +0 -1
- package/dist/src/__tests__/e2e/config.d.ts +0 -35
- package/dist/src/__tests__/e2e/config.js +0 -53
- package/dist/src/__tests__/e2e/config.js.map +0 -1
- package/dist/src/__tests__/e2e/funnel.test.d.ts +0 -13
- package/dist/src/__tests__/e2e/funnel.test.js +0 -90
- package/dist/src/__tests__/e2e/funnel.test.js.map +0 -1
- package/dist/src/__tests__/e2e/mcp-tools.test.d.ts +0 -7
- package/dist/src/__tests__/e2e/mcp-tools.test.js +0 -129
- package/dist/src/__tests__/e2e/mcp-tools.test.js.map +0 -1
- package/dist/src/__tests__/e2e/provisioning.test.d.ts +0 -15
- package/dist/src/__tests__/e2e/provisioning.test.js +0 -123
- package/dist/src/__tests__/e2e/provisioning.test.js.map +0 -1
- package/dist/src/__tests__/handlers-unit.test.d.ts +0 -12
- package/dist/src/__tests__/handlers-unit.test.js +0 -505
- package/dist/src/__tests__/handlers-unit.test.js.map +0 -1
- package/dist/src/__tests__/namespace.test.d.ts +0 -1
- package/dist/src/__tests__/namespace.test.js +0 -114
- package/dist/src/__tests__/namespace.test.js.map +0 -1
- package/dist/src/__tests__/rerank.test.d.ts +0 -1
- package/dist/src/__tests__/rerank.test.js +0 -115
- package/dist/src/__tests__/rerank.test.js.map +0 -1
- package/dist/src/__tests__/utils.test.d.ts +0 -1
- package/dist/src/__tests__/utils.test.js +0 -83
- package/dist/src/__tests__/utils.test.js.map +0 -1
- package/dist/src/bench/cachly-bench.d.ts +0 -35
- package/dist/src/bench/cachly-bench.js +0 -182
- package/dist/src/bench/cachly-bench.js.map +0 -1
- package/dist/src/bench/fixtures.d.ts +0 -37
- package/dist/src/bench/fixtures.js +0 -225
- package/dist/src/bench/fixtures.js.map +0 -1
- package/dist/src/search.test.d.ts +0 -1
- package/dist/src/search.test.js +0 -713
- package/dist/src/search.test.js.map +0 -1
- package/eslint.config.js +0 -16
- package/glama.json +0 -36
- package/llms.txt +0 -155
- 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/e2e-run.sh +0 -160
- package/scripts/generate-icons.js +0 -28
- package/smithery.yaml +0 -45
- package/src/__tests__/auth.test.ts +0 -147
- package/src/__tests__/brain-flow.test.ts +0 -1472
- package/src/__tests__/confidence.test.ts +0 -285
- package/src/__tests__/e2e/api-auth.test.ts +0 -182
- package/src/__tests__/e2e/api-health.test.ts +0 -103
- package/src/__tests__/e2e/cli.test.ts +0 -228
- package/src/__tests__/e2e/config.ts +0 -53
- package/src/__tests__/e2e/funnel.test.ts +0 -99
- package/src/__tests__/e2e/mcp-tools.test.ts +0 -191
- package/src/__tests__/e2e/provisioning.test.ts +0 -128
- package/src/__tests__/handlers-unit.test.ts +0 -677
- package/src/__tests__/namespace.test.ts +0 -126
- package/src/__tests__/rerank.test.ts +0 -130
- package/src/__tests__/utils.test.ts +0 -81
- package/src/auth.ts +0 -46
- package/src/bench/cachly-bench.ts +0 -217
- package/src/bench/fixtures.ts +0 -256
- package/src/ckg.ts +0 -103
- package/src/confidence.ts +0 -38
- package/src/embeddings.ts +0 -217
- package/src/handlers/advanced.ts +0 -647
- package/src/handlers/brain.ts +0 -2622
- package/src/handlers/cache.ts +0 -633
- package/src/handlers/context.ts +0 -222
- package/src/handlers/fedbrain.ts +0 -1232
- package/src/handlers/instances.ts +0 -180
- package/src/handlers/roadmap.ts +0 -201
- package/src/handlers/syndicate.ts +0 -460
- package/src/handlers/tco.ts +0 -345
- package/src/handlers/team.ts +0 -1249
- package/src/index.ts +0 -2815
- package/src/namespace.ts +0 -128
- package/src/notifier.ts +0 -5
- package/src/rerank.ts +0 -148
- package/src/search.test.ts +0 -848
- package/src/search.ts +0 -1781
- package/src/tools.ts +0 -2056
- package/src/utils.ts +0 -90
- package/test-features.cjs +0 -23
- package/tsconfig.json +0 -16
- package/vitest.config.ts +0 -14
package/src/index.ts
DELETED
|
@@ -1,2815 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { jwtExpiryMs, checkJwt, handleApiError } from './auth.js';
|
|
3
|
-
import { handleTcoTool } from './handlers/tco.js';
|
|
4
|
-
import { notify } from './notifier.js';
|
|
5
|
-
import { readFile } from 'node:fs/promises';
|
|
6
|
-
import { fileURLToPath } from 'node:url';
|
|
7
|
-
|
|
8
|
-
// True only when this file is the entry point (not imported by tests or other modules).
|
|
9
|
-
const _isMain = process.argv[1] != null &&
|
|
10
|
-
(process.argv[1] === fileURLToPath(import.meta.url) ||
|
|
11
|
-
process.argv[1].endsWith('/dist/index.js') ||
|
|
12
|
-
process.argv[1].endsWith('/src/index.ts'));
|
|
13
|
-
/**
|
|
14
|
-
* cachly MCP Server v0.4.0
|
|
15
|
-
*
|
|
16
|
-
* Exposes cachly.dev as MCP tools so any AI assistant
|
|
17
|
-
* (GitHub Copilot, Claude, Cursor, Windsurf, Continue.dev ā¦) can:
|
|
18
|
-
*
|
|
19
|
-
* āā Instance Management āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
20
|
-
* ⢠list_instances ā list all your cache instances
|
|
21
|
-
* ⢠create_instance ā provision a new instance (free or paid)
|
|
22
|
-
* ⢠get_instance ā get details + connection string
|
|
23
|
-
* ⢠get_connection_string ā get the redis:// URL
|
|
24
|
-
* ⢠delete_instance ā permanently delete an instance
|
|
25
|
-
*
|
|
26
|
-
* āā Live Cache Operations āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
27
|
-
* ⢠cache_get ā get a value by key
|
|
28
|
-
* ⢠cache_set ā set a key-value pair with optional TTL
|
|
29
|
-
* ⢠cache_delete ā delete one or more keys
|
|
30
|
-
* ⢠cache_exists ā check if keys exist
|
|
31
|
-
* ⢠cache_ttl ā inspect TTL of a key
|
|
32
|
-
* ⢠cache_keys ā list keys matching a glob pattern
|
|
33
|
-
* ⢠cache_stats ā memory, hit rate, ops/sec, keyspace info
|
|
34
|
-
* ⢠semantic_search ā find semantically similar cached entries
|
|
35
|
-
* (needs OPENAI_API_KEY or other embed provider in .env)
|
|
36
|
-
*
|
|
37
|
-
* āā Auth & Status āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
38
|
-
* ⢠get_api_status ā check API health + JWT auth info (Keycloak)
|
|
39
|
-
*
|
|
40
|
-
* āā Travel Chaos Organizer (TCO) āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
41
|
-
* ⢠tco_list_trips ā list all trips
|
|
42
|
-
* ⢠tco_create_trip ā create a new trip
|
|
43
|
-
* ⢠tco_get_timeline ā get timeline items for a trip
|
|
44
|
-
* ⢠tco_delete_trip ā delete a trip
|
|
45
|
-
* ⢠tco_inbox_list ā list Chaos Inbox items
|
|
46
|
-
* ⢠tco_inbox_assign ā assign inbox item to a trip
|
|
47
|
-
* ⢠tco_inbox_reject ā reject/discard an inbox item
|
|
48
|
-
* ⢠tco_parse_url ā fetch a URL and parse with Ollama
|
|
49
|
-
* ⢠tco_import_email ā import raw email text
|
|
50
|
-
* (Requires TCO_API_URL env var; auth forwarded from CACHLY_JWT ā same Keycloak realm)
|
|
51
|
-
*
|
|
52
|
-
* Configuration (env vars):
|
|
53
|
-
* CACHLY_API_URL ā default https://api.cachly.dev
|
|
54
|
-
* CACHLY_JWT ā your JWT (Keycloak access token)
|
|
55
|
-
* CACHLY_EMBED_PROVIDER ā embedding backend: openai (default), gemini, mistral, cohere, ollama, cachly (server fallback)
|
|
56
|
-
* CACHLY_EMBED_MODEL ā override embedding model (optional)
|
|
57
|
-
* TCO_API_URL ā Travel Chaos Organizer backend URL (optional, enables tco_* tools)
|
|
58
|
-
*/
|
|
59
|
-
|
|
60
|
-
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
61
|
-
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
62
|
-
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
63
|
-
import {
|
|
64
|
-
CallToolRequestSchema,
|
|
65
|
-
ErrorCode,
|
|
66
|
-
ListToolsRequestSchema,
|
|
67
|
-
McpError,
|
|
68
|
-
} from '@modelcontextprotocol/sdk/types.js';
|
|
69
|
-
import { Redis } from 'ioredis';
|
|
70
|
-
|
|
71
|
-
// āā Config āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
72
|
-
|
|
73
|
-
const API_URL = process.env.CACHLY_API_URL ?? 'https://api.cachly.dev';
|
|
74
|
-
let JWT = process.env.CACHLY_JWT ?? '';
|
|
75
|
-
const _EMBED_MODEL = process.env.CACHLY_EMBED_MODEL ?? '';
|
|
76
|
-
const CURRENT_VERSION = '0.10.64';
|
|
77
|
-
|
|
78
|
-
// Max time to wait for a freshly-provisioned instance to become "running" before
|
|
79
|
-
// giving up. Free-tier provisioning in high-latency regions can take 45ā90s, so the
|
|
80
|
-
// old hard-coded 25s caused spurious "instance_not_reachable" on first run.
|
|
81
|
-
// Override via CACHLY_PROVISION_TIMEOUT_MS.
|
|
82
|
-
const PROVISION_TIMEOUT_MS = Number(process.env.CACHLY_PROVISION_TIMEOUT_MS ?? 90_000);
|
|
83
|
-
|
|
84
|
-
// āā Default Instance Resolution (for Smithery & single-credential setups) āāāā
|
|
85
|
-
// When CACHLY_BRAIN_INSTANCE_ID is set, tools can omit the instance_id parameter.
|
|
86
|
-
// When neither is set, we auto-fetch the first running instance once per process.
|
|
87
|
-
let _defaultInstanceId: string = process.env.CACHLY_BRAIN_INSTANCE_ID ?? '';
|
|
88
|
-
// Timestamp of last failed fetch ā retries after 30 s (not permanently blocked).
|
|
89
|
-
let _defaultInstanceLastAttempt = 0;
|
|
90
|
-
|
|
91
|
-
async function resolveDefaultInstanceId(): Promise<string> {
|
|
92
|
-
if (_defaultInstanceId) return _defaultInstanceId;
|
|
93
|
-
if (!JWT) return '';
|
|
94
|
-
// Cooldown: don't hammer the API on every tool call after a transient failure.
|
|
95
|
-
const now = Date.now();
|
|
96
|
-
if (_defaultInstanceLastAttempt > 0 && now - _defaultInstanceLastAttempt < 30_000) return '';
|
|
97
|
-
_defaultInstanceLastAttempt = now;
|
|
98
|
-
try {
|
|
99
|
-
const res = await fetch(`${API_URL}/api/v1/instances`, {
|
|
100
|
-
headers: { Authorization: `Bearer ${JWT}`, Accept: 'application/json' },
|
|
101
|
-
signal: AbortSignal.timeout(5000),
|
|
102
|
-
});
|
|
103
|
-
if (!res.ok) return '';
|
|
104
|
-
const data = await res.json() as { data?: Array<{ id: string; status: string }> };
|
|
105
|
-
const instances = (data?.data ?? []) as Array<{ id: string; status: string }>;
|
|
106
|
-
// Prefer running, fall back to provisioning so the ID is resolved even during startup.
|
|
107
|
-
const best = instances.find(i => i.status === 'running') ?? instances.find(i => i.status === 'provisioning');
|
|
108
|
-
if (best) {
|
|
109
|
-
_defaultInstanceId = best.id;
|
|
110
|
-
_defaultInstanceLastAttempt = 0;
|
|
111
|
-
return _defaultInstanceId;
|
|
112
|
-
}
|
|
113
|
-
} catch { /* transient error ā will retry after cooldown */ }
|
|
114
|
-
return '';
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// āā Zero-Credential Device Flow (for Smithery & zero-config installs) āāāāāāāāā
|
|
118
|
-
// When no CACHLY_JWT is set, the server starts an OAuth Device Flow on first tool
|
|
119
|
-
// call. The user visits a short URL, enters a code, and the server polls for the
|
|
120
|
-
// token in the background. After auth, it auto-provisions an instance if needed.
|
|
121
|
-
// State is kept in-memory (works because Smithery keeps one process per session).
|
|
122
|
-
interface DeviceFlowState {
|
|
123
|
-
deviceCode: string;
|
|
124
|
-
userCode: string;
|
|
125
|
-
verifyUrl: string;
|
|
126
|
-
pollInterval: number; // ms
|
|
127
|
-
deadline: number; // epoch ms
|
|
128
|
-
polling: boolean;
|
|
129
|
-
}
|
|
130
|
-
let _deviceFlow: DeviceFlowState | null = null;
|
|
131
|
-
|
|
132
|
-
async function startDeviceFlow(): Promise<DeviceFlowState | null> {
|
|
133
|
-
const AUTH_BASE = 'https://auth.cachly.dev/realms/cachly/protocol/openid-connect';
|
|
134
|
-
const CLIENT_ID = 'cachly-cli';
|
|
135
|
-
try {
|
|
136
|
-
const res = await fetch(`${AUTH_BASE}/auth/device`, {
|
|
137
|
-
method: 'POST',
|
|
138
|
-
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
139
|
-
body: `client_id=${CLIENT_ID}&scope=openid`,
|
|
140
|
-
signal: AbortSignal.timeout(8000),
|
|
141
|
-
});
|
|
142
|
-
if (!res.ok) return null;
|
|
143
|
-
const data = await res.json() as {
|
|
144
|
-
device_code: string; user_code: string;
|
|
145
|
-
verification_uri_complete: string; interval: number;
|
|
146
|
-
};
|
|
147
|
-
return {
|
|
148
|
-
deviceCode: data.device_code,
|
|
149
|
-
userCode: data.user_code,
|
|
150
|
-
verifyUrl: data.verification_uri_complete,
|
|
151
|
-
pollInterval: (data.interval ?? 5) * 1000,
|
|
152
|
-
deadline: Date.now() + 10 * 60 * 1000, // 10 min
|
|
153
|
-
polling: false,
|
|
154
|
-
};
|
|
155
|
-
} catch { return null; }
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
async function pollDeviceFlow(flow: DeviceFlowState): Promise<'pending' | 'expired' | 'done'> {
|
|
159
|
-
if (Date.now() > flow.deadline) return 'expired';
|
|
160
|
-
const AUTH_BASE = 'https://auth.cachly.dev/realms/cachly/protocol/openid-connect';
|
|
161
|
-
const CLIENT_ID = 'cachly-cli';
|
|
162
|
-
try {
|
|
163
|
-
const res = await fetch(`${AUTH_BASE}/token`, {
|
|
164
|
-
method: 'POST',
|
|
165
|
-
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
166
|
-
body: `client_id=${CLIENT_ID}&grant_type=urn:ietf:params:oauth:grant-type:device_code&device_code=${flow.deviceCode}`,
|
|
167
|
-
signal: AbortSignal.timeout(8000),
|
|
168
|
-
});
|
|
169
|
-
const data = await res.json() as { access_token?: string; error?: string };
|
|
170
|
-
if (data.access_token) {
|
|
171
|
-
// Exchange Keycloak JWT ā long-lived API key
|
|
172
|
-
let apiKey = data.access_token;
|
|
173
|
-
try {
|
|
174
|
-
const keyRes = await fetch(`${API_URL}/api/v1/api-keys`, {
|
|
175
|
-
method: 'POST',
|
|
176
|
-
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
|
|
177
|
-
body: JSON.stringify({ name: 'cachly-mcp-smithery', scope: 'read_write' }),
|
|
178
|
-
signal: AbortSignal.timeout(8000),
|
|
179
|
-
});
|
|
180
|
-
if (keyRes.ok) {
|
|
181
|
-
const keyBody = await keyRes.json() as { key: string };
|
|
182
|
-
if (keyBody.key) apiKey = keyBody.key;
|
|
183
|
-
}
|
|
184
|
-
} catch { /* use raw JWT as fallback */ }
|
|
185
|
-
JWT = apiKey;
|
|
186
|
-
setEmbedJwt(apiKey); // keep embeddings.ts in sync
|
|
187
|
-
_deviceFlow = null;
|
|
188
|
-
sendFunnelEvent('device_flow_completed');
|
|
189
|
-
// Persist the API key to ~/.claude/mcp.json so subsequent MCP server restarts
|
|
190
|
-
// keep the token. Without this, JWT is lost on restart and brain_recall_count
|
|
191
|
-
// is never incremented (telemetry has no api_key ā no tenant resolution).
|
|
192
|
-
void (async () => {
|
|
193
|
-
try {
|
|
194
|
-
const { writeFile, mkdir, readFile } = await import('node:fs/promises');
|
|
195
|
-
const { existsSync } = await import('node:fs');
|
|
196
|
-
const { resolve, dirname } = await import('node:path');
|
|
197
|
-
const home = process.env.HOME ?? process.env.USERPROFILE ?? '';
|
|
198
|
-
if (!home) return;
|
|
199
|
-
const configPath = resolve(home, '.claude', 'mcp.json');
|
|
200
|
-
let cfg: { mcpServers?: Record<string, { command?: string; args?: string[]; env?: Record<string, string> }> } = {};
|
|
201
|
-
if (existsSync(configPath)) {
|
|
202
|
-
try { cfg = JSON.parse(await readFile(configPath, 'utf-8')) as typeof cfg; } catch { /* corrupt ā start fresh */ }
|
|
203
|
-
}
|
|
204
|
-
cfg.mcpServers ??= {};
|
|
205
|
-
const existing = cfg.mcpServers['cachly'];
|
|
206
|
-
if (existing) {
|
|
207
|
-
existing.env ??= {};
|
|
208
|
-
existing.env['CACHLY_JWT'] = apiKey;
|
|
209
|
-
} else {
|
|
210
|
-
cfg.mcpServers['cachly'] = {
|
|
211
|
-
command: 'npx', args: ['-y', '@cachly-dev/mcp-server@latest'],
|
|
212
|
-
env: { CACHLY_JWT: apiKey },
|
|
213
|
-
};
|
|
214
|
-
}
|
|
215
|
-
await mkdir(dirname(configPath), { recursive: true });
|
|
216
|
-
await writeFile(configPath, JSON.stringify(cfg, null, 2), 'utf-8');
|
|
217
|
-
} catch { /* non-critical ā don't break the flow on filesystem errors */ }
|
|
218
|
-
})();
|
|
219
|
-
// Auto-provision: find or create the user's brain instance.
|
|
220
|
-
_defaultInstanceLastAttempt = 0;
|
|
221
|
-
await resolveDefaultInstanceId();
|
|
222
|
-
if (!_defaultInstanceId) {
|
|
223
|
-
try {
|
|
224
|
-
const autoRes = await fetch(`${API_URL}/api/v1/instances/auto`, {
|
|
225
|
-
method: 'POST',
|
|
226
|
-
headers: { Authorization: `Bearer ${JWT}`, 'Content-Type': 'application/json' },
|
|
227
|
-
signal: AbortSignal.timeout(12000),
|
|
228
|
-
});
|
|
229
|
-
if (autoRes.ok) {
|
|
230
|
-
const body = await autoRes.json() as {
|
|
231
|
-
instance?: { id: string; status?: string };
|
|
232
|
-
instance_id?: string;
|
|
233
|
-
status?: string;
|
|
234
|
-
};
|
|
235
|
-
const id = body.instance?.id ?? body.instance_id;
|
|
236
|
-
if (id) _defaultInstanceId = id;
|
|
237
|
-
} else {
|
|
238
|
-
// Log so telemetry can detect free-tier auto-provision failures
|
|
239
|
-
void fetch(`${API_URL}/api/v1/telemetry/mcp`, {
|
|
240
|
-
method: 'POST',
|
|
241
|
-
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${JWT}` },
|
|
242
|
-
body: JSON.stringify({ event: 'auto_provision_failed', status: autoRes.status, version: CURRENT_VERSION }),
|
|
243
|
-
signal: AbortSignal.timeout(3000),
|
|
244
|
-
}).catch(() => {});
|
|
245
|
-
}
|
|
246
|
-
} catch { /* non-fatal ā server unreachable, getConnection will surface the error */ }
|
|
247
|
-
}
|
|
248
|
-
// Give a provisioning instance a head-start so getConnection's wait is shorter.
|
|
249
|
-
// The instance is almost always ready by the time the user's tool re-enters.
|
|
250
|
-
if (_defaultInstanceId) {
|
|
251
|
-
try {
|
|
252
|
-
const checkRes = await fetch(`${API_URL}/api/v1/instances/${_defaultInstanceId}`, {
|
|
253
|
-
headers: { Authorization: `Bearer ${JWT}`, Accept: 'application/json' },
|
|
254
|
-
signal: AbortSignal.timeout(4000),
|
|
255
|
-
});
|
|
256
|
-
if (checkRes.ok) {
|
|
257
|
-
const checkInst = await checkRes.json() as { status?: string };
|
|
258
|
-
if (checkInst.status === 'provisioning') {
|
|
259
|
-
// Wait up to 20s here so the re-entered tool call lands on a running instance.
|
|
260
|
-
const pDeadline = Date.now() + 20_000;
|
|
261
|
-
while (checkInst.status === 'provisioning' && Date.now() < pDeadline) {
|
|
262
|
-
await new Promise(r => setTimeout(r, 3000));
|
|
263
|
-
const rr = await fetch(`${API_URL}/api/v1/instances/${_defaultInstanceId}`, {
|
|
264
|
-
headers: { Authorization: `Bearer ${JWT}`, Accept: 'application/json' },
|
|
265
|
-
signal: AbortSignal.timeout(4000),
|
|
266
|
-
}).catch(() => null);
|
|
267
|
-
if (rr?.ok) {
|
|
268
|
-
const ri = await rr.json() as { status?: string };
|
|
269
|
-
(checkInst as { status?: string }).status = ri.status;
|
|
270
|
-
} else { break; }
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
} catch { /* non-fatal ā getConnection will handle the wait if still provisioning */ }
|
|
275
|
-
}
|
|
276
|
-
// Persist instance_id to ~/.claude/mcp.json after it's known.
|
|
277
|
-
// Without this, every restart loses CACHLY_BRAIN_INSTANCE_ID ā resolveDefaultInstanceId
|
|
278
|
-
// makes an extra API call on every startup (with 30s cooldown on failure).
|
|
279
|
-
if (_defaultInstanceId) {
|
|
280
|
-
void (async () => {
|
|
281
|
-
try {
|
|
282
|
-
const { writeFile, readFile } = await import('node:fs/promises');
|
|
283
|
-
const { existsSync } = await import('node:fs');
|
|
284
|
-
const { resolve } = await import('node:path');
|
|
285
|
-
const home = process.env.HOME ?? process.env.USERPROFILE ?? '';
|
|
286
|
-
if (!home) return;
|
|
287
|
-
const configPath = resolve(home, '.claude', 'mcp.json');
|
|
288
|
-
if (!existsSync(configPath)) return;
|
|
289
|
-
let cfg: { mcpServers?: Record<string, { env?: Record<string, string> }> } = {};
|
|
290
|
-
try { cfg = JSON.parse(await readFile(configPath, 'utf-8')) as typeof cfg; } catch { return; }
|
|
291
|
-
const entry = cfg.mcpServers?.['cachly'];
|
|
292
|
-
if (entry?.env) {
|
|
293
|
-
entry.env['CACHLY_BRAIN_INSTANCE_ID'] = _defaultInstanceId;
|
|
294
|
-
await writeFile(configPath, JSON.stringify(cfg, null, 2), 'utf-8');
|
|
295
|
-
}
|
|
296
|
-
} catch { /* non-critical */ }
|
|
297
|
-
})();
|
|
298
|
-
}
|
|
299
|
-
return 'done';
|
|
300
|
-
}
|
|
301
|
-
if (data.error === 'slow_down') flow.pollInterval = Math.min(flow.pollInterval + 2000, 15000);
|
|
302
|
-
return 'pending';
|
|
303
|
-
} catch { return 'pending'; }
|
|
304
|
-
}
|
|
305
|
-
// āā Embeddings (imported from embeddings.ts) āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
306
|
-
import { setEmbedJwt } from './embeddings.js';
|
|
307
|
-
|
|
308
|
-
// āā Search Engine (imported from search.ts) āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
309
|
-
import { tokenize, splitMultiQuery, levenshtein, recencyBoost, extractTimestamp, STOPWORDS,
|
|
310
|
-
katakanaToRomaji, arabicLightStem, expandCrossLingual, CROSS_LINGUAL_MAP,
|
|
311
|
-
ZERO_RESULTS_LOG, zeroResultsTotal } from './search.js';
|
|
312
|
-
|
|
313
|
-
// āā Exported for testing (re-export from search.ts) āāāāāāāāāāāāāāāāāāāāāāāāā
|
|
314
|
-
export { tokenize, splitMultiQuery, levenshtein, recencyBoost, extractTimestamp, STOPWORDS,
|
|
315
|
-
katakanaToRomaji, arabicLightStem, expandCrossLingual, CROSS_LINGUAL_MAP };
|
|
316
|
-
|
|
317
|
-
// āā Connection pool āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
318
|
-
|
|
319
|
-
/** Reuse Redis connections across tool calls (keyed by instance_id). */
|
|
320
|
-
const pool = new Map<string, Redis>();
|
|
321
|
-
|
|
322
|
-
async function getConnection(instance_id: string): Promise<Redis> {
|
|
323
|
-
if (!instance_id) {
|
|
324
|
-
throw new McpError(
|
|
325
|
-
ErrorCode.InvalidRequest,
|
|
326
|
-
'No instance_id provided and no running instance could be resolved automatically.\n\n' +
|
|
327
|
-
'⢠Set CACHLY_BRAIN_INSTANCE_ID in your MCP config, or\n' +
|
|
328
|
-
'⢠Pass instance_id explicitly to the tool, or\n' +
|
|
329
|
-
'⢠Run `list_instances` to see your available instances.'
|
|
330
|
-
);
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
if (pool.has(instance_id)) return pool.get(instance_id)!;
|
|
334
|
-
|
|
335
|
-
// Fetch instance, waiting up to PROVISION_TIMEOUT_MS if it is still provisioning.
|
|
336
|
-
// This covers the zero-friction path: device-flow auth ā auto-provision ā first tool call
|
|
337
|
-
// all happen in quick succession and the instance isn't running yet.
|
|
338
|
-
let inst = await apiFetch<Instance>(`/api/v1/instances/${instance_id}`);
|
|
339
|
-
if (inst.status === 'provisioning') {
|
|
340
|
-
const deadline = Date.now() + PROVISION_TIMEOUT_MS;
|
|
341
|
-
while (inst.status === 'provisioning' && Date.now() < deadline) {
|
|
342
|
-
await new Promise(r => setTimeout(r, 3000));
|
|
343
|
-
inst = await apiFetch<Instance>(`/api/v1/instances/${instance_id}`);
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
if (inst.status !== 'running') {
|
|
348
|
-
// Telemetry: unreachable instance so the team can proactively investigate
|
|
349
|
-
void fetch(`${API_URL}/api/v1/telemetry/mcp`, {
|
|
350
|
-
method: 'POST',
|
|
351
|
-
headers: { 'Content-Type': 'application/json', ...(JWT ? { Authorization: `Bearer ${JWT}` } : {}) },
|
|
352
|
-
body: JSON.stringify({ event: 'instance_not_reachable', instance_id, status: inst.status, version: CURRENT_VERSION }),
|
|
353
|
-
signal: AbortSignal.timeout(3000),
|
|
354
|
-
}).catch(() => {});
|
|
355
|
-
let hint: string;
|
|
356
|
-
if (inst.status === 'provisioning') {
|
|
357
|
-
hint = `ā³ Brain instance "${inst.name}" is still starting up.\n\nFirst-time provisioning typically takes 1ā3 minutes. Please retry in a moment ā the instance will be ready soon.`;
|
|
358
|
-
} else if (inst.status === 'failed') {
|
|
359
|
-
hint = `ā Brain instance "${inst.name}" failed to start.\n\n` +
|
|
360
|
-
`Our system will retry automatically. Check status at: https://cachly.dev/instances\n` +
|
|
361
|
-
`If this persists, contact support@cachly.dev.`;
|
|
362
|
-
} else if (inst.status === 'suspended') {
|
|
363
|
-
hint = `āø Brain instance "${inst.name}" is suspended (billing issue).\n\n` +
|
|
364
|
-
`Update your payment method at: https://cachly.dev/billing`;
|
|
365
|
-
} else if (inst.status === 'pending_payment') {
|
|
366
|
-
hint = `š³ Brain instance "${inst.name}" is waiting for payment.\n\n` +
|
|
367
|
-
`Complete your checkout at: https://cachly.dev/instances`;
|
|
368
|
-
} else {
|
|
369
|
-
hint = `Brain instance "${inst.name}" is not reachable (status: ${inst.status}).\n\n` +
|
|
370
|
-
`⢠View your instance at: https://cachly.dev/instances\n` +
|
|
371
|
-
`⢠Run \`get_api_status\` for a full diagnostic.`;
|
|
372
|
-
}
|
|
373
|
-
throw new McpError(ErrorCode.InvalidRequest, hint);
|
|
374
|
-
}
|
|
375
|
-
if (!inst.host || !inst.port) {
|
|
376
|
-
throw new McpError(ErrorCode.InternalError,
|
|
377
|
-
`Instance "${inst.name}" is running but has no host/port ā contact support@cachly.dev.`);
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
// Fetch connection details (includes password) from the dedicated endpoint.
|
|
381
|
-
let password: string | undefined;
|
|
382
|
-
let tlsEnabled = inst.tls_enabled !== false; // default true
|
|
383
|
-
try {
|
|
384
|
-
const conn = await apiFetch<{ password?: string; tls_enabled?: boolean }>(
|
|
385
|
-
`/api/v1/instances/${instance_id}/connection`
|
|
386
|
-
);
|
|
387
|
-
password = conn.password ?? undefined;
|
|
388
|
-
tlsEnabled = conn.tls_enabled !== false;
|
|
389
|
-
} catch (connErr) {
|
|
390
|
-
// If the connection endpoint fails and the instance has a password, we cannot connect.
|
|
391
|
-
// Surface the error so the user knows why Redis auth will fail rather than getting a cryptic NOAUTH.
|
|
392
|
-
const msg = connErr instanceof Error ? connErr.message : String(connErr);
|
|
393
|
-
if (msg.includes('401') || msg.includes('403') || msg.includes('Unauthorized')) {
|
|
394
|
-
throw new McpError(ErrorCode.InvalidRequest,
|
|
395
|
-
`API key rejected ā run \`cachly setup\` to refresh your credentials.`);
|
|
396
|
-
}
|
|
397
|
-
// Other failures (timeout, 500): proceed without password, Redis will surface NOAUTH if needed
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
const client = new Redis({
|
|
401
|
-
host: inst.host,
|
|
402
|
-
port: inst.port,
|
|
403
|
-
password: password || undefined,
|
|
404
|
-
commandTimeout: 8000,
|
|
405
|
-
...(tlsEnabled ? { tls: {} } : {}),
|
|
406
|
-
lazyConnect: true,
|
|
407
|
-
enableReadyCheck: true,
|
|
408
|
-
connectTimeout: 8000,
|
|
409
|
-
retryStrategy: () => null, // fail fast, no reconnect loops in MCP context
|
|
410
|
-
});
|
|
411
|
-
|
|
412
|
-
const evict = () => pool.delete(instance_id);
|
|
413
|
-
client.on('error', evict);
|
|
414
|
-
client.on('end', evict);
|
|
415
|
-
|
|
416
|
-
try {
|
|
417
|
-
await client.connect();
|
|
418
|
-
} catch (err: unknown) {
|
|
419
|
-
client.disconnect();
|
|
420
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
421
|
-
void fetch(`${API_URL}/api/v1/telemetry/mcp`, {
|
|
422
|
-
method: 'POST',
|
|
423
|
-
headers: { 'Content-Type': 'application/json', ...(JWT ? { Authorization: `Bearer ${JWT}` } : {}) },
|
|
424
|
-
body: JSON.stringify({ event: 'redis_connect_failed', instance_id, host: inst.host, port: inst.port, error: msg, version: CURRENT_VERSION }),
|
|
425
|
-
signal: AbortSignal.timeout(3000),
|
|
426
|
-
}).catch(() => {});
|
|
427
|
-
throw new McpError(
|
|
428
|
-
ErrorCode.InternalError,
|
|
429
|
-
`Could not reach Brain instance "${inst.name}" (${inst.host}:${inst.port}).\n\n` +
|
|
430
|
-
`⢠Check your network connection\n` +
|
|
431
|
-
`⢠Run \`get_api_status\` to verify instance status\n` +
|
|
432
|
-
`⢠Technical detail: ${msg}`
|
|
433
|
-
);
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
pool.set(instance_id, client);
|
|
437
|
-
return client;
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
// āā API helper āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
441
|
-
|
|
442
|
-
async function apiFetch<T>(path: string, options: RequestInit = {}): Promise<T> {
|
|
443
|
-
checkJwt(JWT);
|
|
444
|
-
const timeoutMs = Number(process.env.CACHLY_API_TIMEOUT_MS ?? 15_000);
|
|
445
|
-
const res = await fetch(`${API_URL}${path}`, {
|
|
446
|
-
...options,
|
|
447
|
-
signal: AbortSignal.timeout(timeoutMs),
|
|
448
|
-
headers: {
|
|
449
|
-
'Content-Type': 'application/json',
|
|
450
|
-
Authorization: `Bearer ${JWT}`,
|
|
451
|
-
...options.headers,
|
|
452
|
-
},
|
|
453
|
-
});
|
|
454
|
-
if (!res.ok) {
|
|
455
|
-
const body = await res.json().catch(() => ({ error: res.statusText }));
|
|
456
|
-
const detail = (body as { error?: string }).error ?? res.statusText;
|
|
457
|
-
handleApiError(res.status, detail);
|
|
458
|
-
}
|
|
459
|
-
if (res.status === 204) return undefined as T;
|
|
460
|
-
return res.json() as Promise<T>;
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
import { handleBrainTool } from './handlers/brain.js';
|
|
464
|
-
import { handleContextTool } from './handlers/context.js';
|
|
465
|
-
import { handleInstanceTool } from './handlers/instances.js';
|
|
466
|
-
import { handleCacheTool } from './handlers/cache.js';
|
|
467
|
-
import { handleTeamTool } from './handlers/team.js';
|
|
468
|
-
import { handleRoadmapTool } from './handlers/roadmap.js';
|
|
469
|
-
import { handleAdvancedTool } from './handlers/advanced.js';
|
|
470
|
-
import { handleSyndicateTool } from './handlers/syndicate.js';
|
|
471
|
-
import { handleFedbrainTool, _lastBrainFromGitCounts } from './handlers/fedbrain.js';
|
|
472
|
-
import type { Instance } from './handlers/brain.js';
|
|
473
|
-
|
|
474
|
-
// āā Tools (imported from tools.ts) āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
475
|
-
import { TOOLS } from './tools.js';
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
// āā Handlers āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
479
|
-
|
|
480
|
-
// Fires once per process when no JWT is set (anonymous, opt-out via CACHLY_NO_TELEMETRY=1)
|
|
481
|
-
let _telemetryPingSent = false;
|
|
482
|
-
// Fires once per process on the first successful Brain tool call ā key activation metric
|
|
483
|
-
let _firstCallSuccessSent = false;
|
|
484
|
-
|
|
485
|
-
function detectEditor(): string {
|
|
486
|
-
return process.env.CURSOR_TRACE_ID ? 'cursor'
|
|
487
|
-
: process.env.WINDSURF_SESSION_ID ? 'windsurf'
|
|
488
|
-
: process.env.GITHUB_COPILOT_WORKSPACE ? 'copilot'
|
|
489
|
-
: process.env.CLAUDE_CODE_ENTRYPOINT ? 'claude'
|
|
490
|
-
: 'unknown';
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
function sendFunnelEvent(event: string, extra?: Record<string, unknown>): void {
|
|
494
|
-
if (process.env.CACHLY_NO_TELEMETRY === '1') return;
|
|
495
|
-
void fetch(`${API_URL}/api/v1/telemetry/mcp`, {
|
|
496
|
-
method: 'POST',
|
|
497
|
-
headers: { 'Content-Type': 'application/json' },
|
|
498
|
-
body: JSON.stringify({
|
|
499
|
-
event, version: CURRENT_VERSION, editor: detectEditor(),
|
|
500
|
-
...(JWT ? { jwt: JWT } : {}),
|
|
501
|
-
...extra,
|
|
502
|
-
}),
|
|
503
|
-
signal: AbortSignal.timeout(3000),
|
|
504
|
-
}).catch(() => {/* fire-and-forget */});
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
async function sendAnonymousTelemetry(toolName: string): Promise<void> {
|
|
508
|
-
if (_telemetryPingSent) return;
|
|
509
|
-
if (process.env.CACHLY_NO_TELEMETRY === '1') return;
|
|
510
|
-
_telemetryPingSent = true;
|
|
511
|
-
try {
|
|
512
|
-
await fetch(`${API_URL}/api/v1/telemetry/mcp`, {
|
|
513
|
-
method: 'POST',
|
|
514
|
-
headers: { 'Content-Type': 'application/json' },
|
|
515
|
-
body: JSON.stringify({ event: 'first_call_no_jwt', version: CURRENT_VERSION, editor: detectEditor(), tool: toolName }),
|
|
516
|
-
signal: AbortSignal.timeout(3000),
|
|
517
|
-
});
|
|
518
|
-
} catch { /* fire-and-forget, never block the user */ }
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
async function handleTool(name: string, args: Record<string, unknown>): Promise<string> {
|
|
522
|
-
// Guard: if no JWT, return actionable onboarding message instead of HTTP 401
|
|
523
|
-
if (!JWT) {
|
|
524
|
-
void sendAnonymousTelemetry(name);
|
|
525
|
-
|
|
526
|
-
// āā Zero-credential device flow āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
527
|
-
// 1st call: start device flow, return code + URL
|
|
528
|
-
// 2nd+ calls: poll for token; once authenticated, proceed transparently
|
|
529
|
-
if (_deviceFlow) {
|
|
530
|
-
const result = await pollDeviceFlow(_deviceFlow);
|
|
531
|
-
if (result === 'done') {
|
|
532
|
-
// Auth complete ā re-enter handleTool with now-valid JWT
|
|
533
|
-
return handleTool(name, args);
|
|
534
|
-
}
|
|
535
|
-
if (result === 'expired') {
|
|
536
|
-
_deviceFlow = null;
|
|
537
|
-
return 'ā **Authentication timed out.** Please call any tool again to restart the sign-in flow.';
|
|
538
|
-
}
|
|
539
|
-
// Still pending
|
|
540
|
-
return [
|
|
541
|
-
'ā³ **Waiting for authentication...**',
|
|
542
|
-
'',
|
|
543
|
-
`Sign in at: **${_deviceFlow.verifyUrl}**`,
|
|
544
|
-
`Enter code: **${_deviceFlow.userCode}**`,
|
|
545
|
-
'',
|
|
546
|
-
'Once you complete sign-in in your browser, call this tool again and it will proceed automatically.',
|
|
547
|
-
].join('\n');
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
// No pending flow ā start a new one
|
|
551
|
-
const flow = await startDeviceFlow();
|
|
552
|
-
if (flow) {
|
|
553
|
-
_deviceFlow = flow;
|
|
554
|
-
sendFunnelEvent('device_flow_started', { tool: name });
|
|
555
|
-
// Try to open the browser automatically ā fire-and-forget, never block
|
|
556
|
-
void (async () => {
|
|
557
|
-
try {
|
|
558
|
-
const { exec } = await import('node:child_process');
|
|
559
|
-
const url = flow.verifyUrl;
|
|
560
|
-
const cmd = process.platform === 'win32' ? `start "" "${url}"`
|
|
561
|
-
: process.platform === 'darwin' ? `open "${url}"`
|
|
562
|
-
: `xdg-open "${url}"`;
|
|
563
|
-
exec(cmd);
|
|
564
|
-
} catch { /* non-critical */ }
|
|
565
|
-
})();
|
|
566
|
-
return [
|
|
567
|
-
'š§ **cachly AI Brain ā sign in to activate** (browser opening...)',
|
|
568
|
-
'',
|
|
569
|
-
`š **${flow.verifyUrl}**`,
|
|
570
|
-
'',
|
|
571
|
-
`Code: **${flow.userCode}** (pre-filled if browser opened automatically)`,
|
|
572
|
-
'',
|
|
573
|
-
'After sign-in: call **any tool again** ā your Brain activates instantly.',
|
|
574
|
-
'',
|
|
575
|
-
'⨠Free forever · No credit card · 101 MCP tools · GDPR · EU servers',
|
|
576
|
-
].join('\n');
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
// Device flow unavailable (network issue) ā fall back to manual setup
|
|
580
|
-
return [
|
|
581
|
-
'š§ **cachly AI Brain ā Setup required**',
|
|
582
|
-
'',
|
|
583
|
-
'Run the setup wizard once in your terminal:',
|
|
584
|
-
' ```',
|
|
585
|
-
' npx @cachly-dev/mcp-server@latest setup',
|
|
586
|
-
' ```',
|
|
587
|
-
'',
|
|
588
|
-
'Or get your API key at: https://cachly.dev/setup-ai',
|
|
589
|
-
'',
|
|
590
|
-
'⨠Free tier includes: 1 Brain instance, persistent memory, semantic search.',
|
|
591
|
-
].join('\n');
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
// Auto-resolve instance_id from env / API when not provided in args
|
|
595
|
-
if (!args.instance_id) {
|
|
596
|
-
const defaultId = await resolveDefaultInstanceId();
|
|
597
|
-
if (defaultId) args = { ...args, instance_id: defaultId };
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
// Delegate brain tools (learn, recall, session, etc.)
|
|
601
|
-
const brainResult = await handleBrainTool(name, args, getConnection, apiFetch);
|
|
602
|
-
if (brainResult !== null) {
|
|
603
|
-
if (!_firstCallSuccessSent && JWT) {
|
|
604
|
-
_firstCallSuccessSent = true;
|
|
605
|
-
sendFunnelEvent('first_call_success', { tool: name, instance_id: args.instance_id ?? _defaultInstanceId ?? '' });
|
|
606
|
-
}
|
|
607
|
-
// Per-tool telemetry ā fires after every successful brain tool call.
|
|
608
|
-
// api_key carries the cky_live_... token so the backend can resolve tenant + increment counters.
|
|
609
|
-
const instanceId = (args.instance_id as string | undefined) ?? _defaultInstanceId ?? '';
|
|
610
|
-
const telemetryExtra = JWT ? { api_key: JWT, instance_id: instanceId } : { instance_id: instanceId };
|
|
611
|
-
if (name === 'recall_best_solution') {
|
|
612
|
-
// recall_best_solution ā increments BrainRecallCount + triggers level-up logic
|
|
613
|
-
sendFunnelEvent('recall_best_solution', telemetryExtra);
|
|
614
|
-
} else if (name === 'learn_from_attempts') {
|
|
615
|
-
sendFunnelEvent('learn_from_attempts', telemetryExtra);
|
|
616
|
-
} else if (name === 'session_start') {
|
|
617
|
-
sendFunnelEvent('session_start', telemetryExtra);
|
|
618
|
-
const resultText = typeof brainResult === 'object' && brainResult !== null && 'content' in brainResult
|
|
619
|
-
? JSON.stringify(brainResult)
|
|
620
|
-
: String(brainResult ?? '');
|
|
621
|
-
const isFirstSession = resultText.includes('Welcome! Your AI Brain is live.');
|
|
622
|
-
|
|
623
|
-
if (!isFirstSession && resultText.length > 100) {
|
|
624
|
-
// Existing brain ā session_start acts as implicit recall ā increment counter.
|
|
625
|
-
sendFunnelEvent('recall_best_solution', telemetryExtra);
|
|
626
|
-
} else if (isFirstSession && args.workspace_path) {
|
|
627
|
-
// First session with a known workspace: auto-bootstrap from git history.
|
|
628
|
-
// Runs synchronously so the user sees the result in the same response.
|
|
629
|
-
try {
|
|
630
|
-
const gitBootstrap = await handleFedbrainTool('brain_from_git', {
|
|
631
|
-
instance_id: instanceId,
|
|
632
|
-
repo_path: args.workspace_path as string,
|
|
633
|
-
limit: 50,
|
|
634
|
-
}, getConnection, apiFetch);
|
|
635
|
-
if (gitBootstrap !== null) {
|
|
636
|
-
const gitCounts = _lastBrainFromGitCounts;
|
|
637
|
-
sendFunnelEvent('brain_from_git', { ...telemetryExtra, ...(gitCounts ?? {}) });
|
|
638
|
-
sendFunnelEvent('recall_best_solution', telemetryExtra);
|
|
639
|
-
// Append bootstrap summary to the session_start briefing.
|
|
640
|
-
// brainResult is always a string here (handleBrainTool returns string for session_start).
|
|
641
|
-
const bootstrapText = String(gitBootstrap);
|
|
642
|
-
if (bootstrapText) {
|
|
643
|
-
return String(brainResult) + '\n---\n' + bootstrapText;
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
} catch { /* git bootstrap errors must never break session_start */ }
|
|
647
|
-
}
|
|
648
|
-
} else if (name === 'session_end') {
|
|
649
|
-
sendFunnelEvent('session_end', telemetryExtra);
|
|
650
|
-
} else if (name === 'smart_recall') {
|
|
651
|
-
sendFunnelEvent('smart_recall', telemetryExtra);
|
|
652
|
-
// smart_recall is the primary recall tool in CLAUDE.md ā it retrieves brain lessons
|
|
653
|
-
// just like recall_best_solution. Count it toward BrainRecallCount so the dashboard
|
|
654
|
-
// nudge clears and the first-recall email fires for users following CLAUDE.md.
|
|
655
|
-
const srText = typeof brainResult === 'object' && brainResult !== null && 'content' in brainResult
|
|
656
|
-
? JSON.stringify(brainResult)
|
|
657
|
-
: String(brainResult ?? '');
|
|
658
|
-
if (srText.length > 50 && !srText.includes('No lessons found') && !srText.includes('no lessons') && !srText.includes('No matches found')) {
|
|
659
|
-
sendFunnelEvent('recall_best_solution', telemetryExtra);
|
|
660
|
-
}
|
|
661
|
-
}
|
|
662
|
-
return brainResult;
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
// Delegate context tools (remember/recall/list/forget)
|
|
666
|
-
const contextResult = await handleContextTool(name, args, getConnection, apiFetch);
|
|
667
|
-
if (contextResult !== null) {
|
|
668
|
-
if (!_firstCallSuccessSent && JWT) {
|
|
669
|
-
_firstCallSuccessSent = true;
|
|
670
|
-
sendFunnelEvent('first_call_success', { tool: name, instance_id: args.instance_id ?? _defaultInstanceId ?? '' });
|
|
671
|
-
}
|
|
672
|
-
return contextResult;
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
// Delegate instance + cache tools
|
|
676
|
-
const instanceResult = await handleInstanceTool(name, args, getConnection, apiFetch);
|
|
677
|
-
if (instanceResult !== null) return instanceResult;
|
|
678
|
-
|
|
679
|
-
const cacheResult = await handleCacheTool(name, args, getConnection, apiFetch);
|
|
680
|
-
if (cacheResult !== null) return cacheResult;
|
|
681
|
-
|
|
682
|
-
// Delegate team/brain advanced tools
|
|
683
|
-
const teamResult = await handleTeamTool(name, args, getConnection, apiFetch);
|
|
684
|
-
if (teamResult !== null) return teamResult;
|
|
685
|
-
|
|
686
|
-
const roadmapResult = await handleRoadmapTool(name, args, getConnection, apiFetch);
|
|
687
|
-
if (roadmapResult !== null) return roadmapResult;
|
|
688
|
-
|
|
689
|
-
const advancedResult = await handleAdvancedTool(name, args, getConnection, apiFetch);
|
|
690
|
-
if (advancedResult !== null) return advancedResult;
|
|
691
|
-
|
|
692
|
-
const syndicateResult = await handleSyndicateTool(name, args, getConnection, apiFetch);
|
|
693
|
-
if (syndicateResult !== null) {
|
|
694
|
-
// brain_predict is in syndicate handler ā track telemetry here.
|
|
695
|
-
if (name === 'brain_predict') {
|
|
696
|
-
const instanceId = (args.instance_id as string | undefined) ?? _defaultInstanceId ?? '';
|
|
697
|
-
const telemetryExtra = JWT ? { api_key: JWT, instance_id: instanceId } : { instance_id: instanceId };
|
|
698
|
-
sendFunnelEvent('brain_predict', telemetryExtra);
|
|
699
|
-
}
|
|
700
|
-
return syndicateResult;
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
const fedbrainResult = await handleFedbrainTool(name, args, getConnection, apiFetch);
|
|
704
|
-
if (fedbrainResult !== null) {
|
|
705
|
-
// brain_from_git and brain_predict_failures are in fedbrain handler.
|
|
706
|
-
const instanceId = (args.instance_id as string | undefined) ?? _defaultInstanceId ?? '';
|
|
707
|
-
const telemetryExtra = JWT ? { api_key: JWT, instance_id: instanceId } : { instance_id: instanceId };
|
|
708
|
-
if (name === 'brain_from_git') {
|
|
709
|
-
const gitCounts = _lastBrainFromGitCounts;
|
|
710
|
-
sendFunnelEvent('brain_from_git', { ...telemetryExtra, ...(gitCounts ?? {}) });
|
|
711
|
-
} else if (name === 'brain_predict_failures') {
|
|
712
|
-
sendFunnelEvent('brain_predict_failures', telemetryExtra);
|
|
713
|
-
}
|
|
714
|
-
return fedbrainResult;
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
// Delegate Travel Chaos Organizer tools (requires TCO_API_URL env var)
|
|
718
|
-
const tcoResult = await handleTcoTool(name, args, JWT ?? '');
|
|
719
|
-
if (tcoResult !== null) return tcoResult;
|
|
720
|
-
|
|
721
|
-
switch (name) {
|
|
722
|
-
// āā Instance management āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
723
|
-
case 'get_api_status': {
|
|
724
|
-
// Check health
|
|
725
|
-
let healthStatus = 'unknown';
|
|
726
|
-
try {
|
|
727
|
-
const healthRes = await fetch(`${API_URL}/health`);
|
|
728
|
-
if (healthRes.ok) {
|
|
729
|
-
const body = await healthRes.json() as { status?: string; db?: string };
|
|
730
|
-
healthStatus = `${body.status ?? 'ok'} (db: ${body.db ?? '?'})`;
|
|
731
|
-
} else {
|
|
732
|
-
healthStatus = `HTTP ${healthRes.status}`;
|
|
733
|
-
}
|
|
734
|
-
} catch (e) {
|
|
735
|
-
healthStatus = `unreachable: ${(e as Error).message}`;
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
// Check JWT / auth
|
|
739
|
-
if (!JWT) {
|
|
740
|
-
return [
|
|
741
|
-
`š” **cachly API Status**`,
|
|
742
|
-
``,
|
|
743
|
-
` š API: ${API_URL}`,
|
|
744
|
-
` š Health: ${healthStatus}`,
|
|
745
|
-
` š Auth: ā CACHLY_JWT not set`,
|
|
746
|
-
``,
|
|
747
|
-
`š” Get your API token at https://cachly.dev/instances ā Settings ā API Token`,
|
|
748
|
-
].join('\n');
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
// Decode JWT claims (inspection only, no verification)
|
|
752
|
-
let authInfo = 'ā invalid JWT format';
|
|
753
|
-
try {
|
|
754
|
-
const parts = JWT.split('.');
|
|
755
|
-
if (parts.length === 3) {
|
|
756
|
-
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf-8')) as {
|
|
757
|
-
sub?: string; exp?: number; iss?: string;
|
|
758
|
-
};
|
|
759
|
-
const sub = payload.sub ?? '(unknown)';
|
|
760
|
-
const iss = payload.iss ?? '(unknown)';
|
|
761
|
-
const provider = iss.includes('keycloak') ? 'Keycloak' : 'OIDC';
|
|
762
|
-
const expTs = payload.exp ? new Date(payload.exp * 1000) : null;
|
|
763
|
-
const expired = expTs ? expTs < new Date() : false;
|
|
764
|
-
authInfo = [
|
|
765
|
-
`ā
JWT decoded`,
|
|
766
|
-
` Sub (user ID): ${sub}`,
|
|
767
|
-
` Provider: ${provider}`,
|
|
768
|
-
` Issuer: ${iss}`,
|
|
769
|
-
` Expires: ${expTs ? expTs.toISOString() : 'never'} ${expired ? 'ā ļø EXPIRED ā get a new token!' : 'ā
'}`,
|
|
770
|
-
].join('\n');
|
|
771
|
-
}
|
|
772
|
-
} catch {
|
|
773
|
-
authInfo = 'ā JWT decode failed ā check CACHLY_JWT format';
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
// List instances + connectivity
|
|
777
|
-
let instanceInfo = '';
|
|
778
|
-
try {
|
|
779
|
-
const listRes = await apiFetch<{ data: Instance[] }>('/api/v1/instances');
|
|
780
|
-
const instances = listRes.data ?? [];
|
|
781
|
-
if (instances.length === 0) {
|
|
782
|
-
instanceInfo = '\n\nš§ **Brain Instances:** none ā create one at https://cachly.dev/instances';
|
|
783
|
-
} else {
|
|
784
|
-
const lines: string[] = ['\n\nš§ **Brain Instances:**'];
|
|
785
|
-
for (const inst of instances) {
|
|
786
|
-
const badge = inst.status === 'running' ? 'š¢' : inst.status === 'provisioning' ? 'š”' : 'š“';
|
|
787
|
-
const defaultMark = inst.id === _defaultInstanceId ? ' ā active' : '';
|
|
788
|
-
lines.push(` ${badge} **${inst.name}** (\`${inst.id}\`) Ā· ${inst.status}${defaultMark}`);
|
|
789
|
-
if (inst.status === 'running' && inst.host) {
|
|
790
|
-
// Try a quick ping on the cached connection
|
|
791
|
-
const pooled = pool.get(inst.id);
|
|
792
|
-
if (pooled) {
|
|
793
|
-
try {
|
|
794
|
-
await pooled.ping();
|
|
795
|
-
lines.push(` š Redis: connected`);
|
|
796
|
-
} catch {
|
|
797
|
-
lines.push(` ā ļø Redis: pooled but ping failed ā will reconnect on next use`);
|
|
798
|
-
}
|
|
799
|
-
} else {
|
|
800
|
-
lines.push(` š¤ Redis: not connected yet (connects on first tool use)`);
|
|
801
|
-
}
|
|
802
|
-
} else if (inst.status === 'provisioning') {
|
|
803
|
-
lines.push(` ā³ Still provisioning ā check back in ~30 s`);
|
|
804
|
-
} else if (inst.status !== 'running') {
|
|
805
|
-
lines.push(` ā Not reachable (status: ${inst.status})`);
|
|
806
|
-
}
|
|
807
|
-
}
|
|
808
|
-
instanceInfo = lines.join('\n');
|
|
809
|
-
}
|
|
810
|
-
} catch (e) {
|
|
811
|
-
instanceInfo = `\n\nš§ **Brain Instances:** could not fetch ā ${(e as Error).message}`;
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
return [
|
|
815
|
-
`š” **cachly API Status**`,
|
|
816
|
-
``,
|
|
817
|
-
` š API: ${API_URL}`,
|
|
818
|
-
` š Health: ${healthStatus}`,
|
|
819
|
-
``,
|
|
820
|
-
`š **Auth:**`,
|
|
821
|
-
authInfo,
|
|
822
|
-
instanceInfo,
|
|
823
|
-
].join('\n');
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
default:
|
|
827
|
-
return `ā ļø Unknown tool: ${name}`;
|
|
828
|
-
}
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
const server = new Server(
|
|
832
|
-
{ name: 'cachly-mcp', version: CURRENT_VERSION },
|
|
833
|
-
{ capabilities: { tools: {} } }
|
|
834
|
-
);
|
|
835
|
-
|
|
836
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
837
|
-
|
|
838
|
-
// āā Auto-session management āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
839
|
-
// Transparently starts a Brain session on the first tool call of a process
|
|
840
|
-
// and saves a session_end summary on SIGTERM/exit so users never have to call
|
|
841
|
-
// session_start/session_end manually.
|
|
842
|
-
|
|
843
|
-
let _autoSessionInstanceId: string | null = null;
|
|
844
|
-
let _autoSessionStarted = false;
|
|
845
|
-
let _autoSessionStarting: Promise<void> | null = null; // guard against double-start race
|
|
846
|
-
let _autoSessionToolCount = 0;
|
|
847
|
-
|
|
848
|
-
// āā brain_from_git concurrency limiter āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
849
|
-
// Spawning many `git log` subprocesses in parallel can fork-bomb the host.
|
|
850
|
-
// This simple semaphore caps concurrent brain_from_git calls to 10 per process.
|
|
851
|
-
async function autoStartSession(instanceId: string): Promise<void> {
|
|
852
|
-
if (_autoSessionStarted) return;
|
|
853
|
-
// If another call already started the session, await it instead of double-starting.
|
|
854
|
-
if (_autoSessionStarting) { await _autoSessionStarting; return; }
|
|
855
|
-
_autoSessionStarting = (async () => {
|
|
856
|
-
_autoSessionStarted = true;
|
|
857
|
-
_autoSessionInstanceId = instanceId;
|
|
858
|
-
try {
|
|
859
|
-
await handleTool('session_start', { instance_id: instanceId, focus: 'auto (MCP session)', workspace_path: process.cwd() });
|
|
860
|
-
} catch { /* non-fatal ā session tracking is a best-effort feature */ } finally {
|
|
861
|
-
_autoSessionStarting = null;
|
|
862
|
-
}
|
|
863
|
-
})();
|
|
864
|
-
await _autoSessionStarting;
|
|
865
|
-
|
|
866
|
-
// Auto-index the project if it hasn't been indexed in the last 24h.
|
|
867
|
-
// This is the main lever for growing token savings: more indexed code ā
|
|
868
|
-
// more semantic cache hits ā fewer LLM calls for repeated questions.
|
|
869
|
-
if (process.env.CACHLY_AUTO_INDEX !== 'false') {
|
|
870
|
-
try {
|
|
871
|
-
const redis = await getConnection(instanceId);
|
|
872
|
-
const lastIndexed = await redis.get(`cachly:index:last_indexed:${instanceId}`);
|
|
873
|
-
const staleMs = 24 * 60 * 60 * 1000;
|
|
874
|
-
const isStale = !lastIndexed || (Date.now() - parseInt(lastIndexed, 10)) > staleMs;
|
|
875
|
-
if (isStale) {
|
|
876
|
-
// Mark as indexing now to prevent concurrent re-runs.
|
|
877
|
-
await redis.set(`cachly:index:last_indexed:${instanceId}`, String(Date.now()), 'EX', 90000);
|
|
878
|
-
// Run in background ā don't block the first tool call.
|
|
879
|
-
handleTool('index_project', {
|
|
880
|
-
instance_id: instanceId,
|
|
881
|
-
dir: process.cwd(),
|
|
882
|
-
max_files: 150,
|
|
883
|
-
ttl: 86400 * 7, // cache for 7 days
|
|
884
|
-
namespace: 'cachly:sem:code',
|
|
885
|
-
}).catch(() => undefined);
|
|
886
|
-
}
|
|
887
|
-
} catch { /* never block the session on indexing errors */ }
|
|
888
|
-
}
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
async function autoEndSession(): Promise<void> {
|
|
892
|
-
if (!_autoSessionStarted || !_autoSessionInstanceId) return;
|
|
893
|
-
_autoSessionStarted = false;
|
|
894
|
-
try {
|
|
895
|
-
await handleTool('session_end', {
|
|
896
|
-
instance_id: _autoSessionInstanceId,
|
|
897
|
-
summary: `Auto-ended after ${_autoSessionToolCount} tool call(s). Session was started automatically.`,
|
|
898
|
-
files_changed: [],
|
|
899
|
-
lessons_learned: 0,
|
|
900
|
-
});
|
|
901
|
-
} catch { /* non-fatal */ }
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
905
|
-
const { name, arguments: args } = request.params;
|
|
906
|
-
|
|
907
|
-
// Auto-start brain session on first tool call that has an instance_id.
|
|
908
|
-
// Skip session management tools to avoid recursion.
|
|
909
|
-
const sessionTools = new Set(['session_start', 'session_end', 'auto_learn_session']);
|
|
910
|
-
if (!sessionTools.has(name) && !_autoSessionStarted && !_autoSessionStarting) {
|
|
911
|
-
const instanceId = ((args as Record<string, unknown>)?.instance_id as string | undefined)
|
|
912
|
-
|| _defaultInstanceId
|
|
913
|
-
|| process.env.CACHLY_BRAIN_INSTANCE_ID;
|
|
914
|
-
if (instanceId) {
|
|
915
|
-
await autoStartSession(instanceId).catch(() => undefined);
|
|
916
|
-
}
|
|
917
|
-
} else if (_autoSessionStarting) {
|
|
918
|
-
// Another concurrent call is mid-start ā wait for it before proceeding.
|
|
919
|
-
await _autoSessionStarting.catch(() => undefined);
|
|
920
|
-
}
|
|
921
|
-
if (!sessionTools.has(name)) _autoSessionToolCount++;
|
|
922
|
-
|
|
923
|
-
try {
|
|
924
|
-
const text = await handleTool(name, (args ?? {}) as Record<string, unknown>);
|
|
925
|
-
return { content: [{ type: 'text', text }] };
|
|
926
|
-
} catch (err) {
|
|
927
|
-
if (err instanceof McpError) throw err;
|
|
928
|
-
const msg = (err as Error).message ?? String(err);
|
|
929
|
-
notify('cachly', 'tool_error', { tool: name, error: msg }).catch(() => undefined);
|
|
930
|
-
throw new McpError(ErrorCode.InternalError, msg);
|
|
931
|
-
}
|
|
932
|
-
});
|
|
933
|
-
|
|
934
|
-
// Graceful shutdown ā close all Redis connections + auto-end brain session
|
|
935
|
-
process.on('SIGTERM', async () => {
|
|
936
|
-
await autoEndSession().catch(() => undefined);
|
|
937
|
-
for (const [, client] of pool) await client.quit().catch(() => undefined);
|
|
938
|
-
process.exit(0);
|
|
939
|
-
});
|
|
940
|
-
|
|
941
|
-
process.on('SIGINT', async () => {
|
|
942
|
-
await autoEndSession().catch(() => undefined);
|
|
943
|
-
for (const [, client] of pool) await client.quit().catch(() => undefined);
|
|
944
|
-
process.exit(0);
|
|
945
|
-
});
|
|
946
|
-
|
|
947
|
-
// Safety net: log unhandled rejections instead of crashing the MCP process.
|
|
948
|
-
// The MCP server must stay alive even if a single tool call hits an unexpected error.
|
|
949
|
-
process.on('unhandledRejection', (reason) => {
|
|
950
|
-
const msg = reason instanceof Error ? reason.message : String(reason);
|
|
951
|
-
// Write to stderr only (stdout is the JSON-RPC channel)
|
|
952
|
-
process.stderr.write(`[cachly-mcp] unhandledRejection: ${msg}\n`);
|
|
953
|
-
});
|
|
954
|
-
|
|
955
|
-
// āā CLI helpers āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
956
|
-
|
|
957
|
-
const EDITOR_FILES: Record<string, string> = {
|
|
958
|
-
claude: '.mcp.json',
|
|
959
|
-
cursor: '.cursor/mcp.json',
|
|
960
|
-
windsurf: '.windsurf/mcp.json',
|
|
961
|
-
copilot: '.vscode/mcp.json',
|
|
962
|
-
continue: '.continue/config.json',
|
|
963
|
-
cline: '.vscode/mcp.json', // Cline respects VS Code project MCP config
|
|
964
|
-
zed: '.zed/settings.json', // Zed project-level context_servers
|
|
965
|
-
};
|
|
966
|
-
|
|
967
|
-
const CLAUDE_MD_MARKER_START = '<!-- cachly-brain-start -->';
|
|
968
|
-
const CLAUDE_MD_MARKER_END = '<!-- cachly-brain-end -->';
|
|
969
|
-
|
|
970
|
-
function buildMcpConfig(apiKey: string, instanceId: string, editor: string): string {
|
|
971
|
-
if (editor === 'continue') {
|
|
972
|
-
return JSON.stringify({
|
|
973
|
-
experimental: {
|
|
974
|
-
modelContextProtocolServers: [{
|
|
975
|
-
transport: { type: 'stdio', command: 'npx', args: ['-y', '@cachly-dev/mcp-server@latest'] },
|
|
976
|
-
env: { CACHLY_API_URL: 'https://api.cachly.dev', CACHLY_JWT: apiKey, CACHLY_BRAIN_INSTANCE_ID: instanceId },
|
|
977
|
-
}],
|
|
978
|
-
},
|
|
979
|
-
}, null, 2);
|
|
980
|
-
}
|
|
981
|
-
if (editor === 'zed') {
|
|
982
|
-
return JSON.stringify({
|
|
983
|
-
context_servers: {
|
|
984
|
-
cachly: {
|
|
985
|
-
command: {
|
|
986
|
-
path: 'npx',
|
|
987
|
-
args: ['-y', '@cachly-dev/mcp-server@latest'],
|
|
988
|
-
env: { CACHLY_API_URL: 'https://api.cachly.dev', CACHLY_JWT: apiKey, CACHLY_BRAIN_INSTANCE_ID: instanceId },
|
|
989
|
-
},
|
|
990
|
-
settings: {},
|
|
991
|
-
},
|
|
992
|
-
},
|
|
993
|
-
}, null, 2);
|
|
994
|
-
}
|
|
995
|
-
return JSON.stringify({
|
|
996
|
-
mcpServers: {
|
|
997
|
-
cachly: {
|
|
998
|
-
command: 'npx',
|
|
999
|
-
args: ['-y', '@cachly-dev/mcp-server@latest'],
|
|
1000
|
-
env: { CACHLY_API_URL: 'https://api.cachly.dev', CACHLY_JWT: apiKey, CACHLY_BRAIN_INSTANCE_ID: instanceId },
|
|
1001
|
-
},
|
|
1002
|
-
},
|
|
1003
|
-
}, null, 2);
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
|
-
// mergeMcpConfig reads an existing config file (if any), merges the cachly entry,
|
|
1007
|
-
// and returns the updated JSON string ā preserving all other MCP servers and settings.
|
|
1008
|
-
async function mergeMcpConfig(
|
|
1009
|
-
configPath: string,
|
|
1010
|
-
apiKey: string,
|
|
1011
|
-
instanceId: string,
|
|
1012
|
-
editor: string,
|
|
1013
|
-
fsOps: { readFile: (p: string, enc: BufferEncoding) => Promise<string>; existsSync: (p: string) => boolean },
|
|
1014
|
-
): Promise<string> {
|
|
1015
|
-
const cachlyEntry = {
|
|
1016
|
-
command: 'npx',
|
|
1017
|
-
args: ['-y', '@cachly-dev/mcp-server@latest'],
|
|
1018
|
-
env: { CACHLY_API_URL: 'https://api.cachly.dev', CACHLY_JWT: apiKey, CACHLY_BRAIN_INSTANCE_ID: instanceId },
|
|
1019
|
-
};
|
|
1020
|
-
|
|
1021
|
-
if (!fsOps.existsSync(configPath)) return buildMcpConfig(apiKey, instanceId, editor);
|
|
1022
|
-
|
|
1023
|
-
let existing: Record<string, unknown> = {};
|
|
1024
|
-
try {
|
|
1025
|
-
existing = JSON.parse(await fsOps.readFile(configPath, 'utf-8')) as Record<string, unknown>;
|
|
1026
|
-
} catch {
|
|
1027
|
-
// Corrupt or unreadable JSON ā back it up by appending .bak, then start fresh
|
|
1028
|
-
try {
|
|
1029
|
-
const { rename } = await import('node:fs/promises');
|
|
1030
|
-
await rename(configPath, configPath + '.bak');
|
|
1031
|
-
process.stderr.write(`ā ļø ${configPath} was not valid JSON ā backed up to ${configPath}.bak\n`);
|
|
1032
|
-
} catch { /* backup failed ā still safe to overwrite with valid config */ }
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
|
-
if (editor === 'continue') {
|
|
1036
|
-
// Merge into experimental.modelContextProtocolServers array ā replace cachly entry if present
|
|
1037
|
-
const exp = (existing['experimental'] ?? {}) as Record<string, unknown>;
|
|
1038
|
-
const servers = (exp['modelContextProtocolServers'] ?? []) as Array<Record<string, unknown>>;
|
|
1039
|
-
const filtered = servers.filter((s) => {
|
|
1040
|
-
const env = (s['env'] ?? {}) as Record<string, string>;
|
|
1041
|
-
return !env['CACHLY_JWT'] && !env['CACHLY_BRAIN_INSTANCE_ID'];
|
|
1042
|
-
});
|
|
1043
|
-
filtered.push({ transport: { type: 'stdio', command: 'npx', args: ['-y', '@cachly-dev/mcp-server@latest'] }, env: cachlyEntry.env });
|
|
1044
|
-
existing['experimental'] = { ...exp, modelContextProtocolServers: filtered };
|
|
1045
|
-
return JSON.stringify(existing, null, 2);
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
|
-
if (editor === 'zed') {
|
|
1049
|
-
// Merge only context_servers.cachly ā preserve all other Zed settings
|
|
1050
|
-
const cs = (existing['context_servers'] ?? {}) as Record<string, unknown>;
|
|
1051
|
-
cs['cachly'] = { command: { path: 'npx', args: ['-y', '@cachly-dev/mcp-server@latest'], env: cachlyEntry.env }, settings: {} };
|
|
1052
|
-
existing['context_servers'] = cs;
|
|
1053
|
-
return JSON.stringify(existing, null, 2);
|
|
1054
|
-
}
|
|
1055
|
-
|
|
1056
|
-
// Standard mcpServers format (Claude Code, Cursor, Windsurf, Copilot, Cline)
|
|
1057
|
-
const servers = (existing['mcpServers'] ?? {}) as Record<string, unknown>;
|
|
1058
|
-
servers['cachly'] = cachlyEntry;
|
|
1059
|
-
existing['mcpServers'] = servers;
|
|
1060
|
-
return JSON.stringify(existing, null, 2);
|
|
1061
|
-
}
|
|
1062
|
-
|
|
1063
|
-
function buildClaudeMdBlock(instanceId: string): string {
|
|
1064
|
-
return `${CLAUDE_MD_MARKER_START}
|
|
1065
|
-
## Cachly AI Brain ā Always Active
|
|
1066
|
-
|
|
1067
|
-
The Brain MCP server runs automatically. Instance: \`${instanceId}\`
|
|
1068
|
-
|
|
1069
|
-
---
|
|
1070
|
-
|
|
1071
|
-
### AT THE START of every new conversation (mandatory ā first thing, before anything else):
|
|
1072
|
-
\`\`\`
|
|
1073
|
-
session_start(instance_id="${instanceId}", workspace_path="<absolute path to this project>", focus="<first topic the user mentioned>")
|
|
1074
|
-
\`\`\`
|
|
1075
|
-
Show the full briefing output to the user. If the response says the brain is empty or has 0 lessons, immediately seed it:
|
|
1076
|
-
\`\`\`
|
|
1077
|
-
brain_from_git(instance_id="${instanceId}", repo_path="<workspace_path>", limit=100)
|
|
1078
|
-
\`\`\`
|
|
1079
|
-
Then tell the user: "Your brain just learned from your git history. You're now pre-briefed."
|
|
1080
|
-
|
|
1081
|
-
### BEFORE every task (mandatory ā no exceptions):
|
|
1082
|
-
\`\`\`
|
|
1083
|
-
smart_recall(instance_id="${instanceId}", query="<what you are about to do>")
|
|
1084
|
-
\`\`\`
|
|
1085
|
-
If relevant lessons are found, say: "Your brain knows this pattern ā [lesson]." Then proceed.
|
|
1086
|
-
|
|
1087
|
-
### BEFORE editing any file (mandatory):
|
|
1088
|
-
\`\`\`
|
|
1089
|
-
remember_context(instance_id="${instanceId}", key="wip:<filename>", content="EDITING: <file:line> ā <goal>", ttl=86400)
|
|
1090
|
-
\`\`\`
|
|
1091
|
-
When done: update content to \`"DONE: <file> ā <summary>"\`.
|
|
1092
|
-
|
|
1093
|
-
### AFTER every fix, deploy, or discovery (mandatory):
|
|
1094
|
-
\`\`\`
|
|
1095
|
-
learn_from_attempts(instance_id="${instanceId}", topic="<category:keyword>", outcome="success"|"failure",
|
|
1096
|
-
what_worked="...", what_failed="...", severity="critical"|"major"|"minor",
|
|
1097
|
-
file_paths=["..."], commands=["..."], tags=["..."])
|
|
1098
|
-
\`\`\`
|
|
1099
|
-
|
|
1100
|
-
### AT THE END of every conversation (mandatory):
|
|
1101
|
-
\`\`\`
|
|
1102
|
-
session_end(instance_id="${instanceId}", summary="<one sentence: what was done>", files_changed=["..."])
|
|
1103
|
-
\`\`\`
|
|
1104
|
-
|
|
1105
|
-
### WHEN debugging any error (mandatory before grepping/reading files):
|
|
1106
|
-
\`\`\`
|
|
1107
|
-
causal_trace(instance_id="${instanceId}", problem="<exact error or symptom>")
|
|
1108
|
-
\`\`\`
|
|
1109
|
-
If a known fix exists in the Brain, use it directly. Skip research if Brain has the answer.
|
|
1110
|
-
|
|
1111
|
-
### BEFORE any risky operation (deploy, migration, dependency upgrade):
|
|
1112
|
-
\`\`\`
|
|
1113
|
-
brain_predict(instance_id="${instanceId}", context="<what you are about to do>")
|
|
1114
|
-
\`\`\`
|
|
1115
|
-
Report predicted failure risks to the user before proceeding.
|
|
1116
|
-
|
|
1117
|
-
### Half-finished code rule:
|
|
1118
|
-
Never commit code that does not compile. Run \`tsc --noEmit\` / \`go build ./...\` before every commit.
|
|
1119
|
-
${CLAUDE_MD_MARKER_END}`;
|
|
1120
|
-
}
|
|
1121
|
-
|
|
1122
|
-
async function writeClaudeMd(projectDir: string, instanceId: string): Promise<'written' | 'updated' | 'appended'> {
|
|
1123
|
-
const { writeFile, appendFile, readFile } = await import('node:fs/promises');
|
|
1124
|
-
const { existsSync } = await import('node:fs');
|
|
1125
|
-
const { resolve } = await import('node:path');
|
|
1126
|
-
|
|
1127
|
-
const claudeMdPath = resolve(projectDir, 'CLAUDE.md');
|
|
1128
|
-
const block = '\n' + buildClaudeMdBlock(instanceId) + '\n';
|
|
1129
|
-
|
|
1130
|
-
if (existsSync(claudeMdPath)) {
|
|
1131
|
-
const existing = await readFile(claudeMdPath, 'utf-8');
|
|
1132
|
-
if (existing.includes(CLAUDE_MD_MARKER_START)) {
|
|
1133
|
-
// Idempotent update: replace existing block (new instance-id, refreshed content)
|
|
1134
|
-
const updated = existing.replace(
|
|
1135
|
-
new RegExp(`${CLAUDE_MD_MARKER_START}[\\s\\S]*?${CLAUDE_MD_MARKER_END}`),
|
|
1136
|
-
buildClaudeMdBlock(instanceId)
|
|
1137
|
-
);
|
|
1138
|
-
await writeFile(claudeMdPath, updated, 'utf-8');
|
|
1139
|
-
return 'updated';
|
|
1140
|
-
}
|
|
1141
|
-
await appendFile(claudeMdPath, block, 'utf-8');
|
|
1142
|
-
return 'appended';
|
|
1143
|
-
}
|
|
1144
|
-
await writeFile(claudeMdPath, block.trimStart(), 'utf-8');
|
|
1145
|
-
return 'written';
|
|
1146
|
-
}
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
// āā CLI: cachly digest āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
1151
|
-
// Usage: npx @cachly-dev/mcp-server@latest digest
|
|
1152
|
-
// Weekly brain summary ā what your AI learned this week. Shareable output.
|
|
1153
|
-
|
|
1154
|
-
if (process.argv[2] === 'digest') {
|
|
1155
|
-
const apiKey = process.env.CACHLY_JWT ?? '';
|
|
1156
|
-
const instanceId = process.env.CACHLY_BRAIN_INSTANCE_ID ?? '';
|
|
1157
|
-
|
|
1158
|
-
if (!apiKey || !instanceId) {
|
|
1159
|
-
console.log('\nā ļø CACHLY_JWT and CACHLY_BRAIN_INSTANCE_ID must be set.');
|
|
1160
|
-
console.log(' Run: npx @cachly-dev/mcp-server@latest setup\n');
|
|
1161
|
-
process.exit(1);
|
|
1162
|
-
}
|
|
1163
|
-
|
|
1164
|
-
process.stdout.write('\nš§ Fetching your weekly Brain digest...\n\n');
|
|
1165
|
-
|
|
1166
|
-
try {
|
|
1167
|
-
// Fetch current stats
|
|
1168
|
-
const statsRes = await fetch(`${API_URL}/api/v1/instances/${instanceId}/brain/stats`, {
|
|
1169
|
-
headers: { Authorization: `Bearer ${apiKey}` },
|
|
1170
|
-
signal: AbortSignal.timeout(8000),
|
|
1171
|
-
});
|
|
1172
|
-
if (!statsRes.ok) throw new Error(`stats HTTP ${statsRes.status}`);
|
|
1173
|
-
const stats = await statsRes.json() as {
|
|
1174
|
-
lesson_count?: number; context_count?: number; quality_score?: number;
|
|
1175
|
-
total_recall_count?: number; top_lessons?: Array<{ topic: string; recall_count: number; what_worked: string }>;
|
|
1176
|
-
team_authors?: string[];
|
|
1177
|
-
};
|
|
1178
|
-
|
|
1179
|
-
const lessons = stats.lesson_count ?? 0;
|
|
1180
|
-
const recalls = stats.total_recall_count ?? 0;
|
|
1181
|
-
const score = Math.round((stats.quality_score ?? 0) * 100);
|
|
1182
|
-
const topLessons = (stats.top_lessons ?? []).slice(0, 5);
|
|
1183
|
-
const teamAuthors = stats.team_authors ?? [];
|
|
1184
|
-
const tokensSaved = recalls * 1200;
|
|
1185
|
-
const costSaved = (tokensSaved * 0.000003).toFixed(2);
|
|
1186
|
-
|
|
1187
|
-
const level = lessons < 10 ? 'Junior Dev š§' : lessons < 30 ? 'Mid Dev ā”' :
|
|
1188
|
-
lessons < 60 ? 'Senior Dev š§ ' : lessons < 100 ? 'Staff Eng š' : 'Principal Eng š';
|
|
1189
|
-
|
|
1190
|
-
const now = new Date();
|
|
1191
|
-
const weekStart = new Date(now); weekStart.setDate(now.getDate() - 7);
|
|
1192
|
-
const fmt = (d: Date) => d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
|
1193
|
-
|
|
1194
|
-
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
1195
|
-
console.log(`ā \x1b[1mš§ Brain Weekly Digest\x1b[0m \x1b[90m${fmt(weekStart)} ā ${fmt(now)}\x1b[0m ā`);
|
|
1196
|
-
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤');
|
|
1197
|
-
console.log(`ā Total lessons : \x1b[33m${String(lessons).padEnd(8)}\x1b[0m Brain level : \x1b[32m${level.padEnd(18)}\x1b[0mā`);
|
|
1198
|
-
console.log(`ā Total recalls : \x1b[36m${String(recalls).padEnd(8)}\x1b[0m Quality score: \x1b[32m${String(score).padEnd(3)}%\x1b[0m ā`);
|
|
1199
|
-
console.log(`ā Tokens saved : \x1b[32m${String(Math.round(tokensSaved / 1000) + 'K').padEnd(8)}\x1b[0m Cost saved : \x1b[32m$${costSaved.padEnd(20)}\x1b[0mā`);
|
|
1200
|
-
|
|
1201
|
-
if (teamAuthors.length > 0) {
|
|
1202
|
-
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤');
|
|
1203
|
-
console.log(`ā \x1b[36mš„ Team contributors:\x1b[0m ${teamAuthors.slice(0, 5).join(', ').slice(0, 42).padEnd(42)} ā`);
|
|
1204
|
-
}
|
|
1205
|
-
|
|
1206
|
-
if (topLessons.length > 0) {
|
|
1207
|
-
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤');
|
|
1208
|
-
console.log('ā \x1b[33mš„ Most recalled lessons this week:\x1b[0m ā');
|
|
1209
|
-
for (const l of topLessons) {
|
|
1210
|
-
const line = `${l.topic}: ${l.what_worked}`.slice(0, 57);
|
|
1211
|
-
console.log(`ā \x1b[90m⢠${line.padEnd(58)}\x1b[0mā`);
|
|
1212
|
-
}
|
|
1213
|
-
}
|
|
1214
|
-
|
|
1215
|
-
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
1216
|
-
console.log('');
|
|
1217
|
-
|
|
1218
|
-
// āā Shareable tweet card āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
1219
|
-
const topLesson = topLessons[0];
|
|
1220
|
-
const tweetLines = [
|
|
1221
|
-
`š§ My AI Brain weekly digest (${fmt(weekStart)} ā ${fmt(now)}):`,
|
|
1222
|
-
``,
|
|
1223
|
-
` š ${lessons} lessons learned`,
|
|
1224
|
-
` š ${recalls} recalls Ā· ~${Math.round(tokensSaved / 1000)}K tokens saved`,
|
|
1225
|
-
` šÆ Brain Level: ${level}`,
|
|
1226
|
-
topLesson ? ` š„ Top lesson: "${topLesson.topic}: ${topLesson.what_worked.slice(0, 60)}${topLesson.what_worked.length > 60 ? 'ā¦' : ''}"` : '',
|
|
1227
|
-
``,
|
|
1228
|
-
`Built with @cachly_dev ā AI that actually remembers š`,
|
|
1229
|
-
`cachly.dev`,
|
|
1230
|
-
].filter(Boolean).join('\n');
|
|
1231
|
-
|
|
1232
|
-
const tweetUrl = 'https://twitter.com/intent/tweet?text=' + encodeURIComponent(tweetLines);
|
|
1233
|
-
|
|
1234
|
-
console.log('\x1b[1mš£ Share your digest:\x1b[0m');
|
|
1235
|
-
console.log('');
|
|
1236
|
-
console.log('\x1b[90m' + 'ā'.repeat(63) + '\x1b[0m');
|
|
1237
|
-
for (const line of tweetLines.split('\n')) {
|
|
1238
|
-
console.log(` ${line}`);
|
|
1239
|
-
}
|
|
1240
|
-
console.log('\x1b[90m' + 'ā'.repeat(63) + '\x1b[0m');
|
|
1241
|
-
console.log('');
|
|
1242
|
-
console.log(' \x1b[36mš¦ Tweet this:\x1b[0m');
|
|
1243
|
-
console.log(` \x1b[4m${tweetUrl.slice(0, 90)}...\x1b[0m`);
|
|
1244
|
-
console.log('');
|
|
1245
|
-
console.log(' \x1b[2mš” Invite your team: npx @cachly-dev/mcp-server@latest invite\x1b[0m');
|
|
1246
|
-
console.log(' \x1b[2m Cron: 0 9 * * 1 npx @cachly-dev/mcp-server@latest digest\x1b[0m');
|
|
1247
|
-
console.log('');
|
|
1248
|
-
} catch (e) {
|
|
1249
|
-
console.log(`\nā Could not fetch digest: ${(e as Error).message}\n`);
|
|
1250
|
-
process.exit(1);
|
|
1251
|
-
}
|
|
1252
|
-
process.exit(0);
|
|
1253
|
-
}
|
|
1254
|
-
|
|
1255
|
-
// āā CLI: cachly invite āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
1256
|
-
// Usage: npx @cachly-dev/mcp-server@latest invite [email]
|
|
1257
|
-
// Invites a teammate to share your Brain. Fastest referral loop.
|
|
1258
|
-
|
|
1259
|
-
if (process.argv[2] === 'invite') {
|
|
1260
|
-
// āā invite: fetch the user's unique referral link and show shareable messages āā
|
|
1261
|
-
const apiKey = process.env.CACHLY_JWT ?? '';
|
|
1262
|
-
|
|
1263
|
-
if (!apiKey) {
|
|
1264
|
-
console.log('\nā ļø CACHLY_JWT must be set.');
|
|
1265
|
-
console.log(' Run: npx @cachly-dev/mcp-server@latest setup\n');
|
|
1266
|
-
process.exit(1);
|
|
1267
|
-
}
|
|
1268
|
-
|
|
1269
|
-
try {
|
|
1270
|
-
const res = await fetch(`${API_URL}/api/v1/referral/me`, {
|
|
1271
|
-
headers: { Authorization: `Bearer ${apiKey}` },
|
|
1272
|
-
signal: AbortSignal.timeout(8000),
|
|
1273
|
-
});
|
|
1274
|
-
|
|
1275
|
-
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
1276
|
-
const data = await res.json() as { code?: string; url?: string; referral_count?: number };
|
|
1277
|
-
const referralUrl = data.url ?? `https://cachly.dev/r/${data.code}`;
|
|
1278
|
-
const count = data.referral_count ?? 0;
|
|
1279
|
-
|
|
1280
|
-
const slackMsg = `Hey, I've been using cachly to give my AI persistent memory across sessions ā no more re-explaining my stack every morning.\n\nYou get a free Brain: ${referralUrl}`;
|
|
1281
|
-
const tweetText = encodeURIComponent(`I gave my AI persistent memory. It remembers fixes, patterns, and context across every session ā no more re-explaining.\n\nTry it free: ${referralUrl}\n\n@cachlydev`);
|
|
1282
|
-
|
|
1283
|
-
console.log('');
|
|
1284
|
-
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
1285
|
-
console.log('ā š§ Your cachly Brain invite link ā');
|
|
1286
|
-
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤');
|
|
1287
|
-
console.log(`ā ${referralUrl.padEnd(61)}ā`);
|
|
1288
|
-
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤');
|
|
1289
|
-
if (count > 0) {
|
|
1290
|
-
console.log(`ā \x1b[32mā ${count} developer${count === 1 ? '' : 's'} joined via your link so far\x1b[0m`.padEnd(72) + 'ā');
|
|
1291
|
-
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤');
|
|
1292
|
-
}
|
|
1293
|
-
console.log('ā \x1b[33mSlack / DM message:\x1b[0m ā');
|
|
1294
|
-
console.log('ā ā');
|
|
1295
|
-
for (const line of slackMsg.split('\n')) {
|
|
1296
|
-
console.log(`ā \x1b[90m${line.slice(0, 58).padEnd(58)}\x1b[0m ā`);
|
|
1297
|
-
}
|
|
1298
|
-
console.log('ā ā');
|
|
1299
|
-
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤');
|
|
1300
|
-
console.log(`ā \x1b[36mš Tweet:\x1b[0m https://twitter.com/intent/tweet?text=${tweetText.slice(0, 10)}... ā`);
|
|
1301
|
-
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
1302
|
-
console.log('');
|
|
1303
|
-
console.log('\x1b[2m Each developer who signs up via your link earns you 1 month Pro.\x1b[0m');
|
|
1304
|
-
console.log('');
|
|
1305
|
-
} catch (e) {
|
|
1306
|
-
console.log(`\nā Could not fetch invite link: ${(e as Error).message}\n`);
|
|
1307
|
-
process.exit(1);
|
|
1308
|
-
}
|
|
1309
|
-
process.exit(0);
|
|
1310
|
-
}
|
|
1311
|
-
|
|
1312
|
-
// āā CLI: cachly join <token> āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
1313
|
-
// Usage: npx @cachly-dev/mcp-server@latest join <token>
|
|
1314
|
-
// Accepts a Brain invite link ā writes the shared instance ID into all
|
|
1315
|
-
// detected editor MCP configs and the global ~/.claude/mcp.json.
|
|
1316
|
-
// No account needed if CACHLY_JWT is already set; otherwise prompts for auth.
|
|
1317
|
-
|
|
1318
|
-
if (process.argv[2] === 'join') {
|
|
1319
|
-
const { writeFile: wfJ, readFile: rfJ, mkdir: mkJ } = await import('node:fs/promises');
|
|
1320
|
-
const { existsSync: exJ } = await import('node:fs');
|
|
1321
|
-
const { resolve: resJ, dirname: dirJ } = await import('node:path');
|
|
1322
|
-
const { homedir } = await import('node:os');
|
|
1323
|
-
const { createInterface: ciJ } = await import('node:readline');
|
|
1324
|
-
|
|
1325
|
-
const token = process.argv[3] ?? '';
|
|
1326
|
-
if (!token) {
|
|
1327
|
-
console.log('\nā Usage: npx @cachly-dev/mcp-server@latest join <token>\n');
|
|
1328
|
-
console.log(' Get a token from a teammate: npx @cachly-dev/mcp-server@latest invite\n');
|
|
1329
|
-
process.exit(1);
|
|
1330
|
-
}
|
|
1331
|
-
|
|
1332
|
-
console.log('\nā³ Looking up invite...');
|
|
1333
|
-
|
|
1334
|
-
// āā Step 1: Fetch invite info (public, no auth) āāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
1335
|
-
let inviteInfo: { instance_id: string; instance_name: string; tier: string; expires_at: string; label?: string };
|
|
1336
|
-
try {
|
|
1337
|
-
const res = await fetch(`${API_URL}/api/invite/${token}`, { signal: AbortSignal.timeout(8000) });
|
|
1338
|
-
if (res.status === 404) { console.log('\nā Invite not found ā the link may be invalid.\n'); process.exit(1); }
|
|
1339
|
-
if (res.status === 410) { console.log('\nā This invite link has expired (7-day limit).\n'); process.exit(1); }
|
|
1340
|
-
if (!res.ok) { console.log(`\nā Could not fetch invite: HTTP ${res.status}\n`); process.exit(1); }
|
|
1341
|
-
inviteInfo = await res.json() as typeof inviteInfo;
|
|
1342
|
-
} catch (e) {
|
|
1343
|
-
console.log(`\nā Network error: ${(e as Error).message}\n`);
|
|
1344
|
-
process.exit(1);
|
|
1345
|
-
}
|
|
1346
|
-
|
|
1347
|
-
const { instance_id, instance_name, tier, label } = inviteInfo;
|
|
1348
|
-
const expires = new Date(inviteInfo.expires_at).toLocaleDateString();
|
|
1349
|
-
|
|
1350
|
-
console.log('');
|
|
1351
|
-
console.log(' \x1b[35māāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\x1b[0m');
|
|
1352
|
-
console.log(' \x1b[35mā\x1b[0m \x1b[1mš§ Brain Invite\x1b[0m \x1b[35mā\x1b[0m');
|
|
1353
|
-
console.log(' \x1b[35māāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\x1b[0m');
|
|
1354
|
-
console.log('');
|
|
1355
|
-
console.log(` Brain : \x1b[1m${instance_name}\x1b[0m`);
|
|
1356
|
-
console.log(` Tier : ${tier}`);
|
|
1357
|
-
if (label) console.log(` Note : ${label}`);
|
|
1358
|
-
console.log(` Expires : ${expires}`);
|
|
1359
|
-
console.log('');
|
|
1360
|
-
|
|
1361
|
-
// āā Step 2: Confirm āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
1362
|
-
const rlJ = ciJ({ input: process.stdin, output: process.stdout });
|
|
1363
|
-
const confirmJ = await new Promise<string>((resolve) => {
|
|
1364
|
-
rlJ.question(' Join this Brain? [Y/n] ', (a) => { rlJ.close(); resolve(a.trim().toLowerCase() || 'y'); });
|
|
1365
|
-
});
|
|
1366
|
-
if (confirmJ !== 'y' && confirmJ !== 'yes') {
|
|
1367
|
-
console.log('\n Cancelled.\n');
|
|
1368
|
-
process.exit(0);
|
|
1369
|
-
}
|
|
1370
|
-
|
|
1371
|
-
// āā Step 3: Ensure JWT āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
1372
|
-
let joinJwt = JWT;
|
|
1373
|
-
if (!joinJwt) {
|
|
1374
|
-
console.log('\n No API key found ā starting sign-in (10 seconds)...\n');
|
|
1375
|
-
try {
|
|
1376
|
-
const dcRes = await fetch(`${API_URL}/api/v1/auth/device/code`, {
|
|
1377
|
-
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
1378
|
-
body: JSON.stringify({ client_id: 'cachly-mcp-cli' }),
|
|
1379
|
-
signal: AbortSignal.timeout(10000),
|
|
1380
|
-
});
|
|
1381
|
-
if (dcRes.ok) {
|
|
1382
|
-
const dc = await dcRes.json() as { verification_uri: string; user_code: string; device_code: string; interval: number };
|
|
1383
|
-
console.log(` š Open: \x1b[36m${dc.verification_uri}\x1b[0m`);
|
|
1384
|
-
console.log(` Code: \x1b[1m${dc.user_code}\x1b[0m\n`);
|
|
1385
|
-
// Poll for token
|
|
1386
|
-
const pollInterval = (dc.interval ?? 5) * 1000;
|
|
1387
|
-
for (let i = 0; i < 24; i++) {
|
|
1388
|
-
await new Promise(r => setTimeout(r, pollInterval));
|
|
1389
|
-
try {
|
|
1390
|
-
const tokRes = await fetch(`${API_URL}/api/v1/auth/device/token`, {
|
|
1391
|
-
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
1392
|
-
body: JSON.stringify({ device_code: dc.device_code, client_id: 'cachly-mcp-cli' }),
|
|
1393
|
-
signal: AbortSignal.timeout(8000),
|
|
1394
|
-
});
|
|
1395
|
-
if (tokRes.ok) {
|
|
1396
|
-
const tok = await tokRes.json() as { access_token?: string };
|
|
1397
|
-
if (tok.access_token) { joinJwt = tok.access_token; break; }
|
|
1398
|
-
}
|
|
1399
|
-
} catch { /* polling ā keep going */ }
|
|
1400
|
-
}
|
|
1401
|
-
if (!joinJwt) { console.log('\nā Sign-in timed out. Run the command again.\n'); process.exit(1); }
|
|
1402
|
-
console.log(' ā
Signed in!\n');
|
|
1403
|
-
}
|
|
1404
|
-
} catch (e) {
|
|
1405
|
-
console.log(`\n ā ļø Could not start sign-in: ${(e as Error).message}`);
|
|
1406
|
-
console.log(` Set CACHLY_JWT manually from https://cachly.dev/setup-ai\n`);
|
|
1407
|
-
process.exit(1);
|
|
1408
|
-
}
|
|
1409
|
-
}
|
|
1410
|
-
|
|
1411
|
-
// āā Step 4: Write instance ID into all editor MCP configs āāāāāāāāāāāāāāāā
|
|
1412
|
-
const cwd = process.cwd();
|
|
1413
|
-
const home = homedir();
|
|
1414
|
-
|
|
1415
|
-
const editorFiles: Array<{ label: string; path: string; global?: boolean }> = [
|
|
1416
|
-
{ label: '.mcp.json (Claude Code project)', path: resJ(cwd, '.mcp.json') },
|
|
1417
|
-
{ label: '.cursor/mcp.json', path: resJ(cwd, '.cursor', 'mcp.json') },
|
|
1418
|
-
{ label: '.windsurf/mcp.json', path: resJ(cwd, '.windsurf', 'mcp.json') },
|
|
1419
|
-
{ label: '.vscode/mcp.json (Copilot/Cline)', path: resJ(cwd, '.vscode', 'mcp.json') },
|
|
1420
|
-
{ label: '~/.claude/mcp.json (global)', path: resJ(home, '.claude', 'mcp.json'), global: true },
|
|
1421
|
-
];
|
|
1422
|
-
|
|
1423
|
-
let written = 0;
|
|
1424
|
-
for (const { label: eLabel, path: ePath, global: isGlobal } of editorFiles) {
|
|
1425
|
-
// Only write project configs if the directory exists (or global)
|
|
1426
|
-
const dir = dirJ(ePath);
|
|
1427
|
-
if (!isGlobal && !exJ(dir) && !exJ(ePath)) continue;
|
|
1428
|
-
try {
|
|
1429
|
-
await mkJ(dir, { recursive: true });
|
|
1430
|
-
let cfg: Record<string, unknown> = {};
|
|
1431
|
-
if (exJ(ePath)) {
|
|
1432
|
-
try {
|
|
1433
|
-
cfg = JSON.parse(await rfJ(ePath, 'utf-8')) as Record<string, unknown>;
|
|
1434
|
-
} catch {
|
|
1435
|
-
try { await (await import('node:fs/promises')).rename(ePath, ePath + '.bak'); } catch { /* backup failed */ }
|
|
1436
|
-
}
|
|
1437
|
-
}
|
|
1438
|
-
// Update or create the cachly entry with the shared instance ID
|
|
1439
|
-
const servers = (cfg['mcpServers'] ?? {}) as Record<string, unknown>;
|
|
1440
|
-
const existing = (servers['cachly'] ?? {}) as Record<string, unknown>;
|
|
1441
|
-
const env = (existing['env'] ?? {}) as Record<string, string>;
|
|
1442
|
-
env['CACHLY_BRAIN_INSTANCE_ID'] = instance_id;
|
|
1443
|
-
if (joinJwt && !env['CACHLY_JWT']) env['CACHLY_JWT'] = joinJwt;
|
|
1444
|
-
env['CACHLY_API_URL'] ??= 'https://api.cachly.dev';
|
|
1445
|
-
servers['cachly'] = { ...existing, command: 'npx', args: ['-y', '@cachly-dev/mcp-server@latest'], env };
|
|
1446
|
-
cfg['mcpServers'] = servers;
|
|
1447
|
-
await wfJ(ePath, JSON.stringify(cfg, null, 2), 'utf-8');
|
|
1448
|
-
console.log(` ā
${eLabel}`);
|
|
1449
|
-
written++;
|
|
1450
|
-
} catch (joinWriteErr) {
|
|
1451
|
-
// Only surface unexpected errors ā missing editor dirs are expected and silently skipped above
|
|
1452
|
-
if ((joinWriteErr as NodeJS.ErrnoException).code !== 'ENOENT') {
|
|
1453
|
-
console.log(` ā ļø ${eLabel}: ${(joinWriteErr as Error).message}`);
|
|
1454
|
-
}
|
|
1455
|
-
}
|
|
1456
|
-
}
|
|
1457
|
-
|
|
1458
|
-
console.log('');
|
|
1459
|
-
if (written === 0) {
|
|
1460
|
-
console.log(' ā ļø No editor configs found ā run `cachly setup` first.\n');
|
|
1461
|
-
} else {
|
|
1462
|
-
console.log(` š§ You're now sharing Brain: \x1b[1m${instance_name}\x1b[0m`);
|
|
1463
|
-
console.log(' Restart your editor ā your AI arrives pre-briefed from the team Brain.\n');
|
|
1464
|
-
console.log(' š Add the badge to your README:');
|
|
1465
|
-
console.log(` npx @cachly-dev/mcp-server@latest badge\n`);
|
|
1466
|
-
}
|
|
1467
|
-
process.exit(0);
|
|
1468
|
-
}
|
|
1469
|
-
|
|
1470
|
-
// āā CLI: cachly upgrade āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
1471
|
-
// Usage: npx @cachly-dev/mcp-server@latest upgrade
|
|
1472
|
-
// Checks npm for the latest version and prints upgrade instructions if outdated.
|
|
1473
|
-
|
|
1474
|
-
if (process.argv[2] === 'upgrade') {
|
|
1475
|
-
process.stdout.write('\nā³ Checking for updates...\n');
|
|
1476
|
-
try {
|
|
1477
|
-
const res = await fetch('https://registry.npmjs.org/@cachly-dev/mcp-server/latest', {
|
|
1478
|
-
signal: AbortSignal.timeout(8000),
|
|
1479
|
-
});
|
|
1480
|
-
if (!res.ok) throw new Error(`registry HTTP ${res.status}`);
|
|
1481
|
-
const pkg = await res.json() as { version?: string };
|
|
1482
|
-
const latest = pkg.version;
|
|
1483
|
-
if (!latest || !/^\d+\.\d+\.\d+/.test(latest)) throw new Error('npm registry returned unexpected response');
|
|
1484
|
-
|
|
1485
|
-
if (latest === CURRENT_VERSION) {
|
|
1486
|
-
console.log(`\nā
Already on the latest version: \x1b[32mv${CURRENT_VERSION}\x1b[0m\n`);
|
|
1487
|
-
} else {
|
|
1488
|
-
console.log('');
|
|
1489
|
-
console.log('\x1b[35m āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\x1b[0m');
|
|
1490
|
-
console.log('\x1b[35m ā\x1b[0m \x1b[1mš§ cachly update available\x1b[0m \x1b[35mā\x1b[0m');
|
|
1491
|
-
console.log('\x1b[35m āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\x1b[0m');
|
|
1492
|
-
console.log('');
|
|
1493
|
-
console.log(` Current : \x1b[31mv${CURRENT_VERSION}\x1b[0m`);
|
|
1494
|
-
console.log(` Latest : \x1b[32mv${latest}\x1b[0m`);
|
|
1495
|
-
console.log('');
|
|
1496
|
-
console.log(' Update your editor configs to pick up the new version:');
|
|
1497
|
-
console.log('');
|
|
1498
|
-
console.log(' \x1b[32m npx @cachly-dev/mcp-server@latest setup\x1b[0m');
|
|
1499
|
-
console.log('');
|
|
1500
|
-
console.log(' \x1b[2m(npx always fetches the latest when @latest is specified)\x1b[0m');
|
|
1501
|
-
console.log('');
|
|
1502
|
-
}
|
|
1503
|
-
} catch (e) {
|
|
1504
|
-
console.log(`\nā Could not check for updates: ${(e as Error).message}\n`);
|
|
1505
|
-
process.exit(1);
|
|
1506
|
-
}
|
|
1507
|
-
process.exit(0);
|
|
1508
|
-
}
|
|
1509
|
-
|
|
1510
|
-
// āā CLI: cachly demo āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
1511
|
-
// Usage: npx @cachly-dev/mcp-server@latest demo
|
|
1512
|
-
// Zero-signup. Reads local git history and shows what the AI Brain would know.
|
|
1513
|
-
// The fastest path from "what is this?" to "I need this".
|
|
1514
|
-
|
|
1515
|
-
if (process.argv[2] === 'demo') {
|
|
1516
|
-
const { execSync } = await import('node:child_process');
|
|
1517
|
-
const { existsSync, readFileSync } = await import('node:fs');
|
|
1518
|
-
const { resolve, basename } = await import('node:path');
|
|
1519
|
-
const cwd = process.cwd();
|
|
1520
|
-
|
|
1521
|
-
console.log('\n\x1b[35mš§ cachly Brain ā Live Demo\x1b[0m');
|
|
1522
|
-
console.log('\x1b[90mNo account needed. Reading your git history...\x1b[0m\n');
|
|
1523
|
-
|
|
1524
|
-
// Verify git repo
|
|
1525
|
-
if (!existsSync(resolve(cwd, '.git'))) {
|
|
1526
|
-
console.log('ā ļø No git repository found here.');
|
|
1527
|
-
console.log(' Run this command inside a project directory with git history.\n');
|
|
1528
|
-
console.log(' Example: cd ~/my-project && npx @cachly-dev/mcp-server@latest demo\n');
|
|
1529
|
-
process.exit(0);
|
|
1530
|
-
}
|
|
1531
|
-
|
|
1532
|
-
// Get developer name from git config
|
|
1533
|
-
let devName = '';
|
|
1534
|
-
try {
|
|
1535
|
-
devName = execSync('git config user.name', { cwd, encoding: 'utf-8', stdio: 'pipe' }).trim().split(' ')[0] ?? '';
|
|
1536
|
-
} catch { /* ignore */ }
|
|
1537
|
-
|
|
1538
|
-
// Get project name from package.json or directory name
|
|
1539
|
-
let projectName = basename(cwd);
|
|
1540
|
-
try {
|
|
1541
|
-
const pkg = JSON.parse(readFileSync(resolve(cwd, 'package.json'), 'utf-8')) as { name?: string };
|
|
1542
|
-
if (pkg.name) projectName = pkg.name.replace(/^@[^/]+\//, '');
|
|
1543
|
-
} catch { /* ignore */ }
|
|
1544
|
-
|
|
1545
|
-
// Read git log
|
|
1546
|
-
let logOutput = '';
|
|
1547
|
-
try {
|
|
1548
|
-
logOutput = execSync(
|
|
1549
|
-
'git log HEAD --pretty=format:"%H|||%s|||%ad|||%an" --date=short --no-merges -n 200',
|
|
1550
|
-
{ cwd, encoding: 'utf-8', stdio: 'pipe' }
|
|
1551
|
-
);
|
|
1552
|
-
} catch {
|
|
1553
|
-
console.log('ā ļø Could not read git log. Make sure you have commits.\n');
|
|
1554
|
-
process.exit(0);
|
|
1555
|
-
}
|
|
1556
|
-
|
|
1557
|
-
const commits = logOutput.trim().split('\n').filter(Boolean).map(line => {
|
|
1558
|
-
const [sha, subject, date, author] = line.split('|||');
|
|
1559
|
-
return { sha: (sha ?? '').trim().slice(0, 8), subject: (subject ?? '').trim(), date: (date ?? '').trim(), author: (author ?? '').trim() };
|
|
1560
|
-
});
|
|
1561
|
-
|
|
1562
|
-
if (commits.length === 0) {
|
|
1563
|
-
console.log('ā ļø No commits found.\n');
|
|
1564
|
-
process.exit(0);
|
|
1565
|
-
}
|
|
1566
|
-
|
|
1567
|
-
// Classify commits (same logic as brain_from_git)
|
|
1568
|
-
const classify = (s: string) => {
|
|
1569
|
-
const t = s.toLowerCase();
|
|
1570
|
-
if (/\b(fix|fixed|bug|hotfix|patch|revert|resolve)\b/.test(t)) return 'fix';
|
|
1571
|
-
if (/\b(feat|feature|add|implement|new|introduce)\b/.test(t)) return 'feat';
|
|
1572
|
-
if (/\b(perf|optim|speed|cache|latency)\b/.test(t)) return 'perf';
|
|
1573
|
-
if (/\b(security|cve|auth|csrf|xss|inject|sanitize)\b/.test(t)) return 'security';
|
|
1574
|
-
if (/\b(deploy|ci|cd|docker|k8s|infra)\b/.test(t)) return 'deploy';
|
|
1575
|
-
if (/\b(refactor|clean|simplify|extract)\b/.test(t)) return 'refactor';
|
|
1576
|
-
return 'chore';
|
|
1577
|
-
};
|
|
1578
|
-
|
|
1579
|
-
// Count categories
|
|
1580
|
-
const cats = new Map<string, number>();
|
|
1581
|
-
const fixes: string[] = [];
|
|
1582
|
-
const feats: string[] = [];
|
|
1583
|
-
const security: string[] = [];
|
|
1584
|
-
const authors = new Set<string>();
|
|
1585
|
-
|
|
1586
|
-
for (const c of commits) {
|
|
1587
|
-
const cat = classify(c.subject);
|
|
1588
|
-
cats.set(cat, (cats.get(cat) ?? 0) + 1);
|
|
1589
|
-
if (cat === 'fix' && fixes.length < 5) fixes.push(c.subject.slice(0, 72));
|
|
1590
|
-
if (cat === 'feat' && feats.length < 5) feats.push(c.subject.slice(0, 72));
|
|
1591
|
-
if (cat === 'security' && security.length < 3) security.push(c.subject.slice(0, 72));
|
|
1592
|
-
if (c.author) authors.add(c.author.split(' ')[0]!);
|
|
1593
|
-
}
|
|
1594
|
-
|
|
1595
|
-
const totalLessons = commits.length - (cats.get('chore') ?? 0);
|
|
1596
|
-
const dateRange = `${commits[commits.length - 1]?.date ?? '?'} ā ${commits[0]?.date ?? '?'}`;
|
|
1597
|
-
|
|
1598
|
-
// Estimate time wasted re-explaining (45 min/day * workdays since first commit)
|
|
1599
|
-
const firstDate = commits[commits.length - 1]?.date;
|
|
1600
|
-
let daysActive = 0;
|
|
1601
|
-
if (firstDate) {
|
|
1602
|
-
const ms = Date.now() - new Date(firstDate).getTime();
|
|
1603
|
-
daysActive = Math.max(1, Math.round(ms / (1000 * 60 * 60 * 24) * 5 / 7)); // workdays
|
|
1604
|
-
}
|
|
1605
|
-
const hoursWasted = Math.round(daysActive * 0.75); // 45 min/day
|
|
1606
|
-
|
|
1607
|
-
// Brain Level based on total lessons (matches Go ComputeBrainLevel thresholds)
|
|
1608
|
-
const brainLevelName = totalLessons >= 501 ? 'Oracle' : totalLessons >= 201 ? 'Architect' : totalLessons >= 51 ? 'Expert' : totalLessons >= 11 ? 'Explorer' : 'Apprentice';
|
|
1609
|
-
|
|
1610
|
-
// Personalized header
|
|
1611
|
-
const greeting = devName ? `Hey ${devName} ā ` : '';
|
|
1612
|
-
const headerTitle = `${greeting}here's what your AI would know about ${projectName}`;
|
|
1613
|
-
|
|
1614
|
-
// Display the demo
|
|
1615
|
-
process.stdout.write('\x1b[2K\r');
|
|
1616
|
-
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
1617
|
-
console.log(`ā \x1b[1mš§ ${headerTitle.slice(0, 58).padEnd(58)}\x1b[0mā`);
|
|
1618
|
-
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤');
|
|
1619
|
-
console.log(`ā Commits analysed : \x1b[33m${String(commits.length).padEnd(6)}\x1b[0m Date range: \x1b[90m${dateRange.slice(0, 23).padEnd(23)}\x1b[0m ā`);
|
|
1620
|
-
console.log(`ā Lessons extracted: \x1b[32m${String(totalLessons).padEnd(6)}\x1b[0m Contributors: \x1b[36m${String(authors.size).padEnd(20)}\x1b[0mā`);
|
|
1621
|
-
console.log(`ā Brain Level : \x1b[35m${brainLevelName.padEnd(14)}\x1b[0m Time wasted re-explaining: \x1b[31m${String(hoursWasted + 'h').padEnd(5)}\x1b[0mā`);
|
|
1622
|
-
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤');
|
|
1623
|
-
|
|
1624
|
-
// Category breakdown
|
|
1625
|
-
const sorted = [...cats.entries()].filter(([k]) => k !== 'chore').sort((a, b) => b[1] - a[1]);
|
|
1626
|
-
const bar = (n: number, max: number) => 'ā'.repeat(Math.round((n / max) * 20)).padEnd(20);
|
|
1627
|
-
const maxVal = Math.max(...sorted.map(([, v]) => v), 1);
|
|
1628
|
-
console.log('ā \x1b[90mCategory breakdown:\x1b[0m ā');
|
|
1629
|
-
for (const [cat, count] of sorted) {
|
|
1630
|
-
const emoji = { fix: 'š§', feat: 'āØ', perf: 'ā”', security: 'š', deploy: 'š', refactor: 'š' }[cat] ?? 'ā¢';
|
|
1631
|
-
console.log(`ā ${emoji} \x1b[36m${cat.padEnd(10)}\x1b[0m \x1b[35m${bar(count, maxVal)}\x1b[0m \x1b[33m${String(count).padStart(3)}\x1b[0m ā`);
|
|
1632
|
-
}
|
|
1633
|
-
|
|
1634
|
-
if (security.length > 0) {
|
|
1635
|
-
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤');
|
|
1636
|
-
console.log('ā \x1b[31mš Security fixes your AI would know:\x1b[0m ā');
|
|
1637
|
-
for (const s of security) console.log(`ā \x1b[90m⢠${s.slice(0, 58).padEnd(58)}\x1b[0m ā`);
|
|
1638
|
-
}
|
|
1639
|
-
|
|
1640
|
-
if (fixes.length > 0) {
|
|
1641
|
-
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤');
|
|
1642
|
-
console.log('ā \x1b[33mš§ Bug fixes your AI would never repeat:\x1b[0m ā');
|
|
1643
|
-
for (const f of fixes.slice(0, 4)) console.log(`ā \x1b[90m⢠${f.slice(0, 58).padEnd(58)}\x1b[0m ā`);
|
|
1644
|
-
}
|
|
1645
|
-
|
|
1646
|
-
if (feats.length > 0) {
|
|
1647
|
-
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤');
|
|
1648
|
-
console.log('ā \x1b[32m⨠Features & decisions it would recall:\x1b[0m ā');
|
|
1649
|
-
for (const f of feats.slice(0, 3)) console.log(`ā \x1b[90m⢠${f.slice(0, 58).padEnd(58)}\x1b[0m ā`);
|
|
1650
|
-
}
|
|
1651
|
-
|
|
1652
|
-
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤');
|
|
1653
|
-
console.log('ā \x1b[32mš” With cachly, your AI arrives pre-briefed every session.\x1b[0m ā');
|
|
1654
|
-
console.log('ā \x1b[32m No more re-explaining. No more repeated mistakes.\x1b[0m ā');
|
|
1655
|
-
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
1656
|
-
console.log('');
|
|
1657
|
-
// Generate shareable preview URL with encoded stats
|
|
1658
|
-
const previewParams = new URLSearchParams({
|
|
1659
|
-
repo: projectName,
|
|
1660
|
-
commits: String(commits.length),
|
|
1661
|
-
lessons: String(totalLessons),
|
|
1662
|
-
level: brainLevelName,
|
|
1663
|
-
authors: String(authors.size),
|
|
1664
|
-
hours: String(hoursWasted),
|
|
1665
|
-
});
|
|
1666
|
-
const previewURL = `https://cachly.dev/preview?${previewParams.toString()}`;
|
|
1667
|
-
|
|
1668
|
-
console.log(' \x1b[1mMake this permanent (free, 1ā5 minutes):\x1b[0m');
|
|
1669
|
-
console.log(' \x1b[32m$ npx @cachly-dev/mcp-server@latest setup\x1b[0m');
|
|
1670
|
-
console.log('');
|
|
1671
|
-
console.log(` \x1b[90mš Shareable preview:\x1b[0m \x1b[36m${previewURL}\x1b[0m`);
|
|
1672
|
-
console.log('');
|
|
1673
|
-
console.log(' Works with: Claude Code Ā· Cursor Ā· Windsurf Ā· Copilot Ā· Cline Ā· Zed');
|
|
1674
|
-
console.log(' Free forever Ā· GDPR Ā· German servers Ā· No credit card');
|
|
1675
|
-
console.log('');
|
|
1676
|
-
process.exit(0);
|
|
1677
|
-
}
|
|
1678
|
-
|
|
1679
|
-
// āā CLI: cachly share āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
1680
|
-
// Usage: npx @cachly-dev/mcp-server@latest share
|
|
1681
|
-
// Generates a shareable ASCII card + tweet text with your Brain stats.
|
|
1682
|
-
|
|
1683
|
-
if (process.argv[2] === 'share') {
|
|
1684
|
-
const apiKey = process.env.CACHLY_JWT ?? '';
|
|
1685
|
-
const instanceId = process.env.CACHLY_BRAIN_INSTANCE_ID ?? '';
|
|
1686
|
-
|
|
1687
|
-
if (!apiKey || !instanceId) {
|
|
1688
|
-
console.log('\nā ļø CACHLY_JWT and CACHLY_BRAIN_INSTANCE_ID must be set.');
|
|
1689
|
-
console.log(' Run: npx @cachly-dev/mcp-server@latest setup (takes 1ā5 minutes)\n');
|
|
1690
|
-
process.exit(1);
|
|
1691
|
-
}
|
|
1692
|
-
|
|
1693
|
-
process.stdout.write('\nš§ Fetching your Brain stats...\n');
|
|
1694
|
-
|
|
1695
|
-
try {
|
|
1696
|
-
const statsRes = await fetch(`${API_URL}/api/v1/instances/${instanceId}/brain/stats`, {
|
|
1697
|
-
headers: { Authorization: `Bearer ${apiKey}` },
|
|
1698
|
-
signal: AbortSignal.timeout(8000),
|
|
1699
|
-
});
|
|
1700
|
-
|
|
1701
|
-
if (!statsRes.ok) throw new Error(`HTTP ${statsRes.status}`);
|
|
1702
|
-
const stats = await statsRes.json() as {
|
|
1703
|
-
lesson_count?: number; context_count?: number;
|
|
1704
|
-
quality_score?: number; total_recall_count?: number;
|
|
1705
|
-
};
|
|
1706
|
-
|
|
1707
|
-
const lessons = stats.lesson_count ?? 0;
|
|
1708
|
-
const recalls = stats.total_recall_count ?? 0;
|
|
1709
|
-
const score = Math.round((stats.quality_score ?? 0) * 100);
|
|
1710
|
-
const tokensSaved = recalls * 1200;
|
|
1711
|
-
const costSaved = (tokensSaved * 0.000003).toFixed(2);
|
|
1712
|
-
|
|
1713
|
-
const level = lessons === 0 ? 'Intern š±' :
|
|
1714
|
-
lessons < 10 ? 'Junior Dev š§' :
|
|
1715
|
-
lessons < 30 ? 'Mid Dev ā”' :
|
|
1716
|
-
lessons < 60 ? 'Senior Dev š§ ' :
|
|
1717
|
-
lessons < 100 ? 'Staff Eng š' : 'Principal Eng š';
|
|
1718
|
-
|
|
1719
|
-
console.log('');
|
|
1720
|
-
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
1721
|
-
console.log('ā š§ My AI Brain Ā· powered by \x1b[35mcachly.dev\x1b[0m ā');
|
|
1722
|
-
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤');
|
|
1723
|
-
console.log(`ā Lessons stored : \x1b[33m${String(lessons).padEnd(8)}\x1b[0m Brain level: \x1b[32m${level.padEnd(18)}\x1b[0mā`);
|
|
1724
|
-
console.log(`ā Total recalls : \x1b[36m${String(recalls).padEnd(8)}\x1b[0m Quality score: \x1b[32m${String(score).padEnd(3)}%\x1b[0m ā`);
|
|
1725
|
-
console.log(`ā Tokens saved : \x1b[32m${String((tokensSaved / 1000).toFixed(0) + 'K').padEnd(8)}\x1b[0m Cost saved: \x1b[32m$${costSaved.padEnd(20)}\x1b[0mā`);
|
|
1726
|
-
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤');
|
|
1727
|
-
console.log('ā Works with: Claude Code Ā· Cursor Ā· Windsurf Ā· Copilot ā');
|
|
1728
|
-
console.log('ā Free Ā· GDPR Ā· German servers ā');
|
|
1729
|
-
console.log('ā \x1b[35mnpx @cachly-dev/mcp-server@latest setup\x1b[0m ā');
|
|
1730
|
-
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
1731
|
-
console.log('');
|
|
1732
|
-
|
|
1733
|
-
// Tweet text
|
|
1734
|
-
const shareUrl = 'https://cachly.dev?ref=share&utm_source=x&utm_medium=social&utm_campaign=cli-share';
|
|
1735
|
-
const tweet = `š§ My AI coding assistant now has persistent memory.\n\n${lessons} lessons stored Ā· ${recalls} recalls Ā· ${level}\n\nFix a bug once ā AI remembers forever. Across Claude Code, Cursor, Windsurf, Copilot.\n\n${shareUrl}\n\n#AIMemory #ClaudeCode #Cursor #DeveloperTools`;
|
|
1736
|
-
|
|
1737
|
-
console.log(' \x1b[1mš Share on Twitter/X (copy this):\x1b[0m');
|
|
1738
|
-
console.log(' āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
1739
|
-
console.log(tweet.split('\n').map(l => ` ${l}`).join('\n'));
|
|
1740
|
-
console.log(' āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
1741
|
-
console.log('');
|
|
1742
|
-
console.log(' \x1b[2mPro tip: add a screenshot of your Brain panel for 3x more engagement\x1b[0m');
|
|
1743
|
-
console.log(' \x1b[2mAlso add a live badge to your README:\x1b[0m');
|
|
1744
|
-
console.log(` \x1b[90m npx @cachly-dev/mcp-server@latest badge\x1b[0m`);
|
|
1745
|
-
console.log('');
|
|
1746
|
-
} catch (e) {
|
|
1747
|
-
console.log(`\nā Could not fetch stats: ${(e as Error).message}\n`);
|
|
1748
|
-
process.exit(1);
|
|
1749
|
-
}
|
|
1750
|
-
process.exit(0);
|
|
1751
|
-
}
|
|
1752
|
-
|
|
1753
|
-
// āā CLI: cachly badge āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
1754
|
-
// Outputs the Markdown + HTML snippet for embedding a live Brain lesson-count
|
|
1755
|
-
// badge in any README or website. Badge SVG served by cachly API (public, no auth).
|
|
1756
|
-
|
|
1757
|
-
if (process.argv[2] === 'badge') {
|
|
1758
|
-
const instanceId = process.env.CACHLY_BRAIN_INSTANCE_ID ?? '';
|
|
1759
|
-
|
|
1760
|
-
if (!instanceId) {
|
|
1761
|
-
console.log('\nā ļø CACHLY_BRAIN_INSTANCE_ID must be set.');
|
|
1762
|
-
console.log(' Run: npx @cachly-dev/mcp-server@latest setup (takes 1ā5 minutes)\n');
|
|
1763
|
-
process.exit(1);
|
|
1764
|
-
}
|
|
1765
|
-
|
|
1766
|
-
const badgeUrl = `${API_URL}/api/v1/badge/${instanceId}`;
|
|
1767
|
-
const targetUrl = 'https://cachly.dev';
|
|
1768
|
-
const markdown = `[](${targetUrl})`;
|
|
1769
|
-
const htmlBadge = `<a href="${targetUrl}"><img src="${badgeUrl}" alt="cachly Brain" /></a>`;
|
|
1770
|
-
|
|
1771
|
-
console.log('');
|
|
1772
|
-
console.log('\x1b[35m āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\x1b[0m');
|
|
1773
|
-
console.log('\x1b[35m ā\x1b[0m \x1b[1mš§ cachly Brain Badge\x1b[0m \x1b[35mā\x1b[0m');
|
|
1774
|
-
console.log('\x1b[35m āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\x1b[0m');
|
|
1775
|
-
console.log('');
|
|
1776
|
-
console.log(' Paste into your README.md:');
|
|
1777
|
-
console.log('');
|
|
1778
|
-
console.log(` \x1b[32m${markdown}\x1b[0m`);
|
|
1779
|
-
console.log('');
|
|
1780
|
-
console.log(' āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
1781
|
-
console.log(' Or paste into HTML:');
|
|
1782
|
-
console.log('');
|
|
1783
|
-
console.log(` \x1b[36m${htmlBadge}\x1b[0m`);
|
|
1784
|
-
console.log('');
|
|
1785
|
-
console.log(' āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
1786
|
-
console.log(' Badge URL (live, updates every hour):');
|
|
1787
|
-
console.log(` \x1b[90m${badgeUrl}\x1b[0m`);
|
|
1788
|
-
console.log('');
|
|
1789
|
-
console.log(' \x1b[2mThe badge shows your current lesson count ā no auth required,');
|
|
1790
|
-
console.log(' safe to embed in public repos. Updates hourly.\x1b[0m');
|
|
1791
|
-
console.log('');
|
|
1792
|
-
process.exit(0);
|
|
1793
|
-
}
|
|
1794
|
-
|
|
1795
|
-
// āā CLI: no-args splash screen āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
1796
|
-
// When run with no recognized subcommand, show a compelling pitch + help.
|
|
1797
|
-
|
|
1798
|
-
if (!process.argv[2] && process.stdout.isTTY) {
|
|
1799
|
-
console.log('');
|
|
1800
|
-
console.log('\x1b[35m āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\x1b[0m');
|
|
1801
|
-
console.log('\x1b[35m ā\x1b[0m \x1b[1mš§ cachly ā Persistent AI Memory for Developers\x1b[0m \x1b[35mā\x1b[0m');
|
|
1802
|
-
console.log('\x1b[35m āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\x1b[0m');
|
|
1803
|
-
console.log('');
|
|
1804
|
-
console.log(' Stop re-teaching your AI every morning.');
|
|
1805
|
-
console.log(' cachly gives it a permanent brain ā pre-briefed every session.');
|
|
1806
|
-
console.log('');
|
|
1807
|
-
console.log(' \x1b[1mCommands:\x1b[0m');
|
|
1808
|
-
console.log(' \x1b[32m npx @cachly-dev/mcp-server@latest demo\x1b[0m ā Start here (no account)');
|
|
1809
|
-
console.log(' \x1b[32m npx @cachly-dev/mcp-server@latest setup\x1b[0m ā Wire up your AI editors');
|
|
1810
|
-
console.log(' \x1b[36m npx @cachly-dev/mcp-server@latest health\x1b[0m ā Check everything works');
|
|
1811
|
-
console.log(' \x1b[36m npx @cachly-dev/mcp-server@latest digest\x1b[0m ā Weekly Brain summary');
|
|
1812
|
-
console.log(' \x1b[36m npx @cachly-dev/mcp-server@latest share\x1b[0m ā Share your Brain stats');
|
|
1813
|
-
console.log(' \x1b[36m npx @cachly-dev/mcp-server@latest badge\x1b[0m ā README badge for your Brain');
|
|
1814
|
-
console.log(' \x1b[36m npx @cachly-dev/mcp-server@latest invite\x1b[0m ā Invite a teammate');
|
|
1815
|
-
console.log(' \x1b[36m npx @cachly-dev/mcp-server@latest join <token>\x1b[0m ā Accept a Brain invite');
|
|
1816
|
-
console.log(' \x1b[36m npx @cachly-dev/mcp-server@latest upgrade\x1b[0m ā Check for updates');
|
|
1817
|
-
console.log('');
|
|
1818
|
-
console.log(' \x1b[90mWorks with: Claude Code Ā· Cursor Ā· Windsurf Ā· GitHub Copilot Ā· Cline Ā· Zed\x1b[0m');
|
|
1819
|
-
console.log(' \x1b[90mFree forever Ā· GDPR Ā· German servers Ā· 101 MCP tools\x1b[0m');
|
|
1820
|
-
console.log('');
|
|
1821
|
-
process.exit(0);
|
|
1822
|
-
}
|
|
1823
|
-
|
|
1824
|
-
// āā CLI: cachly init āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
1825
|
-
// Usage: npx @cachly-dev/mcp-server init --instance-id <id> --api-key <key> [--editor claude|cursor|windsurf|copilot|continue] [--project-dir /path]
|
|
1826
|
-
|
|
1827
|
-
if (process.argv[2] === 'init') {
|
|
1828
|
-
const { writeFile, mkdir } = await import('node:fs/promises');
|
|
1829
|
-
const { resolve, dirname } = await import('node:path');
|
|
1830
|
-
|
|
1831
|
-
const argv = process.argv.slice(3);
|
|
1832
|
-
const flag = (name: string) => { const i = argv.indexOf(`--${name}`); return i !== -1 ? argv[i + 1] : undefined; };
|
|
1833
|
-
|
|
1834
|
-
const instanceId = flag('instance-id') ?? process.env.CACHLY_BRAIN_INSTANCE_ID;
|
|
1835
|
-
const apiKey = flag('api-key') ?? process.env.CACHLY_JWT;
|
|
1836
|
-
const editor = (flag('editor') ?? 'claude').toLowerCase();
|
|
1837
|
-
const projectDir = resolve(flag('project-dir') ?? '.');
|
|
1838
|
-
|
|
1839
|
-
if (!instanceId || !apiKey) {
|
|
1840
|
-
console.error('\nUsage: npx @cachly-dev/mcp-server@latest init --instance-id <uuid> --api-key <cky_live_...> [--editor claude|cursor|windsurf|copilot|continue] [--project-dir /path]\n');
|
|
1841
|
-
console.error('Or run interactively (no flags needed): npx @cachly-dev/mcp-server@latest setup\n');
|
|
1842
|
-
console.error('Get your credentials from: https://cachly.dev/setup-ai\n');
|
|
1843
|
-
process.exit(1);
|
|
1844
|
-
}
|
|
1845
|
-
|
|
1846
|
-
const configFile = EDITOR_FILES[editor] ?? '.mcp.json';
|
|
1847
|
-
const configPath = resolve(projectDir, configFile);
|
|
1848
|
-
await mkdir(dirname(configPath), { recursive: true });
|
|
1849
|
-
const { existsSync: exInit } = await import('node:fs');
|
|
1850
|
-
const merged = await mergeMcpConfig(configPath, apiKey, instanceId, editor, { readFile, existsSync: exInit });
|
|
1851
|
-
await writeFile(configPath, merged, 'utf-8');
|
|
1852
|
-
console.log(`\nā
${exInit(configPath) ? 'Updated' : 'Written'}: ${configFile}`);
|
|
1853
|
-
|
|
1854
|
-
// Always write CLAUDE.md (idempotent ā safe to run multiple times)
|
|
1855
|
-
const result = await writeClaudeMd(projectDir, instanceId);
|
|
1856
|
-
const action = result === 'updated' ? 'ā
Updated' : result === 'appended' ? 'ā
Appended to' : 'ā
Written';
|
|
1857
|
-
console.log(`${action}: CLAUDE.md`);
|
|
1858
|
-
|
|
1859
|
-
// āā CLS Phase 4: Auto-install git post-commit hook āāāāāāāāāāāāāāāāāāāāāāāāā
|
|
1860
|
-
try {
|
|
1861
|
-
const { existsSync } = await import('node:fs');
|
|
1862
|
-
const gitHookDir = resolve(projectDir, '.git', 'hooks');
|
|
1863
|
-
const hookPath = resolve(gitHookDir, 'post-commit');
|
|
1864
|
-
if (existsSync(resolve(projectDir, '.git'))) {
|
|
1865
|
-
await mkdir(gitHookDir, { recursive: true });
|
|
1866
|
-
const hookScript = [
|
|
1867
|
-
`#!/bin/sh`,
|
|
1868
|
-
`# cachly CLS ā Continuous Learning Stream (installed by cachly init)`,
|
|
1869
|
-
`# Runs silently on every commit to keep your brain up to date.`,
|
|
1870
|
-
`CACHLY_INSTANCE="${instanceId}"`,
|
|
1871
|
-
`SHA=$(git rev-parse HEAD 2>/dev/null || echo "")`,
|
|
1872
|
-
`MSG=$(git log -1 --pretty=%B 2>/dev/null | head -1 | tr '"' "'" | cut -c1-200)`,
|
|
1873
|
-
`FILES=$(git diff-tree --no-commit-id -r --name-only HEAD 2>/dev/null | tr '\\n' ',' | sed 's/,$//')`,
|
|
1874
|
-
`node -e "try{require('child_process').execSync('npx @cachly-dev/mcp-server@latest cls-ingest \\''+ JSON.stringify({instance_id:'$CACHLY_INSTANCE',source:'git_commit',payload:{message:'$MSG',sha:'$SHA',files:'$FILES'.split(',').filter(Boolean)}})+'\\'' ,{stdio:'ignore',timeout:5000})}catch(e){}" 2>/dev/null &`,
|
|
1875
|
-
`exit 0`,
|
|
1876
|
-
].join('\n');
|
|
1877
|
-
let existing = '';
|
|
1878
|
-
try { const { readFile } = await import('node:fs/promises'); existing = await readFile(hookPath, 'utf-8'); } catch { /* no existing hook */ }
|
|
1879
|
-
if (existing && !existing.includes('cachly CLS')) {
|
|
1880
|
-
// Append to existing hook
|
|
1881
|
-
await writeFile(hookPath, existing.trimEnd() + '\n\n' + hookScript + '\n', 'utf-8');
|
|
1882
|
-
console.log(`ā
Appended: .git/hooks/post-commit (CLS hook)`);
|
|
1883
|
-
} else if (!existing) {
|
|
1884
|
-
await writeFile(hookPath, hookScript + '\n', 'utf-8');
|
|
1885
|
-
const { chmod } = await import('node:fs/promises');
|
|
1886
|
-
await chmod(hookPath, 0o755);
|
|
1887
|
-
console.log(`ā
Written: .git/hooks/post-commit (CLS hook)`);
|
|
1888
|
-
} else {
|
|
1889
|
-
console.log(`ā CLS hook already present in .git/hooks/post-commit`);
|
|
1890
|
-
}
|
|
1891
|
-
}
|
|
1892
|
-
} catch { /* non-critical ā git hook is a best-effort feature */ }
|
|
1893
|
-
|
|
1894
|
-
console.log(`\nš§ Cachly AI Brain configured for ${editor === 'claude' ? 'Claude Code' : editor}!`);
|
|
1895
|
-
console.log(` Restart your editor ā the \`cachly\` MCP tools will appear.`);
|
|
1896
|
-
console.log(`\n š Add a live badge to your README:`);
|
|
1897
|
-
console.log(` npx @cachly-dev/mcp-server@latest badge\n`);
|
|
1898
|
-
process.exit(0);
|
|
1899
|
-
}
|
|
1900
|
-
|
|
1901
|
-
// āā CLI: cachly health āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
1902
|
-
// Usage: npx @cachly-dev/mcp-server@latest health
|
|
1903
|
-
// Checks: JWT valid, Brain API reachable, editor configs found, git hook present.
|
|
1904
|
-
|
|
1905
|
-
// āā CLI: cachly status āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
1906
|
-
// Usage: npx @cachly-dev/mcp-server@latest status
|
|
1907
|
-
// Shows Brain health, team size, and quick stats at a glance.
|
|
1908
|
-
|
|
1909
|
-
if (process.argv[2] === 'status') {
|
|
1910
|
-
const apiKey = process.env.CACHLY_JWT ?? '';
|
|
1911
|
-
const instanceId = process.env.CACHLY_BRAIN_INSTANCE_ID ?? '';
|
|
1912
|
-
|
|
1913
|
-
if (!apiKey || !instanceId) {
|
|
1914
|
-
console.log('\nā ļø CACHLY_JWT and CACHLY_BRAIN_INSTANCE_ID must be set.');
|
|
1915
|
-
console.log(' Run: npx @cachly-dev/mcp-server@latest setup\n');
|
|
1916
|
-
process.exit(1);
|
|
1917
|
-
}
|
|
1918
|
-
|
|
1919
|
-
try {
|
|
1920
|
-
const [statsRes, referralRes] = await Promise.all([
|
|
1921
|
-
fetch(`${API_URL}/api/v1/instances/${instanceId}/brain/stats`, {
|
|
1922
|
-
headers: { Authorization: `Bearer ${apiKey}` },
|
|
1923
|
-
signal: AbortSignal.timeout(8000),
|
|
1924
|
-
}),
|
|
1925
|
-
fetch(`${API_URL}/api/v1/referral/me`, {
|
|
1926
|
-
headers: { Authorization: `Bearer ${apiKey}` },
|
|
1927
|
-
signal: AbortSignal.timeout(8000),
|
|
1928
|
-
}),
|
|
1929
|
-
]);
|
|
1930
|
-
|
|
1931
|
-
if (!statsRes.ok) throw new Error(`stats HTTP ${statsRes.status}`);
|
|
1932
|
-
const stats = await statsRes.json() as {
|
|
1933
|
-
lesson_count?: number; total_recall_count?: number;
|
|
1934
|
-
quality_score?: number; team_authors?: string[];
|
|
1935
|
-
};
|
|
1936
|
-
const referral = referralRes.ok
|
|
1937
|
-
? await referralRes.json() as { url?: string; referral_count?: number }
|
|
1938
|
-
: null;
|
|
1939
|
-
|
|
1940
|
-
const lessons = stats.lesson_count ?? 0;
|
|
1941
|
-
const recalls = stats.total_recall_count ?? 0;
|
|
1942
|
-
const score = Math.round((stats.quality_score ?? 0) * 100);
|
|
1943
|
-
const team = stats.team_authors ?? [];
|
|
1944
|
-
const refCount = referral?.referral_count ?? 0;
|
|
1945
|
-
const refUrl = referral?.url ?? '';
|
|
1946
|
-
|
|
1947
|
-
const level = lessons === 0 ? 'Intern š±' :
|
|
1948
|
-
lessons < 10 ? 'Junior Dev š§' :
|
|
1949
|
-
lessons < 30 ? 'Mid Dev ā”' :
|
|
1950
|
-
lessons < 60 ? 'Senior Dev š§ ' :
|
|
1951
|
-
lessons < 100 ? 'Staff Eng š' : 'Principal Eng š';
|
|
1952
|
-
|
|
1953
|
-
const statusIcon = lessons > 0 ? '\x1b[32mā\x1b[0m' : '\x1b[33mā\x1b[0m';
|
|
1954
|
-
|
|
1955
|
-
console.log('');
|
|
1956
|
-
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
1957
|
-
console.log(`ā ${statusIcon} \x1b[1mBrain status\x1b[0m Ā· instance ${instanceId.slice(0, 8)}... ā`);
|
|
1958
|
-
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤');
|
|
1959
|
-
console.log(`ā Lessons : \x1b[33m${String(lessons).padEnd(8)}\x1b[0m Level : \x1b[32m${level.padEnd(18)}\x1b[0mā`);
|
|
1960
|
-
console.log(`ā Recalls : \x1b[36m${String(recalls).padEnd(8)}\x1b[0m Quality score: \x1b[32m${String(score).padEnd(3)}%\x1b[0m ā`);
|
|
1961
|
-
|
|
1962
|
-
if (team.length > 0) {
|
|
1963
|
-
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤');
|
|
1964
|
-
const teamStr = team.slice(0, 4).join(', ');
|
|
1965
|
-
console.log(`ā \x1b[36mš„ Team Brain\x1b[0m Ā· ${String(team.length).padEnd(2)} developer${team.length === 1 ? ' ' : 's'} sharing lessons${' '.repeat(Math.max(0, 19 - teamStr.length))} ā`);
|
|
1966
|
-
console.log(`ā ${teamStr.slice(0, 56).padEnd(56)} ā`);
|
|
1967
|
-
}
|
|
1968
|
-
|
|
1969
|
-
if (refUrl) {
|
|
1970
|
-
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤');
|
|
1971
|
-
const refLine = `${refUrl.slice(0, 44)}${refUrl.length > 44 ? 'ā¦' : ''}`;
|
|
1972
|
-
console.log(`ā \x1b[35mš Invite link\x1b[0m Ā· ${refCount > 0 ? `\x1b[32m${refCount} joined\x1b[0m` : 'share to grow your team'}${' '.repeat(Math.max(0, 35 - String(refCount).length))} ā`);
|
|
1973
|
-
console.log(`ā \x1b[2m${refLine.padEnd(56)}\x1b[0m ā`);
|
|
1974
|
-
}
|
|
1975
|
-
|
|
1976
|
-
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
1977
|
-
console.log('');
|
|
1978
|
-
if (lessons === 0) {
|
|
1979
|
-
console.log(' \x1b[33mš” No lessons yet.\x1b[0m Run a session with cachly connected to start learning.');
|
|
1980
|
-
console.log('');
|
|
1981
|
-
}
|
|
1982
|
-
} catch (e) {
|
|
1983
|
-
console.log(`\nā Could not fetch status: ${(e as Error).message}\n`);
|
|
1984
|
-
process.exit(1);
|
|
1985
|
-
}
|
|
1986
|
-
process.exit(0);
|
|
1987
|
-
}
|
|
1988
|
-
|
|
1989
|
-
if (process.argv[2] === 'health') {
|
|
1990
|
-
const { existsSync } = await import('node:fs');
|
|
1991
|
-
const { readFile } = await import('node:fs/promises');
|
|
1992
|
-
const { resolve } = await import('node:path');
|
|
1993
|
-
|
|
1994
|
-
const cwd = process.cwd();
|
|
1995
|
-
let passed = 0;
|
|
1996
|
-
let failed = 0;
|
|
1997
|
-
|
|
1998
|
-
const ok = (msg: string) => { console.log(` ā
${msg}`); passed++; };
|
|
1999
|
-
const warn = (msg: string) => { console.log(` ā ļø ${msg}`); };
|
|
2000
|
-
const fail = (msg: string) => { console.log(` ā ${msg}`); failed++; };
|
|
2001
|
-
|
|
2002
|
-
console.log('\nš§ cachly health check\n');
|
|
2003
|
-
|
|
2004
|
-
// āā 1. JWT token āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
2005
|
-
console.log('š Auth token');
|
|
2006
|
-
const jwt = process.env.CACHLY_JWT ?? '';
|
|
2007
|
-
if (!jwt) {
|
|
2008
|
-
fail('CACHLY_JWT not set ā run: npx @cachly-dev/mcp-server@latest setup');
|
|
2009
|
-
} else {
|
|
2010
|
-
try {
|
|
2011
|
-
const parts = jwt.split('.');
|
|
2012
|
-
if (parts.length !== 3) throw new Error('not a JWT');
|
|
2013
|
-
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf-8')) as {
|
|
2014
|
-
sub?: string; exp?: number;
|
|
2015
|
-
};
|
|
2016
|
-
const expMs = payload.exp ? payload.exp * 1000 : null;
|
|
2017
|
-
const minsLeft = expMs ? Math.floor((expMs - Date.now()) / 60_000) : null;
|
|
2018
|
-
if (expMs && expMs < Date.now()) {
|
|
2019
|
-
fail(`JWT expired ${Math.abs(minsLeft!)} minute(s) ago ā get a new one: https://cachly.dev/setup-ai`);
|
|
2020
|
-
} else if (minsLeft !== null && minsLeft < 30) {
|
|
2021
|
-
warn(`JWT expires in ${minsLeft} minute(s) ā refresh soon at https://cachly.dev/setup-ai`);
|
|
2022
|
-
} else {
|
|
2023
|
-
ok(`JWT valid${minsLeft !== null ? ` (expires in ${Math.floor(minsLeft / 60)}h ${minsLeft % 60}m)` : ''}`);
|
|
2024
|
-
}
|
|
2025
|
-
} catch {
|
|
2026
|
-
fail('CACHLY_JWT format invalid (expected JWT with 3 parts)');
|
|
2027
|
-
}
|
|
2028
|
-
}
|
|
2029
|
-
|
|
2030
|
-
// āā 2. Brain API reachability āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
2031
|
-
console.log('\nš Brain API');
|
|
2032
|
-
const apiUrl = process.env.CACHLY_API_URL ?? 'https://api.cachly.dev';
|
|
2033
|
-
try {
|
|
2034
|
-
const res = await fetch(`${apiUrl}/health`, { signal: AbortSignal.timeout(5000) });
|
|
2035
|
-
if (res.ok) {
|
|
2036
|
-
const body = await res.json() as { status?: string };
|
|
2037
|
-
ok(`${apiUrl} reachable (status: ${body.status ?? 'ok'})`);
|
|
2038
|
-
} else if (res.status === 401 || res.status === 403) {
|
|
2039
|
-
ok(`${apiUrl} reachable (auth required ā expected)`);
|
|
2040
|
-
} else {
|
|
2041
|
-
fail(`${apiUrl}/health returned HTTP ${res.status}`);
|
|
2042
|
-
}
|
|
2043
|
-
} catch (e) {
|
|
2044
|
-
fail(`${apiUrl} unreachable ā ${(e as Error).message}`);
|
|
2045
|
-
}
|
|
2046
|
-
|
|
2047
|
-
// āā 3. Brain instance āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
2048
|
-
console.log('\nš§ Brain instance');
|
|
2049
|
-
const instanceId = process.env.CACHLY_BRAIN_INSTANCE_ID ?? '';
|
|
2050
|
-
if (!instanceId) {
|
|
2051
|
-
warn('CACHLY_BRAIN_INSTANCE_ID not set ā will auto-resolve on first tool call');
|
|
2052
|
-
} else if (jwt) {
|
|
2053
|
-
try {
|
|
2054
|
-
const instRes = await fetch(`${apiUrl}/api/v1/instances/${instanceId}`, {
|
|
2055
|
-
headers: { Authorization: `Bearer ${jwt}`, Accept: 'application/json' },
|
|
2056
|
-
signal: AbortSignal.timeout(5000),
|
|
2057
|
-
});
|
|
2058
|
-
if (instRes.ok) {
|
|
2059
|
-
const inst = await instRes.json() as { name?: string; status?: string; host?: string; port?: number; tls_enabled?: boolean };
|
|
2060
|
-
if (inst.status === 'running') {
|
|
2061
|
-
ok(`Instance "${inst.name}" (${instanceId.slice(0, 8)}ā¦) is running`);
|
|
2062
|
-
// Attempt actual Redis PING to verify connectivity end-to-end
|
|
2063
|
-
console.log('\nš Redis connectivity');
|
|
2064
|
-
if (inst.host && inst.port) {
|
|
2065
|
-
try {
|
|
2066
|
-
let pingPassword: string | undefined;
|
|
2067
|
-
let pingTls = inst.tls_enabled !== false;
|
|
2068
|
-
try {
|
|
2069
|
-
const connRes = await fetch(`${apiUrl}/api/v1/instances/${instanceId}/connection`, {
|
|
2070
|
-
headers: { Authorization: `Bearer ${jwt}`, Accept: 'application/json' },
|
|
2071
|
-
signal: AbortSignal.timeout(5000),
|
|
2072
|
-
});
|
|
2073
|
-
if (connRes.ok) {
|
|
2074
|
-
const conn = await connRes.json() as { password?: string; tls_enabled?: boolean };
|
|
2075
|
-
pingPassword = conn.password;
|
|
2076
|
-
pingTls = conn.tls_enabled !== false;
|
|
2077
|
-
}
|
|
2078
|
-
} catch { /* proceed without password */ }
|
|
2079
|
-
const pingClient = new Redis({
|
|
2080
|
-
host: inst.host, port: inst.port,
|
|
2081
|
-
password: pingPassword,
|
|
2082
|
-
...(pingTls ? { tls: {} } : {}),
|
|
2083
|
-
lazyConnect: true, connectTimeout: 5000, retryStrategy: () => null,
|
|
2084
|
-
});
|
|
2085
|
-
await pingClient.connect();
|
|
2086
|
-
const pong = await pingClient.ping();
|
|
2087
|
-
pingClient.disconnect();
|
|
2088
|
-
if (pong === 'PONG') {
|
|
2089
|
-
ok(`Redis PING ā PONG (${inst.host}:${inst.port})`);
|
|
2090
|
-
} else {
|
|
2091
|
-
warn(`Redis responded but not PONG: ${pong}`);
|
|
2092
|
-
}
|
|
2093
|
-
} catch (pingErr) {
|
|
2094
|
-
fail(`Redis unreachable ā ${(pingErr as Error).message}`);
|
|
2095
|
-
}
|
|
2096
|
-
} else {
|
|
2097
|
-
warn('Instance has no host/port yet');
|
|
2098
|
-
}
|
|
2099
|
-
} else {
|
|
2100
|
-
warn(`Instance "${inst.name}" status: ${inst.status}`);
|
|
2101
|
-
}
|
|
2102
|
-
} else {
|
|
2103
|
-
fail(`Instance ${instanceId.slice(0, 8)}⦠not found (HTTP ${instRes.status})`);
|
|
2104
|
-
}
|
|
2105
|
-
} catch (e) {
|
|
2106
|
-
fail(`Could not check instance ā ${(e as Error).message}`);
|
|
2107
|
-
}
|
|
2108
|
-
}
|
|
2109
|
-
|
|
2110
|
-
// āā 4. Editor configs āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
2111
|
-
console.log('\nš Editor configs (project dir)');
|
|
2112
|
-
const editorConfigs: Array<{ editor: string; path: string }> = [
|
|
2113
|
-
{ editor: 'Claude Code', path: '.mcp.json' },
|
|
2114
|
-
{ editor: 'Cursor', path: '.cursor/mcp.json' },
|
|
2115
|
-
{ editor: 'Windsurf', path: '.windsurf/mcp.json' },
|
|
2116
|
-
{ editor: 'Copilot / Cline', path: '.vscode/mcp.json' },
|
|
2117
|
-
{ editor: 'Zed', path: '.zed/settings.json' },
|
|
2118
|
-
{ editor: 'Continue.dev', path: '.continue/config.json' },
|
|
2119
|
-
];
|
|
2120
|
-
let configsFound = 0;
|
|
2121
|
-
for (const { editor, path } of editorConfigs) {
|
|
2122
|
-
const fullPath = resolve(cwd, path);
|
|
2123
|
-
if (existsSync(fullPath)) {
|
|
2124
|
-
try {
|
|
2125
|
-
const content = await readFile(fullPath, 'utf-8');
|
|
2126
|
-
const hasCachly = content.includes('@cachly-dev/mcp-server') || content.includes('cachly');
|
|
2127
|
-
if (hasCachly) {
|
|
2128
|
-
ok(`${editor}: ${path}`);
|
|
2129
|
-
configsFound++;
|
|
2130
|
-
} else {
|
|
2131
|
-
warn(`${editor}: ${path} exists but cachly not found inside`);
|
|
2132
|
-
}
|
|
2133
|
-
} catch {
|
|
2134
|
-
warn(`${editor}: ${path} exists but could not read`);
|
|
2135
|
-
}
|
|
2136
|
-
}
|
|
2137
|
-
}
|
|
2138
|
-
if (configsFound === 0) {
|
|
2139
|
-
fail('No editor MCP configs found ā run: npx @cachly-dev/mcp-server@latest setup');
|
|
2140
|
-
}
|
|
2141
|
-
|
|
2142
|
-
// āā 5. Git hook āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
2143
|
-
console.log('\nšŖ Git hook (ambient learning)');
|
|
2144
|
-
const hookPath = resolve(cwd, '.git', 'hooks', 'post-commit');
|
|
2145
|
-
if (!existsSync(resolve(cwd, '.git'))) {
|
|
2146
|
-
warn('Not in a git repository ā skip');
|
|
2147
|
-
} else if (existsSync(hookPath)) {
|
|
2148
|
-
try {
|
|
2149
|
-
const hook = await readFile(hookPath, 'utf-8');
|
|
2150
|
-
if (hook.includes('cachly')) {
|
|
2151
|
-
ok('.git/hooks/post-commit (cachly CLS hook present)');
|
|
2152
|
-
} else {
|
|
2153
|
-
warn('.git/hooks/post-commit exists but cachly hook not found ā run setup to add it');
|
|
2154
|
-
}
|
|
2155
|
-
} catch {
|
|
2156
|
-
warn('.git/hooks/post-commit exists but could not read');
|
|
2157
|
-
}
|
|
2158
|
-
} else {
|
|
2159
|
-
warn('.git/hooks/post-commit not found ā run: npx @cachly-dev/mcp-server@latest setup');
|
|
2160
|
-
}
|
|
2161
|
-
|
|
2162
|
-
// āā Summary āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
2163
|
-
console.log(`\n${'ā'.repeat(40)}`);
|
|
2164
|
-
if (failed === 0) {
|
|
2165
|
-
console.log(`ā
All checks passed (${passed} ok)\n`);
|
|
2166
|
-
} else {
|
|
2167
|
-
console.log(`ā ${failed} check(s) failed, ${passed} passed\n`);
|
|
2168
|
-
console.log(`š” Fix issues with: npx @cachly-dev/mcp-server@latest setup\n`);
|
|
2169
|
-
process.exit(1);
|
|
2170
|
-
}
|
|
2171
|
-
process.exit(0);
|
|
2172
|
-
}
|
|
2173
|
-
|
|
2174
|
-
// āā CLI: cachly setup (interactive ā no flags required) āāāāāāāāāāāāāāāāāāāāāāā
|
|
2175
|
-
// Usage: npx @cachly-dev/mcp-server setup
|
|
2176
|
-
|
|
2177
|
-
if (process.argv[2] === 'setup') {
|
|
2178
|
-
const { writeFile, mkdir, readFile } = await import('node:fs/promises');
|
|
2179
|
-
const { existsSync } = await import('node:fs');
|
|
2180
|
-
const { resolve, dirname } = await import('node:path');
|
|
2181
|
-
const { createInterface } = await import('node:readline');
|
|
2182
|
-
|
|
2183
|
-
sendFunnelEvent('setup_started');
|
|
2184
|
-
|
|
2185
|
-
// --yes / -y ā non-interactive mode (skips all prompts, picks defaults)
|
|
2186
|
-
const nonInteractive = process.argv.includes('--yes') || process.argv.includes('-y');
|
|
2187
|
-
|
|
2188
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
2189
|
-
const ask = (q: string, defaultVal = ''): Promise<string> => {
|
|
2190
|
-
if (nonInteractive) { console.log(`${q}${defaultVal}`); return Promise.resolve(defaultVal); }
|
|
2191
|
-
return new Promise(res => rl.question(q, ans => res(ans.trim() || defaultVal)));
|
|
2192
|
-
};
|
|
2193
|
-
|
|
2194
|
-
console.log('\nš§ cachly AI Brain ā Setup');
|
|
2195
|
-
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n');
|
|
2196
|
-
|
|
2197
|
-
// āā Step 1: Authenticate via OAuth Device Flow āāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
2198
|
-
let token = process.env.CACHLY_JWT ?? '';
|
|
2199
|
-
if (token) {
|
|
2200
|
-
console.log('ā Using token from CACHLY_JWT env var\n');
|
|
2201
|
-
} else {
|
|
2202
|
-
const AUTH_BASE = 'https://auth.cachly.dev/realms/cachly/protocol/openid-connect';
|
|
2203
|
-
const CLIENT_ID = 'cachly-cli';
|
|
2204
|
-
|
|
2205
|
-
console.log('Step 1: Sign in to cachly (free, no credit card)\n');
|
|
2206
|
-
sendFunnelEvent('setup_auth_started');
|
|
2207
|
-
|
|
2208
|
-
// Start device flow
|
|
2209
|
-
let deviceCode = '', userCode = '', verifyUri = '', pollInterval = 5000;
|
|
2210
|
-
try {
|
|
2211
|
-
const deviceRes = await fetch(`${AUTH_BASE}/auth/device`, {
|
|
2212
|
-
method: 'POST',
|
|
2213
|
-
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
2214
|
-
body: `client_id=${CLIENT_ID}&scope=openid`,
|
|
2215
|
-
});
|
|
2216
|
-
if (!deviceRes.ok) throw new Error(`Device flow error: HTTP ${deviceRes.status}`);
|
|
2217
|
-
const data = await deviceRes.json() as {
|
|
2218
|
-
device_code: string; user_code: string;
|
|
2219
|
-
verification_uri_complete: string; interval: number;
|
|
2220
|
-
};
|
|
2221
|
-
deviceCode = data.device_code;
|
|
2222
|
-
userCode = data.user_code;
|
|
2223
|
-
verifyUri = data.verification_uri_complete;
|
|
2224
|
-
pollInterval = (data.interval ?? 5) * 1000;
|
|
2225
|
-
} catch (e) {
|
|
2226
|
-
console.error(`\nFailed to start device flow: ${(e as Error).message}`);
|
|
2227
|
-
console.error('Falling back: sign in at https://cachly.dev/setup-ai and paste your API token.\n');
|
|
2228
|
-
token = await ask(' Paste API token (cky_live_...): ');
|
|
2229
|
-
if (!token) { console.error('\nToken is required. Aborting.\n'); rl.close(); process.exit(1); }
|
|
2230
|
-
sendFunnelEvent('setup_auth_completed');
|
|
2231
|
-
console.log('');
|
|
2232
|
-
deviceCode = ''; // mark as fallback so we skip polling
|
|
2233
|
-
}
|
|
2234
|
-
|
|
2235
|
-
if (deviceCode!) {
|
|
2236
|
-
// Open browser
|
|
2237
|
-
console.log(` Code: \x1b[1;33m${userCode!}\x1b[0m`);
|
|
2238
|
-
console.log(` URL: ${verifyUri!}\n`);
|
|
2239
|
-
try {
|
|
2240
|
-
const { execSync } = await import('node:child_process');
|
|
2241
|
-
const openCmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
2242
|
-
execSync(`${openCmd} "${verifyUri!}"`, { stdio: 'ignore' });
|
|
2243
|
-
console.log(' ā Browser opened ā confirm the code above to continue...\n');
|
|
2244
|
-
} catch {
|
|
2245
|
-
console.log(' š Open the URL above in your browser and enter the code.\n');
|
|
2246
|
-
}
|
|
2247
|
-
|
|
2248
|
-
// Poll for token
|
|
2249
|
-
process.stdout.write(' Waiting for authorization');
|
|
2250
|
-
const deadline = Date.now() + 10 * 60 * 1000; // 10 min
|
|
2251
|
-
while (Date.now() < deadline) {
|
|
2252
|
-
await new Promise(r => setTimeout(r, pollInterval!));
|
|
2253
|
-
process.stdout.write('.');
|
|
2254
|
-
try {
|
|
2255
|
-
const tokenRes = await fetch(`${AUTH_BASE}/token`, {
|
|
2256
|
-
method: 'POST',
|
|
2257
|
-
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
2258
|
-
body: `client_id=${CLIENT_ID}&grant_type=urn:ietf:params:oauth:grant-type:device_code&device_code=${deviceCode!}`,
|
|
2259
|
-
});
|
|
2260
|
-
const tokenData = await tokenRes.json() as { access_token?: string; error?: string };
|
|
2261
|
-
if (tokenData.access_token) {
|
|
2262
|
-
token = tokenData.access_token;
|
|
2263
|
-
console.log(' \x1b[32mā Authorized!\x1b[0m\n');
|
|
2264
|
-
sendFunnelEvent('setup_auth_completed');
|
|
2265
|
-
break;
|
|
2266
|
-
}
|
|
2267
|
-
// authorization_pending = keep polling; slow_down = increase interval
|
|
2268
|
-
if (tokenData.error === 'slow_down') pollInterval = Math.min(pollInterval! + 2000, 15000);
|
|
2269
|
-
else if (tokenData.error && tokenData.error !== 'authorization_pending') {
|
|
2270
|
-
console.error(`\nAuth error: ${tokenData.error}. Aborting.\n`);
|
|
2271
|
-
rl.close(); process.exit(1);
|
|
2272
|
-
}
|
|
2273
|
-
} catch { /* network hiccup ā keep polling */ }
|
|
2274
|
-
}
|
|
2275
|
-
if (!token) { console.error('\nTimed out waiting for authorization. Aborting.\n'); rl.close(); process.exit(1); }
|
|
2276
|
-
console.log('');
|
|
2277
|
-
}
|
|
2278
|
-
}
|
|
2279
|
-
|
|
2280
|
-
// āā Step 1b: Exchange Keycloak JWT ā long-lived cky_live_ API key āāāāāāāāāā
|
|
2281
|
-
// Only do this when the token looks like a Keycloak JWT (starts with "eyJ"),
|
|
2282
|
-
// not when the user already pasted a cky_live_ key directly.
|
|
2283
|
-
if (token.startsWith('eyJ')) {
|
|
2284
|
-
process.stdout.write('ā³ Generating your API key...');
|
|
2285
|
-
try {
|
|
2286
|
-
const keyRes = await fetch(`${API_URL}/api/v1/api-keys`, {
|
|
2287
|
-
method: 'POST',
|
|
2288
|
-
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
|
|
2289
|
-
body: JSON.stringify({ name: 'cachly-mcp-setup', scope: 'read_write' }),
|
|
2290
|
-
});
|
|
2291
|
-
if (!keyRes.ok) throw new Error(`HTTP ${keyRes.status}`);
|
|
2292
|
-
const keyBody = await keyRes.json() as { key: string };
|
|
2293
|
-
if (!keyBody.key) throw new Error('no key in response');
|
|
2294
|
-
token = keyBody.key; // swap JWT ā cky_live_...
|
|
2295
|
-
console.log(' ā\n');
|
|
2296
|
-
} catch {
|
|
2297
|
-
console.log(' (skipped)\n');
|
|
2298
|
-
// Non-fatal: fall back to using the Keycloak JWT directly.
|
|
2299
|
-
// It will expire but setup still works for now.
|
|
2300
|
-
}
|
|
2301
|
-
}
|
|
2302
|
-
|
|
2303
|
-
// āā Step 2: Fetch & pick instance āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
2304
|
-
process.stdout.write('ā³ Fetching your instances...');
|
|
2305
|
-
let instances: Array<{ id: string; name: string; status: string; tier: string; region: string }> = [];
|
|
2306
|
-
try {
|
|
2307
|
-
const res = await fetch(`${API_URL}/api/v1/instances`, {
|
|
2308
|
-
headers: { Authorization: `Bearer ${token}` },
|
|
2309
|
-
});
|
|
2310
|
-
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
2311
|
-
const body = await res.json() as { data: typeof instances };
|
|
2312
|
-
instances = (body.data ?? []).filter(i => i.status === 'running');
|
|
2313
|
-
} catch (e) {
|
|
2314
|
-
const msg = (e as Error).message;
|
|
2315
|
-
console.error(`\n\nFailed to fetch instances: ${msg}`);
|
|
2316
|
-
if (msg.includes('401')) {
|
|
2317
|
-
console.error('Token rejected. Get a valid token at https://cachly.dev/setup-ai\n');
|
|
2318
|
-
try {
|
|
2319
|
-
const { execSync } = await import('node:child_process');
|
|
2320
|
-
const openCmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
2321
|
-
execSync(`${openCmd} https://cachly.dev/setup-ai`, { stdio: 'ignore' });
|
|
2322
|
-
} catch { /* ignore */ }
|
|
2323
|
-
}
|
|
2324
|
-
rl.close(); process.exit(1);
|
|
2325
|
-
}
|
|
2326
|
-
console.log(` found ${instances.length}\n`);
|
|
2327
|
-
|
|
2328
|
-
if (instances.length === 0) {
|
|
2329
|
-
// Auto-provision a free Brain instance so users don't have to visit the website.
|
|
2330
|
-
process.stdout.write('ā³ Creating your free Brain instance...');
|
|
2331
|
-
try {
|
|
2332
|
-
const autoRes = await fetch(`${API_URL}/api/v1/instances/auto`, {
|
|
2333
|
-
method: 'POST',
|
|
2334
|
-
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
|
|
2335
|
-
});
|
|
2336
|
-
if (!autoRes.ok) throw new Error(`HTTP ${autoRes.status}`);
|
|
2337
|
-
const autoBody = await autoRes.json() as { instance?: { id: string; name: string; status: string; tier: string; region: string }; instance_id?: string; status?: string; created?: boolean };
|
|
2338
|
-
if (autoBody.instance) {
|
|
2339
|
-
// Returned an existing instance.
|
|
2340
|
-
instances = [autoBody.instance];
|
|
2341
|
-
} else if (autoBody.instance_id) {
|
|
2342
|
-
// Newly created ā poll until running or give up after 30 s.
|
|
2343
|
-
const newId = autoBody.instance_id;
|
|
2344
|
-
console.log(` ā created (${newId.slice(0, 8)}ā¦)\n`);
|
|
2345
|
-
// 45 attempts Ć 3s = 135s ā generous for free-tier in slow regions.
|
|
2346
|
-
process.stdout.write('ā³ Waiting for instance to start');
|
|
2347
|
-
for (let attempt = 0; attempt < 45; attempt++) {
|
|
2348
|
-
await new Promise(r => setTimeout(r, 3000));
|
|
2349
|
-
process.stdout.write('.');
|
|
2350
|
-
try {
|
|
2351
|
-
const checkRes = await fetch(`${API_URL}/api/v1/instances/${newId}`, { headers: { Authorization: `Bearer ${token}` } });
|
|
2352
|
-
if (checkRes.ok) {
|
|
2353
|
-
const inst = await checkRes.json() as { id: string; name: string; status: string; tier: string; region: string };
|
|
2354
|
-
if (inst.status === 'running') { instances = [inst]; break; }
|
|
2355
|
-
}
|
|
2356
|
-
} catch { /* keep polling */ }
|
|
2357
|
-
}
|
|
2358
|
-
console.log('');
|
|
2359
|
-
}
|
|
2360
|
-
} catch (e) {
|
|
2361
|
-
console.log(` failed: ${(e as Error).message}\n`);
|
|
2362
|
-
}
|
|
2363
|
-
|
|
2364
|
-
if (instances.length === 0) {
|
|
2365
|
-
console.error('\nCould not create an instance automatically. Opening https://cachly.dev/instances ā¦\n');
|
|
2366
|
-
try {
|
|
2367
|
-
const { execSync } = await import('node:child_process');
|
|
2368
|
-
const openCmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
2369
|
-
execSync(`${openCmd} https://cachly.dev/instances`, { stdio: 'ignore' });
|
|
2370
|
-
} catch { /* ignore */ }
|
|
2371
|
-
rl.close(); process.exit(1);
|
|
2372
|
-
}
|
|
2373
|
-
}
|
|
2374
|
-
|
|
2375
|
-
// Auto-pick the most recently created running instance ā no prompt.
|
|
2376
|
-
const instance = instances[0];
|
|
2377
|
-
if (instances.length > 1) {
|
|
2378
|
-
console.log(`ā¹ļø Multiple instances found ā using most recent: ${instance.name}`);
|
|
2379
|
-
console.log(` (Run with --instance-id <id> to use a different one)\n`);
|
|
2380
|
-
}
|
|
2381
|
-
console.log(`ā Instance: ${instance.name} (${instance.id.slice(0, 8)}ā¦)\n`);
|
|
2382
|
-
sendFunnelEvent('setup_instance_ready', { instance_id: instance.id });
|
|
2383
|
-
|
|
2384
|
-
// āā Step 3: Detect editors āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
2385
|
-
const cwd = process.cwd();
|
|
2386
|
-
const detected: string[] = [];
|
|
2387
|
-
const { homedir } = await import('node:os');
|
|
2388
|
-
const home = homedir();
|
|
2389
|
-
// Claude Code: always include (CLAUDE.md is universal)
|
|
2390
|
-
detected.push('claude');
|
|
2391
|
-
if (existsSync(resolve(cwd, '.cursor'))) detected.push('cursor');
|
|
2392
|
-
if (existsSync(resolve(cwd, '.windsurf'))) detected.push('windsurf');
|
|
2393
|
-
if (existsSync(resolve(cwd, '.vscode'))) detected.push('copilot');
|
|
2394
|
-
if (existsSync(resolve(cwd, '.continue'))) detected.push('continue');
|
|
2395
|
-
// Cline: VS Code extension ā detect via globalStorage on the machine
|
|
2396
|
-
const clineGlobalDir = process.platform === 'darwin'
|
|
2397
|
-
? resolve(home, 'Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev')
|
|
2398
|
-
: resolve(home, '.vscode/extensions');
|
|
2399
|
-
if (existsSync(clineGlobalDir) && !detected.includes('copilot')) detected.push('cline');
|
|
2400
|
-
else if (existsSync(clineGlobalDir)) detected.push('cline'); // both copilot + cline share .vscode/mcp.json ā fine
|
|
2401
|
-
// Zed editor ā detect via app data dir
|
|
2402
|
-
const zedDir = process.platform === 'darwin'
|
|
2403
|
-
? resolve(home, 'Library/Application Support/Zed')
|
|
2404
|
-
: resolve(home, '.config/zed');
|
|
2405
|
-
if (existsSync(zedDir)) detected.push('zed');
|
|
2406
|
-
|
|
2407
|
-
const editorLabel = (e: string) =>
|
|
2408
|
-
({ claude: 'Claude Code', cursor: 'Cursor', windsurf: 'Windsurf', copilot: 'GitHub Copilot', continue: 'Continue.dev', cline: 'Cline (VSCode)', zed: 'Zed' })[e] ?? e;
|
|
2409
|
-
|
|
2410
|
-
// Auto-configure all detected editors ā no prompt needed.
|
|
2411
|
-
const editorsToSetup = detected;
|
|
2412
|
-
console.log(`Step 3: Configuring for: ${editorsToSetup.map(editorLabel).join(', ')}\n`);
|
|
2413
|
-
|
|
2414
|
-
// āā Step 4: Write editor configs (project-level) āāāāāāāāāāāāāāāāāāāāāāāāā
|
|
2415
|
-
// Uses mergeMcpConfig to preserve all existing MCP servers ā only adds/updates
|
|
2416
|
-
// the cachly entry without touching filesystem, github, or other servers.
|
|
2417
|
-
const { existsSync: exSetup } = await import('node:fs');
|
|
2418
|
-
for (const editor of editorsToSetup) {
|
|
2419
|
-
const configFile = EDITOR_FILES[editor] ?? '.mcp.json';
|
|
2420
|
-
const configPath = resolve(cwd, configFile);
|
|
2421
|
-
try {
|
|
2422
|
-
await mkdir(dirname(configPath), { recursive: true });
|
|
2423
|
-
const wasExisting = exSetup(configPath);
|
|
2424
|
-
const merged = await mergeMcpConfig(configPath, token, instance.id, editor, { readFile, existsSync: exSetup });
|
|
2425
|
-
await writeFile(configPath, merged, 'utf-8');
|
|
2426
|
-
console.log(`ā
${wasExisting ? 'Updated' : 'Written'}: ${configFile}`);
|
|
2427
|
-
} catch (writeErr) {
|
|
2428
|
-
console.log(`ā ļø Could not write ${configFile}: ${(writeErr as Error).message}`);
|
|
2429
|
-
console.log(` Fix permissions or run with sudo, then re-run setup.`);
|
|
2430
|
-
}
|
|
2431
|
-
}
|
|
2432
|
-
|
|
2433
|
-
// āā Step 4b: Write global Claude Code config (~/.claude/mcp.json) āāāāāāāāā
|
|
2434
|
-
// Merges cachly into the existing global config so it works in every project,
|
|
2435
|
-
// without removing other MCP servers the user may have configured.
|
|
2436
|
-
const globalClaudePath = resolve(home, '.claude', 'mcp.json');
|
|
2437
|
-
try {
|
|
2438
|
-
await mkdir(dirname(globalClaudePath), { recursive: true });
|
|
2439
|
-
let globalConfig: { mcpServers?: Record<string, unknown> } = {};
|
|
2440
|
-
if (existsSync(globalClaudePath)) {
|
|
2441
|
-
try {
|
|
2442
|
-
globalConfig = JSON.parse(await readFile(globalClaudePath, 'utf-8')) as typeof globalConfig;
|
|
2443
|
-
} catch { /* corrupt ā start fresh */ }
|
|
2444
|
-
}
|
|
2445
|
-
globalConfig.mcpServers ??= {};
|
|
2446
|
-
globalConfig.mcpServers['cachly'] = {
|
|
2447
|
-
command: 'npx',
|
|
2448
|
-
args: ['-y', '@cachly-dev/mcp-server@latest'],
|
|
2449
|
-
env: { CACHLY_API_URL: 'https://api.cachly.dev', CACHLY_JWT: token, CACHLY_BRAIN_INSTANCE_ID: instance.id },
|
|
2450
|
-
};
|
|
2451
|
-
await writeFile(globalClaudePath, JSON.stringify(globalConfig, null, 2), 'utf-8');
|
|
2452
|
-
console.log(`ā
Written: ~/.claude/mcp.json (global ā works in every project)`);
|
|
2453
|
-
} catch (e) {
|
|
2454
|
-
console.log(`ā ļø Could not write ~/.claude/mcp.json: ${(e as Error).message}`);
|
|
2455
|
-
}
|
|
2456
|
-
|
|
2457
|
-
// āā Step 5: CLAUDE.md (always ā idempotent) āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
2458
|
-
const mdResult = await writeClaudeMd(cwd, instance.id);
|
|
2459
|
-
const mdLabel = mdResult === 'updated' ? 'ā
Updated' : mdResult === 'appended' ? 'ā
Appended to' : 'ā
Written';
|
|
2460
|
-
console.log(`${mdLabel}: CLAUDE.md\n`);
|
|
2461
|
-
|
|
2462
|
-
// āā Step 5b: Bootstrap Brain from git history āāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
2463
|
-
// Pre-populates the Brain with real lessons so the first session_start shows
|
|
2464
|
-
// actual project knowledge instead of an empty brain.
|
|
2465
|
-
if (existsSync(resolve(cwd, '.git'))) {
|
|
2466
|
-
process.stdout.write('ā³ Bootstrapping Brain from git history (~10s)...');
|
|
2467
|
-
try {
|
|
2468
|
-
JWT = token; // set global JWT so handleTool can authenticate
|
|
2469
|
-
const gitResult = await handleTool('brain_from_git', {
|
|
2470
|
-
instance_id: instance.id,
|
|
2471
|
-
repo_path: cwd,
|
|
2472
|
-
limit: 100,
|
|
2473
|
-
});
|
|
2474
|
-
const match = gitResult.match(/(\d+) lesson/);
|
|
2475
|
-
console.log(` ā ${match ? match[1] + ' lessons' : 'done'} extracted from git history`);
|
|
2476
|
-
} catch (e) {
|
|
2477
|
-
console.log(` (skipped: ${(e as Error).message.slice(0, 80)})`);
|
|
2478
|
-
}
|
|
2479
|
-
}
|
|
2480
|
-
|
|
2481
|
-
// āā Step 6: Show Brain health (Aha moment) āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
2482
|
-
// Fetch brain health from the API to show the user what their agent will see.
|
|
2483
|
-
process.stdout.write('\nā³ Fetching your Brain health preview...');
|
|
2484
|
-
try {
|
|
2485
|
-
const brainRes = await fetch(`${API_URL}/api/v1/instances/${instance.id}/brain/stats`, {
|
|
2486
|
-
headers: { Authorization: `Bearer ${token}` },
|
|
2487
|
-
signal: AbortSignal.timeout(6000),
|
|
2488
|
-
});
|
|
2489
|
-
if (brainRes.ok) {
|
|
2490
|
-
const brainData = await brainRes.json() as {
|
|
2491
|
-
lesson_count?: number; context_count?: number;
|
|
2492
|
-
open_failures?: number; quality_score?: number;
|
|
2493
|
-
};
|
|
2494
|
-
const lessons = brainData.lesson_count ?? 0;
|
|
2495
|
-
const contexts = brainData.context_count ?? 0;
|
|
2496
|
-
const score = brainData.quality_score ?? 0;
|
|
2497
|
-
const level = lessons === 0 ? 'Intern š±' :
|
|
2498
|
-
lessons < 10 ? 'Junior Dev š§' :
|
|
2499
|
-
lessons < 30 ? 'Mid Dev ā”' :
|
|
2500
|
-
lessons < 60 ? 'Senior Dev š§ ' :
|
|
2501
|
-
lessons < 100 ? 'Staff Eng š' : 'Principal Eng š';
|
|
2502
|
-
console.log(' ā\n');
|
|
2503
|
-
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
2504
|
-
console.log(`ā š§ Brain Health Report ā`);
|
|
2505
|
-
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤');
|
|
2506
|
-
console.log(`ā Lessons stored : ${String(lessons).padEnd(6)} ā Level: ${level.padEnd(20)}ā`);
|
|
2507
|
-
console.log(`ā Context entries : ${String(contexts).padEnd(6)} ā Quality score: ${String(Math.round(score * 100)).padEnd(3)}% ā`);
|
|
2508
|
-
if (lessons === 0) {
|
|
2509
|
-
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤');
|
|
2510
|
-
console.log('ā Your Brain is empty and ready to learn. ā');
|
|
2511
|
-
console.log('ā After your first coding session it will contain: ā');
|
|
2512
|
-
console.log('ā ⢠Lessons from every bug you fixed ā');
|
|
2513
|
-
console.log('ā ⢠Your project\'s indexed files ā');
|
|
2514
|
-
console.log('ā ⢠A session summary for next time ā');
|
|
2515
|
-
}
|
|
2516
|
-
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
2517
|
-
|
|
2518
|
-
// āā Step 6b: brain_predict preview (only if we have real lessons) āāāāāāā
|
|
2519
|
-
if (lessons >= 5) {
|
|
2520
|
-
process.stdout.write('\nā³ Running predictive risk scan...');
|
|
2521
|
-
try {
|
|
2522
|
-
const predictResult = await handleTool('brain_predict', {
|
|
2523
|
-
instance_id: instance.id,
|
|
2524
|
-
context: 'starting a new development session',
|
|
2525
|
-
});
|
|
2526
|
-
const predictLines = predictResult
|
|
2527
|
-
.split('\n')
|
|
2528
|
-
.filter((l: string) => l.trim())
|
|
2529
|
-
.slice(0, 6);
|
|
2530
|
-
if (predictLines.length > 0) {
|
|
2531
|
-
console.log(' ā\n');
|
|
2532
|
-
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
2533
|
-
console.log('ā š® Brain Risk Preview (brain_predict) ā');
|
|
2534
|
-
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤');
|
|
2535
|
-
for (const pl of predictLines) {
|
|
2536
|
-
console.log(`ā ${pl.slice(0, 51).padEnd(51)} ā`);
|
|
2537
|
-
}
|
|
2538
|
-
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
2539
|
-
} else {
|
|
2540
|
-
console.log(' (no risks found ā clean slate!)');
|
|
2541
|
-
}
|
|
2542
|
-
} catch { console.log(' (skipped)'); }
|
|
2543
|
-
}
|
|
2544
|
-
} else {
|
|
2545
|
-
console.log(' (skipped)');
|
|
2546
|
-
}
|
|
2547
|
-
} catch { console.log(' (skipped)'); }
|
|
2548
|
-
|
|
2549
|
-
console.log(`\nš Your AI just got a permanent brain.`);
|
|
2550
|
-
console.log(` Restart your editor ā from this session on, it arrives pre-briefed.\n`);
|
|
2551
|
-
console.log(` No more re-explaining your stack. No repeated mistakes.\n`);
|
|
2552
|
-
console.log(` Dashboard: https://cachly.dev/instances/${instance.id}`);
|
|
2553
|
-
console.log(`\n š Add a live badge to your README:`);
|
|
2554
|
-
console.log(` npx @cachly-dev/mcp-server@latest badge\n`);
|
|
2555
|
-
|
|
2556
|
-
// āā Step 7: Email opt-in (non-blocking ā at the very end) āāāāāāāāāāāāāāāā
|
|
2557
|
-
if (!nonInteractive) {
|
|
2558
|
-
const email = await ask(' š¬ Stay in the loop? Email for release notes [Enter to skip]: ');
|
|
2559
|
-
if (email && email.includes('@')) {
|
|
2560
|
-
try {
|
|
2561
|
-
await fetch(`${API_URL}/api/newsletter/subscribe`, {
|
|
2562
|
-
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
2563
|
-
body: JSON.stringify({ email: email.trim().toLowerCase(), source: 'mcp-setup' }),
|
|
2564
|
-
signal: AbortSignal.timeout(5000),
|
|
2565
|
-
});
|
|
2566
|
-
console.log(' ā
Subscribed ā you\'ll only hear from us when it matters.\n');
|
|
2567
|
-
} catch { /* fire and forget */ }
|
|
2568
|
-
}
|
|
2569
|
-
}
|
|
2570
|
-
|
|
2571
|
-
// āā CLS Phase 4: Auto-install git post-commit hook in setup flow āāāāāāāāā
|
|
2572
|
-
try {
|
|
2573
|
-
const { existsSync } = await import('node:fs');
|
|
2574
|
-
const { writeFile: wf2, mkdir: mk2, chmod: ch2, readFile: rf2 } = await import('node:fs/promises');
|
|
2575
|
-
const { resolve: res2 } = await import('node:path');
|
|
2576
|
-
const setupProjectDir = process.cwd();
|
|
2577
|
-
const gitHookDir2 = res2(setupProjectDir, '.git', 'hooks');
|
|
2578
|
-
const hookPath2 = res2(gitHookDir2, 'post-commit');
|
|
2579
|
-
if (existsSync(res2(setupProjectDir, '.git'))) {
|
|
2580
|
-
await mk2(gitHookDir2, { recursive: true });
|
|
2581
|
-
const hs2 = [
|
|
2582
|
-
`#!/bin/sh`,
|
|
2583
|
-
`# cachly CLS ā Continuous Learning Stream (installed by cachly setup)`,
|
|
2584
|
-
`CACHLY_INSTANCE="${instance.id}"`,
|
|
2585
|
-
`SHA=$(git rev-parse HEAD 2>/dev/null || echo "")`,
|
|
2586
|
-
`MSG=$(git log -1 --pretty=%B 2>/dev/null | head -1 | tr '"' "'" | cut -c1-200)`,
|
|
2587
|
-
`FILES=$(git diff-tree --no-commit-id -r --name-only HEAD 2>/dev/null | tr '\\n' ',' | sed 's/,$//')`,
|
|
2588
|
-
`node -e "try{require('child_process').execSync('npx @cachly-dev/mcp-server@latest cls-ingest \\''+ JSON.stringify({instance_id:'$CACHLY_INSTANCE',source:'git_commit',payload:{message:'$MSG',sha:'$SHA',files:'$FILES'.split(',').filter(Boolean)}})+'\\'' ,{stdio:'ignore',timeout:5000})}catch(e){}" 2>/dev/null &`,
|
|
2589
|
-
`exit 0`,
|
|
2590
|
-
].join('\n');
|
|
2591
|
-
let ex2 = '';
|
|
2592
|
-
try { ex2 = await rf2(hookPath2, 'utf-8'); } catch { /* no existing */ }
|
|
2593
|
-
if (!ex2) {
|
|
2594
|
-
await wf2(hookPath2, hs2 + '\n', 'utf-8');
|
|
2595
|
-
await ch2(hookPath2, 0o755);
|
|
2596
|
-
console.log(`\nā
CLS git hook installed ā your brain will learn from every commit.`);
|
|
2597
|
-
} else if (!ex2.includes('cachly CLS')) {
|
|
2598
|
-
await wf2(hookPath2, ex2.trimEnd() + '\n\n' + hs2 + '\n', 'utf-8');
|
|
2599
|
-
console.log(`\nā
CLS git hook appended to existing post-commit.`);
|
|
2600
|
-
}
|
|
2601
|
-
}
|
|
2602
|
-
} catch { /* non-critical */ }
|
|
2603
|
-
|
|
2604
|
-
// Setup reached the end successfully ā close the funnel.
|
|
2605
|
-
JWT = token;
|
|
2606
|
-
sendFunnelEvent('setup_completed', { instance_id: instance.id });
|
|
2607
|
-
|
|
2608
|
-
rl.close();
|
|
2609
|
-
// Give the fire-and-forget telemetry a moment to flush before exit.
|
|
2610
|
-
await new Promise(r => setTimeout(r, 300));
|
|
2611
|
-
process.exit(0);
|
|
2612
|
-
}
|
|
2613
|
-
|
|
2614
|
-
// āā CLI: cachly index <dir> āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
2615
|
-
// Usage: npx @cachly-dev/mcp-server@latest index [./path/to/project]
|
|
2616
|
-
// Indexes the project directory into the Brain ā perfect for CI/CD cron jobs.
|
|
2617
|
-
|
|
2618
|
-
if (process.argv[2] === 'index') {
|
|
2619
|
-
const { resolve } = await import('node:path');
|
|
2620
|
-
const argv = process.argv.slice(3);
|
|
2621
|
-
const flag = (name: string) => { const i = argv.indexOf(`--${name}`); return i !== -1 ? argv[i + 1] : undefined; };
|
|
2622
|
-
|
|
2623
|
-
const dir = resolve(flag('dir') ?? argv.find(a => !a.startsWith('--')) ?? '.');
|
|
2624
|
-
const instanceId = flag('instance-id') ?? process.env.CACHLY_BRAIN_INSTANCE_ID;
|
|
2625
|
-
const maxFiles = parseInt(flag('max-files') ?? '500', 10);
|
|
2626
|
-
const namespace = flag('namespace') ?? 'cachly:sem:code';
|
|
2627
|
-
|
|
2628
|
-
if (!instanceId || !JWT) {
|
|
2629
|
-
console.error('\nā CACHLY_BRAIN_INSTANCE_ID and CACHLY_JWT must be set\n');
|
|
2630
|
-
console.error(' export CACHLY_BRAIN_INSTANCE_ID=<uuid>');
|
|
2631
|
-
console.error(' export CACHLY_JWT=<cky_live_...>');
|
|
2632
|
-
console.error(' npx @cachly-dev/mcp-server@latest index ./my-project\n');
|
|
2633
|
-
process.exit(1);
|
|
2634
|
-
}
|
|
2635
|
-
|
|
2636
|
-
console.log(`\nš Indexing: ${dir}`);
|
|
2637
|
-
console.log(` Instance: ${instanceId.slice(0, 8)}⦠Max files: ${maxFiles}\n`);
|
|
2638
|
-
|
|
2639
|
-
try {
|
|
2640
|
-
const result = await handleTool('index_project', {
|
|
2641
|
-
instance_id: instanceId,
|
|
2642
|
-
dir,
|
|
2643
|
-
max_files: maxFiles,
|
|
2644
|
-
ttl: 86400 * 7,
|
|
2645
|
-
namespace,
|
|
2646
|
-
});
|
|
2647
|
-
console.log(result);
|
|
2648
|
-
console.log('\nā
Indexing complete.\n');
|
|
2649
|
-
} catch (err) {
|
|
2650
|
-
console.error(`\nā Indexing failed: ${(err as Error).message}\n`);
|
|
2651
|
-
process.exit(1);
|
|
2652
|
-
}
|
|
2653
|
-
process.exit(0);
|
|
2654
|
-
}
|
|
2655
|
-
|
|
2656
|
-
// āā CLI: cachly learn-git āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
2657
|
-
// Usage: npx @cachly-dev/mcp-server@latest learn-git [./repo] [--max-commits 50]
|
|
2658
|
-
// Auto-learns brain lessons from recent git commits ā ideal for CI on PR merge.
|
|
2659
|
-
// Each meaningful commit becomes a lesson, so the Brain grows with zero manual work.
|
|
2660
|
-
|
|
2661
|
-
if (process.argv[2] === 'learn-git') {
|
|
2662
|
-
const { resolve } = await import('node:path');
|
|
2663
|
-
const argv = process.argv.slice(3);
|
|
2664
|
-
const flag = (name: string) => { const i = argv.indexOf(`--${name}`); return i !== -1 ? argv[i + 1] : undefined; };
|
|
2665
|
-
|
|
2666
|
-
const repoDir = resolve(flag('dir') ?? argv.find(a => !a.startsWith('--')) ?? '.');
|
|
2667
|
-
const instanceId = flag('instance-id') ?? process.env.CACHLY_BRAIN_INSTANCE_ID;
|
|
2668
|
-
const maxCommits = parseInt(flag('max-commits') ?? '50', 10);
|
|
2669
|
-
|
|
2670
|
-
if (!instanceId || !JWT) {
|
|
2671
|
-
console.error('\nā CACHLY_BRAIN_INSTANCE_ID and CACHLY_JWT must be set\n');
|
|
2672
|
-
console.error(' export CACHLY_BRAIN_INSTANCE_ID=<uuid>');
|
|
2673
|
-
console.error(' export CACHLY_JWT=<cky_live_...>');
|
|
2674
|
-
console.error(' npx @cachly-dev/mcp-server@latest learn-git .\n');
|
|
2675
|
-
process.exit(1);
|
|
2676
|
-
}
|
|
2677
|
-
|
|
2678
|
-
console.log(`\nš§ Learning from git history: ${repoDir}`);
|
|
2679
|
-
console.log(` Instance: ${instanceId.slice(0, 8)}⦠Max commits: ${maxCommits}\n`);
|
|
2680
|
-
|
|
2681
|
-
try {
|
|
2682
|
-
const result = await handleTool('brain_from_git', {
|
|
2683
|
-
instance_id: instanceId,
|
|
2684
|
-
repo_path: repoDir,
|
|
2685
|
-
limit: maxCommits,
|
|
2686
|
-
});
|
|
2687
|
-
console.log(result);
|
|
2688
|
-
sendFunnelEvent('brain_from_git', { api_key: JWT, instance_id: instanceId });
|
|
2689
|
-
console.log('\nā
Brain learned from your commits.\n');
|
|
2690
|
-
// Let fire-and-forget telemetry flush before exit.
|
|
2691
|
-
await new Promise(r => setTimeout(r, 300));
|
|
2692
|
-
} catch (err) {
|
|
2693
|
-
console.error(`\nā learn-git failed: ${(err as Error).message}\n`);
|
|
2694
|
-
process.exit(1);
|
|
2695
|
-
}
|
|
2696
|
-
process.exit(0);
|
|
2697
|
-
}
|
|
2698
|
-
|
|
2699
|
-
// āā Start āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
2700
|
-
|
|
2701
|
-
// Warn on stderr when credentials are missing so the user sees a clear
|
|
2702
|
-
// actionable message in their editor's MCP log instead of silent failures.
|
|
2703
|
-
// Skip for CLI commands that intentionally run without credentials.
|
|
2704
|
-
const _cliNoAuthCommands = ['demo', 'share', 'health', 'setup', 'init', 'digest', 'invite', 'badge', 'join', 'upgrade'];
|
|
2705
|
-
if (!JWT && !_cliNoAuthCommands.includes(process.argv[2] ?? '') && !(!process.argv[2] && process.stdout.isTTY)) {
|
|
2706
|
-
// No args + no JWT in a non-TTY context = running as MCP server without credentials.
|
|
2707
|
-
// Print actionable setup banner to stdout so it's captured by callers/tests.
|
|
2708
|
-
const banner =
|
|
2709
|
-
'\n' +
|
|
2710
|
-
'āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n' +
|
|
2711
|
-
'ā š§ cachly AI Brain ā Setup required ā\n' +
|
|
2712
|
-
'ā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā£\n' +
|
|
2713
|
-
'ā ā\n' +
|
|
2714
|
-
'ā CACHLY_JWT is not set. Get your free credentials at: ā\n' +
|
|
2715
|
-
'ā ā\n' +
|
|
2716
|
-
'ā š https://cachly.dev/setup-ai ā\n' +
|
|
2717
|
-
'ā ā\n' +
|
|
2718
|
-
'ā Then run the interactive setup wizard: ā\n' +
|
|
2719
|
-
'ā ā\n' +
|
|
2720
|
-
'ā npx @cachly-dev/mcp-server@latest setup ā\n' +
|
|
2721
|
-
'ā ā\n' +
|
|
2722
|
-
'ā Free tier ā no credit card required. ā\n' +
|
|
2723
|
-
'āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n' +
|
|
2724
|
-
'\n';
|
|
2725
|
-
// When invoked with no args as the main entry point, write to stdout + exit cleanly.
|
|
2726
|
-
// When used as MCP server (args present), write to stderr so editors see it.
|
|
2727
|
-
if (!process.argv[2] && _isMain) {
|
|
2728
|
-
process.stdout.write(banner);
|
|
2729
|
-
process.exit(0);
|
|
2730
|
-
} else {
|
|
2731
|
-
process.stderr.write(banner);
|
|
2732
|
-
}
|
|
2733
|
-
} else {
|
|
2734
|
-
// Warn if the JWT is already expired or expiring within the hour.
|
|
2735
|
-
const expMs = jwtExpiryMs(JWT);
|
|
2736
|
-
if (expMs !== null) {
|
|
2737
|
-
const minsLeft = Math.floor((expMs - Date.now()) / 60_000);
|
|
2738
|
-
if (minsLeft <= 0) {
|
|
2739
|
-
process.stderr.write(
|
|
2740
|
-
`\nā ļø cachly: CACHLY_JWT expired ${Math.abs(minsLeft)} minute(s) ago.\n` +
|
|
2741
|
-
` All tool calls will fail. Get a fresh token at https://cachly.dev/setup-ai\n\n`,
|
|
2742
|
-
);
|
|
2743
|
-
} else if (minsLeft < 60) {
|
|
2744
|
-
process.stderr.write(
|
|
2745
|
-
`\nā ļø cachly: CACHLY_JWT expires in ${minsLeft} minute(s).\n` +
|
|
2746
|
-
` Refresh it soon at https://cachly.dev/setup-ai to avoid interruptions.\n\n`,
|
|
2747
|
-
);
|
|
2748
|
-
}
|
|
2749
|
-
}
|
|
2750
|
-
}
|
|
2751
|
-
|
|
2752
|
-
// āā Update nudge (non-blocking, fire-and-forget) āāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
2753
|
-
// Check npm registry once per process start; if outdated, log to stderr so
|
|
2754
|
-
// the editor's MCP log shows an actionable one-liner. Skipped if opted out.
|
|
2755
|
-
if (!process.env.CACHLY_NO_UPDATE_CHECK) {
|
|
2756
|
-
(async () => {
|
|
2757
|
-
try {
|
|
2758
|
-
const res = await fetch(
|
|
2759
|
-
`https://registry.npmjs.org/@cachly-dev/mcp-server/latest`,
|
|
2760
|
-
{ signal: AbortSignal.timeout(4000) },
|
|
2761
|
-
);
|
|
2762
|
-
if (res.ok) {
|
|
2763
|
-
const data = await res.json() as { version: string };
|
|
2764
|
-
const latest = data?.version ?? '';
|
|
2765
|
-
if (latest && latest !== CURRENT_VERSION) {
|
|
2766
|
-
process.stderr.write(
|
|
2767
|
-
`\nā” cachly update available: ${CURRENT_VERSION} ā ${latest}\n` +
|
|
2768
|
-
` Run: npx @cachly-dev/mcp-server@latest setup\n\n`,
|
|
2769
|
-
);
|
|
2770
|
-
}
|
|
2771
|
-
}
|
|
2772
|
-
} catch { /* ignore ā network unavailable or timeout */ }
|
|
2773
|
-
})();
|
|
2774
|
-
}
|
|
2775
|
-
|
|
2776
|
-
const httpPort = process.env.PORT ? parseInt(process.env.PORT, 10) : undefined;
|
|
2777
|
-
|
|
2778
|
-
if (httpPort) {
|
|
2779
|
-
// āā HTTP mode (Streamable HTTP transport) āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
2780
|
-
// Used for Smithery URL deployment: PORT=3000 node dist/src/index.js
|
|
2781
|
-
const { createServer } = await import('node:http');
|
|
2782
|
-
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
|
|
2783
|
-
await server.connect(transport);
|
|
2784
|
-
const httpServer = createServer((req, res) => {
|
|
2785
|
-
const url = new URL(req.url ?? '/', `http://localhost:${httpPort}`);
|
|
2786
|
-
if (url.pathname === '/health') {
|
|
2787
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
2788
|
-
res.end(JSON.stringify({
|
|
2789
|
-
status: 'ok',
|
|
2790
|
-
version: CURRENT_VERSION,
|
|
2791
|
-
zeroResults: {
|
|
2792
|
-
total: zeroResultsTotal,
|
|
2793
|
-
last10: ZERO_RESULTS_LOG.slice(-10).map(e => ({ query: e.query, ts: new Date(e.ts).toISOString() })),
|
|
2794
|
-
},
|
|
2795
|
-
}));
|
|
2796
|
-
return;
|
|
2797
|
-
}
|
|
2798
|
-
if (url.pathname === '/mcp' || url.pathname === '/') {
|
|
2799
|
-
transport.handleRequest(req, res);
|
|
2800
|
-
return;
|
|
2801
|
-
}
|
|
2802
|
-
res.writeHead(404);
|
|
2803
|
-
res.end('not found');
|
|
2804
|
-
});
|
|
2805
|
-
httpServer.listen(httpPort, () => {
|
|
2806
|
-
process.stderr.write(`cachly-mcp HTTP server listening on :${httpPort}\n`);
|
|
2807
|
-
});
|
|
2808
|
-
} else {
|
|
2809
|
-
// āā stdio mode (default for local editor use) āāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
2810
|
-
const transport = new StdioServerTransport();
|
|
2811
|
-
await server.connect(transport);
|
|
2812
|
-
}
|
|
2813
|
-
|
|
2814
|
-
notify('cachly', 'startup', { version: CURRENT_VERSION, mode: process.env.MCP_HTTP_PORT ? 'http' : 'stdio' }).catch(() => undefined);
|
|
2815
|
-
|