@cachly-dev/mcp-server 0.10.56 → 0.10.58
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/eas-build-android.yml +58 -0
- package/.github/workflows/eas-update.yml +55 -0
- package/.github/workflows/publish.yml +2 -2
- package/BENCH.md +91 -0
- package/CHANGELOG.md +44 -0
- package/README.md +6 -34
- package/STRATEGY.md +228 -0
- package/apps/travel-chaos-organizer/.env.example +49 -0
- package/apps/travel-chaos-organizer/DEPLOY.md +328 -0
- package/apps/travel-chaos-organizer/Makefile +57 -0
- package/apps/travel-chaos-organizer/README.md +159 -0
- package/apps/travel-chaos-organizer/backend/.env.example +10 -0
- package/apps/travel-chaos-organizer/backend/Dockerfile +25 -0
- package/apps/travel-chaos-organizer/backend/alembic/env.py +56 -0
- package/apps/travel-chaos-organizer/backend/alembic/script.py.mako +24 -0
- package/apps/travel-chaos-organizer/backend/alembic/versions/.gitkeep +0 -0
- package/apps/travel-chaos-organizer/backend/alembic/versions/0001_stripe_customer_id.py +24 -0
- package/apps/travel-chaos-organizer/backend/alembic/versions/0002_telegram_chat_id.py +24 -0
- package/apps/travel-chaos-organizer/backend/alembic.ini +39 -0
- package/apps/travel-chaos-organizer/backend/app/__init__.py +0 -0
- package/apps/travel-chaos-organizer/backend/app/auth/__init__.py +0 -0
- package/apps/travel-chaos-organizer/backend/app/auth/keycloak.py +81 -0
- package/apps/travel-chaos-organizer/backend/app/config.py +66 -0
- package/apps/travel-chaos-organizer/backend/app/db/__init__.py +0 -0
- package/apps/travel-chaos-organizer/backend/app/db/database.py +20 -0
- package/apps/travel-chaos-organizer/backend/app/db/migrations/001_init.sql +71 -0
- package/apps/travel-chaos-organizer/backend/app/db/migrations/002_telemetry_freemium.sql +31 -0
- package/apps/travel-chaos-organizer/backend/app/db/migrations/003_telegram_chat_id.sql +4 -0
- package/apps/travel-chaos-organizer/backend/app/db/migrations/004_stripe_and_email.sql +5 -0
- package/apps/travel-chaos-organizer/backend/app/limiter.py +4 -0
- package/apps/travel-chaos-organizer/backend/app/logging_config.py +25 -0
- package/apps/travel-chaos-organizer/backend/app/main.py +77 -0
- package/apps/travel-chaos-organizer/backend/app/models/__init__.py +0 -0
- package/apps/travel-chaos-organizer/backend/app/models/schemas.py +142 -0
- package/apps/travel-chaos-organizer/backend/app/routers/__init__.py +0 -0
- package/apps/travel-chaos-organizer/backend/app/routers/admin.py +237 -0
- package/apps/travel-chaos-organizer/backend/app/routers/cache.py +13 -0
- package/apps/travel-chaos-organizer/backend/app/routers/events.py +29 -0
- package/apps/travel-chaos-organizer/backend/app/routers/export.py +122 -0
- package/apps/travel-chaos-organizer/backend/app/routers/inbox.py +122 -0
- package/apps/travel-chaos-organizer/backend/app/routers/items.py +130 -0
- package/apps/travel-chaos-organizer/backend/app/routers/mail.py +108 -0
- package/apps/travel-chaos-organizer/backend/app/routers/parse.py +303 -0
- package/apps/travel-chaos-organizer/backend/app/routers/payments.py +179 -0
- package/apps/travel-chaos-organizer/backend/app/routers/trips.py +134 -0
- package/apps/travel-chaos-organizer/backend/app/routers/users.py +169 -0
- package/apps/travel-chaos-organizer/backend/app/routers/waitlist.py +37 -0
- package/apps/travel-chaos-organizer/backend/app/services/__init__.py +0 -0
- package/apps/travel-chaos-organizer/backend/app/services/cache.py +131 -0
- package/apps/travel-chaos-organizer/backend/app/services/drip.py +128 -0
- package/apps/travel-chaos-organizer/backend/app/services/email.py +61 -0
- package/apps/travel-chaos-organizer/backend/app/services/errors.py +44 -0
- package/apps/travel-chaos-organizer/backend/app/services/notifier.py +10 -0
- package/apps/travel-chaos-organizer/backend/app/services/ollama.py +101 -0
- package/apps/travel-chaos-organizer/backend/app/services/parser.py +22 -0
- package/apps/travel-chaos-organizer/backend/app/services/quota.py +85 -0
- package/apps/travel-chaos-organizer/backend/app/services/sentry.py +11 -0
- package/apps/travel-chaos-organizer/backend/app/services/telemetry.py +35 -0
- package/apps/travel-chaos-organizer/backend/pytest.ini +4 -0
- package/apps/travel-chaos-organizer/backend/requirements-dev.txt +6 -0
- package/apps/travel-chaos-organizer/backend/requirements.txt +25 -0
- package/apps/travel-chaos-organizer/backend/tests/__init__.py +0 -0
- package/apps/travel-chaos-organizer/backend/tests/conftest.py +223 -0
- package/apps/travel-chaos-organizer/backend/tests/test_admin.py +99 -0
- package/apps/travel-chaos-organizer/backend/tests/test_cache.py +142 -0
- package/apps/travel-chaos-organizer/backend/tests/test_drip.py +32 -0
- package/apps/travel-chaos-organizer/backend/tests/test_e2e_chain.py +121 -0
- package/apps/travel-chaos-organizer/backend/tests/test_export.py +48 -0
- package/apps/travel-chaos-organizer/backend/tests/test_inbox.py +107 -0
- package/apps/travel-chaos-organizer/backend/tests/test_items.py +138 -0
- package/apps/travel-chaos-organizer/backend/tests/test_mail.py +113 -0
- package/apps/travel-chaos-organizer/backend/tests/test_ollama.py +145 -0
- package/apps/travel-chaos-organizer/backend/tests/test_parse_url.py +98 -0
- package/apps/travel-chaos-organizer/backend/tests/test_parser.py +72 -0
- package/apps/travel-chaos-organizer/backend/tests/test_payments.py +163 -0
- package/apps/travel-chaos-organizer/backend/tests/test_quota.py +46 -0
- package/apps/travel-chaos-organizer/backend/tests/test_search.py +67 -0
- package/apps/travel-chaos-organizer/backend/tests/test_trips.py +123 -0
- package/apps/travel-chaos-organizer/backend/tests/test_users.py +52 -0
- package/apps/travel-chaos-organizer/backups/.gitkeep +0 -0
- package/apps/travel-chaos-organizer/caddy/Caddyfile +27 -0
- package/apps/travel-chaos-organizer/docker-compose.node1.yml +31 -0
- package/apps/travel-chaos-organizer/docker-compose.override.yml +14 -0
- package/apps/travel-chaos-organizer/docker-compose.yml +128 -0
- package/apps/travel-chaos-organizer/frontend/.env.example +13 -0
- package/apps/travel-chaos-organizer/frontend/app/(app)/_layout.tsx +80 -0
- package/apps/travel-chaos-organizer/frontend/app/(app)/inbox.tsx +166 -0
- package/apps/travel-chaos-organizer/frontend/app/(app)/search.tsx +130 -0
- package/apps/travel-chaos-organizer/frontend/app/(app)/settings.tsx +381 -0
- package/apps/travel-chaos-organizer/frontend/app/(app)/trips/[id].tsx +346 -0
- package/apps/travel-chaos-organizer/frontend/app/(app)/trips.tsx +363 -0
- package/apps/travel-chaos-organizer/frontend/app/(app)/upgrade/cancel.tsx +34 -0
- package/apps/travel-chaos-organizer/frontend/app/(app)/upgrade/success.tsx +35 -0
- package/apps/travel-chaos-organizer/frontend/app/(auth)/login.tsx +91 -0
- package/apps/travel-chaos-organizer/frontend/app/_layout.tsx +71 -0
- package/apps/travel-chaos-organizer/frontend/app/auth.tsx +29 -0
- package/apps/travel-chaos-organizer/frontend/app/index.tsx +16 -0
- package/apps/travel-chaos-organizer/frontend/app/onboarding.tsx +119 -0
- package/apps/travel-chaos-organizer/frontend/app.config.ts +54 -0
- package/apps/travel-chaos-organizer/frontend/assets/adaptive-icon.svg +66 -0
- package/apps/travel-chaos-organizer/frontend/assets/icon.svg +79 -0
- package/apps/travel-chaos-organizer/frontend/assets/splash.svg +15 -0
- package/apps/travel-chaos-organizer/frontend/components/BottomSheet.tsx +102 -0
- package/apps/travel-chaos-organizer/frontend/components/ConfettiCannon.tsx +107 -0
- package/apps/travel-chaos-organizer/frontend/components/ConfirmDialog.tsx +109 -0
- package/apps/travel-chaos-organizer/frontend/components/ErrorBoundary.tsx +45 -0
- package/apps/travel-chaos-organizer/frontend/components/FileUploadButton.tsx +19 -0
- package/apps/travel-chaos-organizer/frontend/components/ImportSheet.tsx +155 -0
- package/apps/travel-chaos-organizer/frontend/components/OfflineBadge.tsx +55 -0
- package/apps/travel-chaos-organizer/frontend/components/QRCodeViewer.tsx +80 -0
- package/apps/travel-chaos-organizer/frontend/components/ScoreRing.tsx +39 -0
- package/apps/travel-chaos-organizer/frontend/components/Screen.tsx +39 -0
- package/apps/travel-chaos-organizer/frontend/components/Skeleton.tsx +70 -0
- package/apps/travel-chaos-organizer/frontend/components/SwipeableInboxItem.tsx +121 -0
- package/apps/travel-chaos-organizer/frontend/components/SwipeableTimelineItem.tsx +70 -0
- package/apps/travel-chaos-organizer/frontend/components/TimelineItem.tsx +73 -0
- package/apps/travel-chaos-organizer/frontend/components/TimelineItemDetail.tsx +130 -0
- package/apps/travel-chaos-organizer/frontend/components/Toast.tsx +60 -0
- package/apps/travel-chaos-organizer/frontend/components/ToastContext.tsx +42 -0
- package/apps/travel-chaos-organizer/frontend/components/TripCard.tsx +146 -0
- package/apps/travel-chaos-organizer/frontend/components/TripQRModal.tsx +70 -0
- package/apps/travel-chaos-organizer/frontend/components/UpgradeBanner.tsx +44 -0
- package/apps/travel-chaos-organizer/frontend/eas.json +22 -0
- package/apps/travel-chaos-organizer/frontend/hooks/useInbox.ts +24 -0
- package/apps/travel-chaos-organizer/frontend/hooks/useNetworkSync.ts +60 -0
- package/apps/travel-chaos-organizer/frontend/hooks/useTrips.ts +112 -0
- package/apps/travel-chaos-organizer/frontend/lib/api.ts +223 -0
- package/apps/travel-chaos-organizer/frontend/lib/auth.ts +93 -0
- package/apps/travel-chaos-organizer/frontend/lib/db.ts +86 -0
- package/apps/travel-chaos-organizer/frontend/lib/fileCache.ts +54 -0
- package/apps/travel-chaos-organizer/frontend/lib/haptics.ts +13 -0
- package/apps/travel-chaos-organizer/frontend/lib/notifications.ts +112 -0
- package/apps/travel-chaos-organizer/frontend/lib/offlineQueue.ts +145 -0
- package/apps/travel-chaos-organizer/frontend/lib/quota.tsx +43 -0
- package/apps/travel-chaos-organizer/frontend/lib/sentry.ts +28 -0
- package/apps/travel-chaos-organizer/frontend/lib/shareIntent.ts +44 -0
- package/apps/travel-chaos-organizer/frontend/package-lock.json +13221 -0
- package/apps/travel-chaos-organizer/frontend/package.json +44 -0
- package/apps/travel-chaos-organizer/frontend/tsconfig.json +23 -0
- package/apps/travel-chaos-organizer/keycloak/realm-export.json +59 -0
- package/apps/travel-chaos-organizer/landing/index.html +773 -0
- package/apps/travel-chaos-organizer/scripts/backup.sh +14 -0
- package/apps/travel-chaos-organizer/scripts/configure-env.sh +82 -0
- package/apps/travel-chaos-organizer/scripts/deploy.sh +54 -0
- package/apps/travel-chaos-organizer/scripts/health-check.sh +36 -0
- package/apps/travel-chaos-organizer/scripts/migrate.sh +7 -0
- package/apps/travel-chaos-organizer/scripts/server-setup.sh +85 -0
- package/apps/travel-chaos-organizer/scripts/smoke-test.sh +29 -0
- package/apps/travel-chaos-organizer/scripts/verify-tracking.sh +71 -0
- package/apps/travel-chaos-organizer/telegram-bot/.env.example +3 -0
- package/apps/travel-chaos-organizer/telegram-bot/Dockerfile +6 -0
- package/apps/travel-chaos-organizer/telegram-bot/bot.py +504 -0
- package/apps/travel-chaos-organizer/telegram-bot/requirements.txt +3 -0
- package/dist/packages/telegram-notify/client.d.ts +19 -0
- package/dist/packages/telegram-notify/client.js +75 -0
- package/dist/packages/telegram-notify/client.js.map +1 -0
- package/dist/src/__tests__/auth.test.js.map +1 -0
- package/dist/{__tests__ → src/__tests__}/brain-flow.test.js +141 -0
- package/dist/src/__tests__/brain-flow.test.js.map +1 -0
- package/dist/src/__tests__/confidence.test.js.map +1 -0
- package/dist/src/__tests__/e2e/api-auth.test.js.map +1 -0
- package/dist/src/__tests__/e2e/api-health.test.js.map +1 -0
- package/dist/src/__tests__/e2e/cli.test.js.map +1 -0
- package/dist/src/__tests__/e2e/config.js.map +1 -0
- package/dist/src/__tests__/e2e/funnel.test.js.map +1 -0
- package/dist/src/__tests__/e2e/mcp-tools.test.js.map +1 -0
- package/dist/src/__tests__/e2e/provisioning.test.js.map +1 -0
- package/dist/{__tests__ → src/__tests__}/handlers-unit.test.js +0 -2
- package/dist/src/__tests__/handlers-unit.test.js.map +1 -0
- package/dist/src/__tests__/namespace.test.js.map +1 -0
- package/dist/src/__tests__/rerank.test.js +115 -0
- package/dist/src/__tests__/rerank.test.js.map +1 -0
- package/dist/src/auth.js.map +1 -0
- package/dist/src/bench/cachly-bench.d.ts +35 -0
- package/dist/src/bench/cachly-bench.js +182 -0
- package/dist/src/bench/cachly-bench.js.map +1 -0
- package/dist/src/bench/fixtures.d.ts +37 -0
- package/dist/src/bench/fixtures.js +225 -0
- package/dist/src/bench/fixtures.js.map +1 -0
- package/dist/src/ckg.js.map +1 -0
- package/dist/src/confidence.js.map +1 -0
- package/dist/src/embeddings.js.map +1 -0
- package/dist/{handlers → src/handlers}/advanced.d.ts +1 -1
- package/dist/{handlers → src/handlers}/advanced.js +1 -2
- package/dist/src/handlers/advanced.js.map +1 -0
- package/dist/{handlers → src/handlers}/brain.js +361 -191
- package/dist/src/handlers/brain.js.map +1 -0
- package/dist/src/handlers/cache.js.map +1 -0
- package/dist/{handlers → src/handlers}/context.js +9 -4
- package/dist/src/handlers/context.js.map +1 -0
- package/dist/{handlers → src/handlers}/fedbrain.d.ts +6 -0
- package/dist/{handlers → src/handlers}/fedbrain.js +19 -7
- package/dist/src/handlers/fedbrain.js.map +1 -0
- package/dist/src/handlers/instances.js.map +1 -0
- package/dist/{handlers → src/handlers}/roadmap.d.ts +1 -1
- package/dist/{handlers → src/handlers}/roadmap.js +8 -4
- package/dist/src/handlers/roadmap.js.map +1 -0
- package/dist/src/handlers/syndicate.js.map +1 -0
- package/dist/src/handlers/tco.d.ts +16 -0
- package/dist/src/handlers/tco.js +303 -0
- package/dist/src/handlers/tco.js.map +1 -0
- package/dist/{handlers → src/handlers}/team.js +58 -7
- package/dist/src/handlers/team.js.map +1 -0
- package/dist/{index.js → src/index.js} +207 -429
- package/dist/src/index.js.map +1 -0
- package/dist/src/namespace.js.map +1 -0
- package/dist/src/notifier.d.ts +5 -0
- package/dist/src/notifier.js +6 -0
- package/dist/src/notifier.js.map +1 -0
- package/dist/src/rerank.d.ts +55 -0
- package/dist/src/rerank.js +111 -0
- package/dist/src/rerank.js.map +1 -0
- package/dist/{search.js → src/search.js} +2 -2
- package/dist/src/search.js.map +1 -0
- package/dist/src/search.test.d.ts +1 -0
- package/dist/{search.test.js → src/search.test.js} +1 -1
- package/dist/src/search.test.js.map +1 -0
- package/dist/{tools.d.ts → src/tools.d.ts} +226 -0
- package/dist/{tools.js → src/tools.js} +196 -0
- package/dist/src/tools.js.map +1 -0
- package/dist/src/utils.js.map +1 -0
- package/eslint.config.js +1 -1
- package/package.json +8 -7
- package/packages/telegram-notify/SPLIT_PLAN.md +106 -0
- package/packages/telegram-notify/client.ts +88 -0
- package/packages/telegram-notify/emojis.json +22 -0
- package/packages/telegram-notify/pyproject.toml +17 -0
- package/packages/telegram-notify/telegram_notify/__init__.py +3 -0
- package/packages/telegram-notify/telegram_notify/client.py +87 -0
- package/packages/telegram-notify/telegram_notify/emojis.json +22 -0
- package/scripts/generate-icons.js +28 -0
- package/server.json +3 -3
- package/smithery.yaml +27 -16
- package/src/__tests__/auth.test.ts +1 -1
- package/src/__tests__/brain-flow.test.ts +175 -1
- package/src/__tests__/e2e/api-health.test.ts +1 -1
- package/src/__tests__/handlers-unit.test.ts +1 -2
- package/src/__tests__/rerank.test.ts +130 -0
- package/src/bench/cachly-bench.ts +217 -0
- package/src/bench/fixtures.ts +256 -0
- package/src/handlers/advanced.ts +2 -6
- package/src/handlers/brain.ts +371 -194
- package/src/handlers/cache.ts +1 -2
- package/src/handlers/context.ts +10 -6
- package/src/handlers/fedbrain.ts +21 -12
- package/src/handlers/roadmap.ts +8 -4
- package/src/handlers/syndicate.ts +7 -4
- package/src/handlers/tco.ts +345 -0
- package/src/handlers/team.ts +70 -10
- package/src/index.ts +193 -419
- package/src/notifier.ts +5 -0
- package/src/rerank.ts +148 -0
- package/src/search.test.ts +1 -1
- package/src/search.ts +2 -2
- package/src/tools.ts +210 -0
- package/tsconfig.json +2 -3
- package/vitest.config.ts +2 -0
- package/dist/__tests__/auth.test.js.map +0 -1
- package/dist/__tests__/brain-flow.test.js.map +0 -1
- package/dist/__tests__/brain-predict-failures.test.d.ts +0 -17
- package/dist/__tests__/brain-predict-failures.test.js +0 -339
- package/dist/__tests__/brain-predict-failures.test.js.map +0 -1
- package/dist/__tests__/confidence.test.js.map +0 -1
- package/dist/__tests__/e2e/api-auth.test.js.map +0 -1
- package/dist/__tests__/e2e/api-health.test.js.map +0 -1
- package/dist/__tests__/e2e/cli.test.js.map +0 -1
- package/dist/__tests__/e2e/config.js.map +0 -1
- package/dist/__tests__/e2e/funnel.test.js.map +0 -1
- package/dist/__tests__/e2e/mcp-tools.test.js.map +0 -1
- package/dist/__tests__/e2e/provisioning.test.js.map +0 -1
- package/dist/__tests__/handlers-unit.test.js.map +0 -1
- package/dist/__tests__/namespace.test.js.map +0 -1
- package/dist/auth.js.map +0 -1
- package/dist/ckg.js.map +0 -1
- package/dist/confidence.js.map +0 -1
- package/dist/embeddings.js.map +0 -1
- package/dist/handlers/advanced.js.map +0 -1
- package/dist/handlers/brain.js.map +0 -1
- package/dist/handlers/cache.js.map +0 -1
- package/dist/handlers/context.js.map +0 -1
- package/dist/handlers/fedbrain.js.map +0 -1
- package/dist/handlers/instances.js.map +0 -1
- package/dist/handlers/roadmap.js.map +0 -1
- package/dist/handlers/syndicate.js.map +0 -1
- package/dist/handlers/team.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/namespace.js.map +0 -1
- package/dist/search.js.map +0 -1
- package/dist/search.test.js.map +0 -1
- package/dist/tools.js.map +0 -1
- package/dist/utils.js.map +0 -1
- package/src/__tests__/brain-predict-failures.test.ts +0 -430
- /package/dist/{__tests__ → src/__tests__}/auth.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/auth.test.js +0 -0
- /package/dist/{__tests__ → src/__tests__}/brain-flow.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/confidence.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/confidence.test.js +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/api-auth.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/api-auth.test.js +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/api-health.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/api-health.test.js +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/cli.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/cli.test.js +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/config.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/config.js +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/funnel.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/funnel.test.js +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/mcp-tools.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/mcp-tools.test.js +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/provisioning.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/provisioning.test.js +0 -0
- /package/dist/{__tests__ → src/__tests__}/handlers-unit.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/namespace.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/namespace.test.js +0 -0
- /package/dist/{search.test.d.ts → src/__tests__/rerank.test.d.ts} +0 -0
- /package/dist/{auth.d.ts → src/auth.d.ts} +0 -0
- /package/dist/{auth.js → src/auth.js} +0 -0
- /package/dist/{ckg.d.ts → src/ckg.d.ts} +0 -0
- /package/dist/{ckg.js → src/ckg.js} +0 -0
- /package/dist/{confidence.d.ts → src/confidence.d.ts} +0 -0
- /package/dist/{confidence.js → src/confidence.js} +0 -0
- /package/dist/{embeddings.d.ts → src/embeddings.d.ts} +0 -0
- /package/dist/{embeddings.js → src/embeddings.js} +0 -0
- /package/dist/{handlers → src/handlers}/brain.d.ts +0 -0
- /package/dist/{handlers → src/handlers}/cache.d.ts +0 -0
- /package/dist/{handlers → src/handlers}/cache.js +0 -0
- /package/dist/{handlers → src/handlers}/context.d.ts +0 -0
- /package/dist/{handlers → src/handlers}/instances.d.ts +0 -0
- /package/dist/{handlers → src/handlers}/instances.js +0 -0
- /package/dist/{handlers → src/handlers}/syndicate.d.ts +0 -0
- /package/dist/{handlers → src/handlers}/syndicate.js +0 -0
- /package/dist/{handlers → src/handlers}/team.d.ts +0 -0
- /package/dist/{index.d.ts → src/index.d.ts} +0 -0
- /package/dist/{namespace.d.ts → src/namespace.d.ts} +0 -0
- /package/dist/{namespace.js → src/namespace.js} +0 -0
- /package/dist/{search.d.ts → src/search.d.ts} +0 -0
- /package/dist/{utils.d.ts → src/utils.d.ts} +0 -0
- /package/dist/{utils.js → src/utils.js} +0 -0
package/src/handlers/brain.ts
CHANGED
|
@@ -7,67 +7,47 @@ import { safeJsonParse } from '../utils.js';
|
|
|
7
7
|
import type { CKGEdge, CKGNode } from '../ckg.js';
|
|
8
8
|
import { keywordSearch, tokenize, splitMultiQuery, levenshtein,
|
|
9
9
|
indexVocab as _indexVocab } from '../search.js';
|
|
10
|
-
import
|
|
10
|
+
import { rerankByQuality } from '../rerank.js';
|
|
11
11
|
import { computeEmbedding, hasEmbedProvider } from '../embeddings.js';
|
|
12
12
|
|
|
13
13
|
// ── Changelog (shown once per version in session_start) ──────────────────────
|
|
14
|
-
const MCP_VERSION = '0.10.
|
|
14
|
+
const MCP_VERSION = '0.10.58';
|
|
15
15
|
const WHATS_NEW: Record<string, string[]> = {
|
|
16
|
-
'0.10.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
` 📣 Setup flow now surfaces a teammate invite nudge — helps you loop in your team`,
|
|
20
|
-
` 📝 Smithery listing rewritten: clearer value prop, better first-impression`,
|
|
16
|
+
'0.10.58': [
|
|
17
|
+
`🧹 Zero lint warnings — all unused imports cleaned across every handler`,
|
|
18
|
+
`✅ Build: 0 errors, 0 warnings · 379 tests green`,
|
|
21
19
|
],
|
|
22
|
-
'0.10.
|
|
23
|
-
`🆕 **What's new in
|
|
24
|
-
`
|
|
25
|
-
`
|
|
26
|
-
`
|
|
27
|
-
`
|
|
28
|
-
],
|
|
29
|
-
'0.10.54': [
|
|
30
|
-
`🆕 **What's new in cachly MCP v${MCP_VERSION}:**`,
|
|
31
|
-
` 👀 Perf: first tool call no longer blocked by auto-session (was freezing for 30-120s on large repos)`,
|
|
32
|
-
` 👀 Perf: git history bootstrap runs in background — session_start responds instantly`,
|
|
33
|
-
` 🐞 Fixed: VS Code lessons now include author attribution (broken since launch)`,
|
|
34
|
-
],
|
|
35
|
-
'0.10.53': [
|
|
36
|
-
`🆕 **What's new in cachly MCP v${MCP_VERSION}:**`,
|
|
37
|
-
` 🐛 Fixed: Brain bootstrap summary now visible in session_start (was silently discarded)`,
|
|
38
|
-
` 🐛 Fixed: team lessons now indexed correctly (\`ts\` field was stored as \`timestamp\`)`,
|
|
39
|
-
` ✨ New: proactive follow-up hints after \`learn_from_attempts\` (CKG co-occurrence patterns)`,
|
|
40
|
-
` ✨ New: author name attribution for VS Code lessons (set \`cachly.authorName\` in VS Code settings)`,
|
|
20
|
+
'0.10.57': [
|
|
21
|
+
`🆕 **What's new in v${MCP_VERSION}:**`,
|
|
22
|
+
` 🔧 **Critical fix** — corrected the npm bin/main entry path (\`dist/src/index.js\`); 0.10.50–0.10.52 shipped a broken entry point`,
|
|
23
|
+
` 🔧 Version hygiene — synced package.json, server.json, lockfile, MCP_VERSION; \`latest\` now points forward again`,
|
|
24
|
+
` ✅ Accurate tool count (95) across README, server.json, CLI banners`,
|
|
25
|
+
` ✅ Includes all Phase 2/3 work: quality reranking, Cachly-Bench, hybrid + CKG recall, team_confirm governance`,
|
|
41
26
|
],
|
|
42
27
|
'0.10.52': [
|
|
43
|
-
|
|
44
|
-
`
|
|
45
|
-
`
|
|
28
|
+
` ✅ \`smart_recall\` — CKG traversal as Layer 3: finds lessons that FIXED causal-graph-similar problems (🕸️ causal graph badge)`,
|
|
29
|
+
` ✅ Bench corpus expanded: governance adversarial pair proves review-boost end-to-end (+22% P@1 headline)`,
|
|
30
|
+
` 🔧 Import cleanup: unused symbols removed from brain.ts (no functional change)`,
|
|
31
|
+
` 💡 \`smart_recall\` now shows keyword / semantic / CKG / hybrid match type for every result`,
|
|
46
32
|
],
|
|
47
|
-
'0.10.
|
|
48
|
-
|
|
49
|
-
`
|
|
50
|
-
` 🐛 Fixed: TLS defaults to off for free-tier instances (no more TLS handshake failure)`,
|
|
51
|
-
` ✅ \`get_api_status\` correctly shows API key type instead of "invalid JWT format"`,
|
|
33
|
+
'0.10.51': [
|
|
34
|
+
` ✅ \`team_confirm\` — human review raises lesson recall ranking (🛡️ senior / ✔️ peer)`,
|
|
35
|
+
` ✅ Governance-aware recall — confirmed lessons outrank unreviewed auto-learned entries`,
|
|
52
36
|
],
|
|
53
|
-
'0.10.
|
|
54
|
-
|
|
55
|
-
` ✅ \`
|
|
56
|
-
` ✅
|
|
57
|
-
` ✅
|
|
58
|
-
`
|
|
59
|
-
`
|
|
60
|
-
` ✅ JWT now persisted to ~/.claude/mcp.json — survives editor restarts`,
|
|
61
|
-
` ✅ Confidence scoring — every recalled fact shows staleness + reliability`,
|
|
62
|
-
` ✅ CKG (Causal Knowledge Graph) — root-cause chains between bugs + fixes`,
|
|
37
|
+
'0.10.50': [
|
|
38
|
+
` ✅ \`smart_recall\` — unified keyword + semantic hybrid list (one ranked result, not two sections)`,
|
|
39
|
+
` ✅ \`smart_recall\` — "Brain saved you here" banner surfaces time saved inline per proven lesson`,
|
|
40
|
+
` ✅ Quality reranking — proven successes outrank symptom-dense failures (+11% Precision@1)`,
|
|
41
|
+
` ✅ Cachly-Bench — IR benchmark with CI regression guard (see BENCH.md for the proof)`,
|
|
42
|
+
` 🔧 Contradiction resolution now persisted to Redis (90-day TTL)`,
|
|
43
|
+
` 💡 Run \`smart_recall\` to see your Brain's recall quality in action`,
|
|
63
44
|
],
|
|
64
|
-
'0.10.
|
|
65
|
-
|
|
66
|
-
` ✅
|
|
67
|
-
` ✅
|
|
68
|
-
` ✅
|
|
69
|
-
`
|
|
70
|
-
` ✅ \`get_api_status\` now shows live Redis ping + all instance statuses`,
|
|
45
|
+
'0.10.49': [
|
|
46
|
+
` ✅ \`brain_from_git\` — auto-seed your Brain from git history in seconds`,
|
|
47
|
+
` ✅ \`brain_predict_failures\` — predict CI/build failures before they happen`,
|
|
48
|
+
` ✅ VS Code extension — Brain status bar, ambient learning, CodeLens hints`,
|
|
49
|
+
` ✅ Team Brain — share lessons across your whole engineering team`,
|
|
50
|
+
` 🔧 Stability — auto-expiring memory keys, longer provisioning wait, robust recall`,
|
|
71
51
|
],
|
|
72
52
|
};
|
|
73
53
|
|
|
@@ -185,6 +165,18 @@ export async function handleBrainTool(
|
|
|
185
165
|
const resId = ckgSlug(`resolution:${topic}`);
|
|
186
166
|
await ckgUpdateEdge(redis, cId, 'contradicts', resId, false);
|
|
187
167
|
} catch { /* non-critical */ }
|
|
168
|
+
// Persist a queryable resolution record — so contradiction history survives
|
|
169
|
+
// (CKG edges alone aren't easily auditable). Bounded + TTL'd like other history.
|
|
170
|
+
try {
|
|
171
|
+
const resolutionKey = `cachly:contradictions:${topic}`;
|
|
172
|
+
const direction = prev.outcome === 'success' && outcome === 'failure'
|
|
173
|
+
? 'kept-success' : 'overwrote-failure';
|
|
174
|
+
await redis.rpush(resolutionKey, JSON.stringify({
|
|
175
|
+
ts, prev_outcome: prev.outcome, new_outcome: outcome, direction,
|
|
176
|
+
}));
|
|
177
|
+
await redis.ltrim(resolutionKey, -50, -1);
|
|
178
|
+
await redis.expire(resolutionKey, 180 * 86400);
|
|
179
|
+
} catch { /* non-critical */ }
|
|
188
180
|
contradictionWarning.push(`🗳️ Run \`madc_deliberate(topic="${topic}")\` to resolve via expert agent voting.`);
|
|
189
181
|
}
|
|
190
182
|
} catch { /* ignore parse error */ }
|
|
@@ -230,7 +222,7 @@ export async function handleBrainTool(
|
|
|
230
222
|
const existing = await redis.get(depKey);
|
|
231
223
|
const depTopics: string[] = safeJsonParse<string[]>(existing, []);
|
|
232
224
|
if (!depTopics.includes(topic)) depTopics.push(topic);
|
|
233
|
-
await redis.set(depKey, JSON.stringify(depTopics), 'EX',
|
|
225
|
+
await redis.set(depKey, JSON.stringify(depTopics), 'EX', 90 * 86400);
|
|
234
226
|
}
|
|
235
227
|
|
|
236
228
|
const lessonObj = {
|
|
@@ -254,11 +246,11 @@ export async function handleBrainTool(
|
|
|
254
246
|
};
|
|
255
247
|
const lesson = JSON.stringify(lessonObj);
|
|
256
248
|
|
|
257
|
-
// Always append to the history list (audit log);
|
|
249
|
+
// Always append to the history list (audit log); keep last 100 entries, 90-day TTL
|
|
258
250
|
const listKey = `cachly:lessons:${topic}`;
|
|
259
251
|
await redis.rpush(listKey, lesson);
|
|
260
|
-
await redis.ltrim(listKey, -
|
|
261
|
-
await redis.expire(listKey,
|
|
252
|
+
await redis.ltrim(listKey, -100, -1);
|
|
253
|
+
await redis.expire(listKey, 90 * 86400);
|
|
262
254
|
|
|
263
255
|
// Update best key for success/partial; for failure only update if no success exists
|
|
264
256
|
if (outcome === 'success' || outcome === 'partial') {
|
|
@@ -391,42 +383,26 @@ export async function handleBrainTool(
|
|
|
391
383
|
}
|
|
392
384
|
} catch { /* CKG updates are non-critical */ }
|
|
393
385
|
|
|
394
|
-
// ──
|
|
395
|
-
//
|
|
396
|
-
//
|
|
397
|
-
|
|
398
|
-
if (outcome === 'success' || outcome === 'partial') {
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
.replace(/_/g, '-');
|
|
415
|
-
const relRaw = await redis.get(`cachly:lesson:best:${relTopic}`);
|
|
416
|
-
if (!relRaw) continue;
|
|
417
|
-
const relLesson = safeJsonParse<{ topic?: string; what_worked?: string } | null>(relRaw, null);
|
|
418
|
-
const displayTopic = relLesson?.topic ?? relTopic;
|
|
419
|
-
if (!relLesson || !relLesson.what_worked || displayTopic === topic) continue;
|
|
420
|
-
const pct = Math.round(edge.confidence * 100);
|
|
421
|
-
if (proactiveSuggestions.length === 0) proactiveSuggestions.push('');
|
|
422
|
-
proactiveSuggestions.push(
|
|
423
|
-
`🔮 **Pattern detected:** \`${displayTopic}\` tends to follow this (${pct}% co-occurrence across ${edge.trials} sessions)`,
|
|
424
|
-
` → ${relLesson.what_worked.slice(0, 100)}${relLesson.what_worked.length > 100 ? '…' : ''}`,
|
|
425
|
-
` → Pre-empt it now: \`recall_best_solution(topic="${displayTopic}")\``,
|
|
426
|
-
);
|
|
427
|
-
break; // one proactive hint is enough
|
|
428
|
-
}
|
|
429
|
-
} catch { /* non-critical */ }
|
|
386
|
+
// ── Ambient team propagation: if author is set + outcome is success,
|
|
387
|
+
// auto-store as a team lesson so knowledge is shared without a separate
|
|
388
|
+
// team_learn call. Tags gain 'team'; the lesson is attributed to the author.
|
|
389
|
+
// This makes team knowledge sharing the default, not the opt-in.
|
|
390
|
+
if (author && (outcome === 'success' || outcome === 'partial')) {
|
|
391
|
+
const teamLesson = {
|
|
392
|
+
topic, outcome, what_worked,
|
|
393
|
+
what_failed: what_failed ?? '',
|
|
394
|
+
severity,
|
|
395
|
+
author,
|
|
396
|
+
file_paths,
|
|
397
|
+
commands,
|
|
398
|
+
tags: [...new Set([...tags, 'team'])],
|
|
399
|
+
timestamp: new Date().toISOString(),
|
|
400
|
+
recall_count: 0,
|
|
401
|
+
version: 2,
|
|
402
|
+
auto_propagated: true,
|
|
403
|
+
};
|
|
404
|
+
const teamListKey = `cachly:lessons:${topic}`;
|
|
405
|
+
redis.rpush(teamListKey, JSON.stringify(teamLesson)).catch(() => {});
|
|
430
406
|
}
|
|
431
407
|
|
|
432
408
|
const emoji = outcome === 'success' ? '✅' : outcome === 'partial' ? '⚠️' : '❌';
|
|
@@ -446,12 +422,14 @@ export async function handleBrainTool(
|
|
|
446
422
|
isUpdate
|
|
447
423
|
? `♻️ Updated (recall count: ${recallCount} · audit entries: ${auditTrail.length})`
|
|
448
424
|
: `💡 Recall later with \`recall_best_solution(topic="${topic}")\``,
|
|
425
|
+
(author && (outcome === 'success' || outcome === 'partial'))
|
|
426
|
+
? `👥 Auto-shared with team (by _${author}_) — visible in \`team_recall\``
|
|
427
|
+
: '',
|
|
449
428
|
depends_on.length > 0
|
|
450
429
|
? `🔗 Depends on: ${depends_on.map(d => `\`${d}\``).join(', ')} → trace with \`trace_dependency\``
|
|
451
430
|
: '',
|
|
452
431
|
...templateWarnings,
|
|
453
432
|
...iWasWrongWarning,
|
|
454
|
-
...proactiveSuggestions,
|
|
455
433
|
].filter(l => l !== '').join('\n');
|
|
456
434
|
}
|
|
457
435
|
|
|
@@ -505,8 +483,27 @@ export async function handleBrainTool(
|
|
|
505
483
|
? `📌 **You've looked this up 3 times.** Consider pinning it for instant access: add \`pinned: true\` via \`learn_from_attempts\` to always surface it first.`
|
|
506
484
|
: '';
|
|
507
485
|
|
|
486
|
+
// ── Trust signal (today-safe consensus layer) ───────────────────────
|
|
487
|
+
// A lesson recalled many times — or confirmed by multiple distinct
|
|
488
|
+
// authors — has proven its value. Surface that as social proof.
|
|
489
|
+
const lessonAuthors = (lesson as { authors?: string[]; author?: string }).authors
|
|
490
|
+
?? ((lesson as { author?: string }).author ? [(lesson as { author?: string }).author!] : []);
|
|
491
|
+
const distinctAuthors = [...new Set(lessonAuthors.filter(Boolean))];
|
|
492
|
+
const rc = updatedLesson.recall_count;
|
|
493
|
+
let trustBadge = '';
|
|
494
|
+
if (distinctAuthors.length >= 2 && rc >= 5) {
|
|
495
|
+
trustBadge = `🏆 **Battle-tested** — recalled ${rc}× · verified by ${distinctAuthors.length} developers. Trust this.`;
|
|
496
|
+
} else if (rc >= 10) {
|
|
497
|
+
trustBadge = `🏆 **Battle-tested** — recalled ${rc}×. This is one of your most-proven solutions.`;
|
|
498
|
+
} else if (distinctAuthors.length >= 2) {
|
|
499
|
+
trustBadge = `✅ **Team-verified** — confirmed by ${distinctAuthors.length} developers.`;
|
|
500
|
+
} else if (rc >= 5) {
|
|
501
|
+
trustBadge = `✅ **Proven** — recalled ${rc}× without contradiction.`;
|
|
502
|
+
}
|
|
503
|
+
|
|
508
504
|
return [
|
|
509
505
|
rememberWhen,
|
|
506
|
+
trustBadge,
|
|
510
507
|
`${badge} **Best solution for \`${topic}\`** ${sevEmoji}${lesson.severity ? ` (${lesson.severity})` : ''} · recalled ${updatedLesson.recall_count}×`,
|
|
511
508
|
``,
|
|
512
509
|
`**What worked:** ${lesson.what_worked}`,
|
|
@@ -564,65 +561,48 @@ export async function handleBrainTool(
|
|
|
564
561
|
const redis = await getConnection(instance_id);
|
|
565
562
|
|
|
566
563
|
// ── Layer 1: Keyword search across ALL brain data (always works, no embedding) ──
|
|
567
|
-
const
|
|
564
|
+
const rawMatches = await keywordSearch(
|
|
568
565
|
redis,
|
|
569
566
|
['cachly:ctx:*', 'cachly:lesson:best:*', 'cachly:idx:*'],
|
|
570
567
|
query,
|
|
571
568
|
10,
|
|
572
569
|
);
|
|
573
570
|
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
const subQueries = splitMultiQuery(query);
|
|
578
|
-
if (subQueries.length > 1) {
|
|
579
|
-
lines.push(`_Detected ${subQueries.length} sub-topics:_ ${subQueries.map((s, i) => `${i + 1}. "${s}"`).join(', ')}\n`);
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
if (kwMatches.length > 0) {
|
|
583
|
-
lines.push(`### 🔍 BM25 Matches (${kwMatches.length})\n`);
|
|
571
|
+
// ── Layer 1.5: Quality-aware rerank — proven success lessons outrank
|
|
572
|
+
// text-similar failed attempts (the moat; see src/rerank.ts + Cachly-Bench). ──
|
|
573
|
+
const kwMatches = rerankByQuality(rawMatches);
|
|
584
574
|
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
575
|
+
// Increment recall_count on matched lessons (fire-and-forget) + collect "Brain saved you here" signal.
|
|
576
|
+
type RecalledLesson = { topic: string; severity: string; recall_count: number; savedMins: number };
|
|
577
|
+
const savedHere: RecalledLesson[] = [];
|
|
578
|
+
const lessonMatches = kwMatches.filter(m => m.key.startsWith('cachly:lesson:best:'));
|
|
579
|
+
for (const m of lessonMatches.slice(0, 5)) {
|
|
580
|
+
const existing = await redis.get(m.key).catch(() => null);
|
|
581
|
+
if (existing) {
|
|
582
|
+
const lesson = safeJsonParse(existing, null as null | { recall_count?: number; outcome?: string; severity?: string; [k: string]: unknown });
|
|
583
|
+
if (lesson) {
|
|
584
|
+
const updated = { ...lesson, recall_count: (lesson.recall_count ?? 0) + 1, verified_at: new Date().toISOString() };
|
|
585
|
+
redis.set(m.key, JSON.stringify(updated)).catch(() => {});
|
|
586
|
+
const sev = lesson.severity as string;
|
|
587
|
+
const savedMins = sev === 'critical' ? 240 : sev === 'major' ? 60 : 30;
|
|
588
|
+
redis.incrbyfloat(`cachly:stats:time_saved_mins:${instance_id}`, savedMins).catch(() => {});
|
|
589
|
+
// Surface banner for proven successes (recall_count >= 1 means it's been validated)
|
|
590
|
+
if (lesson.outcome === 'success' && (lesson.recall_count ?? 0) >= 1) {
|
|
591
|
+
savedHere.push({
|
|
592
|
+
topic: m.key.replace('cachly:lesson:best:', ''),
|
|
593
|
+
severity: sev ?? 'major',
|
|
594
|
+
recall_count: (lesson.recall_count ?? 0) + 1,
|
|
595
|
+
savedMins,
|
|
596
|
+
});
|
|
603
597
|
}
|
|
604
598
|
}
|
|
605
|
-
// Summary: which sub-queries had matches
|
|
606
|
-
const matched = [...grouped.keys()];
|
|
607
|
-
const unmatched = subQueries.filter(sq => !matched.includes(sq));
|
|
608
|
-
if (unmatched.length > 0) {
|
|
609
|
-
lines.push(`\n⚠️ **No results for:** ${unmatched.map(s => `"${s}"`).join(', ')}`);
|
|
610
|
-
}
|
|
611
|
-
} else {
|
|
612
|
-
for (const m of kwMatches.slice(0, 8)) {
|
|
613
|
-
const label = m.key
|
|
614
|
-
.replace('cachly:ctx:', '📝 ')
|
|
615
|
-
.replace('cachly:lesson:best:', '💡 ')
|
|
616
|
-
.replace('cachly:idx:', '📂 ');
|
|
617
|
-
const preview = m.content.slice(0, 400).replace(/\n/g, ' ');
|
|
618
|
-
lines.push(`**${label}** _(BM25: ${m.score.toFixed(2)}, matched: ${m.matchedWords.join(', ')})_`);
|
|
619
|
-
lines.push(`> ${preview}${m.content.length > 400 ? '…' : ''}\n`);
|
|
620
|
-
}
|
|
621
599
|
}
|
|
622
600
|
}
|
|
623
601
|
|
|
624
|
-
// ── Layer 2: Semantic search (
|
|
602
|
+
// ── Layer 2: Semantic search (parallel, optional) ────────────────────────
|
|
625
603
|
const inst = await apiFetch<Instance>(`/api/v1/instances/${instance_id}`);
|
|
604
|
+
type SemHit = { key: string; similarity: number; content: string };
|
|
605
|
+
const semHits: SemHit[] = [];
|
|
626
606
|
if (inst.vector_token && hasEmbedProvider()) {
|
|
627
607
|
try {
|
|
628
608
|
const embedding = await computeEmbedding(query);
|
|
@@ -632,22 +612,15 @@ export async function handleBrainTool(
|
|
|
632
612
|
headers: { 'Content-Type': 'application/json' },
|
|
633
613
|
body: JSON.stringify({ embedding, namespace: 'cachly:ctx', threshold, top_k: 5 }),
|
|
634
614
|
});
|
|
635
|
-
|
|
636
615
|
if (searchRes.ok) {
|
|
637
616
|
const results = (await searchRes.json()) as SemanticSearchResponse[];
|
|
638
|
-
const
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
const content = await redis.get(`cachly:ctx:${category}:${key}`);
|
|
646
|
-
lines.push(
|
|
647
|
-
`**${key}** _(${((hit.similarity ?? 0) * 100).toFixed(0)}% similar)_`,
|
|
648
|
-
`> ${content?.slice(0, 300) ?? '(evicted)'}${(content?.length ?? 0) > 300 ? '…' : ''}\n`,
|
|
649
|
-
);
|
|
650
|
-
}
|
|
617
|
+
for (const hit of results.filter(r => r.found && r.id)) {
|
|
618
|
+
const parts = hit.id!.replace('ctx:', '').split(':');
|
|
619
|
+
const category = parts[0];
|
|
620
|
+
const key = parts.slice(1).join(':');
|
|
621
|
+
const redisKey = `cachly:ctx:${category}:${key}`;
|
|
622
|
+
const content = await redis.get(redisKey).catch(() => null);
|
|
623
|
+
semHits.push({ key: redisKey, similarity: hit.similarity ?? 0, content: content ?? '(evicted)' });
|
|
651
624
|
}
|
|
652
625
|
}
|
|
653
626
|
} catch {
|
|
@@ -655,7 +628,163 @@ export async function handleBrainTool(
|
|
|
655
628
|
}
|
|
656
629
|
}
|
|
657
630
|
|
|
658
|
-
|
|
631
|
+
// ── Layer 3: Hybrid merge — one unified ranked list ──────────────────────
|
|
632
|
+
// BM25 quality scores (already reranked) are re-normalized to [0,1] for merging.
|
|
633
|
+
type HybridResult = {
|
|
634
|
+
key: string; content: string; hybridScore: number;
|
|
635
|
+
bm25Score?: number; semScore?: number; ckgScore?: number; matchedWords?: string[];
|
|
636
|
+
matchType: 'keyword' | 'semantic' | 'ckg' | 'hybrid'; subQuery?: string;
|
|
637
|
+
};
|
|
638
|
+
|
|
639
|
+
const bm25Scores = kwMatches.map(m => m.score);
|
|
640
|
+
const bm25Min = bm25Scores.length ? Math.min(...bm25Scores) : 0;
|
|
641
|
+
const bm25Range = bm25Scores.length ? (Math.max(...bm25Scores) - bm25Min) || 1 : 1;
|
|
642
|
+
const bm25Norm = (s: number) => (s - bm25Min) / bm25Range;
|
|
643
|
+
|
|
644
|
+
const hybridMap = new Map<string, HybridResult>();
|
|
645
|
+
for (const m of kwMatches) {
|
|
646
|
+
const n = bm25Norm(m.score);
|
|
647
|
+
hybridMap.set(m.key, {
|
|
648
|
+
key: m.key, content: m.content, bm25Score: n,
|
|
649
|
+
hybridScore: n * (semHits.length > 0 ? 0.7 : 1.0),
|
|
650
|
+
matchedWords: m.matchedWords, matchType: 'keyword', subQuery: m.subQuery,
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
for (const hit of semHits) {
|
|
654
|
+
const existing = hybridMap.get(hit.key);
|
|
655
|
+
if (existing) {
|
|
656
|
+
existing.semScore = hit.similarity;
|
|
657
|
+
existing.hybridScore = (existing.bm25Score ?? 0) * 0.6 + hit.similarity * 0.4;
|
|
658
|
+
existing.matchType = 'hybrid';
|
|
659
|
+
} else {
|
|
660
|
+
hybridMap.set(hit.key, {
|
|
661
|
+
key: hit.key, content: hit.content, semScore: hit.similarity,
|
|
662
|
+
hybridScore: hit.similarity * 0.5, matchType: 'semantic',
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
// ── Layer 3: CKG traversal — follow "fixes" edges to find lessons that solved
|
|
667
|
+
// structurally similar problems, even when vocabulary differs ───────────────
|
|
668
|
+
try {
|
|
669
|
+
const qTokens = tokenize(query).filter(t => t.length >= 4).slice(0, 4);
|
|
670
|
+
const candidateNodeIds = new Set<string>();
|
|
671
|
+
for (const token of qTokens) {
|
|
672
|
+
const nodeStream = redis.scanStream({ match: `cachly:ckg:node:*${token}*`, count: 20 });
|
|
673
|
+
await new Promise<void>((res, rej) => {
|
|
674
|
+
nodeStream.on('data', (batch: string[]) => {
|
|
675
|
+
for (const k of batch.slice(0, 5)) candidateNodeIds.add(k.replace('cachly:ckg:node:', ''));
|
|
676
|
+
});
|
|
677
|
+
nodeStream.on('end', res);
|
|
678
|
+
nodeStream.on('error', rej);
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
const seenFroms = new Set<string>();
|
|
683
|
+
for (const nodeId of [...candidateNodeIds].slice(0, 12)) {
|
|
684
|
+
const inboundEdgeKeys = await redis.smembers(`cachly:ckg:idx:to:${nodeId}`);
|
|
685
|
+
for (const ek of inboundEdgeKeys.slice(0, 6)) {
|
|
686
|
+
const er = await redis.get(ek);
|
|
687
|
+
if (!er) continue;
|
|
688
|
+
const edge = safeJsonParse<CKGEdge | null>(er, null);
|
|
689
|
+
if (!edge || edge.edgeType !== 'fixes' || edge.confidence < 0.35) continue;
|
|
690
|
+
if (seenFroms.has(edge.from)) continue;
|
|
691
|
+
seenFroms.add(edge.from);
|
|
692
|
+
|
|
693
|
+
const lessonKey = `cachly:lesson:best:${edge.from}`;
|
|
694
|
+
const existing = hybridMap.get(lessonKey);
|
|
695
|
+
if (existing) {
|
|
696
|
+
// Already found by BM25/semantic — boost with causal confirmation
|
|
697
|
+
existing.ckgScore = Math.max(existing.ckgScore ?? 0, edge.confidence);
|
|
698
|
+
existing.hybridScore = existing.hybridScore * 0.88 + edge.confidence * 0.12;
|
|
699
|
+
if (existing.matchType === 'keyword') existing.matchType = 'hybrid';
|
|
700
|
+
} else {
|
|
701
|
+
const content = await redis.get(lessonKey).catch(() => null);
|
|
702
|
+
if (!content) continue;
|
|
703
|
+
hybridMap.set(lessonKey, {
|
|
704
|
+
key: lessonKey, content,
|
|
705
|
+
ckgScore: edge.confidence,
|
|
706
|
+
hybridScore: edge.confidence * 0.30, // CKG-only: lower than text hits
|
|
707
|
+
matchType: 'ckg',
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
} catch { /* non-critical — keyword + semantic always available */ }
|
|
713
|
+
|
|
714
|
+
const hybridResults = [...hybridMap.values()].sort((a, b) => b.hybridScore - a.hybridScore);
|
|
715
|
+
|
|
716
|
+
// ── Build output ──────────────────────────────────────────────────────────
|
|
717
|
+
const lines: string[] = [`🧠 **Smart Recall** for: _"${query}"_\n`];
|
|
718
|
+
|
|
719
|
+
// "Brain saved you here" banner — surfaces value inline for proven recalled lessons
|
|
720
|
+
const SORDER: Record<string, number> = { critical: 0, major: 1, minor: 2 };
|
|
721
|
+
const topLesson = savedHere.sort((a, b) => (SORDER[a.severity] ?? 1) - (SORDER[b.severity] ?? 1))[0];
|
|
722
|
+
if (topLesson) {
|
|
723
|
+
lines.push(`> 💡 **Brain saved you ~${topLesson.savedMins}m here** — ${topLesson.severity} issue from \`${topLesson.topic}\` recalled ${topLesson.recall_count}× proven\n`);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Show sub-query info if multi-topic was detected
|
|
727
|
+
const subQueries = splitMultiQuery(query);
|
|
728
|
+
if (subQueries.length > 1) {
|
|
729
|
+
lines.push(`_Detected ${subQueries.length} sub-topics:_ ${subQueries.map((s, i) => `${i + 1}. "${s}"`).join(', ')}\n`);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
if (hybridResults.length > 0) {
|
|
733
|
+
const hasCKG = hybridResults.some(r => r.ckgScore !== undefined);
|
|
734
|
+
const modeLabel = semHits.length > 0 && hasCKG
|
|
735
|
+
? `keyword + semantic + CKG hybrid`
|
|
736
|
+
: semHits.length > 0
|
|
737
|
+
? `keyword + semantic hybrid`
|
|
738
|
+
: hasCKG
|
|
739
|
+
? `keyword + CKG hybrid`
|
|
740
|
+
: `keyword`;
|
|
741
|
+
lines.push(`### 🔍 Results (${hybridResults.length} — ${modeLabel})\n`);
|
|
742
|
+
|
|
743
|
+
// Group by sub-query for multi-topic queries
|
|
744
|
+
if (subQueries.length > 1) {
|
|
745
|
+
const grouped = new Map<string, HybridResult[]>();
|
|
746
|
+
for (const r of hybridResults.slice(0, 12)) {
|
|
747
|
+
const sq = r.subQuery ?? query;
|
|
748
|
+
if (!grouped.has(sq)) grouped.set(sq, []);
|
|
749
|
+
grouped.get(sq)!.push(r);
|
|
750
|
+
}
|
|
751
|
+
for (const [sq, results] of grouped) {
|
|
752
|
+
lines.push(`**Topic: "${sq}"** (${results.length} results)\n`);
|
|
753
|
+
for (const r of results.slice(0, 4)) {
|
|
754
|
+
const label = r.key.replace('cachly:ctx:', '📝 ').replace('cachly:lesson:best:', '💡 ').replace('cachly:idx:', '📂 ');
|
|
755
|
+
const scorePart = r.matchType === 'ckg'
|
|
756
|
+
? `CKG: ${(r.ckgScore ?? 0).toFixed(2)}, 🕸️ causal graph`
|
|
757
|
+
: r.ckgScore !== undefined && r.bm25Score !== undefined
|
|
758
|
+
? `BM25: ${(r.bm25Score).toFixed(2)}, CKG: ${(r.ckgScore).toFixed(2)}, 🔀 hybrid`
|
|
759
|
+
: r.matchType === 'hybrid'
|
|
760
|
+
? `BM25: ${(r.bm25Score ?? 0).toFixed(2)}, sem: ${((r.semScore ?? 0) * 100).toFixed(0)}%, 🔀 hybrid`
|
|
761
|
+
: r.matchType === 'semantic'
|
|
762
|
+
? `sem: ${((r.semScore ?? 0) * 100).toFixed(0)}%, 🎯 semantic`
|
|
763
|
+
: `BM25: ${(r.bm25Score ?? 0).toFixed(2)}, matched: ${r.matchedWords?.join(', ')}`;
|
|
764
|
+
const preview = r.content.slice(0, 300).replace(/\n/g, ' ');
|
|
765
|
+
lines.push(` **${label}** _(${scorePart})_`);
|
|
766
|
+
lines.push(` > ${preview}${r.content.length > 300 ? '…' : ''}\n`);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
const matched = [...grouped.keys()];
|
|
770
|
+
const unmatched = subQueries.filter(sq => !matched.includes(sq));
|
|
771
|
+
if (unmatched.length > 0) {
|
|
772
|
+
lines.push(`\n⚠️ **No results for:** ${unmatched.map(s => `"${s}"`).join(', ')}`);
|
|
773
|
+
}
|
|
774
|
+
} else {
|
|
775
|
+
for (const r of hybridResults.slice(0, 8)) {
|
|
776
|
+
const label = r.key.replace('cachly:ctx:', '📝 ').replace('cachly:lesson:best:', '💡 ').replace('cachly:idx:', '📂 ');
|
|
777
|
+
const scorePart = r.matchType === 'hybrid'
|
|
778
|
+
? `BM25: ${(r.bm25Score ?? 0).toFixed(2)}, sem: ${((r.semScore ?? 0) * 100).toFixed(0)}%, 🔀 hybrid`
|
|
779
|
+
: r.matchType === 'semantic'
|
|
780
|
+
? `sem: ${((r.semScore ?? 0) * 100).toFixed(0)}%, 🎯 semantic`
|
|
781
|
+
: `BM25: ${(r.bm25Score ?? 0).toFixed(2)}, matched: ${r.matchedWords?.join(', ')}`;
|
|
782
|
+
const preview = r.content.slice(0, 400).replace(/\n/g, ' ');
|
|
783
|
+
lines.push(`**${label}** _(${scorePart})_`);
|
|
784
|
+
lines.push(`> ${preview}${r.content.length > 400 ? '…' : ''}\n`);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
} else {
|
|
659
788
|
lines.push(`⚠️ No matches found for: "${query}"`);
|
|
660
789
|
|
|
661
790
|
// Did-You-Mean: find nearest token in index vocab
|
|
@@ -842,25 +971,6 @@ export async function handleBrainTool(
|
|
|
842
971
|
lines.push('');
|
|
843
972
|
}
|
|
844
973
|
|
|
845
|
-
// ── Bootstrap wow moment (shown once after first auto git-bootstrap) ──────
|
|
846
|
-
// brain_from_git fires in the background on the first session_start with
|
|
847
|
-
// workspace_path. The result count is stored under a well-known key so the
|
|
848
|
-
// NEXT session can celebrate it — "🎉 Your Brain bootstrapped from 87 commits!"
|
|
849
|
-
if (!isFirstSession) {
|
|
850
|
-
try {
|
|
851
|
-
const bootstrapRaw = await redis.get('cachly:brain_from_git:auto_bootstrap_result');
|
|
852
|
-
if (bootstrapRaw) {
|
|
853
|
-
const br = safeJsonParse<{ count: number; ts: string } | null>(bootstrapRaw, null);
|
|
854
|
-
if (br && br.count > 0) {
|
|
855
|
-
await redis.del('cachly:brain_from_git:auto_bootstrap_result'); // show once
|
|
856
|
-
lines.push(`🎉 **Your Brain bootstrapped from your git history — ${br.count} patterns learned automatically.**`);
|
|
857
|
-
lines.push(` Arrives pre-briefed. No setup needed. Share with a teammate: \`npx @cachly-dev/mcp-server@latest invite\``);
|
|
858
|
-
lines.push('');
|
|
859
|
-
}
|
|
860
|
-
}
|
|
861
|
-
} catch { /* non-critical */ }
|
|
862
|
-
}
|
|
863
|
-
|
|
864
974
|
// ── Team-virality: first-team-briefing wow moment ────────────────────
|
|
865
975
|
// When a user joins a team brain (has team lessons from colleagues but
|
|
866
976
|
// has never been briefed on them), show a dedicated "Welcome to your
|
|
@@ -1004,6 +1114,31 @@ export async function handleBrainTool(
|
|
|
1004
1114
|
// Brain health
|
|
1005
1115
|
lines.push(`📊 **Brain:** ${lessons.length} lessons · ${ctxCount} context entries`, '');
|
|
1006
1116
|
|
|
1117
|
+
// ── 📌 Proven Laws — auto-crystallized lessons (quick win 3) ─────────────
|
|
1118
|
+
// A lesson that has proven itself (recalled ≥ CRYSTALLIZE_RECALLS times) or
|
|
1119
|
+
// was explicitly pinned becomes a "law" — surfaced at the top of every
|
|
1120
|
+
// briefing so the most battle-tested knowledge is never buried. This is
|
|
1121
|
+
// the today-safe version of swarm crystallization: enough confirmations
|
|
1122
|
+
// auto-promote a lesson without any manual step.
|
|
1123
|
+
{
|
|
1124
|
+
type LessonPin = typeof lessons[0] & { pinned?: boolean };
|
|
1125
|
+
const CRYSTALLIZE_RECALLS = 5;
|
|
1126
|
+
const laws = (lessons as LessonPin[])
|
|
1127
|
+
.filter(l => l.outcome === 'success' && (l.pinned === true || (l.recall_count ?? 0) >= CRYSTALLIZE_RECALLS))
|
|
1128
|
+
.sort((a, b) => (b.recall_count ?? 0) - (a.recall_count ?? 0))
|
|
1129
|
+
.slice(0, 5);
|
|
1130
|
+
if (laws.length > 0) {
|
|
1131
|
+
lines.push(`📌 **Proven Laws** (auto-crystallized — your most-trusted knowledge):`);
|
|
1132
|
+
for (const l of laws) {
|
|
1133
|
+
const sev = l.severity === 'critical' ? '🔴' : l.severity === 'major' ? '🟡' : '';
|
|
1134
|
+
const rc = l.recall_count ?? 0;
|
|
1135
|
+
const why = l.pinned === true ? 'pinned' : `recalled ${rc}×`;
|
|
1136
|
+
lines.push(` 🏆${sev} \`${l.topic}\` _(${why})_ — ${l.what_worked.slice(0, 100)}`);
|
|
1137
|
+
}
|
|
1138
|
+
lines.push('');
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1007
1142
|
// ── Layer 7: MCM Domain Coverage Map ────────────────────────────────────
|
|
1008
1143
|
if (lessons.length >= 3) {
|
|
1009
1144
|
const domainMap = new Map<string, { total: number; success: number; critical: number }>();
|
|
@@ -1103,8 +1238,14 @@ export async function handleBrainTool(
|
|
|
1103
1238
|
for (const l of topRecalled) {
|
|
1104
1239
|
const emoji = l.outcome === 'success' ? '✅' : l.outcome === 'partial' ? '⚠️' : '❌';
|
|
1105
1240
|
const sev = l.severity === 'critical' ? '🔴' : l.severity === 'major' ? '🟡' : '';
|
|
1106
|
-
const
|
|
1107
|
-
|
|
1241
|
+
const rc = l.recall_count ?? 0;
|
|
1242
|
+
const la = (l as { authors?: string[]; author?: string }).authors
|
|
1243
|
+
?? ((l as { author?: string }).author ? [(l as { author?: string }).author!] : []);
|
|
1244
|
+
const nAuthors = new Set(la.filter(Boolean)).size;
|
|
1245
|
+
const trust = (nAuthors >= 2 && rc >= 5) || rc >= 10 ? ' 🏆'
|
|
1246
|
+
: nAuthors >= 2 || rc >= 5 ? ' ✅' : '';
|
|
1247
|
+
const rct = rc > 1 ? ` _(${rc}× recalled)_` : '';
|
|
1248
|
+
lines.push(` ${emoji}${sev}${trust} \`${l.topic}\`${rct} — ${l.what_worked.slice(0, 100)}`);
|
|
1108
1249
|
}
|
|
1109
1250
|
lines.push('');
|
|
1110
1251
|
}
|
|
@@ -1172,27 +1313,63 @@ export async function handleBrainTool(
|
|
|
1172
1313
|
}
|
|
1173
1314
|
|
|
1174
1315
|
// ── 🔮 Predictive Pre-Warning — intent-based danger detection ────────────
|
|
1175
|
-
// Fires BEFORE work starts.
|
|
1176
|
-
|
|
1316
|
+
// Fires BEFORE work starts. Uses explicit focus when given; otherwise
|
|
1317
|
+
// derives the likely work area from the last session's changed files +
|
|
1318
|
+
// summary (you usually keep working where you left off). This makes the
|
|
1319
|
+
// warning fire even when the caller forgets to pass `focus`.
|
|
1320
|
+
{
|
|
1177
1321
|
type LessonAny = typeof lessons[0] & { author?: string; tags?: string[] };
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1322
|
+
|
|
1323
|
+
// Common path noise / extensions to drop when deriving terms from files.
|
|
1324
|
+
const PATH_NOISE = new Set([
|
|
1325
|
+
'src', 'lib', 'test', 'tests', 'dist', 'index', 'main', 'app',
|
|
1326
|
+
'internal', 'pkg', 'cmd', 'node_modules', 'components', 'utils',
|
|
1327
|
+
'ts', 'tsx', 'js', 'jsx', 'go', 'py', 'rs', 'java', 'json', 'yaml', 'yml',
|
|
1328
|
+
]);
|
|
1329
|
+
const deriveTermsFromFiles = (files: string[]): string[] => {
|
|
1330
|
+
const terms = new Set<string>();
|
|
1331
|
+
for (const f of files) {
|
|
1332
|
+
for (const seg of f.toLowerCase().split(/[/\\._-]/)) {
|
|
1333
|
+
if (seg.length > 3 && !PATH_NOISE.has(seg)) terms.add(seg);
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
return [...terms];
|
|
1337
|
+
};
|
|
1338
|
+
|
|
1339
|
+
let warnTerms = focusTerms;
|
|
1340
|
+
let warnLabel = focus;
|
|
1341
|
+
let derived = false;
|
|
1342
|
+
if (warnTerms.length === 0 && lastSession) {
|
|
1343
|
+
const fromFiles = deriveTermsFromFiles(lastSession.files_changed ?? []);
|
|
1344
|
+
const fromSummary = (lastSession.summary ?? '').toLowerCase()
|
|
1345
|
+
.replace(/[^a-z0-9\s:_-]/g, ' ').split(/\s+/).filter(t => t.length > 3 && !PATH_NOISE.has(t));
|
|
1346
|
+
warnTerms = [...new Set([...fromFiles, ...fromSummary])];
|
|
1347
|
+
warnLabel = 'where you left off last session';
|
|
1348
|
+
derived = true;
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
if (warnTerms.length > 0) {
|
|
1352
|
+
const dangerLessons = (lessons as LessonAny[]).filter(l => {
|
|
1353
|
+
if (l.outcome === 'success') return false;
|
|
1354
|
+
const topicCategory = l.topic.split(':')[0];
|
|
1355
|
+
return warnTerms.some(term =>
|
|
1356
|
+
l.topic.toLowerCase().includes(term) ||
|
|
1357
|
+
topicCategory === term ||
|
|
1358
|
+
(l.tags ?? []).some((t: string) => t.toLowerCase() === term),
|
|
1359
|
+
);
|
|
1360
|
+
});
|
|
1361
|
+
if (dangerLessons.length >= 1) {
|
|
1362
|
+
const headline = derived
|
|
1363
|
+
? ` You're likely to continue **${warnLabel}** — ${dangerLessons.length} known pitfall${dangerLessons.length > 1 ? 's' : ''} there:`
|
|
1364
|
+
: ` Known pitfalls for **"${warnLabel}"** (${dangerLessons.length} past failure${dangerLessons.length > 1 ? 's' : ''}):`;
|
|
1365
|
+
const warning = [
|
|
1366
|
+
`🚨 **PRE-WARNING** — Read this BEFORE starting:`,
|
|
1367
|
+
headline,
|
|
1368
|
+
...dangerLessons.slice(0, 3).map(l => ` ❌ \`${l.topic}\` — ${(l.what_failed ?? l.what_worked).slice(0, 80)}`),
|
|
1369
|
+
'',
|
|
1370
|
+
];
|
|
1371
|
+
lines.splice(2, 0, ...warning); // after '🧠 **Session Briefing**' + empty line
|
|
1372
|
+
}
|
|
1196
1373
|
}
|
|
1197
1374
|
}
|
|
1198
1375
|
|