@cachly-dev/mcp-server 0.10.65 → 0.10.67

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.
Files changed (277) hide show
  1. package/dist/src/embeddings.js +10 -0
  2. package/dist/src/embeddings.js.map +1 -1
  3. package/dist/src/handlers/brain.js +2 -1
  4. package/dist/src/handlers/brain.js.map +1 -1
  5. package/dist/src/handlers/cache.js +4 -0
  6. package/dist/src/handlers/cache.js.map +1 -1
  7. package/dist/src/handlers/tco.js +1 -0
  8. package/dist/src/handlers/tco.js.map +1 -1
  9. package/dist/src/index.js +2 -2
  10. package/dist/src/index.js.map +1 -1
  11. package/package.json +7 -1
  12. package/server.json +2 -2
  13. package/.env.test.example +0 -23
  14. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -50
  15. package/.github/ISSUE_TEMPLATE/config.yml +0 -8
  16. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -25
  17. package/.github/workflows/eas-build-android.yml +0 -58
  18. package/.github/workflows/eas-update.yml +0 -55
  19. package/.github/workflows/publish.yml +0 -72
  20. package/BENCH.md +0 -113
  21. package/CHANGELOG.md +0 -653
  22. package/Dockerfile +0 -16
  23. package/LAUNCH_TEXTS.md +0 -182
  24. package/PROGRESS.md +0 -141
  25. package/STRATEGY.md +0 -228
  26. package/VISION_10X.md +0 -133
  27. package/apps/travel-chaos-organizer/.env.example +0 -49
  28. package/apps/travel-chaos-organizer/DEPLOY.md +0 -328
  29. package/apps/travel-chaos-organizer/Makefile +0 -57
  30. package/apps/travel-chaos-organizer/README.md +0 -159
  31. package/apps/travel-chaos-organizer/backend/.env.example +0 -10
  32. package/apps/travel-chaos-organizer/backend/Dockerfile +0 -25
  33. package/apps/travel-chaos-organizer/backend/alembic/env.py +0 -56
  34. package/apps/travel-chaos-organizer/backend/alembic/script.py.mako +0 -24
  35. package/apps/travel-chaos-organizer/backend/alembic/versions/.gitkeep +0 -0
  36. package/apps/travel-chaos-organizer/backend/alembic/versions/0001_stripe_customer_id.py +0 -24
  37. package/apps/travel-chaos-organizer/backend/alembic/versions/0002_telegram_chat_id.py +0 -24
  38. package/apps/travel-chaos-organizer/backend/alembic.ini +0 -39
  39. package/apps/travel-chaos-organizer/backend/app/__init__.py +0 -0
  40. package/apps/travel-chaos-organizer/backend/app/auth/__init__.py +0 -0
  41. package/apps/travel-chaos-organizer/backend/app/auth/keycloak.py +0 -81
  42. package/apps/travel-chaos-organizer/backend/app/config.py +0 -66
  43. package/apps/travel-chaos-organizer/backend/app/db/__init__.py +0 -0
  44. package/apps/travel-chaos-organizer/backend/app/db/database.py +0 -20
  45. package/apps/travel-chaos-organizer/backend/app/db/migrations/001_init.sql +0 -71
  46. package/apps/travel-chaos-organizer/backend/app/db/migrations/002_telemetry_freemium.sql +0 -31
  47. package/apps/travel-chaos-organizer/backend/app/db/migrations/003_telegram_chat_id.sql +0 -4
  48. package/apps/travel-chaos-organizer/backend/app/db/migrations/004_stripe_and_email.sql +0 -5
  49. package/apps/travel-chaos-organizer/backend/app/limiter.py +0 -4
  50. package/apps/travel-chaos-organizer/backend/app/logging_config.py +0 -25
  51. package/apps/travel-chaos-organizer/backend/app/main.py +0 -77
  52. package/apps/travel-chaos-organizer/backend/app/models/__init__.py +0 -0
  53. package/apps/travel-chaos-organizer/backend/app/models/schemas.py +0 -142
  54. package/apps/travel-chaos-organizer/backend/app/routers/__init__.py +0 -0
  55. package/apps/travel-chaos-organizer/backend/app/routers/admin.py +0 -237
  56. package/apps/travel-chaos-organizer/backend/app/routers/cache.py +0 -13
  57. package/apps/travel-chaos-organizer/backend/app/routers/events.py +0 -29
  58. package/apps/travel-chaos-organizer/backend/app/routers/export.py +0 -122
  59. package/apps/travel-chaos-organizer/backend/app/routers/inbox.py +0 -122
  60. package/apps/travel-chaos-organizer/backend/app/routers/items.py +0 -130
  61. package/apps/travel-chaos-organizer/backend/app/routers/mail.py +0 -108
  62. package/apps/travel-chaos-organizer/backend/app/routers/parse.py +0 -303
  63. package/apps/travel-chaos-organizer/backend/app/routers/payments.py +0 -179
  64. package/apps/travel-chaos-organizer/backend/app/routers/trips.py +0 -134
  65. package/apps/travel-chaos-organizer/backend/app/routers/users.py +0 -169
  66. package/apps/travel-chaos-organizer/backend/app/routers/waitlist.py +0 -37
  67. package/apps/travel-chaos-organizer/backend/app/services/__init__.py +0 -0
  68. package/apps/travel-chaos-organizer/backend/app/services/cache.py +0 -131
  69. package/apps/travel-chaos-organizer/backend/app/services/drip.py +0 -128
  70. package/apps/travel-chaos-organizer/backend/app/services/email.py +0 -61
  71. package/apps/travel-chaos-organizer/backend/app/services/errors.py +0 -44
  72. package/apps/travel-chaos-organizer/backend/app/services/notifier.py +0 -10
  73. package/apps/travel-chaos-organizer/backend/app/services/ollama.py +0 -101
  74. package/apps/travel-chaos-organizer/backend/app/services/parser.py +0 -22
  75. package/apps/travel-chaos-organizer/backend/app/services/quota.py +0 -85
  76. package/apps/travel-chaos-organizer/backend/app/services/sentry.py +0 -11
  77. package/apps/travel-chaos-organizer/backend/app/services/telemetry.py +0 -35
  78. package/apps/travel-chaos-organizer/backend/pytest.ini +0 -4
  79. package/apps/travel-chaos-organizer/backend/requirements-dev.txt +0 -6
  80. package/apps/travel-chaos-organizer/backend/requirements.txt +0 -25
  81. package/apps/travel-chaos-organizer/backend/tests/__init__.py +0 -0
  82. package/apps/travel-chaos-organizer/backend/tests/conftest.py +0 -223
  83. package/apps/travel-chaos-organizer/backend/tests/test_admin.py +0 -99
  84. package/apps/travel-chaos-organizer/backend/tests/test_cache.py +0 -142
  85. package/apps/travel-chaos-organizer/backend/tests/test_drip.py +0 -32
  86. package/apps/travel-chaos-organizer/backend/tests/test_e2e_chain.py +0 -121
  87. package/apps/travel-chaos-organizer/backend/tests/test_export.py +0 -48
  88. package/apps/travel-chaos-organizer/backend/tests/test_inbox.py +0 -107
  89. package/apps/travel-chaos-organizer/backend/tests/test_items.py +0 -138
  90. package/apps/travel-chaos-organizer/backend/tests/test_mail.py +0 -113
  91. package/apps/travel-chaos-organizer/backend/tests/test_ollama.py +0 -145
  92. package/apps/travel-chaos-organizer/backend/tests/test_parse_url.py +0 -98
  93. package/apps/travel-chaos-organizer/backend/tests/test_parser.py +0 -72
  94. package/apps/travel-chaos-organizer/backend/tests/test_payments.py +0 -163
  95. package/apps/travel-chaos-organizer/backend/tests/test_quota.py +0 -46
  96. package/apps/travel-chaos-organizer/backend/tests/test_search.py +0 -67
  97. package/apps/travel-chaos-organizer/backend/tests/test_trips.py +0 -123
  98. package/apps/travel-chaos-organizer/backend/tests/test_users.py +0 -52
  99. package/apps/travel-chaos-organizer/backups/.gitkeep +0 -0
  100. package/apps/travel-chaos-organizer/caddy/Caddyfile +0 -27
  101. package/apps/travel-chaos-organizer/docker-compose.node1.yml +0 -31
  102. package/apps/travel-chaos-organizer/docker-compose.override.yml +0 -14
  103. package/apps/travel-chaos-organizer/docker-compose.yml +0 -128
  104. package/apps/travel-chaos-organizer/frontend/.env.example +0 -13
  105. package/apps/travel-chaos-organizer/frontend/app/(app)/_layout.tsx +0 -80
  106. package/apps/travel-chaos-organizer/frontend/app/(app)/inbox.tsx +0 -166
  107. package/apps/travel-chaos-organizer/frontend/app/(app)/search.tsx +0 -130
  108. package/apps/travel-chaos-organizer/frontend/app/(app)/settings.tsx +0 -381
  109. package/apps/travel-chaos-organizer/frontend/app/(app)/trips/[id].tsx +0 -346
  110. package/apps/travel-chaos-organizer/frontend/app/(app)/trips.tsx +0 -363
  111. package/apps/travel-chaos-organizer/frontend/app/(app)/upgrade/cancel.tsx +0 -34
  112. package/apps/travel-chaos-organizer/frontend/app/(app)/upgrade/success.tsx +0 -35
  113. package/apps/travel-chaos-organizer/frontend/app/(auth)/login.tsx +0 -91
  114. package/apps/travel-chaos-organizer/frontend/app/_layout.tsx +0 -71
  115. package/apps/travel-chaos-organizer/frontend/app/auth.tsx +0 -29
  116. package/apps/travel-chaos-organizer/frontend/app/index.tsx +0 -16
  117. package/apps/travel-chaos-organizer/frontend/app/onboarding.tsx +0 -119
  118. package/apps/travel-chaos-organizer/frontend/app.config.ts +0 -54
  119. package/apps/travel-chaos-organizer/frontend/assets/adaptive-icon.svg +0 -66
  120. package/apps/travel-chaos-organizer/frontend/assets/icon.svg +0 -79
  121. package/apps/travel-chaos-organizer/frontend/assets/splash.svg +0 -15
  122. package/apps/travel-chaos-organizer/frontend/components/BottomSheet.tsx +0 -102
  123. package/apps/travel-chaos-organizer/frontend/components/ConfettiCannon.tsx +0 -107
  124. package/apps/travel-chaos-organizer/frontend/components/ConfirmDialog.tsx +0 -109
  125. package/apps/travel-chaos-organizer/frontend/components/ErrorBoundary.tsx +0 -45
  126. package/apps/travel-chaos-organizer/frontend/components/FileUploadButton.tsx +0 -19
  127. package/apps/travel-chaos-organizer/frontend/components/ImportSheet.tsx +0 -155
  128. package/apps/travel-chaos-organizer/frontend/components/OfflineBadge.tsx +0 -55
  129. package/apps/travel-chaos-organizer/frontend/components/QRCodeViewer.tsx +0 -80
  130. package/apps/travel-chaos-organizer/frontend/components/ScoreRing.tsx +0 -39
  131. package/apps/travel-chaos-organizer/frontend/components/Screen.tsx +0 -39
  132. package/apps/travel-chaos-organizer/frontend/components/Skeleton.tsx +0 -70
  133. package/apps/travel-chaos-organizer/frontend/components/SwipeableInboxItem.tsx +0 -121
  134. package/apps/travel-chaos-organizer/frontend/components/SwipeableTimelineItem.tsx +0 -70
  135. package/apps/travel-chaos-organizer/frontend/components/TimelineItem.tsx +0 -73
  136. package/apps/travel-chaos-organizer/frontend/components/TimelineItemDetail.tsx +0 -130
  137. package/apps/travel-chaos-organizer/frontend/components/Toast.tsx +0 -60
  138. package/apps/travel-chaos-organizer/frontend/components/ToastContext.tsx +0 -42
  139. package/apps/travel-chaos-organizer/frontend/components/TripCard.tsx +0 -146
  140. package/apps/travel-chaos-organizer/frontend/components/TripQRModal.tsx +0 -70
  141. package/apps/travel-chaos-organizer/frontend/components/UpgradeBanner.tsx +0 -44
  142. package/apps/travel-chaos-organizer/frontend/eas.json +0 -22
  143. package/apps/travel-chaos-organizer/frontend/hooks/useInbox.ts +0 -24
  144. package/apps/travel-chaos-organizer/frontend/hooks/useNetworkSync.ts +0 -60
  145. package/apps/travel-chaos-organizer/frontend/hooks/useTrips.ts +0 -112
  146. package/apps/travel-chaos-organizer/frontend/lib/api.ts +0 -223
  147. package/apps/travel-chaos-organizer/frontend/lib/auth.ts +0 -93
  148. package/apps/travel-chaos-organizer/frontend/lib/db.ts +0 -86
  149. package/apps/travel-chaos-organizer/frontend/lib/fileCache.ts +0 -54
  150. package/apps/travel-chaos-organizer/frontend/lib/haptics.ts +0 -13
  151. package/apps/travel-chaos-organizer/frontend/lib/notifications.ts +0 -112
  152. package/apps/travel-chaos-organizer/frontend/lib/offlineQueue.ts +0 -145
  153. package/apps/travel-chaos-organizer/frontend/lib/quota.tsx +0 -43
  154. package/apps/travel-chaos-organizer/frontend/lib/sentry.ts +0 -28
  155. package/apps/travel-chaos-organizer/frontend/lib/shareIntent.ts +0 -44
  156. package/apps/travel-chaos-organizer/frontend/package-lock.json +0 -13221
  157. package/apps/travel-chaos-organizer/frontend/package.json +0 -44
  158. package/apps/travel-chaos-organizer/frontend/tsconfig.json +0 -23
  159. package/apps/travel-chaos-organizer/keycloak/realm-export.json +0 -59
  160. package/apps/travel-chaos-organizer/landing/index.html +0 -773
  161. package/apps/travel-chaos-organizer/scripts/backup.sh +0 -14
  162. package/apps/travel-chaos-organizer/scripts/configure-env.sh +0 -82
  163. package/apps/travel-chaos-organizer/scripts/deploy.sh +0 -54
  164. package/apps/travel-chaos-organizer/scripts/health-check.sh +0 -36
  165. package/apps/travel-chaos-organizer/scripts/migrate.sh +0 -7
  166. package/apps/travel-chaos-organizer/scripts/server-setup.sh +0 -85
  167. package/apps/travel-chaos-organizer/scripts/smoke-test.sh +0 -29
  168. package/apps/travel-chaos-organizer/scripts/verify-tracking.sh +0 -71
  169. package/apps/travel-chaos-organizer/telegram-bot/.env.example +0 -3
  170. package/apps/travel-chaos-organizer/telegram-bot/Dockerfile +0 -6
  171. package/apps/travel-chaos-organizer/telegram-bot/bot.py +0 -504
  172. package/apps/travel-chaos-organizer/telegram-bot/requirements.txt +0 -3
  173. package/dist/src/__tests__/auth.test.d.ts +0 -1
  174. package/dist/src/__tests__/auth.test.js +0 -107
  175. package/dist/src/__tests__/auth.test.js.map +0 -1
  176. package/dist/src/__tests__/brain-flow.test.d.ts +0 -12
  177. package/dist/src/__tests__/brain-flow.test.js +0 -1241
  178. package/dist/src/__tests__/brain-flow.test.js.map +0 -1
  179. package/dist/src/__tests__/confidence.test.d.ts +0 -13
  180. package/dist/src/__tests__/confidence.test.js +0 -233
  181. package/dist/src/__tests__/confidence.test.js.map +0 -1
  182. package/dist/src/__tests__/e2e/api-auth.test.d.ts +0 -7
  183. package/dist/src/__tests__/e2e/api-auth.test.js +0 -162
  184. package/dist/src/__tests__/e2e/api-auth.test.js.map +0 -1
  185. package/dist/src/__tests__/e2e/api-health.test.d.ts +0 -7
  186. package/dist/src/__tests__/e2e/api-health.test.js +0 -95
  187. package/dist/src/__tests__/e2e/api-health.test.js.map +0 -1
  188. package/dist/src/__tests__/e2e/cli.test.d.ts +0 -10
  189. package/dist/src/__tests__/e2e/cli.test.js +0 -204
  190. package/dist/src/__tests__/e2e/cli.test.js.map +0 -1
  191. package/dist/src/__tests__/e2e/config.d.ts +0 -35
  192. package/dist/src/__tests__/e2e/config.js +0 -53
  193. package/dist/src/__tests__/e2e/config.js.map +0 -1
  194. package/dist/src/__tests__/e2e/funnel.test.d.ts +0 -13
  195. package/dist/src/__tests__/e2e/funnel.test.js +0 -90
  196. package/dist/src/__tests__/e2e/funnel.test.js.map +0 -1
  197. package/dist/src/__tests__/e2e/mcp-tools.test.d.ts +0 -7
  198. package/dist/src/__tests__/e2e/mcp-tools.test.js +0 -129
  199. package/dist/src/__tests__/e2e/mcp-tools.test.js.map +0 -1
  200. package/dist/src/__tests__/e2e/provisioning.test.d.ts +0 -15
  201. package/dist/src/__tests__/e2e/provisioning.test.js +0 -123
  202. package/dist/src/__tests__/e2e/provisioning.test.js.map +0 -1
  203. package/dist/src/__tests__/handlers-unit.test.d.ts +0 -12
  204. package/dist/src/__tests__/handlers-unit.test.js +0 -505
  205. package/dist/src/__tests__/handlers-unit.test.js.map +0 -1
  206. package/dist/src/__tests__/namespace.test.d.ts +0 -1
  207. package/dist/src/__tests__/namespace.test.js +0 -114
  208. package/dist/src/__tests__/namespace.test.js.map +0 -1
  209. package/dist/src/__tests__/rerank.test.d.ts +0 -1
  210. package/dist/src/__tests__/rerank.test.js +0 -125
  211. package/dist/src/__tests__/rerank.test.js.map +0 -1
  212. package/dist/src/__tests__/utils.test.d.ts +0 -1
  213. package/dist/src/__tests__/utils.test.js +0 -83
  214. package/dist/src/__tests__/utils.test.js.map +0 -1
  215. package/dist/src/bench/cachly-bench.d.ts +0 -39
  216. package/dist/src/bench/cachly-bench.js +0 -223
  217. package/dist/src/bench/cachly-bench.js.map +0 -1
  218. package/dist/src/bench/fixtures.d.ts +0 -37
  219. package/dist/src/bench/fixtures.js +0 -225
  220. package/dist/src/bench/fixtures.js.map +0 -1
  221. package/dist/src/search.test.d.ts +0 -1
  222. package/dist/src/search.test.js +0 -713
  223. package/dist/src/search.test.js.map +0 -1
  224. package/eslint.config.js +0 -16
  225. package/glama.json +0 -36
  226. package/llms.txt +0 -155
  227. package/packages/telegram-notify/SPLIT_PLAN.md +0 -106
  228. package/packages/telegram-notify/client.ts +0 -88
  229. package/packages/telegram-notify/emojis.json +0 -22
  230. package/packages/telegram-notify/pyproject.toml +0 -17
  231. package/packages/telegram-notify/telegram_notify/__init__.py +0 -3
  232. package/packages/telegram-notify/telegram_notify/client.py +0 -87
  233. package/packages/telegram-notify/telegram_notify/emojis.json +0 -22
  234. package/scripts/e2e-run.sh +0 -160
  235. package/scripts/generate-icons.js +0 -28
  236. package/smithery.yaml +0 -45
  237. package/src/__tests__/auth.test.ts +0 -147
  238. package/src/__tests__/brain-flow.test.ts +0 -1472
  239. package/src/__tests__/confidence.test.ts +0 -285
  240. package/src/__tests__/e2e/api-auth.test.ts +0 -182
  241. package/src/__tests__/e2e/api-health.test.ts +0 -103
  242. package/src/__tests__/e2e/cli.test.ts +0 -228
  243. package/src/__tests__/e2e/config.ts +0 -53
  244. package/src/__tests__/e2e/funnel.test.ts +0 -99
  245. package/src/__tests__/e2e/mcp-tools.test.ts +0 -191
  246. package/src/__tests__/e2e/provisioning.test.ts +0 -128
  247. package/src/__tests__/handlers-unit.test.ts +0 -677
  248. package/src/__tests__/namespace.test.ts +0 -126
  249. package/src/__tests__/rerank.test.ts +0 -141
  250. package/src/__tests__/utils.test.ts +0 -81
  251. package/src/auth.ts +0 -46
  252. package/src/bench/cachly-bench.ts +0 -263
  253. package/src/bench/fixtures.ts +0 -256
  254. package/src/ckg.ts +0 -103
  255. package/src/confidence.ts +0 -38
  256. package/src/embeddings.ts +0 -217
  257. package/src/handlers/advanced.ts +0 -647
  258. package/src/handlers/brain.ts +0 -2622
  259. package/src/handlers/cache.ts +0 -633
  260. package/src/handlers/context.ts +0 -222
  261. package/src/handlers/fedbrain.ts +0 -1232
  262. package/src/handlers/instances.ts +0 -180
  263. package/src/handlers/roadmap.ts +0 -201
  264. package/src/handlers/syndicate.ts +0 -460
  265. package/src/handlers/tco.ts +0 -345
  266. package/src/handlers/team.ts +0 -1249
  267. package/src/index.ts +0 -2815
  268. package/src/namespace.ts +0 -128
  269. package/src/notifier.ts +0 -5
  270. package/src/rerank.ts +0 -148
  271. package/src/search.test.ts +0 -848
  272. package/src/search.ts +0 -1781
  273. package/src/tools.ts +0 -2056
  274. package/src/utils.ts +0 -90
  275. package/test-features.cjs +0 -23
  276. package/tsconfig.json +0 -16
  277. package/vitest.config.ts +0 -14
