@cachly-dev/mcp-server 0.10.48 → 0.10.50
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/BENCH.md +80 -0
- package/README.md +5 -33
- package/STRATEGY.md +218 -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 +98 -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 +34 -0
- package/dist/src/bench/fixtures.js +199 -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 +261 -87
- package/dist/src/handlers/brain.js.map +1 -0
- package/dist/src/handlers/cache.js.map +1 -0
- package/dist/{handlers → src/handlers}/context.js +9 -4
- package/dist/src/handlers/context.js.map +1 -0
- package/dist/{handlers → src/handlers}/fedbrain.d.ts +6 -0
- package/dist/{handlers → src/handlers}/fedbrain.js +19 -7
- package/dist/src/handlers/fedbrain.js.map +1 -0
- package/dist/src/handlers/instances.js.map +1 -0
- package/dist/{handlers → src/handlers}/roadmap.js +7 -3
- package/dist/src/handlers/roadmap.js.map +1 -0
- package/dist/src/handlers/syndicate.js.map +1 -0
- package/dist/src/handlers/tco.d.ts +16 -0
- package/dist/src/handlers/tco.js +303 -0
- package/dist/src/handlers/tco.js.map +1 -0
- package/dist/src/handlers/team.js.map +1 -0
- package/dist/{index.js → src/index.js} +153 -218
- package/dist/src/index.js.map +1 -0
- package/dist/src/namespace.js.map +1 -0
- package/dist/src/notifier.d.ts +5 -0
- package/dist/src/notifier.js +6 -0
- package/dist/src/notifier.js.map +1 -0
- package/dist/src/rerank.d.ts +50 -0
- package/dist/src/rerank.js +92 -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} +196 -0
- package/dist/{tools.js → src/tools.js} +178 -0
- package/dist/src/tools.js.map +1 -0
- package/dist/src/utils.js.map +1 -0
- package/package.json +4 -3
- package/packages/telegram-notify/SPLIT_PLAN.md +106 -0
- package/packages/telegram-notify/client.ts +88 -0
- package/packages/telegram-notify/emojis.json +22 -0
- package/packages/telegram-notify/pyproject.toml +17 -0
- package/packages/telegram-notify/telegram_notify/__init__.py +3 -0
- package/packages/telegram-notify/telegram_notify/client.py +87 -0
- package/packages/telegram-notify/telegram_notify/emojis.json +22 -0
- package/scripts/generate-icons.js +28 -0
- package/smithery.yaml +21 -8
- package/src/__tests__/brain-flow.test.ts +174 -0
- package/src/__tests__/handlers-unit.test.ts +1 -2
- package/src/__tests__/rerank.test.ts +111 -0
- package/src/bench/cachly-bench.ts +217 -0
- package/src/bench/fixtures.ts +226 -0
- package/src/handlers/brain.ts +276 -92
- package/src/handlers/context.ts +9 -4
- package/src/handlers/fedbrain.ts +21 -7
- package/src/handlers/roadmap.ts +7 -3
- package/src/handlers/tco.ts +347 -0
- package/src/index.ts +139 -220
- package/src/notifier.ts +5 -0
- package/src/rerank.ts +123 -0
- package/src/tools.ts +191 -0
- package/tsconfig.json +2 -3
- package/dist/__tests__/auth.test.js.map +0 -1
- package/dist/__tests__/brain-flow.test.js.map +0 -1
- package/dist/__tests__/brain-predict-failures.test.d.ts +0 -17
- package/dist/__tests__/brain-predict-failures.test.js +0 -339
- package/dist/__tests__/brain-predict-failures.test.js.map +0 -1
- package/dist/__tests__/confidence.test.js.map +0 -1
- package/dist/__tests__/e2e/api-auth.test.js.map +0 -1
- package/dist/__tests__/e2e/api-health.test.js.map +0 -1
- package/dist/__tests__/e2e/cli.test.js.map +0 -1
- package/dist/__tests__/e2e/config.js.map +0 -1
- package/dist/__tests__/e2e/funnel.test.js.map +0 -1
- package/dist/__tests__/e2e/mcp-tools.test.js.map +0 -1
- package/dist/__tests__/e2e/provisioning.test.js.map +0 -1
- package/dist/__tests__/handlers-unit.test.js.map +0 -1
- package/dist/__tests__/namespace.test.js.map +0 -1
- package/dist/auth.js.map +0 -1
- package/dist/ckg.js.map +0 -1
- package/dist/confidence.js.map +0 -1
- package/dist/embeddings.js.map +0 -1
- package/dist/handlers/advanced.js.map +0 -1
- package/dist/handlers/brain.js.map +0 -1
- package/dist/handlers/cache.js.map +0 -1
- package/dist/handlers/context.js.map +0 -1
- package/dist/handlers/fedbrain.js.map +0 -1
- package/dist/handlers/instances.js.map +0 -1
- package/dist/handlers/roadmap.js.map +0 -1
- package/dist/handlers/syndicate.js.map +0 -1
- package/dist/handlers/team.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/namespace.js.map +0 -1
- package/dist/search.js.map +0 -1
- package/dist/search.test.js.map +0 -1
- package/dist/tools.js.map +0 -1
- package/dist/utils.js.map +0 -1
- package/src/__tests__/brain-predict-failures.test.ts +0 -430
- /package/dist/{__tests__ → src/__tests__}/auth.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/auth.test.js +0 -0
- /package/dist/{__tests__ → src/__tests__}/brain-flow.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/confidence.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/confidence.test.js +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/api-auth.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/api-auth.test.js +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/api-health.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/api-health.test.js +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/cli.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/cli.test.js +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/config.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/config.js +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/funnel.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/funnel.test.js +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/mcp-tools.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/mcp-tools.test.js +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/provisioning.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/provisioning.test.js +0 -0
- /package/dist/{__tests__ → src/__tests__}/handlers-unit.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/namespace.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/namespace.test.js +0 -0
- /package/dist/{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/{handlers → src/handlers}/team.js +0 -0
- /package/dist/{index.d.ts → src/index.d.ts} +0 -0
- /package/dist/{namespace.d.ts → src/namespace.d.ts} +0 -0
- /package/dist/{namespace.js → src/namespace.js} +0 -0
- /package/dist/{search.d.ts → src/search.d.ts} +0 -0
- /package/dist/{search.js → src/search.js} +0 -0
- /package/dist/{search.test.js → src/search.test.js} +0 -0
- /package/dist/{utils.d.ts → src/utils.d.ts} +0 -0
- /package/dist/{utils.js → src/utils.js} +0 -0
package/src/handlers/brain.ts
CHANGED
|
@@ -8,19 +8,28 @@ import type { CKGEdge, CKGNode } from '../ckg.js';
|
|
|
8
8
|
import { keywordSearch, tokenize, splitMultiQuery, levenshtein,
|
|
9
9
|
ZERO_RESULTS_LOG, zeroResultsTotal, indexVocab as _indexVocab } from '../search.js';
|
|
10
10
|
import type { KeywordMatch } from '../search.js';
|
|
11
|
+
import { rerankByQuality } from '../rerank.js';
|
|
11
12
|
import { computeEmbedding, hasEmbedProvider, embedProviderHint, EMBED_PROVIDER } from '../embeddings.js';
|
|
12
13
|
import { detectNamespace } from '../namespace.js';
|
|
13
14
|
|
|
14
15
|
// ── Changelog (shown once per version in session_start) ──────────────────────
|
|
15
|
-
const MCP_VERSION = '0.10.
|
|
16
|
+
const MCP_VERSION = '0.10.50';
|
|
16
17
|
const WHATS_NEW: Record<string, string[]> = {
|
|
17
|
-
'0.10.
|
|
18
|
+
'0.10.50': [
|
|
18
19
|
`🆕 **What's new in v${MCP_VERSION}:**`,
|
|
19
|
-
` ✅
|
|
20
|
-
` ✅
|
|
21
|
-
` ✅
|
|
22
|
-
` ✅
|
|
23
|
-
`
|
|
20
|
+
` ✅ \`smart_recall\` — unified keyword + semantic hybrid list (one ranked result, not two sections)`,
|
|
21
|
+
` ✅ \`smart_recall\` — "Brain saved you here" banner surfaces time saved inline per proven lesson`,
|
|
22
|
+
` ✅ Quality reranking — proven successes outrank symptom-dense failures (+11% Precision@1)`,
|
|
23
|
+
` ✅ Cachly-Bench — IR benchmark with CI regression guard (see BENCH.md for the proof)`,
|
|
24
|
+
` 🔧 Contradiction resolution now persisted to Redis (90-day TTL)`,
|
|
25
|
+
` 💡 Run \`smart_recall\` to see your Brain's recall quality in action`,
|
|
26
|
+
],
|
|
27
|
+
'0.10.49': [
|
|
28
|
+
` ✅ \`brain_from_git\` — auto-seed your Brain from git history in seconds`,
|
|
29
|
+
` ✅ \`brain_predict_failures\` — predict CI/build failures before they happen`,
|
|
30
|
+
` ✅ VS Code extension — Brain status bar, ambient learning, CodeLens hints`,
|
|
31
|
+
` ✅ Team Brain — share lessons across your whole engineering team`,
|
|
32
|
+
` 🔧 Stability — auto-expiring memory keys, longer provisioning wait, robust recall`,
|
|
24
33
|
],
|
|
25
34
|
};
|
|
26
35
|
|
|
@@ -138,6 +147,18 @@ export async function handleBrainTool(
|
|
|
138
147
|
const resId = ckgSlug(`resolution:${topic}`);
|
|
139
148
|
await ckgUpdateEdge(redis, cId, 'contradicts', resId, false);
|
|
140
149
|
} catch { /* non-critical */ }
|
|
150
|
+
// Persist a queryable resolution record — so contradiction history survives
|
|
151
|
+
// (CKG edges alone aren't easily auditable). Bounded + TTL'd like other history.
|
|
152
|
+
try {
|
|
153
|
+
const resolutionKey = `cachly:contradictions:${topic}`;
|
|
154
|
+
const direction = prev.outcome === 'success' && outcome === 'failure'
|
|
155
|
+
? 'kept-success' : 'overwrote-failure';
|
|
156
|
+
await redis.rpush(resolutionKey, JSON.stringify({
|
|
157
|
+
ts, prev_outcome: prev.outcome, new_outcome: outcome, direction,
|
|
158
|
+
}));
|
|
159
|
+
await redis.ltrim(resolutionKey, -50, -1);
|
|
160
|
+
await redis.expire(resolutionKey, 180 * 86400);
|
|
161
|
+
} catch { /* non-critical */ }
|
|
141
162
|
contradictionWarning.push(`🗳️ Run \`madc_deliberate(topic="${topic}")\` to resolve via expert agent voting.`);
|
|
142
163
|
}
|
|
143
164
|
} catch { /* ignore parse error */ }
|
|
@@ -183,7 +204,7 @@ export async function handleBrainTool(
|
|
|
183
204
|
const existing = await redis.get(depKey);
|
|
184
205
|
const depTopics: string[] = safeJsonParse<string[]>(existing, []);
|
|
185
206
|
if (!depTopics.includes(topic)) depTopics.push(topic);
|
|
186
|
-
await redis.set(depKey, JSON.stringify(depTopics));
|
|
207
|
+
await redis.set(depKey, JSON.stringify(depTopics), 'EX', 90 * 86400);
|
|
187
208
|
}
|
|
188
209
|
|
|
189
210
|
const lessonObj = {
|
|
@@ -207,9 +228,11 @@ export async function handleBrainTool(
|
|
|
207
228
|
};
|
|
208
229
|
const lesson = JSON.stringify(lessonObj);
|
|
209
230
|
|
|
210
|
-
// Always append to the history list (audit log)
|
|
231
|
+
// Always append to the history list (audit log); keep last 100 entries, 90-day TTL
|
|
211
232
|
const listKey = `cachly:lessons:${topic}`;
|
|
212
233
|
await redis.rpush(listKey, lesson);
|
|
234
|
+
await redis.ltrim(listKey, -100, -1);
|
|
235
|
+
await redis.expire(listKey, 90 * 86400);
|
|
213
236
|
|
|
214
237
|
// Update best key for success/partial; for failure only update if no success exists
|
|
215
238
|
if (outcome === 'success' || outcome === 'partial') {
|
|
@@ -417,8 +440,27 @@ export async function handleBrainTool(
|
|
|
417
440
|
? `📌 **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.`
|
|
418
441
|
: '';
|
|
419
442
|
|
|
443
|
+
// ── Trust signal (today-safe consensus layer) ───────────────────────
|
|
444
|
+
// A lesson recalled many times — or confirmed by multiple distinct
|
|
445
|
+
// authors — has proven its value. Surface that as social proof.
|
|
446
|
+
const lessonAuthors = (lesson as { authors?: string[]; author?: string }).authors
|
|
447
|
+
?? ((lesson as { author?: string }).author ? [(lesson as { author?: string }).author!] : []);
|
|
448
|
+
const distinctAuthors = [...new Set(lessonAuthors.filter(Boolean))];
|
|
449
|
+
const rc = updatedLesson.recall_count;
|
|
450
|
+
let trustBadge = '';
|
|
451
|
+
if (distinctAuthors.length >= 2 && rc >= 5) {
|
|
452
|
+
trustBadge = `🏆 **Battle-tested** — recalled ${rc}× · verified by ${distinctAuthors.length} developers. Trust this.`;
|
|
453
|
+
} else if (rc >= 10) {
|
|
454
|
+
trustBadge = `🏆 **Battle-tested** — recalled ${rc}×. This is one of your most-proven solutions.`;
|
|
455
|
+
} else if (distinctAuthors.length >= 2) {
|
|
456
|
+
trustBadge = `✅ **Team-verified** — confirmed by ${distinctAuthors.length} developers.`;
|
|
457
|
+
} else if (rc >= 5) {
|
|
458
|
+
trustBadge = `✅ **Proven** — recalled ${rc}× without contradiction.`;
|
|
459
|
+
}
|
|
460
|
+
|
|
420
461
|
return [
|
|
421
462
|
rememberWhen,
|
|
463
|
+
trustBadge,
|
|
422
464
|
`${badge} **Best solution for \`${topic}\`** ${sevEmoji}${lesson.severity ? ` (${lesson.severity})` : ''} · recalled ${updatedLesson.recall_count}×`,
|
|
423
465
|
``,
|
|
424
466
|
`**What worked:** ${lesson.what_worked}`,
|
|
@@ -476,65 +518,48 @@ export async function handleBrainTool(
|
|
|
476
518
|
const redis = await getConnection(instance_id);
|
|
477
519
|
|
|
478
520
|
// ── Layer 1: Keyword search across ALL brain data (always works, no embedding) ──
|
|
479
|
-
const
|
|
521
|
+
const rawMatches = await keywordSearch(
|
|
480
522
|
redis,
|
|
481
523
|
['cachly:ctx:*', 'cachly:lesson:best:*', 'cachly:idx:*'],
|
|
482
524
|
query,
|
|
483
525
|
10,
|
|
484
526
|
);
|
|
485
527
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
const subQueries = splitMultiQuery(query);
|
|
490
|
-
if (subQueries.length > 1) {
|
|
491
|
-
lines.push(`_Detected ${subQueries.length} sub-topics:_ ${subQueries.map((s, i) => `${i + 1}. "${s}"`).join(', ')}\n`);
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
if (kwMatches.length > 0) {
|
|
495
|
-
lines.push(`### 🔍 BM25 Matches (${kwMatches.length})\n`);
|
|
528
|
+
// ── Layer 1.5: Quality-aware rerank — proven success lessons outrank
|
|
529
|
+
// text-similar failed attempts (the moat; see src/rerank.ts + Cachly-Bench). ──
|
|
530
|
+
const kwMatches = rerankByQuality(rawMatches);
|
|
496
531
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
532
|
+
// Increment recall_count on matched lessons (fire-and-forget) + collect "Brain saved you here" signal.
|
|
533
|
+
type RecalledLesson = { topic: string; severity: string; recall_count: number; savedMins: number };
|
|
534
|
+
const savedHere: RecalledLesson[] = [];
|
|
535
|
+
const lessonMatches = kwMatches.filter(m => m.key.startsWith('cachly:lesson:best:'));
|
|
536
|
+
for (const m of lessonMatches.slice(0, 5)) {
|
|
537
|
+
const existing = await redis.get(m.key).catch(() => null);
|
|
538
|
+
if (existing) {
|
|
539
|
+
const lesson = safeJsonParse(existing, null as null | { recall_count?: number; outcome?: string; severity?: string; [k: string]: unknown });
|
|
540
|
+
if (lesson) {
|
|
541
|
+
const updated = { ...lesson, recall_count: (lesson.recall_count ?? 0) + 1, verified_at: new Date().toISOString() };
|
|
542
|
+
redis.set(m.key, JSON.stringify(updated)).catch(() => {});
|
|
543
|
+
const sev = lesson.severity as string;
|
|
544
|
+
const savedMins = sev === 'critical' ? 240 : sev === 'major' ? 60 : 30;
|
|
545
|
+
redis.incrbyfloat(`cachly:stats:time_saved_mins:${instance_id}`, savedMins).catch(() => {});
|
|
546
|
+
// Surface banner for proven successes (recall_count >= 1 means it's been validated)
|
|
547
|
+
if (lesson.outcome === 'success' && (lesson.recall_count ?? 0) >= 1) {
|
|
548
|
+
savedHere.push({
|
|
549
|
+
topic: m.key.replace('cachly:lesson:best:', ''),
|
|
550
|
+
severity: sev ?? 'major',
|
|
551
|
+
recall_count: (lesson.recall_count ?? 0) + 1,
|
|
552
|
+
savedMins,
|
|
553
|
+
});
|
|
515
554
|
}
|
|
516
555
|
}
|
|
517
|
-
// Summary: which sub-queries had matches
|
|
518
|
-
const matched = [...grouped.keys()];
|
|
519
|
-
const unmatched = subQueries.filter(sq => !matched.includes(sq));
|
|
520
|
-
if (unmatched.length > 0) {
|
|
521
|
-
lines.push(`\n⚠️ **No results for:** ${unmatched.map(s => `"${s}"`).join(', ')}`);
|
|
522
|
-
}
|
|
523
|
-
} else {
|
|
524
|
-
for (const m of kwMatches.slice(0, 8)) {
|
|
525
|
-
const label = m.key
|
|
526
|
-
.replace('cachly:ctx:', '📝 ')
|
|
527
|
-
.replace('cachly:lesson:best:', '💡 ')
|
|
528
|
-
.replace('cachly:idx:', '📂 ');
|
|
529
|
-
const preview = m.content.slice(0, 400).replace(/\n/g, ' ');
|
|
530
|
-
lines.push(`**${label}** _(BM25: ${m.score.toFixed(2)}, matched: ${m.matchedWords.join(', ')})_`);
|
|
531
|
-
lines.push(`> ${preview}${m.content.length > 400 ? '…' : ''}\n`);
|
|
532
|
-
}
|
|
533
556
|
}
|
|
534
557
|
}
|
|
535
558
|
|
|
536
|
-
// ── Layer 2: Semantic search (
|
|
559
|
+
// ── Layer 2: Semantic search (parallel, optional) ────────────────────────
|
|
537
560
|
const inst = await apiFetch<Instance>(`/api/v1/instances/${instance_id}`);
|
|
561
|
+
type SemHit = { key: string; similarity: number; content: string };
|
|
562
|
+
const semHits: SemHit[] = [];
|
|
538
563
|
if (inst.vector_token && hasEmbedProvider()) {
|
|
539
564
|
try {
|
|
540
565
|
const embedding = await computeEmbedding(query);
|
|
@@ -544,22 +569,15 @@ export async function handleBrainTool(
|
|
|
544
569
|
headers: { 'Content-Type': 'application/json' },
|
|
545
570
|
body: JSON.stringify({ embedding, namespace: 'cachly:ctx', threshold, top_k: 5 }),
|
|
546
571
|
});
|
|
547
|
-
|
|
548
572
|
if (searchRes.ok) {
|
|
549
573
|
const results = (await searchRes.json()) as SemanticSearchResponse[];
|
|
550
|
-
const
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
const content = await redis.get(`cachly:ctx:${category}:${key}`);
|
|
558
|
-
lines.push(
|
|
559
|
-
`**${key}** _(${((hit.similarity ?? 0) * 100).toFixed(0)}% similar)_`,
|
|
560
|
-
`> ${content?.slice(0, 300) ?? '(evicted)'}${(content?.length ?? 0) > 300 ? '…' : ''}\n`,
|
|
561
|
-
);
|
|
562
|
-
}
|
|
574
|
+
for (const hit of results.filter(r => r.found && r.id)) {
|
|
575
|
+
const parts = hit.id!.replace('ctx:', '').split(':');
|
|
576
|
+
const category = parts[0];
|
|
577
|
+
const key = parts.slice(1).join(':');
|
|
578
|
+
const redisKey = `cachly:ctx:${category}:${key}`;
|
|
579
|
+
const content = await redis.get(redisKey).catch(() => null);
|
|
580
|
+
semHits.push({ key: redisKey, similarity: hit.similarity ?? 0, content: content ?? '(evicted)' });
|
|
563
581
|
}
|
|
564
582
|
}
|
|
565
583
|
} catch {
|
|
@@ -567,7 +585,106 @@ export async function handleBrainTool(
|
|
|
567
585
|
}
|
|
568
586
|
}
|
|
569
587
|
|
|
570
|
-
|
|
588
|
+
// ── Layer 3: Hybrid merge — one unified ranked list ──────────────────────
|
|
589
|
+
// BM25 quality scores (already reranked) are re-normalized to [0,1] for merging.
|
|
590
|
+
type HybridResult = {
|
|
591
|
+
key: string; content: string; hybridScore: number;
|
|
592
|
+
bm25Score?: number; semScore?: number; matchedWords?: string[];
|
|
593
|
+
matchType: 'keyword' | 'semantic' | 'hybrid'; subQuery?: string;
|
|
594
|
+
};
|
|
595
|
+
|
|
596
|
+
const bm25Scores = kwMatches.map(m => m.score);
|
|
597
|
+
const bm25Min = bm25Scores.length ? Math.min(...bm25Scores) : 0;
|
|
598
|
+
const bm25Range = bm25Scores.length ? (Math.max(...bm25Scores) - bm25Min) || 1 : 1;
|
|
599
|
+
const bm25Norm = (s: number) => (s - bm25Min) / bm25Range;
|
|
600
|
+
|
|
601
|
+
const hybridMap = new Map<string, HybridResult>();
|
|
602
|
+
for (const m of kwMatches) {
|
|
603
|
+
const n = bm25Norm(m.score);
|
|
604
|
+
hybridMap.set(m.key, {
|
|
605
|
+
key: m.key, content: m.content, bm25Score: n,
|
|
606
|
+
hybridScore: n * (semHits.length > 0 ? 0.7 : 1.0),
|
|
607
|
+
matchedWords: m.matchedWords, matchType: 'keyword', subQuery: m.subQuery,
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
for (const hit of semHits) {
|
|
611
|
+
const existing = hybridMap.get(hit.key);
|
|
612
|
+
if (existing) {
|
|
613
|
+
existing.semScore = hit.similarity;
|
|
614
|
+
existing.hybridScore = (existing.bm25Score ?? 0) * 0.6 + hit.similarity * 0.4;
|
|
615
|
+
existing.matchType = 'hybrid';
|
|
616
|
+
} else {
|
|
617
|
+
hybridMap.set(hit.key, {
|
|
618
|
+
key: hit.key, content: hit.content, semScore: hit.similarity,
|
|
619
|
+
hybridScore: hit.similarity * 0.5, matchType: 'semantic',
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
const hybridResults = [...hybridMap.values()].sort((a, b) => b.hybridScore - a.hybridScore);
|
|
624
|
+
|
|
625
|
+
// ── Build output ──────────────────────────────────────────────────────────
|
|
626
|
+
const lines: string[] = [`🧠 **Smart Recall** for: _"${query}"_\n`];
|
|
627
|
+
|
|
628
|
+
// "Brain saved you here" banner — surfaces value inline for proven recalled lessons
|
|
629
|
+
const SORDER: Record<string, number> = { critical: 0, major: 1, minor: 2 };
|
|
630
|
+
const topLesson = savedHere.sort((a, b) => (SORDER[a.severity] ?? 1) - (SORDER[b.severity] ?? 1))[0];
|
|
631
|
+
if (topLesson) {
|
|
632
|
+
lines.push(`> 💡 **Brain saved you ~${topLesson.savedMins}m here** — ${topLesson.severity} issue from \`${topLesson.topic}\` recalled ${topLesson.recall_count}× proven\n`);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// Show sub-query info if multi-topic was detected
|
|
636
|
+
const subQueries = splitMultiQuery(query);
|
|
637
|
+
if (subQueries.length > 1) {
|
|
638
|
+
lines.push(`_Detected ${subQueries.length} sub-topics:_ ${subQueries.map((s, i) => `${i + 1}. "${s}"`).join(', ')}\n`);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
if (hybridResults.length > 0) {
|
|
642
|
+
const modeLabel = semHits.length > 0
|
|
643
|
+
? `keyword + semantic hybrid`
|
|
644
|
+
: `keyword`;
|
|
645
|
+
lines.push(`### 🔍 Results (${hybridResults.length} — ${modeLabel})\n`);
|
|
646
|
+
|
|
647
|
+
// Group by sub-query for multi-topic queries
|
|
648
|
+
if (subQueries.length > 1) {
|
|
649
|
+
const grouped = new Map<string, HybridResult[]>();
|
|
650
|
+
for (const r of hybridResults.slice(0, 12)) {
|
|
651
|
+
const sq = r.subQuery ?? query;
|
|
652
|
+
if (!grouped.has(sq)) grouped.set(sq, []);
|
|
653
|
+
grouped.get(sq)!.push(r);
|
|
654
|
+
}
|
|
655
|
+
for (const [sq, results] of grouped) {
|
|
656
|
+
lines.push(`**Topic: "${sq}"** (${results.length} results)\n`);
|
|
657
|
+
for (const r of results.slice(0, 4)) {
|
|
658
|
+
const label = r.key.replace('cachly:ctx:', '📝 ').replace('cachly:lesson:best:', '💡 ').replace('cachly:idx:', '📂 ');
|
|
659
|
+
const scorePart = r.matchType === 'hybrid'
|
|
660
|
+
? `BM25: ${(r.bm25Score ?? 0).toFixed(2)}, sem: ${((r.semScore ?? 0) * 100).toFixed(0)}%, 🔀 hybrid`
|
|
661
|
+
: r.matchType === 'semantic'
|
|
662
|
+
? `sem: ${((r.semScore ?? 0) * 100).toFixed(0)}%, 🎯 semantic`
|
|
663
|
+
: `BM25: ${(r.bm25Score ?? 0).toFixed(2)}, matched: ${r.matchedWords?.join(', ')}`;
|
|
664
|
+
const preview = r.content.slice(0, 300).replace(/\n/g, ' ');
|
|
665
|
+
lines.push(` **${label}** _(${scorePart})_`);
|
|
666
|
+
lines.push(` > ${preview}${r.content.length > 300 ? '…' : ''}\n`);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
const matched = [...grouped.keys()];
|
|
670
|
+
const unmatched = subQueries.filter(sq => !matched.includes(sq));
|
|
671
|
+
if (unmatched.length > 0) {
|
|
672
|
+
lines.push(`\n⚠️ **No results for:** ${unmatched.map(s => `"${s}"`).join(', ')}`);
|
|
673
|
+
}
|
|
674
|
+
} else {
|
|
675
|
+
for (const r of hybridResults.slice(0, 8)) {
|
|
676
|
+
const label = r.key.replace('cachly:ctx:', '📝 ').replace('cachly:lesson:best:', '💡 ').replace('cachly:idx:', '📂 ');
|
|
677
|
+
const scorePart = r.matchType === 'hybrid'
|
|
678
|
+
? `BM25: ${(r.bm25Score ?? 0).toFixed(2)}, sem: ${((r.semScore ?? 0) * 100).toFixed(0)}%, 🔀 hybrid`
|
|
679
|
+
: r.matchType === 'semantic'
|
|
680
|
+
? `sem: ${((r.semScore ?? 0) * 100).toFixed(0)}%, 🎯 semantic`
|
|
681
|
+
: `BM25: ${(r.bm25Score ?? 0).toFixed(2)}, matched: ${r.matchedWords?.join(', ')}`;
|
|
682
|
+
const preview = r.content.slice(0, 400).replace(/\n/g, ' ');
|
|
683
|
+
lines.push(`**${label}** _(${scorePart})_`);
|
|
684
|
+
lines.push(`> ${preview}${r.content.length > 400 ? '…' : ''}\n`);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
} else {
|
|
571
688
|
lines.push(`⚠️ No matches found for: "${query}"`);
|
|
572
689
|
|
|
573
690
|
// Did-You-Mean: find nearest token in index vocab
|
|
@@ -897,6 +1014,31 @@ export async function handleBrainTool(
|
|
|
897
1014
|
// Brain health
|
|
898
1015
|
lines.push(`📊 **Brain:** ${lessons.length} lessons · ${ctxCount} context entries`, '');
|
|
899
1016
|
|
|
1017
|
+
// ── 📌 Proven Laws — auto-crystallized lessons (quick win 3) ─────────────
|
|
1018
|
+
// A lesson that has proven itself (recalled ≥ CRYSTALLIZE_RECALLS times) or
|
|
1019
|
+
// was explicitly pinned becomes a "law" — surfaced at the top of every
|
|
1020
|
+
// briefing so the most battle-tested knowledge is never buried. This is
|
|
1021
|
+
// the today-safe version of swarm crystallization: enough confirmations
|
|
1022
|
+
// auto-promote a lesson without any manual step.
|
|
1023
|
+
{
|
|
1024
|
+
type LessonPin = typeof lessons[0] & { pinned?: boolean };
|
|
1025
|
+
const CRYSTALLIZE_RECALLS = 5;
|
|
1026
|
+
const laws = (lessons as LessonPin[])
|
|
1027
|
+
.filter(l => l.outcome === 'success' && (l.pinned === true || (l.recall_count ?? 0) >= CRYSTALLIZE_RECALLS))
|
|
1028
|
+
.sort((a, b) => (b.recall_count ?? 0) - (a.recall_count ?? 0))
|
|
1029
|
+
.slice(0, 5);
|
|
1030
|
+
if (laws.length > 0) {
|
|
1031
|
+
lines.push(`📌 **Proven Laws** (auto-crystallized — your most-trusted knowledge):`);
|
|
1032
|
+
for (const l of laws) {
|
|
1033
|
+
const sev = l.severity === 'critical' ? '🔴' : l.severity === 'major' ? '🟡' : '';
|
|
1034
|
+
const rc = l.recall_count ?? 0;
|
|
1035
|
+
const why = l.pinned === true ? 'pinned' : `recalled ${rc}×`;
|
|
1036
|
+
lines.push(` 🏆${sev} \`${l.topic}\` _(${why})_ — ${l.what_worked.slice(0, 100)}`);
|
|
1037
|
+
}
|
|
1038
|
+
lines.push('');
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
|
|
900
1042
|
// ── Layer 7: MCM Domain Coverage Map ────────────────────────────────────
|
|
901
1043
|
if (lessons.length >= 3) {
|
|
902
1044
|
const domainMap = new Map<string, { total: number; success: number; critical: number }>();
|
|
@@ -996,8 +1138,14 @@ export async function handleBrainTool(
|
|
|
996
1138
|
for (const l of topRecalled) {
|
|
997
1139
|
const emoji = l.outcome === 'success' ? '✅' : l.outcome === 'partial' ? '⚠️' : '❌';
|
|
998
1140
|
const sev = l.severity === 'critical' ? '🔴' : l.severity === 'major' ? '🟡' : '';
|
|
999
|
-
const
|
|
1000
|
-
|
|
1141
|
+
const rc = l.recall_count ?? 0;
|
|
1142
|
+
const la = (l as { authors?: string[]; author?: string }).authors
|
|
1143
|
+
?? ((l as { author?: string }).author ? [(l as { author?: string }).author!] : []);
|
|
1144
|
+
const nAuthors = new Set(la.filter(Boolean)).size;
|
|
1145
|
+
const trust = (nAuthors >= 2 && rc >= 5) || rc >= 10 ? ' 🏆'
|
|
1146
|
+
: nAuthors >= 2 || rc >= 5 ? ' ✅' : '';
|
|
1147
|
+
const rct = rc > 1 ? ` _(${rc}× recalled)_` : '';
|
|
1148
|
+
lines.push(` ${emoji}${sev}${trust} \`${l.topic}\`${rct} — ${l.what_worked.slice(0, 100)}`);
|
|
1001
1149
|
}
|
|
1002
1150
|
lines.push('');
|
|
1003
1151
|
}
|
|
@@ -1065,27 +1213,63 @@ export async function handleBrainTool(
|
|
|
1065
1213
|
}
|
|
1066
1214
|
|
|
1067
1215
|
// ── 🔮 Predictive Pre-Warning — intent-based danger detection ────────────
|
|
1068
|
-
// Fires BEFORE work starts.
|
|
1069
|
-
|
|
1216
|
+
// Fires BEFORE work starts. Uses explicit focus when given; otherwise
|
|
1217
|
+
// derives the likely work area from the last session's changed files +
|
|
1218
|
+
// summary (you usually keep working where you left off). This makes the
|
|
1219
|
+
// warning fire even when the caller forgets to pass `focus`.
|
|
1220
|
+
{
|
|
1070
1221
|
type LessonAny = typeof lessons[0] & { author?: string; tags?: string[] };
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1222
|
+
|
|
1223
|
+
// Common path noise / extensions to drop when deriving terms from files.
|
|
1224
|
+
const PATH_NOISE = new Set([
|
|
1225
|
+
'src', 'lib', 'test', 'tests', 'dist', 'index', 'main', 'app',
|
|
1226
|
+
'internal', 'pkg', 'cmd', 'node_modules', 'components', 'utils',
|
|
1227
|
+
'ts', 'tsx', 'js', 'jsx', 'go', 'py', 'rs', 'java', 'json', 'yaml', 'yml',
|
|
1228
|
+
]);
|
|
1229
|
+
const deriveTermsFromFiles = (files: string[]): string[] => {
|
|
1230
|
+
const terms = new Set<string>();
|
|
1231
|
+
for (const f of files) {
|
|
1232
|
+
for (const seg of f.toLowerCase().split(/[/\\._-]/)) {
|
|
1233
|
+
if (seg.length > 3 && !PATH_NOISE.has(seg)) terms.add(seg);
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
return [...terms];
|
|
1237
|
+
};
|
|
1238
|
+
|
|
1239
|
+
let warnTerms = focusTerms;
|
|
1240
|
+
let warnLabel = focus;
|
|
1241
|
+
let derived = false;
|
|
1242
|
+
if (warnTerms.length === 0 && lastSession) {
|
|
1243
|
+
const fromFiles = deriveTermsFromFiles(lastSession.files_changed ?? []);
|
|
1244
|
+
const fromSummary = (lastSession.summary ?? '').toLowerCase()
|
|
1245
|
+
.replace(/[^a-z0-9\s:_-]/g, ' ').split(/\s+/).filter(t => t.length > 3 && !PATH_NOISE.has(t));
|
|
1246
|
+
warnTerms = [...new Set([...fromFiles, ...fromSummary])];
|
|
1247
|
+
warnLabel = 'where you left off last session';
|
|
1248
|
+
derived = true;
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
if (warnTerms.length > 0) {
|
|
1252
|
+
const dangerLessons = (lessons as LessonAny[]).filter(l => {
|
|
1253
|
+
if (l.outcome === 'success') return false;
|
|
1254
|
+
const topicCategory = l.topic.split(':')[0];
|
|
1255
|
+
return warnTerms.some(term =>
|
|
1256
|
+
l.topic.toLowerCase().includes(term) ||
|
|
1257
|
+
topicCategory === term ||
|
|
1258
|
+
(l.tags ?? []).some((t: string) => t.toLowerCase() === term),
|
|
1259
|
+
);
|
|
1260
|
+
});
|
|
1261
|
+
if (dangerLessons.length >= 1) {
|
|
1262
|
+
const headline = derived
|
|
1263
|
+
? ` You're likely to continue **${warnLabel}** — ${dangerLessons.length} known pitfall${dangerLessons.length > 1 ? 's' : ''} there:`
|
|
1264
|
+
: ` Known pitfalls for **"${warnLabel}"** (${dangerLessons.length} past failure${dangerLessons.length > 1 ? 's' : ''}):`;
|
|
1265
|
+
const warning = [
|
|
1266
|
+
`🚨 **PRE-WARNING** — Read this BEFORE starting:`,
|
|
1267
|
+
headline,
|
|
1268
|
+
...dangerLessons.slice(0, 3).map(l => ` ❌ \`${l.topic}\` — ${(l.what_failed ?? l.what_worked).slice(0, 80)}`),
|
|
1269
|
+
'',
|
|
1270
|
+
];
|
|
1271
|
+
lines.splice(2, 0, ...warning); // after '🧠 **Session Briefing**' + empty line
|
|
1272
|
+
}
|
|
1089
1273
|
}
|
|
1090
1274
|
}
|
|
1091
1275
|
|
package/src/handlers/context.ts
CHANGED
|
@@ -64,13 +64,18 @@ export async function handleContextTool(
|
|
|
64
64
|
if (ttl && ttl > 0) {
|
|
65
65
|
body['expires_at'] = new Date(Date.now() + ttl * 1000).toISOString();
|
|
66
66
|
}
|
|
67
|
-
await fetch(`${vectorUrl}/entries`, {
|
|
67
|
+
const res = await fetch(`${vectorUrl}/entries`, {
|
|
68
68
|
method: 'POST',
|
|
69
69
|
headers: { 'Content-Type': 'application/json' },
|
|
70
70
|
body: JSON.stringify(body),
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
signal: AbortSignal.timeout(8000),
|
|
72
|
+
});
|
|
73
|
+
if (!res.ok) {
|
|
74
|
+
process.stderr.write(`[cachly] semantic index skipped (HTTP ${res.status}) — smart_recall on this context will be keyword-only\n`);
|
|
75
|
+
}
|
|
76
|
+
} catch (e) {
|
|
77
|
+
// Embedding is optional — keyword recall still works. Log so it's debuggable.
|
|
78
|
+
process.stderr.write(`[cachly] semantic index failed: ${e instanceof Error ? e.message : String(e)} — smart_recall on this context will be keyword-only\n`);
|
|
74
79
|
}
|
|
75
80
|
}
|
|
76
81
|
|
package/src/handlers/fedbrain.ts
CHANGED
|
@@ -9,6 +9,9 @@ import { keywordSearch, tokenize } from '../search.js';
|
|
|
9
9
|
import { safeJsonParse } from '../utils.js';
|
|
10
10
|
import { computeEmbedding, hasEmbedProvider } from '../embeddings.js';
|
|
11
11
|
|
|
12
|
+
// Last brain_from_git category counts — set after each run so index.ts can include them in telemetry
|
|
13
|
+
export let _lastBrainFromGitCounts: { fixes: number; features: number; refactors: number; total: number } | null = null;
|
|
14
|
+
|
|
12
15
|
// Git concurrency semaphore (for brain_from_git parallel workers)
|
|
13
16
|
let _gitSemCount = 0;
|
|
14
17
|
const _GIT_SEM_MAX = 10;
|
|
@@ -363,8 +366,6 @@ export async function handleFedbrainTool(
|
|
|
363
366
|
|
|
364
367
|
if (hooksArr.includes('ci')) {
|
|
365
368
|
lines.push(`### GitHub Actions CI outcome hook`);
|
|
366
|
-
lines.push(`**Setup:** add a repo secret \`CACHLY_CACHE_TOKEN\` = your instance's cache token`);
|
|
367
|
-
lines.push(`(find it via \`get_connection_string\` — the token in the cache URL).`);
|
|
368
369
|
lines.push(`**Add at the end of each job** (after build/test steps):`);
|
|
369
370
|
lines.push('```yaml');
|
|
370
371
|
lines.push(`- name: cachly CLS — record CI outcome`);
|
|
@@ -372,14 +373,15 @@ export async function handleFedbrainTool(
|
|
|
372
373
|
lines.push(` run: |`);
|
|
373
374
|
lines.push(` node -e "`);
|
|
374
375
|
lines.push(` const r=require('https');`);
|
|
375
|
-
lines.push(` const d=JSON.stringify({source:'ci_outcome',payload:{`);
|
|
376
|
+
lines.push(` const d=JSON.stringify({instance_id:'${instance_id}',source:'ci_outcome',payload:{`);
|
|
376
377
|
lines.push(` status:'\${{ job.status }}',prev_status:'unknown',job:'\${{ github.job }}',`);
|
|
377
378
|
lines.push(` context:'github-actions run \${{ github.run_number }}'}});`);
|
|
378
|
-
lines.push(` r.request({hostname:'api.cachly.dev',path:'/v1/
|
|
379
|
-
lines.push(` headers:{'Content-Type':'application/json','
|
|
379
|
+
lines.push(` r.request({hostname:'api.cachly.dev',path:'/api/v1/cls/ingest',method:'POST',`);
|
|
380
|
+
lines.push(` headers:{'Content-Type':'application/json','Authorization':'Bearer $CACHLY_JWT',`);
|
|
381
|
+
lines.push(` 'Content-Length':d.length}},()=>{}).end(d);`);
|
|
380
382
|
lines.push(` " 2>/dev/null || true`);
|
|
381
383
|
lines.push(` env:`);
|
|
382
|
-
lines.push(`
|
|
384
|
+
lines.push(` CACHLY_JWT: \${{ secrets.CACHLY_JWT }}`);
|
|
383
385
|
lines.push('```');
|
|
384
386
|
lines.push('');
|
|
385
387
|
}
|
|
@@ -1013,11 +1015,16 @@ export async function handleFedbrainTool(
|
|
|
1013
1015
|
// Only store if no existing lesson for this topic (avoid overwriting higher-confidence lessons)
|
|
1014
1016
|
const existing = await redis.get(`cachly:lesson:best:${topic}`);
|
|
1015
1017
|
if (!existing) {
|
|
1016
|
-
|
|
1018
|
+
// Auto-inferred git lessons get a 90-day TTL — they self-refresh on the next
|
|
1019
|
+
// brain_from_git run, and shouldn't pin memory forever if the repo goes stale.
|
|
1020
|
+
await redis.set(`cachly:lesson:best:${topic}`, JSON.stringify(lessonObj), 'EX', 90 * 86400);
|
|
1017
1021
|
await redis.rpush(`cachly:lessons:${topic}`, JSON.stringify(lessonObj));
|
|
1022
|
+
await redis.ltrim(`cachly:lessons:${topic}`, -100, -1);
|
|
1023
|
+
await redis.expire(`cachly:lessons:${topic}`, 90 * 86400);
|
|
1018
1024
|
}
|
|
1019
1025
|
await redis.rpush('cachly:lessons:brain_from_git:all', JSON.stringify({ topic, sha: commit.sha.slice(0, 8), subject: commit.subject.slice(0, 60) }));
|
|
1020
1026
|
await redis.ltrim('cachly:lessons:brain_from_git:all', -500, -1);
|
|
1027
|
+
await redis.expire('cachly:lessons:brain_from_git:all', 90 * 86400);
|
|
1021
1028
|
|
|
1022
1029
|
// Update CKG
|
|
1023
1030
|
const conceptId = ckgSlug(topic);
|
|
@@ -1034,6 +1041,13 @@ export async function handleFedbrainTool(
|
|
|
1034
1041
|
|
|
1035
1042
|
const categoryBreakdown = [...categoryCount.entries()].sort((a, b) => b[1] - a[1]).map(([k, v]) => ` • **${k}** (${v})`).join('\n');
|
|
1036
1043
|
|
|
1044
|
+
_lastBrainFromGitCounts = {
|
|
1045
|
+
fixes: categoryCount.get('fix') ?? 0,
|
|
1046
|
+
features: categoryCount.get('feat') ?? 0,
|
|
1047
|
+
refactors: categoryCount.get('refactor') ?? 0,
|
|
1048
|
+
total: ingested,
|
|
1049
|
+
};
|
|
1050
|
+
|
|
1037
1051
|
return [
|
|
1038
1052
|
`🔁 **brain_from_git: ${repoDir}**`,
|
|
1039
1053
|
isIncremental ? `🔄 Incremental mode — only new commits since \`${lastSha.slice(0, 8)}\` were processed` : '',
|
package/src/handlers/roadmap.ts
CHANGED
|
@@ -157,9 +157,12 @@ export async function handleRoadmapTool(
|
|
|
157
157
|
const PRIORITY_ORDER: Record<string, number> = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
158
158
|
const PRIORITY_ICON: Record<string, string> = { critical: '🔴', high: '🟠', medium: '🟡', low: '🔵' };
|
|
159
159
|
const items = Object.values(all)
|
|
160
|
-
.
|
|
160
|
+
.flatMap(v => {
|
|
161
|
+
const item = safeJsonParse<Record<string, unknown> | null>(v as string, null);
|
|
162
|
+
return item ? [item] : [];
|
|
163
|
+
})
|
|
161
164
|
.filter(i => i.status === 'in-progress' || i.status === 'planned')
|
|
162
|
-
.filter(i => !filterTag || (i.tags
|
|
165
|
+
.filter(i => !filterTag || (Array.isArray(i.tags) ? i.tags : []).includes(filterTag));
|
|
163
166
|
if (items.length === 0) return '🎉 **No open roadmap items!** All tasks are done (or use `roadmap_list` to check).';
|
|
164
167
|
// in-progress first, then by priority
|
|
165
168
|
items.sort((a, b) => {
|
|
@@ -169,7 +172,8 @@ export async function handleRoadmapTool(
|
|
|
169
172
|
});
|
|
170
173
|
const next = items[0];
|
|
171
174
|
const remaining = items.length - 1;
|
|
172
|
-
const
|
|
175
|
+
const nextTags = Array.isArray(next.tags) ? next.tags as string[] : [];
|
|
176
|
+
const tags = nextTags.length ? `\nTags: ${nextTags.join(', ')}` : '';
|
|
173
177
|
const milestone = next.milestone ? `\nMilestone: ${next.milestone}` : '';
|
|
174
178
|
const notes = next.notes ? `\nNotes: ${(next.notes as string).split('\n').pop()?.slice(0, 120)}` : '';
|
|
175
179
|
return [
|