@cachly-dev/mcp-server 0.10.55 → 0.10.57
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/eas-build-android.yml +58 -0
- package/.github/workflows/eas-update.yml +55 -0
- package/.github/workflows/publish.yml +2 -2
- package/BENCH.md +91 -0
- package/CHANGELOG.md +44 -0
- package/README.md +6 -34
- package/STRATEGY.md +228 -0
- package/apps/travel-chaos-organizer/.env.example +49 -0
- package/apps/travel-chaos-organizer/DEPLOY.md +328 -0
- package/apps/travel-chaos-organizer/Makefile +57 -0
- package/apps/travel-chaos-organizer/README.md +159 -0
- package/apps/travel-chaos-organizer/backend/.env.example +10 -0
- package/apps/travel-chaos-organizer/backend/Dockerfile +25 -0
- package/apps/travel-chaos-organizer/backend/alembic/env.py +56 -0
- package/apps/travel-chaos-organizer/backend/alembic/script.py.mako +24 -0
- package/apps/travel-chaos-organizer/backend/alembic/versions/.gitkeep +0 -0
- package/apps/travel-chaos-organizer/backend/alembic/versions/0001_stripe_customer_id.py +24 -0
- package/apps/travel-chaos-organizer/backend/alembic/versions/0002_telegram_chat_id.py +24 -0
- package/apps/travel-chaos-organizer/backend/alembic.ini +39 -0
- package/apps/travel-chaos-organizer/backend/app/__init__.py +0 -0
- package/apps/travel-chaos-organizer/backend/app/auth/__init__.py +0 -0
- package/apps/travel-chaos-organizer/backend/app/auth/keycloak.py +81 -0
- package/apps/travel-chaos-organizer/backend/app/config.py +66 -0
- package/apps/travel-chaos-organizer/backend/app/db/__init__.py +0 -0
- package/apps/travel-chaos-organizer/backend/app/db/database.py +20 -0
- package/apps/travel-chaos-organizer/backend/app/db/migrations/001_init.sql +71 -0
- package/apps/travel-chaos-organizer/backend/app/db/migrations/002_telemetry_freemium.sql +31 -0
- package/apps/travel-chaos-organizer/backend/app/db/migrations/003_telegram_chat_id.sql +4 -0
- package/apps/travel-chaos-organizer/backend/app/db/migrations/004_stripe_and_email.sql +5 -0
- package/apps/travel-chaos-organizer/backend/app/limiter.py +4 -0
- package/apps/travel-chaos-organizer/backend/app/logging_config.py +25 -0
- package/apps/travel-chaos-organizer/backend/app/main.py +77 -0
- package/apps/travel-chaos-organizer/backend/app/models/__init__.py +0 -0
- package/apps/travel-chaos-organizer/backend/app/models/schemas.py +142 -0
- package/apps/travel-chaos-organizer/backend/app/routers/__init__.py +0 -0
- package/apps/travel-chaos-organizer/backend/app/routers/admin.py +237 -0
- package/apps/travel-chaos-organizer/backend/app/routers/cache.py +13 -0
- package/apps/travel-chaos-organizer/backend/app/routers/events.py +29 -0
- package/apps/travel-chaos-organizer/backend/app/routers/export.py +122 -0
- package/apps/travel-chaos-organizer/backend/app/routers/inbox.py +122 -0
- package/apps/travel-chaos-organizer/backend/app/routers/items.py +130 -0
- package/apps/travel-chaos-organizer/backend/app/routers/mail.py +108 -0
- package/apps/travel-chaos-organizer/backend/app/routers/parse.py +303 -0
- package/apps/travel-chaos-organizer/backend/app/routers/payments.py +179 -0
- package/apps/travel-chaos-organizer/backend/app/routers/trips.py +134 -0
- package/apps/travel-chaos-organizer/backend/app/routers/users.py +169 -0
- package/apps/travel-chaos-organizer/backend/app/routers/waitlist.py +37 -0
- package/apps/travel-chaos-organizer/backend/app/services/__init__.py +0 -0
- package/apps/travel-chaos-organizer/backend/app/services/cache.py +131 -0
- package/apps/travel-chaos-organizer/backend/app/services/drip.py +128 -0
- package/apps/travel-chaos-organizer/backend/app/services/email.py +61 -0
- package/apps/travel-chaos-organizer/backend/app/services/errors.py +44 -0
- package/apps/travel-chaos-organizer/backend/app/services/notifier.py +10 -0
- package/apps/travel-chaos-organizer/backend/app/services/ollama.py +101 -0
- package/apps/travel-chaos-organizer/backend/app/services/parser.py +22 -0
- package/apps/travel-chaos-organizer/backend/app/services/quota.py +85 -0
- package/apps/travel-chaos-organizer/backend/app/services/sentry.py +11 -0
- package/apps/travel-chaos-organizer/backend/app/services/telemetry.py +35 -0
- package/apps/travel-chaos-organizer/backend/pytest.ini +4 -0
- package/apps/travel-chaos-organizer/backend/requirements-dev.txt +6 -0
- package/apps/travel-chaos-organizer/backend/requirements.txt +25 -0
- package/apps/travel-chaos-organizer/backend/tests/__init__.py +0 -0
- package/apps/travel-chaos-organizer/backend/tests/conftest.py +223 -0
- package/apps/travel-chaos-organizer/backend/tests/test_admin.py +99 -0
- package/apps/travel-chaos-organizer/backend/tests/test_cache.py +142 -0
- package/apps/travel-chaos-organizer/backend/tests/test_drip.py +32 -0
- package/apps/travel-chaos-organizer/backend/tests/test_e2e_chain.py +121 -0
- package/apps/travel-chaos-organizer/backend/tests/test_export.py +48 -0
- package/apps/travel-chaos-organizer/backend/tests/test_inbox.py +107 -0
- package/apps/travel-chaos-organizer/backend/tests/test_items.py +138 -0
- package/apps/travel-chaos-organizer/backend/tests/test_mail.py +113 -0
- package/apps/travel-chaos-organizer/backend/tests/test_ollama.py +145 -0
- package/apps/travel-chaos-organizer/backend/tests/test_parse_url.py +98 -0
- package/apps/travel-chaos-organizer/backend/tests/test_parser.py +72 -0
- package/apps/travel-chaos-organizer/backend/tests/test_payments.py +163 -0
- package/apps/travel-chaos-organizer/backend/tests/test_quota.py +46 -0
- package/apps/travel-chaos-organizer/backend/tests/test_search.py +67 -0
- package/apps/travel-chaos-organizer/backend/tests/test_trips.py +123 -0
- package/apps/travel-chaos-organizer/backend/tests/test_users.py +52 -0
- package/apps/travel-chaos-organizer/backups/.gitkeep +0 -0
- package/apps/travel-chaos-organizer/caddy/Caddyfile +27 -0
- package/apps/travel-chaos-organizer/docker-compose.node1.yml +31 -0
- package/apps/travel-chaos-organizer/docker-compose.override.yml +14 -0
- package/apps/travel-chaos-organizer/docker-compose.yml +128 -0
- package/apps/travel-chaos-organizer/frontend/.env.example +13 -0
- package/apps/travel-chaos-organizer/frontend/app/(app)/_layout.tsx +80 -0
- package/apps/travel-chaos-organizer/frontend/app/(app)/inbox.tsx +166 -0
- package/apps/travel-chaos-organizer/frontend/app/(app)/search.tsx +130 -0
- package/apps/travel-chaos-organizer/frontend/app/(app)/settings.tsx +381 -0
- package/apps/travel-chaos-organizer/frontend/app/(app)/trips/[id].tsx +346 -0
- package/apps/travel-chaos-organizer/frontend/app/(app)/trips.tsx +363 -0
- package/apps/travel-chaos-organizer/frontend/app/(app)/upgrade/cancel.tsx +34 -0
- package/apps/travel-chaos-organizer/frontend/app/(app)/upgrade/success.tsx +35 -0
- package/apps/travel-chaos-organizer/frontend/app/(auth)/login.tsx +91 -0
- package/apps/travel-chaos-organizer/frontend/app/_layout.tsx +71 -0
- package/apps/travel-chaos-organizer/frontend/app/auth.tsx +29 -0
- package/apps/travel-chaos-organizer/frontend/app/index.tsx +16 -0
- package/apps/travel-chaos-organizer/frontend/app/onboarding.tsx +119 -0
- package/apps/travel-chaos-organizer/frontend/app.config.ts +54 -0
- package/apps/travel-chaos-organizer/frontend/assets/adaptive-icon.svg +66 -0
- package/apps/travel-chaos-organizer/frontend/assets/icon.svg +79 -0
- package/apps/travel-chaos-organizer/frontend/assets/splash.svg +15 -0
- package/apps/travel-chaos-organizer/frontend/components/BottomSheet.tsx +102 -0
- package/apps/travel-chaos-organizer/frontend/components/ConfettiCannon.tsx +107 -0
- package/apps/travel-chaos-organizer/frontend/components/ConfirmDialog.tsx +109 -0
- package/apps/travel-chaos-organizer/frontend/components/ErrorBoundary.tsx +45 -0
- package/apps/travel-chaos-organizer/frontend/components/FileUploadButton.tsx +19 -0
- package/apps/travel-chaos-organizer/frontend/components/ImportSheet.tsx +155 -0
- package/apps/travel-chaos-organizer/frontend/components/OfflineBadge.tsx +55 -0
- package/apps/travel-chaos-organizer/frontend/components/QRCodeViewer.tsx +80 -0
- package/apps/travel-chaos-organizer/frontend/components/ScoreRing.tsx +39 -0
- package/apps/travel-chaos-organizer/frontend/components/Screen.tsx +39 -0
- package/apps/travel-chaos-organizer/frontend/components/Skeleton.tsx +70 -0
- package/apps/travel-chaos-organizer/frontend/components/SwipeableInboxItem.tsx +121 -0
- package/apps/travel-chaos-organizer/frontend/components/SwipeableTimelineItem.tsx +70 -0
- package/apps/travel-chaos-organizer/frontend/components/TimelineItem.tsx +73 -0
- package/apps/travel-chaos-organizer/frontend/components/TimelineItemDetail.tsx +130 -0
- package/apps/travel-chaos-organizer/frontend/components/Toast.tsx +60 -0
- package/apps/travel-chaos-organizer/frontend/components/ToastContext.tsx +42 -0
- package/apps/travel-chaos-organizer/frontend/components/TripCard.tsx +146 -0
- package/apps/travel-chaos-organizer/frontend/components/TripQRModal.tsx +70 -0
- package/apps/travel-chaos-organizer/frontend/components/UpgradeBanner.tsx +44 -0
- package/apps/travel-chaos-organizer/frontend/eas.json +22 -0
- package/apps/travel-chaos-organizer/frontend/hooks/useInbox.ts +24 -0
- package/apps/travel-chaos-organizer/frontend/hooks/useNetworkSync.ts +60 -0
- package/apps/travel-chaos-organizer/frontend/hooks/useTrips.ts +112 -0
- package/apps/travel-chaos-organizer/frontend/lib/api.ts +223 -0
- package/apps/travel-chaos-organizer/frontend/lib/auth.ts +93 -0
- package/apps/travel-chaos-organizer/frontend/lib/db.ts +86 -0
- package/apps/travel-chaos-organizer/frontend/lib/fileCache.ts +54 -0
- package/apps/travel-chaos-organizer/frontend/lib/haptics.ts +13 -0
- package/apps/travel-chaos-organizer/frontend/lib/notifications.ts +112 -0
- package/apps/travel-chaos-organizer/frontend/lib/offlineQueue.ts +145 -0
- package/apps/travel-chaos-organizer/frontend/lib/quota.tsx +43 -0
- package/apps/travel-chaos-organizer/frontend/lib/sentry.ts +28 -0
- package/apps/travel-chaos-organizer/frontend/lib/shareIntent.ts +44 -0
- package/apps/travel-chaos-organizer/frontend/package-lock.json +13221 -0
- package/apps/travel-chaos-organizer/frontend/package.json +44 -0
- package/apps/travel-chaos-organizer/frontend/tsconfig.json +23 -0
- package/apps/travel-chaos-organizer/keycloak/realm-export.json +59 -0
- package/apps/travel-chaos-organizer/landing/index.html +773 -0
- package/apps/travel-chaos-organizer/scripts/backup.sh +14 -0
- package/apps/travel-chaos-organizer/scripts/configure-env.sh +82 -0
- package/apps/travel-chaos-organizer/scripts/deploy.sh +54 -0
- package/apps/travel-chaos-organizer/scripts/health-check.sh +36 -0
- package/apps/travel-chaos-organizer/scripts/migrate.sh +7 -0
- package/apps/travel-chaos-organizer/scripts/server-setup.sh +85 -0
- package/apps/travel-chaos-organizer/scripts/smoke-test.sh +29 -0
- package/apps/travel-chaos-organizer/scripts/verify-tracking.sh +71 -0
- package/apps/travel-chaos-organizer/telegram-bot/.env.example +3 -0
- package/apps/travel-chaos-organizer/telegram-bot/Dockerfile +6 -0
- package/apps/travel-chaos-organizer/telegram-bot/bot.py +504 -0
- package/apps/travel-chaos-organizer/telegram-bot/requirements.txt +3 -0
- package/dist/packages/telegram-notify/client.d.ts +19 -0
- package/dist/packages/telegram-notify/client.js +75 -0
- package/dist/packages/telegram-notify/client.js.map +1 -0
- package/dist/src/__tests__/auth.test.js.map +1 -0
- package/dist/{__tests__ → src/__tests__}/brain-flow.test.js +141 -0
- package/dist/src/__tests__/brain-flow.test.js.map +1 -0
- package/dist/src/__tests__/confidence.test.js.map +1 -0
- package/dist/src/__tests__/e2e/api-auth.test.js.map +1 -0
- package/dist/src/__tests__/e2e/api-health.test.js.map +1 -0
- package/dist/src/__tests__/e2e/cli.test.js.map +1 -0
- package/dist/src/__tests__/e2e/config.js.map +1 -0
- package/dist/src/__tests__/e2e/funnel.test.js.map +1 -0
- package/dist/src/__tests__/e2e/mcp-tools.test.js.map +1 -0
- package/dist/src/__tests__/e2e/provisioning.test.js.map +1 -0
- package/dist/{__tests__ → src/__tests__}/handlers-unit.test.js +0 -2
- package/dist/src/__tests__/handlers-unit.test.js.map +1 -0
- package/dist/src/__tests__/namespace.test.js.map +1 -0
- package/dist/src/__tests__/rerank.test.js +115 -0
- package/dist/src/__tests__/rerank.test.js.map +1 -0
- package/dist/src/auth.js.map +1 -0
- package/dist/src/bench/cachly-bench.d.ts +35 -0
- package/dist/src/bench/cachly-bench.js +182 -0
- package/dist/src/bench/cachly-bench.js.map +1 -0
- package/dist/src/bench/fixtures.d.ts +37 -0
- package/dist/src/bench/fixtures.js +225 -0
- package/dist/src/bench/fixtures.js.map +1 -0
- package/dist/src/ckg.js.map +1 -0
- package/dist/src/confidence.js.map +1 -0
- package/dist/src/embeddings.js.map +1 -0
- package/dist/src/handlers/advanced.js.map +1 -0
- package/dist/{handlers → src/handlers}/brain.js +335 -168
- package/dist/src/handlers/brain.js.map +1 -0
- package/dist/src/handlers/cache.js.map +1 -0
- package/dist/{handlers → src/handlers}/context.js +9 -4
- package/dist/src/handlers/context.js.map +1 -0
- package/dist/{handlers → src/handlers}/fedbrain.d.ts +6 -0
- package/dist/{handlers → src/handlers}/fedbrain.js +19 -7
- package/dist/src/handlers/fedbrain.js.map +1 -0
- package/dist/src/handlers/instances.js.map +1 -0
- package/dist/{handlers → src/handlers}/roadmap.js +7 -3
- package/dist/src/handlers/roadmap.js.map +1 -0
- package/dist/src/handlers/syndicate.js.map +1 -0
- package/dist/src/handlers/tco.d.ts +16 -0
- package/dist/src/handlers/tco.js +303 -0
- package/dist/src/handlers/tco.js.map +1 -0
- package/dist/{handlers → src/handlers}/team.js +57 -7
- package/dist/src/handlers/team.js.map +1 -0
- package/dist/{index.js → src/index.js} +209 -420
- package/dist/src/index.js.map +1 -0
- package/dist/src/namespace.js.map +1 -0
- package/dist/src/notifier.d.ts +5 -0
- package/dist/src/notifier.js +6 -0
- package/dist/src/notifier.js.map +1 -0
- package/dist/src/rerank.d.ts +55 -0
- package/dist/src/rerank.js +111 -0
- package/dist/src/rerank.js.map +1 -0
- package/dist/src/search.js.map +1 -0
- package/dist/src/search.test.d.ts +1 -0
- package/dist/src/search.test.js.map +1 -0
- package/dist/{tools.d.ts → src/tools.d.ts} +226 -0
- package/dist/{tools.js → src/tools.js} +196 -0
- package/dist/src/tools.js.map +1 -0
- package/dist/src/utils.js.map +1 -0
- package/package.json +8 -7
- package/packages/telegram-notify/SPLIT_PLAN.md +106 -0
- package/packages/telegram-notify/client.ts +88 -0
- package/packages/telegram-notify/emojis.json +22 -0
- package/packages/telegram-notify/pyproject.toml +17 -0
- package/packages/telegram-notify/telegram_notify/__init__.py +3 -0
- package/packages/telegram-notify/telegram_notify/client.py +87 -0
- package/packages/telegram-notify/telegram_notify/emojis.json +22 -0
- package/scripts/generate-icons.js +28 -0
- package/server.json +3 -3
- package/smithery.yaml +21 -8
- package/src/__tests__/brain-flow.test.ts +174 -0
- package/src/__tests__/handlers-unit.test.ts +1 -2
- package/src/__tests__/rerank.test.ts +130 -0
- package/src/bench/cachly-bench.ts +217 -0
- package/src/bench/fixtures.ts +256 -0
- package/src/handlers/brain.ts +345 -172
- package/src/handlers/context.ts +9 -4
- package/src/handlers/fedbrain.ts +21 -7
- package/src/handlers/roadmap.ts +7 -3
- package/src/handlers/tco.ts +347 -0
- package/src/handlers/team.ts +67 -7
- package/src/index.ts +224 -406
- package/src/notifier.ts +5 -0
- package/src/rerank.ts +148 -0
- package/src/tools.ts +210 -0
- package/tsconfig.json +2 -3
- package/dist/__tests__/auth.test.js.map +0 -1
- package/dist/__tests__/brain-flow.test.js.map +0 -1
- package/dist/__tests__/brain-predict-failures.test.d.ts +0 -17
- package/dist/__tests__/brain-predict-failures.test.js +0 -339
- package/dist/__tests__/brain-predict-failures.test.js.map +0 -1
- package/dist/__tests__/confidence.test.js.map +0 -1
- package/dist/__tests__/e2e/api-auth.test.js.map +0 -1
- package/dist/__tests__/e2e/api-health.test.js.map +0 -1
- package/dist/__tests__/e2e/cli.test.js.map +0 -1
- package/dist/__tests__/e2e/config.js.map +0 -1
- package/dist/__tests__/e2e/funnel.test.js.map +0 -1
- package/dist/__tests__/e2e/mcp-tools.test.js.map +0 -1
- package/dist/__tests__/e2e/provisioning.test.js.map +0 -1
- package/dist/__tests__/handlers-unit.test.js.map +0 -1
- package/dist/__tests__/namespace.test.js.map +0 -1
- package/dist/auth.js.map +0 -1
- package/dist/ckg.js.map +0 -1
- package/dist/confidence.js.map +0 -1
- package/dist/embeddings.js.map +0 -1
- package/dist/handlers/advanced.js.map +0 -1
- package/dist/handlers/brain.js.map +0 -1
- package/dist/handlers/cache.js.map +0 -1
- package/dist/handlers/context.js.map +0 -1
- package/dist/handlers/fedbrain.js.map +0 -1
- package/dist/handlers/instances.js.map +0 -1
- package/dist/handlers/roadmap.js.map +0 -1
- package/dist/handlers/syndicate.js.map +0 -1
- package/dist/handlers/team.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/namespace.js.map +0 -1
- package/dist/search.js.map +0 -1
- package/dist/search.test.js.map +0 -1
- package/dist/tools.js.map +0 -1
- package/dist/utils.js.map +0 -1
- package/src/__tests__/brain-predict-failures.test.ts +0 -430
- /package/dist/{__tests__ → src/__tests__}/auth.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/auth.test.js +0 -0
- /package/dist/{__tests__ → src/__tests__}/brain-flow.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/confidence.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/confidence.test.js +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/api-auth.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/api-auth.test.js +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/api-health.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/api-health.test.js +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/cli.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/cli.test.js +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/config.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/config.js +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/funnel.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/funnel.test.js +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/mcp-tools.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/mcp-tools.test.js +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/provisioning.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/provisioning.test.js +0 -0
- /package/dist/{__tests__ → src/__tests__}/handlers-unit.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/namespace.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/namespace.test.js +0 -0
- /package/dist/{search.test.d.ts → src/__tests__/rerank.test.d.ts} +0 -0
- /package/dist/{auth.d.ts → src/auth.d.ts} +0 -0
- /package/dist/{auth.js → src/auth.js} +0 -0
- /package/dist/{ckg.d.ts → src/ckg.d.ts} +0 -0
- /package/dist/{ckg.js → src/ckg.js} +0 -0
- /package/dist/{confidence.d.ts → src/confidence.d.ts} +0 -0
- /package/dist/{confidence.js → src/confidence.js} +0 -0
- /package/dist/{embeddings.d.ts → src/embeddings.d.ts} +0 -0
- /package/dist/{embeddings.js → src/embeddings.js} +0 -0
- /package/dist/{handlers → src/handlers}/advanced.d.ts +0 -0
- /package/dist/{handlers → src/handlers}/advanced.js +0 -0
- /package/dist/{handlers → src/handlers}/brain.d.ts +0 -0
- /package/dist/{handlers → src/handlers}/cache.d.ts +0 -0
- /package/dist/{handlers → src/handlers}/cache.js +0 -0
- /package/dist/{handlers → src/handlers}/context.d.ts +0 -0
- /package/dist/{handlers → src/handlers}/instances.d.ts +0 -0
- /package/dist/{handlers → src/handlers}/instances.js +0 -0
- /package/dist/{handlers → src/handlers}/roadmap.d.ts +0 -0
- /package/dist/{handlers → src/handlers}/syndicate.d.ts +0 -0
- /package/dist/{handlers → src/handlers}/syndicate.js +0 -0
- /package/dist/{handlers → src/handlers}/team.d.ts +0 -0
- /package/dist/{index.d.ts → src/index.d.ts} +0 -0
- /package/dist/{namespace.d.ts → src/namespace.d.ts} +0 -0
- /package/dist/{namespace.js → src/namespace.js} +0 -0
- /package/dist/{search.d.ts → src/search.d.ts} +0 -0
- /package/dist/{search.js → src/search.js} +0 -0
- /package/dist/{search.test.js → src/search.test.js} +0 -0
- /package/dist/{utils.d.ts → src/utils.d.ts} +0 -0
- /package/dist/{utils.js → src/utils.js} +0 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""
|
|
2
|
+
telegram-notify — shared Telegram notification client.
|
|
3
|
+
|
|
4
|
+
Used by TCO backend (Python) and importable as a pip local package.
|
|
5
|
+
Single source of truth for: emoji map, message format, env var convention.
|
|
6
|
+
|
|
7
|
+
Interface: notify(app, event, payload)
|
|
8
|
+
notify_user(db, user_id, message) — direct DM via stored chat_id
|
|
9
|
+
|
|
10
|
+
Env vars:
|
|
11
|
+
TELEGRAM_BOT_TOKEN — bot token from @BotFather
|
|
12
|
+
{APP_UPPER}_TELEGRAM_CHAT_ID — app-specific channel (e.g. TCO_TELEGRAM_CHAT_ID)
|
|
13
|
+
TELEGRAM_CHAT_ID — fallback for all apps
|
|
14
|
+
|
|
15
|
+
No-op when unconfigured. Never throws.
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
import os
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
_EMOJI: dict[str, str] = json.loads(
|
|
25
|
+
(Path(__file__).parent / "emojis.json").read_text()
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _token() -> str:
|
|
30
|
+
return os.getenv("TELEGRAM_BOT_TOKEN", "")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _chat_id(app: str) -> str:
|
|
34
|
+
key = f"{app.upper()}_TELEGRAM_CHAT_ID"
|
|
35
|
+
return os.getenv(key) or os.getenv("TELEGRAM_CHAT_ID", "")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def format_message(app: str, event: str, payload: dict[str, Any]) -> str:
|
|
39
|
+
icon = _EMOJI.get(event, _EMOJI["default"])
|
|
40
|
+
lines = [f"{icon} *{app.upper()}* · `{event}`"]
|
|
41
|
+
for k, v in payload.items():
|
|
42
|
+
if v is not None and v != "":
|
|
43
|
+
lines.append(f" {k}: `{v}`")
|
|
44
|
+
return "\n".join(lines)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
async def notify(app: str, event: str, payload: dict[str, Any] | None = None) -> None:
|
|
48
|
+
"""Send event notification to the app's configured Telegram channel."""
|
|
49
|
+
token = _token()
|
|
50
|
+
chat_id = _chat_id(app)
|
|
51
|
+
if not token or not chat_id:
|
|
52
|
+
return
|
|
53
|
+
try:
|
|
54
|
+
import httpx
|
|
55
|
+
text = format_message(app, event, payload or {})
|
|
56
|
+
async with httpx.AsyncClient(timeout=5) as client:
|
|
57
|
+
await client.post(
|
|
58
|
+
f"https://api.telegram.org/bot{token}/sendMessage",
|
|
59
|
+
json={"chat_id": chat_id, "text": text, "parse_mode": "Markdown"},
|
|
60
|
+
)
|
|
61
|
+
except Exception:
|
|
62
|
+
pass # never raises
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
async def notify_user(db: Any, user_id: str, message: str) -> None:
|
|
66
|
+
"""Send a direct Telegram message to a user by their DB user_id.
|
|
67
|
+
Looks up telegram_chat_id from the users table. No-op if not linked."""
|
|
68
|
+
token = _token()
|
|
69
|
+
if not token:
|
|
70
|
+
return
|
|
71
|
+
try:
|
|
72
|
+
from sqlalchemy import text as sql_text
|
|
73
|
+
result = await db.execute(
|
|
74
|
+
sql_text("SELECT telegram_chat_id FROM users WHERE id = :uid"),
|
|
75
|
+
{"uid": user_id},
|
|
76
|
+
)
|
|
77
|
+
row = result.fetchone()
|
|
78
|
+
if not row or not row[0]:
|
|
79
|
+
return
|
|
80
|
+
import httpx
|
|
81
|
+
async with httpx.AsyncClient(timeout=5) as client:
|
|
82
|
+
await client.post(
|
|
83
|
+
f"https://api.telegram.org/bot{token}/sendMessage",
|
|
84
|
+
json={"chat_id": row[0], "text": message, "parse_mode": "Markdown"},
|
|
85
|
+
)
|
|
86
|
+
except Exception:
|
|
87
|
+
pass # never raises
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"tool_call": "🔧",
|
|
3
|
+
"tool_error": "🔴",
|
|
4
|
+
"cache_hit": "⚡",
|
|
5
|
+
"cache_miss": "💭",
|
|
6
|
+
"auth_ok": "🔑",
|
|
7
|
+
"auth_fail": "🚫",
|
|
8
|
+
"startup": "🚀",
|
|
9
|
+
"waitlist_signup": "📬",
|
|
10
|
+
"parse_file": "📄",
|
|
11
|
+
"parse_text": "📝",
|
|
12
|
+
"parse_url": "🔗",
|
|
13
|
+
"trip_created": "🗺️",
|
|
14
|
+
"trip_deleted": "🗑️",
|
|
15
|
+
"error": "🔴",
|
|
16
|
+
"stripe_payment": "💳",
|
|
17
|
+
"stripe_checkout": "🛒",
|
|
18
|
+
"new_user": "🆕",
|
|
19
|
+
"pdf_export": "📑",
|
|
20
|
+
"trip_search": "🔍",
|
|
21
|
+
"default": "🔔"
|
|
22
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Converts SVG icon assets to PNG for Expo.
|
|
4
|
+
* Run: node scripts/generate-icons.js
|
|
5
|
+
* Requires: npm install -g sharp-cli OR npx sharp-cli
|
|
6
|
+
*/
|
|
7
|
+
const { execSync } = require('child_process');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
const frontend = path.join(__dirname, '..', 'apps', 'travel-chaos-organizer', 'frontend');
|
|
11
|
+
|
|
12
|
+
const conversions = [
|
|
13
|
+
{ input: 'assets/icon.svg', output: 'assets/icon.png', width: 1024, height: 1024 },
|
|
14
|
+
{ input: 'assets/adaptive-icon.svg', output: 'assets/adaptive-icon.png', width: 1024, height: 1024 },
|
|
15
|
+
{ input: 'assets/icon.svg', output: 'assets/splash.png', width: 1284, height: 2778 },
|
|
16
|
+
{ input: 'assets/icon.svg', output: 'assets/favicon.png', width: 32, height: 32 },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
for (const { input, output, width, height } of conversions) {
|
|
20
|
+
const src = path.join(frontend, input);
|
|
21
|
+
const dst = path.join(frontend, output);
|
|
22
|
+
try {
|
|
23
|
+
execSync(`npx sharp-cli --input "${src}" --output "${dst}" resize ${width} ${height}`, { stdio: 'inherit' });
|
|
24
|
+
console.log(`✓ ${output}`);
|
|
25
|
+
} catch {
|
|
26
|
+
console.error(`✗ ${output} — install sharp-cli: npm i -g sharp-cli`);
|
|
27
|
+
}
|
|
28
|
+
}
|
package/server.json
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
3
|
"name": "io.github.cachly-dev/cachly-mcp",
|
|
4
|
-
"description": "Your AI forgets everything between sessions. cachly fixes that permanently. A causal brain that learns WHY things break — built from every bug fix, git commit, and CI run, without any explicit calls. Arrives pre-briefed each session. Predicts failures before they happen. Never re-researches the same problem.
|
|
4
|
+
"description": "Your AI forgets everything between sessions. cachly fixes that permanently. A causal brain that learns WHY things break — built from every bug fix, git commit, and CI run, without any explicit calls. Arrives pre-briefed each session. Predicts failures before they happen. Never re-researches the same problem. 95 MCP tools · Free tier · EU servers · GDPR.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"url": "https://github.com/cachly-dev/cachly-mcp",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "0.10.
|
|
9
|
+
"version": "0.10.57",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"name": "@cachly-dev/mcp-server",
|
|
14
14
|
"identifier": "@cachly-dev/mcp-server",
|
|
15
|
-
"version": "0.10.
|
|
15
|
+
"version": "0.10.57",
|
|
16
16
|
"transport": {
|
|
17
17
|
"type": "stdio"
|
|
18
18
|
}
|
package/smithery.yaml
CHANGED
|
@@ -7,19 +7,15 @@ description: >
|
|
|
7
7
|
BM25+ search in 11 languages. 80 MCP tools. Free tier. GDPR. German servers.
|
|
8
8
|
Zero-config: sign in once via browser, auto-provisions your Brain instance.
|
|
9
9
|
|
|
10
|
-
# Streamable HTTP — hosted on our infrastructure (mcp.cachly.dev).
|
|
11
|
-
# Multi-tenant: per-request CACHLY_JWT + instance ID resolved from
|
|
12
|
-
# Authorization header + X-Cachly-Instance-Id, or Smithery ?config= param.
|
|
13
10
|
startCommand:
|
|
14
|
-
type:
|
|
15
|
-
url: https://mcp.cachly.dev/mcp
|
|
11
|
+
type: stdio
|
|
16
12
|
configSchema:
|
|
17
13
|
type: object
|
|
18
14
|
properties:
|
|
19
15
|
CACHLY_JWT:
|
|
20
16
|
type: string
|
|
21
|
-
title: "API Key"
|
|
22
|
-
description: "
|
|
17
|
+
title: "API Key (optional — sign in via browser on first use)"
|
|
18
|
+
description: "Leave blank for zero-config: on your first tool call the server opens a browser sign-in (OAuth Device Flow) and auto-provisions your Brain instance. Or pre-fill with your API key from cachly.dev/setup-ai."
|
|
23
19
|
examples:
|
|
24
20
|
- "cky_live_abc123..."
|
|
25
21
|
CACHLY_BRAIN_INSTANCE_ID:
|
|
@@ -29,4 +25,21 @@ startCommand:
|
|
|
29
25
|
pattern: "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
|
|
30
26
|
examples:
|
|
31
27
|
- "9d4077aa-bfa2-468b-89cd-0a8d8f3ec483"
|
|
32
|
-
|
|
28
|
+
CACHLY_API_URL:
|
|
29
|
+
type: string
|
|
30
|
+
title: "API URL (optional)"
|
|
31
|
+
description: "Leave blank unless you self-host Cachly."
|
|
32
|
+
default: "https://api.cachly.dev"
|
|
33
|
+
required: []
|
|
34
|
+
commandFunction: |-
|
|
35
|
+
(config) => ({
|
|
36
|
+
command: "npx",
|
|
37
|
+
args: ["-y", "@cachly-dev/mcp-server@latest"],
|
|
38
|
+
env: Object.fromEntries(
|
|
39
|
+
Object.entries({
|
|
40
|
+
CACHLY_JWT: config.CACHLY_JWT,
|
|
41
|
+
CACHLY_BRAIN_INSTANCE_ID: config.CACHLY_BRAIN_INSTANCE_ID,
|
|
42
|
+
CACHLY_API_URL: config.CACHLY_API_URL ?? "https://api.cachly.dev",
|
|
43
|
+
}).filter(([, v]) => v != null && v !== "")
|
|
44
|
+
)
|
|
45
|
+
})
|
|
@@ -17,6 +17,7 @@ import { EventEmitter } from 'node:events';
|
|
|
17
17
|
import { ckgSlug, extractProblemConcept, ckgUpsertNode, ckgUpdateEdge } from '../ckg.js';
|
|
18
18
|
import { EMBED_PROVIDER, hasEmbedProvider, embedProviderHint, embedConfig, setEmbedJwt } from '../embeddings.js';
|
|
19
19
|
import { keywordSearch } from '../search.js';
|
|
20
|
+
import { handleBrainTool } from '../handlers/brain.js';
|
|
20
21
|
import type { Redis } from 'ioredis';
|
|
21
22
|
|
|
22
23
|
// ── In-memory Redis mock ──────────────────────────────────────────────────────
|
|
@@ -67,6 +68,21 @@ class MockRedis {
|
|
|
67
68
|
return [...(this.sets.get(key) ?? [])];
|
|
68
69
|
}
|
|
69
70
|
|
|
71
|
+
async mget(...keys: string[]): Promise<(string | null)[]> {
|
|
72
|
+
return keys.map(k => this.store.get(k) ?? null);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async exists(key: string): Promise<number> {
|
|
76
|
+
return this.store.has(key) ? 1 : 0;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async incrbyfloat(key: string, increment: number): Promise<string> {
|
|
80
|
+
const cur = parseFloat(this.store.get(key) ?? '0');
|
|
81
|
+
const next = cur + increment;
|
|
82
|
+
this.store.set(key, String(next));
|
|
83
|
+
return String(next);
|
|
84
|
+
}
|
|
85
|
+
|
|
70
86
|
/** Simplified scanStream: returns all matching keys in a single 'data' event. */
|
|
71
87
|
scanStream(opts: { match: string; count?: number }): EventEmitter {
|
|
72
88
|
const emitter = new EventEmitter();
|
|
@@ -872,3 +888,161 @@ describe('Brain Flow: advanced scenarios', () => {
|
|
|
872
888
|
});
|
|
873
889
|
});
|
|
874
890
|
});
|
|
891
|
+
|
|
892
|
+
// ── New feature coverage (0.10.41 / 0.10.42) ─────────────────────────────────
|
|
893
|
+
// Trust badges, Proven Laws crystallization, and focus-less predictive warning.
|
|
894
|
+
// These exercise the REAL handleBrainTool code path with the MockRedis.
|
|
895
|
+
|
|
896
|
+
const noopApiFetch = (async () => null) as unknown as Parameters<typeof handleBrainTool>[3];
|
|
897
|
+
|
|
898
|
+
/** Seed a best-solution lesson with an explicit recall_count / authors / pinned. */
|
|
899
|
+
async function seedLesson(
|
|
900
|
+
redis: MockRedis,
|
|
901
|
+
topic: string,
|
|
902
|
+
opts: {
|
|
903
|
+
outcome?: 'success' | 'failure' | 'partial';
|
|
904
|
+
what_worked?: string;
|
|
905
|
+
what_failed?: string;
|
|
906
|
+
recall_count?: number;
|
|
907
|
+
severity?: 'critical' | 'major' | 'minor';
|
|
908
|
+
pinned?: boolean;
|
|
909
|
+
author?: string;
|
|
910
|
+
tags?: string[];
|
|
911
|
+
} = {},
|
|
912
|
+
): Promise<void> {
|
|
913
|
+
const ts = new Date().toISOString();
|
|
914
|
+
const obj = {
|
|
915
|
+
topic,
|
|
916
|
+
outcome: opts.outcome ?? 'success',
|
|
917
|
+
what_worked: opts.what_worked ?? `solution for ${topic}`,
|
|
918
|
+
what_failed: opts.what_failed,
|
|
919
|
+
severity: opts.severity ?? 'minor',
|
|
920
|
+
recall_count: opts.recall_count ?? 0,
|
|
921
|
+
ts,
|
|
922
|
+
verified_at: ts,
|
|
923
|
+
confidence: 1.0,
|
|
924
|
+
tags: opts.tags ?? [],
|
|
925
|
+
...(opts.pinned ? { pinned: true } : {}),
|
|
926
|
+
...(opts.author ? { author: opts.author } : {}),
|
|
927
|
+
};
|
|
928
|
+
await redis.set(`cachly:lesson:best:${topic}`, JSON.stringify(obj));
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
describe('Trust badges (recall_best_solution)', () => {
|
|
932
|
+
let redis: MockRedis;
|
|
933
|
+
const getConn = async () => redis as unknown as Redis;
|
|
934
|
+
beforeEach(() => { redis = new MockRedis(); });
|
|
935
|
+
|
|
936
|
+
it('shows "Battle-tested" when recall_count crosses 10', async () => {
|
|
937
|
+
await seedLesson(redis, 'redis:pool', { recall_count: 9, what_worked: 'set max_retries=3' });
|
|
938
|
+
const out = (await handleBrainTool('recall_best_solution', { instance_id: 'i1', topic: 'redis:pool' }, getConn, noopApiFetch))!;
|
|
939
|
+
expect(out).toContain('Battle-tested');
|
|
940
|
+
expect(out).toContain('recalled 10×');
|
|
941
|
+
});
|
|
942
|
+
|
|
943
|
+
it('shows "Proven" when recall_count is between 5 and 9', async () => {
|
|
944
|
+
await seedLesson(redis, 'api:auth', { recall_count: 4 });
|
|
945
|
+
const out = (await handleBrainTool('recall_best_solution', { instance_id: 'i1', topic: 'api:auth' }, getConn, noopApiFetch))!;
|
|
946
|
+
expect(out).toContain('Proven');
|
|
947
|
+
expect(out).toContain('recalled 5×');
|
|
948
|
+
expect(out).not.toContain('Battle-tested');
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
it('shows no trust badge for a fresh low-recall lesson', async () => {
|
|
952
|
+
await seedLesson(redis, 'misc:thing', { recall_count: 0 });
|
|
953
|
+
const out = (await handleBrainTool('recall_best_solution', { instance_id: 'i1', topic: 'misc:thing' }, getConn, noopApiFetch))!;
|
|
954
|
+
expect(out).not.toContain('Battle-tested');
|
|
955
|
+
expect(out).not.toContain('Proven —');
|
|
956
|
+
expect(out).not.toContain('Team-verified');
|
|
957
|
+
});
|
|
958
|
+
|
|
959
|
+
it('increments recall_count and persists it', async () => {
|
|
960
|
+
await seedLesson(redis, 'deploy:x', { recall_count: 2 });
|
|
961
|
+
await handleBrainTool('recall_best_solution', { instance_id: 'i1', topic: 'deploy:x' }, getConn, noopApiFetch);
|
|
962
|
+
const stored = JSON.parse((await redis.get('cachly:lesson:best:deploy:x'))!);
|
|
963
|
+
expect(stored.recall_count).toBe(3);
|
|
964
|
+
});
|
|
965
|
+
});
|
|
966
|
+
|
|
967
|
+
describe('Proven Laws crystallization (session_start)', () => {
|
|
968
|
+
let redis: MockRedis;
|
|
969
|
+
const getConn = async () => redis as unknown as Redis;
|
|
970
|
+
beforeEach(async () => {
|
|
971
|
+
redis = new MockRedis();
|
|
972
|
+
// Provide a last session so session_start does not attempt git reconstruction.
|
|
973
|
+
await redis.set('cachly:session:last', JSON.stringify({ summary: 'prior work', ts: new Date().toISOString(), files_changed: [] }));
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
it('surfaces lessons recalled >= 5 in the Proven Laws section', async () => {
|
|
977
|
+
await seedLesson(redis, 'infra:k3s', { recall_count: 7, what_worked: 'add WireGuard IP to TLS-SAN' });
|
|
978
|
+
await seedLesson(redis, 'random:note', { recall_count: 1 });
|
|
979
|
+
const out = (await handleBrainTool('session_start', { instance_id: 'i1' }, getConn, noopApiFetch))!;
|
|
980
|
+
expect(out).toContain('Proven Laws');
|
|
981
|
+
expect(out).toContain('infra:k3s');
|
|
982
|
+
expect(out).toContain('recalled 7×');
|
|
983
|
+
});
|
|
984
|
+
|
|
985
|
+
it('surfaces an explicitly pinned lesson even with low recall', async () => {
|
|
986
|
+
await seedLesson(redis, 'team:convention', { recall_count: 0, pinned: true, what_worked: 'always snake_case handlers' });
|
|
987
|
+
const out = (await handleBrainTool('session_start', { instance_id: 'i1' }, getConn, noopApiFetch))!;
|
|
988
|
+
expect(out).toContain('Proven Laws');
|
|
989
|
+
expect(out).toContain('team:convention');
|
|
990
|
+
expect(out).toContain('pinned');
|
|
991
|
+
});
|
|
992
|
+
|
|
993
|
+
it('omits the Proven Laws section when nothing qualifies', async () => {
|
|
994
|
+
await seedLesson(redis, 'small:thing', { recall_count: 1 });
|
|
995
|
+
const out = (await handleBrainTool('session_start', { instance_id: 'i1' }, getConn, noopApiFetch))!;
|
|
996
|
+
expect(out).not.toContain('Proven Laws');
|
|
997
|
+
});
|
|
998
|
+
|
|
999
|
+
it('does not crystallize failure lessons', async () => {
|
|
1000
|
+
await seedLesson(redis, 'broken:flow', { recall_count: 9, outcome: 'failure', what_failed: 'never works' });
|
|
1001
|
+
const out = (await handleBrainTool('session_start', { instance_id: 'i1' }, getConn, noopApiFetch))!;
|
|
1002
|
+
expect(out).not.toContain('Proven Laws');
|
|
1003
|
+
});
|
|
1004
|
+
});
|
|
1005
|
+
|
|
1006
|
+
describe('Predictive pre-warning without explicit focus (session_start)', () => {
|
|
1007
|
+
let redis: MockRedis;
|
|
1008
|
+
const getConn = async () => redis as unknown as Redis;
|
|
1009
|
+
beforeEach(() => { redis = new MockRedis(); });
|
|
1010
|
+
|
|
1011
|
+
it('derives danger area from last session files and warns', async () => {
|
|
1012
|
+
// Last session touched payment files → likely to continue there.
|
|
1013
|
+
await redis.set('cachly:session:last', JSON.stringify({
|
|
1014
|
+
summary: 'worked on payment integration',
|
|
1015
|
+
ts: new Date().toISOString(),
|
|
1016
|
+
files_changed: ['api/payment/stripe_webhook.go'],
|
|
1017
|
+
}));
|
|
1018
|
+
// A known failure in the payment area.
|
|
1019
|
+
await seedLesson(redis, 'payment:webhook', { outcome: 'failure', what_failed: 'missed idempotency key → double charge', tags: ['payment'] });
|
|
1020
|
+
const out = (await handleBrainTool('session_start', { instance_id: 'i1' }, getConn, noopApiFetch))!;
|
|
1021
|
+
expect(out).toContain('PRE-WARNING');
|
|
1022
|
+
expect(out).toContain('payment:webhook');
|
|
1023
|
+
});
|
|
1024
|
+
|
|
1025
|
+
it('does not warn when last-session area has no known failures', async () => {
|
|
1026
|
+
await redis.set('cachly:session:last', JSON.stringify({
|
|
1027
|
+
summary: 'wrote docs',
|
|
1028
|
+
ts: new Date().toISOString(),
|
|
1029
|
+
files_changed: ['docs/readme.md'],
|
|
1030
|
+
}));
|
|
1031
|
+
await seedLesson(redis, 'payment:webhook', { outcome: 'failure', what_failed: 'x', tags: ['payment'] });
|
|
1032
|
+
const out = (await handleBrainTool('session_start', { instance_id: 'i1' }, getConn, noopApiFetch))!;
|
|
1033
|
+
expect(out).not.toContain('PRE-WARNING');
|
|
1034
|
+
});
|
|
1035
|
+
|
|
1036
|
+
it('filters path noise so common segments do not trigger false matches', async () => {
|
|
1037
|
+
// Last session only touched generic noise paths (src/lib/index).
|
|
1038
|
+
await redis.set('cachly:session:last', JSON.stringify({
|
|
1039
|
+
summary: 'refactor',
|
|
1040
|
+
ts: new Date().toISOString(),
|
|
1041
|
+
files_changed: ['src/lib/index.ts'],
|
|
1042
|
+
}));
|
|
1043
|
+
// A failure tagged 'src' — should NOT match because 'src' is noise.
|
|
1044
|
+
await seedLesson(redis, 'src:thing', { outcome: 'failure', what_failed: 'noise', tags: ['src'] });
|
|
1045
|
+
const out = (await handleBrainTool('session_start', { instance_id: 'i1' }, getConn, noopApiFetch))!;
|
|
1046
|
+
expect(out).not.toContain('PRE-WARNING');
|
|
1047
|
+
});
|
|
1048
|
+
});
|
|
@@ -334,8 +334,7 @@ describe('handleRoadmapTool', () => {
|
|
|
334
334
|
// Add and complete one item
|
|
335
335
|
const addResult = await handleRoadmapTool('roadmap_add', { instance_id: INSTANCE, title: 'Finished task', priority: 'low' }, getConnection, apiFetch);
|
|
336
336
|
const idMatch = addResult!.match(/`(rm_[a-z0-9_]+)`/);
|
|
337
|
-
|
|
338
|
-
const id = idMatch[1];
|
|
337
|
+
const id = idMatch![1];
|
|
339
338
|
await handleRoadmapTool('roadmap_update', { instance_id: INSTANCE, id, status: 'done' }, getConnection, apiFetch);
|
|
340
339
|
|
|
341
340
|
const result = await handleRoadmapTool('roadmap_list', { instance_id: INSTANCE, status: 'done' }, getConnection, apiFetch);
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for quality-aware reranking and the Cachly-Bench regression guard.
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect } from 'vitest';
|
|
5
|
+
import { qualityMultiplier, extractLessonQuality, rerankByQuality, type LessonQuality } from '../rerank.js';
|
|
6
|
+
import type { KeywordMatch } from '../search.js';
|
|
7
|
+
import { runBenchmark } from '../bench/cachly-bench.js';
|
|
8
|
+
|
|
9
|
+
const lessonMatch = (topic: string, score: number, lesson: Partial<LessonQuality> & Record<string, unknown>): KeywordMatch => ({
|
|
10
|
+
key: `cachly:lesson:best:${topic}`,
|
|
11
|
+
content: JSON.stringify(lesson),
|
|
12
|
+
score,
|
|
13
|
+
matchedWords: ['x'],
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe('qualityMultiplier', () => {
|
|
17
|
+
it('returns neutral 1.0 for documents with no quality signal', () => {
|
|
18
|
+
expect(qualityMultiplier(null)).toBe(1.0);
|
|
19
|
+
expect(qualityMultiplier({})).toBe(1.0);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('ranks a proven success above a fresh failure', () => {
|
|
23
|
+
const success = qualityMultiplier({ outcome: 'success', confidence: 0.95, recall_count: 12 });
|
|
24
|
+
const failure = qualityMultiplier({ outcome: 'failure', confidence: 0.3, recall_count: 0 });
|
|
25
|
+
expect(success).toBeGreaterThan(failure);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('rewards proven-ness (recall_count) but saturates', () => {
|
|
29
|
+
const never = qualityMultiplier({ outcome: 'success', confidence: 0.8, recall_count: 0 });
|
|
30
|
+
const some = qualityMultiplier({ outcome: 'success', confidence: 0.8, recall_count: 5 });
|
|
31
|
+
const many = qualityMultiplier({ outcome: 'success', confidence: 0.8, recall_count: 500 });
|
|
32
|
+
expect(some).toBeGreaterThan(never);
|
|
33
|
+
expect(many).toBeGreaterThan(some);
|
|
34
|
+
expect(many - some).toBeLessThan(some - never + 0.2); // diminishing returns
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('clamps within [0.5, 2.5]', () => {
|
|
38
|
+
const hi = qualityMultiplier({ outcome: 'success', confidence: 1, recall_count: 100000, severity: 'critical', review_level: 'senior', reviewed_by: 'x', endorsements: 99 });
|
|
39
|
+
const lo = qualityMultiplier({ outcome: 'failure', confidence: 0, recall_count: 0, severity: 'minor' });
|
|
40
|
+
expect(hi).toBeLessThanOrEqual(2.5);
|
|
41
|
+
expect(lo).toBeGreaterThanOrEqual(0.5);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('boosts a human-reviewed lesson above an unreviewed equal', () => {
|
|
45
|
+
const base = { outcome: 'success', confidence: 0.8, recall_count: 3 } as const;
|
|
46
|
+
const unreviewed = qualityMultiplier({ ...base });
|
|
47
|
+
const peer = qualityMultiplier({ ...base, reviewed_by: 'alice', review_level: 'peer', endorsements: 1 });
|
|
48
|
+
const senior = qualityMultiplier({ ...base, reviewed_by: 'bob', review_level: 'senior', endorsements: 1 });
|
|
49
|
+
expect(peer).toBeGreaterThan(unreviewed);
|
|
50
|
+
expect(senior).toBeGreaterThan(peer);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('rewards additional distinct endorsements but saturates', () => {
|
|
54
|
+
const base = { outcome: 'success', confidence: 0.8, recall_count: 3, reviewed_by: 'a', review_level: 'peer' } as const;
|
|
55
|
+
const one = qualityMultiplier({ ...base, endorsements: 1 });
|
|
56
|
+
const three = qualityMultiplier({ ...base, endorsements: 3 });
|
|
57
|
+
const fifty = qualityMultiplier({ ...base, endorsements: 50 });
|
|
58
|
+
expect(three).toBeGreaterThan(one);
|
|
59
|
+
expect(fifty).toBeGreaterThan(three);
|
|
60
|
+
expect(fifty - three).toBeLessThan(three - one + 0.1);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('extractLessonQuality', () => {
|
|
65
|
+
it('parses lesson JSON for lesson keys', () => {
|
|
66
|
+
const q = extractLessonQuality({ key: 'cachly:lesson:best:foo', content: '{"outcome":"success"}' });
|
|
67
|
+
expect(q?.outcome).toBe('success');
|
|
68
|
+
});
|
|
69
|
+
it('returns null for non-lesson keys (e.g. context)', () => {
|
|
70
|
+
expect(extractLessonQuality({ key: 'cachly:ctx:custom:foo', content: 'plain text' })).toBeNull();
|
|
71
|
+
});
|
|
72
|
+
it('returns null for corrupt lesson JSON', () => {
|
|
73
|
+
expect(extractLessonQuality({ key: 'cachly:lesson:best:foo', content: '{not json' })).toBeNull();
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('rerankByQuality', () => {
|
|
78
|
+
it('promotes a proven success over a higher-BM25 failure distractor', () => {
|
|
79
|
+
const matches: KeywordMatch[] = [
|
|
80
|
+
lessonMatch('symptom-failure', 10, { outcome: 'failure', confidence: 0.2, recall_count: 0 }),
|
|
81
|
+
lessonMatch('proven-success', 8, { outcome: 'success', confidence: 0.95, recall_count: 12 }),
|
|
82
|
+
];
|
|
83
|
+
const ranked = rerankByQuality(matches);
|
|
84
|
+
expect(ranked[0].key).toBe('cachly:lesson:best:proven-success');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('keeps a strong text match competitive (does not bury it on quality alone)', () => {
|
|
88
|
+
const matches: KeywordMatch[] = [
|
|
89
|
+
lessonMatch('weak-but-perfect', 1, { outcome: 'success', confidence: 1, recall_count: 100 }),
|
|
90
|
+
lessonMatch('strong-text', 100, { outcome: 'partial', confidence: 0.6, recall_count: 0 }),
|
|
91
|
+
];
|
|
92
|
+
const ranked = rerankByQuality(matches);
|
|
93
|
+
expect(ranked[0].key).toBe('cachly:lesson:best:strong-text');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('is stable and returns every input', () => {
|
|
97
|
+
const matches: KeywordMatch[] = [
|
|
98
|
+
lessonMatch('a', 5, { outcome: 'success' }),
|
|
99
|
+
lessonMatch('b', 5, { outcome: 'success' }),
|
|
100
|
+
];
|
|
101
|
+
const ranked = rerankByQuality(matches);
|
|
102
|
+
expect(ranked).toHaveLength(2);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('handles empty input', () => {
|
|
106
|
+
expect(rerankByQuality([])).toEqual([]);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe('Cachly-Bench regression guard', () => {
|
|
111
|
+
it('quality rerank never regresses headline metrics vs baseline', async () => {
|
|
112
|
+
const r = await runBenchmark();
|
|
113
|
+
// Cachly must be >= baseline on the headline metrics (the moat must not hurt).
|
|
114
|
+
expect(r.cachly.mrr).toBeGreaterThanOrEqual(r.baseline.mrr - 1e-9);
|
|
115
|
+
expect(r.cachly.precisionAt1).toBeGreaterThanOrEqual(r.baseline.precisionAt1 - 1e-9);
|
|
116
|
+
expect(r.cachly.ndcgAt5).toBeGreaterThanOrEqual(r.baseline.ndcgAt5 - 1e-9);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('demonstrates a positive lift on Precision@1 and MRR (the proof)', async () => {
|
|
120
|
+
const r = await runBenchmark();
|
|
121
|
+
expect(r.lift.precisionAt1).toBeGreaterThan(0);
|
|
122
|
+
expect(r.lift.mrr).toBeGreaterThan(0);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('baseline is already strong (corpus is not rigged to make rerank look good)', async () => {
|
|
126
|
+
const r = await runBenchmark();
|
|
127
|
+
// A fair benchmark: BM25 alone already gets most queries right.
|
|
128
|
+
expect(r.baseline.mrr).toBeGreaterThan(0.7);
|
|
129
|
+
});
|
|
130
|
+
});
|