@@ -1,1232 +0,0 @@
1
- import { createHmac } from 'node:crypto';
2
- import type { Redis } from 'ioredis';
3
- import { ckgSlug, extractProblemConcept, ckgUpsertNode, ckgUpdateEdge,
4
- ckgUpsertPersonNode, ckgUpsertFileNode } from '../ckg.js';
5
- import type { CKGEdge, CKGNode } from '../ckg.js';
6
- import { safeJsonParse, normalizeGitPath } from '../utils.js';
7
-
8
- // Last brain_from_git category counts — set after each run so index.ts can include them in telemetry
9
- export let _lastBrainFromGitCounts: { fixes: number; features: number; refactors: number; total: number } | null = null;
10
-
11
- // Git concurrency semaphore (for brain_from_git parallel workers)
12
- let _gitSemCount = 0;
13
- const _GIT_SEM_MAX = 10;
14
- const _gitSemQueue: Array<() => void> = [];
15
- function _gitSemAcquire(): Promise<void> {
16
- if (_gitSemCount < _GIT_SEM_MAX) { _gitSemCount++; return Promise.resolve(); }
17
- return new Promise(resolve => _gitSemQueue.push(resolve));
18
- }
19
- function _gitSemRelease(): void {
20
- const next = _gitSemQueue.shift();
21
- if (next) { next(); } else { _gitSemCount--; }
22
- }
23
-
24
- type GetConnection = (instanceId: string) => Promise<Redis>;
25
- type ApiFetch = <T>(path: string, options?: RequestInit) => Promise<T>;
26
-
27
- export const FEDBRAIN_TOOL_NAMES = new Set([
28
- 'madc_deliberate', 'cls_ingest', 'cls_install_hooks', 'fedbrain_contribute', 'fedbrain_search',
29
- 'fedbrain_confirm', 'fedbrain_status', 'brain_federate', 'crystal_view', 'compact_recover',
30
- 'brain_from_git', 'brain_predict_failures',
31
- ]);
32
-
33
- export async function handleFedbrainTool(
34
- name: string,
35
- args: Record<string, unknown>,
36
- getConnection: GetConnection,
37
- apiFetch: ApiFetch,
38
- ): Promise<string | null> {
39
- switch (name) {
40
- case 'madc_deliberate': {
41
- const { instance_id, topic } = args as { instance_id: string; topic: string };
42
- const redis = await getConnection(instance_id);
43
-
44
- const historyRaw = await redis.lrange(`cachly:lessons:${topic}`, 0, -1);
45
- const history = historyRaw.map(r => { try { return JSON.parse(r) as { outcome: string; what_worked?: string; what_failed?: string; ts?: string }; } catch { return null; } }).filter(Boolean) as Array<{ outcome: string; what_worked?: string; what_failed?: string; ts?: string }>;
46
-
47
- if (history.length < 2) {
48
- return [
49
- `🗳️ **MADC: "${topic}"**`, '',
50
- `Not enough history for deliberation (need ≥ 2 entries, found ${history.length}).`,
51
- '', `Call \`learn_from_attempts\` with conflicting outcomes to trigger deliberation.`,
52
- ].join('\n');
53
- }
54
-
55
- // Specialist agents and their domain keywords
56
- const AGENTS = [
57
- { name: 'InfraAgent', domains: ['infra', 'k8s', 'docker', 'server', 'wireguard', 'helm'] },
58
- { name: 'AuthAgent', domains: ['auth', 'jwt', 'keycloak', 'oauth', 'oidc', 'token'] },
59
- { name: 'DeployAgent', domains: ['deploy', 'ci', 'pipeline', 'rsync', 'release'] },
60
- { name: 'DatabaseAgent', domains: ['db', 'gorm', 'migration', 'postgres', 'clickhouse', 'redis'] },
61
- { name: 'DebugAgent', domains: ['debug', 'panic', 'race', 'nil', 'fix', 'error'] },
62
- { name: 'APIAgent', domains: ['api', 'http', 'grpc', 'rest', 'fiber', 'web'] },
63
- ];
64
-
65
- const topicDomain = topic.split(':')[0] ?? '';
66
- const relevantAgents = AGENTS.filter(a => a.domains.some(d => topicDomain === d || topic.includes(d)));
67
- const votingAgents = relevantAgents.length > 0 ? relevantAgents : AGENTS;
68
-
69
- // Measure each agent's CKG coverage in their domains
70
- const agentCoverage = new Map<string, number>();
71
- for (const agent of votingAgents) {
72
- let edgeCount = 0;
73
- for (const domain of agent.domains) {
74
- const nodeKeys: string[] = [];
75
- const nStream = redis.scanStream({ match: `cachly:ckg:node:${domain}*`, count: 50 });
76
- await new Promise<void>((res, rej) => { nStream.on('data', (b: string[]) => nodeKeys.push(...b)); nStream.on('end', res); nStream.on('error', rej); });
77
- edgeCount += nodeKeys.length;
78
- }
79
- agentCoverage.set(agent.name, edgeCount);
80
- }
81
-
82
- const successLessons = history.filter(l => l.outcome === 'success' || l.outcome === 'partial');
83
- const failureLessons = history.filter(l => l.outcome === 'failure');
84
-
85
- if (failureLessons.length === 0) {
86
- return [
87
- `🗳️ **MADC: "${topic}"**`, '',
88
- `No contradictions found — all ${successLessons.length} entries have non-failure outcomes.`,
89
- '', `Use \`ckg_inspect(concept="${ckgSlug(topic)}")\` to explore the confidence graph.`,
90
- ].join('\n');
91
- }
92
-
93
- // Agent voting logic
94
- const votes: Array<{ agent: string; vote: 'success' | 'failure' | 'abstain'; coverage: number; reason: string }> = [];
95
- for (const agent of votingAgents) {
96
- const coverage = agentCoverage.get(agent.name) ?? 0;
97
- let vote: 'success' | 'failure' | 'abstain';
98
- let reason: string;
99
- if (coverage < 2) {
100
- vote = 'abstain'; reason = 'insufficient domain coverage';
101
- } else if (successLessons.length >= failureLessons.length * 2) {
102
- vote = 'success'; reason = `${successLessons.length}/${history.length} entries confirm success`;
103
- } else if (failureLessons.length >= successLessons.length * 2) {
104
- vote = 'failure'; reason = `${failureLessons.length}/${history.length} entries confirm failure`;
105
- } else {
106
- vote = 'abstain'; reason = `contested (${successLessons.length} success vs ${failureLessons.length} failure)`;
107
- }
108
- votes.push({ agent: agent.name, vote, coverage, reason });
109
- }
110
-
111
- const successVotes = votes.filter(v => v.vote === 'success').length;
112
- const failureVotes = votes.filter(v => v.vote === 'failure').length;
113
- const abstainVotes = votes.filter(v => v.vote === 'abstain').length;
114
-
115
- let resolution: 'unanimous_success' | 'unanimous_failure' | 'contested';
116
- let resolutionText: string;
117
- if (successVotes > 0 && failureVotes === 0) {
118
- resolution = 'unanimous_success';
119
- resolutionText = `✅ **Unanimous: SUCCESS** — ${successVotes} agent(s) confirm, ${abstainVotes} abstain`;
120
- } else if (failureVotes > 0 && successVotes === 0) {
121
- resolution = 'unanimous_failure';
122
- resolutionText = `❌ **Unanimous: FAILURE** — ${failureVotes} agent(s) confirm, ${abstainVotes} abstain`;
123
- } else {
124
- resolution = 'contested';
125
- resolutionText = `⚠️ **CONTESTED** — ${successVotes} success votes, ${failureVotes} failure votes, ${abstainVotes} abstain`;
126
- }
127
-
128
- // Store resolution as CKG node
129
- const resNodeId = ckgSlug(`resolution:${topic}`);
130
- const resNode = {
131
- id: resNodeId, domain: 'resolution', type: 'resolution', count: 1,
132
- ts: new Date().toISOString(), resolution, topic,
133
- votes: { success: successVotes, failure: failureVotes, abstain: abstainVotes },
134
- };
135
- await redis.set(`cachly:ckg:node:${resNodeId}`, JSON.stringify(resNode));
136
-
137
- // Write contradicts edge if contested; decay loser confidence to 0.1 if unanimous
138
- if (resolution === 'contested') {
139
- await ckgUpdateEdge(redis, ckgSlug(topic), 'contradicts', resNodeId, false);
140
- } else {
141
- // Unanimous — demote the losing side's edges to near-zero confidence
142
- const loserOutcome = resolution === 'unanimous_success' ? 'failure' : 'success';
143
- const loserLessons = (loserOutcome === 'failure' ? failureLessons : successLessons);
144
- if (loserLessons.length > 0) {
145
- // Decay all 'fixes' edges from this concept that contradict the resolution
146
- const fromKeys = await redis.smembers(`cachly:ckg:idx:from:${ckgSlug(topic)}`);
147
- for (const fk of fromKeys) {
148
- const er = await redis.get(fk);
149
- if (!er) continue;
150
- try {
151
- const edge: CKGEdge = JSON.parse(er);
152
- if (edge.edgeType === 'fixes' && edge.confidence > 0.15) {
153
- if (resolution === 'unanimous_failure') {
154
- // Fix was wrong — decay to 0.1
155
- edge.confidence = 0.1;
156
- edge.last_updated = new Date().toISOString();
157
- await redis.set(fk, JSON.stringify(edge));
158
- }
159
- }
160
- } catch { /* skip corrupt edges */ }
161
- }
162
- }
163
- // Remove the conflict marker — deliberation resolved it
164
- await redis.del(`cachly:ckg:conflict:${ckgSlug(topic)}`);
165
- }
166
-
167
- const lines = [
168
- `🗳️ **MADC Deliberation: "${topic}"**`, '',
169
- `📊 Evidence: ${successLessons.length} success/partial vs ${failureLessons.length} failure entries (${history.length} total)`,
170
- '', `**Voting agents (${votingAgents.length}):**`,
171
- ...votes.map(v => {
172
- const icon = v.vote === 'success' ? '✅' : v.vote === 'failure' ? '❌' : '⬜';
173
- const covBar = '▓'.repeat(Math.min(v.coverage, 5)) + '░'.repeat(Math.max(0, 5 - v.coverage));
174
- return ` ${icon} **${v.agent}** [${covBar}] ${v.coverage} CKG edges — ${v.reason}`;
175
- }),
176
- '', resolutionText, '',
177
- ];
178
-
179
- if (resolution === 'unanimous_success') {
180
- lines.push(`🔧 Failure entries superseded — store confirmed lesson: \`learn_from_attempts(topic="${topic}", outcome="success", ...)\``);
181
- } else if (resolution === 'unanimous_failure') {
182
- lines.push(`🚫 Success claims unconfirmed — re-verify: \`recall_best_solution(topic="${topic}")\``);
183
- } else {
184
- lines.push(`⚠️ Contested — run \`causal_trace\` before acting. Explore: \`ckg_inspect(concept="${ckgSlug(topic)}")\``);
185
- }
186
-
187
- lines.push('', `📝 Resolution node: \`cachly:ckg:node:${resNodeId}\``);
188
- return lines.join('\n');
189
- }
190
-
191
- // ── Layer 5: CLS — cls_ingest ─────────────────────────────────────────────
192
- case 'cls_ingest': {
193
- const { instance_id, source, payload } = args as {
194
- instance_id: string;
195
- source: 'git_commit' | 'ci_outcome' | 'ide_diagnostic';
196
- payload: Record<string, unknown>;
197
- };
198
- const redis = await getConnection(instance_id);
199
- const ts = new Date().toISOString();
200
- const clsKey = 'cachly:cls:events';
201
-
202
- if (source === 'git_commit') {
203
- const message = String(payload.message ?? '');
204
- const sha = String(payload.sha ?? '');
205
- const files = (Array.isArray(payload.files) ? payload.files : []) as string[];
206
-
207
- const domain = /^fix/i.test(message) ? 'fix' : /^feat/i.test(message) ? 'feat' : /^refactor/i.test(message) ? 'refactor' : /^test/i.test(message) ? 'test' : 'commit';
208
- const slug = `${domain}:${ckgSlug(message.slice(0, 60))}`;
209
- const conceptId = ckgSlug(slug);
210
-
211
- await ckgUpsertNode(redis, conceptId, domain, 'commit');
212
- for (const f of files.slice(0, 10)) {
213
- const fd = f.includes('auth') ? 'auth' : f.includes('api') ? 'api' : f.includes('infra') ? 'infra' : f.includes('web') ? 'web' : 'code';
214
- const fileId = ckgSlug(`file:${fd}`);
215
- await ckgUpsertNode(redis, fileId, 'file', fd);
216
- await ckgUpdateEdge(redis, conceptId, 'co-occurs', fileId, true);
217
- }
218
-
219
- const lessonObj = {
220
- topic: slug, outcome: 'success' as const, what_worked: message, what_failed: '',
221
- context: `CLS/git: sha=${sha}`, severity: 'minor' as const,
222
- file_paths: files.slice(0, 10), commands: sha ? [`git show ${sha}`] : [],
223
- tags: ['cls', 'git'], depends_on: [], recall_count: 0, ts, verified_at: ts,
224
- confidence: 0.6, audit_trail: [{ ts, action: 'cls_git_commit' }], version: 3,
225
- };
226
- await redis.rpush(`cachly:lessons:${slug}`, JSON.stringify(lessonObj));
227
- const existing = await redis.get(`cachly:lesson:best:${slug}`);
228
- if (!existing) await redis.set(`cachly:lesson:best:${slug}`, JSON.stringify(lessonObj));
229
-
230
- await redis.rpush(clsKey, JSON.stringify({ source, payload: { message, sha }, ts }));
231
- await redis.ltrim(clsKey, -200, -1);
232
-
233
- return [
234
- `📨 **CLS Ingested: git_commit**`, '',
235
- `Commit \`${sha.slice(0, 8) || '?'}\`: ${message.slice(0, 80)}`,
236
- `Concept: \`${conceptId}\` (${domain}) · Files: ${files.length}`,
237
- '', `🕸️ CKG: \`${conceptId}\` + ${files.length} file edges · Lesson: \`${slug}\``,
238
- `💡 Inspect: \`ckg_inspect(concept="${domain}")\``,
239
- ].join('\n');
240
- }
241
-
242
- if (source === 'ci_outcome') {
243
- const status = String(payload.status ?? '');
244
- const prev_status = String(payload.prev_status ?? '');
245
- const job = String(payload.job ?? 'unknown');
246
- const ciCtx = String(payload.context ?? '');
247
-
248
- const isFixed = ['failure', 'red', 'error'].includes(prev_status) && ['success', 'green', 'passed'].includes(status);
249
- const isBroken = ['success', 'green', 'passed'].includes(prev_status) && ['failure', 'red', 'error'].includes(status);
250
-
251
- const slug = `ci:${ckgSlug(job)}`;
252
- const conceptId = ckgSlug(slug);
253
- await ckgUpsertNode(redis, conceptId, 'ci', 'job');
254
-
255
- if (isFixed) {
256
- const problemId = ckgSlug(`problem:${ckgSlug(job)}`);
257
- await ckgUpsertNode(redis, problemId, 'problem', 'ci-failure');
258
- await ckgUpdateEdge(redis, conceptId, 'fixes', problemId, true);
259
- const lessonObj = {
260
- topic: slug, outcome: 'success' as const,
261
- what_worked: `CI job "${job}" went ${prev_status} → ${status}`,
262
- what_failed: `Job "${job}" was failing`, context: `CLS/ci: ${ciCtx}`,
263
- severity: 'major' as const, file_paths: [], commands: [], tags: ['cls', 'ci'],
264
- depends_on: [], recall_count: 0, ts, verified_at: ts, confidence: 0.75,
265
- audit_trail: [{ ts, action: 'cls_ci_fixed' }], version: 3,
266
- };
267
- await redis.rpush(`cachly:lessons:${slug}`, JSON.stringify(lessonObj));
268
- await redis.set(`cachly:lesson:best:${slug}`, JSON.stringify(lessonObj));
269
- } else if (isBroken) {
270
- const causeId = ckgSlug(`cause:${ckgSlug(job)}`);
271
- await ckgUpsertNode(redis, causeId, 'cause', 'ci-break');
272
- await ckgUpdateEdge(redis, conceptId, 'causes', causeId, false);
273
- }
274
-
275
- await redis.rpush(clsKey, JSON.stringify({ source, payload: { status, prev_status, job }, ts }));
276
- await redis.ltrim(clsKey, -200, -1);
277
-
278
- const statusIcon = isFixed ? '✅ Fixed' : isBroken ? '🔴 Broken' : '📊 Recorded';
279
- return [
280
- `📨 **CLS Ingested: ci_outcome**`, '',
281
- `${statusIcon}: \`${job}\` — ${prev_status || '?'} → ${status}`,
282
- isFixed ? `🔧 CKG \`fixes\` edge added (75% confidence)` : isBroken ? `⚡ CKG \`causes\` edge added` : `📊 State recorded`,
283
- `💡 Lesson: \`${slug}\` | Predict: \`brain_predict(context="${job}")\``,
284
- ].join('\n');
285
- }
286
-
287
- if (source === 'ide_diagnostic') {
288
- const error = String(payload.error ?? '');
289
- const fix = String(payload.fix ?? '');
290
- const file = String(payload.file ?? '');
291
-
292
- const errorConcept = extractProblemConcept(error) ?? 'unknown-error';
293
- const slug = `debug:${ckgSlug(errorConcept)}`;
294
- const conceptId = ckgSlug(slug);
295
- const problemId = ckgSlug(`problem:${errorConcept}`);
296
-
297
- await ckgUpsertNode(redis, conceptId, 'debug', 'diagnostic');
298
- await ckgUpsertNode(redis, problemId, 'problem', 'compiler-error');
299
- await ckgUpdateEdge(redis, conceptId, 'fixes', problemId, true);
300
-
301
- const lessonObj = {
302
- topic: slug, outcome: 'success' as const, what_worked: fix, what_failed: error,
303
- context: `CLS/ide: ${file}`, severity: 'minor' as const,
304
- file_paths: file ? [file] : [], commands: [], tags: ['cls', 'ide-diagnostic'],
305
- depends_on: [], recall_count: 0, ts, verified_at: ts, confidence: 0.65,
306
- audit_trail: [{ ts, action: 'cls_ide_diagnostic' }], version: 3,
307
- };
308
- await redis.rpush(`cachly:lessons:${slug}`, JSON.stringify(lessonObj));
309
- const existingL = await redis.get(`cachly:lesson:best:${slug}`);
310
- if (!existingL) await redis.set(`cachly:lesson:best:${slug}`, JSON.stringify(lessonObj));
311
-
312
- await redis.rpush(clsKey, JSON.stringify({ source, payload: { error: error.slice(0, 60), fix: fix.slice(0, 60), file }, ts }));
313
- await redis.ltrim(clsKey, -200, -1);
314
-
315
- return [
316
- `📨 **CLS Ingested: ide_diagnostic**`, '',
317
- `Error: \`${error.slice(0, 80)}\``,
318
- `Fix: ${fix.slice(0, 100)}`,
319
- file ? `File: \`${file}\`` : '',
320
- '', `🕸️ CKG: \`${conceptId}\` → fixes → \`${problemId}\` | Lesson: \`${slug}\``,
321
- ].filter(l => l !== '').join('\n');
322
- }
323
-
324
- return `❌ Unknown CLS source: "${source}". Valid: git_commit, ci_outcome, ide_diagnostic`;
325
- }
326
-
327
- // ── Layer 5: CLS — cls_install_hooks ─────────────────────────────────────
328
- case 'cls_install_hooks': {
329
- const { instance_id, repo_path = '.', hooks = ['git', 'ci'] } = args as {
330
- instance_id: string; repo_path?: string; hooks?: string[];
331
- };
332
- const hooksArr = Array.isArray(hooks) ? hooks : ['git', 'ci'];
333
- const lines: string[] = [`🔌 **CLS Hook Installation Guide**\n`];
334
-
335
- if (hooksArr.includes('git')) {
336
- const hookScript = [
337
- `#!/bin/sh`,
338
- `# cachly CLS — Continuous Learning Stream git hook`,
339
- `# Installed by cls_install_hooks · runs silently on every commit`,
340
- `INSTANCE="${instance_id}"`,
341
- `SHA=$(git rev-parse HEAD 2>/dev/null || echo "")`,
342
- `MSG=$(git log -1 --pretty=%B 2>/dev/null | head -1)`,
343
- `FILES=$(git diff-tree --no-commit-id -r --name-only HEAD 2>/dev/null | tr '\\n' ',' | sed 's/,$//')`,
344
- `node -e "`,
345
- `const p={instance_id:'$INSTANCE',source:'git_commit',payload:{message:$(echo "$MSG" | jq -R . 2>/dev/null || echo '"commit"'),sha:'$SHA',files:'$FILES'.split(',').filter(Boolean)}};`,
346
- `try{require('child_process').execSync('npx @cachly-dev/mcp-server@latest cls-ingest \\''+JSON.stringify(p)+'\\'',{stdio:'ignore',timeout:5000})}catch(e){}`,
347
- `" 2>/dev/null &`,
348
- `exit 0`,
349
- ].join('\n');
350
-
351
- lines.push(`### Git post-commit hook`);
352
- lines.push(`**Quick install (run once per repo):**`);
353
- lines.push('```sh');
354
- lines.push(`cat > ${repo_path}/.git/hooks/post-commit << 'HOOK'`);
355
- lines.push(hookScript);
356
- lines.push(`HOOK`);
357
- lines.push(`chmod +x ${repo_path}/.git/hooks/post-commit`);
358
- lines.push('```');
359
- lines.push(`After install: every \`git commit\` automatically updates your brain's CKG.`);
360
- lines.push('');
361
- }
362
-
363
- if (hooksArr.includes('ci')) {
364
- lines.push(`### GitHub Actions CI outcome hook`);
365
- lines.push(`**Add at the end of each job** (after build/test steps):`);
366
- lines.push('```yaml');
367
- lines.push(`- name: cachly CLS — record CI outcome`);
368
- lines.push(` if: always()`);
369
- lines.push(` run: |`);
370
- lines.push(` node -e "`);
371
- lines.push(` const r=require('https');`);
372
- lines.push(` const d=JSON.stringify({instance_id:'${instance_id}',source:'ci_outcome',payload:{`);
373
- lines.push(` status:'\${{ job.status }}',prev_status:'unknown',job:'\${{ github.job }}',`);
374
- lines.push(` context:'github-actions run \${{ github.run_number }}'}});`);
375
- lines.push(` r.request({hostname:'api.cachly.dev',path:'/api/v1/cls/ingest',method:'POST',`);
376
- lines.push(` headers:{'Content-Type':'application/json','Authorization':'Bearer $CACHLY_JWT',`);
377
- lines.push(` 'Content-Length':d.length}},()=>{}).end(d);`);
378
- lines.push(` " 2>/dev/null || true`);
379
- lines.push(` env:`);
380
- lines.push(` CACHLY_JWT: \${{ secrets.CACHLY_JWT }}`);
381
- lines.push('```');
382
- lines.push('');
383
- }
384
-
385
- lines.push(`💡 Once installed: \`brain_search(query="cls")\` to verify events are arriving.`);
386
- lines.push(`📊 Monitor CKG growth: \`ckg_inspect(concept="ci")\` or \`ckg_inspect(concept="fix")\``);
387
- return lines.join('\n');
388
- }
389
-
390
- // ── Layer 6: FedBrain — fedbrain_contribute ───────────────────────────────
391
- case 'fedbrain_contribute': {
392
- const { instance_id, lesson_key, visibility = 'public' } = args as {
393
- instance_id: string; lesson_key: string; visibility?: string;
394
- };
395
- const redis = await getConnection(instance_id);
396
-
397
- const raw = await redis.get(`cachly:lesson:best:${lesson_key}`);
398
- if (!raw) return `❌ Lesson \`${lesson_key}\` not found. Store it first with \`learn_from_attempts\`.`;
399
-
400
- const lesson = safeJsonParse(raw, null as null | { topic: string; outcome: string; what_worked: string; what_failed?: string; tags?: string[]; commands?: string[]; severity?: string; ts?: string });
401
- if (!lesson) return `❌ Lesson \`${lesson_key}\` data is corrupted. Re-store with \`learn_from_attempts\`.`;
402
-
403
- const domainTokens = [lesson.topic.split(':')[0], ...(lesson.tags ?? [])].filter(Boolean);
404
- const domainFingerprint = [...new Set(domainTokens)].sort().join(',');
405
-
406
- // HMAC certificate ID (non-reversible, privacy-safe)
407
- const certContent = `${lesson.topic}:${lesson.outcome}:${lesson.what_worked}`;
408
- const certId = createHmac('sha256', `cachly-fedbrain:${instance_id}`).update(certContent).digest('hex').slice(0, 16);
409
-
410
- const cert = {
411
- cert_id: certId, lesson_key, visibility,
412
- domain_fingerprint: domainFingerprint,
413
- contributed_at: new Date().toISOString(),
414
- confirm_count: 0,
415
- trust_score: lesson.outcome === 'success' ? 0.85 : 0.5,
416
- };
417
- await redis.set(`cachly:fedbrain:cert:${certId}`, JSON.stringify(cert));
418
- await redis.sadd('cachly:fedbrain:contributed', certId);
419
-
420
- // Try global commons via syndication API
421
- let syndicationResult: string;
422
- try {
423
- await apiFetch('/api/v1/syndication/contribute', {
424
- method: 'POST',
425
- body: JSON.stringify({
426
- topic: lesson.topic, outcome: lesson.outcome,
427
- what_worked: lesson.what_worked, what_failed: lesson.what_failed ?? '',
428
- severity: lesson.severity ?? 'major', cert_id: certId,
429
- domain_fingerprint: domainFingerprint, visibility,
430
- }),
431
- });
432
- syndicationResult = `✅ Contributed to global commons`;
433
- } catch {
434
- syndicationResult = `📦 Stored locally (commons API unavailable — will sync when online)`;
435
- }
436
-
437
- return [
438
- `🌐 **FedBrain Contribute: "${lesson_key}"**`, '',
439
- `📜 Certificate: \`${certId}\``,
440
- `🏷️ Domain fingerprint: ${domainTokens.slice(0, 6).map(t => `\`${t}\``).join(', ')}`,
441
- `🔒 Visibility: ${visibility}`,
442
- '', syndicationResult, '',
443
- `💡 At 10 independent confirms → 🏆 Gold Standard`,
444
- `🔍 Search: \`fedbrain_search(query="${lesson.topic.split(':').slice(-1)[0]}")\``,
445
- ].join('\n');
446
- }
447
-
448
- // ── Layer 6: FedBrain — fedbrain_search ──────────────────────────────────
449
- case 'fedbrain_search': {
450
- const { instance_id, query, context_hints = [], limit = 10 } = args as {
451
- instance_id: string; query: string; context_hints?: string[]; limit?: number;
452
- };
453
- const redis = await getConnection(instance_id);
454
-
455
- // Build local domain context from contributed certificates + explicit hints
456
- const contribIds = await redis.smembers('cachly:fedbrain:contributed');
457
- const localDomains = new Map<string, number>();
458
- for (const certId of contribIds.slice(0, 30)) {
459
- const certRaw = await redis.get(`cachly:fedbrain:cert:${certId}`);
460
- if (!certRaw) continue;
461
- try {
462
- const cert = JSON.parse(certRaw) as { domain_fingerprint?: string };
463
- for (const d of (cert.domain_fingerprint ?? '').split(',').filter(Boolean)) {
464
- localDomains.set(d, (localDomains.get(d) ?? 0) + 1);
465
- }
466
- } catch { /* skip */ }
467
- }
468
- for (const hint of (Array.isArray(context_hints) ? context_hints : [])) {
469
- localDomains.set(hint.toLowerCase(), (localDomains.get(hint.toLowerCase()) ?? 0) + 2);
470
- }
471
-
472
- // Search global commons
473
- type SynResult = { id: string; topic: string; category: string; outcome: string; what_worked: string; what_failed?: string; severity: string; confirm_count: number; created_at: string; domain_fingerprint?: string };
474
- let results: SynResult[] = [];
475
- try {
476
- const params = new URLSearchParams({ q: query, limit: String(Math.min((limit as number) * 2, 50)) });
477
- const res = await apiFetch<{ results: SynResult[]; count: number }>(`/api/v1/syndication/search?${params}`);
478
- results = res.results ?? [];
479
- } catch {
480
- // Fallback: search local lessons
481
- const lessonKeys: string[] = [];
482
- const lStream = redis.scanStream({ match: 'cachly:lesson:best:*', count: 200 });
483
- await new Promise<void>((res, rej) => { lStream.on('data', (b: string[]) => lessonKeys.push(...b)); lStream.on('end', res); lStream.on('error', rej); });
484
- for (const k of lessonKeys.slice(0, 60)) {
485
- const r = await redis.get(k);
486
- if (!r) continue;
487
- try {
488
- const l = JSON.parse(r) as { topic: string; outcome: string; what_worked?: string; what_failed?: string; severity?: string; ts?: string };
489
- const haystack = `${l.topic} ${l.what_worked ?? ''} ${l.what_failed ?? ''}`.toLowerCase();
490
- if (query.toLowerCase().split(/\s+/).some(t => t.length > 2 && haystack.includes(t))) {
491
- results.push({ id: k.split(':').pop() ?? k, topic: l.topic, category: l.topic.split(':')[0], outcome: l.outcome, what_worked: l.what_worked ?? '', severity: l.severity ?? 'major', confirm_count: 0, created_at: l.ts ?? new Date().toISOString() });
492
- }
493
- } catch { /* skip */ }
494
- }
495
- }
496
-
497
- if (results.length === 0) {
498
- return [`🌐 **FedBrain Search: "${query}"**`, '', `No results. Contribute: \`fedbrain_contribute(lesson_key="fix:...")\``].join('\n');
499
- }
500
-
501
- // Context-weighted ranking
502
- const ranked = results.map(r => {
503
- const rDomains = (r.domain_fingerprint ?? r.category ?? '').split(',').filter(Boolean);
504
- const overlap = rDomains.reduce((s, d) => s + (localDomains.get(d) ?? 0), 0);
505
- const contextScore = localDomains.size > 0 ? overlap / Math.max(1, localDomains.size + rDomains.length) : 0;
506
- const confirmedScore = Math.min(1, r.confirm_count / 10);
507
- const weightedScore = (contextScore * 0.4) + (confirmedScore * 0.4) + (r.outcome === 'success' ? 0.2 : 0);
508
- return { ...r, weightedScore, isGoldStandard: r.confirm_count >= 10 };
509
- }).sort((a, b) => b.weightedScore - a.weightedScore).slice(0, limit as number);
510
-
511
- const lines = [`🌐 **FedBrain Search: "${query}"** — ${ranked.length} result${ranked.length !== 1 ? 's' : ''} (context-weighted)\n`];
512
- for (const r of ranked) {
513
- const icon = r.outcome === 'success' ? '✅' : r.outcome === 'failure' ? '❌' : '⚠️';
514
- const goldBadge = r.isGoldStandard ? ' 🏆 _Gold Standard_' : r.confirm_count >= 3 ? ` ✓${r.confirm_count}` : '';
515
- const ctxPct = Math.round(r.weightedScore * 100);
516
- lines.push(`${icon}${goldBadge} **\`${r.topic}\`** [ctx: ${ctxPct}%]`);
517
- if (r.what_worked) lines.push(` ✅ ${r.what_worked.slice(0, 150)}`);
518
- if (r.what_failed) lines.push(` ❌ ${r.what_failed.slice(0, 80)}`);
519
- lines.push(` _${r.confirm_count} confirm${r.confirm_count !== 1 ? 's' : ''} | \`${r.id.slice(0, 12)}\`_`, '');
520
- }
521
- if (localDomains.size > 0) {
522
- const topDomains = [...localDomains.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([d, n]) => `\`${d}\`(${n})`).join(', ');
523
- lines.push(`🎯 Your context: ${topDomains}`);
524
- }
525
- lines.push(`💡 Confirm: \`fedbrain_confirm(topic="<topic>", outcome="worked")\``);
526
- return lines.join('\n');
527
- }
528
-
529
- // ── Layer 6: FedBrain — fedbrain_confirm ─────────────────────────────────
530
- case 'fedbrain_confirm': {
531
- const { instance_id, topic, outcome } = args as {
532
- instance_id: string; topic: string; outcome: 'worked' | 'partially_worked' | 'did_not_work';
533
- };
534
- const redis = await getConnection(instance_id);
535
- const ts = new Date().toISOString();
536
-
537
- const confirmEntry = JSON.stringify({ topic, outcome, ts });
538
- await redis.rpush('cachly:fedbrain:confirmations', confirmEntry);
539
- await redis.ltrim('cachly:fedbrain:confirmations', -200, -1);
540
-
541
- // Update local CKG confidence
542
- const worked = outcome === 'worked';
543
- const partial = outcome === 'partially_worked';
544
- await ckgUpdateEdge(redis, ckgSlug(topic), 'fixes', ckgSlug(`syndicated:${topic}`), worked, partial);
545
-
546
- // Propagate to global commons
547
- let propResult: string;
548
- try {
549
- await apiFetch('/api/v1/syndication/confirm', { method: 'POST', body: JSON.stringify({ topic, outcome }) });
550
- propResult = `✅ Confirmation propagated to global commons`;
551
- } catch {
552
- await redis.rpush('cachly:fedbrain:pending_confirms', confirmEntry);
553
- await redis.ltrim('cachly:fedbrain:pending_confirms', -50, -1);
554
- propResult = `📦 Queued locally (API unavailable — will propagate on next online session)`;
555
- }
556
-
557
- const icon = worked ? '✅' : partial ? '⚠️' : '❌';
558
- return [
559
- `${icon} **FedBrain Confirm: "${topic}"** → ${outcome}`, '',
560
- propResult, '',
561
- `🕸️ CKG confidence ${worked || partial ? 'boosted' : 'reduced'} for \`${ckgSlug(topic)}\``,
562
- `💡 Your confirmation helps other brains worldwide.`,
563
- `📊 Status: \`fedbrain_status(instance_id="...")\``,
564
- ].join('\n');
565
- }
566
-
567
- // ── Layer 6: FedBrain — fedbrain_status ──────────────────────────────────
568
- case 'fedbrain_status': {
569
- const { instance_id } = args as { instance_id: string };
570
- const redis = await getConnection(instance_id);
571
-
572
- const contribIds = await redis.smembers('cachly:fedbrain:contributed');
573
- const confirmsRaw = await redis.lrange('cachly:fedbrain:confirmations', -10, -1);
574
- const pendingConfirms = await redis.llen('cachly:fedbrain:pending_confirms');
575
- const confirms = confirmsRaw.map(r => { try { return JSON.parse(r) as { topic: string; outcome: string; ts: string }; } catch { return null; } }).filter(Boolean) as Array<{ topic: string; outcome: string; ts: string }>;
576
-
577
- const certDetails: Array<{ cert_id: string; lesson_key: string; confirm_count: number; trust_score: number; isGold: boolean }> = [];
578
- for (const certId of contribIds.slice(0, 15)) {
579
- const raw = await redis.get(`cachly:fedbrain:cert:${certId}`);
580
- if (!raw) continue;
581
- try {
582
- const cert = JSON.parse(raw) as { cert_id: string; lesson_key: string; confirm_count?: number; trust_score?: number };
583
- certDetails.push({ cert_id: cert.cert_id, lesson_key: cert.lesson_key, confirm_count: cert.confirm_count ?? 0, trust_score: cert.trust_score ?? 0.5, isGold: (cert.confirm_count ?? 0) >= 10 });
584
- } catch { /* skip */ }
585
- }
586
-
587
- const lines = [
588
- `🌐 **FedBrain Status**\n`,
589
- `### 📤 Contributed Lessons: ${contribIds.length}`,
590
- ];
591
-
592
- if (certDetails.length > 0) {
593
- for (const c of certDetails) {
594
- const goldBadge = c.isGold ? ' 🏆 Gold Standard' : '';
595
- const confBar = '█'.repeat(Math.min(10, c.confirm_count)) + '░'.repeat(Math.max(0, 10 - c.confirm_count));
596
- lines.push(` \`${c.lesson_key}\` [${confBar}] ×${c.confirm_count}${goldBadge}`);
597
- }
598
- } else {
599
- lines.push(` _None yet. Contribute with \`fedbrain_contribute(lesson_key="fix:...")\`_`);
600
- }
601
-
602
- lines.push('', `### 📥 Recent Confirmations: ${confirms.length}`);
603
- if (confirms.length > 0) {
604
- for (const c of confirms.slice(-5)) {
605
- const icon = c.outcome === 'worked' ? '✅' : c.outcome === 'partially_worked' ? '⚠️' : '❌';
606
- lines.push(` ${icon} \`${c.topic}\` — ${c.outcome} (${new Date(c.ts).toLocaleDateString('de-DE')})`);
607
- }
608
- } else {
609
- lines.push(` _None yet. Confirm syndicated lessons with \`fedbrain_confirm\`_`);
610
- }
611
-
612
- if (pendingConfirms > 0) {
613
- lines.push('', `⚠️ ${pendingConfirms} confirmation${pendingConfirms !== 1 ? 's' : ''} pending propagation`);
614
- }
615
-
616
- lines.push('', '---',
617
- `**Contribute:** \`fedbrain_contribute(lesson_key="fix:...")\``,
618
- `**Search:** \`fedbrain_search(query="...")\``,
619
- `**Confirm:** \`fedbrain_confirm(topic="...", outcome="worked")\``,
620
- );
621
- return lines.join('\n');
622
- }
623
-
624
- // ── Layer 6: FedBrain — brain_federate ───────────────────────────────────
625
- case 'brain_federate': {
626
- const { instance_id, source, domain, min_confidence = 0.6, dry_run = false } = args as {
627
- instance_id: string; source: string; domain: string;
628
- min_confidence?: number; dry_run?: boolean;
629
- };
630
- if (instance_id === source) return `❌ Source and destination cannot be the same brain.`;
631
-
632
- const destRedis = await getConnection(instance_id);
633
- const srcRedis = await getConnection(source);
634
-
635
- const domainPattern = domain === '*' ? 'cachly:ckg:node:*' : `cachly:ckg:node:${domain}*`;
636
-
637
- // Scan source CKG nodes matching the domain
638
- const nodeKeys: string[] = [];
639
- const nStream = srcRedis.scanStream({ match: domainPattern, count: 100 });
640
- await new Promise<void>((res, rej) => { nStream.on('data', (b: string[]) => nodeKeys.push(...b)); nStream.on('end', res); nStream.on('error', rej); });
641
-
642
- if (nodeKeys.length === 0) {
643
- return [
644
- `🧠 **brain_federate: "${domain}"**`, '',
645
- `No CKG nodes found in source brain for domain \`${domain}\`.`,
646
- `The source brain may not have knowledge in this area yet.`,
647
- `Try: \`fedbrain_search(query="${domain}")\` to find global commons knowledge instead.`,
648
- ].join('\n');
649
- }
650
-
651
- // Transfer nodes + their outgoing edges
652
- let nodesTransferred = 0;
653
- let edgesTransferred = 0;
654
- let lessonsTransferred = 0;
655
- let skippedLowConf = 0;
656
- const transferLog: string[] = [];
657
-
658
- for (const nk of nodeKeys) {
659
- const nodeRaw = await srcRedis.get(nk);
660
- if (!nodeRaw) continue;
661
- let node: CKGNode;
662
- try { node = JSON.parse(nodeRaw); } catch { continue; }
663
-
664
- if (!dry_run) await destRedis.set(nk, nodeRaw);
665
- nodesTransferred++;
666
-
667
- // Transfer outgoing edges for this node
668
- const edgeKeys = await srcRedis.smembers(`cachly:ckg:idx:from:${node.id}`);
669
- for (const ek of edgeKeys) {
670
- const edgeRaw = await srcRedis.get(ek);
671
- if (!edgeRaw) continue;
672
- let edge: CKGEdge;
673
- try { edge = JSON.parse(edgeRaw); } catch { continue; }
674
- if (edge.confidence < (min_confidence as number)) { skippedLowConf++; continue; }
675
- if (!dry_run) {
676
- await destRedis.set(ek, edgeRaw);
677
- await destRedis.sadd(`cachly:ckg:idx:from:${edge.from}`, ek);
678
- await destRedis.sadd(`cachly:ckg:idx:to:${edge.to}`, ek);
679
- }
680
- edgesTransferred++;
681
- }
682
-
683
- // Transfer best-lesson for the same topic slug
684
- const lessonKey = `cachly:lesson:best:${node.id}`;
685
- const lessonRaw = await srcRedis.get(lessonKey);
686
- if (lessonRaw) {
687
- if (!dry_run) await destRedis.set(lessonKey, lessonRaw);
688
- lessonsTransferred++;
689
- if (transferLog.length < 8) {
690
- try {
691
- const l = JSON.parse(lessonRaw) as { topic: string; outcome: string; what_worked?: string };
692
- const icon = l.outcome === 'success' ? '✅' : l.outcome === 'failure' ? '❌' : '⚠️';
693
- transferLog.push(` ${icon} \`${l.topic}\` — ${(l.what_worked ?? '').slice(0, 80)}`);
694
- } catch { /* skip */ }
695
- }
696
- }
697
- }
698
-
699
- // Record federation provenance
700
- if (!dry_run) {
701
- const provEntry = JSON.stringify({
702
- source, domain, transferred_at: new Date().toISOString(),
703
- nodes: nodesTransferred, edges: edgesTransferred, lessons: lessonsTransferred,
704
- });
705
- await destRedis.rpush('cachly:fedbrain:federations', provEntry);
706
- await destRedis.ltrim('cachly:fedbrain:federations', -50, -1);
707
- }
708
-
709
- const dryTag = dry_run ? ' (DRY RUN — no writes)' : '';
710
- const lines = [
711
- `🧠 **brain_federate: "${domain}"**${dryTag}`, '',
712
- `📤 Source: \`${source}\``,
713
- `📥 Destination: \`${instance_id}\``,
714
- `🔍 Domain filter: \`${domain}\` | min_confidence: ${min_confidence}`,
715
- '',
716
- `### Transfer Summary`,
717
- ` 🕸️ CKG nodes: ${nodesTransferred}`,
718
- ` 🔗 CKG edges: ${edgesTransferred} (${skippedLowConf} skipped, confidence < ${min_confidence})`,
719
- ` 📚 Lessons: ${lessonsTransferred}`,
720
- '',
721
- ];
722
- if (transferLog.length > 0) {
723
- lines.push(`### Sample lessons transferred:`, ...transferLog);
724
- if (lessonsTransferred > transferLog.length) lines.push(` _... and ${lessonsTransferred - transferLog.length} more_`);
725
- lines.push('');
726
- }
727
- if (dry_run) {
728
- lines.push(`💡 Run without \`dry_run: true\` to apply the transfer.`);
729
- } else {
730
- lines.push(`✅ Transfer complete. Your brain now has the \`${domain}\` knowledge from \`${source}\`.`);
731
- lines.push(`🔍 Explore: \`ckg_inspect(concept="${domain}")\` | \`recall_best_solution(topic="${domain}:...")\``);
732
- }
733
- return lines.join('\n');
734
- }
735
-
736
- // ── crystal_view ──────────────────────────────────────────────────────────
737
- case 'crystal_view': {
738
- const { instance_id, show_raw = false } = args as { instance_id: string; show_raw?: boolean };
739
- const redis = await getConnection(instance_id);
740
-
741
- const raw = await redis.get('cachly:crystal:latest');
742
- if (!raw) {
743
- return [
744
- `💎 **Memory Crystal: not yet created**`, '',
745
- `No crystal found. Create one with \`memory_crystalize()\` to compress your accumulated wisdom.`,
746
- '', `💡 Tip: run \`memory_crystalize\` monthly for best results.`,
747
- ].join('\n');
748
- }
749
-
750
- type Crystal = { label: string; ts: string; session_count: number; lesson_count: number; top_patterns: Array<{ category: string; insight: string; count: number }>; categories: string[]; created_from: string };
751
- const crystal = safeJsonParse<Crystal | null>(raw, null);
752
- if (!crystal) return `❌ Memory Crystal data is corrupted. Re-create with \`memory_crystalize()\`.`;
753
- const age = Math.floor((Date.now() - new Date(crystal.ts).getTime()) / 86400000);
754
- const freshEmoji = age <= 7 ? '🟢' : age <= 30 ? '🟡' : '🔴';
755
-
756
- const lines = [
757
- `💎 **Memory Crystal: ${crystal.label}**`, '',
758
- `📅 Created: ${new Date(crystal.ts).toLocaleDateString('de-DE')} (${age}d ago ${freshEmoji})`,
759
- `📊 Compressed from: ${crystal.created_from}`,
760
- `🗂️ Categories: ${crystal.categories.slice(0, 10).map(c => `\`${c}\``).join(', ')}${crystal.categories.length > 10 ? ` +${crystal.categories.length - 10} more` : ''}`,
761
- '',
762
- `**🔑 Top patterns (${crystal.top_patterns.length}):**`,
763
- ];
764
- for (const p of crystal.top_patterns) {
765
- lines.push(` • **${p.category}** (${p.count}×): ${p.insight.slice(0, 110)}`);
766
- }
767
- if (age > 30) {
768
- lines.push('', `⚠️ Crystal is ${age}d old — run \`memory_crystalize()\` to refresh it.`);
769
- }
770
- if (show_raw) {
771
- lines.push('', '```json', JSON.stringify(crystal, null, 2), '```');
772
- }
773
- lines.push('', `💡 Refresh: \`memory_crystalize()\` | Recover: \`compact_recover(instance_id="...")\``);
774
- return lines.join('\n');
775
- }
776
-
777
- // ── compact_recover ───────────────────────────────────────────────────────
778
- case 'compact_recover': {
779
- const { instance_id, focus = '' } = args as { instance_id: string; focus?: string };
780
- const redis = await getConnection(instance_id);
781
-
782
- const lines = [`🔁 **Compact Recovery Briefing**\n`];
783
- lines.push(`> *Call this first after any context limit hit. Reconstructs where you left off.*\n`);
784
-
785
- // 1. Memory Crystal
786
- const crystalRaw = await redis.get('cachly:crystal:latest');
787
- if (crystalRaw) {
788
- type Crystal = { label: string; ts: string; session_count: number; lesson_count: number; top_patterns: Array<{ category: string; insight: string; count: number }> };
789
- const crystal = safeJsonParse<Crystal | null>(crystalRaw, null);
790
- if (crystal) {
791
- lines.push(`### 💎 Memory Crystal: ${crystal.label}`);
792
- lines.push(`Compressed from ${crystal.session_count} sessions, ${crystal.lesson_count} lessons.`);
793
- const topN = focus
794
- ? crystal.top_patterns.filter(p => p.category.toLowerCase().includes(focus.toLowerCase()) || p.insight.toLowerCase().includes(focus.toLowerCase())).slice(0, 4)
795
- : crystal.top_patterns.slice(0, 4);
796
- for (const p of topN) lines.push(` • **${p.category}**: ${p.insight.slice(0, 100)}`);
797
- lines.push('');
798
- }
799
- }
800
-
801
- // 2. Last session summary
802
- const lastSession = await redis.get('cachly:session:last');
803
- if (lastSession) {
804
- type Session = { summary?: string; ts?: string; focus?: string };
805
- const sess = safeJsonParse<Session | null>(lastSession, null);
806
- if (sess) {
807
- lines.push(`### 🕐 Last Session`);
808
- if (sess.focus) lines.push(`Focus: _${sess.focus}_`);
809
- if (sess.summary) lines.push(`Summary: ${sess.summary.slice(0, 300)}`);
810
- lines.push('');
811
- }
812
- }
813
-
814
- // 3. Session handoff
815
- const handoff = await redis.get('cachly:session:handoff');
816
- if (handoff) {
817
- type Handoff = { remaining_tasks?: string[]; instructions?: string; context_summary?: string; blocked_on?: string };
818
- const h = safeJsonParse<Handoff | null>(handoff, null);
819
- if (h) {
820
- lines.push(`### 📋 Handoff (from last window)`);
821
- if (h.context_summary) lines.push(`Context: ${h.context_summary.slice(0, 200)}`);
822
- if (h.remaining_tasks?.length) {
823
- lines.push(`Remaining tasks:`);
824
- for (const t of h.remaining_tasks.slice(0, 5)) lines.push(` • ${t}`);
825
- }
826
- if (h.instructions) lines.push(`⚠️ Instructions: ${h.instructions.slice(0, 200)}`);
827
- if (h.blocked_on) lines.push(`🚧 Blocked on: ${h.blocked_on}`);
828
- lines.push('');
829
- }
830
- }
831
-
832
- // 4. WIP registry
833
- const wipRaw = await redis.get('cachly:ctx:wip-registry');
834
- if (wipRaw) {
835
- type Ctx = { content?: string };
836
- const wip = safeJsonParse<Ctx | null>(wipRaw, null);
837
- if (wip?.content) {
838
- lines.push(`### 🔧 WIP Registry`);
839
- lines.push(wip.content.slice(0, 400));
840
- lines.push('');
841
- }
842
- }
843
-
844
- // 5. Open failures (roadmap with status=blocked/in_progress)
845
- const roadmapKeys: string[] = [];
846
- const rStream = redis.scanStream({ match: 'cachly:roadmap:*', count: 100 });
847
- await new Promise<void>((res, rej) => { rStream.on('data', (b: string[]) => roadmapKeys.push(...b)); rStream.on('end', res); rStream.on('error', rej); });
848
- const openItems: Array<{ title: string; status: string; priority?: string }> = [];
849
- for (const k of roadmapKeys.slice(0, 30)) {
850
- const r = await redis.get(k);
851
- if (!r) continue;
852
- try {
853
- const item = JSON.parse(r) as { title?: string; status?: string; priority?: string };
854
- if (item.status === 'in_progress' || item.status === 'blocked') openItems.push({ title: item.title ?? k, status: item.status, priority: item.priority });
855
- } catch { /* skip */ }
856
- }
857
- if (openItems.length > 0) {
858
- lines.push(`### 🚧 Open Items`);
859
- for (const i of openItems.slice(0, 5)) lines.push(` • [${i.status}] ${i.title}`);
860
- lines.push('');
861
- }
862
-
863
- // 6. Focus-relevant lessons
864
- if (focus) {
865
- const lessonKeys: string[] = [];
866
- const lStream = redis.scanStream({ match: 'cachly:lesson:best:*', count: 200 });
867
- await new Promise<void>((res, rej) => { lStream.on('data', (b: string[]) => lessonKeys.push(...b)); lStream.on('end', res); lStream.on('error', rej); });
868
- const relevant: Array<{ topic: string; what_worked: string }> = [];
869
- for (const k of lessonKeys) {
870
- const r = await redis.get(k);
871
- if (!r) continue;
872
- try {
873
- const l = JSON.parse(r) as { topic: string; what_worked?: string };
874
- if (l.topic.toLowerCase().includes(focus.toLowerCase()) && l.what_worked) {
875
- relevant.push({ topic: l.topic, what_worked: l.what_worked });
876
- }
877
- } catch { /* skip */ }
878
- }
879
- if (relevant.length > 0) {
880
- lines.push(`### 💡 Relevant Lessons for "${focus}"`);
881
- for (const l of relevant.slice(0, 4)) lines.push(` • **${l.topic}**: ${l.what_worked.slice(0, 100)}`);
882
- lines.push('');
883
- }
884
- }
885
-
886
- if (lines.length <= 3) {
887
- lines.push(`_No brain data found. Start accumulating knowledge with \`learn_from_attempts\` and \`session_start\`._`);
888
- }
889
- lines.push(`---`, `🧠 Brain is ready. Continue your work — full context restored.`);
890
- return lines.join('\n');
891
- }
892
-
893
- // ── brain_from_git ────────────────────────────────────────────────────────
894
- case 'brain_from_git': {
895
- const { instance_id, repo_path = '.', limit = 100, branch = 'HEAD', since = '', incremental = true } = args as {
896
- instance_id: string; repo_path?: string; limit?: number; branch?: string; since?: string; incremental?: boolean;
897
- };
898
- const redis = await getConnection(instance_id);
899
- const { execSync } = await import('node:child_process');
900
- const { resolve } = await import('node:path');
901
-
902
- const repoDir = resolve(repo_path);
903
- const maxCommits = Math.min(Number(limit) || 100, 500);
904
-
905
- // Semaphore: max 10 concurrent git subprocesses per MCP process
906
- await _gitSemAcquire();
907
- try {
908
- // Verify it's a git repo
909
- try {
910
- execSync('git rev-parse --git-dir', { cwd: repoDir, stdio: 'pipe' });
911
- } catch {
912
- return `❌ Not a git repository: \`${repoDir}\`. Pass \`repo_path\` pointing to a git checkout.`;
913
- }
914
-
915
- // Incremental mode: resume from last processed commit SHA
916
- const lastShaKey = `cachly:brain_from_git:last_sha:${Buffer.from(repoDir).toString('base64').slice(0, 32)}`;
917
- let lastSha = '';
918
- let isIncremental = false;
919
- if (incremental !== false && !since) {
920
- lastSha = (await redis.get(lastShaKey)) ?? '';
921
- isIncremental = Boolean(lastSha);
922
- }
923
-
924
- // Build git log command — use --name-only to get changed files per commit
925
- const sinceFlag = since ? `--since="${since}"` : '';
926
- const afterFlag = isIncremental ? `${lastSha}..HEAD` : '';
927
- const revRange = afterFlag || branch;
928
- const logCmd = `git log ${revRange} ${sinceFlag} --pretty=format:"COMMIT|||%H|||%s|||%ad|||%an" --date=short --no-merges --name-only -n ${maxCommits}`;
929
-
930
- let logOutput = '';
931
- try {
932
- logOutput = execSync(logCmd, { cwd: repoDir, encoding: 'utf-8', stdio: 'pipe' });
933
- } catch (e) {
934
- return `❌ git log failed: ${(e as Error).message}. Check \`repo_path\` and \`branch\`.`;
935
- }
936
-
937
- // Parse multi-line output: each commit block = header line + blank line + file lines
938
- type CommitEntry = { sha: string; subject: string; date: string; author: string; files: string[] };
939
- const commits: CommitEntry[] = [];
940
- let current: CommitEntry | null = null;
941
- for (const raw of logOutput.split('\n')) {
942
- const line = raw.trim();
943
- if (line.startsWith('COMMIT|||')) {
944
- if (current) commits.push(current);
945
- const [, sha, subject, date, author] = line.split('|||');
946
- current = { sha: (sha ?? '').trim(), subject: (subject ?? '').trim(), date: (date ?? '').trim(), author: (author ?? '').trim(), files: [] };
947
- } else if (line && current) {
948
- const norm = normalizeGitPath(line);
949
- if (norm) current.files.push(norm);
950
- }
951
- }
952
- if (current) commits.push(current);
953
-
954
- if (commits.length === 0) {
955
- if (isIncremental) {
956
- return `✅ brain_from_git: Brain is up to date — no new commits since last run (last SHA: \`${lastSha.slice(0, 8)}\`).`;
957
- }
958
- return `⚠️ No commits found in \`${repoDir}\` on branch \`${branch}\`${since ? ` since ${since}` : ''}.`;
959
- }
960
-
961
- // Pattern classifiers
962
- const classifyCommit = (subject: string): { category: string; outcome: 'success' | 'failure' | 'partial'; severity: 'critical' | 'major' | 'minor' } => {
963
- const s = subject.toLowerCase();
964
- if (/\b(fix|fixed|fixes|bug|hotfix|patch|revert|resolve|closes? #\d+)\b/.test(s)) {
965
- const sev: 'critical' | 'major' | 'minor' = /\b(critical|crash|security|auth|data loss|outage|prod|production)\b/.test(s) ? 'critical' : /\b(major|breaking|regression|hotfix)\b/.test(s) ? 'major' : 'minor';
966
- return { category: 'fix', outcome: 'success', severity: sev };
967
- }
968
- if (/\b(feat|feature|add|added|implement|new|introduce)\b/.test(s)) return { category: 'feat', outcome: 'success', severity: 'minor' };
969
- if (/\b(refactor|clean|cleanup|improve|simplify|extract|rename)\b/.test(s)) return { category: 'refactor', outcome: 'success', severity: 'minor' };
970
- if (/\b(perf|optimize|speed|cache|latency|memory|performance)\b/.test(s)) return { category: 'perf', outcome: 'success', severity: 'major' };
971
- if (/\b(security|cve|auth|csrf|xss|sql|injection|sanitize|escape|encrypt)\b/.test(s)) return { category: 'security', outcome: 'success', severity: 'critical' };
972
- if (/\b(deploy|ci|cd|build|docker|k8s|helm|infra|devops)\b/.test(s)) return { category: 'deploy', outcome: 'success', severity: 'major' };
973
- if (/\b(test|spec|coverage|assert|mock|unit|integration)\b/.test(s)) return { category: 'test', outcome: 'success', severity: 'minor' };
974
- return { category: 'chore', outcome: 'success', severity: 'minor' };
975
- };
976
-
977
- // Extract domain keywords from commit subject
978
- const extractDomain = (subject: string): string => {
979
- const s = subject.toLowerCase();
980
- const tokens = s.replace(/[^a-z0-9\s\-_]/g, ' ').split(/\s+/).filter(t => t.length > 3 && !['that', 'this', 'with', 'from', 'when', 'into', 'also', 'some', 'were'].includes(t));
981
- return tokens.slice(0, 3).join('-') || 'general';
982
- };
983
-
984
- const ts = new Date().toISOString();
985
- let ingested = 0;
986
- let skipped = 0;
987
- const categoryCount = new Map<string, number>();
988
- const total = commits.length;
989
- const progressInterval = Math.max(1, Math.floor(total / 10)); // emit ~10 progress updates
990
- let lastProgressAt = 0;
991
-
992
- process.stderr.write(`\n🧠 brain_from_git: processing ${total} commits from ${repoDir}...\n`);
993
-
994
- for (let i = 0; i < commits.length; i++) {
995
- const commit = commits[i]!;
996
-
997
- // Emit progress to stderr so editors can display it in MCP logs
998
- if (i - lastProgressAt >= progressInterval) {
999
- process.stderr.write(` ⏳ Processing ${i}/${total} commits (${ingested} lessons so far)...\n`);
1000
- lastProgressAt = i;
1001
- }
1002
-
1003
- if (!commit.subject) { skipped++; continue; }
1004
- const { category, outcome, severity } = classifyCommit(commit.subject);
1005
- const domain = extractDomain(commit.subject);
1006
- const topic = `${category}:${domain}`;
1007
- categoryCount.set(category, (categoryCount.get(category) ?? 0) + 1);
1008
-
1009
- const commitFiles = commit.files.filter(f => f.length > 0).slice(0, 12);
1010
- const lessonObj = {
1011
- topic, outcome, severity,
1012
- what_worked: commit.subject.slice(0, 200),
1013
- what_failed: '',
1014
- context: `git:${commit.sha.slice(0, 8)} by ${commit.author} on ${commit.date}`,
1015
- author: commit.author || undefined,
1016
- file_paths: commitFiles,
1017
- commands: [`git show ${commit.sha.slice(0, 8)}`],
1018
- tags: ['brain_from_git', category, 'git-history'],
1019
- visibility: 'team',
1020
- depends_on: [], recall_count: 0, ts, verified_at: ts,
1021
- confidence: 0.55, // lower confidence for auto-inferred lessons
1022
- audit_trail: [{ ts, action: 'brain_from_git', sha: commit.sha.slice(0, 8) }],
1023
- version: 3,
1024
- };
1025
-
1026
- // Only store if no existing lesson for this topic (avoid overwriting higher-confidence lessons)
1027
- const existing = await redis.get(`cachly:lesson:best:${topic}`);
1028
- if (!existing) {
1029
- // Auto-inferred git lessons get a 90-day TTL — they self-refresh on the next
1030
- // brain_from_git run, and shouldn't pin memory forever if the repo goes stale.
1031
- await redis.set(`cachly:lesson:best:${topic}`, JSON.stringify(lessonObj), 'EX', 90 * 86400);
1032
- await redis.rpush(`cachly:lessons:${topic}`, JSON.stringify(lessonObj));
1033
- await redis.ltrim(`cachly:lessons:${topic}`, -100, -1);
1034
- await redis.expire(`cachly:lessons:${topic}`, 90 * 86400);
1035
- }
1036
- await redis.rpush('cachly:lessons:brain_from_git:all', JSON.stringify({ topic, sha: commit.sha.slice(0, 8), subject: commit.subject.slice(0, 60) }));
1037
- await redis.ltrim('cachly:lessons:brain_from_git:all', -500, -1);
1038
- await redis.expire('cachly:lessons:brain_from_git:all', 90 * 86400);
1039
-
1040
- // Update CKG — concept node + person node + file nodes
1041
- const conceptId = ckgSlug(topic);
1042
- await ckgUpsertNode(redis, conceptId, category, 'git-derived');
1043
-
1044
- // Phase 3A: auto-build person + file nodes from git history
1045
- if (commit.author) {
1046
- try {
1047
- const personId = await ckgUpsertPersonNode(redis, commit.author, category);
1048
- await ckgUpdateEdge(redis, personId, 'authored', conceptId, outcome === 'success', outcome === 'partial');
1049
- for (const fp of commitFiles.slice(0, 8)) {
1050
- const fileId = await ckgUpsertFileNode(redis, fp);
1051
- await ckgUpdateEdge(redis, personId, 'touched', fileId, true);
1052
- }
1053
- } catch { /* non-critical */ }
1054
- }
1055
-
1056
- ingested++;
1057
- }
1058
-
1059
- process.stderr.write(` ✅ brain_from_git complete: ${ingested}/${total} commits ingested\n\n`);
1060
-
1061
- // Save the latest commit SHA for incremental runs
1062
- if (commits.length > 0 && commits[0]?.sha) {
1063
- await redis.set(lastShaKey, commits[0].sha, 'EX', 90 * 24 * 3600); // expire after 90 days
1064
- }
1065
-
1066
- const categoryBreakdown = [...categoryCount.entries()].sort((a, b) => b[1] - a[1]).map(([k, v]) => ` • **${k}** (${v})`).join('\n');
1067
-
1068
- _lastBrainFromGitCounts = {
1069
- fixes: categoryCount.get('fix') ?? 0,
1070
- features: categoryCount.get('feat') ?? 0,
1071
- refactors: categoryCount.get('refactor') ?? 0,
1072
- total: ingested,
1073
- };
1074
-
1075
- return [
1076
- `🔁 **brain_from_git: ${repoDir}**`,
1077
- isIncremental ? `🔄 Incremental mode — only new commits since \`${lastSha.slice(0, 8)}\` were processed` : '',
1078
- ``,
1079
- `📂 Branch: \`${revRange}\` | Processed: **${commits.length}** commits | Ingested: **${ingested}** lessons | Skipped: ${skipped}`,
1080
- ``,
1081
- `**Breakdown by category:**`,
1082
- categoryBreakdown,
1083
- ``,
1084
- `💡 New lessons are stored with confidence 0.55 (auto-inferred).`,
1085
- `💡 As you confirm them via \`learn_from_attempts\`, confidence rises automatically.`,
1086
- `🔍 Explore: \`brain_search(query="fix")\` | \`ckg_inspect(concept="deploy")\``,
1087
- `💾 Next run will automatically continue from the latest commit (incremental mode).`,
1088
- ].filter(Boolean).join('\n');
1089
- } finally {
1090
- _gitSemRelease();
1091
- }
1092
- }
1093
-
1094
- // ── brain_predict_failures ─────────────────────────────────────────────────
1095
- case 'brain_predict_failures': {
1096
- const { instance_id, context: ctx, top_k = 5, format = 'detailed' } = args as {
1097
- instance_id: string; context: string; top_k?: number; format?: 'brief' | 'detailed';
1098
- };
1099
- const redis = await getConnection(instance_id);
1100
-
1101
- const ctxTokens = ctx.toLowerCase().replace(/[^a-z0-9\s\-_:.]/g, ' ').split(/\s+/).filter(t => t.length > 2);
1102
-
1103
- type FailurePred = { concept: string; failure: string; probability: number; fix?: string; topic?: string; source: 'ckg' | 'lesson' };
1104
- const failures: FailurePred[] = [];
1105
-
1106
- // Step 1: CKG — find 'causes' and 'degrades_under' edges from context tokens
1107
- for (const token of ctxTokens.slice(0, 8)) {
1108
- const nodeKeys: string[] = [];
1109
- const nStream = redis.scanStream({ match: `cachly:ckg:node:*${token}*`, count: 50 });
1110
- await new Promise<void>((res, rej) => { nStream.on('data', (b: string[]) => nodeKeys.push(...b)); nStream.on('end', res); nStream.on('error', rej); });
1111
-
1112
- for (const nk of nodeKeys.slice(0, 5)) {
1113
- const nodeRaw = await redis.get(nk);
1114
- if (!nodeRaw) continue;
1115
- const node = safeJsonParse<CKGNode | null>(nodeRaw, null);
1116
- if (!node) continue;
1117
- const edgeKeys = await redis.smembers(`cachly:ckg:idx:from:${node.id}`);
1118
- for (const ek of edgeKeys.slice(0, 20)) {
1119
- const edgeRaw = await redis.get(ek);
1120
- if (!edgeRaw) continue;
1121
- const edge = safeJsonParse<CKGEdge | null>(edgeRaw, null);
1122
- if (!edge) continue;
1123
- if (edge.edgeType !== 'causes' && edge.edgeType !== 'degrades_under') continue;
1124
-
1125
- // Look up fix for this failure from CKG 'fixes' edges
1126
- const fixEdgeKeys = await redis.smembers(`cachly:ckg:idx:from:${edge.to}`);
1127
- let fix: string | undefined;
1128
- for (const fek of fixEdgeKeys.slice(0, 10)) {
1129
- const feRaw = await redis.get(fek);
1130
- if (!feRaw) continue;
1131
- const fe = safeJsonParse<CKGEdge | null>(feRaw, null);
1132
- if (!fe) continue;
1133
- if (fe.edgeType === 'fixes') {
1134
- const lessonRaw = await redis.get(`cachly:lesson:best:${fe.from}`);
1135
- if (lessonRaw) {
1136
- const lesson = safeJsonParse(lessonRaw, null as null | { what_worked?: string });
1137
- fix = lesson?.what_worked?.slice(0, 120);
1138
- break;
1139
- }
1140
- }
1141
- }
1142
-
1143
- failures.push({
1144
- concept: node.id,
1145
- failure: edge.to.replace(/-/g, ' '),
1146
- probability: edge.confidence,
1147
- fix,
1148
- source: 'ckg',
1149
- });
1150
- }
1151
- }
1152
- }
1153
-
1154
- // Step 2: Lesson history — find failure-outcome lessons matching context
1155
- const lessonKeys: string[] = [];
1156
- const lStream = redis.scanStream({ match: 'cachly:lesson:best:*', count: 300 });
1157
- await new Promise<void>((res, rej) => { lStream.on('data', (b: string[]) => lessonKeys.push(...b)); lStream.on('end', res); lStream.on('error', rej); });
1158
-
1159
- for (const k of lessonKeys.slice(0, 100)) {
1160
- const r = await redis.get(k);
1161
- if (!r) continue;
1162
- try {
1163
- const l = JSON.parse(r) as { topic: string; outcome: string; what_failed?: string; what_worked?: string; confidence?: number; severity?: string };
1164
- if (l.outcome !== 'failure' && l.outcome !== 'partial') continue;
1165
- if (!l.what_failed) continue;
1166
- const haystack = `${l.topic} ${l.what_failed}`.toLowerCase();
1167
- const matchScore = ctxTokens.filter(t => haystack.includes(t)).length / Math.max(1, ctxTokens.length);
1168
- if (matchScore < 0.15) continue;
1169
- const sevBoost = l.severity === 'critical' ? 0.15 : l.severity === 'major' ? 0.05 : 0;
1170
- failures.push({
1171
- concept: l.topic,
1172
- failure: l.what_failed.slice(0, 80),
1173
- probability: Math.min(0.97, (l.confidence ?? 0.5) * matchScore * 1.5 + sevBoost),
1174
- fix: l.what_worked?.slice(0, 120),
1175
- topic: l.topic,
1176
- source: 'lesson',
1177
- });
1178
- } catch { /* skip */ }
1179
- }
1180
-
1181
- if (failures.length === 0) {
1182
- return [
1183
- `🔮 **Failure Prediction: "${ctx}"**`, '',
1184
- `No known failure patterns found for this context.`,
1185
- `💡 The brain learns from every \`learn_from_attempts(outcome="failure")\` call.`,
1186
- `🔍 Try: \`brain_predict(context="${ctx}")\` for broader predictions.`,
1187
- ].join('\n');
1188
- }
1189
-
1190
- // Deduplicate and rank by probability
1191
- const seen = new Set<string>();
1192
- const ranked = failures.filter(f => {
1193
- const k = f.failure.slice(0, 40);
1194
- if (seen.has(k)) return false;
1195
- seen.add(k);
1196
- return true;
1197
- }).sort((a, b) => b.probability - a.probability).slice(0, Number(top_k));
1198
-
1199
- const lines = [`🔮 **Failure Prediction for: "${ctx}"**\n`];
1200
- lines.push(`> Pre-deploy failure analysis based on ${failures.length} patterns. Ranked by probability.\n`);
1201
-
1202
- for (let i = 0; i < ranked.length; i++) {
1203
- const f = ranked[i];
1204
- const pct = Math.round(f.probability * 100);
1205
- const bar = '█'.repeat(Math.round(pct / 10)) + '░'.repeat(10 - Math.round(pct / 10));
1206
- const icon = pct >= 70 ? '🔴' : pct >= 40 ? '🟡' : '🟢';
1207
- lines.push(`${icon} **${i + 1}. ${f.failure}**`);
1208
- lines.push(` Probability: ${bar} **${pct}%** (${f.source === 'ckg' ? 'CKG causal edge' : 'lesson history'})`);
1209
- if (format === 'detailed' && f.fix) {
1210
- lines.push(` ✅ Pre-loaded fix: _${f.fix}_`);
1211
- }
1212
- if (format === 'detailed' && f.topic) {
1213
- lines.push(` 📚 Lesson: \`${f.topic}\``);
1214
- }
1215
- lines.push('');
1216
- }
1217
-
1218
- const highRisk = ranked.filter(f => f.probability >= 0.6);
1219
- if (highRisk.length > 0) {
1220
- lines.push(`⚠️ **${highRisk.length} high-risk failure${highRisk.length > 1 ? 's' : ''} detected** (≥60% probability). Review fixes before proceeding.`);
1221
- } else {
1222
- lines.push(`✅ No high-risk failures detected. Proceed with caution and monitor closely.`);
1223
- }
1224
- lines.push('', `💡 After deploy: \`learn_from_attempts(topic="deploy:...", outcome="success|failure")\` to improve future predictions.`);
1225
- return lines.join('\n');
1226
- }
1227
-
1228
-
1229
- default:
1230
- return null;
1231
- }
1232
- }